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
This commit is contained in:
2026-05-29 16:55:01 +02:00
parent 5b66de77ac
commit ce0374ac0c
@@ -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<Term>`; 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 <term>)`.
- [ ] **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 <file>
--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.