fix: Pattern::Lit::Float typecheck-reject is unreachable through check_module
This commit is contained in:
@@ -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(())
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user