124 Commits

Author SHA1 Message Date
Brummel 74849160b7 spec: cross-model harness corpus revival (refs #68)
The cma authoring-form harness (experiments/2026-05-12-cross-model-authoring)
is milestone-complete but its corpus went schema-dead: the implicit-cutover
made param_modes/ret_mode mandatory, so today's ail check rejects 100% of the
master examples + reference solutions and the renderer crashes on load. This
spec revives the corpus to the current language and gets a mock run green.

Scope (corpus-first, method-form-later per #68): minimal field-completion
preserving existing modes; two new author-facing examples (loop/recur, new);
spec.md section 4 rewritten to own/borrow with the borrow-over-value rule
surfaced under the parse-gate; spec_completeness allowlists the non-authorable
Term::Intrinsic out; mock fixture migrated so harness score assertions hold.
No live IONOS call. Old specs 0017/0018 left intact as closed-milestone
history.
2026-06-02 16:32:31 +02:00
Brummel d72fe0c2e6 fix(codegen): drop branch-consume-split owned params (#63 leg 3)
The last leg of the #63 drop-soundness cluster: an (own ...) heap param
consumed on one match-arm / if-branch but live on a sibling was never
dropped on the live path, leaking one slab per call. This was the
residual live=2 leak in series_sma that kept the series headline from
being leak-clean.

Root: the per-fn aggregate consume_count (the worst-case max over all
branches from uniqueness::merge_states) was codegen's only consume
signal, and every owned-param drop site gated on it. A param consumed
on SOME branch makes the aggregate >= 1, so every site skipped it —
including the branch where it stays live. match and if share this root
(if is first-class MTerm::If, not desugared to match).

Mechanism (spec 0068):
- CAPTURE: uniqueness retains the per-branch consume snapshots it
  already computes per branch and discarded at the max merge — new
  BranchConsume + infer_module_with_cross_branches (the aggregate-only
  infer_module_with_cross now delegates and drops the channel).
- CARRY: additive MArm.consume / MTerm::If.{then,else}_consume, attached
  by lower_to_mir via a traversal-order cursor (post-order pop, kind- and
  exhaustion-asserts make a desync a loud panic; neutral for const bodies
  which uniqueness does not walk). The AST carries no node id, so the
  correspondence is the structural pre-order both walks share — pinned by
  branch_consume_maps_attach_to_matching_arms.
- GATE: emit_leakclass_branch_param_drops fires a fall-through drop only
  for the leak class (branch_consume==0 AND aggregate>=1), in match arms
  + both if branches; the pre-tail-call dec switches its gate source from
  the aggregate to per-arm. The fn-return dec and arm-close pattern-binder
  dec are UNCHANGED.

Safety:
- Double-free: the drop sites partition by aggregate (==0 -> existing
  fn-return/pre-tail-call; >=1 -> new per-branch). Disjoint, so no param
  is dropped twice — no fn-return disable, no tail-position analysis.
- Use-after-free: the checker rejects use-after-consume, so an
  aggregate>=1 param is provably dead past the construct (path-terminal
  drop, tail position irrelevant).
- Type eligibility (found by the full-suite gate during implement; spec
  0068 refined): the new agg>=1 site is the FIRST drop site that can
  reach a STATIC closure-pair param (a top-level fn ref like inc in
  Either.either, consumed on one arm, live on the other) — a .rodata
  constant with no rc-header. field_drop_call routes Type::Fn / static-Str
  / Type::Var to the bare ailang_rc_dec, which underflowed on it. The
  helper now drops only params with a real per-type heap-ADT drop fn
  (field_drop_call != "ailang_rc_dec"). Sound (no underflow) and
  leak-correct (a static closure/Str allocates nothing). Witnessed green
  by std_either_demo / std_list_demo / poly_rec_capture_demo.

Verification: full workspace suite green (116 binaries); both new leak
pins (if + match) and series_sma_no_leak_pin green at live=0; legs 1/2
pins still green; INTERCEPTS<->(intrinsic) bijection intact; no
Pattern::Lit reject path added.

The leg-3 helper's type precondition was applied inline by the
orchestrator (a narrowing guard clause in an already-reviewed Task-4
helper, full context loaded) after the implement-orchestrator correctly
surfaced the spec gap rather than papering over the regression.

This clears the series_sma leak tail; the series milestone (#61) close
stays a separate deliberate step (its end-to-end milestone fieldtest).

closes #63
2026-06-02 15:47:01 +02:00
Brummel f7226cfba5 spec: branch-consume-split drop soundness (#63 leg 3)
Design spec for the last open leg of bug #63: an (own ...) heap
param consumed on one branch of a match/if but live on a sibling
branch is never dropped on the live branch, leaking one slab per
call. This is the residual live=2 leak in series_sma that keeps
the series milestone #61 from closing.

The leak is general (match AND if, not Series-specific): the
per-fn aggregate consume_count — the worst-case max over all
branches from uniqueness::merge_states — is codegen's only consume
signal, and every owned-param drop site gates on it. A param
consumed on some branch makes the aggregate >= 1, so every drop
site skips it, including the branch where it stays live.

Approach A (chosen, scope = the whole branch-consume-split class):
retain the per-branch consume snapshots the uniqueness walker
already computes and currently discards at the max merge; carry
them additively on MArm.consume and MTerm::If.{then,else}_consume
(attached by lower_to_mir in structural traversal-order lock-step,
since Term/Match/If/Arm carry no node id); codegen reads them at
the branch drop sites. The check pass stays the sole consume
authority (the mir.3a direction).

Mechanism (refined during planning recon, simpler + provably safe
than the first draft's fn-return-disable): the drop sites PARTITION
by aggregate consume, so the per-branch map only ever ADDS drops
for the leak class and never collides with an existing site:

  - fn-return dec (lib.rs) and arm-close pattern-binder dec: UNCHANGED.
    They keep firing for aggregate == 0 (live on every path).
  - pre-tail-call dec (match_lower.rs): gate source switches from the
    per-fn aggregate to the per-arm map. Identical decision for legs
    1/2 (aggregate 0 => per-arm 0 on every arm); additionally drops a
    param live on this tail-call arm but consumed on a sibling.
  - NEW fall-through drop (match arms + if branches), before the br to
    the join: fires ONLY for the leak class — branch_consume == 0 AND
    aggregate >= 1.

Double-free safety is disjointness by aggregate: the new drop needs
aggregate >= 1, fn-return needs aggregate == 0 — mutually exclusive,
so no fn-return disable and no tail-position analysis are needed.
Use-after-free safety: the checker REJECTS using an owned value
after it was consumed on any branch (use-after-consume), pinned green
by use_after_consume_on_own_param_is_reported and
harden_ownership_heap_double_consume_still_errors — so an aggregate>=1
param is provably never live past the construct, making the per-branch
drop path-terminal regardless of tail position.

grounding-check PASS (all load-bearing assumptions ratified by green
tests / structural fact, incl. the disjointness and use-after-consume
claims). Two RED repros carried: the already-committed ignored match
pin and a new if-branch pin.

refs #63
2026-06-02 15:18:08 +02:00
Brummel 3b3c8c4f07 fieldtest(series-step1): 5 examples, 5 findings (1 bug, 1 friction, 1 spec-gap)
Post-ship field test of the shipped `series` library extension as a
downstream Form-A author working from the public surface only. Five
fixtures under examples/fieldtest/ across the construction/auto-import,
ring-buffer/financial-indexing, len/total_count bookkeeping, param-in
error-quality, and operator-naming axes; spec at
docs/specs/0067-fieldtest-series-step1.md.

Wins (carry-on): auto-import works with no `(import series)`; the
financial index (0 = newest) maps directly onto the bounded-buffer
mental model and stays correct after multiple wraps; the param-in
diagnostic for a disallowed element type names the type, the
type-variable, the qualified type, and the allowed set.

Findings routed:
- [bug] An owned ADT threaded through tail-recursion whose base case
  neither returns nor consumes it is never dropped — a general
  drop-soundness leak (a plain `Box` control with no Series/RawBuf
  also leaks). Series surfaces it heavily on the whitepaper's headline
  streaming pattern, and the committed reference examples/series_sma.ail
  itself leaks (live=8). Minimal repro: series-step1_5 (live=4).
- [friction] `ail describe ... Series.push` (the canonical type-scoped
  form) reports module-not-found; only `series.push` / bare `push`
  resolve, and no command lists a type's op set.
- [spec_gap] referencing a kernel base type (`RawBuf`) from a user ADT
  ctor field requires full qualification (`raw_buf.RawBuf`) while bare
  `Series` works in type-position via auto-import — the asymmetry is
  undocumented.

The fixtures that leak still produce correct output; the leak is a
separate observable under AILANG_RC_STATS and an orthogonal,
pre-existing bug, not a Series defect.

refs #61
2026-06-02 13:22:30 +02:00
Brummel 9545257333 fieldtest: raw-buf milestone — 4 examples, 3 findings (refs #7)
Milestone-close fieldtest for the raw-buf milestone, milestone-scope
variant: four end-to-end scenarios derived top-down from the milestone
promise (construct via (new RawBuf <elem> <size>), fill/read via
set/get, size-only buffers, element variety across {Int,Float,Bool},
forbidden-element reject at check, buffer threading under own/borrow).

Result confirms the milestone-promise hypothesis — RawBuf is clean
end to end, zero bugs:
- mrb_1 Int fill/read loop: check ok, run + native build both print 70
- mrb_2 Float own->own threading + borrow read: prints 3.5
- mrb_3 size-only Bool buffer (#51 legitimate-but-unobserved case): prints 8
- mrb_4 Unit element: rejected at check with a precise
  param-not-in-restricted-set diagnostic; build stops at the same
  error, codegen never reached (the #51 fix)

Two [working] findings (carry-on) and one [spec_gap]: the
kernel-extensions whitepaper (design/models/0007) teaches bare-mode
fn-type slots that the post-#55 parser rejects. Verified and triaged
separately; the ledger fix follows in its own commit.
2026-06-02 10:46:16 +02:00
Brummel e25580e3a3 test(cutover): live accept/reject corpus for the #55 ownership surface
Wires the #55-cutover fieldtest into a durable regression net. The
fieldtest exercised the post-cutover ParamMode={Own,Borrow} surface as a
downstream LLM author would and produced 12 fixtures + a report; left as
loose files they would be dead fixtures that drift. This commits them as
a protected property.

crates/ail/tests/cut55_cutover_surface.rs — one table-driven test running
each fixture through `ail check` and asserting accept (exit 0) or reject
at the right pipeline stage. Reject rows key on the bracketed diagnostic
CODE (consume-while-borrowed, borrow-over-value, borrow-return-not-permitted,
surface-parse-error), NOT message text, so the test is decoupled from the
diagnostic-wording improvement tracked separately.

examples/fieldtest/cut55_*.ail — the corpus. cut55_2c is the #58 repro
(now correctly rejected). cut55_2b was misanalysed by the fieldtest as a
consume-while-borrowed reject; in fact bump's recursive param is
(borrow List) so the tail is only borrowed, never consumed into an own
slot — `ail check` correctly accepts it. Renamed to
cut55_2b_borrow_traversal_clean and recast as the #58 false-positive
guard (a borrow-position use of a heap sub-binder must stay accepted).
cut55_1b similarly does not fire over-strict-mode (its recursive call
consumes the tail, so the lint's consume_count==0 premise fails) — comment
corrected; it is a plain accept. cut55_1c is the genuine over-strict-mode
example (accepts exit 0, emits the warning).

docs/specs/0065-eliminate-implicit-mode fieldtest report — moved 2c to
the reject set, added the 2b false-positive-guard section, marked the
double-free spec_gap RESOLVED (it shipped as the #58 fix).

Relates to #55.
2026-06-02 00:45:21 +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 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 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 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 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 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 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 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 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 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 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 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 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 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 55ce6d0d70 spec: schema-camelcase-fix — expand scope to data-model.md + rustdoc layer (refs #30)
Spec amendment after plan-recon flagged four missed touch-points
in the brainstorm grounding-check loop:

1. `design/contracts/data-model.md:142-147` — fenced JSON-block in
   the canonical data-model contract. The data-model contract IS
   the canonical-schema doc (INDEX.md row, ratifying test
   `tests/design_schema_drift.rs`); leaving it on camelCase after
   `ast.rs` ships kebab is a direct Honesty-Rule violation.

2. Three rustdoc strings in production source that describe the
   present-state schema vocabulary: `ast.rs:8`, `parse.rs:81`,
   `check/lib.rs:1707`. Each enumerates the rename targets in
   prose; left unchanged they would describe a state that no
   longer exists.

3. Better home for the new schema-shape pin: `design_schema_drift.rs`
   (not `schema_coverage.rs`). That file already operates as the
   data-model-contract ratifying test, already builds Term::Lam
   exemplars at L121-129, and uses `anchor_in_jsonc_block` to walk
   data-model.md fenced blocks — the proposed extension slots
   directly into the existing pin family.

4. Experiment-tree files (`experiments/2026-05-12-.../master/spec.md`,
   `rendered/*.md`, `runs/**`) carry old tags. Per Honesty-Rule
   analogy with docs/plans/* — these are frozen historical
   artefacts of the 2026-05-12 cross-model-authoring experiment
   and are NOT migrated. The experiment's `master/examples/*.ail.json`
   fixture IS migrated (live JSON the workspace loader can
   deserialise); surrounding prose is not.

Plus an editorial fix: the fixture-occurrence count parenthetical
corrected from "3" to "2" — each fixture has one `paramTypes` +
one `retType`, one per line.

Spec re-dispatched through `ailang-grounding-check` (Step 7.5
re-PASS). All 9 load-bearing claims ratified, with 2 negative-grep
"no test pins this" ratifications openly flagged in the agent
report as a Boss-override-eligible shape. Acceptance criteria
renumbered to 7 (was 6).

Touch-point count now: 2 serde-renames + 1 workspace.rs literal +
2 .ail.json fixtures + 1 data-model.md fenced block + 3 rustdoc
strings = 9 files, all small edits. No hash-pin refresh required.
2026-05-21 12:45:51 +02:00
Brummel 7d086e69ce spec: schema-camelcase-fix — paramTypes/retType → param-types/ret-type (closes #30)
Brainstorm output. Single-iteration milestone: rename the two
camelCase JSON tags on `Term::Lam` — the only camelCase outliers
in the AST schema — to kebab-case, matching the convention every
other compound-key tag (`reuse-as`, etc.) already follows.

Scope is verified-tiny: ast.rs (2 serde-rename strings) +
workspace.rs in-source test JSON-literal + 2 `.ail.json` fixtures.
None of the five hash-pinned `.ail` modules contains a lambda,
so the milestone refreshes zero hash pins. Form-A is untouched —
the surface uses `(params (typed ...))` / `(ret ...)`, never the
camelCase tags.

Feature-acceptance gate: passes weakly-but-honestly. Clause 1 is
indirect (schema-internal consistency reduces the LLM author's
"compound-tag-is-kebab" generalisation failure rate); there is no
empirical preference measurement for this axis. Clause 2 holds
(one fewer special case to memorise). Clause 3 vacuous (no
semantic surface touched).

Grounding-check (Step 7.5) PASS: all four load-bearing claims
ratified by currently-green tests — round_trip.rs (Form-A
invariance), hash_pin.rs (no lambda in pinned modules),
workspace.rs in-source ct1_validator test (exhaustive call-site
list), design_schema_drift.rs (kebab convention pin).

Not bundled with #27 (arith-rename) per the spec preamble: #27
carries its own four open brainstorm questions and is a separate
milestone; each gets one re-pin wave with its own rationale, no
churn savings from bundling.
2026-05-21 12:37:45 +02:00
Brummel 8d61599b8d doc: fix bare (class Eq) → (class prelude.Eq) in operator-routing-eq-ord spec north-star
Fieldtest (505eb84) finding spec_gap → tighten-the-design-ledger:
the spec's §"Concrete code shapes" north-star example at line 113
wrote `(class Eq)` (bare). An LLM-author who copies the spec
verbatim hits `bare-cross-module-class-ref` — the language
requires `(class prelude.Eq)` (qualified) at instance-declaration
sites. The shipped fixture `examples/eq_user_adt_smoke.ail`
already uses the qualified form (line 5 — implementer caught the
drift during Task 1 fixture authoring), so the corpus was
consistent; only the spec text drifted. Fix: rewrite line 113
to match the qualified form the language and the existing
corpus require.

One-line doc fix; no code surface change. Spec stays as
`docs/specs/2026-05-20-operator-routing-eq-ord.md` (Status: Draft
unchanged — content correction, not a re-issue).
2026-05-21 01:34:54 +02:00
Brummel 505eb8484e fieldtest: operator-routing-eq-ord — 5 examples, 6 findings (4 working, 1 friction, 1 spec_gap)
Post-audit field test of the operator-routing-eq-ord milestone
(closed via 5170b6a + 4d45bc6). Five `.ail` Surface-form fixtures
under `examples/fieldtest/` written by an LLM-author working
strictly from the design/ ledger + public examples (no
`crates/` / `runtime/` / `bench/` reads — Iron Law honoured):

  eqord_1_fizzbuzz.ail        — FizzBuzz [1..15] via (app eq …)
                                 on Int+Str and (app gt …) on Int
  eqord_2_rational_eq.ail     — data Rational + user-instance
                                 (class prelude.Eq) by cross-mult;
                                 inner Int-eq nested in instance body
  eqord_3_newton_sqrt.ail     — Newton's method via float_lt
                                 (convergence + fabs) and float_eq
                                 (zero-guard)
  eqord_4_float_ord_must_fail.ail — (app lt 1.5 2.5) must reject
                                     at typecheck with float_lt hint
  eqord_5_float_eq_must_fail.ail  — (app eq 1.5 1.5) must reject
                                     at typecheck with float_eq hint

Findings:

  [working] x4 — primitive Eq/Ord at Int/Bool/Str/Unit;
    user-ADT Eq with nested primitive eq calls; Float named-fn
    surface (float_eq/float_lt/etc.); NoInstance Eq/Ord Float
    diagnostic with float_eq / float_lt addendum. The milestone's
    central thesis (class-dispatch is the only comparison
    surface; Float opts out via named fns) is LLM-natural —
    every (app eq …) / (app gt …) / (app float_lt …) call I
    wrote compiled on first try with no diagnostic friction.

  [spec_gap] x1 — `docs/specs/2026-05-20-operator-routing-eq-ord.md:113`
    north-star example writes bare `(class Eq)` where the
    language requires `(class prelude.Eq)`. An LLM-author who
    copies the spec verbatim hits `bare-cross-module-class-ref`.
    The milestone spec is the highest-information reference an
    LLM-author consults; the bare-vs-qualified drift mis-primes
    the pattern. Resolution: tighten-the-design-ledger — fix
    the spec example inline (separate follow-up commit). The
    existing `examples/eq_ord_user_adt.ail` already uses the
    qualified form, so the corpus is consistent; only the spec
    drifted.

  [friction] x1 — `(app compare 1.5 2.5)` at Float fires the
    same diagnostic as `(app lt …)` at Float, naming
    `float_eq / float_lt (and siblings)` as the alternative.
    But the alternatives don't return Ordering; an LLM-author
    who wanted three-way LT/EQ/GT can't satisfy that with
    float_lt + float_eq alone. The spec (lines 433-436)
    acknowledged this case in commentary — "no float_compare
    ships; build it from float_lt + float_eq if you need
    three-way" — but the diagnostic doesn't say so. Resolution:
    file as Gitea backlog issue for a follow-up tidy iteration
    (codegen-side: branch the Float-aware NoInstance addendum
    on the called method-name; the `compare` arm gets a
    one-sentence "no float_compare; build it from float_lt +
    float_eq if you need three-way" addendum).

Status: clean — no bugs, no blockers, the milestone surface is
solidly LLM-usable. Both non-working findings are tractable
forward-fixes that don't disturb the milestone's core
contracts.

Spec: docs/specs/2026-05-21-fieldtest-operator-routing-eq-ord.md
2026-05-21 01:34:19 +02:00
Brummel a68d7b6353 spec: operator-routing-eq-ord — drop comparator builtins, route through Eq/Ord
Resolves Gitea #1. Realises the "P2 follow-up" called out in
examples/prelude.ail line 9. Approach A (single-iter atomic
milestone): one cohesive cut across seven layers in one iter,
plus the regenerated prelude module hash pin.

Three design forks resolved with the user via brainstorm Q&A:

  (1) Operator-name surface: `==` `!=` `<` `<=` `>` `>=` die from
      the language. The LLM-author writes only the class-method
      names `eq` `ne` `lt` `le` `gt` `ge` `compare`. One mental
      model: AILang has class-dispatch, operators are not a
      separate concept. Option 2 (keep names as surface aliases)
      was rejected because it adds a second spelling for an
      identical operation — the redundancy the milestone is
      supposed to remove gets reintroduced syntactically. Option 3
      (two-track: primitive Built-in for Int/Bool/Str/Unit, class
      for user-types) was rejected because the two-pathy state is
      precisely what this milestone exists to dismantle.

  (2) Float comparison surface: Float keeps comparison capability
      via six named prelude fns (`float_eq` `float_ne` `float_lt`
      `float_le` `float_gt` `float_ge`), no `Eq Float` /
      `Ord Float` instance. Motivation: all three feature-acceptance
      clauses simultaneously satisfied — clause 1 (LLM-natural via
      Library-Convention-Pattern like Python's `math.isclose` or
      Rust's `approx`-crates), clause 2 (within the polymorphic
      surface there is one path; Float is honestly stamped
      "non-polymorphic"), clause 3 (the NaN-comparison anti-pattern
      stays visible in code as `float_eq` rather than hiding
      behind `eq`). Option A (Float loses all comparison) was
      rejected as overstretch — workarounds via `is_nan` +
      arithmetic are more bug-prone than a named fn that emits the
      right fcmp directly. Option C (Eq Float with IEEE semantics)
      was rejected as clause-3 violation — it would reintroduce
      the silent NaN-comparison bug class that the existing
      Float-no-Eq/Ord design exists to prevent.

  (3) Codegen mechanism for primitive Eq/Ord instances: option β
      (always-call through dispatch, with `alwaysinline` attribute
      on intercept-emitted bodies as pre-emptive `-O0` mitigation,
      and option α — call-site intercept — held as the bench-gate
      fallback if measurements show real regression). Motivation:
      semantic honesty (class methods ARE calls, optimiser folds
      primitives uniformly at Int and User-Point alike), parity
      with `Show` (which is also full-call today with no
      intercept), smaller IR-shape contract for the existing pins.
      Under `-O2` the inliner deterministically collapses 2-
      instruction bodies; bench corpus runs `-O2`. Under `-O0`
      `alwaysinline` overrides the no-inline default, giving the
      same per-call IR shape as today. α was the initial-framed
      Recommendation but was scrutinised in user pushback ("spricht
      irgendwas FÜR β?") and after substantive re-balancing β
      came out coherent.

Grounding-check (ailang-grounding-check) PASS on re-dispatch — 11
load-bearing assumptions ratified by:
`crates/ail/tests/e2e.rs::eq_demo` + `::lit_pat_demo`,
`crates/ail/tests/eq_ord_e2e.rs::eq_ord_polymorphic_runs_end_to_end`
+ `::eq_ord_user_adt_runs_end_to_end` + `::eq_ord_user_adt_eq_intbox_hash_stable`,
`crates/ail/tests/eq_float_noinstance.rs::eq_at_float_fires_float_aware_noinstance`,
`crates/ail/tests/prelude_free_fns.rs::ne_at_int_produces_mono_symbol` (+4 siblings),
`crates/ailang-surface/tests/prelude_module_hash_pin.rs::prelude_parse_yields_canonical_hash`,
plus in-source `#[cfg(test)] mod tests` ratifiers in
`crates/ailang-check/src/lib.rs` (`eq_typechecks_at_int/bool/str/unit`,
`mq2_env_method_to_candidate_classes_built`) and
`crates/ailang-codegen/src/lib.rs` (`lower_eq_str_calls_strcmp_with_bytes_pointer`).
The `alwaysinline` LLVM attribute is correctly classified as new
feature-work commitment (no live occurrences today), not as a
load-bearing assumption about present state.

First grounding-check pass BLOCKED on one spec defect — the spec
mischaracterised `crates/ailang-core/src/desugar.rs:2414` as "a
separate desugar pass" when it is in fact a `#[cfg(test)] mod tests`
AST-literal scaffold. Fixed inline: §Architecture and
§Components/4 now enumerate only `build_eq` (desugar.rs:1099) as
the sole production desugar site; `desugar.rs:2414`, `lib.rs:6092`,
`lib.rs:6220` are framed as test-scaffold migrations alongside the
production change, not as desugar-pass work. Re-dispatch PASS.

Out of scope (tracked separately):
  - Deriving for Eq/Ord — `typeclasses.md:177` "No deriving"
    stands; instance bodies remain hand-written.
  - Parameterised-ADT instances (`instance Eq (List a)` etc.) —
    requires constraint-propagation infrastructure, belongs to
    Gitea #2 ("22c typeclass corpus expansion").
  - Eq/Ord for the `Ordering` ADT itself — no use case; consumed
    via match, not compared.
  - `and` / `or` as Builtins — north-star fixture uses
    `(if … … false)` for short-circuit conjunction; separate
    concern.

refs #1
2026-05-20 23:43:13 +02:00
Brummel 50dc478ca5 spec: boehm-retirement — drop the transitional Boehm GC path
Resolves Gitea #4. Approach A (atomic single iteration). Six layers
in one cohesive cut: CLI `--alloc=gc` arm removed; codegen
`AllocStrategy::Gc` variant deleted with the default flipped to
`Rc`; libgc link branch removed; ~3 pure-differential e2e tests
plus the `gc_stress.ail` fixture deleted; ~9 RC-feature tests that
used GC-stdout as backstop lose only the differential assertion
(absolute fixed-stdout pin retained); M2 staticlib alloc-guard
drops its gc-arm (bump-arm preserved); design/models/rc-uniqueness
excises the Boehm-parity-oracle narrative; pipeline.md drops the
libgc pipeline-diagram arm; docs_honesty_pin flips from
Boehm-present-tense anchor to four absence-pins against
Boehm-zombie strings.

Three design forks were resolved with the user via brainstorm Q&A:

  (1) bump survives as bench-floor — `AllocStrategy::Bump`, the
      `--alloc=bump` CLI flag, and `runtime/bump.c` all stay; the
      enum keeps two variants (Rc, Bump); the codegen
      negative-complement test retargets from `AllocStrategy::Gc`
      to `AllocStrategy::Bump`. Bump's standing role is the
      raw-alloc bench-floor for RC-overhead measurement, not a
      production target.
  (2) the ~12 RC-vs-GC differential e2e tests are NOT all deleted
      wholesale — pure-differential ones (test name literally
      `*_matches_gc_*` or `*_same_stdout_as_gc`) are deleted; the
      RC-feature tests with GC as backstop keep their absolute
      stdout pin (`assert_eq!(stdout_rc.trim(), "<n>")`) and only
      lose the differential assertion. This nuance was added at
      spec time on top of the user's "delete the differential
      pattern" answer, because the differential was incidental to
      tests like `rc_box_drop` / `rc_list_drop_borrow` that pin
      drop-fn correctness under RC and would lose unrelated
      coverage if deleted entirely.
  (3) the 1.3× RC-over-bump number is retained in
      design/models/rc-uniqueness.md but reframed as a
      bench-health regression gate (not a Boehm-retirement gate);
      the closure-chain ±15% wider band is preserved analogously.

Grounding-check (ailang-grounding-check) PASS — 7 load-bearing
assumptions ratified, all spec-named paths and line numbers
verified (±2 lines), all proposed-for-removal strings present at
the spec-named locations. Assumption #7 (codegen default
currently Gc) is structurally self-evident: removing the variant
forces the default onto a surviving one by construction.

Out of scope, tracked separately:
  - Gitea #3 "Closure-pair slab / pool" — would tighten the
    closure-chain ±15% band; not blocked by retirement.
  - Any further `AllocStrategy::Bump` rework — still a
    single-variant bench instrument.

refs #4
2026-05-20 20:02:32 +02:00
Brummel a97aaebd45 spec: bench-harness-recalibration — degate max_us/p99_9, recapture baselines
One-iteration infra milestone closing Gitea #15 + #16. Two issues
framed distinct symptoms; reproduction on 2026-05-20 HEAD via two
back-to-back `bench/check.py -n 5` runs collapses them onto a
single picture:

- #15 ("*.bump_s stale") is wider than the issue framed.  Drift
  reproduces deterministically on `bench_list_sum.bump_s`
  (+13.91% / +15.56%) AND on `bench_hof_pipeline.gc_s` (+10.60% /
  +10.94%) — not just the bump_s family.  Drift correlates with
  memory pressure: `bench_tree_walk` and `bench_compute_collatz`
  are clean across both runs.

- #16 ("*.max_us structural false-positive") has matured.  The
  "vanishes on rerun" pattern the issue documented from
  2026-05-16 / 2026-05-18 audits is gone: today
  `implicit_at_rc.max_us` reproduces at +47.20% / +49.42%, and
  `implicit_at_rc.p99_9_us` at +27.79% / +41.92%.  Same arm
  only (`implicit @ rc`); the other two latency arms are clean.
  The metrics are now drift-elevated AND structurally jitter-
  prone — both reasons to retire them from the gate.

Decision: one cohesive change, all in `bench/baseline.json`.  Drop
the six unreliable entries (`max_us` + `p99_9_us` × 3 latency
arms), then `bench/check.py --update-baseline` from current HEAD
to absorb the environmental drift in one honest cut.  No code
change to `check.py` / `run.sh` / `latency_harness.py` — the
harness already iterates only entries present in `baseline.json`,
so the schema mechanics work as-is.

Alternatives considered and rejected:

- Adding a `gated: bool` field per metric to keep `max_us` /
  `p99_9_us` measured-but-not-gated.  Pure speculative
  infrastructure: today's pathology is drift, not jitter, and
  the diagnostic value of `max_us` in the `check.py` report is
  hypothetical (it's already in `run.sh` output upstream).
  User push-back ("was soll 1?") was correct.

- Tolerance widening on the drifted metrics.  Hides the drift
  under a wider band; a real +10% codegen regress would
  slip through.

- `-n` raise for tail metrics.  Doesn't address the structural
  problem (max-of-distribution is OS-jitter-dominated regardless
  of N); blows up bench wall time ~10x.

- cpuset / `isolcpus=` pinning.  Sysadmin-layer fix that
  doesn't survive CI move or a new dev machine.

- Histogram-based latency methodology rework (Gitea #19).  The
  proper long-term fix, but a separate brainstorm; this
  milestone is the stop-gap that buys back the noise floor in
  the meantime.

Grounding-check (Step 7.5): PASS, trivial-spec path — no Rust
compiler / checker / codegen / schema / runtime claims (all
load-bearing claims are about the out-of-tree Python harness, and
are covered by the spec's own replay-pass + synthetic-injection
acceptance checks).
2026-05-20 16:02:46 +02:00
Brummel 42ff44adf6 spec: design-ledger-formal-links — corpus-grounded amendment (clause-5 fence-skip + closed enumeration)
Plan-time corpus verification surfaced a third distinct gap the
brainstorm-sample missed: data-model.md:38/66/79/206/226 are 'see §"..."'
annotations inside fenced jsonc code blocks (verified: fences at
30-87 and 203-228; the 5 refs fall inside), where a Markdown link is
literal text — not a navigable link on any renderer.

Per the "two+ surfaced ⇒ ground the spec properly once, don't iter-patch"
discipline: amendment 2 is the definitive corpus-grounded pass, not a
fourth-patch returning later. Every convert-set ref's prose-vs-fence
status + exact bytes personally verified before amending.

- clause-5 (Concrete a): adds a strip_fences helper toggling on
  triple-backtick / tilde lines; the per-file scan runs
  targets(&strip_fences(&raw)); the RED-first synthetic vector gains
  two fence-skip assertions (identity-stub of strip_fences ⇒ first
  assert fails ⇒ genuine RED). Contract grows from 3 to 4 predicates
  (+ fenced-code-not-scanned).
- Scope section: third out-of-scope carve-out — a 'see §"..."' inside
  a fenced code block is schema-example documentation = the inline-
  annotation analog of the nominal-mention carve-out (a // comment in
  a code example is code documentation, not a 'go browse there'
  pointer). Clean extension of the converged navigational-vs-nominal
  principle.
- Acceptance 3: convert-set CLOSED and exhaustive — 8 prose refs
  (float-semantics:69/100, embedding-abi:45, memory-model:44/105,
  scope-boundaries:48/88), 2 disposition-(b) homeless removals
  (pipeline:61, authoring-surface:180), 6 stay-prose (data-model
  in-fence x5, embedding-abi:51). Iter-provenance suffixes stripped
  on conversion. scope-boundaries:88 splits into a source link plus a
  Pipeline cross-file link.

grounding-check third dispatch on the amended bytes: PASS, all 8
assumptions ratified (incl. corpus-state-verified A4-A7 per the
rolesplit precedent for text-enumeration claims).
2026-05-19 23:14:56 +02:00
Brummel b1a0364bf2 spec: design-ledger-formal-links — recon-driven amendment (clause-6 + cross-ref definition)
Planner Step-2 recon (ailang-plan-recon) surfaced two spec defects;
forward-fix amendment (63b669f stands, main forward-only):

- clause-6 homeless-ref remedy was an incomplete binary. Recon proved
  roundtrip-invariant.md does NOT carry the ail-merge-prose-cycle
  content that pipeline.md:61 / authoring-surface.md:180 cite — so the
  prior canonical retarget sample was wrong. Replaced with three
  priority-ordered dispositions; PROSE_ROUNDTRIP refs take (b) (drop
  the cross-tier pointer, preserve the present-tense behavioural prose).
- Added a precise 'what counts as a cross-reference' definition:
  navigational pointer = in scope; nominal source-symbol mention = out
  of scope (keeps the milestone 'formalise the cross-references', not
  'hyperlink the ledger'). Source link only for a genuine navigational
  (see crates/...). Folds OQ2 (strip iter-provenance suffixes on
  conversion) + OQ3 (no-title directional stays prose) as byte-policies.

grounding-check re-dispatched on the amended bytes: PASS, all
assumptions ratified incl. the amendment-introduced ones.
2026-05-19 23:03:03 +02:00
Brummel 63b669fa8f spec: design-ledger-formal-links — formal cross-links in the design/ ledger
Positive completion of the DESIGN.md -> design/ split: informal prose
cross-references become formal file-relative Markdown links, gated by a
new RED-first design_index_pin.rs clause-5 (every design/ body link
resolves into the durable tier or fails the build). INDEX spine stays
repo-root-relative (registry tier, clause-1 unchanged). No authoring
surface. Eight converged commitments + the user-decided INDEX sub-fork;
grounding-check PASS 7/7. roadmap [~] in-flight.
2026-05-19 22:51:21 +02:00
Brummel 314e5e4920 spec: design-md-rolesplit — Boss-adjudicated relocation appendix (planner recon resolution)
plan-recon surfaced 7 open questions (genuine Boss design judgement,
not architectural forks). Adjudicated and folded into the spec as an
authoritative placeholder-free Appendix relocation map (every ##/###
of DESIGN.md -> exactly one destination):

- design_index_pin.rs clause-2 widened to accept crates/**/src/*.rs
  in-source #[cfg(test)] mod tests as first-class ratifiers.
- INDEX ratifier tokens resolved to recon-verified paths; memory-model
  -> uniqueness.rs (in-source), tail-calls -> lib.rs:5044
  tail_call_in_non_tail_position_is_rejected (NOT loop_recur — different
  construct), env-construction path corrected to ailang-check/tests.
- 3 ledger additions the 12-list under-counted: qualified-xref
  (source-link only), str-abi (prose+source; the one sanctioned
  sentence-level move, 2802 bold para), scope-boundaries (present-tense
  honesty-pinned). Net 15 contracts / 5 models / 1 decision-record journal.
- OQ7: lib.rs:103 dangling 'Iter 13b' cite is deleted not retargeted
  (no forward target exists; a pointer would be fiction).

grounding-check re-dispatched after the material edit: PASS, all 6
changed/new ratifiers resolve to named currently-green tests.
2026-05-19 12:11:53 +02:00
Brummel a64b2ccb2a spec: design-md-rolesplit — DESIGN.md role-split into design/ contracts+models ledger
Role-split the 3020-line docs/DESIGN.md into design/contracts/ (the
hot, test-linked invariant set), design/models/ (one whitepaper per
model), and design/INDEX.md as the sole addressable spine; decision-
records rehomed to docs/journals/. Clean cut (DESIGN.md deleted, no
stub), ###-subsection relocation granularity, build-atomic landing
(design_schema_drift.rs include_str! forces one commit), polymorphic
INDEX links (code-SoT contracts point at source //!, no prose dup),
typed-INDEX anti-regrowth pin (design_index_pin.rs). grounding-check
PASS 13/13 after one re-dispatch (commitment-4 pin-status claim
corrected: Decision-6 :242 audit-trail sentence is pinned by no test,
retired to the decision-record journal, no successor pin by design).

roadmap: DESIGN.md->design/ marked [~] in flight (spec landed).
2026-05-19 11:58:07 +02:00
Brummel ae905de00c spec: embedding-abi-m5 — ail-embed adapter + data-server wiring + thread-swarm backtest
User-approved 2026-05-19; grounding-check PASS (all load-bearing
assumptions are already-shipped M3 ABI behaviour, each pinned by a
green non-ignored integration test: embed_tick_e2e own+borrow,
embed/tick_roundtrip.c, embed_staticlib_cli, embed_swarm_tsan,
embed_staticlib_alloc_guard). Terminal embedding milestone, zero
language/compiler/runtime change — ail-embed is a lean reusable
embedding module (Rust port of the audited tick_roundtrip.c) + the
real E2E; in-repo, [workspace]-excluded so the compiler workspace
owes nothing to ../libs/data-server or /mnt (Invariant 1 in the
dependency graph, not just on paper). Real data-server + real
Pepperstone ticks; symbol-fan headline + time-shard
boundary-invisibility proof (per-shard bit-exact; whole-window only
within f64-reassociation tolerance — self-review fix).

roadmap: M5 [ ]->[~] open in-flight, spec pointer added, dangling
"todo above" dependency reconciled to shipped 170464f; M4-retired
and Tick-coverage struck/done entries pruned (their own text
scheduled deletion at next-milestone-start; permanent rationale
survives in 2026-05-18-brainstorm-embedding-abi-m4-retired.md, still
pointed to by M5's context line). Mirrors the M3 1fbb9c4 precedent
(spec + roadmap-flip in one commit).
2026-05-19 00:52:38 +02:00
Brummel d5c565d48d iter embedding-abi-m3.1 (PARTIAL 5/7 + Boss spec-defect repair): single-ctor scalar record crosses the C ABI, ownership follows declared mode
Tasks 1-5 GREEN. T1 baseline pins (re-point annotation + @ailang_rc_alloc
heap-box byte-pin: size=8+n*8, tag@0, fields@8/16). T2 export gate widened
(is_c_scalar -> two-level is_c_abi_type: single-ctor all-Int/Float record;
multi-ctor/Str/List/nested still RED; gate suite 10/10; M1 adt-ret must-fail
re-pointed to multi-ctor+Str Reading). T3 codegen forwarder widened
(llvm_scalar record Type::Con -> ptr; M2 forwarder body byte-unchanged;
3/3 staticlib pins). T4/T5 E2E record round-trip own+borrow, global
leak-freedom.

Boss spec-consistency repair (M2.1-precedent class): orchestrator
correctly BLOCKED Task 5 on a genuine spec defect -- the single-ctx-readback
allocs==frees proof model is unsatisfiable for borrow (and only
coincidentally passes for own) because M2's TLS-ctx is bound only during
the synchronous forwarder call, so host-side decs land on g_rc_*, not ctx.
Boss-verified globally leak-free + value-correct both modes. Spec + plan +
harness amended to the stronger global model (sum all ailang_rc_stats:
lines; the M2-TLS cross-attribution documented as correct behaviour). No
fresh grounding-check (removes an over-strong measurement assumption).

Tasks 6 (DESIGN.md frozen-layout SSOT + lockstep pointers + freeze wording
+ enforceability demo) and 7 (workspace-green gate) re-dispatched on the
amended plan. Bench/architect milestone-close is audit-owned.

iter embedding-abi-m3.1 (PARTIAL); INDEX.md line deferred to the DONE commit
2026-05-18 21:16:41 +02:00
Brummel 1fbb9c433b spec: embedding-abi-m3 — frozen value layout + single ADT/record crossing + RC ownership contract (user-approved, grounding-check PASS 8/8); open in-flight [~] 2026-05-18 20:35:05 +02:00