source: scrub iter-code / Decision-N residue from inline comments

Follow-up to bcd4181: the remaining ~530 inline `//` and `///`
comments still carrying opaque shorthand are now reformulated to
their content phrases. The only surviving `iter-<code>` reference
in source is the literal filename
`docs/journals/2026-05-13-iter-mq.3.md` (a real journal file).

Sweep covered:
  - `// Iter X.Y: <text>` prefixes (Iter 13a / 14a / 14e / 15g-aux /
    16b.x / 16d / 16e / 18b / 18c.x / 18d.x / 18e / 18g.x / 19a /
    19a.1 / 19b / 20a / 20f / 22-floats.x / 22b.x / 22c / 23.x /
    24.1 / cli-diag-human / hs.x / str-concat / etc.) — fully
    removed; the descriptive text that followed each prefix stays.
  - `// (Decision N)` and `per Decision N` and `Decision N axis 3` —
    replaced with the content phrase plus the relevant contract
    file (`design/contracts/tail-calls.md` for Decision 8,
    `design/contracts/memory-model.md` for Decision 10,
    `design/contracts/typeclasses.md` and `design/models/typeclasses.md`
    for Decision 11, `design/contracts/authoring-surface.md` for
    Decision 6, "the transitional dual-allocator" for Decision 9,
    "Effect prose" for Decision 3).
  - `// mq.X / mq.X (Task N) / mq.X journal / mq.X invariant` ->
    "the canonical-class-form rule / Class-class repurpose /
    method-dispatch-refactor journal / canonical-class-form
    invariant".
  - `// ct.X / ct.X (canonical-type-names) / ct.1.5a + ctt.2 /
    ct.2 Task N` -> "the canonical-form rule for type references /
    the canonical-form normalisation step / canonical-type-lookup
    refactor".
  - `// eob.X` -> "heap-Str-ABI" / "the Str carve-out".
  - `// rpe.X` -> "the per-type-print-op retirement".
  - `// post-mq.X / Pre-ct.X / pre-mq.X` -> "post-canonical-class-form" /
    "Pre-canonical-type-form" etc.
  - `/// Iter X regression: / /// Iter X.Y: / /// Iter A arm-close /
    /// Iter ct.4 (...): / /// Iter rpe.1 ...` -> descriptive
    phrases.

The journal filename in `crates/ailang-core/src/workspace.rs:573`
stays verbatim because it points at an actual file under
`docs/journals/`.

Tests: full `cargo test --workspace` green (80/80 test-result blocks
clean, no FAILED line). design_index_pin 5/5 + docs_honesty_pin 5/5
gating tests pass.
This commit is contained in:
2026-05-20 09:58:04 +02:00
parent bcd41810f4
commit ac4d545570
43 changed files with 1004 additions and 1000 deletions
+43 -42
View File
@@ -137,10 +137,10 @@ enum Cmd {
/// `runtime/rc.c`'s `@ailang_rc_alloc` (libc-malloc backing +
/// 8-byte refcount header) with `ailang_rc_inc`/`_dec`
/// instrumentation emitted by codegen; this is the runtime
/// AILang's memory model (RC + uniqueness, Decision 10) is
/// AILang's memory model (RC + uniqueness) is
/// designed for. `gc` retains the Boehm conservative GC path
/// (`@GC_malloc`, libgc); it is kept as a differential parity
/// oracle for codegen diagnosis (Decision 9). `bump` is a
/// oracle for codegen diagnosis (the transitional dual-allocator). `bump` is a
/// bench-only no-free stub from `runtime/bump.c` — it leaks
/// every allocation by design.
#[arg(long, default_value = "rc", value_parser = ["gc", "bump", "rc"])]
@@ -199,7 +199,7 @@ enum Cmd {
/// Loads a workspace (entry module + transitive imports) and lists
/// all reachable modules with hash and def count.
///
/// Iter 5a: listing only. Cross-module typecheck/codegen follow in
/// listing only. Cross-module typecheck/codegen follow in
/// 5b/5c; existing subcommands continue to work per single module.
Workspace {
entry: PathBuf,
@@ -217,14 +217,14 @@ enum Cmd {
#[arg(short, long)]
output: Option<PathBuf>,
},
/// Iter 20a: prints the module as human-readable prose (form B).
/// prints the module as human-readable prose (form B).
///
/// One-way projection: the renderer is deterministic, but no
/// parser exists for the prose surface. The canonical authoring
/// surface remains form (A) (`render` / `parse`) and the canonical
/// hashable artefact remains the JSON-AST.
Prose { path: PathBuf },
/// Iter 20d: composes a prompt for the prose-edit round-trip.
/// composes a prompt for the prose-edit round-trip.
///
/// Given the original `.ail.json` and the edited `.prose.txt`,
/// prints a prompt that asks an external LLM to emit an updated
@@ -241,7 +241,7 @@ enum Cmd {
/// Edited `.prose.txt` (carries the human's intent).
edited: PathBuf,
},
/// ct.1 (dev-only): rewrite every `*.ail.json` under `<dir>` so
/// Dev-only: rewrite every `*.ail.json` under `<dir>` so
/// that bare cross-module `Type::Con` and `Term::Ctor.type_name`
/// references gain their owning module's qualifier. Idempotent.
///
@@ -268,10 +268,10 @@ enum Cmd {
/// string is the role-statement, contract, and output spec that frame
/// the LLM's task.
///
/// Iter 20f rewrote this function: the prompt now embeds
/// the prompt now embeds
/// [`ailang_core::FORM_A_SPEC`] (the language reference an LLM needs
/// to generate AILang from scratch) and the original module as Form-A
/// (the canonical authoring surface, Decision 6) instead of JSON-AST.
/// (the canonical authoring surface) instead of JSON-AST.
/// JSON-AST stays the canonical hashable artefact, but no human or
/// LLM should write it directly — `ail parse` converts the LLM's
/// Form-A output to JSON.
@@ -444,15 +444,15 @@ fn main() -> Result<()> {
print!("{}", ailang_surface::print(&m));
}
Cmd::Prose { path } => {
// Iter 20a: load via `load_module` (single-module mode,
// load via `load_module` (single-module mode,
// matching how `render` / `parse` work). Workspace-wide
// prose is not in scope for 20a.
let m = ailang_surface::load_module(&path)?;
print!("{}", ailang_prose::module_to_prose(&m));
}
Cmd::MergeProse { original, edited } => {
// Iter 20f: load the original .ail.json, render it as
// Form-A (the canonical authoring surface, Decision 6),
// load the original .ail.json, render it as
// Form-A (the canonical authoring surface),
// and embed *that* in the prompt — not the raw JSON-AST.
// Form-A is the form an LLM should produce; JSON is the
// hashable artefact, not the writing surface. The prose
@@ -582,7 +582,7 @@ fn main() -> Result<()> {
}
}
Cmd::Check { path, json } => {
// Iter 5b: `ail check` now **always** loads via
// `ail check` now **always** loads via
// `load_workspace` and checks cross-module. For modules
// without imports, the workspace loader behaves equivalently
// to `load_module` plus a hash consistency check of the
@@ -626,7 +626,7 @@ fn main() -> Result<()> {
d.message,
);
}
// Iter 19a: only Error-severity diagnostics fail the
// only Error-severity diagnostics fail the
// command. Warning-severity diagnostics (e.g. the
// `over-strict-mode` lint) print to stderr but the
// module is still considered to have typechecked.
@@ -646,7 +646,7 @@ fn main() -> Result<()> {
}
}
Cmd::EmitIr { path, out, emit } => {
// Iter 5c: workspace lowering. For single-module programs the
// workspace lowering. For single-module programs the
// workspace is effectively a trivial workspace with one module.
let ws = load_workspace_human(&path)?;
let diags = ailang_check::check_workspace(&ws);
@@ -666,7 +666,7 @@ fn main() -> Result<()> {
d.message,
);
}
// Iter 19a: only Error-severity blocks codegen.
// only Error-severity blocks codegen.
if diags
.iter()
.any(|d| matches!(d.severity, ailang_check::Severity::Error))
@@ -674,7 +674,7 @@ fn main() -> Result<()> {
std::process::exit(1);
}
}
// Iter 23.4: emit-ir must run the same pre-codegen pipeline
// emit-ir must run the same pre-codegen pipeline
// as `build` — `lift_letrecs` per module, then
// `monomorphise_workspace`. Pre-iter-23.4 this was implicit:
// codegen's poly-call path handled specialisation internally.
@@ -726,7 +726,7 @@ fn main() -> Result<()> {
}
}
Cmd::Run { path, opt, alloc, args } => {
// Iter 9b: build into a fresh tempdir per run, exec, propagate
// build into a fresh tempdir per run, exec, propagate
// exit code. The artefact dir is left around (no cleanup) so
// it can be inspected in case of a crash; OS temp policy
// collects them.
@@ -1098,9 +1098,10 @@ fn workspace_error_to_diagnostic(
"module": name,
})),
),
// Iter 22b.1: typeclass-coherence diagnostics emitted from
// typeclass-coherence diagnostics emitted from
// `workspace::build_registry`. The codes follow the JOURNAL
// queue's wording and Decision 11 §"Diagnostic categories".
// queue's wording and the diagnostic-categories section of
// `design/contracts/typeclasses.md`.
W::OrphanInstance {
class,
type_repr,
@@ -1231,7 +1232,7 @@ fn workspace_error_to_diagnostic(
"type": type_repr,
})),
),
// Iter 23.1: the user named a module `prelude`, which the
// the user named a module `prelude`, which the
// loader reserves for the auto-injected prelude module.
W::ReservedModuleName { name } => Some(
ailang_check::Diagnostic::error(
@@ -1244,7 +1245,7 @@ fn workspace_error_to_diagnostic(
"module": name,
})),
),
// ct.1 (canonical-type-names): bare `Type::Con` that does not
// the canonical-form rule for type references: bare `Type::Con` that does not
// resolve to a primitive or a local TypeDef of the owning
// module. `candidates` lists qualified suggestions scanned
// from imports.
@@ -1263,7 +1264,7 @@ fn workspace_error_to_diagnostic(
"candidates": candidates,
})),
),
// ct.1: qualified `<owner>.<type>` where `<owner>` is not a
// qualified `<owner>.<type>` where `<owner>` is not a
// known module or `<owner>` has no `TypeDef <type>`.
W::BadCrossModuleTypeRef { module, name } => Some(
ailang_check::Diagnostic::error(
@@ -1278,7 +1279,7 @@ fn workspace_error_to_diagnostic(
"name": name,
})),
),
// mq.1 (canonical-class-names): bare class-ref that does not
// the canonical-form rule for class references: bare class-ref that does not
// resolve to a local class of the owning module. Sibling of
// `BareCrossModuleTypeRef` for class-reference fields.
W::BareCrossModuleClassRef { module, name, candidates } => Some(
@@ -1295,7 +1296,7 @@ fn workspace_error_to_diagnostic(
"candidates": candidates,
})),
),
// mq.1: qualified `<owner>.<class>` where `<owner>` is not a
// qualified `<owner>.<class>` where `<owner>` is not a
// known module or `<owner>` has no class `<class>`.
W::BadCrossModuleClassRef { module, name } => Some(
ailang_check::Diagnostic::error(
@@ -1310,7 +1311,7 @@ fn workspace_error_to_diagnostic(
"name": name,
})),
),
// ct.1: a class-reference field contains a `.` — class names
// a class-reference field contains a `.` — class names
// are not module-qualified in this milestone.
W::QualifiedClassName { module, name, field } => Some(
ailang_check::Diagnostic::error(
@@ -1343,7 +1344,7 @@ fn workspace_error_to_diagnostic(
}
}
/// Iter cli-diag-human (2026-05-14): loads a workspace and, on
/// loads a workspace and, on
/// `WorkspaceLoadError`, routes through `workspace_error_to_diagnostic`
/// to print a stderr line matching the JSON path's `[code]`-bracketed
/// format before exiting non-zero. Pure I/O errors have no diagnostic
@@ -1416,7 +1417,7 @@ fn collect_refs(def: &ailang_core::Def) -> std::collections::BTreeSet<String> {
}
}
}
// Iter 22b.1: `ail deps` does not yet trace references inside
// `ail deps` does not yet trace references inside
// class/instance defs. Walking method signatures and bodies
// (default bodies, instance method bodies) lands in 22b.2 along
// with the typecheck arms — until then a class/instance def
@@ -1514,7 +1515,7 @@ fn walk_term(
walk_term(rhs, out, builtins, scope);
}
Term::LetRec { name, params, body, in_term, .. } => {
// Iter 16b.1: `deps` walks the on-disk module before the
// `deps` walks the on-disk module before the
// desugar pass, so a `Term::LetRec` is reachable here.
// Treat it like a fn def for dependency purposes:
// `name` shadows for both body and in_term; params shadow
@@ -1536,12 +1537,12 @@ fn walk_term(
}
}
Term::Clone { value } => {
// Iter 18c.1: clone is identity for dependency-walk
// clone is identity for dependency-walk
// purposes. The wrapper introduces no new symbols.
walk_term(value, out, builtins, scope);
}
Term::ReuseAs { source, body } => {
// Iter 18d.1: walk both children; the wrapper introduces no
// walk both children; the wrapper introduces no
// new bindings of its own.
walk_term(source, out, builtins, scope);
walk_term(body, out, builtins, scope);
@@ -1851,7 +1852,7 @@ fn def_summary(d: &ailang_core::Def) -> (&'static str, String, Vec<String>) {
.join(" | ");
("type", s, vec![])
}
// Iter 22b.1: a one-line `class C a` / `instance C T` summary
// a one-line `class C a` / `instance C T` summary
// for `ail manifest`. The `effects` slot is empty for both —
// class methods carry their own effect annotations on each
// method signature, but the class itself does not (it is a
@@ -2243,13 +2244,13 @@ fn locate_str_runtime() -> Result<PathBuf> {
)
}
/// Iter 9b: shared build helper for `Cmd::Build` and `Cmd::Run`.
/// shared build helper for `Cmd::Build` and `Cmd::Run`.
/// Loads the workspace, runs the typechecker, emits IR, and links via
/// clang. On typecheck failure, prints diagnostics to stderr and exits
/// the process with code 1. On clang failure, returns a Result error
/// (the .ll path is preserved for post-mortem inspection).
///
/// Iter 16b.3: between `check_workspace` and `lower_workspace` we run
/// between `check_workspace` and `lower_workspace` we run
/// `ailang_check::lift_letrecs` per module. The lift eliminates any
/// `Term::LetRec` that the desugar pass left in place (specifically:
/// LetRecs that capture `Term::Let`-bound names, whose types are
@@ -2287,7 +2288,7 @@ fn build_to(
d.message,
);
}
// Iter 19a: only Error-severity blocks the build. Warning-
// only Error-severity blocks the build. Warning-
// severity diagnostics (e.g. `over-strict-mode`) print but
// do not abort.
if diags
@@ -2297,7 +2298,7 @@ fn build_to(
std::process::exit(1);
}
}
// Iter 16b.3: run `lift_letrecs` per module on the post-desugar
// run `lift_letrecs` per module on the post-desugar
// form. Codegen's internal desugar pass is idempotent on a
// module that contains no `Term::LetRec`, so the lifted output
// can be handed directly to `lower_workspace`.
@@ -2312,13 +2313,13 @@ fn build_to(
entry: ws.entry.clone(),
modules: lifted_modules,
root_dir: ws.root_dir.clone(),
// Iter 22b.1: pass the registry through the lift pass. Lift
// pass the registry through the lift pass. Lift
// only rewrites `Term::LetRec` into top-level fns; it does
// not touch class/instance defs, so the registry built at
// load-time remains valid.
registry: ws.registry.clone(),
};
// Iter 22b.3: monomorphisation pass. After `lift_letrecs` and
// monomorphisation pass. After `lift_letrecs` and
// before codegen, every (class-method, concrete-type) call-site
// pair becomes a synthesised top-level fn; user call sites are
// rewritten to target those names. Class-free workspaces flow
@@ -2333,7 +2334,7 @@ fn build_to(
let out_bin = out.unwrap_or_else(|| Path::new(".").join(&ws.entry).with_extension(""));
let mut clang = std::process::Command::new("clang");
clang.arg(opt).arg("-o").arg(&out_bin).arg(&ll_path);
// Iter 23.2: compile and link `runtime/str.c` unconditionally —
// compile and link `runtime/str.c` unconditionally —
// `@ail_str_eq` is emitted in the IR header without an alloc-strategy
// guard (it backs the prelude `eq__Str` mono symbol). The .o is
// cached at <tmpdir>/str.o per build invocation; if a program never
@@ -2356,7 +2357,7 @@ fn build_to(
);
}
clang.arg(&str_obj);
// Iter hs.4: compile and link `runtime/rc.c` unconditionally,
// compile and link `runtime/rc.c` unconditionally,
// hoisted out of the AllocStrategy::Rc arm. `runtime/str.c`'s
// `ailang_int_to_str` / `ailang_float_to_str` call
// `ailang_rc_alloc` defined in rc.c; the weak extern declaration
@@ -2386,7 +2387,7 @@ fn build_to(
clang.arg(&rc_obj);
match alloc {
ailang_codegen::AllocStrategy::Gc => {
// Boehm conservative GC (Decision 9 / Iter 14f). The lowered
// Boehm conservative GC (the transitional dual-allocator). The lowered
// IR calls @GC_malloc; libgc supplies it. Pthread/dl are
// pulled in transitively via libgc.so on Linux, so a single
// -lgc suffices.
@@ -2418,7 +2419,7 @@ fn build_to(
clang.arg(&bump_obj);
}
ailang_codegen::AllocStrategy::Rc => {
// Iter hs.4: rc.c compile-and-link hoisted to the
// rc.c compile-and-link hoisted to the
// unconditional path above. `--alloc=rc` is still a
// valid CLI flag — it drives codegen's
// `@ailang_rc_alloc`-vs-`@GC_malloc` selection in the IR
@@ -2547,7 +2548,7 @@ fn run_cmd(bin: &str, args: &[&str], ctx: &str) -> Result<()> {
Ok(())
}
/// ct.1 migration helper: rewrite bare cross-module Type::Con names
/// Canonical-form migration helper: rewrite bare cross-module Type::Con names
/// and Term::Ctor.type_name to their qualified form. Sets
/// `*changed` to `true` if any rewrite happened. Uses the
/// first-match-in-imports-order disambiguation rule with prelude as
+5 -5
View File
@@ -86,12 +86,12 @@ fn check_json_emits_bad_cross_module_type_ref() {
);
}
/// Property: mq.1 — a qualified class name in an `InstanceDef.class`
/// Property: with class references in canonical form, a qualified class name in an `InstanceDef.class`
/// field is the canonical form, not a rejection. The
/// `test_ct1_qualified_class_rejected` fixture (declares
/// `instance prelude.Eq Int` outside prelude and outside Int's
/// defining module) is now rejected by the downstream coherence
/// check with `orphan-instance` instead of the pre-mq.1
/// check with `orphan-instance` instead of the pre-canonical-class-form
/// `qualified-class-name`. Guards against
/// `workspace_error_to_diagnostic` losing the OrphanInstance arm
/// AND against any regression that would reintroduce
@@ -119,12 +119,12 @@ fn check_json_emits_orphan_instance_on_xmod_class_without_coherence_post_mq1() {
);
assert!(
!arr.iter().any(|d| d["code"] == "qualified-class-name"),
"must NOT fire `qualified-class-name` on a referencing field post-mq.1; got {stdout}"
"must NOT fire `qualified-class-name` on a referencing field post-canonical-class-form; got {stdout}"
);
}
/// Property: in non-JSON (human) mode `ail check` exits non-zero on a
/// ct.1 validator failure and writes an actionable error message to
/// canonical-type-form validator failure and writes an actionable error message to
/// stderr — naming both the offending type and the migration command
/// the author should run. Pinning the human-mode path separately
/// because in this mode the loader error short-circuits via `anyhow`
@@ -160,7 +160,7 @@ fn check_human_mode_emits_actionable_message_to_stderr() {
/// Property: the post-migration `examples/ordering_match.ail.json`
/// (Term::Ctor `Ordering` -> `prelude.Ordering`) typechecks cleanly
/// through the CLI — `ail check` exits 0 with no diagnostics.
/// Guards against (a) a revert of the ct.1.5 migration, (b) a
/// Guards against (a) a revert of the canonical-form migration, (b) a
/// regression in `Registry::normalize_type_for_lookup` that would make
/// the canonical (qualified) form fail to dispatch where the bare
/// form used to succeed.
+88 -87
View File
@@ -37,7 +37,7 @@ fn build_and_run(example: &str) -> String {
String::from_utf8(output.stdout).expect("stdout utf8")
}
/// Iter 18b: build with an explicit `--alloc=<alloc>` and run.
/// build with an explicit `--alloc=<alloc>` and run.
/// Mirrors [`build_and_run`] but threads the allocator selector through
/// to `ail build`. Used by the alloc-equivalence tests that assert the
/// `gc` and `rc` paths produce byte-identical stdout.
@@ -140,7 +140,7 @@ fn list_sum_via_match() {
assert_eq!(stdout.trim(), "42");
}
/// Iter 7: first-class function references — `apply(inc, 41)` must
/// first-class function references — `apply(inc, 41)` must
/// produce 42, exercising fn-typed parameters and indirect call.
#[test]
fn higher_order_apply_inc() {
@@ -148,7 +148,7 @@ fn higher_order_apply_inc() {
assert_eq!(stdout.trim(), "42");
}
/// Iter 8b: lambdas with capture. `let n = 3 in apply(\x. x + n, 39)`
/// lambdas with capture. `let n = 3 in apply(\x. x + n, 39)`
/// must print 42 — the lambda captures `n` from the enclosing `let`,
/// the env struct is malloc'd, the closure pair is passed to `apply`,
/// and apply's indirect call unpacks both halves.
@@ -158,7 +158,7 @@ fn closure_captures_let_n() {
assert_eq!(stdout.trim(), "42");
}
/// Iter 9 dogfood: a non-trivial program that combines ADTs, recursion,
/// a non-trivial program that combines ADTs, recursion,
/// closure-without-capture, fn-typed parameters, IO effects, and `let`-
/// sequencing of effectful sub-expressions in a pattern-match arm.
/// `map (\x. x * 2)` over `[1, 2, 3]` then print each element.
@@ -169,7 +169,7 @@ fn list_map_doubles_then_prints() {
assert_eq!(lines, vec!["2", "4", "6"]);
}
/// Iter 14a: end-to-end exercise of parameterised ADTs through a
/// end-to-end exercise of parameterised ADTs through a
/// polymorphic higher-order fn. `data List a` plus
/// `map : forall a b. ((a) -> b, List<a>) -> List<b>` recursive,
/// instantiated at `(Int, Int)`. Guards: forall with two type vars +
@@ -183,7 +183,7 @@ fn list_map_poly_inc_then_prints() {
assert_eq!(lines, vec!["2", "3", "4"]);
}
/// Iter 14f: stress the Boehm conservative GC integration end-to-end.
/// stress the Boehm conservative GC integration end-to-end.
/// `build 50` performs 50 `Cons` allocations via `GC_malloc`; `sum_list`
/// walks the resulting `List Int` and prints the sum (1275 = 50*51/2).
/// If GC is wired wrongly — missing `-lgc`, missing
@@ -202,7 +202,7 @@ fn gc_handles_recursive_list_construction() {
);
}
/// Iter 17a: per-fn arena via stack `alloca` for non-escaping
/// per-fn arena via stack `alloca` for non-escaping
/// allocations. The fixture's `peek` and `count` fns each build a
/// `Box(_)` whose payload is dropped via a wildcard pattern; the
/// allocation is fully consumed locally and never flows to a fn
@@ -264,7 +264,7 @@ fn iter17a_local_box_alloca() {
}
}
/// Iter 14e: `tail: true` annotation on `print_list`'s recursive
/// `tail: true` annotation on `print_list`'s recursive
/// call must reach LLVM as a `musttail call`. Asserted by emitting IR
/// for list_map_poly and grepping for the exact instruction. This is
/// the only direct evidence that the type-system marker actually
@@ -320,7 +320,7 @@ fn iter14e_print_list_recursion_emits_musttail() {
panic!("musttail call line not found (sanity check)");
}
/// Iter 11 dogfood: insertion sort over an 11-element IntList.
/// insertion sort over an 11-element IntList.
/// Exercises `<=`, `if`, mutual-leaf recursion (`insert` and `sort`),
/// nested ctor construction, and the Iter 10 seq operator inside
/// print_list. Validates that the language handles deeper ADT
@@ -335,7 +335,7 @@ fn insertion_sort_orders_list() {
);
}
/// Iter 12b: polymorphism end-to-end. `id : forall a. (a) -> a`
/// polymorphism end-to-end. `id : forall a. (a) -> a`
/// gets called with `42` (Int) and `true` (Bool); each instantiation
/// triggers a separate specialised LLVM fn. Output: 42, then "true".
#[test]
@@ -345,7 +345,7 @@ fn polymorphic_id_at_int_and_bool() {
assert_eq!(lines, vec!["42", "true"]);
}
/// Iter 12b: polymorphism with a function-typed parameter.
/// polymorphism with a function-typed parameter.
/// `apply : forall a b. ((a) -> b, a) -> b` invoked as
/// `apply(succ, 41) == 42`. Exercises the closure-pair ABI inside a
/// monomorphised body (a fn-typed param survives substitution).
@@ -355,7 +355,7 @@ fn polymorphic_apply_with_fn_param() {
assert_eq!(stdout.trim(), "42");
}
/// Iter 13b: round-trip a primitive through a parameterised ADT and a
/// round-trip a primitive through a parameterised ADT and a
/// polymorphic-fn boundary. `type Box[a] = MkBox(a)` plus
/// `unbox : forall a. (Box<a>) -> a` plus `print_int(unbox(MkBox(42)))`.
/// Guards: ctor lower with substituted LLVM field types,
@@ -367,7 +367,7 @@ fn parameterised_box_round_trip() {
assert_eq!(stdout.trim(), "42");
}
/// Iter 13b: pattern-match on `Maybe<Int>`. `or_else(Some(7), 99) == 7`
/// pattern-match on `Maybe<Int>`. `or_else(Some(7), 99) == 7`
/// then `or_else(None, 99) == 99`. Guards: match-arm field
/// substitution at a parameterised ctor (the `Some(x)` arm must bind
/// `x : Int`, not `x : a`).
@@ -378,13 +378,14 @@ fn parameterised_maybe_match() {
assert_eq!(lines, vec!["7", "99"]);
}
/// Iter 15a: cross-module reference to a parameterised ADT, including
/// cross-module reference to a parameterised ADT, including
/// its ctors and a polymorphic combinator instantiated at `(Int, Int)`.
/// `std_maybe_demo` imports `std_maybe`, qualifies the type-name slot
/// of every `term-ctor` (`std_maybe.Maybe`), and exercises
/// `from_maybe`, `is_some`, `is_none`, and `map_maybe` once each.
/// Property protected: the qualified-only convention (Decision 6's
/// architectural pin extended to types and ctors per the brief)
/// Property protected: the qualified-only convention (the
/// authoring surface's architectural pin extended to types and
/// ctors per the brief)
/// flows end-to-end through check, codegen, and runtime.
#[test]
fn cross_module_maybe_demo() {
@@ -393,7 +394,7 @@ fn cross_module_maybe_demo() {
assert_eq!(lines, vec!["7", "99", "true", "true", "42"]);
}
/// Iter 15b: drives the polymorphic `std_list` combinators end-to-end.
/// drives the polymorphic `std_list` combinators end-to-end.
/// Exercises a recursive cross-module ADT (`std_list.List<a>`) consumed
/// from a separate module that also imports `std_maybe`. Guards the
/// `qualify_local_types` propagation in both `Term::Ctor` synth and
@@ -412,7 +413,7 @@ fn std_list_demo() {
);
}
/// Iter 15c: empirical stress test for `std_list`'s folds at N=1000.
/// empirical stress test for `std_list`'s folds at N=1000.
///
/// Property protected: `fold_left`'s tail-call marker actually
/// survives monomorphisation and lowers to `musttail call` in the
@@ -450,7 +451,7 @@ fn std_list_stress_1000_element_folds() {
// `crates/ail/tests/roundtrip_cli.rs` (added in T1), which is strictly
// stronger (whole-corpus vs. one-fixture).
/// Iter 16a: nested constructor patterns. Property protected:
/// nested constructor patterns. Property protected:
/// `ailang_core::desugar` flattens `Cons a (Cons b _)` to a chain of
/// single-level matches before the checker/codegen sees it. Without
/// the desugar pass the checker would emit
@@ -463,7 +464,7 @@ fn nested_ctor_pattern_first_two_sum() {
assert_eq!(lines, vec!["30"]);
}
/// Iter 15f: `std_pair` end-to-end. Polymorphic product type with
/// `std_pair` end-to-end. Polymorphic product type with
/// two type vars and a single constructor; six combinators including
/// the 3-type-var `map_first` / `map_second` reshapers. Property
/// protected: `Pair<a, b> -> Pair<c, b>` and `Pair<a, b> -> Pair<a, c>`
@@ -477,7 +478,7 @@ fn std_pair_demo() {
assert_eq!(lines, vec!["7", "9", "9", "7", "8", "18"]);
}
/// Iter 15d: `std_either` end-to-end. First stdlib ADT with two type
/// `std_either` end-to-end. First stdlib ADT with two type
/// parameters (`Either<e, a>`); first combinator with three type vars
/// (`either : (e -> c) -> (a -> c) -> Either<e, a> -> c`).
///
@@ -497,7 +498,7 @@ fn std_either_demo() {
);
}
/// Iter 15g: `std_either_list` end-to-end. First stdlib fn set that
/// `std_either_list` end-to-end. First stdlib fn set that
/// imports three other stdlib modules (`std_list`, `std_either`,
/// `std_pair`) and returns a compound polymorphic ADT tree
/// (`partition_eithers : List<Either<e,a>> -> Pair<List<e>, List<a>>`).
@@ -519,7 +520,7 @@ fn std_either_list_demo() {
assert_eq!(lines, vec!["2", "3", "2", "3"]);
}
/// Iter 15h: `std_list.take` and `std_list.drop` end-to-end. Both are
/// `std_list.take` and `std_list.drop` end-to-end. Both are
/// index-driven recursive ADT-pattern fns — the first `std_list`
/// combinators that pair `if (<= n 0)` Int-arithmetic guards with a
/// recursive Cons/Nil match. Property protected: the `if` base-case
@@ -534,7 +535,7 @@ fn std_list_more_demo() {
assert_eq!(lines, vec!["0", "3", "5", "5", "3", "0"]);
}
/// Iter 16b.1: local recursive `let`. Property protected: a
/// local recursive `let`. Property protected: a
/// `(let-rec ...)` term in a fn body whose recursive body has no
/// captures from the enclosing scope is lifted by the 16a desugar
/// pass to a synthetic top-level fn (`<name>$lr_N`), and the
@@ -552,7 +553,7 @@ fn local_rec_factorial_demo() {
assert_eq!(lines, vec!["1", "6", "120"]);
}
/// Iter 16b.2: LetRec capture of a fn-param (path-1 safe subset).
/// LetRec capture of a fn-param (path-1 safe subset).
/// Property protected: the desugar pass lifts a LetRec whose body
/// captures one or more enclosing-scope names (here: `n` from
/// `sum_below`'s params) into a synthetic top-level fn whose
@@ -569,7 +570,7 @@ fn local_rec_capture_demo() {
assert_eq!(lines, vec!["0", "10", "45"]);
}
/// Iter 16b.3: LetRec capture of a `Term::Let`-bound name. Property
/// LetRec capture of a `Term::Let`-bound name. Property
/// protected: the desugar pass leaves a LetRec whose only outside-
/// scope captures are Let-bound (type unknown until typecheck) in
/// place; the post-typecheck `lift_letrecs` pass in `ailang-check`
@@ -587,7 +588,7 @@ fn local_rec_let_capture_demo() {
assert_eq!(lines, vec!["0", "5", "9"]);
}
/// Iter 16b.4: LetRec capture of a Match-arm pattern binding.
/// LetRec capture of a Match-arm pattern binding.
/// Property protected: the desugar pass leaves a LetRec whose
/// captures are `Pattern::Ctor`-Var-bound (here `threshold` and
/// `n`, both fields of `MkPair` in `Pair Int Int`) in place; the
@@ -607,7 +608,7 @@ fn local_rec_match_capture_demo() {
assert_eq!(lines, vec!["0", "5", "9"]);
}
/// Iter 16b.5: LetRec name as a value in the in-clause (no capture).
/// LetRec name as a value in the in-clause (no capture).
/// Property protected: the desugar pass detects a non-callee use of
/// the LetRec name in `in_term` and wraps the rewritten in-term in
/// `(let factorial (lam ...) ...)` whose lam eta-expands the lifted
@@ -623,7 +624,7 @@ fn local_rec_as_value_demo() {
assert_eq!(lines, vec!["120"]);
}
/// Iter 16b.5: LetRec name as a value in the in-clause WITH capture.
/// LetRec name as a value in the in-clause WITH capture.
/// Property protected: the eta-Lam wrap composes correctly with the
/// 16b.2 capture-augmentation. The lifted fn has signature
/// `factorial_plus$lr_N(n: Int, base: Int) -> Int`; the eta-Lam
@@ -639,7 +640,7 @@ fn local_rec_as_value_capture_demo() {
assert_eq!(lines, vec!["1320", "12120"]);
}
/// Iter 16b.6: LetRec inside a polymorphic enclosing fn.
/// LetRec inside a polymorphic enclosing fn.
/// Property protected: when the enclosing fn is `Forall(a). Fn(...)`,
/// the desugar/lift pipeline now (a) enters fn-params as `KnownType`
/// (not `LetBound`) so a LetRec inside CAN capture them, and (b)
@@ -667,7 +668,7 @@ fn poly_rec_capture_demo() {
assert_eq!(lines, vec!["5", "false"]);
}
/// Iter 16b.7: nested LetRec where the inner one captures the OUTER
/// nested LetRec where the inner one captures the OUTER
/// LetRec's PARAMS (not its name).
///
/// Property protected: when an inner LetRec lifts inside an outer
@@ -868,7 +869,7 @@ fn workspace_lists_imported_modules() {
);
let modules = v["modules"].as_array().expect("modules must be array");
// Iter 23.1: the loader auto-injects the `prelude` module, so
// the loader auto-injects the `prelude` module, so
// the count is the user's two modules plus prelude.
assert_eq!(modules.len(), 3, "expected 3 modules: {stdout}");
@@ -930,7 +931,7 @@ fn ordering_match_via_prelude_prints_1() {
assert_eq!(stdout, "1\n");
}
/// Iter 23.3 Task 4 (resumed in ct.4): the end-to-end
/// the end-to-end
/// demonstration that the iter-23.1 cross-module Type::Con
/// mismatch bug is closed. `examples/compare_primitives_smoke.ail.json`
/// imports prelude, calls `compare` on three pairs each of
@@ -949,7 +950,7 @@ fn compare_primitives_smoke_prints_1_2_3_thrice() {
assert_eq!(stdout, "1\n2\n3\n1\n2\n3\n1\n2\n3\n");
}
/// Iter 23.2: end-to-end coverage for the auto-loaded `Eq` class
/// end-to-end coverage for the auto-loaded `Eq` class
/// and its three primitive instances. The fixture calls `eq` on
/// Int / Bool / Str with both an equal and an unequal pair each;
/// the monomorphiser synthesises `eq__Int` / `eq__Bool` /
@@ -1272,7 +1273,7 @@ fn check_json_unbound_var() {
);
}
/// Iter 16c: literal patterns at top level and inside Ctor
/// literal patterns at top level and inside Ctor
/// sub-patterns. Property protected: the 16a desugar pass rewrites
/// every `Pattern::Lit` to a `Term::If` against the scrutinee/field
/// before either typecheck or codegen sees it. Without 16c, the
@@ -1281,7 +1282,7 @@ fn check_json_unbound_var() {
/// the path. The fixture exercises both sites: `classify` (top-
/// level lit arms) and `categorize_first` (nested lit-in-Ctor).
///
/// Iter 16d update: the trailing `(case _ 0)` arm of
/// the trailing `(case _ 0)` arm of
/// `categorize_first` was removed when the chain machinery's
/// terminator switched from a `Unit` literal to the polymorphic
/// `__unreachable__` builtin. Output is unchanged — this test
@@ -1294,7 +1295,7 @@ fn lit_pat_demo() {
assert_eq!(lines, vec!["100", "200", "999", "-1", "0", "7"]);
}
/// Iter 16d: `__unreachable__` as an explicit user-callable
/// `__unreachable__` as an explicit user-callable
/// primitive. Property protected: a polymorphic `forall a. a` value
/// reference (a) typechecks against any expected type at the use
/// site (here `Int`), and (b) codegen lowers it to LLVM
@@ -1309,7 +1310,7 @@ fn unreachable_demo() {
assert_eq!(lines, vec!["4", "5"]);
}
/// Iter 18a: `(borrow T)` and `(own T)` mode annotations on
/// `(borrow T)` and `(own T)` mode annotations on
/// fn-type parameters. Properties protected:
/// (1) the parser accepts `(borrow ...)` and `(own ...)` only as
/// wrappers in fn-type param/ret slots and round-trips them
@@ -1366,7 +1367,7 @@ fn borrow_own_demo_modes_are_metadata_only() {
assert_eq!(lines, vec!["3", "6"]);
}
/// Iter 16e: polymorphic `==`. Properties protected:
/// polymorphic `==`. Properties protected:
/// (1) `==` typechecks at Int / Bool / Str / Unit (the fixture
/// uses every supported case directly via `(app == ...)`);
/// (2) codegen dispatches each arg-type to the right LLVM shape
@@ -1392,7 +1393,7 @@ fn eq_demo() {
);
}
/// Iter 18b: --alloc=rc routes allocation through ailang_rc_alloc
/// --alloc=rc routes allocation through ailang_rc_alloc
/// (8-byte refcount header, libc malloc backing). With no inc/dec
/// emission yet (18c work), programs leak under this mode but must
/// still produce correct stdout — that's the validation 18b ships.
@@ -1406,7 +1407,7 @@ fn alloc_rc_produces_same_stdout_as_gc() {
assert_eq!(stdout_rc.trim(), "42");
}
/// Iter 18c.1: `Term::Clone` is a pure schema addition. In 18c.1 the
/// `Term::Clone` is a pure schema addition. In 18c.1 the
/// wrapper is identity for both typechecker (same type as inner) and
/// codegen (same SSA reg, no extra IR) — programs that contain
/// `(clone X)` produce the same stdout they would have produced
@@ -1418,7 +1419,7 @@ fn clone_demo_is_identity_in_18c1() {
assert_eq!(stdout.trim(), "42");
}
/// Iter 18d.2: under `--alloc=rc`, `Term::ReuseAs { source, body =
/// under `--alloc=rc`, `Term::ReuseAs { source, body =
/// Term::Ctor }` lowers to a runtime refcount-1 dispatch — when the
/// source's box is unique we overwrite it in place (skipping the
/// alloc + cascade-dec round-trip); otherwise we fall back to a
@@ -1497,7 +1498,7 @@ fn reuse_as_demo_under_rc_uses_inplace_rewrite() {
root_dir: ws.root_dir.clone(),
registry: ws.registry.clone(),
};
// Iter rpe.1: post-print-migration, fixtures use `(app print x)`
// post-print-migration, fixtures use `(app print x)`
// which monomorphises to `print__<T>`. Adding the mono pass here
// mirrors `ail build`'s pipeline (desugar -> lift -> mono ->
// lower); without it, lowering errors with `UnknownVar("print")`.
@@ -1537,7 +1538,7 @@ fn reuse_as_demo_under_rc_uses_inplace_rewrite() {
!reuse_arm.contains("@ailang_rc_alloc"),
"reuse arm must not call @ailang_rc_alloc (the slot is reused in place). Reuse arm IR was:\n{reuse_arm}"
);
// Iter 18d.3: in this canonical fixture the pattern `(Cons h t)`
// in this canonical fixture the pattern `(Cons h t)`
// moves the only pointer-typed slot of the source's old ctor
// (slot 1, the tail; slot 0 is Int and never a dec candidate).
// The reuse arm therefore must emit NEITHER a per-field drop
@@ -1556,7 +1557,7 @@ fn reuse_as_demo_under_rc_uses_inplace_rewrite() {
);
}
/// Iter 18b: extends `alloc_rc_produces_same_stdout_as_gc` to a larger
/// extends `alloc_rc_produces_same_stdout_as_gc` to a larger
/// fixture (`std_list_demo`) so more allocation sites — folds, maps,
/// cross-module ctors — are exercised under `--alloc=rc`. Same
/// invariant: stdout must be byte-identical to the `gc` build.
@@ -1571,7 +1572,7 @@ fn alloc_rc_matches_gc_on_std_list_demo() {
);
}
/// Iter 18c.3: codegen actually emits `ailang_rc_dec` at end-of-scope
/// codegen actually emits `ailang_rc_dec` at end-of-scope
/// for trackable RC binders under `--alloc=rc`. This is the first
/// fixture where the `dec` path runs at runtime — `b` is bound to a
/// heap-allocated `MkBox`, the box is read via `match` (a borrow,
@@ -1605,7 +1606,7 @@ fn alloc_rc_emits_dec_for_unique_let_bound_box() {
assert_eq!(stdout_gc, stdout_rc, "alloc=rc must match alloc=gc on rc_box_drop");
}
/// Iter 18c.4: per-type drop fns + recursive `dec` cascade. Builds a
/// per-type drop fns + recursive `dec` cascade. Builds a
/// 5-element `IntList` and reduces it via `sum_list`. Under
/// `--alloc=rc` the codegen emits `define void @drop_<m>_IntList`
/// whose `Cons` arm recursively calls itself on the `tail` field —
@@ -1634,7 +1635,7 @@ fn alloc_rc_recursive_list_sum() {
);
}
/// Iter 18c.4 / 18d.4: exercise the recursive drop cascade at
/// exercise the recursive drop cascade at
/// runtime AND the arm-close pattern-binder dec.
///
/// 18c.4 originally used this fixture to lock in the recursive
@@ -1679,8 +1680,8 @@ fn alloc_rc_borrow_only_recursive_list_drop() {
"alloc=rc must match alloc=gc on rc_list_drop_borrow"
);
// Iter 18d.4 IR-shape assertion: the Cons arm of main's match
// emits a drop on `t` after lowering the body. We re-lower under
// IR-shape assertion: the Cons arm of main's match emits a drop
// on `t` after lowering the body. We re-lower under
// rc and inspect main's IR — the arm sequence must contain
// call void @ai/print_int(...) (the `print h`)
// call void @drop_rc_list_drop_borrow_IntList(ptr %v...) (NEW: 18d.4 t-drop)
@@ -1703,7 +1704,7 @@ fn alloc_rc_borrow_only_recursive_list_drop() {
root_dir: ws.root_dir.clone(),
registry: ws.registry.clone(),
};
// Iter rpe.1: post-print-migration, fixtures use `(app print x)`
// post-print-migration, fixtures use `(app print x)`
// which monomorphises to `print__<T>`. Adding the mono pass here
// mirrors `ail build`'s pipeline (desugar -> lift -> mono ->
// lower); without it, lowering errors with `UnknownVar("print")`.
@@ -1731,7 +1732,7 @@ fn alloc_rc_borrow_only_recursive_list_drop() {
);
}
/// Iter 18d.3: move-aware pattern bindings — let-close inlines a
/// move-aware pattern bindings — let-close inlines a
/// per-field dec sequence that skips moved slots and dec's non-moved
/// (e.g. wildcarded) pointer-typed slots.
///
@@ -1785,7 +1786,7 @@ fn alloc_rc_partial_drop_skips_moved_keeps_wildcarded() {
root_dir: ws.root_dir.clone(),
registry: ws.registry.clone(),
};
// Iter rpe.1: post-print-migration, fixtures use `(app print x)`
// post-print-migration, fixtures use `(app print x)`
// which monomorphises to `print__<T>`. Adding the mono pass here
// mirrors `ail build`'s pipeline (desugar -> lift -> mono ->
// lower); without it, lowering errors with `UnknownVar("print")`.
@@ -1828,7 +1829,7 @@ fn alloc_rc_partial_drop_skips_moved_keeps_wildcarded() {
);
}
/// Iter 18d.4: Own-param dec at fn return. Symmetric to 18c.3/18c.4's
/// Own-param dec at fn return. Symmetric to 18c.3/18c.4's
/// `Term::Let`-scope-close drop emission and 18d.4's arm-close
/// pattern-binder dec, fired at the lexical close of a fn body. The
/// `head_or_zero` fn declares its only parameter `xs` as
@@ -1881,7 +1882,7 @@ fn alloc_rc_own_param_dec_at_fn_return() {
root_dir: ws.root_dir.clone(),
registry: ws.registry.clone(),
};
// Iter rpe.1: post-print-migration, fixtures use `(app print x)`
// post-print-migration, fixtures use `(app print x)`
// which monomorphises to `print__<T>`. Adding the mono pass here
// mirrors `ail build`'s pipeline (desugar -> lift -> mono ->
// lower); without it, lowering errors with `UnknownVar("print")`.
@@ -1947,7 +1948,7 @@ fn alloc_rc_own_param_dec_at_fn_return() {
);
}
/// Iter 18e: `(drop-iterative)` opt-in annotation. The fixture
/// `(drop-iterative)` opt-in annotation. The fixture
/// declares `IntList` with `(drop-iterative)` and runs an N=1,000,000
/// cell list through `head_or_zero`. The fn signature is
/// `(own (con IntList))` — 18d.4 emits a drop on the param at fn
@@ -1987,7 +1988,7 @@ fn alloc_rc_drop_iterative_handles_million_cell_list() {
);
}
/// Iter 18e: IR-shape signature of `(drop-iterative)`. The drop fn
/// IR-shape signature of `(drop-iterative)`. The drop fn
/// for the annotated type contains a worklist loop (`br label
/// %loop_head`, the runtime-helper calls, a backedge from each ctor
/// arm) and does NOT contain a recursive `call void @drop_<m>_<T>(...)`
@@ -2013,7 +2014,7 @@ fn iter18e_drop_iterative_emits_worklist_body_no_self_recursion() {
root_dir: ws.root_dir.clone(),
registry: ws.registry.clone(),
};
// Iter rpe.1: post-print-migration, fixtures use `(app print x)`
// post-print-migration, fixtures use `(app print x)`
// which monomorphises to `print__<T>`. Adding the mono pass here
// mirrors `ail build`'s pipeline (desugar -> lift -> mono ->
// lower); without it, lowering errors with `UnknownVar("print")`.
@@ -2071,7 +2072,7 @@ fn iter18e_drop_iterative_emits_worklist_body_no_self_recursion() {
);
}
/// Iter 18e: control — the same fixture WITHOUT the
/// control — the same fixture WITHOUT the
/// `(drop-iterative)` annotation must still emit the recursive
/// 18c.4 body. We synthesise the unannotated module on the fly
/// (mutating the workspace's parsed AST) so this test is independent
@@ -2103,7 +2104,7 @@ fn iter18e_no_annotation_keeps_recursive_drop_body() {
root_dir: ws.root_dir.clone(),
registry: ws.registry.clone(),
};
// Iter rpe.1: post-print-migration, fixtures use `(app print x)`
// post-print-migration, fixtures use `(app print x)`
// which monomorphises to `print__<T>`. Adding the mono pass here
// mirrors `ail build`'s pipeline (desugar -> lift -> mono ->
// lower); without it, lowering errors with `UnknownVar("print")`.
@@ -2137,7 +2138,7 @@ fn iter18e_no_annotation_keeps_recursive_drop_body() {
);
}
/// Iter 18d.4 regression: pattern-binder dec at arm close (Iter A in
/// pattern-binder dec at arm close (Iter A in
/// the 18d.4 emission seam) was firing on pattern-bound pointer
/// fields whose enclosing fn is Implicit-mode. The dec freed memory
/// the caller still referenced, producing refcount underflow or
@@ -2174,7 +2175,7 @@ fn alloc_rc_pattern_bind_in_implicit_fn_does_not_dec_borrowed_children() {
);
}
/// Iter 18g.0: build the example under `--alloc=rc`, run with
/// build the example under `--alloc=rc`, run with
/// `AILANG_RC_STATS=1`, and return the parsed `(allocs, frees, live)`
/// triple from the runtime's atexit summary on stderr. The summary
/// line shape is fixed by `runtime/rc.c`'s `ailang_rc_stats_atexit`:
@@ -2246,7 +2247,7 @@ fn build_and_run_with_rc_stats(example: &str) -> (String, u64, u64, i64) {
)
}
/// Iter 18g.1 regression: explicit-mode tail-recursive list-sum must
/// explicit-mode tail-recursive list-sum must
/// not leak the LCons outer cells.
///
/// Background: 18f.2's tail-latency bench found that
@@ -2287,7 +2288,7 @@ fn alloc_rc_explicit_mode_tail_sum_does_not_leak_outer_cells() {
);
}
/// Iter 18g.2 regression: a `let`-binder whose value is the result of
/// a `let`-binder whose value is the result of
/// an Own-returning function call must be dropped at let-scope close.
///
/// Background: 18c.3's `is_rc_heap_allocated` returns `false` for any
@@ -2324,7 +2325,7 @@ fn alloc_rc_let_binder_for_owned_returning_app_drops_at_scope_close() {
);
}
/// Iter 18g tidy negative-coverage: a let-binder whose value is the
/// a let-binder whose value is the
/// result of an `Implicit`-ret-mode (default, unannotated) call must
/// NOT be trackable. Implicit is the back-compat lane that 18c.3
/// documented as "params don't get any dec — leak rather than mis-
@@ -2364,7 +2365,7 @@ fn alloc_rc_let_binder_for_implicit_returning_app_does_not_drop() {
);
}
/// Iter 18g tidy follow-up: let-alias-aware mode propagation.
/// let-alias-aware mode propagation.
///
/// 18d.4 Iter A's fix gated arm-close pattern-binder dec on the
/// scrutinee's `current_param_modes` lookup, but only fn-params
@@ -2402,8 +2403,8 @@ fn alloc_rc_let_alias_of_implicit_param_does_not_dec_borrowed_children() {
);
}
/// Iter 18g.tidy.fu2 regression — RED-then-GREEN for the
/// dynamic-tag partial-drop carve-out at fn-return.
/// Regression — RED-then-GREEN for the dynamic-tag partial-drop
/// carve-out at fn-return.
///
/// Three carve-out sites previously fell back to a shallow
/// `ailang_rc_dec` when a binder's `moved_slots` was non-empty
@@ -2451,8 +2452,7 @@ fn alloc_rc_partial_drop_at_fn_return_does_not_leak_unmoved_fields() {
);
}
/// Iter 18g.tidy.fu2 regression — site 2: Iter A arm-close
/// pattern-binder dec (match_lower.rs).
/// Arm-close pattern-binder dec (match_lower.rs).
///
/// An outer match's arm-bound pattern-binder may itself be the
/// scrutinee of an inner match that moves out some of its fields.
@@ -2489,7 +2489,7 @@ fn alloc_rc_partial_drop_at_arm_close_does_not_leak_unmoved_fields() {
);
}
/// Iter 18g.tidy.fu2 regression — site 3: let-close for an
/// let-close for an
/// App-bound binder (drop.rs `emit_inlined_partial_drop`
/// non-Ctor branch).
///
@@ -2523,7 +2523,7 @@ fn alloc_rc_partial_drop_at_app_let_close_does_not_leak_unmoved_fields() {
);
}
/// Iter 20d: `ail merge-prose` reads two files and prints a prompt
/// `ail merge-prose` reads two files and prints a prompt
/// that frames the prose-round-trip task for an external LLM. Smoke
/// test: writes two temp inputs, invokes the binary, asserts exit 0
/// and that the captured stdout carries the role-statement landmark
@@ -2537,7 +2537,7 @@ fn merge_prose_prints_framed_prompt() {
std::fs::create_dir_all(&tmp).unwrap();
let orig_path = tmp.join("orig.ail.json");
let prose_path = tmp.join("edited.prose.txt");
// Iter 20f: merge-prose now loads the module via ailang_core,
// merge-prose now loads the module via ailang_core,
// renders to Form-A, and embeds *that* in the prompt — so the
// input must be a well-formed ailang/v0 JSON-AST that
// load_module accepts. The schema, name, and an empty defs list
@@ -2681,7 +2681,7 @@ fn ail_run_accepts_ail_source_with_same_stdout_as_ail_json() {
);
}
/// Iter hs.4: `int_to_str(42)` prints "42\n" through the standard
/// `int_to_str(42)` prints "42\n" through the standard
/// `do io/print_str(...)` path. Smoke pin for the heap-Str builtin
/// wired in hs.4 — exercises checker (signature install), codegen
/// (lower_app arm + IR-header declare + is_static_callee whitelist),
@@ -2696,7 +2696,7 @@ fn int_to_str_smoke() {
assert_eq!(out, "42\n");
}
/// Iter hs.4: `float_to_str(3.5)` prints a libc-%g-conformant
/// `float_to_str(3.5)` prints a libc-%g-conformant
/// rendering of 3.5. The runtime's `ailang_float_to_str` uses
/// `snprintf(..., "%g", x)` with no locale call, so the C locale
/// default applies and the dev target's glibc produces the three
@@ -2744,11 +2744,11 @@ fn str_field_in_adt_drops_heap_str_correctly() {
assert_eq!(live, 0, "no leaked RC slabs allowed; live={live}");
}
/// Iter eob.1 / rpe.1: primitive-Int passed to the polymorphic
/// `print` helper. Pre-rpe.1 the fixture used `(do io/print_int n)`
/// directly and the iter-eob.1 "Term::Do args = Borrow" rule meant
/// primitive-Int passed to the polymorphic
/// `print` helper. Pre-per-type-print-retirement the fixture used `(do io/print_int n)`
/// directly and the heap-Str-ABI "Term::Do args = Borrow" rule meant
/// zero RC traffic on the unboxed Int (allocs == 0, frees == 0).
/// Post-rpe.1, `print n` desugars through `Show Int.show` →
/// Post-per-type-print-retirement, `print n` desugars through `Show Int.show` →
/// `int_to_str` (heap-Str alloc, `ret_mode: Own`) → `io/print_str`
/// → slab drops at scope close. Pin shifts to the new canonical
/// post-iter shape: exactly one heap-Str slab cycle per `print`
@@ -2761,14 +2761,15 @@ fn int_arg_to_effect_op_does_not_rc_track() {
let (stdout, allocs, frees, live) =
build_and_run_with_rc_stats("int_to_print_int_borrow.ail");
assert_eq!(stdout, "7\n");
// Post-rpe.1: print n for n : Int allocates one heap-Str slab
// via Show Int → int_to_str, frees it at scope close. Spec §E.
// After the per-type-print-op retirement, `print n` for n : Int
// allocates one heap-Str slab via Show Int → int_to_str, frees
// it at scope close. Spec §E.
assert_eq!(allocs, 1, "expected exactly one Show-Int heap-Str slab; got allocs={allocs}");
assert_eq!(frees, 1, "expected exactly one free; got frees={frees}");
assert_eq!(live, 0, "no leaked RC slabs allowed; live={live}");
}
/// Iter eob.1: heap-Str let-binder consumed by two `io/print_str`
/// heap-Str let-binder consumed by two `io/print_str`
/// calls in sequence. Pins the linearity-side consequence of the new
/// rule: under the old Position::Consume walk for Term::Do args, the
/// second print would have triggered `use-after-consume`. Under the
@@ -2786,7 +2787,7 @@ fn heap_str_repeated_print_balances_rc_stats() {
assert_eq!(live, 0, "no leaked heap-Str slabs allowed; live={live}");
}
/// Iter 24.1: `bool_to_str(true)` bound to a let, consumed by
/// `bool_to_str(true)` bound to a let, consumed by
/// `io/print_str`. The heap-Str slab from `ailang_bool_to_str`
/// rides the same rc_header + ailang_rc_dec path as int_to_str's
/// output — `allocs == 1, frees == 1, live == 0` and stdout is
@@ -2802,7 +2803,7 @@ fn bool_to_str_drop_balances_rc_stats() {
assert_eq!(live, 0, "no leaked heap-Str slabs allowed; live={live}");
}
/// Iter 24.1: stdout-smoke for the true branch — same fixture as
/// stdout-smoke for the true branch — same fixture as
/// `bool_to_str_drop_balances_rc_stats` but checked here without
/// the RC-stats overhead. Pins the byte content of `ailang_bool_to_str`'s
/// "true" slab.
@@ -2812,7 +2813,7 @@ fn bool_to_str_emits_true_branch() {
assert_eq!(out, "true\n");
}
/// Iter 24.1: stdout-smoke for the false branch. Pins the byte
/// stdout-smoke for the false branch. Pins the byte
/// content of `ailang_bool_to_str`'s "false" slab.
#[test]
fn bool_to_str_emits_false_branch() {
@@ -2820,7 +2821,7 @@ fn bool_to_str_emits_false_branch() {
assert_eq!(out, "false\n");
}
/// Iter 24.1: `str_clone("hello")` bound to a let, consumed by
/// `str_clone("hello")` bound to a let, consumed by
/// `io/print_str`. The heap-Str clone allocates a fresh slab (via
/// str_alloc) and the let-binder drops it at scope close.
/// `allocs == 1, frees == 1, live == 0`; stdout is `hello\n` (puts
@@ -2835,7 +2836,7 @@ fn str_clone_drop_balances_rc_stats() {
assert_eq!(live, 0, "no leaked heap-Str slabs allowed; live={live}");
}
/// Iter 24.1: cross-realisation invariant — `str_clone` works
/// cross-realisation invariant — `str_clone` works
/// uniformly on heap-Str input (`int_to_str 42`'s output) AND on
/// static-Str input (literal `"abc"`). Both clones print their
/// input bytes; RC stats account for three heap-Str slabs (one
+2 -2
View File
@@ -51,7 +51,7 @@ fn examples_dir() -> PathBuf {
/// `head_or_zero : test_mono_ctor_listmod.List<Int> -> Int` that
/// matches on `Cons h _ | Nil`.
///
/// Pre-ct.2 repro: `monomorphise_workspace` returned
/// Pre-canonical-type-form repro: `monomorphise_workspace` returned
/// `Err(CheckError::PatternTypeMismatch { ctor: "Cons", ty:
/// "test_mono_ctor_listmod.List<Int>" })`. The same workspace
/// typechecked cleanly because the typecheck pass overlaid a
@@ -59,7 +59,7 @@ fn examples_dir() -> PathBuf {
/// qualified `resolved_type_name`, while the mono pass kept the
/// workspace-flat index whose local hit produced the bare one.
///
/// Post-ct.2 expectation: `monomorphise_workspace` returns `Ok`
/// Post-canonical-type-form expectation: `monomorphise_workspace` returns `Ok`
/// because the Pattern::Ctor lookup no longer consults
/// `ctor_index` at all — it walks directly from the scrutinee's
/// canonical `Type::Con.name` to its TypeDef in
+3 -3
View File
@@ -38,7 +38,7 @@ fn run_ail_check_json(entry: &str) -> std::process::Output {
.expect("ail binary must launch")
}
/// mq.3.6 (Trajectory C): bare `show 42` in a workspace with two
/// Multi-class trajectory C: bare `show 42` in a workspace with two
/// `Show` classes (modA and modB) each shipping `Show Int` is
/// genuinely ambiguous. `ail check` rejects with
/// `ambiguous-method-resolution` and names both candidate classes.
@@ -71,7 +71,7 @@ fn mq3_two_show_ambiguous_fires_ambiguous_method_resolution() {
);
}
/// mq.3.6 (Trajectory E): explicit qualifier
/// Multi-class trajectory E: explicit qualifier
/// `mq3_two_show_ambiguous_a.Show.show 42` disambiguates the same
/// workspace as the ambiguous fixture. Typecheck succeeds.
#[test]
@@ -85,7 +85,7 @@ fn mq3_two_show_qualified_resolves_clean() {
);
}
/// mq.3.6 (class-fn shadow): bare `myeq 1 2` in a workspace with
/// Class-fn shadow: bare `myeq 1 2` in a workspace with
/// `class MyEq { myeq }` in one module and `fn myeq` in another
/// resolves to the fn per lookup precedence. Typecheck succeeds AND
/// the structured warning `class-method-shadowed-by-fn` fires so the
+5 -5
View File
@@ -34,19 +34,19 @@ fn class_method_is_in_module_globals() {
let mod_globals = globals
.get("test_22b2_class_method_lookup")
.expect("module globals present");
// mq.3: ModuleGlobals accessors are tuple-keyed by (class, method).
// Pre-mq.3 callers passed the bare method name only; post-mq.3 they
// ModuleGlobals accessors are tuple-keyed by (class, method).
// Pre-canonical-class-form callers passed the bare method name only; post-canonical-class-form they
// must disambiguate by class.
assert!(
mod_globals.has_class_method("test_22b2_class_method_lookup.TShow", "tshow"),
"class method `tshow` (in class `TShow`) must appear in module globals"
);
// mq.1: class_method_class returns the qualified form
// class_method_class returns the qualified form
// `<defining_module>.<Class>`.
assert_eq!(
mod_globals.class_method_class("test_22b2_class_method_lookup.TShow", "tshow"),
Some("test_22b2_class_method_lookup.TShow"),
"class method `tshow` must remember its class is `TShow` (qualified post-mq.1)"
"class method `tshow` must remember its class is `TShow` (qualified post-canonical-class-form)"
);
}
@@ -183,7 +183,7 @@ fn fn_with_correct_constraint_typechecks_green() {
/// `(Eq, a)` is satisfied because `Ord` extends `Eq`, so the expanded
/// declared set must include `(Eq, a)`. Without the superclass walk the
/// fn would spuriously fire `missing-constraint`. This is the positive
/// fixture for Decision 11's one-step superclass closure.
/// fixture for the typeclass design's one-step superclass closure.
#[test]
fn fn_calling_superclass_method_via_subclass_constraint_typechecks_green() {
let entry = examples_dir()
+4 -4
View File
@@ -148,7 +148,7 @@ fn collect_mono_targets_single_concrete_call_site() {
let t = &targets[0];
match t {
ailang_check::mono::MonoTarget::ClassMethod { class, method, type_, defining_module } => {
// mq.1: MonoTarget.class carries the qualified workspace-key shape.
// MonoTarget.class carries the qualified workspace-key shape.
assert_eq!(class, "test_22b2_instance_present.TShow");
assert_eq!(method, "tshow");
assert!(
@@ -483,7 +483,7 @@ fn rewrite_walker_skips_locally_shadowed_class_method() {
let entry = examples_dir().join("test_22b3_shadow_class_method.ail");
let ws = ailang_surface::load_workspace(&entry).expect("load");
let diags = ailang_check::check_workspace(&ws);
// mq.3: this fixture intentionally shadows the class method `tshow`
// this fixture intentionally shadows the class method `tshow`
// with a local `let show = "shadow"`, which fires the new
// `class-method-shadowed-by-fn` warning. The warning is an
// expected correct consequence; filter it out so the assertion
@@ -617,7 +617,7 @@ fn rewrite_uses_qualified_name_for_cross_module_call_site() {
);
}
/// Iter 22b.3.7 GATING: the synthetic class+instance fixture must
/// the synthetic class+instance fixture must
/// typecheck cleanly under `check_workspace`. Pre-condition for the
/// end-to-end run gate (`synthetic_fixture_runs_and_prints_five`):
/// if the fixture is ill-typed, the run gate's failure mode would
@@ -630,7 +630,7 @@ fn synthetic_fixture_typechecks_clean() {
assert!(diags.is_empty(), "fixture must typecheck: {:?}", diags);
}
/// Iter 22b.3.7 GATING: the synthetic class+instance fixture must
/// the synthetic class+instance fixture must
/// build via `ail run` and emit `5\n` to stdout. This is the
/// spec's close-out criterion for 22b.3 — proves the full mono
/// pipeline (class declaration + instance + class-method call site
+19 -19
View File
@@ -48,7 +48,7 @@ pub struct EffectOpSig {
/// [`list()`] is resolvable in `env`. Idempotent for a fresh `Env`; calling
/// it twice would shadow the same entries with identical types.
pub fn install(env: &mut crate::Env) {
// Iter 22-floats.3: arithmetic and comparison ops are polymorphic.
// arithmetic and comparison ops are polymorphic.
// Same shape as `==` below — the {Int, Float}-restriction is
// enforced at codegen, not at typecheck. `%` stays Int-only
// (no fmod yet — `%` semantics for Float require an explicit
@@ -134,7 +134,7 @@ pub fn install(env: &mut crate::Env) {
},
);
// Iter 16d: `__unreachable__` is the polymorphic bottom value.
// `__unreachable__` is the polymorphic bottom value.
// Type: `forall a. a`. Used by the desugar pass as the chain
// terminator of an exhaustive match, and available to user code as
// a primitive panic point. Codegen lowers it to LLVM `unreachable`.
@@ -149,7 +149,7 @@ pub fn install(env: &mut crate::Env) {
},
);
// Iter 22-floats.3: Float-conversion and inspection builtins.
// Float-conversion and inspection builtins.
// Codegen lowering lands in iter 4; iter 3 only registers types.
// `neg` is polymorphic (`forall a. (a) -> a`) for the same reason
// the widened `+` is — Int and Float negation share one symbol;
@@ -254,7 +254,7 @@ pub fn install(env: &mut crate::Env) {
},
);
// Iter 22-floats.3: Float bit-pattern constants. Bare values, not
// Float bit-pattern constants. Bare values, not
// fns — reference site is `(var nan)`. Codegen emits
// `double 0x7FF8000000000000` (NaN), `double 0x7FF0000000000000`
// (+Inf), `double 0xFFF0000000000000` (-Inf) at the use site in
@@ -382,7 +382,7 @@ mod tests {
}
}
/// Iter 22-floats.3: regression — `(+ 1 2)` still resolves to `Int`
/// regression — `(+ 1 2)` still resolves to `Int`
/// after the widening from `(Int, Int) -> Int` to
/// `forall a. (a, a) -> a`. Protects the no-regression invariant
/// for every existing Int-using fixture: the polymorphic `+`
@@ -394,7 +394,7 @@ mod tests {
assert_eq!(ty, Type::int(), "(+ 1 2) must still type as Int");
}
/// Iter 22-floats.3: new acceptance — `(+ 1.5 2.5)` types as `Float`.
/// new acceptance — `(+ 1.5 2.5)` types as `Float`.
/// Pre-widening this would have failed with `TypeMismatch` because
/// `+` was monomorphic `(Int, Int) -> Int`. The widening to
/// `forall a. (a, a) -> a` makes Float arithmetic a typecheck-clean
@@ -408,7 +408,7 @@ mod tests {
assert_eq!(ty, Type::float(), "(+ 1.5 2.5) must type as Float");
}
/// Iter 22-floats.3: regression — `(< 1 2)` still resolves to `Bool`
/// regression — `(< 1 2)` still resolves to `Bool`
/// after the widening to `forall a. (a, a) -> Bool`. Mirrors the
/// `+` regression check for the comparison-op path.
#[test]
@@ -417,7 +417,7 @@ mod tests {
assert_eq!(ty, Type::bool_(), "(< 1 2) must still type as Bool");
}
/// Iter 22-floats.3: new acceptance — `(< 1.5 2.5)` types as `Bool`.
/// new acceptance — `(< 1.5 2.5)` types as `Bool`.
/// Pre-widening this would have failed with `TypeMismatch`. The
/// widening to `forall a. (a, a) -> Bool` lets Float ordering
/// typecheck cleanly; codegen lowers to `fcmp olt double` in iter 4.
@@ -429,7 +429,7 @@ mod tests {
assert_eq!(ty, Type::bool_(), "(< 1.5 2.5) must type as Bool");
}
/// Iter 22-floats.3: `neg` is polymorphic — `forall a. (a) -> a`.
/// `neg` is polymorphic — `forall a. (a) -> a`.
/// Both `(neg 5) : Int` and `(neg 1.5) : Float` typecheck. The
/// polymorphic shape is the same as the widened arithmetic ops, so
/// Int and Float negation share one symbol; codegen dispatches on
@@ -443,7 +443,7 @@ mod tests {
assert_eq!(ty_float, Type::float(), "(neg 1.5) must type as Float");
}
/// Iter 22-floats.3: `int_to_float : (Int) -> Float`. The
/// `int_to_float : (Int) -> Float`. The
/// monomorphic conversion builtin — codegen lowers via `sitofp` in
/// iter 4. Typecheck only validates the signature here.
#[test]
@@ -452,7 +452,7 @@ mod tests {
assert_eq!(ty, Type::float(), "(int_to_float 5) must type as Float");
}
/// Iter 22-floats.3: `float_to_int_truncate : (Float) -> Int`.
/// `float_to_int_truncate : (Float) -> Int`.
/// Saturating truncation toward zero per spec A4 — typecheck only
/// validates the signature; semantics is iter 4's codegen lowering
/// via `@llvm.fptosi.sat.i64.f64`.
@@ -463,7 +463,7 @@ mod tests {
assert_eq!(ty, Type::int(), "(float_to_int_truncate 1.5) must type as Int");
}
/// Iter 22-floats.3: `float_to_str : (Float) -> Str`. Codegen lowers
/// `float_to_str : (Float) -> Str`. Codegen lowers
/// via runtime C glue in iter 4; typecheck only validates the signature.
#[test]
fn install_float_to_str_signature() {
@@ -472,7 +472,7 @@ mod tests {
assert_eq!(ty, Type::str_(), "(float_to_str 1.5) must type as Str");
}
/// Iter hs.4: `int_to_str : (Int) -> Str`. Codegen lowers via the
/// `int_to_str : (Int) -> Str`. Codegen lowers via the
/// runtime C glue `ailang_int_to_str` from `runtime/str.c`.
#[test]
fn install_int_to_str_signature() {
@@ -480,7 +480,7 @@ mod tests {
assert_eq!(ty, Type::str_(), "(int_to_str 42) must type as Str");
}
/// Iter 24.1: `bool_to_str : (Bool) -> Str`. Codegen lowers via the
/// `bool_to_str : (Bool) -> Str`. Codegen lowers via the
/// runtime C glue `ailang_bool_to_str` from `runtime/str.c`.
#[test]
fn install_bool_to_str_signature() {
@@ -491,7 +491,7 @@ mod tests {
assert_eq!(ty, Type::str_(), "(bool_to_str true) must type as Str");
}
/// Iter 24.1: `str_clone : (Str borrow) -> Str`. Codegen lowers via the
/// `str_clone : (Str borrow) -> Str`. Codegen lowers via the
/// runtime C glue `ailang_str_clone` from `runtime/str.c`.
#[test]
fn install_str_clone_signature() {
@@ -504,7 +504,7 @@ mod tests {
#[test]
fn install_str_concat_signature() {
// Iter str-concat: `str_concat : (Str borrow, Str borrow) -> Str
// `str_concat : (Str borrow, Str borrow) -> Str
// own`. Codegen lowers via the runtime C glue `ailang_str_concat`
// from `runtime/str.c`.
let mut env = Env::default();
@@ -539,7 +539,7 @@ mod tests {
}
}
/// Iter 22-floats.3: `is_nan : (Float) -> Bool`. Codegen lowers to
/// `is_nan : (Float) -> Bool`. Codegen lowers to
/// `fcmp uno double %x, %x` in iter 4 — typecheck only validates
/// the signature here.
#[test]
@@ -549,7 +549,7 @@ mod tests {
assert_eq!(ty, Type::bool_(), "(is_nan 1.5) must type as Bool");
}
/// Iter 22-floats.3: `nan`, `inf`, `neg_inf` are bare-value constants
/// `nan`, `inf`, `neg_inf` are bare-value constants
/// of type `Float`. They are NOT functions — reference site is
/// `(var nan)`, not `(app nan)`. Parallel to `__unreachable__` which
/// is `forall a. a`, but here the type is the concrete `Float`
@@ -565,7 +565,7 @@ mod tests {
assert_eq!(ty_neg_inf, Type::float(), "neg_inf must type as Float");
}
/// Iter 22-floats.3: pattern-matching on Float literals is hard-
/// pattern-matching on Float literals is hard-
/// rejected at typecheck per spec line 723-735 recommendation (a).
/// IEEE-`==` semantics make Float patterns semantically dubious
/// (NaN never matches; equality is bit-exact not approximate).
+7 -8
View File
@@ -109,9 +109,8 @@ use serde::Serialize;
///
/// Serializes as a lowercase string (`"error"` / `"warning"`) so that
/// `ail check --json` consumers can branch on it without parsing prose.
/// Iter 19a is the first iter to emit [`Severity::Warning`] (via the
/// linearity-pass `over-strict-mode` lint); pre-19a the variant was
/// reserved.
/// Currently the only emitter of [`Severity::Warning`] is the
/// linearity-pass `over-strict-mode` lint.
#[derive(Serialize, Debug, Clone, Copy, PartialEq, Eq)]
#[serde(rename_all = "lowercase")]
pub enum Severity {
@@ -145,7 +144,7 @@ pub struct Diagnostic {
pub def: Option<String>,
/// Free structured context. Empty = `{}`.
pub ctx: serde_json::Value,
/// Iter 18c.2: machine-applicable fix suggestions, each a snippet of
/// machine-applicable fix suggestions, each a snippet of
/// form-A AILang the author can paste back at the offending site.
/// Empty = no rewrite suggested. The list is always emitted (so JSON
/// consumers see a stable shape; they can branch on `.is_empty()`).
@@ -156,7 +155,7 @@ pub struct Diagnostic {
/// One machine-applicable rewrite suggestion attached to a [`Diagnostic`].
///
/// Iter 18c.2: emitted by the linearity check to point the author (or an
/// emitted by the linearity check to point the author (or an
/// LLM consumer of `ail check --json`) at the spelled fix. `replacement`
/// is form-A AILang text — parseable by `ailang_surface::parse_term`.
#[derive(Serialize, Debug, Clone)]
@@ -188,7 +187,7 @@ impl Diagnostic {
}
}
/// Iter 19a: builds a [`Severity::Warning`] diagnostic. Same shape
/// builds a [`Severity::Warning`] diagnostic. Same shape
/// as [`Diagnostic::error`] otherwise. Used by lint-style passes
/// that report observations rather than hard typecheck failures
/// (currently the only emitter is the linearity pass's
@@ -204,7 +203,7 @@ impl Diagnostic {
}
}
/// Iter 18c.2: append a [`SuggestedRewrite`]. Builder-style. Used by
/// append a [`SuggestedRewrite`]. Builder-style. Used by
/// the linearity check to attach the form-A fix it computed. Other
/// diagnostics leave the field empty.
pub fn with_suggested_rewrite(
@@ -265,7 +264,7 @@ mod tests {
assert!(s.contains("\"actual\":\"Bool\""), "{s}");
}
/// Iter 18c.2: a diagnostic carrying a `SuggestedRewrite` serialises
/// a diagnostic carrying a `SuggestedRewrite` serialises
/// the rewrite under `suggested_rewrites` with the description and
/// the form-A replacement.
#[test]
File diff suppressed because it is too large Load Diff
+24 -24
View File
@@ -35,7 +35,7 @@ use std::collections::{BTreeMap, BTreeSet};
use crate::{builtins, synth, CheckError, Env, Result, Subst};
/// Iter 16b.3: post-typecheck pass that eliminates every surviving
/// post-typecheck pass that eliminates every surviving
/// `Term::LetRec` from `m` by lifting it to a synthetic top-level
/// `Def::Fn`. Returns a new module that goes to codegen.
///
@@ -99,7 +99,7 @@ pub fn lift_letrecs(m: &Module) -> Result<Module> {
name: def.name().to_string(),
args: vec![],
},
// Iter 22b.1: skip class/instance defs in the global-type
// skip class/instance defs in the global-type
// collection. Class methods become globally typed names
// only after 22b.3 monomorphisation; until then there is
// no concrete `Type` to register here.
@@ -159,7 +159,7 @@ pub fn lift_letrecs(m: &Module) -> Result<Module> {
}
}
}
// Iter 13a: install rigid vars from a Forall enclosing
// install rigid vars from a Forall enclosing
// fn so check_type_well_formed inside synth doesn't
// reject them. Save and restore so the lifter env is
// clean across defs.
@@ -171,7 +171,7 @@ pub fn lift_letrecs(m: &Module) -> Result<Module> {
}
}
}
// Iter 16b.6: stash the enclosing fn's Forall.vars (or
// stash the enclosing fn's Forall.vars (or
// empty for a monomorphic enclosing fn) for the LetRec
// arm. Save and restore so it never leaks across defs.
let saved_forall_vars =
@@ -191,7 +191,7 @@ pub fn lift_letrecs(m: &Module) -> Result<Module> {
c.value = lifter.lift_in_term(&c.value, &mut locals, &c.name)?;
}
Def::Type(_) => {}
// Iter 22b.1: class/instance defs do not enter the lift
// class/instance defs do not enter the lift
// pass yet. 22b.2 will lift method-body lambdas in instance
// methods; for 22b.1 this is a passthrough.
Def::Class(_) | Def::Instance(_) => {}
@@ -206,7 +206,7 @@ pub fn lift_letrecs(m: &Module) -> Result<Module> {
/// `ailang-core::desugar` but resolves capture types via `synth`
/// instead of relying on a `ScopeEntry` map.
///
/// Iter 16b.6 field:
/// Forall-tracking field:
/// - `current_def_forall_vars` carries the enclosing fn's
/// `Type::Forall.vars` while lifting its body. Empty for monomorphic
/// enclosing fns. Read by the LetRec arm to wrap the synthetic
@@ -218,10 +218,10 @@ struct Lifter<'a> {
counter: u64,
module_names: &'a mut BTreeSet<String>,
lifted: Vec<Def>,
/// Iter 16b.6: enclosing fn's Forall.vars (or empty if mono).
/// enclosing fn's Forall.vars (or empty if mono).
/// Reset on entry to each `Def::Fn`.
current_def_forall_vars: Vec<String>,
/// Iter 16b.7: names of LetRecs whose bodies are currently being
/// names of LetRecs whose bodies are currently being
/// lifted (i.e. enclosing `Term::LetRec` names visible at this
/// point in the walk). Mirrors the desugar pass's
/// `ScopeEntry::EnclosingLetRec` marker. An inner LetRec capturing
@@ -367,11 +367,11 @@ impl<'a> Lifter<'a> {
rhs: Box::new(self.lift_in_term(rhs, locals, in_def)?),
}),
Term::Clone { value } => Ok(Term::Clone {
// Iter 18c.1: structural recursion through the wrapper.
// structural recursion through the wrapper.
value: Box::new(self.lift_in_term(value, locals, in_def)?),
}),
Term::ReuseAs { source, body } => Ok(Term::ReuseAs {
// Iter 18d.1: structural recursion through both children.
// structural recursion through both children.
source: Box::new(self.lift_in_term(source, locals, in_def)?),
body: Box::new(self.lift_in_term(body, locals, in_def)?),
}),
@@ -395,7 +395,7 @@ impl<'a> Lifter<'a> {
.collect::<Result<Vec<_>>>()?,
}),
Term::LetRec { name, ty, params, body, in_term } => {
// Iter 16b.3: post-order traversal — lift any inner
// post-order traversal — lift any inner
// LetRecs first. Within the body's scope, `name` and
// `params` are visible.
//
@@ -422,7 +422,7 @@ impl<'a> Lifter<'a> {
let prev = locals.insert(n.clone(), t.clone());
body_pushed.push((n.clone(), prev));
}
// Iter 16b.7: mark `name` as an enclosing LetRec while
// mark `name` as an enclosing LetRec while
// lifting its body, so any inner LetRec that captures
// `name` panics with the closure-of-self message rather
// than silently lifting with `name` as a fn-typed extra
@@ -470,7 +470,7 @@ impl<'a> Lifter<'a> {
// is built below after we know `lifted_name` and `extras`.
let in_has_value_use = find_non_callee_use(&lifted_in, name).is_some();
// Iter 16b.6: reject name-as-value in `in_term` when the
// reject name-as-value in `in_term` when the
// enclosing fn is polymorphic — the eta-Lam wrap would
// need to be `Forall`-quantified, but `Term::Lam` has no
// such slot. Closure conversion with polymorphism is the
@@ -501,7 +501,7 @@ impl<'a> Lifter<'a> {
.cloned()
.collect();
// Iter 16b.7: if any capture refers to an enclosing
// if any capture refers to an enclosing
// LetRec's NAME (rather than its params or a regular
// local), reject. Lifting `name$lr_N` with `outer` as a
// fn-typed extra param would mis-compile: the outer
@@ -538,7 +538,7 @@ impl<'a> Lifter<'a> {
// Build the lifted FnDef.
//
// Iter 16b.6: when the enclosing fn is polymorphic, wrap
// when the enclosing fn is polymorphic, wrap
// the augmented Fn in a `Type::Forall` mirroring the
// enclosing fn's type vars (`current_def_forall_vars`).
// The capture types may mention any of those vars; the
@@ -673,7 +673,7 @@ impl<'a> Lifter<'a> {
}
}
/// Iter 16b.3: produce a fresh `<hint>$lr_N` name not present in
/// produce a fresh `<hint>$lr_N` name not present in
/// `module_names`. Bumps `counter` and `module_names` so a later
/// lift cannot collide.
fn fresh_lifted(&mut self, hint: &str) -> String {
@@ -704,17 +704,17 @@ impl<'a> Lifter<'a> {
let mut subst = Subst::default();
let mut counter: u32 = 0;
let mut effects: BTreeSet<String> = BTreeSet::new();
// Iter 22b.2 (Task 9): the lift pass does its own type
// the lift pass does its own type
// synthesis to figure out free-var types for letrec captures.
// Class-method residuals collected here are discarded — the
// missing-constraint check has already run for the enclosing
// fn during `check_fn`. Threading the accumulator only keeps
// `synth`'s signature uniform.
let mut residuals: Vec<crate::ResidualConstraint> = Vec::new();
// Iter 23.4: free-fn-call observations are similarly discarded
// free-fn-call observations are similarly discarded
// here — the mono pass will re-synth bodies post-lift.
let mut free_fn_calls: Vec<crate::FreeFnCall> = Vec::new();
// mq.3: lift's synth re-entry is post-typecheck — warnings
// lift's synth re-entry is post-typecheck — warnings
// (e.g. class-method-shadowed-by-fn) have already been
// surfaced upstream by `check_workspace`. Discard here.
let mut warnings_discarded: Vec<crate::diagnostic::Diagnostic> = Vec::new();
@@ -727,7 +727,7 @@ impl<'a> Lifter<'a> {
}
}
/// Iter 16b.3: returns true iff any `Def::Fn` body or `Def::Const`
/// returns true iff any `Def::Fn` body or `Def::Const`
/// value in `m` reaches a `Term::LetRec`. Used as a fast-path skip:
/// modules without any deferred LetRec need no traversal at all.
fn contains_any_letrec(m: &Module) -> bool {
@@ -749,9 +749,9 @@ fn contains_any_letrec(m: &Module) -> bool {
Term::Lam { body, .. } => term_has_letrec(body),
Term::Seq { lhs, rhs } => term_has_letrec(lhs) || term_has_letrec(rhs),
Term::LetRec { .. } => true,
// Iter 18c.1: identity passthrough for the letrec-detection scan.
// identity passthrough for the letrec-detection scan.
Term::Clone { value } => term_has_letrec(value),
// Iter 18d.1: structural recursion through both children.
// structural recursion through both children.
Term::ReuseAs { source, body } => term_has_letrec(source) || term_has_letrec(body),
Term::Loop { binders, body } => {
binders.iter().any(|b| term_has_letrec(&b.init)) || term_has_letrec(body)
@@ -769,7 +769,7 @@ fn contains_any_letrec(m: &Module) -> bool {
false
}
/// Iter 16b.3: scan `defs` for the highest existing `*$lr_N` suffix
/// scan `defs` for the highest existing `*$lr_N` suffix
/// and return that N. Used to seed `Lifter.counter` past any
/// 16b.2-lifted defs that already live in the desugared module.
fn highest_lr_suffix(defs: &[Def]) -> Option<u64> {
@@ -786,7 +786,7 @@ fn highest_lr_suffix(defs: &[Def]) -> Option<u64> {
max
}
/// Iter 16b.3: minimal pattern → bindings inference for the lift
/// minimal pattern → bindings inference for the lift
/// pass. Called only for patterns the typechecker has already
/// accepted, so we propagate just enough to resolve capture types
/// (we don't re-run unification here — the typechecker did that
+29 -29
View File
@@ -145,7 +145,7 @@ use ailang_core::ast::{Arm, Ctor, Def, FnDef, Module, ParamMode, Pattern, Term,
use ailang_surface::{term_to_form_a, type_to_form_a};
use std::collections::HashMap;
/// Iter 19a.1: a `Type::Con` whose name is `Int`/`Bool`/`Str`/`Unit`
/// a `Type::Con` whose name is `Int`/`Bool`/`Str`/`Unit`
/// is a primitive value type — no RC, no heap. Reading a primitive
/// pattern-binder bumps `consume_count` in the uniqueness table but
/// does not force the scrutinee param to be `Own`. The lint filters
@@ -191,7 +191,7 @@ struct BinderState {
borrow_count: u32,
}
/// Iter 18c.2: top-level entry. Walks every fn in `m` and emits
/// top-level entry. Walks every fn in `m` and emits
/// linearity diagnostics for the all-explicit-mode fns. Other fns are
/// skipped without traversal.
///
@@ -207,7 +207,7 @@ pub(crate) fn check_module(m: &Module) -> Vec<Diagnostic> {
check_module_with_visible(m, &[])
}
/// Iter 23.5: workspace-aware entry. `visible_extra` is a slice of
/// workspace-aware entry. `visible_extra` is a slice of
/// modules whose top-level fns and class methods should be visible
/// to `callee_arg_modes` (so a Borrow-mode user fn that forwards its
/// borrow params into an imported polymorphic free fn — e.g. the
@@ -219,13 +219,13 @@ pub(crate) fn check_module(m: &Module) -> Vec<Diagnostic> {
/// — they are checked when their own module is processed.
pub(crate) fn check_module_with_visible(m: &Module, visible_extra: &[&Module]) -> Vec<Diagnostic> {
let mut globals: HashMap<String, Type> = HashMap::new();
// Iter 19a.1: ctor name → field types, used by the
// ctor name → field types, used by the
// `over-strict-mode` lint to filter out primitive-typed
// pattern-binders (Int / Bool / Str / Unit) — their consume
// does not force ownership of the scrutinee.
let mut ctors: HashMap<String, Vec<Type>> = HashMap::new();
// Iter 24.1: register builtin signatures so `callee_arg_modes`
// register builtin signatures so `callee_arg_modes`
// resolves them when a user fn forwards a Borrow-mode param
// into a builtin like `str_clone : (Str borrow) -> Str`. The
// previously-registered class-method types (below, in the
@@ -238,7 +238,7 @@ pub(crate) fn check_module_with_visible(m: &Module, visible_extra: &[&Module]) -
globals.insert(name.clone(), strip_forall(ty).clone());
}
// Iter 23.5: register fns + class methods from visible-extra
// register fns + class methods from visible-extra
// modules first, so a name-clash inside `m` overwrites the
// imported entry (matching the synth-side scope rule that
// local-defs shadow implicit imports).
@@ -277,7 +277,7 @@ pub(crate) fn check_module_with_visible(m: &Module, visible_extra: &[&Module]) -
ctors.insert(name.clone(), fields.clone());
}
}
// Iter 23.4-prep: class methods register their types into
// class methods register their types into
// the globals map so `callee_arg_modes` can see their
// `param_modes`. Without this, a borrow-mode fn that
// forwards its borrow params into a class-method call
@@ -293,7 +293,7 @@ pub(crate) fn check_module_with_visible(m: &Module, visible_extra: &[&Module]) -
}
}
// Iter 19a: the over-strict-mode lint reads `consume_count` from
// the over-strict-mode lint reads `consume_count` from
// the uniqueness side table. Build it once for the whole module;
// reusing the existing pass keeps the "what counts as a consume"
// definition in a single place.
@@ -371,7 +371,7 @@ fn check_fn(
checker.walk(&f.body, Position::Consume);
// Iter 19a / 19a.1: `over-strict-mode` lint. After the linearity
// `over-strict-mode` lint. After the linearity
// walk has completed (and any errors have been recorded), inspect
// each `Own`-annotated param: if `consume_count(p) == 0` AND no
// pattern-binder of any `(match p ...)` arm in the body has
@@ -565,7 +565,7 @@ impl<'a> Checker<'a> {
self.walk(value, Position::Borrow);
}
Term::ReuseAs { source, body } => {
// Iter 18d.1: linearity for `(reuse-as SRC BODY)`:
// linearity for `(reuse-as SRC BODY)`:
// - `SRC` must be a bare `Var { name }` of an
// in-scope binder (else `reuse-as-source-not-bare-var`).
// - The binder must currently have `consumed == false`
@@ -779,7 +779,7 @@ fn make_use_after_consume(def: &str, binder: &str) -> Diagnostic {
)
}
/// Iter 18d.1: build a `reuse-as-source-not-bare-var` diagnostic.
/// build a `reuse-as-source-not-bare-var` diagnostic.
/// Fires when a `Term::ReuseAs.source` is anything other than a bare
/// `Term::Var { name }` referring to an in-scope binder. The
/// suggested rewrite drops the meaningless wrapper and keeps the
@@ -817,7 +817,7 @@ fn make_reuse_as_source_not_bare_var(def: &str, source: &Term, body: &Term) -> D
)
}
/// Iter 18d.1: `use-after-consume` raised at a `(reuse-as <var> <body>)`
/// `use-after-consume` raised at a `(reuse-as <var> <body>)`
/// site whose `<var>` binder has already been consumed elsewhere. Same
/// code as the regular use-after-consume; the suggested rewrite is to
/// drop the (now unsatisfiable) reuse hint and keep the body — the
@@ -838,7 +838,7 @@ fn make_use_after_consume_at_reuse_as(def: &str, binder: &str, body: &Term) -> D
)
}
/// Iter 19a.1: precise sub-binder check for the over-strict-mode
/// precise sub-binder check for the over-strict-mode
/// lint. Returns `true` iff some `(match p ...)` somewhere in `t`,
/// where `p` is the param `pname`, has an arm whose pattern-binders
/// include at least one binder with `consume_count > 0` in the
@@ -959,7 +959,7 @@ fn any_sub_binder_consumed_for(
}
}
/// Iter 19a.1: returns `true` if `pat` introduces any **heap-typed**
/// returns `true` if `pat` introduces any **heap-typed**
/// binder whose recorded `consume_count` is `> 0`. Primitive-typed
/// pattern-binders (`Int` / `Bool` / `Str` / `Unit`) never count
/// because reading a primitive does not force the scrutinee to be
@@ -982,7 +982,7 @@ fn pattern_has_consumed_heap_binder(
pattern_has_consumed_heap_binder_at(pat, None, uniq, def_name, ctors)
}
/// Iter 19a.1: helper carrying the optional declared type of `pat`.
/// helper carrying the optional declared type of `pat`.
/// At the top of an arm pattern, `declared_ty` is `None` (we don't
/// track the scrutinee's type in the lint); the type becomes known
/// when we descend into a `Pattern::Ctor`'s fields, where the
@@ -1136,7 +1136,7 @@ fn term_mentions_any_binder(t: &Term, binders: &[String]) -> bool {
}
}
/// Iter 19a: a scrutinee "is" `pname` if it's a bare `Var { pname }`
/// a scrutinee "is" `pname` if it's a bare `Var { pname }`
/// or `Clone(Var { pname })`. Anything else (a fresh App result, a
/// let-binder, …) does not put `pname` directly in scrutinee
/// position.
@@ -1148,7 +1148,7 @@ fn scrutinee_matches_param(scrutinee: &Term, pname: &str) -> bool {
}
}
/// Iter 19a: build the `over-strict-mode` warning. `inner` is the
/// build the `over-strict-mode` warning. `inner` is the
/// fn-type with the original mode; we synthesise a relaxed copy with
/// `param_modes[i] = Borrow` and render it through
/// [`type_to_form_a`] to produce the suggested rewrite.
@@ -1173,7 +1173,7 @@ fn make_over_strict_mode(def: &str, binder: &str, inner: &Type, i: usize) -> Dia
)
}
/// Iter 19a: clone of `inner` (assumed `Type::Fn`) with
/// clone of `inner` (assumed `Type::Fn`) with
/// `param_modes[i]` rewritten to `ParamMode::Borrow`. Other slots
/// are preserved bit-for-bit. Falls back to returning `inner`
/// unchanged if `inner` is not a `Type::Fn` (defensive — caller
@@ -1310,7 +1310,7 @@ mod tests {
assert!(check_module(&m).is_empty());
}
/// Iter 18d.1: a `(reuse-as 42 (Cons ...))` whose source is a literal
/// a `(reuse-as 42 (Cons ...))` whose source is a literal
/// (not a bare `Var`) is flagged with `reuse-as-source-not-bare-var`.
/// The suggested rewrite drops the wrapper and keeps the body.
#[test]
@@ -1347,7 +1347,7 @@ mod tests {
.unwrap_or_else(|e| panic!("suggested rewrite must parse: {rep} ({e})"));
}
/// Iter 18d.1: a `(reuse-as p0 ...)` where `p0` was already
/// a `(reuse-as p0 ...)` where `p0` was already
/// consumed by an earlier sibling fires `use-after-consume` at the
/// reuse-as site (not `reuse-as-source-not-bare-var`).
#[test]
@@ -1383,7 +1383,7 @@ mod tests {
use crate::diagnostic::Severity;
/// Iter 19a, positive: a fn with `(own T)` whose body merely
/// a fn with `(own T)` whose body merely
/// borrows the param via a `Borrow`-mode call → fires
/// `over-strict-mode` (Warning) with binder=p0, current=own,
/// suggested=borrow, and a non-empty rewrite.
@@ -1453,7 +1453,7 @@ mod tests {
);
}
/// Iter 19a, negative: a fn with `(own T)` whose body consumes
/// a fn with `(own T)` whose body consumes
/// the param directly does not fire `over-strict-mode`.
#[test]
fn over_strict_mode_silent_when_body_consumes_param() {
@@ -1475,7 +1475,7 @@ mod tests {
);
}
/// Iter 19a, negative: a fn with `(borrow T)` and a borrowing
/// a fn with `(borrow T)` and a borrowing
/// body never fires `over-strict-mode` (the lint is gated on
/// `Own`).
#[test]
@@ -1498,7 +1498,7 @@ mod tests {
);
}
/// Iter 19a.1, positive: a fn with `(own T)` whose body is
/// a fn with `(own T)` whose body is
/// `(match p0 ...)` and never consumes `p0` directly nor any of
/// `p0`'s sub-binders DOES fire `over-strict-mode` under the
/// precise sub-binder analysis. This is the test that 19a left
@@ -1532,7 +1532,7 @@ mod tests {
assert_eq!(osm[0].def.as_deref(), Some("f"));
}
/// Iter 19a.1: helper to build an `IntList = Nil | Cons(Int,
/// helper to build an `IntList = Nil | Cons(Int,
/// IntList)` ADT def. Used by the head_or_zero-shape tests
/// (positive + negative variants below) so the lint can look up
/// each ctor field's type and filter out primitives.
@@ -1555,7 +1555,7 @@ mod tests {
})
}
/// Iter 19a.1: helper to build a fn whose single param is
/// helper to build a fn whose single param is
/// `(own IntList)` and whose body is `body`. Distinct from
/// `fn_with_modes` (which uses a generic `List` type for params)
/// so the test exercises a real ctor-field-types lookup.
@@ -1577,7 +1577,7 @@ mod tests {
})
}
/// Iter 19a.1, negative: a fn with `(own IntList)` whose body
/// a fn with `(own IntList)` whose body
/// is `match xs { Cons(h, t) => t }` consumes the *heap-typed*
/// pattern-binder `t`. The lint must NOT fire — the
/// sub-consume forces ownership.
@@ -1707,7 +1707,7 @@ mod tests {
);
}
/// Iter 19a.1, positive: a fn with `(own IntList)` whose body
/// a fn with `(own IntList)` whose body
/// is `match xs { Nil => 0, Cons(h, t) => h }` returns the
/// **primitive-typed** `h` (`Int`) and never moves the
/// heap-typed `t`. The lint MUST fire — `(borrow IntList)`
@@ -1765,7 +1765,7 @@ mod tests {
);
}
/// Iter 23.4-prep Task 1: a borrow-mode fn that forwards its borrow
/// a borrow-mode fn that forwards its borrow
/// params into a class-method call must not fire
/// `consume-while-borrowed` false positives. The pre-fix behaviour
/// is two diagnostics (one per param) because
+50 -52
View File
@@ -61,7 +61,7 @@ use crate::Result;
use indexmap::IndexMap;
use std::collections::{BTreeMap, BTreeSet};
/// Iter 22b.3: workspace-wide monomorphisation pass entry. See the
/// workspace-wide monomorphisation pass entry. See the
/// module-level doc for the architecture and contract.
///
/// Pre-condition: `ws` has been typechecked (`check_workspace(ws)`
@@ -113,7 +113,7 @@ pub fn monomorphise_workspace(ws: &Workspace) -> Result<Workspace> {
// target's `defining_module`. Mark the key as synthesised
// so the next round won't re-collect.
//
// Iter 22b.3 uses ws_owned.registry as the source of
// The mono pass uses ws_owned.registry as the source of
// truth for ClassDef + InstanceDef lookups. Both come
// from the un-modified workspace registry — synthesis
// never mutates the registry.
@@ -179,7 +179,7 @@ pub fn monomorphise_workspace(ws: &Workspace) -> Result<Workspace> {
// collection-phase walk (cursor alignment depends on it).
let mut env_mod = env.clone();
apply_per_module_types_overlay(&mut env_mod, &ws_owned, mname);
// Iter 23.4: precompute the per-module poly-free-fn name
// precompute the per-module poly-free-fn name
// set used as the rewrite walker's second predicate.
let poly_free_fns = poly_free_fn_names_for_module(&ws_owned, &env_mod, mname);
// 2026-05-14 bugfix: per-poly-free-fn-name constraint count,
@@ -255,7 +255,7 @@ pub fn monomorphise_workspace(ws: &Workspace) -> Result<Workspace> {
Ok(ws_owned)
}
/// Iter 22b.3: walk every fn / const body in the workspace,
/// walk every fn / const body in the workspace,
/// returning the union of [`collect_mono_targets`] outputs. Const
/// bodies are wrapped in a synthetic zero-arg `FnDef` for the
/// collection call — the residual gathering only depends on body
@@ -290,7 +290,7 @@ fn collect_targets_workspace_wide(
Ok(out)
}
/// Iter 22b.3.6: wrap a [`ConstDef`] in a synthetic zero-arg
/// wrap a [`ConstDef`] in a synthetic zero-arg
/// [`AstFnDef`] for residual-collection / rewrite purposes. Const
/// bodies have no parameter list, so the residual gathering only
/// depends on body shape — the wrapping is a structural adapter,
@@ -309,7 +309,7 @@ fn const_as_pseudo_fn(c: &ConstDef) -> AstFnDef {
}
}
/// Iter 23.4: compute the set of names (bare + dot-qualified) that
/// compute the set of names (bare + dot-qualified) that
/// `synth`'s Var arm would resolve to a polymorphic free fn when
/// called from module `mname`. The rewrite walker and the slot
/// collector both consult this set to decide whether a `Term::Var`
@@ -409,12 +409,12 @@ fn poly_free_fn_constraint_counts_for_module(
out
}
/// Iter 22b.3: workspace-wide `class-name -> ClassDef` index.
/// workspace-wide `class-name -> ClassDef` index.
/// Used by the fixpoint to look up the matching class definition
/// when synthesising a fn.
///
/// mq.1: keys carry the qualified class name
/// (`<defining_module>.<Class>`) to match the post-mq.1 residual /
/// keys carry the qualified class name
/// (`<defining_module>.<Class>`) to match the post-canonical-class-form residual /
/// `MonoTarget::ClassMethod.class` field and the registry's
/// qualified entries key.
fn build_class_index(ws: &Workspace) -> BTreeMap<String, ClassDef> {
@@ -430,7 +430,7 @@ fn build_class_index(ws: &Workspace) -> BTreeMap<String, ClassDef> {
idx
}
/// Iter 22b.3 / iter 23.4: returns `true` iff any module in `ws`
/// returns `true` iff any module in `ws`
/// declares at least one specialisable target — a [`Def::Class`]
/// or [`Def::Instance`] (class-method residuals) OR a
/// `Type::Forall`-quantified [`Def::Fn`] (free-fn case). Cheap
@@ -451,7 +451,7 @@ fn workspace_has_specialisable_targets(ws: &Workspace) -> bool {
})
}
/// Iter 22b.3 / iter 23.4: deterministic mono-symbol name for a
/// deterministic mono-symbol name for a
/// `(base-name, types...)` tuple. The N-ary form supports both
/// single-type-var class methods (today's six primitive Eq/Ord
/// symbols pass a one-element slice and produce identical output
@@ -480,7 +480,7 @@ pub fn mono_symbol(base: &str, ty: &Type) -> String {
mono_symbol_n(base, std::slice::from_ref(ty))
}
/// Iter 23.4: N-ary variant of [`mono_symbol`]. The single-type
/// N-ary variant of [`mono_symbol`]. The single-type
/// public-API stays as [`mono_symbol`] (used by class-method
/// emission) for surface compatibility; N-ary callers (free-fn
/// emission with multi-arg type instantiations) use this form
@@ -519,7 +519,7 @@ fn primitive_surface_name(ty: &Type) -> Option<&'static str> {
}
}
/// Iter 22b.3 / iter 23.4: a specialisation request. One synthesised
/// a specialisation request. One synthesised
/// monomorphic `Def::Fn` will be produced per unique
/// [`mono_target_key`]. Two source-body kinds share one fixpoint:
///
@@ -551,7 +551,7 @@ pub enum MonoTarget {
},
}
/// Iter 22b.3 / iter 23.4: dedup key for a [`MonoTarget`]. Class-method
/// dedup key for a [`MonoTarget`]. Class-method
/// targets key on `("class", "<class>.<method>", <type-hash>)`; free-fn
/// targets key on `("free", "<name>", <joined-type-hashes>)`. The two
/// kinds are guaranteed-disjoint by the first component, so a
@@ -614,7 +614,7 @@ fn apply_per_module_types_overlay(env: &mut crate::Env, ws: &Workspace, module_n
}
}
/// Iter 22b.3: re-run [`crate::synth`] on `f`'s body to recover
/// re-run [`crate::synth`] on `f`'s body to recover
/// the per-fn residual class constraints. Filter to fully-
/// concrete residuals (the only ones eligible for monomorphisation),
/// look up each `(class, type-hash)` in the registry to recover
@@ -632,7 +632,7 @@ fn apply_per_module_types_overlay(env: &mut crate::Env, ws: &Workspace, module_n
/// target-collection site, or `Type::unit()`-default at the residual-
/// ordering site).
///
/// Iter 24.tidy: extracted from two byte-identical call sites at
/// extracted from two byte-identical call sites at
/// `collect_mono_targets` and `collect_residuals_ordered` per
/// audit-24's [medium-2] drift item. The byte-identity invariant
/// (Phase 2 synthesis name must match Phase 3 rewrite cursor's
@@ -678,25 +678,23 @@ pub fn collect_mono_targets(
env.rigid_vars.insert(v.clone());
}
env.current_module = module_name.to_string();
// Iter 22c (sibling of commit 5c5180f): seed `env.globals` from
// the current module's fns so `synth`'s `Term::Var` lookup
// (lib.rs:1678) resolves bare same-module references — most
// importantly self-recursive top-level fns. Mirrors
// `check_in_workspace` (lib.rs:1135-1139). Top-level fn names
// are only per-module-unique (workspace-wide collisions are
// legal), so seeding must be scoped to the current module —
// Seed `env.globals` from the current module's fns so `synth`'s
// `Term::Var` lookup (lib.rs:1678) resolves bare same-module
// references — most importantly self-recursive top-level fns.
// Mirrors `check_in_workspace` (lib.rs:1135-1139). Top-level fn
// names are only per-module-unique (workspace-wide collisions
// are legal), so seeding must be scoped to the current module —
// unlike the workspace-wide flat `env.types` / `env.ctor_index`
// tables that 5c5180f populated in `build_workspace_env`.
// tables that `build_workspace_env` populates.
if let Some(g) = env.module_globals.get(module_name).cloned() {
for (n, t) in g {
env.globals.insert(n, t);
}
}
// Iter 22c (sibling of commit 13b36cc): seed `env.imports` from
// the current module's import list so `synth`'s qualified-var
// path (lib.rs:1697) resolves `Mod.fn` references. Mirrors
// `check_in_workspace` (lib.rs:1147-1152). Per-module because
// aliases collide across modules.
// Seed `env.imports` from the current module's import list so
// `synth`'s qualified-var path (lib.rs:1697) resolves `Mod.fn`
// references. Mirrors `check_in_workspace` (lib.rs:1147-1152).
// Per-module because aliases collide across modules.
if let Some(im) = env.module_imports.get(module_name).cloned() {
env.imports = im;
}
@@ -710,7 +708,7 @@ pub fn collect_mono_targets(
let mut counter: u32 = 0;
let mut residuals: Vec<crate::ResidualConstraint> = Vec::new();
let mut free_fn_calls: Vec<crate::FreeFnCall> = Vec::new();
// mq.3: mono's residual-collection synth re-entry collects-and-
// mono's residual-collection synth re-entry collects-and-
// discards warnings — typecheck has already run and surfaced any
// shadow warnings; mono is a post-typecheck pass.
let mut warnings_discarded: Vec<crate::diagnostic::Diagnostic> = Vec::new();
@@ -756,8 +754,8 @@ pub fn collect_mono_targets(
defining_module: entry.defining_module.clone(),
});
}
// Iter 23.4 Task 4: free-fn arm. Each `FreeFnCall` observation
// recorded by `synth`'s Var arm carries the bare name, the
// Free-fn arm: each `FreeFnCall` observation recorded by
// `synth`'s Var arm carries the bare name, the
// owning module (resolved through synth's lookup ladder), the
// forall vars (declaration order), and the per-var fresh
// metavars. Post-`synth`, `subst.apply` each meta to recover
@@ -772,7 +770,7 @@ pub fn collect_mono_targets(
// would have rejected; the Unit default is sound and
// deterministic.
for fc in free_fn_calls {
// Iter 23.5: if any resolved type-arg is a rigid Type::Var
// if any resolved type-arg is a rigid Type::Var
// (i.e. a forall var of the *enclosing* polymorphic fn,
// not a free metavar), skip this target. The enclosing fn
// will be monomorphised in its own right, and that mono
@@ -825,7 +823,7 @@ pub fn collect_mono_targets(
Ok(out)
}
/// Iter 23.5: true iff `t` contains a non-metavar `Type::Var` anywhere.
/// true iff `t` contains a non-metavar `Type::Var` anywhere.
/// Used by free-fn target collection to distinguish rigid forall vars
/// (skip — wait for the enclosing fn's mono pass) from unbound metavars
/// (Unit-default — no later round will pin them). The naming
@@ -843,7 +841,7 @@ fn contains_rigid_var(t: &Type) -> bool {
}
}
/// Iter 22b.3: produce a `FnDef` for a single (target, class,
/// produce a `FnDef` for a single (target, class,
/// instance) triple. The synthesised fn:
///
/// 1. has name [`mono_symbol`]`(target.method, target.type_)`,
@@ -949,7 +947,7 @@ pub fn synthesise_mono_fn(
})
}
/// Iter 23.4: synthesise a monomorphic `FnDef` for a polymorphic
/// synthesise a monomorphic `FnDef` for a polymorphic
/// free-fn target. The source body comes directly from the
/// polymorphic `Def::Fn`'s `body` (NOT from `Registry::entries`);
/// rigid-var substitution applies the `target.type_args` to the
@@ -1049,7 +1047,7 @@ pub fn synthesise_mono_fn_for_free_fn(
})
}
/// Iter 22b.3 / iter 23.4: rewrite every polymorphic call site in
/// rewrite every polymorphic call site in
/// `body` to the corresponding mono symbol, using `ordered_targets`
/// — the per-call-site resolved targets collected by a parallel
/// synth-replay run on the same body via [`collect_residuals_ordered`].
@@ -1096,7 +1094,7 @@ fn rewrite_mono_calls(
// residual OR a poly-free-fn observation. Locally
// shadowed names match neither — synth pushes nothing.
//
// mq.3: the class-method side is method-keyed natively via
// the class-method side is method-keyed natively via
// `method_to_candidate_classes`. Class disambiguation
// (which class declared the resolved method) lives in the
// residual slot the cursor consumes, not here.
@@ -1248,7 +1246,7 @@ fn rewrite_mono_calls(
}
}
/// Iter 22b.3.6: collect the variable binders introduced by a
/// collect the variable binders introduced by a
/// match pattern. Used by the walker's `Term::Match` arm to extend
/// `locals` for each arm body. Mirrors `Pattern`'s shape:
/// - [`Pattern::Wild`] / [`Pattern::Lit`] bind nothing,
@@ -1271,7 +1269,7 @@ fn pattern_binders(pat: &Pattern) -> Vec<String> {
out
}
/// mq.2: resolve a residual's class for mono target construction.
/// resolve a residual's class for mono target construction.
/// Returns `Some(class)` for a discharge-ready residual (single-class
/// or refined-multi-candidate); `None` if the multi-candidate residual
/// cannot be refined — in which case typecheck-side already raised a
@@ -1292,7 +1290,7 @@ pub fn resolve_residual_class_for_mono(
}
}
/// Iter 22b.3: traversal-ordered residual collection — used by
/// traversal-ordered residual collection — used by
/// the rewrite walker to align cursor positions. Unlike
/// [`collect_mono_targets`], this includes non-concrete residuals
/// as `None`, preserving one-entry-per-callsite alignment.
@@ -1317,7 +1315,7 @@ pub(crate) fn collect_residuals_ordered(
env.rigid_vars.insert(v.clone());
}
env.current_module = module_name.to_string();
// Iter 22c: same `env.globals` seeding as `collect_mono_targets`
// same `env.globals` seeding as `collect_mono_targets`
// — see the comment there for rationale. Both fns re-run `synth`
// and must agree with the main check path's per-module env shape.
if let Some(g) = env.module_globals.get(module_name).cloned() {
@@ -1325,7 +1323,7 @@ pub(crate) fn collect_residuals_ordered(
env.globals.insert(n, t);
}
}
// Iter 22c: same `env.imports` seeding as `collect_mono_targets`
// same `env.imports` seeding as `collect_mono_targets`
// — see the comment there for rationale.
if let Some(im) = env.module_imports.get(module_name).cloned() {
env.imports = im;
@@ -1340,7 +1338,7 @@ pub(crate) fn collect_residuals_ordered(
let mut counter: u32 = 0;
let mut residuals: Vec<crate::ResidualConstraint> = Vec::new();
let mut free_fn_calls: Vec<crate::FreeFnCall> = Vec::new();
// mq.3: mono's residual-collection synth re-entry collects-and-
// mono's residual-collection synth re-entry collects-and-
// discards warnings — typecheck has already run and surfaced any
// shadow warnings; mono is a post-typecheck pass.
let mut warnings_discarded: Vec<crate::diagnostic::Diagnostic> = Vec::new();
@@ -1362,8 +1360,8 @@ pub(crate) fn collect_residuals_ordered(
&mut warnings_discarded,
)?;
// Iter 23.4 Task 6: build two per-channel slot lists — one for
// class-method residuals, one for poly-free-fn observations —
// Build two per-channel slot lists — one for class-method
// residuals, one for poly-free-fn observations —
// each in synth's push order. Then walk the AST in synth's
// traversal order, classifying each `Term::Var` site as
// class-method, poly-free-fn, or neither (and consuming from
@@ -1380,7 +1378,7 @@ pub(crate) fn collect_residuals_ordered(
let r_ty_norm = env
.workspace_registry
.normalize_type_for_lookup(module_name, &r_ty);
// mq.2: resolve the residual's class via the multi-
// resolve the residual's class via the multi-
// candidate refinement helper. Single-class residuals
// (`candidates: None`) flow through `r.class.clone()`
// unchanged; multi-candidate residuals filter against
@@ -1463,13 +1461,13 @@ pub(crate) fn collect_residuals_ordered(
Ok(out)
}
/// Iter 23.4 helper for `collect_residuals_ordered`: walk a body
/// walk a body
/// in synth's traversal order, interleaving class-method and
/// poly-free-fn slots in source-AST order. The walker MUST stay
/// in lockstep with `rewrite_mono_calls` — same predicates, same
/// shadowing handling.
///
/// mq.3: class-method presence is method-keyed natively via
/// class-method presence is method-keyed natively via
/// `method_to_candidate_classes` (post-`MethodNameCollision`-retirement,
/// `class_methods` is tuple-keyed by `(class, method)` and not
/// directly probable by method name alone).
@@ -1625,7 +1623,7 @@ mod tests {
use super::*;
use ailang_core::ast::Type;
/// mq.2.8: mono's residual-class resolver refines multi-candidate
/// mono's residual-class resolver refines multi-candidate
/// residuals via the same logic as discharge. A multi-candidate
/// residual with a concrete `type_` and a single registry-survivor
/// resolves to that class.
@@ -1650,7 +1648,7 @@ mod tests {
assert_eq!(resolved, Some("prelude.Show".to_string()));
}
/// mq.2.8: single-class residual (candidates: None) flows through
/// single-class residual (candidates: None) flows through
/// unchanged.
#[test]
fn mq2_mono_single_class_residual_unchanged() {
@@ -1665,7 +1663,7 @@ mod tests {
assert_eq!(resolved, Some("prelude.Eq".to_string()));
}
/// mq.2.8: multi-candidate residual that cannot refine (zero
/// multi-candidate residual that cannot refine (zero
/// registry survivors) returns None.
#[test]
fn mq2_mono_multi_candidate_no_survivors_returns_none() {
+1 -1
View File
@@ -71,7 +71,7 @@ use ailang_core::ast::{Arm, Def, FnDef, Module, ParamMode, Pattern, Term, Type,
use ailang_surface::term_to_form_a;
use std::collections::HashMap;
/// Iter 18d.2: top-level entry. Walks every fn in `m` and emits
/// top-level entry. Walks every fn in `m` and emits
/// `reuse-as-shape-mismatch` diagnostics for the all-explicit-mode
/// fns. Other fns are skipped.
///
+6 -6
View File
@@ -131,7 +131,7 @@ mod tests {
}
}
/// Iter 19b: a suppress entry with a matching `(def, code)` pair
/// a suppress entry with a matching `(def, code)` pair
/// drops the diagnostic from the accumulated list.
#[test]
fn matching_suppress_drops_the_diagnostic() {
@@ -148,7 +148,7 @@ mod tests {
assert!(diags.is_empty(), "diagnostic should have been dropped: {diags:?}");
}
/// Iter 19b: an empty `because` produces an `empty-suppress-reason`
/// an empty `because` produces an `empty-suppress-reason`
/// Error AND does NOT drop the original diagnostic.
#[test]
fn empty_because_errors_and_does_not_suppress() {
@@ -179,7 +179,7 @@ mod tests {
);
}
/// Iter 19b: whitespace-only `because` is treated the same as
/// whitespace-only `because` is treated the same as
/// empty — the trimmed text is what matters.
#[test]
fn whitespace_only_because_errors() {
@@ -205,7 +205,7 @@ mod tests {
);
}
/// Iter 19b: a suppress entry whose `code` does not match any
/// a suppress entry whose `code` does not match any
/// diagnostic in the list is silently a no-op (the diagnostic
/// registry is open-set; a typo costs "warning still fires", not
/// a meta-error).
@@ -226,7 +226,7 @@ mod tests {
assert_eq!(diags[0].code, "over-strict-mode");
}
/// Iter 19b: a diagnostic on a *different* def is not affected by
/// a diagnostic on a *different* def is not affected by
/// a suppression on this fn.
#[test]
fn suppression_only_drops_diagnostics_on_same_def() {
@@ -246,7 +246,7 @@ mod tests {
assert_eq!(diags[0].def.as_deref(), Some("g"));
}
/// Iter 19b: a diagnostic with `def == None` is never dropped by
/// a diagnostic with `def == None` is never dropped by
/// any suppression (no def-name to match against).
#[test]
fn def_none_diagnostic_is_never_dropped() {
+3 -3
View File
@@ -108,7 +108,7 @@ pub type UniquenessTable = BTreeMap<(String, String), UniquenessInfo>;
/// typecheck first.
pub fn infer_module(m: &Module) -> UniquenessTable {
let mut globals: BTreeMap<String, Type> = BTreeMap::new();
// Iter 24.1: register builtin signatures so the App-arg walker
// register builtin signatures so the App-arg walker
// sees their `param_modes` and walks `Borrow`-mode args as
// `Position::Borrow` (not `Consume`). Without this, a builtin
// like `str_clone : (Str borrow) -> Str` invoked on a heap-Str
@@ -131,7 +131,7 @@ pub fn infer_module(m: &Module) -> UniquenessTable {
globals.insert(c.name.clone(), strip_forall(&c.ty).clone());
}
Def::Type(_) => {}
// Iter 22b.1: class/instance defs are skipped here. Class
// class/instance defs are skipped here. Class
// method signatures contribute global names once
// monomorphisation (22b.3) materialises them; the
// pre-monomorphisation form has no concrete `Type` to
@@ -327,7 +327,7 @@ impl<'a> Walker<'a> {
self.walk(value, Position::Borrow);
}
Term::ReuseAs { source, body } => {
// Iter 18d.1: `(reuse-as SRC BODY)` is a Consume of
// `(reuse-as SRC BODY)` is a Consume of
// `SRC` (the binder's slot is being taken over) and
// a normal walk of `BODY`. So a bare-Var SRC bumps
// its `consume_count` by one. Linearity is the
@@ -98,7 +98,7 @@ fn build_env_inline_pre_refactor(ws: &ailang_core::workspace::Workspace) -> Env
// env.class_superclasses — walk every module's Def::Class.
//
// mq.1: both key and value carry the qualified workspace-key shape;
// both key and value carry the qualified workspace-key shape;
// bare `SuperclassRef.class` is lifted using the parent class's
// defining module.
for (mod_name, m) in &ws.modules {
@@ -24,12 +24,12 @@ fn examples_dir() -> std::path::PathBuf {
.join("examples")
}
/// mq.3.5 (class-class repurpose): the fixture
/// Class-class repurpose: the fixture
/// `examples/test_22b2_method_name_collision_class_class.ail.json`
/// declares two classes `A` and `B` in the same module, each declaring
/// the method `foo`. Pre-mq.3 this fired
/// the method `foo`. Pre-canonical-class-form this fired
/// `WorkspaceLoadError::MethodNameCollision { kind: "class-class" }`.
/// Post-mq.3 the workspace loads cleanly and
/// Post-canonical-class-form the workspace loads cleanly and
/// `env.method_to_candidate_classes` carries both qualified classes as
/// candidates for the method name `foo`.
#[test]
@@ -37,7 +37,7 @@ fn mq3_class_class_collision_loads_clean_and_populates_candidates() {
let entry = examples_dir()
.join("test_22b2_method_name_collision_class_class.ail");
let ws = load_workspace(&entry)
.expect("post-mq.3: workspace loads without collision diagnostic");
.expect("post-canonical-class-form: workspace loads without collision diagnostic");
let env = build_check_env(&ws);
let candidates = env
@@ -59,18 +59,18 @@ fn mq3_class_class_collision_loads_clean_and_populates_candidates() {
);
}
/// mq.3.5 (class-fn repurpose): the fixture
/// Class-fn repurpose: the fixture
/// `examples/test_22b2_method_name_collision_class_fn.ail.json`
/// declares `class Greet { greet }` and `fn greet`. Pre-mq.3 this
/// declares `class Greet { greet }` and `fn greet`. Pre-canonical-class-form this
/// fired `WorkspaceLoadError::MethodNameCollision { kind: "class-fn" }`.
/// Post-mq.3 the workspace loads cleanly — the method-vs-fn
/// Post-canonical-class-form the workspace loads cleanly — the method-vs-fn
/// coexistence is now legal at load time; the call-site emits a
/// `class-method-shadowed-by-fn` warning (covered by mq.3.6 E2E,
/// `class-method-shadowed-by-fn` warning (covered by the multi-class E2E,
/// not this load-level pin).
#[test]
fn mq3_class_fn_collision_loads_clean() {
let entry = examples_dir()
.join("test_22b2_method_name_collision_class_fn.ail");
load_workspace(&entry)
.expect("post-mq.3: workspace loads — class-vs-fn name overlap is no longer a load error");
.expect("post-canonical-class-form: workspace loads — class-vs-fn name overlap is no longer a load error");
}
@@ -27,7 +27,7 @@ fn per_type_print_effect_ops_are_retired() {
for retired in ["io/print_int", "io/print_bool", "io/print_float"] {
assert!(
!env.effect_ops.contains_key(retired),
"{retired} must not appear in builtin registry post-rpe.1; \
"{retired} must not appear in builtin registry post-per-type-print-retirement; \
use polymorphic `print` (Show-based) instead"
);
}
+7 -7
View File
@@ -8,7 +8,7 @@ use ailang_check::{check_workspace, Severity};
use ailang_surface::load_workspace;
use std::path::Path;
/// Iter 18c.2: assert that the linearity check's `suggested_rewrites`
/// assert that the linearity check's `suggested_rewrites`
/// payload is non-empty AND each `replacement` parses as a form-A AILang
/// term. This is the contract the check promises to consumers of
/// `ail check --json`: a machine can take the replacement string and
@@ -123,7 +123,7 @@ fn invalid_def_name_with_dot_is_reported() {
);
}
/// Iter 6: body-check is multi-diagnose. A module with two independent
/// body-check is multi-diagnose. A module with two independent
/// errors in different defs must produce two diagnostics — the first
/// error must not short-circuit the second.
#[test]
@@ -202,7 +202,7 @@ fn body_errors_accumulate_across_defs() {
assert!(defs.contains(&"bad_b"), "missing bad_b; got {defs:?}");
}
/// Iter 18c.2: a fn with all-explicit modes that consumes its
/// a fn with all-explicit modes that consumes its
/// `(own (con List))` parameter twice should trigger
/// `use-after-consume` on the second occurrence and ship a
/// non-empty, well-formed `suggested_rewrites`.
@@ -320,7 +320,7 @@ fn use_after_consume_on_own_param_is_reported() {
assert_suggested_rewrites_well_formed(d);
}
/// Iter 18c.2: a fn whose body passes `xs` to a `Borrow` arg and then,
/// a fn whose body passes `xs` to a `Borrow` arg and then,
/// in a SIBLING arg slot of the same call, consumes it via an `Own`
/// arg, should trigger `consume-while-borrowed`.
///
@@ -424,7 +424,7 @@ fn consume_while_borrowed_in_sibling_arg_is_reported() {
assert_suggested_rewrites_well_formed(d);
}
/// Iter 18d.1: happy-path `(reuse-as xs (term-ctor List Cons ...))`
/// happy-path `(reuse-as xs (term-ctor List Cons ...))`
/// inside an all-explicit-mode `map_inc`-style fn must produce NO
/// diagnostics — neither `reuse-as-non-allocating-body` (body is a
/// Ctor) nor `reuse-as-source-not-bare-var` (source is `xs`) nor
@@ -548,7 +548,7 @@ fn reuse_as_happy_path_in_map_inc_is_linearity_clean() {
);
}
/// Iter 18d.2: a deliberately mismatched reuse-as — `(reuse-as xs
/// a deliberately mismatched reuse-as — `(reuse-as xs
/// (Nil))` inside the Cons arm of a match on `xs` — must surface as
/// a `reuse-as-shape-mismatch` diagnostic. The source's path-ctor
/// (Cons, 2 fields) and the body ctor (Nil, 0 fields) have different
@@ -675,7 +675,7 @@ fn reuse_as_shape_mismatch_is_reported_on_cons_to_nil() {
assert_suggested_rewrites_well_formed(d);
}
/// Iter 18c.2: positive control. The ON-DISK `borrow_own_demo` fixture
/// positive control. The ON-DISK `borrow_own_demo` fixture
/// (the only currently-shipping all-explicit-mode program) must remain
/// linearity-clean. If this regresses, the check has become incorrect:
/// `borrow_own_demo`'s `list_length` (borrow) and `sum_list` (own)
+20 -20
View File
@@ -32,7 +32,7 @@ use super::synth::llvm_type;
use super::{AllocStrategy, Emitter, FnSig, Result};
impl<'a> Emitter<'a> {
/// Iter 18c.4: emit a `void @drop_<module>_<TypeName>(ptr %p)`
/// emit a `void @drop_<module>_<TypeName>(ptr %p)`
/// function that decrements the refcount of every pointer-typed
/// field of every ctor, then frees the outer box.
///
@@ -160,7 +160,7 @@ impl<'a> Emitter<'a> {
self.body.push_str(&out);
}
/// Iter 18e: emit `drop_<m>_<T>` for a `(drop-iterative)` type.
/// emit `drop_<m>_<T>` for a `(drop-iterative)` type.
/// Replaces the recursive cascade in [`Self::emit_drop_fn_for_type`]
/// with an iterative-with-explicit-worklist body so cells of
/// arbitrary chain depth can free without consuming proportional
@@ -328,7 +328,7 @@ impl<'a> Emitter<'a> {
self.body.push_str(&out);
}
/// Iter 18e helper: is `fty` the same ADT as `td_name` in the
/// is `fty` the same ADT as `td_name` in the
/// current module? Used by the iterative-drop body to decide
/// "push to worklist" (same type) vs. "call its drop fn directly"
/// (different type).
@@ -365,7 +365,7 @@ impl<'a> Emitter<'a> {
}
}
/// Iter 18c.4: pick the drop-fn symbol to call for a single
/// pick the drop-fn symbol to call for a single
/// pointer-typed field. Routes ADT fields to their own
/// `drop_<owner>_<T>` symbol so the recursion cascades through
/// recursive types (List, Tree). Falls back to `ailang_rc_dec`
@@ -430,7 +430,7 @@ impl<'a> Emitter<'a> {
}
}
/// Iter 18c.3: predicate the `Term::Let` lowering uses to decide
/// predicate the `Term::Let` lowering uses to decide
/// whether a let-binder owns a fresh RC-heap allocation that
/// codegen should `dec` at scope close.
///
@@ -443,7 +443,7 @@ impl<'a> Emitter<'a> {
/// non-escaping ones become stack `alloca`s and must NOT be
/// `dec`'d (they are freed by LLVM at fn return).
///
/// Iter 18g.2 widens this to include `Term::App` whose callee's
/// The widened input set includes `Term::App` whose callee's
/// fn-type carries `ret_mode == Own`. The mode contract states that
/// the callee hands the returned cell's ownership to the caller's
/// frame; the let-scope close is the right place for the caller's
@@ -464,7 +464,7 @@ impl<'a> Emitter<'a> {
!self.non_escape.contains(&term_ptr)
}
Term::App { callee, .. } => {
// Iter 18g.2: a call whose callee carries
// a call whose callee carries
// `ret_mode == Own` hands a fresh heap allocation to
// the caller's frame. Trackable. `Borrow` and
// `Implicit` ret-modes do not carry that signal —
@@ -479,7 +479,7 @@ impl<'a> Emitter<'a> {
}
}
/// Iter 18g.2: lookup helper for a callee's `ret_mode`. Returns
/// lookup helper for a callee's `ret_mode`. Returns
/// `Some(mode)` when the callee resolves to a fn-typed term;
/// `None` for shapes whose type is not a `Type::Fn` (typechecker
/// would already have rejected an App on a non-fn, but the helper
@@ -494,7 +494,7 @@ impl<'a> Emitter<'a> {
}
}
/// Iter 18c.4: pick the drop-fn symbol to call at the close of a
/// pick the drop-fn symbol to call at the close of a
/// trackable `Term::Let` scope. For a `Term::Ctor` binder the
/// symbol is `drop_<owner>_<TypeName>` — derived from the ctor's
/// `type_name` (which already encodes the owning module via the
@@ -524,7 +524,7 @@ impl<'a> Emitter<'a> {
.get(val_ssa)
.cloned()
.unwrap_or_else(|| "ailang_rc_dec".to_string()),
// Iter 18g.2: an Own-returning call hands a freshly heap-
// an Own-returning call hands a freshly heap-
// allocated cell whose static type is the callee's
// `ret`. Resolve the per-type drop fn from that ret-type
// so the cascade walks the cell's pointer-typed children
@@ -559,7 +559,7 @@ impl<'a> Emitter<'a> {
}
}
/// Iter 18g.tidy.fu2: tag-conditional partial-drop helper.
/// tag-conditional partial-drop helper.
/// Emits `void @partial_drop_<m>_<T>(ptr %p, i64 %mask)` —
/// structurally parallel to [`Self::emit_drop_fn_for_type`] but
/// gates each ptr-field dec on a bit of `%mask`. If bit j of the
@@ -699,7 +699,7 @@ impl<'a> Emitter<'a> {
self.body.push_str(&out);
}
/// Iter 18g.tidy.fu2: resolve the `partial_drop_<owner>_<T>`
/// resolve the `partial_drop_<owner>_<T>`
/// symbol for a type, parallel to the existing per-type drop
/// dispatch in `field_drop_call`. Returns `None` for non-ADT
/// types (Str, fn-typed, type-vars) — callers fall back to
@@ -728,7 +728,7 @@ impl<'a> Emitter<'a> {
}
}
/// Iter 18g.tidy.fu2: build the i64 moved-slots bitmask for a
/// build the i64 moved-slots bitmask for a
/// `partial_drop_<m>_<T>` call. Bit j is set iff slot j is in
/// `moved`. We reject slot indices ≥ 64 with `None` — no
/// language-level ADT has that many fields, but the cap is
@@ -747,7 +747,7 @@ impl<'a> Emitter<'a> {
Some(mask)
}
/// Iter 18d.3: emit a partial-drop sequence inline at a let-close
/// emit a partial-drop sequence inline at a let-close
/// site whose binder has moved-out pattern slots. Replaces the
/// uniform `drop_<m>_<T>(ptr)` call: load each pointer-typed
/// field whose slot index is NOT in `moved`, dispatch through
@@ -757,11 +757,11 @@ impl<'a> Emitter<'a> {
/// transferred to a pattern-bound binder that owns the dec
/// for them.
///
/// Iter 18g.2 widened the input set to `Term::App` (Own-
/// returning call). The static per-field emission below is keyed
/// The widened input set includes `Term::App` (Own-returning
/// call). The static per-field emission below is keyed
/// against a `Term::Ctor`'s known ctor; a `Term::App` binder
/// whose body pattern-matches it has a *dynamic* runtime tag.
/// Iter 18g.tidy.fu2 closes that path: instead of falling back
/// instead of falling back
/// to shallow `ailang_rc_dec` (which leaked the unmoved fields),
/// we route through the tag-conditional helper
/// [`Self::emit_partial_drop_fn_for_type`], which dispatches on
@@ -775,7 +775,7 @@ impl<'a> Emitter<'a> {
let (type_name, ctor_name) = match value {
Term::Ctor { type_name, ctor, .. } => (type_name.as_str(), ctor.as_str()),
_ => {
// Iter 18g.tidy.fu2: dynamic-tag partial-drop via the
// dynamic-tag partial-drop via the
// per-type helper. `value` is `Term::App` (Own-
// returning) — the binder's static type is the App's
// ret type, recovered through `synth_arg_type` /
@@ -836,7 +836,7 @@ impl<'a> Emitter<'a> {
Ok(())
}
/// Iter 18c.4: build the IR text for a closure env's drop fn.
/// build the IR text for a closure env's drop fn.
/// Layout: 8 bytes per capture, in declaration order.
/// For each pointer-typed capture, emit a load + drop call;
/// finally `ailang_rc_dec` the env block.
@@ -878,7 +878,7 @@ impl<'a> Emitter<'a> {
out
}
/// Iter 18c.4: build the IR text for a closure pair's drop fn.
/// build the IR text for a closure pair's drop fn.
/// Layout: { ptr thunk, ptr env } — env at offset 8.
/// Loads env, calls the env drop, then decs the pair box.
pub(crate) fn build_pair_drop_fn(
+7 -7
View File
@@ -174,18 +174,18 @@ fn walk(t: &Term, out: &mut NonEscapeSet) {
walk(body, out);
}
Term::LetRec { body, in_term, .. } => {
// Iter 16b.x: LetRec is normally eliminated before codegen.
// LetRec is normally eliminated before codegen.
// Still walk for completeness.
walk(body, out);
walk(in_term, out);
}
Term::Clone { value } => {
// Iter 18c.1: clone is identity at IR level — only walk
// clone is identity at IR level — only walk
// sub-terms looking for Let-allocation candidates.
walk(value, out);
}
Term::ReuseAs { source, body } => {
// Iter 18d.1: identity at IR level (codegen lowers `body`,
// identity at IR level (codegen lowers `body`,
// ignores `source`). Walk both children for completeness.
walk(source, out);
walk(body, out);
@@ -361,13 +361,13 @@ fn escapes(t: &Term, tainted: &BTreeSet<String>, in_tail: bool) -> bool {
true
}
Term::Clone { value } => {
// Iter 18c.1: clone is identity. The wrapper's escape
// clone is identity. The wrapper's escape
// verdict is exactly that of its inner term, threaded
// through with the same `in_tail` context.
escapes(value, tainted, in_tail)
}
Term::ReuseAs { source, body } => {
// Iter 18d.1: identity at IR level. The whole expression's
// identity at IR level. The whole expression's
// value is `body`, so its escape verdict is the body's
// (threaded through `in_tail`). The `source` is evaluated
// but its value is discarded; it cannot escape via the
@@ -490,11 +490,11 @@ fn collect_free_vars(t: &Term, bound: &mut BTreeSet<String>, out: &mut BTreeSet<
collect_free_vars(in_term, bound, out);
}
Term::Clone { value } => {
// Iter 18c.1: clone is identity for free-var collection.
// clone is identity for free-var collection.
collect_free_vars(value, bound, out);
}
Term::ReuseAs { source, body } => {
// Iter 18d.1: free vars are the union of source and body.
// free vars are the union of source and body.
collect_free_vars(source, bound, out);
collect_free_vars(body, bound, out);
}
+11 -11
View File
@@ -31,7 +31,7 @@ use super::synth::{fn_sig_from_type, llvm_type};
use super::{AllocStrategy, CodegenError, Emitter, FnSig, Result};
impl<'a> Emitter<'a> {
/// Iter 8b: lower a `Term::Lam`. Walks the body to find captures
/// lower a `Term::Lam`. Walks the body to find captures
/// (free vars relative to the enclosing scope, minus builtins and
/// top-level fns), allocates a heap env for the captures, lifts the
/// body to a top-level thunk fn `@ail_<m>_<def>_lam<id>`, and
@@ -122,17 +122,17 @@ impl<'a> Emitter<'a> {
let saved_terminated = self.block_terminated;
self.block_terminated = false;
let saved_sigs = std::mem::take(&mut self.ssa_fn_sigs);
// Iter 17a: a lambda thunk is its own fn frame for escape
// a lambda thunk is its own fn frame for escape
// analysis. Save the outer fn's non-escape set and recompute
// for the lambda body. Restored at the end alongside the rest
// of the saved emitter state.
let saved_non_escape = std::mem::take(&mut self.non_escape);
self.non_escape = escape::analyze_fn_body(lam_body);
// Iter 18d.3: a lambda thunk is its own fn frame for move
// a lambda thunk is its own fn frame for move
// tracking — the outer fn's binders are not in scope inside
// the thunk body (only its captures + params). Save and reset.
let saved_moved = std::mem::take(&mut self.moved_slots);
// Iter mut.3: a lambda thunk is its own fn frame for the
// a lambda thunk is its own fn frame for the
// mut-var bookkeeping introduced in iter mut.3 — the outer
// fn's loop binders are not in scope inside the thunk (a
// lambda body cannot reference enclosing loop binders; the
@@ -150,7 +150,7 @@ impl<'a> Emitter<'a> {
let saved_loop_frames = std::mem::take(&mut self.loop_frames);
let saved_pending_allocas = std::mem::take(&mut self.pending_entry_allocas);
let saved_entry_marker = self.entry_block_end_marker.take();
// Iter 18d.4 fix: a lambda thunk is its own fn frame for
// A lambda thunk is its own fn frame for
// param-mode lookup. The outer fn's params are not in scope
// inside the thunk; the thunk's own params are pushed below
// and (currently) carry no mode annotation, so they default
@@ -269,7 +269,7 @@ impl<'a> Emitter<'a> {
// 3. Emit allocation + capture filling + closure-pair packing
// in the OUTER body. Captures use 8 bytes each; closure-pair
// is 16 bytes.
// Iter 17a: env and closure-pair share the Lam term's escape
// env and closure-pair share the Lam term's escape
// status — they have parallel lifetimes. If the closure pair
// does not escape (only used as a callee in the outer body),
// both can be `alloca`'d. The escape-analysis lookup runs
@@ -334,7 +334,7 @@ impl<'a> Emitter<'a> {
};
self.ssa_fn_sigs.insert(clos.clone(), fs);
// Iter 18c.4: emit per-pair drop fns for heap-allocated
// emit per-pair drop fns for heap-allocated
// closures under `--alloc=rc`. `lam_local` closures live on
// the stack and need no drop fn — LLVM `alloca` reclaims the
// pair on fn return. For heap closures we emit two symbols:
@@ -365,7 +365,7 @@ impl<'a> Emitter<'a> {
Ok((clos, "ptr".into()))
}
/// Iter 8b: walk a Term collecting free-variable names that should
/// walk a Term collecting free-variable names that should
/// be captured by an enclosing lambda. Builtins and top-level fns
/// are excluded — they're globally accessible. Qualified names
/// (`prefix.def`) are excluded too.
@@ -461,16 +461,16 @@ impl<'a> Emitter<'a> {
Self::collect_captures(rhs, bound, captures, captures_set, builtins, top_level);
}
Term::LetRec { .. } => {
// Iter 16b.1: eliminated by desugar before codegen.
// eliminated by desugar before codegen.
unreachable!("Term::LetRec eliminated by desugar")
}
Term::Clone { value } => {
// Iter 18c.1: clone is identity for capture analysis —
// clone is identity for capture analysis —
// free vars of `(clone X)` are exactly the free vars of `X`.
Self::collect_captures(value, bound, captures, captures_set, builtins, top_level);
}
Term::ReuseAs { source, body } => {
// Iter 18d.1: free vars are the union of source and body.
// free vars are the union of source and body.
Self::collect_captures(source, bound, captures, captures_set, builtins, top_level);
Self::collect_captures(body, bound, captures, captures_set, builtins, top_level);
}
File diff suppressed because it is too large Load Diff
+29 -29
View File
@@ -33,7 +33,7 @@ impl<'a> Emitter<'a> {
/// i1 and i8 fields also occupy a full 8-byte slot — the typed
/// load/store instructions write/read only the required size.
///
/// Iter 17a: `term_ptr` is the pointer-as-usize of the lowered
/// `term_ptr` is the pointer-as-usize of the lowered
/// `Term::Ctor` node. If escape analysis flagged this site as
/// non-escaping (i.e., the value cannot live past the current fn
/// frame), allocation lowers to LLVM `alloca` instead of
@@ -52,13 +52,13 @@ impl<'a> Emitter<'a> {
"ctor `{type_name}/{ctor_name}` arity"
)));
}
// Iter 13b: for parameterised ADTs the precomputed `cref.fields`
// for parameterised ADTs the precomputed `cref.fields`
// is meaningless because field types reference rigid type vars.
// Derive a per-use-site substitution from the arg types and
// re-lower each field type. Monomorphic ADTs hit the fast path
// (no var-set, substitution is empty, ail_fields lower exactly
// like cref.fields).
// Iter 15b: for cross-module ctors, qualify any local type-cons
// for cross-module ctors, qualify any local type-cons
// in `cref.ail_fields` (symmetric to the term-ctor synth fix).
let qualified_ail_fields: Vec<Type> = if type_name.matches('.').count() == 1 {
let (prefix, _) = type_name.split_once('.').expect("checked");
@@ -108,7 +108,7 @@ impl<'a> Emitter<'a> {
// design/contracts/frozen-value-layout.md. size = 8 + n*8, tag@0, fields@8+i*8.
let size_bytes = 8 + (compiled.len() * 8) as i64;
let p = self.fresh_ssa();
// Iter 17a: pick allocator based on escape analysis. `alloca`
// pick allocator based on escape analysis. `alloca`
// for non-escaping (stack-allocated, freed on fn return);
// `@GC_malloc` for everything else.
if self.non_escape.contains(&term_ptr) {
@@ -139,7 +139,7 @@ impl<'a> Emitter<'a> {
Ok((p, "ptr".into()))
}
/// Iter 18d.2: lower `Term::ReuseAs { source, body = Term::Ctor }`
/// lower `Term::ReuseAs { source, body = Term::Ctor }`
/// under `--alloc=rc` as a runtime refcount-1 dispatch.
///
/// IR shape (Lean 4 / Roc lineage):
@@ -192,7 +192,7 @@ impl<'a> Emitter<'a> {
"reuse-as source type must be ptr, got {src_ty}"
)));
}
// Iter 18d.3: the source binder name keys into `moved_slots`
// the source binder name keys into `moved_slots`
// so the reuse arm can skip dec'ing fields that earlier
// pattern-extracted out of the source. Linearity already
// ensures `source` is `Term::Var` here; the var-resolution
@@ -323,7 +323,7 @@ impl<'a> Emitter<'a> {
// 5. Reuse arm: overwrite tag and field slots in place.
//
// Iter 18d.3: per-field dec is now safe — moved-out slots
// per-field dec is now safe — moved-out slots
// are skipped via the codegen `moved_slots` side table;
// non-moved slots are dec'd via `field_drop_call`'s null-
// guarded drop fns BEFORE the new field values overwrite
@@ -344,7 +344,7 @@ impl<'a> Emitter<'a> {
// pointer-typed fields are dec'd via `field_drop_call`,
// closing the 18d.2 leak.
self.start_block(&reuse_lbl);
// Iter 18d.3: dec non-moved pointer-typed slots before the
// dec non-moved pointer-typed slots before the
// overwrite. The dec fns are null-guarded, so an empty slot
// is a no-op. Clone the move set out of the side table so
// the loop below can mutably borrow `self` for SSA allocation.
@@ -449,7 +449,7 @@ impl<'a> Emitter<'a> {
"match on non-ADT scrutinee (got {s_ty}); MVP supports only ADTs"
)));
}
// Iter 18d.3: move tracking key — the bare-Var binder name of
// move tracking key — the bare-Var binder name of
// the scrutinee, if it has one. A complex expression scrutinee
// (e.g. the result of `(map_inc xs)` matched directly) has no
// binder we can key off; moves through such matches are
@@ -486,7 +486,7 @@ impl<'a> Emitter<'a> {
open_var = Some(name.clone());
}
Pattern::Ctor { ctor, fields } => {
// Iter 15a: lookup falls back to imported modules when
// lookup falls back to imported modules when
// a bare ctor name doesn't resolve locally.
let cref = self.lookup_ctor_in_pattern(ctor)?;
let bindings: Vec<Option<String>> = fields
@@ -530,7 +530,7 @@ impl<'a> Emitter<'a> {
for (i, (cref, arm, bindings)) in ctor_arms.iter().enumerate() {
self.start_block(&arm_labels[i]);
// Iter 13b: derive substitution for parameterised ADTs from
// derive substitution for parameterised ADTs from
// the scrutinee's concrete type-args. Map `cref.type_vars[i]`
// → `s_ail.args[i]`. For monomorphic ADTs the mapping is
// empty and substitution is a no-op. The substituted AILang
@@ -559,7 +559,7 @@ impl<'a> Emitter<'a> {
}
};
// Load fields and bind as locals.
// Iter 15b: when the scrutinee's ADT lives in another module,
// when the scrutinee's ADT lives in another module,
// `cref.ail_fields[idx]` carries the field type written in
// the owner's local namespace. Qualify it before substituting
// — symmetric to the term-ctor and pat-ctor fixes in
@@ -571,13 +571,13 @@ impl<'a> Emitter<'a> {
_ => None,
};
let mut pushed = 0usize;
// Iter 18d.3: collect the binder names this arm pushes so
// collect the binder names this arm pushes so
// we can drop their `moved_slots` entries when the arm
// body closes (these binders are themselves freshly named
// here — their move tracking starts empty and ends at arm
// body close).
//
// Iter 18d.4: the metadata is widened to (name, SSA,
// the metadata is widened to (name, SSA,
// llvm-type, AILang-type) so that the arm-close drop
// emission can route the call through the binder's
// per-type drop symbol without re-walking `self.locals`.
@@ -608,7 +608,7 @@ impl<'a> Emitter<'a> {
self.body.push_str(&format!(
" {v} = load {fty}, ptr {addr}, align 8\n"
));
// Iter 18d.3: a non-wildcard, pointer-typed slot
// a non-wildcard, pointer-typed slot
// bound here is treated as moved out of the
// scrutinee binder. The source slot is NOT
// mutated; codegen records the move statically so
@@ -632,9 +632,9 @@ impl<'a> Emitter<'a> {
pushed += 1;
}
}
// Iter 18d.4 fix + 18g.1: scrutinee ownership signal,
// hoisted here so both the pre-tail-call shallow-dec
// (Iter 18g.1, below) and the arm-close pattern-binder
// Scrutinee ownership signal, hoisted here so both the
// pre-tail-call shallow-dec (below) and the arm-close
// pattern-binder
// dec (Iter A, further below) consult the same gate.
//
// If the scrutinee resolves to a fn-param whose mode is
@@ -657,7 +657,7 @@ impl<'a> Emitter<'a> {
},
None => true,
};
// Iter 18g.1: pre-tail-call shallow-dec for moved-from
// pre-tail-call shallow-dec for moved-from
// scrutinee outer cell. Iter A (arm-close pattern-binder
// dec, below) cannot fire when `arm.body` is a tail call:
// `lower_term` emits `musttail call ... ret` and sets
@@ -739,7 +739,7 @@ impl<'a> Emitter<'a> {
}
}
let (val, vty) = self.lower_term(&arm.body)?;
// Iter 18d.4: arm-close pattern-binder dec. Symmetric to
// arm-close pattern-binder dec. Symmetric to
// 18c.3/18c.4's `Term::Let`-scope-close drop emission, but
// fired at the lexical close of a match arm. For each
// pattern-bound binder this arm pushed, emit a drop call
@@ -763,11 +763,11 @@ impl<'a> Emitter<'a> {
// through the tail slot. Now the arm itself dec's `t`,
// freeing the 4-cell tail.
//
// Iter 18d.4 fix (regression
// `alloc_rc_pattern_bind_in_implicit_fn_does_not_dec_borrowed_children`):
// Iter A is symmetric to Iter B (Own-param dec at fn
// return) and must share Iter B's mode gate. If the
// scrutinee resolves to a fn-param whose mode is
// Regression
// `alloc_rc_pattern_bind_in_implicit_fn_does_not_dec_borrowed_children`:
// arm-close pattern-binder dec is symmetric to Own-param
// dec at fn return and must share the same mode gate. If
// the scrutinee resolves to a fn-param whose mode is
// `Borrow` or `Implicit`, the caller still holds a
// reference to the heap value the pattern-binders were
// loaded from. Dec'ing those binders fragments the
@@ -823,7 +823,7 @@ impl<'a> Emitter<'a> {
" call void @{drop_call}(ptr {b_ssa})\n"
));
} else {
// Iter 18g.tidy.fu2: pattern-binder with
// pattern-binder with
// statically-recorded moved slots routes
// through the tag-conditional helper
// `partial_drop_<owner>_<T>(b, mask)`. The
@@ -852,13 +852,13 @@ impl<'a> Emitter<'a> {
for _ in 0..pushed {
self.locals.pop();
}
// Iter 18d.3: arm-bound binders go out of scope at arm
// arm-bound binders go out of scope at arm
// body close. Drop their move-tracking entries (any moves
// recorded against `h`/`t` belonged to this arm only).
for (bname, _, _, _) in &arm_pushed_binders {
self.moved_slots.remove(bname);
}
// Iter 14e: if the arm body lowered to a `musttail call` +
// if the arm body lowered to a `musttail call` +
// `ret`, the block is already terminated. Skip the
// fall-through `br` and exclude this arm from the join phi.
if !self.block_terminated {
@@ -912,7 +912,7 @@ impl<'a> Emitter<'a> {
}
// join
// Iter 14e: if every arm tail-called and terminated its own
// if every arm tail-called and terminated its own
// block, no predecessor branches into the join. Mark the whole
// match as block-terminated and emit no join body — the
// surrounding context (top-level fn body, seq rhs, etc.) checks
+6 -6
View File
@@ -15,7 +15,7 @@ use std::collections::{BTreeMap, BTreeSet};
use super::{CodegenError, Result};
/// Iter 12b: derive a name → concrete-type substitution from the
/// derive a name → concrete-type substitution from the
/// declared params of a `Forall` body and the actual arg types at a
/// call site. Walks both sides in parallel; whenever a `Type::Var`
/// (rigid name) appears on the params side, binds it to the
@@ -45,7 +45,7 @@ pub(crate) fn derive_substitution(
// through return-type unification, but at the call site we only
// see args; if needed, callers can extend this with expected-ret
// info.
// Iter 15a: a forall var that the args couldn't pin (e.g.
// a forall var that the args couldn't pin (e.g.
// `is_none(Nothing) : forall a. (Maybe a) -> Bool` — `a` is
// genuinely unobservable from the args alone) defaults to `Unit`.
// The specialised body must not actually read an `a`-typed value,
@@ -71,7 +71,7 @@ pub(crate) fn unify_for_subst(
vars: &BTreeSet<&str>,
subst: &mut BTreeMap<String, Type>,
) -> Result<()> {
// Iter 14a fix, extended in 15g-aux: a `$u`-prefixed var is a
// A `$u`-prefixed var is a
// synth-only wildcard produced by `synth_arg_type` for nullary
// ctors of a parameterised ADT (e.g. `Nil : List<$u>`). It
// carries no real constraint — accept without binding so a
@@ -104,7 +104,7 @@ pub(crate) fn unify_for_subst(
match (param, arg) {
(Type::Var { name }, _) if vars.contains(name.as_str()) => {
if let Some(prev) = subst.get(name).cloned() {
// Iter 15b: the previously-bound type may be more
// the previously-bound type may be more
// concrete than `arg` (e.g. `prev = List<Int>` from a
// sibling binding, `arg = List<$u>` from a synth-
// wildcard nullary ctor). Use recursive unification
@@ -149,7 +149,7 @@ pub(crate) fn unify_for_subst(
}
}
/// Iter 15a: rewrites bare `Type::Con` references that resolve against
/// rewrites bare `Type::Con` references that resolve against
/// `owner_local_types` into qualified `module.Type` form. Mirrors
/// `ailang_check::qualify_local_types`. Used when the codegen pulls a
/// polymorphic fn signature across the import boundary; without this
@@ -204,7 +204,7 @@ pub(crate) fn qualify_local_types_codegen(
}
}
/// Iter 12b: substitute rigid type vars in `t` according to `subst`.
/// substitute rigid type vars in `t` according to `subst`.
/// Used to specialise the type of a polymorphic def for a given
/// instantiation.
pub(crate) fn apply_subst_to_type(t: &Type, subst: &BTreeMap<String, Type>) -> Type {
+8 -8
View File
@@ -28,7 +28,7 @@ pub(crate) fn llvm_type(t: &Type) -> Result<String> {
// at the LLVM level. The actual signature travels via the
// emitter's `ssa_fn_sigs` sidetable.
Type::Fn { .. } => Ok("ptr".into()),
// Iter 13b: an unresolved rigid `Type::Var` reaching codegen is
// an unresolved rigid `Type::Var` reaching codegen is
// a substitution bug. Earlier this silently lowered as `ptr`
// (via the ADT fallback) and produced garbage IR; failing loudly
// here surfaces the bug in the test suite.
@@ -56,11 +56,11 @@ pub(crate) fn fn_sig_from_type(t: &Type) -> Option<FnSig> {
None
}
/// Iter 12b: AILang type of a builtin operator. Used by
/// AILang type of a builtin operator. Used by
/// `synth_arg_type` for arg-type inference at polymorphic call sites.
/// Mirrors what the typechecker installs in its env via `builtins`.
pub(crate) fn builtin_ail_type(name: &str) -> Option<Type> {
// Iter 22-floats.3: same widening as `crates/ailang-check/src/
// same widening as `crates/ailang-check/src/
// builtins.rs` — `+`/`-`/`*`/`/` and `!=`/`<`/`<=`/`>`/`>=` are
// polymorphic. `%` stays monomorphic-Int. Codegen lowering for
// these ops still goes through `builtin_binop` (Int-only) in
@@ -104,7 +104,7 @@ pub(crate) fn builtin_ail_type(name: &str) -> Option<Type> {
"+" | "-" | "*" | "/" => poly_a_a_to_a(),
"%" => int_int_int(),
"!=" | "<" | "<=" | ">" | ">=" => poly_a_a_to_bool(),
// Iter 16e: `==` is polymorphic — `forall a. (a, a) -> Bool`.
// `==` is polymorphic — `forall a. (a, a) -> Bool`.
// The mono pipeline asks `synth_arg_type` for the actual arg
// types at the call site; `lower_app` then dispatches to the
// right LLVM instruction (icmp eq i64 / i1, @strcmp, or
@@ -130,14 +130,14 @@ pub(crate) fn builtin_ail_type(name: &str) -> Option<Type> {
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
// Iter 16d: `__unreachable__` is the polymorphic bottom value
// `__unreachable__` is the polymorphic bottom value
// (`forall a. a`). Mirrors the typechecker's `builtins::install`.
"__unreachable__" => Type::Forall {
vars: vec!["a".into()],
constraints: vec![],
body: Box::new(Type::Var { name: "a".into() }),
},
// Iter 22-floats.3: Float-conversion and inspection builtins.
// Float-conversion and inspection builtins.
// Same lockstep with `crates/ailang-check/src/builtins.rs`.
"neg" => Type::Forall {
vars: vec!["a".into()],
@@ -199,7 +199,7 @@ pub(crate) fn builtin_ail_type(name: &str) -> Option<Type> {
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
// Iter 22-floats.3: Float bit-pattern constants. Bare values,
// Float bit-pattern constants. Bare values,
// not fns — referenced via `Term::Var`. Mirrors the
// typechecker's `builtins::install`. Codegen emits LLVM hex-
// float literals at the use site in iter 4.
@@ -208,7 +208,7 @@ pub(crate) fn builtin_ail_type(name: &str) -> Option<Type> {
})
}
/// Iter 12b: AILang return type of a built-in effect op. The op's
/// AILang return type of a built-in effect op. The op's
/// param signature is irrelevant here since we only consume the ret.
pub(crate) fn builtin_effect_op_ret(op: &str) -> Option<Type> {
Some(match op {
@@ -30,7 +30,7 @@ fn lower_eq_primitives_smoke() -> String {
lower_workspace(&ws).expect("lower")
}
/// Iter 23.2: integration with the auto-loaded prelude. Compiling a
/// integration with the auto-loaded prelude. Compiling a
/// workspace that calls `eq` on an Int pair must produce a
/// mono-synthesised `@ail_prelude_eq__Int` whose body lowers to
/// `icmp eq i64`. Full pipeline: end-of-typecheck → monomorphise →
@@ -48,7 +48,7 @@ fn eq_int_call_produces_icmp_i64_mono_fn() {
);
}
/// Iter 23.2: same shape as `eq_int_call_produces_icmp_i64_mono_fn`
/// same shape as `eq_int_call_produces_icmp_i64_mono_fn`
/// for Bool. Asserts the mono-synthesised `@ail_prelude_eq__Bool`
/// lowers to `icmp eq i1`.
#[test]
@@ -64,7 +64,7 @@ fn eq_bool_call_produces_icmp_i1_mono_fn() {
);
}
/// Iter 23.2: Str's mono-symbol body is hand-rolled by the
/// Str's mono-symbol body is hand-rolled by the
/// `try_emit_primitive_instance_body` intercept. Asserts the intercept
/// fires for the prelude-loaded `eq__Str` synthesised against a real
/// user fixture that calls `eq` on Str.
+1 -1
View File
@@ -204,7 +204,7 @@ pub struct FnDef {
/// Optional source-level documentation string.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub doc: Option<String>,
/// Iter embedding-abi-m1: when `Some(sym)`, this fn is the
/// when `Some(sym)`, this fn is the
/// embedding boundary. Codegen's `Target::StaticLib` mode
/// additionally emits an externally-visible C entrypoint
/// `@<sym>` (signature frozen as of M3 —
+63 -63
View File
@@ -109,7 +109,7 @@
use crate::ast::*;
use std::collections::{BTreeMap, BTreeSet};
/// Iter 16b.2: per-name scope information used by the LetRec lifter.
/// per-name scope information used by the LetRec lifter.
/// `KnownType(t)` is set for fn-params and Lam-params, where the
/// type is statically declared. Other binder kinds use the
/// placeholder variants and the LetRec lifter rejects captures of
@@ -169,7 +169,7 @@ pub fn desugar_module(m: &Module) -> Module {
Def::Type(td) => {
used.insert(td.name.clone());
}
// Iter 22b.1: class/instance defs are passthrough for the
// class/instance defs are passthrough for the
// desugar pass — their bodies (default methods, instance
// method bodies) will be desugared once 22b.2 wires the
// class/instance arms into typecheck. For 22b.1 the names
@@ -183,7 +183,7 @@ pub fn desugar_module(m: &Module) -> Module {
}
}
}
// Iter 16b.1: pre-collect every top-level def name. The LetRec
// pre-collect every top-level def name. The LetRec
// lifter consults this set to (a) know whether a free var of a
// LetRec body resolves to a top-level def (no capture) and
// (b) keep its fresh-name generator from colliding with an
@@ -203,8 +203,8 @@ pub fn desugar_module(m: &Module) -> Module {
for def in &mut out.defs {
match def {
Def::Fn(f) => {
// Iter 16b.2 / 16b.6: build initial scope from fn-params
// with their declared types (peeled out of `f.ty`).
// Build initial scope from fn-params with their
// declared types (peeled out of `f.ty`).
//
// 16b.6: a `Type::Forall { vars, body: Fn(ptys, ...) }`
// enclosing fn is now supported. Its param types may
@@ -245,7 +245,7 @@ pub fn desugar_module(m: &Module) -> Module {
}
}
}
// Iter 16b.6: stash the enclosing fn's Forall.vars (or
// stash the enclosing fn's Forall.vars (or
// empty for a mono enclosing fn) for the LetRec arm.
let saved = std::mem::take(&mut d.current_def_forall_vars);
d.current_def_forall_vars = match &f.ty {
@@ -260,14 +260,14 @@ pub fn desugar_module(m: &Module) -> Module {
c.value = d.desugar_term(&c.value, &scope);
}
Def::Type(_) => {}
// Iter 22b.1: class/instance defs are not desugared yet.
// class/instance defs are not desugared yet.
// Their bodies (default methods, instance method bodies)
// will be processed once 22b.2 lands the class/instance
// arms in typecheck and 22b.3 in codegen.
Def::Class(_) | Def::Instance(_) => {}
}
}
// Iter 16b.1: append every lifted fn to the desugared module.
// append every lifted fn to the desugared module.
// Order: original defs first, then lifts in the order they were
// produced by the bottom-up walk. Typecheck/codegen are order-
// insensitive at the def list level (they index by name), so this
@@ -338,11 +338,11 @@ fn collect_used_in_term(t: &Term, used: &mut BTreeSet<String>) {
collect_used_in_term(in_term, used);
}
Term::Clone { value } => {
// Iter 18c.1: identity for the used-name walk.
// identity for the used-name walk.
collect_used_in_term(value, used);
}
Term::ReuseAs { source, body } => {
// Iter 18d.1: structural recursion through both children.
// structural recursion through both children.
collect_used_in_term(source, used);
collect_used_in_term(body, used);
}
@@ -383,7 +383,7 @@ fn collect_used_in_pattern(p: &Pattern, used: &mut BTreeSet<String>) {
/// contains every source-level identifier (var-bind or var-reference)
/// in the module so that [`fresh`](Self::fresh) cannot shadow one.
///
/// Iter 16b.1 fields:
/// LetRec-lift fields:
/// - `lifted` accumulates synthetic top-level fns produced by
/// [`Term::LetRec`] desugaring. Appended to `Module.defs` once the
/// per-def walk finishes.
@@ -393,7 +393,7 @@ fn collect_used_in_pattern(p: &Pattern, used: &mut BTreeSet<String>) {
/// generator [`fresh_lifted`](Self::fresh_lifted) consults it so
/// later lifts cannot collide with earlier ones.
///
/// Iter 16b.6 field:
/// Forall-tracking field:
/// - `current_def_forall_vars` carries the enclosing fn's
/// `Type::Forall.vars` while desugaring its body. Empty for
/// monomorphic enclosing fns. Read by the LetRec lifter to wrap
@@ -406,7 +406,7 @@ struct Desugarer {
used: BTreeSet<String>,
lifted: Vec<Def>,
module_top_names: BTreeSet<String>,
/// Iter 16b.6: the enclosing fn's Forall.vars (or empty if mono).
/// the enclosing fn's Forall.vars (or empty if mono).
/// Reset on entry to each `Def::Fn`.
current_def_forall_vars: Vec<String>,
}
@@ -427,7 +427,7 @@ impl Desugarer {
}
}
/// Iter 16b.1: returns a name of the form `<hint>$lr_N` not in
/// returns a name of the form `<hint>$lr_N` not in
/// `used` and not in `module_top_names`. Bumps both sets so a
/// later lift cannot collide. The `hint` is the source-level
/// LetRec name; it makes lifted bindings traceable through the
@@ -541,12 +541,12 @@ impl Desugarer {
rhs: Box::new(self.desugar_term(rhs, scope)),
},
Term::Clone { value } => Term::Clone {
// Iter 18c.1: pure structural recursion through the
// pure structural recursion through the
// wrapper. Same pattern as `Term::Let`'s value branch.
value: Box::new(self.desugar_term(value, scope)),
},
Term::ReuseAs { source, body } => Term::ReuseAs {
// Iter 18d.1: pure structural recursion through both
// pure structural recursion through both
// children. Same pattern as `Term::Clone`.
source: Box::new(self.desugar_term(source, scope)),
body: Box::new(self.desugar_term(body, scope)),
@@ -582,18 +582,18 @@ impl Desugarer {
.collect(),
},
Term::LetRec { name, ty, params, body, in_term } => {
// Iter 16b.1: lift to a synthetic top-level fn (no-capture).
// Iter 16b.2: extend the lift to the path-1 safe subset —
// lift to a synthetic top-level fn (no-capture).
// extend the lift to the path-1 safe subset —
// captures of fn-params / Lam-params (whose types are
// known statically).
// Iter 16b.3: when ANY capture is `LetBound` the desugar
// when ANY capture is `LetBound` the desugar
// pass cannot resolve its type (the let-value's type is
// only known after typecheck). For that case we LEAVE
// the LetRec in place, with body and in_term recursively
// desugared. A post-typecheck pass (`lift_letrecs` in
// `ailang-check`) walks every surviving LetRec and lifts
// it using the typechecker's resolved types.
// Iter 16b.4: extends the defer path to `MatchArm`
// extends the defer path to `MatchArm`
// captures. The post-typecheck pass already walks
// `Term::Match` arms and (since 16b.3) calls
// `type_check_pattern_for_lift` to extend locals with
@@ -672,7 +672,7 @@ impl Desugarer {
}
let in_has_value_use = find_non_callee_use(&desugared_in, name).is_some();
// Iter 16b.6: reject name-as-value in `in_term` when the
// reject name-as-value in `in_term` when the
// enclosing fn is polymorphic. The 16b.5 wrap synthesises
// a `Term::Lam` whose AST has no `Forall` slot, so
// wrapping a polymorphic lifted fn into a monomorphic Lam
@@ -773,7 +773,7 @@ impl Desugarer {
// Build augmented type: original Fn with capture types
// appended to `params`.
//
// Iter 16b.6: when the ENCLOSING fn is polymorphic
// when the ENCLOSING fn is polymorphic
// (`current_def_forall_vars` non-empty), wrap the
// augmented Fn in a `Type::Forall` mirroring the
// enclosing fn's type vars. The capture types may
@@ -933,7 +933,7 @@ impl Desugarer {
}
let s = self.fresh();
let s_var = Term::Var { name: s.clone() };
// Iter 16d: `default` is unreachable for valid programs (the
// `default` is unreachable for valid programs (the
// typechecker requires either a catch-all arm or exhaustive
// ctor coverage). Use the polymorphic bottom builtin
// `__unreachable__` (`forall a. a`) so the terminator unifies
@@ -975,7 +975,7 @@ impl Desugarer {
body: Box::new(arm.body.clone()),
},
Pattern::Lit { lit } => {
// Iter 16c: a top-level lit arm desugars to `if (== s_var lit)
// a top-level lit arm desugars to `if (== s_var lit)
// then arm.body else fall_k`. Eliminates `Pattern::Lit` from
// the desugared output entirely; codegen never sees it.
let cmp = build_eq(s_var.clone(), lit);
@@ -1035,7 +1035,7 @@ impl Desugarer {
body: Box::new(body),
},
Pattern::Lit { lit } => {
// Iter 16c: a lit sub-pattern desugars to `if (== fv lit) body
// a lit sub-pattern desugars to `if (== fv lit) body
// else fall_k`. Same shape as the top-level case, but on the
// field-bound fresh variable rather than the original
// scrutinee.
@@ -1070,7 +1070,7 @@ impl Desugarer {
/// by [`Desugarer::desugar_match`] to skip the let-binding and chain
/// construction for already-flat matches.
///
/// Iter 16c: [`Pattern::Lit`] is **not** flat — it always desugars to a
/// [`Pattern::Lit`] is **not** flat — it always desugars to a
/// [`Term::If`] via [`build_eq`], so it must take the chain path even
/// when no other arm needs flattening. Otherwise the early-return in
/// [`Desugarer::desugar_match`] would leak a `Pattern::Lit` arm to
@@ -1078,7 +1078,7 @@ impl Desugarer {
fn is_flat(p: &Pattern) -> bool {
match p {
Pattern::Wild | Pattern::Var { .. } => true,
// Iter 16c: lit patterns are not flat; they desugar to If.
// lit patterns are not flat; they desugar to If.
Pattern::Lit { .. } => false,
Pattern::Ctor { fields, .. } => fields
.iter()
@@ -1086,7 +1086,7 @@ fn is_flat(p: &Pattern) -> bool {
}
}
/// Iter 16c: Build an equality-test term for a lit pattern. The shape is
/// Build an equality-test term for a lit pattern. The shape is
/// `(app == scrutinee lit_term)` for Int/Bool/Str. Unit literals are
/// degenerate — every Unit value is equal — so an emitted `true` literal
/// stands in.
@@ -1114,7 +1114,7 @@ fn build_eq(scrutinee: Term, lit: &Literal) -> Term {
}
}
/// Iter 16b.1: collect free variable names of `t`, with `bound` being
/// collect free variable names of `t`, with `bound` being
/// the set of names lexically bound at this point. A
/// [`Term::Var`] `{ name }` is "free" iff `name` is not in `bound`.
/// Compound binders ([`Term::Let`], [`Term::Lam`], [`Term::Match`]
@@ -1123,7 +1123,7 @@ fn build_eq(scrutinee: Term, lit: &Literal) -> Term {
/// Used by the [`Term::LetRec`] desugar to decide whether a local
/// recursive let would capture any name from the enclosing scope.
///
/// Iter 16b.3: made `pub` so the post-typecheck `lift_letrecs` pass
/// made `pub` so the post-typecheck `lift_letrecs` pass
/// in `ailang-check` can reuse it (same free-var computation; the
/// post-typecheck pass needs it to recompute captures of LetRec
/// nodes that desugar deferred).
@@ -1195,11 +1195,11 @@ pub fn free_vars_in_term(t: &Term, bound: &BTreeSet<String>, out: &mut BTreeSet<
free_vars_in_term(in_term, &b_in, out);
}
Term::Clone { value } => {
// Iter 18c.1: `(clone X)` has the same free vars as `X`.
// `(clone X)` has the same free vars as `X`.
free_vars_in_term(value, bound, out);
}
Term::ReuseAs { source, body } => {
// Iter 18d.1: free vars are the union of source and body.
// free vars are the union of source and body.
free_vars_in_term(source, bound, out);
free_vars_in_term(body, bound, out);
}
@@ -1222,11 +1222,11 @@ pub fn free_vars_in_term(t: &Term, bound: &BTreeSet<String>, out: &mut BTreeSet<
}
}
/// Iter 16b.1: collect every name a pattern binds into `out`. Mirrors
/// collect every name a pattern binds into `out`. Mirrors
/// `Pattern::pattern_bound_names` in `ailang-codegen`; duplicated here
/// because `ailang-core` cannot depend on the codegen crate.
///
/// Iter 16b.3: made `pub` so callers (e.g. `ailang-check::lift_letrecs`)
/// made `pub` so callers (e.g. `ailang-check::lift_letrecs`)
/// can reproduce the same shadowing semantics during their own
/// scope-aware walks.
pub fn pattern_binds(p: &Pattern, out: &mut BTreeSet<String>) {
@@ -1243,7 +1243,7 @@ pub fn pattern_binds(p: &Pattern, out: &mut BTreeSet<String>) {
}
}
/// Iter 16b.1: rewrite every free [`Term::Var`] `{ name == from }`
/// rewrite every free [`Term::Var`] `{ name == from }`
/// reference in `t` to [`Term::Var`] `{ name = to }`. Respects
/// shadowing: if a binder rebinds `from`, occurrences inside that
/// binder's scope stop being free and are left alone.
@@ -1252,7 +1252,7 @@ pub fn pattern_binds(p: &Pattern, out: &mut BTreeSet<String>) {
/// fn — every recursive self-call site in the lifted body and every
/// reference in the in-term needs to point at the new global name.
///
/// Iter 16b.3: made `pub` for the same reason as
/// made `pub` for the same reason as
/// [`subst_call_with_extras`].
pub fn subst_var(t: &Term, from: &str, to: &str) -> Term {
match t {
@@ -1355,11 +1355,11 @@ pub fn subst_var(t: &Term, from: &str, to: &str) -> Term {
}
}
Term::Clone { value } => Term::Clone {
// Iter 18c.1: structural recursion through the wrapper.
// structural recursion through the wrapper.
value: Box::new(subst_var(value, from, to)),
},
Term::ReuseAs { source, body } => Term::ReuseAs {
// Iter 18d.1: structural recursion through both children.
// structural recursion through both children.
source: Box::new(subst_var(source, from, to)),
body: Box::new(subst_var(body, from, to)),
},
@@ -1402,7 +1402,7 @@ pub fn subst_var(t: &Term, from: &str, to: &str) -> Term {
}
}
/// Iter 16b.2: peel a `Type::Forall { body: Type::Fn { ... } }` to
/// peel a `Type::Forall { body: Type::Fn { ... } }` to
/// expose the inner `Type::Fn`. Returns `Some(&Type::Fn)` if the
/// type is `Fn` or `Forall<Fn>`; `None` otherwise.
fn peel_forall_to_fn(t: &Type) -> Option<&Type> {
@@ -1413,13 +1413,13 @@ fn peel_forall_to_fn(t: &Type) -> Option<&Type> {
}
}
/// Iter 16b.2: walk `t` and rewrite every `Term::App { callee =
/// walk `t` and rewrite every `Term::App { callee =
/// Var{name}, args }` to `Term::App { callee = Var{lifted}, args =
/// args ++ extras_as_vars }`. Recurses into all sub-terms. Does
/// not touch `Term::Var { name }` in non-callee positions — that's
/// flagged separately by [`find_non_callee_use`].
///
/// Iter 16b.3: made `pub` so the post-typecheck `lift_letrecs` pass
/// made `pub` so the post-typecheck `lift_letrecs` pass
/// in `ailang-check` can reuse it (same call-site rewrite shape; the
/// only difference is that the capture types come from the
/// typechecker's env rather than being statically known).
@@ -1512,11 +1512,11 @@ pub fn subst_call_with_extras(t: &Term, name: &str, lifted: &str, extras: &[Stri
in_term: Box::new(subst_call_with_extras(in_term, name, lifted, extras)),
},
Term::Clone { value } => Term::Clone {
// Iter 18c.1: structural recursion through the wrapper.
// structural recursion through the wrapper.
value: Box::new(subst_call_with_extras(value, name, lifted, extras)),
},
Term::ReuseAs { source, body } => Term::ReuseAs {
// Iter 18d.1: structural recursion through both children.
// structural recursion through both children.
source: Box::new(subst_call_with_extras(source, name, lifted, extras)),
body: Box::new(subst_call_with_extras(body, name, lifted, extras)),
},
@@ -1540,13 +1540,13 @@ pub fn subst_call_with_extras(t: &Term, name: &str, lifted: &str, extras: &[Stri
}
}
/// Iter 16b.2: returns `Some(t)` if `t` contains a `Term::Var {
/// returns `Some(t)` if `t` contains a `Term::Var {
/// name }` reference in a position that is **not** the callee of a
/// `Term::App`. Returns `None` if every reference is in callee
/// position. Used by the LetRec lifter to enforce the "direct-call
/// only" restriction in 16b.2.
///
/// Iter 16b.3: made `pub` for the same reason as
/// made `pub` for the same reason as
/// [`subst_call_with_extras`].
pub fn find_non_callee_use(t: &Term, name: &str) -> Option<Term> {
match t {
@@ -1585,9 +1585,9 @@ pub fn find_non_callee_use(t: &Term, name: &str) -> Option<Term> {
}
Term::LetRec { body, in_term, .. } => find_non_callee_use(body, name)
.or_else(|| find_non_callee_use(in_term, name)),
// Iter 18c.1: clone is identity for the non-callee scan.
// clone is identity for the non-callee scan.
Term::Clone { value } => find_non_callee_use(value, name),
// Iter 18d.1: scan both source and body.
// scan both source and body.
Term::ReuseAs { source, body } => {
find_non_callee_use(source, name).or_else(|| find_non_callee_use(body, name))
}
@@ -1676,7 +1676,7 @@ mod tests {
}
}
/// Iter 16a: a `match` containing `(Cons a (Cons b _))` desugars to
/// a `match` containing `(Cons a (Cons b _))` desugars to
/// a tree with no nested ctor sub-patterns. Property: the rewriter's
/// output never has a `Pattern::Ctor` whose field is itself a
/// `Pattern::Ctor`.
@@ -1804,7 +1804,7 @@ mod tests {
assert!(matches!(body, Term::Match { .. }));
}
/// Iter 16b.1: a [`Term::LetRec`] whose body has no captures from
/// a [`Term::LetRec`] whose body has no captures from
/// the enclosing scope is lifted to a synthetic top-level fn. The
/// resulting module's `defs` grows by one and the original LetRec
/// is gone from the term tree.
@@ -1886,7 +1886,7 @@ mod tests {
}
}
/// Iter 16b.2: a LetRec whose body captures a fn-param of the
/// a LetRec whose body captures a fn-param of the
/// enclosing scope (here, `outer` is a parameter of `main`) is
/// **lifted** with the capture appended to the lifted fn's
/// signature. The original 16b.1 panic ("would capture") is
@@ -2009,7 +2009,7 @@ mod tests {
}
}
/// Iter 16b.3: a LetRec whose body captures a `Term::Let`-bound
/// a LetRec whose body captures a `Term::Let`-bound
/// name is no longer rejected at desugar time. Instead the
/// desugar pass leaves the LetRec in place (with body and
/// in_term recursively desugared) so the post-typecheck pass in
@@ -2088,7 +2088,7 @@ mod tests {
);
}
/// Iter 16b.4: a LetRec whose body captures a name bound by a
/// a LetRec whose body captures a name bound by a
/// `Pattern::Ctor` Var sub-pattern (a `ScopeEntry::MatchArm`
/// binding) is no longer rejected at desugar time. The desugar
/// pass leaves the LetRec in place (with body and in_term
@@ -2205,7 +2205,7 @@ mod tests {
);
}
/// Iter 16b.5: name-as-value INSIDE the LetRec's own body is
/// name-as-value INSIDE the LetRec's own body is
/// still rejected (would require an eta-Lam that calls the
/// pre-lift name — chicken-and-egg with the call-site rewrite).
/// Here `(let g f ...)` binds the LetRec name `f` to a let-bound
@@ -2264,7 +2264,7 @@ mod tests {
let _ = desugar_module(&m);
}
/// Iter 16b.5: name-as-value in the in-clause is now SUPPORTED.
/// name-as-value in the in-clause is now SUPPORTED.
/// The desugar pass lifts the LetRec to a synthetic top-level fn
/// AND wraps the rewritten in-term in `(let f (lam ...) ...)`
/// whose lam eta-expands the lifted fn. After desugar, the
@@ -2369,7 +2369,7 @@ mod tests {
}
}
/// Iter 16b.6: a LetRec inside a polymorphic enclosing fn gets its
/// a LetRec inside a polymorphic enclosing fn gets its
/// fn-param captures lifted with a `Forall(vars, Fn(...))` augmented
/// signature, mirroring the enclosing fn's type vars. Without 16b.6,
/// the desugar pass would have classified the fn-params as
@@ -2510,7 +2510,7 @@ mod tests {
assert_eq!(lifted.params, vec!["k".to_string(), "x".to_string()]);
}
/// Iter 16b.6: a LetRec inside a polymorphic enclosing fn whose
/// a LetRec inside a polymorphic enclosing fn whose
/// `in_term` uses the LetRec name as a value (not as a callee) is
/// rejected. The 16b.5 eta-Lam wrap can't quantify `Forall.vars`
/// because `Term::Lam` has no Forall slot — wrapping a polymorphic
@@ -2569,7 +2569,7 @@ mod tests {
let _ = desugar_module(&m);
}
/// Iter 16b.7: a nested LetRec whose inner LetRec captures the
/// a nested LetRec whose inner LetRec captures the
/// OUTER LetRec's NAME (not its params or any other local) is
/// rejected at desugar time with the closure-of-self message.
/// Capturing the outer's PARAMS is supported (covered by the
@@ -2651,7 +2651,7 @@ mod tests {
let _ = desugar_module(&m);
}
/// Iter 16b.7 (positive companion): a nested LetRec whose inner
/// a nested LetRec whose inner
/// LetRec captures the OUTER LetRec's PARAM (not its name) lifts
/// cleanly through the existing 16b.2 fast path. The outer's
/// params live in the inner's outer-scope as `KnownType`, so the
@@ -2757,7 +2757,7 @@ mod tests {
}
}
/// Iter 16c: walks a term, returns true iff any [`Pattern::Lit`] is
/// walks a term, returns true iff any [`Pattern::Lit`] is
/// reachable in any [`Term::Match`] arm. After 16c desugaring, the
/// invariant is `false` for every desugared output — `Pattern::Lit`
/// is gone before typecheck/codegen runs.
@@ -2808,7 +2808,7 @@ mod tests {
}
}
/// Iter 16c: a top-level lit arm desugars to a `Term::If` whose
/// a top-level lit arm desugars to a `Term::If` whose
/// condition is `(== s_var lit)`. The desugared body must contain
/// no `Pattern::Lit` anywhere.
#[test]
@@ -2876,7 +2876,7 @@ mod tests {
);
}
/// Iter 16c: a `(pat-ctor Cons (pat-lit 0) _)` sub-pattern desugars
/// a `(pat-ctor Cons (pat-lit 0) _)` sub-pattern desugars
/// to a tree with no `Pattern::Lit` anywhere — the lit is rewritten
/// to a `Term::If` against the field-bound fresh variable.
#[test]
@@ -2944,7 +2944,7 @@ mod tests {
);
}
/// Iter 16c regression guard: `is_flat` must classify
/// `is_flat` must classify
/// `Pattern::Lit` as **not** flat, so the chain machinery in
/// [`Desugarer::desugar_match`] is always invoked for lit-arms
/// (rather than the early-return that would leak the lit pattern
@@ -2960,7 +2960,7 @@ mod tests {
);
}
/// Iter 16d: the chain machinery's deepest fall-through is the
/// the chain machinery's deepest fall-through is the
/// polymorphic `__unreachable__` builtin (`forall a. a`), not a
/// `Unit` literal. A match with two lit arms and a wildcard
/// catches all paths via the wild-arm body, so the deepest
+1 -1
View File
@@ -63,7 +63,7 @@ pub fn manifest(m: &Module) -> String {
};
("type", body)
}
// Iter 22b.1: one-line summary `class C a` / `instance C T`.
// one-line summary `class C a` / `instance C T`.
// Full pretty rendering of method signatures and bodies
// lands in 22b.4 alongside the prose projection arms.
Def::Class(c) => ("class", format!("{} {}", c.name, c.param)),
+119 -117
View File
@@ -40,7 +40,7 @@ use std::path::{Path, PathBuf};
/// by `Module.name`. `root_dir` is the directory the entry file lives
/// in; all imports are resolved relative to it.
///
/// Iter 22b.1 (Decision 11): `registry` is the workspace-global
/// `registry` is the workspace-global
/// instance registry, built at the end of [`load_workspace`] after the
/// DFS over imports completes. It is empty for any workspace whose
/// modules contain no [`crate::ast::Def::Instance`] defs.
@@ -55,11 +55,11 @@ pub struct Workspace {
/// Directory the entry file lives in; all imports are resolved
/// relative to it.
pub root_dir: PathBuf,
/// Iter 22b.1: workspace-global typeclass instance registry.
/// workspace-global typeclass instance registry.
pub registry: Registry,
}
/// Iter 22b.1: workspace-global instance registry (Decision 11).
/// workspace-global instance registry (the typeclass design).
///
/// Built at the end of [`load_workspace`] after all modules are
/// loaded. Keyed by `(class-name, canonical-type-hash)`; values are
@@ -71,7 +71,7 @@ pub struct Workspace {
/// 22b.1 enforces three coherence checks during build:
///
/// 1. **Coherence (orphan-freedom).** Every `instance C T` lives in
/// the module of `C` or in the module of `T` (per Decision 11
/// the module of `C` or in the module of `T` (per the typeclass design
/// axis 3). Otherwise → [`WorkspaceLoadError::OrphanInstance`].
/// 2. **Uniqueness.** No two entries share a key. Otherwise →
/// [`WorkspaceLoadError::DuplicateInstance`].
@@ -82,7 +82,7 @@ pub struct Workspace {
pub struct Registry {
/// Map from `(class-name, type-hash)` to the registry entry.
pub entries: BTreeMap<(String, String), RegistryEntry>,
/// ct.1.5a: workspace-wide map from user-defined type-name to its
/// workspace-wide map from user-defined type-name to its
/// defining module. Used by [`Self::normalize_type_for_lookup`] to
/// rewrite a bare `Type::Con.name` to its always-qualified form
/// before computing the registry key. Primitives are not present.
@@ -104,7 +104,7 @@ pub struct Registry {
}
impl Registry {
/// ct.1.5a + ctt.2: produce the canonical form of `t` for
/// the canonical-form normalisation step: produce the canonical form of `t` for
/// registry-key hashing. Bare-non-primitive `Type::Con` names
/// get qualified to `<defining_module>.<name>`; already-qualified
/// names stay; bare names whose defining module is unknown stay
@@ -201,10 +201,11 @@ pub enum WorkspaceLoadError {
)]
ModuleHashMismatch { name: String },
/// Iter 22b.1: an [`crate::ast::Def::Instance`] was declared in a
/// an [`crate::ast::Def::Instance`] was declared in a
/// module that is neither the class's defining module nor the
/// instance type's defining module. Coherence violation per
/// Decision 11 axis 3 ("orphan-freedom"). The lookup is hard:
/// the orphan-freedom axis of the typeclass design. The lookup
/// is hard:
/// AILang does not provide a `--allow-orphans` flag.
#[error(
"orphan instance: `instance {class} {type_repr}` declared in module `{defining_module}`, \
@@ -218,10 +219,10 @@ pub enum WorkspaceLoadError {
type_module: String,
},
/// Iter 22b.1: two [`crate::ast::Def::Instance`]s share the same
/// two [`crate::ast::Def::Instance`]s share the same
/// `(class, canonical-type-hash)` key. Coherence requires
/// uniqueness; the registry has no way to disambiguate at
/// resolution time. Per Decision 11 there is no
/// resolution time. Per the typeclass design there is no
/// `AmbiguousInstance` diagnostic — coherence makes the lookup
/// unambiguous by construction, and a duplicate is a workspace
/// configuration error, not a per-call-site one.
@@ -235,11 +236,11 @@ pub enum WorkspaceLoadError {
second_module: String,
},
/// Iter 22b.1: an [`crate::ast::Def::Instance`] does not specify
/// an [`crate::ast::Def::Instance`] does not specify
/// a body for a required (non-default) method of its class.
/// Default-bearing methods may be inherited; non-default
/// (abstract-required) methods must be specified by every
/// instance. Per Decision 11 §"Defaults and superclasses".
/// instance. Per the typeclass design §"Defaults and superclasses".
#[error(
"instance `{class} {type_repr}` is missing a body for method `{method}` (no default)"
)]
@@ -249,8 +250,8 @@ pub enum WorkspaceLoadError {
method: String,
},
/// Iter 22b.2: class-schema validation. A class's `superclass.type`
/// does not equal its own `param`. Decision 11 single-superclass
/// class-schema validation. A class's `superclass.type`
/// does not equal its own `param`. the typeclass design's single-superclass
/// model requires the superclass to be applied to the same param
/// (e.g. `class Ord a extends Eq a`, not `extends Eq b`).
#[error(
@@ -264,7 +265,7 @@ pub enum WorkspaceLoadError {
got_type: String,
},
/// Iter 22b.2: class-schema validation. A class method's
/// class-schema validation. A class method's
/// signature contains a constraint referencing a type variable
/// that is neither bound by the method's `Forall.vars` nor equal
/// to the class's `param`.
@@ -278,7 +279,7 @@ pub enum WorkspaceLoadError {
var: String,
},
/// Iter 22b.2: an instance specifies a body for a method name
/// an instance specifies a body for a method name
/// that the corresponding class does not declare. Symmetric to
/// `MissingMethod` but in the opposite direction.
#[error(
@@ -290,9 +291,9 @@ pub enum WorkspaceLoadError {
method: String,
},
/// Iter 22b.2: an instance `C T` was declared, but `C`'s
/// an instance `C T` was declared, but `C`'s
/// superclass `S` does not have an instance for the same type
/// `T`. Decision 11 single-superclass model requires `instance S
/// `T`. the typeclass design's single-superclass model requires `instance S
/// T` to exist whenever `instance C T` exists.
#[error(
"instance `{class} {type_repr}` requires superclass instance `{superclass} {type_repr}`, but none was found"
@@ -303,14 +304,14 @@ pub enum WorkspaceLoadError {
type_repr: String,
},
/// Iter 23.1: a user module attempted to use the reserved
/// a user module attempted to use the reserved
/// module name `prelude`. The prelude is auto-injected by
/// the loader, so a user module of the same name would
/// collide silently. Reject explicitly.
#[error("module name {name:?} is reserved (auto-injected by the loader)")]
ReservedModuleName { name: String },
/// ct.1 (canonical-type-names): a `Type::Con` whose `name` is
/// the canonical-form rule for type references: a `Type::Con` whose `name` is
/// neither a primitive nor a local TypeDef of the owning module
/// was encountered. Under the canonical-form rule, bare =
/// local; a bare cross-module ref is a schema violation.
@@ -327,7 +328,7 @@ pub enum WorkspaceLoadError {
candidates: Vec<String>,
},
/// ct.1: a qualified `Type::Con` of the form `<owner>.<type>`
/// a qualified `Type::Con` of the form `<owner>.<type>`
/// was encountered, but `<owner>` is not a known module in the
/// workspace, or `<owner>` is known but declares no TypeDef
/// named `<type>`.
@@ -340,11 +341,11 @@ pub enum WorkspaceLoadError {
name: String,
},
/// mq.1 (canonical-class-names): a class-reference field
/// the canonical-form rule for class references: a class-reference field
/// (`InstanceDef.class`, `Constraint.class`, or
/// `SuperclassRef.class`) carries a bare name that does not resolve
/// to a local class of the owning module. Under the canonical-form
/// rule extended in mq.1, bare = local-class-of-owning-module; a
/// rule extended for class references, bare = local-class-of-owning-module; a
/// bare cross-module class reference is a schema violation.
/// `candidates` lists the qualified forms found by scanning the
/// owning module's imports for matching class declarations.
@@ -359,7 +360,7 @@ pub enum WorkspaceLoadError {
candidates: Vec<String>,
},
/// mq.1 (canonical-class-names): a qualified class reference of
/// the canonical-form rule for class references: a qualified class reference of
/// the form `<owner>.<class>` was encountered, but `<owner>` is
/// not a known module in the workspace, or `<owner>` is known
/// but declares no class by that name. Sibling of
@@ -373,14 +374,14 @@ pub enum WorkspaceLoadError {
name: String,
},
/// ct.1: a class-reference field (`InstanceDef.class`,
/// a class-reference field (`InstanceDef.class`,
/// `SuperclassRef.class`, `Constraint.class`, or
/// `ClassDef.name`) contains a `.` — under this milestone class
/// names are NOT module-qualified (see DESIGN spec §"Out of
/// scope: Class names"). The schema rejects qualified forms so
/// half-migrated files cannot silently load.
///
/// mq.1: narrowed to `ClassDef.name` only; the three other
/// narrowed to `ClassDef.name` only; the three other
/// fields now follow the canonical-form rule and use
/// `BareCrossModuleClassRef` / `BadCrossModuleClassRef`.
#[error(
@@ -497,10 +498,10 @@ pub fn build_workspace(
// `ailang_surface::PRELUDE_AIL` + `ailang_surface::parse_prelude`); core
// exposes the two-phase composition.
/// mq.1: take a class-ref field value (`InstanceDef.class`,
/// take a class-ref field value (`InstanceDef.class`,
/// `SuperclassRef.class`, `Constraint.class`) and a defining context,
/// and produce the qualified workspace key. Bare ⇒ prepend the
/// `caller_module` argument; qualified ⇒ as-is. Symmetric to ct.1's
/// `caller_module` argument; qualified ⇒ as-is. Symmetric to the canonical-form rule's
/// `normalize_type_for_registry` for `Type::Con`.
///
/// For an `InstanceDef.class` or `Constraint.class` field the caller
@@ -514,7 +515,7 @@ fn qualify_class_ref(class_ref: &str, caller_module: &str) -> String {
}
}
/// Iter 22b.1: build the workspace-global typeclass instance registry.
/// build the workspace-global typeclass instance registry.
///
/// Two passes:
///
@@ -534,7 +535,7 @@ fn build_registry(
// Pass 1: collect "where is X defined" maps, plus a class lookup
// by name (needed for the method-completeness check).
//
// mq.1: `class_def_module` and `class_by_name` are keyed by the
// `class_def_module` and `class_by_name` are keyed by the
// qualified class name `<defining_module>.<class>`. All consumers
// (Pass-2 coherence lookup, method-completeness check, superclass
// walk, etc.) query with `qualify_class_ref` applied to the field
@@ -562,7 +563,7 @@ fn build_registry(
}
}
// mq.3: the `MethodNameCollision` pre-pass (variant + Origin enum +
// the `MethodNameCollision` pre-pass (variant + Origin enum +
// per-def loop) was retired here. Bare-method resolution no longer
// requires workspace-wide method-name uniqueness — synth's
// `Term::Var` arm consults `Env.method_to_candidate_classes`
@@ -588,7 +589,7 @@ fn build_registry(
// user-defined module — instances on primitives
// therefore must live in the class's module.
//
// mq.1: lookup keys are qualified
// lookup keys are qualified
// (`<defining_module>.<class>`); the `inst.class`
// field is the canonical-form value (bare or qualified)
// which `qualify_class_ref` lifts to the workspace key.
@@ -597,7 +598,7 @@ fn build_registry(
.get(&inst_class_key)
.cloned()
.unwrap_or_else(|| "<unknown-class>".into());
// mq.1: type-leg lookup must accept the canonical form
// type-leg lookup must accept the canonical form
// for the head name. Bare head ⇒ key by
// (caller_module, head); qualified `<owner>.<bare>` ⇒
// key by (owner, bare) directly (the owner IS the
@@ -625,14 +626,14 @@ fn build_registry(
}
// Uniqueness: a `(class, type-hash)` key must appear
// at most once across the whole workspace. ct.1.5a: the
// at most once across the whole workspace. The
// type expression is normalised to its always-qualified
// form before hashing so a bare-local declaration in
// the type's defining module and a qualified-cross-module
// declaration from elsewhere produce the same key (both
// refer to the same type under the canonical-form rule).
//
// mq.1: registry key is keyed by the qualified class
// registry key is keyed by the qualified class
// form too, so a bare same-module instance and a
// qualified cross-module instance on the same
// (class, type) collide on this check.
@@ -677,7 +678,7 @@ fn build_registry(
// Symmetric to MissingMethod: an instance must
// not specify a body for a method name the class
// never declared. Decision 11 forbids ad-hoc
// never declared. The typeclass design forbids ad-hoc
// additions to a class's method set at the
// instance site.
let declared: BTreeSet<&str> =
@@ -704,11 +705,11 @@ fn build_registry(
}
}
// Iter 22b.2: superclass-instance completeness. For every entry,
// superclass-instance completeness. For every entry,
// walk the class's superclass chain and require an entry for each
// step at the same type-hash.
//
// mq.1: `class_name` (the entries key first half) is the qualified
// `class_name` (the entries key first half) is the qualified
// class name; the superclass-ref field `sc.class` is canonical-form,
// which `qualify_class_ref` lifts to the qualified key — using the
// parent class's defining module as the caller context (the
@@ -716,7 +717,7 @@ fn build_registry(
for (key, entry) in entries.iter() {
let (class_name, type_hash) = key;
let type_repr = type_head_name(&entry.instance.type_);
// Iter 22b.2 leaves superclass-cycle detection to a future arm;
// Superclass-cycle detection is left to a future arm;
// here we just terminate the walk.
let mut visited: BTreeSet<&str> = BTreeSet::new();
let mut current = class_by_name.get(class_name.as_str()).copied();
@@ -738,7 +739,7 @@ fn build_registry(
type_repr: type_repr.clone(),
});
}
// mq.1: walk lookup uses the qualified key; the next
// walk lookup uses the qualified key; the next
// step's owning-module context is the superclass's
// defining module (read from `class_def_module`).
current = class_by_name.get(sc_class_key.as_str()).copied();
@@ -758,7 +759,7 @@ fn build_registry(
})
}
/// Iter 22b.2: class-schema validation. Runs before `build_registry`.
/// class-schema validation. Runs before `build_registry`.
/// Two diagnostics fire from here: `invalid-superclass-param`,
/// `constraint-references-unbound-type-var`. The `kind-mismatch`
/// diagnostic was retired at ctt.3 — the malformed shape now
@@ -805,7 +806,7 @@ fn validate_classdefs(
Ok(())
}
/// ct.1: the five primitive type names that are always bare under
/// the five primitive type names that are always bare under
/// the canonical-form rule. Kept in sync with `Type::int`,
/// `Type::bool_`, `Type::str_`, `Type::unit`, `Type::float` in
/// `crate::ast`.
@@ -813,7 +814,7 @@ fn is_primitive_type_name(name: &str) -> bool {
matches!(name, "Int" | "Bool" | "Str" | "Unit" | "Float")
}
/// ct.1.5a: normalize a `Type` by qualifying every bare-non-primitive
/// normalize a `Type` by qualifying every bare-non-primitive
/// `Type::Con.name` to its always-qualified form
/// (`<defining_module>.<name>`). Primitives stay bare; already-qualified
/// names stay as-is; bare names whose defining module is unknown stay
@@ -891,7 +892,7 @@ fn normalize_type_for_registry(
}
}
/// ct.1: enforce the canonical-form rule on every `Type::Con` and
/// enforce the canonical-form rule on every `Type::Con` and
/// `Term::Ctor.type_name` reference in every loaded module. Runs
/// after prelude injection and before class-schema validation, so a
/// stale bare cross-module ref fires the canonical-form diagnostic
@@ -922,7 +923,7 @@ pub(crate) fn validate_canonical_type_names(
local_types.insert(mod_name.clone(), s);
}
// mq.1: symmetric pre-pass over `Def::Class` names. The map is
// symmetric pre-pass over `Def::Class` names. The map is
// module → bare-class-name set; used by `check_class_ref` to apply
// the canonical-form rule to the three migrated class-ref fields
// (`InstanceDef.class`, `Constraint.class`, `SuperclassRef.class`).
@@ -961,7 +962,7 @@ pub(crate) fn validate_canonical_type_names(
})?;
check_class_name_fields(def, mod_name)?;
// mq.1: apply the canonical-form rule to the three
// apply the canonical-form rule to the three
// migrated class-ref fields.
match def {
Def::Instance(id) => {
@@ -994,7 +995,7 @@ pub(crate) fn validate_canonical_type_names(
Ok(())
}
/// mq.1: apply the canonical-form rule to a class-reference field
/// apply the canonical-form rule to a class-reference field
/// value (`InstanceDef.class`, `Constraint.class`, or
/// `SuperclassRef.class`). Sibling of `check_type_con_name` for
/// class refs.
@@ -1071,7 +1072,7 @@ fn check_class_ref(
})
}
/// ct.1: apply the canonical-form rule to one `Type::Con.name`
/// apply the canonical-form rule to one `Type::Con.name`
/// (also reused for `Term::Ctor.type_name` in Task 2).
fn check_type_con_name(
name: &str,
@@ -1140,7 +1141,7 @@ fn check_type_con_name(
})
}
/// ct.1: walk every `Type` reachable from a single `Def`, calling
/// walk every `Type` reachable from a single `Def`, calling
/// `f` on each. Recurses into `Type::Fn.params/ret`, `Type::Con.args`,
/// `Type::Forall.constraints/body`, plus the obvious top-level fields
/// of each `Def` variant AND every Type annotation embedded in a Term
@@ -1188,7 +1189,7 @@ where
}
}
/// ct.1: walk a `Term` and call `f` on every Type annotation embedded
/// walk a `Term` and call `f` on every Type annotation embedded
/// in it (Lam.param_tys, Lam.ret_ty, LetRec.ty). Recurses through
/// every Term sub-position. Does NOT fire on `Term::Ctor.type_name`
/// (that's a `String`, not a `Type`; handled by `walk_def_terms` in
@@ -1263,7 +1264,7 @@ where
}
}
/// ct.1: recursive walk of a `Type`, calling `f` at every node.
/// recursive walk of a `Type`, calling `f` at every node.
fn walk_type<F>(t: &Type, f: &mut F) -> Result<(), WorkspaceLoadError>
where
F: FnMut(&Type) -> Result<(), WorkspaceLoadError>,
@@ -1292,7 +1293,7 @@ where
}
}
/// ct.1: walk every `Term::Ctor.type_name` reachable from a single
/// walk every `Term::Ctor.type_name` reachable from a single
/// `Def`, calling `f` on the type_name strings. Used to enforce the
/// canonical-form rule on term-side type references.
fn walk_def_terms<F>(def: &Def, f: &mut F) -> Result<(), WorkspaceLoadError>
@@ -1320,7 +1321,7 @@ where
}
}
/// ct.1: recursive walk of a `Term`, calling `f` on every
/// recursive walk of a `Term`, calling `f` on every
/// `Term::Ctor.type_name`. Embedded `Type` annotations (Lam param /
/// return types, LetRec types) ride the `walk_def_types` /
/// `walk_term_embedded_types` path — but `Term::Ctor.type_name` is
@@ -1397,7 +1398,7 @@ where
}
}
/// ct.1: Pattern::Ctor carries a `ctor` name (matches against a
/// Pattern::Ctor carries a `ctor` name (matches against a
/// scrutinee's TypeDef) but NOT a type_name field — the type is
/// inferred from the scrutinee. So Pattern walking only recurses;
/// no canonical-form check fires here. `f` is kept in the signature
@@ -1419,11 +1420,11 @@ where
}
}
/// ct.1: reject any `.` in `ClassDef.name`. The defining-site name is
/// reject any `.` in `ClassDef.name`. The defining-site name is
/// bare by convention (symmetric to `TypeDef.name`); a qualified form
/// indicates a malformed file.
///
/// mq.1: narrowed from the four class-reference fields to
/// narrowed from the four class-reference fields to
/// `ClassDef.name` only. The three referencing fields
/// (`InstanceDef.class`, `Constraint.class`, `SuperclassRef.class`)
/// now follow the canonical-form rule and are validated by
@@ -1445,14 +1446,15 @@ fn check_class_name_fields(
Ok(())
}
/// Iter 22b.1: extract the head-constructor name of a type, for the
/// extract the head-constructor name of a type, for the
/// "where is this type defined" lookup and for diagnostic-message
/// rendering.
///
/// For `Type::Con { name, .. }` (the only legal head shape for a
/// non-orphan instance) returns `name`. Other variants (`Var`,
/// `Forall`, `Fn`) are not legal as instance heads — Decision 11
/// requires a concrete type expression at the instance head — so we
/// `Forall`, `Fn`) are not legal as instance heads — the typeclass
/// design requires a concrete type expression at the instance head —
/// so we
/// emit a stable fallback string. The fallback prevents diagnostic
/// rendering from panicking on a malformed fixture; the "real"
/// rejection of non-concrete instance heads will arrive as a
@@ -1640,7 +1642,7 @@ mod tests {
// ailang-surface dev-dep cycle disallows `surface::load_workspace`
// calls from this in-mod test crate).
// Iter 22b.1: a workspace whose own modules contain no
// a workspace whose own modules contain no
// `Def::Instance` defs produces no non-prelude registry entries.
// This is the happy-path baseline for the registry-build pass —
// every pre-22b workspace falls into this case. With the
@@ -1660,7 +1662,7 @@ mod tests {
.join("examples")
}
// Iter 22b.1: a coherent instance (in the class's module)
// a coherent instance (in the class's module)
// loads cleanly and produces one registry entry from the
// fixture itself. With the auto-loaded prelude (iter 23.2)
// the registry also contains the prelude's own entries; the
@@ -1668,7 +1670,7 @@ mod tests {
// `iter22b1_instance_in_class_module_loads_clean` relocated to
// `crates/ailang-core/tests/workspace_pin.rs` in iter form-a.1 Task 5.
// Iter 22b.1: an instance declared in a module that is neither
// an instance declared in a module that is neither
// the class's module nor the type's module fires `OrphanInstance`.
// The fixture imports `test_22b1_orphan_third_classmod` (which
// owns `class TShow`) and the entry module declares `instance
@@ -1678,17 +1680,17 @@ mod tests {
// `iter22b1_orphan_instance_fires_diagnostic` relocated to
// `crates/ailang-core/tests/workspace_pin.rs` in iter form-a.1 Task 5.
// Iter 22b.1 / mq.1: two instances of the same `(class, type)`
// pair collide on the registry's uniqueness check.
// Two instances of the same `(class, type)` pair collide on the
// registry's uniqueness check.
//
// Pre-mq.1 the test used a two-module fixture
// Pre-canonical-class-form the test used a two-module fixture
// (`test_22b1_dup_a` declared the class + first instance,
// `test_22b1_dup_b` declared the type + second instance) which
// relied on bare cross-module class refs. The mq.1
// relied on bare cross-module class refs. The canonical-form rule
// canonical-form rule makes that shape structurally
// unrepresentable (any second instance in a module that owns
// neither the class nor the type fires `OrphanInstance` first).
// Post-mq.1 the only way to land two instances on the same
// Post-canonical-class-form the only way to land two instances on the same
// canonical key is to have them in the same module (the class's
// or the type's module). The fixture now declares both
// instances in `test_22b1_dup_same_module` — both class-leg
@@ -1697,7 +1699,7 @@ mod tests {
// `iter22b1_duplicate_instance_fires_diagnostic` relocated to
// `crates/ailang-core/tests/workspace_pin.rs` in iter form-a.1 Task 5.
// Iter 22b.1: an instance that omits a required (non-default)
// an instance that omits a required (non-default)
// method of its class fires `MissingMethod`. The fixture's
// `class TEq` declares `teq` and `tne` as both non-default; the
// instance only specifies `tne`, leaving `teq` missing.
@@ -1713,13 +1715,13 @@ mod tests {
// pd.2 (post-shim retirement; in-mod tests cannot reach
// `ailang_surface::load_workspace` due to the dev-dep cycle).
// Iter 22b.2: an instance that specifies a body for a method
// an instance that specifies a body for a method
// name the class never declared must fire
// `OverridingNonExistentMethod`. Symmetric counterpart to
// `MissingMethod`: the latter fires when the class declares a
// non-default method that the instance omits; this one fires
// when the instance provides a body the class did not ask for.
// Decision 11 forbids ad-hoc additions to a class's method set
// The typeclass design forbids ad-hoc additions to a class's method set
// at the instance site.
// (Class is `TEq` rather than `Eq` to avoid colliding with the
// auto-loaded prelude's `class Eq`.)
@@ -1730,20 +1732,20 @@ mod tests {
// relocated to `crates/ailang-core/tests/workspace_pin.rs` in iter
// pd.2.
// mq.3: the two `..._method_name_collision_fires` pin tests
// the two `..._method_name_collision_fires` pin tests
// (class-class and class-fn variants) were retired here and
// re-located as positive-load tests in
// `crates/ailang-check/tests/method_collision_pin.rs`. Post-
// mq.3 those on-disk fixtures load cleanly and the equivalent
// Now those on-disk fixtures load cleanly and the equivalent
// observations move to `env.method_to_candidate_classes`
// (multi-entry set) plus the call-site warning.
// Iter 22b.2: an instance `C T` whose class `C` declares a
// an instance `C T` whose class `C` declares a
// superclass `S` requires that `instance S T` also exist in the
// workspace. The fixture declares `class TEq a`, `class TOrd a
// extends TEq a`, and `instance TOrd Int` — but no `instance TEq
// Int` — so registry build must fire
// `MissingSuperclassInstance`. Decision 11 single-superclass
// `MissingSuperclassInstance`. the typeclass design's single-superclass
// model requires `instance S T` whenever `instance C T` exists.
// (Classes are `TEq` / `TOrd` rather than `Eq` / `Ord` to avoid
// colliding with the auto-loaded prelude's `class Eq`.)
@@ -1812,7 +1814,7 @@ mod tests {
})).unwrap()
}
/// ct.1: a `Type::Con` whose `name` is a primitive (`Int` / `Bool` /
/// a `Type::Con` whose `name` is a primitive (`Int` / `Bool` /
/// `Str` / `Unit` / `Float`) must be accepted bare. The five
/// primitive names are the only legal bare-non-local Type::Con names
/// under the canonical-form rule.
@@ -1824,7 +1826,7 @@ mod tests {
validate_canonical_type_names(&modules, &["prelude"]).expect("Float must be accepted");
}
/// ct.1: a bare Type::Con whose `name` matches a local TypeDef in
/// a bare Type::Con whose `name` matches a local TypeDef in
/// the same module must be accepted (the canonical-form rule: bare =
/// local).
#[test]
@@ -1834,7 +1836,7 @@ mod tests {
.expect("local Foo must be accepted");
}
/// ct.1: a bare Type::Con whose `name` is neither a primitive nor a
/// a bare Type::Con whose `name` is neither a primitive nor a
/// local TypeDef must fire `BareCrossModuleTypeRef`. With no imports,
/// the candidates list is empty.
#[test]
@@ -1853,7 +1855,7 @@ mod tests {
}
}
/// ct.1: a bare Type::Con whose `name` resolves to one imported
/// a bare Type::Con whose `name` resolves to one imported
/// module's TypeDef must fire `BareCrossModuleTypeRef` with that
/// qualified form in `candidates` (so the diagnostic can suggest the
/// fix).
@@ -1873,7 +1875,7 @@ mod tests {
}
}
/// ct.1: a qualified Type::Con `<owner>.<type>` where `<owner>` is a
/// a qualified Type::Con `<owner>.<type>` where `<owner>` is a
/// known module AND `<type>` is one of its TypeDefs must be accepted.
#[test]
fn ct1_validator_accepts_qualified_xmod_ref() {
@@ -1884,7 +1886,7 @@ mod tests {
.expect("other.Ordering must be accepted");
}
/// ct.1: a qualified Type::Con `<owner>.<type>` where `<owner>` is
/// a qualified Type::Con `<owner>.<type>` where `<owner>` is
/// NOT a known module must fire `BadCrossModuleTypeRef`. Symmetric
/// case: `<owner>` known but no TypeDef `<type>` in it.
#[test]
@@ -1901,7 +1903,7 @@ mod tests {
}
}
/// ct.1: a Type::Con embedded inside a `Term::Lam.param_tys` is a
/// a Type::Con embedded inside a `Term::Lam.param_tys` is a
/// Type-position occurrence, just inside a Term tree. The validator
/// must walk into Lam-internal types so an LLM author can't smuggle
/// a bare cross-module ref past it by hiding it in a lambda
@@ -1939,7 +1941,7 @@ mod tests {
}
}
/// ct.1: a `Term::Ctor` whose `type_name` is a bare cross-module ref
/// a `Term::Ctor` whose `type_name` is a bare cross-module ref
/// must fire `BareCrossModuleTypeRef`. Symmetric to the Type::Con
/// rule but the field lives on Term, not Type.
#[test]
@@ -1980,7 +1982,7 @@ mod tests {
}
}
/// ct.1: a qualified `ClassDef.name` (e.g. `other.Eq`) must fire
/// a qualified `ClassDef.name` (e.g. `other.Eq`) must fire
/// `QualifiedClassName`. Class names stay bare in this milestone.
#[test]
fn ct1_validator_rejects_qualified_classdef_name() {
@@ -2009,9 +2011,9 @@ mod tests {
}
}
/// mq.1: a qualified `InstanceDef.class` referencing a class
/// declared in another known module is the canonical form post-mq.1
/// and is accepted by the validator. Inverted from the pre-mq.1
/// a qualified `InstanceDef.class` referencing a class
/// declared in another known module is the canonical form post-canonical-class-form
/// and is accepted by the validator. Inverted from the pre-canonical-class-form
/// `ct1_validator_rejects_qualified_instancedef_class` test:
/// `InstanceDef.class` moved bare→canonical.
#[test]
@@ -2042,11 +2044,11 @@ mod tests {
modules.insert("other".to_string(), other);
modules.insert("m".to_string(), m);
validate_canonical_type_names(&modules, &["prelude"])
.expect("qualified InstanceDef.class is the canonical form post-mq.1");
.expect("qualified InstanceDef.class is the canonical form post-canonical-class-form");
}
/// mq.1: a qualified `SuperclassRef.class` referencing a class in
/// another known module is the canonical form post-mq.1 and is
/// a qualified `SuperclassRef.class` referencing a class in
/// another known module is the canonical form post-canonical-class-form and is
/// accepted. Inverted from
/// `ct1_validator_rejects_qualified_superclassref_class`.
#[test]
@@ -2078,12 +2080,12 @@ mod tests {
modules.insert("other".to_string(), other);
modules.insert("m".to_string(), m);
validate_canonical_type_names(&modules, &["prelude"])
.expect("qualified SuperclassRef.class is the canonical form post-mq.1");
.expect("qualified SuperclassRef.class is the canonical form post-canonical-class-form");
}
/// mq.1: a qualified `Constraint.class` (inside a `Type::Forall`)
/// a qualified `Constraint.class` (inside a `Type::Forall`)
/// referencing a class in another known module is the canonical
/// form post-mq.1 and is accepted. Inverted from
/// form post-canonical-class-form and is accepted. Inverted from
/// `ct1_validator_rejects_qualified_constraint_class`.
#[test]
fn ct1_validator_accepts_qualified_constraint_class() {
@@ -2124,17 +2126,17 @@ mod tests {
modules.insert("other".to_string(), other);
modules.insert("m".to_string(), m);
validate_canonical_type_names(&modules, &["prelude"])
.expect("qualified Constraint.class is the canonical form post-mq.1");
.expect("qualified Constraint.class is the canonical form post-canonical-class-form");
}
/// ct.1.5a: registry-side duplicate detection survives the
/// registry-side duplicate detection survives the
/// asymmetric canonical-form representation. Two coherent instances
/// on the same type-def, one declared bare-local (in the type's
/// defining module) and one declared qualified-cross-module (in the
/// class's defining module), must collide on the registry's
/// `(class, type-hash)` key.
///
/// This is the regression that broke during the ct.1.5 migration
/// This is the regression that broke during the canonical-form migration
/// dry-run: after migrating `test_22b1_dup_a.ail.json` to qualified
/// `test_22b1_dup_b.MyInt`, the unmigrated `test_22b1_dup_b.ail.json`
/// (which keeps bare `MyInt` because the type IS local there)
@@ -2185,7 +2187,7 @@ mod tests {
// Module `other`: declares `type MyInt` and the qualified-
// cross-module instance `instance cls.TShow MyInt`. Imports
// `cls` for the class ref. The `class` field carries the
// canonical form (qualified for cross-module per mq.1); the
// canonical form (qualified for cross-module per the canonical-class-form rule); the
// `type` field stays bare-local.
let other: Module = serde_json::from_value(serde_json::json!({
"schema": crate::SCHEMA,
@@ -2231,7 +2233,7 @@ mod tests {
}
}
/// ct.1.5a follow-up: `normalize_type_for_registry` must recurse into
/// canonical-form normalisation follow-up: `normalize_type_for_registry` must recurse into
/// `Type::Forall.constraints[].type_` just like it does into
/// `Type::Forall.body`. A bare `Type::Con` nested inside a Forall
/// constraint that lives in a different module must be rewritten to
@@ -2291,14 +2293,14 @@ mod tests {
}
}
/// ct.1: a qualified `Constraint.class` nested inside a
/// a qualified `Constraint.class` nested inside a
/// `ClassDef.methods[].ty.Forall.constraints` must fire
/// `QualifiedClassName`. Distinct from the `Def::Fn` site: the
/// `Def::Class` branch's per-method Forall walk needs independent
/// coverage.
///
/// mq.1: inverted — qualified `Constraint.class` inside a
/// `ClassDef.methods` Forall is the canonical form post-mq.1 and is
/// inverted — qualified `Constraint.class` inside a
/// `ClassDef.methods` Forall is the canonical form post-canonical-class-form and is
/// accepted by the validator.
#[test]
fn ct1_validator_accepts_qualified_constraint_class_in_classdef_method() {
@@ -2343,37 +2345,37 @@ mod tests {
modules.insert("other".to_string(), other);
modules.insert("m".to_string(), m);
validate_canonical_type_names(&modules, &["prelude"])
.expect("qualified Constraint.class in ClassDef-method Forall is the canonical form post-mq.1");
.expect("qualified Constraint.class in ClassDef-method Forall is the canonical form post-canonical-class-form");
}
/// ct.1: on-disk fixture for `BareCrossModuleTypeRef`. Bare
/// on-disk fixture for `BareCrossModuleTypeRef`. Bare
/// `Ordering` Term::Ctor with no imports; the validator catches it
/// after prelude injection (so the candidate list contains
/// `prelude.Ordering`).
// `ct1_fixture_bare_xmod_rejected` relocated to
// `crates/ailang-core/tests/workspace_pin.rs` in iter pd.2.
/// ct.1: on-disk fixture for `BadCrossModuleTypeRef`. Qualified
/// on-disk fixture for `BadCrossModuleTypeRef`. Qualified
/// `Mystery.Type` with `Mystery` not a known module.
// `ct1_fixture_bad_qualifier` relocated to
// `crates/ailang-core/tests/workspace_pin.rs` in iter pd.2.
/// mq.1: on-disk fixture for the post-mq.1 rejection path. The
/// on-disk fixture for the post-canonical-class-form rejection path. The
/// fixture declares `instance prelude.Eq Int` outside the prelude
/// and outside Int's defining module — under the canonical-form
/// rule the qualified `prelude.Eq` ref is now schema-valid (post-
/// mq.1), and the downstream coherence check rejects the instance
/// class references in canonical form), and the downstream coherence check rejects the instance
/// with `OrphanInstance` instead.
///
/// Pre-mq.1 this test asserted `QualifiedClassName` on the same
/// Pre-canonical-class-form this test asserted `QualifiedClassName` on the same
/// fixture; the rename + reshape is the on-disk-fixture half of
/// the four in-test inversions further up.
// `ct1_fixture_qualified_class_orphan_post_mq1` relocated to
// `crates/ailang-core/tests/workspace_pin.rs` in iter pd.2.
// mq.1: on-disk fixture pin — a workspace where the consumer's
// on-disk fixture pin — a workspace where the consumer's
// `Constraint.class` references a class in an imported module via
// the qualified form loads cleanly. Symmetric to ct.1's positive
// the qualified form loads cleanly. Symmetric to the canonical-form rule's positive
// cross-module type-ref fixture (`ct1_validator_accepts_qualified_xmod_ref`
// in-test sibling). Guards the full load → validator → registry
// path on a real on-disk pair.
@@ -2427,7 +2429,7 @@ mod tests {
format!(r#"{{"schema":"ailang/v0","name":"{name}","imports":[],"defs":[]}}"#)
}
/// mq.1.1: `BareCrossModuleClassRef` is the sibling diagnostic for
/// `BareCrossModuleClassRef` is the sibling diagnostic for
/// bare cross-module class references on `InstanceDef.class`,
/// `Constraint.class`, and `SuperclassRef.class`. Verifies the
/// variant is constructible and its Display surface names the
@@ -2445,7 +2447,7 @@ mod tests {
assert!(rendered.contains("prelude.Show"), "Display must list candidates, got: {rendered}");
}
/// mq.1.1b: `BadCrossModuleClassRef` is the sibling diagnostic for
/// `BadCrossModuleClassRef` is the sibling diagnostic for
/// qualified class references whose owner module is unknown or
/// declares no class by that name. Verifies the variant is
/// constructible and its Display surface names the offending
@@ -2462,7 +2464,7 @@ mod tests {
"Display must mention class or module context, got: {rendered}");
}
/// mq.1.3: `InstanceDef.class` carrying a bare name that does NOT
/// `InstanceDef.class` carrying a bare name that does NOT
/// resolve to a local class of the owning module must fire
/// `BareCrossModuleClassRef`. The fixture imports a sibling module
/// that declares the class under the bare name, so the candidate
@@ -2505,7 +2507,7 @@ mod tests {
}
}
/// mq.1.3: `Constraint.class` on a `Def::Fn` carrying a bare name
/// `Constraint.class` on a `Def::Fn` carrying a bare name
/// that does not resolve to a local class fires
/// `BareCrossModuleClassRef`.
#[test]
@@ -2558,7 +2560,7 @@ mod tests {
}
}
/// mq.1.3: `SuperclassRef.class` on a `Def::Class` carrying a bare
/// `SuperclassRef.class` on a `Def::Class` carrying a bare
/// name that does not resolve to a local class fires
/// `BareCrossModuleClassRef`.
#[test]
@@ -2599,9 +2601,9 @@ mod tests {
}
}
/// mq.1.3: a qualified `InstanceDef.class` referencing a class in
/// an imported module is the canonical form post-mq.1 and is
/// accepted by `validate_canonical_type_names`. Symmetric to ct.1's
/// a qualified `InstanceDef.class` referencing a class in
/// an imported module is the canonical form post-canonical-class-form and is
/// accepted by `validate_canonical_type_names`. Symmetric to the canonical-form rule's
/// "qualified Type::Con resolves" positive path.
#[test]
fn mq1_qualified_instancedef_class_accepted() {
@@ -2665,7 +2667,7 @@ mod tests {
fn module_with_bare_classref(name: &str, class_ref: &str) -> Module {
// A module containing a single Def::Instance whose `class` field is
// a bare class reference. The instance's `type` field is a local
// ADT defined in the same module to keep ct.1 happy.
// ADT defined in the same module to keep the canonical-form rule satisfied.
serde_json::from_value(serde_json::json!({
"schema": crate::SCHEMA,
"name": name,
@@ -31,7 +31,7 @@ const EXPECTED: &[&str] = &[
"test_ct1_bad_qualifier.ail.json",
"test_ct1_bare_xmod_rejected.ail.json",
"test_ct1_qualified_class_rejected.ail.json",
// Iter loop-recur.tidy — lambda-capture-of-loop-binder rejection
// Lambda-capture-of-loop-binder rejection
"test_loop_binder_captured_by_lambda.ail.json",
// loop-recur iter 2 — negative typecheck fixtures
"test_recur_arity_mismatch.ail.json",
@@ -41,7 +41,7 @@ fn two_modules_with_same_bare_foo_both_register() {
args: vec![],
});
// mq.1: registry-entries key is keyed by the qualified class name.
// registry-entries key is keyed by the qualified class name.
let main_key = ("ctt2_collision_cls.MyC".to_string(), main_foo_hash);
let lib_key = ("ctt2_collision_cls.MyC".to_string(), lib_foo_hash);
+2 -2
View File
@@ -81,9 +81,9 @@ fn design_md_has_no_doc_archaeology() {
"design/: 'previously all diagnostics were Error' is project history → docs/journals/");
assert!(!d.contains("What did change in 2026-05-13 (this milestone) is"),
"design/: 'what did change in <date> (this milestone)' is post-mortem narrative");
assert!(!d.contains("primitives were retired 2026-05-14 in iter rpe.1"),
assert!(!d.contains("primitives were retired 2026-05-14 in the per-type-print-op retirement"),
"design/: 'X were retired <date> in iter Y' is history → docs/journals/");
assert!(!d.contains("were retired in iter rpe.1 (2026-05-14)"),
assert!(!d.contains("were retired in the per-type-print-op retirement (2026-05-14)"),
"design/: the second per-type-print-op retirement-narrative sentence is history");
assert!(!d.contains("originally listed here as the eighth fixture, was retired"),
"design/: 'originally listed … was retired by milestone X' is doc-archaeology");
@@ -25,7 +25,7 @@ fn norm(s: &str) -> String {
#[test]
fn design_md_effect_prose_is_true() {
// Decision 3 effect prose -> design/models/effects.md; the
// Effect prose -> design/models/effects.md; the
// "the built-in IO and Diverge ops" absent-pin's home
// `## What is not (yet) supported` -> design/contracts/scope-boundaries.md.
let d = norm(&[read("design/models/effects.md"),
+14 -13
View File
@@ -68,7 +68,7 @@ fn hash_changes_with_content() {
assert_ne!(h1, h2);
}
/// Iter 13a regression: adding `vars` to TypeDef and `args` to
/// adding `vars` to TypeDef and `args` to
/// `Type::Con` must NOT change canonical-JSON hashes of any pre-13a
/// definition. Recorded hashes were captured from on-disk modules
/// before the schema extension; if this fires, a
@@ -110,7 +110,7 @@ fn loop_recur_schema_extension_preserves_pre_loop_recur_hashes() {
assert_eq!(def_hash(int_list_def), "b082192bd0c99202");
}
/// Iter 19b regression: adding `suppress` to FnDef must NOT change
/// adding `suppress` to FnDef must NOT change
/// canonical-JSON hashes of any pre-19b fn whose `suppress` is empty.
/// The `skip_serializing_if = "Vec::is_empty"` predicate on the field
/// is what enforces this; if it is wrong, every existing fixture's
@@ -146,7 +146,7 @@ fn iter19b_empty_suppress_preserves_pre_19b_hashes() {
);
}
/// Iter 19b: an on-disk pre-19b fixture must still load cleanly
/// an on-disk pre-19b fixture must still load cleanly
/// (no `suppress` field present in the JSON) and produce its
/// canonical-byte hash unchanged.
#[test]
@@ -159,7 +159,7 @@ fn iter19b_schema_extension_preserves_pre_19b_hashes() {
assert_eq!(def_hash(sum_def), "db33f57cb329935e");
}
/// Iter 22b.1 regression: adding `Def::Class` and `Def::Instance` must
/// adding `Def::Class` and `Def::Instance` must
/// NOT change canonical-JSON hashes of any pre-22b def. The new variants
/// are alternatives, not field additions — pre-22b fixtures simply do
/// not produce a `Class` / `Instance` `Def`, and the existing arms are
@@ -183,7 +183,7 @@ fn iter22b1_schema_extension_preserves_pre_22b_hashes() {
assert_eq!(def_hash(int_list_def), "b082192bd0c99202");
}
/// Iter 22b.1: a ClassDef with no doc, no superclass, and an empty
/// a ClassDef with no doc, no superclass, and an empty
/// methods list serialises to a stable canonical form. Two construction
/// paths (default-elided optionals vs. JSON without those keys at all)
/// hash bit-identically.
@@ -207,7 +207,7 @@ fn iter22b1_classdef_empty_optionals_hash_stable() {
);
}
/// Iter 22b.2: adding `constraints` to `Type::Forall` must NOT alter
/// adding `constraints` to `Type::Forall` must NOT alter
/// canonical-JSON bytes of any pre-22b.2 polymorphic type.
#[test]
fn forall_without_constraints_hashes_bit_identical_to_pre_22b2() {
@@ -228,9 +228,9 @@ fn forall_without_constraints_hashes_bit_identical_to_pre_22b2() {
);
}
/// Iter ct.4 (canonical-type-names milestone close): pin the
/// pin the
/// canonical-form hashes of the two cross-module fixtures migrated by
/// `ail migrate-canonical-types` in ct.1. These hashes are the
/// `ail migrate-canonical-types` in the canonical-form migration. These hashes are the
/// post-migration state.
#[test]
fn ct4_migrated_fixtures_have_canonical_form_hashes() {
@@ -239,13 +239,14 @@ fn ct4_migrated_fixtures_have_canonical_form_hashes() {
let ord_mod = ailang_surface::load_module(&examples.join("ordering_match.ail"))
.expect("examples/ordering_match.ail loads");
let main_def = ord_mod.defs.iter().find(|d| d.name() == "main").unwrap();
// Iter rpe.1 updated the hash: `ordering_match.ail`'s body migrated
// from `(do io/print_int x)` to `(app print x)` as part of the
// corpus migration retiring the per-type print effect-ops.
// The hash reflects a corpus migration: `ordering_match.ail`'s
// body moved from `(do io/print_int x)` to `(app print x)` when
// the per-type print effect-ops were retired in favour of the
// polymorphic `print` helper.
assert_eq!(
def_hash(main_def),
"b65a7f834703ffb4",
"ordering_match::main canonical hash must match captured post-rpe.1 value"
"ordering_match::main canonical hash must match captured post-per-type-print-retirement value"
);
let dup_a_mod = ailang_surface::load_module(&examples.join("test_22b1_dup_a.ail"))
@@ -267,7 +268,7 @@ fn ct4_migrated_fixtures_have_canonical_form_hashes() {
);
}
/// Iter ct.4: re-assert that the canonical-form tightening did NOT
/// re-assert that the canonical-form tightening did NOT
/// change hashes of intra-module-only fixtures.
#[test]
fn ct4_unmigrated_fixtures_remain_bit_identical() {
+2 -2
View File
@@ -289,7 +289,7 @@ fn spec_mentions_every_def_kind() {
Def::Fn(_) => "fn",
Def::Const(_) => "const",
Def::Type(_) => "type",
// Iter 22b.1: class/instance Def variants exist but are
// class/instance Def variants exist but are
// not yet anchored in the prose-spec block. Once 22b.4
// adds prose projection for them, the FORM_A_SPEC text
// gains `(class ` / `(instance ` anchors and this match
@@ -305,7 +305,7 @@ fn spec_mentions_every_def_kind() {
}
}
/// The mode keywords (Decision 10) must appear so an LLM knows the
/// The mode keywords (the RC memory model) must appear so an LLM knows the
/// Form-A wrapper syntax.
#[test]
fn spec_mentions_mode_keywords() {
+14 -14
View File
@@ -49,14 +49,14 @@ fn loads_example_workspace_happy_path() {
assert_eq!(ws.entry, "ws_main");
assert!(ws.modules.contains_key("ws_main"));
assert!(ws.modules.contains_key("ws_lib"));
// Iter 23.1: the loader auto-injects the `prelude` module,
// the loader auto-injects the `prelude` module,
// so the count is the user's two modules plus prelude.
assert_eq!(ws.modules.len(), 3);
}
#[test]
fn loads_workspace_auto_injects_prelude() {
// Iter 23.1: the prelude module is implicitly part of every
// the prelude module is implicitly part of every
// workspace, regardless of whether the user's modules import
// it. Loading any well-formed workspace must result in
// `ws.modules["prelude"]` being present with the Ordering
@@ -78,7 +78,7 @@ fn loads_workspace_auto_injects_prelude() {
"prelude must contain Ordering type def"
);
// Iter 23.2: prelude also ships the `Eq` class plus three
// prelude also ships the `Eq` class plus three
// primitive instances (Eq Int, Eq Bool, Eq Str).
assert!(
prelude.defs.iter().any(|d| matches!(
@@ -114,7 +114,7 @@ fn loads_workspace_auto_injects_prelude() {
"prelude must contain `instance Eq Str`; saw Eq instances on: {eq_instance_types:?}"
);
// Iter 23.3: prelude also ships the `Ord` class plus three
// prelude also ships the `Ord` class plus three
// primitive instances (Ord Int, Ord Bool, Ord Str).
assert!(
prelude.defs.iter().any(|d| matches!(
@@ -179,7 +179,7 @@ fn iter22b1_instance_in_class_module_loads_clean() {
.collect();
assert_eq!(fixture_entries.len(), 1);
let (key, entry) = fixture_entries[0];
// mq.1: registry key is keyed by the qualified class name.
// registry key is keyed by the qualified class name.
// 24.2: class renamed `Show` → `TShow` workspace-wide.
assert_eq!(&key.0, "test_22b1_orphan_class.TShow");
assert_eq!(entry.defining_module, "test_22b1_orphan_class");
@@ -188,7 +188,7 @@ fn iter22b1_instance_in_class_module_loads_clean() {
assert_eq!(entry.instance.class, "TShow");
}
/// Iter 22b.1: an instance declared in a module that is neither
/// an instance declared in a module that is neither
/// the class's module nor the type's module fires `OrphanInstance`.
#[test]
fn iter22b1_orphan_instance_fires_diagnostic() {
@@ -209,8 +209,8 @@ fn iter22b1_orphan_instance_fires_diagnostic() {
}
}
/// Iter 22b.1 / mq.1: two instances of the same `(class, type)` pair
/// collide on the registry's uniqueness check. Post-mq.1 both
/// two instances of the same `(class, type)` pair
/// collide on the registry's uniqueness check. Post-canonical-class-form both
/// instances must live in the class's or the type's module (see
/// fixture).
#[test]
@@ -233,7 +233,7 @@ fn iter22b1_duplicate_instance_fires_diagnostic() {
}
}
/// Iter 22b.1: an instance that omits a required (non-default) method
/// an instance that omits a required (non-default) method
/// of its class fires `MissingMethod`.
#[test]
fn iter22b1_missing_method_fires_diagnostic() {
@@ -253,7 +253,7 @@ fn iter22b1_missing_method_fires_diagnostic() {
}
}
/// Iter 22b.2: an instance that specifies a body for a method name the
/// an instance that specifies a body for a method name the
/// class never declared must fire `OverridingNonExistentMethod`.
/// (`test_22b2_overriding_nonexistent` is NOT a §C4 carve-out — it
/// stays as a `.ail`-loadable fixture.)
@@ -274,7 +274,7 @@ fn instance_overriding_nonexistent_method_fires() {
}
}
/// Iter 22b.2: an instance `C T` whose class `C` declares a superclass
/// an instance `C T` whose class `C` declares a superclass
/// `S` requires that `instance S T` also exist in the workspace.
#[test]
fn instance_without_superclass_instance_fires() {
@@ -293,7 +293,7 @@ fn instance_without_superclass_instance_fires() {
}
}
/// mq.1: positive on-disk pair where `Constraint.class` references a
/// positive on-disk pair where `Constraint.class` references a
/// class in an imported module via the qualified form loads cleanly.
#[test]
fn mq1_xmod_constraint_class_fixture_loads() {
@@ -332,7 +332,7 @@ fn write_simple_module_json(dir: &Path, name: &str, imports: &[&str]) -> PathBuf
path
}
/// Iter 23.1 / pd.2 relocation: the loader auto-injects a module named
/// the loader auto-injects a module named
/// "prelude", so a user module that also claims that name would collide
/// silently. The collision is caught explicitly with `ReservedModuleName`.
#[test]
@@ -485,7 +485,7 @@ fn ct1_fixture_bad_qualifier() {
/// pd.2 relocation: §C4 (a) carve-out fixture
/// `test_ct1_qualified_class_rejected.ail.json` — declares
/// `instance prelude.Eq Int` outside the prelude AND outside Int's
/// defining module; post-mq.1 the qualified class-ref is schema-valid
/// defining module; post-canonical-class-form the qualified class-ref is schema-valid
/// and the downstream coherence check rejects with `OrphanInstance`.
#[test]
fn ct1_fixture_qualified_class_orphan_post_mq1() {
+6 -6
View File
@@ -89,7 +89,7 @@ fn write_def(out: &mut String, def: &Def, level: usize, owning_module: &str) {
}
}
/// Iter 19b: render the [`FnDef::suppress`] list as one
/// render the [`FnDef::suppress`] list as one
/// `// @suppress <code>: <reason>` line per entry, indented to
/// `level`. Multiple entries stack in declaration order; an empty
/// list produces no output (and therefore no leading blank line).
@@ -233,7 +233,7 @@ fn write_ctor(out: &mut String, c: &Ctor, owning_module: &str) {
}
fn write_fn_def(out: &mut String, fd: &FnDef, level: usize, owning_module: &str) {
// Iter 19b: render `suppress` entries as `// @suppress <code>: <reason>`
// render `suppress` entries as `// @suppress <code>: <reason>`
// comment lines **above** the doc string. They are contract metadata
// that the LLM-reader needs to see to understand why an annotation
// that looks over-strict is correct here. One line per entry, in
@@ -1783,7 +1783,7 @@ mod tests {
assert!(out.contains("() -> Unit with IO {"), "got:\n{out}");
}
/// Iter 19b: a FnDef with a single `Suppress` entry renders the
/// a FnDef with a single `Suppress` entry renders the
/// `// @suppress <code>: <reason>` line **above** the doc string,
/// preserving both the suppression visibility and the doc content.
/// The suppress line must appear before the `///`-prefixed doc
@@ -1829,7 +1829,7 @@ mod tests {
);
}
/// Iter 19b: multiple `Suppress` entries render as one
/// multiple `Suppress` entries render as one
/// `// @suppress <code>: <reason>` line each, in declaration
/// order, all above the doc string.
#[test]
@@ -1860,7 +1860,7 @@ mod tests {
// Order: declaration order is preserved.
}
/// Iter 19b: a FnDef with an empty `suppress` Vec emits no
/// a FnDef with an empty `suppress` Vec emits no
/// `// @suppress` line at all (and therefore no extra leading
/// blank line). Pre-19b prose snapshots stay byte-identical when
/// re-rendered through the 19b code.
@@ -1928,7 +1928,7 @@ mod tests {
}
// ======================================================================
// Iter 20b: formatting polish
// formatting polish
// ======================================================================
/// Convenience: two-arg App of a named callee. Used pervasively in
+1 -1
View File
@@ -63,7 +63,7 @@ fn snapshot_rc_app_let_partial_drop_leak() {
check_snapshot("rc_app_let_partial_drop_leak");
}
/// Iter 20b: this fixture exercises the infix renderer + paren elision.
/// this fixture exercises the infix renderer + paren elision.
/// Pre-20b, the body had `if ==(n, 0) { ... }` and `tail sum_acc(-(n,
/// 1), ICons(-(n, 1), acc))`; after 20b those collapse to `if n == 0`
/// and `n - 1`. If this regresses, the renderer is broken.
+28 -28
View File
@@ -385,7 +385,7 @@ impl<'a> Parser<'a> {
ctors.push(self.parse_ctor()?);
}
Some("drop-iterative") => {
// Iter 18e: `(drop-iterative)` opt-in annotation.
// `(drop-iterative)` opt-in annotation.
// Takes no arguments — it is a flag. A second
// `(drop-iterative)` clause is a parse error
// (rejected here so that the JSON schema's
@@ -510,7 +510,7 @@ impl<'a> Parser<'a> {
let mut ty: Option<Type> = None;
let mut params: Option<Vec<String>> = None;
let mut body: Option<Term> = None;
// Iter 19b: every `(suppress ...)` clause appends one entry. Order
// every `(suppress ...)` clause appends one entry. Order
// is preserved (matches the on-disk JSON-AST order).
let mut suppress: Vec<Suppress> = Vec::new();
loop {
@@ -586,7 +586,7 @@ impl<'a> Parser<'a> {
})
}
/// Iter 19b: parse one `(suppress (code "<c>") (because "<r>"))`
/// parse one `(suppress (code "<c>") (because "<r>"))`
/// clause, returning a [`Suppress`] entry. Unknown sub-keywords
/// inside the clause are rejected with [`ParseError::Production`].
/// The `because` text is allowed to be empty here (the typechecker
@@ -639,7 +639,7 @@ impl<'a> Parser<'a> {
Ok(Suppress { code, because })
}
/// Iter 19b: helper — consume one string-literal token. Used by
/// helper — consume one string-literal token. Used by
/// [`Self::parse_suppress_attr`].
fn expect_string(&mut self, ctx: &'static str) -> Result<String, ParseError> {
match self.peek().cloned() {
@@ -1013,7 +1013,7 @@ impl<'a> Parser<'a> {
"con" => self.parse_type_con(),
"fn-type" => self.parse_fn_type(),
"forall" => self.parse_forall_type(),
// Iter 18a: `borrow` / `own` are valid only as
// `borrow` / `own` are valid only as
// wrappers around `fn-type` params or `ret`. At
// top-level type position they are a parse error
// with a clear message.
@@ -1109,7 +1109,7 @@ impl<'a> Parser<'a> {
})
}
/// Iter 18a: parse one fn-type slot — a type, optionally wrapped
/// parse one fn-type slot — a type, optionally wrapped
/// in `(borrow T)` or `(own T)`. The mode is `Implicit` for a
/// bare type, `Borrow` for `(borrow T)`, `Own` for `(own T)`.
fn parse_param_with_mode(&mut self) -> Result<(Type, ParamMode), ParseError> {
@@ -1304,7 +1304,7 @@ impl<'a> Parser<'a> {
self.parse_app_body(false, "app-term")
}
/// Iter 14e: `(tail-app callee arg+)` — same shape as `app` but
/// `(tail-app callee arg+)` — same shape as `app` but
/// constructs `Term::App { tail: true, .. }`. The typechecker's
/// tail-position pass verifies that the call really is in tail
/// position; an unmarked call in tail position is also legal.
@@ -1398,7 +1398,7 @@ impl<'a> Parser<'a> {
self.parse_do_body(false, "do-term")
}
/// Iter 14e: `(tail-do op arg*)` — same shape as `do` but
/// `(tail-do op arg*)` — same shape as `do` but
/// constructs `Term::Do { tail: true, .. }`.
fn parse_tail_do(&mut self) -> Result<Term, ParseError> {
self.expect_lparen("tail-do-term")?;
@@ -1501,7 +1501,7 @@ impl<'a> Parser<'a> {
})
}
/// Iter 16b.1: `(let-rec NAME (params PARAM*) (type T) (body TERM)
/// `(let-rec NAME (params PARAM*) (type T) (body TERM)
/// (in TERM))` — local recursive fn-shaped binding. Eliminated by
/// the desugar pass (lifted to a synthetic top-level fn) before
/// typecheck. The body's recursive references to `NAME` are
@@ -1529,7 +1529,7 @@ impl<'a> Parser<'a> {
})
}
/// Iter 18c.1: `(clone TERM)` — explicit RC clone wrapper. In 18c.1
/// `(clone TERM)` — explicit RC clone wrapper. In 18c.1
/// the wrapper is identity for typechecker and codegen; in 18c.3
/// the codegen will emit `call void @ailang_rc_inc` before yielding
/// the inner value's SSA reg under `--alloc=rc`.
@@ -1560,7 +1560,7 @@ impl<'a> Parser<'a> {
})
}
/// Iter 18d.1: `(reuse-as SOURCE BODY)` — explicit reuse-as wrapper.
/// `(reuse-as SOURCE BODY)` — explicit reuse-as wrapper.
/// In 18d.1 the wrapper is identity for codegen (lowers `body` and
/// drops `source`); 18d.2 will lower this as in-place rewrite under
/// `--alloc=rc`. The parser accepts any term in either slot — the
@@ -1834,7 +1834,7 @@ mod tests {
assert!(matches!(m.defs.len(), 1));
}
/// Iter 18a: `(borrow T)` and `(own T)` wrappers in fn-type
/// `(borrow T)` and `(own T)` wrappers in fn-type
/// param/ret slots round-trip into [`ParamMode::Borrow`] /
/// [`ParamMode::Own`] on `Type::Fn`. A bare type stays
/// [`ParamMode::Implicit`] (and its mode is elided from the
@@ -1870,7 +1870,7 @@ mod tests {
}
}
/// Iter 18a: `(borrow T)` outside an `fn-type` param/ret slot is
/// `(borrow T)` outside an `fn-type` param/ret slot is
/// a parse error. Specifically, a `const` whose declared type is
/// `(borrow ...)` must be rejected with a clear message — modes
/// are *not* a top-level type production.
@@ -1892,7 +1892,7 @@ mod tests {
);
}
/// Iter 18c.1: `(clone X)` parses to `Term::Clone { value: Var "x" }`.
/// `(clone X)` parses to `Term::Clone { value: Var "x" }`.
#[test]
fn parses_clone_wraps_inner_term() {
let m = parse(
@@ -1918,7 +1918,7 @@ mod tests {
}
}
/// Iter 18c.1: `(clone)` with no inner term is rejected with a
/// `(clone)` with no inner term is rejected with a
/// clear message.
#[test]
fn rejects_clone_without_argument() {
@@ -1939,7 +1939,7 @@ mod tests {
);
}
/// Iter 18d.1: `(reuse-as xs (term-ctor List Cons (...)))` parses to
/// `(reuse-as xs (term-ctor List Cons (...)))` parses to
/// `Term::ReuseAs { source: Var "xs", body: Ctor "Cons" ... }`.
#[test]
fn parses_reuse_as_wraps_source_and_body() {
@@ -1976,7 +1976,7 @@ mod tests {
}
}
/// Iter 18d.1: `(reuse-as)` with no args is rejected.
/// `(reuse-as)` with no args is rejected.
#[test]
fn rejects_reuse_as_with_no_arguments() {
let err = parse(
@@ -1996,7 +1996,7 @@ mod tests {
);
}
/// Iter 18d.1: `(reuse-as x)` with a single arg is rejected.
/// `(reuse-as x)` with a single arg is rejected.
#[test]
fn rejects_reuse_as_with_one_argument() {
let err = parse(
@@ -2016,7 +2016,7 @@ mod tests {
);
}
/// Iter 18d.1: round-trip via `parse_term` / `term_to_form_a` —
/// round-trip via `parse_term` / `term_to_form_a` —
/// `(reuse-as xs (term-ctor List Cons 1 xs))` survives a print/parse
/// cycle as the same `Term::ReuseAs`.
#[test]
@@ -2040,7 +2040,7 @@ mod tests {
assert_eq!(term_to_form_a(&parsed), printed);
}
/// Iter 18e: `(data T (drop-iterative))` parses with
/// `(data T (drop-iterative))` parses with
/// `drop_iterative = true` AND round-trips through the printer
/// back to the same canonical surface form.
#[test]
@@ -2073,7 +2073,7 @@ mod tests {
}
}
/// Iter 18e: `(data T)` with no `(drop-iterative)` clause parses
/// `(data T)` with no `(drop-iterative)` clause parses
/// with `drop_iterative = false`. Default state must be the
/// pre-18e shape so legacy fixtures' canonical bytes are stable.
#[test]
@@ -2096,7 +2096,7 @@ mod tests {
}
}
/// Iter 18e: `(drop-iterative ...arg...)` is rejected — the
/// `(drop-iterative ...arg...)` is rejected — the
/// annotation is a flag with no payload.
#[test]
fn rejects_drop_iterative_with_arguments() {
@@ -2116,7 +2116,7 @@ mod tests {
);
}
/// Iter 16b.1: minimal `(let-rec ...)` round-trips through the
/// minimal `(let-rec ...)` round-trips through the
/// parser into a `Term::LetRec` whose `name`, `params`, `body` and
/// `in_term` line up with the source.
#[test]
@@ -2157,7 +2157,7 @@ mod tests {
}
}
/// Iter 19b: a `(fn ...)` carrying a single
/// a `(fn ...)` carrying a single
/// `(suppress (code "...") (because "..."))` clause parses into
/// [`FnDef::suppress`] with the corresponding [`Suppress`] entry.
#[test]
@@ -2183,7 +2183,7 @@ mod tests {
}
}
/// Iter 19b: multiple `(suppress ...)` clauses accumulate into
/// multiple `(suppress ...)` clauses accumulate into
/// `FnDef::suppress` in declaration order. A second clause does
/// NOT overwrite the first.
#[test]
@@ -2212,7 +2212,7 @@ mod tests {
}
}
/// Iter 19b: a `(fn ...)` without a `(suppress ...)` clause has
/// a `(fn ...)` without a `(suppress ...)` clause has
/// `FnDef::suppress` empty (the default — round-trip identity
/// with pre-19b fixtures).
#[test]
@@ -2235,7 +2235,7 @@ mod tests {
}
}
/// Iter 19b: a `(suppress ...)` with `(because "")` (empty
/// a `(suppress ...)` with `(because "")` (empty
/// reason) still parses cleanly — the parser is intentionally
/// permissive; the typechecker is what emits
/// `empty-suppress-reason`. This keeps the surface symmetric
@@ -2263,7 +2263,7 @@ mod tests {
}
}
/// Iter 19b: an unknown sub-attribute inside `(suppress ...)`
/// an unknown sub-attribute inside `(suppress ...)`
/// produces a `ParseError::Production` naming the bad keyword and
/// listing the legal ones (`code` / `because`).
#[test]
+11 -11
View File
@@ -44,7 +44,7 @@ pub fn term_to_form_a(t: &Term) -> String {
out
}
/// Iter 19a: render a [`Type`] as form-A. Used by diagnostics that want
/// render a [`Type`] as form-A. Used by diagnostics that want
/// to suggest a relaxed signature (e.g. `over-strict-mode` shows the
/// `(fn-type ...)` with `(borrow T)` in place of `(own T)`). Output is
/// the same shape that appears as the `type` slot of a `(fn ...)` def.
@@ -123,7 +123,7 @@ fn write_type_def(out: &mut String, td: &TypeDef, level: usize) {
out.push('\n');
write_ctor(out, c, level + 1);
}
// Iter 18e: `(drop-iterative)` annotation. Printed last (after
// `(drop-iterative)` annotation. Printed last (after
// every ctor) so the canonical form lands consistent with the
// design/contracts/data-model.md example. Omitted when `drop_iterative == false`.
if td.drop_iterative {
@@ -163,7 +163,7 @@ fn write_fn_def(out: &mut String, fd: &FnDef, level: usize) {
write_string_lit(out, export);
out.push(')');
}
// Iter 19b: emit one `(suppress ...)` clause per entry, after the
// emit one `(suppress ...)` clause per entry, after the
// doc string and before the type. Round-trip stable: parser
// re-reads each clause back into [`FnDef::suppress`].
for s in &fd.suppress {
@@ -191,7 +191,7 @@ fn write_fn_def(out: &mut String, fd: &FnDef, level: usize) {
out.push(')');
}
/// Iter 19b: print one `(suppress (code "<c>") (because "<r>"))`
/// print one `(suppress (code "<c>") (because "<r>"))`
/// clause. Indentation matches the rest of the fn-def (one level
/// deeper than the `(fn ...)` head).
fn write_suppress(out: &mut String, s: &Suppress, level: usize) {
@@ -323,7 +323,7 @@ fn write_instance_method(out: &mut String, m: &InstanceMethod, level: usize) {
// ---- types ----------------------------------------------------------------
/// Iter 18a: print one fn-type param/ret slot, wrapping with
/// print one fn-type param/ret slot, wrapping with
/// `(borrow ...)` or `(own ...)` when the slot has an explicit
/// mode. `Implicit` is printed bare so pre-18a fixtures round-trip
/// unchanged.
@@ -343,7 +343,7 @@ fn write_fn_type_slot(out: &mut String, t: &Type, mode: ParamMode) {
}
}
/// Iter 22b.4a.4.5: print one `(constraint <Class> <type>)` pair, used
/// print one `(constraint <Class> <type>)` pair, used
/// inside the optional `(constraints …)` clause of a `(forall …)`. The
/// clause itself is omitted entirely when `Type::Forall.constraints` is
/// empty so pre-22b.2 forall fixtures stay bit-identical.
@@ -422,7 +422,7 @@ fn write_term(out: &mut String, t: &Term, level: usize) {
Term::Lit { lit } => write_lit(out, lit),
Term::Var { name } => out.push_str(name),
Term::App { callee, args, tail } => {
// Iter 14e: `tail-app` is the form for `App { tail: true }`;
// `tail-app` is the form for `App { tail: true }`;
// otherwise the regular `app` head is used. Both productions
// are positional analogues of each other — only the head
// keyword differs.
@@ -469,7 +469,7 @@ fn write_term(out: &mut String, t: &Term, level: usize) {
out.push(')');
}
Term::Do { op, args, tail } => {
// Iter 14e: `tail-do` mirrors `tail-app` for effect ops.
// `tail-do` mirrors `tail-app` for effect ops.
out.push_str(if *tail { "(tail-do " } else { "(do " });
out.push_str(op);
for a in args {
@@ -537,7 +537,7 @@ fn write_term(out: &mut String, t: &Term, level: usize) {
out.push(')');
}
Term::Clone { value } => {
// Iter 18c.1: print as `(clone <inner>)`. The wrapper is
// print as `(clone <inner>)`. The wrapper is
// identity at typecheck/codegen in 18c.1; only authored
// intent is recorded for the future inc/dec emission pass.
out.push_str("(clone ");
@@ -545,7 +545,7 @@ fn write_term(out: &mut String, t: &Term, level: usize) {
out.push(')');
}
Term::ReuseAs { source, body } => {
// Iter 18d.1: print as `(reuse-as <source> <body>)`. The
// print as `(reuse-as <source> <body>)`. The
// wrapper is identity at codegen in 18d.1 (the `body` is
// lowered, the `source` is dropped); 18d.2 will lower this
// as in-place rewrite under `--alloc=rc`.
@@ -812,7 +812,7 @@ mod tests {
round_trip(m, "minimal_instance");
}
/// Iter 22-floats.2 RED: `lex(print(L)) == L` round-trip property
/// `lex(print(L)) == L` round-trip property
/// for Float literals. Six representative bit patterns:
/// `1.5`, `0.0`, `-0.0`, `10.0`, `-0.375`, `1e10`. The round-trip
/// uses the existing `round_trip(Module, &str)` helper, which