Files
AILang/crates/ailang-check/src/pre_desugar_validation.rs
T
Brummel 078c39a76a iter prep.2-term-new-construct (DONE 4/4): Term::New AST + Form-A surface + checker + drift — closes #32
Second iteration of the kernel-extension-mechanics milestone. Ships
the `new` Form-A keyword + `Term::New { type_name, args: Vec<NewArg> }`
AST variant + `NewArg::Type|Value` enum end-to-end through schema /
surface / checker, with two new diagnostics
(`NewTypeNotConstructible`, `NewArgKindMismatch`), a new arm in
prep.1's workspace-wide normalisation pre-pass `qualify_workspace_term`
that rewrites bare `Term::New.type_name` to qualified form
(symmetric to the existing `Term::Ctor` arm), and the data-model.md
+ form_a.md anchor additions. Codegen out of scope per spec; codegen
sites get `CodegenError::Internal` arms naming the raw-buf milestone
as the carrier.

Verification:

- `cargo test --workspace`: 654 tests passing, 0 failed.
- 3 new in-source checker tests pin the elaboration paths:
  `new_resolves_via_type_scope` (the spec's worked Counter example
  checks cleanly), `new_type_not_constructible` (missing `new` def
  in home module fires the new diagnostic), `new_arg_kind_mismatch_value_where_type`
  (kind-mismatch detection).
- 2 new schema-drift pins (`term_new_round_trips`,
  `term_new_type_arg_round_trips`) ratify the JSON byte shape:
  `{"t": "new", "type": "<TypeName>", "args": [{"kind": "type"|"value",
  "value": ...}]}`.
- 1 new surface round-trip pin exercises mixed-kind args through
  Form-A → JSON → Form-A.
- 44+ exhaustive Term-match sites across 24 files extended with
  `Term::New` arms — workspace-wide cargo build clean.

Concerns documented inline:

- `crates/ailang-core/tests/schema_coverage.rs` deliberately does
  NOT register `VariantTag::TermNew` in the `EXPECTED_VARIANTS`
  set. The coverage test asserts every declared variant is
  observed in the .ail fixture corpus; no .ail fixture in the
  current tree emits `"t": "new"` (codegen-runnable Term::New
  programs require the raw-buf milestone's plugin registry).
  Registering would fail the coverage check. The `visit_term` arm
  IS extended (compile-forced); only the enum registration is
  deferred. Inline rationale in the source. Future milestone that
  ships a Term::New fixture extends both sides.
- `crates/ailang-surface/src/print.rs` `write_term` Term::New arm
  was added in Task 1 (not Task 2 as planned), because without it
  ailang-core's tests fail to compile (the surface crate is in
  the dependency graph of ailang-core's test binaries). Task 2's
  round-trip pin verified the arm's correctness.
- Task 3 (synth elaboration) needed one implementer re-loop: the
  first attempt over-qualified within-module type-references via
  `qualify_local_types` on `new`'s signature when the home module
  was the calling module itself. Fixed by skipping qualification
  when `home_module == env.current_module`.

Architectural composition with prep.1: `Term::New` joins
`Term::Ctor` as a `type_name`-bearing site that goes through
`qualify_workspace_term`'s bare→qualified rewrite before the
checker sees it. The checker's `synth` arm for `Term::New`
therefore receives an already-qualified `type_name` (or a local
bare name for same-module construction).

Milestone status: kernel-extension-mechanics (Gitea #6) advances
2/3 iters. Next: prep.3 (kernel-tier modules + param-in), issue #33.
2026-05-28 15:31:13 +02:00

165 lines
5.5 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)
}
Term::Loop { binders, body } => {
for b in binders {
walk_term(&b.init)?;
}
walk_term(body)
}
Term::Recur { args } => {
for a in args {
walk_term(a)?;
}
Ok(())
}
// prep.2 (kernel-extension-mechanics): structural recurse
// through NewArg::Value subterms; NewArg::Type carries no term
// to validate at this stage (pre-desugar). Validity of the
// resolved `new` def is checked at synth time.
Term::New { args, .. } => {
for arg in args {
if let NewArg::Value(v) = arg {
walk_term(v)?;
}
}
Ok(())
}
}
}
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(())
}
}
}