From fd37fa64894a29ff838cb19b7c9137e120ec8225 Mon Sep 17 00:00:00 2001 From: Brummel Date: Fri, 29 May 2026 19:39:35 +0200 Subject: [PATCH] iter raw-buf.3 typescoped-intrinsic-mechanism (DONE 2/2): scope-qualified mono symbols + bijection expansion (refs #7) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit New language mechanism: a type-scoped polymorphic top-level (intrinsic) op now mints a scope-qualified mono symbol (StubT_peek__Int, not the colliding bare peek__Int), and the intrinsic<->intercept bijection expresses one such polymorphic marker as its N per-param-in-element entries. Ratified standalone on the stub via a new StubT.peek op; no RawBuf, no Term::New, no codegen E2E this iteration (RawBuf consumes the mechanism in raw-buf.4). Task 1 — scope threading. Added scope: Option to FreeFnCall + MonoTarget::FreeFn + the synth free_fn_owner tuple. Populated Some(prefix) ONLY on the type-scoped T.f resolution branch (lib.rs:3538, gated on type_home.is_some()); None on every local / same-module / implicit-import branch, so existing bare-resolved symbols stay byte-identical. Consumed via a scoped_base helper at both mono-symbol mint sites (synthesise + rewrite, which read the same MonoTarget → automatically in lockstep) and folded into mono_target_key so a scoped op and a bare op of the same name never dedup-collide. Task 2 — ratifier + bijection. Added StubT.peek to the stub source; extended workspace_intrinsic_markers with a type-scoped-poly arm that finds the unique same-module TypeDef referenced in the fn signature and expands over its param-in set, building the T_f__ strings via the SAME mono_symbol_n on Type::Con elems so collector and mint agree byte-for-byte; registered StubT_peek__Int/Float entries + emit fns; extended kernel_stub_module_round_trips to cover peek; added mono_scoped_symbol integration test (own-param calls, no Term::New, no codegen — asserts both scoped symbols minted and bare peek__Int absent). Latent bug fixed inline (regression caught at the full-suite gate, RED via the existing escape_local_demo E2E). kernel_stub is implicitly imported, so poly_free_fn_names_for_module unconditionally added peek's bare name to every consumer; a consumer with its OWN monomorphic peek then had its local call mistreated as a poly-free-fn site, over-advancing the mono slot cursor and misaligning a print target. Fix: suppress the bare implicit-import name when the current module declares a same-name global, mirroring synth's resolution ladder (same-module global beats implicit-import). Applied in lockstep to both poly_free_fn name + and constraint-count maps; dot-qualified / type-scoped spellings stay unconditional. This was a pre-existing inconsistency between mono's name-set and synth's resolution that peek (first implicitly-imported poly free fn colliding with a fixture-local name) exposed. Plan deviations (all sound): collect_con_heads omits Borrow/Own recursion (no such Type variant — borrow/own are ParamMode on Type::Fn, verified ast.rs:752); scope bound via the existing synthesise_mono_fn_for_free_fn MonoTarget destructure rather than a new param; the mono-pin fixture uses own params (borrow + peek-returns-a trips consume-while-borrowed; peek is never instantiated so the mode is immaterial to the scope-symbol ratification). Verification (orchestrator, this session): cargo build --workspace clean; cargo test --workspace 670 passed / 0 failed / 2 ignored (669 baseline + the one new mono test). Bijection green (1 marker -> 2 entries), round-trip green, escape_local_demo green. .ll snapshots unchanged (peek polymorphic -> never instantiated without a call site). Existing eq__*/ compare__*/float_* symbols byte-identical (mono_hash_stability green). --- crates/ail/tests/mono_scoped_symbol.rs | 49 ++++++ crates/ail/tests/mono_unification.rs | 2 + crates/ailang-check/src/lib.rs | 30 +++- crates/ailang-check/src/mono.rs | 67 +++++++-- crates/ailang-codegen/src/intercepts.rs | 140 +++++++++++++++++- .../ailang-core/tests/design_schema_drift.rs | 8 + .../ailang-kernel/src/kernel_stub/source.ail | 5 + examples/peek_mono_pin_smoke.ail | 9 ++ 8 files changed, 292 insertions(+), 18 deletions(-) create mode 100644 crates/ail/tests/mono_scoped_symbol.rs create mode 100644 examples/peek_mono_pin_smoke.ail diff --git a/crates/ail/tests/mono_scoped_symbol.rs b/crates/ail/tests/mono_scoped_symbol.rs new file mode 100644 index 0000000..3723001 --- /dev/null +++ b/crates/ail/tests/mono_scoped_symbol.rs @@ -0,0 +1,49 @@ +//! raw-buf.3: a type-scoped polymorphic intrinsic call `StubT.peek` +//! monomorphises to the scope-qualified symbol `StubT_peek__` +//! (not the bare `peek__`). Proves the scope threads +//! resolution → FreeFnCall → MonoTarget → mint. No codegen / no +//! Term::New: assertion is on the monomorphised def names. + +use ailang_core::ast::Def; +use ailang_surface::load_workspace; +use std::path::PathBuf; + +fn fixture_path() -> PathBuf { + let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR")); + d.pop(); + d.pop(); + d.join("examples").join("peek_mono_pin_smoke.ail") +} + +#[test] +fn stubt_peek_monomorphises_to_scope_qualified_symbols() { + let ws = load_workspace(&fixture_path()).expect("workspace loads"); + let diags = ailang_check::check_workspace(&ws); + assert!(diags.is_empty(), "typecheck diagnostics: {diags:?}"); + let post_mono = ailang_check::monomorphise_workspace(&ws).expect("mono green"); + + // Free-fn mono defs are appended to the polymorphic source's + // defining module — `kernel_stub`, where `peek` lives. + let names: Vec = post_mono + .modules + .values() + .flat_map(|m| m.defs.iter()) + .filter_map(|d| match d { + Def::Fn(f) => Some(f.name.clone()), + _ => None, + }) + .collect(); + + assert!( + names.iter().any(|n| n == "StubT_peek__Int"), + "expected scope-qualified StubT_peek__Int; got: {names:?}" + ); + assert!( + names.iter().any(|n| n == "StubT_peek__Float"), + "expected scope-qualified StubT_peek__Float; got: {names:?}" + ); + assert!( + !names.iter().any(|n| n == "peek__Int"), + "bare peek__Int must NOT be minted (scope-qualified only)" + ); +} diff --git a/crates/ail/tests/mono_unification.rs b/crates/ail/tests/mono_unification.rs index ec64480..0a3b14a 100644 --- a/crates/ail/tests/mono_unification.rs +++ b/crates/ail/tests/mono_unification.rs @@ -179,6 +179,7 @@ fn synthesise_mono_fn_free_fn_arm_substitutes_rigid_vars() { name: "cmp_max".into(), type_args: vec![int_ty.clone()], defining_module: "cmp_max_smoke".into(), + scope: None, }; let synth = synthesise_mono_fn_for_free_fn(&target, &ws) .expect("synthesis succeeds"); @@ -212,6 +213,7 @@ fn mono_target_free_fn_variant_keys_distinctly_from_class_method() { name: "cmp_max".into(), type_args: vec![int_ty.clone()], defining_module: "prelude".into(), + scope: None, }; assert_ne!( mono_target_key(&class_tgt), diff --git a/crates/ailang-check/src/lib.rs b/crates/ailang-check/src/lib.rs index 79489b9..77703ab 100644 --- a/crates/ailang-check/src/lib.rs +++ b/crates/ailang-check/src/lib.rs @@ -2753,6 +2753,13 @@ pub struct FreeFnCall { /// yields the concrete type for `forall_vars[i]` iff the call site's /// substitution was fully pinned. pub metas: Vec, + /// The TypeDef scope the call resolved through, when it was + /// spelled type-scoped (`T.f`, prep.1) — `Some("StubT")` for + /// `StubT.peek`, `Some("RawBuf")` for `RawBuf.get`. `None` for a + /// bare / dot-qualified / local resolution. raw-buf.3: drives the + /// scope-qualified mono symbol `T_f__` so two types' ops + /// of the same name never collide. + pub scope: Option, } /// lift a canonical-form class-ref value to the qualified @@ -3307,7 +3314,7 @@ pub(crate) fn synth( } }; - let (raw, free_fn_owner): (Type, Option<(String, String)>) = if let Some(t) = locals.get(name) { + let (raw, free_fn_owner): (Type, Option<(String, String, Option)>) = if let Some(t) = locals.get(name) { // Locals (lambda / let / fn-params) — fn wins per // lookup precedence. A class method shadowed by a // local binding fires the shadow warning so the @@ -3323,7 +3330,7 @@ pub(crate) fn synth( .module_globals .get(&env.current_module) .filter(|m| m.contains_key(name)) - .map(|_| (env.current_module.clone(), name.clone())); + .map(|_| (env.current_module.clone(), name.clone(), None)); emit_shadow_warning_if_class_method(name, env.current_module.as_str(), warnings); (t.clone(), owner) } else if let Some((owner_module, raw_ty)) = env @@ -3361,7 +3368,7 @@ pub(crate) fn synth( let qualified = qualify_local_types(&raw_ty, &owner_module, &owner_types); // Bare name resolves through implicit import; the // unqualified name in the owner module is the same `name`. - (qualified, Some((owner_module, name.clone()))) + (qualified, Some((owner_module, name.clone(), None))) } else if is_class_method_dispatch(name, env) { // type-driven dispatch entry. The Var arm runs // before App-arm unification, so the arg type is not @@ -3531,8 +3538,18 @@ pub(crate) fn synth( qualify_local_types(&raw_ty, &target_module, &owner_types) }; // Dot-qualified `prefix.suffix`: the unqualified name in - // the owner module is `suffix`. - (qualified, Some((target_module, suffix.to_string()))) + // the owner module is `suffix`. raw-buf.3: when the + // resolution went through the TypeDef-first ladder + // (`type_home.is_some()`), the call is type-scoped — carry + // the TypeDef prefix as the mono-symbol scope. The legacy + // module-as-receiver sub-case (`type_home == None`) is not + // type-scoped → `scope = None`. + let scope = if type_home.is_some() { + Some(prefix.to_string()) + } else { + None + }; + (qualified, Some((target_module, suffix.to_string(), scope))) } else { return Err(CheckError::UnknownIdent(name.clone())); }; @@ -3546,7 +3563,7 @@ pub(crate) fn synth( // by bare name); the source-level `name` may be a // dot-qualified form (`prefix.suffix`) the synth started // with. - if let (Type::Forall { vars, constraints, body }, Some((owner, unqualified_name))) = + if let (Type::Forall { vars, constraints, body }, Some((owner, unqualified_name, scope))) = (&raw, &free_fn_owner) { let (metas, inst) = instantiate(vars, body, counter); @@ -3604,6 +3621,7 @@ pub(crate) fn synth( owner_module: owner.clone(), forall_vars: vars.clone(), metas: metas.clone(), + scope: scope.clone(), }); return Ok(inst); } diff --git a/crates/ailang-check/src/mono.rs b/crates/ailang-check/src/mono.rs index 8901af2..6b6a616 100644 --- a/crates/ailang-check/src/mono.rs +++ b/crates/ailang-check/src/mono.rs @@ -358,6 +358,22 @@ fn poly_free_fn_names_for_module( // `.` for every TypeDef declared in the imported // module — `synth`'s Var-arm resolves these via the TypeDef-first // ladder and `rewrite_mono_calls` consumes the same spelling. + // + // raw-buf.3: the bare implicit-import name is suppressed when the + // current module declares a same-name global. `synth`'s Var-arm + // resolves a same-module global (lib.rs ~3317) BEFORE the + // implicit-import fall-through (~3329), so a bare call there is the + // local def, not the imported poly fn. Adding the bare name here + // anyway would make `rewrite_mono_calls` treat the local call as a + // poly-free-fn site and over-advance the slot cursor (surfaced when + // kernel_stub gained a `peek` colliding with a fixture-local + // monomorphic `peek`). The dot-qualified / type-scoped spellings + // are unambiguous and stay unconditional. + let local_global_names: BTreeSet = env + .module_globals + .get(mname) + .map(|g| g.keys().cloned().collect()) + .unwrap_or_default(); if let Some(imports) = env.module_imports.get(mname) { for (alias_or_name, target_mod) in imports { let target_types: Vec = env @@ -370,7 +386,10 @@ fn poly_free_fn_names_for_module( if matches!(t, Type::Forall { .. }) { // Implicit-import bare name (today only the // prelude is implicit; the loop is generic). - out.insert(n.clone()); + // Suppressed if shadowed by a same-module global. + if !local_global_names.contains(n) { + out.insert(n.clone()); + } // Dot-qualified form. out.insert(format!("{alias_or_name}.{n}")); // prep.1: type-scoped form per TypeDef. @@ -419,6 +438,15 @@ fn poly_free_fn_constraint_counts_for_module( // prep.1: also enumerate type-scoped spellings `.` // per TypeDef in the imported module — mirror of // `poly_free_fn_names_for_module`. + // + // raw-buf.3: same bare-name shadow suppression as + // `poly_free_fn_names_for_module` — kept in lockstep so the two + // maps agree on which spellings count as poly-free-fn sites. + let local_global_names: BTreeSet = env + .module_globals + .get(mname) + .map(|g| g.keys().cloned().collect()) + .unwrap_or_default(); if let Some(imports) = env.module_imports.get(mname) { for (alias_or_name, target_mod) in imports { let target_types: Vec = env @@ -429,7 +457,9 @@ fn poly_free_fn_constraint_counts_for_module( if let Some(target_globals) = env.module_globals.get(target_mod) { for (n, t) in target_globals { if let Type::Forall { constraints, .. } = t { - out.insert(n.clone(), constraints.len()); + if !local_global_names.contains(n) { + out.insert(n.clone(), constraints.len()); + } out.insert(format!("{alias_or_name}.{n}"), constraints.len()); for tn in &target_types { out.insert(format!("{tn}.{n}"), constraints.len()); @@ -527,6 +557,17 @@ pub fn mono_symbol_n(base: &str, types: &[Type]) -> String { parts.join("__") } +/// raw-buf.3: the symbol base for a free-fn mono target. A +/// type-scoped op (`scope = Some("StubT")`, name `"peek"`) bases on +/// `"StubT_peek"`, so `mono_symbol_n` mints `StubT_peek__Int`; a +/// bare free fn (`scope = None`) keeps its bare base. +pub fn scoped_base(scope: &Option, name: &str) -> String { + match scope { + Some(t) => format!("{t}_{name}"), + None => name.to_string(), + } +} + fn type_to_mono_suffix(ty: &Type) -> String { match primitive_surface_name(ty) { Some(p) => p.to_string(), @@ -581,6 +622,10 @@ pub enum MonoTarget { /// Module in which the polymorphic source `Def::Fn` is declared; /// the synthesised mono `Def::Fn` is appended there. defining_module: String, + /// The TypeDef scope (`Some("StubT")`) when the call resolved + /// type-scoped; `None` for a bare free fn. Part of the dedup + /// key and the minted symbol base. + scope: Option, }, } @@ -597,13 +642,13 @@ pub fn mono_target_key(t: &MonoTarget) -> (String, String, String) { format!("{class}.{method}"), ailang_core::canonical::type_hash(type_), ), - MonoTarget::FreeFn { name, type_args, .. } => { + MonoTarget::FreeFn { name, type_args, scope, .. } => { let joined = type_args .iter() .map(ailang_core::canonical::type_hash) .collect::>() .join("|"); - ("free".into(), name.clone(), joined) + ("free".into(), scoped_base(scope, name), joined) } } } @@ -858,6 +903,7 @@ pub fn collect_mono_targets( name: fc.name.clone(), type_args, defining_module: fc.owner_module.clone(), + scope: fc.scope.clone(), }); } Ok(out) @@ -1004,9 +1050,9 @@ pub fn synthesise_mono_fn_for_free_fn( target: &MonoTarget, ws: &Workspace, ) -> Result { - let (name, type_args, defining_module) = match target { - MonoTarget::FreeFn { name, type_args, defining_module } => { - (name, type_args, defining_module) + let (name, type_args, defining_module, scope) = match target { + MonoTarget::FreeFn { name, type_args, defining_module, scope } => { + (name, type_args, defining_module, scope) } MonoTarget::ClassMethod { .. } => { return Err(crate::CheckError::Internal( @@ -1077,7 +1123,7 @@ pub fn synthesise_mono_fn_for_free_fn( let new_body = crate::substitute_rigids_in_term(&source_fn.body, &mapping); Ok(AstFnDef { - name: mono_symbol_n(name, type_args.as_slice()), + name: mono_symbol_n(&scoped_base(scope, name), type_args.as_slice()), ty: new_type, params: source_fn.params.clone(), body: new_body, @@ -1172,8 +1218,8 @@ fn rewrite_mono_calls( MonoTarget::ClassMethod { method, type_, defining_module, .. } => { (mono_symbol(method, type_), defining_module.as_str()) } - MonoTarget::FreeFn { name: fn_name, type_args, defining_module } => { - (mono_symbol_n(fn_name, type_args.as_slice()), defining_module.as_str()) + MonoTarget::FreeFn { name: fn_name, type_args, defining_module, scope } => { + (mono_symbol_n(&scoped_base(scope, fn_name), type_args.as_slice()), defining_module.as_str()) } }; let new_name = if defining_module == caller_module { @@ -1490,6 +1536,7 @@ pub(crate) fn collect_residuals_ordered( name: fc.name.clone(), type_args, defining_module: fc.owner_module.clone(), + scope: fc.scope.clone(), }) }) .collect(); diff --git a/crates/ailang-codegen/src/intercepts.rs b/crates/ailang-codegen/src/intercepts.rs index 24974ed..8f175e3 100644 --- a/crates/ailang-codegen/src/intercepts.rs +++ b/crates/ailang-codegen/src/intercepts.rs @@ -171,6 +171,20 @@ pub(crate) static INTERCEPTS: &[Intercept] = &[ wants_alwaysinline: false, emit: emit_answer, }, + Intercept { + name: "StubT_peek__Int", + expected_params: &["ptr"], // borrow (con StubT Int) lowers to ptr + expected_ret: "i64", + wants_alwaysinline: false, + emit: emit_stubt_peek_int, + }, + Intercept { + name: "StubT_peek__Float", + expected_params: &["ptr"], + expected_ret: "double", + wants_alwaysinline: false, + emit: emit_stubt_peek_float, + }, ]; pub(crate) fn lookup(name: &str) -> Option<&'static Intercept> { @@ -458,11 +472,38 @@ pub(crate) fn emit_answer(emitter: &mut Emitter<'_>) -> Result<()> { Ok(()) } +/// raw-buf.3 ratifier emit. NOTE: never instantiated to codegen this +/// iteration (no Term::New to build a StubT; unit-test-ratified only). +/// The load offset/correctness is exercised end-to-end only by +/// raw-buf.4's RawBuf path; here the entry exists to satisfy the +/// bijection. Retires with the stub in raw-buf.5. +pub(crate) fn emit_stubt_peek_int(emitter: &mut Emitter<'_>) -> Result<()> { + let n = emitter.locals.len(); + let s = emitter.locals[n - 1].1.clone(); + let v = emitter.fresh_ssa(); + emitter.body.push_str(&format!(" {v} = load i64, ptr {s}\n")); + emitter.body.push_str(&format!(" ret i64 {v}\n")); + emitter.body.push_str("}\n\n"); + emitter.block_terminated = true; + Ok(()) +} + +pub(crate) fn emit_stubt_peek_float(emitter: &mut Emitter<'_>) -> Result<()> { + let n = emitter.locals.len(); + let s = emitter.locals[n - 1].1.clone(); + let v = emitter.fresh_ssa(); + emitter.body.push_str(&format!(" {v} = load double, ptr {s}\n")); + emitter.body.push_str(&format!(" ret double {v}\n")); + emitter.body.push_str("}\n\n"); + emitter.block_terminated = true; + Ok(()) +} + #[cfg(test)] mod tests { use super::{lookup, INTERCEPTS}; - use ailang_check::mono::mono_symbol; - use ailang_core::ast::{Def, Term}; + use ailang_check::mono::{mono_symbol, mono_symbol_n}; + use ailang_core::ast::{Def, FnDef, Module, Term, Type}; use std::collections::BTreeSet; /// INTERCEPTS entries that intercept the monomorphised `__Int` @@ -485,6 +526,34 @@ mod tests { ] { for def in &module.defs { match def { + // raw-buf.3: a polymorphic top-level (intrinsic) op + // type-scoped to a same-module TypeDef T (its + // signature mentions `(con T …)`) expands to one + // marker per element type in T's `param-in` set — + // the same `T_f__` strings the mono pass mints + // (mono::scoped_base + mono_symbol_n). One marker → N + // entries. + Def::Fn(f) + if matches!(f.body, Term::Intrinsic) + && matches!(f.ty, Type::Forall { .. }) => + { + match scope_typedef_and_elems(f, &module) { + Some((tdef, elems)) => { + for elem in elems { + markers.insert(mono_symbol_n( + &format!("{tdef}_{}", f.name), + std::slice::from_ref(&elem), + )); + } + } + // a Forall intrinsic not scoped to a + // param-in TypeDef keeps the bare name (no + // such case ships today; future-proofing). + None => { + markers.insert(f.name.clone()); + } + } + } // Top-level intrinsic fn: name is already the symbol // (float_eq, answer, ...). Def::Fn(f) if matches!(f.body, Term::Intrinsic) => { @@ -548,4 +617,71 @@ mod tests { "optimisation-only allowlist names not in INTERCEPTS: {stale_allow:?}" ); } + + /// raw-buf.3: for a type-scoped polymorphic intrinsic fn `f`, return + /// `(TypeDef-name, element-types)` where the TypeDef is the unique + /// same-module `Def::Type` referenced via `(con T …)` in `f`'s + /// signature and the element types are its `param-in` set expressed as + /// `Type::Con` values (so `mono_symbol_n` produces the same suffix the + /// mono pass mints). `None` if `f` references zero or several + /// same-module TypeDefs. + fn scope_typedef_and_elems(f: &FnDef, module: &Module) -> Option<(String, Vec)> { + let typedef_names: BTreeSet<&str> = module + .defs + .iter() + .filter_map(|d| match d { + Def::Type(td) => Some(td.name.as_str()), + _ => None, + }) + .collect(); + // collect Con-heads referenced anywhere in f's type + let mut referenced: BTreeSet = BTreeSet::new(); + collect_con_heads(&f.ty, &mut referenced); + let scoped: Vec<&String> = referenced + .iter() + .filter(|n| typedef_names.contains(n.as_str())) + .collect(); + let tdef = match scoped.as_slice() { + [one] => (*one).clone(), + _ => return None, + }; + let td = module.defs.iter().find_map(|d| match d { + Def::Type(td) if td.name == tdef => Some(td), + _ => None, + })?; + // param-in is keyed by the type var; take its allowed surface + // names and build `Type::Con` element types. The element strings + // (`Int`, `Float`) flow through `mono_symbol_n`'s + // `primitive_surface_name` gate to the same `__Int` / `__Float` + // suffixes the mono pass mints. + let allowed = td.param_in.values().next()?; // single-var TypeDefs + let elems: Vec = allowed + .iter() + .map(|s| Type::Con { name: s.clone(), args: vec![] }) + .collect(); + Some((tdef, elems)) + } + + /// raw-buf.3: push every `Type::Con` head name reachable in `ty` + /// into `out`. `borrow` / `own` are `ParamMode` metadata on + /// `Type::Fn`, not wrapper `Type` variants, so the only recursive + /// shapes are `Forall.body`, `Fn.params`/`ret`, and `Con.args`. + fn collect_con_heads(ty: &Type, out: &mut BTreeSet) { + match ty { + Type::Con { name, args } => { + out.insert(name.clone()); + for a in args { + collect_con_heads(a, out); + } + } + Type::Fn { params, ret, .. } => { + for p in params { + collect_con_heads(p, out); + } + collect_con_heads(ret, out); + } + Type::Forall { body, .. } => collect_con_heads(body, out), + Type::Var { .. } => {} + } + } } diff --git a/crates/ailang-core/tests/design_schema_drift.rs b/crates/ailang-core/tests/design_schema_drift.rs index 95c9f9d..8227dfc 100644 --- a/crates/ailang-core/tests/design_schema_drift.rs +++ b/crates/ailang-core/tests/design_schema_drift.rs @@ -763,4 +763,12 @@ fn kernel_stub_module_round_trips() { let allowed = td.param_in.get("a").expect("StubT.a restricted"); assert!(allowed.contains("Int")); assert!(allowed.contains("Float")); + + // raw-buf.3: the type-scoped polymorphic intrinsic ratifier op + // round-trips and is an `(intrinsic)` marker. + let peek = recovered.defs.iter().find_map(|d| match d { + Def::Fn(f) if f.name == "peek" => Some(f), + _ => None, + }).expect("kernel_stub ships the peek ratifier op (raw-buf.3)"); + assert!(matches!(peek.body, Term::Intrinsic), "peek is an (intrinsic) marker"); } diff --git a/crates/ailang-kernel/src/kernel_stub/source.ail b/crates/ailang-kernel/src/kernel_stub/source.ail index b565e9d..04e36d4 100644 --- a/crates/ailang-kernel/src/kernel_stub/source.ail +++ b/crates/ailang-kernel/src/kernel_stub/source.ail @@ -12,4 +12,9 @@ (doc "Ratifies the intrinsic mechanism end-to-end: codegen intercept answer emits ret i64 42.") (type (fn-type (params) (ret (con Int)))) (params) + (intrinsic)) + (fn peek + (doc "Ratifies the type-scoped polymorphic intrinsic mechanism: StubT.peek is polymorphic over a (param-in {Int,Float}) and type-scoped to StubT. Codegen intercepts StubT_peek__Int / StubT_peek__Float emit a load of the Stub payload. Retires with the stub in raw-buf.5.") + (type (forall (vars a) (fn-type (params (borrow (con StubT a))) (ret a)))) + (params s) (intrinsic))) diff --git a/examples/peek_mono_pin_smoke.ail b/examples/peek_mono_pin_smoke.ail new file mode 100644 index 0000000..242511a --- /dev/null +++ b/examples/peek_mono_pin_smoke.ail @@ -0,0 +1,9 @@ +(module peek_mono_pin_smoke + (fn at_int + (type (fn-type (params (own (con StubT (con Int)))) (ret (con Int)))) + (params s) + (body (app StubT.peek s))) + (fn at_float + (type (fn-type (params (own (con StubT (con Float)))) (ret (con Float)))) + (params s) + (body (app StubT.peek s))))