iter raw-buf.3 typescoped-intrinsic-mechanism (DONE 2/2): scope-qualified mono symbols + bijection expansion (refs #7)

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<String> 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__<elem> 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).
This commit is contained in:
2026-05-29 19:39:35 +02:00
parent 8508182f84
commit fd37fa6489
8 changed files with 292 additions and 18 deletions
+138 -2
View File
@@ -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__<elem>` 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<Type>)> {
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<String> = 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<Type> = 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<String>) {
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 { .. } => {}
}
}
}