Files
AILang/crates/ailang-core/src/lib.rs
T
Brummel 0e90709a94 Iter 16a: nested constructor patterns via AST desugaring
Lifts the gate that rejected `(pat-ctor Cons a (pat-ctor Cons b _))`.
Approach: a pure AST → AST pass `ailang_core::desugar::desugar_module`
that flattens nested-Ctor sub-patterns into chains of single-level
matches with let-bound fresh vars and duplicate fall-through. Both
ailang-check and ailang-codegen call the pass at pipeline entry.
Hash-relevant canonical bytes are untouched: the pass runs after
load_module, in memory only. `check`'s returned CheckedModule.symbols
still carries hashes of the original defs so `ail diff` / `ail manifest`
see on-disk identities.

Lit sub-patterns inside a Ctor still rejected — that's the narrower
scope of `nested-ctor-pattern-not-allowed` post-16a; the variant doc
reflects the new meaning.

Fresh-name safety: `$` is a valid ident character in form (A), so
the Desugarer pre-walks every Term::Var / Pattern::Var name into a
BTreeSet and bumps its counter past any user-spelled `$mp_N`.

Already-flat matches go through `is_flat()` and emit identical AST
shapes; every existing fixture produces the same output as before.

New: examples/nested_pat.{ailx,ail.json} prints 30 from
first_two_sum on [10, 20, 30]. e2e test
`nested_ctor_pattern_first_two_sum` guards the path.

Tests: 92/92 (e2e 32→33, ailang-core unit 10→12).

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-05-07 19:10:01 +02:00

116 lines
4.7 KiB
Rust

//! Foundation crate of the AILang toolchain.
//!
//! `core` owns the data model that every other crate consumes:
//!
//! - **AST** — see [`ast`]. The source of an AILang translation unit is an
//! [`ast::Module`] expressed as JSON; the types here mirror that schema
//! one-to-one via serde.
//! - **Canonical JSON** — see [`canonical`]. A deterministic byte form
//! (no whitespace, lexicographically sorted object keys) used as the
//! pre-image for hashing.
//! - **Content-addressed hashes** — see [`hash`] and [`def_hash`]. Each
//! definition gets a stable 16-hex-char BLAKE3 prefix derived from its
//! canonical bytes, which is how the rest of the pipeline refers to it.
//! - **Pretty-printing** — see [`pretty`]. AST → human-readable text for
//! diff/review. The JSON form remains the source of truth.
//! - **Workspace loading** — see [`workspace`] and [`load_workspace`].
//! Resolves transitive imports of an entry module and returns a
//! [`Workspace`].
//! - **AST → AST desugaring** — see [`desugar`]. Pure rewrite that runs
//! *after* `load_module` and before the typechecker / codegen
//! consume a module. Iter 16a flattens nested constructor patterns
//! in `match` so downstream stages always see single-level patterns;
//! canonical bytes / hashes are unaffected because the pass runs in
//! memory only.
//!
//! ## Place in the pipeline
//!
//! `core` → `check` (typecheck) → `codegen` (LLVM emission) → `ail` CLI.
//! `core` has no upstream dependencies inside the workspace; everything
//! else depends on it.
//!
//! ## Central invariant
//!
//! Canonical JSON is **deterministic**: the byte output of
//! [`canonical::to_bytes`] for a given value never changes across runs,
//! versions of `serde_json`, or platforms. Hashes are
//! **content-addressed**: equal canonical bytes ⇒ equal hash. Schema
//! identifier is `ailang/v0`; a [`Module`] whose `schema` field disagrees
//! is rejected at load time. Schema extensions added in later iterations
//! (e.g. parameterised ADTs in Iter 13a) are written so that all
//! pre-existing on-disk hashes remain bit-identical — see
//! [`ast::TypeDef::vars`] and the `args` field of [`ast::Type::Con`].
//!
//! ## Entry points
//!
//! - Read a single module from disk: [`load_module`].
//! - Read an entry module plus all imports: [`load_workspace`].
//! - Hash a [`Def`]: [`def_hash`]. Hash a whole [`Module`]:
//! [`workspace::module_hash`].
//! - Form-(A) text projection of a [`Module`]: see
//! `ailang-surface::print` (round-trippable; the inverse of
//! `ailang-surface::parse`).
//! - Diagnostic stringification (manifest summary, type/pattern
//! for error messages): [`pretty::manifest`],
//! [`pretty::type_to_string`], [`pretty::pattern_to_string`].
pub mod ast;
pub mod canonical;
pub mod desugar;
pub mod hash;
pub mod pretty;
pub mod workspace;
pub use ast::{
def_kind, def_name, ConstDef, Def, FnDef, Import, Literal, Module, Term, Type,
};
pub use hash::def_hash;
pub use workspace::{load_workspace, module_hash, Workspace, WorkspaceLoadError};
/// Errors raised while loading a single module file.
///
/// Workspace-level loading uses a richer enum that wraps this one; see
/// [`WorkspaceLoadError`].
#[derive(Debug, thiserror::Error)]
pub enum Error {
/// Module file parses as JSON but its `schema` field does not match
/// [`SCHEMA`].
#[error("schema mismatch: expected {expected:?}, got {got:?}")]
SchemaMismatch { expected: String, got: String },
/// Module file failed to parse as JSON or did not match the AST
/// schema (missing/unexpected fields).
#[error("json: {0}")]
Json(#[from] serde_json::Error),
/// File could not be read.
#[error("io: {0}")]
Io(#[from] std::io::Error),
}
/// Result alias scoped to the crate-level [`Error`].
pub type Result<T> = std::result::Result<T, Error>;
/// Schema identifier this version of `core` accepts. Every loaded
/// [`Module`] must carry exactly this string in its `schema` field, or
/// loading fails with [`Error::SchemaMismatch`].
pub const SCHEMA: &str = "ailang/v0";
/// Load a single module file, validate its schema, and deserialize it
/// into a [`Module`].
///
/// This is the lowest-level entry point and does **not** resolve imports
/// — for the transitive closure use [`load_workspace`]. Errors are
/// returned as [`Error::Io`] / [`Error::Json`] / [`Error::SchemaMismatch`].
pub fn load_module(path: &std::path::Path) -> Result<Module> {
let bytes = std::fs::read(path)?;
let module: Module = serde_json::from_slice(&bytes)?;
if module.schema != SCHEMA {
return Err(Error::SchemaMismatch {
expected: SCHEMA.to_string(),
got: module.schema,
});
}
Ok(module)
}