Commit graph

15 commits

Author SHA1 Message Date
Stephen Dennis
1baefcf167 test(jit_diff): add float, branch and nested-evaluation corpus shapes
Three consecutive defects escaped this fuzzer for the same reason: the
corpus was string and list functions over literal operands.

  #1143  float PHI arms collapsed to a string PHI, copying raw double
         bits as a C string
  #1157  a bare %N used as an ifelse() condition folded to 0
  #1159  FP slot addresses pinned at 0 in nested compiles, so every
         float temporary aliased guest address 0

None of the three is reachable with literal operands: a folded float
allocates no FP slot and a folded condition emits no branch. So runtime
values come from v(fz.*) attributes, defined in a per-batch preamble.
v() is an ECALL the compiler cannot see through, whereas compile-time %q
tracking can fold %q back to a constant -- the trap that made
ifelse(%q0,...) look healthy while ifelse(%0,...) was broken.

New default-on shapes, roughly 15% float / 15% branch / 14% nested:

  - float arithmetic (fdiv, mul, sqrt, power, fmod, round, ...) with at
    least one runtime operand guaranteed
  - ifelse()/switch() over a runtime condition with type-MIXED arms,
    which is the PHI-typing surface
  - u()/ulocal() of a generated attribute body, always consumed by an
    outer call -- the wrapper is the point, since a bare u() compiles
    through the one-shot compiler and was always correct while a u()
    inside a compiled expression routes through the shared heap

Also fixes two minimizer defects that made the new shapes unusable:

  - run_chunk and show_value never replayed the corpus setup, so
    v(fz.*) was empty and u(me/fz.uN) undefined in every minimized
    run. Every nested finding reduced to "no divergence" and was
    misfiled as STATE-DEPENDENT. gen.py now writes setup.txt and the
    minimizer replays it.
  - show_value ran BOTH sides in the toggle-on process, so its INTERP
    column was really the JIT value and it printed two identical
    numbers under a heading claiming they differ. It now splits across
    exp.conf/int.conf exactly as run_chunk does.

Before: 5 bogus reproducers with equal values, 23 STATE-DEPENDENT.
After:  28 distinct minimal reproducers, 0 STATE-DEPENDENT.

Verified against 681b1bc66 (all three defects present): 28 LOGIC, with
minimal reproducers landing on each class -- u(me/fz.uN) for #1159,
ifelse(ab cd,...) for #1157, switch() with float arms returning empty
for #1143. Against current master: 0 LOGIC at SEED=1 and SEED=7, and
--brackets/--utf8/--longreg all still clean.

SEED=42 surfaces #1171, a real pre-existing mul()/power() precision
divergence this corpus found on a clean master. sqrt/power are left in
deliberately rather than trimmed to keep runs green; it is recorded
under Known divergences in the README.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-25 02:18:15 -06:00
Stephen Dennis
d41748e255 jit: flip jit_eval_brackets default ON, retire jiteval (Phase 5 flip)
The guard-lift campaign's final change (docs/plan-jit-evalbracket-lift
.md): eval brackets are JIT-compiled by default.  Per the #1001 review
checklist, the same commit removes the netmux.conf soak opt-in (that
file ships) and retires jiteval() — with the default on, the
production route reaches everything the gate-bypass existed for.

Harnesses moved off default-off assumptions:
- jit_diff's I-side conf sets jit_eval_brackets 0 explicitly (relying
  on the default would compare JIT against JIT).
- The q-register oracle now compares PRODUCTION evaluation across two
  workspaces (default conf = JIT vs explicit 0 = AST) with u()
  carriers and a jitstats canary; all nine shapes green.

The flip surfaced three latent items — Makesmoke bakes the smoke
database by EXECUTING setup commands under the conf default, a third
evaluation context never before run toggle-on:
1. #$ (switch token) was unimplemented in the lowerer and fell
   through as literal text: @switch actions like [idiv(#$,2)]
   computed idiv("#$",2) = 0.  The lowerer now bails compilation on
   #$, preserving AST semantics.
2. fdepth()/fcount() read func_nest_lev/func_invk_ctr, which
   compiled code does not maintain (native lowering flattens the
   nest): they read 0.  The lowerer bails on both.  The wider
   function_recursion_limit design question (cost guard vs semantic
   contract) is filed as #1002 — not a blocker, as the divergence
   direction is fail-open into more capability.
3. The Smoke classifier greped case-sensitively for 'Failed';
   nested_depth.mux's lowercase "failed" message slipped through and
   the suite reported ALL PASSED around a real failure.  Now -ci.

Final matrix: smoke 1318/1318 with the new default AND with the
toggle explicitly off; the smoke.flat re-bake is byte-identical to
the pre-flip bake; oracle 9/9 on the production route; sweeps
standard and brackets+utf8+longreg(SEED=7) both 400/0 LOGIC.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 09:09:39 -06:00
Stephen Dennis
3bd5df2c12 jit: apply #1001 soak review — COLOR halts, log history, flip checklist
- soak.sh halts on any COLOR-encoding divergence too (post-#995 the
  baseline is 0 everywhere, so a new one is a regression signal that
  previously scrolled by as a summary count), and appends a
  timestamped header instead of truncating prior soak history.
- Plan doc records the completed two-box soak evidence and the flip
  checklist — including that the default-flip commit removes the
  netmux.conf soak opt-in (that file ships; the opt-in must not
  outlive the soak).

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 08:35:14 -06:00
Stephen Dennis
03c377eb84 jit: start the Phase 5 toggle-on soak
- testcases/tools/jit_diff/soak.sh: soak driver — rounds of 400-expr
  differential sweeps with rotating seeds across every corpus mode
  combination (brackets always on; utf8/longreg rotate), plus a full
  toggle-on smoke run every 10th round.  Stops with the work dir
  preserved on any LOGIC divergence.  Runs identically on the Linux
  box.
- mux/game/netmux.conf: dev instances now run with jit_eval_brackets
  on, so all interactive/manual testing exercises the bracket surface.
  The SHIPPED default (conf.cpp init) remains off until the soak
  completes and the default flips.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 06:28:15 -06:00
Stephen Dennis
08fb38a523 jit: long q-register bitmap + read diamond (#996 step 2)
Implements the reviewed #996 step-2 design: registers whose
authoritative value exceeds the 256-byte SUBST slot are tracked in a
guest-memory bitmap, and %q reads branch to the authoritative fun_r
ECALL instead of reading a truncated slot.  Long-register programs now
stay on the JIT (step 1's entry declines removed) and the previously
unfixable mid-program case — a long value created inside the program
by SETQ_SYNC — reads correctly.

Per the review requirements:
- QREG_LONGBITS: dedicated u64 at the SUBST-end/DMA gap; the entry
  marshal whole-word writes it every run (computed bits for masked
  registers, zero elsewhere) so the reused runtime buffer can never
  leak a previous program's stale bits.
- Writers: entry marshal, ECALL_SETQ/ECALL_SETQ_PACK (bit update from
  the vlen they already measure), and the post-ECALL resync.  Scope
  restores fall out of the resync — proven by the new four-transition
  oracle shapes (long->short->restore and the SETQ_SYNC-created-long
  inverse: '1300' / '3001').
- Readers: single choke point emit_qreg_read() at the only
  SUBST_QREG0 sref site (grep-audited); the diamond composes from
  existing HIR ops (HIR_LUA_ALOAD as plain guest u64 load, SHR, BAND,
  BRC/PHI) — no new opcodes or codegen.
- fun_r exempted from the conservative post-ECALL resync (pointer
  compare), so the long path doesn't re-marshal per read.
- Honest cost: load+test+branch per %q read; rvbench six-read expr
  0.19us/call vs 0.16 pre-diamond, still ~3x faster than native AST.

bail_longreg remains in jitstats and now reads 0 permanently (the
acceptance signal that declines were superseded).  Corpus --longreg
gains mid-program setq-then-read shapes; TC018 locks mid-program and
both scope transitions bracket-free.

Tests: smoke 1318/1318 both toggles; oracle 9/9; sweeps standard,
longreg x2 seeds, longreg+utf8+brackets all 400/0 LOGIC; entry (303),
mid-program (303), and short (203) shapes verified with
eval_handled=1 bail_longreg=0.

Closes #996.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 01:12:38 -06:00
Stephen Dennis
f40add066c jit: decline runs on long q-registers at entry marshal (#996 step 1)
%q SUBST slots are 256 bytes; register values longer than 255 bytes
silently truncated on every slot write, so a JIT %q read returned 255
bytes where the interpreter returns the full value (found by review,
#996; production-reachable: setq(0,repeat(x,300)) then a %q0 read).

Step 1, the entry-side half: marshal_qregs_to_slots now reports when a
masked register exceeds the slot, and both entry-marshal sites decline
the run to the AST evaluator — nothing has executed yet, so declining
is side-effect-free (the #875 "decline cleanly" pattern).  New
bail_longreg jitstats counter.  The mid-program resync cannot decline
and still truncates; that half is #996 step 2, designed for review in
docs/plan-jit-evalbracket-lift.md (guest-memory long-register bitmap
maintained by marshal/SETQ/resync, %q reads branching to a fun_r ECALL
when the bit is set; OUT_SLOT=LBUF so long values flow intact
everywhere else).

Corpus: JITDIFF_LONGREG=1 mode — an interpreter preamble sets %q9 to
lengths straddling 256 and the measured expression reads it (mid-
program setq-then-read shapes deliberately excluded until step 2).
TC017 locks the decline via a fresh u() inner evaluation: 300 bytes
declines (result 303, was 258), 200 bytes stays on the JIT.

Tests: repro returns 303 with eval_bailout=1 bail_longreg=1; smoke
1317/1317 both toggles; oracle 7/7; sweeps standard, longreg x2 seeds,
and longreg+utf8+brackets all 400/0 LOGIC.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 00:47:18 -06:00
Stephen Dennis
1d598e1ab0 jit: tier2 wrapper audit — five interpreter-mirror fixes + UTF-8 corpus
Systematic audit of all 79 rv64/co_*_wrap tier2 wrappers against their
interpreter authorities (three parallel code readers; every claimed
divergence verified or refuted empirically on both routes before
touching anything).  Five live divergences, all default-route:

- co_lpos_wrap: mirror fun_lpos — empty pattern defaults to a space
  (was: empty result) and the FULL pattern matches via color-aware
  co_search (was: first byte only, so lpos(abxab,bx) reported every
  'b').  The constant-fold path used single-byte co_lpos too; it now
  declines patterns longer than one byte.
- co_splice_wrap: search-word validation counts words like countwords
  (space delim trims/collapses), so a blank or padded search word is
  no longer "#-1 TOO MANY WORDS".
- setunion/setdiff/setinter: the blob comparator implements only
  a/i/n/d — AutoDetect ('?' or present-but-empty) and f/u/c sort
  types now gate to the interpreter at compile time (was: '?' sorted
  ASCII where the interpreter autodetects numeric).
- rv64_isnum: full ParseFloat mirror — exponents ([eE][+-]1..4
  digits), surrounding whitespace, and Inf/Ind/Nan are numbers.
- rv64_isint: is_integer mirror — surrounding whitespace accepted.

Refuted (unreachable, verified empirically incl. runtime-computed
separators): the multi-char sep and osep-edge divergences the wrappers
would have are all blocked by the hir_lower delim gating.  Dead rows
noted in the plan: co_vislen_wrap, rv64_dec2hex/hex2dec (unrouted),
rv64_null, rv64_round (deliberately ECALL).  ~65 wrappers MIRROR OK.

Adds JITDIFF_UTF8=1 corpus mode (multi-byte words incl. emoji+modifier
cluster in every generator shape) and bracket-free TC016 locking the
five fixes on the default JIT route.  softlib.rv64 rebuilt.

Tests: smoke 1316/1316 both toggles; oracle 7/7; sweeps standard,
utf8, and utf8+brackets(SEED=7) all 400/0 LOGIC.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-21 00:27:17 -06:00
Stephen Dennis
d9f8b771e9 jit: mirror fun_after/fun_before in the rv64 wrappers (#993, #980)
Hunting the #993 "state-dependent" divergence found it was
deterministic all along: minimize.py's isolation replay was never
updated for the Phase 4 J/I process split — it replayed both sides in
one toggle-on workspace, comparing JIT against JIT, and misclassified
the finding as STATE-DEPENDENT.  The minimizer now replays the I side
in a separate process under int.conf (toggle off), like run.sh.

The underlying bug: rv64_after/rv64_before did a raw byte search with
no trim_space_sep, so the default-pattern haystack kept leading and
trailing spaces the interpreter trims — after(cat(a,),) returned "b "
vs the AST's "b" (the #993 LOGIC, surfacing through
stripansi(after(cat(...),extract(...))) in the bracket sweep).

Both wrappers are now exact fun_after/fun_before mirrors:
- default single-space pattern trims the haystack via a pointer+length
  trim_space_sep equivalent (guest fargs are const);
- the search is color-aware co_search, and after() advances past the
  needle with co_visible_advance over visible code points.

The color-aware search also fixes #980 (a colored needle never matched
the raw-byte comparison: after(ansi(r,ij x cd ab x),ansi(r,cd))
returned empty) and #993's COLOR-encoding case (raw shas now equal).

Locked by bracket-free jit_parity_fn.mux TC015 (trim + color + explicit
-pattern regression) on the default JIT route.  softlib.rv64 rebuilt.

Tests: smoke 1315/1315 both toggles; oracle 7/7; jit_diff standard +
brackets x 3 seeds (default/7/13) all 0 LOGIC, 0 COLOR — the two
formerly-failing seeds fully clean.

Fixes #993.  Fixes #980.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 22:34:08 -06:00
Stephen Dennis
46d6cae13f jit: Phase 4 — lift the AST_EVALBRACKET guard behind jit_eval_brackets
The guard-lift itself (docs/plan-jit-evalbracket-lift.md, Phase 4):
jit_can_handle() now admits terminated [...] eval brackets when the new
jit_eval_brackets config directive is on (cf_bool, CA_GOD, default
OFF — production behavior unchanged).  Unterminated brackets always
bail; EV_NOFCHECK text is gated at the call site (literal passthrough
that the lowerer doesn't model).

Harness upgrades required to make the toggle-on green check
meaningful:
- jit_diff J/I sides split into separate processes: the I side always
  runs with the toggle off, keeping the eval-bracket bail (the true
  production interpreter route, production flags) as the oracle.  An
  asteval({...})-forced in-process I side was tried first and
  manufactured ~113 false LOGIC divergences — fun_asteval trims a
  trailing space after an empty-yielding bracket that production
  preserves (filed as #987).
- JITDIFF_BRACKETS=1 mode: bracket-wrapped corpus (embedded/adjacent/
  pure shapes), toggle in the J-side conf only, and a canary that
  fails fast if the toggle didn't take.
- SMOKE_EXTRA_CONF passthrough in the smoke runner for whole-suite
  toggle-on runs.

Results: with the toggle ON, letq_fn TC002 (the historical INNERINNER
failure) and the localize scoping cases route through the JIT and
pass; the bracket sweep is 400/0 LOGIC.  The toggle-on smoke run also
witnessed two PRE-EXISTING production JIT bugs (bracket-independent,
reproduced bracket-free on today's default route): #988 maxArgsParsed
comma-catenation dropped (sha1(abc,def) -> sha1("abc")) and #989
tier2 wordpos() UTF-8 position miscount.  Both block the Phase 5
default-on flip, not this landing.

Toggle OFF (default): oracle 7/7, smoke 1310/1310, standard sweep
400/0 LOGIC — byte-identical, cleanly gated.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-20 21:01:21 -06:00
Stephen Dennis
288bd6d10e tests: jit_diff minimizer -- standalone re-verification and robustness
Closes the isolation gap from the #784 review:

- The original expression is confirmed to diverge IN ISOLATION (fresh
  single-expression muxscript process) before shrinking.  If it only
  diverges alongside its fuzz batch -- the order/state-dependent #778
  class -- it is reported as STATE-DEPENDENT with its original form,
  instead of shrink() returning the input and show_value() printing a
  "minimal reproducer" whose JIT and interp values are equal.
- Candidate reductions are still screened in shared batches for speed,
  but a reduction is only ACCEPTED after it diverges standalone; batch
  artifacts fall through to the next-shortest candidate.  diverges()
  is no longer dead code.
- run.sh preserves the work dir on a failing run (trap - EXIT) so a
  STATE-DEPENDENT finding's batch (b*.txt) can actually be replayed --
  previously the trap deleted the only reproducer.

Robustness items from the same review:

- Batches are chunked at 50 candidates per fresh muxscript process
  (variants() can emit hundreds, overflowing the command queue), and
  "Run away" in any run now warns to stderr instead of silently
  reading dropped results as "does not diverge".
- Process timeout moved from a parsed "timeout 90 <path>" prefix
  string (which broke on paths with spaces) to subprocess timeout=
  with a JITDIFF_MUX_BIN / JITDIFF_TIMEOUT interface; the legacy
  JITDIFF_MUX prefix is still accepted.  TimeoutExpired warns and
  degrades instead of crashing.
- Result-line indices are digit-validated, so stray J~/I~ text in
  evaluated output can no longer crash the minimizer (ValueError).

Verified end-to-end against a real divergence by installing the
pre-#789 blob (known remove() trailing-empty-word bug):
squish(remove(strcat(cd|x|gh,|),x,|)) shrank to the remove() call
with correct differing values; a non-diverging input was classified
STATE-DEPENDENT; a full run.sh fuzz round found, minimized, and
preserved the work dir with exit 1.  Restored blob: 2x200
expressions, 0 LOGIC, 0 COLOR.

Closes #784.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 07:06:18 -06:00
Stephen Dennis
51259fad18 tests: color-encoding byte-parity coverage
- delim_parity_fn.mux TC007: encode64()-based raw-byte assertions --
  no-op ldelete/replace/insert return their colored input
  byte-identically, setunion tie-breaking (1x1 keeps LIST1's copy,
  general merge keeps LIST2's), and mid() excludes color outside the
  copied cluster range.
- jit_diff gen.py: delimiter lists can now be single-element, the
  shape that reaches handle_sets' identical-1x1 special case.
- README: COLOR reports are no longer expected noise -- a clean run is
  0 LOGIC and 0 COLOR; any COLOR report is a real finding.

smoke.flat regenerated (1109 tests).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 06:48:48 -06:00
Stephen Dennis
0aaac9ceb7 tests: empty-element split_token coverage (#789)
- delim_parity_fn.mux TC006: eleven fail-closed assertions covering
  the empty-element model -- words(a||b,|)=3, member of middle and
  trailing empties, match(,)=1, graball/revwords/elements/remove with
  trailing empty words.
- jit_diff gen.py: delimiter-list elements are now empty ~30% of the
  time, the coverage that surfaced the rv64_remove trailing-empty
  drop.  smoke.flat regenerated (1108 tests).

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-10 05:55:22 -06:00
Stephen Dennis
2700e56a15 tests: jit_diff -- probe for a JIT build, account for vanished batches
Two false-pass paths in the differential fuzzer:

- --enable-jit is off by default and the whole JIT path is compiled
  out without it, so on a default build both sides ran the AST
  interpreter and run.sh exited 0 claiming "no logic divergence".
  Probe via jitstats() (only registered under TINYMUX_JIT) before
  generating anything, and exit 2 with reconfigure instructions on a
  non-JIT build.  README now states the --enable-jit requirement.

- If an entire batch crashed or timed out, its ids never reached
  results.txt and vanished from the awk seen[] array -- no MISSING
  lines, exit 0, with only a lower "compared" count on stderr as a
  hint.  Iterate ids 0..COUNT-1 instead of the ids that produced
  output, so dropped batches surface as MISSING.

Also mark #772 fixed in the README's known-divergences section (the
bTrunc flip landed in 4fc70fa54) and document the minimizer's
shared-process limitation (#784).

Verified: run.sh 30 -> probe passes on this JIT build, 30 compared,
0 LOGIC, 0 missing.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-06-09 23:42:34 -06:00
Stephen Dennis
7636a40b80 tests: jit_diff fuzzer — add minimizer, hoisting, and delim/osep coverage
- minimize.py: shrink divergent expressions to minimal reproducers via
  delta-debugging (typed leaf replacement + subtree hoisting), tested by
  running candidates through muxscript; prints deduped minimal forms with
  their actual JIT and interpreter values.  run.sh auto-minimizes the LOGIC
  divergences instead of dumping verbose raw expressions.
- gen.py: ~25% of roots now exercise custom single-char delimiters and
  output separators (incl. multi-char osep), built on flat leaf lists so the
  list/delimiter stay consistent — the class where ldelete/extract bugs lived.

Immediately paid off: the delim coverage found that setunion/setdiff/setinter
drop a multi-char osep to a single char under the JIT (e.g. setdiff(...,|,::)
-> "cd:kl" vs interp "cd::kl") — same class as #768, but the set functions
were missing from the hir_lower multi-char-separator guard. Fix follows.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 22:23:00 -05:00
Stephen Dennis
1248c201bd tests: add JIT differential fuzzer (testcases/tools/jit_diff)
A developer tool that finds correctness divergences between the softcode JIT
(tier2/rv64 wrappers + HIR lowering) and the interpreter (ast_eval), by
generating random typed, nested softcode and evaluating each expression both
ways (JIT via an @if condition, interpreter via an eval-bracket), comparing
by SHA1.

It catches compositional bugs the per-function audit misses — it found #772
(ljust/rjust/center don't truncate when width < content), which only surfaces
through nested width args.

Soundness: the generator never embeds a function inside literal text, because a
bracket-less mid-string call (foo add(2,3) bar) is correctly literal when bare
but evaluated inside [...], which would be a false positive (the cause of the
closed #773). Colored leaves are a single ansi() call wrapping the list.
Results are split into LOGIC (stripansi'd hashes differ — real) and COLOR
(raw differ, stripped match — internal color-encoding only, non-fatal).

Validated: a 120-expr run reports only ljust/rjust/center (the open #772) as
LOGIC, with color-encoding cases correctly tagged COLOR and no mid-string
false positives.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
2026-06-07 21:12:46 -05:00