0) {
+ n = put(dst, n, 32);
+ n = raw(dst, n, cls);
+ }
+ return raw(dst, n, "\">");
+}
+
+fn rule_html(dst: []u8, n0: u32) -> u32 {
+ return raw(dst, n0, "");
+}
+
+/// line(left, right, cls) up to the left text.
+fn line_open(dst: []u8, n0: u32, cls: []const u8) -> u32 {
+ var n : u32 = div_open(dst, n0, "br-line", cls);
+ return raw(dst, n, "");
+}
+
+fn line_mid(dst: []u8, n0: u32) -> u32 {
+ return raw(dst, n0, "");
+}
+
+fn line_close(dst: []u8, n0: u32) -> u32 {
+ return raw(dst, n0, "
");
+}
+
+fn put_all(dst: []u8, n0: u32, s: []const u8, l: u32) -> u32 {
+ var n : u32 = n0;
+ var i : u32 = 0;
+ while (i < l) {
+ n = put_escaped(dst, n, s[i as usize]);
+ i = i + 1;
+ }
+ return n;
+}
+
+/// The text of a textContent / child assignment: null and undefined add nothing.
+fn tc_text(dst: []u8, n0: u32, d: []const u8, sp: i64) -> u32 {
+ if (nullish(d, sp)) { return n0; }
+ return jv_text(dst, n0, d, sp);
+}
+
+/// (ms / 1000).toFixed(3) in SAY_SECONDS.
+fn secs_text(dst: []u8, n0: u32, ms: f64) -> u32 {
+ var vb : [128]u8 = [0; 128];
+ const vn : u32 = jv_vput_fixed(vb[0..], 0, "s", ms / 1000.0, 3);
+ return jv_fill(dst, n0, SAY_SECONDS, vb[0..], vn, false);
+}
+
+/// `x ?? 0` then Number(): the coerced number of a field with a default of 0.
+fn num_or0(d: []const u8, sp: i64) -> f64 {
+ if (nullish(d, sp)) { return 0.0; }
+ return jv_num(d, sp);
+}
+
+/// Trim JavaScript white space off buf[0..l); the new start in lo and end in hi (written back through the arrays).
+fn trim_range(buf: []const u8, l: u32, out: []u32) -> u32 {
+ var lo : u32 = 0;
+ var hi : u32 = l;
+ var w : u32 = 1;
+ while (lo < hi and w > 0) {
+ w = jv_ws_at(buf, lo, hi);
+ lo = lo + w;
+ }
+ w = 1;
+ while (hi > lo and w > 0) {
+ w = jv_ws_before(buf, lo, hi);
+ hi = hi - w;
+ }
+ out[0] = lo;
+ out[1] = hi;
+ return 0;
+}
+
+/// size(bytes) as raw bytes into out, trimmed: `12.34 MB (12345678 B)` / `4492 B`; the length.
+fn size_raw(out: []u8, d: []const u8, sp: i64) -> u32 {
+ var tmp : [512]u8 = [0; 512];
+ var t : u32 = 0;
+ var v : f64 = jv_num(d, sp);
+ var i : u32 = 0;
+ const last : u32 = (SAY_UNITS.len as u32) - 1;
+ while (v >= 1000.0 and i < last) {
+ v = v / 1000.0;
+ i = i + 1;
+ }
+ if (i == 0) {
+ t = jv_str(tmp[0..], t, d, sp);
+ } else {
+ t = js_to_fixed(tmp[0..], t, v, 2);
+ }
+ t = put(tmp[0..], t, 32);
+ t = jn_raw(tmp[0..], t, SAY_UNITS[i as usize]);
+ if (i != 0) {
+ t = jn_raw(tmp[0..], t, " (");
+ t = jv_str(tmp[0..], t, d, sp);
+ t = put(tmp[0..], t, 32);
+ t = jn_raw(tmp[0..], t, SAY_UNITS[0]);
+ t = put(tmp[0..], t, 41);
+ }
+ var rg : [2]u32 = [0; 2];
+ trim_range(tmp[0..], t, rg[0..]);
+ var n : u32 = 0;
+ var q : u32 = rg[0];
+ while (q < rg[1]) {
+ n = jn_put(out, n, tmp[q as usize]);
+ q = q + 1;
+ }
+ return n;
+}
+
+fn size_text(dst: []u8, n0: u32, d: []const u8, sp: i64) -> u32 {
+ var tmp : [600]u8 = [0; 600];
+ const l : u32 = size_raw(tmp[0..], d, sp);
+ return put_all(dst, n0, tmp[0..], l);
+}
+
+/// toLowerCase for ASCII and Latin-1 (UTF-8 C3 80..9E except 97), in place; the length does not change.
+fn lower_in_place(buf: []u8, l: u32) -> u32 {
+ var i : u32 = 0;
+ while (i < l) {
+ const c : u8 = buf[i as usize];
+ if (c >= 65 and c <= 90) { buf[i as usize] = c + 32; }
+ if (c == 195 and i + 1 < l) {
+ const c2 : u8 = buf[(i + 1) as usize];
+ if (c2 >= 128 and c2 <= 158 and c2 != 151) { buf[(i + 1) as usize] = c2 + 32; }
+ i = i + 1;
+ }
+ i = i + 1;
+ }
+ return l;
+}
+
+/// toolLine(s): the version alone when it already names the tool, else `tool | version`.
+fn tool_line(dst: []u8, n0: u32, d: []const u8, s: i64) -> u32 {
+ var ver : [512]u8 = [0; 512];
+ var vl : u32 = 0;
+ const vv : i64 = obj_get(d, s, "version");
+ if (!nullish(d, vv)) { vl = jv_str(ver[0..], 0, d, vv); }
+ var tool : [512]u8 = [0; 512];
+ const tl : u32 = jv_str(tool[0..], 0, d, obj_get(d, s, "tool"));
+ var fl : u32 = 0;
+ while (fl < tl and tool[fl as usize] != 32) { fl = fl + 1; }
+ var lver : [512]u8 = [0; 512];
+ var ltool : [512]u8 = [0; 512];
+ var c : u32 = 0;
+ while (c < vl) {
+ lver[c as usize] = ver[c as usize];
+ c = c + 1;
+ }
+ c = 0;
+ while (c < fl) {
+ ltool[c as usize] = tool[c as usize];
+ c = c + 1;
+ }
+ lower_in_place(lver[0..], vl);
+ lower_in_place(ltool[0..], fl);
+ // does lower(ver) include lower(first)?
+ var found : bool = fl == 0;
+ var a : u32 = 0;
+ while (!found and a + fl <= vl) {
+ var m : bool = true;
+ var b : u32 = 0;
+ while (m and b < fl) {
+ if (lver[(a + b) as usize] != ltool[b as usize]) { m = false; }
+ b = b + 1;
+ }
+ if (m) { found = true; }
+ a = a + 1;
+ }
+ if (found) { return put_all(dst, n0, ver[0..], vl); }
+ var n : u32 = put_all(dst, n0, tool[0..], tl);
+ n = jn_raw(dst, n, " | ");
+ return put_all(dst, n, ver[0..], vl);
+}
+
+fn hexval(b: u8) -> u32 {
+ if (b >= 48 and b <= 57) { return (b - 48) as u32; }
+ if (b >= 97 and b <= 102) { return (b - 97) as u32 + 10; }
+ if (b >= 65 and b <= 70) { return (b - 65) as u32 + 10; }
+ return 99;
+}
+
+/// barcode(hex): one bar and one gap per hex digit, widths 1 to 4, with a guard at each end.
+fn barcode_html(dst: []u8, n0: u32, d: []const u8, sp: i64) -> u32 {
+ var tmp : [512]u8 = [0; 512];
+ var l : u32 = 0;
+ if (jv_truthy(d, sp)) { l = jv_str(tmp[0..], 0, d, sp); }
+ var parts : [600]u32 = [0; 600];
+ var pc : u32 = 0;
+ parts[0] = 1; parts[1] = 1; parts[2] = 1; parts[3] = 1;
+ pc = 4;
+ var i : u32 = 0;
+ while (i < l and pc < 590) {
+ const hv : u32 = hexval(tmp[i as usize]);
+ if (hv < 16) {
+ parts[pc as usize] = hv / 4 + 1;
+ parts[(pc + 1) as usize] = hv % 4 + 1;
+ pc = pc + 2;
+ }
+ i = i + 1;
+ }
+ parts[pc as usize] = 1; parts[(pc + 1) as usize] = 1; parts[(pc + 2) as usize] = 1; parts[(pc + 3) as usize] = 1;
+ pc = pc + 4;
+ var width : u32 = 12;
+ i = 0;
+ while (i < pc) {
+ width = width + parts[i as usize];
+ i = i + 1;
+ }
+ var n : u32 = raw(dst, n0, "");
+}
+
+// --- the stages -----------------------------------------------------------------------------------
+
+fn stage_item(dst: []u8, n0: u32, d: []const u8, s: i64, ix: u32) -> u32 {
+ const status : i64 = obj_get(d, s, "status");
+ var n : u32 = raw(dst, n0, "
");
+ // line(name, ...)
+ n = line_open(dst, n, "");
+ n = js_str_u64(dst, n, (ix + 1) as u64);
+ n = put(dst, n, 32);
+ if (ix < (SAY_STAGE_NAMES.len as u32)) { n = text(dst, n, SAY_STAGE_NAMES[ix as usize]); } else { n = jv_text(dst, n, d, obj_get(d, s, "id")); }
+ n = line_mid(dst, n);
+ if (jv_is(d, status, "not run")) {
+ n = text(dst, n, SAY_NOT_RUN);
+ n = line_close(dst, n);
+ n = div_open(dst, n, "br-sub", "");
+ n = tc_text(dst, n, d, obj_get(d, s, "tool"));
+ n = raw(dst, n, "
");
+ return n;
+ }
+ n = secs_text(dst, n, num_or0(d, obj_get(d, s, "ms")));
+ n = line_close(dst, n);
+ n = div_open(dst, n, "br-sub", "");
+ n = tool_line(dst, n, d, s);
+ n = raw(dst, n, "");
+ if (failed) {
+ n = div_open(dst, n, "br-sub", "br-bad");
+ var vb : [256]u8 = [0; 256];
+ var vn : u32 = 0;
+ const ex : i64 = obj_get(d, s, "exit");
+ if (nullish(d, ex)) { vn = jv_vput_str(vb[0..], 0, "exit", ""); } else { vn = jv_vput_val(vb[0..], 0, "exit", d, ex); }
+ n = jv_fill(dst, n, SAY_FAILED, vb[0..], vn, false);
+ n = raw(dst, n, "");
+ if (jv_truthy(d, obj_get(d, s, "error"))) {
+ n = raw(dst, n, "
");
+ n = jv_text(dst, n, d, obj_get(d, s, "error"));
+ n = raw(dst, n, "
");
+ }
+ return raw(dst, n, "");
+ }
+ const out : i64 = obj_get(d, s, "output");
+ if (jv_truthy(d, out)) {
+ n = div_open(dst, n, "br-sub", "br-detail");
+ var ob : [1024]u8 = [0; 1024];
+ var on : u32 = jv_vput_val(ob[0..], 0, "file", d, obj_get(d, out, "file"));
+ var szn : [600]u8 = [0; 600];
+ const tmpn : u32 = size_raw(szn[0..], d, obj_get(d, out, "bytes"));
+ on = jv_vput_str(ob[0..], on, "size", szn[0..(tmpn as usize)]);
+ n = jv_fill(dst, n, SAY_OUT, ob[0..], on, false);
+ n = raw(dst, n, "");
+ n = div_open(dst, n, "br-sub", "br-hex br-detail");
+ var hb : [512]u8 = [0; 512];
+ var hn : u32 = jv_vput_val(hb[0..], 0, "sha", d, obj_get(d, out, "sha256"));
+ n = jv_fill(dst, n, SAY_SHA, hb[0..], hn, false);
+ n = raw(dst, n, "");
+ }
+ const cpu : i64 = obj_get(d, s, "cpu_s");
+ if (!nullish(d, cpu)) {
+ n = div_open(dst, n, "br-sub", "br-faint br-detail");
+ var cb : [256]u8 = [0; 256];
+ var cn : u32 = jv_vput_val(cb[0..], 0, "cpu", d, cpu);
+ cn = jv_vput_f64(cb[0..], cn, "rss", js_round(num_or0(d, obj_get(d, s, "rss_mb"))));
+ n = jv_fill(dst, n, SAY_CPU, cb[0..], cn, false);
+ n = raw(dst, n, "");
+ }
+ return raw(dst, n, "");
+}
+
+/// The names specDiff() lists: where the generated constants of this spec and receipt.json disagree.
+fn spec_diff(dst: []u8, n0: u32, d: []const u8, r: i64) -> u32 {
+ const st : i64 = obj_get(d, r, "stages");
+ const cnt : u32 = arr_len(d, st);
+ var n : u32 = n0;
+ var any : bool = false;
+ const sc : i64 = obj_get(d, r, "stage_count");
+ if (!(kind(d, sc) == J_NUMBER and jv_num(d, sc) == (K_STAGE_COUNT as f64) and jv_num(d, sc) == (cnt as f64))) {
+ n = raw(dst, n, "K_STAGE_COUNT");
+ any = true;
+ }
+ const so : i64 = obj_get(d, r, "stages_ok");
+ if (!(kind(d, so) == J_NUMBER and jv_num(d, so) == (K_STAGES_OK as f64))) {
+ if (any) { n = raw(dst, n, ", "); }
+ n = raw(dst, n, "K_STAGES_OK");
+ any = true;
+ }
+ var msbad : bool = false;
+ if ((K_STAGE_MS.len as u32) != cnt) { msbad = true; }
+ var sum : f64 = 0.0;
+ // reduce((a, b) => a + b, 0): numbers add; once a string (or object / array) is met the rest is concatenated
+ var sbuf : [1100]u8 = [0; 1100];
+ var sl : u32 = 0;
+ var strmode : bool = false;
+ var i : u32 = 0;
+ while (i < cnt) {
+ const ms : i64 = obj_get(d, arr_at(d, st, i), "ms");
+ const mk : u8 = kind(d, ms);
+ var mv : f64 = 0.0;
+ var is_num : bool = true;
+ if (!nullish(d, ms)) {
+ mv = jv_num(d, ms);
+ is_num = mk == J_NUMBER;
+ }
+ if (i < (K_STAGE_MS.len as u32)) {
+ if (!is_num or mv != (K_STAGE_MS[i as usize] as f64)) { msbad = true; }
+ }
+ if (!strmode and (mk == J_STRING or mk == J_OBJECT or mk == J_ARRAY)) {
+ strmode = true;
+ sl = js_str_num(sbuf[0..], 0, sum);
+ }
+ if (strmode) {
+ if (nullish(d, ms)) { sl = jn_raw(sbuf[0..], sl, "0"); } else { sl = jv_str(sbuf[0..], sl, d, ms); }
+ } else {
+ sum = sum + mv;
+ }
+ i = i + 1;
+ }
+ if (strmode) { sum = jv_number_of_text(sbuf[0..], sl); }
+ if (msbad) {
+ if (any) { n = raw(dst, n, ", "); }
+ n = raw(dst, n, "K_STAGE_MS");
+ any = true;
+ }
+ const tm : i64 = obj_get(d, r, "total_ms");
+ if (!(kind(d, tm) == J_NUMBER and jv_num(d, tm) == (K_TOTAL_MS as f64))) {
+ if (any) { n = raw(dst, n, ", "); }
+ n = raw(dst, n, "K_TOTAL_MS");
+ any = true;
+ }
+ const bit : i64 = obj_get(d, r, "bit");
+ if (jv_truthy(d, bit)) {
+ const bb : i64 = obj_get(d, bit, "bytes");
+ if (!(kind(d, bb) == J_NUMBER and jv_num(d, bb) == (K_BIT_BYTES as f64))) {
+ if (any) { n = raw(dst, n, ", "); }
+ n = raw(dst, n, "K_BIT_BYTES");
+ any = true;
+ }
+ }
+ var gap : f64 = sum - jv_num(d, tm);
+ if (gap < 0.0) { gap = 0.0 - gap; }
+ if (gap > (K_ROUND_TOL_MS as f64)) {
+ if (any) { n = raw(dst, n, ", "); }
+ n = raw(dst, n, "total_ms");
+ }
+ return n;
+}
+
+fn member_value(d: []const u8, ut: i64, ix: u32, key_out: []u8, key_len: []u32) -> i64 {
+ const hi : usize = span_hi(ut);
+ var p : usize = skip_ws(d, span_lo(ut) + 1);
+ var at : u32 = 0;
+ while (p < hi and d[p] == 34) {
+ const ke : usize = string_end(d, p);
+ const q : usize = skip_ws(d, ke);
+ const v : usize = skip_ws(d, q + 1);
+ const ve : usize = value_end(d, v);
+ if (at == ix) {
+ key_len[0] = jv_str_to(key_out, 0, d, jspan(p, ke));
+ return jspan(v, ve);
+ }
+ at = at + 1;
+ p = skip_ws(d, ve);
+ if (p < hi and d[p] == 44) { p = skip_ws(d, p + 1); }
+ }
+ return NO_SPAN;
+}
+
+fn member_count(d: []const u8, ut: i64) -> u32 {
+ const hi : usize = span_hi(ut);
+ var p : usize = skip_ws(d, span_lo(ut) + 1);
+ var at : u32 = 0;
+ while (p < hi and d[p] == 34) {
+ const ke : usize = string_end(d, p);
+ const q : usize = skip_ws(d, ke);
+ const v : usize = skip_ws(d, q + 1);
+ const ve : usize = value_end(d, v);
+ at = at + 1;
+ p = skip_ws(d, ve);
+ if (p < hi and d[p] == 44) { p = skip_ws(d, p + 1); }
+ }
+ return at;
+}
+
+fn paper_html(dst: []u8, n0: u32, d: []const u8, r: i64) -> u32 {
+ const bit : i64 = obj_get(d, r, "bit");
+ const mach : i64 = obj_get(d, r, "machine");
+ const des : i64 = obj_get(d, r, "design");
+ const sha : i64 = obj_get(d, bit, "sha256");
+ var n : u32 = raw(dst, n0, "");
+ // the receipt number: the payload digest (else the file digest), first 8 characters, upper case
+ var pay : i64 = obj_get(d, bit, "payload_sha256");
+ if (nullish(d, pay)) { pay = sha; }
+ var nb : [64]u8 = [0; 64];
+ var nl : u32 = 0;
+ if (kind(d, pay) == J_STRING) {
+ var raw8 : [512]u8 = [0; 512];
+ const rl : u32 = jv_str(raw8[0..], 0, d, pay);
+ var units : u32 = 0;
+ var q : u32 = 0;
+ while (q < rl and units < 8 and nl < 56) {
+ const c : u8 = raw8[q as usize];
+ var w : u32 = 1;
+ var u : u32 = 1;
+ if (c >= 192) { w = 2; }
+ if (c >= 224) { w = 3; }
+ if (c >= 240) { w = 4; u = 2; }
+ if (units + u > 8) { break; }
+ if (c >= 97 and c <= 122) {
+ nb[nl as usize] = c - 32;
+ nl = nl + 1;
+ } else {
+ if (c == 195 and q + 1 < rl) {
+ // Latin-1: a..o / o..y with a diaeresis etc. go up by 32 (not the division sign); y with diaeresis and sharp s are special
+ const c2 : u8 = raw8[(q + 1) as usize];
+ if (c2 == 159) {
+ nb[nl as usize] = 83;
+ nb[(nl + 1) as usize] = 83;
+ nl = nl + 2;
+ } else {
+ if (c2 == 191) {
+ nb[nl as usize] = 197;
+ nb[(nl + 1) as usize] = 184;
+ nl = nl + 2;
+ } else {
+ nb[nl as usize] = c;
+ if (c2 >= 160 and c2 <= 190 and c2 != 183) { nb[(nl + 1) as usize] = c2 - 32; } else { nb[(nl + 1) as usize] = c2; }
+ nl = nl + 2;
+ }
+ }
+ } else {
+ if (c == 194 and q + 1 < rl and raw8[(q + 1) as usize] == 181) {
+ nb[nl as usize] = 206;
+ nb[(nl + 1) as usize] = 156;
+ nl = nl + 2;
+ } else {
+ var z : u32 = 0;
+ while (z < w and q + z < rl) {
+ nb[(nl + z) as usize] = raw8[(q + z) as usize];
+ z = z + 1;
+ }
+ nl = nl + z;
+ }
+ }
+ }
+ q = q + w;
+ units = units + u;
+ }
+ }
+ var vb : [512]u8 = [0; 512];
+ var vn : u32 = jv_vput_str(vb[0..], 0, "no", nb[0..(nl as usize)]);
+ n = div_open(dst, n, "br-center", "br-shop");
+ n = text(dst, n, SAY_SHOP);
+ n = raw(dst, n, "");
+ n = div_open(dst, n, "br-center", "");
+ n = jv_fill(dst, n, SAY_RECEIPT, vb[0..], vn, false);
+ n = raw(dst, n, "");
+ // when
+ n = div_open(dst, n, "br-center", "");
+ var wb : [1200]u8 = [0; 1200];
+ var wtmp : [1100]u8 = [0; 1100];
+ const wl : u32 = utc_raw(wtmp[0..], d, obj_get(d, r, "when_utc"));
+ var wn : u32 = jv_vput_str(wb[0..], 0, "when", wtmp[0..(wl as usize)]);
+ n = jv_fill(dst, n, SAY_WHEN, wb[0..], wn, false);
+ n = raw(dst, n, "");
+ // machine
+ n = div_open(dst, n, "br-center", "br-faint");
+ var mb : [1024]u8 = [0; 1024];
+ var mn : u32 = 0;
+ if (kind(d, mach) == J_OBJECT) {
+ mn = jv_vput_defined(mb[0..], mn, "cpu", d, obj_get(d, mach, "cpu"));
+ mn = jv_vput_defined(mb[0..], mn, "cores", d, obj_get(d, mach, "cores"));
+ mn = jv_vput_defined(mb[0..], mn, "macos", d, obj_get(d, mach, "macos"));
+ }
+ n = jv_fill(dst, n, SAY_MACHINE, mb[0..], mn, false);
+ n = raw(dst, n, "");
+ n = div_open(dst, n, "br-center", "br-faint");
+ var lb : [256]u8 = [0; 256];
+ var ln : u32 = 0;
+ const la : i64 = obj_get(d, mach, "loadavg_1m_at_start");
+ if (nullish(d, la)) { ln = jv_vput_str(lb[0..], 0, "load", ""); } else { ln = jv_vput_val(lb[0..], 0, "load", d, la); }
+ n = jv_fill(dst, n, SAY_LOAD, lb[0..], ln, false);
+ n = raw(dst, n, "");
+ n = rule_html(dst, n);
+ // design
+ n = div_open(dst, n, "br-sub", "");
+ n = text(dst, n, SAY_LABEL_SPEC);
+ n = raw(dst, n, ": ");
+ n = nn_text(dst, n, d, obj_get(d, des, "spec"));
+ n = raw(dst, n, "");
+ n = div_open(dst, n, "br-sub", "");
+ n = text(dst, n, SAY_LABEL_PART);
+ n = raw(dst, n, ": ");
+ n = nn_text(dst, n, d, obj_get(d, des, "part"));
+ n = raw(dst, n, "");
+ n = div_open(dst, n, "br-sub", "br-detail");
+ n = text(dst, n, SAY_LABEL_BOARD);
+ n = raw(dst, n, ": ");
+ n = nn_text(dst, n, d, obj_get(d, des, "board"));
+ n = raw(dst, n, "");
+ n = div_open(dst, n, "br-sub", "br-faint br-detail");
+ var db : [1024]u8 = [0; 1024];
+ var dn : u32 = 0;
+ dn = jv_vput_nn(db[0..], dn, "name", d, obj_get(d, des, "datapath"));
+ dn = jv_vput_nn(db[0..], dn, "lines", d, obj_get(d, des, "wrapper_lines"));
+ n = jv_fill(dst, n, SAY_WRAPPER, db[0..], dn, false);
+ n = raw(dst, n, "");
+ n = rule_html(dst, n);
+ n = line_open(dst, n, "br-head");
+ n = text(dst, n, SAY_COL_ITEM);
+ n = line_mid(dst, n);
+ n = text(dst, n, SAY_COL_TIME);
+ n = line_close(dst, n);
+ // items
+ const st : i64 = obj_get(d, r, "stages");
+ const cnt : u32 = arr_len(d, st);
+ var i : u32 = 0;
+ while (i < cnt) {
+ n = stage_item(dst, n, d, arr_at(d, st, i), i);
+ i = i + 1;
+ }
+ n = rule_html(dst, n);
+ // totals
+ n = div_open(dst, n, "br-line", "");
+ var tb : [256]u8 = [0; 256];
+ var tn : u32 = jv_vput_val(tb[0..], 0, "ok", d, obj_get(d, r, "stages_ok"));
+ tn = jv_vput_val(tb[0..], tn, "count", d, obj_get(d, r, "stage_count"));
+ n = jv_fill(dst, n, SAY_ITEMS, tb[0..], tn, false);
+ n = raw(dst, n, "");
+ const total_ms : f64 = jv_num(d, obj_get(d, r, "total_ms"));
+ if (jv_truthy(d, obj_get(d, r, "complete"))) {
+ n = line_open(dst, n, "br-total");
+ n = text(dst, n, SAY_TOTAL);
+ n = line_mid(dst, n);
+ n = secs_text(dst, n, total_ms);
+ n = line_close(dst, n);
+ } else {
+ n = div_open(dst, n, "br-sub", "br-bad br-total-miss");
+ var sb : [256]u8 = [0; 256];
+ var stmp : [200]u8 = [0; 200];
+ const sl : u32 = secs_text_raw(stmp[0..], total_ms);
+ const sn : u32 = jv_vput_str(sb[0..], 0, "s", stmp[0..(sl as usize)]);
+ n = jv_fill(dst, n, SAY_NO_TOTAL, sb[0..], sn, false);
+ n = raw(dst, n, "");
+ }
+ var vt : [64]u8 = [0; 64];
+ var vtl : u32 = 0;
+ const vend : i64 = obj_get(d, r, "vendor_tools_run");
+ if (nullish(d, vend)) { vtl = jn_raw(vt[0..], 0, "0"); } else { vtl = jv_str(vt[0..], 0, d, vend); }
+ n = line_open(dst, n, "br-big");
+ n = text(dst, n, SAY_VIVADO);
+ n = line_mid(dst, n);
+ n = put_all(dst, n, vt[0..], vtl);
+ n = line_close(dst, n);
+ n = line_open(dst, n, "");
+ n = text(dst, n, SAY_VENDOR);
+ n = line_mid(dst, n);
+ n = put_all(dst, n, vt[0..], vtl);
+ n = line_close(dst, n);
+ n = div_open(dst, n, "br-sub", "br-faint");
+ var eb : [256]u8 = [0; 256];
+ var etmp : [200]u8 = [0; 200];
+ const el : u32 = secs_text_raw(etmp[0..], num_or0(d, obj_get(d, r, "wall_ms_end_to_end")));
+ const en : u32 = jv_vput_str(eb[0..], 0, "s", etmp[0..(el as usize)]);
+ n = jv_fill(dst, n, SAY_END_TO_END, eb[0..], en, false);
+ n = raw(dst, n, "");
+ n = rule_html(dst, n);
+ // the design that came out
+ const fm : i64 = obj_get(d, r, "fmax");
+ if (jv_truthy(d, fm)) {
+ const pass : bool = jv_truthy(d, obj_get(d, fm, "pass"));
+ if (pass) { n = line_open(dst, n, ""); } else { n = line_open(dst, n, "br-bad"); }
+ var fb : [512]u8 = [0; 512];
+ var fn_ : u32 = jv_vput_val(fb[0..], 0, "clock", d, obj_get(d, fm, "clock"));
+ fn_ = jv_vput_val(fb[0..], fn_, "mhz", d, obj_get(d, fm, "mhz"));
+ fn_ = jv_vput_val(fb[0..], fn_, "target", d, obj_get(d, fm, "target_mhz"));
+ n = jv_fill(dst, n, SAY_FMAX, fb[0..], fn_, false);
+ n = line_mid(dst, n);
+ if (pass) { n = text(dst, n, SAY_PASS); } else { n = text(dst, n, SAY_FAIL); }
+ n = line_close(dst, n);
+ }
+ const ut : i64 = obj_get(d, r, "utilisation");
+ var has_entries : bool = false;
+ if (jv_truthy(d, ut)) {
+ if (kind(d, ut) == J_ARRAY) { has_entries = arr_len(d, ut) > 0; }
+ if (kind(d, ut) == J_OBJECT) { has_entries = member_count(d, ut) > 0; }
+ }
+ if (has_entries) {
+ n = div_open(dst, n, "br-sub", "");
+ n = text(dst, n, SAY_UTIL_TITLE);
+ n = raw(dst, n, "");
+ var ecount : u32 = 0;
+ if (kind(d, ut) == J_ARRAY) { ecount = arr_len(d, ut); } else { ecount = member_count(d, ut); }
+ var e : u32 = 0;
+ while (e < ecount) {
+ var kb : [512]u8 = [0; 512];
+ var kl : [1]u32 = [0; 1];
+ var val : i64 = NO_SPAN;
+ if (kind(d, ut) == J_ARRAY) {
+ val = arr_at(d, ut, e);
+ kl[0] = js_str_u64(kb[0..], 0, e as u64);
+ } else {
+ val = member_value(d, ut, e, kb[0..], kl[0..]);
+ }
+ n = line_open(dst, n, "br-small");
+ n = raw(dst, n, " ");
+ n = put_all(dst, n, kb[0..], kl[0]);
+ n = line_mid(dst, n);
+ n = jv_text(dst, n, d, arr_at(d, val, 0));
+ n = raw(dst, n, " / ");
+ n = jv_text(dst, n, d, arr_at(d, val, 1));
+ n = line_close(dst, n);
+ e = e + 1;
+ }
+ }
+ if (jv_truthy(d, fm) or jv_truthy(d, ut)) { n = rule_html(dst, n); }
+ // the bitstream
+ if (jv_truthy(d, bit)) {
+ n = div_open(dst, n, "br-sub", "");
+ n = text(dst, n, SAY_FILE_SHA);
+ n = raw(dst, n, ": ");
+ n = size_text(dst, n, d, obj_get(d, bit, "bytes"));
+ n = raw(dst, n, "");
+ n = div_open(dst, n, "br-sub", "br-hex");
+ n = tc_text(dst, n, d, obj_get(d, bit, "sha256"));
+ n = raw(dst, n, "");
+ n = div_open(dst, n, "br-sub", "br-faint");
+ n = text(dst, n, SAY_FILE_SHA_NOTE);
+ n = raw(dst, n, "");
+ n = div_open(dst, n, "br-sub", "");
+ var pb : [600]u8 = [0; 600];
+ var pstr : [600]u8 = [0; 600];
+ const pl : u32 = size_raw(pstr[0..], d, obj_get(d, bit, "payload_bytes"));
+ const pn : u32 = jv_vput_str(pb[0..], 0, "size", pstr[0..(pl as usize)]);
+ n = jv_fill(dst, n, SAY_PAYLOAD_SHA, pb[0..], pn, false);
+ n = raw(dst, n, "");
+ n = div_open(dst, n, "br-sub", "br-hex");
+ n = tc_text(dst, n, d, obj_get(d, bit, "payload_sha256"));
+ n = raw(dst, n, "");
+ const er : i64 = obj_get(d, r, "earlier_runs");
+ var ec : u32 = 0;
+ if (kind(d, er) == J_ARRAY) { ec = arr_len(d, er); }
+ if (ec > 0) {
+ const same : bool = jv_truthy(d, obj_get(d, r, "payload_same_as_earlier"));
+ if (same) { n = div_open(dst, n, "br-sub", ""); } else { n = div_open(dst, n, "br-sub", "br-bad"); }
+ if (same) {
+ var sb2 : [64]u8 = [0; 64];
+ const sn2 : u32 = jv_vput_f64(sb2[0..], 0, "n", ec as f64);
+ n = jv_fill(dst, n, SAY_SAME_PAYLOAD, sb2[0..], sn2, false);
+ } else {
+ n = text(dst, n, SAY_OTHER_PAYLOAD);
+ }
+ n = raw(dst, n, "");
+ var x : u32 = 0;
+ while (x < ec) {
+ const e2 : i64 = arr_at(d, er, x);
+ n = div_open(dst, n, "br-sub", "br-faint");
+ var xb : [1600]u8 = [0; 1600];
+ var xt : [1100]u8 = [0; 1100];
+ var xtl : u32 = utc_raw(xt[0..], d, obj_get(d, e2, "when_utc"));
+ var xn : u32 = jv_vput_str(xb[0..], 0, "when", xt[0..(xtl as usize)]);
+ var xs : [200]u8 = [0; 200];
+ const xsl : u32 = secs_text_raw(xs[0..], jv_num(d, obj_get(d, e2, "total_ms")));
+ xn = jv_vput_str(xb[0..], xn, "s", xs[0..(xsl as usize)]);
+ xn = jv_vput_val(xb[0..], xn, "load", d, obj_get(d, e2, "loadavg_1m_at_start"));
+ n = jv_fill(dst, n, SAY_EARLIER, xb[0..], xn, false);
+ n = raw(dst, n, "");
+ x = x + 1;
+ }
+ }
+ const cc : i64 = obj_get(d, r, "cross_check");
+ const cs : i64 = obj_get(d, cc, "status");
+ const cok : bool = jv_is(d, cs, "ok");
+ const ceq : bool = jv_truthy(d, obj_get(d, cc, "payload_equal"));
+ if (cok and !ceq) { n = div_open(dst, n, "br-sub", "br-bad"); } else { n = div_open(dst, n, "br-sub", ""); }
+ if (cok and ceq) {
+ var cb : [256]u8 = [0; 256];
+ const cn : u32 = jv_vput_val(cb[0..], 0, "ms", d, obj_get(d, cc, "ms"));
+ n = jv_fill(dst, n, SAY_CROSS_OK, cb[0..], cn, false);
+ } else {
+ if (cok) { n = text(dst, n, SAY_CROSS_BAD); } else { n = text(dst, n, SAY_CROSS_SKIP); }
+ }
+ n = raw(dst, n, "");
+ n = rule_html(dst, n);
+ n = barcode_html(dst, n, d, sha);
+ n = div_open(dst, n, "br-center", "br-hex br-tiny");
+ n = tc_text(dst, n, d, sha);
+ n = raw(dst, n, "");
+ }
+ n = div_open(dst, n, "br-center", "br-small");
+ n = text(dst, n, SAY_NOT_PROGRAMMED);
+ n = raw(dst, n, "");
+ n = div_open(dst, n, "br-center", "br-faint br-small");
+ n = text(dst, n, SAY_FOOTER);
+ n = raw(dst, n, "");
+ return raw(dst, n, "");
+}
+
+/// `x ?? ''` as a child / template-literal piece: the value's String(), nothing for null / missing.
+fn nn_text(dst: []u8, n0: u32, d: []const u8, sp: i64) -> u32 {
+ if (nullish(d, sp)) { return n0; }
+ return jv_text(dst, n0, d, sp);
+}
+
+/// A pool entry for `{key: x ?? ''}`.
+fn jv_vput_nn(vb: []u8, n0: u32, key: []const u8, d: []const u8, sp: i64) -> u32 {
+ if (nullish(d, sp)) { return jv_vput_str(vb, n0, key, ""); }
+ return jv_vput_val(vb, n0, key, d, sp);
+}
+
+/// utc(iso) as raw bytes (not escaped) into out; the length.
+fn utc_raw(out: []u8, d: []const u8, sp: i64) -> u32 {
+ var l : u32 = 0;
+ if (!nullish(d, sp)) { l = jv_str(out, 0, d, sp); }
+ var k : u32 = 0;
+ var done : bool = false;
+ while (k < l and !done) {
+ if (out[k as usize] == 84) {
+ out[k as usize] = 32;
+ done = true;
+ }
+ k = k + 1;
+ }
+ if (l > 0 and out[(l - 1) as usize] == 90) {
+ var e : u32 = l - 1;
+ var j : u32 = e;
+ while (j > 0 and out[(j - 1) as usize] >= 48 and out[(j - 1) as usize] <= 57) { j = j - 1; }
+ if (j < e and j > 0 and out[(j - 1) as usize] == 46) { e = j - 1; }
+ l = e;
+ }
+ return l;
+}
+
+/// secs(ms) as raw bytes into out: SAY_SECONDS with (ms / 1000).toFixed(3).
+fn secs_text_raw(out: []u8, ms: f64) -> u32 {
+ var tmp : [128]u8 = [0; 128];
+ const l : u32 = js_to_fixed(tmp[0..], 0, ms / 1000.0, 3);
+ return fill_raw(out, SAY_SECONDS, tmp[0..(l as usize)]);
+}
+
+/// The template with {s} replaced by v, as raw (unescaped) bytes.
+fn fill_raw(out: []u8, t: []const u8, v: []const u8) -> u32 {
+ var n : u32 = 0;
+ var i : usize = 0;
+ while (i < t.len) {
+ if (t[i] == 123 and i + 2 < t.len and t[i + 1] == 115 and t[i + 2] == 125) {
+ n = jn_raw(out, n, v);
+ i = i + 3;
+ } else {
+ n = jn_put(out, n, t[i]);
+ i = i + 1;
+ }
+ }
+ return n;
+}
+
+pub fn w_render(dst: []u8) -> u32 {
+ var n : u32 = 0;
+ if (DATA_STATE == 0) {
+ n = raw(dst, n, "
");
+ n = text(dst, n, SAY_LOADING);
+ n = raw(dst, n, "
");
+ if (!ok(n)) { return 0; }
+ return n;
+ }
+ if (DATA_STATE == 2) {
+ n = raw(dst, n, "
");
+ n = text(dst, n, SAY_ERROR);
+ n = raw(dst, n, "
");
+ if (!ok(n)) { return 0; }
+ return n;
+ }
+ const d : []const u8 = DATA[0..(DATA_LEN as usize)];
+ const r : i64 = root(d);
+ n = raw(dst, n, "
");
+ var diff : [512]u8 = [0; 512];
+ const dl : u32 = spec_diff(diff[0..], 0, d, r);
+ if (dl > 0) {
+ n = raw(dst, n, "
");
+ var wb : [600]u8 = [0; 600];
+ const wn : u32 = jv_vput_str(wb[0..], 0, "what", diff[0..(dl as usize)]);
+ n = jv_fill(dst, n, SAY_SPEC_DIFF, wb[0..], wn, false);
+ n = raw(dst, n, "
");
+ }
+ n = paper_html(dst, n, d, r);
+ n = raw(dst, n, "
");
+ const cm : i64 = obj_get(d, r, "commands");
+ if (kind(d, cm) == J_ARRAY) {
+ const cc : u32 = arr_len(d, cm);
+ var c : u32 = 0;
+ while (c < cc) {
+ if (c > 0) { n = put(dst, n, 10); }
+ const el : i64 = arr_at(d, cm, c);
+ if (!nullish(d, el)) { n = jv_text(dst, n, d, el); }
+ c = c + 1;
+ }
+ }
+ n = raw(dst, n, "
");
+ n = text(dst, n, SAY_VERIFY_TITLE);
+ n = raw(dst, n, "
");
+ var v : usize = 0;
+ while (v < SAY_VERIFY_STEPS.len) {
+ n = raw(dst, n, "
");
+ n = text(dst, n, SAY_VERIFY_STEPS[v]);
+ n = raw(dst, n, "
");
+ v = v + 1;
+ }
+ n = raw(dst, n, "
");
+ if (!ok(n)) { return 0; }
+ return n;
+}
+
+/// The splicer follows only the names a module itself mentions, not the names the imported functions
+/// mention. This anchor mentions every helper the writer, the JSON reader and the number rules call.
+/// Never called.
+pub fn imports_anchor(o: []u8, s: []const u8) -> u32 {
+ var n : u32 = 0;
+ n = n + (ok(0) as u32);
+ n = n + OVER;
+ n = n + put(o, 0, 0);
+ n = n + raw(o, 0, s);
+ n = n + put_escaped(o, 0, 0);
+ n = n + text(o, 0, s);
+ n = n + num(o, 0, 0);
+ n = n + attr(o, 0, s, s);
+ n = n + fill(o, 0, s, s, s, s, 0);
+ n = n + fill_named(o, 0, s, s, s, s, s, s, s, s, s, s, s, s, s);
+ n = n + dec_to(o, 0);
+ n = n + (name_is(s, 0, 0, s) as u32);
+ n = n + (is_word_byte(0) as u32);
+ n = n + (H_LT as u32);
+ n = n + (H_GT as u32);
+ n = n + (H_AMP as u32);
+ n = n + (H_QUOTE as u32);
+ n = n + (H_APOS as u32);
+ n = n + (H_SPACE as u32);
+ n = n + (H_SLASH as u32);
+ n = n + (H_EQ as u32);
+ n = n + ((NO_SPAN < 0) as u32);
+ n = n + ((SPAN > 0) as u32);
+ n = n + (J_NONE as u32);
+ n = n + (J_OBJECT as u32);
+ n = n + (J_ARRAY as u32);
+ n = n + (J_STRING as u32);
+ n = n + (J_NUMBER as u32);
+ n = n + (J_TRUE as u32);
+ n = n + (J_FALSE as u32);
+ n = n + (J_NULL as u32);
+ n = n + (jspan(0, 0) as u32);
+ n = n + (span_lo(0) as u32);
+ n = n + (span_hi(0) as u32);
+ n = n + (skip_ws(s, 0) as u32);
+ n = n + (string_end(s, 0) as u32);
+ n = n + (value_end(s, 0) as u32);
+ n = n + (root(s) as u32);
+ n = n + (kind(s, 0) as u32);
+ n = n + (key_is(s, 0, 0, s) as u32);
+ n = n + (obj_get(s, 0, s) as u32);
+ n = n + arr_len(s, 0);
+ n = n + (arr_at(s, 0, 0) as u32);
+ n = n + hex4(s, 0);
+ n = n + escape_at(s, 0, 0);
+ n = n + cp_utf8_len(0);
+ n = n + (cp_utf8_byte(0, 0) as u32);
+ n = n + str_len(s, 0);
+ n = n + (str_byte(s, 0, 0) as u32);
+ n = n + JN_OVER;
+ n = n + JN_CAP;
+ n = n + ((JN_TWO52 > 0.0) as u32);
+ n = n + ((JN_TWO53 > 0.0) as u32);
+ n = n + jn_put(o, 0, 0);
+ n = n + jn_raw(o, 0, s);
+ n = n + jn_digits(o, 0);
+ n = n + jn_mul(o, 0, 0);
+ n = n + jn_mul2(o, 0, 0);
+ n = n + jn_mul5(o, 0, 0);
+ n = n + jn_shift(o, 0, 0);
+ n = n + (jn_cmp(s, s, 0) as u32);
+ n = n + (jn_expo(0.0) as u32);
+ n = n + (jn_mant(0.0) as u32);
+ n = n + ((jn_make(0, 0) > 0.0) as u32);
+ n = n + jn_exact(o, 0, 0);
+ n = n + jn_fscale(0);
+ n = n + (js_is_nan(0.0) as u32);
+ n = n + ((js_nan(0.0) > 0.0) as u32);
+ n = n + ((js_inf(1.0) > 0.0) as u32);
+ n = n + ((js_floor(0.0) > 0.0) as u32);
+ n = n + ((js_ceil(0.0) > 0.0) as u32);
+ n = n + ((js_max(0.0, 0.0) > 0.0) as u32);
+ n = n + ((js_min(0.0, 0.0) > 0.0) as u32);
+ n = n + ((js_round(0.0) > 0.0) as u32);
+ n = n + (jn_shortest(o, 1.0) as u32);
+ n = n + jn_copy(o, s, 0);
+ n = n + jn_put_run(o, 0, s, 0, 0);
+ n = n + jn_layout(o, 0, s, 0, 0);
+ n = n + js_str_num(o, 0, 0.0);
+ n = n + js_to_fixed(o, 0, 0.0, 0);
+ n = n + js_locale_num(o, 0, 0.0);
+ n = n + (jn_num_ok(s, 0, 0) as u32);
+ n = n + (jn_cmp_text(s, 0, 0, 0, 0) as u32);
+ n = n + ((js_parse_num(s, 0, 0) > 0.0) as u32);
+ n = n + ((jn_dec_value(s, 0, 0) > 0.0) as u32);
+ n = n + (jn_abs(0) as u32);
+ n = n + js_str_u64(o, 0, 0);
+ n = n + jv_str_to(o, 0, s, 0);
+ n = n + jv_text_str(o, 0, s, 0);
+ n = n + ((jv_numval(s, 0) > 0.0) as u32);
+ n = n + jv_str(o, 0, s, 0);
+ n = n + jv_text(o, 0, s, 0);
+ n = n + (jv_is(s, 0, s) as u32);
+ n = n + (jv_truthy(s, 0) as u32);
+ n = n + (jv_is_ws(0) as u32);
+ n = n + ((jv_number_of_text(s, 0) > 0.0) as u32);
+ n = n + ((jv_num(s, 0) > 0.0) as u32);
+ n = n + jv_vput_str(o, 0, s, s);
+ n = n + jv_vput_f64(o, 0, s, 0.0);
+ n = n + jv_vput_val(o, 0, s, s, 0);
+ n = n + jv_vput_defined(o, 0, s, s, 0);
+ n = n + jv_vput_fixed(o, 0, s, 0.0, 0);
+ n = n + jv_kid(o, 0, s, 0);
+ n = n + jv_find(s, 0, s);
+ n = n + jv_fill(o, 0, s, s, 0, true);
+ n = n + (jv_string_ok(s, 0) as u32);
+ n = n + (jv_number_ok(s, 0) as u32);
+ n = n + (jv_lit_ok(s, 0, s) as u32);
+ n = n + (jv_valid(s) as u32);
+ n = n + jv_ws_at(s, 0, 0);
+ n = n + jv_ws_before(s, 0, 0);
+ n = n + jv_digit_value(0);
+ return n;
+}
diff --git a/specs/widgets/catalog.t27 b/specs/widgets/catalog.t27
index 0791601bb9..1803efd18f 100644
--- a/specs/widgets/catalog.t27
+++ b/specs/widgets/catalog.t27
@@ -34,9 +34,10 @@
; 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.
+; 14667449, lut-explain 1395124, bit-diff 275620, vcd-wrapped 76076, pin-map 180587, synth-smells 87094, fsm-sketch 91657 (its Math.sin/cos/atan2 stay with the host: V8 differs from libm in the last bit), bit-autopsy 96007.
+; With these, all 12 pure logic files of the census (2775 lines of JavaScript) exist as .t27.
; 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
+; and jtag-verdict, race-the-bee, t27-vs-nvfp4, t27-cell, build-receipt, uart-bucket and test-touch 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
@@ -90,7 +91,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, 2, 1, 1, 2, 1, 1, 3, 2, 2, 2, 1, 1, 3, 1, 1, 2, 1, 1, 1, 1, 1, 1, 1, 1];
+pub const TOOL_PORT : [28]u8 = [2, 2, 3, 1, 2, 1, 2, 2, 1, 1, 3, 2, 2, 2, 2, 1, 3, 1, 1, 2, 2, 3, 3, 1, 3, 1, 3, 2];
; 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.
@@ -226,17 +227,18 @@ test the_waves_cover_every_tool_once {
assert hand_lines(12) == 1432;
}
-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;
+test seven_pages_are_native_and_the_twelve_logic_files_are_ported {
+ assert count_port(PORT_DRAW) == 7;
+ assert count_port(PORT_WORDS) == 9;
+ assert count_port(PORT_LOGIC) == 12;
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 count_port(PORT_LOGIC) + count_port(PORT_DRAW) == 19;
assert TOOL_PORT[10] == PORT_DRAW;
assert TOOL_PORT[16] == PORT_DRAW;
+ assert TOOL_PORT[2] == PORT_DRAW;
+ assert TOOL_PORT[21] == PORT_DRAW;
+ assert TOOL_PORT[22] == PORT_DRAW;
+ assert TOOL_PORT[24] == PORT_DRAW;
+ assert TOOL_PORT[26] == PORT_DRAW;
+ assert TOOL_PORT[7] == PORT_LOGIC;
}
diff --git a/specs/widgets/fsm_sketch_logic.t27 b/specs/widgets/fsm_sketch_logic.t27
new file mode 100644
index 0000000000..6016c62fb2
--- /dev/null
+++ b/specs/widgets/fsm_sketch_logic.t27
@@ -0,0 +1,2837 @@
+// SPDX-License-Identifier: Apache-2.0
+; specs/widgets/fsm_sketch_logic.t27 -- the FSM reader and layout of the fsm-sketch widget, as pure rules
+; over bytes and numbers: the JavaScript file apps/website/public/widgets/fsm-sketch/fsmparse.js (379 lines,
+; gHashTag/trinity) ported to .t27. Epic gHashTag/t27#10937, issue #10938. Owner rule (Only t27): the logic is
+; written in .t27, never by hand in JavaScript. The page reads a pasted or dropped yosys log or KISS2 file
+; in the reader's browser and sends nothing anywhere; these rules keep that: they see only the bytes and
+; numbers the glue hands over, fill buffers the glue hands over, and hold no state between calls.
+; --
+; Table: every export of fsmparse.js and the function(s) that replace it.
+; sniff(text) sniff: 0 none, 1 log, 2 kiss2.
+; parseAny(text) fsm_parse with mode 0: sniff, then parse_log or parse_kiss2; one result stream, see
+; "the result stream" before fsm_parse's helpers.
+; parseLog, parseKiss2 fsm_parse with mode 1 and mode 2 (parse_log, parse_kiss2).
+; countsOf(f) counts_of.
+; codeValue(code) code_value: -1 for null.
+; conditionOf(pat,names) condition_of: the (index, bit) pairs; the glue joins "name=bit" (names[idx] ?? in[idx]).
+; walkOrder(f) walk_order.
+; layoutFsm(f, opts) layout_fsm. opts are passed explicitly: font, charW, edgeFont, maxLabelRows; the
+; JavaScript defaults (14, 0.6 * font, round(0.86 * font), 3) are the glue's. The
+; label lines of a state are given as two numbers each: how many lines and the
+; longest line's length in UTF-16 units (a label's text is drawn by the glue).
+; js_number is Number() of a KISS2 header token, which parseKiss2 uses.
+; Not ported: nothing of fsmparse.js is left out, but the three libm calls of layoutFsm stay with the host
+; (see "Floating point"). The private helpers stateNum, unslash, unit, rot, boxExit, labelBox, overlap,
+; placeLabels, bounds and place are the functions of the same names here (stateNum is state_num).
+; --
+; Domain. ASCII or UTF-8 bytes; bytes above 127 are ordinary non-space characters (JavaScript's \s and trim()
+; also take NBSP, U+2028 and friends as space: not ported, not claimed). A number in a log or KISS2 file is read
+; as an integer of up to 15 digits; an index above 2147483647 or a state number above 15 digits sets status 5
+; ("out of the claimed domain"). A KISS2 header value goes through Number() in the JavaScript: decimal
+; integers and decimals with at most 15 significant digits and an exponent within 22, hex, octal and
+; binary literals and Infinity are read the same here; any other form is NaN, as there. A longer decimal sets
+; status 5.
+; One JavaScript throw is reproduced as status 6: a "FSM `...' from module `...':" line above an
+; "Information on FSM" line whose module name has no non-quote text right after "from module `".
+; --
+; Floating point. layout_fsm repeats fsmparse.js's arithmetic operation for operation in f64. Math.hypot is
+; V8's (the largest absolute value, then Kahan-summed squares of the ratios); Math.max and Math.min
+; propagate NaN and order -0 below +0; Math.round is floor(x + 0.5) without the 0.49999999999999994 trap.
+; The three libm calls of the JavaScript stay with the host, because Math.sin, Math.cos and Math.atan2 are
+; the engine's own (measured: V8 12.9 on arm64 differs in the last bit from fdlibm, musl and the system
+; libm in about 1 call in 100, so no port can promise their bits): (1) the cos and sin of the node angles
+; -Math.PI / 2 + (2 * Math.PI * k) / n, k below n, and of -0.45, 0.45, -0.55, 0.55 come in as the table
+; trig (cos, sin pairs: 2n for the nodes, then 8 for the four fixed angles, in that order); node_angle(k, n)
+; gives the angle to ask the host for. (2) arrow.angle, which no other result depends on, is
+; Math.atan2(arrow.y - c.y, arrow.x - c.x) with c the second control point of a self loop (ef 4, 5) and
+; the control point of a curve (ef 2, 3): the glue computes it from the returned points; ef 10 is 0.
+; The `**` of the JavaScript is exact for every base and exponent it is applied to with a constant t of
+; 0.25 0.5 0.75, and x ** 2 is x * x.
+; ASCII only (L3), English only (LANG-EN).
+; phi^2 + 1/phi^2 = 3 | TRINITY
+
+module widget_fsm_sketch_logic;
+
+pub const NONE : u32 = 4294967295;
+pub const NO_TEXT : i64 = -1;
+pub const PACK : i64 = 4294967296;
+pub const PI : f64 = 3.141592653589793;
+
+pub var V : [8]f64 = [0.0; 8];
+
+// ------------------------------------------------------------------------------------------------
+// numbers and the small pure functions
+// ------------------------------------------------------------------------------------------------
+
+pub fn pack(lo: usize, hi: usize) -> i64 {
+ return (lo as i64) * PACK + (hi as i64);
+}
+
+pub fn lo_of(p: i64) -> usize {
+ return (p / PACK) as usize;
+}
+
+pub fn hi_of(p: i64) -> usize {
+ return (p % PACK) as usize;
+}
+
+pub fn inf() -> f64 {
+ var z : f64 = 0.0;
+ return 1.0 / z;
+}
+
+pub fn nan() -> f64 {
+ var z : f64 = 0.0;
+ return z / z;
+}
+
+pub fn neg_zero(x: f64) -> bool {
+ if (x != 0.0) { return false; }
+ var y : f64 = x;
+ return 1.0 / y < 0.0;
+}
+
+/// JavaScript's truthiness of a number: not 0, not NaN.
+pub fn truthy(x: f64) -> bool {
+ return x != 0.0 and x == x;
+}
+
+/// Math.max of two numbers: NaN wins, +0 is above -0.
+pub fn jsmax(a: f64, b: f64) -> f64 {
+ if (a != a) { return a; }
+ if (b != b) { return b; }
+ if (a > b) { return a; }
+ if (b > a) { return b; }
+ if (a == 0.0 and neg_zero(a)) { return b; }
+ return a;
+}
+
+/// Math.min of two numbers: NaN wins, -0 is below +0.
+pub fn jsmin(a: f64, b: f64) -> f64 {
+ if (a != a) { return a; }
+ if (b != b) { return b; }
+ if (a < b) { return a; }
+ if (b < a) { return b; }
+ if (a == 0.0 and !neg_zero(a)) { return b; }
+ return a;
+}
+
+/// Math.round: the nearest integer, halves toward +infinity.
+pub fn jsround(x: f64) -> f64 {
+ const r : f64 = @floor(x);
+ if (x - r >= 0.5) {
+ if (x < 0.0 and r + 1.0 == 0.0) {
+ var m1 : f64 = 0.0 - 1.0;
+ return 0.0 * m1;
+ }
+ return r + 1.0;
+ }
+ return r;
+}
+
+/// V8's Math.hypot(x, y).
+pub fn hypot2(x: f64, y: f64) -> f64 {
+ const ax : f64 = @abs(x);
+ const ay : f64 = @abs(y);
+ var max : f64 = 0.0;
+ if (ax == ax and ax > max) { max = ax; }
+ if (ay == ay and ay > max) { max = ay; }
+ if (max == inf()) { return inf(); }
+ if (x != x or y != y) { return nan(); }
+ if (max == 0.0) { return 0.0; }
+ var sum : f64 = 0.0;
+ var comp : f64 = 0.0;
+ var k : u32 = 0;
+ while (k < 2) {
+ var v : f64 = ax;
+ if (k == 1) { v = ay; }
+ const q : f64 = v / max;
+ const summand : f64 = q * q - comp;
+ const prelim : f64 = sum + summand;
+ comp = (prelim - sum) - summand;
+ sum = prelim;
+ k = k + 1;
+ }
+ return @sqrt(sum) * max;
+}
+
+/// The unit vector of (x, y) into V[0], V[1]: unit = (x, y) => { const l = Math.hypot(x, y) || 1; ... }.
+pub fn unit_to(x: f64, y: f64) {
+ var l : f64 = hypot2(x, y);
+ if (!truthy(l)) { l = 1.0; }
+ V[0] = x / l;
+ V[1] = y / l;
+}
+
+/// rot([x, y], a) into V[0], V[1], given cos(a) and sin(a) (the glue's Math.cos and Math.sin).
+pub fn rot_to(x: f64, y: f64, cs: f64, sn: f64) {
+ V[0] = x * cs - y * sn;
+ V[1] = x * sn + y * cs;
+}
+
+/// The angle of the k-th state on the ellipse of n states: -Math.PI / 2 + (2 * Math.PI * k) / n.
+pub fn node_angle(k: usize, n: usize) -> f64 {
+ return (0.0 - PI) / 2.0 + (2.0 * PI * (k as f64)) / (n as f64);
+}
+
+/// Where a ray from a box's centre in direction (dx, dy) leaves the box, into V[0], V[1].
+pub fn box_exit(nx: f64, ny: f64, nw: f64, nh: f64, dx: f64, dy: f64, pad: f64) {
+ const hw : f64 = nw / 2.0 + pad;
+ const hh : f64 = nh / 2.0 + pad;
+ var a : f64 = inf();
+ if (truthy(dx)) { a = hw / @abs(dx); }
+ var b : f64 = inf();
+ if (truthy(dy)) { b = hh / @abs(dy); }
+ const t : f64 = jsmin(a, b);
+ V[0] = nx + dx * t;
+ V[1] = ny + dy * t;
+}
+
+/// parseInt(code, 2) of a code of 0 and 1 of at most 52 digits; -1 when it holds anything else.
+pub fn code_value(code: []const u8) -> i64 {
+ if (code.len == 0 or code.len > 52) { return -1; }
+ var v : i64 = 0;
+ var i : usize = 0;
+ while (i < code.len) {
+ const c : u8 = code[i];
+ if (c != 48 and c != 49) { return -1; }
+ v = v * 2 + ((c - 48) as i64);
+ i = i + 1;
+ }
+ return v;
+}
+
+/// conditionOf: for every character of the pattern that is not "-", its input index (highest first as
+/// yosys prints) in out_idx and the character in out_bit; returns how many.
+pub fn condition_of(pattern: []const u8, out_idx: []u32, out_bit: []u8) -> usize {
+ const n : usize = pattern.len;
+ var cnt : usize = 0;
+ var j : usize = 0;
+ while (j < n) {
+ const c : u8 = pattern[j];
+ if (c != 45) {
+ if (cnt < out_idx.len and cnt < out_bit.len) {
+ out_idx[cnt] = (n - 1 - j) as u32;
+ out_bit[cnt] = c;
+ }
+ cnt = cnt + 1;
+ }
+ j = j + 1;
+ }
+ return cnt;
+}
+
+/// countsOf: out = [states, transitions, inputs, outputs, bits].
+pub fn counts_of(n_states: u32, n_rows: u32, n_in: f64, n_out: f64, n_bits: f64, out: []f64) {
+ out[0] = n_states as f64;
+ out[1] = n_rows as f64;
+ out[2] = n_in;
+ out[3] = n_out;
+ out[4] = n_bits;
+}
+
+// ------------------------------------------------------------------------------------------------
+// walkOrder
+// ------------------------------------------------------------------------------------------------
+
+/// States in the order a reader follows them: breadth-first from reset (or 0), then any left over.
+/// order gets n states; work needs 2 * n + nrows + 2 words. Rows whose ends are not below n are ignored.
+/// Returns n.
+pub fn walk_order(n: usize, reset: i64, nrows: usize, rows_from: []const u32, rows_to: []const u32, order: []u32, work: []u32) -> usize {
+ // work[0 .. n] offsets, work[n + 1 .. n + 1 + nrows] neighbours, work[n + 1 + nrows .. 2n + 1 + nrows] seen
+ const adj : usize = n + 1;
+ const seen : usize = n + 1 + nrows;
+ var i : usize = 0;
+ while (i <= n) {
+ work[i] = 0;
+ i = i + 1;
+ }
+ i = 0;
+ while (i < n) {
+ work[seen + i] = 0;
+ i = i + 1;
+ }
+ var r : usize = 0;
+ while (r < nrows) {
+ const f : usize = rows_from[r] as usize;
+ const t : usize = rows_to[r] as usize;
+ if (f < n and t < n and f != t) { work[f + 1] = work[f + 1] + 1; }
+ r = r + 1;
+ }
+ i = 0;
+ while (i < n) {
+ work[i + 1] = work[i + 1] + work[i];
+ i = i + 1;
+ }
+ // fill with a cursor kept in order[] (free until the walk starts)
+ i = 0;
+ while (i < n) {
+ order[i] = work[i];
+ i = i + 1;
+ }
+ r = 0;
+ while (r < nrows) {
+ const f : usize = rows_from[r] as usize;
+ const t : usize = rows_to[r] as usize;
+ if (f < n and t < n and f != t) {
+ work[adj + (order[f] as usize)] = t as u32;
+ order[f] = order[f] + 1;
+ }
+ r = r + 1;
+ }
+ // sort each neighbour list ascending
+ i = 0;
+ while (i < n) {
+ const lo : usize = work[i] as usize;
+ const hi : usize = work[i + 1] as usize;
+ var a : usize = lo + 1;
+ while (a < hi) {
+ const v : u32 = work[adj + a];
+ var b : usize = a;
+ while (b > lo and work[adj + b - 1] > v) {
+ work[adj + b] = work[adj + b - 1];
+ b = b - 1;
+ }
+ work[adj + b] = v;
+ a = a + 1;
+ }
+ i = i + 1;
+ }
+ var start : usize = 0;
+ if (reset >= 0) { start = reset as usize; }
+ var tail : usize = 0;
+ var head : usize = 0;
+ var s : usize = 0;
+ var pass : usize = 0;
+ // the roots are start, then 0 .. n - 1
+ while (pass <= n) {
+ var root : usize = start;
+ if (pass > 0) { root = pass - 1; }
+ pass = pass + 1;
+ if (root < n and work[seen + root] == 0) {
+ work[seen + root] = 1;
+ order[tail] = root as u32;
+ tail = tail + 1;
+ while (head < tail) {
+ const x : usize = order[head] as usize;
+ head = head + 1;
+ var q : usize = work[x] as usize;
+ const qe : usize = work[x + 1] as usize;
+ while (q < qe) {
+ const y : usize = work[adj + q] as usize;
+ if (work[seen + y] == 0) {
+ work[seen + y] = 1;
+ order[tail] = y as u32;
+ tail = tail + 1;
+ }
+ q = q + 1;
+ }
+ }
+ }
+ }
+ s = tail;
+ return n;
+}
+
+// ------------------------------------------------------------------------------------------------
+// layoutFsm
+// ------------------------------------------------------------------------------------------------
+
+/// The box a label covers, into V[0 .. 3] = x0, y0, x1, y1. anchor 0 middle 1 start 2 end; valign 0 middle
+/// 1 bottom 2 top; labmax is the longest line length (at least 1, or the "more" text length + 6); shown and
+/// more as in the label.
+pub fn label_box(lx: f64, ly: f64, anchor: u32, valign: u32, labmax: u32, shown: u32, more: u32, edge_font: f64, edge_char_w: f64) {
+ const lh : f64 = edge_font * 1.15;
+ const bw : f64 = (labmax as f64) * edge_char_w;
+ var extra : f64 = 0.0;
+ if (more != 0) { extra = 1.0; }
+ const bh : f64 = ((shown as f64) + extra) * lh;
+ var x0 : f64 = lx - bw / 2.0;
+ if (anchor == 1) { x0 = lx; }
+ if (anchor == 2) { x0 = lx - bw; }
+ var y0 : f64 = ly - bh / 2.0;
+ if (valign == 1) { y0 = ly - bh; }
+ if (valign == 2) { y0 = ly; }
+ V[0] = x0;
+ V[1] = y0;
+ V[2] = x0 + bw;
+ V[3] = y0 + bh;
+}
+
+/// overlap of two boxes (x0, y0, x1, y1).
+pub fn overlap(px0: f64, py0: f64, px1: f64, py1: f64, qx0: f64, qy0: f64, qx1: f64, qy1: f64) -> f64 {
+ const w : f64 = jsmax(0.0, jsmin(px1, qx1) - jsmax(px0, qx0));
+ const h : f64 = jsmax(0.0, jsmin(py1, qy1) - jsmax(py0, qy0));
+ return w * h;
+}
+
+pub fn digits_of(v: u32) -> u32 {
+ var n : u32 = 1;
+ var x : u32 = v;
+ while (x >= 10) {
+ x = x / 10;
+ n = n + 1;
+ }
+ return n;
+}
+
+pub fn anchor_of(nx: f64) -> u32 {
+ if (@abs(nx) < 0.35) { return 0; }
+ if (nx > 0.0) { return 1; }
+ return 2;
+}
+
+pub fn valign_of(ny: f64) -> u32 {
+ if (ny < -0.35) { return 1; }
+ if (ny > 0.35) { return 2; }
+ return 0;
+}
+
+/// layoutFsm: node boxes and edge curves for one FSM in a w x h box. See the header of the file.
+/// rows_from, rows_to, rows_inlen: nrows entries (every from and to below n).
+/// label_n, label_maxlen: n entries.
+/// nodes (f64, 4 per state): x, y, w, h.
+/// ef (f64, 13 per edge): p0 x y, c1 (the control point of a quadratic) x y, c2 x y (self loops only; 0
+/// otherwise), p1 x y, arrow x y angle, label x y. For a quadratic the points are p0, c, p1 and the
+/// entries 4 to 7 hold 0, 0, then p1: [p0, c, 0, 0, p1]. Self loops: [p0, c1, c2, p1].
+/// eu (u32, 10 per edge): from, to, self, rows, lines shown, more, anchor, valign, longest label line,
+/// unused. The shown lines are the first rows of the pair, in row order.
+/// order: n words (the walk order), work: u32 of 2n + nrows + 2 + 2nrows + 4, fw: f64 of 4 * (n + 2 * nrows) + 8.
+/// Returns the number of edges.
+pub fn layout_fsm(n: usize, reset: i64, nrows: usize, rows_from: []const u32, rows_to: []const u32, rows_inlen: []const u32, label_n: []const u32, label_maxlen: []const u32, w: f64, h: f64, font: f64, charw: f64, edgefont: f64, maxrows: i64, nodes: []f64, ef: []f64, eu: []u32, order: []u32, work: []u32, fw: []f64, trig: []const f64) -> usize {
+ const edge_char_w : f64 = edgefont * (charw / font);
+ const line_h : f64 = jsround(font * 1.25);
+ // boxes
+ var maxw : f64 = 0.0;
+ var maxh : f64 = 0.0;
+ var i : usize = 0;
+ while (i < n) {
+ var tw : f64 = (0.0 - inf()) * charw;
+ if (label_n[i] > 0) { tw = (label_maxlen[i] as f64) * charw; }
+ const bw : f64 = jsmax(tw + font * 1.4, font * 3.2);
+ const bh : f64 = (label_n[i] as f64) * line_h + font * 0.9;
+ nodes[i * 4 + 2] = bw;
+ nodes[i * 4 + 3] = bh;
+ maxw = jsmax(maxw, bw);
+ maxh = jsmax(maxh, bh);
+ i = i + 1;
+ }
+ const loop_r : f64 = jsmax(maxh * 0.8, font * 2.2);
+ walk_order(n, reset, nrows, rows_from, rows_to, order, work);
+ // one edge per (from, to) pair, in order of first row
+ var ne : usize = 0;
+ var r : usize = 0;
+ while (r < nrows) {
+ var e : usize = 0;
+ var hit : bool = false;
+ while (e < ne and !hit) {
+ if (eu[e * 10] == rows_from[r] and eu[e * 10 + 1] == rows_to[r]) { hit = true; } else { e = e + 1; }
+ }
+ if (!hit) {
+ eu[ne * 10] = rows_from[r];
+ eu[ne * 10 + 1] = rows_to[r];
+ eu[ne * 10 + 3] = 0;
+ e = ne;
+ ne = ne + 1;
+ }
+ eu[e * 10 + 3] = eu[e * 10 + 3] + 1;
+ r = r + 1;
+ }
+ var e : usize = 0;
+ while (e < ne) {
+ const cntr : i64 = eu[e * 10 + 3] as i64;
+ var shown : i64 = cntr;
+ var more : i64 = 0;
+ if (cntr > maxrows) {
+ var keep : i64 = maxrows - 1;
+ if (keep < 1) { keep = 1; }
+ shown = keep;
+ more = cntr - keep;
+ }
+ eu[e * 10 + 2] = 0;
+ if (eu[e * 10] == eu[e * 10 + 1]) { eu[e * 10 + 2] = 1; }
+ eu[e * 10 + 4] = shown as u32;
+ eu[e * 10 + 5] = more as u32;
+ var lm : u32 = 1;
+ var seen : i64 = 0;
+ r = 0;
+ while (r < nrows and seen < shown) {
+ if (rows_from[r] == eu[e * 10] and rows_to[r] == eu[e * 10 + 1]) {
+ if (rows_inlen[r] > lm) { lm = rows_inlen[r]; }
+ seen = seen + 1;
+ }
+ r = r + 1;
+ }
+ if (more != 0) {
+ const mt : u32 = digits_of(more as u32) + 6;
+ if (mt > lm) { lm = mt; }
+ }
+ eu[e * 10 + 8] = lm;
+ eu[e * 10 + 9] = 0;
+ e = e + 1;
+ }
+ // the ellipse, shrunk until everything fits
+ const fx0 : f64 = 4.0;
+ const fy0 : f64 = 4.0;
+ const fx1 : f64 = w - 4.0;
+ const fy1 : f64 = h - 4.0;
+ var rx : f64 = jsmax(0.0, w / 2.0 - maxw / 2.0 - 6.0);
+ var ry : f64 = jsmax(0.0, h / 2.0 - maxh / 2.0 - 6.0);
+ var pass : u32 = 0;
+ var done : bool = false;
+ while (pass < 12 and !done) {
+ place(n, ne, rx, ry, w, h, edgefont, edge_char_w, loop_r, nodes, ef, eu, order, fw, trig);
+ // bounds
+ var bx0 : f64 = inf();
+ var by0 : f64 = inf();
+ var bx1 : f64 = 0.0 - inf();
+ var by1 : f64 = 0.0 - inf();
+ i = 0;
+ while (i < n) {
+ const x : f64 = nodes[i * 4];
+ const y : f64 = nodes[i * 4 + 1];
+ const nw : f64 = nodes[i * 4 + 2];
+ const nh : f64 = nodes[i * 4 + 3];
+ bx0 = jsmin(bx0, x - nw / 2.0 - 6.0);
+ by0 = jsmin(by0, y - nh / 2.0 - 6.0);
+ bx1 = jsmax(bx1, x + nw / 2.0 + 6.0);
+ by1 = jsmax(by1, y + nh / 2.0 + 6.0);
+ i = i + 1;
+ }
+ e = 0;
+ while (e < ne) {
+ label_box(ef[e * 13 + 11], ef[e * 13 + 12], eu[e * 10 + 6], eu[e * 10 + 7], eu[e * 10 + 8], eu[e * 10 + 4], eu[e * 10 + 5], edgefont, edge_char_w);
+ bx0 = jsmin(bx0, V[0]);
+ by0 = jsmin(by0, V[1]);
+ bx1 = jsmax(bx1, V[2]);
+ by1 = jsmax(by1, V[3]);
+ if (eu[e * 10 + 2] == 1) {
+ var s : u32 = 0;
+ while (s < 3) {
+ var t : f64 = 0.25;
+ if (s == 1) { t = 0.5; }
+ if (s == 2) { t = 0.75; }
+ const u1 : f64 = 1.0 - t;
+ const k0 : f64 = u1 * u1 * u1;
+ const k1 : f64 = 3.0 * (u1 * u1) * t;
+ const k2 : f64 = 3.0 * u1 * t * t;
+ const k3 : f64 = t * t * t;
+ const x : f64 = k0 * ef[e * 13] + k1 * ef[e * 13 + 2] + k2 * ef[e * 13 + 4] + k3 * ef[e * 13 + 6];
+ const y : f64 = k0 * ef[e * 13 + 1] + k1 * ef[e * 13 + 3] + k2 * ef[e * 13 + 5] + k3 * ef[e * 13 + 7];
+ bx0 = jsmin(bx0, x - 2.0);
+ by0 = jsmin(by0, y - 2.0);
+ bx1 = jsmax(bx1, x + 2.0);
+ by1 = jsmax(by1, y + 2.0);
+ s = s + 1;
+ }
+ }
+ e = e + 1;
+ }
+ const ox : f64 = jsmax(jsmax(0.0, fx0 - bx0), bx1 - fx1);
+ const oy : f64 = jsmax(jsmax(0.0, fy0 - by0), by1 - fy1);
+ if (ox < 1.0 and oy < 1.0) {
+ done = true;
+ } else {
+ rx = jsmax(0.0, rx - ox - 1.0);
+ ry = jsmax(0.0, ry - oy - 1.0);
+ }
+ pass = pass + 1;
+ }
+ return ne;
+}
+
+/// place(rx, ry): the node centres on the ellipse, the edge curves, then the edge labels.
+pub fn place(n: usize, ne: usize, rx: f64, ry: f64, w: f64, h: f64, edgefont: f64, edge_char_w: f64, loop_r: f64, nodes: []f64, ef: []f64, eu: []u32, order: []u32, fw: []f64, trig: []const f64) {
+ const cx : f64 = w / 2.0;
+ const cy : f64 = h / 2.0;
+ var i : usize = 0;
+ while (i < n) {
+ nodes[i * 4] = cx;
+ nodes[i * 4 + 1] = cy;
+ i = i + 1;
+ }
+ if (n != 1) {
+ var k : usize = 0;
+ while (k < n) {
+ const st : usize = order[k] as usize;
+ nodes[st * 4] = cx + rx * trig[k * 2];
+ nodes[st * 4 + 1] = cy + ry * trig[k * 2 + 1];
+ k = k + 1;
+ }
+ }
+ var e : usize = 0;
+ while (e < ne) {
+ const af : usize = eu[e * 10] as usize;
+ const bt : usize = eu[e * 10 + 1] as usize;
+ const ax : f64 = nodes[af * 4];
+ const ay : f64 = nodes[af * 4 + 1];
+ const aw : f64 = nodes[af * 4 + 2];
+ const ah : f64 = nodes[af * 4 + 3];
+ const o : usize = e * 13;
+ if (eu[e * 10 + 2] == 1) {
+ var ux : f64 = 0.0;
+ var uy : f64 = 0.0 - 1.0;
+ if (n != 1) {
+ unit_to(ax - cx, ay - cy);
+ ux = V[0];
+ uy = V[1];
+ }
+ rot_to(ux, uy, trig[2 * n], trig[2 * n + 1]);
+ box_exit(ax, ay, aw, ah, V[0], V[1], 0.0);
+ const p0x : f64 = V[0];
+ const p0y : f64 = V[1];
+ rot_to(ux, uy, trig[2 * n + 2], trig[2 * n + 3]);
+ box_exit(ax, ay, aw, ah, V[0], V[1], 0.0);
+ const p1x : f64 = V[0];
+ const p1y : f64 = V[1];
+ rot_to(ux, uy, trig[2 * n + 4], trig[2 * n + 5]);
+ const c1x : f64 = ax + V[0] * (loop_r * 1.6 + aw / 5.0);
+ const c1y : f64 = ay + V[1] * (loop_r * 1.6 + ah / 3.0);
+ rot_to(ux, uy, trig[2 * n + 6], trig[2 * n + 7]);
+ const c2x : f64 = ax + V[0] * (loop_r * 1.6 + aw / 5.0);
+ const c2y : f64 = ay + V[1] * (loop_r * 1.6 + ah / 3.0);
+ const apx : f64 = 0.125 * p0x + 0.375 * c1x + 0.375 * c2x + 0.125 * p1x;
+ const apy : f64 = 0.125 * p0y + 0.375 * c1y + 0.375 * c2y + 0.125 * p1y;
+ ef[o] = p0x;
+ ef[o + 1] = p0y;
+ ef[o + 2] = c1x;
+ ef[o + 3] = c1y;
+ ef[o + 4] = c2x;
+ ef[o + 5] = c2y;
+ ef[o + 6] = p1x;
+ ef[o + 7] = p1y;
+ ef[o + 8] = p1x;
+ ef[o + 9] = p1y;
+ ef[o + 10] = 0.0;
+ ef[o + 11] = apx + ux * edgefont * 0.5;
+ ef[o + 12] = apy + uy * edgefont * 0.5;
+ eu[e * 10 + 6] = anchor_of(ux);
+ eu[e * 10 + 7] = valign_of(uy);
+ } else {
+ const bx : f64 = nodes[bt * 4];
+ const by : f64 = nodes[bt * 4 + 1];
+ const bw : f64 = nodes[bt * 4 + 2];
+ const bh : f64 = nodes[bt * 4 + 3];
+ const dx : f64 = bx - ax;
+ const dy : f64 = by - ay;
+ var len : f64 = hypot2(dx, dy);
+ if (!truthy(len)) { len = 1.0; }
+ const nx : f64 = -dy / len;
+ const ny : f64 = dx / len;
+ var back : bool = false;
+ var q : usize = 0;
+ while (q < ne and !back) {
+ if (eu[q * 10] == eu[e * 10 + 1] and eu[q * 10 + 1] == eu[e * 10]) { back = true; }
+ q = q + 1;
+ }
+ var bend : f64 = len * 0.1;
+ if (back) { bend = len * 0.2; }
+ const cxx : f64 = (ax + bx) / 2.0 + nx * bend;
+ const cyy : f64 = (ay + by) / 2.0 + ny * bend;
+ unit_to(cxx - ax, cyy - ay);
+ box_exit(ax, ay, aw, ah, V[0], V[1], 0.0);
+ const p0x : f64 = V[0];
+ const p0y : f64 = V[1];
+ unit_to(cxx - bx, cyy - by);
+ box_exit(bx, by, bw, bh, V[0], V[1], 2.0);
+ const p1x : f64 = V[0];
+ const p1y : f64 = V[1];
+ ef[o] = p0x;
+ ef[o + 1] = p0y;
+ ef[o + 2] = cxx;
+ ef[o + 3] = cyy;
+ ef[o + 4] = 0.0;
+ ef[o + 5] = 0.0;
+ ef[o + 6] = p1x;
+ ef[o + 7] = p1y;
+ ef[o + 8] = p1x;
+ ef[o + 9] = p1y;
+ ef[o + 10] = 0.0;
+ }
+ e = e + 1;
+ }
+ place_labels(n, ne, w, h, edgefont, edge_char_w, nodes, ef, eu, fw);
+}
+
+/// placeLabels: self-loop labels stay at the loop's apex; every other label tries points along its curve
+/// on either side and takes the cheapest. fw holds the boxes already taken, 4 f64 each.
+pub fn place_labels(n: usize, ne: usize, w: f64, h: f64, edgefont: f64, edge_char_w: f64, nodes: []f64, ef: []f64, eu: []u32, fw: []f64) {
+ var np : usize = 0;
+ var i : usize = 0;
+ while (i < n) {
+ const x : f64 = nodes[i * 4];
+ const y : f64 = nodes[i * 4 + 1];
+ const nw : f64 = nodes[i * 4 + 2];
+ const nh : f64 = nodes[i * 4 + 3];
+ fw[np * 4] = x - nw / 2.0 - 3.0;
+ fw[np * 4 + 1] = y - nh / 2.0 - 3.0;
+ fw[np * 4 + 2] = x + nw / 2.0 + 3.0;
+ fw[np * 4 + 3] = y + nh / 2.0 + 3.0;
+ np = np + 1;
+ i = i + 1;
+ }
+ var e : usize = 0;
+ while (e < ne) {
+ if (eu[e * 10 + 2] == 1) {
+ label_box(ef[e * 13 + 11], ef[e * 13 + 12], eu[e * 10 + 6], eu[e * 10 + 7], eu[e * 10 + 8], eu[e * 10 + 4], eu[e * 10 + 5], edgefont, edge_char_w);
+ fw[np * 4] = V[0];
+ fw[np * 4 + 1] = V[1];
+ fw[np * 4 + 2] = V[2];
+ fw[np * 4 + 3] = V[3];
+ np = np + 1;
+ }
+ e = e + 1;
+ }
+ const gx0 : f64 = 2.0;
+ const gy0 : f64 = 2.0;
+ const gx1 : f64 = w - 2.0;
+ const gy1 : f64 = h - 2.0;
+ e = 0;
+ while (e < ne) {
+ if (eu[e * 10 + 2] == 0) {
+ const o : usize = e * 13;
+ const p0x : f64 = ef[o];
+ const p0y : f64 = ef[o + 1];
+ const cx : f64 = ef[o + 2];
+ const cy : f64 = ef[o + 3];
+ const p1x : f64 = ef[o + 6];
+ const p1y : f64 = ef[o + 7];
+ var have : bool = false;
+ var best_cost : f64 = 0.0;
+ var best_x : f64 = 0.0;
+ var best_y : f64 = 0.0;
+ var best_anchor : u32 = 0;
+ var best_valign : u32 = 0;
+ var bb0 : f64 = 0.0;
+ var bb1 : f64 = 0.0;
+ var bb2 : f64 = 0.0;
+ var bb3 : f64 = 0.0;
+ var ti : u32 = 0;
+ var stop : bool = false;
+ while (ti < 7 and !stop) {
+ var t : f64 = 0.5;
+ if (ti == 1) { t = 0.4; }
+ if (ti == 2) { t = 0.6; }
+ if (ti == 3) { t = 0.3; }
+ if (ti == 4) { t = 0.7; }
+ if (ti == 5) { t = 0.22; }
+ if (ti == 6) { t = 0.78; }
+ const om : f64 = 1.0 - t;
+ const ptx : f64 = om * om * p0x + 2.0 * om * t * cx + t * t * p1x;
+ const pty : f64 = om * om * p0y + 2.0 * om * t * cy + t * t * p1y;
+ unit_to(2.0 * om * (cx - p0x) + 2.0 * t * (p1x - cx), 2.0 * om * (cy - p0y) + 2.0 * t * (p1y - cy));
+ const tanx : f64 = V[0];
+ const tany : f64 = V[1];
+ var si : u32 = 0;
+ while (si < 2) {
+ var side : f64 = 1.0;
+ if (si == 1) { side = 0.0 - 1.0; }
+ const nrx : f64 = -tany * side;
+ const nry : f64 = tanx * side;
+ const lx : f64 = ptx + nrx * edgefont * 0.5;
+ const ly : f64 = pty + nry * edgefont * 0.5;
+ const anc : u32 = anchor_of(nrx);
+ const val : u32 = valign_of(nry);
+ label_box(lx, ly, anc, val, eu[e * 10 + 8], eu[e * 10 + 4], eu[e * 10 + 5], edgefont, edge_char_w);
+ const x0 : f64 = V[0];
+ const y0 : f64 = V[1];
+ const x1 : f64 = V[2];
+ const y1 : f64 = V[3];
+ const out : f64 = (x1 - x0) * (y1 - y0) - overlap(x0, y0, x1, y1, gx0, gy0, gx1, gy1);
+ var sum : f64 = 0.0;
+ var q : usize = 0;
+ while (q < np) {
+ sum = sum + overlap(x0, y0, x1, y1, fw[q * 4], fw[q * 4 + 1], fw[q * 4 + 2], fw[q * 4 + 3]);
+ q = q + 1;
+ }
+ const cost : f64 = sum + out * 4.0 + @abs(t - 0.5) * 4.0;
+ if (!have or cost < best_cost) {
+ have = true;
+ best_cost = cost;
+ best_x = lx;
+ best_y = ly;
+ best_anchor = anc;
+ best_valign = val;
+ bb0 = x0;
+ bb1 = y0;
+ bb2 = x1;
+ bb3 = y1;
+ }
+ si = si + 1;
+ }
+ if (have and best_cost < 1.0) { stop = true; }
+ ti = ti + 1;
+ }
+ ef[o + 11] = best_x;
+ ef[o + 12] = best_y;
+ eu[e * 10 + 6] = best_anchor;
+ eu[e * 10 + 7] = best_valign;
+ fw[np * 4] = bb0;
+ fw[np * 4 + 1] = bb1;
+ fw[np * 4 + 2] = bb2;
+ fw[np * 4 + 3] = bb3;
+ np = np + 1;
+ }
+ e = e + 1;
+ }
+}
+
+// ------------------------------------------------------------------------------------------------
+// text helpers
+// ------------------------------------------------------------------------------------------------
+
+pub const EV : usize = 10;
+
+pub const EV_FOUND : u32 = 1;
+pub const EV_REJ : u32 = 2;
+pub const EV_REASON : u32 = 3;
+pub const EV_RECODE : u32 = 4;
+pub const EV_FSM : u32 = 5;
+pub const EV_NUM : u32 = 6;
+pub const EV_IN : u32 = 7;
+pub const EV_OUT : u32 = 8;
+pub const EV_STATE : u32 = 9;
+pub const EV_ROW : u32 = 10;
+pub const EV_RESET : u32 = 11;
+pub const EV_NAME : u32 = 20;
+pub const EV_ROW2 : u32 = 21;
+pub const EV_ILB : u32 = 22;
+pub const EV_OB : u32 = 23;
+
+pub const N_KISS2_S : u32 = 0;
+pub const N_KISS2_P : u32 = 1;
+pub const N_NO_RESET_NAMED : u32 = 2;
+pub const N_NO_RESET : u32 = 3;
+pub const N_WIDTH_IN : u32 = 4;
+pub const N_RECODED : u32 = 5;
+pub const N_NO_INFO : u32 = 6;
+
+pub const E_UNKNOWN : u32 = 1;
+pub const E_KISS2ROW : u32 = 2;
+pub const E_NOFSMPASS : u32 = 3;
+
+pub var M : [8]usize = [0; 8];
+pub var NUMFLAG : u32 = 0;
+pub var PEND : [2]usize = [0; 2];
+
+pub fn is_ws(b: u8) -> bool {
+ return b == 32 or (b >= 9 and b <= 13);
+}
+
+pub fn is_digit(b: u8) -> bool {
+ return b >= 48 and b <= 57;
+}
+
+pub fn match_in(s: []const u8, p: usize, hi: usize, w: []const u8) -> bool {
+ if (p + w.len > hi) { return false; }
+ var k : usize = 0;
+ while (k < w.len) {
+ if (s[p + k] != w[k]) { return false; }
+ k = k + 1;
+ }
+ return true;
+}
+
+/// p + the length of lit when lit sits at s[p ..] inside s[.. hi], else -1.
+pub fn eat(s: []const u8, p: usize, hi: usize, lit: []const u8) -> i64 {
+ if (match_in(s, p, hi, lit)) { return (p + lit.len) as i64; }
+ return -1;
+}
+
+pub fn span_eq(a: []const u8, alo: usize, ahi: usize, blo: usize, bhi: usize) -> bool {
+ if (ahi - alo != bhi - blo) { return false; }
+ var k : usize = 0;
+ const n : usize = ahi - alo;
+ while (k < n) {
+ if (a[alo + k] != a[blo + k]) { return false; }
+ k = k + 1;
+ }
+ return true;
+}
+
+pub fn skip_ws(s: []const u8, p: usize, hi: usize) -> usize {
+ var q : usize = p;
+ while (q < hi and is_ws(s[q])) { q = q + 1; }
+ return q;
+}
+
+pub fn skip_digits(s: []const u8, p: usize, hi: usize) -> usize {
+ var q : usize = p;
+ while (q < hi and is_digit(s[q])) { q = q + 1; }
+ return q;
+}
+
+/// The end of the non-space run that starts at p.
+pub fn tok_end(s: []const u8, p: usize, hi: usize) -> usize {
+ var q : usize = p;
+ while (q < hi and !is_ws(s[q])) { q = q + 1; }
+ return q;
+}
+
+/// The end of the run of bytes other than ' that starts at p.
+pub fn run_noquote(s: []const u8, p: usize, hi: usize) -> usize {
+ var q : usize = p;
+ while (q < hi and s[q] != 39) { q = q + 1; }
+ return q;
+}
+
+/// The end of the run of [01\-xz] that starts at p.
+pub fn run_code(s: []const u8, p: usize, hi: usize) -> usize {
+ var q : usize = p;
+ while (q < hi and (s[q] == 48 or s[q] == 49 or s[q] == 45 or s[q] == 120 or s[q] == 122)) { q = q + 1; }
+ return q;
+}
+
+pub fn rstrip(s: []const u8, lo: usize, hi: usize) -> usize {
+ var e : usize = hi;
+ while (e > lo and is_ws(s[e - 1])) { e = e - 1; }
+ return e;
+}
+
+/// Does s[lo .. hi] hold no carriage return? (JavaScript's . does not match one.)
+pub fn no_cr(s: []const u8, lo: usize, hi: usize) -> bool {
+ var i : usize = lo;
+ while (i < hi) {
+ if (s[i] == 13 or s[i] == 10) { return false; }
+ i = i + 1;
+ }
+ return true;
+}
+
+pub fn has_sub(s: []const u8, w: []const u8) -> bool {
+ var p : usize = 0;
+ while (p + w.len <= s.len) {
+ if (match_in(s, p, s.len, w)) { return true; }
+ p = p + 1;
+ }
+ return false;
+}
+
+/// Number(digits) for a run of ASCII digits, saturating at 2147483647 and flagging NUMFLAG = 5 above it.
+pub fn dig_val(s: []const u8, lo: usize, hi: usize) -> u32 {
+ var v : u64 = 0;
+ var i : usize = lo;
+ while (i < hi) {
+ v = v * 10 + ((s[i] - 48) as u64);
+ if (v > 2147483647) {
+ NUMFLAG = 5;
+ return 2147483647;
+ }
+ i = i + 1;
+ }
+ return v as u32;
+}
+
+/// text.split(/\r?\n/): ls gets (start, end) for every line; returns how many (0 when ls is too short).
+pub fn split_nl(text: []const u8, ls: []u32) -> usize {
+ if (ls.len < 2 * (text.len + 1)) { return 0; }
+ var n : usize = 0;
+ var start : usize = 0;
+ var i : usize = 0;
+ while (i < text.len) {
+ if (text[i] == 10) {
+ var e : usize = i;
+ if (e > start and text[e - 1] == 13) { e = e - 1; }
+ ls[n * 2] = start as u32;
+ ls[n * 2 + 1] = e as u32;
+ n = n + 1;
+ start = i + 1;
+ }
+ i = i + 1;
+ }
+ ls[n * 2] = start as u32;
+ ls[n * 2 + 1] = text.len as u32;
+ return n + 1;
+}
+
+// ------------------------------------------------------------------------------------------------
+// Number() of a header token, and stateNum
+// ------------------------------------------------------------------------------------------------
+
+pub 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 - 87) as i64; }
+ if (b >= 65 and b <= 70) { return (b - 55) as i64; }
+ return -1;
+}
+
+/// JavaScript's Number(token) for a token with no white space. A form outside the claimed domain sets
+/// NUMFLAG = 5 (see the header).
+pub fn js_number(s: []const u8, lo: usize, hi: usize) -> f64 {
+ var i : usize = lo;
+ var neg : bool = false;
+ if (i >= hi) { return 0.0; }
+ if (hi - lo >= 3 and s[lo] == 48 and (s[lo + 1] == 120 or s[lo + 1] == 88 or s[lo + 1] == 111 or s[lo + 1] == 79 or s[lo + 1] == 98 or s[lo + 1] == 66)) {
+ var radix : f64 = 16.0;
+ if (s[lo + 1] == 111 or s[lo + 1] == 79) { radix = 8.0; }
+ if (s[lo + 1] == 98 or s[lo + 1] == 66) { radix = 2.0; }
+ var v : f64 = 0.0;
+ var k : usize = lo + 2;
+ while (k < hi) {
+ const d : i64 = hex_val(s[k]);
+ if (d < 0 or (d as f64) >= radix) { return nan(); }
+ v = v * radix + (d as f64);
+ k = k + 1;
+ }
+ if (hi - lo - 2 > 13 and radix == 16.0) { NUMFLAG = 5; }
+ if (hi - lo - 2 > 17 and radix == 8.0) { NUMFLAG = 5; }
+ if (hi - lo - 2 > 52 and radix == 2.0) { NUMFLAG = 5; }
+ return v;
+ }
+ if (s[i] == 43 or s[i] == 45) {
+ neg = s[i] == 45;
+ i = i + 1;
+ }
+ if (match_in(s, i, hi, "Infinity") and hi - i == 8) {
+ if (neg) { return 0.0 - inf(); }
+ return inf();
+ }
+ var mant : f64 = 0.0;
+ var sig : u32 = 0;
+ var any : bool = false;
+ var lead : bool = true;
+ var frac : i64 = 0;
+ while (i < hi and is_digit(s[i])) {
+ any = true;
+ if (s[i] != 48 or !lead) {
+ lead = false;
+ sig = sig + 1;
+ }
+ mant = mant * 10.0 + ((s[i] - 48) as f64);
+ i = i + 1;
+ }
+ if (i < hi and s[i] == 46) {
+ i = i + 1;
+ while (i < hi and is_digit(s[i])) {
+ any = true;
+ if (s[i] != 48 or !lead) {
+ lead = false;
+ sig = sig + 1;
+ }
+ mant = mant * 10.0 + ((s[i] - 48) as f64);
+ frac = frac + 1;
+ i = i + 1;
+ }
+ }
+ if (!any) { return nan(); }
+ var e10 : i64 = 0;
+ if (i < hi and (s[i] == 101 or s[i] == 69)) {
+ i = i + 1;
+ var eneg : bool = false;
+ if (i < hi and (s[i] == 43 or s[i] == 45)) {
+ eneg = s[i] == 45;
+ i = i + 1;
+ }
+ const d0 : usize = i;
+ var ev : i64 = 0;
+ while (i < hi and is_digit(s[i])) {
+ if (ev < 100000) { ev = ev * 10 + ((s[i] - 48) as i64); }
+ i = i + 1;
+ }
+ if (i == d0) { return nan(); }
+ e10 = ev;
+ if (eneg) { e10 = 0 - ev; }
+ }
+ if (i != hi) { return nan(); }
+ var result : f64 = 0.0;
+ if (mant != 0.0) {
+ if (sig > 15) { NUMFLAG = 5; }
+ const k10 : i64 = e10 - frac;
+ var p10 : f64 = 1.0;
+ var q : i64 = 0;
+ var ak : i64 = k10;
+ if (k10 < 0) { ak = 0 - k10; }
+ if (ak > 22) { NUMFLAG = 5; }
+ while (q < ak and q < 22) {
+ p10 = p10 * 10.0;
+ q = q + 1;
+ }
+ if (k10 >= 0) { result = mant * p10; } else { result = mant / p10; }
+ }
+ if (neg) { return -result; }
+ return result;
+}
+
+/// stateNum: the number of a name of the form s, or -1 (JavaScript's NaN) when it has another shape.
+pub fn state_num(s: []const u8, lo: usize, hi: usize) -> i64 {
+ if (hi - lo < 2 or s[lo] != 115) { return -1; }
+ var v : i64 = 0;
+ var i : usize = lo + 1;
+ while (i < hi) {
+ if (!is_digit(s[i])) { return -1; }
+ v = v * 10 + ((s[i] - 48) as i64);
+ if (v > 1000000000000000) {
+ NUMFLAG = 5;
+ return 1000000000000000;
+ }
+ i = i + 1;
+ }
+ return v;
+}
+
+// ------------------------------------------------------------------------------------------------
+// sniff
+// ------------------------------------------------------------------------------------------------
+
+pub fn is_nl(b: u8) -> bool {
+ return b == 10 or b == 13;
+}
+
+/// After the end of the second token of a row: (\s+[01\-]*)?\s*$ with $ at a line end.
+pub fn tail_ok(s: []const u8, e2: usize) -> bool {
+ var j : usize = e2;
+ var go : bool = true;
+ while (go) {
+ if (j >= s.len) { return true; }
+ if (is_nl(s[j])) { return true; }
+ if (is_ws(s[j])) { j = j + 1; } else { go = false; }
+ }
+ if (j == e2) { return false; }
+ var k : usize = j;
+ while (k < s.len and (s[k] == 48 or s[k] == 49 or s[k] == 45)) { k = k + 1; }
+ if (k < s.len and !is_ws(s[k])) { return false; }
+ var m : usize = k;
+ go = true;
+ while (go) {
+ if (m >= s.len) { return true; }
+ if (is_nl(s[m])) { return true; }
+ if (is_ws(s[m])) { m = m + 1; } else { go = false; }
+ }
+ return false;
+}
+
+/// /^\s*[01\-]*\s+\S+\s+\S+(\s+[01\-]*)?\s*$/m anywhere in the text.
+pub fn kiss_row_anywhere(s: []const u8) -> bool {
+ var p : usize = 0;
+ while (p <= s.len) {
+ if (p == 0 or is_nl(s[p - 1])) {
+ const q0 : usize = skip_ws(s, p, s.len);
+ if (q0 < s.len) {
+ const e0 : usize = tok_end(s, q0, s.len);
+ var all01 : bool = true;
+ var c : usize = q0;
+ while (c < e0) {
+ if (s[c] != 48 and s[c] != 49 and s[c] != 45) { all01 = false; }
+ c = c + 1;
+ }
+ if (all01) {
+ const p1 : usize = skip_ws(s, e0, s.len);
+ if (p1 > e0 and p1 < s.len) {
+ const e1 : usize = tok_end(s, p1, s.len);
+ const p2 : usize = skip_ws(s, e1, s.len);
+ if (p2 > e1 and p2 < s.len) {
+ const e2 : usize = tok_end(s, p2, s.len);
+ if (tail_ok(s, e2)) { return true; }
+ }
+ }
+ }
+ if (q0 > p) {
+ const p2 : usize = skip_ws(s, e0, s.len);
+ if (p2 > e0 and p2 < s.len) {
+ const e2 : usize = tok_end(s, p2, s.len);
+ if (tail_ok(s, e2)) { return true; }
+ }
+ }
+ }
+ }
+ p = p + 1;
+ }
+ return false;
+}
+
+/// /^\s*\.(i|s|p|r)\s+\S+/m anywhere in the text.
+pub fn kiss_head_anywhere(s: []const u8) -> bool {
+ var q : usize = 0;
+ while (q + 2 < s.len) {
+ if (s[q] == 46 and (s[q + 1] == 105 or s[q + 1] == 115 or s[q + 1] == 112 or s[q + 1] == 114) and is_ws(s[q + 2])) {
+ const after : usize = skip_ws(s, q + 2, s.len);
+ if (after < s.len) {
+ var b : usize = q;
+ while (b > 0 and is_ws(s[b - 1])) { b = b - 1; }
+ var ok : bool = b == 0;
+ var j : usize = b;
+ while (j < q and !ok) {
+ if (is_nl(s[j])) { ok = true; }
+ j = j + 1;
+ }
+ if (ok) { return true; }
+ }
+ }
+ q = q + 1;
+ }
+ return false;
+}
+
+/// sniff: 1 a yosys log, 2 a KISS2 file, 0 neither.
+pub fn sniff(text: []const u8) -> u8 {
+ if (has_sub(text, "Executing FSM_DETECT pass") or has_sub(text, "Information on FSM ") or has_sub(text, "FSM state register")) { return 1; }
+ if (kiss_head_anywhere(text) and kiss_row_anywhere(text)) { return 2; }
+ return 0;
+}
+
+// ------------------------------------------------------------------------------------------------
+// the result stream
+// ------------------------------------------------------------------------------------------------
+// res (u32 x 32): [0] status, [1] ok, [2] kind (0 none, 1 log, 2 kiss2), [3] error id, [4] error line
+// offset, [5] its length, [6] version offset (4294967295 none), [7] its length, [8] detectRan (0, 1;
+// 2 for a KISS2 file), [9] fsms, [10] found, [11] rejected, [12] notes, [13] words written to out,
+// [16] events used (internal), [17] nslots (internal).
+// out (u32), in this order:
+// found 2 words each: offset, length
+// rejected offset, length, number of reasons, then 2 words per reason
+// notes 5 words each: id, a0, a1, a2, 0 -- kiss2_s and kiss2_p: the header value (offset, length)
+// and the count; no_reset_named: the name (offset, length); no_info: found, fsms; width_in:
+// nums[0] holds the number
+// fsms a 16-word header per FSM, then its states (4 words each), rows (6 words each), input
+// entries (3 words each), output entries (3 words each):
+// header [0] id offset, [1] id length (id offset 4294967295: the id is "kiss2"), [2] module offset
+// (4294967295: null), [3] its length, [4] reg offset (4294967295: null), [5] its length,
+// [6] reset (4294967295: -1), [7] encoding offset (4294967295: null), [8] its length,
+// [9] states, [10] rows, [11] inputs array length (4294967295: null), [12] input entries,
+// [13] outputs array length (4294967295: null), [14] output entries, [15] 1 when nBits is null
+// state name offset (4294967295: the name is "s" and the index), name length, code offset
+// (4294967295: null), code length
+// row from, to, in offset, in length, out offset, out length
+// entry index, offset, length (an index below the array length that has no entry is a hole)
+// nums (f64): [0] the nIn of a width_in note; then 3 per FSM: nIn, nOut, nBits.
+// Offsets are in text.
+
+pub fn ow(out: []u32, res: []u32, v: u32) {
+ const at : usize = res[13] as usize;
+ if (at < out.len) {
+ out[at] = v;
+ } else {
+ if (res[0] == 0) { res[0] = 1; }
+ }
+ res[13] = (at + 1) as u32;
+}
+
+/// Write an event of 10 words; returns its index.
+pub fn ev(work: []u32, res: []u32, tag: u32, a1: u32, a2: u32, a3: u32, a4: u32, a5: u32, a6: u32, a7: u32, a8: u32, a9: u32) -> usize {
+ const at : usize = res[16] as usize;
+ if ((at + 1) * EV > work.len) {
+ if (res[0] == 0) { res[0] = 2; }
+ return at;
+ }
+ work[at * EV] = tag;
+ work[at * EV + 1] = a1;
+ work[at * EV + 2] = a2;
+ work[at * EV + 3] = a3;
+ work[at * EV + 4] = a4;
+ work[at * EV + 5] = a5;
+ work[at * EV + 6] = a6;
+ work[at * EV + 7] = a7;
+ work[at * EV + 8] = a8;
+ work[at * EV + 9] = a9;
+ res[16] = (at + 1) as u32;
+ return at;
+}
+
+pub fn clear_res(res: []u32, nums: []f64) {
+ var k : usize = 0;
+ while (k < 32) {
+ res[k] = 0;
+ k = k + 1;
+ }
+ res[6] = NONE;
+ res[8] = 2;
+ nums[0] = 0.0;
+ NUMFLAG = 0;
+}
+
+// ------------------------------------------------------------------------------------------------
+// KISS2
+// ------------------------------------------------------------------------------------------------
+
+/// Is the byte allowed in a KISS2 input or output pattern?
+pub fn pat_ok(s: []const u8, lo: usize, hi: usize) -> bool {
+ var i : usize = lo;
+ while (i < hi) {
+ if (s[i] != 48 and s[i] != 49 and s[i] != 45) { return false; }
+ i = i + 1;
+ }
+ return true;
+}
+
+/// Index of the name s[lo .. hi] in the NAME events (or a new one).
+pub fn name_index(text: []const u8, work: []u32, res: []u32, lo: usize, hi: usize) -> u32 {
+ var idx : u32 = 0;
+ var k : usize = 0;
+ const n : usize = res[16] as usize;
+ while (k < n) {
+ if (work[k * EV] == EV_NAME) {
+ const o : usize = work[k * EV + 1] as usize;
+ if (span_eq(text, o, o + (work[k * EV + 2] as usize), lo, hi)) { return work[k * EV + 3]; }
+ idx = idx + 1;
+ }
+ k = k + 1;
+ }
+ ev(work, res, EV_NAME, lo as u32, (hi - lo) as u32, idx, 0, 0, 0, 0, 0, 0);
+ return idx;
+}
+
+/// parseKiss2: see "the result stream". tmp needs 4 words per state name.
+pub fn parse_kiss2(text: []const u8, ls: []u32, nlines: usize, work: []u32, tmp: []u32, out: []u32, nums: []f64, res: []u32) {
+ // head values: i o s p r, as (offset, length), offset NONE when undefined
+ var hoff : [5]u32 = [4294967295; 5];
+ var hlen : [5]u32 = [0; 5];
+ var ilb_gen : u32 = 0;
+ var ob_gen : u32 = 0;
+ var nrows : u32 = 0;
+ var stopped : bool = false;
+ var li : usize = 0;
+ while (li < nlines and !stopped and res[0] == 0) {
+ const rlo : usize = ls[li * 2] as usize;
+ const rhi : usize = ls[li * 2 + 1] as usize;
+ li = li + 1;
+ // strip the comment: the first # with no CR after it in the line
+ var last_cr : i64 = -1;
+ var c : usize = rlo;
+ while (c < rhi) {
+ if (text[c] == 13) { last_cr = c as i64; }
+ c = c + 1;
+ }
+ var cut : usize = rhi;
+ var found_hash : bool = false;
+ c = rlo;
+ while (c < rhi and !found_hash) {
+ if (text[c] == 35 and (c as i64) > last_cr) {
+ cut = c;
+ found_hash = true;
+ }
+ c = c + 1;
+ }
+ var a : usize = rlo;
+ var b : usize = cut;
+ while (a < b and is_ws(text[a])) { a = a + 1; }
+ while (b > a and is_ws(text[b - 1])) { b = b - 1; }
+ if (b == a) { continue; }
+ if (text[a] == 46) {
+ const kend : usize = tok_end(text, a, b);
+ if (match_in(text, a, kend, ".e") and kend - a == 2) { stopped = true; continue; }
+ if (match_in(text, a, kend, ".end") and kend - a == 4) { stopped = true; continue; }
+ if (match_in(text, a, kend, ".ilb") and kend - a == 4) {
+ ilb_gen = ilb_gen + 1;
+ var p : usize = skip_ws(text, kend, b);
+ while (p < b) {
+ const e : usize = tok_end(text, p, b);
+ ev(work, res, EV_ILB, ilb_gen, p as u32, (e - p) as u32, 0, 0, 0, 0, 0, 0);
+ p = skip_ws(text, e, b);
+ }
+ continue;
+ }
+ if (match_in(text, a, kend, ".ob") and kend - a == 3) {
+ ob_gen = ob_gen + 1;
+ var p : usize = skip_ws(text, kend, b);
+ while (p < b) {
+ const e : usize = tok_end(text, p, b);
+ ev(work, res, EV_OB, ob_gen, p as u32, (e - p) as u32, 0, 0, 0, 0, 0, 0);
+ p = skip_ws(text, e, b);
+ }
+ continue;
+ }
+ var key : i64 = -1;
+ if (kend - a == 2) {
+ if (text[a + 1] == 105) { key = 0; }
+ if (text[a + 1] == 111) { key = 1; }
+ if (text[a + 1] == 115) { key = 2; }
+ if (text[a + 1] == 112) { key = 3; }
+ if (text[a + 1] == 114) { key = 4; }
+ }
+ if (key >= 0) {
+ const p : usize = skip_ws(text, kend, b);
+ if (p < b) {
+ const e : usize = tok_end(text, p, b);
+ hoff[key as usize] = p as u32;
+ hlen[key as usize] = (e - p) as u32;
+ } else {
+ hoff[key as usize] = NONE;
+ hlen[key as usize] = 0;
+ }
+ }
+ continue;
+ }
+ // a transition row
+ var ts : [5]usize = [0; 5];
+ var te : [5]usize = [0; 5];
+ var ntok : usize = 0;
+ var p : usize = a;
+ while (p < b) {
+ const e : usize = tok_end(text, p, b);
+ if (ntok < 5) {
+ ts[ntok] = p;
+ te[ntok] = e;
+ }
+ ntok = ntok + 1;
+ p = skip_ws(text, e, b);
+ }
+ var nin_zero : bool = false;
+ var nout_zero : bool = false;
+ if (hoff[0] != NONE) { nin_zero = js_number(text, hoff[0] as usize, (hoff[0] + hlen[0]) as usize) == 0.0; }
+ if (hoff[1] != NONE) { nout_zero = js_number(text, hoff[1] as usize, (hoff[1] + hlen[1]) as usize) == 0.0; }
+ var io : usize = 0;
+ var il : usize = 0;
+ var fo : usize = 0;
+ var fl : usize = 0;
+ var to : usize = 0;
+ var tl : usize = 0;
+ var oo : usize = 0;
+ var ol : usize = 0;
+ var good : bool = true;
+ if (ntok == 4) {
+ io = ts[0]; il = te[0] - ts[0]; fo = ts[1]; fl = te[1] - ts[1]; to = ts[2]; tl = te[2] - ts[2]; oo = ts[3]; ol = te[3] - ts[3];
+ } else {
+ if (ntok == 3 and nin_zero) {
+ fo = ts[0]; fl = te[0] - ts[0]; to = ts[1]; tl = te[1] - ts[1]; oo = ts[2]; ol = te[2] - ts[2];
+ } else {
+ if (ntok == 3 and nout_zero) {
+ io = ts[0]; il = te[0] - ts[0]; fo = ts[1]; fl = te[1] - ts[1]; to = ts[2]; tl = te[2] - ts[2];
+ } else {
+ good = false;
+ }
+ }
+ }
+ if (good) {
+ if (!pat_ok(text, io, io + il) or !pat_ok(text, oo, oo + ol)) { good = false; }
+ }
+ if (!good) {
+ res[1] = 0;
+ res[3] = E_KISS2ROW;
+ res[4] = rlo as u32;
+ // raw.trim()
+ var ra : usize = rlo;
+ var rb : usize = rhi;
+ while (ra < rb and is_ws(text[ra])) { ra = ra + 1; }
+ while (rb > ra and is_ws(text[rb - 1])) { rb = rb - 1; }
+ res[4] = ra as u32;
+ res[5] = (rb - ra) as u32;
+ return;
+ }
+ const fi : u32 = name_index(text, work, res, fo, fo + fl);
+ const ti : u32 = name_index(text, work, res, to, to + tl);
+ ev(work, res, EV_ROW2, fi, ti, io as u32, il as u32, oo as u32, ol as u32, 0, 0, 0);
+ nrows = nrows + 1;
+ }
+ if (res[0] != 0) { return; }
+ // names
+ var nnames : usize = 0;
+ var k : usize = 0;
+ const nev : usize = res[16] as usize;
+ while (k < nev) {
+ if (work[k * EV] == EV_NAME) { nnames = nnames + 1; }
+ k = k + 1;
+ }
+ if (hoff[0] == NONE and hoff[2] == NONE and nrows == 0) {
+ res[1] = 0;
+ res[3] = E_UNKNOWN;
+ return;
+ }
+ if (tmp.len < 4 * (nnames + 1)) {
+ res[0] = 2;
+ return;
+ }
+ // tmp[0 .. n]: order (new position -> old index); tmp[n .. 2n]: remap (old -> new); numbers in tmp[2n ..]
+ var all_num : bool = nnames > 0;
+ k = 0;
+ var ni : usize = 0;
+ while (k < nev) {
+ if (work[k * EV] == EV_NAME) {
+ const o : usize = work[k * EV + 1] as usize;
+ const sn : i64 = state_num(text, o, o + (work[k * EV + 2] as usize));
+ if (sn < 0) { all_num = false; }
+ tmp[ni] = ni as u32;
+ ni = ni + 1;
+ }
+ k = k + 1;
+ }
+ if (all_num) {
+ // stable insertion sort of the order by state number (the numbers are kept in two words each below)
+ var i : usize = 1;
+ while (i < nnames) {
+ const cur : u32 = tmp[i];
+ const cv : i64 = state_num_of(text, work, nev, cur as usize);
+ var j : usize = i;
+ while (j > 0 and state_num_of(text, work, nev, tmp[j - 1] as usize) > cv) {
+ tmp[j] = tmp[j - 1];
+ j = j - 1;
+ }
+ tmp[j] = cur;
+ i = i + 1;
+ }
+ }
+ var q : usize = 0;
+ while (q < nnames) {
+ tmp[nnames + (tmp[q] as usize)] = q as u32;
+ q = q + 1;
+ }
+ // results
+ res[1] = 1;
+ res[2] = 2;
+ res[9] = 1;
+ // nIn, nOut
+ var nin : f64 = 0.0;
+ var nout : f64 = 0.0;
+ var first_in : u32 = NONE;
+ var first_out : u32 = NONE;
+ k = 0;
+ while (k < nev) {
+ if (work[k * EV] == EV_ROW2) {
+ first_in = work[k * EV + 4];
+ first_out = work[k * EV + 6];
+ k = nev;
+ } else {
+ k = k + 1;
+ }
+ }
+ if (first_in == NONE) { first_in = 0; first_out = 0; }
+ if (hoff[0] != NONE) { nin = js_number(text, hoff[0] as usize, (hoff[0] + hlen[0]) as usize); } else { nin = first_in as f64; }
+ if (hoff[1] != NONE) { nout = js_number(text, hoff[1] as usize, (hoff[1] + hlen[1]) as usize); } else { nout = first_out as f64; }
+ // reset
+ var reset : u32 = NONE;
+ var has_reset_name : bool = hoff[4] != NONE;
+ if (has_reset_name) {
+ q = 0;
+ while (q < nnames) {
+ const oi : usize = tmp[q] as usize;
+ const nm : usize = name_event(work, nev, oi);
+ const no : usize = work[nm * EV + 1] as usize;
+ if (span_eq(text, no, no + (work[nm * EV + 2] as usize), hoff[4] as usize, (hoff[4] + hlen[4]) as usize) and reset == NONE) { reset = q as u32; }
+ q = q + 1;
+ }
+ }
+ // notes
+ var notes : u32 = 0;
+ var out_notes_at : usize = 0;
+ // found, rejected: none
+ res[10] = 0;
+ res[11] = 0;
+ // count the notes first to write the count, then write them
+ // kiss2_s
+ if (hoff[2] != NONE and js_number(text, hoff[2] as usize, (hoff[2] + hlen[2]) as usize) != (nnames as f64)) {
+ ow(out, res, N_KISS2_S);
+ ow(out, res, hoff[2]);
+ ow(out, res, hlen[2]);
+ ow(out, res, nnames as u32);
+ ow(out, res, 0);
+ notes = notes + 1;
+ }
+ if (hoff[3] != NONE and js_number(text, hoff[3] as usize, (hoff[3] + hlen[3]) as usize) != (nrows as f64)) {
+ ow(out, res, N_KISS2_P);
+ ow(out, res, hoff[3]);
+ ow(out, res, hlen[3]);
+ ow(out, res, nrows);
+ ow(out, res, 0);
+ notes = notes + 1;
+ }
+ if (has_reset_name and reset == NONE) {
+ ow(out, res, N_NO_RESET_NAMED);
+ ow(out, res, hoff[4]);
+ ow(out, res, hlen[4]);
+ ow(out, res, 0);
+ ow(out, res, 0);
+ notes = notes + 1;
+ }
+ if (!has_reset_name) {
+ ow(out, res, N_NO_RESET);
+ ow(out, res, 0);
+ ow(out, res, 0);
+ ow(out, res, 0);
+ ow(out, res, 0);
+ notes = notes + 1;
+ }
+ var width_bad : bool = false;
+ k = 0;
+ while (k < nev) {
+ if (work[k * EV] == EV_ROW2) {
+ if ((work[k * EV + 4] as f64) != nin) { width_bad = true; }
+ }
+ k = k + 1;
+ }
+ if (width_bad) {
+ ow(out, res, N_WIDTH_IN);
+ ow(out, res, 0);
+ ow(out, res, 0);
+ ow(out, res, 0);
+ ow(out, res, 0);
+ nums[0] = nin;
+ notes = notes + 1;
+ }
+ res[12] = notes;
+ // the FSM block
+ var in_len : u32 = NONE;
+ var n_in_ent : u32 = 0;
+ var out_len : u32 = NONE;
+ var n_out_ent : u32 = 0;
+ if (ilb_gen > 0) {
+ var cnt : u32 = 0;
+ k = 0;
+ while (k < nev) {
+ if (work[k * EV] == EV_ILB and work[k * EV + 1] == ilb_gen) { cnt = cnt + 1; }
+ k = k + 1;
+ }
+ if ((cnt as f64) == nin) {
+ in_len = cnt;
+ n_in_ent = cnt;
+ }
+ }
+ if (ob_gen > 0) {
+ var cnt : u32 = 0;
+ k = 0;
+ while (k < nev) {
+ if (work[k * EV] == EV_OB and work[k * EV + 1] == ob_gen) { cnt = cnt + 1; }
+ k = k + 1;
+ }
+ if ((cnt as f64) == nout) {
+ out_len = cnt;
+ n_out_ent = cnt;
+ }
+ }
+ ow(out, res, NONE);
+ ow(out, res, 0);
+ ow(out, res, NONE);
+ ow(out, res, 0);
+ ow(out, res, NONE);
+ ow(out, res, 0);
+ ow(out, res, reset);
+ ow(out, res, NONE);
+ ow(out, res, 0);
+ ow(out, res, nnames as u32);
+ ow(out, res, nrows);
+ ow(out, res, in_len);
+ ow(out, res, n_in_ent);
+ ow(out, res, out_len);
+ ow(out, res, n_out_ent);
+ ow(out, res, 1);
+ nums[1] = nin;
+ nums[2] = nout;
+ nums[3] = 0.0;
+ q = 0;
+ while (q < nnames) {
+ const oi : usize = tmp[q] as usize;
+ const nm : usize = name_event(work, nev, oi);
+ ow(out, res, work[nm * EV + 1]);
+ ow(out, res, work[nm * EV + 2]);
+ ow(out, res, NONE);
+ ow(out, res, 0);
+ q = q + 1;
+ }
+ k = 0;
+ while (k < nev) {
+ if (work[k * EV] == EV_ROW2) {
+ ow(out, res, tmp[nnames + (work[k * EV + 1] as usize)]);
+ ow(out, res, tmp[nnames + (work[k * EV + 2] as usize)]);
+ ow(out, res, work[k * EV + 3]);
+ ow(out, res, work[k * EV + 4]);
+ ow(out, res, work[k * EV + 5]);
+ ow(out, res, work[k * EV + 6]);
+ }
+ k = k + 1;
+ }
+ if (n_in_ent > 0) {
+ // inputs = ilb.slice().reverse(): entry i is the (len - 1 - i)-th token
+ var seen : u32 = 0;
+ k = 0;
+ while (k < nev) {
+ if (work[k * EV] == EV_ILB and work[k * EV + 1] == ilb_gen) {
+ ow(out, res, in_len - 1 - seen);
+ ow(out, res, work[k * EV + 2]);
+ ow(out, res, work[k * EV + 3]);
+ seen = seen + 1;
+ }
+ k = k + 1;
+ }
+ }
+ if (n_out_ent > 0) {
+ var seen : u32 = 0;
+ k = 0;
+ while (k < nev) {
+ if (work[k * EV] == EV_OB and work[k * EV + 1] == ob_gen) {
+ ow(out, res, out_len - 1 - seen);
+ ow(out, res, work[k * EV + 2]);
+ ow(out, res, work[k * EV + 3]);
+ seen = seen + 1;
+ }
+ k = k + 1;
+ }
+ }
+}
+
+/// The event index of the idx-th NAME event.
+pub fn name_event(work: []const u32, nev: usize, idx: usize) -> usize {
+ var seen : usize = 0;
+ var k : usize = 0;
+ while (k < nev) {
+ if (work[k * EV] == EV_NAME) {
+ if (seen == idx) { return k; }
+ seen = seen + 1;
+ }
+ k = k + 1;
+ }
+ return 0;
+}
+
+pub fn state_num_of(text: []const u8, work: []const u32, nev: usize, idx: usize) -> i64 {
+ const e : usize = name_event(work, nev, idx);
+ const o : usize = work[e * EV + 1] as usize;
+ return state_num(text, o, o + (work[e * EV + 2] as usize));
+}
+
+// ------------------------------------------------------------------------------------------------
+// the yosys log
+// ------------------------------------------------------------------------------------------------
+
+/// ^Found FSM state register (\S+?)\.?\s*$ -> M[0..1] the name.
+pub fn m_found(s: []const u8, lo: usize, hi: usize) -> bool {
+ const p : i64 = eat(s, lo, hi, "Found FSM state register ");
+ if (p < 0) { return false; }
+ const a : usize = p as usize;
+ if (a >= hi or is_ws(s[a])) { return false; }
+ const e : usize = tok_end(s, a, hi);
+ if (skip_ws(s, e, hi) != hi) { return false; }
+ var x : usize = e;
+ if (e - a >= 2 and s[e - 1] == 46) { x = e - 1; }
+ M[0] = a;
+ M[1] = x;
+ return true;
+}
+
+/// ^Not marking (\S+) as FSM state register:\s*$ -> M[0..1] the register.
+pub fn m_not_marking(s: []const u8, lo: usize, hi: usize) -> bool {
+ const p : i64 = eat(s, lo, hi, "Not marking ");
+ if (p < 0) { return false; }
+ const a : usize = p as usize;
+ const e : usize = tok_end(s, a, hi);
+ if (e == a) { return false; }
+ const q : i64 = eat(s, e, hi, " as FSM state register:");
+ if (q < 0) { return false; }
+ if (skip_ws(s, q as usize, hi) != hi) { return false; }
+ M[0] = a;
+ M[1] = e;
+ return true;
+}
+
+/// ^Recoding FSM `([^']+)' from module `([^']+)' using `([^']+)' encoding: -> M[0..1] the FSM, M[2..3] the encoding.
+pub fn m_recoding(s: []const u8, lo: usize, hi: usize) -> bool {
+ const p : i64 = eat(s, lo, hi, "Recoding FSM `");
+ if (p < 0) { return false; }
+ const a0 : usize = p as usize;
+ const a1 : usize = run_noquote(s, a0, hi);
+ if (a1 == a0) { return false; }
+ const q : i64 = eat(s, a1, hi, "' from module `");
+ if (q < 0) { return false; }
+ const b0 : usize = q as usize;
+ const b1 : usize = run_noquote(s, b0, hi);
+ if (b1 == b0) { return false; }
+ const r : i64 = eat(s, b1, hi, "' using `");
+ if (r < 0) { return false; }
+ const c0 : usize = r as usize;
+ const c1 : usize = run_noquote(s, c0, hi);
+ if (c1 == c0) { return false; }
+ if (eat(s, c1, hi, "' encoding:") < 0) { return false; }
+ M[0] = a0;
+ M[1] = a1;
+ M[2] = c0;
+ M[3] = c1;
+ return true;
+}
+
+/// mapping auto encoding to `([^`']+)'?`? anywhere -> M[0..1].
+pub fn m_mapping(s: []const u8, lo: usize, hi: usize) -> bool {
+ var p : usize = lo;
+ while (p < hi) {
+ const q : i64 = eat(s, p, hi, "mapping auto encoding to `");
+ if (q >= 0) {
+ const a : usize = q as usize;
+ var r : usize = a;
+ while (r < hi and s[r] != 96 and s[r] != 39) { r = r + 1; }
+ if (r > a) {
+ M[0] = a;
+ M[1] = r;
+ return true;
+ }
+ }
+ p = p + 1;
+ }
+ return false;
+}
+
+/// ^\s*Information on FSM (\S+) \((.+)\):\s*$ -> M[0..1] the id, M[2..3] the register.
+pub fn m_info(s: []const u8, lo: usize, hi: usize) -> bool {
+ const p0 : usize = skip_ws(s, lo, hi);
+ const p : i64 = eat(s, p0, hi, "Information on FSM ");
+ if (p < 0) { return false; }
+ const a : usize = p as usize;
+ const e : usize = tok_end(s, a, hi);
+ if (e == a) { return false; }
+ const q : i64 = eat(s, e, hi, " (");
+ if (q < 0) { return false; }
+ const c : usize = q as usize;
+ const re : usize = rstrip(s, lo, hi);
+ if (re < c + 3) { return false; }
+ if (s[re - 2] != 41 or s[re - 1] != 58) { return false; }
+ if (!no_cr(s, c, re - 2)) { return false; }
+ M[0] = a;
+ M[1] = e;
+ M[2] = c;
+ M[3] = re - 2;
+ return true;
+}
+
+/// ^\s*
");
+ n = text(dst, n, SAY_ROUND_NOTE);
+ n = raw(dst, n, "
");
+ n = storage_html(dst, n, d, r);
+ n = raw(dst, n, "");
+ n = limits_html(dst, n, d, r);
+ n = raw(dst, n, "");
+ if (!ok(n)) { return 0; }
+ return n;
+}
+
+/// The splicer follows only the names a module itself mentions, not the names the imported functions
+/// mention. This anchor mentions every helper the writer, the JSON reader and the number rules call.
+/// Never called.
+pub fn imports_anchor(o: []u8, s: []const u8) -> u32 {
+ var n : u32 = 0;
+ n = n + (ok(0) as u32);
+ n = n + OVER;
+ n = n + put(o, 0, 0);
+ n = n + raw(o, 0, s);
+ n = n + put_escaped(o, 0, 0);
+ n = n + text(o, 0, s);
+ n = n + num(o, 0, 0);
+ n = n + attr(o, 0, s, s);
+ n = n + fill(o, 0, s, s, s, s, 0);
+ n = n + fill_named(o, 0, s, s, s, s, s, s, s, s, s, s, s, s, s);
+ n = n + dec_to(o, 0);
+ n = n + (name_is(s, 0, 0, s) as u32);
+ n = n + (is_word_byte(0) as u32);
+ n = n + (H_LT as u32);
+ n = n + (H_GT as u32);
+ n = n + (H_AMP as u32);
+ n = n + (H_QUOTE as u32);
+ n = n + (H_APOS as u32);
+ n = n + (H_SPACE as u32);
+ n = n + (H_SLASH as u32);
+ n = n + (H_EQ as u32);
+ n = n + ((NO_SPAN < 0) as u32);
+ n = n + ((SPAN > 0) as u32);
+ n = n + (J_NONE as u32);
+ n = n + (J_OBJECT as u32);
+ n = n + (J_ARRAY as u32);
+ n = n + (J_STRING as u32);
+ n = n + (J_NUMBER as u32);
+ n = n + (J_TRUE as u32);
+ n = n + (J_FALSE as u32);
+ n = n + (J_NULL as u32);
+ n = n + (jspan(0, 0) as u32);
+ n = n + (span_lo(0) as u32);
+ n = n + (span_hi(0) as u32);
+ n = n + (skip_ws(s, 0) as u32);
+ n = n + (string_end(s, 0) as u32);
+ n = n + (value_end(s, 0) as u32);
+ n = n + (root(s) as u32);
+ n = n + (kind(s, 0) as u32);
+ n = n + (key_is(s, 0, 0, s) as u32);
+ n = n + (obj_get(s, 0, s) as u32);
+ n = n + arr_len(s, 0);
+ n = n + (arr_at(s, 0, 0) as u32);
+ n = n + hex4(s, 0);
+ n = n + escape_at(s, 0, 0);
+ n = n + cp_utf8_len(0);
+ n = n + (cp_utf8_byte(0, 0) as u32);
+ n = n + str_len(s, 0);
+ n = n + (str_byte(s, 0, 0) as u32);
+ n = n + JN_OVER;
+ n = n + JN_CAP;
+ n = n + ((JN_TWO52 > 0.0) as u32);
+ n = n + ((JN_TWO53 > 0.0) as u32);
+ n = n + jn_put(o, 0, 0);
+ n = n + jn_raw(o, 0, s);
+ n = n + jn_digits(o, 0);
+ n = n + jn_mul(o, 0, 0);
+ n = n + jn_mul2(o, 0, 0);
+ n = n + jn_mul5(o, 0, 0);
+ n = n + jn_shift(o, 0, 0);
+ n = n + (jn_cmp(s, s, 0) as u32);
+ n = n + (jn_expo(0.0) as u32);
+ n = n + (jn_mant(0.0) as u32);
+ n = n + ((jn_make(0, 0) > 0.0) as u32);
+ n = n + jn_exact(o, 0, 0);
+ n = n + jn_fscale(0);
+ n = n + (js_is_nan(0.0) as u32);
+ n = n + ((js_nan(0.0) > 0.0) as u32);
+ n = n + ((js_inf(1.0) > 0.0) as u32);
+ n = n + ((js_floor(0.0) > 0.0) as u32);
+ n = n + ((js_ceil(0.0) > 0.0) as u32);
+ n = n + ((js_max(0.0, 0.0) > 0.0) as u32);
+ n = n + ((js_min(0.0, 0.0) > 0.0) as u32);
+ n = n + ((js_round(0.0) > 0.0) as u32);
+ n = n + (jn_shortest(o, 1.0) as u32);
+ n = n + jn_copy(o, s, 0);
+ n = n + jn_put_run(o, 0, s, 0, 0);
+ n = n + jn_layout(o, 0, s, 0, 0);
+ n = n + js_str_num(o, 0, 0.0);
+ n = n + js_to_fixed(o, 0, 0.0, 0);
+ n = n + js_locale_num(o, 0, 0.0);
+ n = n + (jn_num_ok(s, 0, 0) as u32);
+ n = n + (jn_cmp_text(s, 0, 0, 0, 0) as u32);
+ n = n + ((js_parse_num(s, 0, 0) > 0.0) as u32);
+ n = n + ((jn_dec_value(s, 0, 0) > 0.0) as u32);
+ n = n + (jn_abs(0) as u32);
+ n = n + js_str_u64(o, 0, 0);
+ n = n + jv_str_to(o, 0, s, 0);
+ n = n + jv_text_str(o, 0, s, 0);
+ n = n + ((jv_numval(s, 0) > 0.0) as u32);
+ n = n + jv_str(o, 0, s, 0);
+ n = n + jv_text(o, 0, s, 0);
+ n = n + (jv_is(s, 0, s) as u32);
+ n = n + (jv_truthy(s, 0) as u32);
+ n = n + (jv_is_ws(0) as u32);
+ n = n + ((jv_number_of_text(s, 0) > 0.0) as u32);
+ n = n + ((jv_num(s, 0) > 0.0) as u32);
+ n = n + jv_vput_str(o, 0, s, s);
+ n = n + jv_vput_f64(o, 0, s, 0.0);
+ n = n + jv_vput_val(o, 0, s, s, 0);
+ n = n + jv_vput_defined(o, 0, s, s, 0);
+ n = n + jv_vput_fixed(o, 0, s, 0.0, 0);
+ n = n + jv_kid(o, 0, s, 0);
+ n = n + jv_find(s, 0, s);
+ n = n + jv_fill(o, 0, s, s, 0, true);
+ n = n + (jv_string_ok(s, 0) as u32);
+ n = n + (jv_number_ok(s, 0) as u32);
+ n = n + (jv_lit_ok(s, 0, s) as u32);
+ n = n + (jv_valid(s) as u32);
+ n = n + jv_ws_at(s, 0, 0);
+ n = n + jv_ws_before(s, 0, 0);
+ n = n + jv_digit_value(0);
+ return n;
+}
diff --git a/specs/widgets/t27_vs_nvfp4.t27 b/specs/widgets/t27_vs_nvfp4.t27
new file mode 100644
index 0000000000..6676031eec
--- /dev/null
+++ b/specs/widgets/t27_vs_nvfp4.t27
@@ -0,0 +1,943 @@
+// SPDX-License-Identifier: Apache-2.0
+; specs/widgets/t27-vs-nvfp4.t27 -- a scoreboard of 4-bit-class weight formats on one small model
+; Source of truth for public/widgets/t27-vs-nvfp4/ (gHashTag/trinity, apps/website). The page is
+; written by scripts/widget-pages-from-spec.mjs from this file; tool.js holds no English of its
+; own and reads every word below from window.T27_WIDGET, and every number from data.json.
+; ASCII only (L3), English only (LANG-EN).
+
+; WHAT IT SHOWS: the second ternary storage search of trinity-fpga, run once on 2026-09-27. Test
+; perplexity (lower is better) of SmolLM2-135M on wikitext-2 with its weights stored in BASE
+; (unquantised), E2M2, T27_N, NVFP4, MXPLUS and MXFP4; then, for each comparator C, the permille
+; ratio r = 1000 * ppl(T27_N) / ppl(C) against the two pre-registered bars, BEAT 990 and TIE 1010.
+; A dev / test switch shows both splits; dev is where T27_N was chosen among three T27 cells.
+
+; DATA PROVENANCE: scripts/widget-data/t27-vs-nvfp4.mjs reads the runner's own output
+; (research/block/ternary_storage_search_v2.json), compiles the pre-registration
+; (specs/numeric/ternary_storage_search_v2.t27) with the site's wasm t27 compiler for the ladder,
+; the window count and the storage per 32 weights, cross-checks every perplexity and ratio the
+; verdict report prints, and writes public/widgets/t27-vs-nvfp4/data.json. The K_ facts below are
+; that file's numbers as integers (perplexity times 10000, ratios times 10); `--check` fails if one
+; of them disagrees with the run, runs the tests below on them, and runs a negative control that
+; moves T27's perplexity 2 permille and must make the tests fail.
+
+; WHAT IT DOES NOT CLAIM: everything in SAY_LIMITS, taken from the verdict's "What it does not say".
+; The only claim that holds is "better than NVFP4 on a ternary substrate". E2M2 was not on the
+; pre-registered ladder: its ratio is computed here from the two test perplexities, same formula.
+; phi^2 + 1/phi^2 = 3 | TRINITY
+
+module widget_t27_vs_nvfp4;
+
+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::fill_named;
+use widgets::html_out::dec_to;
+use widgets::html_out::name_is;
+use widgets::html_out::is_word_byte;
+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::json_scan::NO_SPAN;
+use widgets::json_scan::SPAN;
+use widgets::json_scan::J_NONE;
+use widgets::json_scan::J_OBJECT;
+use widgets::json_scan::J_ARRAY;
+use widgets::json_scan::J_STRING;
+use widgets::json_scan::J_NUMBER;
+use widgets::json_scan::J_TRUE;
+use widgets::json_scan::J_FALSE;
+use widgets::json_scan::J_NULL;
+use widgets::json_scan::jspan;
+use widgets::json_scan::span_lo;
+use widgets::json_scan::span_hi;
+use widgets::json_scan::skip_ws;
+use widgets::json_scan::string_end;
+use widgets::json_scan::value_end;
+use widgets::json_scan::root;
+use widgets::json_scan::kind;
+use widgets::json_scan::key_is;
+use widgets::json_scan::obj_get;
+use widgets::json_scan::arr_len;
+use widgets::json_scan::arr_at;
+use widgets::json_scan::hex4;
+use widgets::json_scan::escape_at;
+use widgets::json_scan::cp_utf8_len;
+use widgets::json_scan::cp_utf8_byte;
+use widgets::json_scan::str_len;
+use widgets::json_scan::str_byte;
+use widgets::js_num::JN_OVER;
+use widgets::js_num::JN_CAP;
+use widgets::js_num::JN_TWO52;
+use widgets::js_num::JN_TWO53;
+use widgets::js_num::jn_put;
+use widgets::js_num::jn_raw;
+use widgets::js_num::jn_digits;
+use widgets::js_num::jn_mul;
+use widgets::js_num::jn_mul2;
+use widgets::js_num::jn_mul5;
+use widgets::js_num::jn_shift;
+use widgets::js_num::jn_cmp;
+use widgets::js_num::jn_expo;
+use widgets::js_num::jn_mant;
+use widgets::js_num::jn_make;
+use widgets::js_num::jn_exact;
+use widgets::js_num::jn_fscale;
+use widgets::js_num::js_is_nan;
+use widgets::js_num::js_nan;
+use widgets::js_num::js_inf;
+use widgets::js_num::js_floor;
+use widgets::js_num::js_ceil;
+use widgets::js_num::js_max;
+use widgets::js_num::js_min;
+use widgets::js_num::js_round;
+use widgets::js_num::jn_shortest;
+use widgets::js_num::jn_copy;
+use widgets::js_num::jn_put_run;
+use widgets::js_num::jn_layout;
+use widgets::js_num::js_str_num;
+use widgets::js_num::js_to_fixed;
+use widgets::js_num::js_locale_num;
+use widgets::js_num::jn_num_ok;
+use widgets::js_num::jn_cmp_text;
+use widgets::js_num::js_parse_num;
+use widgets::js_num::jn_dec_value;
+use widgets::js_num::jn_abs;
+use widgets::js_num::js_str_u64;
+use widgets::js_val::jv_str_to;
+use widgets::js_val::jv_text_str;
+use widgets::js_val::jv_numval;
+use widgets::js_val::jv_str;
+use widgets::js_val::jv_text;
+use widgets::js_val::jv_is;
+use widgets::js_val::jv_truthy;
+use widgets::js_val::jv_is_ws;
+use widgets::js_val::jv_number_of_text;
+use widgets::js_val::jv_num;
+use widgets::js_val::jv_vput_str;
+use widgets::js_val::jv_vput_f64;
+use widgets::js_val::jv_vput_val;
+use widgets::js_val::jv_vput_defined;
+use widgets::js_val::jv_vput_fixed;
+use widgets::js_val::jv_kid;
+use widgets::js_val::jv_find;
+use widgets::js_val::jv_fill;
+use widgets::js_val::jv_string_ok;
+use widgets::js_val::jv_number_ok;
+use widgets::js_val::jv_lit_ok;
+use widgets::js_val::jv_valid;
+
+pub const KIND : str = "widget-tool";
+pub const ID : str = "t27-vs-nvfp4";
+pub const TITLE : str = "T27 vs NVFP4: six weight formats on one small language model";
+pub const DESCRIPTION : str = "Perplexity of six weight formats on SmolLM2-135M and the pre-registered ladder: T27 better than NVFP4, MX+ and MXFP4 on a trit substrate, worse than E2M2. Every limit listed.";
+pub const IMAGE_ALT : str = "Scoreboard on black: SmolLM2-135M wikitext-2 test perplexity, lower is better: BASE 14.49, E2M2 15.81, T27_N 16.29 in gold, NVFP4 17.47, MX+ 17.98, MXFP4 21.94. T27 to NVFP4 is 932 per mille against a 990 bar, MX+ 906, MXFP4 743; against E2M2 it is 1030, a loss. One model, weights only, no confidence interval.";
+pub const HOOK : str = "One claim with its limits: T27 beats NVFP4 on a ternary substrate, on one small model, and loses to a 5-bit E2M2. See every number and every caveat.";
+pub const CATEGORY : str = "fpga";
+pub const DATA_SOURCES : [4]str = ["trinity-fpga research/block/ternary_storage_search_v2.json @ d31d8523cd (sha256 969e7b196f6afb9a...): the runner's output, perplexity per arm and split, permille ratios and verdicts; python 3.12.12, torch 2.11.0, transformers 4.57.6, run 2026-09-27 11:44:02Z to 11:52:11Z, not retried", "trinity-fpga specs/numeric/ternary_storage_search_v2.t27 @ 49a84ade57 (sha256 88fcf517296ecc77..., the sha the run recorded): the pre-registration, compiled with the wasm t27 compiler for BEAT_PERMILLE, TIE_PERMILLE, NWIN, SEQLEN and the trits and bits per 32 weights", "trinity-fpga research/block/TERNARY_STORAGE_V2_VERDICT_2026-09-27.md @ d31d8523cd: the report; its perplexity and ratio tables are cross-checked against the JSON, and every limit on this page is from its section What it does not say", "node scripts/widget-data/t27-vs-nvfp4.mjs writes data.json; --check re-reads the three files and fails if data.json or a K_ fact here disagrees; card.png is drawn from data.json by scripts/widget-data/t27-vs-nvfp4-card.py"];
+pub const READS_LOCAL_FILES : bool = false;
+pub const SENDS_NOTHING : bool = true;
+
+; --- The run, as integers (scripts/widget-data/t27-vs-nvfp4.mjs --check re-reads every one) -----
+pub const K_DATA : str = "data.json";
+; The arms in page order. K_PPL_*_E4 is perplexity times 10000, rounded; 0 means not run on that split.
+pub const K_ARMS : [6]str = ["BASE", "E2M2", "T27_N", "NVFP4", "MXPLUS", "MXFP4"];
+pub const K_BASE_ARM : u8 = 0;
+pub const K_T27_ARM : u8 = 2;
+pub const K_NVFP4_ARM : u8 = 3;
+pub const K_PPL_TEST_E4 : [6]u32 = [144869, 158083, 162892, 174697, 179780, 219353];
+pub const K_PPL_DEV_E4 : [6]u32 = [0, 162063, 166880, 180213, 183802, 226285];
+; The comparators of T27_N and the arm each one is. r is per mille times 10, rounded.
+pub const K_COMPARATORS : [4]str = ["E2M2", "NVFP4", "MXPLUS", "MXFP4"];
+pub const K_COMPARATOR_ARM : [4]u8 = [1, 3, 4, 5];
+pub const K_R_TEST_X10 : [4]u32 = [10304, 9324, 9061, 7426];
+pub const K_R_DEV_X10 : [4]u32 = [10297, 9260, 9079, 7375];
+; The pre-registered ladder: r <= BEAT is T27 better, r <= TIE a tie, above that C better.
+pub const K_BEAT_PERMILLE : u16 = 990;
+pub const K_TIE_PERMILLE : u16 = 1010;
+pub const K_WINDOWS : u8 = 40;
+pub const K_SEQLEN : u16 = 2048;
+; Storage of a 32-weight block, per arm (0 for BASE). Trits are the pre-registered axis; T27's
+; bits are a dense packing of its trits.
+pub const K_TRITS_32 : [6]u16 = [0, 134, 101, 106, 106, 102];
+pub const K_BITS_32 : [6]u16 = [0, 168, 161, 144, 141, 136];
+; Excess test perplexity over BASE, times 100, and the share of NVFP4's excess T27 removes.
+pub const K_EXCESS_T27_E2 : u16 = 180;
+pub const K_EXCESS_NVFP4_E2 : u16 = 298;
+pub const K_GAP_CLOSED_PCT : u8 = 40;
+
+; --- Every word on the screen -------------------------------------------------------------------
+; Templates take {0}, {1}, ... in order, or {name} for a named value from data.json.
+pub const SAY_ARM_NAMES : [6]str = ["BASE", "E2M2", "T27_N", "NVFP4", "MX+", "MXFP4"];
+pub const SAY_ARM_NOTES : [6]str = ["unquantised", "5-bit cell, no trits", "3 trits, 27 levels, one power-of-two scale per 32", "E2M1 per 16, E4M3 scale; our reimplementation", "our reimplementation of MX+", "E2M1 per 32, E8M0 scale (rule B)"];
+pub const SAY_CLAIM_LABEL : str = "The one claim that holds";
+pub const SAY_CLAIM : str = "Better than NVFP4 on a ternary substrate.";
+pub const SAY_CLAIM_LINE : str = "Test perplexity {0} against {1} (r = {2} per mille, bar {3}), stored in {4} trits per 32 weights against {5}. It loses to E2M2.";
+pub const SAY_SPLIT_LABEL : str = "Split";
+pub const SAY_SPLITS : [2]str = ["test", "dev"];
+pub const SAY_SPLIT_LINES : [2]str = ["Test: each arm run once, after T27_N was chosen on dev.", "Dev: where T27_N was chosen among three T27 cells. Not the result; shown so the choice is visible."];
+pub const SAY_PPL_TITLE : str = "Perplexity, lower is better";
+pub const SAY_PPL_LINE : str = "SmolLM2-135M, wikitext-2 {split}, {windows} windows of {seqlen} tokens; weights quantised, activations not.";
+pub const SAY_TIMES_BASE : str = "x{0} BASE";
+pub const SAY_NOT_RUN : str = "not run on dev";
+pub const SAY_CAND_TITLE : str = "The three T27 cells on dev";
+pub const SAY_CAND_LINE : str = "The lowest dev perplexity went to test; the other two were never run on test.";
+pub const SAY_CHOSEN : str = "chosen";
+pub const SAY_R_TITLE : str = "T27_N against each format, per mille";
+pub const SAY_R_LINE : str = "r = 1000 x ppl(T27_N) / ppl(C). Bars fixed before the run: r up to {0} is T27 better, up to {1} a tie, above that C better.";
+pub const SAY_R_AXIS : str = "r, per mille ({0} = equal)";
+pub const SAY_BEAT_MARK : str = "BEAT {0}";
+pub const SAY_TIE_MARK : str = "TIE {0}";
+pub const SAY_VERDICT_IDS : [3]str = ["beat", "tie", "lose"];
+pub const SAY_VERDICT_TEXT : [3]str = ["T27 better", "tie", "T27 loses"];
+pub const SAY_OFF_LADDER : str = "not on the pre-registered ladder; computed here from the two perplexities";
+pub const SAY_GAP : str = "T27_N removes {0}% of NVFP4's excess over the unquantised model: excess test perplexity {1} against {2}.";
+pub const SAY_STORE_TITLE : str = "Storage per 32 weights";
+pub const SAY_STORE_HEAD : [3]str = ["format", "trits", "bits"];
+pub const SAY_STORE_LINE : str = "Trits are the pre-registered equal-storage axis. In bits, packed densely, T27_N costs {0} against NVFP4's {1}: {2}% more.";
+pub const SAY_NOT_TITLE : str = "What this does NOT show";
+pub const SAY_LIMITS : [8]str = ["One model and one corpus: SmolLM2-135M on wikitext-2. Weights only: nothing about activations or the KV cache, nothing about other models.", "No confidence interval. Each arm is one point on {windows} windows of {seqlen} tokens; a paired bootstrap was not pre-registered, so none is claimed. Large margins are judgement, not a test.", "NVFP4 and MX+ are our own reimplementations, from NVIDIA's published recipe and from the MX+ paper, not the authors' code. MXFP4 uses a ceil E8M0 scale (rule B).", "T27_N was chosen on dev, the lowest of {ncand} T27 cells there. Test was touched once, after the choice.", "Nothing about binary memory. Packed densely, T27_N costs {t27bits} bits per 32 weights against NVFP4's {nvbits}; E2M2 costs {e2m2bits} and beats T27_N. On a bit substrate this is not an equal-storage comparison.", "Nothing about the cost of a trit cell against a bit cell: nobody has measured it, and ReTern argues it is higher.", "The rulers reproduced the first attempt's numbers on the same machine; the numbers of the main branch's machine were not re-run.", "E2M2 was not on the pre-registered ladder; its ratio here uses the same formula on the two test perplexities."];
+pub const SAY_SOURCE : str = "Numbers from trinity-fpga {0} at {1}, ladder from {2} at {3}; read by scripts/widget-data/t27-vs-nvfp4.mjs. Run {4} to {5}, torch {6}, not retried.";
+pub const SAY_ERR_LOAD : str = "The data did not load.";
+pub const SAY_CHART_ALT : str = "Bars of {split} perplexity per weight format, and dots of T27_N's per-mille ratio to each format against the BEAT and TIE bars.";
+
+; --- Tests --------------------------------------------------------------------------------------
+test the_test_ratios_are_the_test_perplexities_within_one_permille {
+ assert K_R_TEST_X10[0] * K_PPL_TEST_E4[K_COMPARATOR_ARM[0]] <= 10000 * K_PPL_TEST_E4[K_T27_ARM] + 10 * K_PPL_TEST_E4[K_COMPARATOR_ARM[0]];
+ assert 10000 * K_PPL_TEST_E4[K_T27_ARM] <= K_R_TEST_X10[0] * K_PPL_TEST_E4[K_COMPARATOR_ARM[0]] + 10 * K_PPL_TEST_E4[K_COMPARATOR_ARM[0]];
+ assert K_R_TEST_X10[1] * K_PPL_TEST_E4[K_COMPARATOR_ARM[1]] <= 10000 * K_PPL_TEST_E4[K_T27_ARM] + 10 * K_PPL_TEST_E4[K_COMPARATOR_ARM[1]];
+ assert 10000 * K_PPL_TEST_E4[K_T27_ARM] <= K_R_TEST_X10[1] * K_PPL_TEST_E4[K_COMPARATOR_ARM[1]] + 10 * K_PPL_TEST_E4[K_COMPARATOR_ARM[1]];
+ assert K_R_TEST_X10[2] * K_PPL_TEST_E4[K_COMPARATOR_ARM[2]] <= 10000 * K_PPL_TEST_E4[K_T27_ARM] + 10 * K_PPL_TEST_E4[K_COMPARATOR_ARM[2]];
+ assert 10000 * K_PPL_TEST_E4[K_T27_ARM] <= K_R_TEST_X10[2] * K_PPL_TEST_E4[K_COMPARATOR_ARM[2]] + 10 * K_PPL_TEST_E4[K_COMPARATOR_ARM[2]];
+ assert K_R_TEST_X10[3] * K_PPL_TEST_E4[K_COMPARATOR_ARM[3]] <= 10000 * K_PPL_TEST_E4[K_T27_ARM] + 10 * K_PPL_TEST_E4[K_COMPARATOR_ARM[3]];
+ assert 10000 * K_PPL_TEST_E4[K_T27_ARM] <= K_R_TEST_X10[3] * K_PPL_TEST_E4[K_COMPARATOR_ARM[3]] + 10 * K_PPL_TEST_E4[K_COMPARATOR_ARM[3]];
+}
+
+test the_dev_ratios_are_the_dev_perplexities_within_one_permille {
+ assert K_R_DEV_X10[0] * K_PPL_DEV_E4[K_COMPARATOR_ARM[0]] <= 10000 * K_PPL_DEV_E4[K_T27_ARM] + 10 * K_PPL_DEV_E4[K_COMPARATOR_ARM[0]];
+ assert 10000 * K_PPL_DEV_E4[K_T27_ARM] <= K_R_DEV_X10[0] * K_PPL_DEV_E4[K_COMPARATOR_ARM[0]] + 10 * K_PPL_DEV_E4[K_COMPARATOR_ARM[0]];
+ assert K_R_DEV_X10[1] * K_PPL_DEV_E4[K_COMPARATOR_ARM[1]] <= 10000 * K_PPL_DEV_E4[K_T27_ARM] + 10 * K_PPL_DEV_E4[K_COMPARATOR_ARM[1]];
+ assert 10000 * K_PPL_DEV_E4[K_T27_ARM] <= K_R_DEV_X10[1] * K_PPL_DEV_E4[K_COMPARATOR_ARM[1]] + 10 * K_PPL_DEV_E4[K_COMPARATOR_ARM[1]];
+ assert K_R_DEV_X10[2] * K_PPL_DEV_E4[K_COMPARATOR_ARM[2]] <= 10000 * K_PPL_DEV_E4[K_T27_ARM] + 10 * K_PPL_DEV_E4[K_COMPARATOR_ARM[2]];
+ assert 10000 * K_PPL_DEV_E4[K_T27_ARM] <= K_R_DEV_X10[2] * K_PPL_DEV_E4[K_COMPARATOR_ARM[2]] + 10 * K_PPL_DEV_E4[K_COMPARATOR_ARM[2]];
+ assert K_R_DEV_X10[3] * K_PPL_DEV_E4[K_COMPARATOR_ARM[3]] <= 10000 * K_PPL_DEV_E4[K_T27_ARM] + 10 * K_PPL_DEV_E4[K_COMPARATOR_ARM[3]];
+ assert 10000 * K_PPL_DEV_E4[K_T27_ARM] <= K_R_DEV_X10[3] * K_PPL_DEV_E4[K_COMPARATOR_ARM[3]] + 10 * K_PPL_DEV_E4[K_COMPARATOR_ARM[3]];
+}
+
+test the_ladder_says_t27_beats_three_and_loses_to_e2m2 {
+ assert K_COMPARATORS[1] == "NVFP4";
+ assert K_R_TEST_X10[1] <= 10 * K_BEAT_PERMILLE;
+ assert K_R_TEST_X10[2] <= 10 * K_BEAT_PERMILLE;
+ assert K_R_TEST_X10[3] <= 10 * K_BEAT_PERMILLE;
+ assert K_COMPARATORS[0] == "E2M2";
+ assert K_R_TEST_X10[0] > 10 * K_TIE_PERMILLE;
+ assert K_R_DEV_X10[1] <= 10 * K_BEAT_PERMILLE;
+ assert K_R_DEV_X10[0] > 10 * K_TIE_PERMILLE;
+ assert K_BEAT_PERMILLE < K_TIE_PERMILLE;
+}
+
+test perplexity_orders_the_arms_as_the_page_lists_them {
+ assert K_PPL_TEST_E4[0] < K_PPL_TEST_E4[1];
+ assert K_PPL_TEST_E4[1] < K_PPL_TEST_E4[2];
+ assert K_PPL_TEST_E4[2] < K_PPL_TEST_E4[3];
+ assert K_PPL_TEST_E4[3] < K_PPL_TEST_E4[4];
+ assert K_PPL_TEST_E4[4] < K_PPL_TEST_E4[5];
+ assert K_PPL_DEV_E4[K_BASE_ARM] == 0;
+}
+
+test t27_removes_forty_percent_of_nvfp4s_excess {
+ assert K_PPL_TEST_E4[K_T27_ARM] - K_PPL_TEST_E4[K_BASE_ARM] + 50 >= 100 * K_EXCESS_T27_E2;
+ assert K_PPL_TEST_E4[K_T27_ARM] - K_PPL_TEST_E4[K_BASE_ARM] + 50 < 100 * K_EXCESS_T27_E2 + 100;
+ assert K_PPL_TEST_E4[K_NVFP4_ARM] - K_PPL_TEST_E4[K_BASE_ARM] + 50 >= 100 * K_EXCESS_NVFP4_E2;
+ assert K_PPL_TEST_E4[K_NVFP4_ARM] - K_PPL_TEST_E4[K_BASE_ARM] + 50 < 100 * K_EXCESS_NVFP4_E2 + 100;
+ assert K_GAP_CLOSED_PCT == (200 * (K_PPL_TEST_E4[K_NVFP4_ARM] - K_PPL_TEST_E4[K_T27_ARM]) + K_PPL_TEST_E4[K_NVFP4_ARM] - K_PPL_TEST_E4[K_BASE_ARM]) / (2 * (K_PPL_TEST_E4[K_NVFP4_ARM] - K_PPL_TEST_E4[K_BASE_ARM]));
+}
+
+test fewer_trits_than_nvfp4_but_more_bits {
+ assert K_TRITS_32[K_T27_ARM] < K_TRITS_32[K_NVFP4_ARM];
+ assert K_BITS_32[K_T27_ARM] > K_BITS_32[K_NVFP4_ARM];
+ assert K_BITS_32[1] > K_BITS_32[K_T27_ARM];
+ assert K_WINDOWS == 40;
+ assert K_SEQLEN == 2048;
+}
+
+test the_page_reads_nothing_of_yours_and_sends_nothing {
+ assert READS_LOCAL_FILES == false;
+ assert SENDS_NOTHING == true;
+ assert CATEGORY == "fpga";
+}
+
+
+; =================================================================================================
+; THE NATIVE PAGE (epic gHashTag/t27#10937, issue #10938, stage "page" of specs/t27f/widget_factory.t27)
+; Everything below replaces public/widgets/t27-vs-nvfp4/tool.js (192 lines of hand-written JavaScript).
+; The words and numbers above are unchanged. The host protocol is race_the_bee.t27's:
+; w_init(hash) the page opens: the empty frame tool.js draws before data.json arrives
+; w_data(i, bytes) file i of K_DATA has been fetched; w_data_failed(i) when it could not be
+; w_click(i) split button i was pressed (0 test, 1 dev)
+; w_render(dst) the page's HTML, length (0 on overflow)
+; Numbers are written the JavaScript way by js_num.t27 (toFixed on the exact value, String(x) the
+; shortest digits for the pct() widths) and values are read the way String() / Number() read a parsed
+; value by js_val.t27, so a null, a missing key or a string where a number is expected prints what
+; tool.js prints. NOT carried over: where tool.js throws a TypeError (an arm with id T27_STAR, NVFP4,
+; BASE or E2M2 missing, d.ratios.test without NVFP4, a missing protocol or run object) it leaves a
+; half-drawn page; this page draws the same frame with the missing values as undefined and compares
+; equal only where tool.js does not throw. The data file is cut at DATA_CAP bytes (the page then says
+; it could not be read).
+; MEASURED 2026-10-19: t27c 0.5.2 gen + a 5-line Zig export wrapper + zig build-exe wasm32-freestanding
+; ReleaseSmall give a 28660-byte wasm. The original page (tool.js) and this one ran in two happy-dom
+; windows against the real 3911-byte data.json and 120 variants of it, 1517 comparisons of tag,
+; attributes and text (the empty frame before the data arrives, with and without a click on dev, the
+; page after it, then the split buttons clicked in random order): arms with a null dev perplexity or
+; null storage, markup / quotes / accents / an emoji / null in names, an unknown arm or verdict word,
+; truthy and falsy chosen / ladder flags, empty candidate and ratio lists, ratios far from 1000 and
+; equal to it, bars moved / equal / strings / null / missing, odd windows and seqlen, perplexities that
+; are ties for toFixed, 1e-7, 1e22, negative, strings and booleans, odd gap and source fields, a
+; reordered and a longer arm list, a duplicated arm id, 60 variants with every number replaced by a
+; random one (17 digits, thousandths, binary fractions), 20 walks of 30 clicks, a 404 and a body that is
+; not JSON: 0 differences. A negative control (one node of the native DOM changed) is reported.
+; Not compared: a data file where tool.js throws (see above); the stylesheet is not loaded (happy-dom).
+
+pub const DATA_CAP : u32 = 32768;
+pub var DATA : [32768]u8 = [0; 32768];
+pub var DATA_LEN : u32 = 0;
+pub var DATA_STATE : u8 = 0;
+pub var SPLIT : u32 = 0;
+pub var CAND_DRAWN : bool = false;
+
+pub fn w_init(h: []const u8) -> u32 {
+ DATA_STATE = 0;
+ DATA_LEN = 0;
+ SPLIT = 0;
+ CAND_DRAWN = false;
+ return 0;
+}
+
+pub fn w_data(i: u32, s: []const u8) -> u32 {
+ if (s.len > (DATA_CAP as usize)) {
+ DATA_STATE = 2;
+ return 0;
+ }
+ var k : usize = 0;
+ while (k < s.len) {
+ DATA[k] = s[k];
+ k = k + 1;
+ }
+ DATA_LEN = s.len as u32;
+ DATA_STATE = 1;
+ if (!jv_valid(DATA[0..(DATA_LEN as usize)])) { DATA_STATE = 2; }
+ if (DATA_STATE == 1 and SPLIT == 1) { CAND_DRAWN = true; }
+ return 0;
+}
+
+pub fn w_data_failed(i: u32) -> u32 {
+ DATA_STATE = 2;
+ return 0;
+}
+
+pub fn w_click(i: u32) -> u32 {
+ if (i < 2) {
+ SPLIT = i;
+ if (DATA_STATE == 1 and SPLIT == 1) { CAND_DRAWN = true; }
+ }
+ return 0;
+}
+
+fn split_key() -> []const u8 {
+ if (SPLIT == 1) { return "dev"; }
+ return "test";
+}
+
+fn is_null(d: []const u8, sp: i64) -> bool {
+ return kind(d, sp) == J_NULL;
+}
+
+/// The arm whose id is `id`, NO_SPAN when none (tool.js would throw).
+fn find_arm(d: []const u8, arms: i64, id: []const u8) -> i64 {
+ const cnt : u32 = arr_len(d, arms);
+ var i : u32 = 0;
+ while (i < cnt) {
+ const a : i64 = arr_at(d, arms, i);
+ if (jv_is(d, obj_get(d, a, "id"), id)) { return a; }
+ i = i + 1;
+ }
+ return NO_SPAN;
+}
+
+/// K_ARMS.indexOf(name): the index or -1.
+fn arm_idx(d: []const u8, sp: i64) -> i32 {
+ var i : usize = 0;
+ while (i < K_ARMS.len) {
+ if (jv_is(d, sp, K_ARMS[i])) { return i as i32; }
+ i = i + 1;
+ }
+ return 0 - 1;
+}
+
+/// armName(name): the word for a known arm, else the data's own text (a child node).
+fn arm_name(dst: []u8, n0: u32, d: []const u8, sp: i64) -> u32 {
+ const ix : i32 = arm_idx(d, sp);
+ if (ix >= 0) { return text(dst, n0, SAY_ARM_NAMES[ix as usize]); }
+ return jv_kid(dst, n0, d, sp);
+}
+
+fn arm_note(dst: []u8, n0: u32, d: []const u8, sp: i64) -> u32 {
+ const ix : i32 = arm_idx(d, sp);
+ if (ix >= 0) { return text(dst, n0, SAY_ARM_NOTES[ix as usize]); }
+ return n0;
+}
+
+fn pct_val(x: f64, lo: f64, hi: f64) -> f64 {
+ return js_max(0.0, js_min(100.0, ((x - lo) / (hi - lo)) * 100.0));
+}
+
+/// `${pct(x, lo, hi)}%`
+fn put_pct(dst: []u8, n0: u32, x: f64, lo: f64, hi: f64) -> u32 {
+ const n : u32 = js_str_num(dst, n0, pct_val(x, lo, hi));
+ return put(dst, n, 37);
+}
+
+fn fixed_text(dst: []u8, n0: u32, x: f64, digits: u32) -> u32 {
+ return js_to_fixed(dst, n0, x, digits);
+}
+
+fn num_at(d: []const u8, sp: i64, key: []const u8) -> f64 {
+ return jv_num(d, obj_get(d, sp, key));
+}
+
+fn claim_html(dst: []u8, n0: u32, d: []const u8, r: i64) -> u32 {
+ const arms : i64 = obj_get(d, r, "arms");
+ const t27 : i64 = find_arm(d, arms, "T27_STAR");
+ const nv : i64 = find_arm(d, arms, "NVFP4");
+ const tests : i64 = obj_get(d, obj_get(d, r, "ratios"), "test");
+ var rat : i64 = NO_SPAN;
+ const cnt : u32 = arr_len(d, tests);
+ var i : u32 = 0;
+ while (i < cnt) {
+ const q : i64 = arr_at(d, tests, i);
+ if (rat == NO_SPAN and jv_is(d, obj_get(d, q, "c"), "NVFP4")) { rat = q; }
+ i = i + 1;
+ }
+ var n : u32 = raw(dst, n0, "");
+ n = text(dst, n, SAY_CLAIM_LABEL);
+ n = raw(dst, n, "
");
+ n = text(dst, n, SAY_CLAIM);
+ n = raw(dst, n, "
");
+}
+
+fn ppl_html(dst: []u8, n0: u32, d: []const u8, r: i64) -> u32 {
+ const key : []const u8 = split_key();
+ const arms : i64 = obj_get(d, r, "arms");
+ const cnt : u32 = arr_len(d, arms);
+ const P : i64 = obj_get(d, r, "protocol");
+ var hi : f64 = 0.0 - js_inf(0.0);
+ var i : u32 = 0;
+ while (i < cnt) {
+ const v : i64 = obj_get(d, arr_at(d, arms, i), key);
+ if (!is_null(d, v)) { hi = js_max(hi, jv_num(d, v)); }
+ i = i + 1;
+ }
+ const base : i64 = find_arm(d, arms, "BASE");
+ const bv : i64 = obj_get(d, base, key);
+ var n : u32 = raw(dst, n0, "
");
+ n = text(dst, n, SAY_PPL_TITLE);
+ n = raw(dst, n, "
");
+ var vb : [512]u8 = [0; 512];
+ var vn : u32 = jv_vput_str(vb[0..], 0, "split", SAY_SPLITS[SPLIT as usize]);
+ vn = jv_vput_defined(vb[0..], vn, "windows", d, obj_get(d, P, "windows"));
+ vn = jv_vput_defined(vb[0..], vn, "seqlen", d, obj_get(d, P, "seqlen"));
+ n = jv_fill(dst, n, SAY_PPL_LINE, vb[0..], vn, false);
+ n = raw(dst, n, "
");
+ i = 0;
+ while (i < cnt) {
+ const a : i64 = arr_at(d, arms, i);
+ const v : i64 = obj_get(d, a, key);
+ const id : i64 = obj_get(d, a, "id");
+ const nm : i64 = obj_get(d, a, "name");
+ n = raw(dst, n, "
");
+ n = arm_name(dst, n, d, nm);
+ n = raw(dst, n, "");
+ n = arm_note(dst, n, d, nm);
+ n = raw(dst, n, "
");
+ if (is_null(d, v)) {
+ n = raw(dst, n, "");
+ n = text(dst, n, SAY_NOT_RUN);
+ n = raw(dst, n, "");
+ } else {
+ n = raw(dst, n, "");
+ }
+ n = raw(dst, n, "
");
+ if (!is_null(d, v)) {
+ n = raw(dst, n, "");
+ n = fixed_text(dst, n, jv_num(d, v), 4);
+ n = raw(dst, n, "");
+ }
+ n = raw(dst, n, "");
+ if (!is_null(d, v) and !is_null(d, bv)) {
+ var rb : [512]u8 = [0; 512];
+ var rn : u32 = jv_vput_fixed(rb[0..], 0, "0", jv_num(d, v) / jv_num(d, bv), 3);
+ n = jv_fill(dst, n, SAY_TIMES_BASE, rb[0..], rn, false);
+ }
+ n = raw(dst, n, "
");
+ i = i + 1;
+ }
+ return raw(dst, n, "
");
+}
+
+fn cand_html(dst: []u8, n0: u32, d: []const u8, r: i64) -> u32 {
+ const cs : i64 = obj_get(d, r, "dev_candidates");
+ const cnt : u32 = arr_len(d, cs);
+ var hi : f64 = 0.0 - js_inf(0.0);
+ var i : u32 = 0;
+ while (i < cnt) {
+ hi = js_max(hi, num_at(d, arr_at(d, cs, i), "dev"));
+ i = i + 1;
+ }
+ var n : u32 = raw(dst, n0, "
");
+ n = text(dst, n, SAY_CAND_TITLE);
+ n = raw(dst, n, "
");
+ n = text(dst, n, SAY_CAND_LINE);
+ n = raw(dst, n, "
");
+ i = 0;
+ while (i < cnt) {
+ const c : i64 = arr_at(d, cs, i);
+ const ch : bool = jv_truthy(d, obj_get(d, c, "chosen"));
+ n = raw(dst, n, "
");
+ n = jv_kid(dst, n, d, obj_get(d, c, "name"));
+ n = raw(dst, n, "");
+ if (ch) { n = text(dst, n, SAY_CHOSEN); }
+ n = raw(dst, n, "
");
+ n = fixed_text(dst, n, num_at(d, c, "dev"), 4);
+ n = raw(dst, n, "
");
+ i = i + 1;
+ }
+ return raw(dst, n, "
");
+}
+
+/// marks(withText): the 1000 line and the two pre-registered bars.
+fn marks_html(dst: []u8, n0: u32, d: []const u8, P: i64, lo: f64, hi: f64, with_text: bool) -> u32 {
+ const beat : i64 = obj_get(d, P, "beat_permille");
+ const tie : i64 = obj_get(d, P, "tie_permille");
+ var n : u32 = raw(dst, n0, "");
+ var vb : [128]u8 = [0; 128];
+ if (with_text) {
+ n = raw(dst, n, "");
+ var vn : u32 = jv_vput_defined(vb[0..], 0, "0", d, beat);
+ n = jv_fill(dst, n, SAY_BEAT_MARK, vb[0..], vn, false);
+ n = raw(dst, n, "");
+ }
+ n = raw(dst, n, "");
+ if (with_text) {
+ n = raw(dst, n, "");
+ var vn2 : u32 = jv_vput_defined(vb[0..], 0, "0", d, tie);
+ n = jv_fill(dst, n, SAY_TIE_MARK, vb[0..], vn2, false);
+ n = raw(dst, n, "");
+ }
+ return raw(dst, n, "");
+}
+
+fn verdict_idx(d: []const u8, sp: i64) -> i32 {
+ var i : usize = 0;
+ while (i < SAY_VERDICT_IDS.len) {
+ if (jv_is(d, sp, SAY_VERDICT_IDS[i])) { return i as i32; }
+ i = i + 1;
+ }
+ return 0 - 1;
+}
+
+fn ratios_html(dst: []u8, n0: u32, d: []const u8, r: i64) -> u32 {
+ const P : i64 = obj_get(d, r, "protocol");
+ const list : i64 = obj_get(d, obj_get(d, r, "ratios"), split_key());
+ const arms : i64 = obj_get(d, r, "arms");
+ const cnt : u32 = arr_len(d, list);
+ const beat : i64 = obj_get(d, P, "beat_permille");
+ const tie : i64 = obj_get(d, P, "tie_permille");
+ var mn : f64 = js_min(js_min(jv_num(d, beat), jv_num(d, tie)), 1000.0);
+ var mx : f64 = js_max(js_max(jv_num(d, beat), jv_num(d, tie)), 1000.0);
+ var i : u32 = 0;
+ while (i < cnt) {
+ const rv : f64 = num_at(d, arr_at(d, list, i), "r");
+ mn = js_min(mn, rv);
+ mx = js_max(mx, rv);
+ i = i + 1;
+ }
+ const step : f64 = 50.0;
+ const lo : f64 = js_floor((mn - 20.0) / step) * step;
+ const hi : f64 = js_ceil((mx + 20.0) / step) * step;
+ var n : u32 = raw(dst, n0, "
");
+ n = text(dst, n, SAY_R_TITLE);
+ n = raw(dst, n, "
");
+ var vb : [256]u8 = [0; 256];
+ var vn : u32 = jv_vput_defined(vb[0..], 0, "0", d, beat);
+ vn = jv_vput_defined(vb[0..], vn, "1", d, tie);
+ n = jv_fill(dst, n, SAY_R_LINE, vb[0..], vn, false);
+ n = raw(dst, n, "
");
+ n = marks_html(dst, n, d, P, lo, hi, true);
+ n = raw(dst, n, "
");
+ i = 0;
+ while (i < cnt) {
+ const q : i64 = arr_at(d, list, i);
+ const qr : f64 = num_at(d, q, "r");
+ const a : f64 = js_min(qr, 1000.0);
+ const b : f64 = js_max(qr, 1000.0);
+ const vd : i64 = obj_get(d, q, "verdict");
+ n = raw(dst, n, "
");
+ // d.arms.find((x) => x.id === q.c).name
+ var found : i64 = NO_SPAN;
+ const ac : u32 = arr_len(d, arms);
+ var j : u32 = 0;
+ while (j < ac) {
+ const x : i64 = arr_at(d, arms, j);
+ if (found == NO_SPAN and ids_equal(d, obj_get(d, x, "id"), obj_get(d, q, "c"))) { found = x; }
+ j = j + 1;
+ }
+ n = arm_name(dst, n, d, obj_get(d, found, "name"));
+ n = raw(dst, n, "");
+ if (!jv_truthy(d, obj_get(d, q, "ladder"))) { n = text(dst, n, SAY_OFF_LADDER); }
+ n = raw(dst, n, "
");
+ n = marks_html(dst, n, d, P, lo, hi, false);
+ n = raw(dst, n, "
");
+ n = fixed_text(dst, n, qr, 1);
+ n = raw(dst, n, "");
+ const vi : i32 = verdict_idx(d, vd);
+ if (vi >= 0) { n = text(dst, n, SAY_VERDICT_TEXT[vi as usize]); } else { n = jv_kid(dst, n, d, vd); }
+ n = raw(dst, n, "
");
+ i = i + 1;
+ }
+ n = raw(dst, n, "
");
+ var x : f64 = lo;
+ var guard : u32 = 0;
+ while (x <= hi and guard < 2000) {
+ n = raw(dst, n, "");
+ n = js_str_num(dst, n, x);
+ n = raw(dst, n, "");
+ x = x + step;
+ guard = guard + 1;
+ }
+ n = raw(dst, n, "
");
+ var ab : [32]u8 = [0; 32];
+ const avn : u32 = jv_vput_str(ab[0..], 0, "0", "1000");
+ n = jv_fill(dst, n, SAY_R_AXIS, ab[0..], avn, false);
+ n = raw(dst, n, "
");
+ const g : i64 = obj_get(d, r, "gap");
+ var gb : [256]u8 = [0; 256];
+ var gn : u32 = jv_vput_fixed(gb[0..], 0, "0", num_at(d, g, "closed") * 100.0, 0);
+ gn = jv_vput_fixed(gb[0..], gn, "1", num_at(d, g, "t27_excess"), 2);
+ gn = jv_vput_fixed(gb[0..], gn, "2", num_at(d, g, "nvfp4_excess"), 2);
+ n = jv_fill(dst, n, SAY_GAP, gb[0..], gn, false);
+ return raw(dst, n, "
");
+}
+
+/// a === b for two string values (both must be strings: tool.js compares ids with ===).
+fn ids_equal(d: []const u8, a: i64, b: i64) -> bool {
+ if (kind(d, a) != J_STRING or kind(d, b) != J_STRING) { return false; }
+ const l : u32 = str_len(d, a);
+ if (l != str_len(d, b)) { return false; }
+ var k : u32 = 0;
+ while (k < l) {
+ if (str_byte(d, a, k) != str_byte(d, b, k)) { return false; }
+ k = k + 1;
+ }
+ return true;
+}
+
+fn store_html(dst: []u8, n0: u32, d: []const u8, r: i64) -> u32 {
+ const arms : i64 = obj_get(d, r, "arms");
+ const cnt : u32 = arr_len(d, arms);
+ var n : u32 = raw(dst, n0, "
");
+ n = text(dst, n, SAY_STORE_TITLE);
+ n = raw(dst, n, "
");
+ var h : usize = 0;
+ while (h < SAY_STORE_HEAD.len) {
+ n = raw(dst, n, "
");
+ n = text(dst, n, SAY_STORE_HEAD[h]);
+ n = raw(dst, n, "
");
+ h = h + 1;
+ }
+ n = raw(dst, n, "
");
+ var i : u32 = 0;
+ while (i < cnt) {
+ const a : i64 = arr_at(d, arms, i);
+ if (!is_null(d, obj_get(d, a, "trits32"))) {
+ n = raw(dst, n, "
");
+ n = arm_name(dst, n, d, obj_get(d, a, "name"));
+ n = raw(dst, n, "
");
+ n = jv_text(dst, n, d, obj_get(d, a, "trits32"));
+ n = raw(dst, n, "
");
+ n = jv_text(dst, n, d, obj_get(d, a, "bits32"));
+ n = raw(dst, n, "
");
+ n = text(dst, n, SAY_ERR_LOAD);
+ n = raw(dst, n, "
");
+ if (!ok(n)) { return 0; }
+ return n;
+ }
+ const loaded : bool = DATA_STATE == 1;
+ var d : []const u8 = DATA[0..0];
+ var r : i64 = NO_SPAN;
+ if (loaded) {
+ d = DATA[0..(DATA_LEN as usize)];
+ r = root(d);
+ }
+ var lab : u32 = 0;
+ if (loaded) { lab = SPLIT; }
+ n = raw(dst, n, "
");
+ if (loaded) { n = claim_html(dst, n, d, r); }
+ n = raw(dst, n, "
");
+ n = text(dst, n, SAY_SPLIT_LABEL);
+ n = raw(dst, n, "
");
+ var i : u32 = 0;
+ while (i < 2) {
+ n = raw(dst, n, "");
+ i = i + 1;
+ }
+ n = raw(dst, n, "
");
+ if (loaded) { n = text(dst, n, SAY_SPLIT_LINES[SPLIT as usize]); }
+ n = raw(dst, n, "
");
+ if (loaded) { n = ppl_html(dst, n, d, r); }
+ n = raw(dst, n, "");
+ if (loaded and CAND_DRAWN) { n = cand_html(dst, n, d, r); }
+ n = raw(dst, n, "");
+ if (loaded) { n = ratios_html(dst, n, d, r); }
+ n = raw(dst, n, "");
+ if (loaded) { n = store_html(dst, n, d, r); }
+ n = raw(dst, n, "");
+ if (loaded) { n = limits_html(dst, n, d, r); }
+ n = raw(dst, n, "
");
+ if (loaded) { n = source_text(dst, n, d, r); }
+ n = raw(dst, n, "
");
+ if (!ok(n)) { return 0; }
+ return n;
+}
+
+/// The splicer follows only the names a module itself mentions, not the names the imported functions
+/// mention. This anchor mentions every helper the writer, the JSON reader and the number rules call.
+/// Never called.
+pub fn imports_anchor(o: []u8, s: []const u8) -> u32 {
+ var n : u32 = 0;
+ n = n + (ok(0) as u32);
+ n = n + OVER;
+ n = n + put(o, 0, 0);
+ n = n + raw(o, 0, s);
+ n = n + put_escaped(o, 0, 0);
+ n = n + text(o, 0, s);
+ n = n + num(o, 0, 0);
+ n = n + attr(o, 0, s, s);
+ n = n + fill(o, 0, s, s, s, s, 0);
+ n = n + fill_named(o, 0, s, s, s, s, s, s, s, s, s, s, s, s, s);
+ n = n + dec_to(o, 0);
+ n = n + (name_is(s, 0, 0, s) as u32);
+ n = n + (is_word_byte(0) as u32);
+ n = n + (H_LT as u32);
+ n = n + (H_GT as u32);
+ n = n + (H_AMP as u32);
+ n = n + (H_QUOTE as u32);
+ n = n + (H_APOS as u32);
+ n = n + (H_SPACE as u32);
+ n = n + (H_SLASH as u32);
+ n = n + (H_EQ as u32);
+ n = n + ((NO_SPAN < 0) as u32);
+ n = n + ((SPAN > 0) as u32);
+ n = n + (J_NONE as u32);
+ n = n + (J_OBJECT as u32);
+ n = n + (J_ARRAY as u32);
+ n = n + (J_STRING as u32);
+ n = n + (J_NUMBER as u32);
+ n = n + (J_TRUE as u32);
+ n = n + (J_FALSE as u32);
+ n = n + (J_NULL as u32);
+ n = n + (jspan(0, 0) as u32);
+ n = n + (span_lo(0) as u32);
+ n = n + (span_hi(0) as u32);
+ n = n + (skip_ws(s, 0) as u32);
+ n = n + (string_end(s, 0) as u32);
+ n = n + (value_end(s, 0) as u32);
+ n = n + (root(s) as u32);
+ n = n + (kind(s, 0) as u32);
+ n = n + (key_is(s, 0, 0, s) as u32);
+ n = n + (obj_get(s, 0, s) as u32);
+ n = n + arr_len(s, 0);
+ n = n + (arr_at(s, 0, 0) as u32);
+ n = n + hex4(s, 0);
+ n = n + escape_at(s, 0, 0);
+ n = n + cp_utf8_len(0);
+ n = n + (cp_utf8_byte(0, 0) as u32);
+ n = n + str_len(s, 0);
+ n = n + (str_byte(s, 0, 0) as u32);
+ n = n + JN_OVER;
+ n = n + JN_CAP;
+ n = n + ((JN_TWO52 > 0.0) as u32);
+ n = n + ((JN_TWO53 > 0.0) as u32);
+ n = n + jn_put(o, 0, 0);
+ n = n + jn_raw(o, 0, s);
+ n = n + jn_digits(o, 0);
+ n = n + jn_mul(o, 0, 0);
+ n = n + jn_mul2(o, 0, 0);
+ n = n + jn_mul5(o, 0, 0);
+ n = n + jn_shift(o, 0, 0);
+ n = n + (jn_cmp(s, s, 0) as u32);
+ n = n + (jn_expo(0.0) as u32);
+ n = n + (jn_mant(0.0) as u32);
+ n = n + ((jn_make(0, 0) > 0.0) as u32);
+ n = n + jn_exact(o, 0, 0);
+ n = n + jn_fscale(0);
+ n = n + (js_is_nan(0.0) as u32);
+ n = n + ((js_nan(0.0) > 0.0) as u32);
+ n = n + ((js_inf(1.0) > 0.0) as u32);
+ n = n + ((js_floor(0.0) > 0.0) as u32);
+ n = n + ((js_ceil(0.0) > 0.0) as u32);
+ n = n + ((js_max(0.0, 0.0) > 0.0) as u32);
+ n = n + ((js_min(0.0, 0.0) > 0.0) as u32);
+ n = n + ((js_round(0.0) > 0.0) as u32);
+ n = n + (jn_shortest(o, 1.0) as u32);
+ n = n + jn_copy(o, s, 0);
+ n = n + jn_put_run(o, 0, s, 0, 0);
+ n = n + jn_layout(o, 0, s, 0, 0);
+ n = n + js_str_num(o, 0, 0.0);
+ n = n + js_to_fixed(o, 0, 0.0, 0);
+ n = n + js_locale_num(o, 0, 0.0);
+ n = n + (jn_num_ok(s, 0, 0) as u32);
+ n = n + (jn_cmp_text(s, 0, 0, 0, 0) as u32);
+ n = n + ((js_parse_num(s, 0, 0) > 0.0) as u32);
+ n = n + ((jn_dec_value(s, 0, 0) > 0.0) as u32);
+ n = n + (jn_abs(0) as u32);
+ n = n + js_str_u64(o, 0, 0);
+ n = n + jv_str_to(o, 0, s, 0);
+ n = n + jv_text_str(o, 0, s, 0);
+ n = n + ((jv_numval(s, 0) > 0.0) as u32);
+ n = n + jv_str(o, 0, s, 0);
+ n = n + jv_text(o, 0, s, 0);
+ n = n + (jv_is(s, 0, s) as u32);
+ n = n + (jv_truthy(s, 0) as u32);
+ n = n + (jv_is_ws(0) as u32);
+ n = n + ((jv_number_of_text(s, 0) > 0.0) as u32);
+ n = n + ((jv_num(s, 0) > 0.0) as u32);
+ n = n + jv_vput_str(o, 0, s, s);
+ n = n + jv_vput_f64(o, 0, s, 0.0);
+ n = n + jv_vput_val(o, 0, s, s, 0);
+ n = n + jv_vput_defined(o, 0, s, s, 0);
+ n = n + jv_vput_fixed(o, 0, s, 0.0, 0);
+ n = n + jv_kid(o, 0, s, 0);
+ n = n + jv_find(s, 0, s);
+ n = n + jv_fill(o, 0, s, s, 0, true);
+ n = n + (jv_string_ok(s, 0) as u32);
+ n = n + (jv_number_ok(s, 0) as u32);
+ n = n + (jv_lit_ok(s, 0, s) as u32);
+ n = n + (jv_valid(s) as u32);
+ return n;
+}
diff --git a/specs/widgets/test_touch.t27 b/specs/widgets/test_touch.t27
new file mode 100644
index 0000000000..df83e346e9
--- /dev/null
+++ b/specs/widgets/test_touch.t27
@@ -0,0 +1,1856 @@
+// SPDX-License-Identifier: Apache-2.0
+; specs/widgets/test-touch.t27 -- did the agent touch its own tests? Every merged pull request on gHashTag/t27
+; Source of truth for public/widgets/test-touch/ (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, and the rule is K_ constants. The data script
+; scripts/widget-data/test-touch.mjs reads the K_ rule from this file, runs the fixture diffs below
+; through its classifier, and exits non-zero when its counts disagree with K_FIX_* or K_SNAP_*.
+; ASCII only (L3), English only (LANG-EN).
+
+; WHAT IT SHOWS: a dated snapshot. For every pull request merged on gHashTag/t27 in the window, the
+; script reads the patch that landed on master (git diff ^1 , the whole file as context
+; for .t27 files) and gives it one badge: tests untouched, tests added, tests changed, or assert
+; removed. Clicking a pull request shows the test lines it changed and links to it on GitHub. The
+; same is done for the human lane, so the bee lane has something to be compared with.
+
+; THE LANE RULE: a pull request is the bee's when its head branch is queen- (the race-the-bee
+; rule); every other one is the human lane, which means human-steered, not hand-typed.
+
+; WHAT IS A TEST LINE: a line inside a test, invariant or bench block of a .t27 file; any line of a
+; file under a tests/, test/ or conformance/ directory; any line of a *_test.* file (and test_*.py,
+; *.test.js, *.spec.ts). Not tests: .trinity/, .claude/, docs/ and .github/ (seals, notes and CI),
+; and JSON records under a test directory that carry a "generated_by" key (reports a tool wrote).
+
+; THE HEURISTIC: removed and added test lines are paired before they are judged. A line that only
+; moved is ignored. A test whose header was rewritten under the same name is "rewritten". A removed
+; check is paired with an added check on the same subject (the left side of the comparison) in the
+; same block first, then the same file. A pair is "expect" when the expected value changed, "loosened"
+; when the comparison got weaker (== to >=, a conjunct dropped, a disjunct added), "trivial" when the
+; new check cannot fail (true, x == x, len >= 0), "tightened" for the reverse. A removed check with no
+; partner, and a whole test block deleted, is "removed". The badge is "assert removed" when any check
+; or test block was removed or any check was loosened or made trivial; "tests changed" when other
+; test lines were removed; "tests added" when test lines were only added; else "tests untouched".
+
+; KNOWN FALSE POSITIVES: an assert merged, split or rewritten with a new subject reads as removed
+; (#5731 rewrote "invariant " checks as "then" checks); two checks in one file can be paired
+; across different tests; a quantifier added to an implication reads as an expected value change;
+; JSON test vectors reformatted (Infinity to "Infinity") or with a status renamed read as changed
+; expectations; a syntax-only rewrite of a test reads as "tests changed".
+; KNOWN FALSE NEGATIVES: a CI workflow or suite expectation file that stops running a test is not a
+; test line (the page flags those pull requests separately); asserts in code outside a test block or
+; test file are counted but not judged; a constant a test reads can change outside the test; an added
+; test that cannot fail is counted as "tests added", it is not judged.
+
+; WHAT IT DOES NOT CLAIM: intent. A removed test can be right: a test of a function that was deleted,
+; a placeholder test, a test that was false. The badge says the tests got smaller or weaker; the
+; reader decides whether that was gaming. It does not claim the lanes do the same work: most bee
+; merges are machine-filed port and test tasks. Numbers are from data.json, written by the script.
+; phi^2 + 1/phi^2 = 3 | TRINITY
+
+module widget_test_touch;
+
+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::fill_named;
+use widgets::html_out::dec_to;
+use widgets::html_out::name_is;
+use widgets::html_out::is_word_byte;
+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::json_scan::NO_SPAN;
+use widgets::json_scan::SPAN;
+use widgets::json_scan::J_NONE;
+use widgets::json_scan::J_OBJECT;
+use widgets::json_scan::J_ARRAY;
+use widgets::json_scan::J_STRING;
+use widgets::json_scan::J_NUMBER;
+use widgets::json_scan::J_TRUE;
+use widgets::json_scan::J_FALSE;
+use widgets::json_scan::J_NULL;
+use widgets::json_scan::jspan;
+use widgets::json_scan::span_lo;
+use widgets::json_scan::span_hi;
+use widgets::json_scan::skip_ws;
+use widgets::json_scan::string_end;
+use widgets::json_scan::value_end;
+use widgets::json_scan::root;
+use widgets::json_scan::kind;
+use widgets::json_scan::key_is;
+use widgets::json_scan::obj_get;
+use widgets::json_scan::arr_len;
+use widgets::json_scan::arr_at;
+use widgets::json_scan::hex4;
+use widgets::json_scan::escape_at;
+use widgets::json_scan::cp_utf8_len;
+use widgets::json_scan::cp_utf8_byte;
+use widgets::json_scan::str_len;
+use widgets::json_scan::str_byte;
+use widgets::js_num::JN_OVER;
+use widgets::js_num::JN_CAP;
+use widgets::js_num::JN_TWO52;
+use widgets::js_num::JN_TWO53;
+use widgets::js_num::jn_put;
+use widgets::js_num::jn_raw;
+use widgets::js_num::jn_digits;
+use widgets::js_num::jn_mul;
+use widgets::js_num::jn_mul2;
+use widgets::js_num::jn_mul5;
+use widgets::js_num::jn_shift;
+use widgets::js_num::jn_cmp;
+use widgets::js_num::jn_expo;
+use widgets::js_num::jn_mant;
+use widgets::js_num::jn_make;
+use widgets::js_num::jn_exact;
+use widgets::js_num::jn_fscale;
+use widgets::js_num::js_is_nan;
+use widgets::js_num::js_nan;
+use widgets::js_num::js_inf;
+use widgets::js_num::js_floor;
+use widgets::js_num::js_ceil;
+use widgets::js_num::js_max;
+use widgets::js_num::js_min;
+use widgets::js_num::js_round;
+use widgets::js_num::jn_shortest;
+use widgets::js_num::jn_copy;
+use widgets::js_num::jn_put_run;
+use widgets::js_num::jn_layout;
+use widgets::js_num::js_str_num;
+use widgets::js_num::js_to_fixed;
+use widgets::js_num::js_locale_num;
+use widgets::js_num::jn_num_ok;
+use widgets::js_num::jn_cmp_text;
+use widgets::js_num::js_parse_num;
+use widgets::js_num::jn_dec_value;
+use widgets::js_num::jn_abs;
+use widgets::js_num::js_str_u64;
+use widgets::js_val::jv_str_to;
+use widgets::js_val::jv_text_str;
+use widgets::js_val::jv_numval;
+use widgets::js_val::jv_str;
+use widgets::js_val::jv_text;
+use widgets::js_val::jv_is;
+use widgets::js_val::jv_truthy;
+use widgets::js_val::jv_is_ws;
+use widgets::js_val::jv_number_of_text;
+use widgets::js_val::jv_num;
+use widgets::js_val::jv_vput_str;
+use widgets::js_val::jv_vput_f64;
+use widgets::js_val::jv_vput_val;
+use widgets::js_val::jv_vput_defined;
+use widgets::js_val::jv_vput_fixed;
+use widgets::js_val::jv_kid;
+use widgets::js_val::jv_find;
+use widgets::js_val::jv_fill;
+use widgets::js_val::jv_string_ok;
+use widgets::js_val::jv_number_ok;
+use widgets::js_val::jv_lit_ok;
+use widgets::js_val::jv_valid;
+use widgets::js_val::jv_ws_at;
+use widgets::js_val::jv_ws_before;
+use widgets::js_val::jv_digit_value;
+use widgets::js_val::jv_fill_raw;
+use widgets::js_val::jv_fill_with;
+
+pub const KIND : str = "widget-tool";
+pub const ID : str = "test-touch";
+pub const TITLE : str = "Did the agent touch its own tests? Every PR merged on t27, checked";
+pub const DESCRIPTION : str = "Every pull request merged on gHashTag/t27 in 14 days, bee lane and human lane: which added tests, changed them or removed asserts, with the exact test lines from git.";
+pub const IMAGE_ALT : str = "Test-touch card, snapshot 2026-10-04 of gHashTag/t27 over 14 days: the Queen's bees merged 367 pull requests, of which 11 removed tests or asserts, 32 changed tests, 238 added tests and 86 left them untouched. People merged 240, of which 17 removed tests or asserts. One bee PR titled Implement the 1 empty function body deleted all 26 tests in its file.";
+pub const HOOK : str = "Bees landed 367 PRs on t27 in 14 days. 11 deleted tests or asserts; one titled 'Implement the 1 empty function body' deleted all 26 tests in its file.";
+pub const CATEGORY : str = "compiler";
+pub const DATA_SOURCES : [3]str = ["gh 2.83.2, gh api graphql on gHashTag/t27: search is:pr is:merged, one day at a time, with head branch, author, merge commit and the file list GitHub reports", "git 2.52.0 on a shallow bare clone of t27 master: git diff -M ^1 , -U1000000 for .t27 files and -U0 for the rest", "node scripts/widget-data/test-touch.mjs: classifies every patch with the rule in this spec and writes public/widgets/test-touch/data.json with its UTC time"];
+pub const READS_LOCAL_FILES : bool = false;
+pub const SENDS_NOTHING : bool = true;
+
+; --- The rule --------------------------------------------------------------------------------
+pub const K_REPO : str = "gHashTag/t27";
+pub const K_BEE_BRANCH_PREFIX : str = "queen-";
+pub const K_WINDOW_DAYS : u8 = 14;
+pub const K_DATA_FILE : str = "./data.json";
+pub const K_LANES : [2]str = ["bee", "human"];
+pub const K_TEST_DIRS : [3]str = ["tests", "test", "conformance"];
+pub const K_TEST_NAME_MARK : str = "_test.";
+pub const K_NOT_TEST_DIRS : [4]str = [".trinity", ".claude", "docs", ".github"];
+pub const K_GENERATED_KEY : str = "generated_by";
+pub const K_TEST_BLOCK_WORDS : [3]str = ["test", "invariant", "bench"];
+pub const K_TAGS : [8]str = ["removed", "trivial", "loosened", "expect", "rewritten", "tightened", "added", "line"];
+
+; --- Fixtures --------------------------------------------------------------------------------
+; Each WANT vector is [badge, class, checks added, checks removed, expect, loosened, test lines
+; added, test lines removed, code lines]. Badge: 0 untouched, 1 added, 2 changed, 3 assert removed.
+; Class: 0 code only, 1 tests only, 2 both, 3 neither.
+pub const K_FIX_COUNT : u8 = 3;
+pub const SAY_FIX_0_PATH : str = "specs/demo/clamp.t27";
+pub const SAY_FIX_0_DIFF : [7]str = ["@@ -1,3 +1,6 @@", " module clamp;", "-pub const K_MAX : u8 = 3;", "+pub const K_MAX : u8 = 4;", "+test max_is_four {", "+ assert K_MAX == 4;", "+}"];
+pub const K_FIX_0_WANT : [9]u16 = [1, 2, 1, 0, 0, 0, 3, 0, 2];
+pub const SAY_FIX_1_PATH : str = "specs/demo/width.t27";
+pub const SAY_FIX_1_DIFF : [5]str = ["@@ -1,3 +1,3 @@", " test width_ok {", "- assert width(27) == 27;", "+ assert width(27) >= 20;", " }"];
+pub const K_FIX_1_WANT : [9]u16 = [3, 1, 0, 0, 0, 1, 1, 1, 0];
+pub const SAY_FIX_2_PATH : str = "bootstrap/tests/fmt.rs";
+pub const SAY_FIX_2_DIFF : [7]str = ["@@ -3,4 +3,4 @@", " fn fmt_zero() {", "- assert_eq!(fmt(0), 0);", "- assert!(ok(1));", "+ assert!(ok(1));", "+ assert_eq!(fmt(0), 1);", " }"];
+pub const K_FIX_2_WANT : [9]u16 = [2, 1, 0, 0, 1, 0, 2, 2, 0];
+
+; --- The snapshot ----------------------------------------------------------------------------
+; [pull requests, untouched, added, changed, assert removed, code only, tests only, both, neither,
+; PRs with an expected value changed, PRs with a check loosened]. Pasted from the script's output;
+; the script exits non-zero when data.json disagrees.
+pub const K_SNAP_DATE : str = "2026-10-04";
+pub const K_SNAP_BEE : [11]u16 = [367, 86, 238, 32, 11, 77, 37, 244, 9, 10, 1];
+pub const K_SNAP_HUMAN : [11]u16 = [240, 95, 69, 59, 17, 64, 10, 135, 31, 39, 2];
+
+; --- Words -----------------------------------------------------------------------------------
+; {name} is filled from data.json by tool.js; nothing else is.
+pub const SAY_SNAPSHOT : str = "Snapshot, not live: taken {at} UTC from GitHub and git, covering every pull request merged on {repo} in the {days} days before it.";
+pub const SAY_LOADING : str = "Reading the snapshot...";
+pub const SAY_LOAD_FAILED : str = "The snapshot file could not be read.";
+pub const SAY_LANE_NAMES : [2]str = ["Bee", "Human"];
+pub const SAY_LANE_LINES : [2]str = ["head branch queen-, written by a bee the Queen dispatched", "any other branch: a person steered the session, often with an AI assistant"];
+pub const SAY_MERGED : str = "merged pull requests";
+pub const SAY_REMOVED_OF : str = "removed or weakened a test";
+pub const SAY_BADGES : [4]str = ["tests untouched", "tests added", "tests changed", "assert removed"];
+pub const SAY_BADGE_LINES : [4]str = ["no test line changed", "test lines only added", "test lines removed or rewritten, no check lost", "a check or test block removed, or a check made weaker"];
+pub const SAY_CLASSES : [4]str = ["code only", "tests only", "code and tests", "neither"];
+pub const SAY_STAT_NAMES : [5]str = ["checks added", "checks removed", "test blocks removed", "expected values changed", "checks loosened"];
+pub const SAY_VERDICT_YES : str = "Yes. {beeRemoved} of {beePrs} bee pull requests removed test blocks or asserts, or made a check weaker: {beeBlocks} test blocks deleted in all. People: {humanRemoved} of {humanPrs}.";
+pub const SAY_VERDICT_NO : str = "No bee pull request in this window removed a test block or an assert, or made a check weaker.";
+pub const SAY_VERDICT_LAST : str = "The last bee one merged {last} UTC; the {after} bee pull requests merged after it removed none.";
+pub const SAY_FILTER_LANE : str = "Lane";
+pub const SAY_FILTER_BADGE : str = "Badge";
+pub const SAY_ALL : str = "All";
+pub const SAY_SHOWING : str = "{n} of {total} pull requests";
+pub const SAY_SHOW_MORE : str = "Show more";
+pub const SAY_NO_TITLE : str = "(title not in English)";
+pub const SAY_TAGS : [8]str = ["removed", "now trivial", "loosened", "expected value", "rewritten", "tightened", "added", "test line"];
+pub const SAY_TEST_FILES : str = "{n} of {all} changed files hold test lines";
+pub const SAY_CHECKS : str = "checks +{added} / -{removed}; test blocks removed {blocks}";
+pub const SAY_NO_LINES : str = "No test line changed in this pull request.";
+pub const SAY_MORE_LINES : str = "+{n} more test lines in the pull request";
+pub const SAY_OPEN_PR : str = "Open the pull request on GitHub";
+pub const SAY_CI_GATE : str = "Also changed a CI workflow or a suite expectation file; those are not counted as test lines.";
+pub const SAY_API_ONLY : str = "GitHub lists {n} more file(s) than the merge diff holds; the badge reads the merge diff.";
+pub const SAY_EXCLUDED : str = "Left out: {notMaster} merged pull requests whose merge commit is not on master.";
+pub const SAY_RULE_TITLE : str = "How a pull request is judged, and where it is wrong";
+pub const SAY_RULE : [5]str = ["Lane: the bee's when the head branch is queen-; every other pull request is the human lane.", "Test lines: test, invariant and bench blocks in .t27 files; files under tests/, test/ or conformance/; *_test.* files. Not seals, docs, CI, or JSON reports a tool generated.", "Diff: git diff ^1 , the patch that landed on master. Lines that only moved are ignored.", "Pairing: a removed check is paired with an added check on the same subject. Then expected value changed, loosened (== to >=, a condition dropped), trivial (cannot fail), tightened, or rewritten. Unpaired: removed.", "Badge: assert removed when a check or test block was removed or a check got weaker; tests changed when other test lines went; tests added when lines were only added."];
+pub const SAY_FALSE_POS_TITLE : str = "Known false positives";
+pub const SAY_FALSE_POS : [4]str = ["An assert merged, split or rewritten with a new subject reads as removed (#5731 rewrote invariant checks as then checks).", "Two checks in one file can be paired across different tests.", "Reformatted JSON vectors (Infinity to a string) or a renamed status read as changed expected values.", "A syntax-only rewrite of a test reads as tests changed."];
+pub const SAY_FALSE_NEG_TITLE : str = "Known false negatives";
+pub const SAY_FALSE_NEG : [4]str = ["A CI workflow or suite expectation that stops running a test is not a test line; those pull requests carry a note.", "Asserts in code outside a test block are counted, not judged.", "A constant a test reads can change outside the test.", "An added test that cannot fail still counts as tests added."];
+pub const SAY_NOT_INTENT : str = "A badge is not a verdict on intent: a removed test can be right (its function was deleted, it was a placeholder, it was false). It says the tests got smaller or weaker; the lines are there to judge.";
+
+; --- Tests -----------------------------------------------------------------------------------
+test every_pull_request_gets_one_badge_and_one_class {
+ assert K_SNAP_BEE[1] + K_SNAP_BEE[2] + K_SNAP_BEE[3] + K_SNAP_BEE[4] == K_SNAP_BEE[0];
+ assert K_SNAP_BEE[5] + K_SNAP_BEE[6] + K_SNAP_BEE[7] + K_SNAP_BEE[8] == K_SNAP_BEE[0];
+ assert K_SNAP_HUMAN[1] + K_SNAP_HUMAN[2] + K_SNAP_HUMAN[3] + K_SNAP_HUMAN[4] == K_SNAP_HUMAN[0];
+ assert K_SNAP_HUMAN[5] + K_SNAP_HUMAN[6] + K_SNAP_HUMAN[7] + K_SNAP_HUMAN[8] == K_SNAP_HUMAN[0];
+ assert K_SNAP_BEE[10] <= K_SNAP_BEE[4];
+ assert K_SNAP_HUMAN[10] <= K_SNAP_HUMAN[4];
+}
+
+test the_hook_matches_the_snapshot {
+ assert K_SNAP_BEE[0] == 367;
+ assert K_SNAP_BEE[4] == 11;
+ assert K_SNAP_HUMAN[4] == 17;
+ assert K_WINDOW_DAYS == 14;
+ assert K_SNAP_DATE == "2026-10-04";
+}
+
+test the_fixtures_cover_add_loosen_and_flip {
+ assert K_FIX_COUNT == 3;
+ assert K_FIX_0_WANT[0] == 1;
+ assert K_FIX_0_WANT[2] == 1;
+ assert K_FIX_1_WANT[0] == 3;
+ assert K_FIX_1_WANT[5] == 1;
+ assert K_FIX_2_WANT[0] == 2;
+ assert K_FIX_2_WANT[4] == 1;
+ assert K_FIX_2_WANT[3] == 0;
+}
+
+test the_badges_and_tags_line_up {
+ assert SAY_BADGES[3] == "assert removed";
+ assert SAY_BADGES[0] == "tests untouched";
+ assert K_TAGS[0] == "removed";
+ assert K_TAGS[7] == "line";
+ assert K_LANES[0] == "bee";
+ assert K_BEE_BRANCH_PREFIX == "queen-";
+}
+
+test the_page_sends_nothing_and_reads_no_file {
+ assert SENDS_NOTHING == true;
+ assert READS_LOCAL_FILES == false;
+ assert K_DATA_FILE == "./data.json";
+ assert KIND == "widget-tool";
+ assert ID == "test-touch";
+}
+
+
+; =================================================================================================
+; THE NATIVE PAGE (epic gHashTag/t27#10937, issue #10938, stage "page" of specs/t27f/widget_factory.t27)
+; Everything below replaces public/widgets/test-touch/tool.js (146 lines of hand-written JavaScript).
+; The words and numbers above are unchanged. The host protocol is race_the_bee.t27's plus:
+; w_click(i) button i in document order: 0..2 the lane filter (All, Bee, Human), 3..7 the badge
+; filter (All, then the four badges from the worst), 8 "Show more"
+; w_toggle(i, open) details i (document order: the pull request rows, then the rule block) was opened
+; or closed; 1 when the page changed (the host then re-renders), else 0. A row's
+; body is built the first time it opens, as tool.js's "toggle" listener does.
+; The host must not restore open by index for this widget (it does not
+; when the widget exports w_toggle): the open state is written as the `open` attribute.
+; The 607 pull requests of data.json are NOT copied into a tree: DATA holds the file's bytes (1 MiB
+; buffer for the 786923-byte file), PRS the byte span of each pull request. The list is sorted ONCE, as
+; tool.js does, by V8's own algorithm: Array.prototype.sort is TimSort and the comparator
+; (b.badge - a.badge || (a.merged < b.merged ? 1 : -1)) never returns 0, so pull requests with the same
+; badge and merge time (the real file has three, #5821 #5675 #5595) come out in the order the algorithm
+; happens to compare them. This file ports that algorithm (array-sort.tq: runs, binary insertion,
+; galloping merges, the same argument order in every Compare) and checks it against node's sort on tie-heavy
+; random lists. Numbers and strings are written as js_num.t27 / js_val.t27 write them.
+; Where tool.js throws while it draws (prs no array, a null pull request, lanes without bee / human or a
+; lane without a vector array) its catch shows SAY_LOAD_FAILED; so does this page (data_ok). A body
+; that tool.js cannot build (a merged stamp that is no string, a line text that is no string) throws inside
+; its toggle handler, which leaves the row open and empty; this page then draws the body as empty strings.
+; NOT carried over: more than PR_CAP (1024) pull requests; a `merged` stamp compared beyond its first 32
+; bytes; sort-key `merged` values that are objects or arrays; UTF-16 order of astral characters; the last
+; merge time when a bad pull request has a non-string stamp (the default .sort() of mixed values).
+; MEMORY: DATA 1 MiB (the file is 786923 bytes), the sort keys 100 KB, the page buffer is the host's: the
+; real file renders 17472 bytes at the start, 187106 bytes with all 607 rows listed and closed, and
+; 1157198 bytes with all 607 rows open (601 bodies), so the host's output buffer is 1 MiB for the first
+; two and must be 2 MiB or more for the last. The wasm module asks for 34 pages (2.2 MB) at start; with
+; the host's 17-page buffer area and the data copied in it holds 65 pages (4.3 MB). w_data takes 64 ms in
+; node (validate, 607 spans, TimSort, filter), a closed 607-row render 24-45 ms.
+; MEASURED 2026-10-19: t27c 0.5.2 gen + a 6-function Zig export wrapper + zig build-exe wasm32-freestanding
+; ReleaseSmall give a 35775-byte wasm. The original page (tool.js) and this one ran in two happy-dom
+; windows: (1) the real 786923-byte data.json, two sessions of 70 interactions (lane and badge filters,
+; show more, rows opened and closed, the rule block): 144 comparisons of tag, attributes and text; (2) 38
+; data variants cut to 45 pull requests (no bad bee PR; a bad one with a missing / null / odd stamp; lanes
+; and badges and classes out of range or as strings; titles empty / null / markup / numbers; line lists
+; empty / odd / many files; flags; counts / author / branch / url missing; merged stamps as numbers; vectors
+; short or with strings; excluded and snapshot fields missing; 1, 0, 40, 41 and 80 pull requests) and 8
+; data files the page cannot draw (prs missing or an object, a null pull request, lanes or a vector
+; missing, a root array): 400 comparisons; (3) the SORT: 156 lists of 2 to 200 pull requests with almost
+; all badges and stamps tied (2937 tied pairs; random, ascending, descending, sawtooth, blocks), every
+; row listed, all and bee only, the order of the rows compared; (4) the t27 TimSort alone against node's
+; Array.prototype.sort with the same comparator over 10560 lists of 2 to 1024 elements (1948600 tied
+; pairs): 0 differences everywhere. A negative control (one title in the native DOM changed) is caught;
+; a 404 and a body that is not JSON end on the same line in both.
+; Not compared, and why: happy-dom's 'toggle' event is the only way a row opening reaches either page (a
+; real browser fires it on the user's click as well); CSS; a body for a pull request the original cannot
+; build; runs of more than 200 tied pull requests in the DOM (the lists of up to 1024 are compared in
+; the sort test only); the host's 'toggle' forwarding is the new host code described in the report.
+
+pub const DATA_CAP : u32 = 1048576;
+pub const PR_CAP : u32 = 1024;
+pub const PAGE : u32 = 40;
+pub var DATA : [1048576]u8 = [0; 1048576];
+pub var DATA_LEN : u32 = 0;
+pub var DATA_STATE : u8 = 0;
+pub var NPR : u32 = 0;
+pub var PRS : [1024]i64 = [0; 1024];
+pub var KB : [1024]f64 = [0.0; 1024];
+pub var KM : [1024]f64 = [0.0; 1024];
+pub var KK : [1024]u8 = [0; 1024];
+pub var KS : [32768]u8 = [0; 32768];
+pub var KL : [1024]u32 = [0; 1024];
+pub var ORDER : [1024]u32 = [0; 1024];
+pub var WORK : [1024]u32 = [0; 1024];
+pub var TMP : [1024]u32 = [0; 1024];
+pub var RUN_BASE : [96]i32 = [0; 96];
+pub var RUN_LEN : [96]i32 = [0; 96];
+pub var RUNS : i32 = 0;
+pub var MIN_GALLOP : i32 = 7;
+pub var LANE : i32 = 0 - 1;
+pub var BADGE : i32 = 0 - 1;
+pub var SHOWN : u32 = 40;
+pub var ROW_OPEN : [1024]bool = [false; 1024];
+pub var ROW_BODY : [1024]bool = [false; 1024];
+pub var RULE_OPEN : bool = false;
+pub var ROWS : [1024]u32 = [0; 1024];
+pub var NROWS : u32 = 0;
+
+pub fn w_init(h: []const u8) -> u32 {
+ DATA_STATE = 0;
+ DATA_LEN = 0;
+ NPR = 0;
+ LANE = 0 - 1;
+ BADGE = 0 - 1;
+ SHOWN = PAGE;
+ RULE_OPEN = false;
+ return 0;
+}
+
+fn nullish(d: []const u8, sp: i64) -> bool {
+ const k : u8 = kind(d, sp);
+ return k == J_NONE or k == J_NULL;
+}
+
+// --- V8's TimSort (third_party/v8/builtins/array-sort.tq), over indexes of the pull requests -------------
+
+/// Compare(a, b) of tool.js's comparator: (b.badge - a.badge) || (a.merged < b.merged ? 1 : -1), never 0.
+fn cmp(a: u32, b: u32) -> i32 {
+ const bd : f64 = KB[b as usize] - KB[a as usize];
+ if (bd == bd and bd != 0.0) {
+ if (bd < 0.0) { return 0 - 1; }
+ return 1;
+ }
+ var lt : bool = false;
+ if (KK[a as usize] == 1 and KK[b as usize] == 1) {
+ const la : u32 = KL[a as usize];
+ const lb : u32 = KL[b as usize];
+ var m : u32 = la;
+ if (lb < m) { m = lb; }
+ var i : u32 = 0;
+ var decided : bool = false;
+ while (i < m and !decided) {
+ const ca : u8 = KS[(a * 32 + i) as usize];
+ const cb : u8 = KS[(b * 32 + i) as usize];
+ if (ca != cb) {
+ lt = ca < cb;
+ decided = true;
+ }
+ i = i + 1;
+ }
+ if (!decided) { lt = la < lb; }
+ } else {
+ lt = KM[a as usize] < KM[b as usize];
+ }
+ if (lt) { return 1; }
+ return 0 - 1;
+}
+
+fn get(arr: u32, i: i32) -> u32 {
+ if (arr == 0) { return WORK[i as usize]; }
+ return TMP[i as usize];
+}
+
+fn copy_range(sa: u32, sp: i32, da: u32, dp: i32, len: i32) -> u32 {
+ if (len <= 0) { return 0; }
+ if (sa == da and sp == dp) { return 0; }
+ var k : i32 = 0;
+ if (sp < dp and sa == da) {
+ k = len - 1;
+ while (k >= 0) {
+ const v : u32 = get(sa, sp + k);
+ if (da == 0) { WORK[(dp + k) as usize] = v; } else { TMP[(dp + k) as usize] = v; }
+ k = k - 1;
+ }
+ } else {
+ while (k < len) {
+ const v2 : u32 = get(sa, sp + k);
+ if (da == 0) { WORK[(dp + k) as usize] = v2; } else { TMP[(dp + k) as usize] = v2; }
+ k = k + 1;
+ }
+ }
+ return 0;
+}
+
+fn put_w(i: i32, v: u32) -> u32 {
+ WORK[i as usize] = v;
+ return 0;
+}
+
+fn put_t(i: i32, v: u32) -> u32 {
+ TMP[i as usize] = v;
+ return 0;
+}
+
+fn gallop_left(arr: u32, key: u32, base: i32, length: i32, hint: i32) -> i32 {
+ var last_ofs : i32 = 0;
+ var offset : i32 = 1;
+ var order : i32 = cmp(get(arr, base + hint), key);
+ if (order < 0) {
+ const max_ofs : i32 = length - hint;
+ while (offset < max_ofs) {
+ order = cmp(get(arr, base + hint + offset), key);
+ if (order >= 0) { break; }
+ last_ofs = offset;
+ offset = offset * 2 + 1;
+ }
+ if (offset > max_ofs) { offset = max_ofs; }
+ last_ofs = last_ofs + hint;
+ offset = offset + hint;
+ } else {
+ const max_ofs2 : i32 = hint + 1;
+ while (offset < max_ofs2) {
+ order = cmp(get(arr, base + hint - offset), key);
+ if (order < 0) { break; }
+ last_ofs = offset;
+ offset = offset * 2 + 1;
+ }
+ if (offset > max_ofs2) { offset = max_ofs2; }
+ const tmp : i32 = last_ofs;
+ last_ofs = hint - offset;
+ offset = hint - tmp;
+ }
+ last_ofs = last_ofs + 1;
+ while (last_ofs < offset) {
+ const m : i32 = last_ofs + ((offset - last_ofs) / 2);
+ order = cmp(get(arr, base + m), key);
+ if (order < 0) { last_ofs = m + 1; } else { offset = m; }
+ }
+ return offset;
+}
+
+fn gallop_right(arr: u32, key: u32, base: i32, length: i32, hint: i32) -> i32 {
+ var last_ofs : i32 = 0;
+ var offset : i32 = 1;
+ var order : i32 = cmp(key, get(arr, base + hint));
+ if (order < 0) {
+ const max_ofs : i32 = hint + 1;
+ while (offset < max_ofs) {
+ order = cmp(key, get(arr, base + hint - offset));
+ if (order >= 0) { break; }
+ last_ofs = offset;
+ offset = offset * 2 + 1;
+ }
+ if (offset > max_ofs) { offset = max_ofs; }
+ const tmp : i32 = last_ofs;
+ last_ofs = hint - offset;
+ offset = hint - tmp;
+ } else {
+ const max_ofs2 : i32 = length - hint;
+ while (offset < max_ofs2) {
+ order = cmp(key, get(arr, base + hint + offset));
+ if (order < 0) { break; }
+ last_ofs = offset;
+ offset = offset * 2 + 1;
+ }
+ if (offset > max_ofs2) { offset = max_ofs2; }
+ last_ofs = last_ofs + hint;
+ offset = offset + hint;
+ }
+ last_ofs = last_ofs + 1;
+ while (last_ofs < offset) {
+ const m : i32 = last_ofs + ((offset - last_ofs) / 2);
+ order = cmp(key, get(arr, base + m));
+ if (order < 0) { offset = m; } else { last_ofs = m + 1; }
+ }
+ return offset;
+}
+
+fn merge_low(base_a: i32, len_a0: i32, base_b: i32, len_b0: i32) -> u32 {
+ var len_a : i32 = len_a0;
+ var len_b : i32 = len_b0;
+ copy_range(0, base_a, 1, 0, len_a);
+ var dest : i32 = base_a;
+ var ct : i32 = 0;
+ var cb : i32 = base_b;
+ put_w(dest, WORK[cb as usize]);
+ dest = dest + 1;
+ cb = cb + 1;
+ var mode : u32 = 0;
+ len_b = len_b - 1;
+ if (len_b == 0) { mode = 1; } else { if (len_a == 1) { mode = 2; } }
+ var min_gallop : i32 = MIN_GALLOP;
+ while (mode == 0) {
+ var wins_a : i32 = 0;
+ var wins_b : i32 = 0;
+ var inner : bool = true;
+ while (inner and mode == 0) {
+ const order : i32 = cmp(WORK[cb as usize], TMP[ct as usize]);
+ if (order < 0) {
+ put_w(dest, WORK[cb as usize]);
+ dest = dest + 1;
+ cb = cb + 1;
+ wins_b = wins_b + 1;
+ len_b = len_b - 1;
+ wins_a = 0;
+ if (len_b == 0) { mode = 1; } else { if (wins_b >= min_gallop) { inner = false; } }
+ } else {
+ put_w(dest, TMP[ct as usize]);
+ dest = dest + 1;
+ ct = ct + 1;
+ wins_a = wins_a + 1;
+ len_a = len_a - 1;
+ wins_b = 0;
+ if (len_a == 1) { mode = 2; } else { if (wins_a >= min_gallop) { inner = false; } }
+ }
+ }
+ if (mode != 0) { break; }
+ min_gallop = min_gallop + 1;
+ var first : bool = true;
+ while (mode == 0 and (wins_a >= 7 or wins_b >= 7 or first)) {
+ first = false;
+ min_gallop = min_gallop - 1;
+ if (min_gallop < 1) { min_gallop = 1; }
+ MIN_GALLOP = min_gallop;
+ wins_a = gallop_right(1, WORK[cb as usize], ct, len_a, 0);
+ if (wins_a > 0) {
+ copy_range(1, ct, 0, dest, wins_a);
+ dest = dest + wins_a;
+ ct = ct + wins_a;
+ len_a = len_a - wins_a;
+ if (len_a == 1) { mode = 2; }
+ if (len_a == 0) { mode = 1; }
+ }
+ if (mode == 0) {
+ put_w(dest, WORK[cb as usize]);
+ dest = dest + 1;
+ cb = cb + 1;
+ len_b = len_b - 1;
+ if (len_b == 0) { mode = 1; }
+ }
+ if (mode == 0) {
+ wins_b = gallop_left(0, TMP[ct as usize], cb, len_b, 0);
+ if (wins_b > 0) {
+ copy_range(0, cb, 0, dest, wins_b);
+ dest = dest + wins_b;
+ cb = cb + wins_b;
+ len_b = len_b - wins_b;
+ if (len_b == 0) { mode = 1; }
+ }
+ }
+ if (mode == 0) {
+ put_w(dest, TMP[ct as usize]);
+ dest = dest + 1;
+ ct = ct + 1;
+ len_a = len_a - 1;
+ if (len_a == 1) { mode = 2; }
+ }
+ }
+ if (mode == 0) {
+ min_gallop = min_gallop + 1;
+ MIN_GALLOP = min_gallop;
+ }
+ }
+ if (mode == 1) {
+ if (len_a > 0) { copy_range(1, ct, 0, dest, len_a); }
+ }
+ if (mode == 2) {
+ copy_range(0, cb, 0, dest, len_b);
+ put_w(dest + len_b, TMP[ct as usize]);
+ }
+ return 0;
+}
+
+fn merge_high(base_a: i32, len_a0: i32, base_b: i32, len_b0: i32) -> u32 {
+ var len_a : i32 = len_a0;
+ var len_b : i32 = len_b0;
+ copy_range(0, base_b, 1, 0, len_b);
+ var dest : i32 = base_b + len_b - 1;
+ var ct : i32 = len_b - 1;
+ var ca : i32 = base_a + len_a - 1;
+ put_w(dest, WORK[ca as usize]);
+ dest = dest - 1;
+ ca = ca - 1;
+ var mode : u32 = 0;
+ len_a = len_a - 1;
+ if (len_a == 0) { mode = 1; } else { if (len_b == 1) { mode = 2; } }
+ var min_gallop : i32 = MIN_GALLOP;
+ while (mode == 0) {
+ var wins_a : i32 = 0;
+ var wins_b : i32 = 0;
+ var inner : bool = true;
+ while (inner and mode == 0) {
+ const order : i32 = cmp(TMP[ct as usize], WORK[ca as usize]);
+ if (order < 0) {
+ put_w(dest, WORK[ca as usize]);
+ dest = dest - 1;
+ ca = ca - 1;
+ wins_a = wins_a + 1;
+ len_a = len_a - 1;
+ wins_b = 0;
+ if (len_a == 0) { mode = 1; } else { if (wins_a >= min_gallop) { inner = false; } }
+ } else {
+ put_w(dest, TMP[ct as usize]);
+ dest = dest - 1;
+ ct = ct - 1;
+ wins_b = wins_b + 1;
+ len_b = len_b - 1;
+ wins_a = 0;
+ if (len_b == 1) { mode = 2; } else { if (wins_b >= min_gallop) { inner = false; } }
+ }
+ }
+ if (mode != 0) { break; }
+ min_gallop = min_gallop + 1;
+ var first : bool = true;
+ while (mode == 0 and (wins_a >= 7 or wins_b >= 7 or first)) {
+ first = false;
+ min_gallop = min_gallop - 1;
+ if (min_gallop < 1) { min_gallop = 1; }
+ MIN_GALLOP = min_gallop;
+ var k : i32 = gallop_right(0, TMP[ct as usize], base_a, len_a, len_a - 1);
+ wins_a = len_a - k;
+ if (wins_a > 0) {
+ dest = dest - wins_a;
+ ca = ca - wins_a;
+ copy_range(0, ca + 1, 0, dest + 1, wins_a);
+ len_a = len_a - wins_a;
+ if (len_a == 0) { mode = 1; }
+ }
+ if (mode == 0) {
+ put_w(dest, TMP[ct as usize]);
+ dest = dest - 1;
+ ct = ct - 1;
+ len_b = len_b - 1;
+ if (len_b == 1) { mode = 2; }
+ }
+ if (mode == 0) {
+ k = gallop_left(1, WORK[ca as usize], 0, len_b, len_b - 1);
+ wins_b = len_b - k;
+ if (wins_b > 0) {
+ dest = dest - wins_b;
+ ct = ct - wins_b;
+ copy_range(1, ct + 1, 0, dest + 1, wins_b);
+ len_b = len_b - wins_b;
+ if (len_b == 1) { mode = 2; }
+ if (len_b == 0) { mode = 1; }
+ }
+ }
+ if (mode == 0) {
+ put_w(dest, WORK[ca as usize]);
+ dest = dest - 1;
+ ca = ca - 1;
+ len_a = len_a - 1;
+ if (len_a == 0) { mode = 1; }
+ }
+ }
+ if (mode == 0) {
+ min_gallop = min_gallop + 1;
+ MIN_GALLOP = min_gallop;
+ }
+ }
+ if (mode == 1) {
+ if (len_b > 0) { copy_range(1, 0, 0, dest - (len_b - 1), len_b); }
+ }
+ if (mode == 2) {
+ dest = dest - len_a;
+ ca = ca - len_a;
+ copy_range(0, ca + 1, 0, dest + 1, len_a);
+ put_w(dest, TMP[ct as usize]);
+ }
+ return 0;
+}
+
+fn merge_at(i: i32) -> u32 {
+ var base_a : i32 = RUN_BASE[i as usize];
+ var len_a : i32 = RUN_LEN[i as usize];
+ const base_b : i32 = RUN_BASE[(i + 1) as usize];
+ var len_b : i32 = RUN_LEN[(i + 1) as usize];
+ RUN_LEN[i as usize] = len_a + len_b;
+ if (i == RUNS - 3) {
+ RUN_BASE[(i + 1) as usize] = RUN_BASE[(i + 2) as usize];
+ RUN_LEN[(i + 1) as usize] = RUN_LEN[(i + 2) as usize];
+ }
+ RUNS = RUNS - 1;
+ const key_right : u32 = WORK[base_b as usize];
+ const k : i32 = gallop_right(0, key_right, base_a, len_a, 0);
+ base_a = base_a + k;
+ len_a = len_a - k;
+ if (len_a == 0) { return 0; }
+ const key_left : u32 = WORK[(base_a + len_a - 1) as usize];
+ len_b = gallop_left(0, key_left, base_b, len_b, len_b - 1);
+ if (len_b == 0) { return 0; }
+ if (len_a <= len_b) { merge_low(base_a, len_a, base_b, len_b); } else { merge_high(base_a, len_a, base_b, len_b); }
+ return 0;
+}
+
+fn run_ok(n: i32) -> bool {
+ if (n < 2) { return true; }
+ return RUN_LEN[(n - 2) as usize] > RUN_LEN[(n - 1) as usize] + RUN_LEN[n as usize];
+}
+
+fn merge_collapse() -> u32 {
+ var go : bool = true;
+ while (RUNS > 1 and go) {
+ var n : i32 = RUNS - 2;
+ if (!run_ok(n + 1) or !run_ok(n)) {
+ if (n > 0 and RUN_LEN[(n - 1) as usize] < RUN_LEN[(n + 1) as usize]) { n = n - 1; }
+ merge_at(n);
+ } else {
+ if (RUN_LEN[n as usize] <= RUN_LEN[(n + 1) as usize]) { merge_at(n); } else { go = false; }
+ }
+ }
+ return 0;
+}
+
+fn merge_force_collapse() -> u32 {
+ while (RUNS > 1) {
+ var n : i32 = RUNS - 2;
+ if (n > 0 and RUN_LEN[(n - 1) as usize] < RUN_LEN[(n + 1) as usize]) { n = n - 1; }
+ merge_at(n);
+ }
+ return 0;
+}
+
+fn count_and_make_run(low_arg: i32, high: i32) -> i32 {
+ const low : i32 = low_arg + 1;
+ if (low == high) { return 1; }
+ var run_length : i32 = 2;
+ const element_low : u32 = WORK[low as usize];
+ const element_high : u32 = WORK[(low - 1) as usize];
+ var order : i32 = cmp(element_low, element_high);
+ const desc : bool = order < 0;
+ var prev : u32 = element_low;
+ var idx : i32 = low + 1;
+ var stop : bool = false;
+ while (idx < high and !stop) {
+ const cur : u32 = WORK[idx as usize];
+ order = cmp(cur, prev);
+ if (desc) {
+ if (order >= 0) { stop = true; }
+ } else {
+ if (order < 0) { stop = true; }
+ }
+ if (!stop) {
+ prev = cur;
+ run_length = run_length + 1;
+ idx = idx + 1;
+ }
+ }
+ if (desc) {
+ var lo : i32 = low_arg;
+ var hi : i32 = low_arg + run_length - 1;
+ while (lo < hi) {
+ const t : u32 = WORK[lo as usize];
+ WORK[lo as usize] = WORK[hi as usize];
+ WORK[hi as usize] = t;
+ lo = lo + 1;
+ hi = hi - 1;
+ }
+ }
+ return run_length;
+}
+
+fn binary_insertion_sort(low: i32, start_arg: i32, high: i32) -> u32 {
+ var start : i32 = start_arg;
+ if (low == start_arg) { start = start_arg + 1; }
+ while (start < high) {
+ var left : i32 = low;
+ var right : i32 = start;
+ const pivot : u32 = WORK[right as usize];
+ while (left < right) {
+ const mid : i32 = left + ((right - left) / 2);
+ const order : i32 = cmp(pivot, WORK[mid as usize]);
+ if (order < 0) { right = mid; } else { left = mid + 1; }
+ }
+ var p : i32 = start;
+ while (p > left) {
+ WORK[p as usize] = WORK[(p - 1) as usize];
+ p = p - 1;
+ }
+ WORK[left as usize] = pivot;
+ start = start + 1;
+ }
+ return 0;
+}
+
+fn min_run_length(n0: i32) -> i32 {
+ var n : i32 = n0;
+ var r : i32 = 0;
+ while (n >= 64) {
+ r = r | (n & 1);
+ n = n / 2;
+ }
+ return n + r;
+}
+
+/// Array.prototype.sort of ORDER[0..n) with cmp, as V8 runs it.
+fn timsort(n: i32) -> u32 {
+ var i : i32 = 0;
+ while (i < n) {
+ WORK[i as usize] = i as u32;
+ i = i + 1;
+ }
+ RUNS = 0;
+ MIN_GALLOP = 7;
+ if (n >= 2) {
+ var low : i32 = 0;
+ var remaining : i32 = n;
+ const min_run : i32 = min_run_length(remaining);
+ while (remaining != 0) {
+ var cur : i32 = count_and_make_run(low, low + remaining);
+ if (cur < min_run) {
+ var forced : i32 = min_run;
+ if (remaining < forced) { forced = remaining; }
+ binary_insertion_sort(low, low + cur, low + forced);
+ cur = forced;
+ }
+ RUN_BASE[RUNS as usize] = low;
+ RUN_LEN[RUNS as usize] = cur;
+ RUNS = RUNS + 1;
+ merge_collapse();
+ low = low + cur;
+ remaining = remaining - cur;
+ }
+ merge_force_collapse();
+ }
+ i = 0;
+ while (i < n) {
+ ORDER[i as usize] = WORK[i as usize];
+ i = i + 1;
+ }
+ return 0;
+}
+
+// --- the data -------------------------------------------------------------------------------------
+
+/// d.lanes[lane].vector is an array of numbers (tool.js reads v[0..4]).
+fn lane_vec(d: []const u8, r: i64, lane: []const u8) -> i64 {
+ return obj_get(d, obj_get(d, obj_get(d, r, "lanes"), lane), "vector");
+}
+
+fn data_ok(d: []const u8, r: i64) -> bool {
+ if (kind(d, r) != J_OBJECT) { return false; }
+ if (kind(d, obj_get(d, r, "prs")) != J_ARRAY) { return false; }
+ if (kind(d, lane_vec(d, r, "bee")) != J_ARRAY or kind(d, lane_vec(d, r, "human")) != J_ARRAY) { return false; }
+ if (nullish(d, obj_get(d, obj_get(d, r, "lanes"), "bee"))) { return false; }
+ return true;
+}
+
+/// Does verdict(d) throw? stamp(last) calls .replace on the greatest stamp of the removed-assert bee pull
+/// requests (by String()); that throws when it is a truthy value that is no string.
+fn verdict_throws(d: []const u8) -> bool {
+ var have : bool = false;
+ var last : [96]u8 = [0; 96];
+ var ll : u32 = 0;
+ var bad_kind : bool = false;
+ var missing : bool = false;
+ var i : u32 = 0;
+ while (i < NPR) {
+ const sp : i64 = PRS[i as usize];
+ const bv : i64 = obj_get(d, sp, "badge");
+ if (jv_is(d, obj_get(d, sp, "lane"), "bee") and kind(d, bv) == J_NUMBER and jv_num(d, bv) == 3.0) {
+ const mv : i64 = obj_get(d, sp, "merged");
+ if (kind(d, mv) == J_NONE) {
+ missing = true;
+ } else {
+ var cand : [96]u8 = [0; 96];
+ const cl : u32 = jv_str(cand[0..], 0, d, mv);
+ if (!have or str_gt(cand[0..], cl, last[0..], ll)) {
+ have = true;
+ bad_kind = jv_truthy(d, mv) and kind(d, mv) != J_STRING;
+ var z : u32 = 0;
+ while (z < cl and z < 96) {
+ last[z as usize] = cand[z as usize];
+ z = z + 1;
+ }
+ ll = z;
+ }
+ }
+ }
+ i = i + 1;
+ }
+ if (missing) { return false; }
+ return have and bad_kind;
+}
+
+pub fn w_data(i: u32, s: []const u8) -> u32 {
+ if (s.len > (DATA_CAP as usize)) {
+ DATA_STATE = 2;
+ return 0;
+ }
+ var k : usize = 0;
+ while (k < s.len) {
+ DATA[k] = s[k];
+ k = k + 1;
+ }
+ DATA_LEN = s.len as u32;
+ DATA_STATE = 1;
+ const d : []const u8 = DATA[0..(DATA_LEN as usize)];
+ if (!jv_valid(d)) {
+ DATA_STATE = 2;
+ return 0;
+ }
+ const r : i64 = root(d);
+ if (!data_ok(d, r)) {
+ DATA_STATE = 2;
+ return 0;
+ }
+ // the span of every pull request, once (arr_at from the start each time would be quadratic)
+ const arr : i64 = obj_get(d, r, "prs");
+ const hi : usize = span_hi(arr);
+ var p : usize = skip_ws(d, span_lo(arr) + 1);
+ var n : u32 = 0;
+ while (p < hi - 1 and n < PR_CAP) {
+ const e : usize = value_end(d, p);
+ if (e > hi) { break; }
+ const sp : i64 = jspan(p, e);
+ if (nullish(d, sp)) {
+ DATA_STATE = 2;
+ return 0;
+ }
+ PRS[n as usize] = sp;
+ KB[n as usize] = jv_num(d, obj_get(d, sp, "badge"));
+ const mv : i64 = obj_get(d, sp, "merged");
+ KM[n as usize] = jv_num(d, mv);
+ KK[n as usize] = 0;
+ KL[n as usize] = 0;
+ if (kind(d, mv) == J_STRING) {
+ KK[n as usize] = 1;
+ var tmp : [64]u8 = [0; 64];
+ var l : u32 = jv_str_to(tmp[0..], 0, d, mv);
+ if (l > 32) { l = 32; }
+ var q : u32 = 0;
+ while (q < l) {
+ KS[(n * 32 + q) as usize] = tmp[q as usize];
+ q = q + 1;
+ }
+ KL[n as usize] = l;
+ }
+ n = n + 1;
+ p = skip_ws(d, e);
+ if (p < hi and d[p] == 44) { p = skip_ws(d, p + 1); }
+ }
+ NPR = n;
+ if (verdict_throws(d)) {
+ DATA_STATE = 2;
+ return 0;
+ }
+ timsort(n as i32);
+ refilter(d);
+ return 0;
+}
+
+pub fn w_data_failed(i: u32) -> u32 {
+ DATA_STATE = 2;
+ return 0;
+}
+
+/// render(): the rows after the filters; every row closed and without a body again.
+fn refilter(d: []const u8) -> u32 {
+ var n : u32 = 0;
+ var i : u32 = 0;
+ while (i < NPR) {
+ const pi : u32 = ORDER[i as usize];
+ const sp : i64 = PRS[pi as usize];
+ var ok_l : bool = true;
+ var ok_b : bool = true;
+ if (LANE >= 0) { ok_l = jv_is(d, obj_get(d, sp, "lane"), K_LANES[LANE as usize]); }
+ if (BADGE >= 0) {
+ const bv : i64 = obj_get(d, sp, "badge");
+ ok_b = kind(d, bv) == J_NUMBER and jv_num(d, bv) == (BADGE as f64);
+ }
+ if (ok_l and ok_b) {
+ ROWS[n as usize] = pi;
+ n = n + 1;
+ }
+ i = i + 1;
+ }
+ NROWS = n;
+ var j : u32 = 0;
+ while (j < PR_CAP) {
+ ROW_OPEN[j as usize] = false;
+ ROW_BODY[j as usize] = false;
+ j = j + 1;
+ }
+ return 0;
+}
+
+pub fn w_click(i: u32) -> u32 {
+ if (DATA_STATE != 1) { return 0; }
+ const d : []const u8 = DATA[0..(DATA_LEN as usize)];
+ if (i < 3) {
+ LANE = (i as i32) - 1;
+ SHOWN = PAGE;
+ refilter(d);
+ }
+ if (i >= 3 and i < 8) {
+ if (i == 3) { BADGE = 0 - 1; } else { BADGE = 7 - (i as i32); }
+ SHOWN = PAGE;
+ refilter(d);
+ }
+ if (i == 8) {
+ SHOWN = SHOWN + PAGE;
+ refilter(d);
+ }
+ return 0;
+}
+
+/// Would prBody(p) in tool.js get to its end? (Else its toggle handler throws and the row stays open and empty.)
+fn body_ok(d: []const u8, p: i64) -> bool {
+ const mv : i64 = obj_get(d, p, "merged");
+ if (jv_truthy(d, mv) and kind(d, mv) != J_STRING) { return false; }
+ if (nullish(d, obj_get(d, p, "counts"))) { return false; }
+ const lines : i64 = obj_get(d, p, "lines");
+ if (kind(d, lines) != J_ARRAY) { return false; }
+ const lc : u32 = arr_len(d, lines);
+ var i : u32 = 0;
+ while (i < lc) {
+ const l : i64 = arr_at(d, lines, i);
+ if (nullish(d, l)) { return false; }
+ if (kind(d, obj_get(d, l, "t")) != J_STRING) { return false; }
+ i = i + 1;
+ }
+ if (lc > 0 and nullish(d, obj_get(d, p, "files"))) { return false; }
+ return true;
+}
+
+pub fn w_toggle(i: u32, open: u32) -> u32 {
+ if (DATA_STATE != 1) { return 0; }
+ var shown_rows : u32 = NROWS;
+ if (SHOWN < shown_rows) { shown_rows = SHOWN; }
+ const o : bool = open != 0;
+ if (i < shown_rows) {
+ var changed : bool = ROW_OPEN[i as usize] != o;
+ ROW_OPEN[i as usize] = o;
+ if (o and !ROW_BODY[i as usize]) {
+ const dd : []const u8 = DATA[0..(DATA_LEN as usize)];
+ if (body_ok(dd, PRS[ROWS[i as usize] as usize])) {
+ ROW_BODY[i as usize] = true;
+ changed = true;
+ }
+ }
+ if (changed) { return 1; }
+ return 0;
+ }
+ if (i == shown_rows) {
+ if (RULE_OPEN != o) {
+ RULE_OPEN = o;
+ return 1;
+ }
+ }
+ return 0;
+}
+
+// --- the page -------------------------------------------------------------------------------------
+
+/// stamp(iso): '' for a falsy value, else the first T as a space and a trailing :dd Z cut, as raw bytes.
+fn stamp_raw(out: []u8, d: []const u8, sp: i64) -> u32 {
+ if (!jv_truthy(d, sp)) { return 0; }
+ var l : u32 = jv_str(out, 0, d, sp);
+ var k : u32 = 0;
+ var done : bool = false;
+ while (k < l and !done) {
+ if (out[k as usize] == 84) {
+ out[k as usize] = 32;
+ done = true;
+ }
+ k = k + 1;
+ }
+ if (l >= 4 and out[(l - 1) as usize] == 90 and out[(l - 4) as usize] == 58 and out[(l - 3) as usize] >= 48 and out[(l - 3) as usize] <= 57 and out[(l - 2) as usize] >= 48 and out[(l - 2) as usize] <= 57) {
+ l = l - 4;
+ }
+ return l;
+}
+
+fn put_esc(dst: []u8, n0: u32, s: []const u8, l: u32) -> u32 {
+ var n : u32 = n0;
+ var i : u32 = 0;
+ while (i < l) {
+ n = put_escaped(dst, n, s[i as usize]);
+ i = i + 1;
+ }
+ return n;
+}
+
+fn lane_index(d: []const u8, sp: i64) -> i32 {
+ var i : usize = 0;
+ while (i < K_LANES.len) {
+ if (jv_is(d, sp, K_LANES[i])) { return i as i32; }
+ i = i + 1;
+ }
+ return 0 - 1;
+}
+
+/// laneName(lane): the word for a known lane, else the data's own text.
+fn lane_name(dst: []u8, n0: u32, d: []const u8, sp: i64) -> u32 {
+ const ix : i32 = lane_index(d, sp);
+ if (ix >= 0) { return text(dst, n0, SAY_LANE_NAMES[ix as usize]); }
+ return jv_kid(dst, n0, d, sp);
+}
+
+/// The array index a value names as a property key (arr[v]): an integer number, or a canonical decimal string; -1 else.
+fn key_index(d: []const u8, sp: i64) -> i64 {
+ const k : u8 = kind(d, sp);
+ if (k == J_NUMBER) {
+ const v : f64 = jv_num(d, sp);
+ if (v >= 0.0 and v < 4294967295.0 and v == js_floor(v)) { return v as i64; }
+ return 0 - 1;
+ }
+ if (k == J_STRING) {
+ const l : u32 = str_len(d, sp);
+ if (l == 0 or l > 9) { return 0 - 1; }
+ var v2 : i64 = 0;
+ var i : u32 = 0;
+ while (i < l) {
+ const c : u8 = str_byte(d, sp, i);
+ if (c < 48 or c > 57) { return 0 - 1; }
+ if (i == 0 and c == 48 and l > 1) { return 0 - 1; }
+ v2 = v2 * 10 + ((c - 48) as i64);
+ i = i + 1;
+ }
+ return v2;
+ }
+ return 0 - 1;
+}
+
+fn badge_span(dst: []u8, n0: u32, d: []const u8, b: i64) -> u32 {
+ var n : u32 = raw(dst, n0, "");
+ const bi : i64 = key_index(d, b);
+ if (bi >= 0 and bi < (SAY_BADGES.len as i64)) { n = text(dst, n, SAY_BADGES[bi as usize]); }
+ return raw(dst, n, "");
+}
+
+fn chip_span(dst: []u8, n0: u32, d: []const u8, lane: i64) -> u32 {
+ var n : u32 = raw(dst, n0, "");
+ n = lane_name(dst, n, d, lane);
+ return raw(dst, n, "");
+}
+
+fn vnum(d: []const u8, v: i64, ix: u32) -> i64 {
+ return arr_at(d, v, ix);
+}
+
+fn lane_card(dst: []u8, n0: u32, d: []const u8, r: i64, li: u32) -> u32 {
+ const L : i64 = obj_get(d, obj_get(d, r, "lanes"), K_LANES[li as usize]);
+ const v : i64 = obj_get(d, L, "vector");
+ var n : u32 = raw(dst, n0, "
");
+ n = text(dst, n, SAY_LANE_NAMES[li as usize]);
+ n = raw(dst, n, "
");
+ n = text(dst, n, SAY_LANE_LINES[li as usize]);
+ n = raw(dst, n, "
");
+ n = jv_text(dst, n, d, vnum(d, v, 4));
+ n = raw(dst, n, " / ");
+ n = jv_text(dst, n, d, vnum(d, v, 0));
+ n = raw(dst, n, "");
+ n = text(dst, n, SAY_REMOVED_OF);
+ n = raw(dst, n, "; ");
+ n = jv_text(dst, n, d, vnum(d, v, 0));
+ n = put(dst, n, 32);
+ n = text(dst, n, SAY_MERGED);
+ n = raw(dst, n, "
1) { n = raw(dst, n, ", "); }
+ n = text(dst, n, SAY_BADGES[(q - 1) as usize]);
+ n = put(dst, n, 32);
+ n = jv_text(dst, n, d, vnum(d, v, q));
+ q = q + 1;
+ }
+ n = raw(dst, n, "\">");
+ q = 4;
+ while (q >= 1) {
+ n = raw(dst, n, "");
+ q = q - 1;
+ }
+ n = raw(dst, n, "
");
+ q = 4;
+ while (q >= 1) {
+ n = raw(dst, n, "
");
+ n = text(dst, n, SAY_BADGES[(q - 1) as usize]);
+ n = raw(dst, n, "");
+ n = jv_text(dst, n, d, vnum(d, v, q));
+ n = raw(dst, n, "
");
+ q = q - 1;
+ }
+ n = raw(dst, n, "
");
+ var s : usize = 0;
+ while (s < SAY_STAT_NAMES.len) {
+ n = raw(dst, n, "
");
+ n = text(dst, n, SAY_STAT_NAMES[s]);
+ n = raw(dst, n, "
");
+ if (s == 0) { n = jv_text(dst, n, d, obj_get(d, L, "checksAdded")); }
+ if (s == 1) { n = jv_text(dst, n, d, obj_get(d, L, "checksRemoved")); }
+ if (s == 2) { n = jv_text(dst, n, d, obj_get(d, L, "testsRemoved")); }
+ if (s == 3) { n = jv_text(dst, n, d, obj_get(d, L, "expect")); }
+ if (s == 4) { n = jv_text(dst, n, d, obj_get(d, L, "loosened")); }
+ n = raw(dst, n, "
");
+ s = s + 1;
+ }
+ return raw(dst, n, "
");
+}
+
+fn verdict_html(dst: []u8, n0: u32, d: []const u8, r: i64) -> u32 {
+ // bad = bee pull requests with badge === 3 ; last = the greatest of their stamps, as .sort().pop() gives it
+ var bad : u32 = 0;
+ var have_last : bool = false;
+ var last_missing : bool = false;
+ var last_null : bool = false;
+ var last : [64]u8 = [0; 64];
+ var ll : u32 = 0;
+ var i : u32 = 0;
+ while (i < NPR) {
+ const sp : i64 = PRS[i as usize];
+ const bv : i64 = obj_get(d, sp, "badge");
+ if (jv_is(d, obj_get(d, sp, "lane"), "bee") and kind(d, bv) == J_NUMBER and jv_num(d, bv) == 3.0) {
+ bad = bad + 1;
+ const mv : i64 = obj_get(d, sp, "merged");
+ if (kind(d, mv) == J_NONE) {
+ last_missing = true;
+ } else {
+ var cand : [96]u8 = [0; 96];
+ const cl : u32 = jv_str(cand[0..], 0, d, mv);
+ if (!have_last or str_gt(cand[0..], cl, last[0..], ll)) {
+ have_last = true;
+ last_null = kind(d, mv) == J_NULL;
+ var z : u32 = 0;
+ while (z < cl and z < 64) {
+ last[z as usize] = cand[z as usize];
+ z = z + 1;
+ }
+ ll = z;
+ }
+ }
+ }
+ i = i + 1;
+ }
+ var n : u32 = n0;
+ if (bad == 0) {
+ n = raw(dst, n, "
");
+ n = text(dst, n, SAY_VERDICT_NO);
+ return raw(dst, n, "
");
+ // after = bee pull requests whose stamp is a string greater than last
+ var after : u32 = 0;
+ if (have_last and !last_missing and !last_null) {
+ i = 0;
+ while (i < NPR) {
+ const sp2 : i64 = PRS[i as usize];
+ const mv2 : i64 = obj_get(d, sp2, "merged");
+ if (jv_is(d, obj_get(d, sp2, "lane"), "bee") and kind(d, mv2) == J_STRING) {
+ var c2 : [96]u8 = [0; 96];
+ const c2l : u32 = jv_str(c2[0..], 0, d, mv2);
+ if (str_gt(c2[0..], c2l, last[0..], ll)) { after = after + 1; }
+ }
+ i = i + 1;
+ }
+ }
+ var lb : [256]u8 = [0; 256];
+ var lt : [200]u8 = [0; 200];
+ var ltl : u32 = 0;
+ if (!last_missing and !last_null) {
+ // stamp(last): last is a string here
+ var tmp : [96]u8 = [0; 96];
+ var z2 : u32 = 0;
+ while (z2 < ll) {
+ tmp[z2 as usize] = last[z2 as usize];
+ z2 = z2 + 1;
+ }
+ var done : bool = false;
+ var k : u32 = 0;
+ while (k < ll and !done) {
+ if (tmp[k as usize] == 84) {
+ tmp[k as usize] = 32;
+ done = true;
+ }
+ k = k + 1;
+ }
+ var l2 : u32 = ll;
+ if (l2 >= 4 and tmp[(l2 - 1) as usize] == 90 and tmp[(l2 - 4) as usize] == 58 and tmp[(l2 - 3) as usize] >= 48 and tmp[(l2 - 3) as usize] <= 57 and tmp[(l2 - 2) as usize] >= 48 and tmp[(l2 - 2) as usize] <= 57) { l2 = l2 - 4; }
+ var y : u32 = 0;
+ while (y < l2) {
+ lt[y as usize] = tmp[y as usize];
+ y = y + 1;
+ }
+ ltl = l2;
+ }
+ var ln : u32 = jv_vput_str(lb[0..], 0, "last", lt[0..(ltl as usize)]);
+ ln = jv_vput_f64(lb[0..], ln, "after", after as f64);
+ n = jv_fill(dst, n, SAY_VERDICT_LAST, lb[0..], ln, false);
+ return raw(dst, n, "
");
+}
+
+/// a > b as strings (UTF-16 order for BMP text, the bytes here).
+fn str_gt(a: []const u8, al: u32, b: []const u8, bl: u32) -> bool {
+ var m : u32 = al;
+ if (bl < m) { m = bl; }
+ var i : u32 = 0;
+ while (i < m) {
+ if (a[i as usize] != b[i as usize]) { return a[i as usize] > b[i as usize]; }
+ i = i + 1;
+ }
+ return al > bl;
+}
+
+fn same_key(d: []const u8, a: i64, b: i64) -> bool {
+ const ka : u8 = kind(d, a);
+ if (ka != kind(d, b)) { return false; }
+ if (ka == J_NUMBER) {
+ const x : f64 = jv_num(d, a);
+ const y : f64 = jv_num(d, b);
+ return x == y or (x != x and y != y);
+ }
+ if (ka == J_STRING) {
+ const l : u32 = str_len(d, a);
+ if (l != str_len(d, b)) { return false; }
+ var k : u32 = 0;
+ while (k < l) {
+ if (str_byte(d, a, k) != str_byte(d, b, k)) { return false; }
+ k = k + 1;
+ }
+ return true;
+ }
+ return ka != J_OBJECT and ka != J_ARRAY and ka != J_NONE;
+}
+
+/// replace(/^\s+/, '') then the rest of the string t as escaped text.
+fn trimmed_text(dst: []u8, n0: u32, d: []const u8, sp: i64) -> u32 {
+ if (kind(d, sp) != J_STRING) { return n0; }
+ var tmp : [4096]u8 = [0; 4096];
+ const l : u32 = jv_str_to(tmp[0..], 0, d, sp);
+ var lo : u32 = 0;
+ var w : u32 = 1;
+ while (lo < l and w > 0) {
+ w = jv_ws_at(tmp[0..], lo, l);
+ lo = lo + w;
+ }
+ var n : u32 = n0;
+ var q : u32 = lo;
+ while (q < l) {
+ n = put_escaped(dst, n, tmp[q as usize]);
+ q = q + 1;
+ }
+ return n;
+}
+
+fn tag_word(dst: []u8, n0: u32, d: []const u8, g: i64) -> u32 {
+ var i : usize = 0;
+ while (i < K_TAGS.len) {
+ if (jv_is(d, g, K_TAGS[i])) { return text(dst, n0, SAY_TAGS[i]); }
+ i = i + 1;
+ }
+ return jv_kid(dst, n0, d, g);
+}
+
+fn body_html(dst: []u8, n0: u32, d: []const u8, p: i64) -> u32 {
+ var n : u32 = raw(dst, n0, "
");
+ var sb : [1100]u8 = [0; 1100];
+ const sl : u32 = stamp_raw(sb[0..], d, obj_get(d, p, "merged"));
+ n = put_esc(dst, n, sb[0..], sl);
+ n = raw(dst, n, ", ");
+ n = jv_text(dst, n, d, obj_get(d, p, "author"));
+ n = raw(dst, n, ", ");
+ n = jv_text(dst, n, d, obj_get(d, p, "branch"));
+ n = raw(dst, n, ", ");
+ const cls : i64 = obj_get(d, p, "cls");
+ const ci : i64 = key_index(d, cls);
+ if (ci >= 0 and ci < (SAY_CLASSES.len as i64)) {
+ n = text(dst, n, SAY_CLASSES[ci as usize]);
+ } else {
+ n = raw(dst, n, "undefined");
+ }
+ n = raw(dst, n, "
");
+ var vb : [512]u8 = [0; 512];
+ var vn : u32 = jv_vput_val(vb[0..], 0, "n", d, obj_get(d, p, "testFiles"));
+ vn = jv_vput_val(vb[0..], vn, "all", d, obj_get(d, p, "allFiles"));
+ n = jv_fill(dst, n, SAY_TEST_FILES, vb[0..], vn, false);
+ n = raw(dst, n, "; ");
+ const c : i64 = obj_get(d, p, "counts");
+ vn = jv_vput_nn0(vb[0..], 0, "added", d, obj_get(d, c, "checksAdded"));
+ vn = jv_vput_nn0(vb[0..], vn, "removed", d, obj_get(d, c, "checksRemoved"));
+ vn = jv_vput_nn0(vb[0..], vn, "blocks", d, obj_get(d, c, "testsRemoved"));
+ n = jv_fill(dst, n, SAY_CHECKS, vb[0..], vn, false);
+ n = raw(dst, n, "
");
+ const lines : i64 = obj_get(d, p, "lines");
+ const lc : u32 = arr_len(d, lines);
+ if (lc > 0) {
+ // group by file in first-seen order
+ var done : [128]bool = [false; 128];
+ var a : u32 = 0;
+ while (a < lc and a < 128) {
+ if (!done[a as usize]) {
+ const la : i64 = arr_at(d, lines, a);
+ const fa : i64 = obj_get(d, la, "f");
+ n = raw(dst, n, "
");
+ const fidx : i64 = key_index(d, fa);
+ if (fidx >= 0) {
+ const fe : i64 = arr_at(d, obj_get(d, p, "files"), fidx as u32);
+ n = nn_text(dst, n, d, obj_get(d, fe, "p"));
+ }
+ n = raw(dst, n, "
");
+ var b : u32 = a;
+ while (b < lc and b < 128) {
+ if (!done[b as usize]) {
+ const lb : i64 = arr_at(d, lines, b);
+ if (same_key(d, fa, obj_get(d, lb, "f"))) {
+ done[b as usize] = true;
+ const sgn : i64 = obj_get(d, lb, "s");
+ n = raw(dst, n, "
");
+ n = tag_word(dst, n, d, obj_get(d, lb, "g"));
+ n = raw(dst, n, "");
+ n = jv_text(dst, n, d, sgn);
+ n = put(dst, n, 32);
+ n = trimmed_text(dst, n, d, obj_get(d, lb, "t"));
+ n = raw(dst, n, "");
+ if (jv_truthy(d, obj_get(d, lb, "b"))) {
+ n = raw(dst, n, "");
+ n = jv_text(dst, n, d, obj_get(d, lb, "b"));
+ n = raw(dst, n, "");
+ }
+ n = raw(dst, n, "
");
+ }
+ }
+ b = b + 1;
+ }
+ n = raw(dst, n, "
");
+ }
+ a = a + 1;
+ }
+ } else {
+ n = raw(dst, n, "
");
+ n = text(dst, n, SAY_NO_LINES);
+ n = raw(dst, n, "
");
+ }
+ if (jv_truthy(d, obj_get(d, p, "more"))) {
+ n = raw(dst, n, "
");
+ var mb : [128]u8 = [0; 128];
+ const mn : u32 = jv_vput_val(mb[0..], 0, "n", d, obj_get(d, p, "more"));
+ n = jv_fill(dst, n, SAY_MORE_LINES, mb[0..], mn, false);
+ n = raw(dst, n, "
");
+ }
+ if (jv_truthy(d, obj_get(d, p, "ciGate"))) {
+ n = raw(dst, n, "
");
+ n = text(dst, n, SAY_CI_GATE);
+ n = raw(dst, n, "
");
+ }
+ if (jv_truthy(d, obj_get(d, p, "apiOnly"))) {
+ n = raw(dst, n, "
");
+ var ab : [128]u8 = [0; 128];
+ const an : u32 = jv_vput_val(ab[0..], 0, "n", d, obj_get(d, p, "apiOnly"));
+ n = jv_fill(dst, n, SAY_API_ONLY, ab[0..], an, false);
+ n = raw(dst, n, "
");
+}
+
+/// A pool entry for `{key: x ?? 0}`.
+fn jv_vput_nn0(vb: []u8, n0: u32, key: []const u8, d: []const u8, sp: i64) -> u32 {
+ if (nullish(d, sp)) { return jv_vput_str(vb, n0, key, "0"); }
+ return jv_vput_val(vb, n0, key, d, sp);
+}
+
+fn nn_text(dst: []u8, n0: u32, d: []const u8, sp: i64) -> u32 {
+ if (nullish(d, sp)) { return n0; }
+ return jv_text(dst, n0, d, sp);
+}
+
+fn row_html(dst: []u8, n0: u32, d: []const u8, ri: u32) -> u32 {
+ const p : i64 = PRS[ROWS[ri as usize] as usize];
+ var n : u32 = raw(dst, n0, "");
+ n = badge_span(dst, n, d, obj_get(d, p, "badge"));
+ n = put(dst, n, 32);
+ n = chip_span(dst, n, d, obj_get(d, p, "lane"));
+ n = raw(dst, n, " #");
+ n = jv_text(dst, n, d, obj_get(d, p, "n"));
+ n = raw(dst, n, "");
+ const tt : i64 = obj_get(d, p, "title");
+ if (jv_truthy(d, tt)) { n = jv_text(dst, n, d, tt); } else { n = text(dst, n, SAY_NO_TITLE); }
+ n = raw(dst, n, "");
+ if (ROW_BODY[ri as usize]) { n = body_html(dst, n, d, p); }
+ return raw(dst, n, "");
+}
+
+fn filter_buttons(dst: []u8, n0: u32, which: u32) -> u32 {
+ var n : u32 = n0;
+ if (which == 0) {
+ var i : i32 = 0 - 1;
+ while (i < 2) {
+ n = raw(dst, n, "");
+ i = i + 1;
+ }
+ } else {
+ var j : i32 = 0 - 1;
+ while (j < 4) {
+ var b : i32 = j;
+ if (j >= 0) { b = 3 - j; }
+ n = raw(dst, n, "");
+ j = j + 1;
+ }
+ }
+ return n;
+}
+
+fn bullets_rule(dst: []u8, n0: u32) -> u32 {
+ var n : u32 = raw(dst, n0, "
");
+ var i : usize = 0;
+ while (i < SAY_RULE.len) {
+ n = raw(dst, n, "
");
+ n = text(dst, n, SAY_RULE[i]);
+ n = raw(dst, n, "
");
+ i = i + 1;
+ }
+ return raw(dst, n, "
");
+}
+
+fn bullets_pos(dst: []u8, n0: u32) -> u32 {
+ var n : u32 = raw(dst, n0, "
");
+ var i : usize = 0;
+ while (i < SAY_FALSE_POS.len) {
+ n = raw(dst, n, "
");
+ n = text(dst, n, SAY_FALSE_POS[i]);
+ n = raw(dst, n, "
");
+ i = i + 1;
+ }
+ return raw(dst, n, "
");
+}
+
+fn bullets_neg(dst: []u8, n0: u32) -> u32 {
+ var n : u32 = raw(dst, n0, "
");
+ var i : usize = 0;
+ while (i < SAY_FALSE_NEG.len) {
+ n = raw(dst, n, "
");
+ n = text(dst, n, SAY_FALSE_NEG[i]);
+ n = raw(dst, n, "
");
+ i = i + 1;
+ }
+ return raw(dst, n, "
");
+}
+
+pub fn w_render(dst: []u8) -> u32 {
+ var n : u32 = 0;
+ if (DATA_STATE == 0) {
+ n = raw(dst, n, "
");
+ n = text(dst, n, SAY_LOADING);
+ n = raw(dst, n, "
");
+ if (!ok(n)) { return 0; }
+ return n;
+ }
+ if (DATA_STATE == 2) {
+ n = raw(dst, n, "
");
+ n = text(dst, n, SAY_LOAD_FAILED);
+ n = raw(dst, n, "
");
+ if (!ok(n)) { return 0; }
+ return n;
+ }
+ const d : []const u8 = DATA[0..(DATA_LEN as usize)];
+ const r : i64 = root(d);
+ n = raw(dst, n, "
");
+ var vb : [1200]u8 = [0; 1200];
+ var st : [1100]u8 = [0; 1100];
+ const stl : u32 = stamp_raw(st[0..], d, obj_get(d, r, "snapshotAt"));
+ var vn : u32 = jv_vput_str(vb[0..], 0, "at", st[0..(stl as usize)]);
+ vn = jv_vput_val(vb[0..], vn, "days", d, obj_get(d, r, "days"));
+ vn = jv_vput_val(vb[0..], vn, "repo", d, obj_get(d, r, "repo"));
+ n = jv_fill(dst, n, SAY_SNAPSHOT, vb[0..], vn, false);
+ n = raw(dst, n, "
");
+ n = verdict_html(dst, n, d, r);
+ n = raw(dst, n, "
");
+ n = lane_card(dst, n, d, r, 0);
+ n = lane_card(dst, n, d, r, 1);
+ n = raw(dst, n, "
");
+ n = text(dst, n, SAY_FILTER_LANE);
+ n = raw(dst, n, "");
+ n = filter_buttons(dst, n, 0);
+ n = raw(dst, n, "
");
+ n = text(dst, n, SAY_FILTER_BADGE);
+ n = raw(dst, n, "");
+ n = filter_buttons(dst, n, 1);
+ n = raw(dst, n, "
");
+ var cb : [128]u8 = [0; 128];
+ var cn : u32 = jv_vput_f64(cb[0..], 0, "n", NROWS as f64);
+ cn = jv_vput_f64(cb[0..], cn, "total", NPR as f64);
+ n = jv_fill(dst, n, SAY_SHOWING, cb[0..], cn, false);
+ n = raw(dst, n, "
");
+ var shown : u32 = NROWS;
+ if (SHOWN < shown) { shown = SHOWN; }
+ var i : u32 = 0;
+ while (i < shown) {
+ n = row_html(dst, n, d, i);
+ i = i + 1;
+ }
+ n = raw(dst, n, "
");
+ n = text(dst, n, SAY_RULE_TITLE);
+ n = raw(dst, n, "");
+ n = bullets_rule(dst, n);
+ n = raw(dst, n, "
");
+ n = text(dst, n, SAY_FALSE_POS_TITLE);
+ n = raw(dst, n, "
");
+ n = bullets_pos(dst, n);
+ n = raw(dst, n, "
");
+ n = text(dst, n, SAY_FALSE_NEG_TITLE);
+ n = raw(dst, n, "
");
+ n = bullets_neg(dst, n);
+ n = raw(dst, n, "
");
+ n = text(dst, n, SAY_NOT_INTENT);
+ n = raw(dst, n, "
");
+ var eb : [128]u8 = [0; 128];
+ const ex : i64 = obj_get(d, obj_get(d, r, "excluded"), "notMaster");
+ var en : u32 = 0;
+ if (nullish(d, ex)) { en = jv_vput_str(eb[0..], 0, "notMaster", "0"); } else { en = jv_vput_val(eb[0..], 0, "notMaster", d, ex); }
+ n = jv_fill(dst, n, SAY_EXCLUDED, eb[0..], en, false);
+ n = raw(dst, n, "
");
+ if (!ok(n)) { return 0; }
+ return n;
+}
+
+/// The splicer follows only the names a module itself mentions, not the names the imported functions
+/// mention. This anchor mentions every helper the writer, the JSON reader and the number rules call.
+/// Never called.
+pub fn imports_anchor(o: []u8, s: []const u8) -> u32 {
+ var n : u32 = 0;
+ n = n + (ok(0) as u32);
+ n = n + OVER;
+ n = n + put(o, 0, 0);
+ n = n + raw(o, 0, s);
+ n = n + put_escaped(o, 0, 0);
+ n = n + text(o, 0, s);
+ n = n + num(o, 0, 0);
+ n = n + attr(o, 0, s, s);
+ n = n + fill(o, 0, s, s, s, s, 0);
+ n = n + fill_named(o, 0, s, s, s, s, s, s, s, s, s, s, s, s, s);
+ n = n + dec_to(o, 0);
+ n = n + (name_is(s, 0, 0, s) as u32);
+ n = n + (is_word_byte(0) as u32);
+ n = n + (H_LT as u32);
+ n = n + (H_GT as u32);
+ n = n + (H_AMP as u32);
+ n = n + (H_QUOTE as u32);
+ n = n + (H_APOS as u32);
+ n = n + (H_SPACE as u32);
+ n = n + (H_SLASH as u32);
+ n = n + (H_EQ as u32);
+ n = n + ((NO_SPAN < 0) as u32);
+ n = n + ((SPAN > 0) as u32);
+ n = n + (J_NONE as u32);
+ n = n + (J_OBJECT as u32);
+ n = n + (J_ARRAY as u32);
+ n = n + (J_STRING as u32);
+ n = n + (J_NUMBER as u32);
+ n = n + (J_TRUE as u32);
+ n = n + (J_FALSE as u32);
+ n = n + (J_NULL as u32);
+ n = n + (jspan(0, 0) as u32);
+ n = n + (span_lo(0) as u32);
+ n = n + (span_hi(0) as u32);
+ n = n + (skip_ws(s, 0) as u32);
+ n = n + (string_end(s, 0) as u32);
+ n = n + (value_end(s, 0) as u32);
+ n = n + (root(s) as u32);
+ n = n + (kind(s, 0) as u32);
+ n = n + (key_is(s, 0, 0, s) as u32);
+ n = n + (obj_get(s, 0, s) as u32);
+ n = n + arr_len(s, 0);
+ n = n + (arr_at(s, 0, 0) as u32);
+ n = n + hex4(s, 0);
+ n = n + escape_at(s, 0, 0);
+ n = n + cp_utf8_len(0);
+ n = n + (cp_utf8_byte(0, 0) as u32);
+ n = n + str_len(s, 0);
+ n = n + (str_byte(s, 0, 0) as u32);
+ n = n + JN_OVER;
+ n = n + JN_CAP;
+ n = n + ((JN_TWO52 > 0.0) as u32);
+ n = n + ((JN_TWO53 > 0.0) as u32);
+ n = n + jn_put(o, 0, 0);
+ n = n + jn_raw(o, 0, s);
+ n = n + jn_digits(o, 0);
+ n = n + jn_mul(o, 0, 0);
+ n = n + jn_mul2(o, 0, 0);
+ n = n + jn_mul5(o, 0, 0);
+ n = n + jn_shift(o, 0, 0);
+ n = n + (jn_cmp(s, s, 0) as u32);
+ n = n + (jn_expo(0.0) as u32);
+ n = n + (jn_mant(0.0) as u32);
+ n = n + ((jn_make(0, 0) > 0.0) as u32);
+ n = n + jn_exact(o, 0, 0);
+ n = n + jn_fscale(0);
+ n = n + (js_is_nan(0.0) as u32);
+ n = n + ((js_nan(0.0) > 0.0) as u32);
+ n = n + ((js_inf(1.0) > 0.0) as u32);
+ n = n + ((js_floor(0.0) > 0.0) as u32);
+ n = n + ((js_ceil(0.0) > 0.0) as u32);
+ n = n + ((js_max(0.0, 0.0) > 0.0) as u32);
+ n = n + ((js_min(0.0, 0.0) > 0.0) as u32);
+ n = n + ((js_round(0.0) > 0.0) as u32);
+ n = n + (jn_shortest(o, 1.0) as u32);
+ n = n + jn_copy(o, s, 0);
+ n = n + jn_put_run(o, 0, s, 0, 0);
+ n = n + jn_layout(o, 0, s, 0, 0);
+ n = n + js_str_num(o, 0, 0.0);
+ n = n + js_to_fixed(o, 0, 0.0, 0);
+ n = n + js_locale_num(o, 0, 0.0);
+ n = n + (jn_num_ok(s, 0, 0) as u32);
+ n = n + (jn_cmp_text(s, 0, 0, 0, 0) as u32);
+ n = n + ((js_parse_num(s, 0, 0) > 0.0) as u32);
+ n = n + ((jn_dec_value(s, 0, 0) > 0.0) as u32);
+ n = n + (jn_abs(0) as u32);
+ n = n + js_str_u64(o, 0, 0);
+ n = n + jv_str_to(o, 0, s, 0);
+ n = n + jv_text_str(o, 0, s, 0);
+ n = n + ((jv_numval(s, 0) > 0.0) as u32);
+ n = n + jv_str(o, 0, s, 0);
+ n = n + jv_text(o, 0, s, 0);
+ n = n + (jv_is(s, 0, s) as u32);
+ n = n + (jv_truthy(s, 0) as u32);
+ n = n + (jv_is_ws(0) as u32);
+ n = n + ((jv_number_of_text(s, 0) > 0.0) as u32);
+ n = n + ((jv_num(s, 0) > 0.0) as u32);
+ n = n + jv_vput_str(o, 0, s, s);
+ n = n + jv_vput_f64(o, 0, s, 0.0);
+ n = n + jv_vput_val(o, 0, s, s, 0);
+ n = n + jv_vput_defined(o, 0, s, s, 0);
+ n = n + jv_vput_fixed(o, 0, s, 0.0, 0);
+ n = n + jv_kid(o, 0, s, 0);
+ n = n + jv_find(s, 0, s);
+ n = n + jv_fill(o, 0, s, s, 0, true);
+ n = n + (jv_string_ok(s, 0) as u32);
+ n = n + (jv_number_ok(s, 0) as u32);
+ n = n + (jv_lit_ok(s, 0, s) as u32);
+ n = n + (jv_valid(s) as u32);
+ n = n + jv_ws_at(s, 0, 0);
+ n = n + jv_ws_before(s, 0, 0);
+ n = n + jv_digit_value(0);
+ n = n + jv_fill_raw(o, 0, s, s, 0, true);
+ n = n + jv_fill_with(o, 0, s, s, 0, true, true);
+ return n;
+}
diff --git a/specs/widgets/uart_bucket.t27 b/specs/widgets/uart_bucket.t27
new file mode 100644
index 0000000000..bc169f5915
--- /dev/null
+++ b/specs/widgets/uart_bucket.t27
@@ -0,0 +1,1141 @@
+// SPDX-License-Identifier: Apache-2.0
+; specs/widgets/uart-bucket.t27 -- the bucket under the tap: answers waiting in a 512-byte UART buffer
+; Source of truth for public/widgets/uart-bucket/ (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, and the buffer rule is K_ constants.
+; ASCII only (L3), English only (LANG-EN).
+
+; WHAT IT SHOWS: the Mac keeps a window of requests in flight to the AX7203 node; every answer the
+; node sends back is 19 bytes and passes through a CP2102N USB-UART bridge with a 512-byte receive
+; buffer. The node has no flow control, so while the Mac pauses reading, up to window x 19 bytes of
+; answers wait in that buffer. The reader drags the window and the bucket fills; past 512 bytes it
+; spills. Below it, the measured runs: two 2,016,000-job diagnostic arms (window 64 and window 24)
+; and two single 403,200-job runs at the edge (window 26 and window 30), then what the link carried.
+
+; WHERE THE NUMBERS COME FROM: scripts/widget-data/uart-bucket.mjs reads the record files of
+; gHashTag/trinity-fpga with git show at the commits named in DATA_SOURCES, parses every number,
+; checks the K_ constants below against the diagnostic's own spec, and writes data.json; the page
+; reads only that file. scripts/widget-data/uart-bucket-card.py draws card.png from data.json.
+
+; WHAT IT DOES NOT CLAIM: that the buffer is the cause. The diagnostic's own record says the pattern
+; fits the buffer and that nothing proves it. One USB path, one host, one evening. The model run
+; below used synthetic activation vectors; no text came out of it, and text generation ran on the Mac.
+; phi^2 + 1/phi^2 = 3 | TRINITY
+
+module widget_uart_bucket;
+
+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::fill_named;
+use widgets::html_out::dec_to;
+use widgets::html_out::name_is;
+use widgets::html_out::is_word_byte;
+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::json_scan::NO_SPAN;
+use widgets::json_scan::SPAN;
+use widgets::json_scan::J_NONE;
+use widgets::json_scan::J_OBJECT;
+use widgets::json_scan::J_ARRAY;
+use widgets::json_scan::J_STRING;
+use widgets::json_scan::J_NUMBER;
+use widgets::json_scan::J_TRUE;
+use widgets::json_scan::J_FALSE;
+use widgets::json_scan::J_NULL;
+use widgets::json_scan::jspan;
+use widgets::json_scan::span_lo;
+use widgets::json_scan::span_hi;
+use widgets::json_scan::skip_ws;
+use widgets::json_scan::string_end;
+use widgets::json_scan::value_end;
+use widgets::json_scan::root;
+use widgets::json_scan::kind;
+use widgets::json_scan::key_is;
+use widgets::json_scan::obj_get;
+use widgets::json_scan::arr_len;
+use widgets::json_scan::arr_at;
+use widgets::json_scan::hex4;
+use widgets::json_scan::escape_at;
+use widgets::json_scan::cp_utf8_len;
+use widgets::json_scan::cp_utf8_byte;
+use widgets::json_scan::str_len;
+use widgets::json_scan::str_byte;
+use widgets::js_num::JN_OVER;
+use widgets::js_num::JN_CAP;
+use widgets::js_num::JN_TWO52;
+use widgets::js_num::JN_TWO53;
+use widgets::js_num::jn_put;
+use widgets::js_num::jn_raw;
+use widgets::js_num::jn_digits;
+use widgets::js_num::jn_mul;
+use widgets::js_num::jn_mul2;
+use widgets::js_num::jn_mul5;
+use widgets::js_num::jn_shift;
+use widgets::js_num::jn_cmp;
+use widgets::js_num::jn_expo;
+use widgets::js_num::jn_mant;
+use widgets::js_num::jn_make;
+use widgets::js_num::jn_exact;
+use widgets::js_num::jn_fscale;
+use widgets::js_num::js_is_nan;
+use widgets::js_num::js_nan;
+use widgets::js_num::js_inf;
+use widgets::js_num::js_floor;
+use widgets::js_num::js_ceil;
+use widgets::js_num::js_max;
+use widgets::js_num::js_min;
+use widgets::js_num::js_round;
+use widgets::js_num::jn_shortest;
+use widgets::js_num::jn_copy;
+use widgets::js_num::jn_put_run;
+use widgets::js_num::jn_layout;
+use widgets::js_num::js_str_num;
+use widgets::js_num::js_to_fixed;
+use widgets::js_num::js_locale_num;
+use widgets::js_num::jn_num_ok;
+use widgets::js_num::jn_cmp_text;
+use widgets::js_num::js_parse_num;
+use widgets::js_num::jn_dec_value;
+use widgets::js_num::jn_abs;
+use widgets::js_num::js_str_u64;
+use widgets::js_val::jv_str_to;
+use widgets::js_val::jv_text_str;
+use widgets::js_val::jv_numval;
+use widgets::js_val::jv_str;
+use widgets::js_val::jv_text;
+use widgets::js_val::jv_is;
+use widgets::js_val::jv_truthy;
+use widgets::js_val::jv_is_ws;
+use widgets::js_val::jv_number_of_text;
+use widgets::js_val::jv_num;
+use widgets::js_val::jv_vput_str;
+use widgets::js_val::jv_vput_f64;
+use widgets::js_val::jv_vput_val;
+use widgets::js_val::jv_vput_defined;
+use widgets::js_val::jv_vput_fixed;
+use widgets::js_val::jv_kid;
+use widgets::js_val::jv_find;
+use widgets::js_val::jv_fill;
+use widgets::js_val::jv_string_ok;
+use widgets::js_val::jv_number_ok;
+use widgets::js_val::jv_lit_ok;
+use widgets::js_val::jv_valid;
+use widgets::js_val::jv_ws_at;
+use widgets::js_val::jv_ws_before;
+use widgets::js_val::jv_digit_value;
+use widgets::js_val::jv_fill_raw;
+use widgets::js_val::jv_fill_with;
+
+pub const KIND : str = "widget-tool";
+pub const ID : str = "uart-bucket";
+pub const TITLE : str = "The bucket under the tap: a 512-byte UART buffer and 19-byte answers";
+pub const DESCRIPTION : str = "Drag the number of requests in flight and watch the answers fill a CP2102N's 512-byte receive buffer, then read the measured runs on the AX7203 board.";
+pub const IMAGE_ALT : str = "The bucket under the tap card: a CP2102N receive buffer holds 512 bytes and each answer is 19 bytes, so 26 answers fit. Window 64 (1216 bytes in flight) lost 574 of 2016000 answers in 424.3 s; window 24 (456 bytes) lost 0 in 427.5 s, on the AX7203 board.";
+pub const HOOK : str = "512 bytes of buffer, 19 bytes per answer: 26 fit. Window 64 lost 574 answers in 2,016,000 jobs; window 24 lost none and took the same time.";
+pub const CATEGORY : str = "fpga";
+pub const DATA_SOURCES : [5]str = ["gHashTag/trinity-fpga conformance/UART_LOSS_DIAG.md at commit 5a3111806: the window-64 and window-24 diagnostic arms (2,016,000 jobs each), the 512 / 19 rule, the wire time of the requests and the caveat", "gHashTag/trinity-fpga specs/trinet/uart_loss_diag_ax7203.t27 at commit df78b3249: REQ_LEN, RESP_LEN, CP2102N_RX_BYTES, ARM_WINDOWS and JOBS_PER_PASS, checked against the K_ constants of this spec", "gHashTag/trinity-fpga conformance/TERN_TC_LAYER_RECEIPTS.md at commit e1834ee6e: the --all runs at window 24 (rerun step 1), window 26 and window 30 (rerun step 3), harness tern_tc_layer_ax7203.py", "gHashTag/trinity-fpga conformance/TERN_TC_GENERATE.md at commit b2bd5a30c: the layer run used synthetic activation vectors and generation ran on the Mac", "scripts/widget-data/uart-bucket.mjs parses those files with git show at those commits into public/widgets/uart-bucket/data.json; scripts/widget-data/uart-bucket-card.py draws card.png from it"];
+pub const READS_LOCAL_FILES : bool = false;
+pub const SENDS_NOTHING : bool = true;
+
+; --- The rule --------------------------------------------------------------------------------
+; The buffer and the frame sizes; uart-bucket.mjs fails if they drift from the diagnostic's spec.
+pub const K_RX_BYTES : u16 = 512;
+pub const K_ANSWER_BYTES : u16 = 19;
+pub const K_REQUEST_BYTES : u16 = 24;
+pub const K_LARGEST_FIT : u16 = 26;
+; The two diagnostic arms and the two single runs at the edge.
+pub const K_ARM_WINDOWS : [2]u16 = [64, 24];
+pub const K_EDGE_WINDOWS : [2]u16 = [26, 30];
+; The slider.
+pub const K_WINDOW_MIN : u16 = 1;
+pub const K_WINDOW_MAX : u16 = 64;
+pub const K_WINDOW_START : u16 = 64;
+; One fill-and-drain cycle of the animation, in milliseconds; none when the reader asks for less motion.
+pub const K_CYCLE_MS : u16 = 3600;
+pub const K_DATA_FILE : str = "./data.json";
+
+; --- Words -----------------------------------------------------------------------------------
+; {name} is filled by tool.js from data.json or from the K_ constants; nothing else is.
+pub const SAY_LOADING : str = "Reading the measured runs...";
+pub const SAY_LOAD_FAILED : str = "The data file could not be read.";
+pub const SAY_INTRO : str = "The Mac keeps a window of requests in flight to the {board} node. Every answer coming back is {answer} bytes. While the Mac pauses reading, answers wait in the USB-UART bridge's {rx}-byte receive buffer; the node has no flow control, so nothing tells it to wait.";
+pub const SAY_TAP : str = "node answers, {answer} B each";
+pub const SAY_BUCKET : str = "CP2102N receive buffer";
+pub const SAY_BRIM : str = "{rx} B";
+pub const SAY_DRAIN : str = "the Mac reads";
+pub const SAY_SPILL : str = "lost";
+pub const SAY_WINDOW_LABEL : str = "Requests in flight (window)";
+pub const SAY_FEWER : str = "One request fewer";
+pub const SAY_MORE : str = "One request more";
+pub const SAY_MINUS : str = "-";
+pub const SAY_PLUS : str = "+";
+pub const SAY_READOUT : str = "{w} x {answer} B = {bytes} B of answers can be waiting; the buffer holds {rx} B.";
+pub const SAY_FITS : str = "Fits, {spare} B to spare, however long the Mac pauses.";
+pub const SAY_OVER : str = "Over by {over} B: if the Mac pauses long enough, answer bytes are lost.";
+pub const SAY_RULE : str = "{rx} / {answer} = {ratio}, so {fit} is the largest window that fits.";
+pub const SAY_MOTION_OFF : str = "Animation off: your system asks for reduced motion. The bucket shows the full level.";
+pub const SAY_RUNS_TITLE : str = "Measured on the {board}: {jobs} jobs per arm, one arm each";
+pub const SAY_ARM_HEAD : str = "window {w}";
+pub const SAY_ARM_ROWS : [6]str = ["bytes in flight", "jobs sent", "answers accepted", "answers lost", "loss events", "elapsed"];
+pub const SAY_SECONDS : str = "s";
+pub const SAY_SHOW : str = "Put window {w} in the bucket";
+pub const SAY_SAME_TIME : str = "The smaller window cost no time: {t64} s against {t24} s. The requests are the slow direction: {req} B x 10 bit at {baud} baud x {jobs} jobs = {wire} s on the wire.";
+pub const SAY_EDGE_TITLE : str = "Two single runs at the edge, {planned} jobs planned each";
+pub const SAY_EDGE_PASS : str = "window {w} ({bytes} B): {ok} of {planned} receipts verified, no slip";
+pub const SAY_EDGE_FAIL : str = "window {w} ({bytes} B): stopped at the first slip after {sent} jobs sent; {ok} of {planned} receipts verified";
+pub const SAY_CAVEAT : str = "What this does not show: the diagnostic's own record says the pattern fits the buffer, but nothing proves the buffer is the cause. One USB path (a Genesys hub), one host, one evening; nothing about other hosts or a hub-free path.";
+pub const SAY_CARRIED_TITLE : str = "What the link then carried, at window {w}";
+pub const SAY_CARRIED_LINE : str = "The {board} board multiplied every ternary matrix of a small language model (igla-coder, tern_tc). Every answer carried a signed receipt, checked by the Mac.";
+pub const SAY_CARRIED_ROWS : [6]str = ["ternary matrices", "rows", "ternary weights", "receipts verified", "rows bit-exact", "elapsed"];
+pub const SAY_RATE : str = "{rate} answers/s";
+pub const SAY_CARRIED_PLAIN : str = "Plainly: the inputs were synthetic test vectors and no text came out of this run. Text generation ran on the Mac. Even in later generation runs, the Mac does everything except the ternary dot products.";
+pub const SAY_SOURCES : str = "Every number on this page is parsed from these files in gHashTag/trinity-fpga:";
+pub const SAY_AT : str = "at";
+
+; --- Tests -----------------------------------------------------------------------------------
+test twenty_six_answers_fit {
+ assert K_RX_BYTES / K_ANSWER_BYTES == K_LARGEST_FIT;
+ assert K_LARGEST_FIT * K_ANSWER_BYTES <= K_RX_BYTES;
+ assert K_ANSWER_BYTES * (K_LARGEST_FIT + 1) > K_RX_BYTES;
+ assert K_LARGEST_FIT * K_ANSWER_BYTES == 494;
+}
+
+test the_two_arms_sit_either_side_of_the_buffer {
+ assert K_ARM_WINDOWS[1] * K_ANSWER_BYTES <= K_RX_BYTES;
+ assert K_RX_BYTES < K_ARM_WINDOWS[0] * K_ANSWER_BYTES;
+ assert K_ARM_WINDOWS[0] * K_ANSWER_BYTES == 1216;
+ assert K_ARM_WINDOWS[1] * K_ANSWER_BYTES == 456;
+}
+
+test the_edge_runs_bracket_the_buffer {
+ assert K_EDGE_WINDOWS[0] == K_LARGEST_FIT;
+ assert K_EDGE_WINDOWS[0] * K_ANSWER_BYTES <= K_RX_BYTES;
+ assert K_EDGE_WINDOWS[1] * K_ANSWER_BYTES > K_RX_BYTES;
+ assert K_EDGE_WINDOWS[1] * K_ANSWER_BYTES == 570;
+}
+
+test the_slider_reaches_every_measured_window {
+ assert K_WINDOW_MIN == 1;
+ assert K_WINDOW_MAX >= K_ARM_WINDOWS[0];
+ assert K_WINDOW_MIN <= K_ARM_WINDOWS[1];
+ assert K_WINDOW_START <= K_WINDOW_MAX;
+ assert K_REQUEST_BYTES > K_ANSWER_BYTES;
+}
+
+test the_page_sends_nothing {
+ assert SENDS_NOTHING == true;
+ assert READS_LOCAL_FILES == false;
+ assert K_DATA_FILE == "./data.json";
+ assert CATEGORY == "fpga";
+}
+
+
+; =================================================================================================
+; THE NATIVE PAGE (epic gHashTag/t27#10937, issue #10938, stage "page" of specs/t27f/widget_factory.t27)
+; Everything below replaces public/widgets/uart-bucket/tool.js (228 lines of hand-written JavaScript).
+; The words and numbers above are unchanged. The host protocol is race_the_bee.t27's plus:
+; w_input(i, text) the slider (input 0) now holds text
+; w_click(i) button i in document order: 0 fewer, 1 more, 2 and 3 the two arms' "put window
+; in the bucket", then one per edge run
+; w_env(reduced, hidden) prefers-reduced-motion matches / document.hidden, before the data and whenever
+; either changes; w_resync() then re-runs set() as tool.js's two listeners do
+; w_now(ms) performance.now(), before every event that can start the animation
+; w_anim() 1 while the animation wants frames (host: keep requestAnimationFrame running)
+; w_frame(now, dst) one animation frame at time `now`: the DOM changes it makes, as patch lines in
+; dst, length. A patch line is `selector@index TAB kind TAB name TAB value LF`,
+; kind a = setAttribute, s = style[name], t = textContent, c = classList.toggle
+; (value 1 or 0); the host runs it with querySelectorAll(selector)[index]
+; w_render(dst) the page's HTML, length (0 on overflow). It holds the LAST DRAWN animation state,
+; so a re-render after an event shows what tool.js's nodes would still hold.
+; The bucket is drawn by the same rules as tool.js: the level, the spill and the three drops are numbers
+; from the cycle clock ((now - start) mod K_CYCLE_MS) written with toFixed(1); the readout, verdict and
+; the SVG follow the slider. Numbers are written the JavaScript way (js_num.t27: toLocaleString for
+; fmt(), toFixed, String(x) of the slider value), values read the way String() reads them (js_val.t27).
+; Where tool.js throws while it draws (arms without the two windows, no edges, a null wire / carried /
+; sources entry) its catch shows SAY_LOAD_FAILED and starts no animation; so does this page (data_ok).
+; NOT carried over: the data file is cut at DATA_CAP bytes (the page then says it could not be read);
+; classList.toggle on happy-dom normalises a class string even when nothing changes (a trailing space of
+; "ub-arm " goes at the first set()); this page writes the normalised string, a browser keeps the space.
+; MEASURED 2026-10-19: t27c 0.5.2 gen + an 11-function Zig export wrapper + zig build-exe wasm32-freestanding
+; ReleaseSmall give a 32562-byte wasm. The original page (tool.js) and this one ran in two happy-dom
+; windows against the real 2040-byte data.json and 95 variants of it, 3288 comparisons of tag, attributes
+; (the animated y / height / class / style / x) and text, with a VIRTUAL clock and requestAnimationFrame
+; on both sides (the same timestamps, so the cycle is compared at 1360 instants across the fill, hold and
+; drain phases, from 16 ms to 20 s apart), 561 slider values (in range, fractional, blank, not a number),
+; 409 presses of fewer / more, 364 presses of the measured-run buttons, 186 changes of prefers-reduced-motion
+; and 216 of document.hidden (the animation stops and a still frame is drawn, then restarts), runs that
+; start reduced / hidden / at a late or fractional clock, data with a markup / null / number board, lost
+; counts zero / string / null, in-flight at the 512 edge, huge / negative / null numbers, swapped / extra /
+; duplicate arms, a string window, three / one / all-slipped / none-slipped edges, odd wire and carried
+; fields, no generation line, empty / odd sources, 14 files the page cannot draw (an arm missing, no edges,
+; a null edge / wire / carried / source, a root that is null or an array): 0 differences. A negative
+; control (one attribute of the native DOM changed) is caught.
+; Not compared, and why: real time (requestAnimationFrame and performance.now are virtual: happy-dom has
+; no frame clock, so smoothness and the 60 Hz cadence are not measured); CSS (happy-dom loads none, so the
+; opacity / transform the stylesheet adds is not seen); focus (range.focus() after a "put window" press is
+; not a DOM difference); real pointer or media-query events.
+
+pub const DATA_CAP : u32 = 16384;
+pub var DATA : [16384]u8 = [0; 16384];
+pub var DATA_LEN : u32 = 0;
+pub var DATA_STATE : u8 = 0;
+pub var WIN : f64 = 64.0;
+pub var TARGET : f64 = 0.0;
+pub var START : f64 = 0.0;
+pub var RAF : bool = false;
+pub var NOW : f64 = 0.0;
+pub var REDUCED : bool = false;
+pub var HIDDEN : bool = false;
+pub var NOTE : bool = false;
+pub var DRAWN : bool = false;
+pub var D_WATER_Y : f64 = 268.0;
+pub var D_OVER : bool = false;
+pub var D_SPILL : u8 = 0;
+pub var D_LVL_TXT : [48]u8 = [0; 48];
+pub var D_LVL_LEN : u32 = 0;
+pub var D_LVL_X : f64 = 0.0;
+pub var D_LVL_Y : f64 = 0.0;
+pub var D_DROP_OP : [3]u8 = [0; 3];
+pub var D_DROP_Y : [3]f64 = [0.0; 3];
+pub var D_DROP_SET : [3]bool = [false; 3];
+
+pub fn w_init(h: []const u8) -> u32 {
+ DATA_STATE = 0;
+ DATA_LEN = 0;
+ RAF = false;
+ DRAWN = false;
+ NOTE = false;
+ WIN = 64.0;
+ TARGET = 0.0;
+ D_SPILL = 0;
+ D_OVER = false;
+ D_WATER_Y = 268.0;
+ D_LVL_LEN = 0;
+ D_DROP_OP[0] = 0; D_DROP_OP[1] = 0; D_DROP_OP[2] = 0;
+ D_DROP_SET[0] = false; D_DROP_SET[1] = false; D_DROP_SET[2] = false;
+ return 0;
+}
+
+pub fn w_env(reduced: u32, hidden: u32) -> u32 {
+ REDUCED = reduced != 0;
+ HIDDEN = hidden != 0;
+ return 0;
+}
+
+pub fn w_now(ms: f64) -> u32 {
+ NOW = ms;
+ return 0;
+}
+
+pub fn w_anim() -> u32 {
+ if (RAF) { return 1; }
+ return 0;
+}
+
+fn nullish(d: []const u8, sp: i64) -> bool {
+ const k : u8 = kind(d, sp);
+ return k == J_NONE or k == J_NULL;
+}
+
+/// The arm whose window is `w` (a number equal to it), NO_SPAN when none.
+fn arm_of(d: []const u8, r: i64, w: u16) -> i64 {
+ const arms : i64 = obj_get(d, r, "arms");
+ const cnt : u32 = arr_len(d, arms);
+ var i : u32 = 0;
+ while (i < cnt) {
+ const a : i64 = arr_at(d, arms, i);
+ const v : i64 = obj_get(d, a, "window");
+ if (kind(d, v) == J_NUMBER and jv_num(d, v) == (w as f64)) { return a; }
+ i = i + 1;
+ }
+ return NO_SPAN;
+}
+
+/// Would draw() in tool.js get to its last line without a TypeError?
+fn data_ok(d: []const u8, r: i64) -> bool {
+ if (nullish(d, r)) { return false; }
+ if (nullish(d, arm_of(d, r, K_ARM_WINDOWS[0])) or nullish(d, arm_of(d, r, K_ARM_WINDOWS[1]))) { return false; }
+ const ed : i64 = obj_get(d, r, "edges");
+ if (kind(d, ed) != J_ARRAY) { return false; }
+ const ec : u32 = arr_len(d, ed);
+ if (ec == 0) { return false; }
+ var i : u32 = 0;
+ while (i < ec) {
+ if (nullish(d, arr_at(d, ed, i))) { return false; }
+ i = i + 1;
+ }
+ if (nullish(d, obj_get(d, r, "wire")) or nullish(d, obj_get(d, r, "carried"))) { return false; }
+ const so : i64 = obj_get(d, r, "sources");
+ if (kind(d, so) != J_ARRAY) { return false; }
+ const sc : u32 = arr_len(d, so);
+ i = 0;
+ while (i < sc) {
+ if (nullish(d, arr_at(d, so, i))) { return false; }
+ i = i + 1;
+ }
+ return true;
+}
+
+// --- the bucket's numbers -----------------------------------------------------------------------
+
+fn rxf() -> f64 {
+ return K_RX_BYTES as f64;
+}
+
+fn level_y(frac: f64) -> f64 {
+ return 268.0 - (268.0 - 104.0) * js_min(frac, 1.0);
+}
+
+/// xAt(y, +1): the right wall at height y.
+fn x_right(y: f64) -> f64 {
+ const t : f64 = (y - 104.0) / (268.0 - 104.0);
+ return 256.0 + (234.0 - 256.0) * t;
+}
+
+/// JavaScript's a % b for doubles: the sign of a, |a| mod |b| exactly.
+fn fmod(a: f64, b: f64) -> f64 {
+ if (a != a or b != b or b == 0.0 or (a - a) != 0.0) { return js_nan(0.0); }
+ var neg : bool = a < 0.0;
+ var x : f64 = a;
+ if (neg) { x = 0.0 - a; }
+ var m : f64 = b;
+ if (m < 0.0) { m = 0.0 - m; }
+ if (m > x) { return a; }
+ // subtract b * 2^k from the top down; every step is exact (x and the subtrahend are within a factor 2)
+ var scaled : f64 = m;
+ var steps : u32 = 0;
+ while (scaled * 2.0 <= x and steps < 1100) {
+ scaled = scaled * 2.0;
+ steps = steps + 1;
+ }
+ while (scaled >= m) {
+ if (x >= scaled) { x = x - scaled; }
+ scaled = scaled / 2.0;
+ }
+ if (neg) { return 0.0 - x; }
+ return x;
+}
+
+/// draw(bytes, overflowing, dropPhase) of tool.js's animator, into the last-drawn state D_*.
+fn draw_state(bytes: f64, over: bool, has_phase: bool, phase: f64) -> u32 {
+ const y : f64 = level_y(bytes / rxf());
+ DRAWN = true;
+ D_WATER_Y = y;
+ D_OVER = over;
+ if (over) { D_SPILL = 49; } else { D_SPILL = 48; }
+ var tmp : [48]u8 = [0; 48];
+ const l : u32 = js_locale_num(tmp[0..], 0, js_round(js_min(bytes, rxf())));
+ var q : u32 = 0;
+ while (q < l and q < 44) {
+ D_LVL_TXT[q as usize] = tmp[q as usize];
+ q = q + 1;
+ }
+ D_LVL_TXT[q as usize] = 32;
+ D_LVL_TXT[(q + 1) as usize] = 66;
+ D_LVL_LEN = q + 2;
+ D_LVL_X = x_right(y) + 8.0;
+ D_LVL_Y = y + 4.0;
+ var i : u32 = 0;
+ while (i < 3) {
+ if (!has_phase) {
+ D_DROP_OP[i as usize] = 48;
+ } else {
+ const p : f64 = fmod(phase + (i as f64) / 3.0, 1.0);
+ const bottom : f64 = js_max(level_y(bytes / rxf()) - 8.0, 56.0);
+ D_DROP_Y[i as usize] = 56.0 + (bottom - 56.0) * p;
+ D_DROP_SET[i as usize] = true;
+ D_DROP_OP[i as usize] = 49;
+ }
+ i = i + 1;
+ }
+ return 0;
+}
+
+fn still() -> u32 {
+ return draw_state(TARGET, TARGET > rxf(), false, 0.0);
+}
+
+/// animator.set(bytes)
+fn anim_set(bytes: f64) -> u32 {
+ TARGET = bytes;
+ if (REDUCED or HIDDEN) {
+ RAF = false;
+ still();
+ return 0;
+ }
+ if (!RAF) {
+ START = NOW;
+ RAF = true;
+ }
+ return 0;
+}
+
+/// set(w) of tool.js's page.
+fn set_win(w0: f64) -> u32 {
+ const lo : f64 = K_WINDOW_MIN as f64;
+ const hi : f64 = K_WINDOW_MAX as f64;
+ WIN = js_max(lo, js_min(hi, js_round(w0)));
+ anim_set(WIN * (K_ANSWER_BYTES as f64));
+ return 0;
+}
+
+pub fn w_data(i: u32, s: []const u8) -> u32 {
+ if (s.len > (DATA_CAP as usize)) {
+ DATA_STATE = 2;
+ return 0;
+ }
+ var k : usize = 0;
+ while (k < s.len) {
+ DATA[k] = s[k];
+ k = k + 1;
+ }
+ DATA_LEN = s.len as u32;
+ DATA_STATE = 1;
+ const d : []const u8 = DATA[0..(DATA_LEN as usize)];
+ if (!jv_valid(d)) {
+ DATA_STATE = 2;
+ return 0;
+ }
+ if (!data_ok(d, root(d))) {
+ DATA_STATE = 2;
+ return 0;
+ }
+ NOTE = REDUCED;
+ set_win(K_WINDOW_START as f64);
+ return 0;
+}
+
+pub fn w_data_failed(i: u32) -> u32 {
+ DATA_STATE = 2;
+ return 0;
+}
+
+pub fn w_input(i: u32, s: []const u8) -> u32 {
+ if (DATA_STATE != 1) { return 0; }
+ if (i == 0) { set_win(jv_number_of_text(s, s.len as u32)); }
+ return 0;
+}
+
+pub fn w_resync() -> u32 {
+ if (DATA_STATE != 1) { return 0; }
+ set_win(WIN);
+ return 0;
+}
+
+pub fn w_click(i: u32) -> u32 {
+ if (DATA_STATE != 1) { return 0; }
+ if (i == 0) { set_win(WIN - 1.0); }
+ if (i == 1) { set_win(WIN + 1.0); }
+ if (i >= 2) {
+ const d : []const u8 = DATA[0..(DATA_LEN as usize)];
+ const r : i64 = root(d);
+ if (i == 2) { set_win(jv_num(d, obj_get(d, arm_of(d, r, K_ARM_WINDOWS[0]), "window"))); }
+ if (i == 3) { set_win(jv_num(d, obj_get(d, arm_of(d, r, K_ARM_WINDOWS[1]), "window"))); }
+ if (i >= 4) {
+ const ed : i64 = obj_get(d, r, "edges");
+ if (i - 4 < arr_len(d, ed)) { set_win(jv_num(d, obj_get(d, arr_at(d, ed, i - 4), "window"))); }
+ }
+ }
+ return 0;
+}
+
+// --- the frame ----------------------------------------------------------------------------------
+
+fn patch(dst: []u8, n0: u32, sel: []const u8, ix: u32, kind_c: u8, name: []const u8) -> u32 {
+ var n : u32 = raw(dst, n0, sel);
+ n = put(dst, n, 64);
+ n = js_str_u64(dst, n, ix as u64);
+ n = put(dst, n, 9);
+ n = put(dst, n, kind_c);
+ n = put(dst, n, 9);
+ n = raw(dst, n, name);
+ return put(dst, n, 9);
+}
+
+/// The patches that bring a page showing the previous state to the state D_*.
+fn emit_patches(dst: []u8, n0: u32) -> u32 {
+ var n : u32 = patch(dst, n0, ".ub-water", 0, 97, "y");
+ n = js_to_fixed(dst, n, D_WATER_Y, 1);
+ n = put(dst, n, 10);
+ n = patch(dst, n, ".ub-water", 0, 97, "height");
+ n = js_to_fixed(dst, n, 268.0 - D_WATER_Y, 1);
+ n = put(dst, n, 10);
+ n = patch(dst, n, ".ub-water", 0, 99, "is-over");
+ if (D_OVER) { n = put(dst, n, 49); } else { n = put(dst, n, 48); }
+ n = put(dst, n, 10);
+ n = patch(dst, n, ".ub-spill", 0, 115, "opacity");
+ n = put(dst, n, D_SPILL);
+ n = put(dst, n, 10);
+ n = patch(dst, n, ".ub-level", 0, 116, "");
+ var q : u32 = 0;
+ while (q < D_LVL_LEN) {
+ n = put(dst, n, D_LVL_TXT[q as usize]);
+ q = q + 1;
+ }
+ n = put(dst, n, 10);
+ n = patch(dst, n, ".ub-level", 0, 97, "x");
+ n = js_to_fixed(dst, n, D_LVL_X, 1);
+ n = put(dst, n, 10);
+ n = patch(dst, n, ".ub-level", 0, 97, "y");
+ n = js_to_fixed(dst, n, D_LVL_Y, 1);
+ n = put(dst, n, 10);
+ var i : u32 = 0;
+ while (i < 3) {
+ if (D_DROP_OP[i as usize] == 49) {
+ n = patch(dst, n, ".ub-drop", i, 97, "y");
+ n = js_to_fixed(dst, n, D_DROP_Y[i as usize], 1);
+ n = put(dst, n, 10);
+ }
+ n = patch(dst, n, ".ub-drop", i, 115, "opacity");
+ n = put(dst, n, D_DROP_OP[i as usize]);
+ n = put(dst, n, 10);
+ i = i + 1;
+ }
+ return n;
+}
+
+/// One animation frame at time `now`; the patch lines, their length (0 when no frame is wanted).
+pub fn w_frame(now: f64, dst: []u8) -> u32 {
+ if (!RAF or DATA_STATE != 1) { return 0; }
+ const cyc : f64 = K_CYCLE_MS as f64;
+ const t : f64 = fmod(now - START, cyc) / cyc;
+ var bytes : f64 = 0.0;
+ var has_phase : bool = false;
+ var phase : f64 = 0.0;
+ if (t < 0.55) {
+ bytes = TARGET * (t / 0.55);
+ has_phase = true;
+ phase = fmod((t / 0.55) * 4.0, 1.0);
+ } else {
+ if (t < 0.75) {
+ bytes = TARGET;
+ } else {
+ bytes = js_min(TARGET, rxf()) * (1.0 - (t - 0.75) / 0.25);
+ }
+ }
+ draw_state(bytes, bytes > rxf(), has_phase, phase);
+ return emit_patches(dst, 0);
+}
+
+// --- the page -----------------------------------------------------------------------------------
+
+/// fmt(n): toLocaleString for a number, String(n) for anything else.
+fn fmt_text(dst: []u8, n0: u32, d: []const u8, sp: i64) -> u32 {
+ if (kind(d, sp) == J_NUMBER) { return js_locale_num(dst, n0, jv_num(d, sp)); }
+ return jv_text(dst, n0, d, sp);
+}
+
+/// fmt(x) as raw bytes (for a pool value).
+fn fmt_raw(out: []u8, d: []const u8, sp: i64) -> u32 {
+ if (kind(d, sp) == J_NUMBER) { return js_locale_num(out, 0, jv_num(d, sp)); }
+ return jv_str(out, 0, d, sp);
+}
+
+fn fmt_f64_raw(out: []u8, v: f64) -> u32 {
+ return js_locale_num(out, 0, v);
+}
+
+fn bucket_html(dst: []u8, n0: u32, d: []const u8, r: i64, readout: []const u8, verdict: []const u8) -> u32 {
+ var n : u32 = raw(dst, n0, "");
+}
+
+/// `x ?? ''` as a child.
+fn nn_text(dst: []u8, n0: u32, d: []const u8, sp: i64) -> u32 {
+ if (nullish(d, sp)) { return n0; }
+ return jv_text(dst, n0, d, sp);
+}
+
+fn arm_card(dst: []u8, n0: u32, d: []const u8, a: i64) -> u32 {
+ const inflight : i64 = obj_get(d, a, "inFlight");
+ const lost : i64 = obj_get(d, a, "lost");
+ const over : bool = jv_num(d, inflight) > rxf();
+ const w : i64 = obj_get(d, a, "window");
+ const shown : bool = kind(d, w) == J_NUMBER and jv_num(d, w) == WIN;
+ var n : u32 = raw(dst, n0, "
");
+ var vb : [256]u8 = [0; 256];
+ var vn : u32 = jv_vput_val(vb[0..], 0, "w", d, w);
+ n = jv_fill(dst, n, SAY_ARM_HEAD, vb[0..], vn, false);
+ n = raw(dst, n, "
");
+ var row : usize = 0;
+ while (row < SAY_ARM_ROWS.len) {
+ n = raw(dst, n, "
");
+ n = text(dst, n, SAY_ARM_ROWS[row]);
+ n = raw(dst, n, "
0.0) { n = raw(dst, n, "ub-lost"); }
+ n = raw(dst, n, "\">");
+ if (row == 0) {
+ n = fmt_text(dst, n, d, inflight);
+ n = raw(dst, n, " B");
+ }
+ if (row == 1) { n = fmt_text(dst, n, d, obj_get(d, a, "jobsSent")); }
+ if (row == 2) { n = fmt_text(dst, n, d, obj_get(d, a, "accepted")); }
+ if (row == 3) { n = fmt_text(dst, n, d, lost); }
+ if (row == 4) { n = fmt_text(dst, n, d, obj_get(d, a, "events")); }
+ if (row == 5) {
+ n = jv_text(dst, n, d, obj_get(d, a, "elapsedS"));
+ n = put(dst, n, 32);
+ n = text(dst, n, SAY_SECONDS);
+ }
+ n = raw(dst, n, "
");
+ row = row + 1;
+ }
+ n = raw(dst, n, "
");
+}
+
+pub fn w_render(dst: []u8) -> u32 {
+ var n : u32 = 0;
+ if (DATA_STATE == 0) {
+ n = raw(dst, n, "
");
+ n = text(dst, n, SAY_LOADING);
+ n = raw(dst, n, "
");
+ if (!ok(n)) { return 0; }
+ return n;
+ }
+ if (DATA_STATE == 2) {
+ n = raw(dst, n, "
");
+ n = text(dst, n, SAY_LOAD_FAILED);
+ n = raw(dst, n, "
");
+ if (!ok(n)) { return 0; }
+ return n;
+ }
+ const d : []const u8 = DATA[0..(DATA_LEN as usize)];
+ const r : i64 = root(d);
+ // the readout and the verdict for the window
+ const bytes : f64 = WIN * (K_ANSWER_BYTES as f64);
+ const over : bool = bytes > rxf();
+ var rd : [256]u8 = [0; 256];
+ var rb : [256]u8 = [0; 256];
+ var tmp : [64]u8 = [0; 64];
+ var rv : u32 = jv_vput_f64(rb[0..], 0, "w", WIN);
+ rv = jv_vput_f64(rb[0..], rv, "answer", K_ANSWER_BYTES as f64);
+ var tl : u32 = fmt_f64_raw(tmp[0..], bytes);
+ rv = jv_vput_str(rb[0..], rv, "bytes", tmp[0..(tl as usize)]);
+ rv = jv_vput_f64(rb[0..], rv, "rx", rxf());
+ // the filled text, unescaped, for the : the fill escapes, so fill into a scratch and unescape
+ var ro : [512]u8 = [0; 512];
+ var rl : u32 = jv_fill_raw(ro[0..], 0, SAY_READOUT, rb[0..], rv, false);
+ var vd : [512]u8 = [0; 512];
+ var vb : [256]u8 = [0; 256];
+ var vl : u32 = 0;
+ if (over) {
+ tl = fmt_f64_raw(tmp[0..], bytes - rxf());
+ const vn : u32 = jv_vput_str(vb[0..], 0, "over", tmp[0..(tl as usize)]);
+ vl = jv_fill_raw(vd[0..], 0, SAY_OVER, vb[0..], vn, false);
+ } else {
+ tl = fmt_f64_raw(tmp[0..], rxf() - bytes);
+ const vn2 : u32 = jv_vput_str(vb[0..], 0, "spare", tmp[0..(tl as usize)]);
+ vl = jv_fill_raw(vd[0..], 0, SAY_FITS, vb[0..], vn2, false);
+ }
+ n = raw(dst, n, "
");
+ var ib : [512]u8 = [0; 512];
+ var in_ : u32 = jv_vput_val(ib[0..], 0, "board", d, obj_get(d, r, "board"));
+ in_ = jv_vput_f64(ib[0..], in_, "answer", K_ANSWER_BYTES as f64);
+ in_ = jv_vput_f64(ib[0..], in_, "rx", rxf());
+ n = jv_fill(dst, n, SAY_INTRO, ib[0..], in_, false);
+ n = raw(dst, n, "
");
+ n = bucket_html(dst, n, d, r, ro[0..(rl as usize)], vd[0..(vl as usize)]);
+ n = raw(dst, n, "");
+ n = text(dst, n, SAY_BUCKET);
+ n = raw(dst, n, "
");
+ n = text(dst, n, SAY_WINDOW_LABEL);
+ n = raw(dst, n, "
");
+ n = put_all2(dst, n, ro[0..], rl);
+ n = raw(dst, n, "
");
+ n = put_all2(dst, n, vd[0..], vl);
+ n = raw(dst, n, "
");
+ var ub : [512]u8 = [0; 512];
+ var un : u32 = jv_vput_f64(ub[0..], 0, "rx", rxf());
+ un = jv_vput_f64(ub[0..], un, "answer", K_ANSWER_BYTES as f64);
+ un = jv_vput_val(ub[0..], un, "ratio", d, obj_get(d, r, "ratio"));
+ un = jv_vput_f64(ub[0..], un, "fit", K_LARGEST_FIT as f64);
+ n = jv_fill(dst, n, SAY_RULE, ub[0..], un, false);
+ n = raw(dst, n, "
");
+ if (NOTE) {
+ n = raw(dst, n, "
");
+ n = text(dst, n, SAY_MOTION_OFF);
+ n = raw(dst, n, "
");
+ }
+ n = raw(dst, n, "
");
+ var hb : [512]u8 = [0; 512];
+ var hn : u32 = jv_vput_val(hb[0..], 0, "board", d, obj_get(d, r, "board"));
+ var jt : [64]u8 = [0; 64];
+ const jl : u32 = fmt_raw(jt[0..], d, obj_get(d, r, "jobsPerArm"));
+ hn = jv_vput_str(hb[0..], hn, "jobs", jt[0..(jl as usize)]);
+ n = jv_fill(dst, n, SAY_RUNS_TITLE, hb[0..], hn, false);
+ n = raw(dst, n, "
");
+ const a64 : i64 = arm_of(d, r, K_ARM_WINDOWS[0]);
+ const a24 : i64 = arm_of(d, r, K_ARM_WINDOWS[1]);
+ n = arm_card(dst, n, d, a64);
+ n = arm_card(dst, n, d, a24);
+ n = raw(dst, n, "
");
+ const wire : i64 = obj_get(d, r, "wire");
+ var sb : [1024]u8 = [0; 1024];
+ var sn : u32 = jv_vput_val(sb[0..], 0, "t64", d, obj_get(d, a64, "elapsedS"));
+ sn = jv_vput_val(sb[0..], sn, "t24", d, obj_get(d, a24, "elapsedS"));
+ sn = jv_vput_val(sb[0..], sn, "req", d, obj_get(d, wire, "requestBytes"));
+ var bt : [64]u8 = [0; 64];
+ var btl : u32 = fmt_raw(bt[0..], d, obj_get(d, wire, "baud"));
+ sn = jv_vput_str(sb[0..], sn, "baud", bt[0..(btl as usize)]);
+ btl = fmt_raw(bt[0..], d, obj_get(d, wire, "jobs"));
+ sn = jv_vput_str(sb[0..], sn, "jobs", bt[0..(btl as usize)]);
+ sn = jv_vput_val(sb[0..], sn, "wire", d, obj_get(d, wire, "seconds"));
+ n = jv_fill(dst, n, SAY_SAME_TIME, sb[0..], sn, false);
+ n = raw(dst, n, "
");
+ const ed : i64 = obj_get(d, r, "edges");
+ var eb : [256]u8 = [0; 256];
+ var et : [64]u8 = [0; 64];
+ const el : u32 = fmt_raw(et[0..], d, obj_get(d, arr_at(d, ed, 0), "planned"));
+ const en : u32 = jv_vput_str(eb[0..], 0, "planned", et[0..(el as usize)]);
+ n = jv_fill(dst, n, SAY_EDGE_TITLE, eb[0..], en, false);
+ n = raw(dst, n, "
");
+ const ec : u32 = arr_len(d, ed);
+ var x : u32 = 0;
+ while (x < ec) {
+ const e : i64 = arr_at(d, ed, x);
+ const slipped : bool = jv_truthy(d, obj_get(d, e, "slipped"));
+ const shown : bool = kind(d, obj_get(d, e, "window")) == J_NUMBER and jv_num(d, obj_get(d, e, "window")) == WIN;
+ n = raw(dst, n, "
");
+ var xb : [1024]u8 = [0; 1024];
+ var xn : u32 = jv_vput_val(xb[0..], 0, "w", d, obj_get(d, e, "window"));
+ var xt : [64]u8 = [0; 64];
+ var xl : u32 = fmt_raw(xt[0..], d, obj_get(d, e, "inFlight"));
+ xn = jv_vput_str(xb[0..], xn, "bytes", xt[0..(xl as usize)]);
+ xl = fmt_raw(xt[0..], d, obj_get(d, e, "receipts"));
+ xn = jv_vput_str(xb[0..], xn, "ok", xt[0..(xl as usize)]);
+ xl = fmt_raw(xt[0..], d, obj_get(d, e, "receiptsOf"));
+ xn = jv_vput_str(xb[0..], xn, "planned", xt[0..(xl as usize)]);
+ xl = fmt_raw(xt[0..], d, obj_get(d, e, "jobsSent"));
+ xn = jv_vput_str(xb[0..], xn, "sent", xt[0..(xl as usize)]);
+ if (slipped) { n = jv_fill(dst, n, SAY_EDGE_FAIL, xb[0..], xn, false); } else { n = jv_fill(dst, n, SAY_EDGE_PASS, xb[0..], xn, false); }
+ n = raw(dst, n, "
");
+ x = x + 1;
+ }
+ n = raw(dst, n, "
");
+ n = text(dst, n, SAY_CAVEAT);
+ n = raw(dst, n, "
");
+ const c : i64 = obj_get(d, r, "carried");
+ var cb : [256]u8 = [0; 256];
+ var cn : u32 = jv_vput_val(cb[0..], 0, "w", d, obj_get(d, c, "window"));
+ n = jv_fill(dst, n, SAY_CARRIED_TITLE, cb[0..], cn, false);
+ n = raw(dst, n, "
");
+ cn = jv_vput_val(cb[0..], 0, "board", d, obj_get(d, r, "board"));
+ n = jv_fill(dst, n, SAY_CARRIED_LINE, cb[0..], cn, false);
+ n = raw(dst, n, "
");
+ var row : usize = 0;
+ while (row < SAY_CARRIED_ROWS.len) {
+ n = raw(dst, n, "
");
+ n = text(dst, n, SAY_CARRIED_ROWS[row]);
+ n = raw(dst, n, "
");
+ if (row == 0) { n = fmt_text(dst, n, d, obj_get(d, c, "matrices")); }
+ if (row == 1) { n = fmt_text(dst, n, d, obj_get(d, c, "modelRows")); }
+ if (row == 2) { n = fmt_text(dst, n, d, obj_get(d, c, "weights")); }
+ if (row == 3) {
+ n = fmt_text(dst, n, d, obj_get(d, c, "receipts"));
+ n = raw(dst, n, " / ");
+ n = fmt_text(dst, n, d, obj_get(d, c, "receiptsOf"));
+ }
+ if (row == 4) {
+ n = fmt_text(dst, n, d, obj_get(d, c, "rows"));
+ n = raw(dst, n, " / ");
+ n = fmt_text(dst, n, d, obj_get(d, c, "rowsOf"));
+ }
+ if (row == 5) {
+ n = jv_text(dst, n, d, obj_get(d, c, "elapsedS"));
+ n = put(dst, n, 32);
+ n = text(dst, n, SAY_SECONDS);
+ n = raw(dst, n, ", ");
+ var qb : [256]u8 = [0; 256];
+ var qt : [64]u8 = [0; 64];
+ const ql : u32 = fmt_raw(qt[0..], d, obj_get(d, c, "answersPerS"));
+ const qn : u32 = jv_vput_str(qb[0..], 0, "rate", qt[0..(ql as usize)]);
+ n = jv_fill(dst, n, SAY_RATE, qb[0..], qn, false);
+ }
+ n = raw(dst, n, "
");
+ row = row + 1;
+ }
+ n = raw(dst, n, "
");
+ if (jv_truthy(d, obj_get(d, r, "generationOnMac"))) {
+ n = raw(dst, n, "
");
+ n = text(dst, n, SAY_CARRIED_PLAIN);
+ n = raw(dst, n, "
");
+ }
+ n = raw(dst, n, "");
+ n = text(dst, n, SAY_SOURCES);
+ n = raw(dst, n, "
");
+ const so : i64 = obj_get(d, r, "sources");
+ const sc : u32 = arr_len(d, so);
+ x = 0;
+ while (x < sc) {
+ const s : i64 = arr_at(d, so, x);
+ n = raw(dst, n, "
");
+ n = jv_text(dst, n, d, obj_get(d, r, "repo"));
+ n = put(dst, n, 32);
+ n = jv_text(dst, n, d, obj_get(d, s, "path"));
+ n = put(dst, n, 32);
+ n = text(dst, n, SAY_AT);
+ n = put(dst, n, 32);
+ n = jv_text(dst, n, d, obj_get(d, s, "commit"));
+ n = raw(dst, n, "
");
+ x = x + 1;
+ }
+ n = raw(dst, n, "
");
+ if (!ok(n)) { return 0; }
+ return n;
+}
+
+fn put_all2(dst: []u8, n0: u32, s: []const u8, l: u32) -> u32 {
+ var n : u32 = n0;
+ var i : u32 = 0;
+ while (i < l) {
+ n = put_escaped(dst, n, s[i as usize]);
+ i = i + 1;
+ }
+ return n;
+}
+
+/// The splicer follows only the names a module itself mentions, not the names the imported functions
+/// mention. This anchor mentions every helper the writer, the JSON reader and the number rules call.
+/// Never called.
+pub fn imports_anchor(o: []u8, s: []const u8) -> u32 {
+ var n : u32 = 0;
+ n = n + (ok(0) as u32);
+ n = n + OVER;
+ n = n + put(o, 0, 0);
+ n = n + raw(o, 0, s);
+ n = n + put_escaped(o, 0, 0);
+ n = n + text(o, 0, s);
+ n = n + num(o, 0, 0);
+ n = n + attr(o, 0, s, s);
+ n = n + fill(o, 0, s, s, s, s, 0);
+ n = n + fill_named(o, 0, s, s, s, s, s, s, s, s, s, s, s, s, s);
+ n = n + dec_to(o, 0);
+ n = n + (name_is(s, 0, 0, s) as u32);
+ n = n + (is_word_byte(0) as u32);
+ n = n + (H_LT as u32);
+ n = n + (H_GT as u32);
+ n = n + (H_AMP as u32);
+ n = n + (H_QUOTE as u32);
+ n = n + (H_APOS as u32);
+ n = n + (H_SPACE as u32);
+ n = n + (H_SLASH as u32);
+ n = n + (H_EQ as u32);
+ n = n + ((NO_SPAN < 0) as u32);
+ n = n + ((SPAN > 0) as u32);
+ n = n + (J_NONE as u32);
+ n = n + (J_OBJECT as u32);
+ n = n + (J_ARRAY as u32);
+ n = n + (J_STRING as u32);
+ n = n + (J_NUMBER as u32);
+ n = n + (J_TRUE as u32);
+ n = n + (J_FALSE as u32);
+ n = n + (J_NULL as u32);
+ n = n + (jspan(0, 0) as u32);
+ n = n + (span_lo(0) as u32);
+ n = n + (span_hi(0) as u32);
+ n = n + (skip_ws(s, 0) as u32);
+ n = n + (string_end(s, 0) as u32);
+ n = n + (value_end(s, 0) as u32);
+ n = n + (root(s) as u32);
+ n = n + (kind(s, 0) as u32);
+ n = n + (key_is(s, 0, 0, s) as u32);
+ n = n + (obj_get(s, 0, s) as u32);
+ n = n + arr_len(s, 0);
+ n = n + (arr_at(s, 0, 0) as u32);
+ n = n + hex4(s, 0);
+ n = n + escape_at(s, 0, 0);
+ n = n + cp_utf8_len(0);
+ n = n + (cp_utf8_byte(0, 0) as u32);
+ n = n + str_len(s, 0);
+ n = n + (str_byte(s, 0, 0) as u32);
+ n = n + JN_OVER;
+ n = n + JN_CAP;
+ n = n + ((JN_TWO52 > 0.0) as u32);
+ n = n + ((JN_TWO53 > 0.0) as u32);
+ n = n + jn_put(o, 0, 0);
+ n = n + jn_raw(o, 0, s);
+ n = n + jn_digits(o, 0);
+ n = n + jn_mul(o, 0, 0);
+ n = n + jn_mul2(o, 0, 0);
+ n = n + jn_mul5(o, 0, 0);
+ n = n + jn_shift(o, 0, 0);
+ n = n + (jn_cmp(s, s, 0) as u32);
+ n = n + (jn_expo(0.0) as u32);
+ n = n + (jn_mant(0.0) as u32);
+ n = n + ((jn_make(0, 0) > 0.0) as u32);
+ n = n + jn_exact(o, 0, 0);
+ n = n + jn_fscale(0);
+ n = n + (js_is_nan(0.0) as u32);
+ n = n + ((js_nan(0.0) > 0.0) as u32);
+ n = n + ((js_inf(1.0) > 0.0) as u32);
+ n = n + ((js_floor(0.0) > 0.0) as u32);
+ n = n + ((js_ceil(0.0) > 0.0) as u32);
+ n = n + ((js_max(0.0, 0.0) > 0.0) as u32);
+ n = n + ((js_min(0.0, 0.0) > 0.0) as u32);
+ n = n + ((js_round(0.0) > 0.0) as u32);
+ n = n + (jn_shortest(o, 1.0) as u32);
+ n = n + jn_copy(o, s, 0);
+ n = n + jn_put_run(o, 0, s, 0, 0);
+ n = n + jn_layout(o, 0, s, 0, 0);
+ n = n + js_str_num(o, 0, 0.0);
+ n = n + js_to_fixed(o, 0, 0.0, 0);
+ n = n + js_locale_num(o, 0, 0.0);
+ n = n + (jn_num_ok(s, 0, 0) as u32);
+ n = n + (jn_cmp_text(s, 0, 0, 0, 0) as u32);
+ n = n + ((js_parse_num(s, 0, 0) > 0.0) as u32);
+ n = n + ((jn_dec_value(s, 0, 0) > 0.0) as u32);
+ n = n + (jn_abs(0) as u32);
+ n = n + js_str_u64(o, 0, 0);
+ n = n + jv_str_to(o, 0, s, 0);
+ n = n + jv_text_str(o, 0, s, 0);
+ n = n + ((jv_numval(s, 0) > 0.0) as u32);
+ n = n + jv_str(o, 0, s, 0);
+ n = n + jv_text(o, 0, s, 0);
+ n = n + (jv_is(s, 0, s) as u32);
+ n = n + (jv_truthy(s, 0) as u32);
+ n = n + (jv_is_ws(0) as u32);
+ n = n + ((jv_number_of_text(s, 0) > 0.0) as u32);
+ n = n + ((jv_num(s, 0) > 0.0) as u32);
+ n = n + jv_vput_str(o, 0, s, s);
+ n = n + jv_vput_f64(o, 0, s, 0.0);
+ n = n + jv_vput_val(o, 0, s, s, 0);
+ n = n + jv_vput_defined(o, 0, s, s, 0);
+ n = n + jv_vput_fixed(o, 0, s, 0.0, 0);
+ n = n + jv_kid(o, 0, s, 0);
+ n = n + jv_find(s, 0, s);
+ n = n + jv_fill(o, 0, s, s, 0, true);
+ n = n + (jv_string_ok(s, 0) as u32);
+ n = n + (jv_number_ok(s, 0) as u32);
+ n = n + (jv_lit_ok(s, 0, s) as u32);
+ n = n + (jv_valid(s) as u32);
+ n = n + jv_ws_at(s, 0, 0);
+ n = n + jv_ws_before(s, 0, 0);
+ n = n + jv_digit_value(0);
+ n = n + jv_fill_raw(o, 0, s, s, 0, true);
+ n = n + jv_fill_with(o, 0, s, s, 0, true, true);
+ return n;
+}
diff --git a/specs/widgets/vcd_wrapped_logic.t27 b/specs/widgets/vcd_wrapped_logic.t27
new file mode 100644
index 0000000000..5827c60afe
--- /dev/null
+++ b/specs/widgets/vcd_wrapped_logic.t27
@@ -0,0 +1,1578 @@
+// SPDX-License-Identifier: Apache-2.0
+; specs/widgets/vcd_wrapped_logic.t27 -- the vcd-wrapped widget's reader: IEEE 1364 VCD text read as a
+; stream of tokens into one counter row per signal, then summarised. Ported from trinity
+; apps/website/public/widgets/vcd-wrapped/vcdstats.js (281 lines). Epic gHashTag/t27#10937, issue #10938.
+; --
+; JS export -> t27 function(s) note
+; VcdError -> the status codes below + vcd_err_* getters (code, detail)
+; createVcdReader -> vcd_begin(max_header_chars), vcd_push(chunk), vcd_end(), vcd_chars()
+; summarize -> vcd_summarize(top) (called by vcd_end with top 5), vs_* / row getters
+; vcdSummary -> vcd_summary(text, max_header_chars): chunks of 1048576 BYTES (JS: UTF-16 units; same for ASCII)
+; spanUnits -> span_units(ticks, has_timescale, mul, exp) + su_value(), su_unit()
+; extend, flipsBetween, isUnknownChar, endCommand, record, headerToken, bodyToken, feed, topOf: internal
+; --
+; Status of vcd_push, vcd_end, vcd_summary: 0 ok; error codes as the JS VcdError.code:
+; 1 binary 2 not-vcd 3 timescale 4 bad-var 5 no-vars 6 unknown-id 7 bad-token 8 no-defs 9 empty;
+; 90 a case not carried (below); -1 over a table limit.
+; vcd_err_info(f): 0 code 1 detail text offset 2 detail text length (bytes of err_byte) 3 time set 4 time (f64 in vcd_err_time())
+; The detail text is the JS detail's token (first 40 UTF-16 units) or text; read it with err_byte(i).
+; Summary layout (after vcd_end returned 0): vs_info(f) (u64): 0 timescale set 1 mul 2 unit (0 s 1 ms 2 us 3 ns 4 ps 5 fs)
+; 3 exp 4 version offset 5 version length 6 stamps 7 chars 8 vars 9 signals 10 params 11 scopes 12 changes
+; 13 flips 14 busiest count 15 quietest count 16 stuck_xz 17 stuck names count 18 ends_xz 19 ends names count
+; 20 never_changed 21 movers; vs_time(f): 0 t_first 1 t_last 2 span (f64, 0 when none).
+; vs_row(kind, i, f): kind 0 busiest, 1 quietest; f 0 name off 1 name len 2 width 3 type off 4 type len
+; 5 changes 6 flips 7 aliases; vs_name(kind, i, f): kind 0 stuck names, 1 ends names; f 0 off 1 len.
+; Strings are read with pool_byte(i) (one pool for names, types, version).
+; --
+; Input: UTF-8 bytes; a chunk must end on a character boundary. Whitespace is the JavaScript \s set.
+; The JS decodes text into UTF-16 first; lengths of `chars` and of the 40-unit error tokens are counted in
+; UTF-16 units here too.
+; Not carried (status 90): a bit-vector value with a byte of 128 or more (JS lowercases Unicode letters
+; when counting flips); a $var width that is not a whole number below 65536 once read as a JavaScript
+; Number (2.5, 1e10, Infinity); a timestamp with more than 19 digits; a 4-byte character split by the
+; 40-unit cut of an error token. Table limits (the JS has none): 65536 signals, 1 MB per token, 16 MB of
+; names, 32 MB of values, scope depth 1024.
+; phi^2 + 1/phi^2 = 3 | TRINITY
+
+module widget_vcd_wrapped_logic;
+
+pub const MS : usize = 65536;
+
+pub const R_STARTED : usize = 0;
+pub const R_HDR : usize = 1;
+pub const R_CHARS : usize = 2;
+pub const R_CMD : usize = 3;
+pub const R_NCT : usize = 4;
+pub const R_CBN : usize = 5;
+pub const R_NSC : usize = 6;
+pub const R_SCOPES : usize = 7;
+pub const R_TSS : usize = 8;
+pub const R_TSM : usize = 9;
+pub const R_TSU : usize = 10;
+pub const R_VERO : usize = 11;
+pub const R_VERL : usize = 12;
+pub const R_VARS : usize = 13;
+pub const R_NSIG : usize = 14;
+pub const R_BLOCK : usize = 15;
+pub const R_PEND : usize = 16;
+pub const R_PENDL : usize = 17;
+pub const R_TSET : usize = 18;
+pub const R_STAMPS : usize = 19;
+pub const R_TFS : usize = 20;
+pub const R_MAXH : usize = 21;
+pub const R_HOLD : usize = 22;
+pub const R_TOKN : usize = 23;
+pub const R_NP : usize = 24;
+pub const R_VP : usize = 25;
+pub const R_ECODE : usize = 26;
+pub const R_EO : usize = 27;
+pub const R_EL : usize = 28;
+pub const R_ETS : usize = 29;
+pub const R_UNSUP : usize = 30;
+pub const R_EBN : usize = 31;
+
+pub var R : [40]u64 = [0; 40];
+pub var TF : [4]f64 = [0.0; 4];
+pub var SUM : [32]u64 = [0; 32];
+pub var SUT : [4]f64 = [0.0; 4];
+pub var SUV : [2]f64 = [0.0; 2];
+pub var P10 : [23]f64 = [0.0; 23];
+
+pub var TOKB : [1048576]u8 = [0; 1048576];
+pub var PVB : [1048576]u8 = [0; 1048576];
+pub var CB : [1048576]u8 = [0; 1048576];
+pub var CTO : [65536]u32 = [0; 65536];
+pub var CTL : [65536]u32 = [0; 65536];
+pub var HOLD : [8388608]u8 = [0; 8388608];
+pub var ERRB : [1048576]u8 = [0; 1048576];
+pub var NAMEP : [16777216]u8 = [0; 16777216];
+pub var VALP : [33554432]u8 = [0; 33554432];
+pub var VE : [65536]u8 = [0; 65536];
+pub var SCO : [1024]u32 = [0; 1024];
+pub var SCL : [1024]u32 = [0; 1024];
+
+pub var SIO : [65536]u32 = [0; 65536];
+pub var SIL : [65536]u32 = [0; 65536];
+pub var SNO : [65536]u32 = [0; 65536];
+pub var SNL : [65536]u32 = [0; 65536];
+pub var STO : [65536]u32 = [0; 65536];
+pub var STL : [65536]u32 = [0; 65536];
+pub var SWD : [65536]u32 = [0; 65536];
+pub var SFG : [65536]u8 = [0; 65536];
+pub var SVO : [65536]u32 = [0; 65536];
+pub var SVL : [65536]u32 = [0; 65536];
+pub var SVC : [65536]u32 = [0; 65536];
+pub var SHV : [65536]u8 = [0; 65536];
+pub var SVA : [65536]u64 = [0; 65536];
+pub var SCH : [65536]u64 = [0; 65536];
+pub var SFL : [65536]u64 = [0; 65536];
+pub var SEK : [65536]u8 = [0; 65536];
+pub var SAL : [65536]u32 = [0; 65536];
+pub var HT : [262144]u32 = [0; 262144];
+
+pub var LIVE : [65536]u32 = [0; 65536];
+pub var SRT : [65536]u32 = [0; 65536];
+pub var STMP : [65536]u32 = [0; 65536];
+pub var BUSY : [8]u32 = [0; 8];
+pub var QUIET : [8]u32 = [0; 8];
+pub var STUCK : [8]u32 = [0; 8];
+pub var ENDSX : [8]u32 = [0; 8];
+
+// ---------------------------------------------------------------- bytes and whitespace
+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 is_digit(b: u8) -> bool {
+ return b >= 48 and b <= 57;
+}
+
+/// The UTF-16 units of the UTF-8 bytes s[lo..hi).
+pub fn units_of(s: []const u8, lo: usize, hi: usize) -> u64 {
+ var n : u64 = 0;
+ var k : usize = lo;
+ while (k < hi) {
+ const b : u8 = s[k];
+ if (b < 128 or b >= 192) { n = n + 1; }
+ if (b >= 240) { n = n + 1; }
+ k = k + 1;
+ }
+ return n;
+}
+
+/// The bytes of the first `units` UTF-16 units of s[lo..hi) (whole characters only); returns -1 when a pair would be split.
+pub fn prefix_bytes(s: []const u8, lo: usize, hi: usize, units: u64) -> i64 {
+ var n : u64 = 0;
+ var k : usize = lo;
+ while (k < hi) {
+ const b : u8 = s[k];
+ var step : usize = 1;
+ var u : u64 = 1;
+ if (b >= 192 and b < 224) { step = 2; }
+ if (b >= 224 and b < 240) { step = 3; }
+ if (b >= 240) {
+ step = 4;
+ u = 2;
+ }
+ if (n + u > units) {
+ if (u == 2 and n + 1 == units) { R[R_UNSUP] = 1; }
+ return (k - lo) as i64;
+ }
+ n = n + u;
+ k = k + step;
+ }
+ return (hi - lo) as i64;
+}
+
+pub fn fail(code: u64, o: usize, l: usize, src: u32) -> i32 {
+ R[R_ECODE] = code;
+ R[R_EO] = 0;
+ R[R_EL] = l as u64;
+ var k : usize = 0;
+ while (k < l and k < 1048576) {
+ if (src == 0) { ERRB[k] = TOKB[o + k]; }
+ if (src == 1) { ERRB[k] = CB[o + k]; }
+ if (src == 2) { ERRB[k] = PVB[o + k]; }
+ if (src == 3) { ERRB[k] = HOLD[o + k]; }
+ k = k + 1;
+ }
+ return code as i32;
+}
+
+pub fn tok_is(w: []const u8) -> bool {
+ if ((R[R_TOKN] as usize) != w.len) { return false; }
+ var k : usize = 0;
+ while (k < w.len) {
+ if (TOKB[k] != w[k]) { return false; }
+ k = k + 1;
+ }
+ return true;
+}
+
+pub fn cb_is(i: usize, w: []const u8) -> bool {
+ if ((CTL[i] as usize) != w.len) { return false; }
+ var k : usize = 0;
+ while (k < w.len) {
+ if (CB[(CTO[i] as usize) + k] != w[k]) { return false; }
+ k = k + 1;
+ }
+ return true;
+}
+
+// ---------------------------------------------------------------- Number(token) for a $var width
+/// -1 NaN or negative, -3 not carried, else the whole number (0 for anything in [0, 1)).
+pub fn js_width(o: usize, l: usize) -> i64 {
+ var p : usize = 0;
+ var neg : bool = false;
+ if (l == 0) { return -1; }
+ const t : []const u8 = CB[0 .. 1048576];
+ const b0 : u8 = t[o];
+ if (b0 == 43 or b0 == 45) {
+ neg = b0 == 45;
+ p = 1;
+ }
+ if (l - p == 8 and t[o + p] == 73 and t[o + p + 1] == 110 and t[o + p + 2] == 102 and t[o + p + 3] == 105 and t[o + p + 4] == 110 and t[o + p + 5] == 105 and t[o + p + 6] == 116 and t[o + p + 7] == 121) {
+ if (neg) { return -1; }
+ return -3;
+ }
+ if (p == 0 and l > 2 and t[o] == 48) {
+ const c : u8 = t[o + 1] | 32;
+ var base : u64 = 0;
+ if (c == 120) { base = 16; }
+ if (c == 111) { base = 8; }
+ if (c == 98) { base = 2; }
+ if (base > 0) {
+ var v : u64 = 0;
+ var k : usize = 2;
+ while (k < l) {
+ const d : u8 = t[o + k];
+ var dv : u64 = 99;
+ if (is_digit(d)) { dv = (d - 48) as u64; }
+ if ((d | 32) >= 97 and (d | 32) <= 102) { dv = ((d | 32) - 87) as u64; }
+ if (dv >= base) { return -1; }
+ if (v < 4294967296) { v = v * base + dv; }
+ k = k + 1;
+ }
+ if (v >= 2147483648) { return -3; }
+ return v as i64;
+ }
+ }
+ var m : u64 = 0;
+ var nd : u64 = 0;
+ var e10 : i64 = 0;
+ var digits : u64 = 0;
+ var seen : bool = false;
+ var big : bool = false;
+ while (p < l and is_digit(t[o + p])) {
+ digits = digits + 1;
+ seen = true;
+ if (m == 0 and t[o + p] == 48) {
+ m = 0;
+ } else {
+ if (nd < 19) {
+ m = m * 10 + ((t[o + p] - 48) as u64);
+ nd = nd + 1;
+ } else {
+ big = true;
+ }
+ }
+ p = p + 1;
+ }
+ if (p < l and t[o + p] == 46) {
+ p = p + 1;
+ while (p < l and is_digit(t[o + p])) {
+ digits = digits + 1;
+ seen = true;
+ if (m == 0 and t[o + p] == 48) {
+ m = 0;
+ } else {
+ if (nd < 19) {
+ m = m * 10 + ((t[o + p] - 48) as u64);
+ nd = nd + 1;
+ } else {
+ big = true;
+ }
+ }
+ e10 = e10 - 1;
+ p = p + 1;
+ }
+ }
+ if (!seen) { return -1; }
+ if (p < l and (t[o + p] == 101 or t[o + p] == 69)) {
+ p = p + 1;
+ var eneg : bool = false;
+ if (p < l and (t[o + p] == 43 or t[o + p] == 45)) {
+ eneg = t[o + p] == 45;
+ p = p + 1;
+ }
+ if (p >= l or !is_digit(t[o + p])) { return -1; }
+ var ev : i64 = 0;
+ while (p < l and is_digit(t[o + p])) {
+ if (ev < 100000) { ev = ev * 10 + ((t[o + p] - 48) as i64); }
+ p = p + 1;
+ }
+ if (eneg) { e10 = e10 - ev; } else { e10 = e10 + ev; }
+ }
+ if (p != l) { return -1; }
+ if (m == 0 and !big) { return 0; }
+ if (neg) { return -1; }
+ if (big) { return -3; }
+ while (m % 10 == 0) {
+ m = m / 10;
+ nd = nd - 1;
+ e10 = e10 + 1;
+ }
+ if ((nd as i64) + e10 <= 0) { return 0; }
+ if (e10 >= 0) {
+ if ((nd as i64) + e10 > 10) { return -3; }
+ var v2 : u64 = m;
+ var k2 : i64 = 0;
+ while (k2 < e10) {
+ v2 = v2 * 10;
+ k2 = k2 + 1;
+ }
+ if (v2 >= 2147483648) { return -3; }
+ return v2 as i64;
+ }
+ return -3;
+}
+
+// ---------------------------------------------------------------- the reader
+pub fn pool_put(w: []const u8, lo: usize, hi: usize, src: u32) -> i64 {
+ const at : u64 = R[R_NP];
+ if (at + ((hi - lo) as u64) >= 16777216) { return -1; }
+ var k : usize = lo;
+ while (k < hi) {
+ if (src == 0) { NAMEP[(at as usize) + (k - lo)] = TOKB[k]; }
+ if (src == 1) { NAMEP[(at as usize) + (k - lo)] = CB[k]; }
+ k = k + 1;
+ }
+ R[R_NP] = at + ((hi - lo) as u64);
+ return at as i64;
+}
+
+pub fn hash_id(o: usize, l: usize, src: u32) -> usize {
+ var h : u64 = 2166136261;
+ var k : usize = 0;
+ while (k < l) {
+ var b : u64 = 0;
+ if (src == 0) { b = TOKB[o + k] as u64; }
+ if (src == 1) { b = CB[o + k] as u64; }
+ h = ((h ^ b) * 16777619) % 4294967296;
+ k = k + 1;
+ }
+ return (h as usize) % 262144;
+}
+
+pub fn id_eq(si: usize, o: usize, l: usize, src: u32) -> bool {
+ if ((SIL[si] as usize) != l) { return false; }
+ var k : usize = 0;
+ while (k < l) {
+ var b : u8 = 0;
+ if (src == 0) { b = TOKB[o + k]; }
+ if (src == 1) { b = CB[o + k]; }
+ if (NAMEP[(SIO[si] as usize) + k] != b) { return false; }
+ k = k + 1;
+ }
+ return true;
+}
+
+/// byId.get(id): the signal index plus one, or 0.
+pub fn find_sig(o: usize, l: usize, src: u32) -> usize {
+ var p : usize = hash_id(o, l, src);
+ while (HT[p] != 0) {
+ if (id_eq((HT[p] - 1) as usize, o, l, src)) { return HT[p] as usize; }
+ p = (p + 1) % 262144;
+ }
+ return 0;
+}
+
+/// endCommand() for $var: tokens in CB.
+pub fn end_var() -> i32 {
+ const nct : usize = R[R_NCT] as usize;
+ if (nct < 4) { return bad_var_text(); }
+ const w : i64 = js_width(CTO[1] as usize, CTL[1] as usize);
+ if (w == -3) {
+ R[R_UNSUP] = 1;
+ return 90;
+ }
+ if (w < 0) { return bad_var_text(); }
+ if (w > 65536) { return -1; }
+ R[R_VARS] = R[R_VARS] + 1;
+ // name = [...scope, ref].join('.') plus the index when it has no ':'
+ const nm0 : u64 = R[R_NP];
+ var k : usize = 0;
+ var nlen : u64 = 0;
+ while (k < (R[R_NSC] as usize)) {
+ var q : usize = 0;
+ while (q < (SCL[k] as usize)) {
+ if (R[R_NP] >= 16777000) { return -1; }
+ NAMEP[R[R_NP] as usize] = NAMEP[(SCO[k] as usize) + q];
+ R[R_NP] = R[R_NP] + 1;
+ q = q + 1;
+ }
+ NAMEP[R[R_NP] as usize] = 46;
+ R[R_NP] = R[R_NP] + 1;
+ k = k + 1;
+ }
+ var q2 : usize = 0;
+ while (q2 < (CTL[3] as usize)) {
+ if (R[R_NP] >= 16777000) { return -1; }
+ NAMEP[R[R_NP] as usize] = CB[(CTO[3] as usize) + q2];
+ R[R_NP] = R[R_NP] + 1;
+ q2 = q2 + 1;
+ }
+ if (nct >= 5) {
+ var has_colon : bool = false;
+ var c : usize = 0;
+ while (c < (CTL[4] as usize)) {
+ if (CB[(CTO[4] as usize) + c] == 58) { has_colon = true; }
+ c = c + 1;
+ }
+ if (!has_colon) {
+ c = 0;
+ while (c < (CTL[4] as usize)) {
+ if (R[R_NP] >= 16777000) { return -1; }
+ NAMEP[R[R_NP] as usize] = CB[(CTO[4] as usize) + c];
+ R[R_NP] = R[R_NP] + 1;
+ c = c + 1;
+ }
+ }
+ }
+ nlen = R[R_NP] - nm0;
+ const ex : usize = find_sig(CTO[2] as usize, CTL[2] as usize, 1);
+ if (ex != 0) {
+ SAL[ex - 1] = SAL[ex - 1] + 1;
+ R[R_NP] = nm0;
+ return 0;
+ }
+ const si : usize = R[R_NSIG] as usize;
+ if (si >= MS) { return -1; }
+ const ido : i64 = pool_put(CB[0 .. 1048576], CTO[2] as usize, (CTO[2] + CTL[2]) as usize, 1);
+ const tyo : i64 = pool_put(CB[0 .. 1048576], CTO[0] as usize, (CTO[0] + CTL[0]) as usize, 1);
+ if (ido < 0 or tyo < 0) { return -1; }
+ SIO[si] = ido as u32;
+ SIL[si] = CTL[2];
+ SNO[si] = nm0 as u32;
+ SNL[si] = nlen as u32;
+ STO[si] = tyo as u32;
+ STL[si] = CTL[0];
+ var wd : u32 = w as u32;
+ if (wd < 1) { wd = 1; }
+ SWD[si] = wd;
+ var fl : u8 = 0;
+ if (cb_is(0, "real") or cb_is(0, "realtime")) { fl = fl + 1; }
+ if (cb_is(0, "parameter")) { fl = fl + 2; }
+ if (cb_is(0, "string")) { fl = fl + 4; }
+ SFG[si] = fl;
+ SHV[si] = 0;
+ SVL[si] = 0;
+ SVC[si] = 0;
+ SVA[si] = 0;
+ SCH[si] = 0;
+ SFL[si] = 0;
+ SEK[si] = 0;
+ SAL[si] = 0;
+ var p : usize = hash_id(CTO[2] as usize, CTL[2] as usize, 1);
+ while (HT[p] != 0) { p = (p + 1) % 262144; }
+ HT[p] = (si + 1) as u32;
+ R[R_NSIG] = (si + 1) as u64;
+ return 0;
+}
+
+pub fn bad_var_text() -> i32 {
+ // detail text = tokens joined with a space
+ var n : usize = 0;
+ var i : usize = 0;
+ while (i < (R[R_NCT] as usize)) {
+ if (i > 0 and n < 1048576) {
+ ERRB[n] = 32;
+ n = n + 1;
+ }
+ var k : usize = 0;
+ while (k < (CTL[i] as usize) and n < 1048576) {
+ ERRB[n] = CB[(CTO[i] as usize) + k];
+ n = n + 1;
+ k = k + 1;
+ }
+ i = i + 1;
+ }
+ R[R_ECODE] = 4;
+ R[R_EO] = 0;
+ R[R_EL] = n as u64;
+ return 4;
+}
+
+pub fn timescale_fail() -> i32 {
+ var n : usize = 0;
+ var i : usize = 0;
+ while (i < (R[R_NCT] as usize)) {
+ if (i > 0 and n < 1048576) {
+ ERRB[n] = 32;
+ n = n + 1;
+ }
+ var k : usize = 0;
+ while (k < (CTL[i] as usize) and n < 1048576) {
+ ERRB[n] = CB[(CTO[i] as usize) + k];
+ n = n + 1;
+ k = k + 1;
+ }
+ i = i + 1;
+ }
+ R[R_ECODE] = 3;
+ R[R_EO] = 0;
+ R[R_EL] = n as u64;
+ return 3;
+}
+
+/// endCommand(): the command word is R_CMD (1 timescale 2 version 3 scope 4 upscope 5 var 6 enddefinitions 7 other).
+pub fn end_command() -> i32 {
+ const cmd : u64 = R[R_CMD];
+ const nct : usize = R[R_NCT] as usize;
+ var rc : i32 = 0;
+ if (cmd == 1) {
+ // join without spaces, then ^(1|10|100)\s*(s|ms|us|ns|ps|fs)$
+ var n : usize = 0;
+ var i : usize = 0;
+ while (i < nct) {
+ var k : usize = 0;
+ while (k < (CTL[i] as usize) and n < 64) {
+ VE[n] = CB[(CTO[i] as usize) + k];
+ n = n + 1;
+ k = k + 1;
+ }
+ i = i + 1;
+ }
+ var d : usize = 0;
+ while (d < n and is_digit(VE[d])) { d = d + 1; }
+ var mul : u64 = 0;
+ if (d == 1 and VE[0] == 49) { mul = 1; }
+ if (d == 2 and VE[0] == 49 and VE[1] == 48) { mul = 10; }
+ if (d == 3 and VE[0] == 49 and VE[1] == 48 and VE[2] == 48) { mul = 100; }
+ var unit : u64 = 9;
+ if (n - d == 1 and VE[d] == 115) { unit = 0; }
+ if (n - d == 2 and VE[d + 1] == 115) {
+ if (VE[d] == 109) { unit = 1; }
+ if (VE[d] == 117) { unit = 2; }
+ if (VE[d] == 110) { unit = 3; }
+ if (VE[d] == 112) { unit = 4; }
+ if (VE[d] == 102) { unit = 5; }
+ }
+ if (mul == 0 or unit == 9 or n >= 64) { return timescale_fail(); }
+ R[R_TSS] = 1;
+ R[R_TSM] = mul;
+ R[R_TSU] = unit;
+ }
+ if (cmd == 2) {
+ // version = tokens joined by a space
+ const at : u64 = R[R_NP];
+ var i2 : usize = 0;
+ while (i2 < nct) {
+ if (i2 > 0) {
+ if (R[R_NP] >= 16777000) { return -1; }
+ NAMEP[R[R_NP] as usize] = 32;
+ R[R_NP] = R[R_NP] + 1;
+ }
+ var k2 : usize = 0;
+ while (k2 < (CTL[i2] as usize)) {
+ if (R[R_NP] >= 16777000) { return -1; }
+ NAMEP[R[R_NP] as usize] = CB[(CTO[i2] as usize) + k2];
+ R[R_NP] = R[R_NP] + 1;
+ k2 = k2 + 1;
+ }
+ i2 = i2 + 1;
+ }
+ R[R_VERO] = at;
+ R[R_VERL] = R[R_NP] - at;
+ }
+ if (cmd == 3) {
+ var pick : usize = 99;
+ if (nct >= 2) { pick = 1; } else { if (nct >= 1) { pick = 0; } }
+ const at2 : u64 = R[R_NP];
+ if (pick == 99) {
+ NAMEP[at2 as usize] = 63;
+ R[R_NP] = at2 + 1;
+ } else {
+ const o : i64 = pool_put(CB[0 .. 1048576], CTO[pick] as usize, (CTO[pick] + CTL[pick]) as usize, 1);
+ if (o < 0) { return -1; }
+ }
+ if (R[R_NSC] >= 1024) { return -1; }
+ SCO[R[R_NSC] as usize] = at2 as u32;
+ SCL[R[R_NSC] as usize] = (R[R_NP] - at2) as u32;
+ R[R_NSC] = R[R_NSC] + 1;
+ R[R_SCOPES] = R[R_SCOPES] + 1;
+ }
+ if (cmd == 4) {
+ if (R[R_NSC] > 0) { R[R_NSC] = R[R_NSC] - 1; }
+ }
+ if (cmd == 5) {
+ rc = end_var();
+ if (rc != 0) { return rc; }
+ }
+ if (cmd == 6) {
+ R[R_HDR] = 0;
+ if (R[R_VARS] == 0) { return fail(5, 0, 0, 0); }
+ }
+ R[R_CMD] = 0;
+ R[R_NCT] = 0;
+ R[R_CBN] = 0;
+ return 0;
+}
+
+pub fn header_token() -> i32 {
+ if (R[R_CMD] != 0) {
+ if (tok_is("$end")) { return end_command(); }
+ const n : usize = R[R_NCT] as usize;
+ const tl : usize = R[R_TOKN] as usize;
+ if (n >= 65536 or (R[R_CBN] as usize) + tl >= 1048576) { return -1; }
+ CTO[n] = R[R_CBN] as u32;
+ CTL[n] = tl as u32;
+ var k : usize = 0;
+ while (k < tl) {
+ CB[(R[R_CBN] as usize) + k] = TOKB[k];
+ k = k + 1;
+ }
+ R[R_CBN] = R[R_CBN] + (tl as u64);
+ R[R_NCT] = (n + 1) as u64;
+ return 0;
+ }
+ if (TOKB[0] != 36) {
+ const pb : i64 = prefix_bytes(TOKB[0 .. 1048576], 0, R[R_TOKN] as usize, 40);
+ return fail(2, 0, pb as usize, 0);
+ }
+ var c : u64 = 7;
+ if (tok_is("$timescale")) { c = 1; }
+ if (tok_is("$version")) { c = 2; }
+ if (tok_is("$scope")) { c = 3; }
+ if (tok_is("$upscope")) { c = 4; }
+ if (tok_is("$var")) { c = 5; }
+ if (tok_is("$enddefinitions")) { c = 6; }
+ R[R_CMD] = c;
+ return 0;
+}
+
+pub fn extend_into(vl: usize, width: usize) -> bool {
+ // PVB[0..vl) -> VE[0..width): the rightmost width characters, or padded on the left
+ if (vl >= width) {
+ var k : usize = 0;
+ while (k < width) {
+ VE[k] = PVB[vl - width + k];
+ k = k + 1;
+ }
+ return true;
+ }
+ var fill : u8 = 48;
+ if (vl > 0) {
+ const c : u8 = PVB[0];
+ if (c == 120 or c == 88) { fill = 120; }
+ if (c == 122 or c == 90) { fill = 122; }
+ }
+ var i : usize = 0;
+ while (i < width - vl) {
+ VE[i] = fill;
+ i = i + 1;
+ }
+ var j : usize = 0;
+ while (j < vl) {
+ VE[width - vl + j] = PVB[j];
+ j = j + 1;
+ }
+ return true;
+}
+
+pub fn is_unknown_char(c: u8) -> bool {
+ return c == 120 or c == 88 or c == 122 or c == 90 or c == 117 or c == 85 or c == 119 or c == 87 or c == 45;
+}
+
+pub fn lower(c: u8) -> u8 {
+ if (c >= 65 and c <= 90) { return c + 32; }
+ return c;
+}
+
+/// /^(nan|x|z)$/i on PVB[0..vl)
+pub fn is_nan_x_z(vl: usize) -> bool {
+ if (vl == 1) {
+ const c : u8 = lower(PVB[0]);
+ return c == 120 or c == 122;
+ }
+ if (vl == 3) { return lower(PVB[0]) == 110 and lower(PVB[1]) == 97 and lower(PVB[2]) == 110; }
+ return false;
+}
+
+pub fn store_value(si: usize, src_is_ve: bool, len: usize) -> bool {
+ if ((SVC[si] as usize) < len) {
+ if (R[R_VP] + (len as u64) >= 33554432) { return false; }
+ SVO[si] = R[R_VP] as u32;
+ SVC[si] = len as u32;
+ R[R_VP] = R[R_VP] + (len as u64);
+ }
+ var k : usize = 0;
+ while (k < len) {
+ if (src_is_ve) { VALP[(SVO[si] as usize) + k] = VE[k]; } else { VALP[(SVO[si] as usize) + k] = PVB[k]; }
+ k = k + 1;
+ }
+ SVL[si] = len as u32;
+ SHV[si] = 1;
+ return true;
+}
+
+/// record(id, value, kind): kind 0 scalar, 1 b, 2 r, 3 s. The value is in PVB[0..vl); the id is TOKB[io..ih).
+pub fn record(io: usize, ih: usize, vl: usize, kind: u32) -> i32 {
+ if (R[R_BLOCK] == 1) { return 0; }
+ const sp : usize = find_sig(io, ih - io, 0);
+ if (sp == 0) {
+ R[R_ETS] = R[R_TSET];
+ return fail(6, io, ih - io, 0);
+ }
+ const si : usize = sp - 1;
+ SVA[si] = SVA[si] + 1;
+ const fl : u8 = SFG[si];
+ const isstr : bool = kind == 2 or kind == 3 or (fl % 2) == 1 or fl >= 4;
+ var known : bool = false;
+ var len : usize = vl;
+ if (isstr) {
+ known = !is_nan_x_z(vl);
+ } else {
+ var z : usize = 0;
+ while (z < vl) {
+ if (PVB[z] >= 128) {
+ R[R_UNSUP] = 1;
+ return 90;
+ }
+ z = z + 1;
+ }
+ len = SWD[si] as usize;
+ extend_into(vl, len);
+ known = true;
+ var i : usize = 0;
+ while (i < len) {
+ if (is_unknown_char(VE[i])) { known = false; }
+ i = i + 1;
+ }
+ }
+ if (known) { SEK[si] = 1; }
+ if (SHV[si] == 0) {
+ if (!store_value(si, !isstr, len)) { return -1; }
+ return 0;
+ }
+ var same : bool = (SVL[si] as usize) == len;
+ var c : usize = 0;
+ while (c < len and same) {
+ var nb : u8 = PVB[c];
+ if (!isstr) { nb = VE[c]; }
+ if (VALP[(SVO[si] as usize) + c] != nb) { same = false; }
+ c = c + 1;
+ }
+ if (same) { return 0; }
+ SCH[si] = SCH[si] + 1;
+ if (isstr) {
+ SFL[si] = SFL[si] + 1;
+ } else {
+ // flipsBetween(old, new) loops over the OLD length; a shorter new value makes JS throw
+ var f : u64 = 0;
+ var d : usize = 0;
+ while (d < (SVL[si] as usize)) {
+ if (d >= len) {
+ R[R_UNSUP] = 1;
+ return 90;
+ }
+ if (lower(VALP[(SVO[si] as usize) + d]) != lower(VE[d])) { f = f + 1; }
+ d = d + 1;
+ }
+ SFL[si] = SFL[si] + f;
+ }
+ if (!store_value(si, !isstr, len)) { return -1; }
+ return 0;
+}
+
+pub fn set_time_from_token() -> bool {
+ // TOKB[1..n) are digits
+ const n : usize = R[R_TOKN] as usize;
+ if (n - 1 > 19) { return false; }
+ var v : u64 = 0;
+ var k : usize = 1;
+ while (k < n) {
+ v = v * 10 + ((TOKB[k] - 48) as u64);
+ k = k + 1;
+ }
+ TF[0] = v as f64;
+ return true;
+}
+
+pub fn body_token() -> i32 {
+ const n : usize = R[R_TOKN] as usize;
+ if (R[R_PEND] != 0) {
+ const kind : u32 = R[R_PEND] as u32;
+ const vl : usize = R[R_PENDL] as usize;
+ R[R_PEND] = 0;
+ return record(0, n, vl, kind);
+ }
+ if (R[R_BLOCK] == 2) {
+ if (tok_is("$end")) { R[R_BLOCK] = 0; }
+ return 0;
+ }
+ const c : u8 = TOKB[0];
+ if (c == 35) {
+ var ok : bool = n >= 2;
+ var k : usize = 1;
+ while (k < n) {
+ if (!is_digit(TOKB[k])) { ok = false; }
+ k = k + 1;
+ }
+ if (!ok) {
+ const pb : i64 = prefix_bytes(TOKB[0 .. 1048576], 0, n, 40);
+ R[R_ETS] = R[R_TSET];
+ return fail(7, 0, pb as usize, 0);
+ }
+ if (!set_time_from_token()) { R[R_UNSUP] = 1; return 90; }
+ R[R_TSET] = 1;
+ if (R[R_TFS] == 0) {
+ TF[1] = TF[0];
+ R[R_TFS] = 1;
+ }
+ TF[2] = TF[0];
+ R[R_STAMPS] = R[R_STAMPS] + 1;
+ return 0;
+ }
+ if (c == 36) {
+ if (tok_is("$end")) {
+ R[R_BLOCK] = 0;
+ } else {
+ if (tok_is("$dumpoff")) { R[R_BLOCK] = 1; } else {
+ if (tok_is("$comment")) { R[R_BLOCK] = 2; } else { R[R_BLOCK] = 3; }
+ }
+ }
+ return 0;
+ }
+ var pk : u32 = 0;
+ if (c == 98 or c == 66) { pk = 1; }
+ if (c == 114 or c == 82) { pk = 2; }
+ if (c == 115 or c == 83) { pk = 3; }
+ if (pk != 0) {
+ var k2 : usize = 1;
+ while (k2 < n) {
+ PVB[k2 - 1] = TOKB[k2];
+ k2 = k2 + 1;
+ }
+ R[R_PEND] = pk as u64;
+ R[R_PENDL] = (n - 1) as u64;
+ return 0;
+ }
+ const sc : bool = c == 48 or c == 49 or c == 120 or c == 88 or c == 122 or c == 90 or c == 117 or c == 85 or c == 119 or c == 87 or c == 108 or c == 76 or c == 104 or c == 72 or c == 45;
+ if (sc and n > 1) {
+ PVB[0] = lower(c);
+ return record(1, n, 1, 0);
+ }
+ const pb2 : i64 = prefix_bytes(TOKB[0 .. 1048576], 0, n, 40);
+ R[R_ETS] = R[R_TSET];
+ return fail(7, 0, pb2 as usize, 0);
+}
+
+pub fn token_done() -> i32 {
+ var rc : i32 = 0;
+ if (R[R_HDR] == 1) { rc = header_token(); } else { rc = body_token(); }
+ R[R_TOKN] = 0;
+ return rc;
+}
+
+/// Feed bytes through the tokenizer (JS \s splits; a trailing partial token stays in TOKB).
+pub fn feed(s: []const u8, lo: usize, hi: usize) -> i32 {
+ var p : usize = lo;
+ while (p < hi) {
+ const w : usize = ws_len_at(s, p, hi);
+ if (w > 0) {
+ if (R[R_TOKN] > 0) {
+ const rc : i32 = token_done();
+ if (rc != 0) { return rc; }
+ }
+ p = p + w;
+ } else {
+ if (R[R_TOKN] >= 1048576) { return -1; }
+ TOKB[R[R_TOKN] as usize] = s[p];
+ R[R_TOKN] = R[R_TOKN] + 1;
+ p = p + 1;
+ }
+ }
+ return 0;
+}
+
+pub fn vcd_begin(max_header: u64) -> i32 {
+ var f : usize = 0;
+ while (f < 40) {
+ R[f] = 0;
+ f = f + 1;
+ }
+ var h : usize = 0;
+ while (h < 262144) {
+ HT[h] = 0;
+ h = h + 1;
+ }
+ R[R_HDR] = 1;
+ R[R_MAXH] = max_header;
+ TF[0] = 0.0;
+ TF[1] = 0.0;
+ TF[2] = 0.0;
+ return 0;
+}
+
+pub fn vcd_chars() -> u64 {
+ return R[R_CHARS];
+}
+
+pub fn vcd_push(text: []const u8) -> i32 {
+ if (R[R_STARTED] == 0) {
+ const hb : i64 = prefix_bytes(text, 0, text.len, 4096);
+ const hl : usize = hb as usize;
+ var z : usize = 0;
+ while (z < hl) {
+ if (text[z] == 0) { return fail(1, 0, 0, 0); }
+ z = z + 1;
+ }
+ var p : usize = 0;
+ var w : usize = ws_len_at(text, p, hl);
+ while (w > 0) {
+ p = p + w;
+ w = ws_len_at(text, p, hl);
+ }
+ if (p >= hl) {
+ if (R[R_HOLD] + (text.len as u64) > 8388608) { return -1; }
+ var k : usize = 0;
+ while (k < text.len) {
+ HOLD[(R[R_HOLD] as usize) + k] = text[k];
+ k = k + 1;
+ }
+ R[R_HOLD] = R[R_HOLD] + (text.len as u64);
+ R[R_CHARS] = R[R_CHARS] + units_of(text, 0, text.len);
+ return 0;
+ }
+ if (text[p] != 36) {
+ var e : usize = p;
+ while (e < hl and ws_len_at(text, e, hl) == 0) { e = e + 1; }
+ const pb : i64 = prefix_bytes(text, p, e, 40);
+ R[R_ECODE] = 2;
+ R[R_EL] = pb as u64;
+ var q : usize = 0;
+ while (q < (pb as usize)) {
+ ERRB[q] = text[p + q];
+ q = q + 1;
+ }
+ return 2;
+ }
+ R[R_STARTED] = 1;
+ }
+ R[R_CHARS] = R[R_CHARS] + units_of(text, 0, text.len);
+ if (R[R_HOLD] > 0) {
+ const rc0 : i32 = feed(HOLD[0 .. 8388608], 0, R[R_HOLD] as usize);
+ R[R_HOLD] = 0;
+ if (rc0 != 0) { return rc0; }
+ }
+ const rc : i32 = feed(text, 0, text.len);
+ if (rc != 0) { return rc; }
+ if (R[R_HDR] == 1 and R[R_CHARS] > R[R_MAXH]) { return fail(8, 0, 0, 0); }
+ return 0;
+}
+
+// ---------------------------------------------------------------- summarize
+pub fn name_less(a: u32, b: u32) -> bool {
+ const la : usize = SNL[a as usize] as usize;
+ const lb : usize = SNL[b as usize] as usize;
+ var k : usize = 0;
+ while (k < la and k < lb) {
+ const x : u8 = NAMEP[(SNO[a as usize] as usize) + k];
+ const y : u8 = NAMEP[(SNO[b as usize] as usize) + k];
+ if (x != y) { return x < y; }
+ k = k + 1;
+ }
+ return la < lb;
+}
+
+/// mode 0 busiest (changes desc, flips desc, name), 1 quietest (changes asc, flips asc, name), 2 name.
+/// true when a sorts strictly before b.
+pub fn before(mode: u32, a: u32, b: u32) -> bool {
+ const ca : u64 = SCH[a as usize];
+ const cb : u64 = SCH[b as usize];
+ const fa : u64 = SFL[a as usize];
+ const fb : u64 = SFL[b as usize];
+ if (mode == 0) {
+ if (ca != cb) { return ca > cb; }
+ if (fa != fb) { return fa > fb; }
+ return name_less(a, b);
+ }
+ if (mode == 1) {
+ if (ca != cb) { return ca < cb; }
+ if (fa != fb) { return fa < fb; }
+ return name_less(a, b);
+ }
+ return name_less(a, b);
+}
+
+/// Stable merge sort of SRT[0..n) by `before`.
+pub fn sort_idx(n: usize, mode: u32) -> bool {
+ 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 !before(mode, SRT[b], SRT[a]))) {
+ STMP[o] = SRT[a];
+ a = a + 1;
+ } else {
+ STMP[o] = SRT[b];
+ b = b + 1;
+ }
+ o = o + 1;
+ }
+ lo = lo + 2 * w;
+ }
+ var c : usize = 0;
+ while (c < n) {
+ SRT[c] = STMP[c];
+ c = c + 1;
+ }
+ w = w * 2;
+ }
+ return true;
+}
+
+pub fn has_xzuw(si: usize) -> bool {
+ var k : usize = 0;
+ while (k < (SVL[si] as usize)) {
+ const c : u8 = lower(VALP[(SVO[si] as usize) + k]);
+ if (c == 120 or c == 122 or c == 117 or c == 119 or c == 45) { return true; }
+ k = k + 1;
+ }
+ return false;
+}
+
+/// summarize(state, top).
+pub fn vcd_summarize(top: usize) -> i32 {
+ const n : usize = R[R_NSIG] as usize;
+ var nlive : usize = 0;
+ var changes : u64 = 0;
+ var flips : u64 = 0;
+ var i : usize = 0;
+ while (i < n) {
+ changes = changes + SCH[i];
+ flips = flips + SFL[i];
+ if (SFG[i] / 2 % 2 == 0) {
+ LIVE[nlive] = i as u32;
+ nlive = nlive + 1;
+ }
+ i = i + 1;
+ }
+ var nm : usize = 0;
+ var never : u64 = 0;
+ var j : usize = 0;
+ while (j < nlive) {
+ const s : usize = LIVE[j] as usize;
+ if (SCH[s] > 0) {
+ SRT[nm] = s as u32;
+ nm = nm + 1;
+ } else {
+ never = never + 1;
+ }
+ j = j + 1;
+ }
+ sort_idx(nm, 0);
+ var nb : usize = nm;
+ if (nb > top) { nb = top; }
+ var k : usize = 0;
+ while (k < nb and k < 8) {
+ BUSY[k] = SRT[k];
+ k = k + 1;
+ }
+ var m2 : usize = 0;
+ while (m2 < nm) {
+ j = 0;
+ m2 = nm;
+ }
+ // quietest: the movers again, other order
+ var q : usize = 0;
+ var t : usize = 0;
+ while (t < nlive) {
+ const s2 : usize = LIVE[t] as usize;
+ if (SCH[s2] > 0) {
+ SRT[q] = s2 as u32;
+ q = q + 1;
+ }
+ t = t + 1;
+ }
+ sort_idx(q, 1);
+ var nq : usize = q;
+ if (nq > top) { nq = top; }
+ k = 0;
+ while (k < nq and k < 8) {
+ QUIET[k] = SRT[k];
+ k = k + 1;
+ }
+ // stuck: values and never known; ends unknown: known once and the last value has x z u w -
+ var ns : usize = 0;
+ var ne : usize = 0;
+ var stuck_all : usize = 0;
+ var ends_all : usize = 0;
+ t = 0;
+ while (t < nlive) {
+ const s3 : usize = LIVE[t] as usize;
+ if (SVA[s3] > 0 and SEK[s3] == 0) {
+ SRT[stuck_all] = s3 as u32;
+ stuck_all = stuck_all + 1;
+ }
+ t = t + 1;
+ }
+ sort_idx(stuck_all, 2);
+ k = 0;
+ while (k < stuck_all and k < top and k < 8) {
+ STUCK[k] = SRT[k];
+ k = k + 1;
+ }
+ ns = stuck_all;
+ t = 0;
+ while (t < nlive) {
+ const s4 : usize = LIVE[t] as usize;
+ if (SVA[s4] > 0 and SEK[s4] == 1 and SHV[s4] == 1 and (SFG[s4] % 2) == 0 and SFG[s4] < 4 and has_xzuw(s4)) {
+ SRT[ends_all] = s4 as u32;
+ ends_all = ends_all + 1;
+ }
+ t = t + 1;
+ }
+ sort_idx(ends_all, 2);
+ k = 0;
+ while (k < ends_all and k < top and k < 8) {
+ ENDSX[k] = SRT[k];
+ k = k + 1;
+ }
+ ne = ends_all;
+ SUM[0] = R[R_TSS];
+ SUM[1] = R[R_TSM];
+ SUM[2] = R[R_TSU];
+ SUM[4] = R[R_VERO];
+ SUM[5] = R[R_VERL];
+ SUM[6] = R[R_STAMPS];
+ SUM[7] = R[R_CHARS];
+ SUM[8] = R[R_VARS];
+ SUM[9] = n as u64;
+ SUM[10] = (n - nlive) as u64;
+ SUM[11] = R[R_SCOPES];
+ SUM[12] = changes;
+ SUM[13] = flips;
+ SUM[14] = nb as u64;
+ SUM[15] = nq as u64;
+ SUM[16] = ns as u64;
+ var shown : usize = ns;
+ if (shown > top) { shown = top; }
+ SUM[17] = shown as u64;
+ SUM[18] = ne as u64;
+ var shown2 : usize = ne;
+ if (shown2 > top) { shown2 = top; }
+ SUM[19] = shown2 as u64;
+ SUM[20] = never;
+ SUM[21] = nm as u64;
+ SUT[0] = 0.0;
+ SUT[1] = 0.0;
+ if (R[R_TFS] == 1) {
+ SUT[0] = TF[1];
+ SUT[1] = TF[2];
+ }
+ SUT[2] = SUT[1] - SUT[0];
+ return 0;
+}
+
+pub fn vcd_end() -> i32 {
+ if (R[R_HOLD] > 0) {
+ const rc0 : i32 = feed(HOLD[0 .. 8388608], 0, R[R_HOLD] as usize);
+ R[R_HOLD] = 0;
+ if (rc0 != 0) { return rc0; }
+ }
+ if (R[R_TOKN] > 0) {
+ const rc : i32 = token_done();
+ if (rc != 0) { return rc; }
+ }
+ if (R[R_STARTED] == 0) { return fail(9, 0, 0, 0); }
+ if (R[R_HDR] == 1) { return fail(8, 0, 0, 0); }
+ if (R[R_PEND] != 0) {
+ // token: kind letter + value
+ var kc : u8 = 98;
+ if (R[R_PEND] == 2) { kc = 114; }
+ if (R[R_PEND] == 3) { kc = 115; }
+ ERRB[0] = kc;
+ var k : usize = 0;
+ while (k < (R[R_PENDL] as usize)) {
+ ERRB[k + 1] = PVB[k];
+ k = k + 1;
+ }
+ R[R_ECODE] = 7;
+ R[R_EL] = R[R_PENDL] + 1;
+ R[R_ETS] = R[R_TSET];
+ return 7;
+ }
+ return vcd_summarize(5);
+}
+
+/// vcdSummary(text, opts): chunks of 1048576 bytes.
+pub fn vcd_summary(text: []const u8, max_header: u64) -> i32 {
+ vcd_begin(max_header);
+ var i : usize = 0;
+ while (i < text.len) {
+ var e : usize = i + 1048576;
+ if (e > text.len) { e = text.len; }
+ const rc : i32 = vcd_push(text[i .. e]);
+ if (rc != 0) { return rc; }
+ i = e;
+ }
+ return vcd_end();
+}
+
+pub fn vcd_err_info(f: u32) -> u64 {
+ if (f == 0) { return R[R_ECODE]; }
+ if (f == 1) { return R[R_EO]; }
+ if (f == 2) { return R[R_EL]; }
+ if (f == 3) { return R[R_ETS]; }
+ return 0;
+}
+
+pub fn vcd_err_time() -> f64 {
+ return TF[0];
+}
+
+pub fn err_byte(i: u32) -> u8 {
+ return ERRB[i as usize];
+}
+
+pub fn pool_byte(i: u32) -> u8 {
+ return NAMEP[i as usize];
+}
+
+pub fn vs_info(f: u32) -> u64 {
+ return SUM[f as usize];
+}
+
+pub fn vs_time(f: u32) -> f64 {
+ return SUT[f as usize];
+}
+
+pub fn vs_row(kind: u32, i: u32, f: u32) -> u64 {
+ var s : usize = BUSY[i as usize] as usize;
+ if (kind == 1) { s = QUIET[i as usize] as usize; }
+ if (f == 0) { return SNO[s] as u64; }
+ if (f == 1) { return SNL[s] as u64; }
+ if (f == 2) { return SWD[s] as u64; }
+ if (f == 3) { return STO[s] as u64; }
+ if (f == 4) { return STL[s] as u64; }
+ if (f == 5) { return SCH[s]; }
+ if (f == 6) { return SFL[s]; }
+ return SAL[s] as u64;
+}
+
+pub fn vs_name(kind: u32, i: u32, f: u32) -> u64 {
+ var s : usize = STUCK[i as usize] as usize;
+ if (kind == 1) { s = ENDSX[i as usize] as usize; }
+ if (f == 0) { return SNO[s] as u64; }
+ return SNL[s] as u64;
+}
+
+// ---------------------------------------------------------------- spanUnits
+/// Small big integers (16 limbs of 32 bits) to place a double exactly against powers of ten.
+pub var BA : [20]u64 = [0; 20];
+pub var BB : [20]u64 = [0; 20];
+
+pub fn big_set(a: u32, v: u64) -> bool {
+ var k : usize = 0;
+ while (k < 20) {
+ if (a == 0) { BA[k] = 0; } else { BB[k] = 0; }
+ k = k + 1;
+ }
+ if (a == 0) {
+ BA[0] = v % 4294967296;
+ BA[1] = v / 4294967296;
+ } else {
+ BB[0] = v % 4294967296;
+ BB[1] = v / 4294967296;
+ }
+ return true;
+}
+
+pub fn big_mul(a: u32, m: u64) -> bool {
+ var carry : u64 = 0;
+ var k : usize = 0;
+ while (k < 20) {
+ var cur : u64 = 0;
+ if (a == 0) { cur = BA[k] * m + carry; } else { cur = BB[k] * m + carry; }
+ if (a == 0) { BA[k] = cur % 4294967296; } else { BB[k] = cur % 4294967296; }
+ carry = cur / 4294967296;
+ k = k + 1;
+ }
+ return true;
+}
+
+pub fn big_mul_pow(a: u32, base: u64, e: i64) -> bool {
+ var k : i64 = 0;
+ while (k < e) {
+ big_mul(a, base);
+ k = k + 1;
+ }
+ return true;
+}
+
+/// compare BA with BB: -1, 0, 1
+pub fn big_cmp() -> i32 {
+ var k : usize = 20;
+ while (k > 0) {
+ k = k - 1;
+ if (BA[k] > BB[k]) { return 1; }
+ if (BA[k] < BB[k]) { return -1; }
+ }
+ return 0;
+}
+
+/// Is val (= m * 2^e2) at least 10^p ? (p may be negative)
+pub fn val_ge_pow10(m: u64, e2: i64, p: i64) -> bool {
+ big_set(0, m);
+ big_set(1, 1);
+ if (e2 > 0) { big_mul_pow(0, 2, e2); } else { big_mul_pow(1, 2, 0 - e2); }
+ if (p < 0) { big_mul_pow(0, 10, 0 - p); } else { big_mul_pow(1, 10, p); }
+ return big_cmp() >= 0;
+}
+
+/// Number(val.toPrecision(4)) for a finite val >= 0 (set in SUV[0]); false when out of the carried range.
+pub fn to_precision4(val: f64) -> bool {
+ if (val == 0.0) {
+ SUV[0] = 0.0;
+ return true;
+ }
+ var x : f64 = val;
+ var e2 : i64 = 0;
+ while (x >= 9007199254740992.0) {
+ x = x / 2.0;
+ e2 = e2 + 1;
+ }
+ while (x < 4503599627370496.0) {
+ x = x * 2.0;
+ e2 = e2 - 1;
+ }
+ const m : u64 = x as u64;
+ var e0 : i64 = 0;
+ var y : f64 = val;
+ while (y >= 10.0) {
+ y = y / 10.0;
+ e0 = e0 + 1;
+ }
+ while (y < 1.0) {
+ y = y * 10.0;
+ e0 = e0 - 1;
+ }
+ while (!val_ge_pow10(m, e2, e0)) { e0 = e0 - 1; }
+ while (val_ge_pow10(m, e2, e0 + 1)) { e0 = e0 + 1; }
+ var q : i64 = e0 - 3;
+ // n = floor(val / 10^q + 1/2): the largest n with n * 2 * den <= 2 * num + den
+ var lo : u64 = 0;
+ var hi : u64 = 20000;
+ while (lo < hi) {
+ const mid : u64 = (lo + hi + 1) / 2;
+ // num = m * 2^e2 * 10^max(-q,0); den = 2^max(-e2,0) * 10^max(q,0)
+ big_set(0, m);
+ big_set(1, 1);
+ if (e2 > 0) { big_mul_pow(0, 2, e2); } else { big_mul_pow(1, 2, 0 - e2); }
+ if (q < 0) { big_mul_pow(0, 10, 0 - q); } else { big_mul_pow(1, 10, q); }
+ // left: mid * 2 * den (in BB), right: 2 * num + den
+ var k : usize = 0;
+ var carry : u64 = 0;
+ var tmp : [20]u64 = [0; 20];
+ while (k < 20) {
+ tmp[k] = BB[k];
+ k = k + 1;
+ }
+ big_mul(1, mid * 2);
+ // BA = 2 * num + den
+ big_mul(0, 2);
+ k = 0;
+ carry = 0;
+ while (k < 20) {
+ const cur : u64 = BA[k] + tmp[k] + carry;
+ BA[k] = cur % 4294967296;
+ carry = cur / 4294967296;
+ k = k + 1;
+ }
+ if (big_cmp() >= 0) { lo = mid; } else { hi = mid - 1; }
+ }
+ var n : u64 = lo;
+ if (n >= 10000) {
+ n = 1000;
+ q = q + 1;
+ }
+ if (q > 22 or q < -22) { return false; }
+ var r : f64 = n as f64;
+ if (q >= 0) { r = r * P10[q as usize]; } else { r = r / P10[(0 - q) as usize]; }
+ SUV[0] = r;
+ return true;
+}
+
+/// spanUnits(ticks, timescale): the value in su_value(), the unit index in the return (-1 without a timescale), -2 not carried.
+pub fn span_units(ticks: f64, has_ts: bool, mul: u64, exp: i64) -> i32 {
+ if (!has_ts) {
+ SUV[0] = ticks;
+ return -1;
+ }
+ P10[0] = 1.0;
+ var e : usize = 1;
+ while (e < 23) {
+ P10[e] = P10[e - 1] * 10.0;
+ e = e + 1;
+ }
+ const v : f64 = ticks * (mul as f64);
+ var i : i64 = 0;
+ while (i < 6) {
+ var ue : i64 = 0;
+ if (i == 1) { ue = -3; }
+ if (i == 2) { ue = -6; }
+ if (i == 3) { ue = -9; }
+ if (i == 4) { ue = -12; }
+ if (i == 5) { ue = -15; }
+ const shift : i64 = exp - ue;
+ var p : f64 = 1.0;
+ if (shift >= 0) { p = P10[shift as usize]; } else { p = 1.0 / P10[(0 - shift) as usize]; }
+ const val : f64 = v * p;
+ if (val >= 1.0 or i == 5) {
+ if (!to_precision4(val)) { return -2; }
+ return i as i32;
+ }
+ i = i + 1;
+ }
+ return -2;
+}
+
+pub fn su_value() -> f64 {
+ return SUV[0];
+}
+
+// ---------------------------------------------------------------- tests
+pub var LST : [1]u8 = [0; 1];
+
+pub fn pool_is(o: u64, l: u64, w: []const u8) -> bool {
+ if ((l as usize) != w.len) { return false; }
+ var k : usize = 0;
+ while (k < w.len) {
+ if (NAMEP[(o as usize) + k] != w[k]) { return false; }
+ k = k + 1;
+ }
+ return true;
+}
+
+pub fn err_is(w: []const u8) -> bool {
+ if ((R[R_EL] as usize) != w.len) { return false; }
+ var k : usize = 0;
+ while (k < w.len) {
+ if (ERRB[k] != w[k]) { return false; }
+ k = k + 1;
+ }
+ return true;
+}
+
+pub fn counter_like() -> i32 {
+ return vcd_summary("$date Mon Oct 5 01:46:09 2026 $end $version Icarus Verilog $end $timescale 1ps $end $scope module tb $end $var wire 1 ! ready $end $var wire 8 \" count [7:0] $end $var reg 1 # clk $end $var reg 1 $ en $end $var parameter 32 % W [31:0] $end $scope module dut $end $var wire 1 # clk $end $upscope $end $upscope $end $enddefinitions $end $comment Show the parameter values. $end $dumpall $end #0 $dumpvars bx \" 0# 0$ b101 % 1! $end #5000 1# b1 \" #10000 0# 1# b11 \" #15000 0# b10 \" 1# #20000 bx \" 0#", 67108864);
+}
+
+test a_small_counter_dump_is_summarised {
+ assert counter_like() == 0;
+ assert vs_info(0) == 1;
+ assert vs_info(1) == 1;
+ assert vs_info(2) == 4;
+ assert pool_is(vs_info(4), vs_info(5), "Icarus Verilog");
+ assert vs_info(6) == 5;
+ assert vs_info(7) == 494;
+ assert vs_time(0) == 0.0;
+ assert vs_time(1) == 20000.0;
+ assert vs_time(2) == 20000.0;
+ assert vs_info(8) == 6;
+ assert vs_info(9) == 5;
+ assert vs_info(10) == 1;
+ assert vs_info(11) == 2;
+ assert vs_info(12) == 10;
+ assert vs_info(13) == 24;
+ assert vs_info(21) == 2;
+ assert vs_info(20) == 2;
+ assert vs_info(18) == 1;
+ assert pool_is(vs_name(1, 0, 0), vs_name(1, 0, 1), "tb.count");
+ assert vs_info(16) == 0;
+}
+
+test the_busiest_and_quietest_rows_sort_by_changes_flips_then_name {
+ assert counter_like() == 0;
+ assert vs_info(14) == 2;
+ assert pool_is(vs_row(0, 0, 0), vs_row(0, 0, 1), "tb.clk");
+ assert vs_row(0, 0, 5) == 6;
+ assert vs_row(0, 0, 6) == 6;
+ assert vs_row(0, 0, 7) == 1;
+ assert pool_is(vs_row(0, 0, 3), vs_row(0, 0, 4), "reg");
+ assert pool_is(vs_row(0, 1, 0), vs_row(0, 1, 1), "tb.count");
+ assert vs_row(0, 1, 2) == 8;
+ assert vs_row(0, 1, 5) == 4;
+ assert vs_row(0, 1, 6) == 18;
+ assert pool_is(vs_row(1, 0, 0), vs_row(1, 0, 1), "tb.count");
+ assert pool_is(vs_row(1, 1, 0), vs_row(1, 1, 1), "tb.clk");
+}
+
+test x_and_z_values_are_stuck_or_end_unknown {
+ assert vcd_summary("$timescale 10 ns $end $scope module m $end $var wire 1 a sa $end $var wire 4 b sb $end $var wire 1 c sc $end $upscope $end $enddefinitions $end #0 xa b1x1 b 1c #4 xa b0000 b 0c #9 zc", 1000) == 0;
+ assert vs_info(1) == 10;
+ assert vs_info(2) == 3;
+ assert vs_info(12) == 3;
+ assert vs_info(13) == 5;
+ assert vs_info(16) == 1;
+ assert pool_is(vs_name(0, 0, 0), vs_name(0, 0, 1), "m.sa");
+ assert vs_info(18) == 1;
+ assert pool_is(vs_name(1, 0, 0), vs_name(1, 0, 1), "m.sc");
+ assert vs_info(20) == 1;
+ assert vs_info(21) == 2;
+ assert pool_is(vs_row(0, 0, 0), vs_row(0, 0, 1), "m.sc");
+ assert vs_row(0, 1, 6) == 3;
+}
+
+test the_errors_carry_codes_and_details {
+ assert vcd_summary("", 1000) == 9;
+ assert vcd_summary("hello world", 1000) == 2;
+ assert err_is("hello");
+ assert vcd_summary("$timescale 5ns $end", 1000) == 3;
+ assert err_is("5ns");
+ assert vcd_summary("$var wire x ! a $end", 1000) == 4;
+ assert err_is("wire x ! a");
+ assert vcd_summary("$timescale 1ns $end $enddefinitions $end", 1000) == 5;
+ assert vcd_summary("$timescale 1ns $end $scope module m $end $var wire 1 a s $end $upscope $end $enddefinitions $end #1 1q", 1000) == 6;
+ assert err_is("q");
+ assert vcd_err_time() == 1.0;
+ assert vcd_summary("$timescale 1ns $end $scope module m $end $var wire 1 a s $end $upscope $end $enddefinitions $end #1 q", 1000) == 7;
+ assert vcd_summary("$timescale 1ns $end $scope module m $end $var wire 1 a s $end", 1000) == 8;
+ assert vcd_summary("$timescale 1ns $end $scope module m $end $var wire 1 a s $end $upscope $end $enddefinitions $end #1 b101", 1000) == 7;
+ assert err_is("b101");
+ assert vcd_summary("$timescale 1ns $end $scope module m $end $var wire 1 a s $end $enddefinitions $end", 3) == 8;
+}
+
+test dumpoff_comments_aliases_and_extension_follow_the_javascript_rules {
+ assert vcd_summary("$timescale 1ns $end $scope module m $end $var wire 4 a v [3:0] $end $scope module n $end $var wire 4 a w $end $upscope $end $var real 1 r x $end $upscope $end $enddefinitions $end #0 b1 a r1.5 r #1 $dumpoff bx a $end b11 a r2.5 r #2 $comment b0 a $end 1a r2.5 r #3 b0101 a", 1000) == 0;
+ assert vs_info(8) == 3;
+ assert vs_info(9) == 2;
+ assert vs_info(12) == 4;
+ assert vs_info(13) == 4;
+ assert vs_row(0, 0, 7) == 1;
+ assert pool_is(vs_row(0, 0, 0), vs_row(0, 0, 1), "m.v");
+ assert vs_row(0, 0, 5) == 3;
+ assert vs_row(0, 0, 6) == 3;
+ assert pool_is(vs_row(0, 1, 0), vs_row(0, 1, 1), "m.x");
+ assert vs_info(21) == 2;
+}
+
+test span_units_picks_the_largest_unit_and_rounds_to_four_digits {
+ assert span_units(2925000.0, true, 1, -12) == 2;
+ assert su_value() == 2.925;
+ assert span_units(450000.0, true, 1, -12) == 3;
+ assert su_value() == 450.0;
+ assert span_units(1234567.0, true, 1, -9) == 1;
+ assert su_value() == 1.235;
+ assert span_units(12345.0, true, 10, -9) == 2;
+ assert su_value() == 123.5;
+ assert span_units(0.0, true, 1, -9) == 5;
+ assert su_value() == 0.0;
+ assert span_units(77.0, false, 1, 0) == -1;
+ assert su_value() == 77.0;
+ assert span_units(3.0, true, 1, 0) == 0;
+ assert su_value() == 3.0;
+}