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 //! sibling subcommands surface `WorkspaceLoadError` with the same
//! bracketed `[code]` prefix that the JSON path emits. //! 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. The unit tests in `workspace.rs` already prove the
//! validator fires; these tests prove the diagnostic survives the //! validator fires; these tests prove the diagnostic survives the
//! `WorkspaceLoadError -> Diagnostic` translation in //! `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 //! Builds a synthetic workspace in a tempdir with one fixture that
//! has a bare cross-module Type::Con ref, runs the migration, then //! 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` //! by `mono::build_workspace_env`, which delegates to `crate::build_check_env`
//! and produces a workspace-flat `ctor_index` and `types` map. //! 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 //! scrutinee's canonical `Type::Con.name` to find the TypeDef directly
//! in `env.module_types`, then validates the ctor name within it. The //! 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; //! 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 //! This test pins the cross-module pattern shape against a minimal
//! 2-module fixture (`test_mono_ctor_main` + `test_mono_ctor_listmod`). //! 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 //! ty: "test_mono_ctor_listmod.List<Int>" }` because the mono env
//! resolved `Cons` to bare `List` via the flat index. Post-ct.2 the //! resolved `Cons` to bare `List` via the flat index. After the
//! lookup is type-driven and `expected.name == "test_mono_ctor_listmod.List"` //! refactor the lookup is type-driven and
//! directly indexes the right TypeDef. //! `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 //! Surfaced when `class Eq a` + Eq Int/Bool/Str instances were added
//! instances to `examples/prelude.ail.json`, flipping the //! to `examples/prelude.ail.json`, flipping the
//! `workspace_has_typeclasses` gate so every workspace exercises the //! `workspace_has_typeclasses` gate so every workspace exercises the
//! mono pass. //! 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 //! retirement multi-candidate dispatch path. Three positive fixtures
//! exercise the three trajectories from the milestone spec //! exercise the three trajectories from the milestone spec
//! §"Data flow": //! §"Data flow":
+2 -1
View File
@@ -4,7 +4,8 @@
//! structurally `Term::Let { name: "s", value: App(show__Int, [x]), //! structurally `Term::Let { name: "s", value: App(show__Int, [x]),
//! body: Term::Do { op: "io/print_str", args: [s] } }`. The explicit //! 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 //! 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 //! 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 //! refactor inlines the let-binder away, this pin fires and surfaces
//! the regression BEFORE the E2E runtime stats produce a confusing //! 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 //! Property protected: under `--alloc=rc`, evaluating the trivial
//! program `(body (app print 42))` does NOT leak the heap-Str //! 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 //! (class methods register into module globals) and is extended by
//! Tasks 9 (`missing-constraint`) and 10 (`no-instance`). //! 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 //! Co-located with `typeclass_22b2.rs` so the typeclass-feature
//! coverage is browsable in one directory. Tests use the same //! 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 //! Milestone-22 acceptance: user-defined classes with instances over
//! instances over user-defined ADTs end-to-end (typecheck → mono → //! user-defined ADTs end-to-end (typecheck → mono → codegen →
//! codegen → binary). The 22b.3 mono pass was tested only against //! binary). The mono pass was previously tested only against
//! instances over primitive types; this file pins the user-ADT path. //! instances over primitive types; this file pins the user-ADT path.
//! //!
//! Co-located with `typeclass_22b3.rs` so the typeclass-feature //! Co-located with `typeclass_22b3.rs` so the typeclass-feature
@@ -23,8 +23,9 @@
//! Root cause (from debugger Phase 1-2): //! Root cause (from debugger Phase 1-2):
//! `crates/ailang-check/src/lib.rs::check_def` early-returns `Ok(())` //! `crates/ailang-check/src/lib.rs::check_def` early-returns `Ok(())`
//! for `Def::Class | Def::Instance` (the comment claims body //! for `Def::Class | Def::Instance` (the comment claims body
//! typechecking landed in iter 22b.2, but the body-walk was never //! typechecking landed alongside the typeclass-typecheck arms, but
//! wired). Only the workspace-load coherence checks //! the body-walk was never wired). Only the workspace-load coherence
//! checks
//! (Orphan/Duplicate/MissingMethod) in `workspace::build_registry` //! (Orphan/Duplicate/MissingMethod) in `workspace::build_registry`
//! touch instance defs, and those only inspect the schema, not the //! touch instance defs, and those only inspect the schema, not the
//! method-body identifier graph. //! method-body identifier graph.
+10 -10
View File
@@ -39,7 +39,7 @@
//! - `module-cycle` — workspace loader (Iter 5b, in the CLI path) //! - `module-cycle` — workspace loader (Iter 5b, in the CLI path)
//! - `module-name-mismatch` — 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) //! - `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); //! - `use-after-consume` — `ctx`: `{"binder": "<n>"}` (Iter 18c.2);
//! carries non-empty [`Diagnostic::suggested_rewrites`] showing how to //! carries non-empty [`Diagnostic::suggested_rewrites`] showing how to
//! spell the fix in form-A AILang. //! spell the fix in form-A AILang.
@@ -61,38 +61,38 @@
//! the build refuses it. Note that an invalid suppression does NOT //! the build refuses it. Note that an invalid suppression does NOT
//! suppress its target diagnostic — the original still fires //! suppress its target diagnostic — the original still fires
//! alongside this error. //! 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 //! by the per-fn typecheck arm when a polymorphic `FnDef` calls a
//! class method (e.g. `show x` where `x: a`) but its declared //! class method (e.g. `show x` where `x: a`) but its declared
//! `Forall.constraints` (after one-step superclass expansion) does //! `Forall.constraints` (after one-step superclass expansion) does
//! not include the residual class constraint. `ctx`: //! not include the residual class constraint. `ctx`:
//! `{"class": "<C>", "method": "<m>", "at_type": "<a>"}`. Concrete- //! `{"class": "<C>", "method": "<m>", "at_type": "<a>"}`. Concrete-
//! type residuals are deferred to the `no-instance` diagnostic. //! 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 //! 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 //! param to a fully-concrete type that has no matching entry in the
//! workspace instance registry (`(class, canonical-type-hash)`). //! workspace instance registry (`(class, canonical-type-hash)`).
//! Dual of `missing-constraint`: when the residual is concrete, the //! Dual of `missing-constraint`: when the residual is concrete, the
//! fn cannot push the obligation to a caller, so an existing //! fn cannot push the obligation to a caller, so an existing
//! instance is the only way to discharge it. `ctx`: //! instance is the only way to discharge it. `ctx`:
//! `{"class": "<C>", "method": "<m>", "at_type": "<T>"}`. mq.2 adds //! `{"class": "<C>", "method": "<m>", "at_type": "<T>"}`. An
//! an optional `candidate_classes` field when the residual originated //! optional `candidate_classes` field is added when the residual
//! from the multi-candidate dispatch path; absent on single-class //! originated from the multi-candidate dispatch path; absent on
//! residuals (back-compat). //! single-class residuals (back-compat).
//! - `ambiguous-method-resolution` (mq.2) — `severity: error`. Emitted //! - `ambiguous-method-resolution` — `severity: error`. Emitted
//! by the type-driven dispatch resolver (or the discharge-time //! by the type-driven dispatch resolver (or the discharge-time
//! multi-candidate refinement) when a bare-method call site survives //! multi-candidate refinement) when a bare-method call site survives
//! both type-driven and constraint-driven filtering with more than //! both type-driven and constraint-driven filtering with more than
//! one candidate class. `ctx`: `{"method": "<m>", "at_type": "<T>", //! one candidate class. `ctx`: `{"method": "<m>", "at_type": "<T>",
//! "candidate_classes": ["<C1>", ...]}`. The author disambiguates //! "candidate_classes": ["<C1>", ...]}`. The author disambiguates
//! by writing `<ClassQualifier>.<method> x`. //! 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 //! type-driven dispatch resolver when an explicit class qualifier
//! in a `Term::Var.name` (e.g. `"prelude.Show.show"`) names a //! in a `Term::Var.name` (e.g. `"prelude.Show.show"`) names a
//! qualified class that is not in the workspace registry of //! qualified class that is not in the workspace registry of
//! candidate classes for the method. `ctx`: //! candidate classes for the method. `ctx`:
//! `{"name": "<qualified-class>"}`. //! `{"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 //! Emitted by `synth`'s `Term::Var` arm when a name resolves to a
//! free fn (locals / caller-module-fn / imported-fn) AND a class //! free fn (locals / caller-module-fn / imported-fn) AND a class
//! method of the same name also exists in the workspace. Fn //! 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`) //! Background. The 16a desugar pass (`ailang-core::desugar::desugar_module`)
//! eliminates most `Term::LetRec` nodes by lifting them to synthetic //! 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`). //! explicit (`Borrow` or `Own`).
//! //!
//! ## Scope //! ## Scope
+2 -2
View File
@@ -1,5 +1,5 @@
//! Workspace monomorphisation pass (introduced iter 22b.3; the //! Workspace monomorphisation pass — class-method entry plus
//! free-fn entry was added in iter 23.4). //! free-fn entry.
//! //!
//! Slots into the build pipeline after [`crate::lift_letrecs`] and //! Slots into the build pipeline after [`crate::lift_letrecs`] and
//! before `ailang_codegen::lower_workspace_with_alloc`. It is only //! 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 //! ## 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. //! [`ailang_core::ast::FnDef::suppress`] entries.
//! //!
//! For each `Def::Fn(f)` and each `Suppress { code, because }` in //! 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 //! This pass classifies every binder of every fn body as either
//! [`Uniqueness::Unique`] or [`Uniqueness::Shared`]. Unlike the //! [`Uniqueness::Unique`] or [`Uniqueness::Shared`]. Unlike the
@@ -1,6 +1,7 @@
//! mq.3.5: repurposed pin tests for the post-`MethodNameCollision`- //! Repurposed pin tests for the post-`MethodNameCollision`-retirement
//! retirement workspace-load path. The two on-disk fixtures that fired //! workspace-load path. The two on-disk fixtures that fired
//! `WorkspaceLoadError::MethodNameCollision` pre-mq.3 now load cleanly; //! `WorkspaceLoadError::MethodNameCollision` before the type-driven
//! dispatch refactor now load cleanly;
//! the assertion migrates from "expect collision diagnostic" to "load //! the assertion migrates from "expect collision diagnostic" to "load
//! successful + `Env.method_to_candidate_classes` carries the //! successful + `Env.method_to_candidate_classes` carries the
//! expected multi-entry set" (class-class case) or "load successful + //! 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 //! dispatch-resolution helper that synth's `Term::Var` arm consults
//! per the spec's 5-step rule. //! 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 //! `io/print_int`, `io/print_bool`, `io/print_float` are retired
//! and must NOT appear in the builtin registry. The polymorphic //! and must NOT appear in the builtin registry. The polymorphic
//! `print` helper (prelude, iter 24.3) is the canonical replacement; //! `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 /// [`Def::name`] method) to inspect generically without matching every
/// variant. /// variant.
/// ///
/// Iter 22b.1 (Decision 11): adds `Class` and `Instance`. Both are /// `Class` and `Instance` are additive — fixtures that predate
/// additive — pre-22b fixtures never produce these tags, so their /// the typeclass layer never produce these tags, so their
/// canonical-JSON bytes (and therefore their content hashes) are /// canonical-JSON bytes (and therefore their content hashes) are
/// unchanged. The `Def`-level match exhaustiveness in downstream /// unchanged. The `Def`-level match exhaustiveness in downstream
/// crates is the only caller that has to acknowledge the variants. /// crates is the only caller that has to acknowledge the variants.
@@ -78,9 +78,9 @@ pub enum Def {
Const(ConstDef), Const(ConstDef),
/// Type (ADT) definition; see [`TypeDef`]. /// Type (ADT) definition; see [`TypeDef`].
Type(TypeDef), Type(TypeDef),
/// Iter 22b.1: typeclass declaration; see [`ClassDef`]. /// Typeclass declaration; see [`ClassDef`].
Class(ClassDef), Class(ClassDef),
/// Iter 22b.1: instance declaration; see [`InstanceDef`]. /// Instance declaration; see [`InstanceDef`].
Instance(InstanceDef), Instance(InstanceDef),
} }
@@ -136,9 +136,8 @@ pub fn def_kind(def: &Def) -> &'static str {
pub struct TypeDef { pub struct TypeDef {
/// Type name (capitalised by convention). /// Type name (capitalised by convention).
pub name: String, pub name: String,
/// Type parameters (Iter 13a parameterised-ADT support). A /// Type parameters for parameterised ADTs. A monomorphic ADT has
/// monomorphic ADT has `vars` empty and is serialized identically /// `vars` empty and the field is **omitted** when empty,
/// to the pre-13a schema (the field is **omitted** when empty),
/// preserving the canonical-JSON hash of every existing module on /// preserving the canonical-JSON hash of every existing module on
/// disk. See the regression test /// disk. See the regression test
/// `iter13a_schema_extension_preserves_pre_13a_hashes` in /// `iter13a_schema_extension_preserves_pre_13a_hashes` in
@@ -151,7 +150,7 @@ pub struct TypeDef {
/// Optional source-level documentation string. /// Optional source-level documentation string.
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub doc: Option<String>, 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 /// codegen emits `drop_<m>_<T>` with an iterative worklist body
/// instead of the recursive cascade — chosen by the LLM-author when /// instead of the recursive cascade — chosen by the LLM-author when
/// the type is expected to form long chains (millions of cells) /// 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; /// Serialised as `"drop-iterative": true` (kebab-case) when set;
/// the field is omitted when `false` so canonical-JSON hashes of /// the field is omitted when `false` so canonical-JSON hashes of
/// every pre-18e fixture remain bit-stable. See the regression /// every fixture that does not opt in remain bit-stable. See the
/// test `iter18e_drop_iterative_default_preserves_hashes` in /// regression test `iter18e_drop_iterative_default_preserves_hashes`
/// [`crate::hash`]. /// in [`crate::hash`].
#[serde( #[serde(
default, default,
rename = "drop-iterative", rename = "drop-iterative",
@@ -219,24 +218,24 @@ pub struct FnDef {
/// the same additive-schema pattern as [`FnDef::doc`]. /// the same additive-schema pattern as [`FnDef::doc`].
#[serde(default, skip_serializing_if = "Option::is_none")] #[serde(default, skip_serializing_if = "Option::is_none")]
pub export: Option<String>, 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 /// fn. Each entry names a diagnostic code and an author-asserted
/// reason. Currently the only consumer is `over-strict-mode` /// reason. The only current consumer is `over-strict-mode`, but
/// (Iter 19a / 19a.1) but the mechanism is generic across codes. /// the mechanism is generic across codes. `because` must be
/// `because` must be non-empty — the typechecker emits /// non-empty — the typechecker emits `empty-suppress-reason`
/// `empty-suppress-reason` (Error) otherwise. /// (Error) otherwise.
/// ///
/// Serialised with `skip_serializing_if = "Vec::is_empty"` so /// Serialised with `skip_serializing_if = "Vec::is_empty"` so
/// every pre-19b fixture's canonical-JSON hash stays bit-identical. /// every pre-suppress fixture's canonical-JSON hash stays
/// The same additive-schema pattern is used by [`TypeDef::vars`] /// bit-identical. The same additive-schema pattern is used by
/// (Iter 13a) and [`Type::Con::args`] (Iter 13a). /// [`TypeDef::vars`] and [`Type::Con::args`].
#[serde(default, skip_serializing_if = "Vec::is_empty")] #[serde(default, skip_serializing_if = "Vec::is_empty")]
pub suppress: Vec<Suppress>, pub suppress: Vec<Suppress>,
} }
/// Iter 19b: one entry in [`FnDef::suppress`]. Marks a structured /// One entry in [`FnDef::suppress`]. Marks a structured diagnostic
/// diagnostic the author has consciously decided to allow on this /// the author has consciously decided to allow on this def, with a
/// def, with a mandatory reason. /// mandatory reason.
/// ///
/// `because` is non-empty by schema rule — the typechecker emits /// `because` is non-empty by schema rule — the typechecker emits
/// `empty-suppress-reason` (Error severity) when it is empty or /// `empty-suppress-reason` (Error severity) when it is empty or
@@ -256,20 +255,22 @@ pub struct Suppress {
pub because: String, 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 /// Single-parameter, multi-method, optional-default, optional-superclass
/// typeclass. The `param` is a single string — multi-param classes are /// typeclass. The `param` is a single string — multi-param classes are
/// rejected by Decision 11 axis 1, and the schema enforces it by shape /// rejected by the typeclass design (concrete-types-only, kind `*`),
/// (`param: String`, not `Vec<String>`). /// and the schema enforces it by shape (`param: String`, not
/// `Vec<String>`).
/// ///
/// `superclass`, when present, MUST have its `type` field equal to /// `superclass`, when present, MUST have its `type` field equal to
/// `param`. The check is enforced in 22b.2 via the /// `param`. The check is enforced via the `InvalidSuperclassParam`
/// `InvalidSuperclassParam` diagnostic; the schema does not encode it. /// diagnostic; the schema does not encode it.
/// ///
/// All optional fields are omitted from canonical JSON when absent / /// All optional fields are omitted from canonical JSON when absent /
/// empty, so future schema evolution can land here without disturbing /// empty, so future schema evolution can land here without disturbing
/// pre-22b hashes. /// existing hashes.
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct ClassDef { pub struct ClassDef {
/// Class name (capitalised by convention). Bare — symmetric to /// Class name (capitalised by convention). Bare — symmetric to
@@ -277,8 +278,7 @@ pub struct ClassDef {
/// a reference. Cross-module class references live in /// a reference. Cross-module class references live in
/// `InstanceDef.class`, `Constraint.class`, and /// `InstanceDef.class`, `Constraint.class`, and
/// `SuperclassRef.class`; those fields carry the canonical form /// `SuperclassRef.class`; those fields carry the canonical form
/// (bare for same-module, `<module>.<Class>` for cross-module) /// (bare for same-module, `<module>.<Class>` for cross-module).
/// per mq.1.
pub name: String, pub name: String,
/// Single type-parameter name. /// Single type-parameter name.
pub param: String, pub param: String,
@@ -292,20 +292,21 @@ pub struct ClassDef {
pub doc: Option<String>, 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)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SuperclassRef { pub struct SuperclassRef {
/// Superclass name in canonical form (mq.1): bare for a /// Superclass name in canonical form: bare for a same-module
/// same-module class, `<module>.<Class>` for a cross-module /// class, `<module>.<Class>` for a cross-module class.
/// class. Symmetric to ct.1's `Type::Con.name` rule. /// Symmetric to `Type::Con.name`'s canonical-form rule.
pub class: String, pub class: String,
/// Type the superclass is applied to. MUST equal the parent /// 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")] #[serde(rename = "type")]
pub type_: String, 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 /// `default` is `None` when the method is abstract-required (every
/// instance must specify it); `Some(body)` when the method has a /// instance must specify it); `Some(body)` when the method has a
@@ -324,7 +325,8 @@ pub struct ClassMethod {
pub default: Option<Term>, 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 /// `class` is the name of the class being instantiated. `type_` is the
/// concrete type expression the class is applied to — never the class /// concrete type expression the class is applied to — never the class
@@ -332,9 +334,9 @@ pub struct ClassMethod {
/// overrides of default-bearing methods. /// overrides of default-bearing methods.
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InstanceDef { pub struct InstanceDef {
/// Class being instantiated, in canonical form (mq.1): bare for /// Class being instantiated, in canonical form: bare for a
/// a same-module class, `<module>.<Class>` for a cross-module /// same-module class, `<module>.<Class>` for a cross-module
/// class. Symmetric to ct.1's `Type::Con.name` rule. /// class. Symmetric to `Type::Con.name`'s canonical-form rule.
pub class: String, pub class: String,
/// Concrete type the class is applied to. /// Concrete type the class is applied to.
#[serde(rename = "type")] #[serde(rename = "type")]
@@ -346,28 +348,29 @@ pub struct InstanceDef {
pub doc: Option<String>, pub doc: Option<String>,
} }
/// Iter 22b.1: one method body in an [`InstanceDef`]. /// One method body in an [`InstanceDef`].
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct InstanceMethod { pub struct InstanceMethod {
/// Method name (must match a method in the corresponding class). /// Method name (must match a method in the corresponding class).
pub name: String, pub name: String,
/// Method body. The class's declared method type with the class /// Method body. The class's declared method type with the class
/// param substituted to the instance type is the body's expected /// param substituted to the instance type is the body's expected
/// type — checked in 22b.2. /// type.
pub body: Term, pub body: Term,
} }
/// Iter 22b.2: a class constraint on a polymorphic function (Decision /// A class constraint on a polymorphic function (narrative in
/// 11). `(class, type)` pair where `class` is a class name and `type` /// `design/contracts/typeclasses.md`). `(class, type)` pair where
/// is a `Type` expression — typically a single `Type::Var` (e.g. /// `class` is a class name and `type` is a `Type` expression —
/// `(Eq, a)` for `Eq a => ...`). Concrete-type constraints are legal /// typically a single `Type::Var` (e.g. `(Eq, a)` for
/// schema-wise but fired as `no-instance` at typecheck time if no /// `Eq a => ...`). Concrete-type constraints are legal schema-wise
/// matching registry entry exists. /// but fired as `no-instance` at typecheck time if no matching
/// registry entry exists.
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Constraint { pub struct Constraint {
/// Class name in canonical form (mq.1): bare for a same-module /// Class name in canonical form: bare for a same-module class,
/// class, `<module>.<Class>` for a cross-module class. /// `<module>.<Class>` for a cross-module class. Symmetric to
/// Symmetric to ct.1's `Type::Con.name` rule. /// `Type::Con.name`'s canonical-form rule.
pub class: String, pub class: String,
/// Type the class is applied to. /// Type the class is applied to.
#[serde(rename = "type")] #[serde(rename = "type")]
@@ -408,11 +411,11 @@ pub enum Term {
/// Function application. `callee` is evaluated to a function value; /// Function application. `callee` is evaluated to a function value;
/// `args` are evaluated left-to-right. /// `args` are evaluated left-to-right.
/// ///
/// Iter 14e: `tail` marks this call as occurring in tail position /// `tail` marks this call as occurring in tail position (see
/// (per Decision 8). When set, codegen lowers the call as /// `design/contracts/tail-calls.md`). When set, codegen lowers
/// `musttail call`. The flag defaults to `false` and is omitted /// the call as `musttail call`. The flag defaults to `false` and
/// during canonical-JSON serialisation when unset, so pre-14e /// is omitted during canonical-JSON serialisation when unset, so
/// fixtures keep bit-identical hashes. /// pre-tail-flag fixtures keep bit-identical hashes.
App { App {
#[serde(rename = "fn")] #[serde(rename = "fn")]
callee: Box<Term>, callee: Box<Term>,
@@ -426,7 +429,7 @@ pub enum Term {
value: Box<Term>, value: Box<Term>,
body: 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`. /// the bound name `name` is recursively visible inside `body`.
/// Eliminated by `crate::desugar` before typecheck — lifted to a /// Eliminated by `crate::desugar` before typecheck — lifted to a
/// synthetic top-level fn when `body` does not capture any name /// 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 /// `match` in codegen (`lower_effect_op`); there is no
/// effect-handler table and no link-time resolution. /// 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 { Do {
op: String, op: String,
args: Vec<Term>, args: Vec<Term>,
@@ -479,7 +482,7 @@ pub enum Term {
scrutinee: Box<Term>, scrutinee: Box<Term>,
arms: Vec<Arm>, 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 /// `body` from the enclosing scope. Param/return types are
/// declared inline so the typechecker stays HM-monomorphic on /// declared inline so the typechecker stays HM-monomorphic on
/// the inferred shape. /// the inferred shape.
@@ -493,7 +496,7 @@ pub enum Term {
effects: Vec<String>, effects: Vec<String>,
body: Box<Term>, 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. /// result discarded; `rhs` is the value of the whole expression.
/// Equivalent to `let _ = lhs in rhs`, but with a dedicated node so /// Equivalent to `let _ = lhs in rhs`, but with a dedicated node so
/// pretty-print and diagnostics read cleanly. /// pretty-print and diagnostics read cleanly.
@@ -501,27 +504,25 @@ pub enum Term {
lhs: Box<Term>, lhs: Box<Term>,
rhs: Box<Term>, rhs: Box<Term>,
}, },
/// Iter 18c.1: explicit RC clone. Lowers identically to its inner /// Explicit RC clone. Codegen emits
/// term in 18c.1; in 18c.3 the codegen will emit
/// `call void @ailang_rc_inc(ptr %v)` before returning `%v` under /// `call void @ailang_rc_inc(ptr %v)` before returning `%v` under
/// `--alloc=rc`. The variant is additive: `(clone X)` round-trips /// `--alloc=rc`. The variant is additive: `(clone X)` round-trips
/// through every pre-18c.1 fixture without their hashes changing /// through every fixture that does not use the tag without its
/// because none of them use the new tag. /// hash changing.
Clone { Clone {
value: Box<Term>, value: Box<Term>,
}, },
/// Iter 18d.1: explicit reuse-as hint. Wraps an allocating `body` /// Explicit reuse-as hint. Wraps an allocating `body` (typically
/// (typically `Term::Ctor`, also `Term::Lam`) and names a `source` /// `Term::Ctor`, also `Term::Lam`) and names a `source` term
/// term whose memory slot the body's allocation should reuse. /// whose memory slot the body's allocation should reuse.
/// Conventionally `source` is a `Term::Var { name }` — the /// Conventionally `source` is a `Term::Var { name }` — the
/// linearity check rejects anything else with /// linearity check rejects anything else with
/// `reuse-as-source-not-bare-var`. The body must be allocating; /// `reuse-as-source-not-bare-var`. The body must be allocating;
/// non-allocating bodies are rejected at typecheck with /// non-allocating bodies are rejected at typecheck with
/// `reuse-as-non-allocating-body`. Lowers as identity in 18d.1 /// `reuse-as-non-allocating-body`. Codegen lowers as in-place
/// (returns `body`'s `(ssa, ty)`, ignores `source`); 18d.2 will /// rewrite under `--alloc=rc`. The variant is additive —
/// lower this as in-place rewrite under `--alloc=rc`. The variant /// fixtures that do not use the tag keep their canonical-JSON
/// is additive — pre-18d fixtures keep their canonical-JSON hash /// hash.
/// because none of them use the new tag.
#[serde(rename = "reuse-as")] #[serde(rename = "reuse-as")]
ReuseAs { ReuseAs {
source: Box<Term>, source: Box<Term>,
@@ -685,7 +686,7 @@ pub enum Type {
/// and parameterised forms like `List<Int>`. /// and parameterised forms like `List<Int>`.
Con { Con {
name: String, 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 /// non-parameterised user ADTs) this stays empty and is
/// **omitted** during serialization, so the canonical-JSON /// **omitted** during serialization, so the canonical-JSON
/// hash of every pre-existing module remains bit-identical. /// 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`] /// set; equality compares it modulo order (see the [`PartialEq`]
/// impl below). /// impl below).
/// ///
/// Iter 18a (Decision 10): `param_modes` and `ret_mode` carry the /// `param_modes` and `ret_mode` carry the `(borrow T)` /
/// `(borrow T)` / `(own T)` wrappers from the surface form. They /// `(own T)` wrappers from the surface form. They are metadata
/// are metadata on `Type::Fn`, not new `Type` variants — so /// on `Type::Fn`, not new `Type` variants — so unification,
/// unification, occurs, apply, and every other `Type` match-arm /// occurs, apply, and every other `Type` match-arm keeps working
/// keeps working unchanged. `param_modes` is omitted from /// unchanged. `param_modes` is omitted from canonical JSON when
/// canonical JSON when every entry is `Implicit`; `ret_mode` is /// every entry is `Implicit`; `ret_mode` is omitted when it is
/// omitted when it is `Implicit`. Pre-18a fixtures therefore hash /// `Implicit`, so pre-mode-annotation fixtures hash
/// bit-identically. /// bit-identically. Full contract in
/// `design/contracts/memory-model.md`.
Fn { Fn {
params: Vec<Type>, params: Vec<Type>,
#[serde(default, skip_serializing_if = "all_implicit")] #[serde(default, skip_serializing_if = "all_implicit")]
@@ -726,9 +728,9 @@ pub enum Type {
/// names, instantiated fresh at each use site. /// names, instantiated fresh at each use site.
Forall { Forall {
vars: Vec<String>, vars: Vec<String>,
/// Iter 22b.2: class constraints quantified together with /// Class constraints quantified together with `vars`. Empty
/// `vars`. Empty for pre-22b.2 polymorphic types; serialised /// for unconstrained polymorphic types; serialised with
/// with `skip_serializing_if = "Vec::is_empty"` so existing /// `skip_serializing_if = "Vec::is_empty"` so existing
/// canonical-JSON bytes stay bit-identical. /// canonical-JSON bytes stay bit-identical.
#[serde(default, skip_serializing_if = "Vec::is_empty")] #[serde(default, skip_serializing_if = "Vec::is_empty")]
constraints: Vec<Constraint>, constraints: Vec<Constraint>,
@@ -759,11 +761,11 @@ impl Type {
Type::Con { name: "Float".into(), args: vec![] } Type::Con { name: "Float".into(), args: vec![] }
} }
/// Iter 18a: build a `Type::Fn` with all parameter modes set to /// Build a `Type::Fn` with all parameter modes set to
/// `ParamMode::Implicit` and `ret_mode` set to `Implicit`. This is /// `ParamMode::Implicit` and `ret_mode` set to `Implicit`. This
/// the form every typechecker / desugar / codegen site that /// is the form every typechecker / desugar / codegen site that
/// synthesises a fn-type should use, so that newly inferred /// 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 { pub fn fn_implicit(params: Vec<Type>, ret: Type, effects: Vec<String>) -> Type {
let n = params.len(); let n = params.len();
Type::Fn { Type::Fn {
@@ -776,21 +778,21 @@ impl Type {
} }
} }
/// Iter 18a (Decision 10): per-parameter / return mode marker on a /// Per-parameter / return mode marker on a [`Type::Fn`]. Full
/// [`Type::Fn`]. /// contract lives in `design/contracts/memory-model.md`.
/// ///
/// `Implicit` is the legacy state for fn-types that were constructed /// `Implicit` is the legacy state for fn-types that were constructed
/// before the borrow/own surface annotations existed. Semantically, /// before the borrow/own surface annotations existed. Semantically,
/// `Implicit ≡ Own` throughout the 18-series; the distinction exists /// `Implicit ≡ Own`; the distinction exists only so pre-annotation
/// only so pre-18a JSON fixtures continue to serialize without a /// JSON fixtures continue to serialize without a `"mode"` wrapper
/// `"mode"` wrapper and therefore keep their canonical-JSON hash. /// and therefore keep their canonical-JSON hash.
/// ///
/// `Own` and `Borrow` are author-asserted: the surface form /// `Own` and `Borrow` are author-asserted: the surface form
/// `(own T)` / `(borrow T)` round-trips through this enum. /// `(own T)` / `(borrow T)` round-trips through this enum.
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)] #[derive(Clone, Copy, Debug, Default, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")] #[serde(rename_all = "lowercase")]
pub enum ParamMode { pub enum ParamMode {
/// Pre-18a / unannotated. Treated as `Own` by the typechecker. /// Unannotated / back-compat. Treated as `Own` by the typechecker.
#[default] #[default]
Implicit, Implicit,
/// `(own T)` — caller transfers ownership; callee consumes. /// `(own T)` — caller transfers ownership; callee consumes.
@@ -807,21 +809,20 @@ impl ParamMode {
} }
} }
/// Iter 18a: serde helper for [`Type::Fn::param_modes`]. Returns /// Serde helper for [`Type::Fn::param_modes`]. Returns `true` when
/// `true` when every entry is [`ParamMode::Implicit`] (or when the /// every entry is [`ParamMode::Implicit`] (or when the list is
/// list is empty), so canonical JSON omits the field for any fn-type /// empty), so canonical JSON omits the field for any fn-type without
/// without explicit `(borrow)` / `(own)` annotations and pre-18a /// explicit `(borrow)` / `(own)` annotations and pre-annotation
/// fixtures keep bit-identical hashes. /// fixtures keep bit-identical hashes.
fn all_implicit(modes: &[ParamMode]) -> bool { fn all_implicit(modes: &[ParamMode]) -> bool {
modes.iter().all(|m| m.is_implicit()) modes.iter().all(|m| m.is_implicit())
} }
/// Iter 18a: equality of [`ParamMode`] for the purposes of `Type` /// Equality of [`ParamMode`] for the purposes of `Type` equality.
/// equality. `Implicit` and `Own` are treated as the same mode /// `Implicit` and `Own` are treated as the same mode; `Borrow` is
/// throughout the 18-series; `Borrow` is distinct. This keeps /// distinct. This keeps pre-annotation fixtures (whose fn-types
/// pre-18a fixtures (whose fn-types serialize `Implicit`) compatible /// serialize `Implicit`) compatible with newly-written fixtures
/// with newly-written 18a fixtures that mark the same fn-type /// that mark the same fn-type explicitly with `(own T)`.
/// explicitly with `(own T)`.
fn mode_eq(a: &ParamMode, b: &ParamMode) -> bool { fn mode_eq(a: &ParamMode, b: &ParamMode) -> bool {
match (a, b) { match (a, b) {
(ParamMode::Borrow, ParamMode::Borrow) => true, (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 / /// "elided when all-implicit" representation used by typechecker /
/// desugar / codegen sites that construct fn-types with /// desugar / codegen sites that construct fn-types with
/// `param_modes: vec![]`. Both slices are normalised to "implicit /// `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")]`. /// Serde helper for `#[serde(skip_serializing_if = "is_false")]`.
/// ///
/// Used by [`Term::App::tail`] and [`Term::Do::tail`] (Iter 14e) so the /// Used by [`Term::App::tail`] and [`Term::Do::tail`] so the `tail`
/// `tail` flag is omitted from the canonical JSON whenever it is false, /// flag is omitted from the canonical JSON whenever it is false,
/// preserving bit-identical hashes for every pre-14e definition. /// preserving bit-identical hashes for every fixture that does not
/// carry the flag.
#[allow(clippy::trivially_copy_pass_by_ref)] #[allow(clippy::trivially_copy_pass_by_ref)]
fn is_false(b: &bool) -> bool { fn is_false(b: &bool) -> bool {
!*b !*b
+2 -2
View File
@@ -51,7 +51,7 @@ pub fn to_bytes<T: serde::Serialize>(value: &T) -> Vec<u8> {
out 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 /// Used by [`crate::workspace::Registry`] to key `InstanceDef`s by
/// their target type. Parallel in shape to [`crate::def_hash`] and /// their target type. Parallel in shape to [`crate::def_hash`] and
@@ -138,7 +138,7 @@ mod tests {
assert!(!s.contains('\n')); 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 /// binary64 bit pattern as a `u64`; canonical JSON encodes it as
/// `{"bits":"<16-lowercase-hex>","kind":"float"}` — string path, /// `{"bits":"<16-lowercase-hex>","kind":"float"}` — string path,
/// NOT through `serde_json::Number` (which is not bit-stable for /// 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). //! re-evaluate effectful scrutinees per arm).
//! 3. Build a chain of single-level matches via //! 3. Build a chain of single-level matches via
//! `build_chain(s_var, arms, default)` where `default` is the //! `build_chain(s_var, arms, default)` where `default` is the
//! polymorphic bottom builtin `__unreachable__` (`forall a. a`, //! polymorphic bottom builtin `__unreachable__` (`forall a. a`)
//! Iter 16d) — codegen lowers it to LLVM `unreachable`. Valid //! — codegen lowers it to LLVM `unreachable`. Valid programs
//! programs never reach it because the checker requires //! never reach it because the checker requires exhaustiveness
//! exhaustiveness (the catch-all arm dominates the chain). //! (the catch-all arm dominates the chain). An earlier shape used
//! Pre-16d the default was a `Unit` literal, which forced any //! a `Unit` literal as the default, which forced any match whose
//! match whose arms returned a non-Unit type to carry a //! arms returned a non-Unit type to carry a synthetic `_` arm
//! synthetic `_` arm dominating the terminator. The polymorphic //! dominating the terminator. The polymorphic `__unreachable__`
//! `__unreachable__` removes that workaround. //! removes that workaround.
//! 4. Each arm is lowered via `desugar_one_arm`: //! 4. Each arm is lowered via `desugar_one_arm`:
//! - `Pattern::Wild` → arm body (catch-all; later arms are dropped). //! - `Pattern::Wild` → arm body (catch-all; later arms are dropped).
//! - `Pattern::Var { name }` → `Let { name = scrutinee_var; body }`. //! - `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`]. /// loading fails with [`Error::SchemaMismatch`].
pub const SCHEMA: &str = "ailang/v0"; pub const SCHEMA: &str = "ailang/v0";
/// Iter 20f: complete LLM-targeted specification of Form-A — the /// Complete LLM-targeted specification of Form-A — the canonical
/// canonical authoring surface (Decision 6). Embedded verbatim into /// authoring surface (see `design/contracts/authoring-surface.md`).
/// any prompt that asks an LLM to produce or edit AILang code; in /// Embedded verbatim into any prompt that asks an LLM to produce or
/// particular, `ail merge-prose` includes it in the round-trip /// edit AILang code; in particular, `ail merge-prose` includes it in
/// prompt template. The string is the raw bytes of /// 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 /// `specs/form_a.md`, co-located with this crate so the spec sits
/// next to the AST it describes; a drift test /// next to the AST it describes; a drift test
/// (`tests/spec_drift.rs`) walks every AST enum variant and asserts /// (`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 //! The spec is hand-curated, but it cannot silently fall behind the
//! language. Every AST enum (`Term`, `Pattern`, `Type`, `Def`, `Literal`, //! 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 //! For each `examples/<name>.ail`, the renderer's output is compared
//! against the committed `examples/<name>.prose.txt`. The snapshot //! against the committed `examples/<name>.prose.txt`. The snapshot
+3 -3
View File
@@ -35,9 +35,9 @@ evolving in lockstep with the language:
`crates/ailang-codegen`): AST, type system, codegen. `crates/ailang-codegen`): AST, type system, codegen.
- **Surface forms** (`crates/ailang-surface`, `crates/ailang-prose`): - **Surface forms** (`crates/ailang-surface`, `crates/ailang-prose`):
the LLM-facing renderings of a module. `ailang-surface` the LLM-facing renderings of a module. `ailang-surface`
is the lossless Form-A printer/parser — the canonical authoring is the lossless Form-A printer/parser — the canonical
surface fixed by Decision 6, with a round-trip property [authoring surface](contracts/authoring-surface.md), with a
`parse ∘ print = id` gating every release. `ailang-prose` round-trip property `parse ∘ print = id` gating every release. `ailang-prose`
is the lossy Form-B projection — human-readable prose for review and is the lossy Form-B projection — human-readable prose for review and
edit, with no parser; re-integration goes through the edit, with no parser; re-integration goes through the
LLM-mediator round-trip documented in `docs/PROSE_ROUNDTRIP.md`. LLM-mediator round-trip documented in `docs/PROSE_ROUNDTRIP.md`.
+7 -7
View File
@@ -1,6 +1,6 @@
# Authoring surface # Authoring surface
## Decision 6: authoring surface ## Authoring surface
Form (A) is implemented as Form (A) is implemented as
the `ailang-surface` crate (parser + printer). Form-A is the `ailang-surface` crate (parser + printer). Form-A is
@@ -22,7 +22,7 @@ one projection among potentially many. Concretely:
addressed representation of a module. All hashing, content- addressed representation of a module. All hashing, content-
addressing, cross-module references, and typecheck/codegen input addressing, cross-module references, and typecheck/codegen input
flow through the JSON-AST. **No new hashable form is introduced.** flow through the JSON-AST. **No new hashable form is introduced.**
- The textual surface (form A, this Decision) is the **AI authoring - The textual surface (form A, this contract) is the **AI authoring
projection**: optimised for me producing programs token-efficiently projection**: optimised for me producing programs token-efficiently
and for foreign LLMs producing programs from a spec. It is not and for foreign LLMs producing programs from a spec. It is not
optimised for human authors and does not need to be human-pleasant. optimised for human authors and does not need to be human-pleasant.
@@ -55,11 +55,11 @@ the AST and remain projection-agnostic.
shape. The full bijection between `.ail.json` and `.ail` (both shape. The full bijection between `.ail.json` and `.ail` (both
directions, BLAKE3-stable hashing, Float-bits-hex encoding, directions, BLAKE3-stable hashing, Float-bits-hex encoding,
workspace-CI enforcement points) is anchored as the top-level workspace-CI enforcement points) is anchored as the top-level
[Roundtrip Invariant](roundtrip-invariant.md) — this constraint records that Decision [Roundtrip Invariant](roundtrip-invariant.md) — this constraint
6's surface-design choice must satisfy that invariant; the records that the surface-design choice must satisfy that
invariant itself lives at top level because the property is invariant; the invariant itself lives at top level because the
load-bearing on the language identity, not on this Decision's property is load-bearing on the language identity, not on this
surface-design rationale. file's surface-design rationale.
3. **No external symbols.** ASCII only. No Greek (`∀`), no arrows 3. **No external symbols.** ASCII only. No Greek (`∀`), no arrows
(`→`), no subscripts. Reasoning: I substitute mojibake for (`→`), no subscripts. Reasoning: I substitute mojibake for
non-ASCII characters under context pressure; foreign LLMs vary non-ASCII characters under context pressure; foreign LLMs vary
+17 -14
View File
@@ -41,7 +41,7 @@ narrative — defaults, superclasses, diagnostics — lives in
"params": ["<id>"...], // names bound in body, in type.params order "params": ["<id>"...], // names bound in body, in type.params order
"body": Term, "body": Term,
"doc": "<optional string>", "doc": "<optional string>",
"export": "<optional C symbol>", // omitted when absent (hash-stable when omitted); see §"Embedding ABI" "export": "<optional C symbol>", // omitted when absent (hash-stable when omitted); embedding-ABI surface — see prose below
"suppress": [Suppress...] // omitted when empty "suppress": [Suppress...] // omitted when empty
} }
@@ -65,11 +65,11 @@ narrative — defaults, superclasses, diagnostics — lives in
"drop-iterative": true // opt-in; omitted when false (hash-stable when omitted) "drop-iterative": true // opt-in; omitted when false (hash-stable when omitted)
} }
// class (typeclass declaration; see Decision 11) // class (typeclass declaration; narrative in contracts/typeclasses.md)
{ "kind": "class", { "kind": "class",
"name": "<id>", // class name (e.g. "Show") "name": "<id>", // class name (e.g. "Show")
"param": "<id>", // single class parameter, kind * "param": "<id>", // single class parameter, kind *
"superclass": null, // or { "class": "<id>", "type": "<param>" } — "class": canonical form (bare for same-module, "<module>.<Class>" for cross-module; see §"Class names" / mq.1) "superclass": null, // or { "class": "<id>", "type": "<param>" } — "class": canonical form (bare for same-module, "<module>.<Class>" for cross-module)
"methods": [ "methods": [
{ "name": "<id>", { "name": "<id>",
"type": Type, // FnSig over the class param "type": Type, // FnSig over the class param
@@ -80,9 +80,9 @@ narrative — defaults, superclasses, diagnostics — lives in
"doc": "<optional string>" "doc": "<optional string>"
} }
// instance (typeclass instance; see Decision 11) // instance (typeclass instance; narrative in contracts/typeclasses.md)
{ "kind": "instance", { "kind": "instance",
"class": "<id>", // class being instantiated; canonical form (bare for same-module, "<module>.<Class>" for cross-module; see §"Class names" / mq.1) "class": "<id>", // class being instantiated; canonical form (bare for same-module, "<module>.<Class>" for cross-module)
"type": Type, // concrete type expression (never the class param) "type": Type, // concrete type expression (never the class param)
"methods": [ "methods": [
{ "name": "<id>", "body": Term } { "name": "<id>", "body": Term }
@@ -215,13 +215,14 @@ metadata is defined and gated there as well.
```jsonc ```jsonc
// Type-constructor application. `args` omitted when empty // Type-constructor application. `args` omitted when empty
// (hash-stable when omitted, for non-parameterised cases like Int, Bool, ...). // (hash-stable when omitted, for non-parameterised cases like Int, Bool, ...).
{ "k": "con", "name": "<id>", "args": [Type...] } // "name": canonical form (bare for same-module / primitives, "<module>.<TypeName>" for cross-module; see §"Type::Con name scoping" / ct.1) { "k": "con", "name": "<id>", "args": [Type...] } // "name": canonical form (bare for same-module / primitives, "<module>.<TypeName>" for cross-module)
// Function type. Decision 10 added paramModes/retMode as // Function type. paramModes/retMode are metadata on Type::Fn —
// metadata on Type::Fn — they are NOT separate Type variants, so every // they are NOT separate Type variants, so every existing match-arm
// existing match-arm in the typechecker (unify, occurs, apply) keeps // in the typechecker (unify, occurs, apply) keeps working.
// working. `paramModes` omitted when every entry is "implicit"; // `paramModes` omitted when every entry is "implicit"; `retMode`
// `retMode` omitted when "implicit" (hash-stable when omitted). // omitted when "implicit" (hash-stable when omitted). Full mode
// contract lives in contracts/memory-model.md.
{ "k": "fn", { "k": "fn",
"params": [Type...], "params": [Type...],
"paramModes": [ParamMode...], "paramModes": [ParamMode...],
@@ -232,14 +233,16 @@ metadata is defined and gated there as well.
{ "k": "var", "name": "<id>" } { "k": "var", "name": "<id>" }
// Top-level polymorphism only. `constraints` carries class // Top-level polymorphism only. `constraints` carries class
// constraints (Decision 11); omitted when empty (hash-stable when omitted). // constraints (narrative in contracts/typeclasses.md); omitted when
// empty (hash-stable when omitted).
{ "k": "forall", { "k": "forall",
"vars": ["<id>"...], "vars": ["<id>"...],
"constraints": [{ "class": "<id>", "type": "<id>" }, ...], // "class": canonical form (bare for same-module, "<module>.<Class>" for cross-module; see §"Class names" / mq.1) "constraints": [{ "class": "<id>", "type": "<id>" }, ...], // "class": canonical form (bare for same-module, "<module>.<Class>" for cross-module)
"body": Type } "body": Type }
``` ```
**`ParamMode`** (Decision 10): **`ParamMode`** (full contract in
[memory model](memory-model.md)):
``` ```
"implicit" — unannotated / back-compat. Treated as `own` by the typechecker. "implicit" — unannotated / back-compat. Treated as `own` by the typechecker.
+9 -9
View File
@@ -36,16 +36,16 @@ A proposed feature ships only if all three hold:
discouraged. A documentation note is not a reshape; the discouraged. A documentation note is not a reshape; the
discriminator is whether the wrong code fails to typecheck, not discriminator is whether the wrong code fails to typecheck, not
whether a guideline advises against it. Worked example: a bare whether a guideline advises against it. Worked example: a bare
`while` over mutable state would pass clauses 1 and 2 yet fail `while` over mutable state would pass criteria 1 and 2 yet fail
clause 3 (it reinstates iterated-mutable-state reasoning the pure criterion 3 (it reinstates iterated-mutable-state reasoning the
core exists to remove); a hypothetical "all repetition is either pure core exists to remove); a hypothetical "all repetition is
structurally-decreasing recursion over an acyclic ADT or an either structurally-decreasing recursion over an acyclic ADT or
explicit named loop" iteration story would, *if it could be built an explicit named loop" iteration story would, *if it could be
without a documented-unenforced precondition*, pass all three — built without a documented-unenforced precondition*, pass all
and the fact that the 2026-05 attempt could not (it forced a three — and the fact that the 2026-05 attempt could not (it
silent-divergence precondition; see forced a silent-divergence precondition; see
`docs/specs/2026-05-16-iteration-discipline-revert.md`) is itself `docs/specs/2026-05-16-iteration-discipline-revert.md`) is itself
the clause-3 mechanism working as intended. the bug-class-reintroduction discriminator working as intended.
This is the positive complement to the CLAUDE.md rule that This is the positive complement to the CLAUDE.md rule that
implementation effort is not a rationale: cost is not a reason *for* a implementation effort is not a rationale: cost is not a reason *for* a
+2 -2
View File
@@ -56,8 +56,8 @@ no `f32` variant. The runtime / codegen contract:
`0x7ff8000000000000` on one target and a different qNaN on `0x7ff8000000000000` on one target and a different qNaN on
another. another.
- The textual rendering of NaN through `float_to_str` (the runtime - The textual rendering of NaN through `float_to_str` (the runtime
C helper that backs `instance Show Float` and, post-iter-rpe.1, C helper that backs `instance Show Float` and every Float-typed
every Float-typed `print` call). The libc `printf("%g", nan)` `print` call). The libc `printf("%g", nan)`
glue used by `float_to_str` is permitted to emit `nan` / `-nan` glue used by `float_to_str` is permitted to emit `nan` / `-nan`
/ `NaN` etc. depending on libc version and the NaN's sign bit; / `NaN` etc. depending on libc version and the NaN's sign bit;
AILang does not normalise this, since the prose / surface-print AILang does not normalise this, since the prose / surface-print
+4 -4
View File
@@ -15,10 +15,10 @@ Two things never belong in a contract or model file:
A cross-reference that does belong stays: it is a formal, A cross-reference that does belong stays: it is a formal,
file-relative Markdown link into the durable tier (`design/` or file-relative Markdown link into the durable tier (`design/` or
source), enforced by source), enforced by the body-link durability gate in
[`design_index_pin.rs`](../../crates/ailang-core/tests/design_index_pin.rs) [`design_index_pin.rs`](../../crates/ailang-core/tests/design_index_pin.rs).
clause-5. A reference that cannot be expressed as such a link is, A reference that cannot be expressed as such a link is, by that
by that fact, the history-or-rationale prose the rule above removes. fact, the history-or-rationale prose the rule above removes.
The single legitimate exception is a present-tense reserved or The single legitimate exception is a present-tense reserved or
deliberately-excluded claim that is explicitly and correctly deliberately-excluded claim that is explicitly and correctly
+6 -5
View File
@@ -4,8 +4,9 @@
The four constraints below are necessary preconditions for RC to The four constraints below are necessary preconditions for RC to
be sound and complete *without* a cycle-collector backstop. They be sound and complete *without* a cycle-collector backstop. They
are not new — AILang already satisfies all four — but Decision 10 are not new — AILang already satisfies all four — but the
makes them load-bearing rather than incidental: [memory model](memory-model.md) makes them load-bearing rather
than incidental:
1. **Strict evaluation.** Every `Term::App` (see 1. **Strict evaluation.** Every `Term::App` (see
[Data model](data-model.md)) argument is fully [Data model](data-model.md)) argument is fully
@@ -33,8 +34,8 @@ These constraints are the precondition for the
[memory model](memory-model.md): a sound RC implementation without [memory model](memory-model.md): a sound RC implementation without
a cycle-collector backstop requires the reference graph to be a a cycle-collector backstop requires the reference graph to be a
DAG, which constraint 4 establishes from 13. They are also the DAG, which constraint 4 establishes from 13. They are also the
load-bearing structural reason behind several load-bearing structural reason behind several rejections under the
[feature-acceptance](feature-acceptance.md) clause-3 rejections [feature-acceptance](feature-acceptance.md) bug-class-reintroduction
(notably iterated-mutable-state reasoning). discriminator (notably iterated-mutable-state reasoning).
Ratified by: `crates/ailang-check/src/uniqueness.rs` (in-source mod tests). Ratified by: `crates/ailang-check/src/uniqueness.rs` (in-source mod tests).
+14 -12
View File
@@ -32,7 +32,8 @@ schema). The substantive reasons for per-position metadata over a
`Type` would let the schema express forms like `Type` would let the schema express forms like
`(con List (borrow Int))` — syntactically possible, semantically `(con List (borrow Int))` — syntactically possible, semantically
meaningless (you cannot separately own/borrow a list element meaningless (you cannot separately own/borrow a list element
from the list it lives in). Decision 1 is "schema = data, from the list it lives in). The
[canonical-schema principle](data-model.md) is "schema = data,
schema permits exactly what is meaningful"; per-position schema permits exactly what is meaningful"; per-position
metadata is the option that holds that line. metadata is the option that holds that line.
- **Compositional clarity.** A `Type` value's identity should - **Compositional clarity.** A `Type` value's identity should
@@ -62,7 +63,7 @@ identical: `param_modes` is skipped when every entry is
`Implicit`, `ret_mode` is skipped when `Implicit`. Existing `Implicit`, `ret_mode` is skipped when `Implicit`. Existing
modules emit the same bytes as before. modules emit the same bytes as before.
**Type::Con name scoping (canonical form, since ct.1).** Within a **Type::Con name scoping (canonical form).** Within a
`.ail.json`, a `Type::Con.name` is interpreted relative to the `.ail.json`, a `Type::Con.name` is interpreted relative to the
file's top-level `"name"` field (the owning module). Bare names file's top-level `"name"` field (the owning module). Bare names
(no `.`) refer to a TypeDef in the owning module's own `defs`. (no `.`) refer to a TypeDef in the owning module's own `defs`.
@@ -75,15 +76,16 @@ qualified references whose owner is unknown are also a violation
(`WorkspaceLoadError::BadCrossModuleTypeRef`). The same rule (`WorkspaceLoadError::BadCrossModuleTypeRef`). The same rule
applies to `Term::Ctor.type_name`. applies to `Term::Ctor.type_name`.
Class names follow the canonical-form rule (mq.1): bare for Class names follow the same canonical-form rule: bare for
same-module references, `<module>.<Class>` for cross-module same-module references, `<module>.<Class>` for cross-module
references — symmetric to `Type::Con.name`'s rule from ct.1. references — symmetric to `Type::Con.name`'s rule above.
Three schema fields carry class references in this form: Three schema fields carry class references in this form:
`InstanceDef.class`, `Constraint.class`, and `SuperclassRef.class`. `InstanceDef.class`, `Constraint.class`, and `SuperclassRef.class`.
`ClassDef.name` itself stays bare (defining-site context, like `ClassDef.name` itself stays bare (defining-site context, like
`TypeDef.name`). `TypeDef.name`).
Method dispatch is type-driven post-mq.3 (see [Method dispatch](method-dispatch.md)): Method dispatch is type-driven (see
[Method dispatch](method-dispatch.md)):
synth resolves a `Term::Var { name: "show" }` by consulting synth resolves a `Term::Var { name: "show" }` by consulting
the workspace's method-to-candidate-class index, filtering by the workspace's method-to-candidate-class index, filtering by
argument type (concrete) or by declared constraint (rigid-var), and argument type (concrete) or by declared constraint (rigid-var), and
@@ -161,24 +163,24 @@ Skipped from serialisation when empty so existing fixtures keep
bit-identical canonical-JSON hashes (regression-pinned by bit-identical canonical-JSON hashes (regression-pinned by
`iter19b_empty_suppress_preserves_pre_19b_hashes` and `iter19b_empty_suppress_preserves_pre_19b_hashes` and
`iter19b_schema_extension_preserves_pre_19b_hashes`). `iter19b_schema_extension_preserves_pre_19b_hashes`).
The canonical-form tightening in ct.1 shifted the hashes of two The canonical-form tightening for `Type::Con.name` shifted the
cross-module fixtures (`ordering_match.ail.json` and hashes of two cross-module fixtures (`ordering_match.ail.json` and
`test_22b1_dup_a.ail.json`); all intra-module fixtures, including `test_22b1_dup_a.ail.json`); all intra-module fixtures, including
the regression-pinned `sum.ail.json` and `list.ail.json`, remain the regression-pinned `sum.ail.json` and `list.ail.json`, remain
bit-identical. The new pins are bit-identical. The new pins are
`ct4_migrated_fixtures_have_canonical_form_hashes` (locks the `ct4_migrated_fixtures_have_canonical_form_hashes` (locks the
post-migration hashes) and post-migration hashes) and
`ct4_unmigrated_fixtures_remain_bit_identical` (re-asserts the `ct4_unmigrated_fixtures_remain_bit_identical` (re-asserts the
existing 13a/19b/22b.1 hashes still hold). pre-tightening hashes still hold).
## Advisory diagnostics ## Advisory diagnostics
The advisory-diagnostics arc introduces the language's first The advisory-diagnostics arc introduces the language's first
**advisory** typechecker diagnostic and the suppression mechanism **advisory** typechecker diagnostic and the suppression mechanism
that goes with it. Decision 10's mandatory-annotation rule is that goes with it. The mandatory-annotation rule of this memory
unchanged: `param_modes` and `ret_mode` remain author-required; model is unchanged: `param_modes` and `ret_mode` remain
the typechecker does not infer them. What's new is feedback when author-required; the typechecker does not infer them. What's new
an authored annotation is *stricter than necessary*. is feedback when an authored annotation is *stricter than necessary*.
**The lint: `over-strict-mode`.** Fires on a **The lint: `over-strict-mode`.** Fires on a
fn-param `p` annotated `(own T)` when: fn-param `p` annotated `(own T)` when:
+8 -6
View File
@@ -6,14 +6,16 @@ The class-method resolution rule that pairs with the
[typeclasses](typeclasses.md) contract; the `Show`/`Eq`/`Ord` instances [typeclasses](typeclasses.md) contract; the `Show`/`Eq`/`Ord` instances
the rule dispatches against live in [prelude classes](prelude-classes.md). the rule dispatches against live in [prelude classes](prelude-classes.md).
Post-mq.3, dispatch is two-mode: Dispatch is two-mode:
**Polymorphic call sites** (inside a fn body with `forall` + **Polymorphic call sites** (inside a fn body with `forall` +
constraint set): the constraint names the class via the qualified constraint set): the constraint names the class via the qualified
`Constraint.class` field (canonical-form per mq.1). Synth's `Constraint.class` field (canonical form: bare for same-module,
residual carries the class name directly; constraint-discharge at `<module>.<Class>` for cross-module; see
fn-body-end matches against the workspace registry by [memory model](memory-model.md) for the canonical-form rule).
`(class, type_hash)` key. Synth's residual carries the class name directly;
constraint-discharge at fn-body-end matches against the workspace
registry by `(class, type_hash)` key.
**Monomorphic call sites**: synth consults the workspace-flat **Monomorphic call sites**: synth consults the workspace-flat
`Env.method_to_candidate_classes: BTreeMap<MethodName, `Env.method_to_candidate_classes: BTreeMap<MethodName,
@@ -42,7 +44,7 @@ dispatch rule:
The `method_to_candidate_classes` index is the load-bearing data The `method_to_candidate_classes` index is the load-bearing data
structure for this routing — its construction in `build_check_env` structure for this routing — its construction in `build_check_env`
inverts the per-module `class_methods` maps (themselves tuple-keyed inverts the per-module `class_methods` maps (themselves tuple-keyed
by `(qualified-class, method)` post-mq.3) to a workspace-flat by `(qualified-class, method)`) to a workspace-flat
method-name-to-class-set map. method-name-to-class-set map.
Class-fn collisions resolve at the call site, not at workspace load Class-fn collisions resolve at the call site, not at workspace load
+2 -1
View File
@@ -24,7 +24,8 @@ corresponding runtime primitive (`int_to_str`, `bool_to_str`,
heap-Str primitives); no codegen intercept is required. The heap-Str primitives); no codegen intercept is required. The
polymorphic helper `print : forall a. Show a => a -> () !IO` has body polymorphic helper `print : forall a. Show a => a -> () !IO` has body
`\x -> let s = show x in do io/print_str s` (explicit let-binder for `\x -> let s = show x in do io/print_str s` (explicit let-binder for
heap-Str RC discipline per eob.1 Str carve-out). The let-binder is heap-Str RC discipline per the Str carve-out in
[Str ABI](str-abi.md)). The let-binder is
structurally pinned by `crates/ail/tests/print_mono_body_shape.rs`. structurally pinned by `crates/ail/tests/print_mono_body_shape.rs`.
Routing through `print` is the path for non-`Str` primitives; Routing through `print` is the path for non-`Str` primitives;
`io/print_str` is the only built-in direct-output effect-op. `io/print_str` is the only built-in direct-output effect-op.
+6 -6
View File
@@ -85,15 +85,15 @@ never by relaxing the test.
### Why this is anchored at top level ### Why this is anchored at top level
The invariant is a property of the language identity, not of any The invariant is a property of the language identity, not of any
one surface-design Decision. Decision 6 introduces the `.ail` one surface-design choice. The [authoring surface](authoring-surface.md)
surface and lists round-trip-as-property as one of its introduces the `.ail` surface and lists round-trip-as-property as
constraints, but the property is load-bearing for every one of its constraints, but the property is load-bearing for every
downstream concern that treats the two forms as exchangeable: downstream concern that treats the two forms as exchangeable:
content-addressed hashing, the LLM-author's choice of authoring content-addressed hashing, the LLM-author's choice of authoring
form, the integrity of fixture cross-references, and the form, the integrity of fixture cross-references, and the
prerequisite for empirical cross-model authoring-form studies. prerequisite for empirical cross-model authoring-form studies.
Lifting it out of Decision 6 makes the property quotable on its Lifting it out of the authoring-surface contract makes the property
own and reviewable by the architect agent at every milestone quotable on its own and reviewable by the architect agent at every
close. milestone close.
Ratified by: `crates/ailang-surface/tests/round_trip.rs`. Ratified by: `crates/ailang-surface/tests/round_trip.rs`.
+2 -1
View File
@@ -118,7 +118,8 @@ What **is** supported (and used as the smoke test for the pipeline):
pair allocates either via `@GC_malloc` (escaping; Boehm-managed) or pair allocates either via `@GC_malloc` (escaping; Boehm-managed) or
via LLVM `alloca` (non-escaping; freed at fn return). The decision via LLVM `alloca` (non-escaping; freed at fn return). The decision
is made by an escape-analysis pre-pass over the fn body — see is made by an escape-analysis pre-pass over the fn body — see
Decision 9's "Per-fn arena via stack `alloca`" subsection. the "Per-fn arena via stack `alloca`" subsection of
[RC + uniqueness](../models/rc-uniqueness.md).
Boehm-only soak tests are unchanged: `examples/gc_stress.ail.json` Boehm-only soak tests are unchanged: `examples/gc_stress.ail.json`
and `examples/std_list_stress.ail.json` still allocate via and `examples/std_list_stress.ail.json` still allocate via
`@GC_malloc` because their boxes flow into other fns and escape. `@GC_malloc` because their boxes flow into other fns and escape.
+1 -1
View File
@@ -1,6 +1,6 @@
# Tail calls # Tail calls
## Decision 8: explicit, verified tail calls ## Explicit, verified tail calls
For an LLM author, recursion is the natural iteration form For an LLM author, recursion is the natural iteration form
(`\n. if n == 0 then () else loop(n-1)` is what I reach for, not (`\n. if n == 0 then () else loop(n-1)` is what I reach for, not
+17 -14
View File
@@ -74,15 +74,18 @@ exactly as for any free function.
## Cross-module references in synthesised bodies ## Cross-module references in synthesised bodies
The unified mono pass (per Decision 11's milestone-23.4 reorganisation) The unified mono pass (see
synthesises mono symbols for polymorphic free fns and class-method [models/typeclasses](../models/typeclasses.md) for the
instances in the symbol's owner module — e.g. `print__Int` lives in resolution/monomorphisation algorithm) synthesises mono symbols for
`prelude` (because `print` is defined in the prelude), but a polymorphic free fns and class-method instances in the symbol's
user-ADT call site `print (MkIntBox 7)` causes synthesis of owner module — e.g. `print__Int` lives in `prelude` (because `print`
`prelude.print__IntBox` whose body references is defined in the prelude), but a user-ADT call site
`show_user_adt.show__IntBox` (the user instance lives in the `print (MkIntBox 7)` causes synthesis of `prelude.print__IntBox`
user-defining module per Decision 11 coherence). The synthesised body whose body references `show_user_adt.show__IntBox` (the user
crosses a module boundary the source template did not. instance lives in the user-defining module per the
orphan-free-coherence rule documented in
[models/typeclasses](../models/typeclasses.md)). The synthesised
body crosses a module boundary the source template did not.
Three invariants make this work: Three invariants make this work:
@@ -127,7 +130,7 @@ Three invariants make this work:
workspace registry; if no instance satisfies the residual at the workspace registry; if no instance satisfies the residual at the
unified concrete type, the `NoInstance` diagnostic fires at unified concrete type, the `NoInstance` diagnostic fires at
typecheck (correctly), not at codegen (confusingly). Without the typecheck (correctly), not at codegen (confusingly). Without the
residual push, milestone-23-shape negative cases (e.g. `print f` residual push, polymorphic-helper negative cases (e.g. `print f`
where `f : Int -> Int`) silently typecheck and surface as where `f : Int -> Int`) silently typecheck and surface as
`unknown variable: show` from codegen instead of the right `unknown variable: show` from codegen instead of the right
typecheck-phase NoInstance Show. typecheck-phase NoInstance Show.
@@ -211,17 +214,17 @@ module.
- `MissingConstraint` — body's residual constraint is not covered - `MissingConstraint` — body's residual constraint is not covered
by declared (and superclass-expanded) constraints. by declared (and superclass-expanded) constraints.
- `NoInstance` — fully concrete constraint has no registry entry. - `NoInstance` — fully concrete constraint has no registry entry.
mq.2 adds an optional `candidate_classes` field surfacing the Carries an optional `candidate_classes` field surfacing the
multi-candidate set when the bare-method dispatch path's filter multi-candidate set when the bare-method dispatch path's filter
collapses to zero registry survivors. collapses to zero registry survivors.
- `AmbiguousMethodResolution` (mq.2) — a monomorphic `Term::Var` - `AmbiguousMethodResolution` — a monomorphic `Term::Var`
call site survives both type-driven and constraint-driven filters call site survives both type-driven and constraint-driven filters
with more than one candidate class. LLM-author writes the explicit with more than one candidate class. LLM-author writes the explicit
qualifier form `<module>.<Class>.<method>` to disambiguate. qualifier form `<module>.<Class>.<method>` to disambiguate.
- `UnknownClass` (mq.2) — an explicit class qualifier in - `UnknownClass` — an explicit class qualifier in
`Term::Var.name` names a qualified class that is not in the `Term::Var.name` names a qualified class that is not in the
workspace's candidate-class index for the method. workspace's candidate-class index for the method.
- `class-method-shadowed-by-fn` (mq.3, warning) — a `Term::Var` - `class-method-shadowed-by-fn` (warning) — a `Term::Var`
resolved via fn lookup precedence (locals → caller-module-fn → resolved via fn lookup precedence (locals → caller-module-fn →
imported-fn) while a class method of the same name also exists imported-fn) while a class method of the same name also exists
in the workspace. Fn resolution proceeds; the warning surfaces in the workspace. Fn resolution proceeds; the warning surfaces
+4 -2
View File
@@ -1,6 +1,6 @@
# Effects — pure core + algebraic effects # Effects — pure core + algebraic effects
## Decision 3: pure core language + algebraic effects ## Pure core language + algebraic effects
The default is total, pure functions. Effects are declared as a set in the The default is total, pure functions. Effects are declared as a set in the
function type: `(Int) -> Int ![IO]`. The effect set is a function type: `(Int) -> Int ![IO]`. The effect set is a
@@ -13,7 +13,9 @@ currently in scope vs. excluded lives in
In the MVP only the effect `IO` is wired up (its sole op is `io/print_str`). In the MVP only the effect `IO` is wired up (its sole op is `io/print_str`).
`Diverge` (for non-termination) is reserved as an effect name but is `Diverge` (for non-termination) is reserved as an effect name but is
unimplemented — no op, no codegen, no checker injection — in the same unimplemented — no op, no codegen, no checker injection — in the same
sense Decision 4 reserves refinements. sense the refinements layer is reserved as a future extension without
present-day surface (see
[scope boundaries](../contracts/scope-boundaries.md)).
This is the most important LLM property: when I read a function, I can trust This is the most important LLM property: when I read a function, I can trust
its signature without reading the body. its signature without reading the body.
+1 -1
View File
@@ -7,7 +7,7 @@
├─ load + validate schema ├─ load + validate schema
├─ resolve names + assign hashes ├─ resolve names + assign hashes
├─ desugar (AST → AST) ├─ desugar (AST → AST)
├─ typecheck (HM, effect rows; mode-strict per Decision 10) ├─ typecheck (HM, effect rows; mode-strict per the memory model)
├─ lift_letrecs (post-typecheck AST → AST) ├─ lift_letrecs (post-typecheck AST → AST)
├─ lower to MIR (SSA-like, named SSA values) ├─ lower to MIR (SSA-like, named SSA values)
├─ emit LLVM IR (.ll) ├─ emit LLVM IR (.ll)
+5 -4
View File
@@ -5,9 +5,9 @@
AILang ships a AILang ships a
second textual projection of the AST: `ailang-prose`, a one-way second textual projection of the AST: `ailang-prose`, a one-way
projection from `Module → human-readable text`. It is **not** an projection from `Module → human-readable text`. It is **not** an
authoring surface; it is the "display" projection that Decision 6's authoring surface; it is the "display" projection that the
architectural pin (see [authoring surface](../contracts/authoring-surface.md)) [authoring surface](../contracts/authoring-surface.md)'s
explicitly anticipated: architectural pin explicitly anticipated:
> *"Future projections are explicitly anticipated: a visual / > *"Future projections are explicitly anticipated: a visual /
> graphical front-end is a plausible second projection for human > graphical front-end is a plausible second projection for human
@@ -41,7 +41,8 @@ form (B) deliberately is not. Re-integrating prose edits requires an
external LLM mediator, not a compiler pass — the prompt template external LLM mediator, not a compiler pass — the prompt template
`ail merge-prose` composes the six-step cycle. `ail merge-prose` composes the six-step cycle.
Form (B) does not weaken any Decision 6 invariant: Form (B) does not weaken any
[authoring surface](../contracts/authoring-surface.md) invariant:
- The [JSON-AST](../contracts/data-model.md) remains the only - The [JSON-AST](../contracts/data-model.md) remains the only
hashable artefact. Prose is not hashed, not content-addressed, hashable artefact. Prose is not hashed, not content-addressed,
+17 -16
View File
@@ -1,6 +1,6 @@
# RC + Uniqueness — memory model whitepaper # RC + Uniqueness — memory model whitepaper
## Decision 9: dual allocator — RC canonical, Boehm parity oracle ## Dual allocator — RC canonical, Boehm parity oracle
AILang ships two allocator backends with an asymmetric role: AILang ships two allocator backends with an asymmetric role:
@@ -25,12 +25,11 @@ when the parity oracle stops paying its keep — concretely, when a
few iter families ship without the gc arm catching anything that few iter families ship without the gc arm catching anything that
the rc arm did not already catch. Until then, the cost of the rc arm did not already catch. Until then, the cost of
keeping libgc as a build dependency is accepted in exchange for keeping libgc as a build dependency is accepted in exchange for
diagnostic leverage. Decision 10 (RC + uniqueness) holds as the diagnostic leverage. The
specification of the canonical runtime (the contract version is [RC + uniqueness memory model](../contracts/memory-model.md), built
[memory model](../contracts/memory-model.md), built on on the [language constraints](../contracts/language-constraints.md),
[language constraints](../contracts/language-constraints.md)); holds as the specification of the canonical runtime; the rest of
the rest of this section documents the Boehm half, retained as this section documents the Boehm half, retained as the oracle.
the oracle.
**Choice: Boehm-Demers-Weiser conservative GC.** The simplest **Choice: Boehm-Demers-Weiser conservative GC.** The simplest
working option: working option:
@@ -130,7 +129,7 @@ The closure-pair and its env share an escape verdict — they have
parallel lifetimes. If the closure pair is non-escaping, the env parallel lifetimes. If the closure pair is non-escaping, the env
is too. is too.
## Decision 10: memory model — RC + Uniqueness with LLM-author annotations ## Memory model — RC + Uniqueness with LLM-author annotations
**The GC bench (`bench/run.sh`) showed **The GC bench (`bench/run.sh`) showed
Boehm contributing a substantial fraction of runtime on allocation-heavy workloads Boehm contributing a substantial fraction of runtime on allocation-heavy workloads
@@ -150,9 +149,10 @@ stdlib is the cheapest moment to commit.
**Choice.** AILang's canonical [memory model](../contracts/memory-model.md) **Choice.** AILang's canonical [memory model](../contracts/memory-model.md)
is reference counting with static uniqueness inference **and is reference counting with static uniqueness inference **and
explicit LLM-author annotations on fn signatures**, in the lineage explicit LLM-author annotations on fn signatures**, in the lineage
of Lean 4 / Roc / Koka. Boehm becomes a transitional allocator (Decision 9) and is of Lean 4 / Roc / Koka. Boehm becomes a transitional allocator (see
retired when the RC pipeline matches the bump-allocator floor the dual-allocator section above) and is retired when the RC pipeline
within an acceptable margin (target 1.3× on `bench/run.sh`). matches the bump-allocator floor within an acceptable margin (target
1.3× on `bench/run.sh`).
**Workload scope of the 1.3× target.** The 1.3× target was **Workload scope of the 1.3× target.** The 1.3× target was
calibrated on the original `bench/run.sh` corpus: linear list calibrated on the original `bench/run.sh` corpus: linear list
@@ -168,16 +168,17 @@ one bump-pointer bump, doubling the allocation tax on closure
construction (current ratio recorded in JOURNAL bench entries). construction (current ratio recorded in JOURNAL bench entries).
This is a representational cost of the closure-pair layout, This is a representational cost of the closure-pair layout,
not a defect in the RC implementation; a future slab/pool not a defect in the RC implementation; a future slab/pool
allocator for fixed-shape pair cells (Decision 9 retirement allocator for fixed-shape pair cells (a Boehm-retirement follow-up)
follow-up) would compress this ratio without changing semantics. would compress this ratio without changing semantics.
The 1.3× retirement target therefore applies to the linear / The 1.3× retirement target therefore applies to the linear /
tree / poly-ADT subset of the corpus. Closure-heavy workloads are tree / poly-ADT subset of the corpus. Closure-heavy workloads are
tracked under a wider band (the closure-chain baseline records its rc/bump ratio as the `rc_over_bump` reference value with ±15% tolerance) and tracked under a wider band (the closure-chain baseline records its rc/bump ratio as the `rc_over_bump` reference value with ±15% tolerance) and
are explicitly excluded from the Boehm-retirement gate until a are explicitly excluded from the Boehm-retirement gate until a
slab/pool answer ships. Decision-10's RC commitment is unchanged; slab/pool answer ships. The
what is scoped is the *quantitative* retirement criterion, not [memory model](../contracts/memory-model.md)'s RC commitment is
the choice of memory model. unchanged; what is scoped is the *quantitative* retirement criterion,
not the choice of memory model.
The architecture has two layers: The architecture has two layers:
+7 -8
View File
@@ -8,7 +8,7 @@ built-in classes shipped in the prelude in
covers the design choices and the resolution / monomorphisation covers the design choices and the resolution / monomorphisation
algorithm. algorithm.
## Decision 11: typeclasses — Haskell-lite, monomorphised, coherent ## The design — Haskell-lite, monomorphised, coherent
**The design pass for typeclasses. Codified after the **The design pass for typeclasses. Codified after the
[Feature-acceptance criterion](../contracts/feature-acceptance.md) [Feature-acceptance criterion](../contracts/feature-acceptance.md)
@@ -158,10 +158,9 @@ component contains `__` by project convention.
**No runtime dispatch, no dictionary passing.** The monomorphisation **No runtime dispatch, no dictionary passing.** The monomorphisation
pass is the ONLY specialiser. Codegen sees only monomorphic pass is the ONLY specialiser. Codegen sees only monomorphic
`Def::Fn`s and direct calls; the pre-iter-23.4 codegen-time `Def::Fn`s and direct calls. A call that cannot be monomorphised —
specialiser (`lower_polymorphic_call` + `module_polymorphic_fns` + for instance, because a constraint remains unresolved at the entry
`mono_queue`) was removed in iter 23.4. A call that cannot be point — is a static error, not a runtime one. This is the
monomorphised — for instance, because a constraint remains LLVM-friendly form and is consistent with the performance
unresolved at the entry point — is a static error, not a runtime commitment of the
one. This is the LLVM-friendly form and is consistent with [RC + uniqueness memory model](../contracts/memory-model.md).
Decision 10's performance commitment.