4ec8f90b199eb89f3aac86204c42a06d916afc4c
140 Commits
| Author | SHA1 | Message | Date | |
|---|---|---|---|---|
|
|
dea93a99fc |
plan: 0064 iter 4 (final) — partition_eithers double-consume rewrite (class 4) (#57)
Final iteration of spec 0064. Closes class 4, the one genuine corpus bug (not a false positive): partition_eithers projects both (app Pair.fst rest) and (app Pair.snd rest) from one owned rest; fst/snd are own-param projections that move a field out, so rest is consumed twice. Universal linearity activation (#55) would reject it. Fix is a body rewrite (destructure rest once via match), not a check change. Latent-bug nature: partition_eithers has a bare (Implicit) param slot today, so the check is skipped on it -- no RED->GREEN flip on the real fixture. The rewrite's correctness is proven by the unchanged 2 3 2 3 E2E output (std_either_list_demo) and by the explicit-mode c4_double_consume must-stay-RED fixture (the double-projection shape IS rejected, verifiable today). Orchestrator verified the rewrite this session: ail check std_either_list -> exit 0; ail run the demo -> 2 3 2 3 (applied, ran, reverted for the implementer to re-apply). Two tasks: Task 1 rewrites partition_eithers + gates on the unchanged E2E output; Task 2 adds c4_double_consume (must-stay-RED, folded into a generalised harden_ownership_heap_double_consume_still_errors loop) and c4_rewrite (stays-clean, added to the false-positives-clean array). plan-recon confirmed no hash-pin on std_either_list (only the e2e 2 3 2 3 output pin, preserved). No check/schema/codegen change. This is the last of the four #57 hardening classes; after it lands, spec 0064 is code-complete and the cycle is ready for audit. refs #57 |
||
|
|
9c4d195806 |
plan: 0064 iter 3 — let-alias borrow propagation (class 2) (#57)
Third iteration of spec 0064. Closes false-positive class 2: Term::Let walks its value in Position::Consume, so (let a t (match a …)) over a borrow-mode t consumes t at the binding -> false consume-while-borrowed. Fix: Checker gains a scoped aliases: HashMap<String,String>. A (let a t body) whose value is a bare Var resolving to a tracked binder records a -> root(t) for the body scope and skips the consume walk. A new resolve_alias helper maps a name to its root, PREFERRING a real binder at each chain step -- so an inner binder named `a` (nested let, pattern binder, lam param) automatically shadows the alias with NO change to with_binder/walk_arm/Lam (the shadowing is handled centrally in the resolver, not at three scattered introduction sites). Every binder-state lookup keyed by name resolves through the map first: use_var, the App borrow bump + decrement, callee_arg_modes, and the reuse-as source. Design calls (orchestrator): - Alias REDIRECT, not state clone (spec scope decision 3): consuming the alias marks the single root consumed, so a real double-consume through an alias is still caught -- a clone would miss it (unsound). Pinned by the let_alias_of_owned_double_consume_still_errors unit test. - resolve_alias prefers binders -> shadowing needs no edits to the three binder-introduction sites (simpler, lower-risk than clearing aliases at each). - reuse-as source (linearity.rs:663) is the 4th binder-state-reading site; the spec's Fix 3 named three illustratively. Resolving there too avoids a spurious reuse-as-source-not-bare-var on an aliased Var. Faithful completion of Fix 3, documented as such. Three tasks, RED-first: Task 1 adds examples/c2_let_alias.ail to the harden list (RED: consume-while-borrowed on count's t); Task 2 is the alias mechanism (GREEN) + the soundness unit test; Task 3 closes the design/contracts/0008-memory-model.md:340 let-alias carve-out (was "has not shipped yet") and extends the Ratified-by trailer to name linearity.rs. Diagnostic-only; no schema/type change. plan-recon mapped current post-iter-2 line numbers; parse gate fired on the inlined fixture (RED confirmed). Class 4 (partition_eithers rewrite) remains for the final iteration. refs #57 |
||
|
|
2ad58ada17 |
plan: 0064 iter 2 — local function-typed binder modes (class 1) (#57)
Second iteration of spec 0064. Closes false-positive class 1: applying
a local function-typed binder (a HOF predicate param like
std_list.filter's `p`) treats its args as Consume because
callee_arg_modes resolves only globals -- so a heap arg reused after
(app p h) false-fires use-after-consume.
Fix: BinderState gains fn_param_modes: Option<Vec<ParamMode>>, seeded
at all three function-typed binder introduction sites -- params and
lam-params from the signature (param_tys), let-binders from the
LetBinderTypes table built in iter 1 -- via a new fn_modes_of helper
(strip_forall + Type::Fn { param_modes }). callee_arg_modes reads a
local Var callee's fn_param_modes before falling back to globals; the
existing App arg-walk maps Borrow to a borrow walk unchanged.
Scope call (orchestrator): all three seeding sites, not just the param
path that unblocks filter -- the point of this hardening cycle is to
leave no latent class (the #57-from-0063-deferral lesson), and the
extraction is the same at each site with the table already built.
Two tasks, RED-first: Task 1 adds examples/c1_local_hof.ail to the
harden list (RED: use-after-consume on filter_box's h); Task 2 adds the
field + seeding + callee_arg_modes read (GREEN), plus two in-source
unit tests (param path via signature, let path via hand-built table)
and a borrow-predicate test helper. Diagnostic-only; no schema/type
change. plan-recon mapped current post-iter-1 line numbers; parse gate
fired on the inlined fixture (RED confirmed).
refs #57
|
||
|
|
ba981689fc |
plan: 0064 iter 1 — let-binder type table + value-typed let exemption (#57)
First iteration of spec 0064 (#57 hardening). Tees the (def,binder)->Type table out of the typecheck pass (the inferred let-binder type computed at synth's Let arm and discarded today) and threads it into the linearity walk, then seeds BinderState.is_value for value-typed let-binders from the table -- closing false-positive class 3 (the class spec 0063 deferred). Two tasks, RED-first: Task 1 adds examples/c3_value_let.ail to the harden_ownership_false_positives_are_clean list (RED: use-after-consume on xnew); Task 2 threads the table end-to-end (synth -> check_fn -> check_in_workspace -> check_workspace -> check_module_with_visible -> Checker, all callers in one compile-atomic task) and seeds is_value at Term::Let (GREEN), plus two in-source unit tests (value-let clean, heap-let still errors). Fixes 1/2/4 (local fn-param modes, alias-redirect, partition_eithers rewrite) are later iterations of the same spec, explicitly out of scope here. plan-recon mapped the threading path; the parse-the-bytes gate fired on the inlined fixture (RED confirmed). refs #57 |
||
|
|
7dc21234f0 |
plan: re-stage 0121 — fold borrow-over-value into the cutover (mono collision)
First implementation contact surfaced a plan defect: the
borrow-over-value reject cannot land green as a standalone pre-cutover
task. The prelude's polymorphic comparison builtins are
`(params (borrow a) …)` (eq/compare/lt/…); the mono pass
(apply_subst_to_type, subst.rs:165) specialises `a := Int/Bool/Float`
into `compare__Int(borrow Int, …)` — the language's OWN generated code
in the forbidden `(borrow value-type)` shape. The standalone check
broke compare_{int,bool}_mono_symbol_emits_branch_ladder.
Spec 0062's "value types always own" rule is universal, so generated
code must honour it too: the principled fix is a mono-pass coercion
`(borrow value-type) -> (own value-type)` (a no-op — value types carry
no RC, so own vs borrow emits no inc/dec and the branch-ladder IR is
unchanged). The check and the coercion are a matched pair and now live
together in Task 4 (Steps B1-B2), which touches the mono path anyway.
Pre-cutover green tasks are now Task 1 (borrow-return reject) + Task 3
(throwaway migration tool). Task 1's check_def destructure binds only
ret_mode (param_modes stays under `..`); Task 4 Step B2 widens it.
Spec gap to record at the next 0062 brainstorm touch: the
borrow-over-value rule needs its mono-coercion clause spelled out — the
spec tested only the authored case.
refs #55
|
||
|
|
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 |
||
|
|
47fb328aca |
plan: harden ownership analysis — is_value_type + value exemption + app-borrow (0120)
Executable projection of spec 0063 into four tasks: (1) is_value_type predicate in ailang-core::primitives; (2) application-is-a-borrow (Term::App callee walked Position::Borrow); (3) value-type exemption (BinderState.is_value seeded at param/pattern/lam sites, use_var short-circuit); (4) on-disk RED->GREEN fixtures + workspace assertions. Placeholder-free with exact code for every edit site. refs #56 |
||
|
|
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
|
||
|
|
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. |
||
|
|
0bd7f47fad |
plan: mir.3b fill MArg.mode, switch the anon-temp gate onto it, delete module_def_ail_types
Executable plan for the second of two iterations delivering spec 0060's mir.3 row. A focused recon (the anon-temp gate + the per-arg mode sources) established two refinements to the spec sketch, both locked in the plan: 1. module_def_ail_types is deleted in mir.3b, not mir.5. A grep proved self.module_def_ail_types has exactly ONE reader — the emit_call anon-temp borrow-slot drop gate. The own-param drop reads param_modes from the def's own f.ty, not this table. So switching that gate onto MArg.mode leaves the table dead; deleting it now is the clean move. mir.5's "last re-derivation residue" shrinks to element-type/Term::New. 2. MArg.mode is filled for App args only; MTerm::Let.mode is not filled. Only App args have a real per-arg mode source (the callee Type::Fn.param_modes). Do/Ctor/Recur args have no per-arg mode (EffectOpSig/Ctor.fields/loop-binders carry none) -> stay Owned default. Let.mode has no source (ast::Term::Let has no mode field) and no consumer (the let-drop gate reads consume, not Let.mode) -> stays Owned. Safety property (recon-confirmed on the #43 anon-temp witness): MArg.mode is filled from the same callee fn-type the gate read from the table, so the drop fires identically. For (app RawBuf.get (app RawBuf.set …) 0), RawBuf.get's param 0 is borrow -> args[0].mode = Borrow, exactly the value module_def_ail_types yielded. Four tasks: (1) producer fills App-arg MArg.mode via a param_mode_at helper; (2) codegen gate reads arg.mode + delete module_def_ail_types (field + construction + threading, compiler-completeness-checked); (3) producer pin (App arg mode == Borrow) + the live=0 anon-temp acceptance (raw_buf_owned_drop_balances_rc_stats); (4) record the refinements in spec 0060. No new .ail fixture (reuses raw_buf_drop_min.ail). |
||
|
|
69749fc3e1 |
plan: mir.3a relocate consume_count into MIR, delete codegen's second uniqueness run
Executable plan for the first of two iterations delivering spec 0060's mir.3 row. Two plan-recon passes (mode/consume/drop-placement surface + the uniqueness-pass signature) established that the named deletion (the second infer_module_with_cross run) depends ONLY on consume_count: all three codegen scope-close drop sites read consume_count from self.uniqueness, while the modes they also gate on (param_modes for the own-param dec, scrutinee_is_owned for the match-arm dec) come from the type, not the uniqueness pass (UniquenessInfo carries no mode). So mir.3 splits: - mir.3a (this plan): relocate consume_count; delete the second run. - mir.3b (next): fill MArg.mode / MTerm::Let.mode from the type modes and switch emit_call's anon-temp borrow-slot gate onto MArg.mode. The split halves the change at the codebase's highest-risk surface (RC/drop correctness, where the raw-buf leak saga lived) and makes each half independently verifiable against the live=0 leak pins. Design decisions locked in the plan: - consume_count lands on a new binder-keyed MirDef.consume: BTreeMap<String,u32>, NOT on MArg. The recon surfaced the central structural tension: the drop sites key consume_count by (def, binder_name) — a per-binder aggregate — while the spec-sketched MArg.consume_count is per-arg-position and never reaches them. The per-def binder map matches both the UniquenessTable's own keying and all three binder kinds (param/let/pattern) uniformly. MArg.consume_count stays a mir.1 default (no consumer). Spec 0060 is updated to record this (Task 5). - Source = run check's infer_module_with_cross inside lower_to_mir on the post-mono module (the synth_pure single-engine-re-walked pattern). The alternative of retaining a check-time result is blocked: check's uniqueness runs pre-mono and on-demand-from-codegen, never during check_workspace, so a retained result would miss the mono specialisations. cross_module_types (= codegen's module_def_ail_types, module->def->Type) is rebuilt in elaborate_workspace from the post-mono workspace. Key safety property carried in the plan: this is a PURE RELOCATION of an identical computation. infer_module_with_cross ran on the post-mono module in codegen and now runs on the same post-mono module in lower_to_mir — same function, same input, same output — so drop placement is byte-identical. The live=0 leak pins (raw_buf_loop_no_leak, loop_recur_heap_binder, print_no_leak) are the value-correctness gate. Five tasks: (1) add MirDef.consume; (2) elaborate_workspace builds cross_module_types + lower_module runs the pass and fills consume; (3) codegen builds its (def,binder)->consume lookup from MIR, the three drop sites read it, delete the run + field + import; (4) producer pin + leak-pin acceptance + grep-clean; (5) record the split in spec 0060. No new .ail fixture (producer pin reuses new_counter_user_adt.ail). |
||
|
|
d81ea93de6 |
plan: mir.2 Callee::Static pre-resolved, codegen stops re-deriving the callee
Executable plan for spec 0060's mir.2 iteration: relocate static-callee
resolution from codegen into lower_to_mir. Two plan-recon passes (codegen
consumer surface + check-side producer surface) mapped the resolution
frontend; this plan locks three design decisions made during planning,
ahead of the tasks:
1. The Callee bridge enum is reshaped beyond the spec sketch: a third
variant Builtin{name,sig} (operators / str-num intrinsics have no
module/fn_name and are lowered inline by name), and a sig:Type on each
resolved variant. The sig is the lossless replacement for the pre-mir.2
Callee::Indirect(inner).ty() read that drop's synth_callee_ret_mode
depends on; it is synth_pure(callee), mode-preserving. NOT a mir.3
pull-forward — the MArg param-modes stay at mir.1 defaults. Spec
0060's Concrete-code-shapes Callee block is updated as part of the
iteration (Task 5).
2. Classification mirrors check's own synth Term::Var ladder
(lib.rs:3409-3582), never copies codegen's is_static_callee allowlist.
The recon's divergence audit confirmed check's builtin set and
codegen's inline-arm set match exactly, so the single-engine rule is
mechanically satisfiable with no env threading gap.
3. type_home_module is fully deletable: a workspace-wide grep confirmed no
program references a type-scoped T.fn as a value, so its second
consumer (resolve_top_level_fn) collapses to the module-name fallback
with no behaviour change. The spec's "grep-clean" acceptance holds; the
spec's "x3 mirrors" wording over-counts (def + 2 live sites).
Five tasks: (1) reshape Callee; (2) lower_to_mir classify_callee +
App arm; (3) the atomic codegen consumer switch (App arm/drop/lower_app
split/delete is_static_callee+type_home_module/resolve_top_level_fn);
(4) pins (strengthen #53 to assert Callee::Static, add a three-way
classification pin) + workspace suite; (5) strike the lower_app <->
is_static_callee lockstep row from CLAUDE.md, update spec 0060, clean
stale comment references. The classify_pin fixture is ail-parse-gated
(exit 0).
|
||
|
|
1a1a68df8b |
plan: mir.1b — codegen consumes MIR (the atomic switch)
Second half of spec iteration mir.1, planned against the landed mir.1a
infrastructure (
|
||
|
|
438a009b83 |
plan: mir.1a — typed-MIR infrastructure (additive half of spec mir.1)
First of two plans for spec iteration mir.1 (docs/specs/0060-typed-mir.md).
mir.1 is the project's largest iteration — a full codegen frontend
switch from walking &Term to walking &MTerm — and it does not fit one
bite-sized plan, because the switch is atomic: there is no compiling
intermediate between "all Term" and "all MTerm" (dual-path is
spec-forbidden). The MIR *producer* side, by contrast, compiles and
tests in isolation. So mir.1 is sequenced across two plans:
- mir.1a (this plan): the additive producer — new leaf crate
ailang-mir (MTerm/MArg/Callee/Mode/StrRep, structurally complete over
all 17 Term variants plus the Str split), ailang-check::lower_to_mir
(post-mono walk that calls canonical synth per node and maintains
locals/loop_stack exactly as synth does — no second type engine), and
ailang-check::elaborate_workspace (check → desugar+lift → mono →
lower_to_mir). Nothing consumes MIR yet; codegen untouched; whole
suite stays green; three ty-fill pins load the #51/#53/#49 witnesses
as fixtures and elaborate the full workspace (prelude included).
- mir.1b (next plan, against the landed types): flip codegen's walk to
&MTerm, delete synth_with_extras + synth_arg_type (9 call sites across
lib.rs/drop.rs/match_lower.rs), thread the 18 lower_workspace* callers,
switch the CLI build path to elaborate_workspace.
Both together satisfy the spec's mir.1 row. The split is plan
granularity, not a spec change — the spec's mir.1 deliverable is
unchanged.
Orchestrator design calls baked in (planner judgement, no user fork):
dedicated MTerm::Str{lit,rep} variant (Str-split installs the mir.4
hook at the #49 loop seed); emit_ir keeps &Module (builds MIR
internally — minimal blast radius); lower_to_mir scaffolding mirrors
mono.rs:771-816; Match-arm binding reuses synth's own type_check_pattern.
Recon (plan-recon) mapped the full deletion blast radius for mir.1b
(9 synth_arg_type sites, not the 3 the focus-hint assumed; 18
lower_workspace* callers). The two new witness fixtures
(new_rawbuf_size_only, new_counter_user_adt) were verified ail
check-clean during planning.
|
||
|
|
fb0dd92a6c | plan: raw-buf.6 kernel-stub-retirement (refs #7) | ||
|
|
559531806d |
plan: reserved-dollar-in-names — dollar-lexer-reservation (refs #44)
Executable plan for spec 0057 (committed
|
||
|
|
1990147467 |
plan: unique-binder-names — desugar alpha-rename for #43 A2b (refs #43)
Executable plan for spec 0056. Three tasks: - Task 1 (atomic compile unit): introduce a `Scope` struct threading two maps — `entries` keyed by each binder's effective (post-rename) name, `rename` mapping authored→effective for shadow detection and Term::Var resolution; add `fresh_binder` (<name>$<n>) and `rename_pattern_binders`; rewrite every binder site (Let, flat-Match pattern, Lam params, Loop binders) with rename-on-shadow, plus the LetRec arm and both def-boundary constructions, to the new API. - Task 2: correct the UniquenessTable doc-comment; verify the four RED tests go GREEN; full-suite regression gate. - Task 3: regression guard — a desugar unit test pinning that a shadowing let is alpha-renamed and that a letrec capturing the shadow-renamed binder still resolves (the entries-keyed-by-effective choice is what keeps capture detection correct under rename). Recon surfaced two facts the spec's sketch did not: the scope is a bare BTreeMap (so resolved_name/bind live on a new Scope type, not the map), and the LetRec capture-detection reads effective names from the desugared body — hence entries are keyed by effective name. Both stay internal to desugar.rs, preserving acceptance criterion 5 (no change to uniqueness.rs logic / codegen gates / linearity.rs). Both no-change consumers verified: match_lower.rs:795 keys by the emitted (renamed) pattern binder; linearity builds table and lookups from the same desugared tree. |
||
|
|
9ff1f22d42 |
plan: raw-buf.4 RawBuf payload + Term::New desugar (refs #7)
Four tasks. Task 1: raw_buf manifest (source.ail + mod + lib re-export)
+ ailang-surface wiring (parse_raw_buf + injection) + round-trip +
workspace count 4->5 — checkable, no codegen (raw_buf not yet in the
bijection collector list, so its markers are uncollected, bijection
stays green). Task 2: the general Term::New -> (app T.new <values>)
desugar (runs before check, so the type-scoped callee flows through the
raw-buf.3 scope threading to RawBuf_new__T), drops the NewArg::Type
(element type inferred from use — no type-ascription term exists),
removes both codegen deferral arms; ratified by a (new StubT 42)
build-and-run. Task 3: the 12 scope-qualified RawBuf_op__T entries +
emit fns (alloc/gep/load/store over an @ailang_rc_alloc slab, Int emits
in full + Float/Bool by an exact element-type/width substitution table)
+ a flat intrinsic-storage drop + raw_buf added to the bijection
collector module list (marker<->entry lockstep closes here). Task 4:
the E2E (int -> 60, float, bool, param-in reject, drop leak).
plan-recon resolved both make-or-break risks favourably: (A) the
alloc/load/store emit has a clean mirror (emit_eq_str does
locals-by-position -> fresh_ssa -> gep -> call -> ret; @ailang_rc_alloc
returns the payload ptr with rc-header auto-prepended); (D) desugar runs
BEFORE check/synth/mono (prepare_workspace_for_check), so Term::New
becomes (app RawBuf.new ..) before synth's type-scoped resolution sets
scope=Some("RawBuf") -> reaches the raw-buf.3 RawBuf_new__Int mint. No
checker tweak needed.
Three orchestrator design calls (spec underspecified the codegen
detail): (1) drop discriminator = derived signal "the TypeDef's `new`
op is (intrinsic)-bodied" -> flat @ailang_rc_dec drop; correctly
separates RawBuf (intrinsic new) from StubT (real-body new), where a
param_in heuristic would misclassify StubT (also param_in). No schema
change, no hash-pin. (2) @ailang_rc_release does not exist -> the real
symbol is @ailang_rc_dec (spec slip corrected). (3) Bool = 1 byte per
spec; the 12 emits are per-element-specialised so each hardcodes its
width (Int/Float 8, Bool 1) + store type (i64/double/i1); the i1
store/load syntax is verify-first, raw_buf_bool_e2e is the catch.
Baseline after raw-buf.3 is 670; gates step 670->671 (round-trip)
->672 (StubT desugar) ->677 (5 RawBuf E2E). kernel_stub stays
(retirement is raw-buf.5).
|
||
|
|
8508182f84 |
plan: raw-buf.3 type-scoped polymorphic intrinsic mechanism (refs #7)
Two tasks. Task 1 threads a TypeDef scope through the free-fn mono path: a scope: Option<String> field on FreeFnCall + MonoTarget::FreeFn, populated Some(prefix) only on the type-scoped T.f resolution branch (lib.rs:3535, gated on type_home.is_some()), None everywhere else; consumed at the two mono-symbol mint sites via a scoped_base helper (both read the same MonoTarget → lockstep) and folded into mono_target_key. Result: StubT.peek @ Int mints StubT_peek__Int, not the colliding bare peek__Int; existing bare-resolved symbols unchanged (669-gate is the no-regression backstop). Task 2 adds the StubT.peek ratifier op to the stub source, extends the bijection collector to expand a polymorphic type-scoped intrinsic over its TypeDef's param-in set (building the same T_f__<elem> strings via mono_symbol_n on Type::Con elems, so collector and mint agree byte-for-byte), registers StubT_peek__Int/Float entries + emit fns, updates kernel_stub_module_round_trips, and adds a mono-symbol integration test (borrow-param calls, no Term::New, no codegen). plan-recon resolved the make-or-break unknown favourably: the TypeDef scope is discarded at lib.rs:3535 but fully available there (prefix + type_home live) — a clean thread-through, not a resolution re-architecture. Three orchestrator design calls folded in: peek's emit is a plausible signature-correct stub, unit-ratified only (never instantiated in .3, no Term::New to build a StubT; RawBuf's emits in .4 are the E2E-verified ones; peek retires in .5); the exact llvm_type(borrow StubT Int) string is an implementer verify-first step (eq__Unit precedent); scope-trigger is the call path (not signature, which would perturb bare-called fns like length), scope-value is the prefix, collector infers from signature — they agree by construction for single-TypeDef kernel modules, bijection + 669-gate backstop. peek Form-A verified to parse+check this session under a probe module (ok, 32 symbols / 3 modules). Final gate 670 (669 + 1 new mono test); .ll snapshots stay green (peek polymorphic → no instantiation without a call site). |
||
|
|
395a40f3e7 |
plan: raw-buf.2-kernel-rename — atomic crate rename + family-crate reshape (refs #7)
Two tasks. Task 1 (atomic): git mv ailang-kernel-stub → ailang-kernel, carve the STUB_AIL Form-A body verbatim into src/kernel_stub/source.ail (incl. the answer (intrinsic) fn — a perturbed byte shifts the .ll snapshots + kernel_stub_module_round_trips), add mod.rs (include_str!) + lib.rs hub (pub use kernel_stub::SOURCE as STUB_AIL), thread all 8 compile sites across 6 files (Cargo package + workspace member + dep-alias + ailang-surface dep + loader.rs use-paths), end with cargo build + cargo test green. Task 2: doc/ledger crate-path honesty fixes (INDEX, CLAUDE.md code-layout + lockstep rows, model 0007) — path-only, retirement narrative left for raw-buf.4. Zero behavioural change; public symbol STUB_AIL preserved via the re-export so ailang-surface + the bijection test bind unchanged. NO raw_buf submodule (that is raw-buf.3). plan-recon mapped 8 compile-breaking sites + 5 doc sites (two beyond the spec's named row list — CLAUDE.md:312 lockstep + model 0007:8,235 — folded in per honesty-rule). Baseline measured this session: 669 passed / 0 failed / 2 ignored (not the stale 667 from the pre-recarve 0105 plan; intrinsic-bodies added 2 tests). |
||
|
|
298fc2d36f |
plan: intrinsic-bodies.2-migration-lock — 4-task prelude migration + bijection pin (refs #9)
Decomposes intrinsic-bodies.2 (parent spec § Architecture points 6-8)
into 4 tasks + a final gate:
Task 1 — migrate the 13 authored prelude dummy bodies to (intrinsic):
7 Eq/Ord instance methods (inner (body false)/(body true)/
(body (term-ctor Ordering EQ)) → (intrinsic), Design X lambda-body
placement) + 6 float_* fns ((body false) → (intrinsic)). Gated by
round-trip + the 4 eq/compare/float E2E ratifiers staying green
(behaviour-preserving: the intercepts emit identical IR).
Task 2 — rebaseline the two moving hash pins: prelude_module_hash_pin
(prelude module hash) and mono_hash_stability's 6 eq/compare
def-hashes (capture-from-failure, replace the constant). The 4
show__* pins do NOT move.
Task 3 — replace the one-directional registry_contains_all_legacy_arms
pin with intercepts_bijection_with_intrinsic_markers: an in-source
test that walks prelude + kernel_stub pre-mono for Term::Intrinsic
markers, recovers mangled names (mono_symbol for instance methods,
fn name for top-level), and asserts the bijection over the
intrinsic-backed class (14) with the 5-name *__Int optimisation-only
allowlist. Both directions + a stale-allowlist guard.
Task 4 — confirm no codegen path lowers an intercepted body (already
true post-.1: intercept-by-name precedes lower_term), delete the
stale dummy-body comment at lib.rs:1310-1319, and conditionally edit
0007-honesty-rule.md ONLY if it names the dummies (recon: it does
not — a general rule; no forced edit).
Recon-driven plan decisions:
1. The bijection pin lives in intercepts.rs's in-source #[cfg(test)]
mod tests, NOT a crates/ail/tests integration test. Visibility
forces it: the pin needs INTERCEPTS (pub(crate) in codegen, in-source
only) AND the parse hops (ailang-surface dev-dep). Verified
ailang-surface does not dep ailang-codegen, so there is no
dev-dep cycle blocking the in-source test from calling parse_prelude
(the dev-dep-cycle hazard that bit pd.2.4/pd.3.1 does not apply
here — that was the ailang-core <-> ailang-surface edge).
2. The pin walks PRE-mono (parse_prelude/parse_kernel_stub) and
reconstructs mangled names via mono_symbol, rather than walking
post-mono. Post-mono only synthesises USED mono symbols, so a
post-mono walk would miss any instance method the pin's entry
fixture does not exercise (e.g. eq__Unit). Pre-mono + mono_symbol
sees all 14 markers unconditionally.
3. Task 2's hashes are capture-from-failure, not pre-computed — the new
prelude bytes determine them. The old->new values get recorded in
the iter commit body by the Boss.
All verification filter strings checked against the tree: the 4 E2E
ratifier fn names, the mono pin name, and the prelude pin target all
resolve to ≥1 real test. No ail/ail-json/ll fenced block in the plan
(the migrated-form snippets are scheme fragments — the (intrinsic) form
is already ratified by .1's kernel_intrinsic_smoke.ail), so the
parse-bytes gate is a documented no-op.
Handoff target: skills/implement on docs/plans/0107-intrinsic-bodies.2-migration-lock.md
|
||
|
|
ce0374ac0c |
plan: intrinsic-bodies.1-mechanism — 8-task Term::Intrinsic cross-crate landing (refs #9)
Decomposes intrinsic-bodies.1 (parent spec docs/specs/0055-intrinsic-bodies.md
§ Architecture points 1-5) into 8 tasks plus a final gate:
Task 1 — Term::Intrinsic unit variant in ast.rs + the in-core
exhaustive-match arms (canonical/hash/visit/pretty), enumerated by
cargo build -p ailang-core (no-wildcard matches break until armed).
Task 2 — surface parse + print: (intrinsic) as a fn body-slot clause
and a lambda positional body, mapped to/from Term::Intrinsic;
round-trip rides the examples/ corpus gate.
Task 3 — cross-crate walker sweep (check + codegen + prose): leaf
arms at the sites that match Term::New today, enumerated by
cargo build --workspace. The two MEANINGFUL arms (codegen lower_term,
checker body-check) are bridged with a temp unreachable! so the gate
is green, then replaced by Tasks 4-5 — no deferred-caller across a
0-error gate.
Task 4 — checker: env.current_module_kernel_tier flag, signature-only
body check for an intrinsic body, intrinsic-outside-kernel-tier
reject (CheckError variant + code() arm). Reject test is subprocess
`ail check --json` on a temp file, modelled exactly on the verified
check_json_unbound_var pattern.
Task 5 — codegen: a Term::Intrinsic body routes through
intercepts::lookup; never reaches lower_term; lower_term's
Term::Intrinsic arm is a codegen-internal error.
Task 6 — `answer` intercept (ret i64 42) + the answer intrinsic in
STUB_AIL + examples/kernel_intrinsic_smoke.ail so the
schema_coverage corpus observes Term::Intrinsic (avoids the
Term::New-in-match-but-not-in-corpus gap).
Task 7 — E2E ratifier examples/kernel_answer.ail (calls
kernel_stub.answer, prints 42); build_and_run("kernel_answer.ail")
asserts stdout 42.
Task 8 — design/contracts/0002-data-model.md gains the
{ "t": "intrinsic" } Term entry; design_schema_drift mirror stays
green.
Three plan-time corrections after plan-recon + harness verification:
1. The reject test was first drafted against an invented ailang_check
API (Workspace::single_with_prelude / check_workspace). Verified
against e2e.rs:1236 that the established reject pattern is
subprocess `ail check --json` + exit-1 + JSON code assertion;
rewrote on a temp file.
2. build_and_run takes the fixture filename WITH .ail extension
(verified e2e.rs:13-38, existing calls build_and_run("sum.ail")) —
the ratifier call is build_and_run("kernel_answer.ail").
3. kernel_intrinsic_smoke.ail carries an intrinsic with no registered
intercept; confirmed no gate builds it (compile_check.py uses a
curated list, no test builds all examples/*.ail) — it rides only
the parse-level round-trip + schema-coverage gates, never a build.
Scope guard: .1 does NOT migrate prelude dummies, does NOT upgrade the
registry pin to a bijection, does NOT remove the dead body-lowering
path — all .2. Hashes stay stable in .1 (Term::Intrinsic is additive;
no existing fixture carries it).
Test trajectory: 667 (post-raw-buf.1) → ~669 (+intrinsic_in_user_module_is_rejected,
+answer_intrinsic_builds_and_runs_printing_42; corpus/round-trip
fixtures ride existing dynamic gates).
Handoff target: skills/implement on docs/plans/0106-intrinsic-bodies.1-mechanism.md
|
||
|
|
647121cb8f |
plan: raw-buf.2-kernel-manifest — 7-task crate-rename + RawBuf submodule + checker-side surface (refs #7)
Decomposes raw-buf.2 (parent spec docs/specs/0054-raw-buf.md
§ Architecture point 2, § Components row 2, § Testing strategy
"raw-buf.2") into 7 atomic tasks:
Task 1 — crate rename + reshape: crates/ailang-kernel-stub/ →
crates/ailang-kernel/, restructured as a family-crate with
src/kernel_stub/{mod.rs,source.ail}; 13 steps covering 8
compile-checked rename sites plus design/INDEX.md + CLAUDE.md
path updates.
Task 2 — add raw_buf submodule: src/raw_buf/{mod.rs,source.ail}
with the spec's Form-A source (4 fns + 1 TypeDef with
param-in (a Int Float Bool)); RAW_BUF_AIL re-export.
Task 3 — wire raw_buf into ailang-surface: parse_raw_buf fn,
re-export at lib.rs:39, workspace injection alongside the
existing kernel_stub block, workspace_pin count bump 4→5.
Task 4 — round-trip test raw_buf_module_round_trips, verbatim
structural clone of kernel_stub_module_round_trips.
Task 5 — E2E raw_buf_check_e2e: (con RawBuf (con Int)) consumer
checks clean.
Task 6 — E2E raw_buf_param_in_reject_e2e: (con RawBuf (con Str))
consumer rejected at check with param-not-in-restricted-set.
Task 7 — E2E raw_buf_build_intercept_missing_e2e: (new RawBuf …)
consumer accepted at check, rejected at build with the
existing Term::New deferral message.
Three substantive plan-time decisions resolving plan-recon's
open questions:
1. The intercept-not-registered diagnostic is NOT introduced.
The spec hedged ("intercept-not-registered: new__RawBuf__Int
(or the existing Term::New-deferral diagnostic)") between
shipping a fresh diagnostic and reusing the existing deferral
at crates/ailang-codegen/src/lib.rs:2075-2078. Plan picks
reuse: the deferral already names "milestone raw-buf" and is
self-describing; a new diagnostic would exist for ~1 iter
(raw-buf.2 → raw-buf.3) before going away — disposable infra.
Task 7's test asserts on the substring "Term::New requires
the type's" which is stable in the existing message.
2. In-tree references to crates/ailang-kernel-stub/ AFTER the
rename — split-scope between raw-buf.2 and raw-buf.3.
The path changes in raw-buf.2 (the crate moves now), so
path-references in design/INDEX.md:112 and the CLAUDE.md
Code-layout table get updated in Task 1 Steps 10-11. The
retire-language (stub as "ratifying fixture for kernel-
extension-mechanics" → "retired by raw-buf") stays for
raw-buf.3 close, per spec § Components row 3.
3. parse_raw_buf gets a mirror re-export at ailang-surface/
src/lib.rs:39 alongside parse_kernel_stub. Recon's natural
reading; load-bearing because the new round-trip test calls
ailang_surface::parse_raw_buf() directly.
Plan honours the project's compile-gate discipline. Task 1's
13 steps thread all 8 rename sites inside the same task —
no deferred caller across the build gate. Task 3 bundles the
workspace_pin count bump with the loader injection that drives
it; the assertion goes 4→5 in the same task that makes it true.
Test-count trajectory: 667 (post-raw-buf.1 baseline) → 667
(Tasks 1, 2, 3 — refactor + wiring, no new test) → 668 (Task 4
round-trip) → 669 (Task 5 check E2E) → 670 (Task 6 reject E2E)
→ 671 (Task 7 deferral E2E).
Handoff target: skills/implement on docs/plans/0105-raw-buf.2-kernel-manifest.md
|
||
|
|
d1612d5463 |
plan: raw-buf.1-intercept-registry — 5-task atomic codegen-registry refactor (refs #7)
Decomposes raw-buf.1 (parent spec docs/specs/0054-raw-buf.md
§ Architecture point 1, § Components row 1) into 5 tasks:
Task 1 — intercepts module skeleton (3 steps)
Task 2 — populate the table: 18 emit fns + visibility bumps (9 steps)
Task 3 — rewire the dispatch site to a thin shim (3 steps)
Task 4 — fold wants_alwaysinline into the registry (4 steps)
Task 5 — registry_contains_all_legacy_arms pin test (3 steps)
Three substantive decisions made at plan time beyond what the spec
dictates, surfaced by the plan-recon report:
1. The match arm count is 18, not the ~8 the spec sentence hand-listed
(eq__Str, compare__Int|Bool|Str, float_*). Five additional arms
(eq__Int, eq__Bool, eq__Unit, plus the lt|le|gt|ge|ne__Int
direct-icmp family) shipped after the spec sentence was written.
All 18 are absorbed by the registry; the pin test enumerates all 18.
2. The `intercept_emit_wants_alwaysinline` predicate at lib.rs:1172 is
a separate hardcoded name-list that mirrors the dispatch arms.
This is a silent-drift class (regression seen earlier in the cycle
on compare__Int perf at +29-47% when the lists diverged). The plan
folds wants_alwaysinline into the Intercept entry as a per-row bool
and rewrites the predicate to consult the registry — one source of
truth instead of two.
3. The wrapper fn `try_emit_primitive_instance_body` survives as a
thin shim (`match intercepts::lookup(name) { Some(i) => ..., None
=> Ok(false) }`) rather than being deleted. Trade-off: a 6-line
wrapper vs. a 14-file comment-ref churn. The wrapper wins —
landmark name preserved, all in-source doc comments stay valid.
Plan honours the project's compile-gate discipline: fn signatures
stay stable throughout (only bodies change), no caller-threading is
deferred across a build gate. Visibility bumps in Task 2
(pub(crate) on three Emitter fields and four helper methods) are
additive and do not change any caller's signature.
Verification commands in each Run step name actual test fns the
plan-recon confirmed exist at specific lines; no filter-zero-match
risk. Pre-flight workspace baseline: 668 tests; post-iter:
669 (the new pin).
Handoff target: skills/implement on docs/plans/0104-raw-buf.1-intercept-registry.md
|
||
|
|
3b4fb42771 |
plan: prep.3-kernel-tier-modules — 9-task schema + surface + workspace + diagnostic + stub crate (refs #33)
Terminal iteration of the kernel-extension-mechanics milestone.
Carves the spec's § Iteration prep.3 into nine bite-sized tasks:
1. Module.kernel field + ~130-site struct-literal sweep + drift
round-trip + data-model anchor.
2. TypeDef.param_in field + ~30-site sweep + drift round-trip +
anchor (BTreeMap<String, BTreeSet<String>>, kebab-cased
`param-in`, skip-if-empty for hash stability).
3. Form-A (kernel) module-header attribute — parse + print +
round-trip.
4. Form-A (param-in (a Int Float) (b Str)) TypeDef attribute —
one outer clause, multiple inner var-lists (OQ1 decision).
5. Prelude becomes kernel-tier + loader generalisation: the
hardcoded `&["prelude"]` literal at loader.rs:108 migrates to
a `modules.values().filter(|m| m.kernel)` derivation;
ReservedModuleName diagnostic repurposed; CLI mapping +
hash-pin refresh in lockstep.
6. New crate `crates/ailang-kernel-stub/` — programmatic stub
module via parse_kernel_stub() mirroring parse_prelude
(OQ2 decision); wired into the loader; basic stub round-trip
drift test ratifies the end-to-end mechanism.
7. CheckError::ParamNotInRestrictedSet variant + code() + ctx() +
enforcement in check_type_well_formed's Type::Con arm +
in-source RED/GREEN tests.
8. New workspace_kernel.rs integration tests — auto-import without
explicit (import …), two kernel-tier modules co-load,
explicit-import-overrides-auto-import precedence.
9. design/INDEX.md + design/models/0007-kernel-extensions.md
STATUS + forward→present transitions for the now-shipped
mechanisms.
The four open questions raised by plan-recon are resolved up front
in the "Open-question decisions" block so no task carries a TBD.
ReservedModuleName variant shape stays `{ name: String }` (OQ3);
workspace.rs:2655 stays a test-site literal (OQ4).
The Module struct-literal sweep is the largest single mechanical
edit (~130 sites; the additive #[serde(default)] covers JSON
deserialise paths, only Rust struct literals break) — handled as
a compiler-driven sweep inside Task 1, with the workspace-build
gate validating no caller is deferred. TypeDef has ~30 sites.
Compile-gate-vs-deferred-caller and pin/replacement-substring
contiguity rules from planner self-review Step 5 are scrubbed:
no signature change defers a caller past its own compile gate,
no verbatim text edit pairs with a soft-wrappable presence pin.
Recon report received DONE_WITH_CONCERNS — concerns were the
four OQs, all decided in this plan.
|
||
|
|
70e6fcd5c0 |
plan: prep.2 Term::New construct — 4-task atomic AST + surface + checker + drift (refs #32)
Second iteration of the kernel-extension-mechanics milestone. The
plan ships the `new` Form-A keyword + Term::New AST variant + NewArg
enum + checker elaboration + two diagnostics, plus the schema-drift
pin and the data-model.md / form_a.md anchor additions.
Decomposition:
- Task 1 (large mechanical): AST variant declaration + workspace-wide
compile-forced arms across 24 files (44+ exhaustive Term-match
sites). Six arm patterns inlined; per-site table assigns one
pattern per site. Includes the prep.1 pre-pass partner — the
`qualify_workspace_term` arm that rewrites bare Term::New.type_name
to qualified form, mirroring the existing Term::Ctor arm. Synth
gets a Pattern-E stub here; Task 3 replaces with real logic.
- Task 2: Form-A lex/parse/print + round-trip fixture. New
`parse_new` method with Type-vs-Term arg disambiguation by
syntactic form (head keyword = Type-production heads
`con`/`fn-type`/`borrow`/`own` → NewArg::Type; else NewArg::Value).
- Task 3: Checker synth real-logic + 2 new CheckError variants
(NewTypeNotConstructible, NewArgKindMismatch) + 3 RED-first
in-source tests covering the spec's worked example, the missing-
`new`-def case, and the kind-mismatch case.
- Task 4: Schema-drift pin (term_new_round_trips +
term_new_type_arg_round_trips) + spec_drift exemplar + form_a.md
keyword cheatsheet + design/contracts/0002-data-model.md JSON-tag
block.
Spec is already correct on prep.2 scope; the recon surfaced one
design decision the plan resolves inline: NewArg uses
`#[serde(tag = "kind", content = "value", rename_all = "lowercase")]`
to land the spec's exact JSON byte shape. Codegen remains explicitly
out of scope per the milestone spec; Task 1's codegen sites get
Pattern-D error-arms ("Term::New requires desugar first; codegen
support lands in the raw-buf milestone").
Forward-reference: Task 1's Pattern-C arm in `qualify_workspace_term`
extends prep.1's workspace-wide normalisation pre-pass — Term::New
joins Term::Ctor and Type::Con as a type_name-bearing site that
gets bare→qualified normalised before the checker sees it.
|
||
|
|
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.
|
||
|
|
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. |
||
|
|
90977663ff |
plan: schema-camelcase-fix — 4-task atomic schema-tag migration (refs #30)
Iteration plan derived from `docs/specs/2026-05-21-schema-camelcase-fix.md` (commit |
||
|
|
400ad7c067 |
plan: operator-routing-eq-ord.1 — atomic single-iter execution layout
Decomposes the operator-routing-eq-ord spec (
|
||
|
|
ad0a8d8786 |
plan: boehm-retirement.1 — atomic single-iter execution layout
Decomposes the Boehm-retirement spec (
|
||
|
|
7d8b3a3c10 |
plan: nullary-app.1 — accept (app f) as canonical zero-arg call form
Resolves the design fork in Gitea #12 (the bounce-back from the 2026-05-20 /boss session). User accepted both half-decisions: (a) the surface form `(app f)` becomes the canonical nullary call shape, dropping the "expected at least one argument" parser guard in `parse_app_body`; (b) `Term::App.args` mirrors the *actual* serde attrs of `Term::Ctor.args` — `#[serde(default)]` for read-tolerance, no `skip_serializing_if`, so writes always emit `"args":[]`. The plan covers five small tasks: a RED→GREEN E2E (nullary user fn called as `(app greet)`), the four-line parser-guard removal, the one-attribute serde edit, and a doc-honesty fix on `design/contracts/data-model.md` — the current "args omitted when empty" comment on `Term::Ctor` is factually false relative to the code (verified by a serde probe at plan time) and gets rewritten to describe actual write/read behaviour while we are in the same jsonc block. `design/contracts/honesty-rule.md` enforcement folded into the same iter because leaving a stale comment behind in the block we're modifying would be the exact failure mode the rule names. Hash-impact verified at plan time: `grep -rn '"args":\[\]' examples/*.ail.json` returns zero matches, so the new `#[serde(default)]` on `App.args` is read-only behaviour change — write side is unchanged, no existing fixture hash mutates. The plan documents this explicitly under Task 3.2. Two prior fieldtest specs already supplied the LLM-natural shape evidence — `docs/specs/2026-05-15-fieldtest-mut-local.md` F3 (mut-local) and `docs/specs/2026-05-18-fieldtest-loop-recur.md` spec_gap (`run_forever`) — so the feature-acceptance gate in `design/contracts/feature-acceptance.md` is already passed; no brainstorm phase needed for this iter. refs #12 |
||
|
|
ddb50c3cb3 |
plan: bench-harness-recalibration.1 — drop 6 latency entries + recapture
One terminal iteration covering the whole spec. Four tasks, all on `bench/baseline.json`: - Task 1: pre-recapture JSON edit — drop `max_us` + `p99_9_us` × 3 latency arms (6 entries) and rewrite the `note` field forward- looking. The 6 entries must go before `--update-baseline` runs because `write_new_baseline` faithfully re-baselines anything left in the file (recon Open Q). The note rewrite is folded in here because `--update-baseline` preserves the existing note as- is, so any change has to happen before recapture (or as a separate post-recapture edit; one-edit is cleaner). - Task 2: `bench/check.py --update-baseline` regenerates every remaining `baseline` value from a fresh `bench/run.sh -n 5`, updating `captured` → 2026-05-20 and `captured_via`. - Task 3: acceptance §1 — fresh-HEAD replay → exit 0, 0 regressed, 57 metrics in summary (= pre-edit 63 minus the 6 drops). - Task 4: acceptance §2 — synthetic injection (halve `bench_list_sum.bump_s.baseline` via `jq`) → exit 1 + REGRESSION row on that metric, then restore via `jq` and confirm a final exit-0 replay. Recon adjudications: - Replay-source (recon Open Q1): fresh `bench/run.sh -n 5` for replay, not the same output that produced the recapture. Realistic acceptance scenario; with 10% throughput tolerance vs. ~1-2% measured run-to-run variance on the affected metrics (run-1 / run-2 reproduction data in spec body), there is ample margin. - Note rewrite (recon Open Q2): forward-looking — drop stale refs (JOURNAL workflow retired 8e586f4; the `*.max_us` tolerance convention is now moot since the metric is gone), replace with pointer to docs/specs/2026-05-20-bench-harness- recalibration.md + closed issues #15 / #16. Spec lives where the rationale lives; the note carries the gate-policy one-liner only. No Rust crate touched. `bench/check.py` / `bench/run.sh` / `bench/latency_harness.py` unmodified. Plan self-review (all 8 checklist items): clean. Step granularity checked; Task 3 collapsed from two `bench/check.py` runs to one (single 3-minute run captures both summary and exit code). Ready for handoff to `skills/implement`. |
||
|
|
36599bedd9 |
plan: design-ledger-formal-links.1 — clause-5 + 7 conversions + 2 disposition-(b) + honesty-rule positive-half
Single iteration covering the whole milestone (same shape as
rolesplit.1; spec is small, no build-atomicity issue — clause-5 is
isolated additive code, prose edits don't affect compilation).
Five tasks, each at task-level review granularity:
Task 1: clause-5 added to design_index_pin.rs RED-first via
identity-stubbed strip_fences (synthetic vector FAILS) →
correct toggle-on-fence impl (GREEN); module //!-header
extended to name clause-5. Test count 4 → 5.
Task 2: 7 prose-ref conversions to file-relative Markdown links
(float-semantics:69/100, embedding-abi:45, memory-model:44/
105, scope-boundaries:48/88 with the source+Pipeline split).
Exact verbatim before→after for every line. clause-5 stays
GREEN by construction (every introduced link resolves
durable).
Task 3: 2 disposition-(b) homeless-ref removals (pipeline:60-61
in-fence CLI block + authoring-surface:178-181 prose).
Pointer dropped, behavioural prose preserved.
Task 4: honesty-rule.md pin-safe positive-half paragraph between
L14 and the existing L15-blank (the two docs_honesty_pin
phrases at L14/L19 each stay verbatim on their physical
line; the additive insertion splits neither). The new
paragraph names design_index_pin.rs clause-5 by name
(nominal mention, not a link — consistent with the
existing 'Ratified by:' footer style and with §Scope).
Task 5: Whole-suite gate (646 → 647) + 5 grep+diff verifications
(no docs/ link, no #fragment, link count = 8, in-fence
schema-annotations intact, INDEX.md + decision-records
journal + clauses 1-4 byte-unchanged).
Self-review 8/8: spec coverage complete; zero placeholders; names
consistent; bite-sized; no commit steps; pin contiguity verified
(L14 + L19 stay verbatim on their physical lines, replacement body
shows L14 contiguous); no signature change ⇒ no compile-gate
ordering issue; every cargo-test filter substring is a real test
name + unfiltered runs have explicit count assertions.
|
||
|
|
f2cdd67e69 |
plan+audit: design-md-rolesplit.tidy re-derived on planning-time evidence (mechanism + scope corrected)
The tidy's gate-first Task 1 RED-verification + Boss independent
verification surfaced two defects in the audit Resolution as first
written — caught exactly where the planner->Boss loop is designed to,
before any contract byte moved. Per the 'two+ defects in one
iteration => fix the upstream artifact, do not patch a third time'
discipline, the audit Resolution is corrected in lockstep with the
plan re-derivation:
- Mechanism (Resolution-4): the 'case-insensitive iter-code regex'
clause-3 design is REJECTED as unworkable — a blanket
iter/milestone detector conflates the memory-model rule-names
'Iter A'/'Iter B' and ordinary words 'pre-existing'/'pre-set'/
'pre-Boehm' with provenance, over-firing on legit present-tense
contract prose (4 no-strip files). Replaced by a FAITHFUL Sweep-1
superset: case-sensitive digit-anchored Sweep-1 line anchors
(confirmed ZERO across all contracts) + Sweep-1's ^[^/]*
path-excluded date (so docs/specs/2026-.. citations are not
flagged — repairs the iso_date over-fire the implement
orchestrator correctly BLOCKED on) + the audit-named
decision-record phrases (case-insensitive). No regex dep. The
load-bearing invariant (clause-3 GREEN => Sweep-1 clean) is
preserved; it never required the blanket detector.
- Scope (Resolution-1): the architect's [medium] was a 3-file
spot-check; the exhaustive scan found roundtrip-invariant.md:73
('at iter form-a.1') and data-model.md:149,161 ('loop-recur iter
1:') also carry lowercase provenance. True strip set = 5 files;
plan Task 5b adds the 2 (spirit-cleanup, not gate-blocking).
Plan self-review 8/8 re-run; clause-3 = hand-rolled faithful Sweep-1
superset, no regex dep, no blanket iter detector.
|
||
|
|
4f61a7aa4f |
plan: design-md-rolesplit.tidy — audit drift fix (history->journal, sweep re-scope, clause-3 widen)
7 tasks executing the audit Resolution's 5 points: (1) gate-first RED — widen design_index_pin.rs clause-3 to a hand-rolled PROVABLE SUPERSET of architect_sweeps Sweep-1 over design/contracts/ (no regex dep; subsumption proof inline), verify RED vs the un-stripped tree; (2-4) sentence-level strip the faithfully-migrated history/decision-record prose out of typeclasses.md / str-abi.md / scope-boundaries.md into the decision-record journal, per recon's per-sentence contract-residue map, each docs_honesty_pin pinned run guarded contiguous; (5) fix float-semantics.md stale 'see Str ABI below' -> str-abi.md; (6) re-scope architect_sweeps DESIGN_GLOB to design/contracts only (models/ is the narrative tier) + lockstep ailang-architect.md; (7) whole-tree GREEN gate (clause-3 RED->GREEN, 4/4, sweep exit 0, suite >=646/0, acceptance grep CLEAN). Self-review 8/8. |
||
|
|
deeffb1872 |
plan: design-md-rolesplit.1 — the whole milestone, build-atomic by task ordering
9 tasks: (1) RED-first design_index_pin.rs 4-clause guard; (2) design/INDEX.md + 14 prose contracts; (3) 5 model whitepapers; (4) decision-record journal + INDEX pointer; (5) retarget the 3 doc-reading tests GREEN vs design/; (6) 2 diagnostics + 2 E2Es lockstep; (7) architect_sweeps.sh spine repoint; (8) ~12 agent lists + 5 SKILL bodies + CLAUDE.md + README + bucket-(e) comment xrefs + OQ7 cite deletion; (9) clean cut (git rm DESIGN.md) + whole-tree gate + acceptance grep. Build-atomicity by construction: include_str! retarget (Task 5) precedes DESIGN.md deletion (Task 9), data-model.md exists from Task 2 — every intermediate cargo build is green, no monster task. Self-review 8/8 (contiguity + compile-gate-ordering + filter-resolution scrubbed). |
||
|
|
e81fb90d88 |
plan: embedding-abi-m5.tidy — milestone-close doc-honesty drift (pin-safe, doc/comment-only)
Resolves the M5 milestone-close audit DRIFT (audit journal |
||
|
|
67027ab0f9 |
plan: embedding-abi-m5.3 — time-shard boundary-invisibility proof + friction harvest
Final functional M5 iteration (spec ae905de; builds on green m5.1 |
||
|
|
9cc9d9c517 |
plan: embedding-abi-m5.2 — data-server adapter + symbol-fan swarm + leak-proof
M5 iteration 2 (spec ae905de; builds on m5.1
|
||
|
|
22f02aa26b |
plan: embedding-abi-m5.1 — lean ail-embed core + build.rs + hermetic smoke
First iteration of M5 (spec
|
||
|
|
44ced513dc | plan: embedding-abi-m3.tidy — reconcile M3-audit [medium]+[low] doc-honesty drift (3 tasks, pin-safe parenthetical-only edit + comment-only fix, M2.tidy precedent) | ||
|
|
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 |
||
|
|
15ee3c5c8f | plan: embedding-abi-m3.1 — frozen value layout + single-ctor record crossing + RC ownership contract (7 tasks, RED-first, task 1 fixed-first baseline pins per spec sequencing) | ||
|
|
d1241b12c7 | plan: embedding-abi-m2.tidy — DESIGN.md '(M1)'->'Embedding ABI' lockstep rename (5 live sites: header+:2354 xref+pin comment/msg+form_a.md:89; pinned :135 substring rename-immune, untouched) | ||
|
|
c9a84b33b3 |
iter embedding-abi-m2.1 (PARTIAL 4/9 + Boss spec-defect repair): ctx ABI + de-globalisation
Tasks 1-4 land fully review-green and are the cohesive shippable
subset (the per-thread embedding ABI, fully wired + proven):
- T1: FIXED-FIRST alloc-guard baseline pin (RED captured -> GREEN at T4).
- T2: runtime/rc.c gains ailang_ctx_t {alloc_count,free_count} +
ailang_ctx_new/_free + __thread __ail_tls_ctx; the two increment
sites become 'if (_ctx) _ctx->... else g_rc_...'. g_rc_* statics +
ailang_rc_stats_atexit + the constructor RETAINED VERBATIM as the
null-ctx (single-threaded executable) fallback. rc_accounting_tsan.c
+ driver: per-ctx 8x200000 tsan-clean (de-globalisation positive proof).
- T3: codegen Target::StaticLib forwarder gains a leading 'ptr %ctx'
+ one '@__ail_tls_ctx = external thread_local global ptr' decl +
TLS save/store/restore around the BYTE-UNCHANGED internal call;
_adapter/_clos + internal arg vector untouched (M1 decision held);
doc-comment provisionality narrowed to the value/record layout.
- T4: build_staticlib rejects --alloc != rc (RC-only swarm artefact).
Boss adjudication of the orchestrator's Task-5 BLOCKED (spec-defect,
correctly surfaced not hacked): swarm.c's -DSHARED_CTX negative
control is structurally impossible — examples/embed_backtest_step.ail
is a non-allocating scalar kernel that never writes a ctx field, and
__ail_tls_ctx is __thread, so a shared-ctx scalar swarm has no
shared-memory write to race on (the spec's OWN item-1 honesty point).
The de-globalisation teeth belong to the item-1 rc_accounting harness
(shared ctx genuinely races on ctx->alloc_count; orchestrator
independently confirmed tsan exit 66). Spec amended in lockstep
(Goal coherent-stop, must-fail-axis item 2, Testing item 3,
Acceptance) so item 3's negative control is item-1's by the
de-globalisation-proof / capability-demo split; plan Task 5
restructured (swarm.c per-ctx capability demo only; negative-control
standing test relocated into the item-1 harness driver) + Task 3's
plan-transcription error (@ail_backtest_step ->
@ail_embed_backtest_step_step) corrected. This is a Boss
consistency-repair of an internally-contradictory clause whose intent
is already correctly realised in the same spec — not a redesign; no
brainstorm bounce. Task-5 working files discarded (corrected plan
reproduces them; a structurally-RED test must not enter main).
INDEX.md + final journal land at iter completion (re-dispatch [5,9]).
|
||
|
|
b3388c8350 | plan: embedding-abi-m2.1 — per-thread runtime context + concurrency safety (9 tasks, RED-first, task 1 fixed-first per spec sequencing) | ||
|
|
03493c9b31 | plan: emit-ir-staticlib — M1 fieldtest spec_gap#2 scoped feature | ||
|
|
4c266a64b4 | plan: form-a-scalar-param-mode-carveout — M1 fieldtest friction+spec_gap#1 docs-honesty tidy | ||
|
|
818177d835 |
iter embedding-abi-m1.1 (PARTIAL 3/7): schema + surface + baseline prerequisites; plan Repair
Tasks 1-3 of docs/plans/embedding-abi-m1.1.md, a verified known-good subset (cargo build --workspace exit 0; full workspace green; the roundtrip_cli all-examples gate green): - Task 1: CLI build-path MissingEntryMain baseline pin (examples/embed_noentry_baseline.ail + embed_missing_main_baseline.rs; triple-assertion, layered on the green codegen-unit pin lib.rs:3314). - Task 2: additive FnDef.export: Option<String> (serde default + skip_serializing_if, modelled on FnDef.doc); compile-driven thread of export: None across ~85 FnDef/AstFnDef literals / 18 files incl. all in-source #[cfg(test)] mod tests + drift/hash/spec-drift literals + the codegen lib.rs:3320 in-source pin; hash-stability golden pin; DESIGN.md fn-JSON "export" line. DONE_WITH_CONCERNS (plan symbol content_hash_fn -> def_hash per hash_pin.rs, plan- pseudo-vs-reality class, plan corrected). - Task 3: Form-A (export "<sym>") modifier — parse_export + parse_fn thread + write_fn_def emission + form_a.md grammar; byte-identical round-trip. Task 4 BLOCKED (Boss plan-defect: fixtures declared module != file stem; AILang's loader hard-enforces module==stem at workspace.rs:438, so ail check panicked on load before the gate). Boss resolution (Option 2, overriding the orchestrator's Option 1, rationale in the journal + plan Design-decision 6): keep descriptive embed_* filenames, rename fixture MODULES to their stems — the C symbol backtest_step stays via (export ...), demonstrating spec Decision 2's mangling- decoupling rather than violating it; archive becomes libembed_backtest_step.a, internal symbol @ail_embed_backtest_step_step, host.c unchanged. Plan fixed (Design-decision 6 + Task-4 module==stem + Task-5/6/7 dependent strings + the no-*. Float-head + content_hash_fn concerns folded). Headline fixture corrected; blocked-Task-4 WIP discarded (recreated by the [4,7] re-dispatch from the fixed plan). PARTIAL commit (not the iter's final commit): the implement Repair path mandates committing the known-good subset so the [4,7] re-dispatch starts from a clean tree that includes Tasks 1-3 (Tasks 4-7 structurally depend on the schema field + surface). INDEX line added when the full iter lands post-re-dispatch. |