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 = nullpre-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_typestable: num for abs/floor/…, bool foris_*/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 = 1floored 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_nrstamp, so captured bindings carry complete merges (still excluded from emitter output, but reads feeding other bindings now contribute real types).
- Compound assignments (
Numbers (vs 58f6ecdd)
Static, compile corpus:
| file | final Δ | 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