iter intrinsic-bodies.1-mechanism (DONE 8/8): Term::Intrinsic leaf + cross-crate wiring (refs #9)

First iteration of the intrinsic-bodies milestone. Introduces the
Form-A `(intrinsic)` body marker as a new leaf AST term and wires it
through surface, checker, codegen, and a kernel_stub ratifier. The
prelude migration + hard-lockstep pin + dead-path removal are .2.

What landed (8 tasks):

  Task 1 — Term::Intrinsic unit variant (ast.rs), tag "t":"intrinsic"
    via the enum's rename_all=lowercase. Additive: no existing fixture
    carries it, hashes bit-identical. In-core exhaustive-match arms
    (canonical/hash/visit/pretty/desugar/workspace) added as leaves.
  Task 2 — surface parse + print: (intrinsic) as a fn body-slot clause
    and a lambda positional body, mapped to/from Term::Intrinsic.
  Task 3 — cross-crate walker sweep (check/codegen/prose/ail-main):
    leaf no-op/identity arms at every no-wildcard Term match the
    compiler flagged.
  Task 4 — checker: new Env.current_module_kernel_tier flag (m.kernel
    || m.name=="prelude"), set alongside current_module. A def whose
    body is intrinsic (top-level fn OR instance-method lambda, via the
    shared is_intrinsic_body helper) is checked signature-only; an
    intrinsic body outside kernel-tier/prelude is rejected with
    intrinsic-outside-kernel-tier.
  Task 5 — codegen: an intrinsic-bodied fn routes through the existing
    try_emit_primitive_instance_body / intercepts::lookup path; if no
    intercept fired it is an internal error, never a lower_term
    fallthrough. lower_term and the synth walker get Term::Intrinsic
    internal-error arms (an intrinsic body reaching either is an
    escape bug).
  Task 6 — answer intercept (ret i64 42) registered in INTERCEPTS;
    the `answer : () -> Int` intrinsic added to STUB_AIL;
    examples/kernel_intrinsic_smoke.ail added so schema_coverage
    observes Term::Intrinsic in the examples/ corpus.
  Task 7 — E2E ratifier: examples/kernel_answer.ail calls
    kernel_stub.answer and prints 42; answer_intrinsic_builds_and_runs_printing_42
    asserts it end-to-end (source → native).
  Task 8 — design/contracts/0002-data-model.md gains the
    { "t": "intrinsic" } Term entry + fn/lam prose; form_a.md grammar
    note updated.

Verification:
  cargo test --workspace → 669 passed, 0 failed (baseline 667 +2:
    intrinsic_in_user_module_is_rejected, answer_intrinsic_builds_and_runs_printing_42).
  bench/check.py + bench/compile_check.py → 0 regressed.
  Reject E2E (subprocess ail check --json, exit 1, code
    intrinsic-outside-kernel-tier) GREEN.
  Round-trip + hash pins GREEN — Term::Intrinsic is additive, no
    existing fixture carries it, no hash moved.

Three implementation completions beyond the plan (all behaviour-
preserving, surfaced during execution):

1. The signature-only skip had to apply at the mono pass's two
   synth-on-body re-entry sites (collect_mono_targets,
   collect_residuals_ordered), not only check_fn — else an intrinsic
   body hits synth's Term::Intrinsic internal-error guard. Repaired by
   extracting the shared crate::is_intrinsic_body helper and applying
   it at all three synth-on-body paths. Not a representation surprise:
   the same signature-only treatment, more call sites.

2. The compiler-enumerated exhaustive-match set was broader than the
   plan's named grep set (the plan anticipated this and made the sweep
   compile-driven). Extra leaf arms in core desugar/workspace, check
   reuse-as + qualify_workspace_term, codegen synth_with_extras,
   ail/src/main.rs, and four test targets.

3. Fixture corrections: emit_answer needed the body-close
   (block terminator) the plan snippet omitted; kernel_answer.ail's
   main is (ret Unit)(effects IO) using (app print ...) since
   io/print_int does not exist (the plan flagged this for the
   implementer to resolve against real effect-op names).

IR snapshots (hello/sum/list/max3/ws_main.ll) refreshed: purely
additive @ail_kernel_stub_answer fn+adapter+closure, emitted into
every workspace exactly as the pre-existing @ail_kernel_stub_new
already was (kernel_stub is auto-injected; confirmed new was present
in the pre-iter hello.ll baseline). No user-fn IR changed.

The .2 iteration migrates the 18 prelude dummy bodies to (intrinsic),
upgrades registry_contains_all_legacy_arms to a source<->registry
bijection pin, and removes the dead body-lowering path.
This commit is contained in:
2026-05-29 17:21:32 +02:00
parent ce0374ac0c
commit 52ff8738b8
33 changed files with 373 additions and 12 deletions
+59
View File
@@ -270,6 +270,7 @@ pub(crate) fn substitute_rigids_in_term(t: &Term, mapping: &BTreeMap<String, Typ
})
.collect(),
},
Term::Intrinsic => t.clone(),
}
}
@@ -755,6 +756,14 @@ pub enum CheckError {
allowed: Vec<String>,
},
/// 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).
/// Code: `intrinsic-outside-kernel-tier`.
#[error("def `{def}` in module `{module}` uses an `(intrinsic)` body, which is allowed only in a kernel-tier module or the prelude")]
IntrinsicOutsideKernelTier { def: String, module: String },
/// an internal invariant in the typechecker / mono pass
/// was violated — surfaced as an error so callers can propagate
/// rather than abort, but in well-formed inputs (typecheck has
@@ -815,6 +824,7 @@ impl CheckError {
CheckError::NewTypeNotConstructible { .. } => "new-type-not-constructible",
CheckError::NewArgKindMismatch { .. } => "new-arg-kind-mismatch",
CheckError::ParamNotInRestrictedSet { .. } => "param-not-in-restricted-set",
CheckError::IntrinsicOutsideKernelTier { .. } => "intrinsic-outside-kernel-tier",
CheckError::Internal(_) => "internal",
}
}
@@ -1844,6 +1854,7 @@ fn check_in_workspace(
}
env.imports = import_map;
env.current_module = m.name.clone();
env.current_module_kernel_tier = m.kernel || m.name == "prelude";
for def in &m.defs {
match check_def(def, &env, out_warnings) {
@@ -2092,6 +2103,20 @@ fn check_type_well_formed(t: &Type, env: &Env) -> Result<()> {
}
}
/// `true` when `f`'s body is an `(intrinsic)` marker: either the body
/// is `Term::Intrinsic` directly (a top-level intrinsic fn) or it is a
/// `Term::Lam` whose inner body is `Term::Intrinsic` (a synthesised
/// instance method). Such a body is signature-only — the typechecker
/// validates the signature and skips body inference, and the mono pass
/// collects no targets from it. Shared by `check_fn` and the mono
/// pass's `synth`-re-entry sites (`mono::collect_mono_targets`,
/// `mono::collect_residuals_ordered`) so every synth-on-body path
/// applies the same skip.
pub(crate) fn is_intrinsic_body(f: &FnDef) -> bool {
matches!(f.body, Term::Intrinsic)
|| matches!(&f.body, Term::Lam { body, .. } if matches!(**body, Term::Intrinsic))
}
fn check_fn(f: &FnDef, env: &Env, out_warnings: &mut Vec<Diagnostic>) -> Result<()> {
// Peel an outer Forall (Iter 12a). The vars become rigid in the
// inner env so they pass `check_type_well_formed` and unify only
@@ -2138,6 +2163,23 @@ fn check_fn(f: &FnDef, env: &Env, out_warnings: &mut Vec<Diagnostic>) -> Result<
}
check_type_well_formed(&ret_ty, &env)?;
// An `(intrinsic)` body is signature-only: the signature is already
// validated above; codegen supplies the implementation via the
// intercept registry. The body is `Term::Intrinsic` directly (a
// top-level intrinsic fn) or a `Term::Lam` whose inner body is
// `Term::Intrinsic` (a synthesised instance method). Skip body
// inference, but first gate the honesty rule: an intrinsic body is
// legal only in a kernel-tier module or the prelude.
if is_intrinsic_body(f) {
if !env.current_module_kernel_tier {
return Err(CheckError::IntrinsicOutsideKernelTier {
def: f.name.clone(),
module: env.current_module.clone(),
});
}
return Ok(());
}
// Embedding-ABI gate (M1 scalars / M2 ctx / M3 record): an
// `(export …)` fn is the C call boundary. Its signature must be
// C-ABI-permitted — `Int`/`Float`, or a single-constructor
@@ -3000,6 +3042,7 @@ pub fn verify_tail_positions(t: &Term, is_tail: bool) -> Result<()> {
}
Ok(())
}
Term::Intrinsic => Ok(()),
}
}
@@ -3105,6 +3148,7 @@ fn verify_loop_body(t: &Term, in_loop_tail: bool) -> Result<()> {
}
Ok(())
}
Term::Intrinsic => Ok(()),
}
}
@@ -4073,6 +4117,7 @@ pub(crate) fn synth(
Term::Loop { .. } => "loop",
Term::Recur { .. } => "recur",
Term::New { .. } => "new",
Term::Intrinsic => "intrinsic",
Term::Ctor { .. } | Term::Lam { .. } => unreachable!(),
};
return Err(CheckError::ReuseAsNonAllocatingBody {
@@ -4331,6 +4376,14 @@ pub(crate) fn synth(
}
Ok(substitute_rigids(&ret_ty, &mapping))
}
// An intrinsic body is signature-only: the per-fn body check
// (`check_fn`) validates the signature from `f.ty` and never
// calls `synth` on a `Term::Intrinsic`. Reaching here means the
// skip-guard was bypassed — a checker-internal invariant break.
Term::Intrinsic => unreachable!(
"synth reached Term::Intrinsic; an intrinsic body is signature-only and must be \
skipped by check_fn before synth runs"
),
}
}
@@ -4614,6 +4667,7 @@ pub(crate) fn qualify_workspace_term(
}
}
}
Term::Intrinsic => {}
}
}
@@ -4828,6 +4882,11 @@ pub struct Env {
/// treat self-references (module name == own name) as local globals,
/// without touching the `imports` channel.
pub current_module: String,
/// `true` when the currently checked module is kernel-tier
/// (`module.kernel`) or the prelude. An `(intrinsic)` body is legal
/// only here; the per-fn body check reads this to gate
/// `IntrinsicOutsideKernelTier`. Set alongside `current_module`.
pub current_module_kernel_tier: bool,
/// Rigid type vars in scope (Iter 12a). Populated by `check_fn` when
/// it peels an outer `Forall` from the def's type. Inside the body,
/// these names are legal as `Type::Var { name }` and unify only
+2
View File
@@ -689,6 +689,7 @@ impl<'a> Lifter<'a> {
args: new_args?,
})
}
Term::Intrinsic => Ok(t.clone()),
}
}
@@ -780,6 +781,7 @@ fn contains_any_letrec(m: &Module) -> bool {
NewArg::Value(v) => term_has_letrec(v),
NewArg::Type(_) => false,
}),
Term::Intrinsic => false,
}
}
for def in &m.defs {
+5
View File
@@ -626,6 +626,7 @@ impl<'a> Checker<'a> {
}
}
}
Term::Intrinsic => {}
}
}
@@ -813,6 +814,7 @@ fn make_reuse_as_source_not_bare_var(def: &str, source: &Term, body: &Term) -> D
Term::Loop { .. } => "loop",
Term::Recur { .. } => "recur",
Term::New { .. } => "new",
Term::Intrinsic => "intrinsic",
};
let replacement = term_to_form_a(body);
Diagnostic::error(
@@ -974,6 +976,7 @@ fn any_sub_binder_consumed_for(
NewArg::Value(v) => any_sub_binder_consumed_for(v, pname, uniq, def_name, ctors),
NewArg::Type(_) => false,
}),
Term::Intrinsic => false,
}
}
@@ -1111,6 +1114,7 @@ fn ctor_uses_any_binder(t: &Term, binders: &[String]) -> bool {
NewArg::Value(v) => ctor_uses_any_binder(v, binders),
NewArg::Type(_) => false,
}),
Term::Intrinsic => false,
}
}
@@ -1165,6 +1169,7 @@ fn term_mentions_any_binder(t: &Term, binders: &[String]) -> bool {
NewArg::Value(v) => term_mentions_any_binder(v, binders),
NewArg::Type(_) => false,
}),
Term::Intrinsic => false,
}
}
+13
View File
@@ -692,6 +692,13 @@ pub fn collect_mono_targets(
module_name: &str,
env: &crate::Env,
) -> Result<Vec<MonoTarget>> {
// An `(intrinsic)` body is signature-only — codegen supplies it via
// the intercept registry, so it carries no mono targets. Skip the
// synth re-entry (which would hit synth's `Term::Intrinsic`
// unreachable guard), matching `check_fn`'s body-check skip.
if crate::is_intrinsic_body(f) {
return Ok(Vec::new());
}
// Build the per-def env exactly as `check_fn` does — install
// rigid vars, set the current module, etc. This mirrors
// `crate::check_fn` minus the diagnostic emission.
@@ -1289,6 +1296,7 @@ fn rewrite_mono_calls(
}
}
Term::Lit { .. } => {}
Term::Intrinsic => {}
}
}
@@ -1347,6 +1355,10 @@ pub(crate) fn collect_residuals_ordered(
poly_free_fns: &BTreeSet<String>,
poly_free_fn_ccounts: &BTreeMap<String, usize>,
) -> Result<Vec<Option<MonoTarget>>> {
// Intrinsic bodies carry no mono slots — see `collect_mono_targets`.
if crate::is_intrinsic_body(f) {
return Ok(Vec::new());
}
let (rigids, inner_ty): (Vec<String>, Type) = match &f.ty {
Type::Forall { vars, constraints: _, body } => (vars.clone(), (**body).clone()),
other => (vec![], other.clone()),
@@ -1671,6 +1683,7 @@ fn interleave_slots(
}
}
Term::Lit { .. } => {}
Term::Intrinsic => {}
}
}
@@ -144,6 +144,7 @@ fn walk_term(t: &Term) -> Result<(), CheckError> {
}
Ok(())
}
Term::Intrinsic => Ok(()),
}
}
+1
View File
@@ -278,6 +278,7 @@ impl<'a> Checker<'a> {
}
}
}
Term::Intrinsic => {}
}
}
+1
View File
@@ -358,6 +358,7 @@ impl<'a> Walker<'a> {
}
}
}
Term::Intrinsic => {}
}
}