Commit Graph

606 Commits

Author SHA1 Message Date
Brummel ba981689fc plan: 0064 iter 1 — let-binder type table + value-typed let exemption (#57)
First iteration of spec 0064 (#57 hardening). Tees the (def,binder)->Type
table out of the typecheck pass (the inferred let-binder type computed at
synth's Let arm and discarded today) and threads it into the linearity
walk, then seeds BinderState.is_value for value-typed let-binders from
the table -- closing false-positive class 3 (the class spec 0063
deferred). Two tasks, RED-first: Task 1 adds examples/c3_value_let.ail to
the harden_ownership_false_positives_are_clean list (RED:
use-after-consume on xnew); Task 2 threads the table end-to-end
(synth -> check_fn -> check_in_workspace -> check_workspace ->
check_module_with_visible -> Checker, all callers in one compile-atomic
task) and seeds is_value at Term::Let (GREEN), plus two in-source unit
tests (value-let clean, heap-let still errors).

Fixes 1/2/4 (local fn-param modes, alias-redirect, partition_eithers
rewrite) are later iterations of the same spec, explicitly out of scope
here. plan-recon mapped the threading path; the parse-the-bytes gate
fired on the inlined fixture (RED confirmed).

refs #57
2026-06-01 17:34:28 +02:00
Brummel f6a8607518 spec: 0064 harden ownership analysis part 2 (#57)
The #55 cutover (plan 0121, Task 4) is blocked: deleting
ParamMode::Implicit activates the strict linearity check universally,
but the migrated corpus does not pass cleanly. #56 (spec 0063) closed
two false-positive classes; universal activation surfaces three more
plus one genuine corpus over-consume. This spec is the "#56 part 2"
hardening, sibling to 0063, landing before the irreversible variant
deletion. It does NOT patch a fifth phase onto plan 0121 (project
rule: 2+ blocking classes = spec defect).

Four additive fixes, no schema/hash/codegen change, check stays
diagnostic-only:
- Type table teed from the typecheck pass (synth Let arm at lib.rs:3808
  computes the binder type then discards it) into (def,binder)->Type,
  threaded to linearity. The "stop discarding known information" fix,
  not a re-run of inference in the walk (ruled out by aaa70d4 / 0063).
- Class 3 (value-typed let-binder): is_value seeded from the table,
  extending 0063's per-binder flag to the let site it deferred.
- Class 1 (local function-typed binder): BinderState.fn_param_modes;
  callee_arg_modes consults local binders (params/lam from signature,
  let from the table) before the global table.
- Class 2 (let-alias of a borrowed value): an alias REDIRECT (not a
  state clone -- a clone would miss a real double-consume), root-
  resolved in use_var. Closes the contract 0008:340 carve-out.
- Class 4 (partition_eithers): genuine double-consume, body rewrite
  (destructure rest once); check unchanged, must-stay-RED fixture pins
  it.

Design call (user delegated): the binder->type table is the chosen
mechanism over local/shallow derivation -- the type is known and
discarded, and 0063's deferral of exactly this class is what produced
#57; a local-only patch re-creates the latent class. All five fenced
ail fixtures parse and reach the linearity stage with the recorded
exit codes (parse-every-block gate fired). Grounding-check PASS: every
load-bearing assumption ratified by a green in-source test or a live
RED trace.

refs #57
2026-06-01 17:26:18 +02:00
Brummel 7dc21234f0 plan: re-stage 0121 — fold borrow-over-value into the cutover (mono collision)
First implementation contact surfaced a plan defect: the
borrow-over-value reject cannot land green as a standalone pre-cutover
task. The prelude's polymorphic comparison builtins are
`(params (borrow a) …)` (eq/compare/lt/…); the mono pass
(apply_subst_to_type, subst.rs:165) specialises `a := Int/Bool/Float`
into `compare__Int(borrow Int, …)` — the language's OWN generated code
in the forbidden `(borrow value-type)` shape. The standalone check
broke compare_{int,bool}_mono_symbol_emits_branch_ladder.

Spec 0062's "value types always own" rule is universal, so generated
code must honour it too: the principled fix is a mono-pass coercion
`(borrow value-type) -> (own value-type)` (a no-op — value types carry
no RC, so own vs borrow emits no inc/dec and the branch-ladder IR is
unchanged). The check and the coercion are a matched pair and now live
together in Task 4 (Steps B1-B2), which touches the mono path anyway.

Pre-cutover green tasks are now Task 1 (borrow-return reject) + Task 3
(throwaway migration tool). Task 1's check_def destructure binds only
ret_mode (param_modes stays under `..`); Task 4 Step B2 widens it.

Spec gap to record at the next 0062 brainstorm touch: the
borrow-over-value rule needs its mono-coercion clause spelled out — the
spec tested only the authored case.

refs #55
2026-06-01 16:33:58 +02:00
Brummel a84ba596cf plan: eliminate the Implicit ownership default (0121)
Executable projection of spec 0062 (#55) into four tasks: (1) the
borrow-return reject, (2) the borrow-over-value reject, (3) a throwaway
`ail migrate-modes` pass (parse -> Implicit->Own, keep explicit
own/borrow -> print explicit), (4) the atomic cutover (run the
migration over the corpus + hand-fix the read-only-heap params the
now-universal linearity check flags, delete the variant + all
compile sites compiler-driven, parser bare-slot reject, reset the hash
pins, flip the leak pin, update both contracts, drop the throwaway).
Placeholder-free; all seven surface fixtures parse-gated against HEAD.

Spec 0062 marked approved (user, 2026-06-01) and its soundness
re-validated against post-#56 HEAD: the three own-return-provenance /
regime-A fixtures still exit 1 under [consume-while-borrowed]; #56's
application-is-a-borrow change does not weaken them (none is an
application of a borrowed binder).

plan-recon folded four spec-enumeration corrections into the plan:
- kernel migration target is raw_buf/source.ail, not the retired
  kernel_stub/source.ail the spec cited;
- FIVE hash pins shift, not the two the spec named (embed_export_hash,
  eq_ord_e2e, mono_hash_stability are the missed twins);
- design/contracts/0002-data-model.md also documents the "implicit"
  JSON form and is drift-anchored -> updates alongside 0008;
- real counts are 261 fn-type fixtures (spec ~208) and 17 fn_implicit
  references (spec 16); the compiler is the authoritative enumerator.

refs #55
2026-06-01 16:19:07 +02:00
Brummel 47fb328aca plan: harden ownership analysis — is_value_type + value exemption + app-borrow (0120)
Executable projection of spec 0063 into four tasks: (1) is_value_type
predicate in ailang-core::primitives; (2) application-is-a-borrow
(Term::App callee walked Position::Borrow); (3) value-type exemption
(BinderState.is_value seeded at param/pattern/lam sites, use_var
short-circuit); (4) on-disk RED->GREEN fixtures + workspace assertions.
Placeholder-free with exact code for every edit site. refs #56
2026-06-01 15:29:12 +02:00
Brummel 8cdac7ecef spec: harden the ownership analysis for universal activation (0063)
Precondition for #55 (eliminate ParamMode::Implicit, spec 0062). The
strict linearity check (use-after-consume / consume-while-borrowed)
runs today only on the ~45 of 258 all-explicit-mode fns; deleting
Implicit turns it on universally and surfaces ~21% false positives in
two classes — value-type params (Int/Bool/Float/Unit, never consumed)
and applied function params in HOFs (application is a borrow).

Two design questions settled against the live tool, not the model:
- Str is heap, NOT a value type for consume-tracking. drop.rs:490-492
  lowers only Int/Bool/Float/Unit to non-ptr; Str is a ptr, RC-dec'd,
  so a multi-consume of Str without clone is a real use-after-free. The
  value-type set is the unboxed {Int,Bool,Float,Unit}, narrower than
  is_primitive_name (which includes Str).
- Applying a function value is a borrow; passing it as an arg follows
  callee param_modes (unchanged). The recursion-passing HOF pattern is
  then consistent under a borrow f-param.

All three false-positive classes reproduced today against explicit-mode
fns (testable without #55) and recorded as RED fixtures; a heap
double-consume fixture stays RED to prove the exemption is type-gated.
grounding-check PASS. refs #56
2026-06-01 15:29:12 +02:00
Brummel c8b765614d spec: eliminate the Implicit ownership default (0062)
Design spec for #55, superseding #54. Deletes ParamMode::Implicit in a
single cutover -> binary {Own, Borrow}; no defaulted ownership mode
survives on any fn-type slot, authored or compiler-synthesised.

The grounding pass corrected the central architecture claim. Both #55
and the first draft assumed a new return-provenance check was needed to
reject an own-return aliasing a borrowed binder, "silently accepted
today". Running the candidate fixtures through the live `ail check`
falsified that: consume-while-borrowed already rejects all three
provenance paths -- direct passthrough, let-indirection, and borrow
escaping into a returned constructor (regime A). Own-return soundness
and regime A are already-green properties. The cutover therefore adds
exactly one new check (borrow-return rejection) plus one signature-level
reject (borrow-over-value), not two new provenance analyses.

Scope decisions ratified: keyword stays verbal (own/borrow), ParamMode
keeps its name; totality is strict including value-type slots (every
slot moded, (own value) trivial, (borrow value) an error, bare slot a
parse error) so an LLM author needs no heap/value knowledge.

The leak fix (spec 0061 symptom) falls out of Implicit->Own at the
synthesis sites: the old typechecker made Implicit and Own
indistinguishable (mode_eq), so setting Own changes no typecheck
outcome -- it only flips the codegen dec gate from skip to emit.

Out of scope: re-enabling borrow-returns (needs the escape/liveness
axis), multi-return, mode polymorphism.

grounding-check: PASS (7 assumptions ratified against named green tests;
all fenced ail blocks validated against the live tool).

refs #55
2026-06-01 14:21:26 +02:00
Brummel 7a99c5dbfa docs(fieldtest): record the typed-MIR milestone-close fieldtest
Milestone-close fieldtest of the typed-MIR work, run by a downstream
LLM author exercising only the public interface. Four curated scenarios
plus a reduction-bisect set, derived top-down from the milestone promise
(heap-Str-across-recur, new-over-user-ADT, cross-module print__<UserType>,
combined render loop).

Three [working] findings ratified: new-over-user-ADT + Show + print
(#51/#53 class), cross-module synthesised print, and heap-Str-across-recur
value correctness all produced correct output first try.

One [bug] found and orchestrator-verified: returning an owned heap-Str
from a callee fn across the call boundary leaks exactly one RC slab
(live=1) — producer-independent (str_concat, int_to_str), loop-independent,
silent (output correct). It sits in the coverage gap the curated
*_no_leak_pin fixtures leave open (they keep the heap-Str expression inside
main). Distinct from #49 (loop-carried within one frame); this is the
function-return leg. Routed to debug RED-first; blocks the deliberate
milestone-close until GREEN.
2026-06-01 01:53:46 +02:00
Brummel a378dad0aa docs(design): ratify the check→codegen boundary (mir.5, typed-MIR close)
mir.5 is the typed-MIR milestone's closing iteration. Its CODE half had
already converged before the iteration began, so mir.5 ships no code —
it ratifies into the design/ ledger the boundary the code already holds.

Verified at iteration entry (empirically, not from the spec sketch):
  - all four named re-derivers grep-clean in codegen: synth_with_extras,
    synth_arg_type, type_home_module, the second infer_module_with_cross;
  - lower_workspace takes &MirWorkspace (codegen consumes MIR);
  - MTerm::New is unreachable!() — raw-buf.4 desugars Term::New to
    (app T.new …) before codegen, so there is no element-type
    re-derivation left to relocate;
  - #51 / #53 (the element-type / new-T codegen crashes) are closed;
    their residue was fixed by ee4107c / 420f75f plus the New-desugar,
    not by a separate raw-buf patch track.

Ledger work (the mir.5 deliverable):
  - NEW design/contracts/0018-check-codegen-boundary.md: the invariant
    "codegen re-derives nothing; MIR is total over what check proved;
    a codegen arm that recomputes a fact instead of reading MIR is
    drift." Ratifier: lower_to_mir_ty.rs::callee_classification_builtin_and_static.
  - 0013-typeclasses invariant 2 retracted: codegen no longer re-resolves
    cross-module names via an import_map fallback; lower_to_mir::classify_callee
    resolves the reference once into Callee::Static and codegen consumes it.
  - 0003-pipeline.md: the "lower to MIR" line names the real stage
    (elaborate_workspace → MirWorkspace → lower_workspace) and a new
    paragraph states the boundary, cross-referencing 0018.
  - INDEX.md: boundary contract row added; qualified-xref re-pointed at
    lower_to_mir + 0018.
  - codegen_import_map_fallback_pin.rs doc-comment made honest — it pins
    the post-mono AST precondition classify_callee relies on, not a
    codegen-side resolution that no longer exists. Assertions unchanged;
    the test stays green.
  - spec 0060 gains a mir.5 refinement note recording the early code
    convergence.

Acceptance criteria 1-7 of docs/specs/0060-typed-mir.md are all met.
Full workspace suite green (exit 0, 0 failed, 2 ignored); 708 passed
carried from mir.4 (no test added or removed).

Not done here (deliberately): the milestone #7 (raw-buf) subsumption
note is an external Gitea tracker write; the /boss auto-mode classifier
declined it as an unauthorised external write and it is surfaced to the
user rather than worked around. The end-to-end milestone fieldtest
remains the deliberate manual close-gate before the tracker milestone
is marked done.

refs #51 #53
2026-06-01 01:34:37 +02:00
Brummel c5fd16a4eb feat(codegen): StrRep loop-carried Str ⇒ Heap, close the #49 recur leak
mir.4 closes bug #49 (a heap-Str loop binder replaced across `recur`
leaks every superseded slab; the loop result leaks too) structurally,
by making every Str a loop binder holds OR a loop returns an owned heap
slab — then deleting the `!is_str` carve-out from BOTH codegen gates
that carried it. The leg-by-leg `!is_str` patches are gone.

Mechanism (the milestone thesis: representation in MIR, codegen reads
it). lower_to_mir sets rep = StrRep::Heap on every Str literal in a
loop-carried position; codegen's MTerm::Str arm promotes a Heap literal
via ailang_str_clone (a fresh ailang_rc_alloc slab, rc_header refcount
1). Three promotion positions, all in the Loop/Recur lowering:
  1. binder seed inits (is_str_ty on the binder type);
  2. recur args at Str-binder positions (read off the innermost
     loop_stack frame);
  3. exit-arm tail result literals (promote_tail_str_literals walks the
     loop body's tail positions when the loop returns Str).
With every loop-carried Str now owned-heap, both gates lose !is_str:
the recur superseded-value dec (lib.rs) frees each intermediate slab;
the loop-result trackability gate (drop.rs:493) lets the caller's
scope-close free the final slab.

Why both gates, and the planning-time error this corrects. The original
plan/spec scoped the deletion to the recur dec gate only, reasoning the
#49 loop result is "consumed by print". The implement-orchestrator
landed that scope (Tasks 1-2) clean and correctly BLOCKED: it took #49
from live=3 to live=1, the residual being the loop RESULT. I verified
the diagnosis empirically:
  - print BORROWS its arg (prelude: `(let s (show x) (do io/print_str
    s))`, the eob.1 heap-Str discipline), so `(print s)` does not free
    the loop result; the caller's let-binder does, via drop.rs:493.
    "consumed by print" != "freed by print" — that was the error.
  - Deleting drop.rs:493's !is_str takes #49 and the recur-literal
    fixture to live=0.
  - Deleting drop.rs:493 WITHOUT exit-arm promotion SIGSEGVs (exit 139)
    on a loop returning a static Str literal — hence the third
    promotion position and the static-exit witness.
The agent surfaced a real spec defect via an empirical BLOCK rather
than guessing; I corrected the spec refinement note (both gates, three
positions) and completed the loop-result leg inline, the context
already loaded from verifying the BLOCK (CLAUDE.md "already loaded
context" carve-out). drop.rs:493's Str carve-out is now sound to delete.

Boundary (asserted ill-typed, not constructible): a borrowed static-Str
Var seeded/recur'd into a consumed Str loop binder — rejected upstream
by uniqueness (a consumed binder position is owned).

Verification (orchestrator-run): all four leak fixtures reach live=0
under AILANG_RC_STATS — #49 (xyyy), recur-literal (reset), static-exit
(result), and the f488d31 boxed-ADT guard (2, no regression). The #49
#[ignore] is lifted. Full workspace: 708 passed / 0 failed / 2 ignored
(mir.3b baseline 702/0/3; +6 passed, -1 ignored = #49 lifted; the 2
remaining ignores are doctests). ir_snapshot: no drift (the Heap
promotion does not reach non-loop-carried literals — the
non_loop_str_literal_stays_static pin confirms at the unit level).
Neither CLAUDE.md lockstep pair touches Str representation.

New witness fixtures: examples/loop_str_recur_literal_no_leak_pin.ail
(recur-arg leg) and examples/loop_str_static_exit_no_leak_pin.ail
(loop-result leg / static-exit soundness). New pins: lower_to_mir_ty
exit-arm producer pin + the flipped seed/recur-arg pins; two runtime
leak pins alongside the lifted #49.

closes #49
2026-06-01 00:54:44 +02:00
Brummel 2536b5f535 plan(mir.4): StrRep loop-carried Str ⇒ Heap, delete the recur !is_str gate
mir.4 closes bug #49 (a heap-Str loop binder replaced across `recur`
leaks every superseded str_concat slab, live=3) structurally rather
than with a fourth leg-by-leg patch.

Design calls made in planning (folded into the spec as the mir.4
refinement note):

- Mechanism: producer-side representation, codegen reads it — the
  milestone thesis. lower_to_mir sets rep = StrRep::Heap on every
  MTerm::Str literal that flows into a Str loop-binder alloca (seed
  inits AND recur args at Str-binder positions); codegen's MTerm::Str
  arm gains a Heap leg that promotes the static literal via
  ailang_str_clone (fresh ailang_rc_alloc slab, rc_header refcount 1).
  No special-casing in the codegen Loop/Recur store arms — the clone is
  emitted automatically when the Str{Heap} node is lowered.

- Recur args are promoted too, not just seeds: `(recur "reset" …)` is
  well-typed (verified: `ail check` accepts it), and a seed-only
  promotion would leave a static literal under the now-unconditional
  dec — a constructible UB hole. Soundness over minimalism on the
  highest-risk RC/drop surface.

- Only the recur dec gate (lib.rs:2231) loses !is_str. The drop.rs
  loop-RESULT trackability gate (drop.rs:493) STAYS conservative:
  deleting it needs a broader "every Str a loop can return is
  owned-heap" guarantee (a static exit-arm literal breaks it) and is
  not required by the #49 acceptance (the #49 loop result is consumed
  by print). drop.rs is untouched.

- Boundary (out of scope, asserted ill-typed): a borrowed static-Str
  Var seeded/recur'd into a consumed Str loop binder is rejected
  upstream by uniqueness (a consumed binder position is owned), so it
  is not constructible.

Plan 0119 carries exact code per step: producer rep promotion + a
loop-frame-driven Str-binder mask for recur args (reusing the existing
ctx.loop_stack, no new ctx field), the codegen Heap leg, the gate
deletion, the #[ignore] lift on the #49 pin, a recur-literal witness
fixture + runtime pin proving the recur-arg leg, and the full-suite +
ir_snapshot verification. The existing lower_to_mir_ty Static-seed pin
flips to assert Heap.
2026-06-01 00:34:18 +02:00
Brummel eba1be8d9d feat(codegen): fill MArg.mode for App args, read the anon-temp drop gate off it, delete module_def_ail_types (mir.3b)
Second of two iterations delivering spec 0060's mir.3 row (plan
docs/plans/0118-mir.3b-marg-mode-and-table-delete.md). lower_to_mir's
Term::App arm fills each MArg.mode from the resolved callee's
param_modes (the sig = synth_pure(callee) it already holds); codegen's
emit_call anon-temp borrow-slot drop gate reads arg.mode instead of
re-looking-up the callee's param_modes from the module_def_ail_types
table; and — since that gate was the table's only reader — the
module_def_ail_types field plus all its construction and threading are
deleted.

Two refinements to the spec's mir.3b sketch, settled in planning from a
focused recon and recorded in spec 0060:

1. module_def_ail_types is deleted in mir.3b, not mir.5. A grep proved
   self.module_def_ail_types had exactly ONE reader (the anon-temp
   gate); the own-param drop reads param_modes from the def's own f.ty,
   not this table. Once the gate moves onto MArg.mode the table is dead,
   so it is removed now rather than carried as dead code to mir.5. The
   mir.5 row's "last re-derivation residue" shrinks to the
   element-type / Term::New (New.elem) work.

2. MArg.mode is filled for App args only; MTerm::Let.mode is not filled.
   Only App args have a real per-arg mode source (the callee
   Type::Fn.param_modes). Do args (EffectOpSig has no param modes), Ctor
   args (no per-field mode), and Recur args (loop binders carry no mode)
   stay Mode::Owned. Let.mode has no source (ast::Term::Let has no mode
   field) and no consumer (the let-drop gate reads consume, not
   Let.mode), so it stays Owned — filling it would invent a value
   nobody reads.

Safety property held: MArg.mode is filled from the same callee fn-type
the gate read from the table, so the drop fires identically. For
(app RawBuf.get (app RawBuf.set …) 0), RawBuf.get's param 0 is borrow,
so args[0].mode = Borrow — exactly the value module_def_ail_types
yielded. Confirmed by the anon-temp witness staying green.

ParamMode -> Mode conversion: Borrow -> Mode::Borrow; Own and Implicit
(the Implicit ≡ Own contract) -> Mode::Owned. The own-param drop keeps
reading ParamMode off f.ty (Type/ParamMode imports retained, live
readers at lib.rs:1356/1507).

Also retires three now-stale doc comments that named the deleted
module_def_ail_types field (ailang-check/src/lib.rs, uniqueness.rs, and
the codegen_import_map_fallback_pin test doc) — a direct consequence of
the deletion, reworded to the current architecture.

Verification (orchestrator, post-implement inspect): diff matches the
plan (App-arg m_args built before m_callee consumes sig — the benign
borrow-order deviation the plan's note flagged); module_def_ail_types
grep-clean across all of crates/; cargo build --workspace clean (no
unused/dead_code); cargo test --workspace 702 passed / 0 failed / 3
ignored (+1 = the new producer pin app_arg_carries_callee_borrow_mode;
no #[ignore] added). The anon-temp drop-correctness witness
raw_buf_owned_drop_balances_rc_stats stays live=0, now driven by
MArg.mode; the other RC-stats leak pins green; #49 stays #[ignore]
(mir.4); #51/#53 build guards green.

mir.3 is now complete (3a relocated consume_count + deleted the second
uniqueness run; 3b filled the mode annotation + deleted
module_def_ail_types). Remaining: mir.4 (#49 StrRep RED->GREEN),
mir.5 (element-type/Term::New + ledger).
2026-05-31 20:10:25 +02:00
Brummel 0bd7f47fad plan: mir.3b fill MArg.mode, switch the anon-temp gate onto it, delete module_def_ail_types
Executable plan for the second of two iterations delivering spec 0060's
mir.3 row. A focused recon (the anon-temp gate + the per-arg mode
sources) established two refinements to the spec sketch, both locked in
the plan:

1. module_def_ail_types is deleted in mir.3b, not mir.5. A grep proved
   self.module_def_ail_types has exactly ONE reader — the emit_call
   anon-temp borrow-slot drop gate. The own-param drop reads param_modes
   from the def's own f.ty, not this table. So switching that gate onto
   MArg.mode leaves the table dead; deleting it now is the clean move.
   mir.5's "last re-derivation residue" shrinks to element-type/Term::New.

2. MArg.mode is filled for App args only; MTerm::Let.mode is not filled.
   Only App args have a real per-arg mode source (the callee
   Type::Fn.param_modes). Do/Ctor/Recur args have no per-arg mode
   (EffectOpSig/Ctor.fields/loop-binders carry none) -> stay Owned
   default. Let.mode has no source (ast::Term::Let has no mode field)
   and no consumer (the let-drop gate reads consume, not Let.mode) ->
   stays Owned.

Safety property (recon-confirmed on the #43 anon-temp witness): MArg.mode
is filled from the same callee fn-type the gate read from the table, so
the drop fires identically. For (app RawBuf.get (app RawBuf.set …) 0),
RawBuf.get's param 0 is borrow -> args[0].mode = Borrow, exactly the
value module_def_ail_types yielded.

Four tasks: (1) producer fills App-arg MArg.mode via a param_mode_at
helper; (2) codegen gate reads arg.mode + delete module_def_ail_types
(field + construction + threading, compiler-completeness-checked);
(3) producer pin (App arg mode == Borrow) + the live=0 anon-temp
acceptance (raw_buf_owned_drop_balances_rc_stats); (4) record the
refinements in spec 0060. No new .ail fixture (reuses raw_buf_drop_min.ail).
2026-05-31 20:00:12 +02:00
Brummel 6bc0b501cb feat(codegen): relocate per-binder consume_count into MIR; delete codegen's second uniqueness run (mir.3a)
First of two iterations delivering spec 0060's mir.3 row (plan
docs/plans/0117-mir.3a-consume-count-relocation.md). The per-binder
consume_count — the only thing codegen read from its second
infer_module_with_cross run — now lives on a new binder-keyed
MirDef.consume: BTreeMap<String,u32>, filled once by lower_to_mir on the
post-mono body. Codegen builds its existing (def,binder)->consume_count
lookup from those maps and the three scope-close drop sites (own-param,
let-binder, match-arm) read it; the codegen uniqueness run, its
UniquenessTable field, and the import are deleted.

mir.3 is split into two iterations (recorded in spec 0060): the named
deletion depends ONLY on consume_count. All three drop sites read
consume_count from self.uniqueness, while the modes they also gate on
(param_modes for the own-param dec, scrutinee_is_owned for the match
dec) come from the type, not the uniqueness pass (UniquenessInfo carries
no mode). So mir.3a relocates consume_count and ships the deletion;
mir.3b (next) fills MArg.mode/MTerm::Let.mode and switches emit_call's
anon-temp gate onto MArg.mode. The split halves the change at the
codebase's highest-risk surface (RC/drop correctness, the raw-buf leak
saga) and makes each half independently verifiable against the live=0
leak pins.

Design: consume_count lands on MirDef (binder-keyed), NOT on MArg. The
drop sites key by (def, binder_name) — a per-binder aggregate — while the
spec-sketched MArg.consume_count is per-arg-position and never reaches
them; the per-def map matches the UniquenessTable's own keying and all
three binder kinds (param/let/pattern) uniformly. MArg.consume_count
stays a mir.1 default. Source = run check's infer_module_with_cross
inside lower_to_mir post-mono (the synth_pure single-engine-re-walked
pattern); retaining a check-time result is blocked (check's uniqueness
is pre-mono and runs on-demand-from-codegen, never in check_workspace,
so it would miss the mono specialisations). cross_module_types
(= codegen's module_def_ail_types) is rebuilt in elaborate_workspace
from the post-mono workspace.

Safety property held: this is a PURE RELOCATION of an identical
computation — infer_module_with_cross ran on the post-mono module in
codegen and now runs on the same post-mono module in lower_to_mir, so
drop placement is byte-identical. cross_module_types matches
module_def_ail_types exactly (both insert f.name->f.ty over post-mono
Def::Fn), confirmed by the leak pins staying green.

Verification (orchestrator, post-implement inspect): diff matches the
plan; codegen grep-clean for infer_module_with_cross + UniquenessTable
(now ailang-check-internal); cargo build --workspace clean (no errors,
no unused warnings — module_def_ail_types survives for emit_call's
anon-temp param_modes gate); cargo test --workspace 701 passed / 0
failed / 3 ignored (+1 = the new producer pin
mirdef_consume_is_populated_for_let_binder; no #[ignore] added). The
live=0 drop-correctness witnesses all green:
alloc_rc_loop_valued_rawbuf_binder_drops_at_scope_close (#43/#47),
alloc_rc_heap_loop_binder_replaced_by_recur_does_not_leak,
alloc_rc_print_int_does_not_leak_show_result_str. #49 stays #[ignore]
(mir.4); #51/#53 build guards green.
2026-05-31 19:48:57 +02:00
Brummel 69749fc3e1 plan: mir.3a relocate consume_count into MIR, delete codegen's second uniqueness run
Executable plan for the first of two iterations delivering spec 0060's
mir.3 row. Two plan-recon passes (mode/consume/drop-placement surface +
the uniqueness-pass signature) established that the named deletion (the
second infer_module_with_cross run) depends ONLY on consume_count: all
three codegen scope-close drop sites read consume_count from
self.uniqueness, while the modes they also gate on (param_modes for the
own-param dec, scrutinee_is_owned for the match-arm dec) come from the
type, not the uniqueness pass (UniquenessInfo carries no mode). So mir.3
splits:

- mir.3a (this plan): relocate consume_count; delete the second run.
- mir.3b (next): fill MArg.mode / MTerm::Let.mode from the type modes
  and switch emit_call's anon-temp borrow-slot gate onto MArg.mode.

The split halves the change at the codebase's highest-risk surface
(RC/drop correctness, where the raw-buf leak saga lived) and makes each
half independently verifiable against the live=0 leak pins.

Design decisions locked in the plan:

- consume_count lands on a new binder-keyed MirDef.consume:
  BTreeMap<String,u32>, NOT on MArg. The recon surfaced the central
  structural tension: the drop sites key consume_count by
  (def, binder_name) — a per-binder aggregate — while the spec-sketched
  MArg.consume_count is per-arg-position and never reaches them. The
  per-def binder map matches both the UniquenessTable's own keying and
  all three binder kinds (param/let/pattern) uniformly. MArg.consume_count
  stays a mir.1 default (no consumer). Spec 0060 is updated to record
  this (Task 5).

- Source = run check's infer_module_with_cross inside lower_to_mir on
  the post-mono module (the synth_pure single-engine-re-walked pattern).
  The alternative of retaining a check-time result is blocked: check's
  uniqueness runs pre-mono and on-demand-from-codegen, never during
  check_workspace, so a retained result would miss the mono
  specialisations. cross_module_types (= codegen's module_def_ail_types,
  module->def->Type) is rebuilt in elaborate_workspace from the post-mono
  workspace.

Key safety property carried in the plan: this is a PURE RELOCATION of an
identical computation. infer_module_with_cross ran on the post-mono
module in codegen and now runs on the same post-mono module in
lower_to_mir — same function, same input, same output — so drop
placement is byte-identical. The live=0 leak pins (raw_buf_loop_no_leak,
loop_recur_heap_binder, print_no_leak) are the value-correctness gate.

Five tasks: (1) add MirDef.consume; (2) elaborate_workspace builds
cross_module_types + lower_module runs the pass and fills consume;
(3) codegen builds its (def,binder)->consume lookup from MIR, the three
drop sites read it, delete the run + field + import; (4) producer pin +
leak-pin acceptance + grep-clean; (5) record the split in spec 0060. No
new .ail fixture (producer pin reuses new_counter_user_adt.ail).
2026-05-31 19:40:54 +02:00
Brummel a6fd93adba feat(codegen): pre-resolve the App callee in MIR; delete is_static_callee + type_home_module (mir.2)
Second re-deriver removal of the typed-MIR milestone (spec
docs/specs/0060-typed-mir.md, plan docs/plans/0116-mir.2-callee-static.md).
Static-callee resolution moves out of codegen and into lower_to_mir:
every Term::App callee is classified once, mirroring check's own synth
Term::Var ladder (lib.rs:3409-3582), and emitted as a resolved Callee.
Codegen reads the callee identity off MIR and routes each kind to the
right lowering. The codegen re-derivers is_static_callee and
type_home_module are deleted, and the lower_app <-> is_static_callee
lockstep pair is struck from CLAUDE.md.

Callee reshape (beyond the spec's original {module, fn_name} sketch —
spec 0060's Concrete-code-shapes block is updated in this iteration):

  pub enum Callee {
      Static  { module, fn_name, sig: Type },  // user/prelude fn -> emit_call, module pre-resolved
      Builtin { name, sig: Type },             // operator/intrinsic -> inline opcode lowering
      Indirect(Box<MTerm>),                    // dynamic: fn-pointer / closure / shadowed name
  }

Two forced refinements, both settled in planning:

- sig:Type on each resolved variant. drop::synth_callee_ret_mode reads
  the callee ret_mode, today off Callee::Indirect(inner).ty(). A
  resolved callee has no sub-term, so the sig (= synth_pure(callee),
  mode-preserving) is the lossless replacement. This is NOT a mir.3
  pull-forward: the MArg param-mode / consume_count fields stay at their
  mir.1 defaults. Without it, every statically-resolved call would lose
  its drop signal and the RawBuf / int_to_str RC-stats pins mir.1b fixed
  would regress.

- Builtin variant. Operators and the str-num builtins (+, not,
  str_concat, ...) are lowered inline by name and have no module/fn_name;
  a separate variant is cleaner than a sentinel module. The classifier
  decides Builtin vs Static from CHECK'S OWN resolution (a name in
  env.globals with no owning module is a builtin), never a copy of
  codegen's old is_static_callee allowlist. The recon's divergence audit
  confirmed check's builtin set and codegen's inline-arm set match
  exactly, so the single-engine rule is mechanically satisfiable with no
  env threading gap.

type_home_module is fully deletable: a workspace-wide grep confirmed no
program references a type-scoped T.fn as a value, so its only other
consumer (resolve_top_level_fn) collapses to the module-name fallback
with no behaviour change. (The spec's "x3 mirrors" wording over-counted;
the live surface was the definition plus two call sites.)

Alternatives rejected during planning: sentinel module in Static
(ugly); builtins left as Indirect (breaks — no fn-pointer for an
operator); name-rewrite Static{name,sig} keeping lower_app's by-name
dispatch (blurs builtin/user and still needs sig). The mir.1b
re-synth-strictness trap (post-mono synth_pure re-unify surfacing
mode-strip / bare-vs-qualified / metavar leaks) did NOT fire under the
new producer path — qualify_local_types mode-preservation and the $u
wildcard normalisation from mir.1b held.

Verification (orchestrator, post-implement inspect): diff matches the
plan; grep-clean for is_static_callee + type_home_module across
ailang-codegen and ail; cargo build --workspace green; cargo test
--workspace 700 passed / 0 failed / 3 ignored (no #[ignore] added this
iteration — the 3 are the pre-existing #49 Str-leg pin + two doctests).
Acceptance witnesses green: #53 (mono_new_over_user_adt_builds_and_runs),
#51 (rawbuf_new_int_size_only_builds_and_runs still builds), show_print
cross-module (print_user_adt_runs_end_to_end), the new mir.2 pins
(mir2_resolved_callee_kinds_run_end_to_end, callee_classification_*,
strengthened new_over_user_adt_carries_node_types asserting Callee::Static).
#49 heap-Str loop binder stays #[ignore] (lifts at mir.4).

Two new examples/ fixtures back the new pins (classify_pin.ail for the
producer classification pin, mir2_callee_kinds.ail for the E2E),
ail-parse / round-trip clean.

Forward note (cycle-close architect): crates/ail/tests/codegen_import_map_fallback_pin.rs:14-15
doc prose names lower_app's cross-module arm and synth_with_extras as
failure-mode targets — both now deleted (mir.2 / mir.1b). The pin's
protected invariant is still valid and green; the prose is stale and
wants a doc-honesty reword, left out of this iteration's footprint.
2026-05-31 19:27:56 +02:00
Brummel d81ea93de6 plan: mir.2 Callee::Static pre-resolved, codegen stops re-deriving the callee
Executable plan for spec 0060's mir.2 iteration: relocate static-callee
resolution from codegen into lower_to_mir. Two plan-recon passes (codegen
consumer surface + check-side producer surface) mapped the resolution
frontend; this plan locks three design decisions made during planning,
ahead of the tasks:

1. The Callee bridge enum is reshaped beyond the spec sketch: a third
   variant Builtin{name,sig} (operators / str-num intrinsics have no
   module/fn_name and are lowered inline by name), and a sig:Type on each
   resolved variant. The sig is the lossless replacement for the pre-mir.2
   Callee::Indirect(inner).ty() read that drop's synth_callee_ret_mode
   depends on; it is synth_pure(callee), mode-preserving. NOT a mir.3
   pull-forward — the MArg param-modes stay at mir.1 defaults. Spec
   0060's Concrete-code-shapes Callee block is updated as part of the
   iteration (Task 5).

2. Classification mirrors check's own synth Term::Var ladder
   (lib.rs:3409-3582), never copies codegen's is_static_callee allowlist.
   The recon's divergence audit confirmed check's builtin set and
   codegen's inline-arm set match exactly, so the single-engine rule is
   mechanically satisfiable with no env threading gap.

3. type_home_module is fully deletable: a workspace-wide grep confirmed no
   program references a type-scoped T.fn as a value, so its second
   consumer (resolve_top_level_fn) collapses to the module-name fallback
   with no behaviour change. The spec's "grep-clean" acceptance holds; the
   spec's "x3 mirrors" wording over-counts (def + 2 live sites).

Five tasks: (1) reshape Callee; (2) lower_to_mir classify_callee +
App arm; (3) the atomic codegen consumer switch (App arm/drop/lower_app
split/delete is_static_callee+type_home_module/resolve_top_level_fn);
(4) pins (strengthen #53 to assert Callee::Static, add a three-way
classification pin) + workspace suite; (5) strike the lower_app <->
is_static_callee lockstep row from CLAUDE.md, update spec 0060, clean
stale comment references. The classify_pin fixture is ail-parse-gated
(exit 0).
2026-05-31 19:10:37 +02:00
Brummel 1a1a68df8b plan: mir.1b — codegen consumes MIR (the atomic switch)
Second half of spec iteration mir.1, planned against the landed mir.1a
infrastructure (135f4ce). This is the project's largest single
mechanical change and it is atomic: codegen's entire lowering walk
flips from &Term to &MTerm with no compiling intermediate (dual-path is
spec-forbidden). plan-recon proved the blast radius with a compile stub
(flipping only lower_term's signature → 58 cascading errors across
lib.rs/drop.rs/match_lower.rs/escape.rs/lambda.rs, baseline restored
green). The plan gives a Term→MTerm correspondence rule for the
mechanical arm renames (the build gate is the totality checker), exact
before→after for the judgement sites (the 9 synth_arg_type reads → ty(),
the dual-source Emitter, escape pointer-identity, entry-point
signatures, CLI diagnostics), and a satisfiable gate per task.

Four recon open-questions resolved by orchestrator judgement (within
the approved spec's intent; the spec's MirModule sketch is illustrative
and exact shape is the planner's):
- OQ1: MirModule gains `ast: Module` — codegen reads structural data
  (Type/Const/ctor/intrinsic markers/symbol tables) from .ast, typed fn
  bodies from .defs. MirWorkspace stays the single artefact (it now
  contains the post-mono AST); structural reads were never a
  re-derivation, so this does not reintroduce one. Also resolves the
  intrinsic-fn gap (lower_module skips intrinsic bodies; codegen
  iterates ast.defs and looks up MirDef bodies by name).
- OQ2: emit_fn borrows mir_def.body:&MTerm once and shares it between
  escape::analyze_fn_body and lower_term (pointer identity is only
  stable within one borrowed tree); casts flip to *const MTerm.
- OQ3: MTerm::New's codegen arm stays unreachable! — New is desugared
  away before codegen (RawBuf→payload, monomorphic user-ADT new→dotted
  App), exactly as Term::New is today; #51 builds unchanged.
- OQ4: the CLI build/emit-ir paths drop their separate check_workspace
  call and let elaborate_workspace's Err drive diagnostics (one check,
  not two); `ail check` stays on check_workspace.
- emit_ir keeps &Module, calls elaborate_workspace internally,
  converts Err(diags)→CodegenError; its builtin-only in-source tests
  check clean without a prelude.

Task structure: Task 1 (additive) extends MirModule + populates ast +
adds codegen's ailang-mir dep, gate cargo test --workspace. Task 2 (the
atomic flip) converts the whole codegen crate including its tests, gate
cargo test -p ailang-codegen (deliberately excludes crates/ail so the
not-yet-threaded external callers don't fail this gate). Task 3 threads
the crates/ail callers (CLI build/emit-ir/staticlib + 6 e2e callers),
gate cargo test --workspace == 102 ok (the mir.1a baseline), #51/#53
build+run, synth_with_extras/synth_arg_type grep-clean.

refs #49
2026-05-31 14:23:26 +02:00
Brummel 438a009b83 plan: mir.1a — typed-MIR infrastructure (additive half of spec mir.1)
First of two plans for spec iteration mir.1 (docs/specs/0060-typed-mir.md).
mir.1 is the project's largest iteration — a full codegen frontend
switch from walking &Term to walking &MTerm — and it does not fit one
bite-sized plan, because the switch is atomic: there is no compiling
intermediate between "all Term" and "all MTerm" (dual-path is
spec-forbidden). The MIR *producer* side, by contrast, compiles and
tests in isolation. So mir.1 is sequenced across two plans:

- mir.1a (this plan): the additive producer — new leaf crate
  ailang-mir (MTerm/MArg/Callee/Mode/StrRep, structurally complete over
  all 17 Term variants plus the Str split), ailang-check::lower_to_mir
  (post-mono walk that calls canonical synth per node and maintains
  locals/loop_stack exactly as synth does — no second type engine), and
  ailang-check::elaborate_workspace (check → desugar+lift → mono →
  lower_to_mir). Nothing consumes MIR yet; codegen untouched; whole
  suite stays green; three ty-fill pins load the #51/#53/#49 witnesses
  as fixtures and elaborate the full workspace (prelude included).

- mir.1b (next plan, against the landed types): flip codegen's walk to
  &MTerm, delete synth_with_extras + synth_arg_type (9 call sites across
  lib.rs/drop.rs/match_lower.rs), thread the 18 lower_workspace* callers,
  switch the CLI build path to elaborate_workspace.

Both together satisfy the spec's mir.1 row. The split is plan
granularity, not a spec change — the spec's mir.1 deliverable is
unchanged.

Orchestrator design calls baked in (planner judgement, no user fork):
dedicated MTerm::Str{lit,rep} variant (Str-split installs the mir.4
hook at the #49 loop seed); emit_ir keeps &Module (builds MIR
internally — minimal blast radius); lower_to_mir scaffolding mirrors
mono.rs:771-816; Match-arm binding reuses synth's own type_check_pattern.

Recon (plan-recon) mapped the full deletion blast radius for mir.1b
(9 synth_arg_type sites, not the 3 the focus-hint assumed; 18
lower_workspace* callers). The two new witness fixtures
(new_rawbuf_size_only, new_counter_user_adt) were verified ail
check-clean during planning.
2026-05-31 13:51:33 +02:00
Brummel 8bc2972594 spec: typed-mir — place lower_to_mir post-mono, correct one-engine framing
plan-recon flagged that monomorphise_workspace runs between check and
codegen, and codegen lowers the post-mono workspace. The first draft of
this spec placed lower_to_mir as the final phase of check_workspace
(pre-mono) and claimed "nothing re-walks the AST". That is wrong on
both counts:

- Built pre-mono, MIR would not carry the monomorphic specialisations
  monomorphise_workspace appends — exactly the post-mono bodies codegen
  emits today — reintroducing the check↔codegen gap this milestone
  exists to close.
- synth is a pure `&Term → Type` function and the authoring AST carries
  no node-ids, so MIR cannot be a side-table read off a check pass; it
  is built inline by a walk. The honest property is therefore not "no
  second walk" but "no second *engine*": codegen's synth_with_extras is
  a hand-copied mirror of synth that drifts (every raw-buf bug is a
  drift point). The milestone replaces the mirror with a single
  post-mono call to canonical synth, materialised as MIR.

Corrections: architecture diagram and data flow now run
check → lift → monomorphise → lower_to_mir; lower_to_mir is wrapped by
a new front-end entry elaborate_workspace that subsumes the lift+mono
orchestration the CLI does today; check_workspace stays for the
diagnostics-only path (`ail check`). Node count fixed 27 → 17 Term
variants (ast.rs:436).

Re-dispatched grounding-check after the edit (post-PASS edit
invalidates the prior report) — PASS, all load-bearing post-mono
assumptions ratified against live code (main.rs build order,
mono.rs synth use, 17-variant Term, no node-ids).
2026-05-31 13:24:08 +02:00
Brummel 207c63649f spec: typed-mir check→codegen boundary contract
Open a new milestone introducing a typed mid-level IR (`ailang-mir`) as
the single artefact crossing the check → codegen boundary, so codegen
re-derives nothing.

Root cause (architect drift review): the boundary today carries no typed
artefact. `crates/ail/src/main.rs:2293` calls `check_workspace`, discards
the result, and hands `lower_workspace` the raw `Workspace`.
`CheckedModule` (ailang-check/src/lib.rs:1283) carries only top-level
(Type, hash). Codegen therefore re-runs four independent derivations of
what check already proved — type synthesis (`synth_with_extras`), callee
resolution (`type_home_module` ×3 + `is_static_callee`), uniqueness/mode
(`infer_module_with_cross`, second run), and Str representation (`!is_str`
recur gate). Every raw-buf bug (#43/#46/#47/#49/#51/#53, all "check-clean,
build-divergent") is a point where one of those re-derivations disagreed
with check and got patched one leg at a time.

Honest framing: this is removal of a re-derivation architecture, not a
RED→GREEN of crashing programs. #51 and #53 build today (patched by
ee4107c / 420703d) and are regression guards — they must keep building as
the codegen mirrors that hold them green are deleted. #49 is the one open
leg (builds, leaks live=3, test #[ignore]'d) and is the single RED→GREEN.

Five iterations, each adding one MIR annotation class and deleting the
matching codegen re-deriver: mir.1 structural MIR + `ty`; mir.2
`Callee::Static`; mir.3 `Mode`/`consume_count`; mir.4 `StrRep` (#49
live=0); mir.5 element-type residue + ledger (new boundary contract,
retract 0013:106-120, fix 0003-pipeline model, INDEX, close #51/#53,
reset milestone #7 to not-met).

Forward rewrite, not revert (the prior raw-buf milestone close was wrong;
the language infrastructure is healthy and survives — only the codegen
re-derivation is the disease). Approach B: a separate typed MIR carrier,
authoring AST unchanged.

Gates: parse-every-block clean (all three .ail witnesses `ail check`-clean,
exit 0); grounding-check PASS (13 load-bearing assumptions ratified against
green tests / verified code facts).
2026-05-31 12:45:52 +02:00
Brummel 1b2d23ec42 fieldtest: raw-buf comprehensive — 4 examples, 8 findings (refs #7)
Comprehensive usability re-test of the RawBuf kernel extension in
natural LLM-author decompositions the prior field test (0058) did not
exercise: a Float RawBuf in a user ADT read through two borrow helpers,
a Bool flag buffer filled with a computed value, an Int fib buffer whose
fill reads its own writes plus two composed borrow summaries, and a
forbidden core-primitive element. All three working fixtures build, run
to expected values, and report live=0 across Float/Bool/Int widths —
the core RawBuf surface (borrow-helper composition, fill loops,
RawBuf-in-ADT, element widths) genuinely holds together.

Orchestrator triage corrected the fieldtester's run, which executed a
stale target/release/ail (built before the #50 fix). Every outcome was
re-verified against a fresh build:

- F2 (bare RawBuf ADT field unresolved) — RETRACTED: a stale-binary
  false positive. On a fresh build the bare name resolves in every
  configuration (ctor name == or != type name; builds/runs live=0). The
  #50 fix is correct and general.
- F7 (NEW, issue #51) — a `check`-clean program crashes `build`: a
  buffer whose element type is never observed by a later get/set
  defaults its element to Unit in monomorphisation and hits an
  unregistered `RawBuf_new__Unit` intercept. Affects a legitimate
  `RawBuf<Int>` used only for its size AND a forbidden locally-
  constructed element. Root: the `(new T ...)` desugar drops the
  explicit element annotation; honouring it reverses the milestone's
  § Term::New desugar decision, so it routes through brainstorm.
- F8 (process) — the field test ran stale; the fieldtester agent now
  builds from the current tree before running (plugin fix).

F1 (spec_gap) resolved here: the design ledger's §Series substrate ctor
wrote the storage field `(own (RawBuf a))`, which does not parse (`own`
is fn-param/ret only). Rewritten to the verified-authorable `(con RawBuf
a)`. The rest of the §Series listing is illustrative and its element-
less `(new RawBuf lookback)` construction is entangled with #51; a full
compile-verification of that listing belongs with the Series-substrate
work.

F5 (param-in rejects a forbidden core primitive, message names the set)
and F4/F3 (borrow composition; substrate composite) carry on. F6
(param-in set rendered as a Rust-debug list) filed as #52.

refs #7
2026-05-30 18:29:00 +02:00
Brummel 8e9f0f06a6 fieldtest: raw-buf — 4 examples, 6 findings (refs #7)
Downstream field test of the raw-buf surface as a consumer with only
the public interface (design ledger + `ail` CLI; no crate source).
Four fixtures under examples/fieldtest/, spec at
docs/specs/0058-fieldtest-raw-buf.md. Every recorded outcome
reproduced independently before commit.

Findings (3 bug, 1 spec_gap, 2 working):

- B1 [bug] `borrow (RawBuf a)` receiver is unusable. A fn taking
  `borrow (RawBuf Int)` that calls RawBuf.get/.size on the parameter
  even once is rejected `consume-while-borrowed` — the diagnostic
  names the wrong cause (the receiver is not consumed). The ledger
  advertises get/size as borrow-receiver ops, so the documented use
  is unreachable from any helper-function decomposition. This blocks
  Series (#8) — the milestone's own downstream raison d'être —
  whose at/total_count read through a `borrow (Series a)`.

- B2 [bug] An owned RawBuf threaded through a `loop` binder passes
  check but panics codegen `unknown variable: b` at build. A plain
  Int loop binder builds fine, so the fault is specific to an owned
  RawBuf loop binder. Breaks the check↔codegen agreement.

- B3 [bug] `param-not-in-restricted-set` omits the allowed set.
  design/models/0007 §4 promises the diagnostic "names the offending
  type and the allowed set"; the shipped message names the type, the
  type-var, and the home type but not `{Int, Float, Bool}`. A
  downstream author is told what is wrong but not what is right.

- B4 [spec_gap] RawBuf.set receiver double-consume is not enforced;
  two set calls on the same owned buffer silently alias the slab.
  Not raw-buf-specific (owned Str/ADT behave identically) — a
  pre-existing linearity-non-enforcement property. Recorded because
  RawBuf's own→own mutation is the first place a silent alias yields
  a mutated-in-place surprise. Ratify in the ledger or open a backlog
  item for full linear enforcement.

- W1 [working] `(new …)` sugar + inference-from-use across
  Int/Float/Bool builds and runs first try (sensor_pair prints 5.0);
  the Bool i1/i8 packing edge round-trips. The milestone's cleanest win.

- W2 [working] param-in reject fires for both Str and a user TypeDef.

Net: B1+B2 mean the only RawBuf programs that build today are
straight-line owned let-threading with literal indices — the
shipped-fixture shape. Helper decomposition (B1) and runtime-length
loop fill (B2) both fail. fieldtest does not self-resolve; routing
is the orchestrator's call.
2026-05-30 16:26:48 +02:00
Brummel fb0dd92a6c plan: raw-buf.6 kernel-stub-retirement (refs #7) 2026-05-30 15:53:54 +02:00
Brummel 559531806d plan: reserved-dollar-in-names — dollar-lexer-reservation (refs #44)
Executable plan for spec 0057 (committed c760570). Single iteration,
four tasks: (1) LexError::ReservedDollar variant + inline tokenize
guard + 3 in-source lexer tests (RED-first on the must-fail
dollar_in_ident_is_reserved); (2) fresh_binder doc-comment correction;
(3) a surface integration test pinning the
parse -> ParseError::Lex(ReservedDollar) surfacing chain; (4)
no-regression sweep (workspace suite + CLI checks on a comment-dollar
example and the #43 shadow idiom).

plan-recon verified all line anchors against HEAD (lex.rs:183
run-slice, lex.rs:67-76 enum, 19 in-source tests, desugar.rs:528-536
doc-comment, parse.rs:103/126/149, main.rs:1339, ct1_check_cli.rs:310)
and confirmed none of the three CLAUDE.md lockstep pairs apply.
Self-review caught and fixed two defects before hand-off: the
integration test imported the private lex/parse module paths
(corrected to the crate-root re-exports
ailang_surface::{parse, LexError, ParseError}), and the compile-gate
check confirmed no external exhaustive match on LexError exists, so
adding a variant cannot break compilation and the Task 1 RED is a
genuine runtime expect_err panic.
2026-05-30 14:51:41 +02:00
Brummel c76057008e spec: reserve $ in the Form-A lexer (refs #44)
#44 asks to enforce-or-retract the `$`-in-authored-binder-names
reservation that `fresh_binder` (ailang-core::desugar) relies on for
collision-free shadow-rename mints. This spec chooses enforcement, and
places it in the Form-A lexer rather than the check layer.

The placement is the load-bearing design decision. An earlier draft put
the reject in `pre_desugar_validation` (check layer), justified by "a
client could construct `.ail.json` directly, bypassing the lexer". The
user corrected the threat model: the client LLM author is forbidden from
emitting canonical `.ail.json` — it writes Form A exclusively, and only
the orchestrator hand-authors JSON in rare exceptions. So the entire
hallucinating-client attack surface is Form-A source, which always flows
through `tokenize`. That collapses the design:

- `$` is a reserved character (like `(`/`)`) with no legitimate authored
  use in any position — verified: zero authored `$` idents exist in the
  checked-in `.ail` corpus; the six `$` occurrences are all in comments.
  No positional split, so no AST-aware walker is needed — unlike `.`/`/`,
  which DO have legitimate uses (`std_list.map`) and therefore live in
  the check layer (`InvalidDefName`). The issue's premise "lex.rs
  reserves only `.`" is false on two counts and is corrected in the spec.
- The reject is a single new `LexError::ReservedDollar`, raised inline in
  the `tokenize` run-classification arm, surfaced through the existing
  `ParseError::Lex` -> `W::SurfaceParse` -> `surface-parse-error` channel.
  No new `CheckError`, no AST walker, no multi-entry-point wiring (the
  check layer has four entry points, only two of which currently run the
  pre-desugar pass — a trap the lexer placement sidesteps entirely).

Documented non-goals (honesty rule): the `.ail.json` deserialization path
stays unguarded (the orchestrator's self-responsible channel, scoped out
by the user); the `fresh_binder` probe-body simplification (issue Q4) is
not bundled — only its now-false doc-comment is corrected.

grounding-check PASS on the final bytes: all six load-bearing assumptions
ratified by green tests; all three Form-A example blocks parse-gate clean
(exit 0 today, by design — the reject is new behaviour). String- and
comment-internal `$` stay legal by scan order; no must-fail `$` fixture
goes under examples/ (the round-trip test parses every fixture there).
2026-05-30 14:41:04 +02:00
Brummel 1990147467 plan: unique-binder-names — desugar alpha-rename for #43 A2b (refs #43)
Executable plan for spec 0056. Three tasks:
- Task 1 (atomic compile unit): introduce a `Scope` struct threading
  two maps — `entries` keyed by each binder's effective (post-rename)
  name, `rename` mapping authored→effective for shadow detection and
  Term::Var resolution; add `fresh_binder` (<name>$<n>) and
  `rename_pattern_binders`; rewrite every binder site (Let, flat-Match
  pattern, Lam params, Loop binders) with rename-on-shadow, plus the
  LetRec arm and both def-boundary constructions, to the new API.
- Task 2: correct the UniquenessTable doc-comment; verify the four RED
  tests go GREEN; full-suite regression gate.
- Task 3: regression guard — a desugar unit test pinning that a
  shadowing let is alpha-renamed and that a letrec capturing the
  shadow-renamed binder still resolves (the entries-keyed-by-effective
  choice is what keeps capture detection correct under rename).

Recon surfaced two facts the spec's sketch did not: the scope is a bare
BTreeMap (so resolved_name/bind live on a new Scope type, not the map),
and the LetRec capture-detection reads effective names from the
desugared body — hence entries are keyed by effective name. Both stay
internal to desugar.rs, preserving acceptance criterion 5 (no change to
uniqueness.rs logic / codegen gates / linearity.rs). Both no-change
consumers verified: match_lower.rs:795 keys by the emitted (renamed)
pattern binder; linearity builds table and lookups from the same
desugared tree.
2026-05-30 12:35:07 +02:00
Brummel 0015f3dad1 spec: unique binder names per fn — A2b leg of RawBuf drop-leak (refs #43)
The last open leg of the #43 owned-heap drop-leak cluster is the
UniquenessTable shadow-name collapse: the side-table keys by
(def_name, binder_name), so a fn that shadows a binder name collapses
every shadow onto one key. The outermost binding (records last, on pop)
overwrites the inner ones; codegen's scope-close drop gates then read
the collapsed consume_count for the innermost binding and suppress its
drop, leaking the owned slab.

Spec 0056 resolves this as a compiler-internal naming collision — NOT
the deeper, separate problem of persistent AST provenance back to the
authored form (certain to be needed eventually, deliberately deferred;
conflating the two is a category error). Fix: alpha-rename shadowing
binders during desugar (reusing the existing fresh-name machinery that
already mints $mp_N / $lr_N), making the (def, name) key injective
again. No consumer of the uniqueness table changes — they all key by
name, and the name becomes a per-fn injective identity. IR-neutral:
codegen names heap by fresh SSA, never by source binder name. The
pre-desugar hashed form is untouched, so module identity and hash-pin
tests are unaffected.

RED fixtures securing the binder-kind class (the user's gating
condition before proceeding to the uniform-across-kinds fix):
- Let-shadow: the three raw_buf_{int,float,bool}_shadow_rebind tests
  (already in tree, committed f7f4c3b) — assert live == 0.
- Flat-pattern-shadow: a differential test plus its two fixtures
  (flat_pat_shadow_leak.ail shadows the outer binder; _control.ail
  alpha-renames it to a distinct name). The shadow must reach the
  control's live count. Differential by design, to isolate the
  shadow-collapse drop from unrelated baseline drop gaps out of
  #43's scope.

Grounding-check PASS; ail-check parse-gate green on every fenced block.
2026-05-30 12:21:27 +02:00
Brummel b49f57d9c6 spec: raw-buf — re-carve drop ratification to raw-buf.5, retirement to .6 (refs #7)
raw-buf.4 (RawBuf payload + Term::New desugar) implemented and about to
land, but it surfaced that the drop ratification was mis-scoped: the
flat drop FUNCTION is emittable in .4, but the drop CALL needs a codegen
resolution mechanism the .4 plan did not scope. An owned RawBuf binder's
value is a cross-module type-scoped intrinsic call ((app RawBuf.set ..) :
own (RawBuf a)); codegen's is_rc_heap_allocated only marks an App binder
drop-trackable when synth_callee_ret_mode resolves the callee's Own mode,
but codegen synth_arg_type has no TypeDef-first / cross-module-mono
resolution ladder (unlike the checker's lib.rs:3465), so the binder is
non-trackable and no drop call is inserted. A second site
(uniqueness::infer_module's per-module globals) misses the cross-module
borrow mode too.

Re-carve (six iterations; .1-.4 done, .5/.6 remain):
  raw-buf.4 — RawBuf payload + Term::New desugar + flat drop FUNCTION
              (DONE). Worked program prints 60; Float/Bool/reject ship.
  raw-buf.5 — owned cross-module type-scoped drop-call resolution: a
              TypeDef-first + cross-module-mono arm in codegen
              synth_arg_type feeding is_rc_heap_allocated /
              synth_callee_ret_mode / drop_symbol_for_binder, plus
              cross-module op visibility in uniqueness::infer_module.
              Ratified by raw_buf_no_leak (live == 0).
  raw-buf.6 — kernel_stub retirement (was .5).

Also documents the raw-buf.4 diagnostic-behaviour change: because the
Term::New desugar now runs before check, (new T ..) with a missing
new-op surfaces type-scoped-member-not-found (more precise) rather than
the prep.2 new-type-not-constructible it supersedes; new-arg-kind-mismatch
is obsoleted (the desugar drops the type-arg). Same rejection conditions,
preserved. And corrects the @ailang_rc_release spec slip to the real
symbol @ailang_rc_dec.

The drop-call resolution is its own iteration for the same reason
raw-buf.3 became a mechanism prep: it is a codegen-resolution mechanism
(parity with the checker's type-scoped/cross-module ladder), separable
from the RawBuf payload, and isolating it keeps the resolution change
off the .4 payload diff.
2026-05-30 00:49:28 +02:00
Brummel 9ff1f22d42 plan: raw-buf.4 RawBuf payload + Term::New desugar (refs #7)
Four tasks. Task 1: raw_buf manifest (source.ail + mod + lib re-export)
+ ailang-surface wiring (parse_raw_buf + injection) + round-trip +
workspace count 4->5 — checkable, no codegen (raw_buf not yet in the
bijection collector list, so its markers are uncollected, bijection
stays green). Task 2: the general Term::New -> (app T.new <values>)
desugar (runs before check, so the type-scoped callee flows through the
raw-buf.3 scope threading to RawBuf_new__T), drops the NewArg::Type
(element type inferred from use — no type-ascription term exists),
removes both codegen deferral arms; ratified by a (new StubT 42)
build-and-run. Task 3: the 12 scope-qualified RawBuf_op__T entries +
emit fns (alloc/gep/load/store over an @ailang_rc_alloc slab, Int emits
in full + Float/Bool by an exact element-type/width substitution table)
+ a flat intrinsic-storage drop + raw_buf added to the bijection
collector module list (marker<->entry lockstep closes here). Task 4:
the E2E (int -> 60, float, bool, param-in reject, drop leak).

plan-recon resolved both make-or-break risks favourably: (A) the
alloc/load/store emit has a clean mirror (emit_eq_str does
locals-by-position -> fresh_ssa -> gep -> call -> ret; @ailang_rc_alloc
returns the payload ptr with rc-header auto-prepended); (D) desugar runs
BEFORE check/synth/mono (prepare_workspace_for_check), so Term::New
becomes (app RawBuf.new ..) before synth's type-scoped resolution sets
scope=Some("RawBuf") -> reaches the raw-buf.3 RawBuf_new__Int mint. No
checker tweak needed.

Three orchestrator design calls (spec underspecified the codegen
detail): (1) drop discriminator = derived signal "the TypeDef's `new`
op is (intrinsic)-bodied" -> flat @ailang_rc_dec drop; correctly
separates RawBuf (intrinsic new) from StubT (real-body new), where a
param_in heuristic would misclassify StubT (also param_in). No schema
change, no hash-pin. (2) @ailang_rc_release does not exist -> the real
symbol is @ailang_rc_dec (spec slip corrected). (3) Bool = 1 byte per
spec; the 12 emits are per-element-specialised so each hardcodes its
width (Int/Float 8, Bool 1) + store type (i64/double/i1); the i1
store/load syntax is verify-first, raw_buf_bool_e2e is the catch.

Baseline after raw-buf.3 is 670; gates step 670->671 (round-trip)
->672 (StubT desugar) ->677 (5 RawBuf E2E). kernel_stub stays
(retirement is raw-buf.5).
2026-05-29 19:53:36 +02:00
Brummel 8508182f84 plan: raw-buf.3 type-scoped polymorphic intrinsic mechanism (refs #7)
Two tasks. Task 1 threads a TypeDef scope through the free-fn mono path:
a scope: Option<String> field on FreeFnCall + MonoTarget::FreeFn,
populated Some(prefix) only on the type-scoped T.f resolution branch
(lib.rs:3535, gated on type_home.is_some()), None everywhere else;
consumed at the two mono-symbol mint sites via a scoped_base helper
(both read the same MonoTarget → lockstep) and folded into
mono_target_key. Result: StubT.peek @ Int mints StubT_peek__Int, not
the colliding bare peek__Int; existing bare-resolved symbols unchanged
(669-gate is the no-regression backstop).

Task 2 adds the StubT.peek ratifier op to the stub source, extends the
bijection collector to expand a polymorphic type-scoped intrinsic over
its TypeDef's param-in set (building the same T_f__<elem> strings via
mono_symbol_n on Type::Con elems, so collector and mint agree
byte-for-byte), registers StubT_peek__Int/Float entries + emit fns,
updates kernel_stub_module_round_trips, and adds a mono-symbol
integration test (borrow-param calls, no Term::New, no codegen).

plan-recon resolved the make-or-break unknown favourably: the TypeDef
scope is discarded at lib.rs:3535 but fully available there (prefix +
type_home live) — a clean thread-through, not a resolution
re-architecture. Three orchestrator design calls folded in: peek's emit
is a plausible signature-correct stub, unit-ratified only (never
instantiated in .3, no Term::New to build a StubT; RawBuf's emits in .4
are the E2E-verified ones; peek retires in .5); the exact
llvm_type(borrow StubT Int) string is an implementer verify-first step
(eq__Unit precedent); scope-trigger is the call path (not signature,
which would perturb bare-called fns like length), scope-value is the
prefix, collector infers from signature — they agree by construction
for single-TypeDef kernel modules, bijection + 669-gate backstop.

peek Form-A verified to parse+check this session under a probe module
(ok, 32 symbols / 3 modules). Final gate 670 (669 + 1 new mono test);
.ll snapshots stay green (peek polymorphic → no instantiation without a
call site).
2026-05-29 19:25:04 +02:00
Brummel d2885c7ae6 spec: raw-buf — re-carve .3/.4/.5, type-scoped polymorphic intrinsic prep (refs #7)
Second re-carve. Planning raw-buf.3 (via plan-recon) surfaced that
RawBuf's ops are a THIRD intrinsic shape the intrinsic-bodies mechanism
never handled: type-scoped polymorphic top-level intrinsics (forall a,
param-in {Int,Float,Bool}, called as RawBuf.get). The existing bijection
+ symbol story covers only monomorphic top-level intrinsics (answer,
float_*) and per-type class-method instances (eq__Int).

The gap (both confirmed in source by recon + grounding-check):
- mono_symbol_n mints <base>__<T> from the bare fn name, so RawBuf.get
  @ Int → get__Int — the RawBuf scope never enters the symbol;
  new/get/set/size @ Int would collide with any other poly free fn of
  those (very common) names.
- the bijection collector's Def::Fn arm records the bare name "get" as
  one marker, matching neither the per-type entries nor their symbols;
  one polymorphic marker must map to N per-element entries, which the
  1-marker-to-1-entry bijection cannot express.

So raw-buf.3 is no longer "add 12 table rows" — it is a new language
mechanism. Re-carve (3 remaining iterations):
  raw-buf.3 — type-scoped polymorphic intrinsic mechanism: scope-
              qualified mono symbols (RawBuf_get__Int) + bijection-
              collector expansion over param-in. Built and ratified
              standalone on the stub (a StubT.peek op + its 2 scoped
              entries + mono/bijection unit tests), the prep pattern
              prep.1/.2/.3 used. No RawBuf, no Term::New dependency.
  raw-buf.4 — RawBuf payload (module + 12 scoped entries + drop) +
              Term::New desugar ((new RawBuf ...) sugar, removes both
              deferral arms). Pure consumer of .3. Worked program → 60.
  raw-buf.5 — kernel_stub retirement (peek + answer + stub leave;
              RawBuf carries all roles).

Decided design (Option A): scope-qualified symbols, not bare. Rationale
is semantic — the type-scoped call convention RawBuf.get should carry
through to a collision-free, IR-legible symbol; bare get__Int is a
latent collision landmine on the most-reused op names. Removing that
collision class is itself feature-acceptance criterion-3 evidence.

The raw_buf module Form-A in § Concrete code shapes is gate-verified
(ail check → ok, 35 symbols / 3 modules). Grounding-check PASS: all 10
current-behaviour assumptions ratified by named green tests; the
Term::New codegen-deferral (lib.rs ~2096 + ~3298) is the same
about-to-be-deleted prep.2 transient, reported-not-blocked (consistent
with the override logged on 3ec406e).
2026-05-29 19:10:19 +02:00
Brummel 395a40f3e7 plan: raw-buf.2-kernel-rename — atomic crate rename + family-crate reshape (refs #7)
Two tasks. Task 1 (atomic): git mv ailang-kernel-stub → ailang-kernel,
carve the STUB_AIL Form-A body verbatim into
src/kernel_stub/source.ail (incl. the answer (intrinsic) fn — a
perturbed byte shifts the .ll snapshots + kernel_stub_module_round_trips),
add mod.rs (include_str!) + lib.rs hub (pub use kernel_stub::SOURCE as
STUB_AIL), thread all 8 compile sites across 6 files (Cargo package +
workspace member + dep-alias + ailang-surface dep + loader.rs
use-paths), end with cargo build + cargo test green. Task 2: doc/ledger
crate-path honesty fixes (INDEX, CLAUDE.md code-layout + lockstep rows,
model 0007) — path-only, retirement narrative left for raw-buf.4.

Zero behavioural change; public symbol STUB_AIL preserved via the
re-export so ailang-surface + the bijection test bind unchanged. NO
raw_buf submodule (that is raw-buf.3).

plan-recon mapped 8 compile-breaking sites + 5 doc sites (two beyond
the spec's named row list — CLAUDE.md:312 lockstep + model 0007:8,235 —
folded in per honesty-rule). Baseline measured this session: 669
passed / 0 failed / 2 ignored (not the stale 667 from the pre-recarve
0105 plan; intrinsic-bodies added 2 tests).
2026-05-29 18:34:47 +02:00
Brummel 3ec406e687 spec: raw-buf — re-carve .2/.3/.4 post intrinsic-bodies (refs #7)
raw-buf.1 (intercept registry) shipped (140a0c0). The intrinsic-bodies
milestone (#9) then landed and pulled the foundation out from under the
original raw-buf.2/.3 split, so this revises the spec.

Two foundation changes from intrinsic-bodies:

1. The placeholder-body convention the original spec leaned on
   ((body x) round-trip stubs) is gone. prelude.ail now uses
   (intrinsic) markers, and check_fn (lib.rs:2173) typechecks every
   non-intrinsic body including kernel-tier ones. RawBuf.get with a
   placeholder body (declared (ret a), body constructs a RawBuf) is now
   a hard type error — get/new/set/size MUST be (intrinsic).

2. An (intrinsic) marker now requires a matching INTERCEPTS entry as a
   lockstep pair (intercepts_bijection_with_intrinsic_markers). The
   "manifest visible / codegen deferred" intermediate state the original
   raw-buf.2 was designed to ship is forbidden by that invariant — the
   marker and its codegen entry must land in the same iteration.

Re-carve (3 remaining iterations, retirement isolated):
  raw-buf.2 — kernel family-crate rename ailang-kernel-stub →
              ailang-kernel. Pure refactor, zero behavioural change.
  raw-buf.3 — RawBuf end-to-end: raw_buf module ((intrinsic) markers)
              + 12 INTERCEPTS entries + Term::New desugar + drop +
              ailang-surface wiring + E2E. Manifest+codegen land
              together. Workspace count 4 → 5.
  raw-buf.4 — kernel_stub retirement: RawBuf subsumes every
              ratification role (Term::New, param-in, kernel-tier
              auto-import, intrinsic); answer entry + stub removed,
              count 5 → 4.

Preserved unchanged: Goal, slab layout, feature-acceptance argument.

The raw_buf module Form-A source in § Concrete code shapes is
gate-verified: ail check → ok (35 symbols across 3 modules); the four
(intrinsic) markers are legal because the module is (kernel).

Grounding-check: 8/9 load-bearing current-behaviour assumptions
ratified by named green tests. The single override is the Term::New
codegen-deferral arm (lib.rs:2094 + sibling at lib.rs:3296, the latter
surfaced by the grounding agent) — a transient prep.2 marker that
raw-buf.3 deletes, not a relied-upon invariant; forward-ratified by
raw-buf.3's build-and-run E2E. Override logged, not a discard.
2026-05-29 18:26:59 +02:00
Brummel 298fc2d36f plan: intrinsic-bodies.2-migration-lock — 4-task prelude migration + bijection pin (refs #9)
Decomposes intrinsic-bodies.2 (parent spec § Architecture points 6-8)
into 4 tasks + a final gate:

  Task 1 — migrate the 13 authored prelude dummy bodies to (intrinsic):
    7 Eq/Ord instance methods (inner (body false)/(body true)/
    (body (term-ctor Ordering EQ)) → (intrinsic), Design X lambda-body
    placement) + 6 float_* fns ((body false) → (intrinsic)). Gated by
    round-trip + the 4 eq/compare/float E2E ratifiers staying green
    (behaviour-preserving: the intercepts emit identical IR).
  Task 2 — rebaseline the two moving hash pins: prelude_module_hash_pin
    (prelude module hash) and mono_hash_stability's 6 eq/compare
    def-hashes (capture-from-failure, replace the constant). The 4
    show__* pins do NOT move.
  Task 3 — replace the one-directional registry_contains_all_legacy_arms
    pin with intercepts_bijection_with_intrinsic_markers: an in-source
    test that walks prelude + kernel_stub pre-mono for Term::Intrinsic
    markers, recovers mangled names (mono_symbol for instance methods,
    fn name for top-level), and asserts the bijection over the
    intrinsic-backed class (14) with the 5-name *__Int optimisation-only
    allowlist. Both directions + a stale-allowlist guard.
  Task 4 — confirm no codegen path lowers an intercepted body (already
    true post-.1: intercept-by-name precedes lower_term), delete the
    stale dummy-body comment at lib.rs:1310-1319, and conditionally edit
    0007-honesty-rule.md ONLY if it names the dummies (recon: it does
    not — a general rule; no forced edit).

Recon-driven plan decisions:

1. The bijection pin lives in intercepts.rs's in-source #[cfg(test)]
   mod tests, NOT a crates/ail/tests integration test. Visibility
   forces it: the pin needs INTERCEPTS (pub(crate) in codegen, in-source
   only) AND the parse hops (ailang-surface dev-dep). Verified
   ailang-surface does not dep ailang-codegen, so there is no
   dev-dep cycle blocking the in-source test from calling parse_prelude
   (the dev-dep-cycle hazard that bit pd.2.4/pd.3.1 does not apply
   here — that was the ailang-core <-> ailang-surface edge).
2. The pin walks PRE-mono (parse_prelude/parse_kernel_stub) and
   reconstructs mangled names via mono_symbol, rather than walking
   post-mono. Post-mono only synthesises USED mono symbols, so a
   post-mono walk would miss any instance method the pin's entry
   fixture does not exercise (e.g. eq__Unit). Pre-mono + mono_symbol
   sees all 14 markers unconditionally.
3. Task 2's hashes are capture-from-failure, not pre-computed — the new
   prelude bytes determine them. The old->new values get recorded in
   the iter commit body by the Boss.

All verification filter strings checked against the tree: the 4 E2E
ratifier fn names, the mono pin name, and the prelude pin target all
resolve to ≥1 real test. No ail/ail-json/ll fenced block in the plan
(the migrated-form snippets are scheme fragments — the (intrinsic) form
is already ratified by .1's kernel_intrinsic_smoke.ail), so the
parse-bytes gate is a documented no-op.

Handoff target: skills/implement on docs/plans/0107-intrinsic-bodies.2-migration-lock.md
2026-05-29 17:37:45 +02:00
Brummel 8301ca3ee8 spec: intrinsic-bodies — correct .2 count + bijection split (refs #9)
Forward-fix on 5b66de7, prompted by plan-recon for intrinsic-bodies.2.
Two spec-vs-reality gaps in the .2 (migration + lock) sections, both
caught before the .2 plan was written:

1. Count. The spec said "18 dummy bodies" migrate to (intrinsic). That
   conflated the INTERCEPTS entry count with authored prelude bodies.
   Reality: examples/prelude.ail carries 13 authored dummy bodies — the
   7 Eq/Ord instance methods (eq Int/Bool/Str/Unit, compare
   Int/Bool/Str) + the 6 float_* free fns. The registry has 19 entries
   (18 legacy + the .1 `answer`); the other 6 are not prelude dummies.

2. The strict bijection cannot hold. lt__Int/le__Int/gt__Int/ge__Int/
   ne__Int (5 INTERCEPTS entries) intercept the monomorphised __Int
   specialisations of the polymorphic free fns lt/le/gt/ge/ne, which
   carry REAL bodies in the prelude (ne = (app not (app eq x y)); the
   four ordering helpers are (match (app compare x y) ...)). Those
   bodies are honest and live — lowered for every non-Int instantiation;
   only the Int specialisation is intercepted for a faster direct icmp.
   They are an optimisation class, not a compiler-supplied-body class:
   no lie, no source body to replace, no (intrinsic) marker. A strict
   bijection over all INTERCEPTS entries would be red for these 5.

Corrections:
- § Architecture point 6: 13 authored prelude sites (named), with the
  5 icmp-family + `answer` explicitly listed as non-prelude / not
  migrated and why.
- § Architecture point 7: the pin splits the registry into
  intrinsic-backed (13 prelude markers + answer = 14) and
  optimisation-only (the 5 *__Int, an explicit documented allowlist).
  The bijection holds over the intrinsic-backed class only; the pin
  loads the workspace + monomorphises to recover mangled names for the
  marker direction.
- § Architecture point 8: reframed from "dead-path removal" to
  "dead-path confirmation" — .1's intercept-by-name already bypasses
  the dummy body before lower_term sees it (committed reality at
  52ff873), so .2 confirms no path lowers an intercepted body and
  deletes stale comments; the .1 Term::Intrinsic escape-guards stay.
- § Components + § Testing: the two moving hash pins named
  (prelude_module_hash_pin.rs; mono_hash_stability.rs's 6 eq/compare
  pins move, its 4 show pins do not); hash_pin.rs carries no
  prelude-derived hash. IR snapshots do not change.

Grounding-check PASS on all five corrected .2 claims against the
shipped .1 baseline (count, the 5 real-bodied helpers, the three hash
pins' behaviour, the already-dead path, the bijection-pin pipeline
reachability). No ail/ail-json/ll fenced block changed — parse-gate a
documented no-op for this revision.

The deeper observation this surfaced — that INTERCEPTS conflates two
concepts (compiler-supplied bodies vs. optimisation of a real body) —
is noted but NOT resolved here; splitting the registry is out of scope
for intrinsic-bodies and would be its own milestone.
2026-05-29 17:31:25 +02:00
Brummel ce0374ac0c plan: intrinsic-bodies.1-mechanism — 8-task Term::Intrinsic cross-crate landing (refs #9)
Decomposes intrinsic-bodies.1 (parent spec docs/specs/0055-intrinsic-bodies.md
§ Architecture points 1-5) into 8 tasks plus a final gate:

  Task 1 — Term::Intrinsic unit variant in ast.rs + the in-core
    exhaustive-match arms (canonical/hash/visit/pretty), enumerated by
    cargo build -p ailang-core (no-wildcard matches break until armed).
  Task 2 — surface parse + print: (intrinsic) as a fn body-slot clause
    and a lambda positional body, mapped to/from Term::Intrinsic;
    round-trip rides the examples/ corpus gate.
  Task 3 — cross-crate walker sweep (check + codegen + prose): leaf
    arms at the sites that match Term::New today, enumerated by
    cargo build --workspace. The two MEANINGFUL arms (codegen lower_term,
    checker body-check) are bridged with a temp unreachable! so the gate
    is green, then replaced by Tasks 4-5 — no deferred-caller across a
    0-error gate.
  Task 4 — checker: env.current_module_kernel_tier flag, signature-only
    body check for an intrinsic body, intrinsic-outside-kernel-tier
    reject (CheckError variant + code() arm). Reject test is subprocess
    `ail check --json` on a temp file, modelled exactly on the verified
    check_json_unbound_var pattern.
  Task 5 — codegen: a Term::Intrinsic body routes through
    intercepts::lookup; never reaches lower_term; lower_term's
    Term::Intrinsic arm is a codegen-internal error.
  Task 6 — `answer` intercept (ret i64 42) + the answer intrinsic in
    STUB_AIL + examples/kernel_intrinsic_smoke.ail so the
    schema_coverage corpus observes Term::Intrinsic (avoids the
    Term::New-in-match-but-not-in-corpus gap).
  Task 7 — E2E ratifier examples/kernel_answer.ail (calls
    kernel_stub.answer, prints 42); build_and_run("kernel_answer.ail")
    asserts stdout 42.
  Task 8 — design/contracts/0002-data-model.md gains the
    { "t": "intrinsic" } Term entry; design_schema_drift mirror stays
    green.

Three plan-time corrections after plan-recon + harness verification:

1. The reject test was first drafted against an invented ailang_check
   API (Workspace::single_with_prelude / check_workspace). Verified
   against e2e.rs:1236 that the established reject pattern is
   subprocess `ail check --json` + exit-1 + JSON code assertion;
   rewrote on a temp file.
2. build_and_run takes the fixture filename WITH .ail extension
   (verified e2e.rs:13-38, existing calls build_and_run("sum.ail")) —
   the ratifier call is build_and_run("kernel_answer.ail").
3. kernel_intrinsic_smoke.ail carries an intrinsic with no registered
   intercept; confirmed no gate builds it (compile_check.py uses a
   curated list, no test builds all examples/*.ail) — it rides only
   the parse-level round-trip + schema-coverage gates, never a build.

Scope guard: .1 does NOT migrate prelude dummies, does NOT upgrade the
registry pin to a bijection, does NOT remove the dead body-lowering
path — all .2. Hashes stay stable in .1 (Term::Intrinsic is additive;
no existing fixture carries it).

Test trajectory: 667 (post-raw-buf.1) → ~669 (+intrinsic_in_user_module_is_rejected,
+answer_intrinsic_builds_and_runs_printing_42; corpus/round-trip
fixtures ride existing dynamic gates).

Handoff target: skills/implement on docs/plans/0106-intrinsic-bodies.1-mechanism.md
2026-05-29 16:55:01 +02:00
Brummel 5b66de77ac spec: intrinsic-bodies — revise AST repr to Term::Intrinsic leaf (refs #9)
Forward-fix on c42034b. plan-recon for intrinsic-bodies.1 surfaced
that the original AST representation — FnDef.body / Term::Lam.body
made Option<Term> plus an `intrinsic: bool` flag — has a ~150-site
blast radius across six crates: every body read/construct site breaks
when a mandatory public field goes optional. That blast radius is the
signal (CLAUDE.md design-rationale rule) that the representation was
wrong, not merely expensive.

The Form-A surface (user's chosen Approach 1) and the Local-Reasoning
semantics (Design X marker placement) are UNCHANGED. Only the internal
AST representation changes, which is orchestrator authority over AST
design.

New representation: a single new leaf Term variant, Term::Intrinsic
({ "t": "intrinsic" }), is the body of a compiler-supplied definition.
FnDef.body (Term) and Term::Lam.body (Box<Term>) keep their existing
types. A def is intrinsic iff matches!(body, Term::Intrinsic).

Three structural reasons (not effort):

1. Established pattern. The project adds new constructs as additive
   Term variants — Term::New, Term::Loop, Term::Recur, Term::Clone,
   Term::ReuseAs all landed this way, documented "strictly additive,
   pre-existing fixtures hash bit-identically" in
   design/contracts/0002-data-model.md. Term::Intrinsic follows it.
   Option A introduced a brand-new pattern (mandatory field → optional)
   absent from the schema.

2. Meaning at the right locus. "This body is compiler-supplied" is a
   property of the body, not the container. Term::Intrinsic sits at the
   body position — fn body, or instance-method lambda body (Design X
   local-signature placement preserved exactly).

3. Illegal state unrepresentable. Option A admitted intrinsic:true with
   body:Some(...), forcing an intrinsic-with-body reject. Under the
   variant a body is either Term::Intrinsic or a real term, never both
   — the reject is deleted, the state cannot occur. This is the same
   make-illegal-states-unrepresentable discipline as the honesty theme
   the milestone exists to serve.

Blast radius collapses from ~150 body-read/construct sites to the
exhaustive match-on-Term arms (canonical/hash/visit + schema_coverage),
which the no-wildcard Term match turns into compile errors until each
gains a Term::Intrinsic case — the project's normal new-variant
discipline.

Sections revised: § Architecture points 1/3/4, § Concrete code shapes
(Implementation shape now shows the leaf variant, not the
Option+flag), § Components, § Data flow, § Error handling (the
intrinsic-with-body row removed), § Testing strategy (the
both-body-and-intrinsic reject test removed; schema_coverage Term::Intrinsic
observation added). The scheme/ail surface examples are byte-unchanged.

Re-ran the brainstorm gates on the revision: Step-7 parse gate green
(both ail blocks exit 0, unchanged); Step-7.5 grounding-check PASS on
the four new load-bearing claims (additive-variant precedent +
contract wording, exhaustive-Term-match mechanism, mono.rs
synthesise_mono_fn destructure unchanged under preserved body type,
Term::Recur as non-reducing-leaf precedent).
2026-05-29 16:45:47 +02:00
Brummel c42034b38d spec: intrinsic-bodies — (intrinsic) Form-A body marker (refs #9)
New milestone, triggered by the raw-buf.2 BLOCKED chain (the .2 work
was discarded; spec 4ad003d and plan 647121c stay on main per the
forward-only rule). The Form-A surface currently forces every fn /
instance-method to carry a (body ...) clause, which produces three
problems the marker resolves:

1. Prelude dummy-body lies. The eq/compare/ne/lt/le/gt/ge instance
   methods ship placeholder bodies — (body false), (body (term-ctor
   Ordering EQ)) — that parse and type-check but never run; codegen
   discards them and emits the intercept (registry shipped in
   raw-buf.1, intercepts.rs). A standing honesty-rule infraction
   (design/contracts/0007-honesty-rule.md): a reader who trusts the
   source is wrong about what runs.

2. Polymorphic kernel-tier fns are structurally impossible. RawBuf's
   get : RawBuf a -> Int -> a needs a placeholder body producing a
   value of type a, and AILang has no value of polymorphic type. The
   dummy-body requirement made the fn unauthorable — what BLOCKED
   raw-buf.2.

3. No surface affordance for "compiler supplies this body". Every
   systems language has one (LLVM declare, Rust extern
   "rust-intrinsic", Haskell foreign import prim, C builtins). The
   intercept registry IS AILang's compiler-supplied-body table;
   (intrinsic) is the surface declaration of membership.

Decomposition (2 iterations, full cut):

  intrinsic-bodies.1 — the mechanism. FnDef.body / Term::Lam.body
  become optional; an additive intrinsic: bool rides each
  (skip_serializing_if, hash-stable when omitted). Form-A parses +
  prints (intrinsic); round-trip gated. Checker checks signature-only,
  rejects body+intrinsic, rejects intrinsic outside kernel-tier /
  prelude (intrinsic-outside-kernel-tier). Codegen routes intrinsic
  defs through intercepts::lookup. Ratified by a throwaway `answer`
  smoke intrinsic in the kernel_stub fixture, end-to-end to native.

  intrinsic-bodies.2 — migration + lock. The dummy bodies swap for
  (intrinsic); a hard-lockstep pin asserts a bijection between
  INTERCEPTS entries and intrinsic markers reachable in the loaded
  workspace (extends raw-buf.1's registry_contains_all_legacy_arms
  from a one-way name check to a two-way source<->registry bijection).
  Dead body-lowering path for intercepted defs removed.

Design decisions locked during brainstorm:

- Marker placement (Design X). For a top-level fn, (intrinsic)
  replaces the (body ...) clause directly — the (type ...) signature
  stays beside it. For an instance method, the marker sits on the
  lambda BODY, not the method: the lambda's typed shell (params / ret)
  IS the method's local signature, and intrinsic drops the body, not
  the signature — exactly as LLVM declare / Rust extern-intrinsic keep
  the full signature. Hoisting the marker to (method eq (intrinsic))
  would erase the local signature (a reader would have to climb to the
  Eq class decl and substitute a := Int), violating local reasoning
  (design/INDEX.md § Goal). The mono pass
  (mono.rs::synthesise_mono_fn) reads params + inner body out of this
  lambda today, so the placement keeps that path unchanged. The Design
  Y alternative was considered and rejected on this signature-locality
  ground.

- Scope guard. (intrinsic) is legal only in (kernel)-tier modules and
  the prelude; user modules are rejected. This is the honesty-rule
  guard at the workspace boundary — user code cannot mark a body as
  compiler-supplied, so the lie cannot re-enter through user modules.

Process note: first spec under the hardened brainstorm pipeline
(Skills issue #1 fixes + the spec_validation parse gates retrofitted
in a2698a8). The Step-7 parse-every-block gate caught a real defect in
the spec's own ail examples on first run (an invalid module-level
(doc ...) head) — the defense line that was absent on raw-buf.2 and
let its unparseable spec bytes through to implement. Grounding-check
PASS on 12 load-bearing assumptions, each ratified by a named green
test.

Out of scope: a user-facing plugin API for custom intercepts (the
scope guard forbids user-module intrinsics); any change to the
raw-buf.1 dispatch mechanism; the raw-buf.2 redo itself (milestone #7,
parked behind this one).
2026-05-29 16:32:36 +02:00
Brummel 647121cb8f plan: raw-buf.2-kernel-manifest — 7-task crate-rename + RawBuf submodule + checker-side surface (refs #7)
Decomposes raw-buf.2 (parent spec docs/specs/0054-raw-buf.md
§ Architecture point 2, § Components row 2, § Testing strategy
"raw-buf.2") into 7 atomic tasks:

  Task 1 — crate rename + reshape: crates/ailang-kernel-stub/ →
    crates/ailang-kernel/, restructured as a family-crate with
    src/kernel_stub/{mod.rs,source.ail}; 13 steps covering 8
    compile-checked rename sites plus design/INDEX.md + CLAUDE.md
    path updates.
  Task 2 — add raw_buf submodule: src/raw_buf/{mod.rs,source.ail}
    with the spec's Form-A source (4 fns + 1 TypeDef with
    param-in (a Int Float Bool)); RAW_BUF_AIL re-export.
  Task 3 — wire raw_buf into ailang-surface: parse_raw_buf fn,
    re-export at lib.rs:39, workspace injection alongside the
    existing kernel_stub block, workspace_pin count bump 4→5.
  Task 4 — round-trip test raw_buf_module_round_trips, verbatim
    structural clone of kernel_stub_module_round_trips.
  Task 5 — E2E raw_buf_check_e2e: (con RawBuf (con Int)) consumer
    checks clean.
  Task 6 — E2E raw_buf_param_in_reject_e2e: (con RawBuf (con Str))
    consumer rejected at check with param-not-in-restricted-set.
  Task 7 — E2E raw_buf_build_intercept_missing_e2e: (new RawBuf …)
    consumer accepted at check, rejected at build with the
    existing Term::New deferral message.

Three substantive plan-time decisions resolving plan-recon's
open questions:

1. The intercept-not-registered diagnostic is NOT introduced.
   The spec hedged ("intercept-not-registered: new__RawBuf__Int
   (or the existing Term::New-deferral diagnostic)") between
   shipping a fresh diagnostic and reusing the existing deferral
   at crates/ailang-codegen/src/lib.rs:2075-2078. Plan picks
   reuse: the deferral already names "milestone raw-buf" and is
   self-describing; a new diagnostic would exist for ~1 iter
   (raw-buf.2 → raw-buf.3) before going away — disposable infra.
   Task 7's test asserts on the substring "Term::New requires
   the type's" which is stable in the existing message.

2. In-tree references to crates/ailang-kernel-stub/ AFTER the
   rename — split-scope between raw-buf.2 and raw-buf.3.
   The path changes in raw-buf.2 (the crate moves now), so
   path-references in design/INDEX.md:112 and the CLAUDE.md
   Code-layout table get updated in Task 1 Steps 10-11. The
   retire-language (stub as "ratifying fixture for kernel-
   extension-mechanics" → "retired by raw-buf") stays for
   raw-buf.3 close, per spec § Components row 3.

3. parse_raw_buf gets a mirror re-export at ailang-surface/
   src/lib.rs:39 alongside parse_kernel_stub. Recon's natural
   reading; load-bearing because the new round-trip test calls
   ailang_surface::parse_raw_buf() directly.

Plan honours the project's compile-gate discipline. Task 1's
13 steps thread all 8 rename sites inside the same task —
no deferred caller across the build gate. Task 3 bundles the
workspace_pin count bump with the loader injection that drives
it; the assertion goes 4→5 in the same task that makes it true.

Test-count trajectory: 667 (post-raw-buf.1 baseline) → 667
(Tasks 1, 2, 3 — refactor + wiring, no new test) → 668 (Task 4
round-trip) → 669 (Task 5 check E2E) → 670 (Task 6 reject E2E)
→ 671 (Task 7 deferral E2E).

Handoff target: skills/implement on docs/plans/0105-raw-buf.2-kernel-manifest.md
2026-05-29 11:17:30 +02:00
Brummel d1612d5463 plan: raw-buf.1-intercept-registry — 5-task atomic codegen-registry refactor (refs #7)
Decomposes raw-buf.1 (parent spec docs/specs/0054-raw-buf.md
§ Architecture point 1, § Components row 1) into 5 tasks:

  Task 1 — intercepts module skeleton (3 steps)
  Task 2 — populate the table: 18 emit fns + visibility bumps (9 steps)
  Task 3 — rewire the dispatch site to a thin shim (3 steps)
  Task 4 — fold wants_alwaysinline into the registry (4 steps)
  Task 5 — registry_contains_all_legacy_arms pin test (3 steps)

Three substantive decisions made at plan time beyond what the spec
dictates, surfaced by the plan-recon report:

1. The match arm count is 18, not the ~8 the spec sentence hand-listed
   (eq__Str, compare__Int|Bool|Str, float_*). Five additional arms
   (eq__Int, eq__Bool, eq__Unit, plus the lt|le|gt|ge|ne__Int
   direct-icmp family) shipped after the spec sentence was written.
   All 18 are absorbed by the registry; the pin test enumerates all 18.

2. The `intercept_emit_wants_alwaysinline` predicate at lib.rs:1172 is
   a separate hardcoded name-list that mirrors the dispatch arms.
   This is a silent-drift class (regression seen earlier in the cycle
   on compare__Int perf at +29-47% when the lists diverged). The plan
   folds wants_alwaysinline into the Intercept entry as a per-row bool
   and rewrites the predicate to consult the registry — one source of
   truth instead of two.

3. The wrapper fn `try_emit_primitive_instance_body` survives as a
   thin shim (`match intercepts::lookup(name) { Some(i) => ..., None
   => Ok(false) }`) rather than being deleted. Trade-off: a 6-line
   wrapper vs. a 14-file comment-ref churn. The wrapper wins —
   landmark name preserved, all in-source doc comments stay valid.

Plan honours the project's compile-gate discipline: fn signatures
stay stable throughout (only bodies change), no caller-threading is
deferred across a build gate. Visibility bumps in Task 2
(pub(crate) on three Emitter fields and four helper methods) are
additive and do not change any caller's signature.

Verification commands in each Run step name actual test fns the
plan-recon confirmed exist at specific lines; no filter-zero-match
risk. Pre-flight workspace baseline: 668 tests; post-iter:
669 (the new pin).

Handoff target: skills/implement on docs/plans/0104-raw-buf.1-intercept-registry.md
2026-05-29 10:53:08 +02:00
Brummel 4ad003d21f spec: raw-buf — 3-iter base-extension milestone (refs #7)
First new milestone after kernel-extension-mechanics close.
RawBuf is the canonical kernel-tier *base* extension: mutable,
indexed, bounded-size flat buffer of primitive elements
({Int, Float, Bool}, restricted via prep.3's param-in). It
unblocks two downstream needs already in the backlog:

- series milestone #8 — library-tier ring buffer wrapping RawBuf.
- Embedding-ABI batch-FFI (subsumes closed #2) — the M5
  friction-harvest measured per-tick FFI at ~206 ns/tick on
  real EURUSD volume; RawBuf is the contiguous-slice primitive
  that amortises that per-tick cost.

The milestone also fulfils the whitepaper § "Plugin contract"
commitment: the migration of the hardcoded
try_emit_primitive_instance_body into a registry is triggered
by the first real base extension shipping.

Decomposition (R — registry-first refactor, 3 iters):

  raw-buf.1 — Intercept registry refactor. Lift the existing
  hard-coded match (eq__Str, compare__Int|Bool|Str, float_*) into
  a registry table. Zero behavioural change; ratified by the
  existing E2E suite (eq_primitives_smoke / compare_primitives_smoke
  in e2e.rs, float_compare_smoke, eq_ord_polymorphic). Pure
  refactor — the cleanest possible bisection target.

  raw-buf.2 — RawBuf kernel-tier manifest + checker side. Rename
  crates/ailang-kernel-stub/ → crates/ailang-kernel/ and reshape
  as a family-crate (src/{kernel_stub,raw_buf}/{mod.rs,source.ail});
  public surface preserved via re-exports. Consumer code with
  (con RawBuf (con Int)) passes ail check; ail build fails with
  intercept-not-registered — that diagnostic IS the ratification.

  raw-buf.3 — RawBuf codegen intercepts + Term::New desugar +
  stub retirement. Register 12 element-type-specialised entries
  (4 ops × {Int,Float,Bool}); lower via @ailang_rc_alloc +
  getelementptr + load/store. Desugar (new T args) →
  (app T.new args) so Term::New is eliminated before codegen
  (completes the prep.2 deferral). Retire the kernel_stub
  submodule + the round-trip test at design_schema_drift.rs:743;
  ailang-kernel crate stays as the family-crate for future
  series/matrix/… modules.

Ordering rationale: raw-buf.1 ships zero behavioural change so
its failure mode is "existing tests break" — cleanest bisection.
raw-buf.2 ships the checker-visible surface on an unchanged
codegen substrate, so its failure mode is checker-isolated.
raw-buf.3 lands codegen on a registry that has already absorbed
every legacy intercept, so the RawBuf entries do not co-mingle
with a registry move.

Kernel-tier crate organisation: rejected pro-modul-crate
(crates/ailang-series/, crates/ailang-matrix/, ...) for sublinear
scaling and onboarding locality; rejected sub-Cargo-crates under
crates/ailang-kernel/ (C1) for Cargo-boilerplate overhead with no
real consumer benefit at AILang's scale. C2 (one family-crate,
sub-folders per module, lib.rs re-exports) keeps Cargo dep
surface flat (ailang-surface needs one dep, not N) and stub
retirement is a submodule delete + re-export drop.

Out of scope: bounds checks (caller checks via RawBuf.size per
whitepaper — UB-on-overflow is the contract), RawBuf.fill /
.copy / .iter (deferred until series or Embedding-ABI concretely
asks), record element types (SoA — Forward Axis).

Grounding-check PASS on 10 load-bearing assumptions about
current code state (intercept dispatch site, existing E2E
ratifiers, ailang-kernel-stub crate layout, parse_kernel_stub
location).

Brainstorm → planner handoff: first iteration scope is raw-buf.1
(§ Architecture point 1, § Components row 1).
2026-05-29 10:44:28 +02:00
Brummel aa49a56d5a fieldtest: kernel-extension-mechanics — 6 examples, 9 findings (3 bugs, 1 friction, 1 spec_gap, 4 working)
Post-audit fieldtest for the kernel-extension-mechanics milestone.
Fieldtester wrote 6 .ail consumer programs against the public
interface only (design ledger + spec + ail CLI; no source crate
reads), one per axis with two for the param-in pair (accept +
reject). All artefacts in examples/fieldtest/ + the spec.

**Working (4):** F5 NewTypeNotConstructible diagnostic is precise
(names the home module + missing def in one sentence). F6 Term::New
codegen-deferral diagnostic explicitly names the raw-buf milestone
where support lands. F7 param-in accept-path is end-to-end (check +
build + run prints 17). F8 ParamNotInRestrictedSet names type-arg
+ var + TypeDef.

**Bugs (3, action: debug):**

- F1 (axis 1) — Same-module type-scoped call `(app Type.member …)`
  rejected with phantom-qualified `expected <this>.T, got T`.
  Bare `(app member …)` from inside Type's home module works.
  Spec's "Canonical form decision" promises type-scoped works
  uniformly; the resolver appears to disagree with the workspace
  pre-pass on whether `<this-module>.T` equals bare `T`. Repro:
  examples/fieldtest/kem_2b_min_repro.ail.

- F3 (axis 3) — Kernel-tier auto-import scopes the name but does
  not register the module as a resolvable qualifier prefix. Per
  the pre-pass, a bare `StubT` in a type slot is qualified to
  `kernel_stub.StubT`; the resolver then rejects `kernel_stub`
  as unknown. Asymmetric: prelude's free fns work (per
  prelude_free_fns.rs), but the stub's TypeDef is effectively
  unreachable from any consumer. Repro:
  examples/fieldtest/kem_3_stub_consumer.ail.

- F4 (axis 3, spec/ship coherence) — The spec's prep.3 worked-
  consumer relies on `(new StubT 42)`, but the shipped STUB_AIL
  has only a TypeDef + ctor — the `(fn new ...)` the spec
  designed is missing from the implementation. The diagnostic
  the resolver does emit on the consumer (F5) is precise; the
  bug is the absent def itself. Resolution: re-add the `new` to
  STUB_AIL (the spec's design), refresh the drift pin.

**Friction (1, action: plan-or-defer):**

- F2 — Loader's sibling-only module resolution forces symlinks
  for any cross-module fixture sitting outside the canonical
  `examples/` directory. The kem_1 example required local
  symlinks for std_list.ail + std_maybe.ail. Secondary: the
  diagnostic message names only the .ail.json path even though
  .ail also resolves — actively misleading. Recommended: small
  tidy iter for loader workspace-search-path + diagnostic clarity.

**Spec-gap (1, action: ratify):**

- F9 — `design/models/0007-kernel-extensions.md:80` uses `unit`
  as a value literal of type Unit; the checker treats it as a
  Term::Var lookup and emits [unbound-var]. Either ratify `unit`
  as the canonical Unit-value literal in the language, or tighten
  the design model to use the existing idiom. The current model/
  surface disagreement is a small but real LLM-author trap.

**Symlinks committed** as evidence of the F2 workaround:
examples/fieldtest/std_list.ail and std_maybe.ail are symlinks
into ../. They are the fingerprint of the friction — removing
them silently would erase the evidence.

Per Iron Law, fieldtester did not work around bugs — they were
all surfaced and recorded. The kem_2b_min_repro.ail fixture
exists specifically because F1 surfaced mid-drafting and got
minimised; kem_3_stub_consumer.ail also stays as the F3 RED-side
fixture.

Triage (next-step routing for /boss):

  F4 → small inline fix (add (fn new ...) to STUB_AIL + drift refresh)
  F9 → small inline ratify (whitepaper edit)
  F2 → backlog issue, deferred (single tidy iter someday)
  F1, F3 → debug skill dispatches, then implement mini-mode
  F5-F8 → carry-on, recorded as wins
2026-05-28 19:19:00 +02:00
Brummel 3b4fb42771 plan: prep.3-kernel-tier-modules — 9-task schema + surface + workspace + diagnostic + stub crate (refs #33)
Terminal iteration of the kernel-extension-mechanics milestone.
Carves the spec's § Iteration prep.3 into nine bite-sized tasks:

  1. Module.kernel field + ~130-site struct-literal sweep + drift
     round-trip + data-model anchor.
  2. TypeDef.param_in field + ~30-site sweep + drift round-trip +
     anchor (BTreeMap<String, BTreeSet<String>>, kebab-cased
     `param-in`, skip-if-empty for hash stability).
  3. Form-A (kernel) module-header attribute — parse + print +
     round-trip.
  4. Form-A (param-in (a Int Float) (b Str)) TypeDef attribute —
     one outer clause, multiple inner var-lists (OQ1 decision).
  5. Prelude becomes kernel-tier + loader generalisation: the
     hardcoded `&["prelude"]` literal at loader.rs:108 migrates to
     a `modules.values().filter(|m| m.kernel)` derivation;
     ReservedModuleName diagnostic repurposed; CLI mapping +
     hash-pin refresh in lockstep.
  6. New crate `crates/ailang-kernel-stub/` — programmatic stub
     module via parse_kernel_stub() mirroring parse_prelude
     (OQ2 decision); wired into the loader; basic stub round-trip
     drift test ratifies the end-to-end mechanism.
  7. CheckError::ParamNotInRestrictedSet variant + code() + ctx() +
     enforcement in check_type_well_formed's Type::Con arm +
     in-source RED/GREEN tests.
  8. New workspace_kernel.rs integration tests — auto-import without
     explicit (import …), two kernel-tier modules co-load,
     explicit-import-overrides-auto-import precedence.
  9. design/INDEX.md + design/models/0007-kernel-extensions.md
     STATUS + forward→present transitions for the now-shipped
     mechanisms.

The four open questions raised by plan-recon are resolved up front
in the "Open-question decisions" block so no task carries a TBD.
ReservedModuleName variant shape stays `{ name: String }` (OQ3);
workspace.rs:2655 stays a test-site literal (OQ4).

The Module struct-literal sweep is the largest single mechanical
edit (~130 sites; the additive #[serde(default)] covers JSON
deserialise paths, only Rust struct literals break) — handled as
a compiler-driven sweep inside Task 1, with the workspace-build
gate validating no caller is deferred. TypeDef has ~30 sites.

Compile-gate-vs-deferred-caller and pin/replacement-substring
contiguity rules from planner self-review Step 5 are scrubbed:
no signature change defers a caller past its own compile gate,
no verbatim text edit pairs with a soft-wrappable presence pin.

Recon report received DONE_WITH_CONCERNS — concerns were the
four OQs, all decided in this plan.
2026-05-28 17:04:41 +02:00
Brummel 70e6fcd5c0 plan: prep.2 Term::New construct — 4-task atomic AST + surface + checker + drift (refs #32)
Second iteration of the kernel-extension-mechanics milestone. The
plan ships the `new` Form-A keyword + Term::New AST variant + NewArg
enum + checker elaboration + two diagnostics, plus the schema-drift
pin and the data-model.md / form_a.md anchor additions.

Decomposition:

- Task 1 (large mechanical): AST variant declaration + workspace-wide
  compile-forced arms across 24 files (44+ exhaustive Term-match
  sites). Six arm patterns inlined; per-site table assigns one
  pattern per site. Includes the prep.1 pre-pass partner — the
  `qualify_workspace_term` arm that rewrites bare Term::New.type_name
  to qualified form, mirroring the existing Term::Ctor arm. Synth
  gets a Pattern-E stub here; Task 3 replaces with real logic.
- Task 2: Form-A lex/parse/print + round-trip fixture. New
  `parse_new` method with Type-vs-Term arg disambiguation by
  syntactic form (head keyword = Type-production heads
  `con`/`fn-type`/`borrow`/`own` → NewArg::Type; else NewArg::Value).
- Task 3: Checker synth real-logic + 2 new CheckError variants
  (NewTypeNotConstructible, NewArgKindMismatch) + 3 RED-first
  in-source tests covering the spec's worked example, the missing-
  `new`-def case, and the kind-mismatch case.
- Task 4: Schema-drift pin (term_new_round_trips +
  term_new_type_arg_round_trips) + spec_drift exemplar + form_a.md
  keyword cheatsheet + design/contracts/0002-data-model.md JSON-tag
  block.

Spec is already correct on prep.2 scope; the recon surfaced one
design decision the plan resolves inline: NewArg uses
`#[serde(tag = "kind", content = "value", rename_all = "lowercase")]`
to land the spec's exact JSON byte shape. Codegen remains explicitly
out of scope per the milestone spec; Task 1's codegen sites get
Pattern-D error-arms ("Term::New requires desugar first; codegen
support lands in the raw-buf milestone").

Forward-reference: Task 1's Pattern-C arm in `qualify_workspace_term`
extends prep.1's workspace-wide normalisation pre-pass — Term::New
joins Term::Ctor and Type::Con as a type_name-bearing site that
gets bare→qualified normalised before the checker sees it.
2026-05-28 15:00:27 +02:00
Brummel b586999e81 iter prep.1-type-scoped-namespacing (DONE 5/5): TypeDef-first resolution + workspace pre-pass — closes #31
First iteration of the kernel-extension-mechanics milestone. Ships
the type-scoped `<TypeName>.<member>` resolution path as the
canonical form for type-associated operations, narrows the
`BareCrossModuleTypeRef` / `BadCrossModuleTypeRef` diagnostics from
"bare = strictly local" to "bare = in-scope by any path", migrates
12 std-library example fixtures, and introduces a workspace-wide
normalisation pre-pass `prepare_workspace_for_check` shared between
`check_workspace` and `monomorphise_workspace`.

Architectural discovery during implementation: the plan covered the
`Term::Var` dot-qualified resolver layer plus the workspace
validator's bare-name acceptance, but the migration of bare-form
fixtures exposed five sites where bare vs. qualified type-names
needed symmetric treatment — `Term::Ctor` resolution, `Type::Con`
well-formedness, mono's poly-free-fn name/constraint-count
enumeration, codegen's `lookup_ctor_by_type` bare-name path, and
the upstream desugar-then-qualify composition. Rather than
scattering TypeDef-first ladders across each site, the implementer
centralised the work into one pre-pass that walks every consumer
module's `Type::Con.name` and `Term::Ctor.type_name`, rewriting
bare cross-module references to their qualified `<home>.<Type>`
form. This is symmetric to the pre-existing `qualify_local_types`
(owner-side); the new pre-pass is the consumer-side mirror.
Downstream passes see qualified Types regardless of authoring form.
The TypeDef-first ladder still lives in `synth`'s `Term::Var` arm
because `<TypeName>.<member>` is term-position-only — `Maybe.from_maybe`
is a Var, not a Type expression, and the pre-pass does not rewrite
Var names.

Alternatives considered:

(a) Add TypeDef-first ladder at every resolution site separately
    (the plan's implicit assumption). Rejected: O(N) extension
    sites, each carrying the same workspace-walking logic; the
    pre-pass version is O(1) — one pass, every downstream consumer
    benefits.
(b) BLOCKED + spec re-brainstorm. Rejected: the architecture
    extension is consistent with prep.1's thesis (bare type-name
    resolves to the workspace-wide TypeDef) and forward-compatible
    with prep.2 (Term::New.type_name falls under the same rewrite)
    and prep.3 (kernel-tier TypeDefs enter the workspace map
    automatically). No design regression to bounce back over.

Spec updated to document the realisation mechanism honestly: the
"Realisation mechanism — workspace pre-pass" subsection clarifies
that the resolver-level semantics described in "Implementation
shape" are the user-facing contract, and the actual code path is
the pre-pass.

Verification:

- `cargo test --workspace`: ALL GREEN. 87 e2e + every crate's unit
  + integration tests pass with no regressions.
- Three NEW in-source tests pin Task 1's resolver paths:
  `type_scoped_member_resolves`, `type_scoped_member_not_found`,
  `type_scoped_receiver_not_a_type`.
- One NEW workspace test pins the narrowed validator:
  `ct1_validator_accepts_bare_with_explicit_import`.
- One renamed-and-flipped existing test:
  `ct1_validator_rejects_bare_xmod_with_import_candidate` →
  `ct1_validator_accepts_bare_xmod_with_import_candidate` (the
  bare-with-import path is now ACCEPTED).
- One NEW companion test for the workspace-wide ctor lookup:
  `ct2_term_ctor_bare_cross_module_via_workspace_resolves`.
- Two pre-existing tests' assertions updated for the new error
  wording: `ct1_check_cli::check_human_mode_emits_actionable_message_to_stderr`
  and `crates/ailang-check/tests/workspace.rs::unknown_module_prefix_is_reported`.
- 12 migrated `.ail` fixtures verified via the existing e2e
  suite (each fixture is the test runner's target for an existing
  `build_and_run` assertion).
- Negative fixture `ct_2_bare_cross_module.ail` semantically
  preserved: dropped its `(import std_maybe)` so bare `Maybe` is
  out-of-scope under the narrowed rule and still fires
  `BareCrossModuleTypeRef`.

Concerns:

- The pre-pass introduces a new architectural layer (consumer-side
  qualification) that the spec did not originally anticipate. Spec
  amendment in this commit documents the layer. Future iterations
  reference `prepare_workspace_for_check` as established
  infrastructure.
- `examples/test_ct1_bare_xmod_rejected.ail.json` switched its
  offending name from bare `Ordering` (which under the prep.1
  semantics may now resolve via implicit prelude) to a still-
  unresolvable `Mystery_Type`. The CLI test's intent (assert that
  a human-mode `ail check` exits non-zero on a still-RED case) is
  preserved.

Milestone status: kernel-extension-mechanics (Gitea #6) advances
1/3 iters. Next: prep.2 (`Term::New` construct) issue #32.
2026-05-28 14:43:03 +02:00
Brummel 46c9aabf00 plan: prep.1 type-scoped namespacing — 5-task atomic resolver + 12-fixture migration (refs #31)
The plan covers the first iteration of the kernel-extension-mechanics
milestone: type-scoped `<TypeName>.<member>` resolution in
`ailang-check`, narrowed `BareCrossModuleTypeRef` /
`BadCrossModuleTypeRef` diagnostics in `ailang-core::workspace`,
two new `CheckError` variants, CLI diagnostic-message rewording,
and atomic rewrite of 12 `.ail` example fixtures from `std_X.Y` to
the type-scoped form. Five tasks, each unit-of-review.

Includes a spec correction (same commit because plan recon
surfaced it): the prep.1 Blast Radius previously claimed
`hash_pin.rs` + `prelude_module_hash_pin.rs` refreshes and a
`design_schema_drift.rs` pin addition. Plan-recon walked every
test crate (per the schema-camelcase-fix hash-pin-blast-radius
lesson) and found:
  * neither hash-pin file pins any fixture in prep.1's migration
    set — refresh is empty;
  * type-scoped resolution is a checker-only change (no new JSON
    tags, no AST shape change) — the drift pin belongs to prep.2
    (`Term::New`) and prep.3 (`kernel: true` + `param-in`), not
    prep.1.

Spec section 'Blast radius' and the 'Iteration scope' summary now
reflect the recon-cleared reality.
2026-05-28 14:04:01 +02:00
Brummel 832375f2ac convention: counter-prefix file naming across docs/specs/, docs/plans/, design/contracts/, design/models/
All 176 files in the four accumulating directories now use a
zero-padded 4-digit counter prefix that reflects creation order
(`NNNN-slug.md`). The counter is assigned per directory in strict
git-log creation order; ties broken alphabetically by original name.
The old `YYYY-MM-DD-` prefix on docs/specs/ and docs/plans/ files is
dropped — the date is recoverable from git log and the counter
carries the ordering.

A file's counter is stable for the life of the file: never reassigned,
never reused, never compacted. Deleted files retire their counter;
subsequent files do not fill the gap. This is the property that lets
cross-references stay literal — refs use the full filename including
the counter (`design/contracts/0007-honesty-rule.md`) so they grep
cleanly and resolve directly without a glob step.

313 cross-references updated across .md/.rs/.toml/.c/.json files
(test pins, include_str! paths, design-INDEX entries, baseline notes,
runtime C comments, inter-contract markdown links incl. bare basename
and `../models/foo.md` forms).

CLAUDE.md gets a new "File-naming convention" section spelling out
the rule and rationale. skills/brainstorm/SKILL.md and
skills/planner/SKILL.md updated so new spec/plan creation produces
counter-prefixed names from the start.

The full test suite (cargo test --workspace) passes.
2026-05-28 13:31:31 +02:00
Brummel 7b8596cef0 spec: kernel-extension-mechanics — three-iter prep milestone
docs/specs/2026-05-28-kernel-extension-mechanics.md: per-milestone
implementation container for the four language-level mechanisms
described in design/models/kernel-extensions.md.

Three iterations, each independently shippable:

- prep.1 — Type-scoped namespacing. Resolver change so
  `<TypeName>.<member>` resolves to the type's home module.
  Migrates ~14 std-library example fixtures from `std_X.Y`-style
  cross-module references to type-scoped form. Hash pins
  refreshed in lockstep (ailang-core + ailang-surface — full
  blast-radius walk, per the hash-pin-audit lesson from
  schema-camelcase-fix).

- prep.2 — Term::New. New AST variant and Form-A keyword
  `(new T args...)`, calls the `new` def in T's home module.
  Uses prep.1's resolution.

- prep.3 — Kernel-tier modules + `param-in`. Schema flag
  `Module.kernel` + TypeDef `param-in` field. Workspace-load
  generalises the existing hardcoded prelude auto-injection
  (loader.rs:98-108 + workspace.rs:308-311, 467, 2655) into a
  flag-driven mechanism; prelude itself gains `kernel: true` as
  a code-path migration (consumer-observable behaviour
  unchanged — prelude_free_fns.rs regression test stays green).
  A new minimal ailang-kernel-stub crate ratifies the mechanism
  without domain content.

Five new diagnostics across the three iters:
TypeScopedMemberNotFound, TypeScopedReceiverNotAType,
NewTypeNotConstructible, NewArgKindMismatch,
ParamNotInRestrictedSet. BareCrossModuleTypeRef and
BadCrossModuleTypeRef diagnostics repositioned (still "type
name not resolvable" but with revised remediation pointers).

Each iter's spec section carries explicit `## Canonical form
decision`, `## Blast radius`, and `## Integration with existing
mechanisms` subsections — the migration-policy discipline made
visible per spec section.

Grounding-check (two dispatches): first BLOCK on prelude
auto-injection mis-framing (claimed it was a new mechanism;
in fact prelude is hardcoded-auto-injected today), corrected
inline; second PASS after the revised two-tier-architecture
framing also landed. All load-bearing assumptions ratified by
named tests in the working tree.

Out of scope, named explicitly: the raw-buf milestone (#7), the
series milestone (#8), record-element SoA support, LSP/MCP
integration for `ail describe`, higher-kinded `param-in`, and
primitive-instance intercept migration (deferred to #7 when
there is a real second consumer for the registry).

Refs Gitea milestone #6.
2026-05-28 13:14:35 +02:00
Brummel 90977663ff plan: schema-camelcase-fix — 4-task atomic schema-tag migration (refs #30)
Iteration plan derived from `docs/specs/2026-05-21-schema-camelcase-fix.md`
(commit 55ce6d0, amended). Single-iteration milestone, four tasks:

- **Task 1 (RED)**: add `lam_serialises_with_kebab_keys` pin to
  `design_schema_drift.rs` + extend `anchor_in_jsonc_block` walk
  to require the new tag spellings inside `data-model.md`'s `lam`
  fenced block. Verifies pin fires RED on current state.

- **Task 2 (GREEN atomic schema swap)**: 10-step coupled
  migration of `ast.rs:492,494` + `workspace.rs:1925-1926` +
  `examples/test_loop_binder_captured_by_lambda.ail.json` +
  `experiments/.../master/examples/fn_with_lambda.ail.json` +
  `design/contracts/data-model.md:142-147`. All five files MUST
  move together — changing only `ast.rs` makes the JSON literals
  undeserialisable (no `#[serde(default)]` on `param_tys`/`ret_ty`,
  so the renamed-away key is a hard error).

- **Task 3 (GREEN-2 rustdoc honesty)**: three pure-prose edits in
  `ast.rs:8`, `parse.rs:81`, `check/lib.rs:1707`. Decoupled from
  the schema swap — no compile or test consequence.

- **Task 4 (Verify)**: exhaustive negative grep, full workspace
  test suite, round-trip invariance, forbidden-files check.

Plan pre-recon by `ailang-plan-recon` (DONE_WITH_CONCERNS) caught
the data-model.md + rustdoc layer the brainstorm initially missed
— spec was amended in commit 55ce6d0 + re-grounding-check PASS
(9 claims ratified, 2 of them via exhaustive negative grep as
"no-test-pins-this" Boss-overrideable shape).

Self-review: 8/8 (spec coverage, no placeholders, name consistency,
step granularity 2-5 min, no commit steps, pin/replacement
contiguity, compile-gate threading complete within Task 2,
verification filters all resolve to named real tests).
2026-05-21 12:48:59 +02:00