design/ + source rustdoc: replace opaque shorthand with content phrases + links

Reader-facing prose and rustdoc carried opaque shorthand like
"Decision 10", "clause-5", "mq.1", "ct.1", "eob.1", "rpe.1",
"post-mq.3", and "Iter 22b.1:" with no in-repo definition the reader
could follow. This commit replaces every such occurrence in the
durable tier the reader is most likely to land on (design/ ledger +
source //! module headers + the central /// public-item rustdoc) with
an inline content phrase plus, where applicable, a Markdown link to
the file that defines the referenced concept.

design/ ledger — 16 files:
  Definition-site headings demoted from "Decision N: <title>" to
  "<title>": authoring-surface, tail-calls, memory-model section in
  rc-uniqueness.md, dual-allocator section, typeclass design,
  effects "pure core + algebraic effects".
  Cross-reference sites: "Decision 1" -> canonical-schema principle
  (data-model); "Decision 3/4" -> effects + scope-boundaries; "Decision
  6" -> authoring-surface; "Decision 8" -> tail-calls; "Decision 9" ->
  rc-uniqueness (dual-allocator); "Decision 10" -> memory-model;
  "Decision 11" -> typeclasses (model). "clause-5" -> body-link
  durability gate. "clause-3" (in language-constraints) ->
  bug-class-reintroduction discriminator. "mq.1/2/3", "ct.1/4",
  "eob.1", "rpe.1" -> the canonical-form rule / the type-driven
  dispatch / the Str carve-out / etc. "post-mq.3" -> "type-driven".

design/contracts/feature-acceptance.md: file-local "clauses 1/2/3"
-> "criteria 1/2/3" (sprachliche Kohärenz mit der File-Überschrift
"Feature-acceptance criterion"); "the clause-3 mechanism" -> "the
bug-class-reintroduction discriminator".

Source //! module headers — 24 files:
  Stripped "Iter X.Y:" prefixes and "(Decision N)" / "(mq.X)" tags
  from spec_drift, uniqueness, reuse_shape, migrate_canonical_types,
  typeclass_22b{2,3,c}, suppress_filter, lift, mono, linearity,
  diagnostic, method_dispatch_pin, method_collision_pin,
  no_per_type_print_ops, mq3_multi_class_e2e, print_mono_body_shape,
  print_no_leak_pin, cli_diag_human_workspace_load_error,
  ct1_check_cli, prose snapshot, unbound_in_instance_method_pin,
  mono_xmod_ctor_pattern, desugar.

Central /// public-item rustdoc:
  ast.rs (full sweep — every "Iter X" + "Decision N" prefix
  reformulated; mode/Type::Fn rustdoc now points at memory-model.md;
  Constraint / SuperclassRef / InstanceDef / ClassDef rustdoc points
  at typeclasses contract).
  diagnostic.rs (all "(Iter X)" / "(mq.X)" tags on diagnostic codes
  removed).
  lib.rs (FORM_A_SPEC rustdoc points at authoring-surface.md
  instead of "Decision 6").
  canonical.rs (type_hash + Float-literal rustdoc).

Still outstanding (for a follow-up commit): ~500 inline `//`
code-body comments with `Iter X.Y` markers across the workspace, and
a handful of `///` rustdoc items in hash_pin / workspace_pin / lift /
mono / suppress_filter test-pin and internal-function bodies. Code
identifiers (test filenames like `mq3_multi_class_e2e.rs`, function
names like `iter18e_drop_iterative_default_preserves_hashes`) stay
verbatim per the user's "code identifiers stay verbatim" rule.

Tests: design_index_pin 5/5 + docs_honesty_pin 5/5; workspace builds
clean; full `cargo test --workspace` previously green (every
`test result: ok` line, no FAILED line).
This commit is contained in:
2026-05-20 09:47:33 +02:00
parent 3e087d759a
commit bcd41810f4
46 changed files with 311 additions and 284 deletions
@@ -1,4 +1,4 @@
//! Iter cli-diag-human (2026-05-14): non-JSON `ail check` and
//! Human-readable CLI diagnostics (2026-05-14): non-JSON `ail check` and
//! sibling subcommands surface `WorkspaceLoadError` with the same
//! bracketed `[code]` prefix that the JSON path emits.
//!
+1 -1
View File
@@ -1,4 +1,4 @@
//! ct.1: E2E coverage for the CLI surface of the canonical-type-names
//! E2E coverage for the CLI surface of the canonical-type-names
//! validator. The unit tests in `workspace.rs` already prove the
//! validator fires; these tests prove the diagnostic survives the
//! `WorkspaceLoadError -> Diagnostic` translation in
+2 -1
View File
@@ -1,4 +1,5 @@
//! ct.1: E2E test for `ail migrate-canonical-types <dir>`.
//! E2E test for `ail migrate-canonical-types <dir>` (the
//! canonical-form migration for `Type::Con.name`).
//!
//! Builds a synthetic workspace in a tempdir with one fixture that
//! has a bare cross-module Type::Con ref, runs the migration, then
+10 -7
View File
@@ -4,7 +4,8 @@
//! by `mono::build_workspace_env`, which delegates to `crate::build_check_env`
//! and produces a workspace-flat `ctor_index` and `types` map.
//!
//! Post-ct.2, `Pattern::Ctor` lookup is type-driven — it consults the
//! After the canonical-form / type-driven-ctor-lookup refactor,
//! `Pattern::Ctor` lookup is type-driven — it consults the
//! scrutinee's canonical `Type::Con.name` to find the TypeDef directly
//! in `env.module_types`, then validates the ctor name within it. The
//! mono pass's flat `ctor_index` is no longer consulted by this path;
@@ -14,14 +15,16 @@
//!
//! This test pins the cross-module pattern shape against a minimal
//! 2-module fixture (`test_mono_ctor_main` + `test_mono_ctor_listmod`).
//! Pre-ct.2 the bug surfaced as `PatternTypeMismatch { ctor: "Cons",
//! Before the refactor the bug surfaced as
//! `PatternTypeMismatch { ctor: "Cons",
//! ty: "test_mono_ctor_listmod.List<Int>" }` because the mono env
//! resolved `Cons` to bare `List` via the flat index. Post-ct.2 the
//! lookup is type-driven and `expected.name == "test_mono_ctor_listmod.List"`
//! directly indexes the right TypeDef.
//! resolved `Cons` to bare `List` via the flat index. After the
//! refactor the lookup is type-driven and
//! `expected.name == "test_mono_ctor_listmod.List"` directly indexes
//! the right TypeDef.
//!
//! Surfaced by iter 23.2 Task 3, which adds `class Eq a` + Eq Int/Bool/Str
//! instances to `examples/prelude.ail.json`, flipping the
//! Surfaced when `class Eq a` + Eq Int/Bool/Str instances were added
//! to `examples/prelude.ail.json`, flipping the
//! `workspace_has_typeclasses` gate so every workspace exercises the
//! mono pass.
+1 -1
View File
@@ -1,4 +1,4 @@
//! mq.3.6: end-to-end coverage of the post-`MethodNameCollision`-
//! End-to-end coverage of the post-`MethodNameCollision`-
//! retirement multi-candidate dispatch path. Three positive fixtures
//! exercise the three trajectories from the milestone spec
//! §"Data flow":
+2 -1
View File
@@ -4,7 +4,8 @@
//! structurally `Term::Let { name: "s", value: App(show__Int, [x]),
//! body: Term::Do { op: "io/print_str", args: [s] } }`. The explicit
//! let-binder around `show__Int x` is load-bearing for the heap-Str RC
//! discipline (eob.1 Str carve-out at `drop_symbol_for_binder` requires
//! discipline (the heap-Str Str carve-out at
//! `drop_symbol_for_binder` requires
//! a let-binder to attach the rc-dec to). If a future codegen / mono
//! refactor inlines the let-binder away, this pin fires and surfaces
//! the regression BEFORE the E2E runtime stats produce a confusing
+2 -1
View File
@@ -1,4 +1,5 @@
//! RED-pin for the 2026-05-14 rpe.1 Cat-A heap-Str leak in `print`.
//! RED-pin for the 2026-05-14 Cat-A heap-Str leak in `print`
//! (uncovered during the per-type-print-op retirement).
//!
//! Property protected: under `--alloc=rc`, evaluating the trivial
//! program `(body (app print 42))` does NOT leak the heap-Str
+3 -2
View File
@@ -1,6 +1,7 @@
//! Iter 22b.2 typeclass typecheck arms — integration tests.
//! Typeclass typecheck arms — integration tests.
//!
//! This file is shared by 22b.2 tasks: it exists from Task 8
//! This file holds the cluster of typeclass typecheck-arm integration
//! tests; it exists from Task 8
//! (class methods register into module globals) and is extended by
//! Tasks 9 (`missing-constraint`) and 10 (`no-instance`).
+1 -1
View File
@@ -1,4 +1,4 @@
//! Iter 22b.3: monomorphisation pass tests.
//! Monomorphisation pass tests.
//!
//! Co-located with `typeclass_22b2.rs` so the typeclass-feature
//! coverage is browsable in one directory. Tests use the same
+3 -3
View File
@@ -1,6 +1,6 @@
//! Iter 22c: milestone-22 acceptance user-defined classes with
//! instances over user-defined ADTs end-to-end (typecheck → mono →
//! codegen → binary). The 22b.3 mono pass was tested only against
//! Milestone-22 acceptance: user-defined classes with instances over
//! user-defined ADTs end-to-end (typecheck → mono → codegen →
//! binary). The mono pass was previously tested only against
//! instances over primitive types; this file pins the user-ADT path.
//!
//! Co-located with `typeclass_22b3.rs` so the typeclass-feature
@@ -23,8 +23,9 @@
//! Root cause (from debugger Phase 1-2):
//! `crates/ailang-check/src/lib.rs::check_def` early-returns `Ok(())`
//! for `Def::Class | Def::Instance` (the comment claims body
//! typechecking landed in iter 22b.2, but the body-walk was never
//! wired). Only the workspace-load coherence checks
//! typechecking landed alongside the typeclass-typecheck arms, but
//! the body-walk was never wired). Only the workspace-load coherence
//! checks
//! (Orphan/Duplicate/MissingMethod) in `workspace::build_registry`
//! touch instance defs, and those only inspect the schema, not the
//! method-body identifier graph.
+10 -10
View File
@@ -39,7 +39,7 @@
//! - `module-cycle` — workspace loader (Iter 5b, in the CLI path)
//! - `module-name-mismatch` — workspace loader (Iter 5b, in the CLI path)
//! - `module-hash-mismatch` — workspace loader (Iter 5b, in the CLI path)
//! - `tail-call-not-in-tail-position` (Iter 14e, see Decision 8)
//! - `tail-call-not-in-tail-position` (see `design/contracts/tail-calls.md`)
//! - `use-after-consume` — `ctx`: `{"binder": "<n>"}` (Iter 18c.2);
//! carries non-empty [`Diagnostic::suggested_rewrites`] showing how to
//! spell the fix in form-A AILang.
@@ -61,38 +61,38 @@
//! the build refuses it. Note that an invalid suppression does NOT
//! suppress its target diagnostic — the original still fires
//! alongside this error.
//! - `missing-constraint` (Iter 22b.2) — `severity: error`. Emitted
//! - `missing-constraint` — `severity: error`. Emitted
//! by the per-fn typecheck arm when a polymorphic `FnDef` calls a
//! class method (e.g. `show x` where `x: a`) but its declared
//! `Forall.constraints` (after one-step superclass expansion) does
//! not include the residual class constraint. `ctx`:
//! `{"class": "<C>", "method": "<m>", "at_type": "<a>"}`. Concrete-
//! type residuals are deferred to the `no-instance` diagnostic.
//! - `no-instance` (Iter 22b.2) — `severity: error`. Emitted by the
//! - `no-instance` — `severity: error`. Emitted by the
//! per-fn typecheck arm when a class-method call resolves the class
//! param to a fully-concrete type that has no matching entry in the
//! workspace instance registry (`(class, canonical-type-hash)`).
//! Dual of `missing-constraint`: when the residual is concrete, the
//! fn cannot push the obligation to a caller, so an existing
//! instance is the only way to discharge it. `ctx`:
//! `{"class": "<C>", "method": "<m>", "at_type": "<T>"}`. mq.2 adds
//! an optional `candidate_classes` field when the residual originated
//! from the multi-candidate dispatch path; absent on single-class
//! residuals (back-compat).
//! - `ambiguous-method-resolution` (mq.2) — `severity: error`. Emitted
//! `{"class": "<C>", "method": "<m>", "at_type": "<T>"}`. An
//! optional `candidate_classes` field is added when the residual
//! originated from the multi-candidate dispatch path; absent on
//! single-class residuals (back-compat).
//! - `ambiguous-method-resolution` — `severity: error`. Emitted
//! by the type-driven dispatch resolver (or the discharge-time
//! multi-candidate refinement) when a bare-method call site survives
//! both type-driven and constraint-driven filtering with more than
//! one candidate class. `ctx`: `{"method": "<m>", "at_type": "<T>",
//! "candidate_classes": ["<C1>", ...]}`. The author disambiguates
//! by writing `<ClassQualifier>.<method> x`.
//! - `unknown-class` (mq.2) — `severity: error`. Emitted by the
//! - `unknown-class` — `severity: error`. Emitted by the
//! type-driven dispatch resolver when an explicit class qualifier
//! in a `Term::Var.name` (e.g. `"prelude.Show.show"`) names a
//! qualified class that is not in the workspace registry of
//! candidate classes for the method. `ctx`:
//! `{"name": "<qualified-class>"}`.
//! - `class-method-shadowed-by-fn` (mq.3) — `severity: warning`.
//! - `class-method-shadowed-by-fn` — `severity: warning`.
//! Emitted by `synth`'s `Term::Var` arm when a name resolves to a
//! free fn (locals / caller-module-fn / imported-fn) AND a class
//! method of the same name also exists in the workspace. Fn
+1 -1
View File
@@ -1,4 +1,4 @@
//! Iter 16b.3: post-typecheck `Term::LetRec` lift.
//! Post-typecheck `Term::LetRec` lift.
//!
//! Background. The 16a desugar pass (`ailang-core::desugar::desugar_module`)
//! eliminates most `Term::LetRec` nodes by lifting them to synthetic
+1 -1
View File
@@ -1,4 +1,4 @@
//! Iter 18c.2: linearity check for fns whose every parameter mode is
//! Linearity check for fns whose every parameter mode is
//! explicit (`Borrow` or `Own`).
//!
//! ## Scope
+2 -2
View File
@@ -1,5 +1,5 @@
//! Workspace monomorphisation pass (introduced iter 22b.3; the
//! free-fn entry was added in iter 23.4).
//! Workspace monomorphisation pass — class-method entry plus
//! free-fn entry.
//!
//! Slots into the build pipeline after [`crate::lift_letrecs`] and
//! before `ailang_codegen::lower_workspace_with_alloc`. It is only
+1 -1
View File
@@ -1,4 +1,4 @@
//! Iter 18d.2: shape-compatibility check for `(reuse-as <var> <body>)`.
//! Shape-compatibility check for `(reuse-as <var> <body>)`.
//!
//! ## Why this is its own pass
//!
+1 -1
View File
@@ -1,4 +1,4 @@
//! Iter 19b: per-module post-process that consumes
//! Per-module post-process that consumes
//! [`ailang_core::ast::FnDef::suppress`] entries.
//!
//! For each `Def::Fn(f)` and each `Suppress { code, because }` in
+1 -1
View File
@@ -1,4 +1,4 @@
//! Iter 18c.3: uniqueness inference over the typed AST.
//! Uniqueness inference over the typed AST.
//!
//! This pass classifies every binder of every fn body as either
//! [`Uniqueness::Unique`] or [`Uniqueness::Shared`]. Unlike the
@@ -1,6 +1,7 @@
//! mq.3.5: repurposed pin tests for the post-`MethodNameCollision`-
//! retirement workspace-load path. The two on-disk fixtures that fired
//! `WorkspaceLoadError::MethodNameCollision` pre-mq.3 now load cleanly;
//! Repurposed pin tests for the post-`MethodNameCollision`-retirement
//! workspace-load path. The two on-disk fixtures that fired
//! `WorkspaceLoadError::MethodNameCollision` before the type-driven
//! dispatch refactor now load cleanly;
//! the assertion migrates from "expect collision diagnostic" to "load
//! successful + `Env.method_to_candidate_classes` carries the
//! expected multi-entry set" (class-class case) or "load successful +
@@ -1,4 +1,4 @@
//! mq.2.5: pin tests on `resolve_method_dispatch` — the new
//! Pin tests on `resolve_method_dispatch` — the
//! dispatch-resolution helper that synth's `Term::Var` arm consults
//! per the spec's 5-step rule.
//!
@@ -1,4 +1,4 @@
//! Hard gate for iter rpe.1: the per-type print effect-ops
//! Hard gate against re-introducing the per-type print effect-ops:
//! `io/print_int`, `io/print_bool`, `io/print_float` are retired
//! and must NOT appear in the builtin registry. The polymorphic
//! `print` helper (prelude, iter 24.3) is the canonical replacement;
+110 -108
View File
@@ -64,8 +64,8 @@ pub struct Import {
/// [`Def::name`] method) to inspect generically without matching every
/// variant.
///
/// Iter 22b.1 (Decision 11): adds `Class` and `Instance`. Both are
/// additive — pre-22b fixtures never produce these tags, so their
/// `Class` and `Instance` are additive — fixtures that predate
/// the typeclass layer never produce these tags, so their
/// canonical-JSON bytes (and therefore their content hashes) are
/// unchanged. The `Def`-level match exhaustiveness in downstream
/// crates is the only caller that has to acknowledge the variants.
@@ -78,9 +78,9 @@ pub enum Def {
Const(ConstDef),
/// Type (ADT) definition; see [`TypeDef`].
Type(TypeDef),
/// Iter 22b.1: typeclass declaration; see [`ClassDef`].
/// Typeclass declaration; see [`ClassDef`].
Class(ClassDef),
/// Iter 22b.1: instance declaration; see [`InstanceDef`].
/// Instance declaration; see [`InstanceDef`].
Instance(InstanceDef),
}
@@ -136,9 +136,8 @@ pub fn def_kind(def: &Def) -> &'static str {
pub struct TypeDef {
/// Type name (capitalised by convention).
pub name: String,
/// Type parameters (Iter 13a parameterised-ADT support). A
/// monomorphic ADT has `vars` empty and is serialized identically
/// to the pre-13a schema (the field is **omitted** when empty),
/// Type parameters for parameterised ADTs. A monomorphic ADT has
/// `vars` empty and the field is **omitted** when empty,
/// preserving the canonical-JSON hash of every existing module on
/// disk. See the regression test
/// `iter13a_schema_extension_preserves_pre_13a_hashes` in
@@ -151,7 +150,7 @@ pub struct TypeDef {
/// Optional source-level documentation string.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub doc: Option<String>,
/// Iter 18e: opt-in `(drop-iterative)` annotation. When `true`,
/// Opt-in `(drop-iterative)` annotation. When `true`,
/// codegen emits `drop_<m>_<T>` with an iterative worklist body
/// instead of the recursive cascade — chosen by the LLM-author when
/// the type is expected to form long chains (millions of cells)
@@ -159,9 +158,9 @@ pub struct TypeDef {
///
/// Serialised as `"drop-iterative": true` (kebab-case) when set;
/// the field is omitted when `false` so canonical-JSON hashes of
/// every pre-18e fixture remain bit-stable. See the regression
/// test `iter18e_drop_iterative_default_preserves_hashes` in
/// [`crate::hash`].
/// every fixture that does not opt in remain bit-stable. See the
/// regression test `iter18e_drop_iterative_default_preserves_hashes`
/// in [`crate::hash`].
#[serde(
default,
rename = "drop-iterative",
@@ -219,24 +218,24 @@ pub struct FnDef {
/// the same additive-schema pattern as [`FnDef::doc`].
#[serde(default, skip_serializing_if = "Option::is_none")]
pub export: Option<String>,
/// Iter 19b: structured-diagnostic suppressions opted into for this
/// Structured-diagnostic suppressions opted into for this
/// fn. Each entry names a diagnostic code and an author-asserted
/// reason. Currently the only consumer is `over-strict-mode`
/// (Iter 19a / 19a.1) but the mechanism is generic across codes.
/// `because` must be non-empty — the typechecker emits
/// `empty-suppress-reason` (Error) otherwise.
/// reason. The only current consumer is `over-strict-mode`, but
/// the mechanism is generic across codes. `because` must be
/// non-empty — the typechecker emits `empty-suppress-reason`
/// (Error) otherwise.
///
/// Serialised with `skip_serializing_if = "Vec::is_empty"` so
/// every pre-19b fixture's canonical-JSON hash stays bit-identical.
/// The same additive-schema pattern is used by [`TypeDef::vars`]
/// (Iter 13a) and [`Type::Con::args`] (Iter 13a).
/// every pre-suppress fixture's canonical-JSON hash stays
/// bit-identical. The same additive-schema pattern is used by
/// [`TypeDef::vars`] and [`Type::Con::args`].
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub suppress: Vec<Suppress>,
}
/// Iter 19b: one entry in [`FnDef::suppress`]. Marks a structured
/// diagnostic the author has consciously decided to allow on this
/// def, with a mandatory reason.
/// One entry in [`FnDef::suppress`]. Marks a structured diagnostic
/// the author has consciously decided to allow on this def, with a
/// mandatory reason.
///
/// `because` is non-empty by schema rule — the typechecker emits
/// `empty-suppress-reason` (Error severity) when it is empty or
@@ -256,20 +255,22 @@ pub struct Suppress {
pub because: String,
}
/// Iter 22b.1: a typeclass declaration (Decision 11).
/// A typeclass declaration (narrative in
/// `design/contracts/typeclasses.md`).
///
/// Single-parameter, multi-method, optional-default, optional-superclass
/// typeclass. The `param` is a single string — multi-param classes are
/// rejected by Decision 11 axis 1, and the schema enforces it by shape
/// (`param: String`, not `Vec<String>`).
/// rejected by the typeclass design (concrete-types-only, kind `*`),
/// and the schema enforces it by shape (`param: String`, not
/// `Vec<String>`).
///
/// `superclass`, when present, MUST have its `type` field equal to
/// `param`. The check is enforced in 22b.2 via the
/// `InvalidSuperclassParam` diagnostic; the schema does not encode it.
/// `param`. The check is enforced via the `InvalidSuperclassParam`
/// diagnostic; the schema does not encode it.
///
/// All optional fields are omitted from canonical JSON when absent /
/// empty, so future schema evolution can land here without disturbing
/// pre-22b hashes.
/// existing hashes.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClassDef {
/// Class name (capitalised by convention). Bare — symmetric to
@@ -277,8 +278,7 @@ pub struct ClassDef {
/// a reference. Cross-module class references live in
/// `InstanceDef.class`, `Constraint.class`, and
/// `SuperclassRef.class`; those fields carry the canonical form
/// (bare for same-module, `<module>.<Class>` for cross-module)
/// per mq.1.
/// (bare for same-module, `<module>.<Class>` for cross-module).
pub name: String,
/// Single type-parameter name.
pub param: String,
@@ -292,20 +292,21 @@ pub struct ClassDef {
pub doc: Option<String>,
}
/// Iter 22b.1: reference to a superclass relation in [`ClassDef`].
/// Reference to a superclass relation in [`ClassDef`].
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SuperclassRef {
/// Superclass name in canonical form (mq.1): bare for a
/// same-module class, `<module>.<Class>` for a cross-module
/// class. Symmetric to ct.1's `Type::Con.name` rule.
/// Superclass name in canonical form: bare for a same-module
/// class, `<module>.<Class>` for a cross-module class.
/// Symmetric to `Type::Con.name`'s canonical-form rule.
pub class: String,
/// Type the superclass is applied to. MUST equal the parent
/// `ClassDef.param` (validated in 22b.2 — schema does not enforce).
/// `ClassDef.param` (validated by typecheck — schema does not
/// enforce).
#[serde(rename = "type")]
pub type_: String,
}
/// Iter 22b.1: one method declared in a [`ClassDef`].
/// One method declared in a [`ClassDef`].
///
/// `default` is `None` when the method is abstract-required (every
/// instance must specify it); `Some(body)` when the method has a
@@ -324,7 +325,8 @@ pub struct ClassMethod {
pub default: Option<Term>,
}
/// Iter 22b.1: an instance declaration (Decision 11).
/// An instance declaration (narrative in
/// `design/contracts/typeclasses.md`).
///
/// `class` is the name of the class being instantiated. `type_` is the
/// concrete type expression the class is applied to — never the class
@@ -332,9 +334,9 @@ pub struct ClassMethod {
/// overrides of default-bearing methods.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InstanceDef {
/// Class being instantiated, in canonical form (mq.1): bare for
/// a same-module class, `<module>.<Class>` for a cross-module
/// class. Symmetric to ct.1's `Type::Con.name` rule.
/// Class being instantiated, in canonical form: bare for a
/// same-module class, `<module>.<Class>` for a cross-module
/// class. Symmetric to `Type::Con.name`'s canonical-form rule.
pub class: String,
/// Concrete type the class is applied to.
#[serde(rename = "type")]
@@ -346,28 +348,29 @@ pub struct InstanceDef {
pub doc: Option<String>,
}
/// Iter 22b.1: one method body in an [`InstanceDef`].
/// One method body in an [`InstanceDef`].
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InstanceMethod {
/// Method name (must match a method in the corresponding class).
pub name: String,
/// Method body. The class's declared method type with the class
/// param substituted to the instance type is the body's expected
/// type — checked in 22b.2.
/// type.
pub body: Term,
}
/// Iter 22b.2: a class constraint on a polymorphic function (Decision
/// 11). `(class, type)` pair where `class` is a class name and `type`
/// is a `Type` expression — typically a single `Type::Var` (e.g.
/// `(Eq, a)` for `Eq a => ...`). Concrete-type constraints are legal
/// schema-wise but fired as `no-instance` at typecheck time if no
/// matching registry entry exists.
/// A class constraint on a polymorphic function (narrative in
/// `design/contracts/typeclasses.md`). `(class, type)` pair where
/// `class` is a class name and `type` is a `Type` expression —
/// typically a single `Type::Var` (e.g. `(Eq, a)` for
/// `Eq a => ...`). Concrete-type constraints are legal schema-wise
/// but fired as `no-instance` at typecheck time if no matching
/// registry entry exists.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Constraint {
/// Class name in canonical form (mq.1): bare for a same-module
/// class, `<module>.<Class>` for a cross-module class.
/// Symmetric to ct.1's `Type::Con.name` rule.
/// Class name in canonical form: bare for a same-module class,
/// `<module>.<Class>` for a cross-module class. Symmetric to
/// `Type::Con.name`'s canonical-form rule.
pub class: String,
/// Type the class is applied to.
#[serde(rename = "type")]
@@ -408,11 +411,11 @@ pub enum Term {
/// Function application. `callee` is evaluated to a function value;
/// `args` are evaluated left-to-right.
///
/// Iter 14e: `tail` marks this call as occurring in tail position
/// (per Decision 8). When set, codegen lowers the call as
/// `musttail call`. The flag defaults to `false` and is omitted
/// during canonical-JSON serialisation when unset, so pre-14e
/// fixtures keep bit-identical hashes.
/// `tail` marks this call as occurring in tail position (see
/// `design/contracts/tail-calls.md`). When set, codegen lowers
/// the call as `musttail call`. The flag defaults to `false` and
/// is omitted during canonical-JSON serialisation when unset, so
/// pre-tail-flag fixtures keep bit-identical hashes.
App {
#[serde(rename = "fn")]
callee: Box<Term>,
@@ -426,7 +429,7 @@ pub enum Term {
value: Box<Term>,
body: Box<Term>,
},
/// Local recursive let-binding (Iter 16b.1). Always fn-shaped:
/// Local recursive let-binding. Always fn-shaped:
/// the bound name `name` is recursively visible inside `body`.
/// Eliminated by `crate::desugar` before typecheck — lifted to a
/// synthetic top-level fn when `body` does not capture any name
@@ -455,7 +458,7 @@ pub enum Term {
/// `match` in codegen (`lower_effect_op`); there is no
/// effect-handler table and no link-time resolution.
///
/// Iter 14e: see [`Term::App`] for the `tail` field semantics.
/// See [`Term::App`] for the `tail` field semantics.
Do {
op: String,
args: Vec<Term>,
@@ -479,7 +482,7 @@ pub enum Term {
scrutinee: Box<Term>,
arms: Vec<Arm>,
},
/// Anonymous function (Iter 8b). Captures any free variables of
/// Anonymous function. Captures any free variables of
/// `body` from the enclosing scope. Param/return types are
/// declared inline so the typechecker stays HM-monomorphic on
/// the inferred shape.
@@ -493,7 +496,7 @@ pub enum Term {
effects: Vec<String>,
body: Box<Term>,
},
/// Sequencing (Iter 10). `lhs` is evaluated for its effects and its
/// Sequencing. `lhs` is evaluated for its effects and its
/// result discarded; `rhs` is the value of the whole expression.
/// Equivalent to `let _ = lhs in rhs`, but with a dedicated node so
/// pretty-print and diagnostics read cleanly.
@@ -501,27 +504,25 @@ pub enum Term {
lhs: Box<Term>,
rhs: Box<Term>,
},
/// Iter 18c.1: explicit RC clone. Lowers identically to its inner
/// term in 18c.1; in 18c.3 the codegen will emit
/// Explicit RC clone. Codegen emits
/// `call void @ailang_rc_inc(ptr %v)` before returning `%v` under
/// `--alloc=rc`. The variant is additive: `(clone X)` round-trips
/// through every pre-18c.1 fixture without their hashes changing
/// because none of them use the new tag.
/// through every fixture that does not use the tag without its
/// hash changing.
Clone {
value: Box<Term>,
},
/// Iter 18d.1: explicit reuse-as hint. Wraps an allocating `body`
/// (typically `Term::Ctor`, also `Term::Lam`) and names a `source`
/// term whose memory slot the body's allocation should reuse.
/// Explicit reuse-as hint. Wraps an allocating `body` (typically
/// `Term::Ctor`, also `Term::Lam`) and names a `source` term
/// whose memory slot the body's allocation should reuse.
/// Conventionally `source` is a `Term::Var { name }` — the
/// linearity check rejects anything else with
/// `reuse-as-source-not-bare-var`. The body must be allocating;
/// non-allocating bodies are rejected at typecheck with
/// `reuse-as-non-allocating-body`. Lowers as identity in 18d.1
/// (returns `body`'s `(ssa, ty)`, ignores `source`); 18d.2 will
/// lower this as in-place rewrite under `--alloc=rc`. The variant
/// is additive — pre-18d fixtures keep their canonical-JSON hash
/// because none of them use the new tag.
/// `reuse-as-non-allocating-body`. Codegen lowers as in-place
/// rewrite under `--alloc=rc`. The variant is additive —
/// fixtures that do not use the tag keep their canonical-JSON
/// hash.
#[serde(rename = "reuse-as")]
ReuseAs {
source: Box<Term>,
@@ -685,7 +686,7 @@ pub enum Type {
/// and parameterised forms like `List<Int>`.
Con {
name: String,
/// Type arguments (Iter 13a). For pre-13a uses (`Int`, `Bool`,
/// Type arguments. For non-parameterised uses (`Int`, `Bool`,
/// non-parameterised user ADTs) this stays empty and is
/// **omitted** during serialization, so the canonical-JSON
/// hash of every pre-existing module remains bit-identical.
@@ -697,14 +698,15 @@ pub enum Type {
/// set; equality compares it modulo order (see the [`PartialEq`]
/// impl below).
///
/// Iter 18a (Decision 10): `param_modes` and `ret_mode` carry the
/// `(borrow T)` / `(own T)` wrappers from the surface form. They
/// are metadata on `Type::Fn`, not new `Type` variants — so
/// unification, occurs, apply, and every other `Type` match-arm
/// keeps working unchanged. `param_modes` is omitted from
/// canonical JSON when every entry is `Implicit`; `ret_mode` is
/// omitted when it is `Implicit`. Pre-18a fixtures therefore hash
/// bit-identically.
/// `param_modes` and `ret_mode` carry the `(borrow T)` /
/// `(own T)` wrappers from the surface form. They are metadata
/// on `Type::Fn`, not new `Type` variants — so unification,
/// occurs, apply, and every other `Type` match-arm keeps working
/// unchanged. `param_modes` is omitted from canonical JSON when
/// every entry is `Implicit`; `ret_mode` is omitted when it is
/// `Implicit`, so pre-mode-annotation fixtures hash
/// bit-identically. Full contract in
/// `design/contracts/memory-model.md`.
Fn {
params: Vec<Type>,
#[serde(default, skip_serializing_if = "all_implicit")]
@@ -726,9 +728,9 @@ pub enum Type {
/// names, instantiated fresh at each use site.
Forall {
vars: Vec<String>,
/// Iter 22b.2: class constraints quantified together with
/// `vars`. Empty for pre-22b.2 polymorphic types; serialised
/// with `skip_serializing_if = "Vec::is_empty"` so existing
/// Class constraints quantified together with `vars`. Empty
/// for unconstrained polymorphic types; serialised with
/// `skip_serializing_if = "Vec::is_empty"` so existing
/// canonical-JSON bytes stay bit-identical.
#[serde(default, skip_serializing_if = "Vec::is_empty")]
constraints: Vec<Constraint>,
@@ -759,11 +761,11 @@ impl Type {
Type::Con { name: "Float".into(), args: vec![] }
}
/// Iter 18a: build a `Type::Fn` with all parameter modes set to
/// `ParamMode::Implicit` and `ret_mode` set to `Implicit`. This is
/// the form every typechecker / desugar / codegen site that
/// Build a `Type::Fn` with all parameter modes set to
/// `ParamMode::Implicit` and `ret_mode` set to `Implicit`. This
/// is the form every typechecker / desugar / codegen site that
/// synthesises a fn-type should use, so that newly inferred
/// fn-types retain pre-18a canonical-JSON bytes.
/// fn-types retain pre-mode-annotation canonical-JSON bytes.
pub fn fn_implicit(params: Vec<Type>, ret: Type, effects: Vec<String>) -> Type {
let n = params.len();
Type::Fn {
@@ -776,21 +778,21 @@ impl Type {
}
}
/// Iter 18a (Decision 10): per-parameter / return mode marker on a
/// [`Type::Fn`].
/// Per-parameter / return mode marker on a [`Type::Fn`]. Full
/// contract lives in `design/contracts/memory-model.md`.
///
/// `Implicit` is the legacy state for fn-types that were constructed
/// before the borrow/own surface annotations existed. Semantically,
/// `Implicit ≡ Own` throughout the 18-series; the distinction exists
/// only so pre-18a JSON fixtures continue to serialize without a
/// `"mode"` wrapper and therefore keep their canonical-JSON hash.
/// `Implicit ≡ Own`; the distinction exists only so pre-annotation
/// JSON fixtures continue to serialize without a `"mode"` wrapper
/// and therefore keep their canonical-JSON hash.
///
/// `Own` and `Borrow` are author-asserted: the surface form
/// `(own T)` / `(borrow T)` round-trips through this enum.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ParamMode {
/// Pre-18a / unannotated. Treated as `Own` by the typechecker.
/// Unannotated / back-compat. Treated as `Own` by the typechecker.
#[default]
Implicit,
/// `(own T)` — caller transfers ownership; callee consumes.
@@ -807,21 +809,20 @@ impl ParamMode {
}
}
/// Iter 18a: serde helper for [`Type::Fn::param_modes`]. Returns
/// `true` when every entry is [`ParamMode::Implicit`] (or when the
/// list is empty), so canonical JSON omits the field for any fn-type
/// without explicit `(borrow)` / `(own)` annotations and pre-18a
/// Serde helper for [`Type::Fn::param_modes`]. Returns `true` when
/// every entry is [`ParamMode::Implicit`] (or when the list is
/// empty), so canonical JSON omits the field for any fn-type without
/// explicit `(borrow)` / `(own)` annotations and pre-annotation
/// fixtures keep bit-identical hashes.
fn all_implicit(modes: &[ParamMode]) -> bool {
modes.iter().all(|m| m.is_implicit())
}
/// Iter 18a: equality of [`ParamMode`] for the purposes of `Type`
/// equality. `Implicit` and `Own` are treated as the same mode
/// throughout the 18-series; `Borrow` is distinct. This keeps
/// pre-18a fixtures (whose fn-types serialize `Implicit`) compatible
/// with newly-written 18a fixtures that mark the same fn-type
/// explicitly with `(own T)`.
/// Equality of [`ParamMode`] for the purposes of `Type` equality.
/// `Implicit` and `Own` are treated as the same mode; `Borrow` is
/// distinct. This keeps pre-annotation fixtures (whose fn-types
/// serialize `Implicit`) compatible with newly-written fixtures
/// that mark the same fn-type explicitly with `(own T)`.
fn mode_eq(a: &ParamMode, b: &ParamMode) -> bool {
match (a, b) {
(ParamMode::Borrow, ParamMode::Borrow) => true,
@@ -831,7 +832,7 @@ fn mode_eq(a: &ParamMode, b: &ParamMode) -> bool {
}
}
/// Iter 18a: equality of two `param_modes` slices, robust to the
/// Equality of two `param_modes` slices, robust to the
/// "elided when all-implicit" representation used by typechecker /
/// desugar / codegen sites that construct fn-types with
/// `param_modes: vec![]`. Both slices are normalised to "implicit
@@ -897,9 +898,10 @@ impl Eq for Type {}
/// Serde helper for `#[serde(skip_serializing_if = "is_false")]`.
///
/// Used by [`Term::App::tail`] and [`Term::Do::tail`] (Iter 14e) so the
/// `tail` flag is omitted from the canonical JSON whenever it is false,
/// preserving bit-identical hashes for every pre-14e definition.
/// Used by [`Term::App::tail`] and [`Term::Do::tail`] so the `tail`
/// flag is omitted from the canonical JSON whenever it is false,
/// preserving bit-identical hashes for every fixture that does not
/// carry the flag.
#[allow(clippy::trivially_copy_pass_by_ref)]
fn is_false(b: &bool) -> bool {
!*b
+2 -2
View File
@@ -51,7 +51,7 @@ pub fn to_bytes<T: serde::Serialize>(value: &T) -> Vec<u8> {
out
}
/// Iter 22b.1: 16-hex-char hash of a [`crate::ast::Type`] in isolation.
/// 16-hex-char hash of a [`crate::ast::Type`] in isolation.
///
/// Used by [`crate::workspace::Registry`] to key `InstanceDef`s by
/// their target type. Parallel in shape to [`crate::def_hash`] and
@@ -138,7 +138,7 @@ mod tests {
assert!(!s.contains('\n'));
}
/// Iter 22-floats.1 RED: a `Literal::Float` carries the IEEE-754
/// A `Literal::Float` carries the IEEE-754
/// binary64 bit pattern as a `u64`; canonical JSON encodes it as
/// `{"bits":"<16-lowercase-hex>","kind":"float"}` — string path,
/// NOT through `serde_json::Number` (which is not bit-stable for
+8 -8
View File
@@ -29,14 +29,14 @@
//! re-evaluate effectful scrutinees per arm).
//! 3. Build a chain of single-level matches via
//! `build_chain(s_var, arms, default)` where `default` is the
//! polymorphic bottom builtin `__unreachable__` (`forall a. a`,
//! Iter 16d) — codegen lowers it to LLVM `unreachable`. Valid
//! programs never reach it because the checker requires
//! exhaustiveness (the catch-all arm dominates the chain).
//! Pre-16d the default was a `Unit` literal, which forced any
//! match whose arms returned a non-Unit type to carry a
//! synthetic `_` arm dominating the terminator. The polymorphic
//! `__unreachable__` removes that workaround.
//! polymorphic bottom builtin `__unreachable__` (`forall a. a`)
//! — codegen lowers it to LLVM `unreachable`. Valid programs
//! never reach it because the checker requires exhaustiveness
//! (the catch-all arm dominates the chain). An earlier shape used
//! a `Unit` literal as the default, which forced any match whose
//! arms returned a non-Unit type to carry a synthetic `_` arm
//! dominating the terminator. The polymorphic `__unreachable__`
//! removes that workaround.
//! 4. Each arm is lowered via `desugar_one_arm`:
//! - `Pattern::Wild` → arm body (catch-all; later arms are dropped).
//! - `Pattern::Var { name }` → `Let { name = scrutinee_var; body }`.
+5 -5
View File
@@ -107,11 +107,11 @@ pub type Result<T> = std::result::Result<T, Error>;
/// loading fails with [`Error::SchemaMismatch`].
pub const SCHEMA: &str = "ailang/v0";
/// Iter 20f: complete LLM-targeted specification of Form-A — the
/// canonical authoring surface (Decision 6). 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
/// Complete LLM-targeted specification of Form-A — the canonical
/// authoring surface (see `design/contracts/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
+1 -1
View File
@@ -1,4 +1,4 @@
//! Iter 20f: drift detection between the AST and `specs/form_a.md`.
//! Drift detection between the AST and `specs/form_a.md`.
//!
//! The spec is hand-curated, but it cannot silently fall behind the
//! language. Every AST enum (`Term`, `Pattern`, `Type`, `Def`, `Literal`,
+1 -1
View File
@@ -1,4 +1,4 @@
//! Iter 20a snapshot tests.
//! Prose-projection snapshot tests.
//!
//! For each `examples/<name>.ail`, the renderer's output is compared
//! against the committed `examples/<name>.prose.txt`. The snapshot