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:
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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,
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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(()),
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -278,6 +278,7 @@ impl<'a> Checker<'a> {
|
||||
}
|
||||
}
|
||||
}
|
||||
Term::Intrinsic => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -358,6 +358,7 @@ impl<'a> Walker<'a> {
|
||||
}
|
||||
}
|
||||
}
|
||||
Term::Intrinsic => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user