diff --git a/crates/ail/src/main.rs b/crates/ail/src/main.rs index 3f70ae0..2349cbb 100644 --- a/crates/ail/src/main.rs +++ b/crates/ail/src/main.rs @@ -1585,6 +1585,7 @@ fn walk_term( } } } + Term::Intrinsic => {} } } @@ -2956,6 +2957,7 @@ fn rewrite_def( } } } + Term::Intrinsic => {} } } diff --git a/crates/ail/tests/codegen_import_map_fallback_pin.rs b/crates/ail/tests/codegen_import_map_fallback_pin.rs index d701b2e..525fe6f 100644 --- a/crates/ail/tests/codegen_import_map_fallback_pin.rs +++ b/crates/ail/tests/codegen_import_map_fallback_pin.rs @@ -106,6 +106,7 @@ fn synthesised_print_uses_user_module_show_via_fallback() { ailang_core::ast::NewArg::Type(_) => false, }), Term::Lit { .. } => false, + Term::Intrinsic => false, } } diff --git a/crates/ail/tests/e2e.rs b/crates/ail/tests/e2e.rs index 4a1fe94..3f84c3c 100644 --- a/crates/ail/tests/e2e.rs +++ b/crates/ail/tests/e2e.rs @@ -82,6 +82,19 @@ fn sum_1_to_10_is_55() { assert_eq!(stdout.trim(), "55"); } +/// Property protected: a kernel-tier `(intrinsic)` fn builds and runs +/// from source to native via the codegen intercept registry. The +/// `kernel_stub.answer` intrinsic (body `Term::Intrinsic`, intercept +/// `emit_answer` → `ret i64 42`) is consumed by a user module and its +/// value reaches stdout. This is the end-to-end ratifier for the whole +/// Term::Intrinsic mechanism: parse → signature-only typecheck → +/// mono-skip → intercept-routed codegen → native 42. +#[test] +fn answer_intrinsic_builds_and_runs_printing_42() { + let out = build_and_run("kernel_answer.ail"); + assert_eq!(out.trim(), "42"); +} + #[test] fn loop_recur_sum_to_runs_to_value() { let stdout = build_and_run("loop_sum_to_run.ail"); @@ -1259,6 +1272,53 @@ fn check_json_unbound_var() { ); } +/// Property protected: an `(intrinsic)` body is rejected in a +/// non-kernel-tier user module. `check_fn`'s honesty-rule guard fires +/// `intrinsic-outside-kernel-tier` (Error) when a module that is +/// neither kernel-tier nor the prelude declares a compiler-supplied +/// body. Without the guard, user code could claim the compiler +/// supplies a body it does not. The reject rides the subprocess +/// `ail check --json` path so the diagnostic code is asserted as the +/// machine consumer would see it; a temp file keeps the reject case +/// out of the round-trip corpus. +#[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}" + ); +} + /// literal patterns at top level and inside Ctor /// sub-patterns. Property protected: the 16a desugar pass rewrites /// every `Pattern::Lit` to a `Term::If` against the scrutinee/field diff --git a/crates/ail/tests/snapshots/hello.ll b/crates/ail/tests/snapshots/hello.ll index 1bcd965..f35c335 100644 --- a/crates/ail/tests/snapshots/hello.ll +++ b/crates/ail/tests/snapshots/hello.ll @@ -26,6 +26,7 @@ declare i64 @llvm.fptosi.sat.i64.f64(double) @ail_hello_main_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_hello_main_adapter, ptr null } @ail_kernel_stub_new_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_kernel_stub_new_adapter, ptr null } +@ail_kernel_stub_answer_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_kernel_stub_answer_adapter, ptr null } @ail_prelude_float_eq_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_prelude_float_eq_adapter, ptr null } @ail_prelude_float_ne_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_prelude_float_ne_adapter, ptr null } @ail_prelude_float_lt_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_prelude_float_lt_adapter, ptr null } @@ -61,6 +62,17 @@ entry: ret ptr %r } +define i64 @ail_kernel_stub_answer() { +entry: + ret i64 42 +} + +define i64 @ail_kernel_stub_answer_adapter(ptr %_env) { +entry: + %r = call i64 @ail_kernel_stub_answer() + ret i64 %r +} + define void @drop_kernel_stub_StubT(ptr %p) { entry: %is_null = icmp eq ptr %p, null diff --git a/crates/ail/tests/snapshots/list.ll b/crates/ail/tests/snapshots/list.ll index 2cccf45..e5ffd73 100644 --- a/crates/ail/tests/snapshots/list.ll +++ b/crates/ail/tests/snapshots/list.ll @@ -23,6 +23,7 @@ declare ptr @ailang_str_concat(ptr, ptr) declare i64 @llvm.fptosi.sat.i64.f64(double) @ail_kernel_stub_new_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_kernel_stub_new_adapter, ptr null } +@ail_kernel_stub_answer_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_kernel_stub_answer_adapter, ptr null } @ail_list_sum_list_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_list_sum_list_adapter, ptr null } @ail_list_main_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_list_main_adapter, ptr null } @ail_prelude_float_eq_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_prelude_float_eq_adapter, ptr null } @@ -48,6 +49,17 @@ entry: ret ptr %r } +define i64 @ail_kernel_stub_answer() { +entry: + ret i64 42 +} + +define i64 @ail_kernel_stub_answer_adapter(ptr %_env) { +entry: + %r = call i64 @ail_kernel_stub_answer() + ret i64 %r +} + define void @drop_kernel_stub_StubT(ptr %p) { entry: %is_null = icmp eq ptr %p, null diff --git a/crates/ail/tests/snapshots/max3.ll b/crates/ail/tests/snapshots/max3.ll index f1fffca..558805f 100644 --- a/crates/ail/tests/snapshots/max3.ll +++ b/crates/ail/tests/snapshots/max3.ll @@ -23,6 +23,7 @@ declare ptr @ailang_str_concat(ptr, ptr) declare i64 @llvm.fptosi.sat.i64.f64(double) @ail_kernel_stub_new_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_kernel_stub_new_adapter, ptr null } +@ail_kernel_stub_answer_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_kernel_stub_answer_adapter, ptr null } @ail_max3_max_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_max3_max_adapter, ptr null } @ail_max3_max3_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_max3_max3_adapter, ptr null } @ail_max3_main_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_max3_main_adapter, ptr null } @@ -51,6 +52,17 @@ entry: ret ptr %r } +define i64 @ail_kernel_stub_answer() { +entry: + ret i64 42 +} + +define i64 @ail_kernel_stub_answer_adapter(ptr %_env) { +entry: + %r = call i64 @ail_kernel_stub_answer() + ret i64 %r +} + define void @drop_kernel_stub_StubT(ptr %p) { entry: %is_null = icmp eq ptr %p, null diff --git a/crates/ail/tests/snapshots/sum.ll b/crates/ail/tests/snapshots/sum.ll index 19a0eea..2007cde 100644 --- a/crates/ail/tests/snapshots/sum.ll +++ b/crates/ail/tests/snapshots/sum.ll @@ -23,6 +23,7 @@ declare ptr @ailang_str_concat(ptr, ptr) declare i64 @llvm.fptosi.sat.i64.f64(double) @ail_kernel_stub_new_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_kernel_stub_new_adapter, ptr null } +@ail_kernel_stub_answer_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_kernel_stub_answer_adapter, ptr null } @ail_prelude_float_eq_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_prelude_float_eq_adapter, ptr null } @ail_prelude_float_ne_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_prelude_float_ne_adapter, ptr null } @ail_prelude_float_lt_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_prelude_float_lt_adapter, ptr null } @@ -49,6 +50,17 @@ entry: ret ptr %r } +define i64 @ail_kernel_stub_answer() { +entry: + ret i64 42 +} + +define i64 @ail_kernel_stub_answer_adapter(ptr %_env) { +entry: + %r = call i64 @ail_kernel_stub_answer() + ret i64 %r +} + define void @drop_kernel_stub_StubT(ptr %p) { entry: %is_null = icmp eq ptr %p, null diff --git a/crates/ail/tests/snapshots/ws_main.ll b/crates/ail/tests/snapshots/ws_main.ll index 931c9db..6fb851c 100644 --- a/crates/ail/tests/snapshots/ws_main.ll +++ b/crates/ail/tests/snapshots/ws_main.ll @@ -23,6 +23,7 @@ declare ptr @ailang_str_concat(ptr, ptr) declare i64 @llvm.fptosi.sat.i64.f64(double) @ail_kernel_stub_new_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_kernel_stub_new_adapter, ptr null } +@ail_kernel_stub_answer_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_kernel_stub_answer_adapter, ptr null } @ail_prelude_float_eq_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_prelude_float_eq_adapter, ptr null } @ail_prelude_float_ne_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_prelude_float_ne_adapter, ptr null } @ail_prelude_float_lt_clos = private unnamed_addr constant { ptr, ptr } { ptr @ail_prelude_float_lt_adapter, ptr null } @@ -48,6 +49,17 @@ entry: ret ptr %r } +define i64 @ail_kernel_stub_answer() { +entry: + ret i64 42 +} + +define i64 @ail_kernel_stub_answer_adapter(ptr %_env) { +entry: + %r = call i64 @ail_kernel_stub_answer() + ret i64 %r +} + define void @drop_kernel_stub_StubT(ptr %p) { entry: %is_null = icmp eq ptr %p, null diff --git a/crates/ailang-check/src/lib.rs b/crates/ailang-check/src/lib.rs index 4eaeeb6..79489b9 100644 --- a/crates/ailang-check/src/lib.rs +++ b/crates/ailang-check/src/lib.rs @@ -270,6 +270,7 @@ pub(crate) fn substitute_rigids_in_term(t: &Term, mapping: &BTreeMap t.clone(), } } @@ -755,6 +756,14 @@ pub enum CheckError { allowed: Vec, }, + /// 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) -> 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) -> 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 diff --git a/crates/ailang-check/src/lift.rs b/crates/ailang-check/src/lift.rs index 6109f54..90dbbcd 100644 --- a/crates/ailang-check/src/lift.rs +++ b/crates/ailang-check/src/lift.rs @@ -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 { diff --git a/crates/ailang-check/src/linearity.rs b/crates/ailang-check/src/linearity.rs index 2515c72..2ac1e65 100644 --- a/crates/ailang-check/src/linearity.rs +++ b/crates/ailang-check/src/linearity.rs @@ -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, } } diff --git a/crates/ailang-check/src/mono.rs b/crates/ailang-check/src/mono.rs index 1cd6bec..8901af2 100644 --- a/crates/ailang-check/src/mono.rs +++ b/crates/ailang-check/src/mono.rs @@ -692,6 +692,13 @@ pub fn collect_mono_targets( module_name: &str, env: &crate::Env, ) -> Result> { + // 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, poly_free_fn_ccounts: &BTreeMap, ) -> Result>> { + // 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, 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 => {} } } diff --git a/crates/ailang-check/src/pre_desugar_validation.rs b/crates/ailang-check/src/pre_desugar_validation.rs index bb741e7..b77a2de 100644 --- a/crates/ailang-check/src/pre_desugar_validation.rs +++ b/crates/ailang-check/src/pre_desugar_validation.rs @@ -144,6 +144,7 @@ fn walk_term(t: &Term) -> Result<(), CheckError> { } Ok(()) } + Term::Intrinsic => Ok(()), } } diff --git a/crates/ailang-check/src/reuse_shape.rs b/crates/ailang-check/src/reuse_shape.rs index e08415b..664ebbf 100644 --- a/crates/ailang-check/src/reuse_shape.rs +++ b/crates/ailang-check/src/reuse_shape.rs @@ -278,6 +278,7 @@ impl<'a> Checker<'a> { } } } + Term::Intrinsic => {} } } diff --git a/crates/ailang-check/src/uniqueness.rs b/crates/ailang-check/src/uniqueness.rs index 0ed7590..a0aa817 100644 --- a/crates/ailang-check/src/uniqueness.rs +++ b/crates/ailang-check/src/uniqueness.rs @@ -358,6 +358,7 @@ impl<'a> Walker<'a> { } } } + Term::Intrinsic => {} } } diff --git a/crates/ailang-codegen/src/escape.rs b/crates/ailang-codegen/src/escape.rs index b4db451..d098230 100644 --- a/crates/ailang-codegen/src/escape.rs +++ b/crates/ailang-codegen/src/escape.rs @@ -213,6 +213,7 @@ fn walk(t: &Term, out: &mut NonEscapeSet) { } } Term::Lit { .. } | Term::Var { .. } => {} + Term::Intrinsic => {} } } @@ -420,6 +421,7 @@ fn escapes(t: &Term, tainted: &BTreeSet, in_tail: bool) -> bool { } false } + Term::Intrinsic => false, } } @@ -552,6 +554,7 @@ fn collect_free_vars(t: &Term, bound: &mut BTreeSet, out: &mut BTreeSet< } } } + Term::Intrinsic => {} } } diff --git a/crates/ailang-codegen/src/intercepts.rs b/crates/ailang-codegen/src/intercepts.rs index b26cba5..2041b02 100644 --- a/crates/ailang-codegen/src/intercepts.rs +++ b/crates/ailang-codegen/src/intercepts.rs @@ -164,6 +164,13 @@ pub(crate) static INTERCEPTS: &[Intercept] = &[ wants_alwaysinline: true, emit: emit_ne_int, }, + Intercept { + name: "answer", + expected_params: &[], + expected_ret: "i64", + wants_alwaysinline: false, + emit: emit_answer, + }, ]; pub(crate) fn lookup(name: &str) -> Option<&'static Intercept> { @@ -440,6 +447,17 @@ pub(crate) fn emit_ne_int(emitter: &mut Emitter<'_>) -> Result<()> { .map(|_| ()) } +/// 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. +pub(crate) fn emit_answer(emitter: &mut Emitter<'_>) -> Result<()> { + emitter.body.push_str(" ret i64 42\n"); + emitter.body.push_str("}\n\n"); + emitter.block_terminated = true; + Ok(()) +} + #[cfg(test)] mod tests { use super::lookup; diff --git a/crates/ailang-codegen/src/lambda.rs b/crates/ailang-codegen/src/lambda.rs index ebc7800..f0f0824 100644 --- a/crates/ailang-codegen/src/lambda.rs +++ b/crates/ailang-codegen/src/lambda.rs @@ -502,6 +502,7 @@ impl<'a> Emitter<'a> { } } } + Term::Intrinsic => {} } } diff --git a/crates/ailang-codegen/src/lib.rs b/crates/ailang-codegen/src/lib.rs index 7aeab03..18cccfb 100644 --- a/crates/ailang-codegen/src/lib.rs +++ b/crates/ailang-codegen/src/lib.rs @@ -1320,6 +1320,21 @@ impl<'a> Emitter<'a> { let body_was_intercepted = self.try_emit_primitive_instance_body(&f.name, &llvm_param_tys, &llvm_ret)?; + // An `(intrinsic)` body is supplied entirely by the intercept + // registry. `try_emit_primitive_instance_body` already consults + // `intercepts::lookup(&f.name)`; if it fired, the body is fully + // emitted. If it did NOT fire, the intrinsic has no registered + // implementation — surface that as an internal error rather than + // falling through to `lower_term`, which cannot lower a + // `Term::Intrinsic` (it is signature-only, not an expression). + if !body_was_intercepted && matches!(f.body, Term::Intrinsic) { + return Err(CodegenError::Internal(format!( + "intrinsic fn `{}` has no registered intercept; an `(intrinsic)` body must \ + resolve through the intercept registry", + f.name + ))); + } + if !body_was_intercepted { let (val, val_ty) = self.lower_term(&f.body)?; if !self.block_terminated { @@ -2076,6 +2091,11 @@ impl<'a> Emitter<'a> { "Term::New requires the type's `new` def to be desugared first; \ codegen does not yet support Term::New directly — milestone raw-buf".into(), )), + 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(), + )), } } @@ -3273,6 +3293,11 @@ impl<'a> Emitter<'a> { "Term::New cannot be synthed at codegen — must be desugared first; \ codegen support lands in the raw-buf milestone".into(), )), + Term::Intrinsic => Err(CodegenError::Internal( + "Term::Intrinsic cannot be synthed at codegen — an intrinsic body is \ + signature-only and routed through the intercept registry, never an expression" + .into(), + )), } } } diff --git a/crates/ailang-core/specs/form_a.md b/crates/ailang-core/specs/form_a.md index 22f073b..c09db3f 100644 --- a/crates/ailang-core/specs/form_a.md +++ b/crates/ailang-core/specs/form_a.md @@ -297,6 +297,7 @@ Parenthesised forms: (loop (NAME TYPE INIT)* BODY-TERM+) ; strict iteration block (loop-recur) (recur ARG*) ; re-enter enclosing loop (loop-recur) (new TYPE-NAME ARG+) ; functional construction (kernel-extension prep.2) +(intrinsic) ; compiler-supplied body (kernel-tier only; fills a fn/lam body slot) ``` Notes: @@ -337,6 +338,12 @@ Notes: `new-type-not-constructible` (T's home module has no `new` def), `new-arg-kind-mismatch` (arg's kind disagrees with the sig at that position). Codegen lands in the raw-buf milestone. +- `(intrinsic)` fills the single body slot of a `fn` def (in place of + `(body TERM)`) or a `lam`. It marks the definition as + compiler-supplied: the typechecker validates only the signature and + codegen routes the body through the intercept registry. It is legal + only in a `(kernel)`-tier module or the prelude — elsewhere it is + rejected with `intrinsic-outside-kernel-tier`. ## Patterns diff --git a/crates/ailang-core/src/ast.rs b/crates/ailang-core/src/ast.rs index c679dab..5eabb5c 100644 --- a/crates/ailang-core/src/ast.rs +++ b/crates/ailang-core/src/ast.rs @@ -604,6 +604,18 @@ pub enum Term { type_name: String, args: Vec, }, + /// 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, } /// One positional arg to a `(new T args...)` call. Either a `Type` diff --git a/crates/ailang-core/src/desugar.rs b/crates/ailang-core/src/desugar.rs index f4e0dbb..7c70642 100644 --- a/crates/ailang-core/src/desugar.rs +++ b/crates/ailang-core/src/desugar.rs @@ -366,6 +366,7 @@ fn collect_used_in_term(t: &Term, used: &mut BTreeSet) { } } } + Term::Intrinsic => {} } } @@ -932,6 +933,7 @@ impl Desugarer { }) .collect(), }, + Term::Intrinsic => t.clone(), } } @@ -1250,6 +1252,7 @@ pub fn free_vars_in_term(t: &Term, bound: &BTreeSet, out: &mut BTreeSet< } } } + Term::Intrinsic => {} } } @@ -1440,6 +1443,7 @@ pub fn subst_var(t: &Term, from: &str, to: &str) -> Term { }) .collect(), }, + Term::Intrinsic => t.clone(), } } @@ -1590,6 +1594,7 @@ pub fn subst_call_with_extras(t: &Term, name: &str, lifted: &str, extras: &[Stri }) .collect(), }, + Term::Intrinsic => t.clone(), } } @@ -1655,6 +1660,7 @@ pub fn find_non_callee_use(t: &Term, name: &str) -> Option { NewArg::Value(v) => find_non_callee_use(v, name), NewArg::Type(_) => None, }), + Term::Intrinsic => None, } } @@ -1705,6 +1711,7 @@ mod tests { NewArg::Value(v) => any_nested_ctor(v), NewArg::Type(_) => false, }), + Term::Intrinsic => false, } } @@ -1738,6 +1745,7 @@ mod tests { NewArg::Value(v) => any_let_rec(v), NewArg::Type(_) => false, }), + Term::Intrinsic => false, } } @@ -2893,6 +2901,7 @@ mod tests { NewArg::Value(v) => any_lit_pattern(v), NewArg::Type(_) => false, }), + Term::Intrinsic => false, } } diff --git a/crates/ailang-core/src/workspace.rs b/crates/ailang-core/src/workspace.rs index 46393d9..ef65886 100644 --- a/crates/ailang-core/src/workspace.rs +++ b/crates/ailang-core/src/workspace.rs @@ -1291,6 +1291,7 @@ where } Ok(()) } + Term::Intrinsic => Ok(()), } } @@ -1434,6 +1435,7 @@ where } Ok(()) } + Term::Intrinsic => Ok(()), } } diff --git a/crates/ailang-core/tests/design_schema_drift.rs b/crates/ailang-core/tests/design_schema_drift.rs index 15ebc2d..95c9f9d 100644 --- a/crates/ailang-core/tests/design_schema_drift.rs +++ b/crates/ailang-core/tests/design_schema_drift.rs @@ -167,6 +167,7 @@ fn design_md_anchors_every_term_variant() { args: vec![], }, ), + (r#""t": "intrinsic""#, Term::Intrinsic), ]; for (anchor, term) in exemplars { @@ -189,6 +190,7 @@ fn design_md_anchors_every_term_variant() { Term::Loop { .. } => "loop", Term::Recur { .. } => "recur", Term::New { .. } => "new", + Term::Intrinsic => "intrinsic", }; assert!( anchor_in_jsonc_block(DATA_MODEL, anchor), diff --git a/crates/ailang-core/tests/schema_coverage.rs b/crates/ailang-core/tests/schema_coverage.rs index d6ed943..6ac314d 100644 --- a/crates/ailang-core/tests/schema_coverage.rs +++ b/crates/ailang-core/tests/schema_coverage.rs @@ -49,6 +49,7 @@ enum VariantTag { TermReuseAs, TermLoop, TermRecur, + TermIntrinsic, // Pattern PatternWild, PatternVar, @@ -96,6 +97,7 @@ const EXPECTED_VARIANTS: &[VariantTag] = &[ VariantTag::TermReuseAs, VariantTag::TermLoop, VariantTag::TermRecur, + VariantTag::TermIntrinsic, VariantTag::PatternWild, VariantTag::PatternVar, VariantTag::PatternLit, @@ -261,6 +263,9 @@ fn visit_term(t: &Term, observed: &mut HashSet) { } } } + Term::Intrinsic => { + observed.insert(VariantTag::TermIntrinsic); + } } } diff --git a/crates/ailang-core/tests/spec_drift.rs b/crates/ailang-core/tests/spec_drift.rs index 528aa02..fbe0085 100644 --- a/crates/ailang-core/tests/spec_drift.rs +++ b/crates/ailang-core/tests/spec_drift.rs @@ -136,6 +136,7 @@ fn spec_mentions_every_term_variant() { })], }, ), + ("(intrinsic", Term::Intrinsic), ]; for (anchor, term) in exemplars { @@ -160,6 +161,7 @@ fn spec_mentions_every_term_variant() { Term::Loop { .. } => "loop", Term::Recur { .. } => "recur", Term::New { .. } => "new", + Term::Intrinsic => "intrinsic", }; assert!( FORM_A_SPEC.contains(anchor), diff --git a/crates/ailang-kernel-stub/src/lib.rs b/crates/ailang-kernel-stub/src/lib.rs index ff49bec..514098a 100644 --- a/crates/ailang-kernel-stub/src/lib.rs +++ b/crates/ailang-kernel-stub/src/lib.rs @@ -33,5 +33,10 @@ pub const STUB_AIL: &str = r#"(module kernel_stub (doc "Construct StubT from an Int. Exercises Term::New end-to-end.") (type (fn-type (params (con Int)) (ret (con StubT (con Int))))) (params x) - (body (term-ctor StubT Stub x)))) + (body (term-ctor StubT Stub x))) + (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))) "#; diff --git a/crates/ailang-prose/src/lib.rs b/crates/ailang-prose/src/lib.rs index adc0297..2c3f19a 100644 --- a/crates/ailang-prose/src/lib.rs +++ b/crates/ailang-prose/src/lib.rs @@ -954,6 +954,7 @@ fn write_term_prec(out: &mut String, t: &Term, level: usize, parent_prec: u8, ow } out.push(')'); } + Term::Intrinsic => out.push_str("compiler-supplied (intrinsic)"), } } @@ -1154,6 +1155,7 @@ fn count_free_var(name: &str, t: &Term) -> usize { NewArg::Type(_) => 0, }) .sum(), + Term::Intrinsic => 0, } } @@ -1314,6 +1316,7 @@ fn subst_var_with_term(t: &Term, name: &str, replacement: &Term) -> Term { }) .collect(), }, + Term::Intrinsic => t.clone(), } } diff --git a/crates/ailang-surface/src/parse.rs b/crates/ailang-surface/src/parse.rs index 87f31a4..cebe091 100644 --- a/crates/ailang-surface/src/parse.rs +++ b/crates/ailang-surface/src/parse.rs @@ -614,12 +614,18 @@ impl<'a> Parser<'a> { } body = Some(self.parse_body_attr()?); } + Some("intrinsic") => { + if body.is_some() { + return Err(self.duplicate_clause_err("fn-def", &format!("fn `{name}`"), "body")); + } + body = Some(self.parse_intrinsic_attr()?); + } Some(other) => { let pos = self.peek().map(|t| t.span.start).unwrap_or(0); return Err(ParseError::Production { production: "fn-def", message: format!( - "unknown fn attribute `{other}`; expected `doc`, `export`, `suppress`, `type`, `params`, or `body`" + "unknown fn attribute `{other}`; expected `doc`, `export`, `suppress`, `type`, `params`, `body`, or `intrinsic`" ), pos, }); @@ -640,7 +646,7 @@ impl<'a> Parser<'a> { })?; let body = body.ok_or_else(|| ParseError::Production { production: "fn-def", - message: format!("fn `{name}` is missing required `(body ...)` attribute"), + message: format!("fn `{name}` is missing required `(body ...)` or `(intrinsic)` attribute"), pos: 0, })?; Ok(FnDef { @@ -767,6 +773,17 @@ impl<'a> Parser<'a> { Ok(t) } + /// Parse an `(intrinsic)` body clause into [`Term::Intrinsic`]. It + /// fills the same single body slot a `(body ...)` clause fills; a + /// def carrying it is compiler-supplied (signature-only at + /// typecheck, routed through the intercept registry at codegen). + fn parse_intrinsic_attr(&mut self) -> Result { + self.expect_lparen("intrinsic-attr")?; + self.expect_keyword("intrinsic")?; + self.expect_rparen("intrinsic-attr")?; + Ok(Term::Intrinsic) + } + // ---- const def ------------------------------------------------------ fn parse_const(&mut self) -> Result { @@ -1526,8 +1543,12 @@ impl<'a> Parser<'a> { if let Some("effects") = self.peek_head_ident() { effects = self.parse_effects_clause()?; } - // (body term) - let body = self.parse_body_attr()?; + // (body term) — or (intrinsic) for a compiler-supplied body + let body = if let Some("intrinsic") = self.peek_head_ident() { + self.parse_intrinsic_attr()? + } else { + self.parse_body_attr()? + }; self.expect_rparen("lam-term")?; Ok(Term::Lam { params, diff --git a/crates/ailang-surface/src/print.rs b/crates/ailang-surface/src/print.rs index 6d8aa58..0c57fa9 100644 --- a/crates/ailang-surface/src/print.rs +++ b/crates/ailang-surface/src/print.rs @@ -209,9 +209,13 @@ fn write_fn_def(out: &mut String, fd: &FnDef, level: usize) { out.push(')'); out.push('\n'); indent(out, level + 1); - out.push_str("(body "); - write_term(out, &fd.body, level + 2); - out.push(')'); + if matches!(fd.body, Term::Intrinsic) { + out.push_str("(intrinsic)"); + } else { + out.push_str("(body "); + write_term(out, &fd.body, level + 2); + out.push(')'); + } out.push(')'); } @@ -549,9 +553,13 @@ fn write_term(out: &mut String, t: &Term, level: usize) { } out.push(')'); } - out.push_str(" (body "); - write_term(out, body, level); - out.push_str("))"); + if matches!(**body, Term::Intrinsic) { + out.push_str(" (intrinsic))"); + } else { + out.push_str(" (body "); + write_term(out, body, level); + out.push_str("))"); + } } Term::Seq { lhs, rhs } => { out.push_str("(seq "); @@ -636,6 +644,11 @@ fn write_term(out: &mut String, t: &Term, level: usize) { } out.push(')'); } + // A general term-printer only reaches Term::Intrinsic via a + // fn/lam body slot, which the fn-def and lam printers above + // already special-case; emitting `(intrinsic)` here keeps the + // exhaustive match correct. + Term::Intrinsic => out.push_str("(intrinsic)"), } } diff --git a/design/contracts/0002-data-model.md b/design/contracts/0002-data-model.md index ce85248..2e3083e 100644 --- a/design/contracts/0002-data-model.md +++ b/design/contracts/0002-data-model.md @@ -191,6 +191,14 @@ narrative — defaults, superclasses, diagnostics — lives in { "t": "new", "type": "", "args": [ NewArg, ... ] } + +// 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" } ``` **`NewArg`** (one positional arg to a `(new T args...)` call): @@ -209,7 +217,14 @@ narrative — defaults, superclasses, diagnostics — lives in In the MVP, `do` is only a direct call to a built-in effect op (no handler); the effect system is described in [effects](../models/0002-effects.md). A `lam` term constructs an anonymous function value; free -variables of its body are captured from the enclosing scope. +variables of its body are captured from the enclosing scope. A `lam` +body may be `{ "t": "intrinsic" }`, in which case the lambda is +compiler-supplied (the synthesised-instance-method form). + +A `fn` def's `body` may be `{ "t": "intrinsic" }`, in which case the +def is compiler-supplied: the typechecker validates only its signature +and codegen routes it through the intercept registry. Such a body is +legal only in a `(kernel)`-tier module or the prelude. Loop binders are alloca-resident: typecheck binds them in the ordinary local scope plus a positional `loop_stack`, and codegen diff --git a/examples/kernel_answer.ail b/examples/kernel_answer.ail new file mode 100644 index 0000000..fbda3f1 --- /dev/null +++ b/examples/kernel_answer.ail @@ -0,0 +1,6 @@ +(module kernel_answer + (fn main + (doc "E2E ratifier: prints the intrinsic answer (42).") + (type (fn-type (params) (ret (con Unit)) (effects IO))) + (params) + (body (app print (app kernel_stub.answer))))) diff --git a/examples/kernel_intrinsic_smoke.ail b/examples/kernel_intrinsic_smoke.ail new file mode 100644 index 0000000..608ac08 --- /dev/null +++ b/examples/kernel_intrinsic_smoke.ail @@ -0,0 +1,7 @@ +(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)))