diff --git a/crates/ailang-check/src/lib.rs b/crates/ailang-check/src/lib.rs index 9324cd0..c34dad5 100644 --- a/crates/ailang-check/src/lib.rs +++ b/crates/ailang-check/src/lib.rs @@ -38,6 +38,7 @@ use indexmap::IndexMap; use std::collections::{BTreeMap, BTreeSet}; mod linearity; +mod pre_desugar_validation; mod reuse_shape; mod suppress_filter; pub mod uniqueness; @@ -696,6 +697,30 @@ pub fn check_module(m: &Module) -> Vec { /// Module iteration order is deterministic: entry first, then the rest in /// BTreeMap order, so output ordering is stable. pub fn check_workspace(ws: &Workspace) -> Vec { + // Floats milestone post-fieldtest B1: hard-reject + // `Pattern::Lit { Literal::Float }` before desugar rewrites lit + // arms into `(== scrutinee lit)`. See `pre_desugar_validation` + // for the full rationale. Walked per-def so the diagnostic + // carries the offending def's name; modules are visited in + // BTreeMap order, defs in declaration order. All hits in this + // pass are accumulated and returned together (consistent with + // pass-2 multi-diagnose); if any are present, later passes do + // not run, since a violating Float-lit pattern would otherwise + // be silently rewritten by desugar before typecheck. + { + let mut pre_desugar_errors: Vec = Vec::new(); + for m in ws.modules.values() { + for def in &m.defs { + if let Err(e) = pre_desugar_validation::reject_float_patterns_in_def(def) { + let wrapped = CheckError::Def(def.name().to_string(), Box::new(e)); + pre_desugar_errors.push(wrapped.to_diagnostic()); + } + } + } + if !pre_desugar_errors.is_empty() { + return pre_desugar_errors; + } + } // Iter 16a: desugar every module of the workspace before any check // logic runs. The rewrite is pure and per-module; we rebuild a // workspace shell around the desugared modules (paths and entry @@ -800,6 +825,13 @@ pub struct CheckedModule { /// kept for legacy callers (snapshot tests, the `manifest` codepath /// that needs the hash-to-type mapping). pub fn check(m: &Module) -> Result { + // Floats milestone post-fieldtest B1: hard-reject + // `Pattern::Lit { Literal::Float }` before desugar runs, because + // `desugar::build_eq` rewrites lit-pattern arms (Float included) + // into `(== scrutinee lit)` and would otherwise hide the + // unreachable Float-pattern arm in `type_check_pattern`. See + // `pre_desugar_validation` for the full rationale. + pre_desugar_validation::reject_float_patterns_in_module(m)?; // Iter 16a: desugar nested ctor patterns before constructing the // trivial workspace. See `check_module` for the rationale. The // returned `CheckedModule.symbols` content hashes are derived from diff --git a/crates/ailang-check/src/pre_desugar_validation.rs b/crates/ailang-check/src/pre_desugar_validation.rs new file mode 100644 index 0000000..49c7cb1 --- /dev/null +++ b/crates/ailang-check/src/pre_desugar_validation.rs @@ -0,0 +1,140 @@ +//! Floats milestone post-fieldtest B1: pre-desugar walker that hard- +//! rejects `Pattern::Lit { lit: Literal::Float { .. } }`. +//! +//! Why a separate pre-desugar pass: +//! [`ailang_core::desugar::build_eq`] rewrites `Pattern::Lit` arms +//! (Int / Bool / Str / **Float**) into `Term::App { callee: ==, .. }` +//! before typecheck runs. As a consequence the in-typechecker +//! Float-pattern-reject arm in [`crate::type_check_pattern`] is +//! unreachable for any input that flows through the public check +//! entry points (`check`, `check_module`, `check_workspace`), all of +//! which call `desugar_module` first. +//! +//! This walker runs *before* desugar and short-circuits on the first +//! `Pattern::Lit { lit: Literal::Float }` it finds, returning the +//! bare [`CheckError::FloatPatternNotAllowed`]. Callers that build a +//! diagnostic (the multi-diagnose entry points) wrap the bare error +//! in [`CheckError::Def`] themselves so the diagnostic carries the +//! offending def's name. The in-typechecker arm at +//! [`crate::type_check_pattern`] remains as defence-in-depth for +//! direct `synth` callers (the `reject_float_pattern_in_match` unit +//! test in `crate::builtins` exercises that path). + +use ailang_core::ast::*; + +use crate::CheckError; + +/// Walks every `Term::Match` arm in `m` and returns +/// `Err(CheckError::FloatPatternNotAllowed)` on the first +/// `Pattern::Lit { lit: Literal::Float }` encountered. Defs are +/// visited in declaration order; the walk bails on the first hit. +/// +/// The returned error is bare (not wrapped in [`CheckError::Def`]). +/// The single-error entry point [`crate::check`] propagates it as-is +/// so callers asserting `matches!(err, CheckError::FloatPatternNotAllowed)` +/// stay clean. Multi-diagnose entry points should call +/// [`reject_float_patterns_in_def`] per def to attach the def name. +pub(crate) fn reject_float_patterns_in_module(m: &Module) -> Result<(), CheckError> { + for def in &m.defs { + reject_float_patterns_in_def(def)?; + } + Ok(()) +} + +/// Per-def variant of [`reject_float_patterns_in_module`]. Returns +/// the bare [`CheckError::FloatPatternNotAllowed`] on first hit; +/// callers wrap with [`CheckError::Def`] themselves to keep the +/// def-name attribution on the diagnostic. +pub(crate) fn reject_float_patterns_in_def(def: &Def) -> Result<(), CheckError> { + match def { + Def::Fn(f) => walk_term(&f.body), + Def::Const(c) => walk_term(&c.value), + Def::Type(_) => Ok(()), + Def::Class(c) => { + for method in &c.methods { + if let Some(default) = &method.default { + walk_term(default)?; + } + } + Ok(()) + } + Def::Instance(i) => { + for method in &i.methods { + walk_term(&method.body)?; + } + Ok(()) + } + } +} + +fn walk_term(t: &Term) -> Result<(), CheckError> { + match t { + Term::Lit { .. } | Term::Var { .. } => Ok(()), + Term::App { callee, args, .. } => { + walk_term(callee)?; + for a in args { + walk_term(a)?; + } + Ok(()) + } + Term::Let { value, body, .. } => { + walk_term(value)?; + walk_term(body) + } + Term::LetRec { body, in_term, .. } => { + walk_term(body)?; + walk_term(in_term) + } + Term::If { cond, then, else_ } => { + walk_term(cond)?; + walk_term(then)?; + walk_term(else_) + } + Term::Do { args, .. } => { + for a in args { + walk_term(a)?; + } + Ok(()) + } + Term::Ctor { args, .. } => { + for a in args { + walk_term(a)?; + } + Ok(()) + } + Term::Match { scrutinee, arms } => { + walk_term(scrutinee)?; + for arm in arms { + walk_pattern(&arm.pat)?; + walk_term(&arm.body)?; + } + Ok(()) + } + Term::Lam { body, .. } => walk_term(body), + Term::Seq { lhs, rhs } => { + walk_term(lhs)?; + walk_term(rhs) + } + Term::Clone { value } => walk_term(value), + Term::ReuseAs { source, body } => { + walk_term(source)?; + walk_term(body) + } + } +} + +fn walk_pattern(p: &Pattern) -> Result<(), CheckError> { + match p { + Pattern::Wild | Pattern::Var { .. } => Ok(()), + Pattern::Lit { + lit: Literal::Float { .. }, + } => Err(CheckError::FloatPatternNotAllowed), + Pattern::Lit { .. } => Ok(()), + Pattern::Ctor { fields, .. } => { + for sub in fields { + walk_pattern(sub)?; + } + Ok(()) + } + } +}