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:
+43
-42
@@ -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
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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).
|
||||
|
||||
@@ -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]
|
||||
|
||||
+201
-199
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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.
|
||||
///
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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"
|
||||
);
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
+147
-147
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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 —
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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);
|
||||
|
||||
|
||||
@@ -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"),
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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() {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user