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:
@@ -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.
|
||||
Reference in New Issue
Block a user