User review revisions: FN001 is scheduled after P001 before C001/R001/ FX001; C001/R001 require joint design review of ordinary record instances. Documented row-specialization key/code growth and dictionary/accessor passing as an option needing an ADR, not a chosen fallback. Lowered IR’s default to test is after specialization, above layouts. D001 records text, numeric and collection foundations; M001 remains required for shared modules. Rust pragmatics are explicit; FX001 compares ZIO-like computations and Koka-like effect rows/handlers. All five review points addressed in the direction document and backlog/handoff. Current P001 worktree untouched.
Fresh npm run verify on main exited 0: 162 tests, zero failures/skips,
zero build warnings/errors, gates, strict rebuild proof and eight isolated
regression proofs. Raw log: .build/language-direction-review-verify.log.
Documentation only; these milestones remain unimplemented.
User requested a local commit of the current language/backend/effect discussion using the planning guidance. Read brainstorming/writing-plans; recorded docs/plans/2026-10-08-language-direction.md as direction and hypotheses, not an implementation spec. Goals, early-release deferrals, row/service scenarios, effect-design questions, inspectable IR and future targets are separated from assistant recommendations and open decisions. Backlog follow-ups FX001 (effect design), L001 (lowered IR direction) and J001 (JavaScript evaluation direction); P001’s approved scope/order intact. README, bootstrap index, next-session, findings and provenance linked. Self-review checked scope, status, references and distinction between proposed acceptance scenarios and implemented behavior. Documentation only.
Fresh npm run verify on main exited 0: 162 tests, zero failures/skips,
zero build warnings/errors, formatting/style/layer gates, strict rebuild
proof and eight isolated regression proofs passed. Raw evidence:
.build/language-direction-verify.log. Existing .vscode/settings.json edit
excluded from this change. No feature implemented and no push authorized.
2026-10-07. The language is renamed from Sprig to Bumpus (A003 Task 3a). Dated
evidence below keeps literal historical identifiers (old module names such as
Sprig.Check, sprigCmpN-style Go helpers, old command lines, .sprig paths
and log names) verbatim because they name artifacts that existed; the prose
language name changes.
2026-10-07. The completed documentation/style audit is merged into main through 1f760af. Fresh review found no blockers; merged verification passed. Baseline 9624bec is preserved in history. Audit branch/worktree removed after retaining their evidence. Task 1 commit: 4410609. No publishing or push. The audit plan is docs/plans/2026-10-07-bootstrap-audit.md.
The following evidence was recorded before skill installation/execution; new worktree evidence follows below. Earlier raw logs are in the original checkout, not assumed to exist in the isolated worktree.
node scripts/verify.mjs (same implementation as npm run verify) completed
with zero source warnings/errors, strict/pedantic workspace builds, 19 passing
Node tests, no skipped tests, and a successful isolated regression proof.
Recent raw output is in ignored .build/verify.log; this summary survives it.
42.A clean-output npm run verify passed, followed by a final successful run
after the formatter-version, cache-portability, unsafe-import, and malformed
negative-literal fixes. Final log: .build/verification-final.log (exit 0).
The MileAhead patch passed git apply --check against the read-only reference.
Online installation and cross-platform execution remain unverified.
PureScript compiler: src/{Domain,Features,Format,Program}/*/.purs and src/Domain/IR/Internal.purs. Boundary: src/Runtime/Node.purs and Node.js. Style tooling: tools/style/src/Style/Check.purs (separate package). Tests: test/*.test.mjs and the regression witness/script. Example, three negative files, and bootstrap/answer.go (example snapshot). Documentation: README, AGENTS/CLAUDE, plan, findings, architecture, language, provenance, ADRs, engineering guidance, bootstrap roadmap, and local backlog.
Local integration is complete; proceed to a separate bounded ADT design/plan. Closed ADTs/exhaustive matching require a separate later design and plan. F001 (fresh compiler-tool setup), F002 (unapplied external patch), E001/E002 and E003 remain explicit.
Historical installation steering: install skills, migrate, then plan the run. 15 skills installed project-locally via bunx at the revision in skills-lock.json; list command confirmed Codex/project scope. The unfinished audit is migrated to docs/plans/2026-10-07-bootstrap-audit.md with a durable historical ledger. At migration, no audit task or feature had run and Git had no HEAD. User subsequently approved inline execution/checkpoint/worktree. W001 is resolved; prior compiler evidence above is preserved.
Fresh migration validation: npm run verify on 2026-10-07 exited 0.
Strict/pedantic build reported zero source/library warnings and errors;
formatting and architecture/CST gates passed; 19 tests passed, zero failures
and skips; isolated restored branch-check defect failed as required while
the fixed compiler passed. Output was read from this session’s command output,
not saved to a new raw log. This validates the preserved compiler after the
installation/migration; it does not complete the pending documentation/style
audit or represent a clean-output rebuild in this session.
User approved checkpoint, isolated worktree, inline execution and final fresh review. Checkpoint 9624bec is on main; audit/bootstrap-docs runs in .worktrees/bootstrap-audit. Clean-output baseline compiled 269 modules with zero source/library warnings/errors, then passed 19 tests and the isolated regression proof. Raw evidence: worktree .build/audit-baseline.log.
| Review focus | Source and evidence | Result/limit |
|---|---|---|
| Setup claims | README; scripts/build.mjs/cache.mjs; baseline log | Cached dependency build verified from clean compiler output. Fresh PATH-tool install/cross-platform setup remains F001. |
| IR boundary | Check/IR.Internal/Go; structure.mjs; layer test | Only checking/lowering imports internal IR in source. Checker validates encountered local/call indices; forged entry/table structure is trusted and documented. |
| Automated vs manual style | Style.Check; structure.mjs; style/structure tests | Essential CST/import/format/line gates pass. Manual fallback/callback/branch issues corrected; ten-arm (now twelve; see BACKLOG E003) tag mapping target exception recorded as E003. Full lint automation stays E001. |
| Snapshot/seed | bootstrap/answer.go; compiler snapshot assertion; docs/bootstrap.md | Example bytes match canonical emission; Stage 1 compiler seed remains proposed. Stage 0 source and locks preserved in baseline. |
| Handoff/external edit | patch; planning-skills; backlog; migration ledger | Skills installed and real BASE created. MileAhead patch remains unapplied (F002); current execution status is synchronized in Task 2. |
Style-only refactors preserve the existing public source-language contract. The passing pre-change suite is the baseline; post-change verification and existing isolated mutation establish continued behavior. No synthetic RED history is asserted for documentation or helper extraction. ADRs 001/002 are unchanged: no semantic/representation decision changed in this audit.
Task 1 post-change npm run verify exited 0: strict/pedantic build with zero
warnings/errors, all gates, 19/19 tests and isolated branch mutation proof
passed. Raw evidence: .build/audit-task-1-green.log. The earlier accidental
IR alias edit failed the same build; its copied defect also failed compile
in isolation. docs/findings.md retains the complete failed-attempt account.
Handoff files now identify the real baseline, audit branch, completed tasks and final fresh review. Historical no-HEAD/no-commit statements are labeled as historical. Review is complete; final completion verification is recorded below. No ADT source or plan was added.
Task 2 pre-review npm run verify exited 0 on 2026-10-07: zero strict-build
warnings/errors, 19/19 tests with no skips, style/layer gates and regression
proof passed. Raw log: .build/audit-task-2-pre-review.log in the audit worktree.
Fresh reviewer inspected 9624bec..56cbde1 read-only: no Critical/Important findings. Two pre-existing test-title inaccuracies are deferred as E004; no assertions were changed. Full review and exhaustive scope rulings are saved in docs/plans/bootstrap-audit-review.md and bootstrap-audit-ledger.md. The server restart interrupted delivery; the same reviewer resumed, avoiding a second review or repeated implementation.
Two separate actual CLI emit invocations produced byte-identical output, also equal to bootstrap/answer.go (cmp checks succeeded). The preserved MileAhead patch again passed git apply –check without being applied. Stage 0, locks, tests, example bytes and pure boundaries remain preserved. ADR 001/002 remain unchanged. At audit closure no language feature, merge, push or publication had occurred.
Raw failure evidence retained outside executor scratch: .build/audit-task-1-failed-alias.log, .build/alias-regression/failure.log, and .build/audit-task-1-fixed.log (the failed reproduction-helper attempt). Successful raw evidence: .build/audit-baseline.log, audit-task-1-green.log, audit-task-1-completion.log, and audit-task-2-pre-review.log in .build.
Final closure npm run verify exited 0 on 2026-10-07: strict/pedantic build
zero warnings/errors; 19/19 tests, zero skips; format/CST/layer gates and
isolated branch mutation proof passed. Raw final log: .build/audit-final.log.
Tasks 1/2 and original bootstrap tasks 7/8 are complete; no blocking review
finding remains. E004 minor titles are intentionally deferred. Integration into
main was still pending at that checkpoint; it is now
complete as recorded below.
The installed task-done helper also verified Task 2 after commit f27b499: 19/19 tests, zero skips, strict build/gates/regression proof passed; commit range 4410609..f27b499 is recorded in the durable ledger. Raw completion log: .build/audit-task-2-completion.log. Executor scratch was copied in full to .build/audit-executor-evidence before cleanup; durable ledger/review remain committed in docs/plans. Only local branch integration is pending.
User explicitly approved merge. Main fast-forwarded 9624bec..1f760af;
npm run verify on the merged checkout exited 0 on 2026-10-07: zero strict
warnings/errors, 19/19 tests, zero skips, formatting/CST/import gates and
isolated regression proof passed. Raw log: .build/merge-verification.log.
Audit raw logs, emitted files, isolated rename-defect copy and executor artifacts were preserved under .build/merged-audit-evidence before removing the merged audit worktree and branch. Historical worktree .build paths above now resolve under that archive. Original checkout logs were not overwritten. Durable ledger/review remain in docs/plans and all audit commits stay in main. The pre-existing .vscode/settings.json spell-check additions are preserved uncommitted. No push or publication occurred.
2026-10-07: brainstorming for closed ADTs and exhaustive matching. User chose recursive types, nested patterns with Maranget usefulness, type/match brace syntax, tagged value-struct representation and sequential first-match lowering. User review added CtorId identity, nil-guarded projections, inhabitedness, canonical witnesses and two-pass type resolution. Approved spec: docs/plans/2026-10-07-closed-adts-design.md. Documentation only; no compiler source changed, so no verification run is claimed for this step.
2026-10-07: spec approved and committed (1d71f2f). writing-plans produced docs/plans/2026-10-07-closed-adts-plan.md: five tasks (syntax; types and construction; patterns and match; coverage; docs and review). Plan-time spec amendments: E_DUPLICATE at the first duplicate (Stage 0 rule), depth-named match parameters, and the Float row moving from E_SYNTAX to E_UNBOUND. Documentation only; no compiler source changed.
2026-10-07: closed-ADT Task 1 (syntax) done. Added |{} and => tokens,
reserved type/match/_, TypeRef, Parse.Declaration and Program dispatch;
Resolve maps NamedRef to E_UNBOUND transitionally. New test/adt-syntax.test.mjs
(7 tests) failed against pre-change source, then passed. npm run verify
exit 0 (26 tests, regression proof, answer.go unchanged).
2026-10-07: closed-ADT Task 2 (types and construction) done. Ty moved to
Resolved
with TData; two-pass type table, shared global table, Construct through Check
and Go (Go.Data). test/adt-types.test.mjs (6 tests) failed before the change
(unbound-type errors), then passed. npm run verify exit 0 (32 tests,
regression
proof, answer.go unchanged).
2026-10-07: closed-ADT Task 3 (patterns and match, no coverage) done.
Parse.Literal
and Parse.Pattern, Resolve.Pattern with pre-order LocalIds, Check.Match,
Go.Match
(depth-named IIFE, nil-guarded projections); examples/shapes.sprig with snapshot
bootstrap/shapes.go. test/adt-match (12) and test/adt-properties (1) failed
before
the change (match was E_SYNTAX), then passed. Regression proof is a table:
branch
and new nil-guard both fail on their mutants. npm run verify exit 0.
Fix round 1: pattern-constructor arity moved from Check.Match into
Resolve.Pattern (E_ARITY in pattern pre-order, before body and later-function
errors); two new rejection rows failed before (E_UNBOUND) and pass after.
npm run verify exit 0.
2026-10-07: closed-ADT Task 4 (coverage) done. Check.Usefulness (inhabitation
fixed point, U(P,q), algorithm I) and Check.Coverage (E_REDUNDANT,
E_NON_EXHAUSTIVE with canonical witness) run after checking; test/adt-coverage
(6) and test/coverage (300-case oracle) failed before (non-exhaustive programs
compiled), then passed; regression row exhaustive fails on its mutant. No
existing test program changed. npm run verify exit 0 (52 tests, three
regression proofs).
2026-10-07: closed-ADT Task 5 (five layers) done. Modules moved to
Domain/Features/Format/Runtime/Program with git mv; Token, isUpper ->
Format.Lex, codeName -> Format.Diagnostic; scripts/structure.mjs per-module
table replaced by layer rules, four new structure cases failed before the gate.
bootstrap/answer.go and shapes.go cmp identical, no stale Sprig./Shell. names.
npm run verify exit 0 (52 tests, three regression proofs).
2026-10-07: closed-ADT Task 6 (structured diagnostics) done. Domain.Problem
holds Problem/TypeName/Witness data; Features build no text; Format.Diagnostic
code/message/wire render it, Program.Main uses wire. test/diagnostics
characterization (39 families) passed at HEAD and unchanged after; its
message/wire assertions failed before (no Domain.Problem); the R5 invalid-TypeId
case compiled at HEAD returned Right (vacuously complete) and is now E_INTERNAL.
Coverage table lookups split into Features.Check.Signature. bootstrap/answer.go
and shapes.go cmp identical. npm run verify exit 0 (55 tests, three regression
proofs; exhaustive mutant now const (const (Right Nothing))).
2026-10-07: closed-ADT Task 7 (capability ports) done. Domain.Host ports over
abstract m; Format.Arguments (argv, usage text) and Format.Wire
(plain/source/tool wire shapes); Program.Command over any Host; Runtime.Node is
port implementations plus argv/stdout/stderr/exit and JSON only.
test/program.test (9 fake-host cases) failed before (no Program.Command), then
passed; two new structure rows (Program.Command importing Effect/Runtime) failed
before the gate. Old/new CLI stdout, stderr and status byte-identical on 15
probes (usage, E_IO read/write, E_TYPE, E_TOOL with PATH=’’ and bad -o,
emit/build/run); no temp dir leaked. bootstrap/answer.go and shapes.go cmp
identical. Clean rm -rf output && npm run verify exit 0 (64 tests, three
regression proofs).
2026-10-07: closed-ADT Task 8 (documentation and closure) done, review step
excluded by controller ruling R6. Wrote ADRs 003 and 004; rewrote
docs/language.md and docs/architecture.md; updated engineering, provenance
(Maranget JFP 2007; MileAhead layers, conceptual only), README, BACKLOG (A001
Done; A002-A005, F005 added; E003 and F004 current), plan index and
next-session. Clean rm -rf output && npm run verify (.build/a001-final.log):
exit 0, 64 tests, 0 skips, regression proofs branch, nil-guard and exhaustive
all passed. Two CLI emits of examples/shapes.sprig byte-equal each other and
bootstrap/shapes.go; examples/answer.sprig emit equals bootstrap/answer.go
(cmp). $TMPDIR still holds the same four stale sprig-regression-* directories
after the run (F005). Final whole-branch review is run by the controller.
2026-10-07: A001 final whole-branch review findings fixed (record
docs/plans/closed-adts-review.md). Function/constructor clashes report at the
earliest declaration by source offset (new adt-types case failed before the fix
at offset 26, passed after; .build/a001-review-minor3-red.log, -green.log).
specialize and checkCtor return E_INTERNAL on field-count mismatch; the new
diagnostics test fails with either change reverted in an isolated copy
(.build/a001-review-isolated-red.log). Coverage oracle now requires witnesses to
cover only unmatched values and checks brute-forced inhabitedness over 120
generated type systems (an inhabitedness mutant fails it:
.build/a001-review-inhabited-mutation.log); 8 generated flat 3-5-arm matches run
in Go (an arm-reordering mutant fails them:
.build/a001-review-flat-mutation.log). verify.mjs runs every test/*.test.mjs (an
unlisted failing dummy made verify exit 1:
.build/a001-review-unlisted-probe.log). Clean rm -rf output && npm run verify
(.build/a001-review-fix.log): exit 0, 0 warnings, 13 test files, 68 tests, 0
skips, regression proofs branch, nil-guard and exhaustive passed. CLI emits of
examples/answer.sprig and examples/shapes.sprig equal bootstrap/answer.go and
bootstrap/shapes.go (cmp). $TMPDIR still holds four stale sprig-regression-*
directories (F005).
Reproduced in an isolated copy: with a shadowed name added to Domain.Host, the
first node scripts/build.mjs failed with ShadowedName and the second exited
rm -rf output && npm run verify (.build/f004-verify.log) and the following
incremental npm run verify (.build/f004-verify-incremental.log) both exit 0:
0 warnings, 68 tests, 0 skips, strict rebuild proof and regression proofs
branch, nil-guard and exhaustive passed. The main item of the milestone is
not yet chosen.User chose A003 with print, equality and ordering. Design direction approved with five clarifications (E_ENTRY keeps missing-main and parameters; round trip recompiles with original declarations and checks structural equality via the interpreter; ordering mutations must fail an oracle or explicit-order assertion, with separately built equal values; nil-field panic rule scoped to visited fields; Bool helper only when needed). Written spec: docs/plans/2026-10-07-adt-printing-design.md, approved by the user. Plan: docs/plans/2026-10-07-adt-printing-plan.md (4 tasks), awaiting review. Documentation only; no compiler source changed.
Operator/Compare through lexer, parser (non-chaining, looser than +),
resolver, checker, coverage and Go lowering; Bool ordering lowers through
sprigCmpBool, emitted only when used. New test/compare.test.mjs (6 tests:
runs, positions, rejections, left-to-right evaluation trace, helper
presence, answer.go unchanged); the old “lone > is E_LEX” test now pins a
lone !, and the branch regression needle gained context so it stays
unique. npm run verify exit 0; bootstrap/answer.go and shapes.go
unchanged.sprigCmpN helper per declared type (tag range check, constructor order,
fields left to right, nil check before a declared field), emitted in
TypeId order; declared comparisons lower to (sprigCmpN(L, R) op 0);
sprigCmpBool is also emitted for any Bool constructor field. New
test/value-oracle.mjs (reference order, printer, parser, alternate
expressions) and test/adt-order.test.mjs (7 tests, all red before the
change: hand cases, structural equality, 8192-element lists, uninhabited
type, malformed values, evaluation trace, 8-seed oracle agreement with
order laws and coverage assertions). Regression rows ctor-order and
first-field fail with their defects restored; their probes run Go under
.build/regression. bootstrap/shapes.go gains only sprigCmp0; answer.go
unchanged. The compiler’s ~3500-character stack limit was recorded under
E002. npm run verify exit 0.main): EntryResult is
gone and a missing entry reads Expected fn main(); main may return any
type. New Format.Go.Show emits one sprigShowN(out []byte, v sprigTyN)
per declared type in TypeId order (cases built from the constructor
table, fmt.Append for Int/Bool fields, nil check before declared
fields, unknown tag panics bumpus: malformed value); a declared main
prints string(sprigShowN(nil, sprigFnK())). The shared panic text moved
to Format.Go.Data. New test/adt-print.test.mjs (7 tests, all red before
the change: hand prints, Int/Bool unchanged, entry rules, 8192-element
list print, malformed panics, 8-seed print round trip, tree snapshot);
diagnostics and adt-types E_ENTRY rows updated per Global Constraints.
Regression row show-fields fails with its defect restored (printed
value lost fields: Cons(1)). New examples/tree.sprig and
bootstrap/tree.go; shapes.go gains only sprigShow0; answer.go
byte-identical. npm run verify exit 0.npm run build rewrote spago.lock (packages
bumpus, bumpus-style); bootstrap/answer.go, shapes.go and tree.go regenerated
by the CLI each equal the old bytes under
sed s/sprig/bumpus/g;s/Sprig/Bumpus/g (cmp, exit 0). New
docs/assets/bumpus.svg. npm run verify exit 0.tailRecM loop (Control.Monad.Rec.Class, tailrec
now a direct dependency; spago.lock gains it), Format.Parse.declarations
is a tailRecM loop, Format.Parse.Core reads tokens by index, and
Format.Stack makes accumulation linear. Two quadratic resolver costs the
20,000-declaration test exposed were fixed: globals are built once, and
function-name duplicates use a sort (Features.Resolve.Repeated). New
test/large-source.test.mjs (5 tests: megabyte padding, 20,000
declarations run in Go, E_LEX at offset 1,000,020, megabyte lex under 5 s,
duplicate among 20,000) all failed on the old code with heap exhaustion
(.build/task3b-red.log); now 0.09 s, 2.2 s, 0.26 s, 0.07 s, 0.08 s. The
structure allowlist admits exactly Control.Monad.Rec.Class (new structure
test red first). Coverage-oracle choose reads the LCG’s high 16 bits (a
new test showed 80 two-way draws alternating 0101…); seed-dependent
preconditions then failed (adt-order seed 22: one T0 constructor,
adt-print nested), so seeds were reselected, assertions unchanged.
Dropping the backward neighbour check in Repeated fails the diagnostics
and large-source duplicate tests in an isolated copy. bootstrap/*.go
unchanged. npm run verify exit 0, 17 files, 96 tests, 0 skips
(.build/task3b-verify.log).
2026-10-07: logo. The README shows assets/branding/bumpus-logo.png (1254 ×
1254 PNG with alpha, a bluetick coonhound with the stolen turkey above the
wordmark), chosen by the user from image-generator candidates; provenance and
final prompt in assets/branding/README.md. The Task 3a SVG icon was removed at
the user’s request; no earlier image versions are kept. Documentation and
assets only.uniqueTypes/uniqueCtors, new T001), findings and
next-session updated; each claim was checked against src/ and test/.
rm -rf output && npm run verify exit 0, 0 warnings, 17 files, 96 tests,
0 skips, strict rebuild proof, six regression proofs (branch, nil-guard,
exhaustive, ctor-order, first-field, show-fields)
(.build/a003-final-verify.log). CLI emits compared with cmp (exit 0):
examples/answer.bumpus to bootstrap/answer.go, shapes.bumpus to shapes.go,
tree.bumpus to tree.go, and tree.bumpus a second time to tree.go. A003 is
In review; Done follows the final whole-branch review.cmp of the CLI emits of examples/answer,
shapes and tree against bootstrap/*.go (exit 0) and 504 programs emitted
identically by b5dde4e and the fix. Tag-3 compare case fails under an
emitted .tag > count+1 mutant. adt-order runs one Go program per seed.
E002, language.md, ADR 005 and architecture.md corrected (I1, I2); G001
added; A003 Done. rm -rf output && npm run verify exit 0, 0 warnings,
17 files, 98 tests, 0 skips, six regression proofs
(.build/a003-review-fix-verify.log).
Follow-up: the final test/large-source.test.mjs run against a b5dde4e
build fails both new tests on their 5 s bounds (types 25.9 s, duplicates
21.8 s). Remaining per-reference name scans (resolveType, findGlobal)
measured at 1.7 s for 20,000 references and recorded in E002 for G001.User approved the merge. main fast-forwarded 74e5c2a..4b5ce2e. Clean
rm -rf output && npm run verify on main exited 0: zero warnings, 98 tests,
zero failures/skips, strict rebuild proof and six regression proofs
(branch, nil-guard, exhaustive, ctor-order, first-field, show-fields). Raw
log: .build/a003-merge-verify.log. Worktree .build logs and the SDD ledger
were archived under .build/merged-a003-evidence before the a003 worktree
and branch were removed. No push. Next: G001 (design first).
User chose an applicative-only parser (no Monad instance) and a depth limit with a structured diagnostic for deep nesting (over a larger Node stack or heap-based phases). Written spec: docs/plans/2026-10-07-applicative-parser-design.md, approved as written with clarifications (section 7). Documentation only; no compiler source changed. Plan: docs/plans/2026-10-07-applicative-parser-plan.md (5 tasks), awaiting user review.
npm run verify exit 0, 101 tests, 0 failures/skips
(.build/g001-task1-verify.log); emits of answer, shapes, tree cmp-identical
to bootstrap/*.go.spanned
is an empty span when nothing is consumed; Functor map is direct. New CST
rule: no do, >>= or =<< in Format.Parse and
Format.Parse.{Literal, Pattern, Expression, Declaration} (Grammar, Cursor
exempt). RED: 1,536 constructors, 1,536 fields and 1,793 arguments
RangeError (.build/g001-task2-red.log), style fixture failing
(.build/g001-task2-style-red.log), empty spanned failing in an isolated
copy (.build/g001-task2-spanned-red.log); GREEN 15 ms, 7 ms, 56 ms
(.build/g001-task2-green.log). fn main(): Int = (1 keeps E_SYNTAX
“Expected ‘)’” at 19-19. Parser lines 889 -> 685 (no module grew).
npm run verify exit 0, 112 tests, 0 failures/skips
(.build/g001-task2-verify.log); answer, shapes, tree emits cmp-identical
to bootstrap/*.go. Cold-CLI nesting depth moved (BACKLOG E002).nested,
infixed, rooted and nestingLimit in Format.Parse.Grammar, depth and
peak in the Cursor state; operators count the depth of the tree they
build, so a deep left operand counts in full; Grammar foldAt runs the
first item directly. Probe (scripts/depth-probe.mjs, limit disabled):
smallest overflow 304 (Cons(1, ), no anomalies after classifying V8’s
regex Stack overflow SyntaxError as an overflow; limit = largest power
of two ≤ 152. RED: test/depth.test.mjs 14 of 26 failing, no E_NESTING
(and go build killed on 128 nested match arms, BACKLOG E005)
(.build/g001-task3-red.log); diagnostics E_NESTING row failing with the
limit disabled; GREEN 26/26 (.build/g001-task3-green.log). npm run
verify exit 0, 138 tests, 0 failures/skips (.build/g001-task3-verify.log);
answer, shapes, tree emits cmp-identical to bootstrap/*.go.fresh,
failure, liftEither, runFresh); arms, arguments and pattern fields
via balanced traverse; Numbered dropped from Domain.Resolved.
Characterization (test/adt-match.test.mjs LocalIds row) recorded from and
passing on the old compiler, failing when arms are numbered before the
scrutinee in an isolated mutant. New: 10,000 binding arguments compile in
about 1 s (test/large-source.test.mjs; RED RangeError on the old resolver,
.build/g001-task4-red.log). Differential over 1,327 distinct sources
compiled by the suite: all identical Go or diagnostics except that row.
npm run verify exit 0, 140 tests, 0 failures/skips
(.build/g001-task4-verify.log); answer, shapes, tree emits cmp-identical
to bootstrap/*.go.tailRecM); specialize checks arity
once, then mapMaybe; parameter duplicates use Repeated. Corrected
thresholds: the coverage overflow was about 1,929 arms (not 3,000), the
pre-G001 resolver overflowed near 1,700 arms or arguments (not 5,000).
RED on the pre-fix compiler (.build/g001-task4b-red.log): 5,000 integer
arms and 3,000 constructors RangeError, 20,000 parameters 3.07 s;
per-site mutants (each foldM restored alone, quadratic uniqueParameter,
reversed Fresh apply for the strengthened 10,000-argument order test)
each fail. GREEN (.build/g001-task4b-green.log): 5,000 arms 1.3 s,
3,000 constructors 1.3 s, 20,000 parameters 0.17 s; still quadratic in
arms (10,000 arms 5.2 s, BACKLOG E002). npm run verify exit 0, 144
tests, 0 failures/skips (.build/g001-task4b-verify.log); exhaustive
regression row holds; answer, shapes, tree emits cmp-identical to
bootstrap/*.go.state-thread
(scripts/regression.mjs, probe in test/regression.mjs): Grammar’s apply
running the second parser from the original state makes
examples/answer.bumpus fail with parser state not threaded (E_SYNTAX
“Expected an identifier” at 1:1; .build/g001-t5-state-thread-mutant.log);
the healthy compiler emits bootstrap/answer.go. CST gate closed
(tools/style/src/Style/Parser.purs): productions also reject bind,
join, discard (plain, qualified, backticked), >=> and <=<, and
only Format.Parse (and Grammar) may import run/initialState; open,
as-only and hiding imports of Grammar or Cursor are rejected. RED: 2 of
8 style tests failing (.build/g001-t5-style-red.log); the positive
fixture fails with the importer exemption removed, in an isolated copy
(.build/g001-t5-style-positive-mutant.log); GREEN 8/8
(.build/g001-t5-style-green.log). Measured at the CLI: a 129-operand flat
sum compiles and 130 is E_NESTING; a 128-element printed list compiles
and 129 is E_NESTING. Docs: language.md (nesting rule, E_NESTING), ADR
005 and 006, architecture.md, engineering.md, findings, README,
next-session; BACKLOG G001 In review, E002 restated, E005 options left to
the user, new O001, H001, F006 (stale output of deleted modules,
recorded, not fixed). rm -rf output && npm run verify exit 0, 20 files,
147 tests, 0 failures/skips, seven regression proofs
(.build/g001-task5-verify.log); answer, shapes, tree emits cmp-identical
to bootstrap/*.go. Final whole-branch review pending.Stack in tailRecM;
I2: inhabitation is a worklist (new Features.Check.Inhabited, lookups in
Features.Check.Tables), and Array.modifyAtIndices is chunked after the
20,000-type test overflowed in it. RED on the pre-fix compiler: all four
test/coverage-scale.test.mjs cases RangeError (5,000-field exhaustive,
non-exhaustive witness and redundant arm; 10,000-type chain after 9.9 s)
(.build/g001-final-fix-red.log); GREEN 0.27-0.36 s, 0.08 s, 0.05 s,
0.64-0.69 s. Remaining cliffs measured and recorded in BACKLOG E002 (4),
(5). M1: the Bind gate also rejects ap, ifM, whenM, unlessM, liftM1,
bindFlipped, composeKleisli, composeKleisliFlipped (style fixture failed
first on ap); M2: test/depth.test.mjs imports nestingLimit; M4: ADR
006 gives the exact uncommitted probe steps; M5: O001 notes. M3: accepted
that src/Format/Parse/Literal.purs grew 51 -> 55 lines, all of it the
purs-tidy one-per-line Grammar import list (spec section 6 “no parser
module grows” is otherwise met). rm -rf output && npm run verify exit 0,
21 files, 151 tests, 0 failures/skips, seven regression proofs (the
exhaustive needle still matches once and its mutant fails)
(.build/g001-final-fix-verify.log); answer, shapes, tree emits
cmp-identical to bootstrap/*.go. G001 Done, pending the controller’s
scoped re-review.User approved the merge and kept the externally rewritten history. main
fast-forwarded to 44a735b. Clean rm -rf output && npm run verify on main
exited 0: zero warnings, 151 tests, zero failures/skips, strict rebuild
proof and seven regression proofs (branch, nil-guard, exhaustive,
ctor-order, first-field, show-fields, state-thread). Raw log:
.build/g001-merge-verify.log. Scoped re-review of the final fixes found all
findings addressed (116,000+ differential coverage cases identical to the
pre-fix build); remaining wording nits fixed here (engineering test count)
or parked (coverage-scale comment, Inhabited complexity formula). The
re-review’s out-of-scope allowlist gap is BACKLOG F007. Worktree evidence and
the SDD ledger are archived under .build/merged-g001-evidence; the g001
worktree and branch are removed.
bumpusFn{f}Match{k} (k in
per-function pre-order, scrutinee before arms), taking its captured
locals in LocalId order and then bumpusScrutinee; lifted functions
follow their function in number order (ADR 003 item 6). New modules
Format.Go.Lowered (counter threading), Format.Go.Expression (moved out of
Format.Go) and Format.Go.Capture; Format.Go.Compare’s comparison now
takes lowered operand code.bumpusFn0Match* functions; .build/e005-red-lift.log);
its capture test passed, as expected for closures, and its RED is the
capture mutant. The 128-deep timing test hit its 10 s spawn timeout
(.build/e005-red-timing.log). The full-limit match-arm and
compare-matches runs were not executed on the closure compiler (prior
record: killed at 128).capture row) and capturing nothing both make the probe
fail with captured local lost and Go’s undefined: bumpusLocal3 /
bumpusLocal1 (.build/e005-mutants.log).go build, Go 1.26.4): closures 0.31 s
at d = 16, 1.26 s / 596 MB at 20, 7.99 s / 2.7 GB at 22; named functions
0.13-0.15 s and about 67 MB at every d from 8 to 128.LocalIds in emitted Go follow source pre-order
pinned the emitted text of one closure-form body; its expected sequence
now covers bumpusFn0 and its four lifted matches (same binder numbering,
plus capture parameters and arguments).bumpusFn0Match0/Match1 functions).rm -rf output && npm run verify exit 0: zero warnings, 22 files, 155
tests, zero failures/skips, eight regression proofs including capture
(.build/e005-verify.log).free;
Format.Go.Capture union/armFree), so captures come from one bottom-up
pass: 0.31 s at 127. New test a deep, wide match ladder compiles in under
1.5 s failed first at 3,028 ms (.build/e005-fix1-red-ladder.log). Generated
Go is byte-identical to the pre-fix head on 21 programs (the three
examples, the match-lift, LocalId and capture programs, an if/match mix,
ladders at 32 and 127, and every depth form at 128) and cmp-identical to
bootstrap/*.go (.build/e005-fix1-identical.log). The capture row now
drops a match’s scrutinee from its free set (Format.Go.Match); its mutant
fails with undefined: bumpusLocal3 (.build/e005-fix1-mutant.log).
(2) The 128-deep timing test emits Go, then runs go build detached in
its own process group, killed as a group on timeout, and removes its temp
directory in finally. Against the closure compiler it failed with go
build killed at 10000 ms and left no go/compile process or temp
directory (.build/e005-fix1-groupkill.log). (3) Its comment now says the
build cache may supply the binary and why the bound stays honest.
rm -rf output && npm run verify exit 0: zero warnings, 22 files, 156
tests, zero failures/skips, eight regression proofs
(.build/e005-fix1-verify.log).User approved. main fast-forwarded to 09f3071 (E005 c928d93 plus review fix
round 09f3071). Clean rm -rf output && npm run verify on main exited 0:
zero warnings, 156 tests, zero failures/skips, eight regression proofs. Raw
log: .build/e005-merge-verify.log. Review: 430-program base/head
differential identical (values and call traces) and malformed-value panics
identical; fix round re-review addressed all three findings. Parked: one
82-column JS test line (rule binds PureScript only), the stale
Format.Go.Capture header comment (fixed with T001), wall-clock budgets with
~5x headroom. Evidence archived under .build/merged-e005-evidence; worktree
and branch removed. Next: T001, then P001 (user order 2026-10-07).
Branch t001 (.worktrees/t001) from 62f8192. Brief: .superpowers/sdd/t001/brief.md (approved with eight binding amendments).
docs: refresh Format.Go.Capture header: the header now
describes bottom-up free-local sets (parked from the E005 review).rm -rf output && npm run verify 76.4 s wall, its node –test phase
33.0 s (duration_ms 33,026; .build/t001-before-verify.log); node --test
alone 27.0 s (.build/t001-before-tests.log); 156 tests. Serial per-file:
large-source 12.4 s, adt-order 8.8, adt-print 8.5, depth 8.0, compare 7.0,
adt-properties 6.3, adt-match 4.7, properties 3.8, compiler 3.1, adt-types
1.7, match-lift 1.5, adt-coverage 0.5.runGoBatch (design and failure contract in
docs/engineering.md). Self-tests (test/go-batch.test.mjs, five tests) were
first run against a stub that throws: 5/5 failed
(.build/t001-selftest-red.log), then passed against the implementation
(.build/t001-selftest-green.log). Each also kills a mutant of the harness
in an isolated copy (.build/t001-mutants.log): a rejection failing the
whole batch, no blame in the compile diagnostic, a disabled shape guard,
clearing the go-batches root, and a lost exit status each fail exactly
the matching self-test; the healthy copy passes 5/5.run), so large-source’s timed
checked() of the 20,000-declaration program is still the first compile
of that source. Its 5,000-arm timed compile now follows the batch’s
compile of the same source (JIT-warm); the 5 s bound still fails a
quadratic regression by far, and a cold-compile stack overflow would
still surface through the batch’s own first compile, rethrown by that
case’s run.rm -rf output && npm run verify exit 0, 60.7 s wall, node –test
phase 19.2 s (duration_ms 19,167; .build/t001-after-verify.log), 23 files,
161 tests, zero failures/skips, eight regression proofs; node --test
alone 15.5 s (.build/t001-after-tests.log). Serial per-file: large-source
10.7 s, adt-order 6.3 (rest is goTest), adt-print 2.4, depth 8.0
(unchanged, out of scope), compare 2.1, adt-properties 0.5, adt-match 2.0,
properties 0.5, compiler 1.6, adt-types 0.4, match-lift 1.2, adt-coverage
0.5, go-batch 1.4.T001 review fix round 1 (2026-10-08; review: ready to merge, six cheap items).
(1) large-source: the 5,000-arm case has its own batch (label arms),
declared inside its test after the timed compile, so that timed compile is
cold again. (2) The guard self-test gains package main followed by
package other, and a second multi-line func main() {; the two guard
clauses that no self-test bound (the package \w+ count and the func main
count), each replaced by true, now fail that test. (3) runGoBatch takes
an array of [name, spec] pairs and refuses a duplicate name (compared as
strings, as run looks names up) at declaration; every caller passes pairs.
New self-test failed first with Missing expected exception
(.build/t001-fix1-dup-red.log). (4) The directory self-test removes its
neighbour marker in finally. (5) docs/engineering.md: batch directories
persist (rm -rf .build/go-batches reclaims them), concurrent test runs in
one checkout race on them, and the 300 s build timeout does not group-kill
(noted rather than changed: spawnSync cannot kill a group). (6) Only the
shape guard’s refusal becomes “generated Go was refused”; filesystem errors
propagate as themselves. Mutants in an isolated copy
(.build/t001-fix1-mutants.log): all eight (the five earlier, both guard
clauses, no duplicate check) fail exactly their self-test; healthy 6/6.
rm -rf output && npm run verify exit 0: 162 tests, zero failures/skips,
eight regression proofs, test phase 19.9 s, 61.8 s wall
(.build/t001-fix1-verify.log).
User approved. main fast-forwarded to 4230eb9 (T001 7cbf2dd, c4e5cd0 and
review fix round 4230eb9). Clean rm -rf output && npm run verify on main
exited 0: zero warnings, 162 tests, zero failures/skips, test phase 19.4 s,
eight regression proofs. Raw log: .build/t001-merge-verify.log. Evidence
and brief archived under .build/merged-t001-evidence; worktree and branch
removed. main pushed to origin at the user’s request. Next: P001.
Plan: docs/plans/2026-10-08-polymorphism-plan.md (spec
docs/plans/2026-10-08-polymorphism-design.md). Branch p001 in
.worktrees/p001; baseline npm run verify exit 0, 162 tests, eight
regression proofs (.build/p001-baseline-verify.log).
Behavior-preserving refactor; no language change. Pipeline is now Parse →
Resolve → Check → Specialize → Go (Program.Compile parse >=> resolve >=>
check >=> specialize, then emit).
TypeId (moved from Domain.Resolved, re-exported
there), VarId, data Ty v = TInt | TBool | TData TypeId (Array (Ty v))
| TVar v with Eq, Ord, Functor, Apply, Applicative, Bind (substitution),
Monad, and ground ∷ Ty v → Maybe (Ty Void). Domain.Resolved re-exports
it; resolved types are Ty VarId. Nothing produces TVar or a type
argument yet.Ty Open, data
Open = Rigid VarId | Hole Int; Call and Construct carry an
Instantiation (always empty now); rigid ∷ Ty VarId → Ty Open.
Features.Check* produce and cover it; check returns Checked.Program
(the CheckedProgram alias is gone).Ty, TypeInfo, CtorInfo,
Tables (ruling R1). Format.Go* change only imports.specialize ∷ Checked.Program → Either
Diagnostic IR.Program, a copy that converts ground TData id []; any
variable, type argument or non-empty instantiation is E_INTERNAL
unspecialized type at the holding span (unreachable until Task 2).candidates) and Match typeName report E_INTERNAL
on a TVar instead of guessing; Task 6 and Task 3/4 replace those arms.{ module, importers } rules in scripts/structure.mjs
internalModules. AGENTS.md, docs/engineering.md, docs/architecture.md
updated. Regression row branch needle follows the rename
(Checked.typeOf); still one target, same defect.specialize is the identity on monomorphic
programs over examples/*.bumpus and the 232 generated programs of the
two property files. Their generators moved to test/generators.mjs
(importing a test file would register its tests and build its Go batch);
draw order and seeds unchanged. test/diagnostics.test.mjs: the two
white-box coverage tests now build their input from
Domain.Checked.Internal and pass TData its (empty) argument array;
their assertions are unchanged.node --test test/structure.test.mjs failed the new gate test
(actual: [], expected [ 'Features.Check imports Domain.IR.Internal' ];
.build/p001-task1-red-structure.log). node --test
test/specialize.test.mjs failed with ERR_MODULE_NOT_FOUND for
output/Features.Specialize (.build/p001-task1-red-specialize.log).rm -rf output && npm run verify exit 0, 85.5 s wall, zero
warnings, 164 tests (162 + 2), zero failures/skips, eight regression
proofs, bootstrap snapshots unchanged (.build/p001-task1-verify.log).Language change: type List(a) = Nil | Cons(a, List(a));, lowercase type
variables, parenthesized application. Status: done.
TypeRef gains VarRef Span String; NamedRef carries
its arguments and spans head through ); TypeDecl.parameters ∷ Array
TypeParameter ({ name, span }); typeRefSpan.(lower, …) after a type name
(type T() = A; is E_SYNTAX Expected a type parameter at )); a type
is Int, Bool, a lowercase name, or an upper name with optional arguments,
each argument one nested level (ADR 006), so 129 deep is E_NESTING at
the 129th argument. List() fails at ); Int(a)/a(Int) at (.TypeArguments (E_ARITY Wrong
number of type arguments for <name>), UnboundTypeVariable,
DuplicateTypeParameter, EntryPolymorphic; TypeName gains
AppliedName, VariableName, HoleName (rendered List(Pair(Int, a)),
a, _). Witness and type argument lists share one listed helper.TypeInfo.parameters, CtorInfo.fieldSyntax (the
field’s source TypeRef, for Task 5’s nested spans), FunctionDecl.variables.laterRepeat, sort-based), then field types. In a declaration
VarId i is its i-th parameter; an unknown lowercase field type is
E_UNBOUND. A function’s variables are every lowercase name in its
signature, first occurrence, parameters then result
(Features.Resolve.Variables signatureVariables). Arity is checked at the
whole reference before its arguments. main with a non-ground result is
E_ENTRY EntryPolymorphic, beside EntryParameters.TypeInfo and CtorInfo explicitly, since
the monomorphic IR does not carry parameters or field syntax.resolved, resolveRejectedAt, checkedPoly,
checkRejectedAt); test/poly-syntax.test.mjs, 21 tests. test/specialize
identical now asserts each checked type has no parameters and each
constructor one syntax reference per field, then drops both before the
identity comparison (they are resolution data the IR does not hold).node --test test/poly-syntax.test.mjs: 19 of 20 failed (the one
pass, Int(a) at (, already held) (.build/p001-task2-red.log). GREEN:
20 of 20 (.build/p001-task2-green.log).fn main(): int = 1; is now a
polymorphic main (E_ENTRY EntryPolymorphic, span 0–19), as design §1
requires, not E_SYNTAX at int. Only the inputs of two characterization
rows changed, each keeping its code, span and text:
test/diagnostics.test.mjs now uses fn main(): 100 = 1; (E_SYNTAX
Expected a type, 11–14) and test/adt-syntax.test.mjs
fn main(): 1 = 1;. poly-syntax pins the new int behavior (21 tests).
The specialize identity-helper edit above was accepted (adds assertions,
removes none).rm -rf output && npm run verify exit 0, 185 tests (164 + 21),
zero failures/skips, zero warnings, eight regression proofs
(.build/p001-task2-verify.log).compile). The CLI does not compile polymorphic
programs until Task 7: a use of a type variable or applied type stops in
Check (E_INTERNAL Unnamed type variable) or Specialize (E_INTERNAL
unspecialized type). A declaration whose parameters no field or
signature uses still compiles, as a monomorphic type.No language change; nothing calls the unifier until Task 4. Status: done.
Flex = Rigid VarId | Meta Int; triangular
Subst (a Map from meta to Ty Flex), empty; substitute (one pass,
Ty’s bind; defined on every substitution) and compose (law
substitute (compose s2 s1) = substitute s2 ∘ substitute s1);
resolve (full normalization, acyclic substitutions only); Failure =
Mismatch | Occurs; unify (rigid only with the same rigid; a meta binds
to its walked partner unless the resolved partner properly contains it;
TData argument-wise, left to right, stopping at the first failure). A
Mismatch carries the first differing subterm pair, an Occurs the meta and
the containing type, both resolved under the substitution reached at the
failure. Meta-to-meta chains are followed by a tailRec loop (walk);
recursion is over type structure only (see E006).InfiniteType TypeName TypeName,
E_TYPE Infinite type: <a> occurs in <b> (raised from Task 4).coreLibraries admits exactly Data.Map, Data.Set and Data.Tuple;
docs/engineering.md records the rule.pure layers may use Data.Map, Data.Set
and Data.Tuple only (rejects Data.List, Data.Map.Internal, Data.MapX,
Data.Tuple.Nested, Data.Set.NonEmpty). test/unify.test.mjs, 9 tests: the
brief’s pairs; left-to-right stop; substitute versus resolve on
{m0 ↦ List(m1), m1 ↦ Int}; 500-case properties (substitute equals a
one-pass reading and obeys the composition law over arbitrary, possibly
cyclic substitutions; resolve idempotent with no bound meta left over
acyclic ones; unify results acyclic, sound, most general for
ground-range σ pairs, and in agreement with test/unify-oracle.mjs, an
independent union-find unifier, on success and on the unified types up
to renaming of metas, for single pairs and for pairs threaded after a
σ pair: 268 of 1,000 sequences succeed); stack safety at 128-deep types
and a 10,000-meta chain; the InfiniteType wire rendering (code and text).resolve that stops after one link passed the resolve
property; the binding is now drawn first.occurs and rigid): occurs check removed,
rigid unifying with any rigid, resolve stopping after one link, no walk
in unify, compose with reversed union bias, arguments right to left: each
fails at least one unify test.node --test test/structure.test.mjs failed the new gate test
([ 'pure module Features.Check.Unify imports Data.Map' ],
.build/p001-task3-red-structure.log); node --test test/unify.test.mjs
failed with ERR_MODULE_NOT_FOUND for output/Features.Check.Unify
(.build/p001-task3-red-unify.log). GREEN: 9 of 9
(.build/p001-task3-green-unify.log).rm -rf output && npm run verify exit 0, 71.2 s wall, zero
warnings, 195 tests (185 + 10), zero failures/skips, eight regression
proofs (.build/p001-task3-verify.log).Language change: rank-1 polymorphic functions and constructors type-check. Status: done.
check, checkFunction), Check.Infer (expressions), Check.Call (calls
and constructions), Check.Arms (checkMatch), Check.Match (patterns;
checkPattern keeps its old signature, from a fresh state),
Check.Require (require/expectType by unification, typeName),
Check.Scheme (state, instantiation, holes), Check.Walk (IR type
traversal), Check.Comparable (comparison groundness).{ subst, next } threaded explicitly through infer in Either;
arrays are threaded by a private applicative (threadAll), whose
traverse is balanced, so 5,000 arms and 10,000 arguments stay linear and
stack-safe. During checking a checked-IR Hole m is the meta m.if else against then; arms against the first);
the result unifies; the resolved body’s comparisons must be ground (rigid
variable: E_TYPE Type <t> is not comparable; otherwise a hole: E_TYPE
Ambiguous type <t> in comparison; pre-order, at the left operand); then
unsolved metas are renumbered Hole 0, 1, … per function in
foldTypes pre-order. Each use of a function instantiates its
variables, each constructor its owner’s parameters; Call and
Construct record the resolved instantiation in that order.Expected <t>, found <u> names both whole operands
resolved under the substitution before the failing unification (not
Unify’s innermost pair); InfiniteType names the meta (_) and the
resolved containing type. Domain.Problem gains NotComparable and
AmbiguousType (E_TYPE). The constructor-pattern arity guard (an
internal error) now runs before the owner lookup and unification.branch now targets Features.Check.Infer’s
checkConditional (still one target, same defect); docs/engineering.md
and docs/architecture.md updated.a and List(a), ambiguous
Nil == Nil and Proxy == Proxy, literal pattern on a), plus a
constructor pattern on a, whole-type messages (Pair(Int, Int) versus
Pair(Int, Bool), Maybe(List(_))), rigid beside a hole; checked-IR
rows: pair(id(1), id(true)) records [Int, Bool] (and [Int],
[Bool], rigid [a, b] inside pair), length(Nil) [Hole 0], dense
per-function hole numbering, and a pattern’s instantiated type.
test/diagnostics.test.mjs and every adt-* test pass untouched.node --test test/poly-check.test.mjs on the pre-change build: 16
of 16 failed (.build/p001-task4-red.log; the final file, rerun against
HEAD c6e87bc in an isolated copy: 16 of 16 failed,
.build/p001-task4-red-final.log). GREEN: 16 of 16.rm -rf output && npm run verify exit 0, 96.3 s wall, zero
warnings, 211
tests (195 + 16), zero failures/skips, eight regression proofs
(.build/p001-task4-verify.log).id nested 128: 1; wrap(x: a): L(a) nested 127: 128; deep(x: a):
L^127(a) nested k: checks at k = 40 (5,081 deep), RangeError in
Unify resolve from k = 44 — within every source limit (now E_NESTING
after fix round 1, below; BACKLOG E006,
.build/p001-task4-measure-depth.log). dup(x: a): Pair(a, a) nested
12/16/20: 21 ms / 208 ms / 3.2 s, doubling per level (BACKLOG E007,
.build/p001-task4-measure-dup.log).inferredTypeLimit = 1000 in Features.Check.Unify. A
type deeper than that is E_NESTING Inferred type nesting exceeds 1000
levels (Problem TypeTooDeep Int). The decision is exceedsLimit, a
stack-safe explicit-stack walk that stops past the limit. It runs on each
expression’s type when it is built (Infer infer, at that expression),
on both operands before each unification and on each binding before the
occurs check (Unify Failure TooDeep), and on every type of a finished
body before it is resolved (Scheme firstTooDeep, at the holder’s span).
Also two tests: one constructor at two types in one function, and the
left-to-right first error g(1, true). matchCtor and checkMatch are
split to stay within the declaration budget (fieldsAgainst,
checkArms, laterArm). Comments now explain the two Infer rows and
mark the checkPattern test seam. Walk’s foldTypes passes spans.deep
from the inside; deep(x: a): L^127(a) nested 44 and 127 deep are
E_NESTING at the same call (each under 5 s, about 2 ms); a type that
deepens after it is built is caught by the final bound at sink’s call.
test/poly-check.test.mjs gains 2 tests (18).rm -rf output && npm run verify exit 0, 77.2 s wall, 219
tests (211 + 8), zero failures/skips, eight regression proofs
(.build/p001-task4-fix1-verify.log). Re-measured: deep nested 8 to 127
is E_NESTING in 2–3 ms. dup nested 12/16/20 now takes 42 ms / 441 ms /
6.8 s (BACKLOG E006, E007 updated).unifyAt, one level per applied type, which equals resolved depth) and
fails with TooDeep past inferredTypeLimit. A Mismatch whose pair is
too deep to resolve also fails with TooDeep. The span is the expression
whose unification failed (Require expectType, the actual operand).
Tests:
same’s second argument.TooDeep) is E_NESTING at same’s second argument.TooDeep.Z,
not at the failed unification.rm -rf output && npm run verify exit 0, 65.9 s wall, 223
tests (219 + 4), zero failures/skips, eight regression proofs
(.build/p001-task4-fix2-verify.log).bindMeta’s strict where resolved the
binding before exceedsLimit ran. Six or more L^997 links chained in one
unification threw a RangeError in that resolve. The resolve and occurs
check now live in bindBounded, which runs only after the bound.
six chained links of L^997 are E_NESTING, not a
crash, at same’s second argument.resolve (.build/p001-task4-fix3-red.log).variables,
Check settled, bindBounded resolved).inferredTypeLimit comment now names unify as the binding
limit.rm -rf output && npm run verify exit 0, 64.7 s wall, 224
tests, zero failures/skips, eight regression proofs
(.build/p001-task4-fix3-verify.log).match m { Just(n) => n, Nothing => 0 } over Maybe(Int))
is E_INTERNAL Coverage of a type variable until Task 6.checkedPoly,
checkRejectedAt). The CLI still rejects polymorphic programs until
Task 7 (Specialize: E_INTERNAL unspecialized type).Language change: polymorphic recursion and nested datatypes that would make specialization infinite are rejected (design §4.1). Status: done.
instantiationRule ∷ Checked.Program → Either Diagnostic Unit
(Features.Check.Instantiation) judges types first, in declaration order
(Features.Check.Nested), then each function’s calls in pre-order. Inside
a strongly connected component of the call graph or of the type
reference graph (an edge for every declared type a field mentions, at any
depth), each type argument must be a bare variable of the referrer or
mention no variable; a hole counts as ground.SpecializationError (wire E_SPECIALIZATION).
PolymorphicRecursion f → Recursive call to <f> changes its type
arguments, at the call; NestedDatatype t → Recursive use of <t>
changes its type arguments, at the offending nested reference, found
by walking the resolved field alongside CtorInfo.fieldSyntax.components ∷ Array (Array Int) → Array Int,
iterative Tarjan (explicit frame stack, self tail calls, Data.Map and
Data.Set state), numbered in closing order.FunctionDecl gain name (Specialize copies it so the identity test
still compares checked and monomorphic IR unchanged; Go ignores it).length(t), mutual even/odd, swapping, dropping, ground
substitution, Rose/Forest, T(b, a)) plus a hole as ground and wrapping
into another component (functions and types); rejected rows with exact
code, span and text (self and mutual List(a) calls, §4.3’s
conservative case, Pair(x, 1), first offence in pre-order, Nest,
the nested W(Pair(Int, a)) reference, mutual types, types before
functions); generator coverage; 200 generated components accepted and
each wrapped variant rejected at its call; components against a
brute-force reachability oracle (300 graphs); 20,000-node chain and
cycle under 5 s each (about 0.23 s for both). test/poly-components.mjs
exports the generator (ruling R3): per component its functions,
arities, calls (variable or ground arguments) and groundTypes, plus
render and wrapped, for Task 8’s bound.W row and the generated test
(.build/p001-task5-mutant-components.log); treating holes as variables
fails the hole row and the generated test
(.build/p001-task5-mutant-hole.log).rm -rf output && npm run verify exit 0, 66.4 s wall, 247 tests
(224 + 23), zero failures/skips, eight regression proofs
(.build/p001-task5-verify.log).length(t) and even/odd match on List(a), which still
ends in E_INTERNAL Coverage of a type variable until Task 6; the test
accepts exactly that diagnostic for those two rows (coverage runs after
the rule, so reaching it proves the rule accepted). Task 6 should make
them plain acceptances.checkRejectedAt,
check). The CLI still rejects polymorphic programs until Task 7.Language change: matches over applied types (List(a), Maybe(Void),
List(List(Int))) and over type variables are checked for exhaustiveness
and redundancy (design §5); the E_INTERNAL Coverage of a type variable
gap is gone. Status: done.
expand ∷ Array TypeInfo → Array CtorInfo →
Array (Ty Open) → Lookup Expansion numbers every application reachable
from the roots by unfolding constructor fields (finite by Task 5),
keyed with variables forgotten (rigid variables and holes are one
abstract type), and builds monomorphic-shaped types/ctors tables
over those numbers: application fields as TData (TypeId n) [], the
abstract type as Int (inhabited, no data). Discovery is a tailRecM
loop of one round per frontier, so it is stack-safe. Roots: every data
type the bodies carry (Features.Check.Walk foldTypes, deduplicated in
a Set) plus every declared type whose constructors mention no variable,
so inhabitation still settles over every monomorphic declaration.buildSignature runs the unchanged inhabitation fixpoint
on the expanded tables; candidates maps an application’s inhabited
expanded constructors back to the declared ones (declaration order) and
gives rigid variables and holes no heads; fieldTypes ∷ Signature →
Ty Open → Head → … substitutes the column’s type arguments. Matrix
complete treats a variable column like Int (never complete). Witness
format unchanged (constructor names only).Nil/Cons(_, _) over List(a), a binder over a,
binders under constructors over a, Nothing alone over Maybe(Void),
Maybe(Maybe(Void)), a hole column, nested heads through
List(List(a)); E_TYPE for Nothing over a (Task 4 guard); exact
witnesses Cons(_, Cons(_, _)) and Cons(Cons(_, _), _) through
List(List(Int)), Pair(_, false), Nothing, Just(_), Cons(_, _)
over a hole; redundant _ after a binder and after Nothing over
Maybe(Void); a generic function with a non-exhaustive match called at
two types reports one diagnostic at the match span; five arm sets over
Maybe(Void) give the same verdict as monomorphic type MV = N |
J(Void) (ADR 003); a 3,000-type parameterized chain where Dead needs
an arm only for U(Int) and U(a), not U(Void), each under 5 s
(about 1.1 s for the test).length(t) and even/odd rows
now require plain acceptance; no poly-* test tolerates E_INTERNAL.Coverage of a type variable; the
Maybe(Void) rows and the chain because inhabitation was per declaration,
not per application) (.build/p001-task6-red.log). The five that passed
pin behavior the old code already had (the E_TYPE guard and witnesses
whose types need no variable column).rm -rf output && npm run verify exit 0, 266 tests (247 + 19),
zero failures/skips, eight regression proofs; coverage.test.mjs,
coverage-scale.test.mjs and adt-coverage.test.mjs unchanged and passing
(10,000-type chain 1.10 s, was 0.89 s in Task 5’s log)
(.build/p001-task6-verify.log).checkedPoly,
checkRejectedAt, check). The CLI still rejects polymorphic programs
until Task 7.Language change: polymorphic programs now compile and run end to end
through the CLI (npm run bumpus -- run examples/lists.bumpus prints
Pair(Cons(Pair(3, true), Cons(Pair(2, false), Nil)), Just(6))).
Status: done.
specialize = specializeWith
TInt; specializeWith ∷ Ty Void → … (hole representative, for tests);
specializationKeys returns { declaration, function, arguments }, types
then functions, each in output-id order. Design §6 worklist: seeds are
every monomorphic type (its fields may reference applications) then every
monomorphic function, in id order; a single first-in first-out list of
work items, filled by a tailRecM loop; each body is copied in pre-order
(signature, then an expression’s type, a call’s instantiation, its
callee, its arguments; a pattern’s type before its fields), and a key is
created on first reference. Monomorphic declarations keep their order and
take the first output ids, so monomorphic programs come out unchanged.
A polymorphic declaration never reached is not emitted.Map (Tuple Int (Array IR.Ty)) Int; IR.Ty gains Ord), so
a key compares in time linear in its arity; specializationKeys
rebuilds Ty Void arguments from those numbers.specializationLimit = 10000 counts only keys of polymorphic
declarations (function instantiations and parameterized type
applications); the reference that would create key 10,001 is
E_SPECIALIZATION More than 10000 specializations (Problem
SpecializationLimit Int). A key found in a constructor field is
reported at its own nested reference (field syntax walked alongside, as
Check.Nested does).traverse keeps wide bodies in shallow stack). Copy
is a sixth module beyond the plan’s Keys and Body. Format.Go needed no
change: specialized constructors carry source names, so values print and
re-read (Cons(Pair(1, true), Nil)).main; Review
Focus 1–5 (namespaces a(5) → 5; loop() fixed by context → 1; zip
value printed and re-read → Cons(Pair(1, true), Nil) both ways; an
8,192-element list from generic append, counted by generic length →
8192; xs < Cons(2, Nil) inside a generic function → true); length at
List(Int) and List(Bool) in one program → Pair(1, 2); length(Nil)
→ 0 with a single length key at Int; exact key order for a small
program; an uninstantiated polymorphic function and type are not
emitted; the limit (compile only): 6,000 function keys + 4,000 type keys
gx(1) and QX(1).
test/large-source.test.mjs: 3,000 distinct instantiations (3,000
function and 3,000 type keys) compile in about 0.6 s (bound 5 s); the
20,000-declaration programs are unchanged and pass.
test/compiler.test.mjs: bootstrap/lists.go snapshot and CLI run.
test/specialize.test.mjs: identity now skips the one polymorphic example
by name (its count assertion is unchanged).npm run bumpus -- emit
examples/lists.bumpus bootstrap/lists.go and reviewed (types numbered in
discovery order, Pair(Int, Bool) first from main’s signature;
constructors in contiguous blocks; one compare and show helper per
specialized type; Bool helper emitted). answer.go, shapes.go and tree.go
are byte-identical (CLI re-emit cmp, and their snapshot assertions).specializationKeys not
exported); with that import stubbed, all 27 tests failed (the Go cases
with E_INTERNAL unspecialized type, the key tests on the stub); the
limit test first failed on its own syntax (match as a + operand, now
parenthesized), then with unspecialized type; the 3,000-instantiation
test failed with unspecialized type (.build/p001-task7-red.log,
.build/p001-task7-red-limit.log).rm -rf output && npm run verify exit 0, 295 tests (266 + 29),
zero failures/skips, eight regression proofs
(.build/p001-task7-verify.log).compile, the CLI and
Go). Task 8 adds the specialization properties.Language change: none (tests only). Status: done.
specializationKeys are equal (normalized keys
such as fn f0[List(List(Int))]), and every program has polymorphic
keys;compile twice gives equal Go; the reversed program
prints the same and has the same normalized key set (sorted arrays,
declaration and type ids replaced by source names);specializeWith TBool and specialize
emit different Go for every program (the holes reach specialization)
and both print the same;main at a random function and random ground arguments
(Task 5’s four plus List(List(Bool)) and List(List(_))); keys per
component ≤ Σ_{d∈C} |T|^arity(d) with T = the entry’s arguments ∪ G_C,
holes normalized to their representative Int (Task 5’s List(_)
reads List(Int)), and every key’s arguments lie in T. outside’s
keys are not counted (its own component). The bound is often reached
exactly. Component programs are only counted, never run (R13);main instantiations at List(List(Int)) beside
List(List(Bool)) and Pair(Int, Bool) beside Pair(Bool, Int) of
the same function.+, strict left-to-right, ADR 005 order), sharing no compiler
code; test/poly-types.mjs, test/poly-expressions.mjs and
test/poly-programs.mjs, the type-directed generator (List, Pair and a
generated G whose recursive fields keep or swap its parameters; four
generic functions calling only earlier ones, with self calls only on a
part of the recursion parameter, so every program terminates);
test/poly-keys.mjs, keys by source name. test/poly-components.mjs:
render takes an optional entry in place of main. The brief named
one helper file; it is split to respect the size target.specialized stubbed to return Left (temporary edit,
reverted), the five properties failed and the two self-checks passed
(.build/p001-task8-red.log). A second temporary mutant, function keys
looked up with every applied-type argument treated as equal, failed the
execution oracle, determinism and representative tests (the batch’s Go
no longer compiled) (.build/p001-task8-mutant-key.log); uniqueness and
the bound do not catch that mutant, because it reuses keys rather than
duplicating them.rm -rf output && npm run verify exit 0, 302 tests (295 + 7),
zero failures/skips, eight regression proofs
(.build/p001-task8-verify.log).compile, specializeWith,
specializationKeys) and Go for the generated programs; the
components through Specialize only.Language change: none (regression rows and documentation). Status: done.
occurs: Features.Check.Unify bindBounded, if mentions meta
resolved then → if false then. Healthy: E_TYPE Infinite type: _
occurs in List(_). Mutant: the cyclic binding is caught later by the
depth bound, E_NESTING Inferred type nesting exceeds 1000 levels, so
the probe fails occurs check missing.rigid: Unify unifyHeads, the rigid-with-same-rigid arm →
TVar (Rigid _), _ → Right subst plus _, TVar (Rigid _) → Right
subst. Healthy: fn f(x: a): Int = x; is E_TYPE Expected Int, found
a. Mutant: accepted (rigid variable unified with Int: null).instantiate: Features.Check.Scheme instantiate no longer advances
the meta counter (, state: state { next = … } → , state: state), so
later uses reuse the same metas. Healthy: pair(id(1), id(true))
prints Pair(1, true). Mutant: rejected (scheme metas shared across
uses: probe program was rejected).spec-key: Features.Specialize.Keys applied keys each data-type
argument as TData (TypeId 0) (Int and Bool kept). Healthy: the
length program over List(List(Int)) and List(List(Bool)) prints
Pair(1, 2). Mutant: Go build fails (cannot use … (value of struct
type bumpusTy1) as bumpusTy0 value), probe fails nested
specialization keys collided.
Evidence: .build/p001-task9-mutants.log (each probe exit 0 on the healthy
build, exit 1 with its message on its mutant).node scripts/regression.mjs: twelve Regression proof (…)
lines, 43.5 s wall.int is
a variable); docs/architecture.md (final pipeline and phase list, depth
bound, P001 test files, new rows); docs/engineering.md (test count, the
four rows); BACKLOG.md (P001 Done on p001; E006 and E007 updated; new
E008 Expand keys, E009 did you mean Int? hint, E010 _ in mismatch
messages, E011 near-limit diagnostic elision); docs/findings.md (P001
observations); README.md (ADR 007 link); docs/next-session.md.rm -rf output && npm run verify exit 0, 85.3 s wall, zero
warnings, 32 test files, 302 tests, zero failures/skips, twelve
regression proofs (.build/p001-task9-verify.log).Rank-1 polymorphism is implemented on branch p001 (Tasks 1-9, commits 959a7d1 onward). Parameterized types and generic functions type-check with rigid signature variables, fresh metas per use and occurs-checked unification; comparisons need ground operand types; unsolved metas are holes that Specialize fills with Int; the instantiation rule makes the set of specializations finite; coverage works over applied types; inferred types are bounded at 1,000 levels (E_NESTING, never a crash); a pure Specialize phase produces the variable-free monomorphic IR with hash-consed keys and a 10,000-key limit. Monomorphic programs emit byte-identical Go. Tests grew from 162 to 302 and regression proofs from eight to twelve. ADR 007 records the decisions (rulings R1-R20 in the plan’s ledger). Open follow-ups: BACKLOG E006-E011. Pending: final whole-branch review and the user’s approval to merge; FN001’s currying direction is to be discussed with the user before its design.
The final whole-branch review (verdict: with fixes) found one Important issue and several minors; all were fixed in one commit on p001.
unifyHeads bound the left (older) of
two unbound metas to the right (newer), so sibling metas joined in turn
formed one chain as long as the siblings, which every later walk
followed: quadratic. Now the newer (larger id) is bound to the older;
self-binding, the depth bound and the occurs check are unchanged
(bindMeta still decides the bound before bindBounded resolves). New
tests in test/large-source.test.mjs, 5 s bound: 5,000 match arms of
Nothing (then _ => Just(0)) and 4,000 Nil arguments to
f(x0: b, …). RED (before the fix, built output): 15.8 s and 8.5 s;
GREEN: 1.55 s and 0.15 s. A first Nothing followed by Just arms grows
no chain (1.6 s before the fix too), so the test uses all-Nothing arms.actual false, expected true. Reproduced
by running the eight P001 test files three at once: it is the 5 s timing
bound of poly-depth an inferred type exactly 1000 deep checks, which
took 2.9 s alone and 7.0-9.0 s under that load. It does not depend on
binding direction (2.8 s with either). Profiling showed nearly all of
that time in Ord (Ty Unit) comparisons of Features.Check.Expand’s
whole-type map keys, 1,000 applications nested up to 1,000 deep. Expand
now keys its numbering by a canonical string spelling of the key (one
native string comparison) and keeps number-to-key in a second map:
2.8 s to 0.35 s alone, 1.9 s under the same triple load, where it no
longer fails; the 3,000-application coverage chain is 0.72 s to 0.65 s.
This is a constant-factor fix; hash-consed keys (BACKLOG E008) remain
the asymptotic one. The same triple load also failed Go batches,
because concurrent runs of one test file in one checkout share
.build/go-batches/compile (support.mjs
rejectedAt), asserting the same code, span and text (design §8,
“through the CLI”); the Check-level rows stay.open maps each open node to
its order, so no lookup can miss; an edge to a node outside the graph,
a component root missing from the stack and an unnumbered node stop the
search with E_INTERNAL (components returns Either Diagnostic). New
test: an edge to a missing node is E_INTERNAL; the reachability and
20,000-node tests pass unchanged in substance.t.Verification: rm -rf output && npm run verify exits 0 in 90 s wall
(97 s on an earlier run) with 318 tests (302 + 2 timing + 1 E_INTERNAL +
13 compile twins), zero warnings and twelve regression proofs
(.build/p001-final-fix-verify.log). The eight files (node --test
test/unify.test.mjs test/poly-*.test.mjs
test/diagnostics.test.mjs, 153 tests) passed three sequential runs
(.build/p001-final-fix-run{1,2,3}.log).
User approved the merge (no push). main fast-forwarded from 79c2327 to
dcb40fb (P001 Tasks 1-9, the final-review fix wave, and the merged main
direction commits). Clean rm -rf output && npm run verify on main exited
0: zero warnings, 318 tests, zero failures/skips, twelve regression
proofs. Raw log: .build/p001-merge-verify.log. Per-task logs, briefs,
reports, review packages and the ledger (rulings R1-R20) archived under
.build/merged-p001-evidence; worktree and branch removed. Next: FN001,
starting with the currying discussion recorded in BACKLOG.
Discussed with the user before any design decision, as BACKLOG required.
Settled: curried semantics with Algol-like multi-argument syntax as
proposed (partial application, over-application, under-application
becomes a function-typed value with a missing N argument(s) hint
replacing today’s under-application E_ARITY); lambdas fn(x) => body
reusing fn and =>, parameter annotations optional and inferred
locally; |> included in FN001 (left-associative, below comparison),
>>/<< wait for a Prelude; no local binding form in FN001 (separate
item), so closures capture parameters and pattern variables by value.
Design draft written the same day:
docs/plans/2026-10-08-functions-design.md (syntax, names, typing, declared
arity, evaluation, instantiation rule, canonical uncurried Go
representation with adapters, tests and regression probes, deliberate
diagnostic changes, four open questions). Not reviewed or approved; no
plan, no code. Next: the user’s review of the draft.
The user’s review of the first draft: revise before approving. The
canonical uncurried representation (A -> B -> C as func(A, B) C with
adapters) lost declared-arity timing: with fn stuck(n: Int): Int -> Int
= stuck(n); and use(f) = drop1(f(1)), use(stuck) must diverge but
the adapter made f(1) a partial application returning 0; lambda
eta-expansion delayed bodies the same way, and the draft’s “delays
nothing observable” claim was wrong. Revision: nested Go function values
func(A) func(B) C, staged wrappers for named functions and constructors
used as values, direct saturated calls unchanged; declared arity is a
stage boundary; pipe evaluates its left operand first (kept as a Pipe
IR node); constructors are curried values; _ discards lambda
arguments; the under-application hint is narrow with provenance rules;
timing tests use instrumented bounded panics, never stack exhaustion;
arrow spines are depth-flat, traversed iteratively, named Go function
types keep text linear, scale tests at 5,000 parameters. Section 12 of
the design maps each finding. Not yet approved; no plan, no code.
The user approved the revised design and asked for the plan. While
writing it, the section 8 go build question was measured on
hand-written Go (scratchpad, Go 1.26.4): nested-closure staged wrappers
took 1.3 s at 50 parameters, 2.6 s at 100, 13.9 s at 200, 221 s at 300,
and did not finish within 10 minutes at 1,000; an environment struct
copied per stage took 1.1 s at 300 and 1,000 and 12.8 s at 5,000. A
parameter bound is not viable (large-source already checks a
20,000-parameter declaration). Recorded as design Amendment A1 (section
13, proposed): no nested Go closures; staged wrappers and every lambda
lifted to top-level stage chains over linked environments; meaning and
types unchanged. Plan: docs/plans/2026-10-08-functions-plan.md (nine
tasks; Task 1 re-measures the linked environment before any lowering
code). Awaiting the user’s confirmation of A1 and review of the plan.
No code.
Verify for this docs change: first run failed one timing test (match
ladder 1,830 ms over its 1.5 s bound under external load, 471 ms alone;
.build/fn001-plan-verify.log), recorded as BACKLOG T003 before
re-running; second run exit 0, 318 tests, twelve regression proofs, the
ladder at 752 ms (.build/fn001-plan-verify-2.log).
The user approved A1’s architecture (top-level stages, immutable
environments) and the nine-task structure with three changes, applied to
the plan: Task 1 has no quadratic fallback (if the linked environment
fails its linearity thresholds, stop and reconsider with the user) and
benchmarks runtime with repeated applications, every argument consumed,
explicit build/run timeouts; equality, ordering and type interning are in
the scale work (hand-written spine-iterative Eq/Ord for both Tys in
Tasks 2 and 5, arrows hash-consed in Specialize.Keys, Go function type
names numbered from interned suffixes in Task 6, a 5,000-parameter
function type as a generic argument in Tasks 5 and 8); the core timing
probes move into Task 6’s first step, Task 7 keeps the interpreter and
generated comparisons. Accepted diagnostic adjustments now in the design
(§2, §4, §9): over-application past a non-function and f() keep
E_ARITY Wrong number of arguments; x() and N() keep E_NOT_CALLABLE.
Task 1 (measurement only) authorized in .worktrees/fn001.
Branch fn001 (.worktrees/fn001). Added scripts/stage-shapes.mjs (Go
generator: linked, copied, nested; attribution baselines direct, bare,
idle, chain; remedy candidates shared, packed) and
scripts/stage-probe.mjs (explicit build/run timeouts, repeated
applications with every argument consumed, checksums recomputed in
JavaScript, growth-exponent verdict). Every run’s checksum matched. The
first verdict line (“linear”) was wrong: its ratio bound admitted a 16×
build for 4× n; replaced by the exponent bound before any decision. Result
(design §13 “A1 measurement”): the linked environment fails the build
bound (k = 1.92 with the statement driver, 1.74 without applying it);
run time is linear. Attribution: the last stage’s n-local unpack and
n-argument call; a packed calling convention fixes the staged value
(k = 1.23 including a k = 1.52 floor of types and f). Applying a value
to n arguments in one Go function is k ≈ 2 in every shape. Stopped per
the user’s ruling: no representation adopted; decision returned to the
user. No compiler change.
User authorized a second measurement round only. Added scripts/stage-baselines.mjs (types, signature, body, call), scripts/stage-mixed.mjs (mixed Int/Bool/pointer parameters, each read twice out of order; today’s n-ary convention, packed per-type arrays, packed struct), scripts/stage-blocks.mjs (block-split application, arguments evaluated in place), scripts/stage-order.mjs (order, body-entry and shared-partial checks with expected output computed from the semantics); probe reports total build times and an exponent per program. All checksums matched; four order cases matched; a hoisting mutant of the helpers failed the order check (scratchpad, not committed). Results in design §13 “round 2”: Go’s growth is confirmed in large bodies (today’s convention, mixed body: 1.51 s / 33.6 s at 5,000 / 20,000); packed arrays plus blocks of 64: 2.45 s / 19.2 s; homogeneous packed, blocks of 64: 2.08 s / 13.1 s; runtime linear. Nothing adopted; proposed Go bounds returned to the user. No compiler change.
The user approved the scale rule (Bumpus phases linear and stack-safe; Go builds at most 10 s per program at 5,000 parameters in verify and 100 s at 20,000 in the milestone probe) and one final round, adoption conditional on the shared-body bound and type-diversity allocation. Added scripts/stage-shared.mjs. Round 3 (.build/fn001-task1/run5-*.log): shared body with direct call, packed entry and a shared partial, total build 3.4 s / 5.1 s at 5,000 (3 / 1,000 types) and 52.1 s / 80.0 s at 20,000; 49.3 / 52.0 bytes per stage; checksums matched. Both conditions hold; convention adopted and written as design §13 rules 1-8; plan Task 6 interfaces and tests updated; conclusions narrowed (“tested large bodies”, not “no representation”). The 20,000 / 1,000-type build uses 80% of its bound. Next: Task 2.
Branch fn001. Behavior-preserving: no syntax produces an arrow yet.
Domain.Type.Ty gains TFun (Ty v) (Ty v) with spine
helpers (spine, spineThrough, arrows, children; a loop each, in
Domain.Type itself since the instances need them). Eq, Ord and Functor
are hand-written: arrow spines are walked in step by a loop, applications
compare inline (the first version, through helpers, overflowed
test/poly-depth an inferred type exactly 1000 deep checks in verify;
fixed. Headroom figures are approximate (JIT variance): my probe gave
compare the derived instance’s spare frames at 1,000 levels and ==
about 8,700; the review measured compare at 8,592 spare frames in
most runs but 6,249 in 2 of 10, and the pipeline’s minimum stack for
the poly-depth 1,000-deep case at about 559 KB both before and after).
Bind (substitution) and ground walk spines by a loop. Unify: arrow
against arrow unifies parameters at level + 1 and results at the same
level in a tailRec loop entered only when both heads are arrows (list
nesting stack headroom unchanged, measured); resolve, exceedsLimit
and the occurs check follow the same measure. Problem.FunctionName,
rendered right-associatively by a loop with function parameters
parenthesized. Comparable: rigid, then hole, then a new function check
through the new Features.Check.Functional (declared types whose fields
reach an arrow, settled once per program over the reverse reference
graph; a program with no arrow field builds no graph). Signature and
Matrix treat an arrow column as abstract; Inhabited and Expand treat
every arrow as inhabited and never unfold it; Expand spells arrows
f(…); Nested’s reference graph enters arrows, and a resolved arrow
field is an Internal field-syntax mismatch until Task 3 adds the
syntax. Specialize.Lower: Internal "unlowered function".TFun before
the code): test/unify-arrow.test.mjs (7 tests: arrow against arrow;
mismatch inside a parameter and a result; meta against an arrow and
occurs through one; rigid against an arrow; a 5,000-long spine unifies
and resolves; 1,001-deep parameter nesting is TooDeep while a 5,000-long
spine is not; a meta bound through a spine), test/type-order.test.mjs (5
tests: Eq/compare on equal and last-parameter-differing 20,000-long
spines; substitution and ground on one; agreement with the derived
order on 2,000 generated arrow-free pairs, whose reference was first run
against the old derived instance and passed; arrow name rendering; a
20,000-long name). test/unify.test.mjs and the independent oracle
test/unify-oracle.mjs generate arrows; shared helpers moved to
test/unify-support.mjs.exceedsLimit; parameters unified at the same level;
recursive arrow equality; recursive, unparenthesized name rendering.
Each fails at least one new test.rm -rf output && npm run verify exit 0, 330 tests (318 + 12),
zero failures/skips, twelve regression proofs; bootstrap snapshots
unchanged.containsFunction; a 50,000-type chain under 5 s), seen failing on an
isolated no-fixed-point mutant (seeds only: 2 of 3 fail); spineThrough
can no longer file a result as a parameter (its unreachable arm adds
none); a redundant Inhabited arm removed; a unify-arrow test renamed;
headroom figures above reworded as approximate.Branch fn001. No existing row changes; bare names and calls of locals keep their P001 resolution until Task 4.
-> and |> longest first. Domain.Syntax
gains FunRef, Lambda, Apply, Pipe, LambdaParam and
typeRefSpine (a loop). The type parser moved to the new
Format.Parse.Type: an arrow chain is read as a list (Grammar
chainRight, Cursor rightAt, a tailRecM loop) and folded right;
(A, B) -> C flattens to A -> B -> C; a list of two or more types not
followed by -> is Expected ->, () is Expected a type at ).
Nesting follows design §1 (parameter side +1, checked at its ->;
result side +0); type parentheses are one level while parsed (Cursor
groupedAt) and none once closed, recorded in ADR 006’s new FN001
section. Format.Parse.Lambda parses fn(x, _: T) => body (Expected a
parameter); Format.Parse.Expression adds |> (a chainLeft1 over
comparisons) and postfix application (Grammar manyOn; flattened by
the review fixes below); a name call stays Call. Domain.Resolved
gains Lambda (with Param = { local, ty, span }), Apply and Pipe;
Features.Resolve.Lambda resolves parameters (E_DUPLICATE Duplicate
parameter x at the first occurrence of the earliest repeated name,
fn(b, a, a, b) at the first b, as for functions; _ gets no
LocalId; annotations against the signature’s variables, E_UNBOUND
Unbound type variable b); resolveType and occurrences walk
written spines by loops. Check.Infer returns Internal "unchecked
function" for the three new nodes. Check.Nested replaces Task 2’s
placeholder: an arrow field is judged beside its written spine
(parameters and final result, paired by a loop), so a nested reference
inside an arrow is reported at its own span. test/poly-parse.mjs (the
oracle’s own parser) reads arrow types, lambdas, postfix application and
pipes, independently. Style gate: Format.Parse.Type and
Format.Parse.Lambda join the applicative production modules. BACKLOG
E003 notes two dispatches now over the branch target.FunctionRef/CtorRef expression constructors.
Domain.Resolved’s GlobalRef already has constructors of those names,
so adding them needs that rename, which belongs with Task 4’s bare-name
resolution.1 +
fn(y) => y is Expected an expression at fn before and after, kept
as a characterization row); test/fn-oracle-syntax.test.mjs, 2 tests,
both failing. Added after the code, each shown to bite by an isolated
mutant (scratchpad copy, not committed): a parameter inside a
parameter counts both levels (mutant: a parameter contributes no level
in rightAt), the (Int -> L^127(Int)) -> Int row (mutant: a result
contributes a level), 127 further applications parse; one more is
E_NESTING (mutant: postfix application without infixed). Further
mutants, each failing the named rows: type parentheses keeping their
level (128 nested parameter positions resolve); no lambda duplicate
check (both E_DUPLICATE rows); Nested arrow arm restored to a mismatch
(all four arrow-field rows); no Expected -> lookahead (both rows)._, duplicates, rigid and unbound annotations). The rows for bare
function and constructor names and calls of locals change in Task 4
(plan Global Constraints), so no interim outcome is pinned here.checkRejectedAt, checkedPoly), whose programs contain no
lambda, application or pipe.rm -rf output && npm run verify exit 0, 380 tests (333 + 47),
zero failures/skips, twelve regression proofs; bootstrap snapshots
unchanged. Log .build/fn001-task3-verify.log.Apply of every group’s arguments in order (g(1)(2)(3) is Apply
(Call g [1]) [2, 3], exact under design §5), the callee one level
deeper once per chain, replacing the per-group level, which made plan
Task 8’s 1,000-application chain E_NESTING. Seen failing first: the new
row a chain of 1000 postfix groups resolves to one Apply failed with
E_NESTING (NestingTooDeep 128 at the 129th group’s (; log
.build/fn001-task3-i1-red.log), as did the flattened shape and span
rows; the 128-application E_NESTING row is removed. M1, ADR 006 states
that a parenthesized type raises its contents’ depth while parsed, with
the four L^127((Int -> Int))/L^128((Int)) rows in the new
test/fn-depth.test.mjs (passing on arrival: they pin behavior). M2,
design §1 clarifies empty value applications. M3, five FN001 forms in
scripts/depth-forms.mjs (functionForms, named-only in
depth-probe.mjs), checked at the limit and one past it through Resolve
by test/fn-depth.test.mjs; the review’s lifted-limit figures in ADR 006;
plan Task 6 Step 3a re-measures them. M4, BACKLOG E003 trigger
reworded. M5, the duplicate wording above, with a fn(b, a, a, b) row.
GREEN: rm -rf output && npm run verify exit 0, 387 tests (380 - 1 + 8),
zero failures/skips, twelve regression proofs, snapshots unchanged (log
.build/fn001-task3-fix-verify.log).Branch fn001. Reach: Parse → Resolve → Check (test/phases.mjs,
test/fn-checked.mjs); one CLI row asserts that checked function programs
reach Specialize as Internal "unlowered function".
GlobalRef constructors are
renamed GlobalFunction FunctionId Int (with the declared parameter
count) and GlobalCtor CtorId, freeing FunctionRef Span FunctionId
and CtorRef Span CtorId for expressions. A bare name is a local, else a
function with parameters (FunctionRef), else a zero-parameter function
(Resolve: E_ARITY Expected f(), new Problem.FunctionNeedsCall), else
a constructor (Construct with none if nullary, CtorRef otherwise),
else E_UNBOUND. A local called with arguments is Apply (Local …), the
local spanning the name; x() stays E_NOT_CALLABLE. CtorNeedsArguments
is removed (no longer produced).FunctionRef, CtorRef, Apply, Lambda (Array Param),
Pipe; Call/Construct hold 1..n arguments (partial when fewer).f() with parameters E_ARITY before arguments; j > n: E_ARITY
before any argument if the declared result can never be a function
(Int, Bool, declared type), else check n, E_ARITY at the call if the
instantiated result is not an arrow or unbound meta, else apply the rest
as Apply (Call f a1…an) rest), Apply (one argument at a time, the type
threaded with the state by Scheme’s threadAll, now generic in its
state; an unbound meta is bound to α -> β; anything else is E_TYPE
Expected a function, found T at that argument), Pipe, Lambda (fresh
metas or rigid annotations), Hint, Context (shared env rows). require
wraps a TypeMismatch in Hinted problem (MissingArguments f k) per §4’s
provenance rules (the found expression’s own node a partial Call or
Construct; expected type’s head, under the state before the failure,
neither an arrow nor an unbound meta). Walk, Comparable, Coverage cover
the new nodes; a function scrutinee admits only _ and binders (by
unification, unchanged Match). Check judges a printable main before any
body: E_ENTRY Expected fn main() with a printable result type when the
result contains an arrow directly or through declared fields
(Functional). Instantiation’s foldCalls enters Apply and Pipe
(calls there are ordinary edges); value references and lambda bodies are
Task 5’s. Specialize.Body returns Internal "unlowered function" for
each new node.a |> g(b1…bk) with k = n is
the over-application g(b1…bk, a): E_ARITY at the right operand’s span
when g’s result is not a function; any other right side is applied to
a (NotAFunction at a). a |> f() keeps f()’s own E_ARITY. (2) The
over-application E_ARITY is checked before arguments when the declared
result is Int, Bool or a declared type, so those wrong counts keep their
pre-FN001 order of errors. (Corrected by the review: a result type
variable cannot keep it; see the review fixes below.) (3) Only a TypeMismatch
is hinted (an occurs
failure never has a non-meta expected head). (4) The printable-entry
check lives in Features.Check (it needs Functional), not
Check.Signature, which is the coverage signature.fn f(): P = Pair;: E_ARITY 37-41 Pair
Constructor Pair needs arguments → E_TYPE 37-41 Pair
Expected P, found Int -> Int -> P.fn main(): Int = Cons;: E_ARITY at Cons
(58-62) Constructor Cons needs arguments → E_TYPE 58-62
Expected Int, found Int -> IntList -> IntList.fn main(): Int = Cons(1);: E_ARITY at Cons(1) (58-65) Wrong number
of arguments → E_TYPE 58-65 Expected Int, found IntList -> IntList;
missing 1 argument to Cons?.fn helper(): Int = 1; fn main(): Int = helper;: E_UNBOUND at the
second helper (80-86) Unbound local helper → E_ARITY 80-86
Expected helper().Cons(f, _) => f(1): E_NOT_CALLABLE at f(1)
Local is not callable: f → E_TYPE at 1 (128-129)
Expected a function, found Int.
The adt-types rows now also assert these messages. No other existing
assertion changed.Int -> Int and
Box(Int), three patterns on a function scrutinee and a redundant arm
after _, which Task 2’s type passes already gave; kept as
characterization). Two markers were then corrected in the tests (an
earlier = x in the prelude) and one unhinted row rewritten to g + 1
so the found expression is the binder, not the match.Apply (add3(1)(2)), over-application always E_ARITY (4
rows), partial application typed as the result (63), a non-function
bound to an arrow instead of NotAFunction (6).node --test test/*.test.mjs run failed
match-lift’s 1.5 s ladder at 1,780 ms (load); evidence added to BACKLOG
T003. BACKLOG E003 counts updated.rm -rf output && npm run verify exit 0, 468 tests (387 + 81),
zero failures/skips, twelve regression proofs, bootstrap snapshots
unchanged; twenty thousand parameters are checked in linear time
passes unchanged (258 ms). Log .build/fn001-task4-verify.log.id(true + 1, 2),
fst(1, true + 1, 3), loop(true, 2) E_ARITY → E_TYPE at true;
id(id(1, 2), 3) keeps E_ARITY at the inner id(1, 2); loop(1, 2)
(loop: Int → a) is accepted by Check (each confirmed). Recorded in
design §9; the first two pinned in test/fn-check.test.mjs (they pass on
0a49dfd, pinning behavior). I2: the Specialize row was vacuous (its
prelude’s k failed first); it now uses its own two-function program and
asserts each node’s own span for (fn(x) => x)(1), 1 |> add(2),
id(add)(1, 2), add(1)(2). Isolated mutants: Specialize copying
Apply’s callee and Pipe’s left operand, and Apply alone, each make
it fail (invalid program compiled). M1: a pipe into a saturated call
whose declared result is Int, Bool or a declared type is E_ARITY right
after the left operand, before the call’s arguments (design §9);
1 |> add(true, 2) failed on 0a49dfd (E_TYPE at true). M2:
Format.Parse.Expression’s primary reports where it starts, so an
application of a parenthesized callee spans from its (; Task 3’s
(g)(x) row in test/fn-syntax.test.mjs moves from g)(x) to (g)(x)
(with ((g))(x)(y) and a bare (g) rows), the hint row
(fn(x) => add(x))(1) likewise, and (add)(1) is a new no-hint row; on
0a49dfd the three span rows and the Specialize row failed (5 failures,
test log in the scratchpad). M3: plan Task 4 Files list corrected. M4:
docs/language.md Names paragraph points to the design as superseding,
pending Task 9. Perf: supplied builds the arrow only for a partial
application. Match ladder alone, six alternating runs before/after the
change: 519/518, 525/519, 524/527, 514/514, 508/535, 578/1,641 ms (the
last pair under an external load spike), then four more under load
average 7.4: 686/642, 741/715, 715/713, 700/705 ms: no measurable
difference (the ladder makes no calls on that path; 0a49dfd’s parent was
508-516 ms against 523-562 ms at 0a49dfd).
GREEN: rm -rf output && npm run verify exit 0, 472 tests (468 + 4),
zero failures/skips, twelve regression proofs, snapshots unchanged;
the ladder took 1,393 ms inside it under external load (BACKLOG T003),
twenty thousand parameters 286 ms (log .build/fn001-task4-fix-verify.log).Branch fn001. Reach: Parse → Resolve → Check → Specialize (specialize,
specializeWith and specializationKeys called directly); the CLI stops
every function program at the temporary guard after Specialize.
foldCalls makes a bare
FunctionRef an edge with its instantiation and enters lambda bodies,
so references at any depth (match arms, lambdas, Apply, Pipe) are
judged and form components (design §6); Components needed no change.
Domain.IR.Internal gains TFun Ty Ty with hand-written Eq/Ord (one
spine loop, the derived order), spine (two loops), and FunctionRef,
CtorRef, Apply, Lambda (Array Param), Pipe. The new
Features.Specialize.Intern hash-conses arrows: a key part is
KInt | KBool | KData n | KFun n, an arrow is numbered by its
(parameter, result) numbers, and Lower interns a spine’s suffixes from
its final result outwards while lowering it (parameters by a balanced
traverse, the interning by Array.foldr), so every suffix is numbered
once and no key is found by walking or spelling a type. Lower returns a
Lowered = { ty, key }; Keys’ Key is Tuple Int (Array Interned),
Work.arguments are Lowered, the state holds the arrow table; arrow
fields are lowered beside their written spine (as Check.Nested pairs
them). Body copies the new nodes through the new Specialize.Values
(callee for calls and bare references, ownerCtor: a partial
construction or constructor value names its owner’s copy, the final
result of its arrow, lambda). specializationKeys rebuilds arrow
arguments by a loop. The new Features.Specialize.Unlowered reject is
called by Program.Compile after specialize. Format.Go: goType
spells TFun inline as func(A) func(B) R along the spine; the field
comparison of an arrow field emits malformed; Usage walks the new
nodes; Expression lowers them to func() T { panic("bumpus: unlowered
function") }(). All unreachable behind the guard; Task 6 replaces them.Proxy(Int ->
Int)) is neither, and a generic function never instantiated is not
emitted, so such programs reach Go (worded so by the review, M1). (2)
Constructors, called or bare, are no edges: they have no body, so they
are in no component with a function. (3) A bare reference keeps the
existing text Recursive call to f changes its type arguments (no new
text is listed). (4) IR’s TFun carries no number: plan and design say
TFun Ty Ty. Task 6’s named Go types (design §13 rule 8) must reach the
numbers through Features.Specialize.Intern or carry them into the IR;
left to Task 6. (Superseded by the review fixes below: the numbers are
in the IR.) (5) The generator’s edge kinds come from a seeded stream
of their own, so P001’s components are unchanged. (6) Representative
independence is checked on the IR as a bisimulation: from both entries,
the bodies agree node for node ignoring types and spans, constructors by
name, and each pair of referenced functions is compared once.checked function programs reach
Specialize as Internal becomes ... stop at the unlowered guard; its
four rows keep code (E_INTERNAL), span (each node’s own) and text
(unlowered function), now reported by the guard; three rows added
(partial call at add(1), arrow field at T(Int -> Int), arrow
parameter at the declaration). The spec-key regression row’s needle
is now key = Tuple declaration (map keyOf arguments) and its mutant
collapses a data argument to dataType 0; still one target, still
fails. No other existing assertion changed.walk, bare step joining a component, bare walk
inside a lambda, and wrapping an intra-component argument over the
generator’s value and lambda edges); the arrow type row and the two
accepted rows passed on arrival (the arrow row since Task 3’s Nested),
kept as characterization. test/fn-specialize.test.mjs (map(id) twice,
constructor values’ owners, the 5,000-parameter keys) 3 of 3 failed;
test/fn-representative.test.mjs failed; poly-properties’ §4.2 bound
failed (each unlowered function from Specialize); the guard row failed
on the arrow-field span.fn id[…] keys merged, the distinctness row
fails; no FunctionRef edge: 3 rejected rows and the wrapped generator
test fail; no lambda edge: 2 rows and the wrapped test fail; owner taken
as the arrow itself: the owner row fails; no guard: the CLI row fails;
a lambda parameter dropping its local when lowered at Bool: the IR
independence property fails, but only after its third wrapper (a
lambda never applied, so its unused parameters are bare holes) was
added: the first version, whose parameters were List(_), let it
survive. The §4.2 bound itself passes on both rule mutants: accepted
generated components use bare variables or ground types only, so it
cannot distinguish them; the wrapped-variant test does.twenty thousand
parameters 310 ms and three thousand distinct instantiations 747 ms
inside verify. Specializing the 5,000-parameter program takes 0.23 s
alone.rm -rf output && npm run verify exit 0, 483 tests (472 + 11),
zero failures/skips, twelve regression proofs; bootstrap snapshots
unchanged. Log .build/fn001-task5-verify.log.The user approved (a) fixing BACKLOG T003 (match-ladder timing margin: 1,480, 1,393 and 1,441 ms against 1,500 inside the last three verifies) on fn001 after the Task 5 review and before Task 6, by reducing the workload, not raising the bound; (b) durability additions: Task 8 Step 1b commits the Task 4 review’s linear-cost table as bounded tests (test/fn-linear.test.mjs), Task 8 Step 4 uses a committed scripts/fn-milestone.mjs for the 20,000 tier, and Task 9 Step 1b promotes four scratchpad mutants to regression rows (block-order, value-edge, functional-fixpoint, arrow-key; 22 proofs); (c) the design §9 documentation of over-application diagnostic changes (Task 4 review).
IR.Ty is TInt | TBool | TData TypeId | TFun
FunTypeId with derived constant-time Eq/Ord (the hand-written spine
instances are gone), and IR.Program carries funTypes ∷ Array {
parameter, result } in interned-number order, from Intern’s table
(numbers for lookup, entries by number). The review measured about
40,050,000 arrow nodes in the expression types of the 5,000-parameter
test’s IR, which any pass re-walking them in Format would visit. With
numbered types the Lowered pair is gone (a lowered type is its own key
part), so Keys’ Key is again Tuple Int (Array IR.Ty) and the
spec-key row is back to its P001 needle and mutant (one target; the
proof passes). IR.spine reads the table by two loops;
specializationKeys rebuilds arrows through it (tests only). A
construction’s or constructor value’s owner is now the owner type at
its instantiation (applied), not the end of its type’s spine. Format.Go
already emits the named types: type bumpusFun<N> func(P) R per entry,
after the data types, and goType (TFun n) is bumpusFun<N> (no
re-walking); a program without arrows emits none, so snapshots are
unchanged. Design §7 and plan Task 5 Interfaces and Task 6 (number
order, not first-use order) are amended as Task 5 review
clarifications. M1: plan Global Constraints and decision (1) above say
the guard judges the monomorphic IR; a phantom arrow argument compiles
and runs pins Proxy(Int -> Int) printing 0 and its unused
bumpusFun0 declaration (written after the code; not run against
5874482, where the guard already let it through by reading). M2:
plan Task 6 Step 3 replaces the two temporary catch-all arms. M3:
docs/architecture.md names Intern, Values, Unlowered and the numbered
TFun; the full FN001 update stays Task 9’s. M4: test/phases.mjs (and
support.mjs, poly-keys.mjs and the two new test files) build failure
messages only on failure; checkedPoly succeeds on a 20,000-parameter
function value failed first with a RangeError from JSON.stringify
(.build/fn001-task5-fix-red.log; a plain 20,000-parameter program did
not reproduce it, since only a value reference puts the long arrow in
the checked IR). Design §9 notes that the Recursive call text covers
value references. New test arrow types in the IR are interned table
entries (different ids for the two 5,000-parameter types, equal ids
for equal types, each spine read through the table, at most 2 × 5,000
entries); a parameter-blind interning mutant (scratchpad) fails it and
the distinctness row. Timing of the 5,000-parameter specialization
(specializationKeys, alone): 0.23 s before, 0.26 s after (three runs
each). test/specialize.test.mjs now checks that a monomorphic program’s
funTypes is empty before comparing the rest; the first verify of
these fixes failed it on the new field, and the match ladder at
1,732 ms (BACKLOG T003). GREEN on the next run: rm -rf output && npm
run verify exit 0, 486 tests (483 + 3), zero failures/skips, twelve
regression proofs, snapshots unchanged; the ladder 1,497 ms, 3 ms
under its bound (T003); twenty thousand parameters 241 ms (logs
.build/fn001-task5-fix-verify.log, .build/fn001-task5-fix-verify2.log).Goal (approved by the user before FN001 Task 6): make the 127-deep, 50-arm ladder in test/match-lift.test.mjs substantially cheaper so its 1.5 s bound has headroom inside verify’s parallel run; the bound, the workload and the test’s place in verify are unchanged. Acceptance was a 2× drop in the isolated median, or, if that needs a design change, the profile and the options.
Profile (c120d48, built in an isolated copy). The measurement is
committed as scripts/ladder-profile.mjs (node scripts/ladder-profile.mjs
--rounds 7 --baseline DIR: cold compiles in fresh processes alternating
with a built baseline tree, steady per-phase medians, load average); the
CPU and allocation profiles below used node --cpu-prof and V8
allocation sampling on the same source. Steady state per phase (median of 15,
in-process): parse
137 ms (lex 55), resolve 22, check 134 (infer 61, coverage 36,
firstTooDeep 9, holes 8, instantiation rule 9, retype 7, comparable 5),
specialize 24, Go emission 46. One cold compile (fresh process, the
test file’s small program first, as in the test) was ~500 ms: about
1.4× the steady sum, from JIT warm-up and GC. The compile allocates about
2.8 GB for the 150 KB source (--trace-gc: 149 scavenges, ~110 ms of
GC). Hot spots: Format.Lex (per-character tailRecM steps over a copied
character array, Array.elem, a fresh slice per two-character-token
probe); isName/decimalToken copying each token into a character
array; Format.Go.Capture’s union (concat, sort, then Array.nubBy,
which sorts again) at 26% of emission; coverage’s per-arm redundancy
query re-simplifying every earlier arm and uncons-copying every row a
specialization drops (quadratic in arms by design, so the constant
matters); tooDeep/walk entering their loops for Int, Bool and rigid
types; retype/holes rebuilding a body that has no bound meta or
hole. Everything else is a flat spread of combinator, Either and record
overhead (no single function above 4% after these fixes).
Changes (each keeps its semantics; no public formula is new):
String.charAt/slice),
skips a whitespace run in one step, tries two-character tokens only
after their first characters, and tests punctuation and reserved
words with case instead of Array.elem; isName and Literal’s
decimalToken count in place.upcoming, the next token’s text, set
once per token consumed, so peekText is a field read.useful takes simplified
rows; Matrix’s specialize/defaults judge a row by Array.head
before copying its rest.tooDeep answers Int, Bool and rigid variables without converting;
Unify’s walk enters its loop only for a meta.retype (resolved subst) when the substitution is empty,
and holes keeps a body with no hole.union/armFree keep the first of each id in one fold over
the sorted array instead of Array.nubBy.
Tried and reverted: a loop in place of Array.find in Grammar’s
dispatch (no measurable gain, and Grammar would pass 250 lines); a
tailRecM threadAll in place of the Thread applicative (no measurable
gain; needed a new Foldable instance).After (steady, median of 15, load average 9-11, alternating with the
baseline): parse 124 → 92, resolve 19 → 19, check 125 → 78 (infer
53 → 35, coverage 31 → 26), specialize 21 → 21, emission 43 → 27;
sum 332 → 237. Allocation (V8 total_allocated_bytes, per phase
summed) 3.3 → 2.4 GB. Isolated cold compile, nine alternating rounds
at load average 5.7 (18:29): baseline 543 504 507 517 502 495 514 512
495, median 507 ms; T003 379 368 363 385 379 376 370 362 366, median
370 ms: 1.37×, short of the 2× target. Cold phases: parse 176 → 125,
check 193 → 131, emission 61 → 42.
Verify (three consecutive npm run verify, every run reported): all
exit 0, 486 tests, zero failures, twelve regression proofs, snapshots
byte-identical. Ladder: 942 ms (load 8.9 after), 968 ms (14.7), 852 ms
(12.0); logs .build/t003-verify{1,2,3}.log. Before T003 the same test
took 1,345-1,732 ms inside verify. Two direct node --test runs during
the work, while other projects’ builds pushed the load average to
16-30, failed it at 1,659 ms and 2,573 ms (.build/t003-tests1.log,
.build/t003-tests2.log); the second also failed coverage-scale’s
5,000-field pattern (5.1 s) and fn-specialize’s 5,000-parameter keys
(2.7 s). Both pass alone (0.80 s and 3.2 s wall under load 27; baseline
0.70 s and 2.7 s in the same minute) and neither path’s timing changed
in kind; these are T003’s external-load symptom in other timing tests.
Why not 2×: what remains is spread over the combinator parser (each argument passes about a dozen Either-threaded combinator layers), monadic threading in Infer, Resolve and Specialize, coverage’s quadratic redundancy query, and GC. Options that would reach 2×, each a design change: (1) a direct token-level parser for expressions in place of the layered combinators (parse is now 35% of the cold compile); (2) redundancy for literal-only columns without one usefulness query per earlier arm; (3) fusing the checker’s remaining whole-body passes (firstTooDeep, comparable, coverage roots, instantiation’s two call folds) into one; (4) an allocation-light State/Parsed representation. BACKLOG T003 stays Open with these.
Re-run with the committed script (18:37, load average 7.03 before, 6.40
after; 7 alternating rounds against the c120d48 copy): cold baseline
513 539 504 494 520 500 501, median 504 ms; current 396 373 392 382 373
368 368, median 373 ms. Steady medians baseline → current: lex 61 → 31,
parse 136 → 96, resolve 21 → 21, check 136 → 86, specialize 22 → 21,
emit 46 → 26 ms. npm run verify with the script committed: exit 0,
486 tests, twelve regression proofs, ladder 748 ms (load average 6.05
before, 19.82 after; .build/t003-script-verify.log).
Review follow-up (2026-10-08): the independent review approved 7c6c0f5
(50,453 differential cases, 0 differences). Minors fixed: Usefulness’s
comment (once per arm, against all earlier arms); Lex spaceEnd reads
each character once (spaceStep); Check’s settle uses a new
Unify.isEmpty (test isEmpty holds exactly until a meta is bound)
instead of Subst(..); Coverage’s import order. The reviewer’s harness
is now scripts/differential.mjs (+ differential-corpus.mjs,
differential-hook.mjs; docs/engineering.md). Against the c120d48 copy,
seed 7, count 1000, harvest regenerated (3,419 parses → 2,355 distinct
sources; load average 11.6 at 18:49): harvested 2,355, fuzz 1,029 plus
1,085 isName probes, matches 1,000, mutations 1,000, 0 differences
(.build/t003-differential.log). Against a copy whose lexer no longer
treats tab as whitespace: 148 of 329 fuzz sources differ at lex, exit 1
(.build/t003-differential-broken.log). npm run verify: exit 0, 487
tests, twelve regression proofs, ladder 811 ms (load average 6.95
before, 12.03 after; .build/t003-review-verify.log).
The user chose to accept the 1.37× isolated gain (7c6c0f5; script scripts/ladder-profile.mjs, 6d0906c) and continue to FN001 Task 6. T003 stays Open with its five recorded design options; ladder in verify now 748-968 ms against 1,500. Task 6 starts after the T003 review.
Branch fn001. Reach: the whole CLI; programs with function types, lambdas, function values, partial and over-application and pipes now compile, build and run. Design §13 rules 1-8 as adopted.
Scope carries the
program’s Shape (signatures and constructors as Wrappers, the
funTypes table, and a (parameter, result) → number map); Lowered
gains wrappers, the staged wrappers its values need. New modules:
Format.Go.Value (a saturated call or construction is today’s n-ary call,
rule 1; a partial one is {f}Value(a1)…(aj); a bare reference of arity
1 is the Go function, of arity n ≥ 2 {f}Value; any other application
and over-application apply the value), .Apply (rule 6: inline up to 64
arguments, else bumpusFn{f}Apply{k} helpers of at most 64 stages
taking the value so far and their arguments’ free locals, each argument
evaluated in place), .Lambda (rule 5: bumpusFn{f}Lambda{k} of its free
locals, ascending, then its parameters, _ for a discard; the value is
the wrapper applied to the free locals through .Apply, or the function
itself with no free local and one parameter), .Pipe (rule 7), .Stage
(rules 2-3: one bumpusNode{N} per distinct argument type of all
wrappers, {f}Value, {f}Stage{k}) and .Entry (rule 4: package-level
{f}Kinds/{f}Slots, one array per distinct type, one loop, one n-ary
call). Capture gains lambdaFree (a lambda’s named parameters are bound
in its body; a set, so a wide lambda is not quadratic) and without.
Expression dispatches every node by an explicit arm (the temporary
wildcard is gone); Compare’s TFun arm and a new explicit Show TFun
arm emit malformed for a function field (never compared or printed,
design §3, but every declared type gets both helpers). Emit order: node
types after the function types, wrappers after the functions. A program
with no function value emits no node, stage, entry or table: the four
bootstrap snapshots are byte-identical (compiler, shapes, tree and
lists tests). Usage and Layout needed no change.{f}Arrow{k} type; every other stage uses the
interned bumpusFun{N}, found by number, so uses and the wrapper agree.
(2) The pipe’s temporary is the parameter bumpusPipe of a lifted
bumpusFn{f}Pipe{k} (free locals, then the operand), not an
immediately invoked closure, so pipe chains nest no closure; a literal
or local left operand is spliced in as the last argument. In both
cases a right side g(b1…bk) with k < n becomes g(b1…bk, v), so
xs |> take(3) is a direct call. (3) Matches, lambdas, pipes and
helpers share one pre-order counter per function. (4) Entries’ tables
are int32, not uint16 (no 65,536-type ceiling). (5) Helper bodies
are one chained expression, not one statement per stage as in Task 1’s
prototype. (6) Wrappers are emitted once each by name, in first-request
order; node types are numbered in first-appearance order. (7)
test/fn-lambdas.mjs now holds Task 5’s lambda wrappers, shared by
test/fn-representative.test.mjs and test/fn-run.test.mjs (moved, not
changed).checked
function programs stop at the unlowered guard), each E_INTERNAL
unlowered function at the span shown, now compiling and printing:
(fn(x) => x)(1) (1), 1 |> add(2) (3), id(add)(1, 2) (3),
add(1)(2) (3), match add(1) { _ => 0 } at add(1) (0), type T =
T(Int -> Int); fn main(): Int = 0; at T(Int -> Int) (0), fn f(g:
Int -> Int): Int = 0; fn main(): Int = 0; at the declaration (0).
test/specialize.test.mjs’s monomorphic-identity set excludes the new
polymorphic examples/functions.bumpus, as it excludes lists.bumpus (its
first verify run failed on the new example’s arrow table,
.build/fn001-task6-verify1.log). No other existing assertion changed.unlowered
function): test/fn-timing.test.mjs, 6 of 6
(.build/fn001-task6-red-timing.log): named value use(stuck), lambda
use(fn(x) => stuck(x)), partial strictness ignore(k3(probe(1))),
pipe order probe1(1) |> g(probe2(2)), helper order (a 65-argument
application of w, whose body is entered at its second stage, before a
third argument’s probe) and sharing (twice(k3(probe(10))) prints 30
with probe entered once in the trace); test/fn-run.test.mjs, 22 of 22
(.build/fn001-task6-red-run.log); test/fn-scale.test.mjs, 1 of 1
(.build/fn001-task6-red-scale.log). After implementing, two structural
rows in fn-run failed because the test assumed declaration-order ids
(a generic length is numbered after the monomorphic functions); the
ids in the tests were corrected, not the assertions.func(x) … { return
func(y) … { return F(x)(y) } }): named value fails (exit 0); a
hoisting application helper (each block’s arguments bound before its
stages): helper order fails (probe2 entered first); lazy partial
arguments (a partial call as nested closures around the saturated call):
partial strictness fails (exit 0) and sharing fails (trace
[3 2 0 1 0 1], probe twice); a rewritten pipe (left operand spliced in
place, no temporary): pipe order fails (probe2 first). Every other
probe passed under each mutant.go build
4.0 s against the 10 s bound (2.8 s for the same program with the
direct call only); the test logs its build time. A first body with
if terms (3,333 immediately invoked closures) built in 5.8 s (4.5 s
direct only); replaced before any verify.forms and test/depth.test.mjs. Re-measured in an isolated copy with
the limit lifted, every form, and the old forms also on an isolated
build of 97740c6: FN001 forms 1,507 / 812 / 607 / 607 / 343; old forms
unchanged by the lowering (within 5); minimum 276
(constructor-argument; 274 at 97740c6), limit 128 unchanged, no
anomalies. The drops since ADR 006’s G001 table (e.g. plus-chain
2,928 → 1,564, constructor-argument 304 → 274) predate Task 6.rm -rf output && npm run verify exit 0, 526 tests (487 at 97740c6,
minus the guard test with its 7 rows, plus 6 timing probes, 22 run
tests, 1 scale, 1 snapshot and 10 CLI depth tests), zero failures or skips,
twelve
regression proofs, the four bootstrap snapshots unchanged; the match
ladder 542 ms; twenty thousand parameters 220 ms; the 5,000-parameter
go build 6,799 ms against its 10 s bound (4.0 s alone; BACKLOG T003
records the margin); load average 1.79 before, 6.71 after
(.build/fn001-task6-verify3.log; earlier green run
.build/fn001-task6-verify2.log). Usage and Layout unchanged.bare <$> named inner map layer, P001 for plus-chain). Fix: named
builds its Primary itself (Format.Parse.Expression; productions stay
applicative). Isolated copies, limit lifted, two alternating rounds:
constructor-argument 275 → 283, call-argument 315 → 326, parens 522 →
522; minimum 283. Differential, d2cf616 plus only the parser change
against d2cf616 (seed 6, count 10,000): 32,403 sources and 10,085
probes, 0 differences (.build/fn001-task6-fix-differential-parser.log).
BACKLOG G002 tracks recovering Task 3’s ~24 levels. Minors: Stage no
longer declares {f}Arrow1, which no stage names (only a helper’s
signature can; it now spells func(A1) <next>); bootstrap/functions.go
loses its one unused bumpusFn6Lambda0Arrow1 line. The full
differential against d2cf616 shows that change alone: harvested 235 of
2,374 sources differ, all at emit, and a scratch check finds every
candidate emission equal to the baseline’s with the Arrow1
declarations removed and their uses spelled out (1,287 compiled, 0
unexplained); fuzz and matches 0 differences; mutations 4 (seed 1) and
32 (seed 6), the shown ones all at emit (.build/fn001-task6-fix-
differential{,-10k}.log). The --regenerate harvest’s own test run
exited 1; differential.mjs discards its output, so the failing test is
unknown (it ran the full suite in parallel under the hook). Apply’s
blocks push their lifted functions, frees and wrappers onto a stack and
concatenate once (was O(blocks × lifted)). test/fn-specialize’s stale
guard comment and support.mjs’s goTest =( spacing are fixed.go build 10584 ms, bound 10000, at
load average 18.3 with only verify’s own runner. The same mixed-body
program builds in 1.8 s at 2,500 parameters, 4.4 s at 5,000 (direct
call only 3.0 s), 17.9 s at 10,000 (direct only 11.2 s); at 20,000 the
Go compiler was killed at 7.4 GB RSS, with the direct call only too.
The cause is the n-ary body’s 2n-term bumpusAdd tree in one Go
expression (the existing lowering of +), not Task 6’s staging.
Recorded in BACKLOG T003.rm -rf output && npm run verify exit 0,
526 tests, zero failures or skips, twelve regression proofs; the
5,000-parameter go build 7,467 ms against 10 s; the match ladder
1,030 ms; load average 3.15 before, 5.34 after
(.build/fn001-task6-fix-verify.log). Run once.During the Task 6 review fixes, scripts/differential.mjs --regenerate
ran the full test suite under its harvest hook and that run exited 1;
the tool discarded the run’s output, so the failing test is unknown
(likeliest a load-sensitive timing test, BACKLOG T003, unconfirmed). The
tool now writes the run’s output to <harvest>.jsonl.log and prints the
failing test names. One rerun afterwards (load 3.3 before, 9.1 after)
exited 0: 2,374 harvested sources, 0 differences against itself.
User decisions (2026-10-09): (A) the 5,000-parameter go build test
failed once at 10,584 ms against 10 s under verify’s own parallel load
(4.0-4.4 s alone); bound and workload stay, and go build-timing scale
tests now run in a separate serial phase: scripts/verify.mjs runs
test/*.serial.test.mjs after the parallel node --test run with
--test-concurrency=1, still discovered by pattern (docs/engineering.md).
(B) At 20,000 parameters the mixed body’s 2n-term bumpusAdd tree in one
Go expression kills the Go compiler (7.4 GB) even for direct calls only,
pre-existing lowering, not FN001 staging (BACKLOG G003, new). The
milestone therefore tests FN001’s machinery at full width with bodies
that read six parameters (first and last three, mixed kinds).
go build timer moved out of fn-scale),
test/fn-linear-forms.mjs, test/fn-linear.test.mjs (20,000, parallel),
test/fn-linear.serial.test.mjs (80,000, serial, about 11.5 s),
scripts/fn-milestone.mjs, scripts/verify.mjs.go build bound 10 s):
5,000-parameter mixed body, direct and value, phases 1,240-1,260 ms,
build 4.0-4.3 s; f(1)(2)…(1000) 74-85 ms, 0.40-0.45 s; a written
Int -> … -> Int of 1,000 parameters 65-77 ms, 0.41-0.44 s; a
5,000-parameter function type through id and Box(a), whole and as
a half partial, 529-581 ms, 2.4-2.5 s. The shared partial (completed
twice) is in the milestone’s wide program; the 5,000-parameter
mixed-body test keeps its workload, so its partial is covered at
5,000 by the generic program’s partial instead. The existing
20,000-parameter and 4,000-Nil tests are unchanged.f(1) (E_TYPE) 175/525, f(1) 310/930,
f(1…n-1) 225/675, (f)(1…n) 370/1,110, pipe 250/750. At 80,000 all
eight finish with the expected outcome, about four times as long.spineThrough as recursion with Array.cons):
6 of 8 forms at 20,000 fail with RangeError (stack); spelling-based
arrow keys (Specialize.Intern keyed by the expanded spelling of each
suffix): 5 of 8 fail with RangeError; derived Eq/Ord on Domain.Type
Ty restored: survives all Task 8 tests (fn-linear at 20,000 and
80,000, fn-scale.serial, large-source, fn-specialize, fn-check), so
no Task 8 form compares long source arrows with Eq/Ord (the
specializer compares interned IR numbers). Not pursued further (user
limited Step 2 to three mutants); open for Task 9’s review. Nested
closures were not rerun.node scripts/fn-milestone.mjs, 20,000
parameters, CLI emit then go build with a 100 s timeout; load 3.1
before, 5.4 after; .build/fn001-task8-milestone.log), all ok: wide
(direct, value, shared partial) emit 2.4 s, build 13.7 s; chain 0.7,
7.6; curried 0.8, 7.5; generic 2.0, 12.1; value 0.7, 6.8; over 0.8,
7.0; lambda 0.8, 6.6; f(1) 0.7, 6.1; f(1…n-1) 0.5, 7.0;
(f)(1…n) 0.7, 6.8; pipe 0.4, 0.3 (the pipe is a direct call: no
staged wrapper is emitted).rm -rf output && npm run verify exit 0 in 1:53 (load average
3.69 before, 4.97 after; .build/fn001-task8-verify.log): parallel phase
533 tests, serial phase 12, zero failures or skips; serial
5,000-parameter build 4,733 ms; match ladder 735 ms; twenty thousand
parameters 221 ms._ parameters), staged application one argument at a time
(a body runs exactly when its stage boundary completes), strict
partial application, over-application, |> with the left operand
first, then the callee and its explicit arguments, and an enters
trace (declaration positions) with optional probes that stop the run
on entry. test/poly-parse.mjs now records constructor field counts.30 [3 0 2 1 1]); the
generated property failed._, capturing match binders), partial and
over-application of functions, constructors and lambdas, constructor
values (flip(Cons), Pair(n)(b), compose(adder, f)) and pipes.
test/fn-oracle.test.mjs runs them in one Go batch against the
interpreter (failures print seed and source) and checks every form
occurs. All 30 agree; no compiler bug found.interpreter agrees: pipe order (and Task 6’s
Go probe). The generated comparison does not see it: generated
programs terminate and have no effects, so pipe order is not
observable in their values; timing stays the probes’ job.rm -rf output && npm run verify exit 0 (load average 3.01 before,
5.77 after; .build/fn001-task7-verify.log): parallel phase 541 tests,
serial phase 12, zero failures
or skips.printed now runs through a shared ran):
stage-value (Format.Go.Expression: a bare arity-1 function whose type
has two or more parameters lowered as a two-level eta adapter; probe
use(stuck) must panic in stuck), stage-lambda (Format.Go.Lambda:
the same adapter around a one-parameter lambda’s lifted function),
partial-strict (Format.Go.Value: a partial application wrapped in a
closure awaiting its next argument, so its arguments run late),
pipe-order (Format.Go.Pipe: every pipe spliced as the call),
lambda-capture (Format.Go.Capture lambdaFree keeps parameters),
fun-compare (Check.Comparable drops the function test),
block-order (Format.Go.Apply: a helper binds its block’s arguments to
variables before applying), value-edge (Check.Instantiation: bare
references and lambda bodies are no edges; mutant outcome More than
10000 specializations), functional-fixpoint (Check.Functional
returns its seeds), arrow-key (Specialize.Intern keys an arrow by
TInt and its result). Timing probes use test/support.mjs
panicOnEntry. Deviation: as built there is no uncurried value
representation, so stage-value/stage-lambda restore the defect’s
effect (eta expansion to the type’s arity) instead. The lowering
mutants’ Go builds, so those probes fail on behavior (0 printed, or
the wrong probe label); lambda-capture and arrow-key fail at Go
build, inherent to those defects. node scripts/regression.mjs
name… now proves only the named rows.Eq/Ord on Domain.Type Ty): rebuilt in
a scratchpad copy with both instances derived; test/type-order.test.mjs
(Task 2) fails 2 of 5 (RangeError on 20,000-long spines), so verify
already catches it; Task 8 had run five other files only. No pipeline
phase compares long source arrows with Eq/Ord: the same mutant
compiles a program unifying two 20,000-parameter written arrows with a
20,000-parameter function (and same(h, h)) correctly. No new test
added, since no pipeline test can observe that defect.wrapperCode emitting the
wrapper as nested Go closures, no nodes or entry) against
test/fn-scale.serial.test.mjs, once (load 2.5 before, 2.9 after): the
5,000-parameter value and the 5,000-parameter generic function type
fail, go build killed at the 120 s timeout (bound 10 s); the
1,000-partial chain (9,315 ms) and the 1,000-long arrow (6,325 ms)
pass, against 542 and 575 ms healthy in the verify below.rm -rf output && npm run verify exit 0 in 150 s (load average 2.39
before, 8.44 after; .build/fn001-task9-verify.log): parallel phase 541
tests, serial phase 12, zero failures or skips; twenty-two Regression
proof (…) lines; serial 5,000-parameter build 4,423 ms; match ladder
640 ms.Fresh reviewer, whole diff 2a61a2b..e2a6735: ready to merge, no Critical or Important findings. Verify in an isolated copy: exit 0, 541 + 12 tests, 22 proofs. Six adversarial cross-task programs matched the interpreter including entry order; 2,300 extra generator seeds matched; ~46,000 sources against a main build: 0 Go differences among sources main accepts, none newly rejected, every non-syntax diagnostic change a design §9 row. Minors fixed: language.md hint condition (M1), generator label (M3), ADR 008 note on syntax diagnostics of invalid sources (M6), BACKLOG T005 for the unidentified harvest failure (M5; renumbered at the merge because main had added its own T004); deferred to BACKLOG FN006: interpreter zero-argument calls (M2), splitting the value-edge row (M4), fn-linear bounds in the parallel phase (M7). Awaiting the user’s merge decision.
At the user’s request, language-direction records proposed do notation
as an expression elaborated into bind and lambdas, with an illustrative
block and its expansion. Result binding, discarded results, final
computation and the distinction between constructing and running deferred
effects are explained. Service/error inference is a goal; bind/pure
selection, binding rules and monadic versus algebraic effects stay open.
This is direction only, with no compiler change or implementation
authorization. Next-session and the FX001 backlog row point to the proposal.
Verification: npm run verify exited 0 with 318 tests, no failures or
skips, and all twelve regression proofs; evidence:
.build/fx001-do-docs-verify.log. The proposed notation is not implemented
or behaviorally verified by this suite.
The user asked to preserve the algebraic-effect/open-row discussion and
defer explicit effect-row constraint syntax to a future feature. The
language-direction document now records direct-style handler evaluation,
candidate Log + ...effects notation, inferred higher-order effect
relationships and proposed implicit universal row quantification. It
distinguishes record rows and R001’s lacks constraints from effect rows.
Outer syntax, quantifier scope and handler semantics remain design work;
no compiler change or implementation authorization. BACKLOG and the
next-session handoff record the future constraint-syntax scope.
Verification: npm run verify exited 0 with 318 tests, no failures or
skips, and twelve regression proofs; evidence:
.build/fx001-row-direction-verify.log. This verifies the existing
compiler, not the proposed effects or notation.
The user’s error-widening question revealed that earlier direction only
mentioned extensible errors generically. Read-only inspection of MileAhead
(../trailmapper, as recorded in provenance) confirmed open error-family
rows, caller composition without conversion wrappers, context-only
wrapping, and closure at handling/test boundaries. Language-direction now
records this brief and independent Bumpus syntax candidates hiding Variant
and injection plumbing. FX001/R001 must resolve error-sum support against
the current general-variant deferral; no feature scope or code changed.
Provenance, BACKLOG and the next-session handoff track the design work.
Clarified after the user’s follow-up: errors is an ordinary implicitly
universally quantified row-variable name, not reserved or existential;
only the spread punctuation is special syntax in the proposal.
Verification: npm run verify exited 0 with 318 tests, no failures or
skips, and twelve regression proofs; evidence:
.build/fx001-error-rows-verify.log. Proposed error sums remain unimplemented.
The user requested planning for common PureScript/Haskell FP helpers with
approachable Rust-informed names. Added STD001 and a dedicated direction
document covering optional/result eliminators, defaults, transformations,
functions, sequences, text, maps/sets and generic collection operations.
Candidate names distinguish getOrElse from full branch elimination and
make eager/lazy evaluation explicit. Delivery follows FN001 and stages
other APIs by their prerequisites; no compiler or helper implementation.
Acceptance requires an upstream coverage matrix, readable examples,
behavioral/law tests and explicit deferrals. Handoff and language-direction
link the brief; exact public names still need reviewed API designs.
Verification: npm run verify exited 0 with 318 tests, no failures or
skips, and twelve regression proofs; evidence:
.build/std001-direction-verify.log. Helpers are planned, not implemented.
The user clarified that approachable naming must preserve theoretical
rigor and HKT-based shared operations. STD001 now pins generic bimap over
any lawful Bifunctor, shared Functor/Applicative/Monad contracts, required
constructor kinds and partial type application, and coherent instance
evidence under the current rank-1/specialization constraints. Concrete
helpers are incremental delivery, not completion of the generic library.
Acceptance includes generic clients, user-defined product/sum instances,
algebraic law properties and cross-operation coherence; pure callback laws
remain distinct from FX001 effectful sequencing. BACKLOG and handoff track
the clarification. No language or library implementation changed.
Verification: npm run verify exited 0 with 318 tests, no failures or
skips, and twelve regression proofs; evidence:
.build/std001-generic-rigor-verify.log. Generic APIs and laws above are
future acceptance requirements, not implemented or proven by this run.
The user asked for a structured audit of leading language specifications
and library/runtime designs. Existing STD001 only audited helper coverage;
there was no broader systematic plan. Added LA001 covering all twelve
named references, a versioned primary-source/section ledger, comparative
briefs, decision and interaction matrices, practical application scenarios,
independent review and durable scope decisions. Primary landing pages were
checked to assemble the roster; detailed reading/audit is not complete.
Relevant findings feed upcoming designs without expanding FN001 or revoking
existing advanced-feature deferrals. BACKLOG and handoff track the plan.
Verification: npm run verify exited 1 with 316 of 318 tests passing,
no skips, and two large-source timing failures (6.572 s and 5.633 s against
5 s bounds). Regression proofs were not reached. Evidence is
.build/la001-plan-verify.log; BACKLOG T004 records the cases, unconfirmed
cause and diagnostic next action. No checks or assertions were changed,
and no rerun concealed the failures. The audit itself remains planned.
The user requested highlighting/IDE support, first-class doctests,
property-based testing, possible literate capabilities, and LLM
skills/discoverability. Added distinct IDE001, DOC001, PBT001, LIT001 and
AI001 entries with docs/plans/2026-10-08-tooling-direction.md. The brief
records outcomes, proposed acceptance and dependencies without selecting
syntax, tool protocols or implementations; literate capabilities remain
exploratory. LA001 now includes these concerns in its comparison axes and
application scenarios. Updated language direction and next-session handoff.
No language/compiler code changed and FN001’s delivery is unchanged.
Verification: npm run verify exited 0 with 318 tests passing, no failures
or skips, and all twelve isolated regression proofs passing; evidence:
.build/tooling-direction-verify.log. This checks the existing compiler,
not the proposed tooling. T004’s earlier timing failures remain recorded
and unresolved; this successful run does not establish their cause.
The user requested distinguishing Bumpus libraries compiled to the host
target, FFI bindings and dependency-inverted services implemented through
host-language libraries, then added package management. Recorded PKG001
in the tooling brief and backlog; updated LA001, language direction and
handoff. Planning includes source packages/host-callable export assessment,
service contract/provider substitution, combined host/Bumpus dependency
resolution, manifests/locks, identity, compatibility and target/runtime
availability. Effect Platform’s primary introduction was checked as a
conceptual precedent; its mechanisms are not adopted. No compiler code,
package manager, binary ABI or provider mechanism is implemented/selected,
and FN001 scope is unchanged.
Verification: npm run verify exited 0 with 318 tests passing, no failures
or skips, and twelve isolated regression proofs passing; evidence:
.build/pkg001-direction-verify.log. This validates existing behavior,
not the proposed package/interop features. T003/T004 remain open.
User approved merge and push. main had gained eight docs-only planning
commits (2190c8f..340953a: FX001 do notation, error rows, STD001, LA001,
tooling, PKG001); they were merged into fn001 (9527233). Conflicts: the
T003 row (fn001’s extends main’s; kept fn001’s) and two different T004
items (main’s large-source timing item kept as T004; fn001’s unidentified
harvest failure renumbered T005); progress kept both sides. Clean rm -rf
output && npm run verify on the merged tree: exit 0, 541 + 12 tests,
twenty-two proofs (.build/fn001-merge-verify.log). main fast-forwarded to
fn001 and pushed to origin; worktree and branch removed.
Branch fx001. No compiler change. Added scripts/effect-probe.mjs (driver:
explicit build 300 s / run 120 s timeouts, five repetitions, checksums
verified, load averages before and after each batch),
scripts/effect-runtime.mjs (Go prelude: bumpusCtx list, non-zero-sized
bumpusMarker, bumpusAbort, lifted bumpusHandle returning
(result, abort), bumpusCleanup, defect report and main wrapper),
scripts/effect-semantics.mjs (Step 2 programs and independently written
expected output) and scripts/effect-programs.mjs (Step 3 loops and the
generated lifted-helper program). Semantic checks: all 7 modes pass (clause
reaches the outer handler; aborts cross unrelated and same-key inner
handles to their target; clause failures after the helper returns reach
the outer handle; nested blocks unwind LIFO; cleanup that handles a
failure internally lets the abort continue; two cleanup failures print the
three confirmed report lines in order, exit 1; normal-exit cleanup
failures; crash not caught by handle with cleanup run; guard line; 1,000
markers distinct). A first run of abort failed on my own expected output
(two nested blocks give d2 d1 d2 d1, I had written one pair); corrected
the expectation, not the program. Two mutants (handle catching every
abort; clause run in its own context) each fail their check (scratchpad,
not committed). A second full run reused Go’s build cache (identical seed
per repetition; 0.2-0.4 s builds) and was discarded; the seed now varies
per run. Measurements (.build/fx001-task1/run3.log): median go build 0.83 s
/ 1.53 s / 2.91 s at 1,000 / 2,000 / 4,000 helpers (bound 10 s). Decision:
ADOPT the shapes. Details in findings “FX001 Task 1”.
Branch fx001. No existing test assertion changes; the five bootstrap snapshots are byte-identical (verify’s snapshot tests).
effect handler handle with let defer
Unit ctl resume at once (controller ruling; pure stays a name).
Domain.Syntax gains UnitRef, UnitValue, Block and Item (Let,
Discard); the new Format.Parse.Block parses {} (Unit), items
separated by ; (a trailing ; makes the value UnitValue at the
closing }, which marks the block trailing; { let x = 1 } is E_SYNTAX
Expected ; at }) and (); { joins the loosest expression dispatch,
every item nested (128 nested blocks parse, 129 are E_NESTING). Domain.Type
and the IR gain TUnit (Go struct{}, value struct{}{}); Resolved,
Checked and IR gain UnitValue, Block and Item. Features.Resolve.Block
threads the scope through the items (new Fresh thread, a balanced
traverse); a let is in scope for later items only, shadowing as a match
binder, its LocalId taken before its expression’s binders. The resolver’s
scope and the checker’s locals became maps (by name, by LocalId) so a
20,000-let block costs no scan per lookup. Features.Check.Block: let is
monomorphic, discards may have any type, the block’s type is its value’s.
Check.Hint adds TrailingSemicolon (rendered ; remove the trailing ;?)
on a mismatch whose found expression is a trailing block. Unit is
comparable (always equal; ordering lowered through an inline
func(struct{}, struct{}) int so both operands still run) and prints
(). Format.Go.Block lifts each block to bumpusFn{f}Block{k} (pre-order,
counter shared with Match/Lambda/Pipe/Apply), captures in LocalId order
(Capture withoutAll), each let a typed var plus _ = x (typed
because an arity-one function value is a lifted Go func of unnamed type;
the brief’s x := would leave it unnamed), _ = e for let _ and
discards, return the value. A Unit main emits func main() {
bumpusFn<n>() } and prints nothing; import "fmt" is emitted only when
main prints or a printer appends an Int/Bool field. The style gate’s
applicative-module list adds Format.Parse.Block. The interpreter
(test/poly-parse.mjs, test/poly-oracle.mjs) reads and runs blocks, let,
(), {} and f() calls.1 + { 2 } was already Expected an
expression at {, kept as a characterization row); log
.build/fx001-task2-red.log. One test-construction slip fixed after GREEN
(the () and f() rows pointed at the first occurrence, inside main()
and fn f()). The 20,000-let timing test moved to
test/fx-block.serial.test.mjs after the first verify: under the parallel
run it took 999 ms against its 1,500 ms bound (load about 5).{ 1; () } row); a let never entering scope (2 run rows); Unit
ordering returning 1 (the two boxed <=/> rows); Unit main printing
(both silent-entry rows).rm -rf output && npm run verify exit 0: 626 parallel tests and
13 serial (was 541 + 12; +85 and +1), zero failures/skips, twenty-two
regression proofs, load 3.6-6.1; the serial 20,000-let test 535 ms of
1,500 (log .build/fx001-task2-verify2.log; the first run, before the
serial split, .build/fx001-task2-verify.log, also exit 0).Branch fx001. Behavior-preserving: no syntax writes a row yet; every
existing TFun carries the closed empty row and every TData empty row
arguments. No existing assertion changes; the five bootstrap snapshots are
byte-identical (verify’s snapshot tests).
TypeId, moved and re-exported by
Domain.Type; EffectId) and Domain.Row (Row t v, Label t,
EffectRef, LabelKey, TypeHead, labelKey, closedRow, openRow,
isPure; parameterized over the argument type, so Domain.Type imports
it; Row hides Prim’s). Domain.Type: TFun (Ty v) (TyRow v) (Ty v),
TData TypeId (Array (Ty v)) (Array (TyRow v)), THandler (Label (Ty
v)) (TyRow v); the Monad/Bind instance is gone, replaced by sort-aware
substitute/substituteRow over Substitution v w = { types, rows }
(a row tail splices its image: labels appended, its tail the new tail);
Functor is substitute with TVar/openRow. Spines are walked as
arrow nodes (stagesThrough, foldStages: no record per arrow);
spine, arrows, ground, children, rowsOf, rowArguments and
typeHead moved to the new Domain.Type.Parts (250-line limit). The
checker’s substitution moved to Features.Check.Subst (Subst { types,
rows, fresh }: fresh row metas count down from -1, apart from the
checker’s, which count up; walkRow, resolveRow), binding and the
depth bound to Features.Check.Binding (bindType, extendRow,
bindTail, exceedsLimit counting label arguments one level deeper),
both re-exported by Features.Check.Unify, whose TFun, TData and
THandler cases unify rows through the new Features.Check.UnifyRow
(Leijen 2005 §7, a tailRec loop over labels, first occurrence by key,
the side condition tail(r1) ∉ dom(θ) in both the rewrite and the
left-over-entry cases; unifyRowsTraced also returns RowEvents by
OccurrenceId, Features.Check.Occurrence; Features.Check.RowSide holds
the operand walk). Failure gains RowMissing, RowExtra,
RowSharedTail and RowMismatch (tails alone differ: not in the brief,
needed for closed against rigid); Require reports each as the existing
whole-type E_TYPE mismatch. Specialize erases rows; THandler is an
Internal error in Lower and Require and abstract to coverage until Tasks
5 and 6. Scheme numbers row holes with type holes. Regression row
occurs now mutates Features.Check.Binding.Clock + ...r/Log + ...r and for a left-over
entry, both in a child process with a 20 s timeout (the generated test
then refuses to run if it failed), RowMissing/RowExtra/
RowMismatch, 1,500 generated pairs (959 unify; 54 RowSharedTail)
against the independent recursive oracle test/row-oracle.mjs (same
acceptance, symmetric acceptance, scoped-label equality, acyclicity,
equal normal forms up to renaming), 1,000-label rows, arrow rows in
unify (occurs and depth through label arguments), traced events, and
five substitution laws. Test support only changed elsewhere: value
construction gains row arguments, two renderers (test/fn-shape.mjs,
test/fn-checked.mjs) leave out pure rows, and imports follow the moved
names.findLastIndex): three tests fail.where binding resolving both
types on every expectType (the mismatch message) made the occurs
regression mutant overflow instead of reporting, and cost about 2x in
Require; it is a function now. A helper frame per applied level in
unify overflowed test/unify-arrow at 1,000 levels; the fold is back in
unifyHeads. compare on applications compares row arguments before
type arguments so the recursive comparison stays last.compare change) against an
isolated build of e110526 (ms, medians): value 415/403, over 605/534, lambda
542/539, mismatch
181/161, partialFirst 423/396, partialAll 300/289, parenthesized
397/400, pipe 289/309. Minimum --stack-size for test/poly-depth’s
1,000-deep check: 575 KB (was 559).rm -rf output && npm run verify exit 0: 640 parallel tests
and 13 serial (was 626 + 13; +14), zero failures/skips, twenty-two
regression proofs, load 2.0-5.9 (log .build/fx001-task3-verify2.log; an
earlier run, .build/fx001-task3-verify.log, failed only the occurs
proof above).User approved the Bumpus -> Waxwing / .wxw migration and subagent help.
Work uses the existing isolated FX001 tree at 3481468 (Tasks 1-3), with
main and its pre-existing editor setting untouched. Separate agents handled
current documentation and test identity edits; a fresh read-only reviewer
found only a stale Provisional name sentence, which was corrected.
CLI is scripts/waxwing.mjs, npm script waxwing; packages are waxwing, waxwing-style and waxwing-bootstrap. Renamed nine example/negative sources; updated Go names/panics, temporary prefixes, harvest instrumentation, current documentation, active FX001 documents, backlog and handoff. Historical plans/progress/findings, prior naming evidence and branding provenance remain. ADR 009 records the decision. No semantics, verbs, aliases, extension-based rejection or final artwork were added.
Evidence so far:
npm run verify exited 1: 638/640 parallel tests
passed; fn-linear lambda 1604.135291 ms > 1545 ms and mismatch
819.499333 ms > 525 ms. Serial tests/proofs not reached; FN006 owns the
recorded failure/next action. Log .build/waxwing-baseline-verify.log.npm run build then passed with zero
warnings/errors, .build/waxwing-build.log.npm run --silent waxwing -- run: outputs saved in
.build/waxwing-examples.json. Program/compiler/shell tests: 21 passed,
no failures/skips, .build/waxwing-cli-green.log.npm run verify exited 1: 639/640 parallel tests passed;
twenty thousand declarations compile and run took 5.106579416 s > 5 s.
Serial tests/proofs not reached. Log .build/waxwing-verify.log; T004
records the timing failure rather than concealing it with a rerun.No whole-suite success, merge, commit or push is claimed by this entry.
The user confirmed the other worker paused. Its eight changed source files remain byte-identical to .build/waxwing-paused-files.json; no worker edit was reverted or folded into the rename’s semantic claims.
npm run verify builds with zero warnings/errors,
passes strict-rebuild/structural gates and 634/640 parallel tests. Five
fn-linear bounds fail (exact timings in FN006); unify-row’s deferred-Fail
assertion expects RowMissing but receives Subst. R003 records the worker’s
remaining test/settlement next action, without changing its assertion.
Log .build/waxwing-paused-verify.log. Serial tests/proofs not reached.npm run verify builds with
zero warnings/errors, passes strict-rebuild/structural gates and 639/640
parallel tests. Only pipe at 20,000 parameters exceeds its unchanged
750 ms bound (942.893 ms); .build/waxwing-isolated-verify.log. FN006 owns
this existing parallel-timing exposure. No whole-suite green is claimed.The original 3481468 compiler was rebuilt in .build/waxwing-baseline-compiled for a differential comparison. Its first scratch build failed because the copy omitted tools/style/src (.build/waxwing-original-build.log); supplying that unchanged directory fixed the setup, and the next build passed with zero warnings/errors (.build/waxwing-original-build-fixed.log).
A scratch copy of scripts/differential.mjs normalizes only Bumpus/Waxwing and bumpus/waxwing in emitted-Go hashes; all seven phase comparisons and diagnostic comparisons remain. The full harvested corpus comparison was interrupted (status 130) after several minutes without a report; T006 owns the investigation. The bounded comparison used sources up to 20,000 characters (2,433 captured sources, 53 stress sources outside this optional comparison only). It exited 0: 2,433 harvested, 1,029 fuzz, 1,000 match and 1,000 mutation sources, plus 1,085 isName probes; zero differences. Log .build/waxwing-differential.log; scope .build/waxwing-differential-scope.json. The bounded run had already completed when an interrupt was requested. A redundant memoized-hash scratch experiment was then stopped (status 130) and is not validation evidence; the final scratch checker was restored to the successful version. No maintained differential check was weakened.
Final smoke: live npm waxwing run examples/answer.wxw prints 42; all eight paused worker files still match their saved hashes; current branding assets match the user-supplied originals (.build/waxwing-final-smoke.log). The user confirmed the visible logo files are right. Read the supplied Claude continuation prompt and the local SDD ledger; they corroborate the paused Task 3 review-fix state. No further effects implementation was started by this rename task.
Rename implementation is complete on fx001. Full verification is not green: the isolated rename tree has the recorded pipe timing failure, and the live combined tree also contains the unfinished deferred-row test mismatch plus timing failures. Serial continuation (13) and all twenty-two regression proofs pass; earlier failures remain recorded. The rename and eight preserved FX001 worker files remain uncommitted; main is unchanged. For the continuation, commit the rename separately from Task 3 review fixes; finish those fixes/tests and re-review before Task 4. No merge or push.
The user resumed FX001 after the rename’s separate commit 40cf312. Preserved and completed the eight paused source files: deferred Fail keys postpone the whole original row equation with type/row/fresh/event rollback; settleRows retries after other constraints and preserves undecidable pairs; groundErased is used only for Seeds/Resolve monomorphism while strict ground remains strict. RowOccurs distinguishes a row’s occurs failure. The left-over-right path also needed a deferred-key guard: new tests first failed 9/10 with RowExtra, then passed after that guard. Exact closed/open reflexivity, one-Fail settling, closed deferred right, never-skip-first, rollback, retry, arrow continuation and erased grounding cases are covered.
The independent transactional oracle now models rollback, postponement and settling. Generators include deferred Fail arguments, same-family Error(Int)/Error(Bool), arrow label arguments and nonempty starting type/row substitutions. Original 1,500-pair solved laws (including symmetric acceptance and scoped equality) remain; 2,000 further pairs in both directions distinguish pending acceptance from solved equality and compare settling after ordinary constraints. Focused run: 44/44 pass, no skips (.build/fx001-task3-fix-focused.log). Seven isolated source mutants all compiled successfully then failed intended assertions; full logs/results in .build/fx001-task3-fix-mutants, summarized in the local Task 3 report.
Verification history retained:
The row fix is verified through direct semantic tests/oracles; no syntax can create nonempty effect rows yet. Task 5 must settle after body constraints and diagnose remaining unknown families. Deferred equations currently carry no spans/provenance; Task 9 must preserve/recover origins when integrating traced retries. UnifyRow is 249 lines. Both are explicit handoff concerns, not claims of completed later tasks. Active effects ADR references now use 010 (009 is naming). Ready for independent review before Task 4; no merge or push. Local report/ledger: .superpowers/sdd/2026-10-09-effects-plan/{task-3-report,progress}.md.
Implemented sorted signature variables with one rigid ambient row, explicit rows, declared operations, stage consumption, Console print and Unit entry output. Rows erase from specialization arguments. Field and operation arrows default to pure, with declared effect names available; lambda annotation omissions share the lambda’s own fresh ambient row. Program.Compile explicitly rejects user effects before specialization; handlers and user effect execution remain later tasks. Wire diagnostics carry related: [] with existing messages and spans preserved.
Clean-output npm run verify exited 0 (.build/fx001-task4-verify.log): zero build warnings/errors, formatting/structure/strict-rebuild gates, 672 parallel tests, 22 serial tests, no failures or skips, and all 22 existing actual-source regression proofs. Four additional isolated actual source mutants built and failed their named regressions: lambda annotation omission, 20,000-arrow row collection, row argument erasure and effect names in field types (.build/fx001-task4-sensitivity/{results.json,*-test.log}). Earlier failed runs remain documented in the local Task 4 report; no unchanged rerun hid a failure. Concrete malformed-input validation gaps are BACKLOG FX004, with reproductions and the Task 5 next action. No merge or push. Implementation/evidence: .superpowers/sdd/2026-10-09-effects-plan/task-4-report.md.
Implemented handler values, with, handle, and fail through Parse,
Resolve, and Check. Handler clauses are checked against declared operations;
with consumes the handler capability while preserving the outside row;
handle settles concrete Fail(E) keys and forwards unhandled error
families. Deferred retries preserve the original semantic failure category
and fail span. Nested failures are traversed through their payloads.
Row-parameterized ADTs support interleaved, multiple type/row parameters,
bare effect labels, and pure; source sorts are retained while VarIds stay
canonical (type slots first). The parser remains Applicative-only; Handler
type syntax is isolated in Format.Parse.HandlerType. A declared ADT named
Handler wins for Handler(Foo), while Handler(Clock) resolves as a handler
type when no such ADT exists. Duplicate effects, operations, effect/type
parameters and function/handler binders use the existing duplicate
diagnostics. Both written row forms reject repeated or nonfinal tails.
Program.Compile rejects checked Handler/HandlerValue, With, Handle and Fail nodes, plus Handler types in signatures, expressions and every constructor field, before Specialize. The iterative type walk stays stack-safe at a 20,000-arrow signature. This is Check-only support: specialization/lowering, runtime behavior, and Go emission remain later FX001 tasks.
Root audit findings 1–13 are fixed and recorded with reproductions in .superpowers/sdd/2026-10-09-effects-plan/task-5-root-audit.md. The previous FX004 validation gaps are closed. Focused handler tests pass 81/81; the large-source suite passes 15/15, with 20,000-type duplicate rejection at 475 ms against its unchanged five-second bound. Eleven new isolated source mutations prove the nested Fail, duplicate validation, deferred diagnostic, metadata guard and duplicate-scan timing regressions.
Clean-output npm run verify exited 0 in
.build/fx-handler-task5-verify5.log: zero build warnings/errors, pinned
formatting, strict rebuild and structural gates, 722 parallel tests and 22
serial tests with no failures or skips, and all 33 actual-source regression
proofs. Earlier real failures remain preserved: verify2 exposed duplicate
and resolver-metadata compatibility issues; verify4 exposed eager
when-argument evaluation in duplicate scanning (6.92 s against the
unchanged five-second limit). Those defects were fixed and their regression
proofs added before verify5. Full interfaces, evidence and the remaining
Task 9 origin-provenance handoff are in
.superpowers/sdd/2026-10-09-effects-plan/task-5-report.md. No runtime or
specialization behavior is claimed for this task.
Resumed in a Claude Code cloud session on branch claude/vibrant-cerf-3km61i (fast-forwarded to fx001 at 9a6befd; no .worktrees, ruling recorded in the SDD ledger). The local SDD ledger of Tasks 1-5 was never committed; it was recreated from git and this file. Task 6 was implemented inline by the controller before the superpowers skills were linked into .claude/skills (32ee2d2); it then received an independent task review.
Implemented: effect keys (EffectRef, ground type arguments) in the one
specialization worklist, created on first reference; user effect keys fill
their operations’ types beside their source (Resolved.OperationInfo gains
syntax), so they reach type keys and the effect keys of handler types;
Handler(L …) lowers to L’s key in bodies and constructor fields; rows are
erased everywhere. The IR gains THandler EffectKey, an effects table and
nodes OperationRef, Perform, HandlerValue, Install, Handle and Abort (fail;
handle clauses and aborts carry the payload’s declared family). Check.Nested
judges one reference graph over type and effect declarations (constructor
fields, operation parameters and results, Handler(L …) edges); Grow, the
mutual pair and the data/effect cycle are E_SPECIALIZATION at the nested
reference, their bare-parameter versions compile. A function whose type
variable appears only in its own row’s labels is polymorphic (it crashed
specialization before). Features.Check.Unlowered is replaced by
Features.Specialize.Unlowered over the emitted IR: effect layouts or effect
nodes stop at E_INTERNAL “unlowered effect”; unused effect declarations reach
Go unchanged. Only keys with type arguments count toward the 10,000 limit.
Tests reach Specialize directly (test/fx-specialize.test.mjs, 18 tests);
executable probes wait for Task 7.
A Task 5 parser defect surfaced: with R inside Handler(L with R) was
silently dropped (and rows after non-arrow types elsewhere). Fixed by a
Sonnet implementer in 5da7865 and 3aa1217 (round 2 pending at this entry),
reviewed by Opus; meaningless rows are now E_SYNTAX “Unexpected effect row”.
Evidence: RED 15/18 (.build/fx001-task6-red.log; three characterization
tests passed before). Clean npm run verify at 85c5867
(.build/fx001-task6-verify.log): zero build warnings, gates pass, 752/752
parallel tests; serial phase 13/22 — the 9 failures are fixed timing bounds
that fail identically on the pre-Task-6 baseline 9a6befd on this container
(BACKLOG T007); regression proofs, run explicitly: 33/33 (.build/fx001-
task6-regression{,2,3}.log; four duplicate-declaration rows now expect an
accepted program because the old guard no longer masks them). Test-helper
defect fixed: test/poly-keys.mjs built a 5,000-deep JSON message eagerly
(stack overflow here). Task review (Opus): spec ✅, quality Approved, no
Critical/Important; Minor items deferred in the ledger.
defer must not fail (2026-10-09)Before Task 8, the user was asked how a cleanup failure during an abort
should behave (abort-to-defect conversion as specified, drop, replace, or
suppressed causes). The user chose the static rule, after Swift’s defer:
a deferred expression that performs an unhandled Fail is E_EFFECT
defer must not fail, but it performs <L>; cleanup that can fail handles
its failure inside the deferred expression. Only defects can fail cleanup,
so a pending typed abort reaches its handle unless a cleanup on the way
raises a defect (the abort then heads the report) or diverges (corrected
per CF001 §11). Spec §2 (defer
rule), §3 (“Cleanup failures”, with rejected alternatives), §5 (report
example, probes 6 and 11), plan Global Constraints and Tasks 8, 10 and 12,
findings and the handoff were updated. Docs only; no code changed.
Subagent-driven: Sonnet implementer (e9a613f, fix round 4b80bd1), Opus task
review and scoped re-review. Effect programs now build and run. ctx mode is
chosen over the emitted IR (any effect layout or effect node); in it every
function, stage, lambda, constructor, lifted helper and function type takes
ctx *waxwingCtx first; otherwise Go output is unchanged (bootstrap
snapshots byte-identical, Console-only programs without ctx). Runtime (Task
1’s adopted shapes, emitted only when used): immutable context list,
non-zero-sized markers, targeted aborts recovered only by their own lifted
handle helper. Keys: user effect EffectId+1, Fail families by declared
head (negative keys); missing handler is panic("no handler for L").
Handler structs per layout (empty for Console/Fail layouts), perform
functions call clauses with the frame’s outer context (intercept-and-
forward), clauses lifted like lambdas. Features.Specialize.Unlowered is
deleted; every ‘unlowered effect’ assertion was replaced by an executable
assertion (test/fx-run.test.mjs, 24 programs with exact stdout).
Two checker defects found and fixed: constructor patterns dropped row
arguments (Check.Match/Tables), and consumption did not resolve a bound row
tail (Check.Consume); Check-level tests in test/fx-check-fixes.test.mjs,
each failing with its fix reverted. Review found one Critical: the clauses
of one handle were installed in reverse (runtime panic for Error(Int) vs
Error(Bool) clauses); fixed with probes. Regression rows: block-order needle
updated; handler-metadata replaced by effect-free-ctx.
Evidence: rm -rf output && npm run verify (.build/fx001-task7-verify.log):
build, gates, 786/786 parallel tests; serial 12/22, the 10 failures are the
BACKLOG T007 environment bounds (same set on pre-change baselines);
regression proofs 33/33 (.build/fx001-task7-regression.log). The
fx-block.serial 20,000-let test sits at its 1,500 ms bound on this
container: 49717bc 1,533-1,574 ms vs Task 7 1,386-1,505 ms, alternating.
defer, crash, cleanup and the defect report (2026-10-09)Subagent-driven: Sonnet implementer (041cb31; fix round d5bfa8d, resumed
after a container restart), Opus task review and scoped re-review.
defer e (block item) and crash(value: a): b (built-in, printable
argument, no effect, uncatchable) run through every phase. Cleanup runs
LIFO, exactly once per exiting activation, in the registration context;
the defect report follows spec §5 (exit status 1, first line the original
cause, later lines cleanup failed: ; <not printable> decided
statically). Runtime pieces (Format.Go.Cleanup, Format.Go.Report) are
emitted only when used; defer and crash do not force ctx; blocks without
defer emit no Go defer. Conventions recorded: with no pending cause the
first cleanup crash is the unprefixed first line; Go runtime panics print
as panic: <text>.
The user’s rule that defer must not fail is enforced soundly after
review: the first implementation checked only labels, so five programs
(callback parameters with ambient or named rows, unsettled closures,
lambdas decided later) let a typed failure escape cleanup and become a
fatal report. Ruling (controller, pending user confirmation of strict over
a lacks-Fail constraint): the deferred expression has its own row; after
the function’s constraints settle, a remaining Fail label or a tail
resolved to a rigid row variable is E_EFFECT (defer must not fail, but it
performs <L> / ... but it may perform any effect of <r>). Accepted
limitations (spec §2, BACKLOG FX007): the bracket idiom needs with pure
callbacks; a local lambda also called outside the defer in a non-main
function is rejected in it. Report.payloadName now walks arrow spines by a
loop (a 5,000-arrow payload crashed the compiler).
Evidence: fx-cleanup 32/32 (RED 24/26 before, then 7 new failing tests
for the fix round; seven isolated mutants caught). npm run verify
(.build/fx001-task8-verify.log): build, gates, 818/818 parallel tests;
serial 12/22, the 10 failures are BACKLOG T007 timing bounds; regression
proofs 33/33 (.build/fx001-task8-regression.log). Re-review probes:
legitimate defers (defer log(1), defer print(x), monomorphic helpers)
accepted; the check is linear (1k-8k defers).
Subagent-driven: Sonnet implementer (2fc7a02; fix rounds 04ab898, cee8542,
b80cc17), Opus task review and two Opus re-reviews, Sonnet re-review of the
last one-line fix. (A first dispatch was lost to a container restart; the
second was briefly stopped by a controller misreading of a user message and
resumed with its work intact.) Effect errors now carry origin, boundary and
path notes (spec §6): occurrence links live in the substitution and are
recorded on every unification path (nested arrow rows, settleRows retries,
consumption); deferred-row origins are kept apart from the function row;
note texts follow ruling R4 and the wire carries related: [{ span,
message }]. Resolved gains rowSpan on functions and parameters so
boundary notes point at the written with (or the parameter name when the
concerned row is nested or not the only pure row). Path hops, note count
and characters are abbreviated (visibleLabels 4, pathHops 2, maxNotes
4, maxCharacters 2000 for notes); labels over 120 characters print
Effect(…) in headlines. Pre-FX001 diagnostics are unchanged (related:
[] by construction; wire JSON identical); 20,000-parameter signatures
check in about 1.5 s.
Known limits recorded: maxCharacters bounds notes only — a headline with a
very long type or effect name can exceed 2,000 characters (BACKLOG E011);
the late-Fail-key defer note follows let-bound locals only and otherwise
gives no note (never a wrong one; ~20 probes); origin notes may point
outside the deferred expression.
Evidence: fx-diagnostics, fx-diagnostic-origins, fx-signature 46/46; the fix-round tests were each seen failing (the round-2 tests by the controller against 04ab898 in an isolated worktree). Final verify (.build/fx001-task9-fix3-verify.log): build, gates, parallel 847/848 — the one failure is large-source “three-thousand-constructor match” timing (BACKLOG T004; passes alone); serial phase failures are the T007 set; regression proofs 33/33 (.build/fx001-task9-fix3-regression.log).
Subagent-driven: Sonnet implementer (50f84ae; fix round 9365db3), Opus task
review, Sonnet scoped re-review. Test-side only: no compiler source
changed (git diff 89de11c -- src scripts is empty). New files:
test/fx-oracle.mjs (independent lexer, parser, values, frames, cleanup and
interpreter; no compiler import), test/fx-gen-.mjs and fx-programs.mjs
(generator), fx-census.mjs, fx-shrink.mjs, fx-diff-support.mjs,
fx-oracle.test.mjs (parallel) and fx-differential.serial.test.mjs (serial,
ruling F11). Executable probes moved unchanged into importable modules and
imported back: fx-cleanup-programs, fx-console-programs, fx-task7-programs
(Task 7’s eight inline run probes from fx-specialize and fx-handler-check).
docs/engineering.md documents the corpus and the knobs WAXWING_FX_SEED
and WAXWING_FX_PROGRAMS.
The interpreter models the shipped semantics: handler frames keyed by effect
identity, Fail by the payload’s declared head, the first clause innermost,
clauses in their with’s outer context, defer LIFO in its registration
context, the pending abort heading a cleanup-crash report, cleanup
failed: lines in execution order. It is validated against every
executable probe of Tasks 2, 4, 7 and 8 (fx-oracle.test.mjs 86/86); the
fx-cleanup injected cases and fx-block orders probes (Go transformed
after emission) are excluded by name. Ruled deviations: the oracle parser
re-implements the grammar rather than extending poly-parse.mjs (a closed
closure that may not be edited); only the first three differences per run
are shrunk (every difference’s seed and source are written first); the
sensitivity check compares the interpreter with its inner-clause-context
flag (50 programs, at least one difference), Go-vs-interpreter equality
being the serial run’s job.
Default run: 500 programs, seed 20261010 (program i uses seed 20261010+i),
five go-batches of 100, 0 differences, 0 rejections, 0 interpreter errors,
12.9 s (.build/fx001-task10-fix-default.log). Census (minimum): nested
same-key 225 (50), intercept-and-forward 68 (50), aborts crossing an
unrelated handle 109 (50), stage order with over-application 90 (50),
escaped callbacks 109 (50); cleanup normal 267, abort 363, defect 119,
crash on normal exit 65, crash while abort pending 38, several crashes 38,
own failure during abort 193, registration context 74, unreached defer 143
(25 each). 25 rejection variants give E_EFFECT with the Task 8 texts at the
defer span. Extra run: seed 7, 2,000 programs, 0 differences
(.build/fx001-task10-large.log). Mutation witnesses (isolated copies):
inner clause context, first clause outermost, FIFO cleanup, abort not
heading the report, eager argument evaluation each fail the hand traces;
disabling item removal or literal replacement fails the shrinker tests.
Finding: the interpreter exposed a hole in the strict defer rule — a
deferred operation whose handler clause fails ends cleanup in a typed abort
(BACKLOG FX008). The generator emits only fail-free random clauses until it
is fixed.
Verify (.build/fx001-task10-fix-verify.log): build, gates, parallel 933/934 — the one failure is large-source “three-thousand-constructor match” (T004; passes alone). Serial phase run directly (.build/fx001-task10-fix-serial.log): the new differential tests pass; failures are timing bounds only — the T007 set, fn-scale 1,000-long arrow (also failed in Task 9’s serial log), the match ladder (T003, also failed in Task 9’s), and fn-scale “chain of 1,000 partial applications” (phases 253 ms; new in this log, and failing alone three times at 310-341 ms with compiler, scripts and that test unchanged since 89de11c — the container’s bound, recorded under T007). Regression proofs 33/33 (.build/fx001-task10-fix-regression.log).
Branch fx008 (from claude/vibrant-cerf-3km61i at fdd375a), on the user’s
machine. Baseline rm -rf output && npm run verify
(.build/fx008-baseline-verify.log): exit 0; build, gates, 934/934
parallel tests, 29/29 serial tests (the T003/T004/T007 timing bounds all
passed here), 33/33 regression proofs. The user ran verify on main in the
same checkout about the same time, and this session had just switched the
checkout to fx008; overlap was not established, and this run passed.
Design only, no code: docs/plans/2026-10-10-abort-tracking-design.md.
Recommended approach A: a nofail mark on each non-Fail label occurrence;
with h gives the body’s label h’s mark; handler clauses get own rows
(the R0 technique), and clause labels relate to the handler by ⊑, not
unification; defer demands N on every label it performs; signatures
write nofail L to rely on a handler not failing; a per-declaration
two-point propagation, linear, with no scheme constraints; marks erased
before specialization, so Go is unchanged. Alternatives B (totality per
effect) and C (inferred marks) are recorded. Newly rejected: one pinned
acceptance (fx-cleanup-programs a defer performing a non-Fail effect,
migrated with with nofail Log), plus the pinned oracle hole. Independent
Opus review dispatched; user decisions D1-D5 pending.
Review (Opus, review-fx008-design-result.md): Accept with fixes. Critical:
C1, the solve was seeded only from defer demands, so a handler mark made N
by a callee’s nofail signature was never checked (the cross-function
hole stayed open); C2, separate clause rows need late consumption that
keeps Fail, to a fixpoint, or the base effect system becomes unsound
(the reviewer’s late-Fail program is Unhandled Fail(E) in main today).
Important: equality merges through monomorphic locals (I1), no mark
polymorphism for forwarding helpers (I2), an understated migration (I3),
and a soundness argument to restate (I4). The reviewer ran the hole and
the I1/I2 programs on the current compiler (accepted; I1/I2 run
correctly). Revision 1 of the note applies every finding: a fixed settle
order, seeding from every N mark, limitations L4/L5 with decisions D6/D7,
the D0 interpretation decision, and minors M1-M10.
Re-review (rereview-fx008-design-result.md): C1, C2, I3, I4 and M1-M10
addressed; new Critical N1: the late-consumption fixpoint does not
terminate when a restricted row shares its tail with its target (the
reviewer’s programs: a never-called lambda with a Console-only cleanup,
accepted today; a no-defer clause form, rejected today, which would
hang). Revision 2 adds a shared-suffix rule, the full settle order (N2)
and N3-N5. Round 3 judged the rule sound but the termination measure
unsound as written (it used provenance links, which may cycle and must
not influence typing); revision 3 uses a copy map local to that step.
Revision 3 is not re-reviewed. Stopped for the user’s decisions D0-D7.
Verify at afedaec (docs only; .build/fx008-design-verify.log): rm -rf output
&& npm run verify exit 0; 934/934 parallel, 29/29 serial, 33/33 proofs.
At the user’s request, documented module-system comparisons and fp-ts-style selection/construction of instances without mandatory newtypes in docs/plans/2026-10-10-modules-and-instance-selection-direction.md. Linked from language direction and C001/M001 backlog entries. Separates user preferences, recommendations and open decisions; covers abstraction/type identity, coherence, collection consistency and evidence for algebraic laws. No implementation or syntax approved. Active FX008 documents left untouched. Documentation inspection only; verification not run, preserving the user’s instruction not to run checks for this discussion.
Outside advice, shared by the user 2026-10-10 and treated as advice only (note §12; D0-D7 open), asked for a focused termination review. Revision 4 rewrote §3.5 steps 1-3. The fresh Opus review (review-fx008-termination-result.md) found:
Revision 5 adopts the corrected procedure:
The procedure is the reviewer’s and has had no second check.
Probes are copied to .build/fx008-probes/. The controller reproduced two defects in today’s checker:
run panics with no handler for
Fail;Effect row consumption
failed.Both are new BACKLOG rows.
Verify at 9b47e4b (docs only; .build/fx008-rev5-verify.log): exit 0; 934/934 parallel, 29/29 serial, 33/33 proofs.
User decisions 2026-10-10 (interview; note §11): D0-D5 as recommended;
D6 mark variables (nofail(k) L) and D7 directional checking are
included in FX008. Revision 6 of the note adds them. Including D7 needed
two structural changes, which remove the merge and forwarding limitations
(L4 mostly, L5):
with consumes the handler’s clause row as a restricted row instead of
unifying it with the context;Mark solving becomes a path check over a two-point lattice with rigid mark variables. One Opus review of the whole note is in progress. FX009 is being fixed test-first in parallel, on branch fx009 in a worktree.
FX009 review (Opus, review-fx009-result.md): Needs fixes.
settleKeys as well, and rejected a
program accepted today.Fail whose payload head is a type
variable is E_TYPE at the written annotation.FX008 revision 6 review (Opus, review-fx008-rev6-result.md): no Critical findings and no unsound acceptance found. The path check is correct, and the revision 5 loop holds with installation entries.
with lost type inference. meta2.wxw,
accepted today, would be rejected, and hole2c.wxw’s Go output would
change.handler Console gives E_INTERNAL.Revision 7 is unreviewed.
FX008 revision 7 check (Opus, review-fx008-rev7-result.md): soundness
established. The two-row split restores meta2 and hole2c, but not all of
today’s inference: late1 and late2, without defer, would be rejected.
User decision: a one-way hook keeps R in step with the clause rows
(revision 8). The other findings are applied as text:
A narrow review of the hook is in progress.
FX009 re-review (Opus, rereview-fx009-result.md):
Fail(Int -> Int), which reproduces the original panic.handle clause payloads give E_INTERNAL.User decision: every written Fail payload without a family key is
E_TYPE at the label, in signatures and in handle clauses. Round 2 is in
progress.
FX008 revision 9 applies the hook review (review-fx008-rev8-hook-result.md; no Critical findings, soundness unaffected):
defer and are accepted today,
would be rejected, and two2’s Go would change.F1 and F2 are unreviewed. A probe gate, which re-runs every review probe (kept in .build/fx008-probes/), is now the first plan task.
FX009’s round 2 was approved by the re-review, and the branch’s docs are
committed (3518678 on fx009). Verify at 3518678: exit 0, 958/958
parallel, 29/29 serial, 36 proofs. It awaits the user’s merge decision.
New BACKLOG FX012: firstUnkeyed runs before deferred rows settle.
Fail payloads rejected (2026-10-10)Branch fx009, from fx008 at 531e438. Sonnet implementer (c445bac; review rounds 33a3a62 and f6cb6d0), Opus review and two re-reviews (docs/sdd/2026-10-09-effects-plan/review-fx009-result.md and rereview-fx009-result.md).
Defect: leftover deferred-key pairs were dropped silently, so a
Fail(a) parameter row let a failure escape at run time (no handler
for Fail).
User decisions:
Fail payloads without a family key (a type variable, a
function type or a handler type) are E_TYPE Fail needs a concrete
error family at the label, in signatures and handle clauses, with a
hint.Fail(Error(a)) and Fail(Unit) are allowed.The reviews found no program that reaches that guard (about 50 probes compared against a build without it), so it is a 3-line rejection. Mutants of the written-label checks are caught by the guard at the wrong span.
Evidence:
rm -rf output && npm run verify: exit 0, 958/958 parallel, 29/29
serial, 36 proofs.Spec §2 “Label keys” updated. Pre-existing and recorded separately: a
handle whose failure key is decided only after deferred rows are
settled is rejected early (firstUnkeyed runs in the first settle).
FX009 merged into fx008 (26fefe3, user decision 2026-10-10). Verify on the merged tree (.build/fx009-merge-verify.log): exit 0, 958/958 parallel, 29/29 serial, 36 proofs. The user asked for one focused review of FX008 revision 9’s F1/F2 procedure (termination, inference compatibility, scheduling). On no substantive issue, the user approves the written spec and the implementation plan follows, starting with the probe gate.
Recorded the user’s requested Schema foundation as SCH001 in docs/plans/2026-10-10-schema-direction.md and the backlog, with links from language/tooling direction and a targeted LA001 comparison. Consulted Effect v4 Schema documentation and the supplied rae-hill/schemata-ts README/interface docs. Covers parse-into-domain boundaries, codecs, selectable interpreters, generators/shrinkers and independent properties. Library/compiler mechanism and v1 subset remain open. No implementation, tests, commit or verification run; preserves the user’s request not to run checks during design discussion.
SCH001 follow-up (2026-10-10): expanded schema-direction with capability overlap versus Effect, the direct io-ts Schema/Schemable precedent, tagless-final research and ZIO Schema’s different representation. No major required capability exclusive to schemata established; interpreter encoding versus AST remains open. Documentation inspection only; no checks run.
Focused F1/F2 review (review-fx008-rev9-f1f2-result.md): a substantive issue. Termination, inference compatibility and scheduling each failed as written (cyc1, ov1, set1, all accepted today). It found no unsound acceptance. Revision 10 applies the corrections:
sync to
a fixpoint;FX008 revision 10 re-check (review-fx008-rev10-result.md): no substantive issue.
The wording fixes are applied. By the user’s condition (2026-10-10), the written spec is approved. The implementation plan is next, starting with the probe gate. Probes are kept in .build/fx008-probes/ (rev6-rev10).
Docs only, from a review of the draft Waxwing article outline. BACKLOG
gains FX006 (general resume) ahead of FX001 Task 12, carrying the plan’s
scope plus the user’s constraints: multiplicity decided on language terms,
one-shot by default and declared per operation so multi-shot stays an
additive, row-visible extension, reserved spelling, at-most-once assumptions
stated in defer/FX003/CF001, and a double-resume regression. FX008
records an unverified hole in defer must not fail through a failing
handler clause. (Merged 2026-10-10 into branch fx008, which already
tracked and verified this hole as FX008: the duplicate row was folded into
that record. See “FX008 merge with the article-outline branch” below.)
language-direction.md records the user’s direction that the
language (frontend, syntax, type system) outranks the Go target. No code
changed; no build or verify ran (a parallel worktree was active).
Branch claude/optimistic-dijkstra-6i21jy (81990d6, 2b42574; docs only,
from main 234d115) is merged into fx008 with a merge commit. Its FX006
row and the language-direction section “Language before target” are kept
unchanged. The FX008 work does not contradict FX006’s constraints:
design §4.6 makes any ctl clause’s handler may-fail until general
resume. Its Task 12 change to the effects plan is kept.
Its FX008 row duplicated this branch’s FX008, recorded there as an unverified hypothesis. It is folded into this branch’s row and removed. The folded-in facts:
Established on the current compiler (probes in .build/fx008-probes/facts/):
fail(E) skips the enclosing
handle for E: exit 1, stderr fail(E): E;fail(F), F never reaches its handle: exit 1, stderr
fail(F): F then cleanup failed: fail(E): E.docs/writing/article-outline.md Part 3 §5 now states:
§4 (the Task 8 case study) was checked against review-task8-result.md: the five programs and the 25/25 tests match. It is unchanged.
Verify at the outline merge (3e24e21; .build/fx008-merge-outline-verify.log):
rm -rf output && GOTOOLCHAIN=go1.26.4 npm run verify exit 0; 958/958
parallel, 29/29 serial (timing bounds included), 36 regression proofs.
History tidied before publication (2026-10-10, user-authorized):
The tree differs from the verified 207474b only in docs.
FX008 implementation plan (docs/plans/2026-10-10-abort-tracking-plan.md, revision 3, 1,228 lines, 12 tasks plus setup). Drafted by an Opus planner and reviewed twice by an independent Opus reviewer (review-fx008-plan-result.md): round 1 “ready with fixes” (8 Important), round 2 “ready with fixes” (one Important, the ec1 expected text). All fixes are applied. The reviewer ran 102 probes, and the plan’s recorded outcomes match. The plan awaits the user’s approval and sign-off on the P3, P5, P6, P7, P10 and P11 decisions. Nothing is implemented.
User approval 2026-10-10: the FX008 plan (at 0d7cc27) is approved, with sign-off on P3, P5, P6, P7, P10 and P11 and P13 accepted. The user added:
Setup and Task 1 are authorized.
Worktree /Users/ethan/Desktop/waxwing-fx008-impl, branch fx008-impl, from 8fe4218. Sonnet implementer (58c3d61; fix round 71d6263), Opus task review and scoped re-review (review-fx008-task1-result.md): Spec ✅, Quality Approved.
test/fx008-probes/ holds 104 programs: 102 review probes, deduplicated, plus the witnesses ec1 and tp1. The table pins each probe’s outcome today:
It also records each probe’s FX008 outcome. 17 entries flip, each with a task, a reason and an exact outcome or a pattern with report-before-pin. 14 slow probes run in child processes with a 20 s timeout. A tamper hash covers the table, and invariant tests check flips; a flipped accepted entry must pin its Go hash.
Rulings:
2 before its abort: the plan’s “empty stdout”
was a transcription error, corrected.The reviewer re-ran all 104 probes independently (0 mismatches), checked the timeout path (fails at 20,006 ms, no orphan process) and nine mutation kinds, each caught. Verify at 71d6263 (.build/task1-fix-verify.log in the worktree): exit 0, 1065/1065 parallel, 29/29 serial, 36 regression proofs. Stopped before Task 2, as authorized.
Sonnet implementer (66e4f5d; fix rounds c6af69f and 5ed5b0e). Opus task review and re-review (review-fx008-task2-result.md): Spec ✅, Quality Approved after the fixes. The second fix round (a test bound and plan text) was checked by the controller.
What it adds:
Mark on every Label;HandlerRows on THandler;mapMarks, stripMarks, markVariables);instantiationRule.Behaviour is preserved: the gate is green, and the Go text and bootstrap snapshots are unchanged.
Review C1 (Critical): two handler rows doubled at every nested handler
type. Depth 22 ran out of memory, where base accepts to 124.
Ruling: clauses ∷ Maybe Row, where Nothing means the same as R.
Passes visit C only when it is Just. Plan P2, P4 and the interfaces of
Tasks 2, 4, 7 and 8 are updated. Timings now match base to depth 124,
with identical Go.
Guards:
The strip test now covers bodies, operations and constructor fields,
with four mutants caught. Plan updates for later tasks: Task 3’s
localMarks gives a Just C only to handler types that are not nested
in a handler row, with a guard. Task 7 gets a 30-literal guard and a note
on unequal derived Eq.
Verify at 5ed5b0e (.build/task2-fix2-verify.log in the worktree): exit 0, 1070/1070 parallel, 31/31 serial, 36 proofs. New BACKLOG G004 (pre-existing): a depth-125 handler type ends in a RangeError instead of E_NESTING.
nofail syntax, resolution and printing (2026-10-10)Sonnet implementer (f16c6b5; fix rounds 1f9f257 and e0580a4). Opus task review and re-review (review-fx008-task3-result.md): Spec ✅, Quality Approved. The second fix round was checked by the controller.
What it adds:
nofail and nofail(k) parsing, contextual through a one-token
lookahead;fail clauses;instantiateMarks and localMarks: nested handler types keep no clause
row (the c1 rule, with a nesting guard);nofail L; once one arrow of a spine prints its row, every
arrow does;The reviewer compared whole-compile outcomes between base and candidate
on 3,371 harvested sources plus 23,029 mutated ones: 0 differences for
programs without nofail. 45 adversarial grammar programs confirm
nofail stays an ordinary name elsewhere.
Rulings:
New BACKLOG E012 (pre-existing): closed and ambient rows print alike. Verify at e0580a4 (.build/task3-fix2-verify.log in the worktree): exit 0, 1105/1105 parallel, 33/33 serial, 36 proofs; gate 107/107.
User decision 2026-10-10 (FX008 Task 4): mismatch texts are symmetric.
When either side of an E_TYPE mismatch shows a handler clause row or an
arrow row, both sides show theirs. An empty handler row prints with
pure, and ambient tails stay omitted. This amends the approved P3
callback text to Expected Unit -> Unit with nofail Log, found Unit ->
Unit with Log.
Sonnet implementer (3b0dd3e; fix rounds aa8db84, a43f56c, 84b684e). Opus task review and re-review (review-fx008-task4-result.md): Spec ✅, Quality Approved. Rounds 2 and 3 were checked by the controller.
What it adds:
RowMode: equal, ignore marks, or directional;consumeLabels);Review fixes:
Evidence:
scripts/differential.mjs --corpus all against e0580a4: 0 differences
in every phase (3,421 harvested, plus fuzz, isName, matches and
mutations).nofail.User decisions 2026-10-10 (FX008 Task 5 probe gate):
defer span, the same cycle as n1. The plan had wrongly predicted it
unchanged.Sonnet implementer (d9a0576; fix round dd45b58; comment b5fab7c). Opus task review and re-review (review-fx008-task5-result.md): Spec ✅, Quality Approved.
The corrected §3.5 loop (Features.Check.Settle, Feed, FeedOrder, Cycle):
settleKeys runs inside each round;settleFinal, and the defer judgement runs after
the loop.Probe gate, with the user’s decisions: dup, n1 and r5 flip as planned, and design-review/r4 (wrongly predicted unchanged) is a fourth flip. All four texts are pinned exactly. The differential against 84b684e shows only those programs differ. A 60,000-graph brute-force check confirms cycle detection.
The review found the settle order quadratic: an eager maybe fallback
took 8,000 defers to 6.4 s against base’s 0.8 s. It is fixed with
maybe'. Now linear, at about 1.3x base. Guards:
Recorded:
Fail in a defer plus n1’s
cycle now reports the cycle.Verify at dd45b58 (.build/task5-fix-verify.log in the worktree): exit 0, 1142/1142 parallel, 34/34 serial, 38 proofs; gate 107/107.
2026-10-10, at the user’s request:
Sonnet implementer (01cbe77 refactor, 418cd38; fix round c8503f9). Opus review (review-fx008-task6-result.md): Spec ✅, Quality Needs fixes; the fix round was checked by the controller.
What it adds, with no behaviour change:
handleFailure and failureClause moved out of
Handler.purs (241 → 184 lines);entries
(deferrals only until Task 7 registers clauses); Site.purs split out.Review fixes: I1, the revert test could not fail (a mutant keeping the
progressed watch passed); it now watches both tails and fails on that
mutant. M1 compose keeps the later watch. M2/M3 comment the invariants
Task 7 relies on. The reviewer killed the other three mutants (one per
test).
Evidence:
Sonnet implementer (wip 8dd22be; 9358d79; fix rounds 95eac9e, 66f1eca). Opus design pre-review (review-fx008-task7-design-result.md, A1-A7, two Critical design defects fixed before code), gate diagnosis (review-fx008-task7-gate-diagnosis.md), code review and re-review (review-fx008-task7-result.md); round 2 checked by the controller.
User decisions 2026-10-11:
Controller ruling: F1′ also runs at the start of each settling round (a plan gap, not a workaround), skipping the rigid branch (the tail pass binds ϱ after the loop), reporting at the handler literal.
Evidence:
Open: no probe distinguishes F1′’s join-group from a new group (cost
only); Task 8 must build env.current labels from R and pair C by key.
User requested a project milestone for automatic requirement inference, then explicitly required error composition during service assembly and business logic. Recorded A1-A10, design alternatives, current named-function inference gap, generic error-family questions, FX008 compatibility and completion evidence in the milestone. Status: requirements defined; language design and implementation plan remain. No compiler changes; active FX008 delivery is unchanged.
Documentation checks: A1-A10 present, tracking links resolve, and
git diff --check passed. Fresh npm run verify exited 1:
958/958 parallel tests and 28/29 serial tests passed; partialFirst at
20,000 parameters took 973.142 ms against its 930 ms bound (T007).
Build, strict rebuild proof and structural gates passed. Regression proofs
were not reached. No retry or assertion change. Evidence:
.build/inf001-definition-verify.log. This run verifies no INF001 behavior.
Sonnet implementer (b30b8b8; fix round ff0c6d1). Opus review (review-fx008-task8-result.md): Spec ✅ given the deviation, Quality Needs fixes; the fix round was checked by the controller.
What it adds, with no probe change: each with registers an
installation with a frame mark (NoFail for Console, else a fresh mark
meta); settling pairs the handler’s clause row C with the installing row
by key and position (Lend.assume), taking arguments from R, and records
the frame ⊑ C edges; the tail pass gains an installation rule (a guard,
unreachable while R ≡ current).
Deviation, accepted by the controller: install does not consume C, as
the plan said; consumption renamed row tails (442-449 check-phase IR
differences, nothing later). The reviewer swapped the plan’s consumption
back in and found identical mark-edge sets over 3,151 sources (570 with
installations), with the fallback never taken. The plan now describes
assume.
Review fixes: I1, 1,000 installations went from 3.97 s (base) to 11.6 s;
now 5.2-6.3 s (eager where bindings recomputed pairing every round);
serial guard 9.5 s. I2, edge endpoints and direction asserted (a
reversed-edge mutant fails 2 of 6), plus a nested duplicate-key case.
Evidence: gate 110/110, no change; differential 0 differences against 66f1eca and b30b8b8; verify at ff0c6d1 (.build/task8-fix-verify.log in the worktree): exit 0, 1182/1182 parallel, 37/37 serial, 49 proof lines.
Sonnet implementer (1649218; fix rounds 360f13e, 889ca99). Opus review and re-review (review-fx008-task9-result.md); round 2 checked by the controller.
What it adds: after settling, markConstraints records frame, own-abort
and defer-demand edges; violation searches paths from Plain and the
declaration’s mark variables to a fail-free atom (P7), and
reportViolation chooses the headline by the last edge (P6), with the
approved P10 texts. The pinned FX008 program is rejected with defer must
not fail, but it performs Log, whose handler may fail. Exactly the 8
listed probes flipped (lexical, hole, c1, cross, pending, l4b, merge2,
q1). The fx-cleanup program and the generator were migrated (§6).
Review findings and user decisions:
fail(E): E; interpreter “typed abort escaped cleanup”). User
decision: a function result’s handler type gets its own C at each use,
keeping R’s tail only when that variable occurs in a parameter or the
own row. New limitation (plan, beside L1, L4, P5): a handler passed
through a generic function and installed from its result gives an M
frame when the context’s rest is M or rigid.Evidence:
Sonnet implementer (d85a791; fix round 52b9df8). Opus review (review-fx008-task10-result.md): Approved, 4 Minor; the fix round was checked by the controller.
Mark rejections now carry the approved P10 notes, from the demand back
to the source: <L> is handled here, `this handler’s clauses perform