Archive — history, not state. Kept for its reasoning and its evidence; its plan is closed.

resolve v2: parameter backward inference + intrinsic returns + write-model soundness

Date: 2026-07-08. Baseline: 58f6ecdd (direct call op). Gate: make check ALL GREEN + make budget OK, re-baselined. Probes: param_infer (escape aliasing, null-predeclare recursion, defaults, missing args), full semantic battery, selfdiff AST identical.

What changed (pit-compiler/resolve.cm)

  • Parameter backward inference. A binding holding exactly one function literal (plus optional var f = null pre-declarations — the recursion pattern) whose every reference is a direct callee gets its parameters typed from the merge of all call-site arguments, evaluated in each caller’s scope, folded into the existing write-merge fixpoint as ordinary writes. A site that omits an argument merges the parameter default when present, otherwise poisons to unknown. Any non-callee reference (return, argument, store, record literal, capture-reassign) escapes the binding and disables inference.
  • Intrinsic return types in expression typing (mirrors streamline’s vetted intrinsic_return_types table: num for abs/floor/…, bool for is_*/starts_with/ends_with).
  • Write-model soundness fixes (both latent in v1):
    • Compound assignments (+=, -=, *=, /=, %=, &&=, ||=) are their own AST kinds and were never recorded as writes — a += on a binding whose declaration was narrower produced an unsound floor. Now recorded (+= under the dual text/num rule, arithmetic forms as num, logical forms as the right-hand side).
    • Reads of bindings with no recorded writes returned PENDING, which merge() silently drops — var b = param; b = 1 floored b as int while it held the parameter’s value. Such reads are now UNKNOWN; PENDING is reserved for keys the fixpoint will fill.
    • Closure writes (assignments at level != 0) now resolve to the defining binding via the semantic checker’s def_function_nr stamp, so captured bindings carry complete merges (still excluded from emitter output, but reads feeding other bindings now contribute real types).

Numbers (vs 58f6ecdd)

Static, compile corpus:

filefinal Δguards Δ
tokenize.cm−6.3%−8.4%
fold.cm−0.3%−0.2%
parse.cm−0.6%−0.6%
streamline.cm−1.7%−1.4%
mcode.cm−2.1%−2.2%
corpus total−1.7%−1.7%

Bench dynamics unchanged (kernels don’t hit the eligible-binding pattern in their hot loops). Compile time ~flat overall (tokenize.cm unit −5%, streamline.cm unit +4% — resolve’s link phase costs ~linear work per call site).

Debugging note — the seed is the real gate (attribution corrected)

The first v2 iteration resolved closure levels through a hand-built function-parent map, defaulting a missing .level to 0, and did not record compound assignments — real soundness holes, fixed above. During bring-up the shop/builder actors repeatedly SIGSEGV’d while compiling small programs (pit test suite’s -e unit, probe scripts), which was first attributed to v2-miscompiled firmware. A parallel debugging session (spawned for the deep-recursion crash) ran the daemon under lldb and proved those crashes (Pit_CallRegisterVMFunction + 17348) were actually a stale-fn GC bug in the direct-call op (58f6ecdd) — see the direct-call note. The v2 holes were still worth fixing on soundness grounds, but the SIGSEGV evidence belonged to the VM bug. Standing lessons: a compiler-lane crash during seeding may be a VM bug wearing a compiler costume — crash-log offsets lie for frame-pointer-less leaves (lldb doesn’t); recovery from a suspect seed is git checkout -- boot.qop boot/firmware && pkill -x pit && make smoke; and the semantic checker already stamps function_nr on every resolved name — use it instead of reconstructing scope chains.

Source: plans/archive/perf-2026-07/2026-07-08-resolve-v2.md