ac9171ebf0
Pure simplification pass, no behaviour change — hashes, canonical forms, diagnostic codes, and emitted IR are byte-identical (hash-pin and IR-pin tests unchanged and green). ailang-check: - strip_forall and collect_pattern_binders were triplicated verbatim across linearity.rs / reuse_shape.rs / uniqueness.rs; hoisted each to a single pub(crate) fn in lib.rs. - mono.rs::pattern_binders was a fourth copy of collect_pattern_binders (iterative style, same result for every Pattern shape); deleted, now calls the shared fn. - maybe_instantiate and expect_eq were single-call wrappers; inlined at their sole call sites and removed. ailang-core: - collect_used_in_pattern was a verbatim duplicate of pattern_binds; deleted, call site repointed. pattern_binds made private (no external callers; the doc-comment's lift_letrecs claim was stale). - is_false serde helper replaced by std::ops::Not::not at the four skip_serializing_if sites (all plain-bool fields); helper removed. - test-only any_nested_ctor / any_let_rec folded into one generic any_term(t, &pred) walker. - removed dead test helpers tmp_dir / examples_dir, orphaned when their tests relocated to tests/workspace_pin.rs. ailang-codegen: - removed the write-only Emitter::types field, its populating loop, and the now-unused CtorInfo struct it fed. - adt_drop_symbol / adt_partial_drop_symbol differed only in a prefix literal; folded into one adt_symbol(prefix, ...). Symbol strings unchanged. Net -180 LOC.
754 lines
28 KiB
Rust
754 lines
28 KiB
Rust
//! Shape-compatibility check for `(reuse-as <var> <body>)`.
|
|
//!
|
|
//! ## Why this is its own pass
|
|
//!
|
|
//! Reuse-as semantics says: "free `<var>`'s heap slot, write `<body>`'s
|
|
//! freshly-allocated payload into it." That is sound only when
|
|
//! `<var>`'s ctor on this control-flow path has the same field count
|
|
//! and the same per-field LLVM types as `<body>`'s ctor — otherwise
|
|
//! the in-place rewrite would either underrun the box (smaller body
|
|
//! into bigger source slot, leaving uninitialised tail bytes) or write
|
|
//! a field of one LLVM type into a slot built for another (e.g.
|
|
//! storing a `ptr` over a previous `i64` slot, both 8-byte but with
|
|
//! different drop semantics for the OLD value at the same offset).
|
|
//!
|
|
//! The typechecker doesn't know enough to do this check: `<var>`'s
|
|
//! AILang type is `(con T)` (the binder type), but the *runtime ctor*
|
|
//! of the box `<var>` points at depends on the path — `<var>` may
|
|
//! flow into the `Cons` arm of a match where the pattern bound it as
|
|
//! the scrutinee, in which case its ctor on this arm is `Cons`. The
|
|
//! shape check resolves that ctor by walking the same way codegen
|
|
//! does, then compares against `<body>`'s ctor (which is always
|
|
//! syntactically a `Term::Ctor` once 18d.1's typecheck is satisfied).
|
|
//!
|
|
//! Mismatches surface as a structured `reuse-as-shape-mismatch`
|
|
//! diagnostic with a suggested rewrite that drops the wrapper. The
|
|
//! build then fails before codegen runs; the LLM author either fixes
|
|
//! the shapes or removes the reuse hint.
|
|
//!
|
|
//! ## Activation gate
|
|
//!
|
|
//! Same as `linearity::check_module`: only fns whose `Type::Fn.param_modes`
|
|
//! is non-empty (a nullary fn has no binders to track). Every mode is
|
|
//! explicit `Own`/`Borrow` (spec 0062).
|
|
//!
|
|
//! ## Path-ctor resolution
|
|
//!
|
|
//! A binder gets a known ctor on the current path if either:
|
|
//!
|
|
//! 1. It was bound by `Term::Let { value: Term::Ctor { type_name, ctor, .. }, .. }`
|
|
//! — the ctor is syntactically the rhs.
|
|
//! 2. It is the scrutinee `Term::Var { name }` of an enclosing
|
|
//! `Term::Match` and we are inside an arm whose `Pattern::Ctor { ctor, .. }`
|
|
//! matched it — the ctor is the arm's pattern.
|
|
//!
|
|
//! Both forms are tracked on a stack-of-bindings (`PathCtor`s) so that
|
|
//! lexical scope is honoured: nested matches and lets shadow correctly,
|
|
//! and on arm exit / let exit the binding pops.
|
|
//!
|
|
//! If the source binder is not in the path-ctor map at the reuse-as
|
|
//! site (e.g. complex control flow, or `<var>` is a fn parameter that
|
|
//! was never matched), we **conservatively reject** with the same
|
|
//! diagnostic — the LLM either spells things to make the path
|
|
//! obvious or removes the wrapper.
|
|
//!
|
|
//! ## Cross-module note
|
|
//!
|
|
//! Both ctors must be in the same module's ctor index for the field-
|
|
//! count and field-type comparison to be meaningful (the field types
|
|
//! are written in the owning module's local namespace; cross-module
|
|
//! comparison would need the same `qualify_local_types` plumbing
|
|
//! `check_in_workspace` uses). For 18d.2 we only inspect ctors with
|
|
//! bare type names (no `module.T` prefix) and conservatively reject
|
|
//! anything else as shape-indeterminate. Cross-module reuse-as is not
|
|
//! exercised by any 18d.2 fixture; lifting the restriction is a
|
|
//! mechanical follow-up if it ever lands.
|
|
|
|
use crate::diagnostic::Diagnostic;
|
|
use ailang_core::ast::{Arm, Def, FnDef, Module, NewArg, ParamMode, Pattern, Term, Type, TypeDef};
|
|
use crate::{collect_pattern_binders, strip_forall};
|
|
use ailang_surface::term_to_form_a;
|
|
use std::collections::HashMap;
|
|
|
|
/// top-level entry. Walks every fn in `m` and emits
|
|
/// `reuse-as-shape-mismatch` diagnostics for the all-explicit-mode
|
|
/// fns. Other fns are skipped.
|
|
///
|
|
/// Output ordering: defs in declaration order; within a def, source-
|
|
/// order discovery (depth-first left-to-right walk).
|
|
pub(crate) fn check_module(m: &Module) -> Vec<Diagnostic> {
|
|
// Collect the module's own type defs once. Cross-module references
|
|
// are out of scope per the module-level note; same-module ctors
|
|
// are the only ones we inspect.
|
|
let mut types: HashMap<String, TypeDef> = HashMap::new();
|
|
for def in &m.defs {
|
|
if let Def::Type(td) = def {
|
|
types.insert(td.name.clone(), td.clone());
|
|
}
|
|
}
|
|
let mut diags = Vec::new();
|
|
for def in &m.defs {
|
|
if let Def::Fn(f) = def {
|
|
check_fn(f, &types, &mut diags);
|
|
}
|
|
}
|
|
diags
|
|
}
|
|
|
|
/// Per-fn check. Skips fns whose param list is empty (no binders
|
|
/// to track).
|
|
fn check_fn(f: &FnDef, types: &HashMap<String, TypeDef>, diags: &mut Vec<Diagnostic>) {
|
|
let param_modes: &[ParamMode] = match strip_forall(&f.ty) {
|
|
Type::Fn { param_modes, .. } => param_modes.as_slice(),
|
|
_ => return,
|
|
};
|
|
if param_modes.is_empty() {
|
|
return;
|
|
}
|
|
|
|
let mut checker = Checker {
|
|
types,
|
|
diags,
|
|
def_name: &f.name,
|
|
path_ctors: HashMap::new(),
|
|
};
|
|
checker.walk(&f.body);
|
|
}
|
|
|
|
/// One entry in the path-ctor stack: the bare ctor name `<var>` carries
|
|
/// on the current control-flow path.
|
|
#[derive(Debug, Clone)]
|
|
struct PathCtor {
|
|
/// The bare ctor name (e.g. `"Cons"`).
|
|
ctor: String,
|
|
/// The bare type name (e.g. `"List"`). Used to look the ctor up in
|
|
/// the module's `types` table. Cross-module (`module.T`) prefixes
|
|
/// disable the comparison — see module-level note.
|
|
type_name: String,
|
|
}
|
|
|
|
struct Checker<'a> {
|
|
types: &'a HashMap<String, TypeDef>,
|
|
diags: &'a mut Vec<Diagnostic>,
|
|
def_name: &'a str,
|
|
/// Path-resolved ctor for each in-scope binder. Modified in place
|
|
/// with save/restore at lexical-scope boundaries (let, match arms).
|
|
/// A binder absent from this map is path-ctor-indeterminate at the
|
|
/// current point (e.g. an as-yet-unmatched fn parameter).
|
|
path_ctors: HashMap<String, PathCtor>,
|
|
}
|
|
|
|
impl<'a> Checker<'a> {
|
|
/// Walk `t`, emitting diagnostics for every reuse-as whose
|
|
/// resolved source-ctor is incompatible with the body-ctor.
|
|
fn walk(&mut self, t: &Term) {
|
|
match t {
|
|
Term::Lit { .. } | Term::Var { .. } => {}
|
|
Term::App { callee, args, .. } => {
|
|
self.walk(callee);
|
|
for a in args {
|
|
self.walk(a);
|
|
}
|
|
}
|
|
Term::Let { name, value, body } => {
|
|
self.walk(value);
|
|
// Push binder→ctor if the rhs is a literal ctor.
|
|
let prev = self.path_ctors.remove(name);
|
|
if let Term::Ctor { type_name, ctor, .. } = value.as_ref() {
|
|
if let Some(bare_type) = bare_name(type_name) {
|
|
self.path_ctors.insert(
|
|
name.clone(),
|
|
PathCtor {
|
|
ctor: ctor.clone(),
|
|
type_name: bare_type,
|
|
},
|
|
);
|
|
}
|
|
}
|
|
self.walk(body);
|
|
self.path_ctors.remove(name);
|
|
if let Some(p) = prev {
|
|
self.path_ctors.insert(name.clone(), p);
|
|
}
|
|
}
|
|
Term::LetRec { name, body, in_term, .. } => {
|
|
// LetRec binds a fn, not a value — never a ctor.
|
|
let prev = self.path_ctors.remove(name);
|
|
self.walk(body);
|
|
self.walk(in_term);
|
|
self.path_ctors.remove(name);
|
|
if let Some(p) = prev {
|
|
self.path_ctors.insert(name.clone(), p);
|
|
}
|
|
}
|
|
Term::If { cond, then, else_ } => {
|
|
self.walk(cond);
|
|
// Branches share the pre-If ctor map; neither branch
|
|
// commits a new ctor binding upward (an If-typed value
|
|
// could in principle be a ctor, but we don't refine
|
|
// through If — too narrow a path).
|
|
let saved = self.path_ctors.clone();
|
|
self.walk(then);
|
|
self.path_ctors = saved.clone();
|
|
self.walk(else_);
|
|
self.path_ctors = saved;
|
|
}
|
|
Term::Match { scrutinee, arms } => {
|
|
self.walk(scrutinee);
|
|
let saved = self.path_ctors.clone();
|
|
// The scrutinee's binder name (if it's a bare Var) is
|
|
// refined per-arm by the arm's pattern.
|
|
let scrut_var = match scrutinee.as_ref() {
|
|
Term::Var { name } => Some(name.clone()),
|
|
_ => None,
|
|
};
|
|
for arm in arms {
|
|
self.path_ctors = saved.clone();
|
|
self.walk_arm(arm, scrut_var.as_deref());
|
|
}
|
|
self.path_ctors = saved;
|
|
}
|
|
Term::Seq { lhs, rhs } => {
|
|
self.walk(lhs);
|
|
self.walk(rhs);
|
|
}
|
|
Term::Ctor { args, .. } => {
|
|
for a in args {
|
|
self.walk(a);
|
|
}
|
|
}
|
|
Term::Do { args, .. } => {
|
|
for a in args {
|
|
self.walk(a);
|
|
}
|
|
}
|
|
Term::Lam { body, .. } => {
|
|
// A lam is a closure boundary. We don't refine
|
|
// captured binders through it (their path-ctor is
|
|
// already what we have); the lam's own params are
|
|
// fresh binders with no known ctor.
|
|
self.walk(body);
|
|
}
|
|
Term::Clone { value } => self.walk(value),
|
|
Term::ReuseAs { source, body } => {
|
|
// Walk both sides first so any nested reuse-as gets
|
|
// reported in source order. Then perform the shape
|
|
// check at this site.
|
|
self.walk(source);
|
|
self.walk(body);
|
|
self.check_reuse_as(source, body);
|
|
}
|
|
Term::Loop { binders, body } => {
|
|
for b in binders {
|
|
self.walk(&b.init);
|
|
}
|
|
self.walk(body);
|
|
}
|
|
Term::Recur { args } => {
|
|
for a in args {
|
|
self.walk(a);
|
|
}
|
|
}
|
|
// prep.2 (kernel-extension-mechanics): walker through
|
|
// NewArg::Value subterms. Term::New itself is not a Ctor
|
|
// site for the reuse-as path-ctor analysis — its value is
|
|
// synth'd to whatever the resolved `new` def returns;
|
|
// refining `path_ctors` from a Term::New result would
|
|
// require knowing the body of `new`, which lives in
|
|
// another module.
|
|
Term::New { args, .. } => {
|
|
for arg in args {
|
|
if let NewArg::Value(v) = arg {
|
|
self.walk(v);
|
|
}
|
|
}
|
|
}
|
|
Term::Intrinsic => {}
|
|
}
|
|
}
|
|
|
|
/// Walk one match arm. If the scrutinee was a bare var and the
|
|
/// pattern is a `Pattern::Ctor`, refine that var's path-ctor to
|
|
/// the pattern's ctor for the arm's body. The pattern's bound
|
|
/// names (h, t) are introduced as path-ctor-indeterminate (they
|
|
/// could be any ctor). Wildcard / Var / Lit patterns leave the
|
|
/// scrutinee's path-ctor untouched.
|
|
fn walk_arm(&mut self, arm: &Arm, scrut_var: Option<&str>) {
|
|
let pattern_binders = collect_pattern_binders(&arm.pat);
|
|
let mut saved: HashMap<String, Option<PathCtor>> = HashMap::new();
|
|
for n in &pattern_binders {
|
|
saved.insert(n.clone(), self.path_ctors.remove(n));
|
|
}
|
|
// Refine the scrutinee's path-ctor for this arm.
|
|
let mut refined_scrut: Option<(String, Option<PathCtor>)> = None;
|
|
if let (Some(sv), Pattern::Ctor { ctor, .. }) = (scrut_var, &arm.pat) {
|
|
// Resolve the type-name of the matched ctor by looking up
|
|
// the ctor in the module's type list. Same-module only —
|
|
// cross-module patterns disable the refinement (consistent
|
|
// with the module-level note).
|
|
if let Some(type_name) = self.find_type_for_ctor(ctor) {
|
|
let prev = self.path_ctors.insert(
|
|
sv.to_string(),
|
|
PathCtor {
|
|
ctor: ctor.clone(),
|
|
type_name,
|
|
},
|
|
);
|
|
refined_scrut = Some((sv.to_string(), prev));
|
|
}
|
|
}
|
|
self.walk(&arm.body);
|
|
// Restore pattern-bound names.
|
|
for n in &pattern_binders {
|
|
self.path_ctors.remove(n);
|
|
if let Some(p) = saved.remove(n).flatten() {
|
|
self.path_ctors.insert(n.clone(), p);
|
|
}
|
|
}
|
|
// Restore the scrutinee's pre-arm path-ctor.
|
|
if let Some((sv, prev)) = refined_scrut {
|
|
self.path_ctors.remove(&sv);
|
|
if let Some(p) = prev {
|
|
self.path_ctors.insert(sv, p);
|
|
}
|
|
}
|
|
}
|
|
|
|
/// The shape check at one reuse-as site. Pre-conditions handled
|
|
/// upstream:
|
|
/// - 18d.1's typecheck guarantees `body` is `Term::Ctor` (or
|
|
/// `Term::Lam`, but lams have no ctor and are not in scope here).
|
|
/// - 18d.1's linearity guarantees `source` is a bare `Term::Var`
|
|
/// referring to an in-scope binder.
|
|
///
|
|
/// What this method enforces: the source's path-resolved ctor and
|
|
/// the body's ctor declare the same field count AND the same
|
|
/// per-field LLVM-equivalent types. Mismatch → emit
|
|
/// `reuse-as-shape-mismatch`. Source's path-ctor unresolved →
|
|
/// also emit (conservative reject — the codegen seam can't lower
|
|
/// what the static check can't verify).
|
|
fn check_reuse_as(&mut self, source: &Term, body: &Term) {
|
|
let src_var = match source {
|
|
Term::Var { name } => name,
|
|
// Linearity already flagged this; skip the shape check to
|
|
// avoid a duplicate diagnostic.
|
|
_ => return,
|
|
};
|
|
let body_ctor = match body {
|
|
Term::Ctor { type_name, ctor, .. } => (type_name.clone(), ctor.clone()),
|
|
// Typecheck already flagged this; skip.
|
|
_ => return,
|
|
};
|
|
let src_ctor = match self.path_ctors.get(src_var) {
|
|
Some(p) => p.clone(),
|
|
None => {
|
|
self.diags.push(make_shape_mismatch(
|
|
self.def_name,
|
|
src_var,
|
|
/*src=*/ None,
|
|
&body_ctor,
|
|
"indeterminate-source-ctor",
|
|
body,
|
|
));
|
|
return;
|
|
}
|
|
};
|
|
// Same-module ctor names only — cross-module is conservative
|
|
// reject.
|
|
let body_type = match bare_name(&body_ctor.0) {
|
|
Some(t) => t,
|
|
None => {
|
|
self.diags.push(make_shape_mismatch(
|
|
self.def_name,
|
|
src_var,
|
|
Some(&src_ctor),
|
|
&body_ctor,
|
|
"cross-module-body-ctor",
|
|
body,
|
|
));
|
|
return;
|
|
}
|
|
};
|
|
// Look up both ctors' declared field types.
|
|
let src_fields = self.lookup_ctor_fields(&src_ctor.type_name, &src_ctor.ctor);
|
|
let body_fields = self.lookup_ctor_fields(&body_type, &body_ctor.1);
|
|
let (sf, bf) = match (src_fields, body_fields) {
|
|
(Some(sf), Some(bf)) => (sf, bf),
|
|
_ => {
|
|
// One or both ctors not resolvable in the local module.
|
|
self.diags.push(make_shape_mismatch(
|
|
self.def_name,
|
|
src_var,
|
|
Some(&src_ctor),
|
|
&body_ctor,
|
|
"ctor-not-in-module",
|
|
body,
|
|
));
|
|
return;
|
|
}
|
|
};
|
|
if sf.len() != bf.len() {
|
|
self.diags.push(make_shape_mismatch(
|
|
self.def_name,
|
|
src_var,
|
|
Some(&src_ctor),
|
|
&body_ctor,
|
|
"field-count-mismatch",
|
|
body,
|
|
));
|
|
return;
|
|
}
|
|
for (s, b) in sf.iter().zip(bf.iter()) {
|
|
if !llvm_shape_equiv(s, b) {
|
|
self.diags.push(make_shape_mismatch(
|
|
self.def_name,
|
|
src_var,
|
|
Some(&src_ctor),
|
|
&body_ctor,
|
|
"field-type-mismatch",
|
|
body,
|
|
));
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Find the bare type name owning the given ctor name in the
|
|
/// current module. Returns the bare type name, or `None` if no
|
|
/// type in the module declares that ctor.
|
|
fn find_type_for_ctor(&self, ctor: &str) -> Option<String> {
|
|
for (tname, td) in self.types {
|
|
if td.ctors.iter().any(|c| c.name == ctor) {
|
|
return Some(tname.clone());
|
|
}
|
|
}
|
|
None
|
|
}
|
|
|
|
/// Look up a ctor's declared field types by (type-name, ctor-name)
|
|
/// in the current module. Returns `None` if the type isn't in
|
|
/// `self.types` or the ctor isn't in the type.
|
|
fn lookup_ctor_fields(&self, type_name: &str, ctor: &str) -> Option<Vec<Type>> {
|
|
let td = self.types.get(type_name)?;
|
|
td.ctors
|
|
.iter()
|
|
.find(|c| c.name == ctor)
|
|
.map(|c| c.fields.clone())
|
|
}
|
|
}
|
|
|
|
/// Returns the bare type name if `name` has no `module.` prefix,
|
|
/// otherwise `None`. The shape check intentionally doesn't try to
|
|
/// reach across module boundaries — see the module-level note.
|
|
fn bare_name(name: &str) -> Option<String> {
|
|
if name.contains('.') {
|
|
None
|
|
} else {
|
|
Some(name.to_string())
|
|
}
|
|
}
|
|
|
|
/// LLVM-equivalence of two AILang [`Type`]s, mirroring the
|
|
/// `llvm_type` shape codegen uses:
|
|
/// - `Int` → `i64`
|
|
/// - `Bool` → `i1`
|
|
/// - `Unit` → `i8`
|
|
/// - `Str` / any other `Con` (ADT) / `Fn` / `Var` → `ptr`
|
|
///
|
|
/// The shape check uses this to compare two ctors' fields slot-by-
|
|
/// slot. Two slots are "shape-equivalent" iff their LLVM lowerings
|
|
/// are equal.
|
|
fn llvm_shape_equiv(a: &Type, b: &Type) -> bool {
|
|
llvm_kind(a) == llvm_kind(b)
|
|
}
|
|
|
|
fn llvm_kind(t: &Type) -> &'static str {
|
|
match t {
|
|
Type::Con { name, .. } => match name.as_str() {
|
|
"Int" => "i64",
|
|
"Bool" => "i1",
|
|
"Unit" => "i8",
|
|
// Str + every user-declared ADT lower to `ptr`.
|
|
_ => "ptr",
|
|
},
|
|
Type::Fn { .. } | Type::Var { .. } => "ptr",
|
|
// Forall is a top-level wrapper; the shape inspector should
|
|
// never see one in a ctor field. Treat conservatively as
|
|
// `ptr` if it ever appears.
|
|
Type::Forall { .. } => "ptr",
|
|
}
|
|
}
|
|
|
|
/// Build a `reuse-as-shape-mismatch` diagnostic. The suggested
|
|
/// rewrite drops the wrapper and keeps the body alone — the LLM can
|
|
/// then either accept the normal allocator path or reshape the body.
|
|
///
|
|
/// `reason` is a stable kebab-case sub-code carried in `ctx.reason`
|
|
/// so consumers can branch on the specific failure mode without
|
|
/// parsing prose.
|
|
fn make_shape_mismatch(
|
|
def: &str,
|
|
binder: &str,
|
|
src: Option<&PathCtor>,
|
|
body_ctor: &(String, String),
|
|
reason: &str,
|
|
body: &Term,
|
|
) -> Diagnostic {
|
|
let replacement = term_to_form_a(body);
|
|
let mut ctx = serde_json::json!({
|
|
"binder": binder,
|
|
"reason": reason,
|
|
"body_type": body_ctor.0,
|
|
"body_ctor": body_ctor.1,
|
|
});
|
|
if let Some(s) = src {
|
|
if let Some(obj) = ctx.as_object_mut() {
|
|
obj.insert(
|
|
"source_type".into(),
|
|
serde_json::Value::String(s.type_name.clone()),
|
|
);
|
|
obj.insert(
|
|
"source_ctor".into(),
|
|
serde_json::Value::String(s.ctor.clone()),
|
|
);
|
|
}
|
|
}
|
|
let msg = match (src, reason) {
|
|
(_, "indeterminate-source-ctor") => format!(
|
|
"reuse-as on `{binder}`: cannot statically determine the source's ctor on this control-flow path; reuse-as requires the source's ctor to be visible from a let or a match arm"
|
|
),
|
|
(_, "cross-module-body-ctor") => format!(
|
|
"reuse-as on `{binder}`: cross-module body ctors are not yet supported by the shape check"
|
|
),
|
|
(_, "ctor-not-in-module") => format!(
|
|
"reuse-as on `{binder}`: source or body ctor is not declared in the current module"
|
|
),
|
|
(Some(s), "field-count-mismatch") => format!(
|
|
"reuse-as on `{binder}`: source ctor `{}` has {} field(s) but body ctor `{}` has {} — reuse requires the same shape",
|
|
s.ctor,
|
|
"?",
|
|
body_ctor.1,
|
|
"?",
|
|
),
|
|
(Some(s), "field-type-mismatch") => format!(
|
|
"reuse-as on `{binder}`: source ctor `{}` and body ctor `{}` have incompatible per-field LLVM types — reuse requires identical per-slot shapes",
|
|
s.ctor, body_ctor.1
|
|
),
|
|
_ => format!("reuse-as on `{binder}`: shape mismatch ({reason})"),
|
|
};
|
|
Diagnostic::error("reuse-as-shape-mismatch", msg)
|
|
.with_def(def)
|
|
.with_ctx(ctx)
|
|
.with_suggested_rewrite(
|
|
"drop the reuse-as wrapper; the body alone allocates a fresh box via the normal allocator",
|
|
replacement,
|
|
)
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
use ailang_core::ast::{Ctor, FnDef, Literal};
|
|
use std::collections::BTreeMap;
|
|
|
|
fn list_type_def() -> TypeDef {
|
|
TypeDef {
|
|
name: "List".into(),
|
|
vars: vec![],
|
|
ctors: vec![
|
|
Ctor { name: "Nil".into(), fields: vec![] },
|
|
Ctor {
|
|
name: "Cons".into(),
|
|
fields: vec![
|
|
Type::int(),
|
|
Type::Con { name: "List".into(), args: vec![] },
|
|
],
|
|
},
|
|
],
|
|
doc: None,
|
|
drop_iterative: false,
|
|
param_in: BTreeMap::new(),
|
|
}
|
|
}
|
|
|
|
fn fn_with_modes(name: &str, modes: Vec<ParamMode>, body: Term) -> Def {
|
|
Def::Fn(FnDef {
|
|
name: name.into(),
|
|
ty: Type::Fn {
|
|
params: modes
|
|
.iter()
|
|
.map(|_| Type::Con { name: "List".into(), args: vec![] })
|
|
.collect(),
|
|
param_modes: modes.clone(),
|
|
ret: Box::new(Type::Con { name: "List".into(), args: vec![] }),
|
|
ret_mode: ParamMode::Own,
|
|
effects: vec![],
|
|
},
|
|
params: (0..modes.len()).map(|i| format!("p{i}")).collect(),
|
|
body,
|
|
suppress: vec![],
|
|
doc: None,
|
|
export: None,
|
|
})
|
|
}
|
|
|
|
/// Happy-path: `(reuse-as xs (Cons ...))` inside the Cons arm of
|
|
/// a match on xs — both ctors are `Cons`, identical shape. Clean.
|
|
#[test]
|
|
fn reuse_as_same_ctor_in_match_arm_is_clean() {
|
|
// body: (match xs (Nil → Nil) (Cons h t → (reuse-as xs (Cons h t))))
|
|
let body = Term::Match {
|
|
scrutinee: Box::new(Term::Var { name: "p0".into() }),
|
|
arms: vec![
|
|
Arm {
|
|
pat: Pattern::Ctor { ctor: "Nil".into(), fields: vec![] },
|
|
body: Term::Ctor {
|
|
type_name: "List".into(),
|
|
ctor: "Nil".into(),
|
|
args: vec![],
|
|
},
|
|
},
|
|
Arm {
|
|
pat: Pattern::Ctor {
|
|
ctor: "Cons".into(),
|
|
fields: vec![
|
|
Pattern::Var { name: "h".into() },
|
|
Pattern::Var { name: "t".into() },
|
|
],
|
|
},
|
|
body: Term::ReuseAs {
|
|
source: Box::new(Term::Var { name: "p0".into() }),
|
|
body: Box::new(Term::Ctor {
|
|
type_name: "List".into(),
|
|
ctor: "Cons".into(),
|
|
args: vec![
|
|
Term::Var { name: "h".into() },
|
|
Term::Var { name: "t".into() },
|
|
],
|
|
}),
|
|
},
|
|
},
|
|
],
|
|
};
|
|
let m = Module {
|
|
schema: ailang_core::SCHEMA.into(),
|
|
name: "t".into(),
|
|
kernel: false,
|
|
imports: vec![],
|
|
defs: vec![
|
|
Def::Type(list_type_def()),
|
|
fn_with_modes("f", vec![ParamMode::Own], body),
|
|
],
|
|
};
|
|
let diags = check_module(&m);
|
|
assert!(
|
|
diags.is_empty(),
|
|
"happy-path same-ctor reuse-as must be clean; got {diags:?}"
|
|
);
|
|
}
|
|
|
|
/// Mismatch: source-ctor is `Cons` (matched in arm) but body-ctor
|
|
/// is `Nil` — different field counts (2 vs 0). Must fire
|
|
/// `reuse-as-shape-mismatch` with reason `field-count-mismatch`.
|
|
#[test]
|
|
fn reuse_as_cons_to_nil_is_shape_mismatch() {
|
|
let body = Term::Match {
|
|
scrutinee: Box::new(Term::Var { name: "p0".into() }),
|
|
arms: vec![
|
|
Arm {
|
|
pat: Pattern::Ctor { ctor: "Nil".into(), fields: vec![] },
|
|
body: Term::Ctor {
|
|
type_name: "List".into(),
|
|
ctor: "Nil".into(),
|
|
args: vec![],
|
|
},
|
|
},
|
|
Arm {
|
|
pat: Pattern::Ctor {
|
|
ctor: "Cons".into(),
|
|
fields: vec![
|
|
Pattern::Var { name: "h".into() },
|
|
Pattern::Var { name: "t".into() },
|
|
],
|
|
},
|
|
// BAD: reuse Cons-shaped slot to write Nil.
|
|
body: Term::ReuseAs {
|
|
source: Box::new(Term::Var { name: "p0".into() }),
|
|
body: Box::new(Term::Ctor {
|
|
type_name: "List".into(),
|
|
ctor: "Nil".into(),
|
|
args: vec![],
|
|
}),
|
|
},
|
|
},
|
|
],
|
|
};
|
|
let m = Module {
|
|
schema: ailang_core::SCHEMA.into(),
|
|
name: "t".into(),
|
|
kernel: false,
|
|
imports: vec![],
|
|
defs: vec![
|
|
Def::Type(list_type_def()),
|
|
fn_with_modes("f", vec![ParamMode::Own], body),
|
|
],
|
|
};
|
|
let diags = check_module(&m);
|
|
assert_eq!(
|
|
diags.len(),
|
|
1,
|
|
"expected one shape-mismatch diagnostic; got {diags:?}"
|
|
);
|
|
let d = &diags[0];
|
|
assert_eq!(d.code, "reuse-as-shape-mismatch");
|
|
assert_eq!(d.def.as_deref(), Some("f"));
|
|
assert_eq!(
|
|
d.ctx.get("reason").and_then(|v| v.as_str()),
|
|
Some("field-count-mismatch")
|
|
);
|
|
assert!(!d.suggested_rewrites.is_empty());
|
|
let rep = &d.suggested_rewrites[0].replacement;
|
|
ailang_surface::parse_term(rep)
|
|
.unwrap_or_else(|e| panic!("suggested rewrite must parse: {rep} ({e})"));
|
|
}
|
|
|
|
/// Source-ctor unresolved: `(reuse-as p0 (Cons ...))` outside any
|
|
/// match — `p0` is a fn parameter but never matched, so we have
|
|
/// no path-ctor for it. Conservative reject.
|
|
#[test]
|
|
fn reuse_as_indeterminate_source_ctor_is_rejected() {
|
|
let body = Term::ReuseAs {
|
|
source: Box::new(Term::Var { name: "p0".into() }),
|
|
body: Box::new(Term::Ctor {
|
|
type_name: "List".into(),
|
|
ctor: "Cons".into(),
|
|
args: vec![
|
|
Term::Lit { lit: Literal::Int { value: 0 } },
|
|
Term::Ctor {
|
|
type_name: "List".into(),
|
|
ctor: "Nil".into(),
|
|
args: vec![],
|
|
},
|
|
],
|
|
}),
|
|
};
|
|
let m = Module {
|
|
schema: ailang_core::SCHEMA.into(),
|
|
name: "t".into(),
|
|
kernel: false,
|
|
imports: vec![],
|
|
defs: vec![
|
|
Def::Type(list_type_def()),
|
|
fn_with_modes("f", vec![ParamMode::Own], body),
|
|
],
|
|
};
|
|
let diags = check_module(&m);
|
|
assert_eq!(diags.len(), 1, "got {diags:?}");
|
|
assert_eq!(diags[0].code, "reuse-as-shape-mismatch");
|
|
assert_eq!(
|
|
diags[0].ctx.get("reason").and_then(|v| v.as_str()),
|
|
Some("indeterminate-source-ctor")
|
|
);
|
|
}
|
|
}
|