832375f2ac
All 176 files in the four accumulating directories now use a zero-padded 4-digit counter prefix that reflects creation order (`NNNN-slug.md`). The counter is assigned per directory in strict git-log creation order; ties broken alphabetically by original name. The old `YYYY-MM-DD-` prefix on docs/specs/ and docs/plans/ files is dropped — the date is recoverable from git log and the counter carries the ordering. A file's counter is stable for the life of the file: never reassigned, never reused, never compacted. Deleted files retire their counter; subsequent files do not fill the gap. This is the property that lets cross-references stay literal — refs use the full filename including the counter (`design/contracts/0007-honesty-rule.md`) so they grep cleanly and resolve directly without a glob step. 313 cross-references updated across .md/.rs/.toml/.c/.json files (test pins, include_str! paths, design-INDEX entries, baseline notes, runtime C comments, inter-contract markdown links incl. bare basename and `../models/foo.md` forms). CLAUDE.md gets a new "File-naming convention" section spelling out the rule and rationale. skills/brainstorm/SKILL.md and skills/planner/SKILL.md updated so new spec/plan creation produces counter-prefixed names from the start. The full test suite (cargo test --workspace) passes.
141 lines
6.1 KiB
Rust
141 lines
6.1 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`],
|
|
//! [`workspace::load_modules_with`], and [`workspace::build_workspace`].
|
|
//! Core exposes the two-phase composition: DFS-only loader returning a
|
|
//! modules-map, then a separate validation + registry-construction
|
|
//! step that takes a caller-supplied `implicit_imports` slice. The
|
|
//! public single-call entry point lives in `ailang-surface` (see
|
|
//! [`ailang_surface::load_workspace`](https://docs.rs/ailang-surface)),
|
|
//! which composes the two halves with a prelude-inject step in
|
|
//! between. pd.2 retired the pd.1 `load_workspace_with` shim and
|
|
//! moved the prelude embed into surface.
|
|
//! - **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`].
|
|
//! - Assemble a workspace from an entry module plus all imports:
|
|
//! `ailang_surface::load_workspace` (which composes
|
|
//! [`workspace::load_modules_with`] + a prelude inject +
|
|
//! [`workspace::build_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 primitives;
|
|
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::{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";
|
|
|
|
/// Complete LLM-targeted specification of Form-A — the canonical
|
|
/// authoring surface (see `design/contracts/0001-authoring-surface.md`).
|
|
/// Embedded verbatim into any prompt that asks an LLM to produce or
|
|
/// edit AILang code; in particular, `ail merge-prose` includes it in
|
|
/// the round-trip prompt template. The string is the raw bytes of
|
|
/// `specs/form_a.md`, co-located with this crate so the spec sits
|
|
/// next to the AST it describes; a drift test
|
|
/// (`tests/spec_drift.rs`) walks every AST enum variant and asserts
|
|
/// its serde tag appears in this string. Adding a new variant
|
|
/// without updating the spec fails the test suite.
|
|
pub const FORM_A_SPEC: &str = include_str!("../specs/form_a.md");
|
|
|
|
/// 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 `ailang_surface::load_workspace`
|
|
/// (which composes [`workspace::load_modules_with`] + a prelude inject
|
|
/// + [`workspace::build_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)
|
|
}
|