From 8508182f8431fa78426d144c411c86e58d6a6421 Mon Sep 17 00:00:00 2001 From: Brummel Date: Fri, 29 May 2026 19:25:04 +0200 Subject: [PATCH] plan: raw-buf.3 type-scoped polymorphic intrinsic mechanism (refs #7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Two tasks. Task 1 threads a TypeDef scope through the free-fn mono path: a scope: Option 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__ 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). --- ...aw-buf.3-typescoped-intrinsic-mechanism.md | 432 ++++++++++++++++++ 1 file changed, 432 insertions(+) create mode 100644 docs/plans/0109-raw-buf.3-typescoped-intrinsic-mechanism.md diff --git a/docs/plans/0109-raw-buf.3-typescoped-intrinsic-mechanism.md b/docs/plans/0109-raw-buf.3-typescoped-intrinsic-mechanism.md new file mode 100644 index 0000000..73c29a6 --- /dev/null +++ b/docs/plans/0109-raw-buf.3-typescoped-intrinsic-mechanism.md @@ -0,0 +1,432 @@ +# raw-buf.3 — Type-scoped polymorphic intrinsic mechanism — Implementation Plan + +> **Parent spec:** `docs/specs/0054-raw-buf.md` +> +> **For agentic workers:** REQUIRED SUB-SKILL: use the +> `implement` skill to run this plan. Steps use `- [ ]` +> checkboxes for tracking. + +**Goal:** Make a type-scoped polymorphic top-level `(intrinsic)` +op mint a *scope-qualified* mono symbol (`StubT_peek__Int`, not +`peek__Int`) and have the intrinsic↔intercept bijection express +one such polymorphic marker as its N per-`param-in`-element entries +— ratified standalone on the stub via a new `StubT.peek` op. + +**Architecture:** Two tasks. Task 1 threads a TypeDef-scope through +the existing free-fn mono path: a new `scope: Option` field +on `FreeFnCall` and `MonoTarget::FreeFn`, populated only when a call +resolved via the type-scoped `T.f` spelling, consumed at the two +mono-symbol mint sites (which already read the same `MonoTarget`, so +they stay in lockstep) and folded into the dedup key. Task 2 adds +the `StubT.peek` ratifier op, extends the bijection collector to +expand a polymorphic type-scoped intrinsic over its TypeDef's +`param-in` set (computing the *identical* scoped strings via the +same `mono_symbol_n`), registers the two `StubT_peek__*` entries, +and adds the mono-symbol + round-trip tests. No RawBuf, no +`Term::New`, no running E2E this iteration (constructing a `StubT` +needs the `Term::New` desugar that lands in raw-buf.4); ratification +is unit/integration tests + the bijection pin. + +**Tech Stack:** Rust. `crates/ailang-check/src/lib.rs` (resolution + +`FreeFnCall`), `crates/ailang-check/src/mono.rs` (mint + key), +`crates/ailang-codegen/src/intercepts.rs` (collector + entries + +emit), `crates/ailang-kernel/src/kernel_stub/source.ail` (the +ratifier op), `crates/ailang-core/tests/design_schema_drift.rs` +(round-trip), a new mono-symbol test. + +--- + +**Files this plan creates or modifies:** + +- Modify: `crates/ailang-check/src/lib.rs:2741` — add `scope: Option` to `FreeFnCall`. +- Modify: `crates/ailang-check/src/lib.rs:3535` (+ every other `free_fn_owner = Some(..)` site) — carry the scope through the resolution tuple; populate `Some(prefix)` only on the type-scoped dotted branch. +- Modify: `crates/ailang-check/src/lib.rs:3602` — set `scope` on the `FreeFnCall` push. +- Modify: `crates/ailang-check/src/mono.rs:577` — add `scope: Option` to `MonoTarget::FreeFn`. +- Modify: `crates/ailang-check/src/mono.rs:600-607` — fold `scope` into `mono_target_key`. +- Modify: `crates/ailang-check/src/mono.rs:857` — set `scope: fc.scope.clone()` at the `MonoTarget::FreeFn` construction. +- Modify: `crates/ailang-check/src/mono.rs:521` (nearby) — add a `scoped_base` helper. +- Modify: `crates/ailang-check/src/mono.rs:1080` + `:1176` — mint via `scoped_base(scope, name)` at both sites. +- Modify: `crates/ailang-kernel/src/kernel_stub/source.ail` — append the `peek` op. +- Modify: `crates/ailang-codegen/src/intercepts.rs:480-509` (`workspace_intrinsic_markers`) — type-scoped-poly expansion arm. +- Modify: `crates/ailang-codegen/src/intercepts.rs:35-174` (`INTERCEPTS`) + after `:459` (emit fns) — `StubT_peek__Int`, `StubT_peek__Float`. +- Modify: `crates/ailang-core/tests/design_schema_drift.rs:744-766` (`kernel_stub_module_round_trips`) — cover `peek`. +- Test: `crates/ail/tests/mono_scoped_symbol.rs` — new; asserts a type-scoped `StubT.peek` call monomorphises to `StubT_peek__Int` / `StubT_peek__Float`. + +--- + +### Task 1: Thread the TypeDef scope into the free-fn mono symbol + +**Files:** +- Modify: `crates/ailang-check/src/lib.rs:2741, 3535(+siblings), 3602` +- Modify: `crates/ailang-check/src/mono.rs:521, 577, 600-607, 857, 1080, 1176` + +- [ ] **Step 1: Add the `scope` field to `FreeFnCall`.** + +Edit `crates/ailang-check/src/lib.rs`. In the `FreeFnCall` struct (the `metas` field is last, at ~2755), append: + +```rust + /// The TypeDef scope the call resolved through, when it was + /// spelled type-scoped (`T.f`, prep.1) — `Some("StubT")` for + /// `StubT.peek`, `Some("RawBuf")` for `RawBuf.get`. `None` for a + /// bare / dot-qualified / local resolution. raw-buf.3: drives the + /// scope-qualified mono symbol `T_f__` so two types' ops + /// of the same name never collide. + pub scope: Option, +``` + +- [ ] **Step 2: Carry the scope through the resolution tuple `free_fn_owner`.** + +The local `free_fn_owner` is `Option<(String, String)>` `(owner_module, unqualified_name)`, set in several resolution branches and destructured at `lib.rs:3549`. Change its element type to `(String, String, Option)` `(owner_module, unqualified_name, scope)`. The compiler flags every `free_fn_owner = Some((..))` site (and the destructure at 3549). Update each: + +- The **type-scoped dotted branch** at `lib.rs:3535`, currently: + ```rust + (qualified, Some((target_module, suffix.to_string()))) + ``` + becomes: + ```rust + let scope = if type_home.is_some() { + Some(prefix.to_string()) + } else { + None + }; + (qualified, Some((target_module, suffix.to_string(), scope))) + ``` + (`type_home` and `prefix` are both live here — `type_home.is_some()` is exactly "resolved via the TypeDef-first ladder"; the legacy module-as-receiver sub-case has `type_home == None` → `scope = None`.) + +- **Every other** `free_fn_owner = Some((m, n))` assignment (the local / same-module / implicit-import branches the compiler flags): add `, None` as the third element. These are not type-scoped, so `scope = None` — existing bare/dot-qualified symbols stay byte-identical. + +- [ ] **Step 3: Destructure and thread the scope to the `FreeFnCall` push.** + +At `lib.rs:3549`, the `if let (Type::Forall { .. }, Some((owner, unqualified_name))) = (&raw, &free_fn_owner)` destructure gains the third binding: +```rust + if let (Type::Forall { vars, constraints, body }, Some((owner, unqualified_name, scope))) = + (&raw, &free_fn_owner) +``` +At the `FreeFnCall` push (`lib.rs:3602`), add the field: +```rust + free_fn_calls.push(FreeFnCall { + name: unqualified_name.clone(), + owner_module: owner.clone(), + forall_vars: vars.clone(), + metas: metas.clone(), + scope: scope.clone(), + }); +``` + +- [ ] **Step 4: Add the `scope` field to `MonoTarget::FreeFn` + the `scoped_base` helper.** + +Edit `crates/ailang-check/src/mono.rs`. In `MonoTarget::FreeFn` (the `defining_module` field is last, ~583), append: +```rust + /// The TypeDef scope (`Some("StubT")`) when the call resolved + /// type-scoped; `None` for a bare free fn. Part of the dedup + /// key and the minted symbol base. + scope: Option, +``` +Add the helper next to `mono_symbol_n` (after its definition, ~528): +```rust +/// raw-buf.3: the symbol base for a free-fn mono target. A +/// type-scoped op (`scope = Some("StubT")`, name `"peek"`) bases on +/// `"StubT_peek"`, so `mono_symbol_n` mints `StubT_peek__Int`; a +/// bare free fn (`scope = None`) keeps its bare base. +pub fn scoped_base(scope: &Option, name: &str) -> String { + match scope { + Some(t) => format!("{t}_{name}"), + None => name.to_string(), + } +} +``` + +- [ ] **Step 5: Fold `scope` into `mono_target_key`.** + +Edit `mono_target_key` (`mono.rs:600-607`). The `MonoTarget::FreeFn` arm currently keys on `("free", name.clone(), joined)`. Include the scope so a scoped op and a bare op of the same name never dedup-collide: +```rust + MonoTarget::FreeFn { name, type_args, scope, .. } => { + let joined = type_args + .iter() + .map(ailang_core::canonical::type_hash) + .collect::>() + .join("|"); + ("free".into(), scoped_base(scope, name), joined) + } +``` + +- [ ] **Step 6: Set `scope` at the `MonoTarget::FreeFn` construction.** + +At `mono.rs:857` (`collect_mono_targets`'s `out.push(MonoTarget::FreeFn { .. })`), add `scope: fc.scope.clone(),`. + +- [ ] **Step 7: Mint the scope-qualified symbol at both mint sites.** + +- `mono.rs:1080` (`synthesise_mono_fn_for_free_fn`): the symbol is `mono_symbol_n(name, type_args.as_slice())`. This fn must receive the scope. Add a `scope: &Option` parameter to `synthesise_mono_fn_for_free_fn`, pass `&target.scope` (or the destructured `scope`) from its caller (the `MonoTarget::FreeFn` match where it is invoked), and change the mint to: + ```rust + name: mono_symbol_n(&scoped_base(scope, name), type_args.as_slice()), + ``` +- `mono.rs:1176` (`rewrite_mono_calls`): the `MonoTarget::FreeFn { name: fn_name, type_args, defining_module }` arm — bind `scope` too and change: + ```rust + MonoTarget::FreeFn { name: fn_name, type_args, defining_module, scope } => { + (mono_symbol_n(&scoped_base(scope, fn_name), type_args.as_slice()), defining_module.as_str()) + } + ``` + Both sites read the same `MonoTarget`, so the minted def name and the rewritten call name stay byte-identical. + +- [ ] **Step 8: Build the workspace.** + +Run: `cargo build --workspace 2>&1 | tail -8` +Expected: build succeeds; the compiler-flagged `free_fn_owner` / `MonoTarget::FreeFn` / `FreeFnCall` sites are all threaded (a missed site is a hard compile error here). + +- [ ] **Step 9: Full suite — no regression (existing symbols unchanged).** + +Run: `cargo test --workspace 2>&1 | grep -E "^test result:" | awk -F"[: ;]+" '{p+=$4; f+=$6; i+=$8} END{print "passed:",p," failed:",f," ignored:",i}'` +Expected: `passed: 669 failed: 0 ignored: 2`. No type-scoped polymorphic intrinsic exists yet (peek arrives in Task 2), and all existing free-fn calls resolve with `scope = None` → bare base → byte-identical symbols. `mono_hash_stability` (six prelude `eq__*`/`compare__*` hashes) and the bijection test stay green, confirming no existing symbol moved. (If any existing test exercised a `Type.fn`-spelled poly-free-fn call and pinned its bare symbol, it would flip here — investigate and report, do not adjust the assertion.) + +--- + +### Task 2: StubT.peek ratifier + bijection expansion + entries + tests + +**Files:** +- Modify: `crates/ailang-kernel/src/kernel_stub/source.ail` +- Modify: `crates/ailang-codegen/src/intercepts.rs` (collector + `INTERCEPTS` + emit fns) +- Modify: `crates/ailang-core/tests/design_schema_drift.rs:744-766` +- Test: `crates/ail/tests/mono_scoped_symbol.rs` (new) + +- [ ] **Step 1: Append the `peek` op to the stub source.** + +Edit `crates/ailang-kernel/src/kernel_stub/source.ail`. The file currently ends with the `answer` fn whose final line is ` (intrinsic)))` (the two closing parens close `answer` and the module). Insert the `peek` fn between `answer` and the module close: change `answer`'s last line from ` (intrinsic)))` to ` (intrinsic))` (close only `answer`), then append `peek` and re-close the module. The fn body (verified to parse + check clean in a kernel module this session): + +```text + (fn peek + (doc "Ratifies the type-scoped polymorphic intrinsic mechanism: StubT.peek is polymorphic over a (param-in {Int,Float}) and type-scoped to StubT. Codegen intercepts StubT_peek__Int / StubT_peek__Float emit a load of the Stub payload. Retires with the stub in raw-buf.5.") + (type (forall (vars a) (fn-type (params (borrow (con StubT a))) (ret a)))) + (params s) + (intrinsic))) +``` + +The final file ends with ` (intrinsic)))` (closing `peek` + the module). Verify with `git diff` that only the `answer` close-paren count changed and `peek` was added — `new`/`answer` bodies are otherwise byte-untouched (a perturbation there would move the `.ll` snapshots). + +- [ ] **Step 2: Extend the bijection collector with a type-scoped-poly arm.** + +Edit `crates/ailang-codegen/src/intercepts.rs`, `workspace_intrinsic_markers()` (~480). The current `Def::Fn(f) if matches!(f.body, Term::Intrinsic)` arm (~490) inserts the bare `f.name`. Insert a MORE-SPECIFIC arm *before* it that handles a polymorphic (`Type::Forall`) intrinsic fn type-scoped to a same-module TypeDef: + +```rust + // raw-buf.3: a polymorphic top-level (intrinsic) op + // type-scoped to a same-module TypeDef T (its + // signature mentions `(con T …)`) expands to one + // marker per element type in T's `param-in` set — + // the same `T_f__` strings the mono pass mints + // (mono::scoped_base + mono_symbol_n). One marker → N + // entries. + Def::Fn(f) + if matches!(f.body, Term::Intrinsic) + && matches!(f.ty, Type::Forall { .. }) => + { + match scope_typedef_and_elems(f, &module) { + Some((tdef, elems)) => { + for elem in elems { + markers.insert(mono_symbol_n( + &format!("{tdef}_{}", f.name), + std::slice::from_ref(&elem), + )); + } + } + // a Forall intrinsic not scoped to a + // param-in TypeDef keeps the bare name (no + // such case ships today; future-proofing). + None => { + markers.insert(f.name.clone()); + } + } + } +``` + +Add the helper `scope_typedef_and_elems` in the same file (or a private mod): + +```rust +/// raw-buf.3: for a type-scoped polymorphic intrinsic fn `f`, return +/// `(TypeDef-name, element-types)` where the TypeDef is the unique +/// same-module `Def::Type` referenced via `(con T …)` in `f`'s +/// signature and the element types are its `param-in` set expressed as +/// `Type::Con` values (so `mono_symbol_n` produces the same suffix the +/// mono pass mints). `None` if `f` references zero or several +/// same-module TypeDefs. +fn scope_typedef_and_elems( + f: &ailang_core::ast::FnDef, + module: &ailang_core::ast::Module, +) -> Option<(String, Vec)> { + use ailang_core::ast::{Def, Type as T}; + let typedef_names: std::collections::BTreeSet<&str> = module + .defs + .iter() + .filter_map(|d| match d { + Def::Type(td) => Some(td.name.as_str()), + _ => None, + }) + .collect(); + // collect Con-heads referenced anywhere in f's type + let mut referenced: std::collections::BTreeSet = Default::default(); + collect_con_heads(&f.ty, &mut referenced); + let scoped: Vec<&String> = referenced + .iter() + .filter(|n| typedef_names.contains(n.as_str())) + .collect(); + let tdef = match scoped.as_slice() { + [one] => (*one).clone(), + _ => return None, + }; + let td = module.defs.iter().find_map(|d| match d { + Def::Type(td) if td.name == tdef => Some(td), + _ => None, + })?; + // param-in is keyed by the type var; take its allowed surface + // names and build `Type::Con` element types. + let allowed = td.param_in.values().next()?; // single-var TypeDefs + let elems: Vec = allowed + .iter() + .map(|s| T::Con { name: s.clone(), args: vec![] }) + .collect(); + Some((tdef, elems)) +} +``` + +(`collect_con_heads` walks a `Type`, pushing every `Type::Con { name, .. }` head into the set; recurse through `Forall.body`, `Fn.params`/`ret`, `Borrow`/`Own` inner, and `Con.args`. Mirror the existing `Type` traversal in this crate. The implementer must confirm the exact `Type` variant names — `Type::Con { name, args }`, the borrow/own wrappers — against `crates/ailang-core/src/ast.rs` and the `param_in` field type `BTreeMap>` against the `TypeDef` struct; build a `Type::Con` exactly as `mono::type_to_mono_suffix` expects so the suffix matches.) + +- [ ] **Step 3: Register the two `StubT_peek__*` entries + emit fns.** + +Append to `INTERCEPTS` (`intercepts.rs:35-174`): + +```rust + Intercept { + name: "StubT_peek__Int", + expected_params: &["ptr"], // borrow (con StubT Int) lowers to ptr + expected_ret: "i64", + wants_alwaysinline: false, + emit: emit_stubt_peek_int, + }, + Intercept { + name: "StubT_peek__Float", + expected_params: &["ptr"], + expected_ret: "double", + wants_alwaysinline: false, + emit: emit_stubt_peek_float, + }, +``` + +Add the emit fns after `emit_answer` (~459). Mirror `emit_answer`'s shape (read the single param SSA from the tail of `emitter.locals`, load the Stub payload field, return it). Plausible bodies: + +```rust +/// raw-buf.3 ratifier emit. NOTE: never instantiated to codegen this +/// iteration (no Term::New to build a StubT; unit-test-ratified only). +/// The load offset/correctness is exercised end-to-end only by +/// raw-buf.4's RawBuf path; here the entry exists to satisfy the +/// bijection. Retires with the stub in raw-buf.5. +pub(crate) fn emit_stubt_peek_int(emitter: &mut Emitter<'_>) -> Result<()> { + let n = emitter.locals.len(); + let s = emitter.locals[n - 1].1.clone(); + let v = emitter.fresh_ssa(); + emitter.body.push_str(&format!(" {v} = load i64, ptr {s}\n")); + emitter.body.push_str(&format!(" ret i64 {v}\n")); + emitter.body.push_str("}\n\n"); + emitter.block_terminated = true; + Ok(()) +} + +pub(crate) fn emit_stubt_peek_float(emitter: &mut Emitter<'_>) -> Result<()> { + let n = emitter.locals.len(); + let s = emitter.locals[n - 1].1.clone(); + let v = emitter.fresh_ssa(); + emitter.body.push_str(&format!(" {v} = load double, ptr {s}\n")); + emitter.body.push_str(&format!(" ret double {v}\n")); + emitter.body.push_str("}\n\n"); + emitter.block_terminated = true; + Ok(()) +} +``` + +(The implementer must confirm against `emit_answer` / `emit_eq_str`: the exact way to read a param SSA (`emitter.locals[n-1].1` vs another accessor), the `fresh_ssa` name, and that `expected_params`/`expected_ret` match what `llvm_type` delivers for `borrow (con StubT Int)` → `"ptr"` and `Int`→`"i64"` / `Float`→`"double"` — the `eq__Unit` entry documents a past sig-mismatch from exactly this. Since the emit is never run this iteration, signature-correctness for `check_sig` is the bar, not load-offset correctness.) + +- [ ] **Step 4: Update `kernel_stub_module_round_trips` to cover `peek`.** + +Edit `crates/ailang-core/tests/design_schema_drift.rs` (`kernel_stub_module_round_trips`, ~744). After the existing `StubT.param_in` assertions, add an assertion that the round-tripped module contains a `peek` `Def::Fn` whose body is `Term::Intrinsic`: + +```rust + let peek = recovered.defs.iter().find_map(|d| match d { + Def::Fn(f) if f.name == "peek" => Some(f), + _ => None, + }).expect("kernel_stub ships the peek ratifier op (raw-buf.3)"); + assert!(matches!(peek.body, Term::Intrinsic), "peek is an (intrinsic) marker"); +``` + +(Confirm the test's in-scope names for `Def` / `Term` / the recovered-module binding against the existing body of the test before editing.) + +- [ ] **Step 5: Add the mono-symbol integration test.** + +Create `crates/ail/tests/mono_scoped_symbol.rs`. Mirror the workspace-build + `monomorphise_workspace` harness used by `crates/ail/tests/mono_hash_stability.rs` (read it for the exact API: how it constructs a `Workspace` from source modules incl. the auto-injected kernel_stub, and how it runs monomorphisation). A consumer module that calls `StubT.peek` at both `Int` and `Float` (each via a `borrow`-param fn, so no `StubT` value need be constructed — no `Term::New`): + +```rust +//! raw-buf.3: a type-scoped polymorphic intrinsic call `StubT.peek` +//! monomorphises to the scope-qualified symbol `StubT_peek__` +//! (not the bare `peek__`). Proves the scope threads +//! resolution → FreeFnCall → MonoTarget → mint. No codegen / no +//! Term::New: assertion is on the monomorphised def names. + +// + +const CONSUMER: &str = "(module peek_consumer\n (fn at_int\n (type (fn-type (params (borrow (con StubT (con Int)))) (ret (con Int))))\n (params s)\n (body (app StubT.peek s)))\n (fn at_float\n (type (fn-type (params (borrow (con StubT (con Float)))) (ret (con Float))))\n (params s)\n (body (app StubT.peek s))))"; + +#[test] +fn stubt_peek_monomorphises_to_scope_qualified_symbols() { + // build a workspace from CONSUMER (kernel_stub auto-injected), + // monomorphise, collect every synthesised mono FnDef name. + let names: Vec = /* mirror mono_hash_stability harness */ ; + assert!(names.iter().any(|n| n == "StubT_peek__Int"), + "expected scope-qualified StubT_peek__Int; got: {names:?}"); + assert!(names.iter().any(|n| n == "StubT_peek__Float"), + "expected scope-qualified StubT_peek__Float; got: {names:?}"); + assert!(!names.iter().any(|n| n == "peek__Int"), + "bare peek__Int must NOT be minted (scope-qualified only)"); +} +``` + +(The `` and the `names` collection are filled by mirroring `mono_hash_stability.rs` verbatim for the workspace-build + monomorphise calls — that file is the authoritative harness; do not invent a new monomorphisation entry point. The three asserts are the load-bearing content.) + +- [ ] **Step 6: Run the new + affected tests.** + +Run: `cargo test -p ail --test mono_scoped_symbol stubt_peek_monomorphises_to_scope_qualified_symbols` +Expected: PASS — both scope-qualified symbols present, bare `peek__Int` absent. + +Run: `cargo test -p ailang-codegen intercepts_bijection_with_intrinsic_markers` +Expected: PASS — the `peek` marker expands to exactly `StubT_peek__Int` + `StubT_peek__Float`, both matched by the new `INTERCEPTS` entries (1-marker→2-entries). + +Run: `cargo test -p ailang-core --test design_schema_drift kernel_stub_module_round_trips` +Expected: PASS — the stub (now with `peek`) round-trips and `peek` is an `(intrinsic)`. + +- [ ] **Step 7: Final full suite.** + +Run: `cargo test --workspace 2>&1 | grep -E "^test result:" | awk -F"[: ;]+" '{p+=$4; f+=$6; i+=$8} END{print "passed:",p," failed:",f," ignored:",i}'` +Expected: `passed: 670 failed: 0 ignored: 2` — baseline 669 + the one new `mono_scoped_symbol` test. The `.ll` snapshot tests stay green: `peek` is polymorphic, so codegen never instantiates a `StubT_peek__*` define without a call site, and this iteration adds no codegen consumer (the mono test stops at monomorphisation). + +--- + +## Self-review + +1. **Spec coverage.** + - § Architecture pt 3 "scope-qualified mono symbols": Task 1. + - § Architecture pt 3 "bijection expansion over param-in": Task 2 Step 2. + - § Concrete "StubT.peek ratifier": Task 2 Step 1 + Step 3. + - § Concrete "scope-qualified mono symbol" (mono.rs:521/the mint sites): Task 1 Steps 4-7. + - § Data-flow "Mechanism": Task 1 (mint) + Task 2 Step 2 (collector). + - § Testing raw-buf.3 bullets (mono unit test, bijection green, round-trip updated, no count change beyond +1): Task 2 Steps 4-7. + +2. **Placeholder scan.** No "TBD/TODO/implement later/similar to/add appropriate". The `` and `names` collection in Task 2 Step 5 are an explicit "mirror file X verbatim" instruction naming the authoritative source (`mono_hash_stability.rs`), with the load-bearing asserts given in full — not a placeholder. + +3. **Type consistency.** `scope: Option` consistent across `FreeFnCall` / `MonoTarget::FreeFn` / `mono_target_key` / construction / mint. `scoped_base` used at both mint sites + the key. Symbol strings `StubT_peek__Int` / `StubT_peek__Float` identical across source-derived (collector), mono-minted (Task 1), and the entry names (Task 2 Step 3) — all flow through `mono_symbol_n` with a `Type::Con` element, guaranteeing byte-match. + +4. **Step granularity.** Each step is one struct edit, one threaded field, one helper, one fn, or one command. Task 1 Step 2's "every other `free_fn_owner` site" is compiler-enumerated (a tuple-shape change), bounded and mechanical. + +5. **No commit steps.** None. + +6. **Pin/replacement substring contiguity.** The asserted substrings (`StubT_peek__Int`, `StubT_peek__Float`, `peek__Int`-absent) are produced at runtime by `mono_symbol_n`, not by a verbatim file body the plan also ships — no soft-wrap split risk. The `peek` source.ail body is byte-checked by `kernel_stub_module_round_trips` (structural), not by a substring pin. + +7. **Compile-gate vs. deferred-caller ordering.** Task 1 changes three shapes (`FreeFnCall` field, `MonoTarget::FreeFn` field, `free_fn_owner` tuple) that break compilation until all sites are threaded; ALL are inside Task 1, which ends with `cargo build --workspace` (Step 8) + full test (Step 9). No caller deferred past the gate. Task 2 adds entries/fns/tests with no cross-task signature break. + +8. **Verification-command filter strings.** Task 2 Steps 6 filters name tests this plan creates (`stubt_peek_monomorphises_to_scope_qualified_symbols`, in the file created Step 5) or that exist per recon (`intercepts_bijection_with_intrinsic_markers`, `kernel_stub_module_round_trips`). Task 1 Step 9 + Task 2 Step 7 use the unfiltered suite with an explicit count assertion (awk), so "0 ran" cannot pass as success. + +9. **Parse-the-bytes gate.** The one surface-language body inlined is the `peek` op (Task 2 Step 1), fenced `text` — `kernel_stub` is a reserved built-in module name (`ail check` standalone → `reserved-module-name`), so the configured `ail` parser cannot validate it in isolation; the gate is N/A for it. Its Form-A was nonetheless verified this session under a non-reserved probe module (`ok (32 symbols across 3 modules)`), and it is structurally ratified by `kernel_stub_module_round_trips` (Task 2 Step 4) + the `implement` compile gate. All other inlined bodies are Rust — caught by the compile gate, not the parse gate.