Files
AILang/crates/ailang-check/src/builtins.rs
T
Brummel 76b21c00eb feat(lang): eliminate the Implicit ownership default — totality + the drop-soundness it demasks (#55)
Deletes `ParamMode::Implicit`. `ParamMode` is now `{Own, Borrow}`:
every fn-type slot on every signature carries an explicit `own` or
`borrow`, no defaulted position survives anywhere (model 0008 §2,
spec 0062). The parser rejects a bare fn-type slot; `borrow-return`
and `borrow-over-value` reject at the signature; the corpus is
migrated to minimal-ownership modes (consumed ⇒ own, read-only-heap
⇒ borrow, value ⇒ trivial-own). The documented `Implicit`-ret-mode
leak is fixed: an owned heap return now drops exactly once (live=0,
acceptance criterion 5).

This was the easy half. Removing the default ACTIVATED a family of
drop paths that `Implicit` had silently skipped — the pre-cutover
language was leaking (and in places mis-dropping) here rather than
crashing, because an Implicit scrutinee turned the drop off. Making
the modes explicit (Own) turned those paths on and exposed two
latent-bug clusters, all fixed RED-first as part of this cutover:

Drop-soundness family (four legs):
  A. lit-sub-pattern double-free — the desugar re-matched the same
     owned scrutinee in the lit fall-through; fixed by grouping
     consecutive same-ctor arms into one match (bind fields once),
     in ailang-core desugar.
  B. Cons-husk leak on non-tail arm bodies — the lit-sub-pattern
     desugar rebound the owned scrutinee via `Let $mp = xs`, which
     bumped consume_count and suppressed the existing fn-return
     partial_drop. Fixed by not rebinding a bare-Var scrutinee
     (one husk-freeing mechanism, not two).
  C. polymorphic `drop_<T>` rc_dec'd monomorphised value fields —
     the per-ADT drop fn was emitted once from the polymorphic
     TypeDef, defaulting type-var fields to ptr and rc_dec'ing
     inline Ints (segfault). Fixed with per-monomorph drop
     functions (new ailang-codegen::dropmono): the drop set is
     collected from the lowered MIR, value-type fields are skipped,
     heap fields still freed once; monomorphic-concrete ADTs keep
     their byte-identical un-suffixed drop symbol.
  D. static Str literal passed to an `(own Str)` param — the
     literal lowers to a header-less rodata constant; the callee's
     now-active rc_dec read its length field as a refcount and
     freed a static address (segfault). Fixed with the missing
     fourth StrRep::Static→Heap promotion in lower_to_mir's App arm,
     gated on Own mode (borrow args stay static, no regression).

over-strict-mode lint over-fired: it suggested `(borrow V)` for
value-typed params (which `borrow-over-value` rejects — own is the
only legal mode there) and fired on `(intrinsic)` bodies (whose
consumption the linearity walk cannot observe). Tightened to skip
both; contract 0008 updated to the narrowed firing scope.

Irreversible step — canonical-form hash reset (model 0008 §6,
acceptance criterion 6). Every signature now carries explicit modes,
so the hashable canonical JSON changed for every module. RATIFY:
the corpus-wide hash-pin reset (hash_pin, prelude_module_hash_pin,
mono_hash_stability, eq_ord_e2e, embed_export_hash_stable, the
ct4/iter*/loop_recur schema-extension pins) and the list ir_snapshot
golden were regenerated once, deliberately, as the intended one-time
consequence of removing the mode elision from the canonical form —
not a regression. Each regenerated hash verified deterministic across
two runs.

Also fixes a pre-existing latent failure surfaced by the verification
gate, unrelated to this cutover: the `every_contract_names_a_resolvable_
ratifying_test` resolver (design_index_pin) could not resolve the
" + " dual-link ratifying-test form (`uniqueness.rs + linearity.rs`)
that the #57 audit-close (dfdc65f) introduced — it shipped red on that
commit. Resolver taught the dual-link form, mirroring its sibling.

Verification: cargo test --workspace = 731 passed, 0 failed (twice,
stable); e2e 102 passed, no binary exits non-zero (corpus crash-free);
grep-clean for Implicit/fn_implicit/mode_eq across crates; every drop
fix confirmed via emitted IR + AILANG_RC_STATS balance on the head==K,
head!=K, and Nil paths. Three BLOCKEDs en route (the unsound first
husk-dec attempt, the over-strict derivation premise, the leg-B fix
direction) were each treated as a real design/spec gap and rediagnosed,
not patched over.

Supersedes #54 (return-position-only leak patch). Precondition #57
(linearity hardening) was already met. Spec docs/specs/0062, plan
docs/plans/0121.

closes #55
2026-06-02 00:03:46 +02:00

572 lines
22 KiB
Rust

//! Built-in operations known to the typechecker (and codegen).
//!
//! This module owns the **fixed** symbol set the language ships with — the
//! arithmetic / comparison / logical operators (`+`, `==`, `not`, ...) and
//! the IO effect ops (`io/print_str`). User code cannot define
//! anything in here; conversely the typechecker treats every entry as
//! always-in-scope without an explicit import.
//!
//! Builtins are kept in a **separate table** from user globals (see
//! [`crate::Env::globals`] vs [`crate::Env::effect_ops`]) only because the
//! two channels surface differently in the AST: value-level builtins are
//! reached through `Term::Var { name }` (they live alongside user defs in
//! `Env.globals`), while effect ops are reached through `Term::Do { op }`
//! and need to carry an extra effect label, hence the dedicated
//! [`struct@EffectOpSig`] payload in `Env.effect_ops`.
//!
//! The typechecker calls [`install()`] once per module-check, before any
//! user-supplied globals are added. The CLI's `ail builtins` subcommand
//! reflects the same data via [`list()`] / [`value_names()`].
use ailang_core::ast::Type;
/// Signature of a builtin effect operation: the effect label it raises,
/// its parameter types, and its return type.
///
/// The typechecker consults this when synthesising the type of a
/// `Term::Do { op, args }` — it unifies `args` against `params`, returns
/// `ret`, and inserts `effect` into the body's accumulated effect set so
/// the surrounding fn's declared effect row is checked against actual
/// usage.
#[derive(Debug, Clone)]
pub struct EffectOpSig {
/// Effect label this op raises (e.g. `"IO"`). Compared against the
/// declared effect row on the enclosing fn type.
pub effect: String,
/// Positional parameter types. Length and order are the contract for
/// `Term::Do { args }`.
pub params: Vec<Type>,
/// Return type of the op. Often [`Type::unit`] for sinks like
/// `io/print_str`.
pub ret: Type,
}
/// Populates `env` with every built-in operator and effect op.
///
/// Called once at the start of `check_in_workspace`, before user
/// type defs and globals are folded in. After this returns, every name in
/// [`list()`] is resolvable in `env`. Idempotent for a fresh `Env`; calling
/// it twice would shadow the same entries with identical types.
pub fn install(env: &mut crate::Env) {
// arithmetic and comparison ops are polymorphic.
// Same shape as `==` below — the {Int, Float}-restriction is
// enforced at codegen, not at typecheck. `%` stays Int-only
// (no fmod yet — `%` semantics for Float require an explicit
// decision on sign-of-result and ±0/±Inf edge cases that has
// not been made).
let poly_a_a_to_a = || Type::Forall {
vars: vec!["a".into()],
constraints: vec![],
body: Box::new(Type::Fn {
params: vec![
Type::Var { name: "a".into() },
Type::Var { name: "a".into() },
],
ret: Box::new(Type::Var { name: "a".into() }),
effects: vec![],
param_modes: vec![],
ret_mode: ailang_core::ast::ParamMode::Own,
}),
};
// 2026-05-21 operator-routing-eq-ord: the `poly_a_a_to_bool`
// helper produced the `forall a. (a, a) -> Bool` shape used by
// the six comparator builtins (`==`/`!=`/`<`/`<=`/`>`/`>=`).
// Those builtins are gone (class-method dispatch via prelude.Eq
// / Ord replaces them); the helper is dead.
let int_int_int = Type::Fn {
params: vec![Type::int(), Type::int()],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ailang_core::ast::ParamMode::Own,
};
for op in ["+", "-", "*", "/"] {
env.globals.insert(op.into(), poly_a_a_to_a());
}
env.globals.insert("%".into(), int_int_int);
// 2026-05-21 operator-routing-eq-ord: the six comparator names
// `==` / `!=` / `<` / `<=` / `>` / `>=` are no longer part of the
// language. Equality / ordering go through the class methods
// `eq` / `compare` (and the free helpers `ne` / `lt` / `le` /
// `gt` / `ge`) dispatched via prelude.Eq / prelude.Ord; Float
// comparison goes through the named fns `float_eq` / `float_ne`
// / `float_lt` / `float_le` / `float_gt` / `float_ge`.
env.globals.insert(
"not".into(),
Type::Fn {
params: vec![Type::bool_()],
ret: Box::new(Type::bool_()),
effects: vec![],
param_modes: vec![],
ret_mode: ailang_core::ast::ParamMode::Own,
},
);
// `__unreachable__` is the polymorphic bottom value.
// Type: `forall a. a`. Used by the desugar pass as the chain
// terminator of an exhaustive match, and available to user code as
// a primitive panic point. Codegen lowers it to LLVM `unreachable`.
// It is a value, not a fn — reference site is `(var __unreachable__)`,
// not `(app __unreachable__)`.
env.globals.insert(
"__unreachable__".into(),
Type::Forall {
vars: vec!["a".into()],
constraints: vec![],
body: Box::new(Type::Var { name: "a".into() }),
},
);
// Float-conversion and inspection builtins.
// Codegen lowering lands in iter 4; iter 3 only registers types.
// `neg` is polymorphic (`forall a. (a) -> a`) for the same reason
// the widened `+` is — Int and Float negation share one symbol;
// the spec section A3 also notes that `(- 0.0 x)` desugar is
// wrong for `-0.0` (returns `+0.0` per IEEE rounding), so Float
// negation needs its own builtin name.
env.globals.insert(
"neg".into(),
Type::Forall {
vars: vec!["a".into()],
constraints: vec![],
body: Box::new(Type::Fn {
params: vec![Type::Var { name: "a".into() }],
ret: Box::new(Type::Var { name: "a".into() }),
effects: vec![],
param_modes: vec![],
ret_mode: ailang_core::ast::ParamMode::Own,
}),
},
);
env.globals.insert(
"int_to_float".into(),
Type::Fn {
params: vec![Type::int()],
ret: Box::new(Type::float()),
effects: vec![],
param_modes: vec![],
ret_mode: ailang_core::ast::ParamMode::Own,
},
);
env.globals.insert(
"float_to_int_truncate".into(),
Type::Fn {
params: vec![Type::float()],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ailang_core::ast::ParamMode::Own,
},
);
env.globals.insert(
"float_to_str".into(),
Type::Fn {
params: vec![Type::float()],
ret: Box::new(Type::str_()),
effects: vec![],
param_modes: vec![],
ret_mode: ailang_core::ast::ParamMode::Own,
},
);
env.globals.insert(
"int_to_str".into(),
Type::Fn {
params: vec![Type::int()],
ret: Box::new(Type::str_()),
effects: vec![],
param_modes: vec![],
ret_mode: ailang_core::ast::ParamMode::Own,
},
);
env.globals.insert(
"bool_to_str".into(),
Type::Fn {
params: vec![Type::bool_()],
ret: Box::new(Type::str_()),
effects: vec![],
param_modes: vec![],
ret_mode: ailang_core::ast::ParamMode::Own,
},
);
env.globals.insert(
"str_clone".into(),
Type::Fn {
params: vec![Type::str_()],
ret: Box::new(Type::str_()),
effects: vec![],
param_modes: vec![ailang_core::ast::ParamMode::Borrow],
ret_mode: ailang_core::ast::ParamMode::Own,
},
);
env.globals.insert(
"str_concat".into(),
Type::Fn {
params: vec![Type::str_(), Type::str_()],
ret: Box::new(Type::str_()),
effects: vec![],
param_modes: vec![
ailang_core::ast::ParamMode::Borrow,
ailang_core::ast::ParamMode::Borrow,
],
ret_mode: ailang_core::ast::ParamMode::Own,
},
);
env.globals.insert(
"is_nan".into(),
Type::Fn {
params: vec![Type::float()],
ret: Box::new(Type::bool_()),
effects: vec![],
param_modes: vec![],
ret_mode: ailang_core::ast::ParamMode::Own,
},
);
// Float bit-pattern constants. Bare values, not
// fns — reference site is `(var nan)`. Codegen emits
// `double 0x7FF8000000000000` (NaN), `double 0x7FF0000000000000`
// (+Inf), `double 0xFFF0000000000000` (-Inf) at the use site in
// iter 4. Parallel to `__unreachable__` but typed as concrete
// `Float` rather than the polymorphic bottom — these constants
// always denote a specific `f64` bit pattern, never a generic
// missing value.
env.globals.insert("nan".into(), Type::float());
env.globals.insert("inf".into(), Type::float());
env.globals.insert("neg_inf".into(), Type::float());
env.effect_ops.insert(
"io/print_str".into(),
EffectOpSig {
effect: "IO".into(),
params: vec![Type::str_()],
ret: Type::unit(),
},
);
}
/// Names of value-level built-ins (operators, `not`) — the ones that
/// show up as `Term::Var { name }` references in user code. Effect ops
/// are excluded because they reach codegen via `Term::Do`, not `Var`.
///
/// Single source of truth: derived from `list()` by filtering out the
/// effect-op rows. Kept as a function (not a `const`) because `list()`
/// already allocates; this is only consulted by tooling (`ail deps`).
pub fn value_names() -> Vec<&'static str> {
list()
.into_iter()
.filter(|(_, sig)| !sig.contains("[effect op]"))
.map(|(n, _)| n)
.collect()
}
/// Returns the list of all registered built-ins. Useful for the CLI subcommand
/// `ail builtins`, when the LLM wants to check expected signatures.
pub fn list() -> Vec<(&'static str, &'static str)> {
vec![
("+", "forall a. (a, a) -> a"),
("-", "forall a. (a, a) -> a"),
("*", "forall a. (a, a) -> a"),
("/", "forall a. (a, a) -> a"),
("%", "(Int, Int) -> Int"),
("not", "(Bool) -> Bool"),
("__unreachable__", "forall a. a"),
("neg", "forall a. (a) -> a"),
("int_to_float", "(Int) -> Float"),
("float_to_int_truncate", "(Float) -> Int"),
("float_to_str", "(Float) -> Str"),
("int_to_str", "(Int) -> Str"),
("bool_to_str", "(Bool) -> Str"),
("str_clone", "(Str) -> Str"),
("str_concat", "(Str, Str) -> Str"),
("is_nan", "(Float) -> Bool"),
("nan", "Float"),
("inf", "Float"),
("neg_inf", "Float"),
("io/print_str", "(Str) -> Unit !IO [effect op]"),
]
}
#[cfg(test)]
mod tests {
use super::*;
use crate::{Env, Subst};
use ailang_core::ast::{Literal, Term};
use indexmap::IndexMap;
use std::collections::BTreeSet;
/// Synthesize the type of a small expression in a fresh Env that has
/// only the builtins installed (no user defs). Returns the fully-
/// applied (substitution-resolved) result type; effects ignored for
/// this helper. Wraps `crate::synth` with the boilerplate state
/// (locals, effects sink, subst, counter, residuals) the real
/// typechecker entry points (`check_module`) would otherwise own.
fn synth_in_builtins_env(t: &Term) -> Type {
let mut env = Env::default();
install(&mut env);
let mut locals: IndexMap<String, Type> = IndexMap::new();
let mut effects: BTreeSet<String> = BTreeSet::new();
let mut subst = Subst::default();
let mut counter: u32 = 0;
let mut residuals = Vec::new();
let mut free_fn_calls = Vec::new();
let mut warnings: Vec<crate::diagnostic::Diagnostic> = Vec::new();
// loop-recur iter 2: test helper synths one term from top-of-
// body — fresh empty loop-stack.
let mut loop_stack: Vec<Vec<(String, Type)>> = Vec::new();
let ty = crate::synth(
t,
&env,
&mut locals,
&mut loop_stack,
&mut effects,
"<test>",
&mut subst,
&mut counter,
&mut residuals,
&mut free_fn_calls,
&mut warnings,
&mut crate::linearity::LetBinderTypes::new(),
)
.expect("synth");
subst.apply(&ty)
}
fn lit_int(v: i64) -> Term {
Term::Lit { lit: Literal::Int { value: v } }
}
fn lit_float(bits: u64) -> Term {
Term::Lit { lit: Literal::Float { bits } }
}
fn app(callee: &str, args: Vec<Term>) -> Term {
Term::App {
callee: Box::new(Term::Var { name: callee.into() }),
args,
tail: false,
}
}
/// regression — `(+ 1 2)` still resolves to `Int`
/// after the widening from `(Int, Int) -> Int` to
/// `forall a. (a, a) -> a`. Protects the no-regression invariant
/// for every existing Int-using fixture: the polymorphic `+`
/// instantiated at `(Int, Int)` must still return `Int`,
/// bit-identical to the pre-widening monomorphic shape.
#[test]
fn widen_plus_keeps_int_int_int() {
let ty = synth_in_builtins_env(&app("+", vec![lit_int(1), lit_int(2)]));
assert_eq!(ty, Type::int(), "(+ 1 2) must still type as Int");
}
/// new acceptance — `(+ 1.5 2.5)` types as `Float`.
/// Pre-widening this would have failed with `TypeMismatch` because
/// `+` was monomorphic `(Int, Int) -> Int`. The widening to
/// `forall a. (a, a) -> a` makes Float arithmetic a typecheck-clean
/// shape; codegen filtering of the {Int, Float} arg-type set
/// happens in iter 4.
#[test]
fn widen_plus_accepts_float_float_float() {
let bits_a = 1.5_f64.to_bits();
let bits_b = 2.5_f64.to_bits();
let ty = synth_in_builtins_env(&app("+", vec![lit_float(bits_a), lit_float(bits_b)]));
assert_eq!(ty, Type::float(), "(+ 1.5 2.5) must type as Float");
}
// 2026-05-21 operator-routing-eq-ord: the two `widen_lt_*` smoke
// tests (Int-Int-Bool + Float-Float-Bool) lived inside the builtin
// env and pinned `<` as a builtin. With `<` deleted from the
// builtin table, the tests are unreachable — comparison is now a
// class-method (`lt` via prelude.Ord) for Int/Bool/Str and a
// named fn (`float_lt`) for Float, neither of which is in the
// `synth_in_builtins_env`-visible namespace. Coverage of the new
// surface lives in `eq_ord_e2e.rs` (Ord class-dispatch) and in
// `float_compare_smoke_e2e.rs` (named-fn Float path).
/// `neg` is polymorphic — `forall a. (a) -> a`.
/// Both `(neg 5) : Int` and `(neg 1.5) : Float` typecheck. The
/// polymorphic shape is the same as the widened arithmetic ops, so
/// Int and Float negation share one symbol; codegen dispatches on
/// the resolved arg type at the call site (iter 4).
#[test]
fn install_neg_is_polymorphic() {
let ty_int = synth_in_builtins_env(&app("neg", vec![lit_int(5)]));
assert_eq!(ty_int, Type::int(), "(neg 5) must type as Int");
let bits = 1.5_f64.to_bits();
let ty_float = synth_in_builtins_env(&app("neg", vec![lit_float(bits)]));
assert_eq!(ty_float, Type::float(), "(neg 1.5) must type as Float");
}
/// `int_to_float : (Int) -> Float`. The
/// monomorphic conversion builtin — codegen lowers via `sitofp` in
/// iter 4. Typecheck only validates the signature here.
#[test]
fn install_int_to_float_signature() {
let ty = synth_in_builtins_env(&app("int_to_float", vec![lit_int(5)]));
assert_eq!(ty, Type::float(), "(int_to_float 5) must type as Float");
}
/// `float_to_int_truncate : (Float) -> Int`.
/// Saturating truncation toward zero per spec A4 — typecheck only
/// validates the signature; semantics is iter 4's codegen lowering
/// via `@llvm.fptosi.sat.i64.f64`.
#[test]
fn install_float_to_int_truncate_signature() {
let bits = 1.5_f64.to_bits();
let ty = synth_in_builtins_env(&app("float_to_int_truncate", vec![lit_float(bits)]));
assert_eq!(ty, Type::int(), "(float_to_int_truncate 1.5) must type as Int");
}
/// `float_to_str : (Float) -> Str`. Codegen lowers
/// via runtime C glue in iter 4; typecheck only validates the signature.
#[test]
fn install_float_to_str_signature() {
let bits = 1.5_f64.to_bits();
let ty = synth_in_builtins_env(&app("float_to_str", vec![lit_float(bits)]));
assert_eq!(ty, Type::str_(), "(float_to_str 1.5) must type as Str");
}
/// `int_to_str : (Int) -> Str`. Codegen lowers via the
/// runtime C glue `ailang_int_to_str` from `runtime/str.c`.
#[test]
fn install_int_to_str_signature() {
let ty = synth_in_builtins_env(&app("int_to_str", vec![lit_int(42)]));
assert_eq!(ty, Type::str_(), "(int_to_str 42) must type as Str");
}
/// `bool_to_str : (Bool) -> Str`. Codegen lowers via the
/// runtime C glue `ailang_bool_to_str` from `runtime/str.c`.
#[test]
fn install_bool_to_str_signature() {
let ty = synth_in_builtins_env(&app(
"bool_to_str",
vec![Term::Lit { lit: Literal::Bool { value: true } }],
));
assert_eq!(ty, Type::str_(), "(bool_to_str true) must type as Str");
}
/// `str_clone : (Str borrow) -> Str`. Codegen lowers via the
/// runtime C glue `ailang_str_clone` from `runtime/str.c`.
#[test]
fn install_str_clone_signature() {
let ty = synth_in_builtins_env(&app(
"str_clone",
vec![Term::Lit { lit: Literal::Str { value: "hi".into() } }],
));
assert_eq!(ty, Type::str_(), "(str_clone \"hi\") must type as Str");
}
#[test]
fn install_str_concat_signature() {
// `str_concat : (Str borrow, Str borrow) -> Str
// own`. Codegen lowers via the runtime C glue `ailang_str_concat`
// from `runtime/str.c`.
let mut env = Env::default();
install(&mut env);
let ty = env
.globals
.get("str_concat")
.expect("str_concat must be installed");
match ty {
Type::Fn {
params,
ret,
effects,
param_modes,
ret_mode,
} => {
assert_eq!(params.len(), 2);
assert!(matches!(params[0], Type::Con { ref name, .. } if name == "Str"));
assert!(matches!(params[1], Type::Con { ref name, .. } if name == "Str"));
assert!(matches!(**ret, Type::Con { ref name, .. } if name == "Str"));
assert!(effects.is_empty(), "str_concat must be effect-free");
assert_eq!(
param_modes,
&vec![
ailang_core::ast::ParamMode::Borrow,
ailang_core::ast::ParamMode::Borrow,
]
);
assert_eq!(*ret_mode, ailang_core::ast::ParamMode::Own);
}
other => panic!("expected Type::Fn; got {other:?}"),
}
}
/// `is_nan : (Float) -> Bool`. Codegen lowers to
/// `fcmp uno double %x, %x` in iter 4 — typecheck only validates
/// the signature here.
#[test]
fn install_is_nan_signature() {
let bits = 1.5_f64.to_bits();
let ty = synth_in_builtins_env(&app("is_nan", vec![lit_float(bits)]));
assert_eq!(ty, Type::bool_(), "(is_nan 1.5) must type as Bool");
}
/// `nan`, `inf`, `neg_inf` are bare-value constants
/// of type `Float`. They are NOT functions — reference site is
/// `(var nan)`, not `(app nan)`. Parallel to `__unreachable__` which
/// is `forall a. a`, but here the type is the concrete `Float`
/// instead of the polymorphic bottom — these constants always denote
/// a specific `f64` bit pattern.
#[test]
fn install_float_constants() {
let ty_nan = synth_in_builtins_env(&Term::Var { name: "nan".into() });
assert_eq!(ty_nan, Type::float(), "nan must type as Float");
let ty_inf = synth_in_builtins_env(&Term::Var { name: "inf".into() });
assert_eq!(ty_inf, Type::float(), "inf must type as Float");
let ty_neg_inf = synth_in_builtins_env(&Term::Var { name: "neg_inf".into() });
assert_eq!(ty_neg_inf, Type::float(), "neg_inf must type as Float");
}
/// pattern-matching on Float literals is hard-
/// rejected at typecheck per spec line 723-735 recommendation (a).
/// IEEE-`==` semantics make Float patterns semantically dubious
/// (NaN never matches; equality is bit-exact not approximate).
/// Surface lex / parser accept the syntax (iter 2); typecheck
/// surfaces the error here.
#[test]
fn reject_float_pattern_in_match() {
use ailang_core::ast::{Arm, Pattern};
let bits = 1.5_f64.to_bits();
let scrut = lit_float(bits);
let arm = Arm {
pat: Pattern::Lit { lit: Literal::Float { bits } },
body: lit_int(0),
};
let term = Term::Match {
scrutinee: Box::new(scrut),
arms: vec![arm],
};
let mut env = Env::default();
install(&mut env);
let mut locals: IndexMap<String, Type> = IndexMap::new();
let mut effects: BTreeSet<String> = BTreeSet::new();
let mut subst = Subst::default();
let mut counter: u32 = 0;
let mut residuals = Vec::new();
let mut free_fn_calls = Vec::new();
let mut warnings: Vec<crate::diagnostic::Diagnostic> = Vec::new();
// loop-recur iter 2: test helper synths one term from top-of-
// body — fresh empty loop-stack.
let mut loop_stack: Vec<Vec<(String, Type)>> = Vec::new();
let err = crate::synth(
&term,
&env,
&mut locals,
&mut loop_stack,
&mut effects,
"<test>",
&mut subst,
&mut counter,
&mut residuals,
&mut free_fn_calls,
&mut warnings,
&mut crate::linearity::LetBinderTypes::new(),
)
.expect_err("must reject");
assert!(
matches!(err, crate::CheckError::FloatPatternNotAllowed),
"expected FloatPatternNotAllowed, got {err:?}"
);
}
}