Root cause confirmed via debugger: 2.13's mux_exec() backslash handler
(eval.cpp line 2439) has no EV_EVAL guard — it runs unconditionally on
every pass. When brace-group arguments go through FN_NOEVAL functions
like switch(), the text passes through mux_exec() twice: once for noeval
arg collection (stripping one backslash layer) and once for evaluation.
This two-pass behavior turns \\% into \% then into literal %.
The 2.14 AST scanner tokenizes once greedily — \\ becomes one ESC token
and % becomes one SUBST token, evaluated independently. This produces
\<space> instead of % for the bboard case.
Fix: add noevalPass() and evalNoevalArg() to the parser prototype.
For brace-group args to FN_NOEVAL functions, strip one backslash layer
via noevalPass(), re-tokenize, then evaluate. This replicates 2.13's
unconditional backslash consumption.
Changes:
- docs/parser-percent-matrix.md: complete semantic matrix of all %
forms across 2.13, 2.14, and PennMUSH with confirmed root cause
- parser/eval.cpp: full % substitution coverage (pronouns, color stubs,
caller, objid, moniker, hash forms, etc.), noevalPass/evalNoevalArg
for two-pass brace-group handling in if/switch/case
- parser/mux_parse.h: tokenizer support for hash forms (##/#@/#$),
Penn %$ stack vars, %w attrs, %iL, angle-bracket helper
- parser/test_eval.sh: 123 tests (was 86), cross-profile escape+percent
tests including the confirmed bboard divergence case
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
- hir_opt.cpp: Normalize commutative binary ops (ADD, MUL, etc.) in CSE to improve redundancy detection.
- parser/eval.cpp: Add IF and EVAL support to study tool for better Issue 2 verification.
- hir_opt.cpp: Replace O(N^2) CSE scan with unordered_map hash table.
- dbt_compile.cpp: Add dbt_compile_cleanup() to release persistent JIT state.
- dbt_compile.cpp: Expand JIT guest memory to 512KB and output scratch ring to 32 slots (256KB).
- engine_com.cpp: Call dbt_compile_cleanup() during server shutdown.
- parser/eval.cpp: Implement EV_FCHECK stripping and mandatory eval logic to match engine.
Evaluator now respects EV_EVAL, EV_FCHECK, EV_FMAND, EV_NOFCHECK,
and EV_STRIP_CURLY flags matching mux_exec behavior:
- %-substitutions only resolved when EV_EVAL set
- Function calls only dispatched when EV_FCHECK set
- [...] brackets force EV_FCHECK | EV_FMAND on contents
- {...} braces pass through raw when EV_STRIP_CURLY not set
- {...} with EV_STRIP_CURLY strips braces, disables EV_FCHECK
82 tests passing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Extract duplicated tokenizer, parser, and AST code from three study
tools into a shared header. Each tool is now a thin main() wrapper.
Added ast_raw_text() utility for source reconstruction. No behavioral
changes — 78 tests still pass.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Rework control flow functions (if, switch, case, cand, cor, iter, lit,
@@) to use deferred evaluation: handlers receive unevaluated AST
subtrees and call eval() selectively. This eliminates the need for
replace_tokens() and ##/#@/#$ text substitution.
Key proof: iter(10 20 30,[add(%i0,1)]) correctly produces "11 21 31"
by pushing %i0 onto the iterator stack and evaluating the body subtree
per item. Nested iterators work via %i0/%i1 stack depth.
Add PARSER_REPLACE.md design document for mux_exec replacement
architecture. 78 tests passing.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Stage 3 of parser study: tree-walking evaluator that demonstrates
pure-expression MUX softcode CAN be evaluated from an AST. Implements
arithmetic, string, list, comparison, boolean, register, and control
flow functions. Includes 64-test harness.
Key finding: FN_NOEVAL functions (iter, switch, if) need deferred
argument evaluation — the evaluator must pass unevaluated AST subtrees
rather than pre-evaluated strings. Solvable but requires a different
dispatch mechanism than simple string-in/string-out handlers.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>