diff --git a/specs/t27f/widget_factory.t27 b/specs/t27f/widget_factory.t27 index 934d3a159f..6ffefd4b06 100644 --- a/specs/t27f/widget_factory.t27 +++ b/specs/t27f/widget_factory.t27 @@ -10,7 +10,7 @@ ; spec the widget's .t27 (words, rules, tests, data sources). The hand column of the catalog is ; everything that is NOT this stage. ; logic the pure functions of the tool (parse, grade, layout), written in .t27 and generated to -; JavaScript by gen-ts. The spec's own test blocks are the fixed answers; a data builder no +; wasm (t27c gen-c, then zig cc --target=wasm32-wasi), not JavaScript: gen-js lowers no bodies. The spec's own test blocks are the fixed answers; a data builder no ; longer re-imports a hand file "so the page cannot drift from the spec". ; plan what is drawn, as a ui_plan.t27 plan: widget ids from an allowlist, no markup, no script. ; The primitives below extend ui_plan's catalog; they are not a second catalog. @@ -30,7 +30,7 @@ ; hand-written lines do not grow. (c) is the same rule as t27b's "net <= 0" and FROZEN_HASH ports. ; -- ; Not claimed: no stage past `spec` exists as a program today. gatle's rules are ported to -; specs/widgets/gatle_rule.t27 and pass, but gen-js/gen-ts do not lower fn bodies, so no JS is generated; no plan is drawn by a renderer; t27b has not compiled a data builder. The primitives are +; specs/widgets/gatle_rule.t27 and built to wasm (0 mismatches over 866178 comparisons against rule.js), but the widget page still loads the hand-written rule.js: the trinity side is not changed; no plan is drawn by a renderer; t27b has not compiled a data builder. The primitives are ; PROPOSED from reading the 28 tools by name, not measured; wave 1 (catalog.t27, 11 tools of at ; most 700 hand lines) measures them and may rename or merge them. ; phi^2 + 1/phi^2 = 3 | TRINITY @@ -83,14 +83,18 @@ pub const CLAW_TOOL_COUNT : u32 = 3; pub const CLAW_TOOLS : [3]str = ["t27f_widgets", "t27f_widget_make", "t27f_widget_port_plan"]; /// MEASURED 2026-10-19 with t27c 0.5.2 on specs/widgets/gatle_rule.t27: gen-js and gen-ts emit -/// declarations (consts, structs) and skip every fn body -- "this backend lowers declarations, not -/// bodies". Until a body-lowering JS backend exists the logic stage writes a tested .t27 rule -/// (the single source) but cannot generate the JavaScript the page runs. Flip this when it can. +/// declarations only and skip every fn body (#10943), so the logic stage does not go through them. +/// It goes through the path the Queen's app.wasm already takes: `t27c gen-c`, then +/// `zig cc --target=wasm32-wasi -Oz -mexec-model=reactor` with one -Wl,--export per function. +/// gatle_rule.t27 became a 490-byte wasm; 866178 comparisons against the hand-written rule.js +/// (random and boundary inputs, every function) gave 0 mismatches. Byte slices and out buffers pass +/// through the module's linear memory; module-level `var` arrays and `[]u8` out parameters work. +pub const LOGIC_VIA_WASM : bool = true; pub const GEN_JS_LOWERS_BODIES : bool = false; -/// The logic stage can produce its artefact only when the backend lowers bodies. +/// The logic stage can produce its artefact when some route lowers bodies. pub fn logic_stage_ready() -> bool { - return GEN_JS_LOWERS_BODIES; + return LOGIC_VIA_WASM or GEN_JS_LOWERS_BODIES; } /// A stage is the next of a port stage only by one step. @@ -224,8 +228,10 @@ test every_claw_tool_is_a_t27_name_the_service_mounts { assert TRI_VERBS[0] == "widget list"; } -test the_logic_stage_waits_for_a_body_lowering_backend { - assert logic_stage_ready() == false; +test the_logic_stage_runs_through_wasm_while_gen_js_lowers_no_bodies { + assert logic_stage_ready(); + assert LOGIC_VIA_WASM; + assert GEN_JS_LOWERS_BODIES == false; } test the_logic_stage_removes_exactly_the_logic_file { diff --git a/specs/widgets/catalog.t27 b/specs/widgets/catalog.t27 index 35f526116f..0791601bb9 100644 --- a/specs/widgets/catalog.t27 +++ b/specs/widgets/catalog.t27 @@ -31,9 +31,16 @@ ; 3. 23 tools have a Node data builder (5572 lines) and 24 a Python card renderer (3415 lines). ; The card is a picture of the page; t27f draws pictures from specs already (SF_STILL). ; -- -; Not claimed: that any hand column has been rewritten. Every tool is at PORT_WORDS today: its -; words live in its spec, its logic and drawing still live in JavaScript. A tool moves up only when -; its output is byte-identical to the hand version, and the factory spec states that gate. +; Stages reached, each with a differential against the hand-written version on the trinity side. +; PORT_LOGIC (the pure logic file is .t27, built to wasm, 0 mismatches against the JavaScript): +; gatle 866178 comparisons, lut-depth 12125, stat-card 30827, fasm-skyline 384784, lut-treemap +; 14667449, lut-explain 1395124. +; gatle is at PORT_LOGIC (specs/widgets/gatle_rule.t27: 866178 comparisons vs rule.js, 0 mismatches) +; and jtag-verdict and race-the-bee at PORT_DRAW (specs/widgets/jtag_verdict.t27: 3015 DOM +; comparisons vs tool.js, 0 mismatches; specs/widgets/race_the_bee.t27: 59, with 13 data variants, 0). Every other tool is at PORT_WORDS: its words live in its spec, its logic and drawing +; still live in JavaScript. A stage is reached on the t27 side first; the page in trinity keeps +; loading the hand-written file until that repo's migration PR deletes it. A tool moves up only when +; its output is identical to the hand version, and the factory spec states that gate. ; The Swift and React widgets are counted, not classified: they are another renderer's targets. ; ASCII only (L3), English only (LANG-EN). ; phi^2 + 1/phi^2 = 3 | TRINITY @@ -83,7 +90,7 @@ pub const PORT_LOGIC : u8 = 2; // pure logic is .t27, generated to JS by gen-ts pub const PORT_DRAW : u8 = 3; // the page is drawn by the trusted renderer from a plan pub const PORT_SEALED : u8 = 4; // the spec seal keys the page, the data and the card pub const PORT_STAGES : u8 = 5; -pub const TOOL_PORT : [28]u8 = [1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1]; +pub const TOOL_PORT : [28]u8 = [1, 1, 1, 1, 2, 1, 1, 2, 1, 1, 3, 2, 2, 2, 1, 1, 3, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1]; ; Port waves, by the hand lines a tool still carries (logic + draw + css + data + card). ; Wave 1 is the small tools, wave 3 the largest; a wave is a batch of port issues. @@ -219,9 +226,17 @@ test the_waves_cover_every_tool_once { assert hand_lines(12) == 1432; } -test no_tool_has_left_the_words_stage_yet { - assert count_port(PORT_WORDS) == TOOL_COUNT; - assert count_port(PORT_LOGIC) == 0; - assert count_port(PORT_DRAW) == 0; +test eight_tools_have_left_the_words_stage_with_a_differential { + assert count_port(PORT_WORDS) == TOOL_COUNT - 8; + assert count_port(PORT_LOGIC) == 6; + assert count_port(PORT_DRAW) == 2; assert count_port(PORT_SEALED) == 0; + assert TOOL_PORT[4] == PORT_LOGIC; + assert TOOL_PORT[7] == PORT_LOGIC; + assert TOOL_PORT[11] == PORT_LOGIC; + assert TOOL_PORT[12] == PORT_LOGIC; + assert TOOL_PORT[13] == PORT_LOGIC; + assert TOOL_PORT[19] == PORT_LOGIC; + assert TOOL_PORT[10] == PORT_DRAW; + assert TOOL_PORT[16] == PORT_DRAW; } diff --git a/specs/widgets/fasm_skyline_logic.t27 b/specs/widgets/fasm_skyline_logic.t27 new file mode 100644 index 0000000000..8365e93ed3 --- /dev/null +++ b/specs/widgets/fasm_skyline_logic.t27 @@ -0,0 +1,1223 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/fasm_skyline_logic.t27 -- the fasm-skyline widget's counting: the features a FASM text sets in +; each tile, the tile grid of the XC7A200T, and the totals per family. Ported from trinity +; apps/website/public/widgets/fasm-skyline/skyline.js (122 lines). Epic gHashTag/t27#10937, issue #10938. +; -- +; JS export -> t27 function(s) note +; parseFasm(text) -> parse_fasm(text) + fa_* getters tiles keep first-appearance order +; decodeGrid(g) -> grid_begin, grid_types, grid_rle, grid_prefixes, grid_segs, grid_except, then +; grid_lookup(name) and grid_at(gx, gy) g.json is split into those calls by the glue +; familyOf(...) -> fam_set(prefixes, families, names) once, then family_of(type) +; summarize(...) -> summarize(), results through sm_* getters famOfType is family_of; nFam = names count +; expand, zeroValue, TILE_RE are not exported by JS: expand is folded into seg_value, zeroValue is +; zero_value, TILE_RE is tile_split. +; -- +; Result layouts (a string is (offset, length) into the pool: read it with pool(i)). +; parse_fasm(text) -> 0 ok, -1 over a limit (a line over 1 MB, 65536 tiles, 8 MB of strings) +; fa_info(f): 0 features 1 comments 2 cleared 3 header count (at most 8) 4 tile count 5 bad count (at most 200) +; fa_header(i, f), fa_bad(i, f): f 0 offset 1 length. fa_tile(i, f): f 0 offset 1 length 2 count. +; Grid. grid_begin(w, h) -> 0, or -1 over 4194304 cells. grid_types(list): the type names, each ended by byte 10. +; grid_rle(u32 pairs value, count) -> 0, or 1 when the runs do not cover w*h cells (decodeGrid throws). +; grid_prefixes(axis, list): the keys of g.x (axis 0) or g.y (axis 1), each ended by byte 10, numbered +; in the order given across both calls. grid_segs(i32 records of 7: axis, prefix number, k0, v0, n, dk, dv). +; grid_except(list of names ended by byte 10, i32 pairs gx, gy). All numbers are whole. +; grid_lookup(name) -> 1 and lk(0..3) = gx, gy, type length (the type is the name without its _XY +; tail), type index; or 0 for JS null. grid_at(gx, gy) -> the cell byte, or -1 for undefined. +; family lists: each item ended by byte 10. family_of(type) -> the family index, -1 when the family +; name is not in the names. +; summarize() -> 0; sm_info(f): 0 columns 1 off-die tiles 2 features 3 placed features 4 tiles +; 5 tallest column (index, or -1); sm_col(i, f): 0 tile index 1 gx 2 gy 3 type length 4 family 5 count +; (the tile index is an fa_tile index; the name is fa_tile(idx,0..1)); sm_off(i, f): 0 tile index 1 count; +; sm_fam(k, f): 0 tiles 1 features (k below 64). +; -- +; JavaScript quirks kept: `#` starts a comment even inside {...}; annotations {..} are cut out before +; the line is cut at '='; a value of 0, 0'b0, 8'h00 (underscores ignored) clears and is counted apart; +; a name without a tile prefix is "bad" (the first 200 are kept); a family -1 is counted nowhere in +; famTiles; a later duplicate type or segment wins, as Map.set does; the grid cell is read at +; gx*h+gy even when gy is past h (it wraps into the next column), and outside 0..w*h it is undefined. +; Not carried: fractional or huge grid numbers; a family list with more than 64 families. +; Trim uses the JavaScript whitespace set on UTF-8 (NBSP, U+2028 ... U+FEFF). +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_fasm_skyline_logic; + +pub const V_PN : usize = 0; +pub const V_FEAT : usize = 1; +pub const V_COM : usize = 2; +pub const V_CLR : usize = 3; +pub const V_NH : usize = 4; +pub const V_NT : usize = 5; +pub const V_NB : usize = 6; +pub const V_GW : usize = 8; +pub const V_GH : usize = 9; +pub const V_NTY : usize = 10; +pub const V_NPF : usize = 11; +pub const V_NSEG : usize = 12; +pub const V_NEX : usize = 13; +pub const V_GP : usize = 14; +pub const V_FPL : usize = 15; +pub const V_FFL : usize = 16; +pub const V_FNL : usize = 17; +pub const V_SM0 : usize = 20; + +pub var V : [32]u64 = [0; 32]; +pub var SC : [4]u64 = [0; 4]; +pub var LK : [4]i64 = [0; 4]; +pub var SV : [2]i64 = [0; 2]; +pub var POOL : [8388608]u8 = [0; 8388608]; +pub var LINE : [1048576]u8 = [0; 1048576]; +pub var VB : [1048576]u8 = [0; 1048576]; +pub var HDRO : [8]u32 = [0; 8]; +pub var HDRL : [8]u32 = [0; 8]; +pub var BADO : [200]u32 = [0; 200]; +pub var BADL : [200]u32 = [0; 200]; +pub var TLO : [65536]u32 = [0; 65536]; +pub var TLL : [65536]u32 = [0; 65536]; +pub var TCT : [65536]u32 = [0; 65536]; +pub var HT : [262144]u32 = [0; 262144]; +pub var SIDX : [65536]u32 = [0; 65536]; +pub var STMP : [65536]u32 = [0; 65536]; + +pub var CELLS : [4194304]u8 = [0; 4194304]; +pub var GPOOL : [262144]u8 = [0; 262144]; +pub var GTO : [4096]u32 = [0; 4096]; +pub var GTL : [4096]u32 = [0; 4096]; +pub var PFO : [4096]u32 = [0; 4096]; +pub var PFL : [4096]u32 = [0; 4096]; +pub var PFA : [4096]u8 = [0; 4096]; +pub var SGA : [65536]i64 = [0; 65536]; +pub var SGP : [65536]i64 = [0; 65536]; +pub var SGK : [65536]i64 = [0; 65536]; +pub var SGV : [65536]i64 = [0; 65536]; +pub var SGN : [65536]i64 = [0; 65536]; +pub var SGD : [65536]i64 = [0; 65536]; +pub var SGE : [65536]i64 = [0; 65536]; +pub var EXO : [4096]u32 = [0; 4096]; +pub var EXL : [4096]u32 = [0; 4096]; +pub var EXX : [4096]i64 = [0; 4096]; +pub var EXY : [4096]i64 = [0; 4096]; + +pub var FPB : [16384]u8 = [0; 16384]; +pub var FFB : [16384]u8 = [0; 16384]; +pub var FNB : [16384]u8 = [0; 16384]; + +pub var COLT : [65536]u32 = [0; 65536]; +pub var COLX : [65536]u32 = [0; 65536]; +pub var COLY : [65536]u32 = [0; 65536]; +pub var COLL : [65536]u32 = [0; 65536]; +pub var COLF : [65536]i32 = [0; 65536]; +pub var OFFT : [65536]u32 = [0; 65536]; +pub var FMT : [64]u32 = [0; 64]; +pub var FMF : [64]u64 = [0; 64]; + +// ---------------------------------------------------------------- whitespace and small helpers +pub fn ws_len_at(s: []const u8, p: usize, hi: usize) -> usize { + if (p >= hi) { return 0; } + const b : u8 = s[p]; + if (b == 32 or (b >= 9 and b <= 13)) { return 1; } + if (b == 194 and p + 1 < hi and s[p + 1] == 160) { return 2; } + if (p + 3 <= hi) { + const b1 : u8 = s[p + 1]; + const b2 : u8 = s[p + 2]; + if (b == 225 and b1 == 154 and b2 == 128) { return 3; } + if (b == 226 and b1 == 128 and ((b2 >= 128 and b2 <= 138) or b2 == 168 or b2 == 169 or b2 == 175)) { return 3; } + if (b == 226 and b1 == 129 and b2 == 159) { return 3; } + if (b == 227 and b1 == 128 and b2 == 128) { return 3; } + if (b == 239 and b1 == 187 and b2 == 191) { return 3; } + } + return 0; +} + +pub fn ws_len_before(s: []const u8, lo: usize, e: usize) -> usize { + if (e > lo and ws_len_at(s, e - 1, e) == 1) { return 1; } + if (e >= lo + 2 and ws_len_at(s, e - 2, e) == 2) { return 2; } + if (e >= lo + 3 and ws_len_at(s, e - 3, e) == 3) { return 3; } + return 0; +} + +/// String.prototype.trim on s[lo..hi): the new start in SC[0], the new end in SC[1]. +pub fn trim_range(s: []const u8, lo0: usize, hi0: usize) -> bool { + var lo : usize = lo0; + var hi : usize = hi0; + var w : usize = ws_len_at(s, lo, hi); + while (w > 0) { + lo = lo + w; + w = ws_len_at(s, lo, hi); + } + w = ws_len_before(s, lo, hi); + while (w > 0) { + hi = hi - w; + w = ws_len_before(s, lo, hi); + } + SC[0] = lo as u64; + SC[1] = hi as u64; + return true; +} + +pub fn is_digit(b: u8) -> bool { + return b >= 48 and b <= 57; +} + +pub fn digits_val(s: []const u8, a: usize, b: usize) -> u64 { + var v : u64 = 0; + var k : usize = a; + while (k < b) { + if (v < 100000000000000000) { v = v * 10 + ((s[k] - 48) as u64); } + k = k + 1; + } + return v; +} + +pub fn find_byte(s: []const u8, from: usize, hi: usize, b: u8) -> i64 { + var p : usize = from; + while (p < hi) { + if (s[p] == b) { return p as i64; } + p = p + 1; + } + return -1; +} + +pub fn pool_put_text(s: []const u8, lo: usize, hi: usize) -> i64 { + const at : u64 = V[V_PN]; + if (at + ((hi - lo) as u64) >= 8388608) { return -1; } + var k : usize = lo; + while (k < hi) { + POOL[(at as usize) + (k - lo)] = s[k]; + k = k + 1; + } + V[V_PN] = at + ((hi - lo) as u64); + return at as i64; +} + +// ---------------------------------------------------------------- the FASM line rules +/// The tile name test /^([A-Z0-9_]+)_X(\d+)Y(\d+)$/ : the length of the type, the two numbers in SC[0], SC[1]; -1 when it does not match. +pub fn tile_split(s: []const u8, lo: usize, hi: usize) -> i64 { + var k : usize = lo; + while (k < hi) { + const b : u8 = s[k]; + if (!((b >= 65 and b <= 90) or is_digit(b) or b == 95)) { return -1; } + k = k + 1; + } + var ys : usize = hi; + while (ys > lo and is_digit(s[ys - 1])) { ys = ys - 1; } + if (ys == hi) { return -1; } + if (ys == lo or s[ys - 1] != 89) { return -1; } + const xe : usize = ys - 1; + var xs : usize = xe; + while (xs > lo and is_digit(s[xs - 1])) { xs = xs - 1; } + if (xs == xe) { return -1; } + if (xs < lo + 2) { return -1; } + if (s[xs - 1] != 88 or s[xs - 2] != 95) { return -1; } + const tl : usize = (xs - 2) - lo; + if (tl < 1) { return -1; } + SC[0] = digits_val(s, xs, xe); + SC[1] = digits_val(s, ys, hi); + return tl as i64; +} + +/// zeroValue(v): v = s[lo..hi). Underscores are dropped, the rest trimmed; a Verilog-style constant is zero +/// when its digits are all 0, a bare number when it is all 0. +pub fn zero_value(s: []const u8, lo: usize, hi: usize) -> bool { + var n : usize = 0; + var k : usize = lo; + while (k < hi) { + if (s[k] != 95) { + VB[n] = s[k]; + n = n + 1; + } + k = k + 1; + } + const t : []const u8 = VB[0 .. 1048576]; + trim_range(t, 0, n); + const a : usize = SC[0] as usize; + const b : usize = SC[1] as usize; + // (\d+)?'([bhdo])([0-9a-f]+)$ with the i flag + var p : usize = a; + while (p < b and is_digit(t[p])) { p = p + 1; } + if (p + 2 < b and t[p] == 39) { + const base : u8 = t[p + 1] | 32; + if (base == 98 or base == 104 or base == 100 or base == 111) { + var q : usize = p + 2; + var allhex : bool = true; + var allzero : bool = true; + while (q < b) { + const c : u8 = t[q]; + const lc : u8 = c | 32; + if (is_digit(c) or (lc >= 97 and lc <= 102)) { + if (c != 48) { allzero = false; } + } else { + allhex = false; + } + q = q + 1; + } + if (allhex) { return allzero; } + } + } + // /^0+$/ + if (b == a) { return false; } + var z : usize = a; + while (z < b) { + if (t[z] != 48) { return false; } + z = z + 1; + } + return true; +} + +pub fn fnv(s: []const u8, lo: usize, hi: usize) -> u32 { + var h : u64 = 2166136261; + var k : usize = lo; + while (k < hi) { + h = (((h ^ (s[k] as u64)) * 16777619) % 4294967296); + k = k + 1; + } + return h as u32; +} + +pub fn pool_eq_text(o: u32, l: u32, s: []const u8, lo: usize, hi: usize) -> bool { + if ((l as usize) != hi - lo) { return false; } + var k : usize = 0; + while (k < (l as usize)) { + if (POOL[(o as usize) + k] != s[lo + k]) { return false; } + k = k + 1; + } + return true; +} + +/// tiles.set(name, (tiles.get(name) || 0) + 1) for a name held in LINE[lo..hi). +pub fn tile_add(lo: usize, hi: usize) -> bool { + const t : []const u8 = LINE[0 .. 1048576]; + var idx : usize = (fnv(t, lo, hi) as usize) % 262144; + while (HT[idx] != 0) { + const ti : usize = (HT[idx] - 1) as usize; + if (pool_eq_text(TLO[ti], TLL[ti], t, lo, hi)) { + TCT[ti] = TCT[ti] + 1; + return true; + } + idx = (idx + 1) % 262144; + } + const nt : usize = V[V_NT] as usize; + if (nt >= 65536) { return false; } + const at : i64 = pool_put_text(t, lo, hi); + if (at < 0) { return false; } + TLO[nt] = at as u32; + TLL[nt] = (hi - lo) as u32; + TCT[nt] = 1; + HT[idx] = (nt + 1) as u32; + V[V_NT] = (nt + 1) as u64; + return true; +} + +/// parseFasm. 0 ok, -1 over a limit. +pub fn parse_fasm(text: []const u8) -> i32 { + var f : usize = 0; + while (f < 8) { + V[f] = 0; + f = f + 1; + } + var h : usize = 0; + while (h < 262144) { + HT[h] = 0; + h = h + 1; + } + const lt : []const u8 = LINE[0 .. 1048576]; + var start : usize = 0; + const n : usize = text.len; + while (start <= n) { + var end : usize = start; + while (end < n and text[end] != 10) { end = end + 1; } + const ls : usize = start; + start = end + 1; + var le : usize = end; + if (le - ls > 1048576) { return -1; } + const hash : i64 = find_byte(text, ls, le, 35); + if (hash >= 0) { + const hp : usize = hash as usize; + trim_range(text, hp + 1, le); + const cl : usize = SC[0] as usize; + const ch : usize = SC[1] as usize; + trim_range(text, ls, hp); + if (SC[0] == SC[1]) { + V[V_COM] = V[V_COM] + 1; + if (V[V_NH] < 8 and ch > cl) { + const at : i64 = pool_put_text(text, cl, ch); + if (at < 0) { return -1; } + HDRO[V[V_NH] as usize] = at as u32; + HDRL[V[V_NH] as usize] = (ch - cl) as u32; + V[V_NH] = V[V_NH] + 1; + } + } + le = hp; + } + // line.replace(/\{[^}]*\}/g, '') + var m : usize = 0; + var i : usize = ls; + var done : bool = false; + while (!done) { + const ob : i64 = find_byte(text, i, le, 123); + if (ob < 0) { + done = true; + } else { + const cb : i64 = find_byte(text, (ob as usize) + 1, le, 125); + if (cb < 0) { + done = true; + } else { + var k : usize = i; + while (k < (ob as usize)) { + LINE[m] = text[k]; + m = m + 1; + k = k + 1; + } + i = (cb as usize) + 1; + } + } + } + var k2 : usize = i; + while (k2 < le) { + LINE[m] = text[k2]; + m = m + 1; + k2 = k2 + 1; + } + trim_range(lt, 0, m); + const a : usize = SC[0] as usize; + const b : usize = SC[1] as usize; + if (b > a) { + const eq : i64 = find_byte(lt, a, b, 61); + var nb : usize = b; + if (eq >= 0) { nb = eq as usize; } + trim_range(lt, a, nb); + const na : usize = SC[0] as usize; + const ne : usize = SC[1] as usize; + var cleared : bool = false; + if (eq >= 0) { + cleared = zero_value(lt, (eq as usize) + 1, b); + } + if (cleared) { + V[V_CLR] = V[V_CLR] + 1; + } else { + const dot : i64 = find_byte(lt, na, ne, 46); + var tl : i64 = -1; + var tend : usize = na; + if (dot > (na as i64)) { + tend = dot as usize; + tl = tile_split(lt, na, tend); + } + if (tl < 0) { + if (V[V_NB] < 200) { + const at : i64 = pool_put_text(lt, a, b); + if (at < 0) { return -1; } + BADO[V[V_NB] as usize] = at as u32; + BADL[V[V_NB] as usize] = (b - a) as u32; + V[V_NB] = V[V_NB] + 1; + } + } else { + V[V_FEAT] = V[V_FEAT] + 1; + if (!tile_add(na, tend)) { return -1; } + } + } + } + } + return 0; +} + +pub fn fa_info(f: u32) -> u64 { + return V[f as usize + 1]; +} + +pub fn fa_header(i: u32, f: u32) -> u32 { + if (f == 0) { return HDRO[i as usize]; } + return HDRL[i as usize]; +} + +pub fn fa_bad(i: u32, f: u32) -> u32 { + if (f == 0) { return BADO[i as usize]; } + return BADL[i as usize]; +} + +pub fn fa_tile(i: u32, f: u32) -> u32 { + if (f == 0) { return TLO[i as usize]; } + if (f == 1) { return TLL[i as usize]; } + return TCT[i as usize]; +} + +pub fn pool(i: u32) -> u8 { + return POOL[i as usize]; +} + +// ---------------------------------------------------------------- the grid +pub fn grid_begin(w: u32, h: u32) -> i32 { + V[V_GW] = w as u64; + V[V_GH] = h as u64; + V[V_NTY] = 0; + V[V_NPF] = 0; + V[V_NSEG] = 0; + V[V_NEX] = 0; + V[V_GP] = 0; + if ((w as u64) * (h as u64) > 4194304) { return -1; } + return 0; +} + +pub fn gpool_put(s: []const u8, lo: usize, hi: usize) -> i64 { + const at : u64 = V[V_GP]; + if (at + ((hi - lo) as u64) >= 262144) { return -1; } + var k : usize = lo; + while (k < hi) { + GPOOL[(at as usize) + (k - lo)] = s[k]; + k = k + 1; + } + V[V_GP] = at + ((hi - lo) as u64); + return at as i64; +} + +pub fn grid_types(list: []const u8) -> i32 { + var a : usize = 0; + while (a < list.len) { + var e : usize = a; + while (e < list.len and list[e] != 10) { e = e + 1; } + const at : i64 = gpool_put(list, a, e); + const n : usize = V[V_NTY] as usize; + if (at < 0 or n >= 4096) { return -1; } + GTO[n] = at as u32; + GTL[n] = (e - a) as u32; + V[V_NTY] = (n + 1) as u64; + a = e + 1; + } + return 0; +} + +pub fn grid_rle(rle: []const u32) -> i32 { + const total : u64 = V[V_GW] * V[V_GH]; + var i : u64 = 0; + var r : usize = 0; + while (r + 1 < rle.len) { + const v : u8 = (rle[r] % 256) as u8; + const c : u64 = rle[r + 1] as u64; + var k : u64 = 0; + while (k < c and i + k < total) { + CELLS[(i + k) as usize] = v; + k = k + 1; + } + i = i + c; + r = r + 2; + } + if (rle.len % 2 == 1) { return 1; } + if (i != total) { return 1; } + return 0; +} + +pub fn grid_prefixes(axis: u32, list: []const u8) -> i32 { + var a : usize = 0; + while (a < list.len) { + var e : usize = a; + while (e < list.len and list[e] != 10) { e = e + 1; } + const at : i64 = gpool_put(list, a, e); + const n : usize = V[V_NPF] as usize; + if (at < 0 or n >= 4096) { return -1; } + PFO[n] = at as u32; + PFL[n] = (e - a) as u32; + PFA[n] = axis as u8; + V[V_NPF] = (n + 1) as u64; + a = e + 1; + } + return 0; +} + +pub fn grid_segs(recs: []const i32) -> i32 { + var r : usize = 0; + while (r + 7 <= recs.len) { + const n : usize = V[V_NSEG] as usize; + if (n >= 65536) { return -1; } + SGA[n] = recs[r] as i64; + SGP[n] = recs[r + 1] as i64; + SGK[n] = recs[r + 2] as i64; + SGV[n] = recs[r + 3] as i64; + SGN[n] = recs[r + 4] as i64; + SGD[n] = recs[r + 5] as i64; + SGE[n] = recs[r + 6] as i64; + V[V_NSEG] = (n + 1) as u64; + r = r + 7; + } + return 0; +} + +pub fn grid_except(names: []const u8, xy: []const i32) -> i32 { + var a : usize = 0; + var i : usize = 0; + while (a < names.len) { + var e : usize = a; + while (e < names.len and names[e] != 10) { e = e + 1; } + const at : i64 = gpool_put(names, a, e); + const n : usize = V[V_NEX] as usize; + if (at < 0 or n >= 4096 or 2 * i + 1 >= xy.len) { return -1; } + EXO[n] = at as u32; + EXL[n] = (e - a) as u32; + EXX[n] = xy[2 * i] as i64; + EXY[n] = xy[2 * i + 1] as i64; + V[V_NEX] = (n + 1) as u64; + i = i + 1; + a = e + 1; + } + return 0; +} + +pub fn gp_eq(o: u32, l: u32, s: []const u8, lo: usize, hi: usize) -> bool { + if ((l as usize) != hi - lo) { return false; } + var k : usize = 0; + while (k < (l as usize)) { + if (GPOOL[(o as usize) + k] != s[lo + k]) { return false; } + k = k + 1; + } + return true; +} + +/// expand(segs).get(k): true with the value in SV[0] (the last segment and step that set key k), false when none did (JS undefined). +pub fn seg_value(pf: i64, k: i64) -> bool { + var found : bool = false; + var s : usize = 0; + while (s < (V[V_NSEG] as usize)) { + if (SGP[s] == pf and SGN[s] > 0) { + const d : i64 = k - SGK[s]; + var i : i64 = -1; + if (SGD[s] == 0) { + if (d == 0) { i = SGN[s] - 1; } + } else { + if (d % SGD[s] == 0) { + const q : i64 = d / SGD[s]; + if (q >= 0 and q < SGN[s]) { i = q; } + } + } + if (i >= 0) { + SV[0] = SGV[s] + i * SGE[s]; + found = true; + } + } + s = s + 1; + } + return found; +} + +pub fn grid_at(gx: i64, gy: i64) -> i32 { + const idx : i64 = gx * (V[V_GH] as i64) + gy; + if (idx < 0 or idx >= (V[V_GW] as i64) * (V[V_GH] as i64)) { return -1; } + return CELLS[idx as usize] as i32; +} + +/// grid.lookup(name) for name = s[lo..hi): true with LK = gx, gy, type length, type index; false for null. +pub fn grid_lookup(s: []const u8, lo: usize, hi: usize) -> bool { + var gx : i64 = 0; + var gy : i64 = 0; + var tl : usize = hi - lo; + var ex : i64 = -1; + var e : usize = 0; + while (e < (V[V_NEX] as usize) and ex < 0) { + if (gp_eq(EXO[e], EXL[e], s, lo, hi)) { ex = e as i64; } + e = e + 1; + } + if (ex >= 0) { + gx = EXX[ex as usize]; + gy = EXY[ex as usize]; + const t0 : i64 = tile_split(s, lo, hi); + if (t0 >= 0) { tl = t0 as usize; } + } else { + const t1 : i64 = tile_split(s, lo, hi); + if (t1 < 0) { return false; } + tl = t1 as usize; + const x : i64 = SC[0] as i64; + const y : i64 = SC[1] as i64; + var px : i64 = -1; + var py : i64 = -1; + var p : usize = 0; + while (p < (V[V_NPF] as usize)) { + if (gp_eq(PFO[p], PFL[p], s, lo, lo + tl)) { + if (PFA[p] == 0) { px = p as i64; } + if (PFA[p] == 1) { py = p as i64; } + } + p = p + 1; + } + if (px < 0 or py < 0) { return false; } + if (!seg_value(px, x)) { return false; } + gx = SV[0]; + if (!seg_value(py, y)) { return false; } + gy = SV[0]; + } + var t : i64 = -1; + var k : usize = V[V_NTY] as usize; + while (k > 0 and t < 0) { + k = k - 1; + if (gp_eq(GTO[k], GTL[k], s, lo, lo + tl)) { t = k as i64; } + } + if (t < 0) { return false; } + if ((grid_at(gx, gy) as i64) != t) { return false; } + LK[0] = gx; + LK[1] = gy; + LK[2] = tl as i64; + LK[3] = t; + return true; +} + +pub fn lk(f: u32) -> i64 { + return LK[f as usize]; +} + +// ---------------------------------------------------------------- families and the summary +pub fn fam_set(prefixes: []const u8, families: []const u8, names: []const u8) -> i32 { + if (prefixes.len > 16384 or families.len > 16384 or names.len > 16384) { return -1; } + var k : usize = 0; + while (k < prefixes.len) { + FPB[k] = prefixes[k]; + k = k + 1; + } + k = 0; + while (k < families.len) { + FFB[k] = families[k]; + k = k + 1; + } + k = 0; + while (k < names.len) { + FNB[k] = names[k]; + k = k + 1; + } + V[V_FPL] = prefixes.len as u64; + V[V_FFL] = families.len as u64; + V[V_FNL] = names.len as u64; + return 0; +} + +/// The packed [start, end) of the i-th item (each ended by byte 10) of a list, or -1. +pub fn item_of(buf: []const u8, used: usize, i: usize) -> i64 { + var a : usize = 0; + var n : usize = 0; + while (a < used) { + var e : usize = a; + while (e < used and buf[e] != 10) { e = e + 1; } + if (n == i) { return (a as i64) * 4294967296 + (e as i64); } + n = n + 1; + a = e + 1; + } + return -1; +} + +pub fn count_items(buf: []const u8, used: usize) -> usize { + var a : usize = 0; + var n : usize = 0; + while (a < used) { + var e : usize = a; + while (e < used and buf[e] != 10) { e = e + 1; } + n = n + 1; + a = e + 1; + } + return n; +} + +/// famNames.indexOf(item) where item is s[lo..hi) of the families list; -1 when absent. +pub fn index_of_name(s: []const u8, lo: usize, hi: usize) -> i32 { + const names : []const u8 = FNB[0 .. 16384]; + const used : usize = V[V_FNL] as usize; + var a : usize = 0; + var n : i32 = 0; + while (a < used) { + var e : usize = a; + while (e < used and names[e] != 10) { e = e + 1; } + if (e - a == hi - lo) { + var same : bool = true; + var k : usize = 0; + while (k < e - a) { + if (names[a + k] != s[lo + k]) { same = false; } + k = k + 1; + } + if (same) { return n; } + } + n = n + 1; + a = e + 1; + } + return -1; +} + +/// familyOf(type, prefixes, families, famNames) with the three lists set by fam_set. +pub fn family_of_range(s: []const u8, lo: usize, hi: usize) -> i32 { + const pf : []const u8 = FPB[0 .. 16384]; + const fm : []const u8 = FFB[0 .. 16384]; + const np : usize = count_items(pf, V[V_FPL] as usize); + var i : usize = 0; + while (i < np) { + const it : i64 = item_of(pf, V[V_FPL] as usize, i); + const a : usize = (it / 4294967296) as usize; + const e : usize = (it % 4294967296) as usize; + if (hi - lo >= e - a) { + var same : bool = true; + var k : usize = 0; + while (k < e - a) { + if (s[lo + k] != pf[a + k]) { same = false; } + k = k + 1; + } + if (same) { + const fi : i64 = item_of(fm, V[V_FFL] as usize, i); + if (fi < 0) { return -1; } + return index_of_name(fm, (fi / 4294967296) as usize, (fi % 4294967296) as usize); + } + } + i = i + 1; + } + return (count_items(FNB[0 .. 16384], V[V_FNL] as usize) as i32) - 1; +} + +pub fn family_of(t: []const u8) -> i32 { + return family_of_range(t, 0, t.len); +} + +pub fn name_less(a: u32, b: u32) -> bool { + const la : usize = TLL[a as usize] as usize; + const lb : usize = TLL[b as usize] as usize; + var k : usize = 0; + while (k < la and k < lb) { + const x : u8 = POOL[(TLO[a as usize] as usize) + k]; + const y : u8 = POOL[(TLO[b as usize] as usize) + k]; + if (x != y) { return x < y; } + k = k + 1; + } + return la < lb; +} + +/// Sort SIDX[0..n) by tile name (default JS sort of ASCII strings), bottom-up merge sort. +pub fn sort_names(n: usize) -> bool { + var i : usize = 0; + while (i < n) { + SIDX[i] = i as u32; + i = i + 1; + } + var w : usize = 1; + while (w < n) { + var lo : usize = 0; + while (lo < n) { + var mid : usize = lo + w; + if (mid > n) { mid = n; } + var hi : usize = lo + 2 * w; + if (hi > n) { hi = n; } + var a : usize = lo; + var b : usize = mid; + var o : usize = lo; + while (a < mid or b < hi) { + if (b >= hi or (a < mid and !name_less(SIDX[b], SIDX[a]))) { + STMP[o] = SIDX[a]; + a = a + 1; + } else { + STMP[o] = SIDX[b]; + b = b + 1; + } + o = o + 1; + } + lo = lo + 2 * w; + } + var c : usize = 0; + while (c < n) { + SIDX[c] = STMP[c]; + c = c + 1; + } + w = w * 2; + } + return true; +} + +/// summarize(parsed, grid, famOfType, nFam). Needs parse_fasm, the grid calls and fam_set done. +pub fn summarize() -> i32 { + const nt : usize = V[V_NT] as usize; + sort_names(nt); + const nfam : usize = count_items(FNB[0 .. 16384], V[V_FNL] as usize); + var f : usize = 0; + while (f < 64) { + FMT[f] = 0; + FMF[f] = 0; + f = f + 1; + } + var ncol : usize = 0; + var noff : usize = 0; + var placed : u64 = 0; + var tallest : i64 = -1; + var tn : u64 = 0; + var i : usize = 0; + const pl : []const u8 = POOL[0 .. 8388608]; + while (i < nt) { + const t : usize = SIDX[i] as usize; + const o : usize = TLO[t] as usize; + const l : usize = TLL[t] as usize; + const n : u64 = TCT[t] as u64; + if (!grid_lookup(pl, o, o + l)) { + OFFT[noff] = t as u32; + noff = noff + 1; + } else { + const tl : usize = LK[2] as usize; + const fam : i32 = family_of_range(pl, o, o + tl); + COLT[ncol] = t as u32; + COLX[ncol] = LK[0] as u32; + COLY[ncol] = LK[1] as u32; + COLL[ncol] = tl as u32; + COLF[ncol] = fam; + if (fam >= 0 and (fam as usize) < nfam and fam < 64) { + FMT[fam as usize] = FMT[fam as usize] + 1; + FMF[fam as usize] = FMF[fam as usize] + n; + } + placed = placed + n; + if (tallest < 0 or n > tn) { + tallest = ncol as i64; + tn = n; + } + ncol = ncol + 1; + } + i = i + 1; + } + V[V_SM0] = ncol as u64; + V[V_SM0 + 1] = noff as u64; + V[V_SM0 + 2] = V[V_FEAT]; + V[V_SM0 + 3] = placed; + V[V_SM0 + 4] = ncol as u64; + V[V_SM0 + 5] = (tallest + 1) as u64; + return 0; +} + +pub fn sm_info(f: u32) -> i64 { + if (f == 5) { return (V[V_SM0 + 5] as i64) - 1; } + return V[V_SM0 + (f as usize)] as i64; +} + +pub fn sm_col(i: u32, f: u32) -> i64 { + const c : usize = i as usize; + if (f == 0) { return COLT[c] as i64; } + if (f == 1) { return COLX[c] as i64; } + if (f == 2) { return COLY[c] as i64; } + if (f == 3) { return COLL[c] as i64; } + if (f == 4) { return COLF[c] as i64; } + return TCT[COLT[c] as usize] as i64; +} + +pub fn sm_off(i: u32, f: u32) -> i64 { + if (f == 0) { return OFFT[i as usize] as i64; } + return TCT[OFFT[i as usize] as usize] as i64; +} + +pub fn sm_fam(k: u32, f: u32) -> u64 { + if (f == 0) { return FMT[k as usize] as u64; } + return FMF[k as usize]; +} + +// ---------------------------------------------------------------- tests +pub var TXT : [8192]u8 = [0; 8192]; +pub var TXN : usize = 0; +pub var LA : [256]u8 = [0; 256]; +pub var LB : [256]u8 = [0; 256]; +pub var LC : [256]u8 = [0; 256]; +pub var LAN : usize = 0; +pub var LBN : usize = 0; +pub var LCN : usize = 0; +pub var RLE : [16]u32 = [0; 16]; +pub var SEG : [28]i32 = [0; 28]; + +pub fn tx_reset() -> usize { + TXN = 0; + return 0; +} + +pub fn tx_line(s: []const u8) -> usize { + var k : usize = 0; + while (k < s.len) { + TXT[TXN] = s[k]; + TXN = TXN + 1; + k = k + 1; + } + TXT[TXN] = 10; + TXN = TXN + 1; + return TXN; +} + +/// Lists, one item per byte-10 end, built from up to four items (an empty item is skipped). +pub fn la_make(a: []const u8, b: []const u8, c: []const u8, d: []const u8) -> usize { + LAN = 0; + var w : u32 = 0; + while (w < 4) { + var s : []const u8 = a; + if (w == 1) { s = b; } + if (w == 2) { s = c; } + if (w == 3) { s = d; } + if (s.len > 0) { + var k : usize = 0; + while (k < s.len) { + LA[LAN] = s[k]; + LAN = LAN + 1; + k = k + 1; + } + LA[LAN] = 10; + LAN = LAN + 1; + } + w = w + 1; + } + return LAN; +} + +pub fn lb_make(a: []const u8, b: []const u8, c: []const u8, d: []const u8) -> usize { + LBN = 0; + var w : u32 = 0; + while (w < 4) { + var s : []const u8 = a; + if (w == 1) { s = b; } + if (w == 2) { s = c; } + if (w == 3) { s = d; } + if (s.len > 0) { + var k : usize = 0; + while (k < s.len) { + LB[LBN] = s[k]; + LBN = LBN + 1; + k = k + 1; + } + LB[LBN] = 10; + LBN = LBN + 1; + } + w = w + 1; + } + return LBN; +} + +pub fn lc_make(a: []const u8, b: []const u8, c: []const u8, d: []const u8) -> usize { + LCN = 0; + var w : u32 = 0; + while (w < 4) { + var s : []const u8 = a; + if (w == 1) { s = b; } + if (w == 2) { s = c; } + if (w == 3) { s = d; } + if (s.len > 0) { + var k : usize = 0; + while (k < s.len) { + LC[LCN] = s[k]; + LCN = LCN + 1; + k = k + 1; + } + LC[LCN] = 10; + LCN = LCN + 1; + } + w = w + 1; + } + return LCN; +} + +pub fn pool_is(o: u32, l: u32, w: []const u8) -> bool { + if ((l as usize) != w.len) { return false; } + return pool_eq_text(o, l, w, 0, w.len); +} + +pub fn seg_set(i: usize, ax: i32, pf: i32, k0: i32, v0: i32, n: i32, dk: i32, dv: i32) -> usize { + SEG[i * 7] = ax; + SEG[i * 7 + 1] = pf; + SEG[i * 7 + 2] = k0; + SEG[i * 7 + 3] = v0; + SEG[i * 7 + 4] = n; + SEG[i * 7 + 5] = dk; + SEG[i * 7 + 6] = dv; + return i; +} + +/// A small die: 3 columns of 2 cells, w = 3, h = 2. Types 0 CLBLL_L, 1 INT_L, 2 BRAM_L. Cells (index gx*2+gy): +/// 0 is CLBLL_L, 1 to 3 are INT_L, 4 and 5 are BRAM_L. X numbers: CLBLL_L 7 -> gx 0; INT_L 5, 6 -> gx 1, 0; +/// BRAM_L 10 -> gx 2. Y numbers for every type: 20, 21 -> gy 0, 1. +pub fn small_die() -> i32 { + grid_begin(3, 2); + var n : usize = la_make("CLBLL_L", "INT_L", "BRAM_L", ""); + grid_types(LA[0 .. n]); + RLE[0] = 0; + RLE[1] = 1; + RLE[2] = 1; + RLE[3] = 3; + RLE[4] = 2; + RLE[5] = 2; + const r : i32 = grid_rle(RLE[0 .. 6]); + grid_prefixes(0, LA[0 .. n]); + grid_prefixes(1, LA[0 .. n]); + seg_set(0, 0, 0, 7, 0, 1, 1, 0); + seg_set(1, 0, 1, 5, 1, 2, 1, -1); + seg_set(2, 0, 2, 10, 2, 1, 1, 0); + grid_segs(SEG[0 .. 21]); + seg_set(0, 1, 3, 20, 0, 2, 1, 1); + seg_set(1, 1, 4, 20, 0, 2, 1, 1); + seg_set(2, 1, 5, 20, 0, 2, 1, 1); + grid_segs(SEG[0 .. 21]); + return r; +} + +test a_fasm_text_is_counted_by_tile { + tx_reset(); + tx_line("# nextpnr-xilinx 0.9.2-107-g7037c948"); + tx_line("# chipdb xc7a200tfbg484-2 version 1 generator prjxray "); + tx_line("CLBLL_L_X2Y10.SLICEL_X0.ALUT.INIT[63:0] = 64'hFFFF0000FFFF0000"); + tx_line("CLBLL_L_X2Y10.SLICEL_X0.CLUT.INIT[63:0] = 64'h0000_0000_0000_0000"); + tx_line("CLBLL_L_X2Y10.SLICEL_X0.FFSYNC"); + tx_line("INT_L_X5Y20.IMUX_L12.LOGIC_OUTS_L3 # a comment after"); + tx_line("INT_L_X5Y20.EE2BEG0.SS2END0 {annotation} "); + tx_line(" BRAM_L_X10Y20.BRAM_L.RAMB18_Y0.WRITE_WIDTH_A_18 = 1'b1"); + tx_line("BRAM_L_X10Y20.BRAM_L.INIT = 0"); + tx_line("garbage line without a tile"); + tx_line("lowercase_X1Y2.FOO"); + tx_line(""); + tx_line(" "); + const t : []const u8 = TXT[0 .. TXN]; + assert parse_fasm(t) == 0; + assert fa_info(0) == 5; + assert fa_info(1) == 2; + assert fa_info(2) == 2; + assert fa_info(3) == 2; + assert fa_info(4) == 3; + assert fa_info(5) == 2; + assert pool_is(fa_header(0, 0), fa_header(0, 1), "nextpnr-xilinx 0.9.2-107-g7037c948"); + assert pool_is(fa_header(1, 0), fa_header(1, 1), "chipdb xc7a200tfbg484-2 version 1 generator prjxray"); + assert pool_is(fa_tile(0, 0), fa_tile(0, 1), "CLBLL_L_X2Y10"); + assert fa_tile(0, 2) == 2; + assert pool_is(fa_tile(1, 0), fa_tile(1, 1), "INT_L_X5Y20"); + assert fa_tile(1, 2) == 2; + assert fa_tile(2, 2) == 1; + assert pool_is(fa_bad(0, 0), fa_bad(0, 1), "garbage line without a tile"); + assert pool_is(fa_bad(1, 0), fa_bad(1, 1), "lowercase_X1Y2.FOO"); +} + +test zero_values_clear_a_feature_and_other_values_set_it { + tx_reset(); + tx_line("T_X1Y1.A = 0"); + tx_line("T_X1Y1.B = 00"); + tx_line("T_X1Y1.C = 8'h00"); + tx_line("T_X1Y1.D = 64'b0000_0000"); + tx_line("T_X1Y1.E = 1"); + tx_line("T_X1Y1.F = 8'h01"); + tx_line("T_X1Y1.G = 'hf"); + tx_line("T_X1Y1.H = 8'x0"); + tx_line("T_X1Y1.I = 0'b"); + tx_line("T_X1Y1.J = "); + tx_line("T_X1Y1.K = 12'D00A"); + tx_line("T_X1Y1.L = _0_"); + assert parse_fasm(TXT[0 .. TXN]) == 0; + assert fa_info(2) == 5; + assert fa_info(0) == 7; + assert fa_tile(0, 2) == 7; +} + +test annotations_and_hashes_follow_the_javascript_order { + tx_reset(); + tx_line("T_X1Y1.A {x # y}"); + tx_line("{note} T_X1Y1.B"); + tx_line("T_X1Y1.C {unclosed"); + tx_line("T_X1Y1.D {a} = {b} 0"); + tx_line("T_X1Y1.{q}E"); + tx_line("T_X1Y1 .F"); + tx_line("T_X1Y1.G = 1 # keep"); + tx_line(".H = 1"); + tx_line("T_X1Y1"); + tx_line("T_X1Y1 = 1"); + assert parse_fasm(TXT[0 .. TXN]) == 0; + assert fa_info(1) == 0; + assert fa_info(0) == 5; + assert fa_info(2) == 1; + assert fa_info(5) == 4; + assert fa_tile(0, 2) == 5; +} + +test the_tile_name_is_split_into_type_and_numbers { + assert tile_split("CLBLL_L_X2Y10", 0, 13) == 7; + assert SC[0] == 2; + assert SC[1] == 10; + assert tile_split("A_X1Y2_X3Y4", 0, 11) == 6; + assert SC[0] == 3; + assert SC[1] == 4; + assert tile_split("A_X007Y0", 0, 8) == 1; + assert SC[0] == 7; + assert tile_split("_X1Y2", 0, 5) == -1; + assert tile_split("A_X1Y", 0, 5) == -1; + assert tile_split("A_X1Y2Y3", 0, 8) == -1; + assert tile_split("a_X1Y2", 0, 6) == -1; + assert tile_split("A_X1Y2 ", 0, 7) == -1; + assert tile_split("AX1Y2", 0, 5) == -1; +} + +test the_grid_finds_a_tile_only_when_the_cell_holds_that_type { + assert small_die() == 0; + assert grid_lookup("CLBLL_L_X7Y20", 0, 13); + assert lk(0) == 0; + assert lk(1) == 0; + assert lk(2) == 7; + assert lk(3) == 0; + assert grid_lookup("INT_L_X5Y21", 0, 11); + assert lk(0) == 1; + assert lk(1) == 1; + assert grid_lookup("INT_L_X6Y21", 0, 11); + assert lk(0) == 0; + assert lk(1) == 1; + assert lk(3) == 1; + assert grid_lookup("BRAM_L_X10Y21", 0, 13); + assert lk(0) == 2; + assert lk(1) == 1; + assert grid_lookup("INT_L_X6Y20", 0, 11) == false; + assert grid_lookup("CLBLL_L_X7Y21", 0, 13) == false; + assert grid_lookup("INT_L_X9Y20", 0, 11) == false; + assert grid_lookup("NOPE_X1Y1", 0, 9) == false; + assert grid_lookup("not a tile", 0, 10) == false; + assert grid_at(2, 1) == 2; + assert grid_at(3, 0) == -1; + assert grid_at(0, 2) == 1; + assert grid_at(-1, 0) == -1; +} + +test the_runs_must_cover_every_cell { + grid_begin(3, 2); + RLE[0] = 0; + RLE[1] = 5; + assert grid_rle(RLE[0 .. 2]) == 1; + RLE[1] = 7; + assert grid_rle(RLE[0 .. 2]) == 1; + RLE[1] = 6; + assert grid_rle(RLE[0 .. 2]) == 0; + RLE[2] = 9; + assert grid_rle(RLE[0 .. 3]) == 1; +} + +test a_summary_totals_the_placed_tiles_by_family { + assert small_die() == 0; + var a : usize = la_make("CLB", "INT", "BRAM", ""); + var b : usize = lb_make("logic", "routing", "memory", ""); + var c : usize = lc_make("logic", "routing", "memory", "other"); + assert fam_set(LA[0 .. a], LB[0 .. b], LC[0 .. c]) == 0; + assert family_of("CLBLL_L") == 0; + assert family_of("INT_L") == 1; + assert family_of("BRAM_L") == 2; + assert family_of("DSP_L") == 3; + a = la_make("INT", "XYZ", "", ""); + b = lb_make("routing", "missing", "", ""); + c = lc_make("logic", "routing", "", ""); + assert fam_set(LA[0 .. a], LB[0 .. b], LC[0 .. c]) == 0; + assert family_of("INT_R") == 1; + assert family_of("XYZ1") == -1; + assert family_of("QQ") == 1; + a = la_make("CLB", "INT", "BRAM", ""); + b = lb_make("logic", "routing", "memory", ""); + c = lc_make("logic", "routing", "memory", "other"); + assert fam_set(LA[0 .. a], LB[0 .. b], LC[0 .. c]) == 0; + tx_reset(); + tx_line("INT_L_X5Y20.A"); + tx_line("INT_L_X5Y20.B"); + tx_line("CLBLL_L_X7Y20.A"); + tx_line("BRAM_L_X10Y21.A"); + tx_line("BRAM_L_X10Y21.B"); + tx_line("BRAM_L_X10Y21.C"); + tx_line("INT_L_X6Y20.A"); + tx_line("INT_L_X6Y20.B"); + tx_line("INT_L_X6Y20.C"); + tx_line("DSP_L_X1Y1.A"); + tx_line("CLBLL_L_X7Y21.A"); + assert parse_fasm(TXT[0 .. TXN]) == 0; + assert summarize() == 0; + assert sm_info(0) == 3; + assert sm_info(1) == 3; + assert sm_info(2) == 11; + assert sm_info(3) == 6; + assert sm_fam(0, 0) == 1; + assert sm_fam(1, 0) == 1; + assert sm_fam(2, 0) == 1; + assert sm_fam(0, 1) == 1; + assert sm_fam(1, 1) == 2; + assert sm_fam(2, 1) == 3; + assert sm_info(5) >= 0; + assert sm_col(sm_info(5) as u32, 5) == 3; + assert sm_col(sm_info(5) as u32, 4) == 2; +} diff --git a/specs/widgets/html_out.t27 b/specs/widgets/html_out.t27 new file mode 100644 index 0000000000..4843e81e92 --- /dev/null +++ b/specs/widgets/html_out.t27 @@ -0,0 +1,280 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/html_out.t27 -- the HTML writer every native widget view uses. Epic gHashTag/t27#10937, +; stage "page" of specs/t27f/widget_factory.t27. A tool.js builds its page with el()/h() helpers and +; DOM calls (28 files, 8920 lines); a native view writes HTML text into the buffer the host gave it +; (a region of the wasm's linear memory) and the host sets it as innerHTML. This file is that writer, +; once: put a byte, put text (raw or HTML-escaped), put a number, put an attribute, fill a template. +; -- +; STATELESS ON PURPOSE. Every function takes the buffer o and the length written so far n and returns +; the new length. There is no module-level state because t27c's splicer does not carry a `pub var` +; across `use` (measured: a view that imported OUT and OUT_LEN got "undeclared identifier"), and +; follows only the names a module itself mentions, not the names an imported function mentions: so a +; view imports every function it calls, and these functions call only each other through the +; ANCHOR in the view (see jtag_verdict.t27, html_out_anchor). A write past the end of o returns OVER, +; and every function returns OVER at once when it is given OVER, so a view can never write outside o +; and checks once, at the end, with ok(n). +; ASCII only (L3), English only (LANG-EN). +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widgets_html_out; + +pub const OVER : u32 = 4294967295; +pub const H_LT : u8 = 60; +pub const H_GT : u8 = 62; +pub const H_AMP : u8 = 38; +pub const H_QUOTE : u8 = 34; +pub const H_APOS : u8 = 39; +pub const H_SPACE : u8 = 32; +pub const H_SLASH : u8 = 47; +pub const H_EQ : u8 = 61; + +pub fn ok(n: u32) -> bool { + return n != OVER; +} + +pub fn put(o: []u8, n: u32, b: u8) -> u32 { + if (n >= (o.len as u32)) { return OVER; } + o[n as usize] = b; + return n + 1; +} + +/// Raw bytes, as the caller wrote them (markup the view itself owns). +pub fn raw(o: []u8, n0: u32, s: []const u8) -> u32 { + var n : u32 = n0; + var i : usize = 0; + while (i < s.len) { + n = put(o, n, s[i]); + i = i + 1; + } + return n; +} + +/// One byte of text, escaped: & < > " ' become entities. +pub fn put_escaped(o: []u8, n: u32, b: u8) -> u32 { + if (b == H_AMP) { return raw(o, n, "&"); } + if (b == H_LT) { return raw(o, n, "<"); } + if (b == H_GT) { return raw(o, n, ">"); } + if (b == H_QUOTE) { return raw(o, n, """); } + if (b == H_APOS) { return raw(o, n, "'"); } + return put(o, n, b); +} + +/// Text from outside the view (data, words). +pub fn text(o: []u8, n0: u32, s: []const u8) -> u32 { + var n : u32 = n0; + var i : usize = 0; + while (i < s.len) { + n = put_escaped(o, n, s[i]); + i = i + 1; + } + return n; +} + +/// An unsigned decimal. +pub fn num(o: []u8, n0: u32, v0: u64) -> u32 { + var n : u32 = n0; + if (v0 == 0) { return put(o, n, 48); } + var digits : [20]u8 = [0; 20]; + var k : usize = 0; + var v : u64 = v0; + while (v > 0) { + digits[k] = ((v % 10) as u8) + 48; + v = v / 10; + k = k + 1; + } + while (k > 0) { + k = k - 1; + n = put(o, n, digits[k]); + } + return n; +} + +/// An unsigned decimal with thousands separated by commas, as toLocaleString('en-US') writes it. +pub fn num_grouped(o: []u8, n0: u32, v0: u64) -> u32 { + var n : u32 = n0; + if (v0 == 0) { return put(o, n, 48); } + var digits : [20]u8 = [0; 20]; + var k : usize = 0; + var v : u64 = v0; + while (v > 0) { + digits[k] = ((v % 10) as u8) + 48; + v = v / 10; + k = k + 1; + } + while (k > 0) { + k = k - 1; + n = put(o, n, digits[k]); + if (k > 0 and k % 3 == 0) { n = put(o, n, 44); } + } + return n; +} + +/// name="value" with the value escaped, preceded by a space. +pub fn attr(o: []u8, n0: u32, name: []const u8, value: []const u8) -> u32 { + var n : u32 = put(o, n0, H_SPACE); + n = raw(o, n, name); + n = put(o, n, H_EQ); + n = put(o, n, H_QUOTE); + n = text(o, n, value); + return put(o, n, H_QUOTE); +} + +/// {0} {1} {2} filled from k values (k up to 3), as fill() does in the tool.js files: a placeholder +/// whose index is not below k stays as written. The template and each value are escaped as text. +pub fn fill(o: []u8, n0: u32, t: []const u8, a: []const u8, b: []const u8, c: []const u8, k: u32) -> u32 { + var n : u32 = n0; + var i : usize = 0; + while (i < t.len) { + if (t[i] == 123 and i + 2 < t.len and t[i + 2] == 125 and t[i + 1] >= 48 and t[i + 1] < 48 + (k as u8) and t[i + 1] <= 50) { + if (t[i + 1] == 48) { n = text(o, n, a); } + if (t[i + 1] == 49) { n = text(o, n, b); } + if (t[i + 1] == 50) { n = text(o, n, c); } + i = i + 3; + } else { + n = put_escaped(o, n, t[i]); + i = i + 1; + } + } + return n; +} + +/// Is `name` (the bytes of t between braces) the key k? An empty key matches nothing. +pub fn name_is(t: []const u8, lo: usize, hi: usize, k: []const u8) -> bool { + if (k.len == 0 or hi - lo != k.len) { return false; } + var i : usize = 0; + while (i < k.len) { + if (t[lo + i] != k[i]) { return false; } + i = i + 1; + } + return true; +} + +pub fn is_word_byte(b: u8) -> bool { + return (b >= 48 and b <= 57) or (b >= 65 and b <= 90) or (b >= 97 and b <= 122) or b == 95; +} + +/// {name} filled from up to six key/value pairs, as the tool.js files' fill(s, { name: value }) does: +/// a placeholder whose name is not a key stays as written. A name is one or more of [A-Za-z0-9_]. +/// The template and each value are escaped as text. +pub fn fill_named(o: []u8, n0: u32, t: []const u8, k0: []const u8, v0: []const u8, k1: []const u8, v1: []const u8, k2: []const u8, v2: []const u8, k3: []const u8, v3: []const u8, k4: []const u8, v4: []const u8, k5: []const u8, v5: []const u8) -> u32 { + var n : u32 = n0; + var i : usize = 0; + while (i < t.len) { + var closed : usize = 0; + if (t[i] == 123) { + var j : usize = i + 1; + while (j < t.len and is_word_byte(t[j])) { j = j + 1; } + if (j > i + 1 and j < t.len and t[j] == 125) { closed = j; } + } + if (closed > 0) { + if (name_is(t, i + 1, closed, k0)) { n = text(o, n, v0); } + else { if (name_is(t, i + 1, closed, k1)) { n = text(o, n, v1); } + else { if (name_is(t, i + 1, closed, k2)) { n = text(o, n, v2); } + else { if (name_is(t, i + 1, closed, k3)) { n = text(o, n, v3); } + else { if (name_is(t, i + 1, closed, k4)) { n = text(o, n, v4); } + else { if (name_is(t, i + 1, closed, k5)) { n = text(o, n, v5); } + else { + var q : usize = i; + while (q <= closed) { n = put(o, n, t[q]); q = q + 1; } + } } } } } } + i = closed + 1; + } else { + n = put_escaped(o, n, t[i]); + i = i + 1; + } + } + return n; +} + +/// An unsigned decimal written to dst (for use as a value in fill()); returns its length. +pub fn dec_to(dst: []u8, v0: u64) -> u32 { + if (v0 == 0) { dst[0] = 48; return 1; } + var digits : [20]u8 = [0; 20]; + var k : usize = 0; + var v : u64 = v0; + while (v > 0) { + digits[k] = ((v % 10) as u8) + 48; + v = v / 10; + k = k + 1; + } + var n : u32 = 0; + while (k > 0) { + k = k - 1; + dst[n as usize] = digits[k]; + n = n + 1; + } + return n; +} + +pub fn same_bytes(o: []const u8, n: u32, want: []const u8) -> bool { + if (n != (want.len as u32)) { return false; } + var i : usize = 0; + while (i < want.len) { + if (o[i] != want[i]) { return false; } + i = i + 1; + } + return true; +} + +test the_writer_escapes_text_and_leaves_markup_alone { + var o : [128]u8 = [0; 128]; + var n : u32 = raw(o[0..], 0, ""); + n = text(o[0..], n, "x < y & 'z'"); + n = raw(o[0..], n, "

"); + assert same_bytes(o[0..], n, "

x < y & 'z'

"); + assert ok(n); +} + +test numbers_are_decimal_and_grouped_like_en_us { + var o : [128]u8 = [0; 128]; + var n : u32 = num(o[0..], 0, 0); + n = put(o[0..], n, 32); + n = num(o[0..], n, 1234567); + n = put(o[0..], n, 32); + n = num_grouped(o[0..], n, 0); + n = put(o[0..], n, 32); + n = num_grouped(o[0..], n, 999); + n = put(o[0..], n, 32); + n = num_grouped(o[0..], n, 1000); + n = put(o[0..], n, 32); + n = num_grouped(o[0..], n, 215360); + n = put(o[0..], n, 32); + n = num_grouped(o[0..], n, 1234567890); + assert same_bytes(o[0..], n, "0 1234567 0 999 1,000 215,360 1,234,567,890"); +} + +test fill_puts_values_in_the_template_and_escapes_them { + var o : [128]u8 = [0; 128]; + var n : u32 = fill(o[0..], 0, "{0} of {1}, {2}{3}", "Gatle", "<6>", "end", 3); + assert same_bytes(o[0..], n, "Gatle of <6>, end{3}"); + n = fill(o[0..], 0, "{0}{1} it's", "x", "", "", 1); + assert same_bytes(o[0..], n, "x{1} it's"); +} + +test the_buffer_never_writes_past_its_end { + var o : [8]u8 = [0; 8]; + var n : u32 = raw(o[0..], 0, "12345678"); + assert ok(n); + assert n == 8; + n = raw(o[0..], n, "9"); + assert ok(n) == false; + n = text(o[0..], n, "more"); + assert n == OVER; + assert o[7] == 56; + var p : [4]u8 = [0; 4]; + assert text(p[0..], 0, "<<") == OVER; +} + +test named_placeholders_fill_from_pairs_and_unknown_ones_stay { + var o : [160]u8 = [0; 160]; + var n : u32 = fill_named(o[0..], 0, "Snapshot {at} UTC, {days} days of {repo}; {nope} {a_b} {} {x", "at", "2026-10-04 17:06", "days", "7", "repo", "gHashTag/t27", "", "", "", "", "", ""); + assert same_bytes(o[0..], n, "Snapshot 2026-10-04 17:06 UTC, 7 days of gHashTag/t27; {nope} {a_b} {} {x"); + n = fill_named(o[0..], 0, "{n} of {boundary}: <{n}>", "n", "5 & 6", "boundary", "9", "", "", "", "", "", "", "", ""); + assert same_bytes(o[0..], n, "5 & 6 of 9: <5 & 6>"); + var d : [20]u8 = [0; 20]; + assert dec_to(d[0..], 0) == 1; + assert dec_to(d[0..], 4294967296) == 10; + assert d[0] == 52 and d[9] == 54; +} diff --git a/specs/widgets/json_scan.t27 b/specs/widgets/json_scan.t27 new file mode 100644 index 0000000000..4466917a2e --- /dev/null +++ b/specs/widgets/json_scan.t27 @@ -0,0 +1,429 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/json_scan.t27 -- read JSON as bytes, without building a tree. Epic gHashTag/t27#10937, +; stage "page" of specs/t27f/widget_factory.t27: 23 of the 28 tools fetch a data.json (or pool.json, +; design.json) and draw from it; their tool.js calls JSON.parse and walks the result. A native view +; cannot allocate a tree, so it asks this file for the SPAN of a value and reads inside it. +; -- +; A span is the byte range of one value, packed in an i64 (lo * 2^32 + hi) as page_reader.t27 packs +; texts; NO_SPAN (-1) is "not there". obj_get finds a key's value, arr_at an element, str_len and +; str_byte read a string with its escapes decoded (\uXXXX and surrogate pairs become UTF-8), num_f64 +; and num_i64 read a number, is_true/is_false/is_null the literals. The input is trusted to be JSON +; the data builder wrote: a malformed byte gives NO_SPAN or a wrong value, never a write outside the +; slice. +; Numbers: num_f64 is m / 10^k (or m * 10^k) with m an integer below 2^53 and 10^k exactly +; representable (k up to 22): one correctly rounded operation, equal to JavaScript's Number() for +; those inputs. Outside them (more than 15 or 16 significant digits, an exponent past 22) the last +; digit may differ. MEASURED on the 143 JSON files of the widgets (284965 values read against +; JSON.parse): 6 values differ, all numbers of 16 or 17 significant digits (t27-cell storage permille +; and the same ratios in t27-vs-nvfp4), each in the last place; a correct slow path is not written. A first version divided by 10 k times and was wrong in the last digit of 8 numbers +; of the widgets' data. +; ASCII only (L3), English only (LANG-EN). +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widgets_json_scan; + +pub const NO_SPAN : i64 = -1; +pub const SPAN : i64 = 4294967296; + +pub const J_OBJECT : u8 = 1; +pub const J_ARRAY : u8 = 2; +pub const J_STRING : u8 = 3; +pub const J_NUMBER : u8 = 4; +pub const J_TRUE : u8 = 5; +pub const J_FALSE : u8 = 6; +pub const J_NULL : u8 = 7; +pub const J_NONE : u8 = 0; + +pub fn jspan(lo: usize, hi: usize) -> i64 { + return (lo as i64) * SPAN + (hi as i64); +} + +pub fn span_lo(sp: i64) -> usize { + return (sp / SPAN) as usize; +} + +pub fn span_hi(sp: i64) -> usize { + return (sp % SPAN) as usize; +} + +pub fn skip_ws(s: []const u8, p0: usize) -> usize { + var p : usize = p0; + while (p < s.len and (s[p] == 32 or s[p] == 9 or s[p] == 10 or s[p] == 13)) { p = p + 1; } + return p; +} + +/// The end offset of the string whose opening quote is at p (just past its closing quote); s.len + 1 when unclosed. +pub fn string_end(s: []const u8, p: usize) -> usize { + var q : usize = p + 1; + while (q < s.len) { + if (s[q] == 92) { q = q + 2; continue; } + if (s[q] == 34) { return q + 1; } + q = q + 1; + } + return s.len + 1; +} + +/// The end offset of the value starting at p (a non-space byte); s.len + 1 when it is cut short. +pub fn value_end(s: []const u8, p: usize) -> usize { + if (p >= s.len) { return s.len + 1; } + const c : u8 = s[p]; + if (c == 34) { return string_end(s, p); } + if (c == 123 or c == 91) { + var depth : u32 = 0; + var q : usize = p; + while (q < s.len) { + const b : u8 = s[q]; + if (b == 34) { + q = string_end(s, q); + continue; + } + if (b == 123 or b == 91) { depth = depth + 1; } + if (b == 125 or b == 93) { + depth = depth - 1; + if (depth == 0) { return q + 1; } + } + q = q + 1; + } + return s.len + 1; + } + var q : usize = p; + while (q < s.len and s[q] != 44 and s[q] != 125 and s[q] != 93 and s[q] != 32 and s[q] != 9 and s[q] != 10 and s[q] != 13) { q = q + 1; } + return q; +} + +/// The span of the whole document: its one value, white space trimmed. +pub fn root(s: []const u8) -> i64 { + const a : usize = skip_ws(s, 0); + if (a >= s.len) { return NO_SPAN; } + const e : usize = value_end(s, a); + if (e > s.len) { return NO_SPAN; } + return jspan(a, e); +} + +pub fn kind(s: []const u8, sp: i64) -> u8 { + if (sp < 0) { return J_NONE; } + const p : usize = span_lo(sp); + const c : u8 = s[p]; + if (c == 123) { return J_OBJECT; } + if (c == 91) { return J_ARRAY; } + if (c == 34) { return J_STRING; } + if (c == 116) { return J_TRUE; } + if (c == 102) { return J_FALSE; } + if (c == 110) { return J_NULL; } + return J_NUMBER; +} + +/// Is the key at s[lo .. hi] (a string with its quotes) equal to `key` once its escapes are decoded? +pub fn key_is(s: []const u8, lo: usize, hi: usize, key: []const u8) -> bool { + const sp : i64 = jspan(lo, hi); + if (str_len(s, sp) != (key.len as u32)) { return false; } + var k : usize = 0; + while (k < key.len) { + if (str_byte(s, sp, k as u32) != key[k]) { return false; } + k = k + 1; + } + return true; +} + +/// The value of `key` in the object at sp (the first such key), NO_SPAN when absent or sp is no object. +pub fn obj_get(s: []const u8, sp: i64, key: []const u8) -> i64 { + if (kind(s, sp) != J_OBJECT) { return NO_SPAN; } + const hi : usize = span_hi(sp); + var p : usize = skip_ws(s, span_lo(sp) + 1); + while (p < hi and s[p] == 34) { + const ke : usize = string_end(s, p); + if (ke > hi) { return NO_SPAN; } + const q : usize = skip_ws(s, ke); + if (q >= hi or s[q] != 58) { return NO_SPAN; } + const v : usize = skip_ws(s, q + 1); + const ve : usize = value_end(s, v); + if (ve > hi) { return NO_SPAN; } + if (key_is(s, p, ke, key)) { return jspan(v, ve); } + p = skip_ws(s, ve); + if (p < hi and s[p] == 44) { p = skip_ws(s, p + 1); } + } + return NO_SPAN; +} + +/// How many elements the array at sp holds (0 when it is no array). +pub fn arr_len(s: []const u8, sp: i64) -> u32 { + if (kind(s, sp) != J_ARRAY) { return 0; } + const hi : usize = span_hi(sp); + var p : usize = skip_ws(s, span_lo(sp) + 1); + var n : u32 = 0; + while (p < hi - 1) { + const e : usize = value_end(s, p); + if (e > hi) { return n; } + n = n + 1; + p = skip_ws(s, e); + if (p < hi and s[p] == 44) { p = skip_ws(s, p + 1); } + } + return n; +} + +/// The i-th element of the array at sp, NO_SPAN past the end. +pub fn arr_at(s: []const u8, sp: i64, i: u32) -> i64 { + if (kind(s, sp) != J_ARRAY) { return NO_SPAN; } + const hi : usize = span_hi(sp); + var p : usize = skip_ws(s, span_lo(sp) + 1); + var n : u32 = 0; + while (p < hi - 1) { + const e : usize = value_end(s, p); + if (e > hi) { return NO_SPAN; } + if (n == i) { return jspan(p, e); } + n = n + 1; + p = skip_ws(s, e); + if (p < hi and s[p] == 44) { p = skip_ws(s, p + 1); } + } + return NO_SPAN; +} + +pub fn hex4(s: []const u8, p: usize) -> u32 { + var v : u32 = 0; + var i : usize = 0; + while (i < 4) { + const b : u8 = s[p + i]; + var d : u32 = 0; + if (b >= 48 and b <= 57) { d = (b - 48) as u32; } + if (b >= 97 and b <= 102) { d = (b - 97) as u32 + 10; } + if (b >= 65 and b <= 70) { d = (b - 65) as u32 + 10; } + v = v * 16 + d; + i = i + 1; + } + return v; +} + +/// The code point (and its source length) at s[p ..], p just after a backslash: returns cp * 16 + srclen. +pub fn escape_at(s: []const u8, p: usize, hi: usize) -> u32 { + const c : u8 = s[p]; + if (c == 110) { return 10 * 16 + 1; } + if (c == 116) { return 9 * 16 + 1; } + if (c == 114) { return 13 * 16 + 1; } + if (c == 98) { return 8 * 16 + 1; } + if (c == 102) { return 12 * 16 + 1; } + if (c == 117 and p + 4 < hi) { + const u : u32 = hex4(s, p + 1); + if (u >= 55296 and u <= 56319 and p + 10 < hi and s[p + 5] == 92 and s[p + 6] == 117) { + const lo : u32 = hex4(s, p + 7); + if (lo >= 56320 and lo <= 57343) { + const cp : u32 = 65536 + (u - 55296) * 1024 + (lo - 56320); + return cp * 16 + 11; + } + } + return u * 16 + 5; + } + return (c as u32) * 16 + 1; +} + +pub fn cp_utf8_len(cp: u32) -> u32 { + if (cp < 128) { return 1; } + if (cp < 2048) { return 2; } + if (cp < 65536) { return 3; } + return 4; +} + +pub fn cp_utf8_byte(cp: u32, j: u32) -> u8 { + if (cp < 128) { return cp as u8; } + if (cp < 2048) { + if (j == 0) { return (192 + cp / 64) as u8; } + return (128 + cp % 64) as u8; + } + if (cp < 65536) { + if (j == 0) { return (224 + cp / 4096) as u8; } + if (j == 1) { return (128 + (cp / 64) % 64) as u8; } + return (128 + cp % 64) as u8; + } + if (j == 0) { return (240 + cp / 262144) as u8; } + if (j == 1) { return (128 + (cp / 4096) % 64) as u8; } + if (j == 2) { return (128 + (cp / 64) % 64) as u8; } + return (128 + cp % 64) as u8; +} + +/// The length in bytes of the string at sp once its escapes are decoded to UTF-8. +pub fn str_len(s: []const u8, sp: i64) -> u32 { + if (kind(s, sp) != J_STRING) { return 0; } + const hi : usize = span_hi(sp) - 1; + var p : usize = span_lo(sp) + 1; + var n : u32 = 0; + while (p < hi) { + if (s[p] == 92 and p + 1 < hi) { + const e : u32 = escape_at(s, p + 1, hi); + n = n + cp_utf8_len(e / 16); + p = p + 1 + ((e % 16) as usize); + } else { + n = n + 1; + p = p + 1; + } + } + return n; +} + +/// The k-th byte of the decoded string at sp; 0 past the end. +pub fn str_byte(s: []const u8, sp: i64, k: u32) -> u8 { + if (kind(s, sp) != J_STRING) { return 0; } + const hi : usize = span_hi(sp) - 1; + var p : usize = span_lo(sp) + 1; + var n : u32 = 0; + while (p < hi) { + var cp : u32 = s[p] as u32; + var adv : usize = 1; + if (s[p] == 92 and p + 1 < hi) { + const e : u32 = escape_at(s, p + 1, hi); + cp = e / 16; + adv = 1 + ((e % 16) as usize); + } + const w : u32 = cp_utf8_len(cp); + if (adv == 1 and s[p] != 92 and s[p] >= 128) { + // a UTF-8 byte already: copied as it stands + if (n == k) { return s[p]; } + n = n + 1; + p = p + 1; + continue; + } + if (k < n + w) { return cp_utf8_byte(cp, k - n); } + n = n + w; + p = p + adv; + } + return 0; +} + +pub fn is_true(s: []const u8, sp: i64) -> bool { + return kind(s, sp) == J_TRUE; +} + +pub fn is_false(s: []const u8, sp: i64) -> bool { + return kind(s, sp) == J_FALSE; +} + +pub fn is_null(s: []const u8, sp: i64) -> bool { + return kind(s, sp) == J_NULL; +} + +/// The number at sp as a double; 0 when it is no number. +pub fn num_f64(s: []const u8, sp: i64) -> f64 { + if (kind(s, sp) != J_NUMBER) { return 0.0; } + const hi : usize = span_hi(sp); + var p : usize = span_lo(sp); + var neg : bool = false; + if (s[p] == 45) { neg = true; p = p + 1; } + var m : f64 = 0.0; + var scale : i32 = 0; + var seen_dot : bool = false; + while (p < hi) { + const b : u8 = s[p]; + if (b >= 48 and b <= 57) { + m = m * 10.0 + ((b - 48) as f64); + if (seen_dot) { scale = scale - 1; } + } else { + if (b == 46) { seen_dot = true; } else { break; } + } + p = p + 1; + } + if (p < hi and (s[p] == 101 or s[p] == 69)) { + p = p + 1; + var eneg : bool = false; + if (p < hi and s[p] == 45) { eneg = true; p = p + 1; } + if (p < hi and s[p] == 43) { p = p + 1; } + var e : i32 = 0; + while (p < hi and s[p] >= 48 and s[p] <= 57) { + e = e * 10 + ((s[p] - 48) as i32); + p = p + 1; + } + if (eneg) { scale = scale - e; } else { scale = scale + e; } + } + // One division or one multiplication by an exactly representable power of ten: correctly rounded. + // Repeated division by 10 is not (measured: 15042 / 10 / 10 / 10 gives 15.042000000000002). + var p10 : f64 = 1.0; + var k : i32 = scale; + if (k < 0) { k = 0 - k; } + while (k > 0) { p10 = p10 * 10.0; k = k - 1; } + var v : f64 = m; + if (scale < 0) { v = m / p10; } else { v = m * p10; } + if (neg) { return 0.0 - v; } + return v; +} + +/// The integer part of the number at sp (truncated toward zero) as an i64. +pub fn num_i64(s: []const u8, sp: i64) -> i64 { + return num_f64(s, sp) as i64; +} + +test objects_give_the_span_of_a_key { + const d : []const u8 = "{ \"days\": 7, \"repo\": \"gHashTag/t27\", \"lanes\": { \"bee\": { \"merged\": 152 } }, \"list\": [1, 22, \"x\"] }"; + const r : i64 = root(d); + assert r >= 0; + assert kind(d, r) == J_OBJECT; + assert num_i64(d, obj_get(d, r, "days")) == 7; + assert str_len(d, obj_get(d, r, "repo")) == 12; + assert str_byte(d, obj_get(d, r, "repo"), 0) == 103; + assert num_i64(d, obj_get(d, obj_get(d, obj_get(d, r, "lanes"), "bee"), "merged")) == 152; + assert obj_get(d, r, "missing") == NO_SPAN; + assert obj_get(d, obj_get(d, r, "days"), "x") == NO_SPAN; +} + +test arrays_give_their_length_and_elements { + const d : []const u8 = "{ \"list\": [1, 22, \"x\", [3, 4], {}] , \"empty\": [] , \"one\": [ 5 ] }"; + const r : i64 = root(d); + const l : i64 = obj_get(d, r, "list"); + assert arr_len(d, l) == 5; + assert num_i64(d, arr_at(d, l, 1)) == 22; + assert kind(d, arr_at(d, l, 2)) == J_STRING; + assert arr_len(d, arr_at(d, l, 3)) == 2; + assert num_i64(d, arr_at(d, arr_at(d, l, 3), 1)) == 4; + assert kind(d, arr_at(d, l, 4)) == J_OBJECT; + assert arr_at(d, l, 5) == NO_SPAN; + assert arr_len(d, obj_get(d, r, "empty")) == 0; + assert arr_len(d, obj_get(d, r, "one")) == 1; + assert arr_len(d, r) == 0; +} + +test strings_decode_their_escapes_to_utf8 { + const d : []const u8 = "{ \"a\": \"x\\\"y\\\\z\\n\", \"u\": \"\\u00e9\\u20ac\\ud83d\\ude00\" }"; + const r : i64 = root(d); + const a : i64 = obj_get(d, r, "a"); + assert str_len(d, a) == 6; + assert str_byte(d, a, 1) == 34; + assert str_byte(d, a, 3) == 92; + assert str_byte(d, a, 5) == 10; + const u : i64 = obj_get(d, r, "u"); + assert str_len(d, u) == 9; + assert str_byte(d, u, 0) == 195; + assert str_byte(d, u, 1) == 169; + assert str_byte(d, u, 2) == 226; + assert str_byte(d, u, 4) == 172; + assert str_byte(d, u, 5) == 240; + assert str_byte(d, u, 8) == 128; +} + +test numbers_and_literals_read_like_javascript { + const d : []const u8 = "{ \"a\": 0.3, \"b\": -17.25, \"c\": 1e3, \"d\": 2.5E-2, \"e\": 12345678, \"f\": true, \"g\": false, \"h\": null }"; + const r : i64 = root(d); + assert num_f64(d, obj_get(d, r, "a")) == 0.3; + assert num_f64(d, obj_get(d, r, "b")) == -17.25; + assert num_f64(d, obj_get(d, r, "c")) == 1000.0; + assert num_f64(d, obj_get(d, r, "d")) == 0.025; + assert num_f64("15.042", root("15.042")) == 15.042; + assert num_f64("7.238", root("7.238")) == 7.238; + assert num_f64("1.584962500721156", root("1.584962500721156")) == 1.584962500721156; + assert num_i64(d, obj_get(d, r, "e")) == 12345678; + assert num_i64(d, obj_get(d, r, "b")) == -17; + assert is_true(d, obj_get(d, r, "f")); + assert is_false(d, obj_get(d, r, "g")); + assert is_null(d, obj_get(d, r, "h")); + assert is_true(d, obj_get(d, r, "g")) == false; +} + +test a_key_is_matched_after_its_escapes_are_decoded { + const d : []const u8 = "{ \"\\\\gf16_add\": 1, \"a\\u0062c\": 2 }"; + const r : i64 = root(d); + assert num_i64(d, obj_get(d, r, "\\gf16_add")) == 1; + assert num_i64(d, obj_get(d, r, "abc")) == 2; + assert obj_get(d, r, "gf16_add") == NO_SPAN; +} + +test a_cut_or_empty_document_has_no_span { + assert root("") == NO_SPAN; + assert root(" ") == NO_SPAN; + assert root("{ \"a\": [1, 2") == NO_SPAN; + assert obj_get("{ \"a\": 1", root("{ \"a\": 1"), "a") == NO_SPAN; +} diff --git a/specs/widgets/jtag_verdict.t27 b/specs/widgets/jtag_verdict.t27 new file mode 100644 index 0000000000..144177d227 --- /dev/null +++ b/specs/widgets/jtag_verdict.t27 @@ -0,0 +1,689 @@ +// SPDX-License-Identifier: Apache-2.0 +; specs/widgets/jtag-verdict.t27 -- one IR capture and one DR word, and what they say about the link +; Source of truth for public/widgets/jtag-verdict/ (gHashTag/trinity): scripts/widget-pages-from-spec.mjs +; compiles this file, checks its constants, evaluates the tests below and writes the page; the page +; hands every constant to tool.js as window.T27_WIDGET. tool.js holds no English of its own: every +; word on screen is a SAY_ constant here, every number the tool relies on is a K_ constant. +; ASCII only (L3), English only (LANG-EN). + +; WHAT IT DOES: the reader types the raw IR capture and the raw DR word a JTAG probe shifted out +; (or picks one of the seven vectors below). The page runs the same five checks, in the same order, +; as t27 specs/port/tools/jtag/tdo_verdict.t27 and `tri fpga-jtag --decode HEX --ir HEX`: all ones, +; all zeros, the IR capture rule, bit 0 of the DR word, and only then the IDCODE itself. It shows +; which check decided, the bits it looked at, and the command that prints the same verdict. + +; THE RULE: IEEE 1149.1 Capture-IR loads binary 01 into the two least significant IR cells, so +; the first two bits out of TDO after an IR scan are 1 then 0 on every compliant TAP. Above them +; the device may put anything: Xilinx 7-series BSDL files give INSTRUCTION_CAPTURE as XXXX01, the +; upper four being status. 0x35 (110101) is one value of that shape and is used here as the +; example; it is not a bench reading. A DR word with bit 0 clear is BYPASS when the IR capture +; holds 01, and noise when it does not: the same even word gets two different verdicts. + +; DATA PROVENANCE: the seven vectors are the test vectors of t27 specs/port/tools/jtag/ +; tdo_verdict.t27 at commit 60166a56194b5730dea355e704e98bc4f540cf14 (gHashTag/t27#8032, issue +; #8030), and the tests below assert the same verdicts. 0x13636093 is the raw IDCODE read from our +; XC7A200T on 2026-10-04 (openFPGALoader 1.1.1 --detect --verbose-level 2, see the idcode widget); +; 0x03631093 is the XC7A100T entry of openFPGALoader v1.1.1 src/part.hpp. + +; WHAT IT DOES NOT CLAIM: that it talks to a cable. Nothing is read from hardware; the values are +; typed. A clean verdict says the shift looks sane, not that the board is configured or healthy. +; The device table has two entries; the idcode widget carries openFPGALoader's full list. +; phi^2 + 1/phi^2 = 3 | TRINITY + +module widget_jtag_verdict; + +use widgets::html_out::ok; +use widgets::html_out::OVER; +use widgets::html_out::put; +use widgets::html_out::raw; +use widgets::html_out::put_escaped; +use widgets::html_out::text; +use widgets::html_out::num; +use widgets::html_out::attr; +use widgets::html_out::fill; +use widgets::html_out::H_LT; +use widgets::html_out::H_GT; +use widgets::html_out::H_AMP; +use widgets::html_out::H_QUOTE; +use widgets::html_out::H_APOS; +use widgets::html_out::H_SPACE; +use widgets::html_out::H_SLASH; +use widgets::html_out::H_EQ; +use widgets::page_reader::match_in; + + +pub const KIND : str = "widget-tool"; +pub const ID : str = "jtag-verdict"; +pub const TITLE : str = "JTAG verdict: IR capture and DR word, BYPASS or a broken shift"; +pub const DESCRIPTION : str = "Type the IR capture and the DR word a JTAG probe shifted out. Five checks in a fixed order say stuck line, broken shift, BYPASS or a real IDCODE, and which bit decided."; +pub const IMAGE_ALT : str = "JTAG verdict card: the same even DR word 0x13636092 read twice. With IR capture 0x35, whose low two bits are 01, the verdict is BYPASS: the TAP is alive. With IR capture 0x34, low bits 00, the verdict is broken shift: the DR word means nothing yet. Below, the five checks in order, from t27 spec tdo_verdict.t27 with 8 tests."; +pub const HOOK : str = "Same DR word, two verdicts: IR 0x35 says BYPASS, IR 0x34 says your shift is broken. Type what your probe read and see which bit decides."; +pub const CATEGORY : str = "fpga"; +pub const DATA_SOURCES : [4]str = ["t27 specs/port/tools/jtag/tdo_verdict.t27 at 60166a56194b5730dea355e704e98bc4f540cf14 (gHashTag/t27#8032, issue #8030): the order of the checks and the seven test vectors", "IEEE Std 1149.1 Capture-IR: binary 01 in the two least significant instruction register cells; Xilinx 7-series BSDL INSTRUCTION_CAPTURE XXXX01", "Bench 2026-10-04 UTC: openFPGALoader 1.1.1 --detect --verbose-level 2 on our XC7A200T: raw IDCODE 0x13636093 (the idcode widget's record)", "openFPGALoader v1.1.1 src/part.hpp: 0x03636093 xc7a200, 0x03631093 xc7a100"]; +pub const READS_LOCAL_FILES : bool = false; +pub const SENDS_NOTHING : bool = true; + +pub const K_CARD_WIDTH : u16 = 1200; +pub const K_CARD_HEIGHT : u16 = 630; + +; --- Numbers the tool relies on ------------------------------------------------------------ +; Capture-IR: the low two bits must read 01. +pub const K_IR_MASK : u32 = 3; +pub const K_IR_LOW : u32 = 1; +; 0xFFFFFFFF: TDO floating high, nothing drives it. +pub const K_ALL_ONES : u32 = 4294967295; +; Verdict codes, the same numbers as tdo_verdict.t27; they index SAY_VERDICT_NAMES from 1. +pub const K_V_STUCK_HIGH : u8 = 1; +pub const K_V_STUCK_LOW : u8 = 2; +pub const K_V_BROKEN_SHIFT : u8 = 3; +pub const K_V_BYPASS : u8 = 4; +pub const K_V_IDCODE : u8 = 5; +; IDCODE lookup ignores the version nibble: 0x0FFFFFFF. +pub const K_VERSION_MASK : u32 = 268435455; +pub const K_KNOWN_IDCODE : [2]u32 = [56844435, 56823955]; +; The seven vectors: IR capture, DR word, expected verdict. +pub const K_CASE_IR : [7]u32 = [53, 53, 52, 54, 63, 0, 63]; +pub const K_CASE_DR : [7]u32 = [325279891, 325279890, 325279890, 325279891, 4294967295, 0, 325279891]; +pub const K_CASE_WANT : [7]u8 = [5, 4, 3, 3, 1, 2, 3]; +; The vector the page opens on: the even word behind a good IR capture. +pub const K_START_CASE : u8 = 1; + +; --- Words on screen ------------------------------------------------------------------------ +; Templates take {0}, {1}, ... in order. +pub const SAY_LOADING : str = "Loading"; +pub const SAY_IR_LABEL : str = "IR capture (hex)"; +pub const SAY_DR_LABEL : str = "DR word (hex)"; +pub const SAY_IR_HINT : str = "The bits TDO shifted out during the IR scan, e.g. 0x35"; +pub const SAY_DR_HINT : str = "The 32 bits shifted out of the data register, e.g. 0x13636093"; +pub const SAY_CASES_LABEL : str = "The spec's test vectors"; +pub const SAY_CASE_NAMES : [7]str = ["real IDCODE", "BYPASS", "even word, IR 0x34", "odd word, IR 0x36", "all ones", "all zeros", "real-looking word, IR 0x3F"]; +pub const SAY_VERDICT_NAMES : [5]str = ["stuck high", "stuck low", "broken shift", "BYPASS", "IDCODE"]; +pub const SAY_VERDICT_WHY : [5]str = ["Every DR bit reads 1: nothing drives TDO. Check the cable's target voltage, board power, the ribbon and its pin 1, then the TDO wire.", "DR and IR both read 0: TDO is held low. Suspect the cable's own buffer before the board.", "IR capture {0} ends in {1}, not 01. Capture-IR always loads 01, so the shift itself is broken: TCK, the TMS order, or the TDO path. The DR word means nothing yet.", "IR capture {0} ends in 01, so the TAP shifts correctly, and DR bit 0 is 0: BYPASS is selected, not IDCODE. Load the IDCODE instruction, or this was read one bit late.", "IR capture {0} ends in 01 and DR bit 0 is 1: a real IDCODE."]; +pub const SAY_LADDER_TITLE : str = "The checks, in the order they run"; +pub const SAY_LADDER_STEPS : [5]str = ["DR is all ones", "DR is 0 and IR is 0", "IR does not end in 01", "DR bit 0 is 0", "otherwise"]; +pub const SAY_LADDER_DECIDED : str = "decided here"; +pub const SAY_LADDER_PASSED : str = "passed"; +pub const SAY_LADDER_SKIPPED : str = "not reached"; +pub const SAY_BITS_TITLE : str = "The bits that decide"; +pub const SAY_BITS_IR : str = "IR, low bit first"; +pub const SAY_BITS_DR : str = "DR bit 0"; +pub const SAY_BITS_IR_RULE : str = "must read 1 then 0"; +pub const SAY_DEVICE_KNOWN : str = "Masked IDCODE {0}: {1}"; +pub const SAY_DEVICE_NAMES : [2]str = ["XC7A200T", "XC7A100T"]; +pub const SAY_DEVICE_UNKNOWN : str = "Masked IDCODE {0}: not in this page's two-entry table."; +pub const SAY_DECODER_LINK : str = "Take the word apart in the IDCODE decoder"; +pub const SAY_CLI_TITLE : str = "The same verdict on the command line"; +pub const SAY_CLI_LINE : str = "tri fpga-jtag --decode {0} --ir {1}"; +pub const SAY_CLI_EXIT : str = "exit {0}"; +pub const SAY_BAD_HEX : str = "Not a hex number that fits in 32 bits: {0}"; +pub const SAY_SELF_CHECK : str = "This page against the spec's {0} vectors: {1} agree."; +pub const SAY_NOTE : str = "Nothing is read from a cable: the values are typed. 0x35 is an example of the XXXX01 shape, not a bench reading. A clean verdict says the shift looks sane, not that the board is configured."; +pub const SAY_SPEC_LINK : str = "The spec: tdo_verdict.t27, 8 tests"; +pub const SAY_SPEC_HREF : str = "https://github.com/gHashTag/t27/pull/8032"; +pub const SAY_DECODER_HREF : str = "../idcode/#id={0}"; +; The page keeps the two words in its address: #ir=0x35&dr=0x13636092. +pub const SAY_HASH_IR : str = "ir"; +pub const SAY_HASH_DR : str = "dr"; +pub const SAY_CARD_TITLE : str = "JTAG verdict"; +pub const SAY_CARD_BRAND : str = "Trinity S3AI"; +pub const SAY_CARD_HEAD : str = "Same DR word, two verdicts"; +pub const SAY_CARD_IR_LINE : str = "IR capture {0}, low bits {1}"; +pub const SAY_CARD_SPEC : str = "t27 spec tdo_verdict.t27, 8 tests"; +pub const SAY_CARD_URL : str = "t27.ai/widgets/jtag-verdict/"; + +test the_card_is_x_shaped { + assert K_CARD_WIDTH == 1200; + assert K_CARD_HEIGHT == 630; +} + +test the_page_sends_nothing { + assert SENDS_NOTHING == true; + assert READS_LOCAL_FILES == false; + assert CATEGORY == "fpga"; +} + +test the_vectors_cover_every_verdict { + assert K_CASE_WANT[0] == K_V_IDCODE; + assert K_CASE_WANT[1] == K_V_BYPASS; + assert K_CASE_WANT[2] == K_V_BROKEN_SHIFT; + assert K_CASE_WANT[4] == K_V_STUCK_HIGH; + assert K_CASE_WANT[5] == K_V_STUCK_LOW; + assert K_CASE_DR[4] == K_ALL_ONES; +} + +test one_even_word_two_verdicts { + assert K_CASE_DR[1] == K_CASE_DR[2]; + assert K_CASE_IR[1] % 4 == K_IR_LOW; + assert K_CASE_IR[2] % 4 != K_IR_LOW; + assert K_CASE_DR[1] % 2 == 0; + assert K_CASE_WANT[1] != K_CASE_WANT[2]; +} + +test the_ir_rule_decides_the_broken_shifts { + assert K_CASE_IR[3] % 4 != K_IR_LOW; + assert K_CASE_IR[6] % 4 != K_IR_LOW; + assert K_CASE_DR[6] % 2 == 1; + assert K_CASE_IR[0] % 4 == K_IR_LOW; + assert K_CASE_DR[0] % 2 == 1; +} + +test the_bench_word_masks_to_the_200t { + assert K_CASE_DR[0] % (K_VERSION_MASK + 1) == K_KNOWN_IDCODE[0]; + assert K_IR_MASK == 3; +} + +test the_page_opens_on_bypass { + assert K_V_IDCODE == 5; + assert K_START_CASE < 7; + assert K_CASE_WANT[K_START_CASE] == K_V_BYPASS; +} + +; ================================================================================================= +; THE NATIVE PAGE (epic gHashTag/t27#10937, stage "page" of specs/t27f/widget_factory.t27) +; Everything below replaces public/widgets/jtag-verdict/tool.js (171 lines of hand-written JavaScript). +; The words and numbers above are unchanged. The page is these functions, built to wasm (t27c gen-c, +; then zig cc --target=wasm32-wasi) and driven by the one host glue every native widget shares: it +; forwards input, click and hashchange events, then sets the bytes w_render() wrote as the page's +; innerHTML, keeping the focused input. Host protocol, all exports of this module: +; w_init(hash) the page opens; hash is the address's #fragment bytes (empty when none) +; w_input(id, text) a field changed: id 0 is the IR field, 1 the DR field; its whole text +; w_click(i) preset button i was pressed +; w_hash(hash) the address changed +; w_render(dst) write the page's HTML into dst, return its length (0 on overflow) +; w_hash_out(dst) the address to show after the last event (length 0: leave it) +; The state is the two field texts; the rest is derived on every render, as tool.js's update() did. +; Not carried over from JavaScript: Unicode white space around a typed number (its \s) is not ASCII; +; field text is cut at FIELD_CAP bytes; a malformed %-escape in the address is read as no value +; (decodeURIComponent would throw). +; MEASURED 2026-10-19: t27c gen, a 6-line Zig export wrapper and `zig build-exe -target +; wasm32-freestanding -O ReleaseSmall` give a 9269-byte wasm. A browser harness loaded the original +; page (tool.js) and this one side by side and compared their DOM (tag, attributes, text, the two +; input values, the address) after 3015 actions: the initial page, the 7 preset buttons, 3000 typed +; values (valid hex with and without 0x, spaces and underscores, 9-digit overflow, empty, quotes, +; markup, random ASCII) and 7 hash changes, plus 7 page loads that carry an address: 0 differences. +; Changing one text node of the native page makes the harness report a difference (control). +; Not carried over from the page: nothing else; focus after a preset click is kept by the host. + +pub const FIELD_CAP : u32 = 255; +pub var IR_TEXT : [255]u8 = [0; 255]; +pub var IR_LEN : u32 = 0; +pub var DR_TEXT : [255]u8 = [0; 255]; +pub var DR_LEN : u32 = 0; +pub var PRESSED : [7]u8 = [0; 7]; +pub var HASH_NEW : [64]u8 = [0; 64]; +pub var HASH_NEW_LEN : u32 = 0; +pub var HEX_A : [16]u8 = [0; 16]; +pub var HEX_B : [16]u8 = [0; 16]; +pub var BIN2 : [2]u8 = [0; 2]; +pub var DECODED : [64]u8 = [0; 64]; +pub const NOT_HEX : i64 = -1; + +/// The five checks, in the order of specs/port/tools/jtag/tdo_verdict.t27 (written in an older dialect +/// that cannot be imported; the seven vectors above are its test vectors and the tests below hold this to them). +pub fn jtag_verdict(ir: u32, dr: u32) -> u8 { + if (dr == K_ALL_ONES) { return K_V_STUCK_HIGH; } + if (dr == 0 and ir == 0) { return K_V_STUCK_LOW; } + if ((ir & K_IR_MASK) != K_IR_LOW) { return K_V_BROKEN_SHIFT; } + if ((dr & 1) == 0) { return K_V_BYPASS; } + return K_V_IDCODE; +} + +fn is_ws(b: u8) -> bool { + return b == 32 or (b >= 9 and b <= 13); +} + +fn hex_val(b: u8) -> i64 { + if (b >= 48 and b <= 57) { return (b - 48) as i64; } + if (b >= 97 and b <= 102) { return (b - 97) as i64 + 10; } + if (b >= 65 and b <= 70) { return (b - 65) as i64 + 10; } + return -1; +} + +/// parseHex: trim, drop white space and underscores, optional 0x, one to eight hex digits; else -1. +pub fn parse_hex(s: []const u8) -> i64 { + var pos : u32 = 0; + var first : u8 = 0; + var digits : u32 = 0; + var val : i64 = 0; + var i : usize = 0; + while (i < s.len) { + const b : u8 = s[i]; + i = i + 1; + if (is_ws(b) or b == 95) { continue; } + if (pos == 1 and first == 48 and (b == 120 or b == 88)) { + digits = 0; + val = 0; + pos = pos + 1; + continue; + } + const h : i64 = hex_val(b); + if (h < 0) { return NOT_HEX; } + if (pos == 0) { first = b; } + digits = digits + 1; + if (digits <= 8) { val = val * 16 + h; } + pos = pos + 1; + } + if (digits < 1 or digits > 8) { return NOT_HEX; } + return val; +} + +/// hexN: "0x" and the lowercase hex digits of v, at least min_digits of them; written to dst, length returned. +pub fn hex_to(dst: []u8, v: u32, min_digits: u32) -> u32 { + dst[0] = 48; + dst[1] = 120; + var count : u32 = 1; + var t : u32 = v / 16; + while (t > 0) { + count = count + 1; + t = t / 16; + } + var width : u32 = count; + if (min_digits > width) { width = min_digits; } + var k : u32 = width; + while (k > 0) { + k = k - 1; + var d : u32 = v; + var sh : u32 = 0; + while (sh < k) { + d = d / 16; + sh = sh + 1; + } + const nib : u32 = d % 16; + if (nib < 10) { dst[(2 + width - 1 - k) as usize] = (nib as u8) + 48; } else { dst[(2 + width - 1 - k) as usize] = (nib as u8) + 87; } + } + return 2 + width; +} + +fn ir_value() -> i64 { + return parse_hex(IR_TEXT[0..(IR_LEN as usize)]); +} + +fn dr_value() -> i64 { + return parse_hex(DR_TEXT[0..(DR_LEN as usize)]); +} + +fn set_text(which: u32, s: []const u8) -> u32 { + var n : usize = s.len; + if (n > (FIELD_CAP as usize)) { n = FIELD_CAP as usize; } + var i : usize = 0; + while (i < n) { + if (which == 0) { IR_TEXT[i] = s[i]; } else { DR_TEXT[i] = s[i]; } + i = i + 1; + } + if (which == 0) { IR_LEN = n as u32; } else { DR_LEN = n as u32; } + return 0; +} + +fn set_field_hex(which: u32, v: u32, digits: u32) -> u32 { + const n : u32 = hex_to(HEX_A[0..], v, digits); + return set_text(which, HEX_A[0..(n as usize)]); +} + +/// The two fields are good when both parse; then the pressed preset and the new address follow. +fn recompute(write_hash: bool) -> u32 { + HASH_NEW_LEN = 0; + const ir : i64 = ir_value(); + const dr : i64 = dr_value(); + if (ir < 0 or dr < 0) { return 0; } + var i : u32 = 0; + while (i < 7) { + PRESSED[i as usize] = 0; + if (K_CASE_IR[i as usize] == (ir as u32) and K_CASE_DR[i as usize] == (dr as u32)) { PRESSED[i as usize] = 1; } + i = i + 1; + } + if (write_hash) { + var n : u32 = 0; + HASH_NEW[0] = 35; + n = 1; + var k : usize = 0; + while (k < SAY_HASH_IR.len) { HASH_NEW[n as usize] = SAY_HASH_IR[k]; n = n + 1; k = k + 1; } + HASH_NEW[n as usize] = 61; + n = n + 1; + n = n + hex_to(HASH_NEW[(n as usize)..], ir as u32, 2); + HASH_NEW[n as usize] = 38; + n = n + 1; + k = 0; + while (k < SAY_HASH_DR.len) { HASH_NEW[n as usize] = SAY_HASH_DR[k]; n = n + 1; k = k + 1; } + HASH_NEW[n as usize] = 61; + n = n + 1; + n = n + hex_to(HASH_NEW[(n as usize)..], dr as u32, 8); + HASH_NEW_LEN = n; + } + return 0; +} + +/// fromHash's get(): the hex value of #key=... or &key=... in the address, -1 when absent or unreadable. +fn hash_get(h: []const u8, key: []const u8) -> i64 { + var p : usize = 0; + while (p + 1 + key.len < h.len) { + if ((h[p] == 35 or h[p] == 38) and match_in(h, p + 1, h.len, key) and h[p + 1 + key.len] == 61) { + var a : usize = p + 2 + key.len; + var e : usize = a; + while (e < h.len and h[e] != 38) { e = e + 1; } + if (e == a) { return NOT_HEX; } + var n : usize = 0; + var i : usize = a; + while (i < e) { + if (n >= 64) { return NOT_HEX; } + if (h[i] == 37) { + if (i + 2 >= e) { return NOT_HEX; } + const hi : i64 = hex_val(h[i + 1]); + const lo : i64 = hex_val(h[i + 2]); + if (hi < 0 or lo < 0) { return NOT_HEX; } + DECODED[n] = ((hi * 16 + lo) as u8); + i = i + 3; + } else { + DECODED[n] = h[i]; + i = i + 1; + } + n = n + 1; + } + return parse_hex(DECODED[0..n]); + } + p = p + 1; + } + return NOT_HEX; +} + +pub fn w_init(h: []const u8) -> u32 { + const ir : i64 = hash_get(h, SAY_HASH_IR); + const dr : i64 = hash_get(h, SAY_HASH_DR); + var start : u32 = K_START_CASE as u32; + if (ir >= 0) { set_field_hex(0, ir as u32, 2); } else { set_field_hex(0, K_CASE_IR[start as usize], 2); } + if (dr >= 0) { set_field_hex(1, dr as u32, 8); } else { set_field_hex(1, K_CASE_DR[start as usize], 8); } + return recompute(false); +} + +pub fn w_input(id: u32, s: []const u8) -> u32 { + set_text(id, s); + return recompute(true); +} + +pub fn w_click(i: u32) -> u32 { + if (i >= 7) { return 0; } + set_field_hex(0, K_CASE_IR[i as usize], 2); + set_field_hex(1, K_CASE_DR[i as usize], 8); + return recompute(true); +} + +pub fn w_hash(h: []const u8) -> u32 { + const ir : i64 = hash_get(h, SAY_HASH_IR); + const dr : i64 = hash_get(h, SAY_HASH_DR); + if (ir < 0 or dr < 0) { return recompute(false); } + set_field_hex(0, ir as u32, 2); + set_field_hex(1, dr as u32, 8); + return recompute(false); +} + +pub fn w_hash_out(dst: []u8) -> u32 { + var i : u32 = 0; + while (i < HASH_NEW_LEN) { + dst[i as usize] = HASH_NEW[i as usize]; + i = i + 1; + } + return HASH_NEW_LEN; +} + +fn agree_count() -> u32 { + var n : u32 = 0; + var i : u32 = 0; + while (i < 7) { + if (jtag_verdict(K_CASE_IR[i as usize], K_CASE_DR[i as usize]) == K_CASE_WANT[i as usize]) { n = n + 1; } + i = i + 1; + } + return n; +} + +fn bit_cells(dst: []u8, n0: u32, v: u32, count: u32) -> u32 { + var n : u32 = n0; + n = raw(dst, n, ""); + var i : u32 = 0; + while (i < count) { + n = raw(dst, n, ""); + n = put(dst, n, (((v >> i) % 2) as u8) + 48); + n = raw(dst, n, ""); + i = i + 1; + } + n = raw(dst, n, ""); + return n; +} + +fn field(dst: []u8, n0: u32, id: u32, label: []const u8, hint: []const u8) -> u32 { + var n : u32 = n0; + if (id == 0) { + n = raw(dst, n, "