From ce0374ac0c20c3cc08f36e313317cd9b1d7f9248 Mon Sep 17 00:00:00 2001 From: Brummel Date: Fri, 29 May 2026 16:55:01 +0200 Subject: [PATCH] =?UTF-8?q?plan:=20intrinsic-bodies.1-mechanism=20?= =?UTF-8?q?=E2=80=94=208-task=20Term::Intrinsic=20cross-crate=20landing=20?= =?UTF-8?q?(refs=20#9)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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 --- .../0106-intrinsic-bodies.1-mechanism.md | 616 ++++++++++++++++++ 1 file changed, 616 insertions(+) create mode 100644 docs/plans/0106-intrinsic-bodies.1-mechanism.md diff --git a/docs/plans/0106-intrinsic-bodies.1-mechanism.md b/docs/plans/0106-intrinsic-bodies.1-mechanism.md new file mode 100644 index 0000000..4619000 --- /dev/null +++ b/docs/plans/0106-intrinsic-bodies.1-mechanism.md @@ -0,0 +1,616 @@ +# intrinsic-bodies.1 — the mechanism — Implementation Plan + +> **Parent spec:** `docs/specs/0055-intrinsic-bodies.md` (committed 5b66de7) +> +> **For agentic workers:** REQUIRED SUB-SKILL: use the `implement` +> skill to run this plan. Steps use `- [ ]` checkboxes for tracking. + +**Goal:** Introduce `Term::Intrinsic`, a new leaf term that is the body +of a compiler-supplied definition, and wire it through surface, checker, +and codegen so a kernel-tier `(intrinsic)` function builds and runs via +the intercept registry — ratified by a throwaway `answer : () -> Int` +intrinsic in the `kernel_stub` fixture. + +**Architecture:** `Term::Intrinsic` is a unit variant on the `Term` +enum (`{"t":"intrinsic"}`, additive, hash-stable like `Term::Recur`). +`FnDef.body` / `Term::Lam.body` keep type `Term` / `Box`; a def is +intrinsic iff `matches!(body, Term::Intrinsic)`. The parser maps +`(intrinsic)` (a top-level fn body-slot clause, or a lambda positional +body) to `Term::Intrinsic`; the printer inverts it. The checker treats +such a body as signature-only and rejects it outside kernel-tier / +prelude. Codegen routes it through `intercepts::lookup` instead of +`lower_term`. The cross-crate `match`-on-`Term` walkers gain a leaf arm, +enumerated by the compiler (no-wildcard exhaustive matches). + +**Tech Stack:** `crates/ailang-core` (ast + canonical/hash/visit), +`crates/ailang-surface` (parse/print), `crates/ailang-check` +(lib + the analysis walkers), `crates/ailang-codegen` (lib + +intercepts + escape/lambda), `crates/ailang-prose`, +`crates/ailang-kernel-stub`, `crates/ail/tests` (E2E), +`design/contracts/0002-data-model.md`. + +--- + +## Files this plan creates or modifies + +- Modify: `crates/ailang-core/src/ast.rs:602-607` — add `Term::Intrinsic` unit variant after `New`. +- Modify: `crates/ailang-core/src/` canonical/hash/visit walkers — leaf arm (compile-driven, Task 1). +- Modify: `crates/ailang-core/tests/schema_coverage.rs:78` — `EXPECTED_VARIANTS` + corpus observation. +- Modify: `crates/ailang-surface/src/parse.rs` — `parse_fn` (572-655), `parse_lam` (1501-1539). +- Modify: `crates/ailang-surface/src/print.rs` — fn-def (211-213), lam (526-554). +- Modify: `crates/ailang-check/src/lib.rs` — `env` kernel-tier flag (~1846), sig-only check (~2095/2244), `CheckError::IntrinsicOutsideKernelTier` + `code()` (772-819). +- Modify: `crates/ailang-check/src/{lift,reuse_shape,pre_desugar_validation,linearity,uniqueness,mono}.rs` — leaf arms (compile-driven, Task 3). +- Modify: `crates/ailang-codegen/src/lib.rs` — routing (1320-1324), and the `lower_term` Term arm (~2075 neighbourhood). +- Modify: `crates/ailang-codegen/src/{escape,lambda}.rs` — leaf arms (compile-driven, Task 3). +- Modify: `crates/ailang-prose/src/lib.rs` — leaf arms (937/1150/1307 neighbourhood, Task 3). +- Modify: `crates/ailang-codegen/src/intercepts.rs:35-168` — `answer` registry entry. +- Modify: `crates/ailang-kernel-stub/src/lib.rs:27-37` — `answer` intrinsic fn in `STUB_AIL`. +- Create: `examples/kernel_answer.ail` — consumer that calls `(answer)`, for the E2E ratifier. +- Modify: `crates/ail/tests/e2e.rs` — `answer` build+run test. +- Modify: `design/contracts/0002-data-model.md:106-194` — `{ "t": "intrinsic" }` Term entry. + +--- + +## Task 1: `Term::Intrinsic` variant + in-core walkers + +**Files:** +- Modify: `crates/ailang-core/src/ast.rs:602-607` +- Modify: core walkers the compiler flags (canonical / hash / visit / pretty) + +- [ ] **Step 1: Add the variant** + +In `crates/ailang-core/src/ast.rs`, after the `New { ... }` variant +(ends line 606) and before the `Term` enum's closing brace (line 607), +add: + +```rust + /// The body of a compiler-supplied ("intrinsic") definition. Legal + /// only as the body of a `FnDef` or a `Term::Lam`, and only in a + /// `(kernel)`-tier module or the prelude (enforced at typecheck, + /// `IntrinsicOutsideKernelTier`). Never reduces to a value: codegen + /// consumes it via `intercepts::lookup` on the def's mangled name; + /// the typechecker treats a def with this body as signature-only. + /// A def is intrinsic iff `matches!(body, Term::Intrinsic)`. Additive + /// `"t":"intrinsic"` tag (unit variant via the enum's + /// `rename_all = "lowercase"`); pre-existing fixtures hash + /// bit-identically — none carry the tag. Precedent: `Term::Recur` + /// (a non-reducing control-transfer leaf). + Intrinsic, +``` + +- [ ] **Step 2: Build core; let the compiler enumerate the broken exhaustive matches** + +Run: `cargo build -p ailang-core 2>&1 | grep -E "not covered|non-exhaustive|E0004" | head -40` +Expected: a list of `match`-on-`Term` sites in `ailang-core/src/` that +lack a wildcard (canonical serialise, hash, any visit/walk). Each is a +site Step 3 fixes. + +- [ ] **Step 3: Add the leaf arm at each flagged core site** + +For each site the compiler flags, add a `Term::Intrinsic` arm by kind: +- **canonical / serialise** (the function that turns a `Term` into + canonical bytes / JSON): `Term::Intrinsic` serialises as the unit + variant `{"t":"intrinsic"}` — serde derives this automatically, so a + hand-written canonicaliser arm (if any) emits the same tag with no + children. +- **hash**: hash the discriminant/tag only (no sub-terms) — mirror the + shape of the `Term::Recur` arm's tag-hash if hashing is hand-rolled; + if hashing goes through serde/canonical bytes, no arm is needed beyond + serialise. +- **visit / walk / map over sub-terms**: `Term::Intrinsic => {}` (no + sub-terms to recurse into) — for a `map`-style walker that must return + a `Term`, `Term::Intrinsic => Term::Intrinsic`. +- **pretty (`crates/ailang-core/src/pretty.rs`)**, if it matches `Term` + exhaustively: emit the diagnostic string `intrinsic`. + +- [ ] **Step 4: Build core green** + +Run: `cargo build -p ailang-core` +Expected: compiles, 0 errors. + +--- + +## Task 2: Surface — parse + print `(intrinsic)` ↔ `Term::Intrinsic` + +**Files:** +- Modify: `crates/ailang-surface/src/parse.rs` (`parse_fn` 572-655, `parse_lam` 1501-1539) +- Modify: `crates/ailang-surface/src/print.rs` (fn-def 211-213, lam 526-554) +- Test: round-trip rides the existing `crates/ailang-surface/tests/round_trip.rs` corpus gate (a fixture lands in Task 6). + +- [ ] **Step 1: Accept `(intrinsic)` as a fn body-slot clause in `parse_fn`** + +In `parse_fn` (parse.rs:572-655): today an unknown attribute head is +rejected at 617-626 (the `unknown fn attribute` error), and a `(body +...)` is mandatory at 641-645. Add an `intrinsic` arm to the +attribute-dispatch so that a `(intrinsic)` clause sets the fn body to +`Term::Intrinsic`, and relax the mandatory-`(body)` enforcement so that +*exactly one* of `(body X)` or `(intrinsic)` is present (both-absent +stays an error; the grammar offers no way to write both, since each +fills the single body slot). Concretely: track the body as it is parsed; +when the `intrinsic` head is seen, set `body = Some(Term::Intrinsic)`; +at 641-645 require `body.is_some()` rather than specifically a `(body)` +clause. + +- [ ] **Step 2: Accept `(intrinsic)` at the lambda positional body in `parse_lam`** + +In `parse_lam` (parse.rs:1501-1539): the positional body term is read at +1530. Where the body term is parsed, if the token form is `(intrinsic)`, +produce `Term::Intrinsic` instead of recursing into general term +parsing. (The lambda's body is positional, so `(intrinsic)` simply *is* +the body term form here — no attribute loop.) + +- [ ] **Step 3: Print `Term::Intrinsic` in fn-def position** + +In `print.rs` fn-def printer (211-213, the `(body …)` emit): when the +fn's body is `Term::Intrinsic`, emit `(intrinsic)` in the body slot +instead of `(body )`. + +- [ ] **Step 4: Print `Term::Intrinsic` in lambda body position** + +In `print.rs` lam printer (526-554, body emit at 552-553): when the +lambda body is `Term::Intrinsic`, emit `(intrinsic)` at the positional +body slot. + +- [ ] **Step 5: Add a print arm if the surface term-printer matches `Term` exhaustively** + +Run: `cargo build -p ailang-surface 2>&1 | grep -E "not covered|non-exhaustive" | head` +Expected: if a general term-printer in print.rs matches `Term` +exhaustively, it is flagged. Add `Term::Intrinsic => …` emitting +`(intrinsic)` (this is the same emit Steps 3-4 use; a general +term-printer reaching `Term::Intrinsic` only happens via a fn/lam body, +so emitting `(intrinsic)` is correct). + +- [ ] **Step 6: Build surface green** + +Run: `cargo build -p ailang-surface` +Expected: compiles, 0 errors. + +--- + +## Task 3: Cross-crate walker sweep (check + codegen + prose) + +**Files:** +- Modify: `crates/ailang-check/src/{lift,reuse_shape,pre_desugar_validation,linearity,uniqueness,mono}.rs` +- Modify: `crates/ailang-codegen/src/{escape,lambda}.rs` +- Modify: `crates/ailang-prose/src/lib.rs` + +(Codegen's `lib.rs` `lower_term` arm and the checker's body-check arm +are NOT here — those are the *meaningful* arms in Tasks 4-5. This task +is only the mechanical leaf arms in the generic walkers.) + +- [ ] **Step 1: Build the workspace; enumerate the remaining broken matches** + +Run: `cargo build --workspace 2>&1 | grep -E "not covered|non-exhaustive|E0004" | head -60` +Expected: a list of `match`-on-`Term` sites across check / codegen / +prose lacking a wildcard. These are exactly the sites that match +`Term::New` today (verified set: `lift.rs`, `reuse_shape.rs`, +`pre_desugar_validation.rs`, `linearity.rs`, `uniqueness.rs`, +`mono.rs:1284/1666`, `escape.rs:205/409/548`, `lambda.rs:498`, +`prose/src/lib.rs:937/1150/1307`, `workspace.rs:1285/1428`). Sites that +have a `_ =>` wildcard will NOT appear — do not add arms there. + +- [ ] **Step 2: Add the leaf arm at each flagged site, by walker kind** + +`Term::Intrinsic` carries no sub-terms, so every arm here is a leaf: +- **analysis walkers** (linearity, uniqueness, escape, reuse_shape, + pre_desugar_validation — these compute over expression structure): + `Term::Intrinsic => { /* no sub-terms; no vars, captures, or + ownership obligations */ }` returning the walker's empty/identity + value (e.g. `false` for an "any subterm satisfies P" predicate, + `Ok(())` for a validation pass, an empty set for a free-vars + collector). +- **`mono.rs:1284/1666`** (mono cursor walk): `Term::Intrinsic => {}` — + no mono targets inside an intrinsic body. (Note: `synthesise_mono_fn` + at 957-970 is NOT touched — its lambda destructure `Term::Lam { + params, body, .. } => (params, *body)` already passes an inner + `Term::Intrinsic` straight through to the synthesised `FnDef.body`.) +- **`lambda.rs:498`** (lambda/closure lowering walk): `Term::Intrinsic + => {}` — an intrinsic body captures nothing and lifts no lambda. +- **`prose/src/lib.rs`** (Form-B projection): `Term::Intrinsic` renders + as the prose phrase `compiler-supplied (intrinsic)` (mirror the + terse style of the surrounding arms; prose is lossy and human-facing). +- **`workspace.rs:1285/1428`** (if flagged — term walk during workspace + load): `Term::Intrinsic => {}` / `Term::Intrinsic` identity. + +- [ ] **Step 3: Build the workspace green (lower_term + check body arms still stubbed)** + +Run: `cargo build --workspace 2>&1 | grep -E "not covered|non-exhaustive" | head` +Expected: empty — OR only the two intentional sites left for Tasks 4-5 +(codegen `lower_term`'s `Term` match and the checker's per-fn body +check). If those two are flagged, add a temporary +`Term::Intrinsic => unreachable!("handled in Task 4/5")` so the build is +green; Tasks 4-5 replace them. If they already have wildcards, nothing +to do. + +- [ ] **Step 4: Full build green** + +Run: `cargo build --workspace` +Expected: compiles, 0 errors. + +--- + +## Task 4: Checker — signature-only + `intrinsic-outside-kernel-tier` + +**Files:** +- Modify: `crates/ailang-check/src/lib.rs` (env flag ~1846, `check_fn` body-check ~2095/2244, `CheckError` 393-818, `code()` 772-819) +- Test: `crates/ailang-check` in-source `#[cfg(test)] mod tests` OR `crates/ail/tests/` E2E (the accept case is the kernel_stub ratifier in Task 7; the reject case is here) + +- [ ] **Step 1: Add the `CheckError` variant** + +In `crates/ailang-check/src/lib.rs` `CheckError` enum (393-818), add a +variant alongside the most-recent additive exemplar +(`ParamNotInRestrictedSet` at ~817): + +```rust + /// An `(intrinsic)` body appears in a module that is neither + /// kernel-tier nor the prelude. User code may not declare a body as + /// compiler-supplied — the honesty-rule guard at the workspace + /// boundary (design/contracts/0007-honesty-rule.md). + IntrinsicOutsideKernelTier { def: String, module: String }, +``` + +- [ ] **Step 2: Register its code in `code()`** + +In the `code()` registry (772-819), add the arm (mirror the +`ParamNotInRestrictedSet` arm shape): + +```rust + CheckError::IntrinsicOutsideKernelTier { .. } => "intrinsic-outside-kernel-tier", +``` + +- [ ] **Step 3: Thread a kernel-tier flag into `env`** + +Where `env.current_module` is set (lib.rs:1846), also set a new +`env.current_module_kernel_tier: bool` computed from the module `m` +being checked: `m.kernel || m.name == "prelude"`. Add the field to the +`Env` struct (default `false`). This avoids threading `m` through every +`check_fn`/`check_def` call. + +- [ ] **Step 4: Signature-only + scope-guard at the per-fn body check** + +At the per-fn body check (the synth/verify over `&f.body`, ~2244): when +`matches!(f.body, Term::Intrinsic)` (top-level fn) OR the fn's body is a +`Term::Lam` whose inner body is `Term::Intrinsic` (synthesised +instance-method), skip body inference (the signature is already +validated from `f.ty`). Before skipping, enforce the scope guard: if +`!env.current_module_kernel_tier`, push +`CheckError::IntrinsicOutsideKernelTier { def: f.name.clone(), module: +env.current_module.clone() }`. (For the instance-method case the +synthesised fn's home module is the instance's module, already reflected +in `env.current_module`.) + +- [ ] **Step 5: Write the reject test (subprocess, temp file)** + +Add to `crates/ail/tests/e2e.rs` a reject test modelled **exactly** on +the verified `check_json_unbound_var` pattern (e2e.rs:1236-1260): it +writes a user-module `.ail` to a temp dir, runs `ail check +--json` via `Command`, asserts exit code 1, parses stdout as a JSON +diagnostics array, and asserts an `error`-severity diagnostic with the +expected `code`. A temp file (not an `examples/` fixture) is used so the +reject case does not enter the round-trip corpus or need a carve-out. +Test source (Rust): + +```rust +#[test] +fn intrinsic_in_user_module_is_rejected() { + let tmp = std::env::temp_dir().join(format!( + "ailang_intrinsic_reject_{}", + std::process::id() + )); + std::fs::create_dir_all(&tmp).unwrap(); + let src = tmp.join("intr_user.ail"); + std::fs::write( + &src, + "(module intr_user (fn f (type (fn-type (params (con Int)) (ret (con Int)))) (params n) (intrinsic)))", + ) + .unwrap(); + + let output = Command::new(ail_bin()) + .args(["check", src.to_str().unwrap(), "--json"]) + .output() + .expect("ail check --json failed to run"); + + let code = output.status.code().expect("process terminated by signal"); + assert_eq!( + code, 1, + "expected exit 1, stderr: {}", + String::from_utf8_lossy(&output.stderr) + ); + let stdout = String::from_utf8(output.stdout).expect("stdout utf8"); + let diags: serde_json::Value = + serde_json::from_str(stdout.trim()).expect("stdout must be valid JSON"); + let arr = diags.as_array().expect("diagnostics must be a JSON array"); + assert!( + arr.iter().any(|d| { + d.get("severity").and_then(|v| v.as_str()) == Some("error") + && d.get("code").and_then(|v| v.as_str()) == Some("intrinsic-outside-kernel-tier") + }), + "expected an error with code intrinsic-outside-kernel-tier; got: {stdout}" + ); +} +``` + +(`ail_bin()` and the `Command`/`serde_json` imports are already present +in e2e.rs — this test reuses the exact harness `check_json_unbound_var` +uses.) + +- [ ] **Step 6: Run the reject test** + +Run: `cargo test -p ail --test e2e intrinsic_in_user_module_is_rejected` +Expected: PASS — exit 1 with code `intrinsic-outside-kernel-tier`. + +--- + +## Task 5: Codegen — route `Term::Intrinsic` through the intercept registry + +**Files:** +- Modify: `crates/ailang-codegen/src/lib.rs` (the fn-body emit at 1320-1324; the `lower_term` `Term` match neighbourhood ~2075) + +- [ ] **Step 1: Skip body-lowering for an intrinsic fn body** + +At the fn-body emit (lib.rs:1320-1324): the path today is +`body_was_intercepted = self.try_emit_primitive_instance_body(&f.name, +…)`; then `if !body_was_intercepted { let (val, val_ty) = +self.lower_term(&f.body)?; … }`. Add: when `matches!(f.body, +Term::Intrinsic)` and `!body_was_intercepted` (no name-based intercept +fired), attempt the registry directly via `intercepts::lookup(&f.name)`; +on hit, emit the intercept (the same call shape +`try_emit_primitive_instance_body` uses) and treat the body as emitted; +on miss, return the existing deferral-style `CodegenError::Internal` +naming the unregistered intrinsic. Critically: never fall through to +`self.lower_term(&f.body)` when the body is `Term::Intrinsic`. + + (Implementer note: `try_emit_primitive_instance_body` already consults + `intercepts::lookup` internally as of raw-buf.1. Confirm whether + `answer` — a top-level nullary fn, name `answer`, not a mangled + `method__Type` — is reached by that existing call with `f.name == + "answer"`. If yes, Step 1 reduces to: ensure an intrinsic body whose + intercept fired is treated as complete, and an intrinsic body whose + intercept did NOT fire is an error rather than a `lower_term` attempt. + If the existing call only matches mangled instance names, add the + explicit `intercepts::lookup(&f.name)` branch here.) + +- [ ] **Step 2: Handle `Term::Intrinsic` in the `lower_term` match** + +In `lower_term`'s `Term` match (the same match that holds the +`Term::New` deferral at ~2075), add: + +```rust + Term::Intrinsic => Err(CodegenError::Internal( + "Term::Intrinsic must be consumed by the intercept route in fn-body \ + emission, not lowered as an expression; reaching lower_term means an \ + intrinsic body escaped its definition slot".into(), + )), +``` + +(This is the "reaching `lower_term` is a codegen-internal error" clause +from the spec § Architecture point 4. It also satisfies the temporary +`unreachable!` if Task 3 Step 3 left one here.) + +- [ ] **Step 3: Build codegen green** + +Run: `cargo build -p ailang-codegen` +Expected: compiles, 0 errors. + +--- + +## Task 6: `answer` intercept + kernel_stub fixture + schema-coverage + +**Files:** +- Modify: `crates/ailang-codegen/src/intercepts.rs:35-168` (INTERCEPTS table) + the `Intercept` struct neighbourhood (27-33) +- Modify: `crates/ailang-kernel-stub/src/lib.rs:27-37` (STUB_AIL) +- Modify: `crates/ailang-core/tests/schema_coverage.rs:78` (EXPECTED_VARIANTS + corpus note) + +- [ ] **Step 1: Register the `answer` intercept** + +In `crates/ailang-codegen/src/intercepts.rs`, add an entry to +`INTERCEPTS` (35-168) following the existing entry shape (`name`, +`expected_params`, `expected_ret`, `wants_alwaysinline`, `emit`): + +```rust + Intercept { + name: "answer", + expected_params: &[], + expected_ret: "i64", + wants_alwaysinline: false, + emit: emit_answer, + }, +``` + +And add the free emit fn alongside the other `emit_*` fns: + +```rust +/// Ratifier intrinsic for intrinsic-bodies.1: `answer : () -> Int` +/// returns the constant 42. Exercises the Term::Intrinsic → registry +/// route end-to-end. May be retired once a real kernel-tier intrinsic +/// (raw-buf) lands. +fn emit_answer(e: &mut Emitter) -> Result<()> { + e.body.push_str(" ret i64 42\n"); + Ok(()) +} +``` + +(The implementer confirms `Result`/`Emitter` imports and the exact +`e.body.push_str` idiom against the sibling emit fns; `answer` is +purely additive and does NOT break `registry_contains_all_legacy_arms`, +which only asserts the 18 legacy names resolve — it does not assert the +table contains *only* them.) + +- [ ] **Step 2: Add the `answer` intrinsic to STUB_AIL** + +In `crates/ailang-kernel-stub/src/lib.rs` `STUB_AIL` (27-37), add an +`answer` fn to the `kernel_stub` module body. The stub already carries +`(kernel)`, so `answer` is in a kernel-tier module and the scope guard +passes. Add (Form-A, inside the module): + +```scheme + (fn answer + (doc "Ratifies the intrinsic mechanism end-to-end: codegen intercept answer emits ret i64 42.") + (type (fn-type (params) (ret (con Int)))) + (params) + (intrinsic)) +``` + +- [ ] **Step 3: Verify the stub round-trips with the intrinsic fn** + +Run: `cargo test -p ailang-core --test design_schema_drift kernel_stub_module_round_trips` +Expected: PASS — the stub (now carrying an intrinsic fn) parses, prints, +re-parses to canonical-byte equality. This is the round-trip ratifier +for `Term::Intrinsic` on both parse and print. + +- [ ] **Step 4: Observe `Term::Intrinsic` in the schema-coverage corpus** + +Run: `cargo test -p ailang-core --test schema_coverage every_ast_variant_is_observed` +Expected: this may FAIL with `Term::Intrinsic` not observed, because the +corpus scans `examples/*.ail` and the stub is an in-crate const, not an +`examples/` fixture. If it fails: add `Term::Intrinsic` handling so the +corpus sees it — the `examples/kernel_answer.ail` fixture created in +Task 7 Step 1 carries a consumer of `answer` but NOT an intrinsic body +itself (the intrinsic lives in the stub). Per the recon note, +`Term::New` is in `visit_term` but absent from `EXPECTED_VARIANTS` +(schema_coverage.rs:252-255) precisely because no `examples/` fixture +exercises it. To avoid repeating that gap, add a minimal `examples/` +fixture that DOES carry an intrinsic body in a kernel-tier module: + +Create `examples/kernel_intrinsic_smoke.ail`: + +```scheme +(module kernel_intrinsic_smoke + (kernel) + (fn smoke + (doc "Schema-coverage fixture: exercises Term::Intrinsic in a kernel-tier module.") + (type (fn-type (params) (ret (con Int)))) + (params) + (intrinsic))) +``` + +Then add `Term::Intrinsic` to `EXPECTED_VARIANTS` (schema_coverage.rs:78) +so the corpus gate asserts it is observed. + +- [ ] **Step 5: Re-run schema-coverage green** + +Run: `cargo test -p ailang-core --test schema_coverage every_ast_variant_is_observed` +Expected: PASS. + +NOTE: `examples/kernel_intrinsic_smoke.ail` also enters the round-trip +corpus gate (`round_trip.rs` reads `examples/*.ail`), so it additionally +ratifies parse∘print=id for a top-level intrinsic fn. Confirm +`cargo test -p ailang-surface --test round_trip` stays green after +adding it. + +--- + +## Task 7: E2E ratifier — `answer` builds and runs, observing 42 + +**Files:** +- Create: `examples/kernel_answer.ail` +- Modify: `crates/ail/tests/e2e.rs` + +- [ ] **Step 1: Create the consumer fixture** + +The `answer` intrinsic lives in `kernel_stub`, which is auto-imported +into every workspace (kernel-tier, prep.3). A consumer module calls it +unqualified-or-qualified per the kernel-tier import rule. Create +`examples/kernel_answer.ail`: + +```scheme +(module kernel_answer + (fn main + (doc "E2E ratifier: prints the intrinsic answer (42).") + (type (fn-type (params) (ret (con Int)))) + (params) + (body (do io/print_int (app kernel_stub.answer))))) +``` + +NOTE: the implementer verifies the exact call form for a kernel-tier +nullary fn against an existing kernel_stub consumer test (how +`kernel_stub.new` is called in the prep.3 / kernel-extension E2E +fixtures) and against the `(app f)` nullary-call form +(`Term::App.args = []`). If `io/print_int` is not the exact effect-op +name, model the print on an existing `examples/*.ail` that prints an Int +(grep `examples/` for `io/print`). + +- [ ] **Step 2: Verify it parses + checks** + +Run: `cargo run -q -p ail --bin ail -- check examples/kernel_answer.ail` +Expected: `ok (...)` — exit 0. (The `answer` intrinsic resolves through +the auto-imported kernel_stub; the consumer body is an ordinary call.) + +- [ ] **Step 3: Write the build+run E2E test** + +Add to `crates/ail/tests/e2e.rs`, modelled on the existing +`build_and_run` helper (13-38) and an existing +`*_compiles_and_runs`/`*_prints_*` test +(e.g. `eq_primitives_smoke_compiles_and_runs`): + +```rust +#[test] +fn answer_intrinsic_builds_and_runs_printing_42() { + let out = build_and_run("kernel_answer.ail"); + assert_eq!(out.trim(), "42"); +} +``` + +(`build_and_run(example: &str) -> String` is verified at e2e.rs:13-38: +it takes the fixture filename *with* `.ail` extension — existing calls +are `build_and_run("sum.ail")` — internally asserts build+run success, +and returns stdout. The `.trim()` + `assert_eq!` shape matches the +existing `*_prints_*` tests.) + +- [ ] **Step 4: Run the ratifier** + +Run: `cargo test -p ail --test e2e answer_intrinsic_builds_and_runs_printing_42` +Expected: PASS — the intrinsic mechanism works from source to native. + +--- + +## Task 8: Data-model contract entry + +**Files:** +- Modify: `design/contracts/0002-data-model.md` (Term section, 106-194) + +- [ ] **Step 1: Document `Term::Intrinsic`** + +In `design/contracts/0002-data-model.md`, in the Term (expression) +section, after the `new` entry (ends ~194), add: + +````markdown +// intrinsic: the body of a compiler-supplied definition. Legal only as +// a FnDef/Lam body, only in a (kernel)-tier module or the prelude +// (typecheck: intrinsic-outside-kernel-tier). Never reduces to a value; +// codegen consumes it via the intercept registry. A def is intrinsic +// iff its body is this term. Strictly additive (no skip_serializing_if; +// pre-existing fixtures hash bit-identically — none carry the tag). +{ "t": "intrinsic" } +```` + +Also add a clause to the `fn` and `lam` prose noting the body may be +`{ "t": "intrinsic" }`, in which case it is compiler-supplied. + +- [ ] **Step 2: Verify the schema-drift mirror stays green** + +Run: `cargo test -p ailang-core --test design_schema_drift` +Expected: PASS — the contract's documented Term shapes match the AST. + +--- + +## Final gate + +- [ ] **Step F1: Full workspace test** + +Run: `cargo test --workspace` +Expected: all green. Baseline was 667 (post-raw-buf.1); this iteration +adds: `intrinsic_in_user_module_is_rejected`, +`answer_intrinsic_builds_and_runs_printing_42`, plus the corpus/round-trip +fixtures ride existing dynamic gates (no new named test). Expected +≈669 passing, 0 failed. Pre-existing hash pins +(`crates/ailang-core/tests/hash_pin.rs`, +`crates/ailang-surface/tests/prelude_module_hash_pin.rs`) MUST stay +green — `Term::Intrinsic` is additive and no existing fixture carries +it, so no hash moves in .1. (The prelude is NOT migrated in .1 — that is +.2.) + +- [ ] **Step F2: Regression scripts** + +Run: `bench/check.py && bench/compile_check.py` +Expected: no regression versus baseline.