Files
AILang/crates/ailang-check/src/pre_desugar_validation.rs
T

141 lines
4.7 KiB
Rust

//! 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(())
}
}
}