Fix issue #82: share match fail chain behind a thunk (exponential compile) #89

Merged
ober merged 17 commits from fix/freebsd-78-82 into master 2026-09-21 15:02:35 -04:00
Owner

Fix issue #82: exponential match compilation (freeze building ober/jerboa-code)

Root cause

Both match macro compilers — src/jerboa/core.ss (compile-match-clauses)
and lib/std/match2.ss (compile-clauses, the prelude's match) — embedded
the compiled rest-of-clauses chain as code at every failure point of a
clause's pattern (2+ per clause for list/vector/tagged patterns). Each clause
duplicated the entire remaining chain, so expansion size doubled per clause:

  • Compiling the minimized tui trigger froze at ~13+ real bodies:
    mr82-c10 1.32s, mr82-c12 7.37s, mr82-c13 18.8s; c16/c20 infeasible.
  • Compiled .so size grew ~1.55x per clause (0.68MB @ k=6 → 3.6MB @ k=10 →
    13.5MB @ k=13) on the biggus FreeBSD harness.
  • The vendored Chez compiler-trace instrumentation (now reverted) showed the
    macro/codegen work markers growing 3.3k → 18k → 123k for the same steps.

Fix

Compile the rest chain once per clause position and share it behind a
zero-argument fail thunk (a letrec-bound match-fail gensym); every failure
point emits a tiny call to that thunk instead of a copy of the chain. The
success body stays a plain spliced expression so it remains inside the
pattern's variable bindings (a first attempt that thunked the ok side too
lifted bodies out of scope — "variable v is not bound" — and was discarded).
The fail chain carries no pattern variables, so calling it from outside the
pattern's bindings is sound.

Also reverts the temporary issue-82 instrumentation from the vendored Chez
sources (vendor/ChezScheme/s/{syntax,compile,cpnanopass}.ss are back to
master state — verified git diff origin/master -- vendor/ is empty).

Verification

Toolchain (mac arm64 + FreeBSD 15.1 biggus):

  • mr82-c10 1.32s→0.10s, mr82-c12 7.37s→0.09s, mr82-c13 18.8s→0.10s;
    mr82-c16/mr82-c20 now compile+run in ~0.1s
  • test-match-syntax 68/68, test-match2 62/62, test-match2-persistent 30/30
  • Full make test: 226 suites, 0 failures
  • make binary (macOS gate) builds; note jerboa-bin --version fails with
    "incompatible record type" on this Mac also on pristine master
    (verified via git stash rebuild) — pre-existing, unrelated to this change
  • New tests/test-issue82-regression.ss (22-clause wide chain + dispatch
    checks) passes and is gated in FreeBSD CI

Real consumer ober/jerboa-code @ 7cb9f19 (biggus, rebuilt toolchain 0.12.15):

  • gmake build exits 0; lib/jcode/ui/tui.so is now 377KB
  • test/run.ss: 1623 passed, 0 failed
  • ssrf-guard 4/4, websearch-worker 6/6, tui-native-loader PASS
  • test/security-regression.sh fails only on a cross-device hardlink
    (/tmp vs repo dataset on this box) — environmental, unrelated

VERSION 0.12.14 → 0.12.15.

Process disclosure: during diagnosis, vendored Chez sources were temporarily
instrumented (committed on this branch across earlier commits and reverted
here), and one interim .ss scratch edit was made with python3 before the
balanced-edit tooling was enforced; no such edits remain in this diff.

# Fix issue #82: exponential match compilation (freeze building ober/jerboa-code) ## Root cause Both match macro compilers — `src/jerboa/core.ss` (`compile-match-clauses`) and `lib/std/match2.ss` (`compile-clauses`, the prelude's `match`) — embedded the compiled rest-of-clauses chain **as code at every failure point** of a clause's pattern (2+ per clause for list/vector/tagged patterns). Each clause duplicated the entire remaining chain, so expansion size doubled per clause: - Compiling the minimized tui trigger froze at ~13+ real bodies: `mr82-c10` 1.32s, `mr82-c12` 7.37s, `mr82-c13` 18.8s; c16/c20 infeasible. - Compiled `.so` size grew ~1.55x per clause (0.68MB @ k=6 → 3.6MB @ k=10 → 13.5MB @ k=13) on the biggus FreeBSD harness. - The vendored Chez compiler-trace instrumentation (now reverted) showed the macro/codegen work markers growing 3.3k → 18k → 123k for the same steps. ## Fix Compile the rest chain once per clause position and share it behind a zero-argument fail thunk (a `letrec`-bound `match-fail` gensym); every failure point emits a tiny call to that thunk instead of a copy of the chain. The success body stays a plain spliced expression so it remains inside the pattern's variable bindings (a first attempt that thunked the ok side too lifted bodies out of scope — "variable v is not bound" — and was discarded). The fail chain carries no pattern variables, so calling it from outside the pattern's bindings is sound. Also reverts the temporary issue-82 instrumentation from the vendored Chez sources (`vendor/ChezScheme/s/{syntax,compile,cpnanopass}.ss` are back to master state — verified `git diff origin/master -- vendor/` is empty). ## Verification Toolchain (mac arm64 + FreeBSD 15.1 biggus): - `mr82-c10` 1.32s→0.10s, `mr82-c12` 7.37s→0.09s, `mr82-c13` 18.8s→0.10s; `mr82-c16`/`mr82-c20` now compile+run in ~0.1s - `test-match-syntax` 68/68, `test-match2` 62/62, `test-match2-persistent` 30/30 - Full `make test`: 226 suites, 0 failures - `make binary` (macOS gate) builds; note `jerboa-bin --version` fails with "incompatible record type" on this Mac **also on pristine master** (verified via `git stash` rebuild) — pre-existing, unrelated to this change - New `tests/test-issue82-regression.ss` (22-clause wide chain + dispatch checks) passes and is gated in FreeBSD CI Real consumer `ober/jerboa-code` @ 7cb9f19 (biggus, rebuilt toolchain 0.12.15): - `gmake build` exits 0; `lib/jcode/ui/tui.so` is now 377KB - `test/run.ss`: **1623 passed, 0 failed** - ssrf-guard 4/4, websearch-worker 6/6, tui-native-loader PASS - `test/security-regression.sh` fails only on a cross-device hardlink (`/tmp` vs repo dataset on this box) — environmental, unrelated VERSION 0.12.14 → 0.12.15. Process disclosure: during diagnosis, vendored Chez sources were temporarily instrumented (committed on this branch across earlier commits and reverted here), and one interim `.ss` scratch edit was made with python3 before the balanced-edit tooling was enforced; no such edits remain in this diff.
ober added 15 commits 2026-09-20 21:27:31 -04:00
- vendored cpnanopass: $jct-enabled?/$jct-full?/$jct! top-level helpers
  (env-gated, memoized, stderr-flushed) + np-begin/np-end in $xpass
  under JERBOA_COMPILER_TRACE=full
- vendored compile sources: read/expand/wpo/cp markers around
  compile-file-help phases and finish-compile pass pipeline (cpvalid, cp0,
  cpletrec, cpcheck, cpcommonize, np-compile)
- jerbuild: generated cross-wpo helpers self-gate on JERBOA_COMPILER_TRACE
  in the child: import-notify + compile-library-handler wrap (LIB-BEGIN/END)
  and PROGRAM/WHOLE phase markers
- tools: two-phase (validate-then-apply) transformer that made the vendored
  edits plus a byte-exact whitespace probe used to build anchors; Makefile
  targets jct-apply/jct-probe

Vendored-Chez edits were applied through the transformer because the balanced
editors false-positive on pristine Chez dialect; transformer is idempotent-
guarded (asserts $jct-enabled? absent before applying).
bootquick fails on the previous layout: PB compiles the kernel patch
sources through module->hash, which rejects new top-level bindings
("undeclared variable assignment to $jct-enabled?" then build-one fails).
Kernel convention is per-file internal definitions (cross-file sharing
goes through include files); each patched file now carries identical
internal copies of the three $jct helpers.

- cpnanopass: helper block moved inside the big let (before define-once)
- compile sources: helper block inserted after the language includes
- tools/jct-move: one-shot repair that performed the move
- vendored syntax sources: $jct markers on rt-revisit enter/done
  (invoke-loaded-library), lc-collect per collection, lc-revisit
  enter/done and lc-retry (library-collector retry loop)
- tools/jct-syn: guarded one-shot transformer (single-line anchors)
- tools/jct-move: repair pass that relocated the cpnanopass helpers
  inside each file's let after bootquick rejected top-level defines
- tools/jct-probe: extended with string-literal probes
Add chi-external parse-loop marker ('libform i, counter fic) and chi-frobs
maplr marker ('cfr i, counter cfc) to vendor/ChezScheme/s/syntax.ss via
one-shot transformer tools/jct-libform (make jct-libform), plus exact-indent
probe tools/jct-probe2 (make jct-probe2). Purpose: identify which tui
library body form freezes expansion during the FreeBSD ta6fb child compile.
chi-frobs 'cfr detail becomes "idx:name" (defined identifier for defines,
head symbol otherwise) via $jct-frob-name helper, so the freeze point
identifies the source form directly. Applied by make jct-cfrname
(tools/jct-cfrname).
The cfrname marker sat inside the let VALUE position, referencing the
let-bound x before it was in scope: bootstrap warned "undeclared variable
reference to x" and bootquick failed in module->hash. Use (car ls).
$jct-src-line helper extracts the first annotation (depth<=6) line via
current-locate-source-object-source; chi-body emits 'body <line> so the
freezing internal body in tui expansion maps to a source line directly.
Bisection isolated the tui.sls freeze to: form 105 (apply-agent-event-body!,
15-clause match) clause 13 + form 183 (apply-ext-turn-error!) + exports.
Compile time scales ~2.3-2.6x per accumulated match clause (exponential):
FreeBSD n=10 3.6s / n=12 34s / n=13 105s / n=14 >240s; macOS same curve.
Not FreeBSD-specific. Expander marker stats (instrumented build): one body
re-chi'd 28,666x, and-expansions 6,152x, cond 2,059x; np-compile 31s at n=14.
Both match compilers (src/jerboa/core.ss compile-match-clauses and
lib/std/match2.ss compile-clauses) embedded the compiled rest-of-clauses
chain as code at every failure point of a clause pattern (2+ per clause
for list/vector/tagged patterns). Each clause therefore duplicated the
entire remaining chain, doubling expansion size per clause: compiling
the minimized tui trigger froze at ~13+ clauses (mr82-c13 took 18.8s,
mr82-c16/c20 were infeasible), and .so size grew ~1.55x per clause
(0.68MB at k=6 -> 3.6MB at k=10 -> 13.5MB at k=13).

Fix: compile the rest chain once per clause position and share it behind
a zero-argument fail thunk (letrec-bound match-fail gensym); all failure
points now emit a tiny call to that thunk. The success body stays a
plain spliced expression so it remains inside the pattern variable
bindings. The fail chain carries no pattern variables, so calling it
from outside those bindings is sound.

Results (mac arm64, this branch):
- mr82-c10 1.32s -> 0.10s; c12 7.37s -> 0.09s; c13 18.8s -> 0.10s
- mr82-c16/c20 now compile+run in ~0.1s
- test-match-syntax 68/68, test-match2 62/62,
  test-match2-persistent 30/30, full make test suite green

Also adds tests/test-issue82-regression.ss (22-clause wide match chain;
blows up compile time if the duplication returns) and larger stress
fixtures mr82-c16/mr82-c20 under tools/issue82/. Reverts the temporary
issue-82 instrumentation from the vendored Chez sources (syntax.ss,
compile.ss, cpnanopass.ss back to master state).

VERSION 0.12.14 -> 0.12.15.
issue #82: gate the regression test in FreeBSD CI
Some checks failed
required-ci / gerbil-compat (pull_request) Successful in 3m58s
version-policy / required (pull_request) Failing after 4m58s
required-ci / required (pull_request) Has been cancelled
dtrace / freebsd-usdt (pull_request) Has been cancelled
freebsd-required / required (pull_request) Has been cancelled
cd08e72b35
Run tests/test-issue82-regression.ss alongside the other platform tests
so any reintroduction of exponential match expansion fails CI fast.
merge master into fix/freebsd-78-82; VERSION to 0.12.18
Some checks failed
required-ci / gerbil-compat (pull_request) Successful in 3m57s
version-policy / required (pull_request) Successful in 4m49s
freebsd-required / required (pull_request) Successful in 12m13s
dtrace / freebsd-usdt (pull_request) Successful in 13m30s
required-ci / required (pull_request) Failing after 13m41s
a6106b63f2
test: use a unique temp path in the capability file test
All checks were successful
required-ci / gerbil-compat (pull_request) Successful in 3m57s
version-policy / required (pull_request) Successful in 5m11s
freebsd-required / required (pull_request) Successful in 12m6s
dtrace / freebsd-usdt (pull_request) Successful in 13m32s
required-ci / required (pull_request) Successful in 14m22s
8dfdb206ed
test-capability.ss wrote a fixed /tmp/cap-test.txt with no cleanup. A
stale file left by an earlier CI run (different user or restrictive
mode) made open-output-file fail and killed the whole suite, turning
required-ci red (same class as the stale-/tmp flakes fixed for the
parallel-compilation test in 3b571806). Generate a unique name under
the real temporary directory, attenuate the capability to that
directory, and delete the file afterwards.
ober scheduled this pull request to auto merge when all checks succeed 2026-09-21 14:44:15 -04:00
ober scheduled this pull request to auto merge when all checks succeed 2026-09-21 15:02:19 -04:00
ober merged commit 1cda118d62 into master 2026-09-21 15:02:35 -04:00
ober referenced this pull request from a commit 2026-09-21 15:02:37 -04:00
Sign in to join this conversation.
No reviewers
No labels
No milestone
No project
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference
ober/jerboa!89
No description provided.