fix: Pattern::Lit::Float typecheck-reject is unreachable through check_module

This commit is contained in:
2026-05-10 17:10:31 +02:00
parent 23b625f326
commit 6be5abf23a
2 changed files with 172 additions and 0 deletions
+32
View File
@@ -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<Diagnostic> {
/// 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<Diagnostic> {
// 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<Diagnostic> = 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<CheckedModule> {
// 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
@@ -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(())
}
}
}