iter form-a.1 (Tasks 1-5): additive phase + relocation

First half of the form-a-default-authoring milestone-close iter
(Boss-decided strategy C, big-bang). All five tasks DONE; cargo
test --workspace green at every per-task boundary.

T1 — Add three new tests:
- parse_is_deterministic_over_every_ail_fixture (round_trip.rs)
- cli_parse_then_render_then_parse_is_idempotent (roundtrip_cli.rs)
- carve_out_inventory.rs (new file; #[ignore]'d until T8 deletion)

T2 — Bulk-render the 99 missing examples/<stem>.ail files via
`ail render`. Corpus 58 .ail (pre-iter) -> 157 .ail. Eight .ail.json
carve-outs (7 §C4(a) subject-matter + 1 §C4(b) prelude) preserved.
One re-loop triggered: load_workspace prefers .ail siblings since
ext-cli.1, so the newly-rendered imports broke seven Group-A entries
whose JSON entry-paths now resolved imports to fresh .ail. Repair:
pre-emptive forward-pull of five T3 migrations + 4 transient
#[ignore]s on workspace.rs mod tests (cleanly relocated in T5).

T3 — 14 Group-A test files migrated from ailang_core::load_workspace
to ailang_surface::load_workspace + .ail paths. Carve-out lines
preserved verbatim (7 sites in typeclass_22b2.rs / typeclass_22b3.rs).

T4 — 12 Group-B test files: bulk regex flip on build_and_run /
build_and_run_with_alloc / build_and_run_with_rc_stats call sites
(~70 e2e.rs invocations + 11 subprocess sites). Four files
mis-classified Group-A as Group-B in plan recon (mono_hash_stability,
prelude_free_fns, print_mono_body_shape, show_mono_synthesis); two
files mis-classified Group-B as Group-A (mono_recursive_fn,
mono_xmod_qualified_ref). Migrated per actual shape, not plan label.

T5 — Relocated #[cfg(test)] mod tests from production source to
integration test crates with ailang-surface dev-dependency:
- crates/ailang-core/tests/hash_pin.rs (10 tests from hash.rs)
- crates/ailang-core/tests/workspace_pin.rs (10 non-carve-out tests
  from workspace.rs)
- crates/ailang-codegen/tests/eq_primitives_pin.rs (3 tests from
  codegen/src/lib.rs:3717-3799)
- ailang-prose/tests/snapshot.rs migrated (helper + 8 fixtures) to
  .ail + ailang_surface::load_module
Carve-out tests in workspace.rs mod tests preserved in-place
(3× 22b2 + 3× ct1 = 6 tests). Tempdir-based loader-mechanism tests
(3 sites) also preserved — they don't consume examples/.

Tests: 560 passed, 0 failed, 4 ignored (was 558 + 3 T1 new -
1 transit carve_out_inventory #[ignore] = 560 active).

Tasks 6-12 (bench-driver suffix flips, e2e diff-test rewrite + 4
additional raw-JSON-inspect handlers, .ail.json deletion, retiring
obsolete roundtrip tests + schema_coverage corpus flip, §C3
DESIGN.md restatement, §A4 doctrine edits, WhatsNew + roadmap
strike) ship in the next dispatch on this iter ID.

Known debt inherited to T6-12: 4 raw-JSON-inspect tests in e2e.rs
(borrow_own_demo / reuse_as_demo / render_parse_round_trip_canonical
/ ail_run_accepts_ail_source_with_same_stdout_as_ail_json dual-form
smoke) need rewrite or #[ignore] before T8 deletion; recorded in
journal Concerns + Known debt sections.
This commit is contained in:
2026-05-13 11:12:48 +02:00
parent 1a065b37f1
commit 77b28ad64d
142 changed files with 2314 additions and 840 deletions
Generated
+3
View File
@@ -36,6 +36,7 @@ version = "0.0.1"
dependencies = [
"ailang-check",
"ailang-core",
"ailang-surface",
"indexmap",
"thiserror",
]
@@ -44,6 +45,7 @@ dependencies = [
name = "ailang-core"
version = "0.0.1"
dependencies = [
"ailang-surface",
"blake3",
"indexmap",
"serde",
@@ -57,6 +59,7 @@ name = "ailang-prose"
version = "0.0.1"
dependencies = [
"ailang-core",
"ailang-surface",
]
[[package]]
@@ -0,0 +1,30 @@
{
"iter_id": "form-a.1",
"date": "2026-05-13",
"mode": "standard",
"outcome": "DONE",
"task_range": [1, 5],
"tasks_total": 5,
"tasks_completed": 5,
"reloops_per_task": {
"1": 0,
"2": 1,
"3": 0,
"4": 0,
"5": 0
},
"review_loops_spec": 0,
"review_loops_quality": 0,
"blocked_reason": null,
"notes": {
"task2_reloop": "Plan recon defect: load_workspace prefers .ail siblings since ext-cli.1, so bulk-render of .ail siblings broke 7 Group-A tests whose entry was .ail.json. Inline forward-pull of Task-3 migrations + 4 transient #[ignore]'s on workspace.rs mod tests (relocated cleanly in Task 5).",
"test_count_pre_iter": 558,
"test_count_post_iter_half": 560,
"test_count_ignored_post_iter_half": 4,
"test_count_ignored_breakdown": "1 carve_out_inventory (T8 un-ignores) + 3 doctests (pre-existing)",
"corpus_ail_pre_iter": 58,
"corpus_ail_post_iter_half": 157,
"corpus_ail_json_post_iter_half": 164,
"corpus_ail_json_target_post_T8": 8
}
}
@@ -19,13 +19,13 @@
use ailang_check::{check_workspace, monomorphise_workspace};
use ailang_core::ast::{Def, Term};
use ailang_core::workspace::load_workspace;
use ailang_surface::load_workspace;
use std::path::PathBuf;
fn fixture_path() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../examples")
.join("show_user_adt.ail.json")
.join("show_user_adt.ail")
}
#[test]
+1 -1
View File
@@ -166,7 +166,7 @@ fn check_human_mode_emits_actionable_message_to_stderr() {
/// form used to succeed.
#[test]
fn check_ordering_match_post_migration_is_clean() {
let fixture = examples_dir().join("ordering_match.ail.json");
let fixture = examples_dir().join("ordering_match.ail");
let output = Command::new(ail_bin())
.args(["check", "--json", fixture.to_str().unwrap()])
.output()
+69 -69
View File
@@ -78,7 +78,7 @@ fn build_and_run_with_alloc(example: &str, alloc: &str) -> String {
#[test]
fn sum_1_to_10_is_55() {
let stdout = build_and_run("sum.ail.json");
let stdout = build_and_run("sum.ail");
assert_eq!(stdout.trim(), "55");
}
@@ -86,20 +86,20 @@ fn sum_1_to_10_is_55() {
/// phi block tracking would produce a wrong result here.
#[test]
fn max3_picks_largest() {
let stdout = build_and_run("max3.ail.json");
let stdout = build_and_run("max3.ail");
assert_eq!(stdout.trim(), "17");
}
#[test]
fn hello_world_str_lit() {
let stdout = build_and_run("hello.ail.json");
let stdout = build_and_run("hello.ail");
assert_eq!(stdout.trim(), "Hello, AILang.");
}
/// Guards ADT codegen + match: recursive list, sum_list via match on Cons/Nil.
#[test]
fn list_sum_via_match() {
let stdout = build_and_run("list.ail.json");
let stdout = build_and_run("list.ail");
assert_eq!(stdout.trim(), "42");
}
@@ -107,7 +107,7 @@ fn list_sum_via_match() {
/// produce 42, exercising fn-typed parameters and indirect call.
#[test]
fn higher_order_apply_inc() {
let stdout = build_and_run("hof.ail.json");
let stdout = build_and_run("hof.ail");
assert_eq!(stdout.trim(), "42");
}
@@ -117,7 +117,7 @@ fn higher_order_apply_inc() {
/// and apply's indirect call unpacks both halves.
#[test]
fn closure_captures_let_n() {
let stdout = build_and_run("closure.ail.json");
let stdout = build_and_run("closure.ail");
assert_eq!(stdout.trim(), "42");
}
@@ -127,7 +127,7 @@ fn closure_captures_let_n() {
/// `map (\x. x * 2)` over `[1, 2, 3]` then print each element.
#[test]
fn list_map_doubles_then_prints() {
let stdout = build_and_run("list_map.ail.json");
let stdout = build_and_run("list_map.ail");
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines, vec!["2", "4", "6"]);
}
@@ -141,7 +141,7 @@ fn list_map_doubles_then_prints() {
/// effectful tail call.
#[test]
fn list_map_poly_inc_then_prints() {
let stdout = build_and_run("list_map_poly.ail.json");
let stdout = build_and_run("list_map_poly.ail");
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines, vec!["2", "3", "4"]);
}
@@ -156,7 +156,7 @@ fn list_map_poly_inc_then_prints() {
/// scrutinee with a wildcard fallback is not currently in the grammar.
#[test]
fn gc_handles_recursive_list_construction() {
let stdout = build_and_run("gc_stress.ail.json");
let stdout = build_and_run("gc_stress.ail");
assert_eq!(
stdout.trim(),
"1275",
@@ -177,7 +177,7 @@ fn gc_handles_recursive_list_construction() {
/// for the Box and zero `@GC_malloc` calls inside `peek`/`count`.
#[test]
fn iter17a_local_box_alloca() {
let stdout = build_and_run("escape_local_demo.ail.json");
let stdout = build_and_run("escape_local_demo.ail");
assert_eq!(
stdout.lines().collect::<Vec<_>>(),
vec!["42", "42", "5", "0"],
@@ -190,7 +190,7 @@ fn iter17a_local_box_alloca() {
let workspace = Path::new(manifest_dir).parent().unwrap().parent().unwrap();
let src = workspace
.join("examples")
.join("escape_local_demo.ail.json");
.join("escape_local_demo.ail");
let tmp = std::env::temp_dir().join(format!(
"ailang_iter17a_alloca_{}",
std::process::id()
@@ -237,7 +237,7 @@ fn iter17a_local_box_alloca() {
fn iter14e_print_list_recursion_emits_musttail() {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let workspace = Path::new(manifest_dir).parent().unwrap().parent().unwrap();
let src = workspace.join("examples").join("list_map_poly.ail.json");
let src = workspace.join("examples").join("list_map_poly.ail");
let tmp = std::env::temp_dir().join(format!(
"ailang_iter14e_musttail_{}",
std::process::id()
@@ -290,7 +290,7 @@ fn iter14e_print_list_recursion_emits_musttail() {
/// recursion + branching without surprises.
#[test]
fn insertion_sort_orders_list() {
let stdout = build_and_run("sort.ail.json");
let stdout = build_and_run("sort.ail");
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(
lines,
@@ -303,7 +303,7 @@ fn insertion_sort_orders_list() {
/// triggers a separate specialised LLVM fn. Output: 42, then "true".
#[test]
fn polymorphic_id_at_int_and_bool() {
let stdout = build_and_run("poly_id.ail.json");
let stdout = build_and_run("poly_id.ail");
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines, vec!["42", "true"]);
}
@@ -314,7 +314,7 @@ fn polymorphic_id_at_int_and_bool() {
/// monomorphised body (a fn-typed param survives substitution).
#[test]
fn polymorphic_apply_with_fn_param() {
let stdout = build_and_run("poly_apply.ail.json");
let stdout = build_and_run("poly_apply.ail");
assert_eq!(stdout.trim(), "42");
}
@@ -326,7 +326,7 @@ fn polymorphic_apply_with_fn_param() {
/// down into a polymorphic fn body.
#[test]
fn parameterised_box_round_trip() {
let stdout = build_and_run("box.ail.json");
let stdout = build_and_run("box.ail");
assert_eq!(stdout.trim(), "42");
}
@@ -336,7 +336,7 @@ fn parameterised_box_round_trip() {
/// `x : Int`, not `x : a`).
#[test]
fn parameterised_maybe_match() {
let stdout = build_and_run("maybe_int.ail.json");
let stdout = build_and_run("maybe_int.ail");
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines, vec!["7", "99"]);
}
@@ -351,7 +351,7 @@ fn parameterised_maybe_match() {
/// flows end-to-end through check, codegen, and runtime.
#[test]
fn cross_module_maybe_demo() {
let stdout = build_and_run("std_maybe_demo.ail.json");
let stdout = build_and_run("std_maybe_demo.ail");
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines, vec!["7", "99", "true", "true", "42"]);
}
@@ -365,7 +365,7 @@ fn cross_module_maybe_demo() {
/// and fail to unify against the qualified scrutinee args.
#[test]
fn std_list_demo() {
let stdout = build_and_run("std_list_demo.ail.json");
let stdout = build_and_run("std_list_demo.ail");
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(
lines,
@@ -395,7 +395,7 @@ fn std_list_demo() {
/// happens at N=1000.
#[test]
fn std_list_stress_1000_element_folds() {
let stdout = build_and_run("std_list_stress.ail.json");
let stdout = build_and_run("std_list_stress.ail");
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(
lines,
@@ -457,7 +457,7 @@ fn render_parse_round_trip_canonical() {
/// the inner binders would be silently dropped by the codegen.
#[test]
fn nested_ctor_pattern_first_two_sum() {
let stdout = build_and_run("nested_pat.ail.json");
let stdout = build_and_run("nested_pat.ail");
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines, vec!["30"]);
}
@@ -471,7 +471,7 @@ fn nested_ctor_pattern_first_two_sum() {
/// for the substituted MkPair output.
#[test]
fn std_pair_demo() {
let stdout = build_and_run("std_pair_demo.ail.json");
let stdout = build_and_run("std_pair_demo.ail");
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines, vec!["7", "9", "9", "7", "8", "18"]);
}
@@ -488,7 +488,7 @@ fn std_pair_demo() {
/// another instantiation.
#[test]
fn std_either_demo() {
let stdout = build_and_run("std_either_demo.ail.json");
let stdout = build_and_run("std_either_demo.ail");
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(
lines,
@@ -513,7 +513,7 @@ fn std_either_demo() {
/// `[Left 1, Right 10, Left 2, Right 20, Right 30]`.
#[test]
fn std_either_list_demo() {
let stdout = build_and_run("std_either_list_demo.ail.json");
let stdout = build_and_run("std_either_list_demo.ail");
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines, vec!["2", "3", "2", "3"]);
}
@@ -528,7 +528,7 @@ fn std_either_list_demo() {
/// n>length) on each combinator. Expected stdout: 0, 3, 5, 5, 3, 0.
#[test]
fn std_list_more_demo() {
let stdout = build_and_run("std_list_more_demo.ail.json");
let stdout = build_and_run("std_list_more_demo.ail");
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines, vec!["0", "3", "5", "5", "3", "0"]);
}
@@ -546,7 +546,7 @@ fn std_list_more_demo() {
/// wrong values.
#[test]
fn local_rec_factorial_demo() {
let stdout = build_and_run("local_rec_demo.ail.json");
let stdout = build_and_run("local_rec_demo.ail");
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines, vec!["1", "6", "120"]);
}
@@ -563,7 +563,7 @@ fn local_rec_factorial_demo() {
/// sum_below(10)=45.
#[test]
fn local_rec_capture_demo() {
let stdout = build_and_run("local_rec_capture.ail.json");
let stdout = build_and_run("local_rec_capture.ail");
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines, vec!["0", "10", "45"]);
}
@@ -581,7 +581,7 @@ fn local_rec_capture_demo() {
/// count_below(15)=9 (i in 1..9 are below threshold 10).
#[test]
fn local_rec_let_capture_demo() {
let stdout = build_and_run("local_rec_let_capture.ail.json");
let stdout = build_and_run("local_rec_let_capture.ail");
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines, vec!["0", "5", "9"]);
}
@@ -601,7 +601,7 @@ fn local_rec_let_capture_demo() {
/// count_below(MkPair 10 15)=9.
#[test]
fn local_rec_match_capture_demo() {
let stdout = build_and_run("local_rec_match_capture.ail.json");
let stdout = build_and_run("local_rec_match_capture.ail");
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines, vec!["0", "5", "9"]);
}
@@ -617,7 +617,7 @@ fn local_rec_match_capture_demo() {
/// Expected stdout: 120 (= apply5 factorial = factorial 5).
#[test]
fn local_rec_as_value_demo() {
let stdout = build_and_run("local_rec_as_value.ail.json");
let stdout = build_and_run("local_rec_as_value.ail");
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines, vec!["120"]);
}
@@ -633,7 +633,7 @@ fn local_rec_as_value_demo() {
/// Expected stdout: 1320 (= 5*4*3*2*(1+10)), 12120 (= 5*4*3*2*(1+100)).
#[test]
fn local_rec_as_value_capture_demo() {
let stdout = build_and_run("local_rec_as_value_capture.ail.json");
let stdout = build_and_run("local_rec_as_value_capture.ail");
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines, vec!["1320", "12120"]);
}
@@ -661,7 +661,7 @@ fn local_rec_as_value_capture_demo() {
/// applied 4 times to false — even count of flips ⇒ false).
#[test]
fn poly_rec_capture_demo() {
let stdout = build_and_run("poly_rec_capture.ail.json");
let stdout = build_and_run("poly_rec_capture.ail");
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines, vec!["5", "false"]);
}
@@ -686,7 +686,7 @@ fn poly_rec_capture_demo() {
/// 1..=n. Expected stdout: 1, 6, 15 for n in {1, 3, 5}.
#[test]
fn nested_let_rec_demo() {
let stdout = build_and_run("nested_let_rec.ail.json");
let stdout = build_and_run("nested_let_rec.ail");
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines, vec!["1", "6", "15"]);
}
@@ -781,7 +781,7 @@ fn diff_detects_changed_def() {
fn diff_no_changes_exit_zero() {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let workspace = Path::new(manifest_dir).parent().unwrap().parent().unwrap();
let src = workspace.join("examples").join("sum.ail.json");
let src = workspace.join("examples").join("sum.ail");
let output = Command::new(ail_bin())
.args([
@@ -822,7 +822,7 @@ fn diff_no_changes_exit_zero() {
fn workspace_lists_imported_modules() {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let workspace = Path::new(manifest_dir).parent().unwrap().parent().unwrap();
let entry = workspace.join("examples").join("ws_main.ail.json");
let entry = workspace.join("examples").join("ws_main.ail");
let output = Command::new(ail_bin())
.args(["workspace", entry.to_str().unwrap(), "--json"])
@@ -866,7 +866,7 @@ fn workspace_lists_imported_modules() {
fn check_workspace_resolves_import() {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let workspace = Path::new(manifest_dir).parent().unwrap().parent().unwrap();
let entry = workspace.join("examples").join("ws_main.ail.json");
let entry = workspace.join("examples").join("ws_main.ail");
let output = Command::new(ail_bin())
.args(["check", entry.to_str().unwrap(), "--json"])
@@ -891,7 +891,7 @@ fn check_workspace_resolves_import() {
/// `@ail_ws_main_main` must call `@ail_ws_lib_add(2,3)` and print `5`.
#[test]
fn workspace_build_runs_imported_fn() {
let stdout = build_and_run("ws_main.ail.json");
let stdout = build_and_run("ws_main.ail");
assert_eq!(stdout.trim(), "5", "ws_lib.add(2,3) should print 5");
}
@@ -904,7 +904,7 @@ fn workspace_build_runs_imported_fn() {
/// `imports: ["prelude"]` entry in the source module.
#[test]
fn ordering_match_via_prelude_prints_1() {
let stdout = build_and_run("ordering_match.ail.json");
let stdout = build_and_run("ordering_match.ail");
assert_eq!(stdout, "1\n");
}
@@ -923,7 +923,7 @@ fn ordering_match_via_prelude_prints_1() {
/// is type-driven, and the run completes cleanly.
#[test]
fn compare_primitives_smoke_prints_1_2_3_thrice() {
let stdout = build_and_run("compare_primitives_smoke.ail.json");
let stdout = build_and_run("compare_primitives_smoke.ail");
assert_eq!(stdout, "1\n2\n3\n1\n2\n3\n1\n2\n3\n");
}
@@ -935,7 +935,7 @@ fn compare_primitives_smoke_prints_1_2_3_thrice() {
/// call. Equal-call lines are `1`, unequal-call lines are `0`.
#[test]
fn eq_primitives_smoke_compiles_and_runs() {
let stdout = build_and_run("eq_primitives_smoke.ail.json");
let stdout = build_and_run("eq_primitives_smoke.ail");
assert_eq!(stdout, "1\n0\n1\n0\n1\n0\n");
}
@@ -945,7 +945,7 @@ fn eq_primitives_smoke_compiles_and_runs() {
fn manifest_workspace_lists_all_defs() {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let workspace = Path::new(manifest_dir).parent().unwrap().parent().unwrap();
let entry = workspace.join("examples").join("ws_main.ail.json");
let entry = workspace.join("examples").join("ws_main.ail");
let output = Command::new(ail_bin())
.args([
@@ -988,7 +988,7 @@ fn manifest_workspace_lists_all_defs() {
fn describe_workspace_resolves_qualified_name() {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let workspace = Path::new(manifest_dir).parent().unwrap().parent().unwrap();
let entry = workspace.join("examples").join("ws_main.ail.json");
let entry = workspace.join("examples").join("ws_main.ail");
let output = Command::new(ail_bin())
.args([
@@ -1031,7 +1031,7 @@ fn describe_workspace_resolves_qualified_name() {
fn deps_filters_builtins_params_locals() {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let workspace = Path::new(manifest_dir).parent().unwrap().parent().unwrap();
let src = workspace.join("examples").join("sum.ail.json");
let src = workspace.join("examples").join("sum.ail");
let output = Command::new(ail_bin())
.args(["deps", src.to_str().unwrap(), "--of", "sum", "--json"])
@@ -1070,7 +1070,7 @@ fn deps_filters_builtins_params_locals() {
fn deps_workspace_filters_builtins_and_params() {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let workspace = Path::new(manifest_dir).parent().unwrap().parent().unwrap();
let entry = workspace.join("examples").join("ws_main.ail.json");
let entry = workspace.join("examples").join("ws_main.ail");
let output = Command::new(ail_bin())
.args(["deps", entry.to_str().unwrap(), "--workspace", "--json"])
@@ -1096,7 +1096,7 @@ fn deps_workspace_filters_builtins_and_params() {
fn deps_workspace_includes_cross_module() {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let workspace = Path::new(manifest_dir).parent().unwrap().parent().unwrap();
let entry = workspace.join("examples").join("ws_main.ail.json");
let entry = workspace.join("examples").join("ws_main.ail");
let output = Command::new(ail_bin())
.args(["deps", entry.to_str().unwrap(), "--workspace", "--json"])
@@ -1266,7 +1266,7 @@ fn check_json_unbound_var() {
/// chain default).
#[test]
fn lit_pat_demo() {
let stdout = build_and_run("lit_pat.ail.json");
let stdout = build_and_run("lit_pat.ail");
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines, vec!["100", "200", "999", "-1", "0", "7"]);
}
@@ -1281,7 +1281,7 @@ fn lit_pat_demo() {
/// print the expected outputs.
#[test]
fn unreachable_demo() {
let stdout = build_and_run("unreachable_demo.ail.json");
let stdout = build_and_run("unreachable_demo.ail");
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines, vec!["4", "5"]);
}
@@ -1321,7 +1321,7 @@ fn borrow_own_demo_modes_are_metadata_only() {
"expected `param_modes:[\"own\"]` in canonical JSON; the schema for `(own T)` regressed"
);
let stdout = build_and_run("borrow_own_demo.ail.json");
let stdout = build_and_run("borrow_own_demo.ail");
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(lines, vec!["3", "6"]);
}
@@ -1338,7 +1338,7 @@ fn borrow_own_demo_modes_are_metadata_only() {
/// non-Int args at typecheck.
#[test]
fn eq_demo() {
let stdout = build_and_run("eq_demo.ail.json");
let stdout = build_and_run("eq_demo.ail");
let lines: Vec<&str> = stdout.lines().collect();
assert_eq!(
lines,
@@ -1374,7 +1374,7 @@ fn alloc_rc_produces_same_stdout_as_gc() {
/// passthrough with `call void @ailang_rc_inc` under `--alloc=rc`.
#[test]
fn clone_demo_is_identity_in_18c1() {
let stdout = build_and_run("clone_demo.ail.json");
let stdout = build_and_run("clone_demo.ail");
assert_eq!(stdout.trim(), "42");
}
@@ -1413,11 +1413,11 @@ fn reuse_as_demo_under_rc_uses_inplace_rewrite() {
);
// (2) gc baseline.
let stdout_gc = build_and_run_with_alloc("reuse_as_demo.ail.json", "gc");
let stdout_gc = build_and_run_with_alloc("reuse_as_demo.ail", "gc");
assert_eq!(stdout_gc.trim(), "9");
// (3) rc matches gc, (4) clean exit (build_and_run_with_alloc
// panics on non-zero status).
let stdout_rc = build_and_run_with_alloc("reuse_as_demo.ail.json", "rc");
let stdout_rc = build_and_run_with_alloc("reuse_as_demo.ail", "rc");
assert_eq!(stdout_rc.trim(), "9");
// (5) Inspect the rc IR text. We re-lower via the codegen API
@@ -2176,7 +2176,7 @@ fn build_and_run_with_rc_stats(example: &str) -> (String, u64, u64, i64) {
#[test]
fn alloc_rc_explicit_mode_tail_sum_does_not_leak_outer_cells() {
let (stdout, allocs, frees, live) =
build_and_run_with_rc_stats("rc_tail_sum_explicit_leak.ail.json");
build_and_run_with_rc_stats("rc_tail_sum_explicit_leak.ail");
assert_eq!(
stdout.trim(),
"4950",
@@ -2211,7 +2211,7 @@ fn alloc_rc_explicit_mode_tail_sum_does_not_leak_outer_cells() {
#[test]
fn alloc_rc_let_binder_for_owned_returning_app_drops_at_scope_close() {
let (stdout, allocs, frees, live) =
build_and_run_with_rc_stats("rc_let_owned_app_leak.ail.json");
build_and_run_with_rc_stats("rc_let_owned_app_leak.ail");
assert_eq!(
stdout.trim(),
"1",
@@ -2255,7 +2255,7 @@ fn alloc_rc_let_binder_for_owned_returning_app_drops_at_scope_close() {
#[test]
fn alloc_rc_let_binder_for_implicit_returning_app_does_not_drop() {
let (stdout, allocs, frees, live) =
build_and_run_with_rc_stats("rc_let_implicit_returning_app.ail.json");
build_and_run_with_rc_stats("rc_let_implicit_returning_app.ail");
assert_eq!(stdout.trim(), "7", "alloc(7) -> unbox should print 7");
assert_eq!(
live, 1,
@@ -2339,7 +2339,7 @@ fn alloc_rc_let_alias_of_implicit_param_does_not_dec_borrowed_children() {
#[test]
fn alloc_rc_partial_drop_at_fn_return_does_not_leak_unmoved_fields() {
let (stdout, allocs, frees, live) =
build_and_run_with_rc_stats("rc_own_param_partial_drop_leak.ail.json");
build_and_run_with_rc_stats("rc_own_param_partial_drop_leak.ail");
assert_eq!(
stdout.trim(),
"1",
@@ -2378,7 +2378,7 @@ fn alloc_rc_partial_drop_at_fn_return_does_not_leak_unmoved_fields() {
#[test]
fn alloc_rc_partial_drop_at_arm_close_does_not_leak_unmoved_fields() {
let (stdout, allocs, frees, live) =
build_and_run_with_rc_stats("rc_match_arm_partial_drop_leak.ail.json");
build_and_run_with_rc_stats("rc_match_arm_partial_drop_leak.ail");
assert_eq!(
stdout.trim(),
"1",
@@ -2411,7 +2411,7 @@ fn alloc_rc_partial_drop_at_arm_close_does_not_leak_unmoved_fields() {
#[test]
fn alloc_rc_partial_drop_at_app_let_close_does_not_leak_unmoved_fields() {
let (stdout, allocs, frees, live) =
build_and_run_with_rc_stats("rc_app_let_partial_drop_leak.ail.json");
build_and_run_with_rc_stats("rc_app_let_partial_drop_leak.ail");
assert_eq!(
stdout.trim(),
"1",
@@ -2571,7 +2571,7 @@ fn ail_run_accepts_ail_source_with_same_stdout_as_ail_json() {
/// every alloc strategy after the hs.4 hoist).
#[test]
fn int_to_str_smoke() {
let out = build_and_run("int_to_str_smoke.ail.json");
let out = build_and_run("int_to_str_smoke.ail");
assert_eq!(out, "42\n");
}
@@ -2584,7 +2584,7 @@ fn int_to_str_smoke() {
/// trip-wire that surfaces it.
#[test]
fn float_to_str_smoke() {
let out = build_and_run("float_to_str_smoke.ail.json");
let out = build_and_run("float_to_str_smoke.ail");
assert_eq!(out, "3.5\n");
}
@@ -2600,7 +2600,7 @@ fn float_to_str_smoke() {
#[test]
fn int_to_str_drop_balances_rc_stats() {
let (stdout, allocs, frees, live) =
build_and_run_with_rc_stats("int_to_str_drop_rc.ail.json");
build_and_run_with_rc_stats("int_to_str_drop_rc.ail");
assert_eq!(stdout, "42\n");
assert!(allocs >= 1, "expected at least one heap-Str allocation; got allocs={allocs}");
assert_eq!(allocs, frees, "every heap-Str alloc must be freed; allocs={allocs}, frees={frees}");
@@ -2616,7 +2616,7 @@ fn int_to_str_drop_balances_rc_stats() {
#[test]
fn str_field_in_adt_drops_heap_str_correctly() {
let (stdout, allocs, frees, live) =
build_and_run_with_rc_stats("str_field_in_adt_heap.ail.json");
build_and_run_with_rc_stats("str_field_in_adt_heap.ail");
assert_eq!(stdout, "42\n");
assert!(allocs >= 2, "expected ADT cell + heap-Str slab; got allocs={allocs}");
assert_eq!(allocs, frees, "every RC slab must be freed; allocs={allocs}, frees={frees}");
@@ -2631,7 +2631,7 @@ fn str_field_in_adt_drops_heap_str_correctly() {
#[test]
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.json");
build_and_run_with_rc_stats("int_to_print_int_borrow.ail");
assert_eq!(stdout, "7\n");
assert_eq!(allocs, 0, "Int arg should not trigger any RC alloc; got allocs={allocs}");
assert_eq!(frees, 0, "Int arg should not trigger any RC free; got frees={frees}");
@@ -2649,7 +2649,7 @@ fn int_arg_to_effect_op_does_not_rc_track() {
#[test]
fn heap_str_repeated_print_balances_rc_stats() {
let (stdout, allocs, frees, live) =
build_and_run_with_rc_stats("heap_str_repeated_print_borrow.ail.json");
build_and_run_with_rc_stats("heap_str_repeated_print_borrow.ail");
assert_eq!(stdout, "42\n42\n");
assert_eq!(allocs, 1, "expected exactly one heap-Str slab; got allocs={allocs}");
assert_eq!(frees, 1, "expected exactly one free; got frees={frees}");
@@ -2665,7 +2665,7 @@ fn heap_str_repeated_print_balances_rc_stats() {
#[test]
fn bool_to_str_drop_balances_rc_stats() {
let (stdout, allocs, frees, live) =
build_and_run_with_rc_stats("bool_to_str_drop_rc.ail.json");
build_and_run_with_rc_stats("bool_to_str_drop_rc.ail");
assert_eq!(stdout, "true\n");
assert_eq!(allocs, 1, "expected exactly one heap-Str slab; got allocs={allocs}");
assert_eq!(frees, 1, "expected exactly one free; got frees={frees}");
@@ -2678,7 +2678,7 @@ fn bool_to_str_drop_balances_rc_stats() {
/// "true" slab.
#[test]
fn bool_to_str_emits_true_branch() {
let out = build_and_run("bool_to_str_drop_rc.ail.json");
let out = build_and_run("bool_to_str_drop_rc.ail");
assert_eq!(out, "true\n");
}
@@ -2686,7 +2686,7 @@ fn bool_to_str_emits_true_branch() {
/// content of `ailang_bool_to_str`'s "false" slab.
#[test]
fn bool_to_str_emits_false_branch() {
let out = build_and_run("bool_to_str_smoke_false.ail.json");
let out = build_and_run("bool_to_str_smoke_false.ail");
assert_eq!(out, "false\n");
}
@@ -2698,7 +2698,7 @@ fn bool_to_str_emits_false_branch() {
#[test]
fn str_clone_drop_balances_rc_stats() {
let (stdout, allocs, frees, live) =
build_and_run_with_rc_stats("str_clone_drop_rc.ail.json");
build_and_run_with_rc_stats("str_clone_drop_rc.ail");
assert_eq!(stdout, "hello\n");
assert_eq!(allocs, 1, "expected exactly one heap-Str clone slab; got allocs={allocs}");
assert_eq!(frees, 1, "expected exactly one free; got frees={frees}");
@@ -2716,7 +2716,7 @@ fn str_clone_drop_balances_rc_stats() {
#[test]
fn str_clone_cross_realisation_uniform_abi() {
let (stdout, allocs, frees, live) =
build_and_run_with_rc_stats("str_clone_cross_realisation.ail.json");
build_and_run_with_rc_stats("str_clone_cross_realisation.ail");
assert_eq!(stdout, "42\nabc\n");
assert_eq!(allocs, 3, "expected one int_to_str slab + two str_clone slabs; got allocs={allocs}");
assert_eq!(frees, 3, "expected three matching frees; got frees={frees}");
+2 -2
View File
@@ -8,7 +8,7 @@
//! than seeing a bare "no instance" message.
use ailang_check::check_workspace;
use ailang_core::workspace::load_workspace;
use ailang_surface::load_workspace;
use std::path::PathBuf;
fn fixture(name: &str) -> PathBuf {
@@ -19,7 +19,7 @@ fn fixture(name: &str) -> PathBuf {
#[test]
fn eq_at_float_fires_float_aware_noinstance() {
let ws = load_workspace(&fixture("eq_float_noinstance.ail.json")).expect("load");
let ws = load_workspace(&fixture("eq_float_noinstance.ail")).expect("load");
let diags = check_workspace(&ws);
assert!(!diags.is_empty(), "expected NoInstance diagnostic, got none");
+4 -4
View File
@@ -22,7 +22,7 @@
use ailang_check::{check_workspace, monomorphise_workspace};
use ailang_core::ast::Def;
use ailang_core::hash::def_hash;
use ailang_core::workspace::load_workspace;
use ailang_surface::load_workspace;
use std::path::PathBuf;
use std::process::Command;
@@ -59,7 +59,7 @@ fn build_and_run(fixture: &str) -> String {
#[test]
fn eq_ord_polymorphic_runs_end_to_end() {
let stdout = build_and_run("eq_ord_polymorphic.ail.json");
let stdout = build_and_run("eq_ord_polymorphic.ail");
// Helper `at_most(3, 5)` at Int = true → 1
// `at_most(true, false)` at Bool = false → 0
// `at_most("abc", "abd")` at Str = true → 1
@@ -70,7 +70,7 @@ fn eq_ord_polymorphic_runs_end_to_end() {
#[test]
fn eq_ord_user_adt_runs_end_to_end() {
let stdout = build_and_run("eq_ord_user_adt.ail.json");
let stdout = build_and_run("eq_ord_user_adt.ail");
// eq(MkIntBox 3, MkIntBox 3) = true → 1
// eq(MkIntBox 3, MkIntBox 5) = false → 0
// `io/print_int` emits one int per line; trimmed stdout is "1\n0".
@@ -79,7 +79,7 @@ fn eq_ord_user_adt_runs_end_to_end() {
#[test]
fn eq_ord_user_adt_eq_intbox_hash_stable() {
let ws = load_workspace(&fixture_path("eq_ord_user_adt.ail.json")).expect("load");
let ws = load_workspace(&fixture_path("eq_ord_user_adt.ail")).expect("load");
let diags = check_workspace(&ws);
assert!(diags.is_empty(), "typecheck failed: {diags:#?}");
let mono = monomorphise_workspace(&ws).expect("mono");
+1 -1
View File
@@ -20,7 +20,7 @@ fn workspace_root() -> PathBuf {
#[test]
fn floats_example_prints_expected_stdout() {
let root = workspace_root();
let example = root.join("examples").join("floats.ail.json");
let example = root.join("examples").join("floats.ail");
assert!(example.exists(), "expected fixture at {:?}", example);
// Build via `cargo run -p ail -- build` to use the in-tree CLI.
+5 -5
View File
@@ -134,22 +134,22 @@ fn check_ir_snapshot(example: &str, snapshot_name: &str) {
#[test]
fn ir_snapshot_sum() {
check_ir_snapshot("sum.ail.json", "sum.ll");
check_ir_snapshot("sum.ail", "sum.ll");
}
#[test]
fn ir_snapshot_max3() {
check_ir_snapshot("max3.ail.json", "max3.ll");
check_ir_snapshot("max3.ail", "max3.ll");
}
#[test]
fn ir_snapshot_hello() {
check_ir_snapshot("hello.ail.json", "hello.ll");
check_ir_snapshot("hello.ail", "hello.ll");
}
#[test]
fn ir_snapshot_list() {
check_ir_snapshot("list.ail.json", "list.ll");
check_ir_snapshot("list.ail", "list.ll");
}
/// Guards workspace lowering (Iter 5c): the entry module `ws_main`
@@ -158,5 +158,5 @@ fn ir_snapshot_list() {
/// `@main` calls `@ail_ws_main_main`.
#[test]
fn ir_snapshot_ws_main() {
check_ir_snapshot("ws_main.ail.json", "ws_main.ll");
check_ir_snapshot("ws_main.ail", "ws_main.ll");
}
+3 -3
View File
@@ -6,21 +6,21 @@
use ailang_core::ast::Def;
use ailang_core::hash::def_hash;
use ailang_core::workspace::load_workspace;
use ailang_surface::load_workspace;
use std::path::PathBuf;
fn fixture_path() -> PathBuf {
let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
d.pop();
d.pop();
d.join("examples").join("mono_hash_pin_smoke.ail.json")
d.join("examples").join("mono_hash_pin_smoke.ail")
}
fn show_fixture_path() -> PathBuf {
let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
d.pop();
d.pop();
d.join("examples").join("show_mono_pin_smoke.ail.json")
d.join("examples").join("show_mono_pin_smoke.ail")
}
#[test]
+1 -1
View File
@@ -40,7 +40,7 @@ fn mono_pass_handles_recursive_fn_in_class_workspace() {
let workspace = Path::new(manifest_dir).parent().unwrap().parent().unwrap();
let src = workspace
.join("examples")
.join("test_mono_recursive_fn_bug.ail.json");
.join("test_mono_recursive_fn_bug.ail");
let tmp = std::env::temp_dir().join(format!(
"ailang_mono_recursive_fn_{}",
std::process::id()
+7 -7
View File
@@ -7,7 +7,7 @@
use ailang_check::mono::{mono_target_key, MonoTarget};
use ailang_core::ast::{Def, Type};
use ailang_core::workspace::load_workspace;
use ailang_surface::load_workspace;
use std::path::PathBuf;
fn fixture(name: &str) -> PathBuf {
@@ -19,7 +19,7 @@ fn fixture(name: &str) -> PathBuf {
#[test]
fn cmp_max_smoke_produces_both_mono_symbols_in_one_fixpoint() {
let ws = load_workspace(&fixture("cmp_max_smoke.ail.json"))
let ws = load_workspace(&fixture("cmp_max_smoke.ail"))
.expect("workspace loads");
let diags = ailang_check::check_workspace(&ws);
assert!(diags.is_empty(), "typecheck diagnostics: {diags:?}");
@@ -62,7 +62,7 @@ fn cmp_max_smoke_produces_both_mono_symbols_in_one_fixpoint() {
#[test]
fn collect_mono_targets_emits_free_fn_target_for_cmp_max_at_int() {
use ailang_check::mono::collect_mono_targets;
let ws = load_workspace(&fixture("cmp_max_smoke.ail.json")).expect("workspace loads");
let ws = load_workspace(&fixture("cmp_max_smoke.ail")).expect("workspace loads");
let diags = ailang_check::check_workspace(&ws);
assert!(diags.is_empty(), "typecheck diagnostics: {diags:?}");
let env = ailang_check::build_check_env(&ws);
@@ -93,7 +93,7 @@ fn cmp_max_smoke_runs_end_to_end() {
use std::process::Command;
let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let workspace = manifest_dir.parent().unwrap().parent().unwrap();
let src = workspace.join("examples").join("cmp_max_smoke.ail.json");
let src = workspace.join("examples").join("cmp_max_smoke.ail");
let tmp = std::env::temp_dir().join(format!(
"ailang_cmp_max_smoke_{}",
std::process::id()
@@ -123,7 +123,7 @@ fn workspace_with_only_poly_free_fn_and_no_classes_still_monomorphises() {
// the predicate to also include `Def::Fn` with `Type::Forall`
// as a specialisable target — which makes the predicate true
// for any workspace with poly free fns (regardless of classes).
let ws = load_workspace(&fixture("poly_id.ail.json")).expect("workspace loads");
let ws = load_workspace(&fixture("poly_id.ail")).expect("workspace loads");
let diags = ailang_check::check_workspace(&ws);
assert!(diags.is_empty(), "typecheck diagnostics: {diags:?}");
let post_mono = ailang_check::monomorphise_workspace(&ws).expect("mono green");
@@ -144,7 +144,7 @@ fn workspace_with_only_poly_free_fn_and_no_classes_still_monomorphises() {
#[test]
fn rewrite_mono_calls_rewrites_bare_poly_free_fn_to_mono_symbol() {
let ws = load_workspace(&fixture("cmp_max_smoke.ail.json")).expect("workspace loads");
let ws = load_workspace(&fixture("cmp_max_smoke.ail")).expect("workspace loads");
let diags = ailang_check::check_workspace(&ws);
assert!(diags.is_empty(), "typecheck diagnostics: {diags:?}");
let post_mono = ailang_check::monomorphise_workspace(&ws).expect("mono green");
@@ -170,7 +170,7 @@ fn rewrite_mono_calls_rewrites_bare_poly_free_fn_to_mono_symbol() {
#[test]
fn synthesise_mono_fn_free_fn_arm_substitutes_rigid_vars() {
use ailang_check::mono::synthesise_mono_fn_for_free_fn;
let ws = load_workspace(&fixture("cmp_max_smoke.ail.json")).expect("workspace loads");
let ws = load_workspace(&fixture("cmp_max_smoke.ail")).expect("workspace loads");
let diags = ailang_check::check_workspace(&ws);
assert!(diags.is_empty(), "typecheck diagnostics: {diags:?}");
+2 -2
View File
@@ -63,8 +63,8 @@ fn examples_dir() -> PathBuf {
/// `env.module_types` and validates the ctor by name there.
#[test]
fn mono_pass_handles_xmod_ctor_pattern() {
let entry = examples_dir().join("test_mono_ctor_main.ail.json");
let ws = ailang_core::load_workspace(&entry).expect("load");
let entry = examples_dir().join("test_mono_ctor_main.ail");
let ws = ailang_surface::load_workspace(&entry).expect("load");
// Pre-condition: the workspace typechecks cleanly. The bug is
// localised to the mono pass; the typecheck path does not have it.
+1 -1
View File
@@ -57,7 +57,7 @@ fn mono_pass_handles_qualified_cross_module_reference() {
let workspace = Path::new(manifest_dir).parent().unwrap().parent().unwrap();
let src = workspace
.join("examples")
.join("test_mono_imports_main.ail.json");
.join("test_mono_imports_main.ail");
let tmp = std::env::temp_dir().join(format!(
"ailang_mono_xmod_qualified_{}",
std::process::id()
+3 -3
View File
@@ -44,7 +44,7 @@ fn run_ail_check_json(entry: &str) -> std::process::Output {
/// `ambiguous-method-resolution` and names both candidate classes.
#[test]
fn mq3_two_show_ambiguous_fires_ambiguous_method_resolution() {
let out = run_ail_check_json("mq3_two_show_ambiguous.ail.json");
let out = run_ail_check_json("mq3_two_show_ambiguous.ail");
assert!(
!out.status.success(),
"expected check to reject ambiguous dispatch; stdout={} stderr={}",
@@ -76,7 +76,7 @@ fn mq3_two_show_ambiguous_fires_ambiguous_method_resolution() {
/// workspace as the ambiguous fixture. Typecheck succeeds.
#[test]
fn mq3_two_show_qualified_resolves_clean() {
let out = run_ail_check_json("mq3_two_show_qualified.ail.json");
let out = run_ail_check_json("mq3_two_show_qualified.ail");
assert!(
out.status.success(),
"expected check to succeed with explicit qualifier; stdout={} stderr={}",
@@ -92,7 +92,7 @@ fn mq3_two_show_qualified_resolves_clean() {
/// LLM-author sees the shadow.
#[test]
fn mq3_class_eq_vs_fn_eq_fn_wins_with_warning() {
let out = run_ail_check_json("mq3_class_eq_vs_fn_eq.ail.json");
let out = run_ail_check_json("mq3_class_eq_vs_fn_eq.ail");
assert!(
out.status.success(),
"expected check to succeed (fn wins); stdout={} stderr={}",
@@ -16,13 +16,13 @@
//! the negative case — confusing diagnostic, hard to bisect.
use ailang_check::check_workspace;
use ailang_core::workspace::load_workspace;
use ailang_surface::load_workspace;
use std::path::PathBuf;
fn fixture_path() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR"))
.join("../../examples")
.join("show_no_instance.ail.json")
.join("show_no_instance.ail")
}
#[test]
+6 -6
View File
@@ -12,7 +12,7 @@
//! the mono symbols are produced.
use ailang_check::{check_workspace, monomorphise_workspace};
use ailang_core::workspace::load_workspace;
use ailang_surface::load_workspace;
use std::path::PathBuf;
fn fixture(name: &str) -> PathBuf {
@@ -38,7 +38,7 @@ fn mono_symbol_names(workspace_name: &str) -> Vec<String> {
#[test]
fn ne_at_int_produces_mono_symbol() {
let names = mono_symbol_names("ne_at_int_smoke.ail.json");
let names = mono_symbol_names("ne_at_int_smoke.ail");
assert!(
names.iter().any(|n| n == "ne__Int"),
"expected ne__Int in workspace, found: {names:#?}"
@@ -47,7 +47,7 @@ fn ne_at_int_produces_mono_symbol() {
#[test]
fn lt_at_int_produces_mono_symbol() {
let names = mono_symbol_names("lt_at_int_smoke.ail.json");
let names = mono_symbol_names("lt_at_int_smoke.ail");
assert!(
names.iter().any(|n| n == "lt__Int"),
"expected lt__Int in workspace, found: {names:#?}"
@@ -56,7 +56,7 @@ fn lt_at_int_produces_mono_symbol() {
#[test]
fn le_at_str_produces_mono_symbol() {
let names = mono_symbol_names("le_at_str_smoke.ail.json");
let names = mono_symbol_names("le_at_str_smoke.ail");
assert!(
names.iter().any(|n| n == "le__Str"),
"expected le__Str in workspace, found: {names:#?}"
@@ -65,7 +65,7 @@ fn le_at_str_produces_mono_symbol() {
#[test]
fn gt_at_bool_produces_mono_symbol() {
let names = mono_symbol_names("gt_at_bool_smoke.ail.json");
let names = mono_symbol_names("gt_at_bool_smoke.ail");
assert!(
names.iter().any(|n| n == "gt__Bool"),
"expected gt__Bool in workspace, found: {names:#?}"
@@ -74,7 +74,7 @@ fn gt_at_bool_produces_mono_symbol() {
#[test]
fn ge_at_int_produces_mono_symbol() {
let names = mono_symbol_names("ge_at_int_smoke.ail.json");
let names = mono_symbol_names("ge_at_int_smoke.ail");
assert!(
names.iter().any(|n| n == "ge__Int"),
"expected ge__Int in workspace, found: {names:#?}"
+2 -2
View File
@@ -17,14 +17,14 @@
//! function values inside `InstanceMethod.body`).
use ailang_core::ast::{Def, Term};
use ailang_core::workspace::load_workspace;
use ailang_surface::load_workspace;
use std::path::PathBuf;
fn fixture_path() -> PathBuf {
let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
d.pop();
d.pop();
d.join("examples").join("show_print_smoke.ail.json")
d.join("examples").join("show_print_smoke.ail")
}
#[test]
+124
View File
@@ -45,6 +45,23 @@ fn list_json_fixtures() -> Vec<PathBuf> {
paths
}
fn list_ail_fixtures() -> Vec<PathBuf> {
let dir = examples_dir();
let mut paths: Vec<PathBuf> = std::fs::read_dir(&dir)
.unwrap_or_else(|e| panic!("read_dir({}): {e}", dir.display()))
.filter_map(|entry| entry.ok())
.map(|e| e.path())
.filter(|p| {
p.file_name()
.and_then(|n| n.to_str())
.map(|n| n.ends_with(".ail"))
.unwrap_or(false)
})
.collect();
paths.sort();
paths
}
fn strip_trailing_newlines(mut v: Vec<u8>) -> Vec<u8> {
while v.last() == Some(&b'\n') {
v.pop();
@@ -144,3 +161,110 @@ fn cli_render_then_parse_preserves_canonical_bytes_on_every_fixture() {
}
eprintln!("CLI roundtrip ok for {passed} fixtures");
}
/// CLI-pipeline idempotency (post-form-a-default-authoring §C3): for
/// every `.ail` fixture, the user-facing pipeline
/// `ail parse <ail> | ail render | ail parse` is byte-identical
/// to direct `ail parse <ail>`. Pins drift internal tests cannot see.
#[test]
fn cli_parse_then_render_then_parse_is_idempotent() {
let fixtures = list_ail_fixtures();
assert!(
!fixtures.is_empty(),
"no .ail fixtures found under {}",
examples_dir().display()
);
let tmpdir = tempfile::TempDir::new().expect("create tempdir");
let mut checked = 0usize;
let mut failures = Vec::<String>::new();
for ail_path in &fixtures {
// Direct parse → JSON tempfile.
let json1 = tmpdir.path().join(format!("v1_{checked}.ail.json"));
let parse1 = Command::new(ail_bin())
.args([
"parse",
ail_path.to_str().unwrap(),
"-o",
json1.to_str().unwrap(),
])
.output()
.expect("spawn ail parse #1");
if !parse1.status.success() {
failures.push(format!(
"{}: first ail parse exit={:?}\nstderr: {}",
ail_path.display(),
parse1.status.code(),
String::from_utf8_lossy(&parse1.stderr),
));
checked += 1;
continue;
}
// Render that JSON → .ail tempfile.
let ail_round = tmpdir.path().join(format!("round_{checked}.ail"));
let rendered = Command::new(ail_bin())
.args(["render", json1.to_str().unwrap()])
.output()
.expect("spawn ail render");
if !rendered.status.success() {
failures.push(format!(
"{}: ail render exit={:?}\nstderr: {}",
ail_path.display(),
rendered.status.code(),
String::from_utf8_lossy(&rendered.stderr),
));
checked += 1;
continue;
}
std::fs::write(&ail_round, &rendered.stdout)
.expect("write ail_round tempfile");
// Re-parse → second JSON tempfile.
let json2 = tmpdir.path().join(format!("v2_{checked}.ail.json"));
let parse2 = Command::new(ail_bin())
.args([
"parse",
ail_round.to_str().unwrap(),
"-o",
json2.to_str().unwrap(),
])
.output()
.expect("spawn ail parse #2");
if !parse2.status.success() {
failures.push(format!(
"{}: second ail parse exit={:?}\nstderr: {}",
ail_path.display(),
parse2.status.code(),
String::from_utf8_lossy(&parse2.stderr),
));
checked += 1;
continue;
}
// Assert the two canonical JSONs are byte-equal.
let b1 = std::fs::read(&json1).expect("read json1");
let b2 = std::fs::read(&json2).expect("read json2");
if b1 != b2 {
let s1 = String::from_utf8_lossy(&b1).into_owned();
let s2 = String::from_utf8_lossy(&b2).into_owned();
failures.push(format!(
"{}: CLI parse|render|parse non-idempotent.\n v1: {s1}\n v2: {s2}",
ail_path.display(),
));
}
checked += 1;
}
if !failures.is_empty() {
panic!(
"CLI parse|render|parse non-idempotent on {} of {} fixtures:\n{}",
failures.len(),
checked,
failures.join("\n\n")
);
}
assert!(checked > 0, "no .ail fixtures found");
eprintln!("CLI parse|render|parse idempotent for {checked} fixtures");
}
+2 -2
View File
@@ -6,7 +6,7 @@
//! post-mono module.
use ailang_core::ast::Def;
use ailang_core::workspace::load_workspace;
use ailang_surface::load_workspace;
use ailang_check::{check_workspace, monomorphise_workspace};
use std::path::PathBuf;
@@ -14,7 +14,7 @@ fn fixture_path() -> PathBuf {
let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
d.pop();
d.pop();
d.join("examples").join("show_mono_pin_smoke.ail.json")
d.join("examples").join("show_mono_pin_smoke.ail")
}
#[test]
+2 -2
View File
@@ -7,7 +7,7 @@
//! and how to declare their own instance for a user type.
use ailang_check::check_workspace;
use ailang_core::workspace::load_workspace;
use ailang_surface::load_workspace;
use std::path::PathBuf;
fn fixture(name: &str) -> PathBuf {
@@ -18,7 +18,7 @@ fn fixture(name: &str) -> PathBuf {
#[test]
fn print_on_fn_type_fires_show_aware_no_instance() {
let ws = load_workspace(&fixture("show_no_instance.ail.json")).expect("load");
let ws = load_workspace(&fixture("show_no_instance.ail")).expect("load");
let diags = check_workspace(&ws);
assert!(
+2 -2
View File
@@ -55,7 +55,7 @@ fn build_and_run(fixture: &str) -> String {
#[test]
fn print_primitives_smoke_runs_end_to_end() {
let stdout = build_and_run("show_print_smoke.ail.json");
let stdout = build_and_run("show_print_smoke.ail");
// print 42 → "42\n" (int_to_str + puts)
// print true → "true\n" (bool_to_str + puts)
// print "hello" → "hello\n" (str_clone + puts)
@@ -67,7 +67,7 @@ fn print_primitives_smoke_runs_end_to_end() {
#[test]
fn print_user_adt_runs_end_to_end() {
let stdout = build_and_run("show_user_adt.ail.json");
let stdout = build_and_run("show_user_adt.ail");
// print (MkIntBox 7) → "7\n" (via user instance prelude.Show
// IntBox that unwraps via match + int_to_str).
assert_eq!(stdout, "7", "got: {stdout:?}");
+24 -24
View File
@@ -25,8 +25,8 @@ fn examples_dir() -> PathBuf {
/// the call site.
#[test]
fn class_method_is_in_module_globals() {
let entry = examples_dir().join("test_22b2_class_method_lookup.ail.json");
let ws = ailang_core::workspace::load_workspace(&entry)
let entry = examples_dir().join("test_22b2_class_method_lookup.ail");
let ws = ailang_surface::load_workspace(&entry)
.expect("workspace must load");
let globals = ailang_check::build_module_globals(&ws)
.expect("globals build");
@@ -59,8 +59,8 @@ fn class_method_is_in_module_globals() {
/// "Str"` with empty effects.
#[test]
fn class_method_entry_carries_full_metadata() {
let entry = examples_dir().join("test_22b2_class_method_lookup.ail.json");
let ws = ailang_core::workspace::load_workspace(&entry)
let entry = examples_dir().join("test_22b2_class_method_lookup.ail");
let ws = ailang_surface::load_workspace(&entry)
.expect("workspace must load");
let globals = ailang_check::build_module_globals(&ws)
.expect("globals build");
@@ -144,8 +144,8 @@ fn class_method_entry_carries_full_metadata() {
/// which dictionary to thread.
#[test]
fn fn_calling_class_method_without_declared_constraint_fires_missing_constraint() {
let entry = examples_dir().join("test_22b2_missing_constraint.ail.json");
let ws = ailang_core::workspace::load_workspace(&entry)
let entry = examples_dir().join("test_22b2_missing_constraint.ail");
let ws = ailang_surface::load_workspace(&entry)
.expect("workspace loads");
let diagnostics = ailang_check::check_workspace(&ws);
assert!(
@@ -164,8 +164,8 @@ fn fn_calling_class_method_without_declared_constraint_fires_missing_constraint(
/// well-formed inside the body.
#[test]
fn fn_with_correct_constraint_typechecks_green() {
let entry = examples_dir().join("test_22b2_constraint_declared.ail.json");
let ws = ailang_core::workspace::load_workspace(&entry)
let entry = examples_dir().join("test_22b2_constraint_declared.ail");
let ws = ailang_surface::load_workspace(&entry)
.expect("workspace loads");
let diagnostics = ailang_check::check_workspace(&ws);
assert!(
@@ -186,8 +186,8 @@ fn fn_with_correct_constraint_typechecks_green() {
#[test]
fn fn_calling_superclass_method_via_subclass_constraint_typechecks_green() {
let entry = examples_dir()
.join("test_22b2_constraint_declared_via_superclass.ail.json");
let ws = ailang_core::workspace::load_workspace(&entry)
.join("test_22b2_constraint_declared_via_superclass.ail");
let ws = ailang_surface::load_workspace(&entry)
.expect("workspace loads");
let diagnostics = ailang_check::check_workspace(&ws);
assert!(
@@ -207,8 +207,8 @@ fn fn_calling_superclass_method_via_subclass_constraint_typechecks_green() {
#[test]
fn fn_with_superclass_constraint_calling_subclass_method_fires_missing_constraint() {
let entry = examples_dir()
.join("test_22b2_missing_constraint_subclass.ail.json");
let ws = ailang_core::workspace::load_workspace(&entry)
.join("test_22b2_missing_constraint_subclass.ail");
let ws = ailang_surface::load_workspace(&entry)
.expect("workspace loads");
let diagnostics = ailang_check::check_workspace(&ws);
assert!(
@@ -230,8 +230,8 @@ fn fn_with_superclass_constraint_calling_subclass_method_fires_missing_constrain
/// the workspace.
#[test]
fn fn_calling_class_method_at_type_without_instance_fires_no_instance() {
let entry = examples_dir().join("test_22b2_no_instance.ail.json");
let ws = ailang_core::workspace::load_workspace(&entry)
let entry = examples_dir().join("test_22b2_no_instance.ail");
let ws = ailang_surface::load_workspace(&entry)
.expect("workspace loads");
let diagnostics = ailang_check::check_workspace(&ws);
assert!(
@@ -253,8 +253,8 @@ fn fn_calling_class_method_at_type_without_instance_fires_no_instance() {
/// `tshow 42` with `instance Show Int` declared in the same workspace.
#[test]
fn fn_calling_class_method_at_type_with_instance_typechecks_green() {
let entry = examples_dir().join("test_22b2_instance_present.ail.json");
let ws = ailang_core::workspace::load_workspace(&entry)
let entry = examples_dir().join("test_22b2_instance_present.ail");
let ws = ailang_surface::load_workspace(&entry)
.expect("workspace loads");
let diagnostics = ailang_check::check_workspace(&ws);
assert!(
@@ -277,8 +277,8 @@ fn fn_calling_class_method_at_type_with_instance_typechecks_green() {
/// `class TShow a`; B = `test_22b2_xmod_no_instance` calls `tshow 42`.
#[test]
fn cross_module_class_method_resolves_and_fires_no_instance() {
let entry = examples_dir().join("test_22b2_xmod_no_instance.ail.json");
let ws = ailang_core::workspace::load_workspace(&entry)
let entry = examples_dir().join("test_22b2_xmod_no_instance.ail");
let ws = ailang_surface::load_workspace(&entry)
.expect("workspace loads");
let diagnostics = ailang_check::check_workspace(&ws);
let codes: Vec<&str> = diagnostics.iter().map(|d| d.code.as_str()).collect();
@@ -307,8 +307,8 @@ fn cross_module_class_method_resolves_and_fires_no_instance() {
/// declares `forall a. (a) -> Str` and calls `tshow x`.
#[test]
fn cross_module_polymorphic_fn_without_constraint_fires_missing_constraint() {
let entry = examples_dir().join("test_22b2_xmod_missing_constraint.ail.json");
let ws = ailang_core::workspace::load_workspace(&entry)
let entry = examples_dir().join("test_22b2_xmod_missing_constraint.ail");
let ws = ailang_surface::load_workspace(&entry)
.expect("workspace loads");
let diagnostics = ailang_check::check_workspace(&ws);
assert!(
@@ -329,8 +329,8 @@ fn cross_module_polymorphic_fn_without_constraint_fires_missing_constraint() {
/// `test_22b2_xmod_classmod`; B = `test_22b2_xmod_instance_present`.
#[test]
fn cross_module_instance_satisfies_concrete_call() {
let entry = examples_dir().join("test_22b2_xmod_instance_present.ail.json");
let ws = ailang_core::workspace::load_workspace(&entry)
let entry = examples_dir().join("test_22b2_xmod_instance_present.ail");
let ws = ailang_surface::load_workspace(&entry)
.expect("workspace loads");
let diagnostics = ailang_check::check_workspace(&ws);
assert!(
@@ -352,8 +352,8 @@ fn cross_module_instance_satisfies_concrete_call() {
/// both ill-formed in the same way; both must surface.
#[test]
fn two_fns_each_missing_constraint_produce_two_diagnostics() {
let entry = examples_dir().join("test_22b2_two_fns_missing_constraint.ail.json");
let ws = ailang_core::workspace::load_workspace(&entry)
let entry = examples_dir().join("test_22b2_two_fns_missing_constraint.ail");
let ws = ailang_surface::load_workspace(&entry)
.expect("workspace loads");
let diagnostics = ailang_check::check_workspace(&ws);
let mc: Vec<&ailang_check::Diagnostic> = diagnostics
+21 -21
View File
@@ -20,8 +20,8 @@ fn examples_dir() -> PathBuf {
fn monomorphise_workspace_is_identity_on_class_free_workspace() {
// Pick any class-free fixture; `examples/hello.ail.json` is
// the canonical class-free smoke test.
let entry = examples_dir().join("hello.ail.json");
let ws = ailang_core::load_workspace(&entry).expect("load");
let entry = examples_dir().join("hello.ail");
let ws = ailang_surface::load_workspace(&entry).expect("load");
let diags = ailang_check::check_workspace(&ws);
assert!(diags.is_empty(), "fixture must typecheck: {:?}", diags);
@@ -29,7 +29,7 @@ fn monomorphise_workspace_is_identity_on_class_free_workspace() {
.expect("mono on class-free workspace");
// Module set unchanged.
let before = ailang_core::load_workspace(&examples_dir().join("hello.ail.json"))
let before = ailang_surface::load_workspace(&examples_dir().join("hello.ail"))
.expect("re-load");
assert_eq!(
ws.modules.keys().collect::<Vec<_>>(),
@@ -63,8 +63,8 @@ fn monomorphise_workspace_is_identity_on_class_free_workspace() {
/// (correctly) materialises into `tshow__Int`.
#[test]
fn monomorphise_workspace_is_identity_when_no_concrete_call_sites() {
let entry = examples_dir().join("test_22b3_no_call_sites.ail.json");
let ws = ailang_core::load_workspace(&entry).expect("load");
let entry = examples_dir().join("test_22b3_no_call_sites.ail");
let ws = ailang_surface::load_workspace(&entry).expect("load");
let diags = ailang_check::check_workspace(&ws);
assert!(diags.is_empty(), "fixture must typecheck: {:?}", diags);
@@ -72,7 +72,7 @@ fn monomorphise_workspace_is_identity_when_no_concrete_call_sites() {
.expect("mono on class-present workspace");
// Module set unchanged.
let before = ailang_core::load_workspace(&entry).expect("re-load");
let before = ailang_surface::load_workspace(&entry).expect("re-load");
assert_eq!(
ws_after.modules.keys().collect::<Vec<_>>(),
before.modules.keys().collect::<Vec<_>>(),
@@ -120,8 +120,8 @@ fn mono_symbol_primitive_types_use_surface_form() {
fn collect_mono_targets_single_concrete_call_site() {
use ailang_check::mono::collect_mono_targets;
let entry = examples_dir().join("test_22b2_instance_present.ail.json");
let ws = ailang_core::load_workspace(&entry).expect("load");
let entry = examples_dir().join("test_22b2_instance_present.ail");
let ws = ailang_surface::load_workspace(&entry).expect("load");
let diags = ailang_check::check_workspace(&ws);
assert!(diags.is_empty(), "fixture must typecheck: {:?}", diags);
@@ -376,8 +376,8 @@ fn synthesise_mono_fn_zero_arg_method_uses_body_verbatim() {
/// dedup contract — the pass must not emit one FnDef per call site.
#[test]
fn fixpoint_dedupes_repeated_calls_at_same_type() {
let entry = examples_dir().join("test_22b3_dup_call_sites.ail.json");
let ws = ailang_core::load_workspace(&entry).expect("load");
let entry = examples_dir().join("test_22b3_dup_call_sites.ail");
let ws = ailang_surface::load_workspace(&entry).expect("load");
let diags = ailang_check::check_workspace(&ws);
assert!(diags.is_empty(), "fixture must typecheck: {:?}", diags);
@@ -416,8 +416,8 @@ fn fixpoint_dedupes_repeated_calls_at_same_type() {
/// body).
#[test]
fn fixpoint_handles_chained_class_method_calls() {
let entry = examples_dir().join("test_22b3_chained_calls.ail.json");
let ws = ailang_core::load_workspace(&entry).expect("load");
let entry = examples_dir().join("test_22b3_chained_calls.ail");
let ws = ailang_surface::load_workspace(&entry).expect("load");
let diags = ailang_check::check_workspace(&ws);
assert!(diags.is_empty(), "fixture must typecheck: {:?}", diags);
@@ -480,8 +480,8 @@ fn fixpoint_handles_chained_class_method_calls() {
fn rewrite_walker_skips_locally_shadowed_class_method() {
use ailang_core::ast::{Def, Term};
let entry = examples_dir().join("test_22b3_shadow_class_method.ail.json");
let ws = ailang_core::load_workspace(&entry).expect("load");
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`
// with a local `let show = "shadow"`, which fires the new
@@ -570,8 +570,8 @@ fn rewrite_walker_skips_locally_shadowed_class_method() {
/// placement contract: the synthesised FnDef lives in B, not A.
#[test]
fn rewrite_uses_qualified_name_for_cross_module_call_site() {
let entry = examples_dir().join("test_22b2_xmod_instance_present.ail.json");
let ws = ailang_core::load_workspace(&entry).expect("load");
let entry = examples_dir().join("test_22b2_xmod_instance_present.ail");
let ws = ailang_surface::load_workspace(&entry).expect("load");
let diags = ailang_check::check_workspace(&ws);
assert!(diags.is_empty(), "fixture must typecheck: {:?}", diags);
@@ -624,8 +624,8 @@ fn rewrite_uses_qualified_name_for_cross_module_call_site() {
/// be ambiguous between mono-pass bug and 22b.2 typecheck bug.
#[test]
fn synthetic_fixture_typechecks_clean() {
let entry = examples_dir().join("test_22b3_mono_synthetic.ail.json");
let ws = ailang_core::load_workspace(&entry).expect("load");
let entry = examples_dir().join("test_22b3_mono_synthetic.ail");
let ws = ailang_surface::load_workspace(&entry).expect("load");
let diags = ailang_check::check_workspace(&ws);
assert!(diags.is_empty(), "fixture must typecheck: {:?}", diags);
}
@@ -641,7 +641,7 @@ fn synthetic_fixture_runs_and_prints_five() {
let manifest_dir = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let ail_bin = std::env::var("CARGO_BIN_EXE_ail")
.expect("CARGO_BIN_EXE_ail set by cargo test");
let fixture = examples_dir().join("test_22b3_mono_synthetic.ail.json");
let fixture = examples_dir().join("test_22b3_mono_synthetic.ail");
let tmp = manifest_dir.join("target").join(format!(
"test_22b3_synthetic_{}", std::process::id()
@@ -674,8 +674,8 @@ fn synthetic_fixture_runs_and_prints_five() {
/// of `tshow`.
#[test]
fn rewrite_replaces_class_method_var_with_mono_symbol_same_module() {
let entry = examples_dir().join("test_22b2_instance_present.ail.json");
let ws = ailang_core::load_workspace(&entry).expect("load");
let entry = examples_dir().join("test_22b2_instance_present.ail");
let ws = ailang_surface::load_workspace(&entry).expect("load");
let diags = ailang_check::check_workspace(&ws);
assert!(diags.is_empty(), "fixture must typecheck: {:?}", diags);
+1 -1
View File
@@ -53,6 +53,6 @@ fn build_and_run(example: &str) -> String {
/// `lib.rs:2316`) fall through.
#[test]
fn user_class_instance_over_user_adt_builds_and_runs() {
let stdout = build_and_run("test_22c_user_class_e2e.ail.json");
let stdout = build_and_run("test_22c_user_class_e2e.ail");
assert_eq!(stdout.trim(), "42");
}
@@ -11,7 +11,7 @@
use ailang_check::{build_check_env, build_module_globals, Env};
use ailang_core::ast::Def;
use ailang_core::load_workspace;
use ailang_surface::load_workspace;
use std::collections::BTreeMap;
use std::path::Path;
@@ -125,7 +125,7 @@ fn build_env_inline_pre_refactor(ws: &ailang_core::workspace::Workspace) -> Env
#[test]
fn build_check_env_matches_inline_seeding() {
let entry = examples_dir().join("test_mono_imports_main.ail.json");
let entry = examples_dir().join("test_mono_imports_main.ail");
let ws = load_workspace(&entry).expect("load test_mono_imports_main");
let env_helper = build_check_env(&ws);
@@ -12,7 +12,7 @@
//! (`build_check_env`) and not visible from `ailang-core`.
use ailang_check::build_check_env;
use ailang_core::load_workspace;
use ailang_surface::load_workspace;
use std::path::Path;
fn examples_dir() -> std::path::PathBuf {
@@ -34,7 +34,7 @@ fn examples_dir() -> std::path::PathBuf {
#[test]
fn mq3_class_class_collision_loads_clean_and_populates_candidates() {
let entry = examples_dir()
.join("test_22b2_method_name_collision_class_class.ail.json");
.join("test_22b2_method_name_collision_class_class.ail");
let ws = load_workspace(&entry)
.expect("post-mq.3: workspace loads without collision diagnostic");
@@ -69,7 +69,7 @@ fn mq3_class_class_collision_loads_clean_and_populates_candidates() {
#[test]
fn mq3_class_fn_collision_loads_clean() {
let entry = examples_dir()
.join("test_22b2_method_name_collision_class_fn.ail.json");
.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");
}
@@ -5,7 +5,7 @@
//! Step-2 singleton (each method has exactly one candidate class globally).
use ailang_check::{build_check_env, check_workspace};
use ailang_core::workspace::load_workspace;
use ailang_surface::load_workspace;
use std::path::PathBuf;
fn fixture_dir() -> PathBuf {
@@ -22,7 +22,7 @@ fn bare_show_routes_to_prelude_show_via_singleton() {
// Asserts: env.method_to_candidate_classes["show"] == {"prelude.Show"}
// (a singleton), so Step-2 of resolve_method_dispatch resolves bare
// "show" without needing Steps 3-4.
let ws_path = fixture_dir().join("show_mono_pin_smoke.ail.json");
let ws_path = fixture_dir().join("show_mono_pin_smoke.ail");
let ws = load_workspace(&ws_path).expect("workspace loads");
let diags = check_workspace(&ws);
assert!(diags.is_empty(), "typecheck diagnostics: {diags:#?}");
@@ -49,7 +49,7 @@ fn bare_tshow_routes_to_user_tshow_via_singleton() {
// (which now declares class TShow with method tshow).
// Asserts: env.method_to_candidate_classes["tshow"] has exactly one
// candidate (the user TShow), workspace-globally.
let ws_path = fixture_dir().join("test_22b1_dup_classmod.ail.json");
let ws_path = fixture_dir().join("test_22b1_dup_classmod.ail");
let ws = load_workspace(&ws_path).expect("workspace loads");
let diags = check_workspace(&ws);
assert!(diags.is_empty(), "typecheck diagnostics: {diags:#?}");
+5 -5
View File
@@ -5,7 +5,7 @@
//! runs in JSON mode against a workspace.
use ailang_check::{check_workspace, Severity};
use ailang_core::load_workspace;
use ailang_surface::load_workspace;
use std::path::Path;
/// Iter 18c.2: assert that the linearity check's `suggested_rewrites`
@@ -39,7 +39,7 @@ fn examples_dir() -> std::path::PathBuf {
fn happy_path_resolves_qualified_import() {
// ws_main imports ws_lib and calls `ws_lib.add` — fully typed.
// Expected: no diagnostics.
let entry = examples_dir().join("ws_main.ail.json");
let entry = examples_dir().join("ws_main.ail");
let ws = load_workspace(&entry).expect("load ws_main");
let diags = check_workspace(&ws);
assert!(
@@ -52,7 +52,7 @@ fn happy_path_resolves_qualified_import() {
#[test]
fn unknown_import_is_reported() {
// ws_broken references `ws_lib.bogus` — module is there, def isn't.
let entry = examples_dir().join("ws_broken.ail.json");
let entry = examples_dir().join("ws_broken.ail");
let ws = load_workspace(&entry).expect("load ws_broken");
let diags = check_workspace(&ws);
assert_eq!(diags.len(), 1, "got: {:?}", diags);
@@ -72,7 +72,7 @@ fn unknown_import_is_reported() {
#[test]
fn unknown_module_prefix_is_reported() {
// ws_unknown_module has no imports but references `nope.x`.
let entry = examples_dir().join("ws_unknown_module.ail.json");
let entry = examples_dir().join("ws_unknown_module.ail");
let ws = load_workspace(&entry).expect("load ws_unknown_module");
let diags = check_workspace(&ws);
assert_eq!(diags.len(), 1, "got: {:?}", diags);
@@ -674,7 +674,7 @@ fn reuse_as_shape_mismatch_is_reported_on_cons_to_nil() {
/// are exactly the canonical accept shape.
#[test]
fn borrow_own_demo_is_linearity_clean() {
let entry = examples_dir().join("borrow_own_demo.ail.json");
let entry = examples_dir().join("borrow_own_demo.ail");
let ws = load_workspace(&entry).expect("load borrow_own_demo");
let diags = check_workspace(&ws);
let lin: Vec<&ailang_check::Diagnostic> = diags
+3
View File
@@ -9,3 +9,6 @@ ailang-core.workspace = true
ailang-check.workspace = true
thiserror.workspace = true
indexmap.workspace = true
[dev-dependencies]
ailang-surface = { path = "../ailang-surface" }
+7 -89
View File
@@ -3708,95 +3708,13 @@ mod tests {
);
}
/// Iter 23.2: 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`. The test runs the full pipeline end-of-typecheck →
/// monomorphise → codegen, then asserts substring shape on the IR.
#[test]
fn eq_int_call_produces_icmp_i64_mono_fn() {
use ailang_core::load_workspace;
use ailang_check::{check_workspace, monomorphise_workspace};
let manifest = env!("CARGO_MANIFEST_DIR");
let entry = std::path::Path::new(manifest)
.parent().unwrap().parent().unwrap()
.join("examples").join("eq_primitives_smoke.ail.json");
let ws = load_workspace(&entry).expect("load workspace");
let diags = check_workspace(&ws);
assert!(
diags.iter().all(|d| !matches!(d.severity, ailang_check::Severity::Error)),
"unexpected check_workspace errors: {diags:?}"
);
let ws = monomorphise_workspace(&ws).expect("monomorphise");
let ir = lower_workspace(&ws).expect("lower");
assert!(
ir.contains("@ail_prelude_eq__Int"),
"ir must contain mono-synthesised @ail_prelude_eq__Int; ir was:\n{ir}"
);
assert!(
ir.contains("icmp eq i64"),
"eq__Int body must lower to icmp eq i64; ir was:\n{ir}"
);
}
/// Iter 23.2: 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]
fn eq_bool_call_produces_icmp_i1_mono_fn() {
use ailang_core::load_workspace;
use ailang_check::{check_workspace, monomorphise_workspace};
let manifest = env!("CARGO_MANIFEST_DIR");
let entry = std::path::Path::new(manifest)
.parent().unwrap().parent().unwrap()
.join("examples").join("eq_primitives_smoke.ail.json");
let ws = load_workspace(&entry).expect("load workspace");
let diags = check_workspace(&ws);
assert!(
diags.iter().all(|d| !matches!(d.severity, ailang_check::Severity::Error)),
"unexpected check_workspace errors: {diags:?}"
);
let ws = monomorphise_workspace(&ws).expect("monomorphise");
let ir = lower_workspace(&ws).expect("lower");
assert!(
ir.contains("@ail_prelude_eq__Bool"),
"ir must contain mono-synthesised @ail_prelude_eq__Bool; ir was:\n{ir}"
);
assert!(
ir.contains("icmp eq i1"),
"eq__Bool body must lower to icmp eq i1; ir was:\n{ir}"
);
}
/// Iter 23.2: 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.
#[test]
fn eq_str_call_produces_ail_str_eq_call_mono_fn() {
use ailang_core::load_workspace;
use ailang_check::{check_workspace, monomorphise_workspace};
let manifest = env!("CARGO_MANIFEST_DIR");
let entry = std::path::Path::new(manifest)
.parent().unwrap().parent().unwrap()
.join("examples").join("eq_primitives_smoke.ail.json");
let ws = load_workspace(&entry).expect("load workspace");
let diags = check_workspace(&ws);
assert!(
diags.iter().all(|d| !matches!(d.severity, ailang_check::Severity::Error)),
"unexpected check_workspace errors: {diags:?}"
);
let ws = monomorphise_workspace(&ws).expect("monomorphise");
let ir = lower_workspace(&ws).expect("lower");
assert!(
ir.contains("@ail_prelude_eq__Str"),
"ir must contain mono-synthesised @ail_prelude_eq__Str; ir was:\n{ir}"
);
assert!(
ir.contains("call zeroext i1 @ail_str_eq("),
"eq__Str body must call @ail_str_eq; ir was:\n{ir}"
);
}
// `eq_int_call_produces_icmp_i64_mono_fn` +
// `eq_bool_call_produces_icmp_i1_mono_fn` +
// `eq_str_call_produces_ail_str_eq_call_mono_fn` relocated to
// `crates/ailang-codegen/tests/eq_primitives_pin.rs` in iter
// form-a.1 Task 5 (use `ailang_surface::load_workspace` on the
// `eq_primitives_smoke.ail` Form-A fixture; the original `.ail.json`
// fixture is deleted in T8).
/// Iter 23.3: `runtime/str.c::ail_str_compare` backs the prelude's
/// `compare__Str` mono symbol (see `try_emit_primitive_instance_body`).
@@ -0,0 +1,82 @@
//! Eq-primitive codegen pin tests, relocated from
//! `ailang-codegen/src/lib.rs` `#[cfg(test)] mod tests` to a `tests/*`
//! integration crate in iter form-a.1 Task 5. The relocation switches
//! workspace loading from `ailang_core::load_workspace` (the JSON-only
//! loader) to `ailang_surface::load_workspace` (the
//! extension-dispatching superset), so the post-iter `.ail` corpus is
//! loaded from Form A rather than the deleted `.ail.json` siblings.
use ailang_check::{check_workspace, monomorphise_workspace, Severity};
use ailang_codegen::lower_workspace;
use ailang_surface::load_workspace;
use std::path::Path;
fn eq_primitives_smoke_path() -> std::path::PathBuf {
let manifest = env!("CARGO_MANIFEST_DIR");
Path::new(manifest)
.parent().unwrap().parent().unwrap()
.join("examples").join("eq_primitives_smoke.ail")
}
fn lower_eq_primitives_smoke() -> String {
let entry = eq_primitives_smoke_path();
let ws = load_workspace(&entry).expect("load workspace");
let diags = check_workspace(&ws);
assert!(
diags.iter().all(|d| !matches!(d.severity, Severity::Error)),
"unexpected check_workspace errors: {diags:?}"
);
let ws = monomorphise_workspace(&ws).expect("monomorphise");
lower_workspace(&ws).expect("lower")
}
/// Iter 23.2: 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 →
/// codegen, then substring-shape assertions on the IR.
#[test]
fn eq_int_call_produces_icmp_i64_mono_fn() {
let ir = lower_eq_primitives_smoke();
assert!(
ir.contains("@ail_prelude_eq__Int"),
"ir must contain mono-synthesised @ail_prelude_eq__Int; ir was:\n{ir}"
);
assert!(
ir.contains("icmp eq i64"),
"eq__Int body must lower to icmp eq i64; ir was:\n{ir}"
);
}
/// Iter 23.2: 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]
fn eq_bool_call_produces_icmp_i1_mono_fn() {
let ir = lower_eq_primitives_smoke();
assert!(
ir.contains("@ail_prelude_eq__Bool"),
"ir must contain mono-synthesised @ail_prelude_eq__Bool; ir was:\n{ir}"
);
assert!(
ir.contains("icmp eq i1"),
"eq__Bool body must lower to icmp eq i1; ir was:\n{ir}"
);
}
/// Iter 23.2: 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.
#[test]
fn eq_str_call_produces_ail_str_eq_call_mono_fn() {
let ir = lower_eq_primitives_smoke();
assert!(
ir.contains("@ail_prelude_eq__Str"),
"ir must contain mono-synthesised @ail_prelude_eq__Str; ir was:\n{ir}"
);
assert!(
ir.contains("call zeroext i1 @ail_str_eq("),
"eq__Str body must call @ail_str_eq; ir was:\n{ir}"
);
}
+1
View File
@@ -13,3 +13,4 @@ indexmap.workspace = true
[dev-dependencies]
tempfile.workspace = true
ailang-surface = { path = "../ailang-surface" }
+7 -302
View File
@@ -47,306 +47,11 @@ pub fn def_hash(def: &Def) -> String {
hex.as_str()[..16].to_string()
}
// `#[cfg(test)] mod tests` block relocated to
// `crates/ailang-core/tests/hash_pin.rs` in iter form-a.1 Task 5. The
// integration-test crate has `ailang-surface` as a dev-dependency so
// the schema-stability pins can load `.ail` fixtures via
// `ailang_surface::load_module`, eliminating the production-source
// dependency on `.ail.json` fixture reads.
#[cfg(test)]
mod tests {
use super::*;
use crate::ast::*;
fn sample_fn() -> Def {
Def::Fn(FnDef {
name: "add".into(),
ty: Type::Fn {
params: vec![Type::int(), Type::int()],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
params: vec!["a".into(), "b".into()],
body: Term::App {
callee: Box::new(Term::Var { name: "+".into() }),
args: vec![
Term::Var { name: "a".into() },
Term::Var { name: "b".into() },
],
tail: false,
},
suppress: vec![],
doc: None,
})
}
#[test]
fn hash_is_stable() {
let h1 = def_hash(&sample_fn());
let h2 = def_hash(&sample_fn());
assert_eq!(h1, h2);
assert_eq!(h1.len(), 16);
}
#[test]
fn hash_changes_with_content() {
let mut def = sample_fn();
let h1 = def_hash(&def);
if let Def::Fn(ref mut f) = def {
f.name = "mul".into();
}
let h2 = def_hash(&def);
assert_ne!(h1, h2);
}
/// Iter 13a regression: 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
/// `skip_serializing_if` is missing or wrong. We deserialise the
/// real example modules from disk to avoid drift between the test
/// and the source-of-truth JSON.
#[test]
fn iter13a_schema_extension_preserves_pre_13a_hashes() {
let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let examples = manifest_dir.join("../../examples");
let sum_src = std::fs::read(examples.join("sum.ail.json"))
.expect("examples/sum.ail.json present");
let sum_mod: crate::ast::Module = serde_json::from_slice(&sum_src).unwrap();
let sum_def = sum_mod.defs.iter().find(|d| d.name() == "sum").unwrap();
assert_eq!(def_hash(sum_def), "db33f57cb329935e");
let list_src = std::fs::read(examples.join("list.ail.json"))
.expect("examples/list.ail.json present");
let list_mod: crate::ast::Module = serde_json::from_slice(&list_src).unwrap();
let int_list_def = list_mod.defs.iter().find(|d| d.name() == "IntList").unwrap();
assert_eq!(def_hash(int_list_def), "b082192bd0c99202");
}
/// Iter 19b regression: adding `suppress` to [`crate::ast::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 hash drifts and `ail diff` /
/// `ail manifest` output breaks.
///
/// We construct two FnDefs that differ only in `suppress` (one
/// empty, one missing the field). They must hash bit-identically:
/// the canonical-JSON of both is the same byte sequence because
/// the empty Vec is elided.
#[test]
fn iter19b_empty_suppress_preserves_pre_19b_hashes() {
let with_empty_suppress = sample_fn();
// Mutate the bare sample to set suppress explicitly to a
// non-empty Vec, then mutate it back to empty: two distinct
// construction paths that should still hash identically.
let mut with_explicit_empty = sample_fn();
if let Def::Fn(ref mut f) = with_explicit_empty {
f.suppress = vec![];
}
assert_eq!(
def_hash(&with_empty_suppress),
def_hash(&with_explicit_empty),
"two FnDefs with empty suppress must hash identically"
);
// And: a FnDef with a *non-empty* suppress hashes
// *differently* (sanity check that the field is in fact in
// the canonical bytes when present).
let mut with_suppress = sample_fn();
if let Def::Fn(ref mut f) = with_suppress {
f.suppress = vec![crate::ast::Suppress {
code: "over-strict-mode".into(),
because: "test reason".into(),
}];
}
assert_ne!(
def_hash(&with_empty_suppress),
def_hash(&with_suppress),
"non-empty suppress must produce a distinct hash"
);
}
/// Iter 19b: an on-disk pre-19b fixture must still load cleanly
/// (no `suppress` field present in the JSON) and produce its
/// canonical-byte hash unchanged. The hash for `sum.sum` was
/// recorded by the 13a regression and must stay 16 hex chars
/// equal to `db33f57cb329935e` — this test re-asserts it after
/// the 19b schema bump.
#[test]
fn iter19b_schema_extension_preserves_pre_19b_hashes() {
let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let examples = manifest_dir.join("../../examples");
let sum_src = std::fs::read(examples.join("sum.ail.json"))
.expect("examples/sum.ail.json present");
let sum_mod: crate::ast::Module = serde_json::from_slice(&sum_src).unwrap();
let sum_def = sum_mod.defs.iter().find(|d| d.name() == "sum").unwrap();
assert_eq!(def_hash(sum_def), "db33f57cb329935e");
}
/// Iter 22b.1 regression: 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 `Fn` / `Const` / `Type` arms are unchanged.
/// This re-asserts the same on-disk hashes the 13a / 19b
/// regressions pinned, after the 22b.1 schema bump.
#[test]
fn iter22b1_schema_extension_preserves_pre_22b_hashes() {
let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let examples = manifest_dir.join("../../examples");
let sum_src = std::fs::read(examples.join("sum.ail.json"))
.expect("examples/sum.ail.json present");
let sum_mod: crate::ast::Module = serde_json::from_slice(&sum_src).unwrap();
let sum_def = sum_mod.defs.iter().find(|d| d.name() == "sum").unwrap();
assert_eq!(def_hash(sum_def), "db33f57cb329935e");
let list_src = std::fs::read(examples.join("list.ail.json"))
.expect("examples/list.ail.json present");
let list_mod: crate::ast::Module = serde_json::from_slice(&list_src).unwrap();
let int_list_def = list_mod
.defs
.iter()
.find(|d| d.name() == "IntList")
.unwrap();
assert_eq!(def_hash(int_list_def), "b082192bd0c99202");
}
/// Iter 22b.1: a [`crate::ast::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. If FAIL: a `skip_serializing_if` is missing
/// on `superclass` or `doc`, or the parser produces different
/// canonical bytes from the same logical value.
#[test]
fn iter22b1_classdef_empty_optionals_hash_stable() {
let bare = Def::Class(crate::ast::ClassDef {
name: "Empty".into(),
param: "a".into(),
superclass: None,
methods: vec![],
doc: None,
});
let json = r#"{"kind":"class","name":"Empty","param":"a","methods":[]}"#;
let parsed: Def = serde_json::from_str(json).unwrap();
assert_eq!(
def_hash(&bare),
def_hash(&parsed),
"ClassDef with elided optionals must hash identically to construction with explicit None"
);
}
/// Iter 22b.2: adding `constraints` to [`crate::ast::Type::Forall`]
/// must NOT alter canonical-JSON bytes of any pre-22b.2 polymorphic
/// type. The `#[serde(default, skip_serializing_if = "Vec::is_empty")]`
/// attribute on the field is what enforces this; if it is wrong,
/// every existing fixture's hash drifts and workspace dedup keys
/// based on the canonical bytes break.
///
/// Property: a `Type::Forall` constructed without specifying
/// `constraints` (i.e. empty vec) must serialise with no
/// `constraints` key in the canonical JSON.
#[test]
fn forall_without_constraints_hashes_bit_identical_to_pre_22b2() {
use crate::ast::Type;
let t = Type::Forall {
vars: vec!["a".into()],
constraints: vec![],
body: Box::new(Type::fn_implicit(
vec![Type::Var { name: "a".into() }],
Type::Var { name: "a".into() },
vec![],
)),
};
let bytes = crate::canonical::to_bytes(&t);
let s = std::str::from_utf8(&bytes).unwrap();
assert!(
!s.contains("constraints"),
"Type::Forall serialised must omit `constraints` when empty; got: {s}"
);
}
/// Iter ct.4 (canonical-type-names milestone close): pin the
/// canonical-form hashes of the two cross-module fixtures
/// migrated by `ail migrate-canonical-types` in ct.1. These
/// hashes are the post-migration state; pre-migration values
/// (when both fixtures carried bare cross-module Type::Con)
/// are no longer reproducible because the ct.1 validator
/// rejects that shape upstream.
#[test]
fn ct4_migrated_fixtures_have_canonical_form_hashes() {
let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let examples = manifest_dir.join("../../examples");
let ord_src = std::fs::read(examples.join("ordering_match.ail.json"))
.expect("examples/ordering_match.ail.json present");
let ord_mod: crate::ast::Module = serde_json::from_slice(&ord_src).unwrap();
let main_def = ord_mod.defs.iter().find(|d| d.name() == "main").unwrap();
assert_eq!(
def_hash(main_def),
"8d17235aa3d2e127",
"ordering_match::main canonical hash must match captured post-migration value"
);
let dup_a_src = std::fs::read(examples.join("test_22b1_dup_a.ail.json"))
.expect("examples/test_22b1_dup_a.ail.json present");
let dup_a_mod: crate::ast::Module = serde_json::from_slice(&dup_a_src).unwrap();
// mq.1: test_22b1_dup_a has one def post-migration — the instance
// of `test_22b1_dup_classmod.TShow` for `test_22b1_dup_b.MyInt`.
// The `TShow` class itself moved to `test_22b1_dup_classmod` to
// break the dup_a ↔ dup_b cycle that the pre-mq.1 bare-class
// shape required for cross-module duplicate-instance coverage.
// 24.2: class renamed `Show` → `TShow` and method renamed
// `show` → `tshow` workspace-wide so prelude.Show can ship; the
// hash captures new bytes accordingly.
assert_eq!(dup_a_mod.defs.len(), 1, "test_22b1_dup_a expected to have exactly 1 def (the instance)");
// Hash captured post-24.2 TShow/tshow rename. A future
// schema-touching change that shifts this hash without intent
// triggers this pin.
assert_eq!(
def_hash(&dup_a_mod.defs[0]),
"392c247f07de6517",
"test_22b1_dup_a instance hash drifted; expected post-24.2 captured value"
);
// Also pin the new test_22b1_dup_classmod fixture's `TShow` class
// hash: the class moved here from dup_a verbatim (same method
// signature), so the hash is independent of the migration but
// does shift with the 24.2 Show→TShow / show→tshow rename.
let dup_classmod_src = std::fs::read(examples.join("test_22b1_dup_classmod.ail.json"))
.expect("examples/test_22b1_dup_classmod.ail.json present");
let dup_classmod_mod: crate::ast::Module = serde_json::from_slice(&dup_classmod_src).unwrap();
assert_eq!(dup_classmod_mod.defs.len(), 1, "test_22b1_dup_classmod expected to have exactly 1 def (class TShow)");
assert_eq!(
def_hash(&dup_classmod_mod.defs[0]),
"b8bca96c2d09ed93",
"test_22b1_dup_classmod class TShow canonical hash; post-24.2 captured value"
);
}
/// Iter ct.4: re-assert that the canonical-form tightening
/// did NOT change hashes of intra-module-only fixtures. The
/// existing iter-13a / iter-19b / iter-22b.1 pin tests already
/// assert these; this one names the canonical-type-names
/// milestone explicitly so future archaeology finds the
/// connection.
#[test]
fn ct4_unmigrated_fixtures_remain_bit_identical() {
let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
let examples = manifest_dir.join("../../examples");
let sum_src = std::fs::read(examples.join("sum.ail.json")).unwrap();
let sum_mod: crate::ast::Module = serde_json::from_slice(&sum_src).unwrap();
let sum_def = sum_mod.defs.iter().find(|d| d.name() == "sum").unwrap();
assert_eq!(def_hash(sum_def), "db33f57cb329935e",
"sum.sum hash drifted across canonical-form tightening — unexpected");
let list_src = std::fs::read(examples.join("list.ail.json")).unwrap();
let list_mod: crate::ast::Module = serde_json::from_slice(&list_src).unwrap();
let int_list_def = list_mod.defs.iter().find(|d| d.name() == "IntList").unwrap();
assert_eq!(def_hash(int_list_def), "b082192bd0c99202",
"list.IntList hash drifted across canonical-form tightening — unexpected");
}
}
mod tests {}
+21 -263
View File
@@ -1609,128 +1609,12 @@ mod tests {
d
}
#[test]
fn loads_example_workspace_happy_path() {
// Uses the canonical example files under `examples/`. This
// test documents that the loader relies on the committed
// workspace example.
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let workspace_root =
Path::new(manifest_dir).parent().unwrap().parent().unwrap();
let entry = workspace_root.join("examples").join("ws_main.ail.json");
// `loads_example_workspace_happy_path` + `loads_workspace_auto_injects_prelude`
// relocated to `crates/ailang-core/tests/workspace_pin.rs` in iter
// form-a.1 Task 5 (use `ailang_surface::load_workspace` on `.ail`
// fixtures; the original `.ail.json` fixtures are deleted in T8).
let ws = load_workspace(&entry).expect("load workspace");
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,
// 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
// 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
// type def.
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let workspace_root =
Path::new(manifest_dir).parent().unwrap().parent().unwrap();
let entry = workspace_root.join("examples").join("ws_main.ail.json");
let ws = load_workspace(&entry).expect("load workspace");
assert!(
ws.modules.contains_key("prelude"),
"prelude module must be auto-injected; modules present: {:?}",
ws.modules.keys().collect::<Vec<_>>()
);
let prelude = &ws.modules["prelude"];
assert_eq!(prelude.name, "prelude");
assert!(
prelude.defs.iter().any(|d| matches!(
d,
crate::ast::Def::Type(t) if t.name == "Ordering"
)),
"prelude must contain Ordering type def"
);
// Iter 23.2: prelude also ships the `Eq` class plus three
// primitive instances (Eq Int, Eq Bool, Eq Str).
assert!(
prelude.defs.iter().any(|d| matches!(
d,
crate::ast::Def::Class(c) if c.name == "Eq"
)),
"prelude must contain Eq class def"
);
let eq_instance_types: Vec<&str> = prelude
.defs
.iter()
.filter_map(|d| match d {
crate::ast::Def::Instance(i) if i.class == "Eq" => {
if let crate::ast::Type::Con { name, .. } = &i.type_ {
Some(name.as_str())
} else {
None
}
}
_ => None,
})
.collect();
assert!(
eq_instance_types.contains(&"Int"),
"prelude must contain `instance Eq Int`; saw Eq instances on: {eq_instance_types:?}"
);
assert!(
eq_instance_types.contains(&"Bool"),
"prelude must contain `instance Eq Bool`; saw Eq instances on: {eq_instance_types:?}"
);
assert!(
eq_instance_types.contains(&"Str"),
"prelude must contain `instance Eq Str`; saw Eq instances on: {eq_instance_types:?}"
);
// Iter 23.3: prelude also ships the `Ord` class plus three
// primitive instances (Ord Int, Ord Bool, Ord Str). Ord
// extends Eq (Decision 11 single-superclass closure), so its
// shape is the same as Eq's instance set; the loader uses the
// superclass walk to verify the Eq instances exist.
assert!(
prelude.defs.iter().any(|d| matches!(
d,
crate::ast::Def::Class(c) if c.name == "Ord"
)),
"prelude must contain Ord class def"
);
let ord_instance_types: Vec<&str> = prelude
.defs
.iter()
.filter_map(|d| match d {
crate::ast::Def::Instance(i) if i.class == "Ord" => {
if let crate::ast::Type::Con { name, .. } = &i.type_ {
Some(name.as_str())
} else {
None
}
}
_ => None,
})
.collect();
assert!(
ord_instance_types.contains(&"Int"),
"prelude must contain `instance Ord Int`; saw: {ord_instance_types:?}"
);
assert!(
ord_instance_types.contains(&"Bool"),
"prelude must contain `instance Ord Bool`; saw: {ord_instance_types:?}"
);
assert!(
ord_instance_types.contains(&"Str"),
"prelude must contain `instance Ord Str`; saw: {ord_instance_types:?}"
);
}
// (See workspace_pin.rs relocation marker above.)
#[test]
fn user_module_named_prelude_is_rejected() {
@@ -1793,25 +1677,8 @@ mod tests {
/// auto-loaded prelude (iter 23.2) the registry is no longer
/// strictly empty, so the invariant is now "no entries whose
/// `defining_module` is anything other than `prelude`".
#[test]
fn iter22b1_workspace_with_no_classes_has_empty_registry() {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
let workspace_root =
Path::new(manifest_dir).parent().unwrap().parent().unwrap();
let entry = workspace_root.join("examples").join("sum.ail.json");
let ws = load_workspace(&entry).expect("sum.ail.json loads");
assert!(
ws.registry.entries.values().all(|e| e.defining_module == "prelude"),
"pre-22b fixture has no class/instance defs of its own; \
all registry entries must come from the auto-injected prelude. \
got non-prelude entries: {:?}",
ws.registry.entries.values()
.filter(|e| e.defining_module != "prelude")
.map(|e| &e.defining_module)
.collect::<Vec<_>>()
);
}
// `iter22b1_workspace_with_no_classes_has_empty_registry` relocated
// to `crates/ailang-core/tests/workspace_pin.rs` in iter form-a.1 Task 5.
fn examples_dir() -> PathBuf {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
@@ -1828,23 +1695,8 @@ mod tests {
/// fixture itself. With the auto-loaded prelude (iter 23.2)
/// the registry also contains the prelude's own entries; the
/// invariant is filtered to fixture-only entries.
#[test]
fn iter22b1_instance_in_class_module_loads_clean() {
let entry = examples_dir().join("test_22b1_orphan_class.ail.json");
let ws = load_workspace(&entry).expect("coherent instance loads");
let fixture_entries: Vec<_> = ws.registry.entries.iter()
.filter(|(_, e)| e.defining_module != "prelude")
.collect();
assert_eq!(fixture_entries.len(), 1);
let (key, entry) = fixture_entries[0];
// mq.1: 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");
// `instance.class` carries the canonical-form on-disk value
// (bare for same-module per the canonical-form rule).
assert_eq!(entry.instance.class, "TShow");
}
// `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
/// the class's module nor the type's module fires `OrphanInstance`.
@@ -1853,26 +1705,8 @@ mod tests {
/// TShow Int` itself — but the entry is not the class's module
/// and `Int` is primitive, so neither leg of coherence is
/// satisfied. (24.2: class renamed `Show` → `TShow`.)
#[test]
fn iter22b1_orphan_instance_fires_diagnostic() {
let entry = examples_dir().join("test_22b1_orphan_third.ail.json");
let err = load_workspace(&entry).expect_err("must fire orphan");
match err {
WorkspaceLoadError::OrphanInstance {
class,
type_repr,
defining_module,
..
} => {
// mq.1: `class` carries the canonical form (qualified
// for cross-module class refs).
assert_eq!(class, "test_22b1_orphan_third_classmod.TShow");
assert_eq!(type_repr, "Int");
assert_eq!(defining_module, "test_22b1_orphan_third");
}
other => panic!("expected OrphanInstance, got {other:?}"),
}
}
// `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.
@@ -1890,29 +1724,8 @@ mod tests {
/// instances in `test_22b1_dup_same_module` — both class-leg
/// coherent, both collide on `(test_22b1_dup_same_module.TShow,
/// type_hash(Int))`. (24.2: class renamed `Show` → `TShow`.)
#[test]
fn iter22b1_duplicate_instance_fires_diagnostic() {
let entry = examples_dir().join("test_22b1_dup_same_module.ail.json");
let err = load_workspace(&entry).expect_err("must fire duplicate");
match err {
WorkspaceLoadError::DuplicateInstance {
class,
type_repr,
first_module,
second_module,
} => {
// `class` is the on-disk `inst.class` value; the
// fixture is intra-module so it stays bare.
assert_eq!(class, "TShow");
assert_eq!(type_repr, "Int");
// Both instances live in the same module post-mq.1,
// by structural necessity — see test doc-comment.
assert_eq!(first_module, "test_22b1_dup_same_module");
assert_eq!(second_module, "test_22b1_dup_same_module");
}
other => panic!("expected DuplicateInstance, got {other:?}"),
}
}
// `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)
/// method of its class fires `MissingMethod`. The fixture's
@@ -1921,23 +1734,8 @@ mod tests {
/// (Class is `TEq` (and method `tne`) rather than `Eq` / `ne` to
/// avoid colliding with the auto-loaded prelude's `class Eq` and
/// — since iter 23.5 — the prelude's polymorphic free fn `ne`.)
#[test]
fn iter22b1_missing_method_fires_diagnostic() {
let entry = examples_dir().join("test_22b1_missing_method.ail.json");
let err = load_workspace(&entry).expect_err("must fire missing-method");
match err {
WorkspaceLoadError::MissingMethod {
class,
type_repr,
method,
} => {
assert_eq!(class, "TEq");
assert_eq!(type_repr, "Int");
assert_eq!(method, "teq");
}
other => panic!("expected MissingMethod, got {other:?}"),
}
}
// `iter22b1_missing_method_fires_diagnostic` relocated to
// `crates/ailang-core/tests/workspace_pin.rs` in iter form-a.1 Task 5.
#[test]
fn class_param_in_applied_position_fires_canonical_form_rejection() {
@@ -1992,24 +1790,8 @@ mod tests {
/// at the instance site.
/// (Class is `TEq` rather than `Eq` to avoid colliding with the
/// auto-loaded prelude's `class Eq`.)
#[test]
fn instance_overriding_nonexistent_method_fires() {
let entry = std::path::PathBuf::from(
"../../examples/test_22b2_overriding_nonexistent.ail.json",
);
let err = load_workspace(&entry)
.expect_err("must fire overriding-non-existent-method");
match err {
WorkspaceLoadError::OverridingNonExistentMethod {
class, type_repr, method,
} => {
assert_eq!(class, "TEq");
assert_eq!(type_repr, "Int");
assert_eq!(method, "ne");
}
other => panic!("expected OverridingNonExistentMethod, got {other:?}"),
}
}
// `instance_overriding_nonexistent_method_fires` relocated to
// `crates/ailang-core/tests/workspace_pin.rs` in iter form-a.1 Task 5.
/// Iter 22b.2: a class method's `Type::Forall` whose `constraints`
/// reference a type variable that is neither bound by `Forall.vars`
@@ -2054,27 +1836,8 @@ mod tests {
/// 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`.)
#[test]
fn instance_without_superclass_instance_fires() {
let entry = examples_dir().join("test_22b2_missing_superclass_instance.ail.json");
let err = load_workspace(&entry)
.expect_err("must fire missing-superclass-instance");
match err {
WorkspaceLoadError::MissingSuperclassInstance {
class, superclass, type_repr,
} => {
// mq.1: `class` is the qualified registry-key class
// name (the chain walker starts from the registry's
// qualified key); `superclass` is the canonical-form
// value from `SuperclassRef.class` (intra-module here
// so it stays bare).
assert_eq!(class, "test_22b2_missing_superclass_instance.TOrd");
assert_eq!(superclass, "TEq");
assert_eq!(type_repr, "Int");
}
other => panic!("expected MissingSuperclassInstance, got {other:?}"),
}
}
// `instance_without_superclass_instance_fires` relocated to
// `crates/ailang-core/tests/workspace_pin.rs` in iter form-a.1 Task 5.
fn module_with_type_def(name: &str, type_name: &str) -> Module {
serde_json::from_value(serde_json::json!({
@@ -2737,13 +2500,8 @@ mod tests {
/// 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.
#[test]
fn mq1_xmod_constraint_class_fixture_loads() {
let entry = examples_dir().join("mq1_xmod_constraint_class.ail.json");
let ws = load_workspace(&entry).expect("workspace must load with qualified Constraint.class");
assert!(ws.modules.contains_key("mq1_xmod_constraint_class"));
assert!(ws.modules.contains_key("mq1_xmod_constraint_class_dep"));
}
// `mq1_xmod_constraint_class_fixture_loads` relocated to
// `crates/ailang-core/tests/workspace_pin.rs` in iter form-a.1 Task 5.
/// ext-cli.1 Task 1: the new `load_workspace_with` injection point
/// invokes the caller-supplied loader exactly once for the entry
@@ -0,0 +1,50 @@
//! Carve-out inventory check — §T4 of the form-a-default-authoring
//! spec. Asserts that exactly the eight named carve-out files exist
//! under `examples/*.ail.json` after milestone close. The list is
//! hardcoded; any change is a deliberate, brainstorm-level decision.
//!
//! Eight carve-outs at iter form-a.1 close:
//! - §C4 (a) subject-matter: 7 fixtures (canonical-form rejection / unbound / class-def rejection).
//! - §C4 (b) compile-time-embed: 1 fixture (prelude.ail.json).
//!
//! When the prelude-embed-refactor milestone retires §C4 (b), this
//! test's pass-count drops to seven and `EXPECTED` is re-published.
use std::collections::BTreeSet;
const EXPECTED: &[&str] = &[
// §C4 (a) — subject-matter
"broken_unbound.ail.json",
"test_22b2_invalid_superclass_param.ail.json",
"test_22b2_kind_mismatch.ail.json",
"test_22b2_unbound_constraint_var.ail.json",
"test_ct1_bad_qualifier.ail.json",
"test_ct1_bare_xmod_rejected.ail.json",
"test_ct1_qualified_class_rejected.ail.json",
// §C4 (b) — compile-time-embed
"prelude.ail.json",
];
fn examples_dir() -> std::path::PathBuf {
let crate_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
crate_dir.parent().unwrap().parent().unwrap().join("examples")
}
#[test]
#[ignore = "GREEN-ratified by Task 8 (.ail.json deletion); RED until then"]
fn examples_ail_json_inventory_matches_carve_outs() {
let expected: BTreeSet<&str> = EXPECTED.iter().copied().collect();
let mut actual: BTreeSet<String> = BTreeSet::new();
for entry in std::fs::read_dir(examples_dir()).expect("read examples/") {
let entry = entry.unwrap();
let name = entry.file_name().into_string().unwrap();
if name.ends_with(".ail.json") {
actual.insert(name);
}
}
let actual_refs: BTreeSet<&str> = actual.iter().map(String::as_str).collect();
assert_eq!(
actual_refs, expected,
"examples/*.ail.json inventory drifted from carve-out list",
);
}
@@ -14,7 +14,7 @@
use ailang_core::canonical;
use ailang_core::ast::Type;
use ailang_core::load_workspace;
use ailang_surface::load_workspace;
use std::path::Path;
fn examples_dir() -> std::path::PathBuf {
@@ -27,7 +27,7 @@ fn examples_dir() -> std::path::PathBuf {
#[test]
fn two_modules_with_same_bare_foo_both_register() {
let entry = examples_dir().join("ctt2_collision_main.ail.json");
let entry = examples_dir().join("ctt2_collision_main.ail");
let ws = load_workspace(&entry)
.expect("expected workspace load to succeed; both `type Foo` declarations live in distinct modules and must register under distinct canonical keys");
+261
View File
@@ -0,0 +1,261 @@
//! Hash-pin integration tests, relocated from `ailang-core/src/hash.rs`
//! `#[cfg(test)] mod tests` to a `tests/*` integration crate in iter
//! form-a.1 Task 5. The relocation removes the production-source file's
//! ad-hoc `examples/<stem>.ail.json` reads in favour of loading the
//! `.ail` siblings via `ailang_surface::load_module`, mirroring the
//! Form-A authoring discipline of the form-a-default-authoring
//! milestone. Hash assertions are unchanged: post-form-a the canonical
//! JSON-AST is still the hashable representation, derived in-process
//! from `.ail` by the surface loader.
//!
//! The `examples/<stem>.ail.json` fixtures that backed the original
//! tests are deleted in Task 8; the `.ail` siblings rendered in Task 2
//! are the authored form going forward. The four schema-stability pins
//! (iter13a / iter19b / iter22b1 / ct4) plus the two structural pins
//! (hash_is_stable / hash_changes_with_content / classdef-empty-optionals
//! / forall-without-constraints) all keep their assertion shape.
use ailang_core::ast::*;
use ailang_core::canonical;
use ailang_core::hash::def_hash;
fn examples_dir() -> std::path::PathBuf {
let manifest_dir = std::path::PathBuf::from(env!("CARGO_MANIFEST_DIR"));
manifest_dir.parent().unwrap().parent().unwrap().join("examples")
}
fn sample_fn() -> Def {
Def::Fn(FnDef {
name: "add".into(),
ty: Type::Fn {
params: vec![Type::int(), Type::int()],
ret: Box::new(Type::int()),
effects: vec![],
param_modes: vec![],
ret_mode: ParamMode::Implicit,
},
params: vec!["a".into(), "b".into()],
body: Term::App {
callee: Box::new(Term::Var { name: "+".into() }),
args: vec![
Term::Var { name: "a".into() },
Term::Var { name: "b".into() },
],
tail: false,
},
suppress: vec![],
doc: None,
})
}
#[test]
fn hash_is_stable() {
let h1 = def_hash(&sample_fn());
let h2 = def_hash(&sample_fn());
assert_eq!(h1, h2);
assert_eq!(h1.len(), 16);
}
#[test]
fn hash_changes_with_content() {
let mut def = sample_fn();
let h1 = def_hash(&def);
if let Def::Fn(ref mut f) = def {
f.name = "mul".into();
}
let h2 = def_hash(&def);
assert_ne!(h1, h2);
}
/// Iter 13a regression: 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
/// `skip_serializing_if` is missing or wrong.
#[test]
fn iter13a_schema_extension_preserves_pre_13a_hashes() {
let examples = examples_dir();
let sum_mod = ailang_surface::load_module(&examples.join("sum.ail"))
.expect("examples/sum.ail loads");
let sum_def = sum_mod.defs.iter().find(|d| d.name() == "sum").unwrap();
assert_eq!(def_hash(sum_def), "db33f57cb329935e");
let list_mod = ailang_surface::load_module(&examples.join("list.ail"))
.expect("examples/list.ail loads");
let int_list_def = list_mod.defs.iter().find(|d| d.name() == "IntList").unwrap();
assert_eq!(def_hash(int_list_def), "b082192bd0c99202");
}
/// Iter 19b regression: 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
/// hash drifts and `ail diff` / `ail manifest` output breaks.
#[test]
fn iter19b_empty_suppress_preserves_pre_19b_hashes() {
let with_empty_suppress = sample_fn();
// Mutate the bare sample to set suppress explicitly to a
// non-empty Vec, then mutate it back to empty: two distinct
// construction paths that should still hash identically.
let mut with_explicit_empty = sample_fn();
if let Def::Fn(ref mut f) = with_explicit_empty {
f.suppress = vec![];
}
assert_eq!(
def_hash(&with_empty_suppress),
def_hash(&with_explicit_empty),
"two FnDefs with empty suppress must hash identically"
);
// And: a FnDef with a *non-empty* suppress hashes differently.
let mut with_suppress = sample_fn();
if let Def::Fn(ref mut f) = with_suppress {
f.suppress = vec![Suppress {
code: "over-strict-mode".into(),
because: "test reason".into(),
}];
}
assert_ne!(
def_hash(&with_empty_suppress),
def_hash(&with_suppress),
"non-empty suppress must produce a distinct hash"
);
}
/// Iter 19b: 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]
fn iter19b_schema_extension_preserves_pre_19b_hashes() {
let examples = examples_dir();
let sum_mod = ailang_surface::load_module(&examples.join("sum.ail"))
.expect("examples/sum.ail loads");
let sum_def = sum_mod.defs.iter().find(|d| d.name() == "sum").unwrap();
assert_eq!(def_hash(sum_def), "db33f57cb329935e");
}
/// Iter 22b.1 regression: 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
/// unchanged.
#[test]
fn iter22b1_schema_extension_preserves_pre_22b_hashes() {
let examples = examples_dir();
let sum_mod = ailang_surface::load_module(&examples.join("sum.ail"))
.expect("examples/sum.ail loads");
let sum_def = sum_mod.defs.iter().find(|d| d.name() == "sum").unwrap();
assert_eq!(def_hash(sum_def), "db33f57cb329935e");
let list_mod = ailang_surface::load_module(&examples.join("list.ail"))
.expect("examples/list.ail loads");
let int_list_def = list_mod
.defs
.iter()
.find(|d| d.name() == "IntList")
.unwrap();
assert_eq!(def_hash(int_list_def), "b082192bd0c99202");
}
/// Iter 22b.1: 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.
#[test]
fn iter22b1_classdef_empty_optionals_hash_stable() {
let bare = Def::Class(ClassDef {
name: "Empty".into(),
param: "a".into(),
superclass: None,
methods: vec![],
doc: None,
});
let json = r#"{"kind":"class","name":"Empty","param":"a","methods":[]}"#;
let parsed: Def = serde_json::from_str(json).unwrap();
assert_eq!(
def_hash(&bare),
def_hash(&parsed),
"ClassDef with elided optionals must hash identically to construction with explicit None"
);
}
/// Iter 22b.2: 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() {
let t = Type::Forall {
vars: vec!["a".into()],
constraints: vec![],
body: Box::new(Type::fn_implicit(
vec![Type::Var { name: "a".into() }],
Type::Var { name: "a".into() },
vec![],
)),
};
let bytes = canonical::to_bytes(&t);
let s = std::str::from_utf8(&bytes).unwrap();
assert!(
!s.contains("constraints"),
"Type::Forall serialised must omit `constraints` when empty; got: {s}"
);
}
/// Iter ct.4 (canonical-type-names milestone close): pin the
/// canonical-form hashes of the two cross-module fixtures migrated by
/// `ail migrate-canonical-types` in ct.1. These hashes are the
/// post-migration state.
#[test]
fn ct4_migrated_fixtures_have_canonical_form_hashes() {
let examples = examples_dir();
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();
assert_eq!(
def_hash(main_def),
"8d17235aa3d2e127",
"ordering_match::main canonical hash must match captured post-migration value"
);
let dup_a_mod = ailang_surface::load_module(&examples.join("test_22b1_dup_a.ail"))
.expect("examples/test_22b1_dup_a.ail loads");
assert_eq!(dup_a_mod.defs.len(), 1, "test_22b1_dup_a expected to have exactly 1 def (the instance)");
assert_eq!(
def_hash(&dup_a_mod.defs[0]),
"392c247f07de6517",
"test_22b1_dup_a instance hash drifted; expected post-24.2 captured value"
);
let dup_classmod_mod = ailang_surface::load_module(&examples.join("test_22b1_dup_classmod.ail"))
.expect("examples/test_22b1_dup_classmod.ail loads");
assert_eq!(dup_classmod_mod.defs.len(), 1, "test_22b1_dup_classmod expected to have exactly 1 def (class TShow)");
assert_eq!(
def_hash(&dup_classmod_mod.defs[0]),
"b8bca96c2d09ed93",
"test_22b1_dup_classmod class TShow canonical hash; post-24.2 captured value"
);
}
/// Iter ct.4: re-assert that the canonical-form tightening did NOT
/// change hashes of intra-module-only fixtures.
#[test]
fn ct4_unmigrated_fixtures_remain_bit_identical() {
let examples = examples_dir();
let sum_mod = ailang_surface::load_module(&examples.join("sum.ail"))
.expect("examples/sum.ail loads");
let sum_def = sum_mod.defs.iter().find(|d| d.name() == "sum").unwrap();
assert_eq!(def_hash(sum_def), "db33f57cb329935e",
"sum.sum hash drifted across canonical-form tightening — unexpected");
let list_mod = ailang_surface::load_module(&examples.join("list.ail"))
.expect("examples/list.ail loads");
let int_list_def = list_mod.defs.iter().find(|d| d.name() == "IntList").unwrap();
assert_eq!(def_hash(int_list_def), "b082192bd0c99202",
"list.IntList hash drifted across canonical-form tightening — unexpected");
}
+283
View File
@@ -0,0 +1,283 @@
//! Workspace-load pin tests, relocated from `ailang-core/src/workspace.rs`
//! `#[cfg(test)] mod tests` to a `tests/*` integration crate in iter
//! form-a.1 Task 5. The relocation switches each test from
//! `ailang_core::load_workspace` (the JSON-only loader) to
//! `ailang_surface::load_workspace` (the extension-dispatching superset),
//! so the post-iter `.ail` corpus is loaded from Form A rather than the
//! deleted `.ail.json` siblings.
//!
//! Carve-out tests (the seven §C4 (a) subject-matter rejections and the
//! prelude embed) stay in-place in `workspace.rs` because their
//! `.ail.json` fixtures are not migrated; this file covers the
//! non-carve-out cohort only.
use ailang_core::workspace::{Registry, RegistryEntry, WorkspaceLoadError};
use ailang_surface::load_workspace;
use std::path::{Path, PathBuf};
fn examples_dir() -> PathBuf {
let manifest_dir = env!("CARGO_MANIFEST_DIR");
Path::new(manifest_dir).parent().unwrap().parent().unwrap().join("examples")
}
#[test]
fn loads_example_workspace_happy_path() {
let entry = examples_dir().join("ws_main.ail");
let ws = load_workspace(&entry).expect("load workspace");
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,
// 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
// 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
// type def.
let entry = examples_dir().join("ws_main.ail");
let ws = load_workspace(&entry).expect("load workspace");
assert!(
ws.modules.contains_key("prelude"),
"prelude module must be auto-injected; modules present: {:?}",
ws.modules.keys().collect::<Vec<_>>()
);
let prelude = &ws.modules["prelude"];
assert_eq!(prelude.name, "prelude");
assert!(
prelude.defs.iter().any(|d| matches!(
d,
ailang_core::ast::Def::Type(t) if t.name == "Ordering"
)),
"prelude must contain Ordering type def"
);
// Iter 23.2: prelude also ships the `Eq` class plus three
// primitive instances (Eq Int, Eq Bool, Eq Str).
assert!(
prelude.defs.iter().any(|d| matches!(
d,
ailang_core::ast::Def::Class(c) if c.name == "Eq"
)),
"prelude must contain Eq class def"
);
let eq_instance_types: Vec<&str> = prelude
.defs
.iter()
.filter_map(|d| match d {
ailang_core::ast::Def::Instance(i) if i.class == "Eq" => {
if let ailang_core::ast::Type::Con { name, .. } = &i.type_ {
Some(name.as_str())
} else {
None
}
}
_ => None,
})
.collect();
assert!(
eq_instance_types.contains(&"Int"),
"prelude must contain `instance Eq Int`; saw Eq instances on: {eq_instance_types:?}"
);
assert!(
eq_instance_types.contains(&"Bool"),
"prelude must contain `instance Eq Bool`; saw Eq instances on: {eq_instance_types:?}"
);
assert!(
eq_instance_types.contains(&"Str"),
"prelude must contain `instance Eq Str`; saw Eq instances on: {eq_instance_types:?}"
);
// Iter 23.3: prelude also ships the `Ord` class plus three
// primitive instances (Ord Int, Ord Bool, Ord Str).
assert!(
prelude.defs.iter().any(|d| matches!(
d,
ailang_core::ast::Def::Class(c) if c.name == "Ord"
)),
"prelude must contain Ord class def"
);
let ord_instance_types: Vec<&str> = prelude
.defs
.iter()
.filter_map(|d| match d {
ailang_core::ast::Def::Instance(i) if i.class == "Ord" => {
if let ailang_core::ast::Type::Con { name, .. } = &i.type_ {
Some(name.as_str())
} else {
None
}
}
_ => None,
})
.collect();
assert!(
ord_instance_types.contains(&"Int"),
"prelude must contain `instance Ord Int`; saw: {ord_instance_types:?}"
);
assert!(
ord_instance_types.contains(&"Bool"),
"prelude must contain `instance Ord Bool`; saw: {ord_instance_types:?}"
);
assert!(
ord_instance_types.contains(&"Str"),
"prelude must contain `instance Ord Str`; saw: {ord_instance_types:?}"
);
}
#[test]
fn iter22b1_workspace_with_no_classes_has_empty_registry() {
let entry = examples_dir().join("sum.ail");
let ws = load_workspace(&entry).expect("sum.ail loads");
assert!(
ws.registry.entries.values().all(|e: &RegistryEntry| e.defining_module == "prelude"),
"pre-22b fixture has no class/instance defs of its own; \
all registry entries must come from the auto-injected prelude. \
got non-prelude entries: {:?}",
ws.registry.entries.values()
.filter(|e| e.defining_module != "prelude")
.map(|e| &e.defining_module)
.collect::<Vec<_>>()
);
// Silence unused-import warning for `Registry`; the type is named
// in the test only via the field-access path above.
let _: Option<&Registry> = None;
}
#[test]
fn iter22b1_instance_in_class_module_loads_clean() {
let entry = examples_dir().join("test_22b1_orphan_class.ail");
let ws = load_workspace(&entry).expect("coherent instance loads");
let fixture_entries: Vec<_> = ws.registry.entries.iter()
.filter(|(_, e)| e.defining_module != "prelude")
.collect();
assert_eq!(fixture_entries.len(), 1);
let (key, entry) = fixture_entries[0];
// mq.1: 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");
// `instance.class` carries the canonical-form on-disk value
// (bare for same-module per the canonical-form rule).
assert_eq!(entry.instance.class, "TShow");
}
/// Iter 22b.1: 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() {
let entry = examples_dir().join("test_22b1_orphan_third.ail");
let err = load_workspace(&entry).expect_err("must fire orphan");
match err {
WorkspaceLoadError::OrphanInstance {
class,
type_repr,
defining_module,
..
} => {
assert_eq!(class, "test_22b1_orphan_third_classmod.TShow");
assert_eq!(type_repr, "Int");
assert_eq!(defining_module, "test_22b1_orphan_third");
}
other => panic!("expected OrphanInstance, got {other:?}"),
}
}
/// Iter 22b.1 / mq.1: two instances of the same `(class, type)` pair
/// collide on the registry's uniqueness check. Post-mq.1 both
/// instances must live in the class's or the type's module (see
/// fixture).
#[test]
fn iter22b1_duplicate_instance_fires_diagnostic() {
let entry = examples_dir().join("test_22b1_dup_same_module.ail");
let err = load_workspace(&entry).expect_err("must fire duplicate");
match err {
WorkspaceLoadError::DuplicateInstance {
class,
type_repr,
first_module,
second_module,
} => {
assert_eq!(class, "TShow");
assert_eq!(type_repr, "Int");
assert_eq!(first_module, "test_22b1_dup_same_module");
assert_eq!(second_module, "test_22b1_dup_same_module");
}
other => panic!("expected DuplicateInstance, got {other:?}"),
}
}
/// Iter 22b.1: an instance that omits a required (non-default) method
/// of its class fires `MissingMethod`.
#[test]
fn iter22b1_missing_method_fires_diagnostic() {
let entry = examples_dir().join("test_22b1_missing_method.ail");
let err = load_workspace(&entry).expect_err("must fire missing-method");
match err {
WorkspaceLoadError::MissingMethod {
class,
type_repr,
method,
} => {
assert_eq!(class, "TEq");
assert_eq!(type_repr, "Int");
assert_eq!(method, "teq");
}
other => panic!("expected MissingMethod, got {other:?}"),
}
}
/// Iter 22b.2: 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.)
#[test]
fn instance_overriding_nonexistent_method_fires() {
let entry = examples_dir().join("test_22b2_overriding_nonexistent.ail");
let err = load_workspace(&entry)
.expect_err("must fire overriding-non-existent-method");
match err {
WorkspaceLoadError::OverridingNonExistentMethod {
class, type_repr, method,
} => {
assert_eq!(class, "TEq");
assert_eq!(type_repr, "Int");
assert_eq!(method, "ne");
}
other => panic!("expected OverridingNonExistentMethod, got {other:?}"),
}
}
/// Iter 22b.2: 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() {
let entry = examples_dir().join("test_22b2_missing_superclass_instance.ail");
let err = load_workspace(&entry)
.expect_err("must fire missing-superclass-instance");
match err {
WorkspaceLoadError::MissingSuperclassInstance {
class, superclass, type_repr,
} => {
assert_eq!(class, "test_22b2_missing_superclass_instance.TOrd");
assert_eq!(superclass, "TEq");
assert_eq!(type_repr, "Int");
}
other => panic!("expected MissingSuperclassInstance, got {other:?}"),
}
}
/// mq.1: 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() {
let entry = examples_dir().join("mq1_xmod_constraint_class.ail");
let ws = load_workspace(&entry)
.expect("workspace must load with qualified Constraint.class");
assert!(ws.modules.contains_key("mq1_xmod_constraint_class"));
assert!(ws.modules.contains_key("mq1_xmod_constraint_class_dep"));
}
+3
View File
@@ -6,3 +6,6 @@ license.workspace = true
[dependencies]
ailang-core.workspace = true
[dev-dependencies]
ailang-surface = { path = "../ailang-surface" }
+8 -8
View File
@@ -1,12 +1,12 @@
//! Iter 20a snapshot tests.
//!
//! For each `examples/<name>.ail.json`, the renderer's output is
//! compared against the committed `examples/<name>.prose.txt`. The
//! snapshot files are part of the repo: changing the renderer requires
//! a deliberate snapshot update, which is the point of this test.
//! For each `examples/<name>.ail`, the renderer's output is compared
//! against the committed `examples/<name>.prose.txt`. The snapshot
//! files are part of the repo: changing the renderer requires a
//! deliberate snapshot update, which is the point of this test.
//!
//! Update protocol (manual): regenerate the file by running
//! `cargo run -p ail -- prose <fixture>.ail.json > <fixture>.prose.txt`
//! `cargo run -p ail -- prose <fixture>.ail > <fixture>.prose.txt`
//! once the renderer change is agreed; commit alongside the change.
//! There is no `UPDATE_SNAPSHOTS=1` environment short-cut here on
//! purpose — Iter 20a is the seeding round, and we want every change
@@ -25,11 +25,11 @@ fn examples_dir() -> PathBuf {
fn check_snapshot(stem: &str) {
let dir = examples_dir();
let json_path = dir.join(format!("{stem}.ail.json"));
let ail_path = dir.join(format!("{stem}.ail"));
let prose_path = dir.join(format!("{stem}.prose.txt"));
let module = ailang_core::load_module(&json_path)
.unwrap_or_else(|e| panic!("failed to load {json_path:?}: {e}"));
let module = ailang_surface::load_module(&ail_path)
.unwrap_or_else(|e| panic!("failed to load {ail_path:?}: {e}"));
let actual = ailang_prose::module_to_prose(&module);
let expected = std::fs::read_to_string(&prose_path)
+25
View File
@@ -275,3 +275,28 @@ fn parse_then_print_then_parse_is_idempotent_on_every_ail_fixture() {
}
eprintln!("parse→print→parse idempotency ok for {passed} .ail fixtures");
}
/// Parse-determinism (post-form-a-default-authoring §C3): for every
/// well-formed `.ail` text `t`, `parse(t)` produces the same canonical
/// bytes every invocation. Loader is a pure function of input — no
/// randomness, no time dependence, no environment leak.
#[test]
fn parse_is_deterministic_over_every_ail_fixture() {
let mut checked = 0usize;
for ail_path in list_ail_fixtures() {
let text = std::fs::read_to_string(&ail_path)
.unwrap_or_else(|e| panic!("read {ail_path:?}: {e}"));
let m1 = ailang_surface::parse(&text)
.unwrap_or_else(|e| panic!("parse {ail_path:?}: {e:?}"));
let m2 = ailang_surface::parse(&text)
.unwrap_or_else(|e| panic!("parse {ail_path:?} (2nd): {e:?}"));
let b1 = ailang_core::canonical::to_bytes(&m1);
let b2 = ailang_core::canonical::to_bytes(&m2);
assert_eq!(
b1, b2,
"parse non-determinism on {ail_path:?}",
);
checked += 1;
}
assert!(checked > 0, "no .ail fixtures found");
}
+187
View File
@@ -0,0 +1,187 @@
# iter form-a.1 — Big-bang migration (Tasks 1-5: additive phase + relocation)
**Date:** 2026-05-13
**Started from:** 1a065b37 (post-iter 24.tidy)
**Status:** DONE (Tasks 1-5 only; Tasks 6-12 deferred to next dispatch)
**Tasks completed:** 5 of 12
## Summary
First half of the form-a-default-authoring milestone-close iter: T1
adds three new tests (parse-determinism + CLI-pipeline-idempotency +
carve-out-inventory-but-`#[ignore]`'d), T2 bulk-renders the 99
missing `examples/<stem>.ail` files via `ail render` (corpus 58 →
157), T3 + T4 migrate ~26 test files from `ailang_core::load_workspace`
to `ailang_surface::load_workspace` (Group A) and flip subprocess
fixture-suffix strings from `.ail.json` to `.ail` (Group B), T5
relocates `#[cfg(test)] mod tests` blocks from production source
(`ailang-core/src/hash.rs`, `ailang-core/src/workspace.rs`
non-carve-out subset, `ailang-codegen/src/lib.rs:3717-3799`) to
integration test crates that carry `ailang-surface` as a
dev-dependency, eliminating the production-source dependency on
`.ail.json` fixture reads. The remaining Tasks 6-12 (bench-driver
suffix flips, e2e diff-test rewrite, the actual 156-file
`.ail.json` deletion, retiring obsolete roundtrip tests +
schema_coverage corpus flip, DESIGN.md §Roundtrip Invariant
restatement, CLAUDE.md + DESIGN.md doctrine edits, WhatsNew +
roadmap close) run in the Boss's next dispatch on this iter ID.
`cargo test --workspace` is green at every per-task boundary (560
passed + 4 ignored at iter-half close; 558 + 3 new 1 transit
`#[ignore]` on the inventory test). One Task-2 plan defect surfaced
inline and was repaired by pre-emptive forward-pull of Task-3
migrations.
## Per-task notes
- **iter form-a.1.1** — Added `parse_is_deterministic_over_every_ail_fixture`
(round_trip.rs) and `cli_parse_then_render_then_parse_is_idempotent`
(roundtrip_cli.rs); created `crates/ailang-core/tests/carve_out_inventory.rs`
with the 8-file hardcoded list and `#[ignore]`'d until Task 8.
The plan's `cli_parse_then_render_then_parse_is_idempotent` body
used an `ail()` helper that doesn't exist in the repo (assert_cmd
is not a dev-dep); adapted to use `Command::new(ail_bin())`
matching the file's existing pattern. Plan acknowledged this
possibility — "verify before writing the body; if `list_ail_fixtures`
is missing in roundtrip_cli.rs, add a parallel helper at lines 47+."
- **iter form-a.1.2** — Rendered the 99 missing `.ail` files. The
bulk render exposed a recon defect: `load_workspace` (since
ext-cli.1) prefers `.ail` siblings for *imports* even when the
entry is `.ail.json`. The seven Group-A test files whose entries
were `.ail.json` but whose imports gained newly-rendered `.ail`
siblings broke at Task 2's green-gate. Repair pulled forward five
Task-3 migrations (mono_xmod_ctor_pattern, 3 sites in
typeclass_22b2, 1 site in typeclass_22b3, env_construction_pin,
4 sites in ailang-check/tests/workspace.rs, ctt2_registry_rekey)
plus `#[ignore]`'d the 4 fixture-loading tests in
`ailang-core/src/workspace.rs` `mod tests` with explicit
`form-a.1 transit: Task 5 relocates...` markers. Also added
`ailang-surface = { path = "../ailang-surface" }` to
`crates/ailang-core/Cargo.toml` `[dev-dependencies]` (required
for the ctt2_registry_rekey integration test to use
`ailang_surface::load_workspace`).
- **iter form-a.1.3** — Finished Group A migration across the
remaining 12 Group-A files (10 + the 2 already migrated in T2
scope-pull). Two files (`mono_recursive_fn.rs`,
`mono_xmod_qualified_ref.rs`) were Group-B-pattern (subprocess
CLI, suffix-only flip) but listed in plan's Group A; treated
per their actual shape.
- **iter form-a.1.4** — Group-B migration: bulk regex flip of
`build_and_run("X.ail.json")``build_and_run("X.ail")` (3
patterns) in e2e.rs across 59 invocations; plus targeted
subprocess `examples/<fixture>.ail.json``.ail` flips at 11
more e2e.rs sites (6× `ws_main`, 2× `sum`, 1× `escape_local_demo`,
1× `list_map_poly`, 1× `std_either` left as-is per its raw-bytes
roundtrip semantics). Four additional sites in e2e.rs preserved
intentionally for raw-JSON inspection (`borrow_own_demo`,
`reuse_as_demo`, the `sum.ail.json` diff-test pending Task 7
rewrite, and the `ail_run_accepts_ail_source...` dual-form smoke).
Four plan-Group-B files (`mono_hash_stability`, `prelude_free_fns`,
`print_mono_body_shape`, `show_mono_synthesis`) were
Group-A-pattern (consumers of `load_workspace`); migrated both
imports AND paths.
- **iter form-a.1.5** — Relocated 10 fixture-loading tests from
`ailang-core/src/workspace.rs` `mod tests` to new
`crates/ailang-core/tests/workspace_pin.rs` (integration test
crate); relocated all 8 tests from `ailang-core/src/hash.rs`
`mod tests` plus 2 in-memory unit tests to
`crates/ailang-core/tests/hash_pin.rs`; relocated 3 tests from
`ailang-codegen/src/lib.rs:3717-3799` to
`crates/ailang-codegen/tests/eq_primitives_pin.rs`; migrated
`ailang-prose/tests/snapshot.rs` (helper + 8 fixtures) to `.ail`
+ `ailang_surface::load_module`. Carve-out tests stay in-place
(3× `class_param_in_applied_position` / `superclass_with_wrong_param`
/ `constraint_with_unbound_var` for 22b2; 3× `ct1_fixture_*`
for ct.1). Tempdir-based loader tests
(`user_module_named_prelude_is_rejected`, `detects_import_cycle`,
`module_not_found_yields_structured_error`) stay in-place too —
they don't consume the examples corpus, so they don't need
migration for the Form-A goal. Added `ailang-surface` as a
`[dev-dependencies]` entry to `ailang-codegen/Cargo.toml` and
`ailang-prose/Cargo.toml`. Hash.rs has `mod tests {}` placeholder
kept; workspace.rs `mod tests` shrunk from 73 to 57 tests +
6 carve-outs unchanged.
## Concerns
- **e2e.rs raw-JSON-inspect tests** (4 sites: `borrow_own_demo_modes_are_metadata_only`,
`reuse_as_demo_under_rc_uses_inplace_rewrite`, `diff_detects_changed_def`,
`render_parse_round_trip_canonical`, plus the dual-form smoke
`ail_run_accepts_ail_source_with_same_stdout_as_ail_json`) currently
read raw `.ail.json` bytes for substring asserts or byte-roundtrip
comparison. They pass today (the `.ail.json` files still exist
pre-T8) but will break at T8 when `.ail.json` siblings are deleted.
Plan Task 7 only addresses the `diff_detects_changed_def` test (the
one that mutates JSON). The other three are out of T1-5 scope and
inherit to Boss for T6-12 dispatch resolution (either parse-derived
JSON via `ail parse` per T7 pattern, or `#[ignore]` with explicit
marker).
- **`ailang-core/src/workspace.rs` `mod tests`**: 3 tempdir-based
loader-mechanism tests (lines :1743, :1759, :1776 pre-relocation;
current line numbers shifted by the deletions) remain in-place
per implementer-judgement (they don't consume `examples/`, so
the Form-A goal is trivially satisfied). Plan suggested
relocating them for doctrinal cleanup; scope reduction is
deliberate. They continue to test what they always tested
(loader cycle/missing-module/user-prelude-conflict behaviour)
and pass.
- **Plan recon defect on Group-A vs. Group-B classification**:
`mono_hash_stability.rs`, `prelude_free_fns.rs`,
`print_mono_body_shape.rs`, `show_mono_synthesis.rs` were listed
in plan's Group B but actually use `ailang_core::workspace::load_workspace`
directly (Group-A pattern). Migrated correctly by switching both
the import and the path. Recorded so a future recon-tightening
pass on the planner skill can review whether path-grep-only
recon is sufficient for Group A/B classification.
- **`mono_recursive_fn.rs` and `mono_xmod_qualified_ref.rs`** were
symmetric — listed in plan's Group A but actually CLI subprocess
callers (Group-B pattern). Suffix-only flip applied as if Group B.
## Known debt
- 4 raw-JSON-inspect tests in e2e.rs need T7-style rewrites or
`#[ignore]` markers before Task 8's `.ail.json` deletion lands
(see Concerns).
- 3 tempdir tests in `ailang-core/src/workspace.rs` `mod tests`
could be relocated as doctrinal cleanup but functionally don't
need to be (see Concerns).
- `mod tests {}` placeholder in `ailang-core/src/hash.rs` could
be deleted entirely; left as a structural breadcrumb pointing at
the integration test crate.
## Files touched
Working tree: 37 modified + 14 new test files + 99 new `.ail`
fixtures = 150 paths. Detail:
### Modified production / test source (37)
- `crates/ail/tests/`: codegen_import_map_fallback_pin.rs,
ct1_check_cli.rs, e2e.rs (extensive build_and_run + 11
subprocess sites), eq_float_noinstance.rs, eq_ord_e2e.rs,
floats_e2e.rs, ir_snapshot.rs, mono_hash_stability.rs,
mono_recursive_fn.rs, mono_unification.rs, mono_xmod_ctor_pattern.rs,
mono_xmod_qualified_ref.rs, mq3_multi_class_e2e.rs,
polyfn_dot_qualified_branch_pin.rs, prelude_free_fns.rs,
print_mono_body_shape.rs, roundtrip_cli.rs, show_mono_synthesis.rs,
show_no_instance_e2e.rs, show_print_e2e.rs, typeclass_22b2.rs,
typeclass_22b3.rs, typeclass_22c.rs (23 files)
- `crates/ailang-check/tests/`: env_construction_pin.rs,
method_collision_pin.rs, show_dispatch_pin.rs, workspace.rs (4)
- `crates/ailang-codegen/`: Cargo.toml + src/lib.rs (2)
- `crates/ailang-core/`: Cargo.toml + src/hash.rs + src/workspace.rs
+ tests/ctt2_registry_rekey.rs (4)
- `crates/ailang-prose/`: Cargo.toml + tests/snapshot.rs (2)
- `crates/ailang-surface/tests/round_trip.rs` (1)
- `Cargo.lock` (1)
### New (14 test files + 99 rendered .ail fixtures)
- `crates/ailang-codegen/tests/eq_primitives_pin.rs` (relocated T5)
- `crates/ailang-core/tests/hash_pin.rs` (relocated T5)
- `crates/ailang-core/tests/workspace_pin.rs` (relocated T5)
- `crates/ailang-core/tests/carve_out_inventory.rs` (new T1)
- 99× `examples/<stem>.ail` rendered via `ail render` (T2)
## Stats
bench/orchestrator-stats/2026-05-13-iter-form-a.1.json
+23
View File
@@ -0,0 +1,23 @@
(module bench_mono_dispatch
(class Foo
(param a)
(method foo
(type (fn-type (params a) (ret (con Int))))))
(instance
(class Foo)
(type (con Int))
(method foo
(body (lam (params (typed x a)) (ret (con Int)) (body (app + (app * x 1103515245) 12345))))))
(class Looper
(param a)
(method loop_call
(type (fn-type (params a (con Int)) (ret (con Int))))))
(instance
(class Looper)
(type (con Int))
(method loop_call
(body (lam (params (typed i a) (typed acc (con Int))) (ret (con Int)) (body (if (app == i 0) acc (tail-app loop_call (app - i 1) (app + acc (app foo (app + acc i))))))))))
(fn main
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body (do io/print_int (app loop_call 100000000 0)))))
+6
View File
@@ -0,0 +1,6 @@
(module bool_to_str_drop_rc
(fn main
(doc "Iter 24.1: pin that `bool_to_str` participates in RC discipline. Bind the heap-Str result to `s`, consume `s` once in `io/print_str`, let the binder drop at scope close. Under --alloc=rc with AILANG_RC_STATS=1 the atexit summary must report allocs == 1 && frees == 1 && live == 0 — the heap-Str slab allocated by str_alloc inside ailang_bool_to_str rides the same rc_header + ailang_rc_dec path as int_to_str.")
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body (let s (app bool_to_str true) (do io/print_str s)))))
+6
View File
@@ -0,0 +1,6 @@
(module bool_to_str_smoke_false
(fn main
(doc "Iter 24.1: stdout-smoke for the false branch of bool_to_str. Stdout must be `false\\n` (puts adds the newline). Companion of bool_to_str_drop_rc.ail.json (which covers the true branch and the RC-stats invariant).")
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body (let s (app bool_to_str false) (do io/print_str s)))))
+11
View File
@@ -0,0 +1,11 @@
(module closure
(fn apply
(doc "apply a fn-of-Int to an Int")
(type (fn-type (params (fn-type (params (con Int)) (ret (con Int))) (con Int)) (ret (con Int))))
(params f x)
(body (app f x)))
(fn main
(doc "captures `n` from the enclosing let, returns 42")
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body (let n 3 (do io/print_int (app apply (lam (params (typed x (con Int))) (ret (con Int)) (body (app + x n))) 39))))))
+11
View File
@@ -0,0 +1,11 @@
(module cmp_max_smoke
(fn cmp_max
(type (forall (vars a) (constraints (constraint prelude.Ord a)) (fn-type (params a a) (ret a))))
(params x y)
(body (match (app compare x y)
(case (pat-ctor LT) y)
(case _ x))))
(fn main
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body (do io/print_int (app cmp_max 3 7)))))
+32
View File
@@ -0,0 +1,32 @@
(module compare_primitives_smoke
(fn main
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body (seq (do io/print_int (match (app compare 1 2)
(case (pat-ctor LT) 1)
(case (pat-ctor EQ) 2)
(case (pat-ctor GT) 3))) (seq (do io/print_int (match (app compare 1 1)
(case (pat-ctor LT) 1)
(case (pat-ctor EQ) 2)
(case (pat-ctor GT) 3))) (seq (do io/print_int (match (app compare 2 1)
(case (pat-ctor LT) 1)
(case (pat-ctor EQ) 2)
(case (pat-ctor GT) 3))) (seq (do io/print_int (match (app compare false true)
(case (pat-ctor LT) 1)
(case (pat-ctor EQ) 2)
(case (pat-ctor GT) 3))) (seq (do io/print_int (match (app compare false false)
(case (pat-ctor LT) 1)
(case (pat-ctor EQ) 2)
(case (pat-ctor GT) 3))) (seq (do io/print_int (match (app compare true false)
(case (pat-ctor LT) 1)
(case (pat-ctor EQ) 2)
(case (pat-ctor GT) 3))) (seq (do io/print_int (match (app compare "a" "b")
(case (pat-ctor LT) 1)
(case (pat-ctor EQ) 2)
(case (pat-ctor GT) 3))) (seq (do io/print_int (match (app compare "a" "a")
(case (pat-ctor LT) 1)
(case (pat-ctor EQ) 2)
(case (pat-ctor GT) 3))) (do io/print_int (match (app compare "b" "a")
(case (pat-ctor LT) 1)
(case (pat-ctor EQ) 2)
(case (pat-ctor GT) 3))))))))))))))
+5
View File
@@ -0,0 +1,5 @@
(module ctt2_collision_cls
(class MyC
(param a)
(method op
(type (fn-type (params a) (ret (con Int)))))))
+9
View File
@@ -0,0 +1,9 @@
(module ctt2_collision_lib
(import ctt2_collision_cls)
(data Foo
(ctor MkLib))
(instance
(class ctt2_collision_cls.MyC)
(type (con Foo))
(method op
(body (lam (params (typed _x (con Foo))) (ret (con Int)) (body 2))))))
+10
View File
@@ -0,0 +1,10 @@
(module ctt2_collision_main
(import ctt2_collision_cls)
(import ctt2_collision_lib)
(data Foo
(ctor MkMain))
(instance
(class ctt2_collision_cls.MyC)
(type (con Foo))
(method op
(body (lam (params (typed _x (con Foo))) (ret (con Int)) (body 1))))))
+5
View File
@@ -0,0 +1,5 @@
(module eq_float_noinstance
(fn main
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body (do io/print_int (if (app eq 1.0 2.0) 1 0)))))
+10
View File
@@ -0,0 +1,10 @@
(module eq_ord_polymorphic
(fn at_most
(doc "Composition of prelude `gt` with `not` — semantically equivalent to `le`. Used to exercise polymorphic-helper-over-prelude-fns composition through the unified mono pass.")
(type (forall (vars a) (constraints (constraint prelude.Ord a)) (fn-type (params (borrow a) (borrow a)) (ret (con Bool)))))
(params x y)
(body (app not (app gt x y))))
(fn main
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body (seq (do io/print_int (if (app at_most 3 5) 1 0)) (seq (do io/print_int (if (app at_most true false) 1 0)) (do io/print_int (if (app at_most "abc" "abd") 1 0)))))))
+24
View File
@@ -0,0 +1,24 @@
(module eq_ord_user_adt
(data IntBox
(doc "A one-field box around an Int, used to demonstrate that the unified mono pass synthesises eq__IntBox from a user-written instance, not from the primitive == operator.")
(ctor MkIntBox (con Int)))
(instance
(class prelude.Eq)
(type (con IntBox))
(doc "Eq IntBox by structural unwrap of the inner Int.")
(method eq
(body (lam (params (typed a (con IntBox)) (typed b (con IntBox))) (ret (con Bool)) (body (match a
(case (pat-ctor MkIntBox ai) (match b
(case (pat-ctor MkIntBox bi) (app == ai bi))))))))))
(instance
(class prelude.Ord)
(type (con IntBox))
(doc "Ord IntBox by delegating to Int's compare on the inner field.")
(method compare
(body (lam (params (typed a (con IntBox)) (typed b (con IntBox))) (ret (con prelude.Ordering)) (body (match a
(case (pat-ctor MkIntBox ai) (match b
(case (pat-ctor MkIntBox bi) (app compare ai bi))))))))))
(fn main
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body (seq (do io/print_int (if (app eq (term-ctor IntBox MkIntBox 3) (term-ctor IntBox MkIntBox 3)) 1 0)) (do io/print_int (if (app eq (term-ctor IntBox MkIntBox 3) (term-ctor IntBox MkIntBox 5)) 1 0))))))
+10
View File
@@ -0,0 +1,10 @@
(module eq_primitives_smoke
(fn to_int
(doc "1 if the bool is true, 0 otherwise. Used to render Eq results as printable Ints.")
(type (fn-type (params (con Bool)) (ret (con Int))))
(params b)
(body (if b 1 0)))
(fn main
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body (seq (do io/print_int (app to_int (app eq 7 7))) (seq (do io/print_int (app to_int (app eq 7 8))) (seq (do io/print_int (app to_int (app eq true true))) (seq (do io/print_int (app to_int (app eq true false))) (seq (do io/print_int (app to_int (app eq "hello" "hello"))) (do io/print_int (app to_int (app eq "hello" "world")))))))))))
+6
View File
@@ -0,0 +1,6 @@
(module float_to_str_smoke
(fn main
(doc "Iter hs.4: smoke pin for the float_to_str heap-Str builtin. `do io/print_str(float_to_str(3.5))` builds through the whole pipeline (checker resolves float_to_str:(Float)->Str, codegen lowers to call ptr @ailang_float_to_str(double 3.5), runtime/str.c's ailang_float_to_str snprintfs into a heap-Str slab via libc %g, io/print_str's @puts prints from the bytes pointer at offset 8). The exact rendering of 3.5 is libc-target-dependent; on the dev target's clang+glibc with default C locale it is the three bytes `3.5`. Single-value smoke; NaN / +Inf / -Inf edge cases are deferred per the hs.4 plan.")
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body (do io/print_str (app float_to_str 3.5)))))
+5
View File
@@ -0,0 +1,5 @@
(module floats
(fn main
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body (seq (do io/print_float (app + 1.5 2.5)) (seq (do io/print_float (app int_to_float 42)) (do io/print_float (app neg 1.5)))))))
+5
View File
@@ -0,0 +1,5 @@
(module ge_at_int_smoke
(fn main
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body (do io/print_int (if (app ge 3 5) 1 0)))))
+5
View File
@@ -0,0 +1,5 @@
(module gt_at_bool_smoke
(fn main
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body (do io/print_int (if (app gt true false) 1 0)))))
@@ -0,0 +1,6 @@
(module heap_str_repeated_print_borrow
(fn main
(doc "Iter eob.1: heap-Str passed to io/print_str twice in sequence. Under the new rule (Term::Do args = Borrow), the second call no longer triggers use-after-consume; the linearity check accepts the program. Under --alloc=rc + AILANG_RC_STATS=1 the slab allocates once and is freed once at scope close (allocs == 1, frees == 1, live == 0).")
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body (let s (app int_to_str 42) (seq (do io/print_str s) (do io/print_str s))))))
+15
View File
@@ -0,0 +1,15 @@
(module hof
(fn inc
(doc "increment by one")
(type (fn-type (params (con Int)) (ret (con Int))))
(params x)
(body (app + x 1)))
(fn apply
(doc "apply a fn-of-Int to an Int")
(type (fn-type (params (fn-type (params (con Int)) (ret (con Int))) (con Int)) (ret (con Int))))
(params f x)
(body (app f x)))
(fn main
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body (do io/print_int (app apply inc 41)))))
+6
View File
@@ -0,0 +1,6 @@
(module int_to_print_int_borrow
(fn main
(doc "Iter eob.1: primitive Int passed to io/print_int. The rule that Term::Do args are Borrow has no observable RC effect here because Int is unboxed; the test pins that no spurious bookkeeping appears (allocs == 0, frees == 0, live == 0).")
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body (let n 7 (do io/print_int n)))))
+6
View File
@@ -0,0 +1,6 @@
(module int_to_str_drop_rc
(fn main
(doc "Iter hs.4: pin that `int_to_str` participates in RC discipline. Bind the heap-Str result to `s`, consume `s` once in `io/print_str`, let the binder drop at scope close. Under --alloc=rc with AILANG_RC_STATS=1 the atexit summary must report allocs == frees && live == 0 — the heap-Str slab allocated by str_alloc inside ailang_int_to_str rides the same rc_header + ailang_rc_dec path as any other RC value.")
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body (let s (app int_to_str 42) (do io/print_str s)))))
+6
View File
@@ -0,0 +1,6 @@
(module int_to_str_smoke
(fn main
(doc "Iter hs.4: smoke pin for the int_to_str heap-Str builtin. `do io/print_str(int_to_str(42))` builds through the whole pipeline (checker resolves int_to_str:(Int)->Str, codegen lowers to call ptr @ailang_int_to_str(i64 42), runtime/str.c's ailang_int_to_str snprintfs into a heap-Str slab, io/print_str's @puts prints from the bytes pointer at offset 8). Single-value smoke; the spec's broader edge-case sweep (0, -1, i64::MAX, i64::MIN) is deferred to a follow-up tidy iter per the hs.4 plan.")
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body (do io/print_str (app int_to_str 42)))))
+5
View File
@@ -0,0 +1,5 @@
(module le_at_str_smoke
(fn main
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body (do io/print_int (if (app le "abc" "abd") 1 0)))))
+17
View File
@@ -0,0 +1,17 @@
(module list
(data IntList
(doc "Einfach verkettete Int-Liste, boxed.")
(ctor Nil)
(ctor Cons (con Int) (con IntList)))
(fn sum_list
(doc "Summiert die Elemente einer Int-Liste rekursiv.")
(type (fn-type (params (con IntList)) (ret (con Int))))
(params xs)
(body (match xs
(case (pat-ctor Nil) 0)
(case (pat-ctor Cons h t) (app + h (app sum_list t))))))
(fn main
(doc "Baut [10, 20, 12] und druckt die Summe (42).")
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body (let xs (term-ctor IntList Cons 10 (term-ctor IntList Cons 20 (term-ctor IntList Cons 12 (term-ctor IntList Nil)))) (do io/print_int (app sum_list xs))))))
+24
View File
@@ -0,0 +1,24 @@
(module list_map
(data IntList
(doc "Singly-linked list of Int, boxed.")
(ctor Nil)
(ctor Cons (con Int) (con IntList)))
(fn map_int
(doc "Apply f to every element. Recursive on the tail.")
(type (fn-type (params (fn-type (params (con Int)) (ret (con Int))) (con IntList)) (ret (con IntList))))
(params f xs)
(body (match xs
(case (pat-ctor Nil) (term-ctor IntList Nil))
(case (pat-ctor Cons h t) (term-ctor IntList Cons (app f h) (app map_int f t))))))
(fn print_list
(doc "Print each Int on its own line.")
(type (fn-type (params (con IntList)) (ret (con Unit)) (effects IO)))
(params xs)
(body (match xs
(case (pat-ctor Nil) (lit-unit))
(case (pat-ctor Cons h t) (seq (do io/print_int h) (app print_list t))))))
(fn main
(doc "Build [1,2,3], double each, print result.")
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body (let xs (term-ctor IntList Cons 1 (term-ctor IntList Cons 2 (term-ctor IntList Cons 3 (term-ctor IntList Nil)))) (app print_list (app map_int (lam (params (typed x (con Int))) (ret (con Int)) (body (app * x 2))) xs))))))
+5
View File
@@ -0,0 +1,5 @@
(module lt_at_int_smoke
(fn main
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body (do io/print_int (if (app lt 3 5) 1 0)))))
+14
View File
@@ -0,0 +1,14 @@
(module max3
(fn max
(type (fn-type (params (con Int) (con Int)) (ret (con Int))))
(params a b)
(body (if (app > a b) a b)))
(fn max3
(doc "Demonstriert verschachteltes if (statt max-call) zum Test des Block-Trackings.")
(type (fn-type (params (con Int) (con Int) (con Int)) (ret (con Int))))
(params a b c)
(body (if (app > a b) (if (app > a c) a c) (if (app > b c) b c))))
(fn main
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body (do io/print_int (app max3 3 17 9)))))
+17
View File
@@ -0,0 +1,17 @@
(module maybe_int
(data Maybe (vars a)
(doc "Optional value with a polymorphic payload.")
(ctor None)
(ctor Some a))
(fn or_else
(doc "Returns the wrapped Int for Some(x) and the default d for None.")
(type (fn-type (params (con Maybe (con Int)) (con Int)) (ret (con Int))))
(params m d)
(body (match m
(case (pat-ctor None) d)
(case (pat-ctor Some x) x))))
(fn main
(doc "Print or_else for both arms; expected 7 then 99.")
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body (seq (do io/print_int (app or_else (term-ctor Maybe Some 7) 99)) (do io/print_int (app or_else (term-ctor Maybe None) 99))))))
+12
View File
@@ -0,0 +1,12 @@
(module mono_hash_pin_smoke
(fn ord_to_int
(type (fn-type (params (con prelude.Ordering)) (ret (con Int))))
(params o)
(body (match o
(case (pat-ctor LT) -1)
(case (pat-ctor EQ) 0)
(case (pat-ctor GT) 1))))
(fn main
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body (seq (do io/print_bool (app eq 1 1)) (seq (do io/print_bool (app eq true true)) (seq (do io/print_bool (app eq "a" "a")) (seq (do io/print_int (app ord_to_int (app compare 1 2))) (seq (do io/print_int (app ord_to_int (app compare false true))) (do io/print_int (app ord_to_int (app compare "a" "b")))))))))))
+7
View File
@@ -0,0 +1,7 @@
(module mq1_xmod_constraint_class
(import mq1_xmod_constraint_class_dep)
(fn useShow
(doc "Positive mq.1 fixture: a polymorphic fn that takes a single value and a Show constraint, ignores the value, and returns Unit. Exercises the qualified `Constraint.class` shape end-to-end (loader + validator + check_workspace).")
(type (forall (vars a) (constraints (constraint mq1_xmod_constraint_class_dep.Show a)) (fn-type (params (borrow a)) (ret (con Unit)))))
(params _x)
(body (lit-unit))))
@@ -0,0 +1,5 @@
(module mq1_xmod_constraint_class_dep
(class Show
(param a)
(method show
(type (fn-type (params a) (ret (con Str)))))))
+8
View File
@@ -0,0 +1,8 @@
(module mq3_class_eq_vs_fn_eq
(import mq3_class_eq_vs_fn_eq_classmod)
(import mq3_class_eq_vs_fn_eq_fnmod)
(fn main
(doc "mq.3.6 fixture (c): bare `myeq 1 2` in a workspace with `class MyEq` (in classmod) and `fn myeq` (in fnmod) both imported. Fn wins per lookup precedence; the typechecker emits `class-method-shadowed-by-fn` warning so the LLM-author sees the shadow.")
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body (do io/print_bool (app myeq 1 2)))))
@@ -0,0 +1,10 @@
(module mq3_class_eq_vs_fn_eq_classmod
(class MyEq
(param a)
(method myeq
(type (fn-type (params (borrow a) (borrow a)) (ret (con Bool))))))
(instance
(class MyEq)
(type (con Int))
(method myeq
(body (lam (params (typed x (con Int)) (typed y (con Int))) (ret (con Bool)) (body true))))))
+6
View File
@@ -0,0 +1,6 @@
(module mq3_class_eq_vs_fn_eq_fnmod
(fn myeq
(doc "mq.3.6 fixture (c) helper: free fn `myeq` that shadows the class method `myeq` declared in `mq3_class_eq_vs_fn_eq_classmod`. Always returns false so the fn-wins precedence is observable at runtime if the e2e test ever runs the binary.")
(type (fn-type (params (borrow (con Int)) (borrow (con Int))) (ret (con Bool))))
(params x y)
(body false)))
+8
View File
@@ -0,0 +1,8 @@
(module mq3_two_show_ambiguous
(import mq3_two_show_ambiguous_a)
(import mq3_two_show_ambiguous_b)
(fn main
(doc "mq.3.6 fixture (a): bare `show 42` in a workspace where two Show classes (in modA and modB) each ship Show Int. Without an explicit qualifier the dispatch is genuinely ambiguous; the typechecker must fire `ambiguous-method-resolution` naming both candidate classes.")
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body (do io/print_str (app show 42)))))
+10
View File
@@ -0,0 +1,10 @@
(module mq3_two_show_ambiguous_a
(class Show
(param a)
(method show
(type (fn-type (params (borrow a)) (ret (con Str))))))
(instance
(class Show)
(type (con Int))
(method show
(body (lam (params (typed x (con Int))) (ret (con Str)) (body (app int_to_str x)))))))
+10
View File
@@ -0,0 +1,10 @@
(module mq3_two_show_ambiguous_b
(class Show
(param a)
(method show
(type (fn-type (params (borrow a)) (ret (con Str))))))
(instance
(class Show)
(type (con Int))
(method show
(body (lam (params (typed x (con Int))) (ret (con Str)) (body (app int_to_str x)))))))
+8
View File
@@ -0,0 +1,8 @@
(module mq3_two_show_qualified
(import mq3_two_show_ambiguous_a)
(import mq3_two_show_ambiguous_b)
(fn main
(doc "mq.3.6 fixture (b): explicit qualifier `mq3_two_show_ambiguous_a.Show.show 42` disambiguates the same workspace as fixture (a). Resolves cleanly to modA's class; typecheck passes.")
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body (do io/print_str (app mq3_two_show_ambiguous_a.Show.show 42)))))
+5
View File
@@ -0,0 +1,5 @@
(module ne_at_int_smoke
(fn main
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body (do io/print_int (if (app ne 3 5) 1 0)))))
+9
View File
@@ -0,0 +1,9 @@
(module ordering_match
(fn main
(doc "Iter 23.1 fixture: pattern-match on prelude Ordering ctor without explicit prelude import.")
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body (do io/print_int (match (term-ctor prelude.Ordering LT)
(case (pat-ctor LT) 1)
(case (pat-ctor EQ) 2)
(case (pat-ctor GT) 3))))))
+13
View File
@@ -0,0 +1,13 @@
(module poly_apply
(fn apply
(type (forall (vars a b) (fn-type (params (fn-type (params a) (ret b)) a) (ret b))))
(params f x)
(body (app f x)))
(fn succ
(type (fn-type (params (con Int)) (ret (con Int))))
(params n)
(body (app + n 1)))
(fn main
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body (do io/print_int (app apply succ 41)))))
+9
View File
@@ -0,0 +1,9 @@
(module poly_id
(fn id
(type (forall (vars a) (fn-type (params a) (ret a))))
(params x)
(body x))
(fn main
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body (seq (do io/print_int (app id 42)) (do io/print_bool (app id true))))))
+10
View File
@@ -0,0 +1,10 @@
(module rc_box_drop
(data Box
(doc "Single-cell ADT around an Int. Iter 18c.3 RC fixture: shallow free is sufficient because Box has no boxed children.")
(ctor MkBox (con Int)))
(fn main
(doc "Bind a heap-allocated MkBox to `b`, read its Int via match, print it. Under --alloc=rc, codegen emits `ailang_rc_dec(b)` at the let scope close: `b` is unique, has zero consume uses (only the match scrutinee, a borrow), and the body's tail value is the printed Unit, not `b` itself. The Box has no boxed children so shallow `free()` is correct.")
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body (let b (term-ctor Box MkBox 42) (match b
(case (pat-ctor MkBox x) (do io/print_int x)))))))
+17
View File
@@ -0,0 +1,17 @@
(module rc_list_drop
(data IntList
(doc "Recursive Int list. Iter 18c.4 RC fixture: codegen emits `drop_rc_list_drop_IntList` whose `Cons` arm recursively calls itself on the tail field.")
(ctor Nil)
(ctor Cons (con Int) (con IntList)))
(fn sum_list
(doc "Recursive fold over IntList.")
(type (fn-type (params (con IntList)) (ret (con Int))))
(params xs)
(body (match xs
(case (pat-ctor Nil) 0)
(case (pat-ctor Cons h t) (app + h (app sum_list t))))))
(fn main
(doc "Build a 5-element IntList [1,2,3,4,5] and print its sum (15). Under --alloc=rc the recursive drop_rc_list_drop_IntList fn cascades through the tail at process exit when sum_list returns ownership-implicit (the consume_count of `xs` is 1 so no dec at the outer let; the test's correctness invariant is the byte-identical stdout — leak quantification is 18f's bench).")
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body (let xs (term-ctor IntList Cons 1 (term-ctor IntList Cons 2 (term-ctor IntList Cons 3 (term-ctor IntList Cons 4 (term-ctor IntList Cons 5 (term-ctor IntList Nil)))))) (do io/print_int (app sum_list xs))))))
+12
View File
@@ -0,0 +1,12 @@
(module rc_list_drop_borrow
(data IntList
(doc "Recursive Int list. Iter 18c.4 RC fixture: codegen emits a recursive drop fn; this fixture keeps the binder consume_count at 0 so the drop call actually fires at scope close, exercising the recursive cascade at runtime over a 5-element list.")
(ctor Nil)
(ctor Cons (con Int) (con IntList)))
(fn main
(doc "Build a 5-element IntList and print only its head via match. xs has consume_count == 0 (only the match scrutinee, a borrow), so under --alloc=rc the let scope close emits `drop_rc_list_drop_borrow_IntList(xs)` which recursively dec-cascades over the whole 5-cell chain.")
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body (let xs (term-ctor IntList Cons 11 (term-ctor IntList Cons 22 (term-ctor IntList Cons 33 (term-ctor IntList Cons 44 (term-ctor IntList Cons 55 (term-ctor IntList Nil)))))) (match xs
(case (pat-ctor Nil) (do io/print_int 0))
(case (pat-ctor Cons h t) (do io/print_int h)))))))
+5
View File
@@ -0,0 +1,5 @@
(module show_mono_pin_smoke
(fn main
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body (let s1 (app show 42) (let s2 (app show true) (let s3 (app show "x") (let s4 (app show 1.5) (do io/print_str s1))))))))
+5
View File
@@ -0,0 +1,5 @@
(module show_no_instance
(fn main
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body (let f (lam (params (typed x (con Int))) (ret (con Int)) (body x)) (app print f)))))
+5
View File
@@ -0,0 +1,5 @@
(module show_print_smoke
(fn main
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body (seq (seq (seq (app print 42) (app print true)) (app print "hello")) (app print 3.14)))))
+13
View File
@@ -0,0 +1,13 @@
(module show_user_adt
(data IntBox
(ctor MkIntBox (con Int)))
(instance
(class prelude.Show)
(type (con IntBox))
(method show
(body (lam (params (typed x (con IntBox))) (ret (con Str)) (body (match x
(case (pat-ctor MkIntBox n) (app int_to_str n))))))))
(fn main
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body (app print (term-ctor IntBox MkIntBox 7)))))
+30
View File
@@ -0,0 +1,30 @@
(module sort
(data IntList
(doc "Singly-linked Int list, boxed.")
(ctor Nil)
(ctor Cons (con Int) (con IntList)))
(fn insert
(doc "Insert y into a sorted list xs, preserving order.")
(type (fn-type (params (con Int) (con IntList)) (ret (con IntList))))
(params y xs)
(body (match xs
(case (pat-ctor Nil) (term-ctor IntList Cons y (term-ctor IntList Nil)))
(case (pat-ctor Cons h t) (if (app <= y h) (term-ctor IntList Cons y (term-ctor IntList Cons h t)) (term-ctor IntList Cons h (app insert y t)))))))
(fn sort
(doc "Insertion sort: build the result by inserting each head into the sorted tail.")
(type (fn-type (params (con IntList)) (ret (con IntList))))
(params xs)
(body (match xs
(case (pat-ctor Nil) (term-ctor IntList Nil))
(case (pat-ctor Cons h t) (app insert h (app sort t))))))
(fn print_list
(type (fn-type (params (con IntList)) (ret (con Unit)) (effects IO)))
(params xs)
(body (match xs
(case (pat-ctor Nil) (lit-unit))
(case (pat-ctor Cons h t) (seq (do io/print_int h) (tail-app print_list t))))))
(fn main
(doc "Sort and print [3, 1, 4, 1, 5, 9, 2, 6, 5, 3, 5].")
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body (let xs (term-ctor IntList Cons 3 (term-ctor IntList Cons 1 (term-ctor IntList Cons 4 (term-ctor IntList Cons 1 (term-ctor IntList Cons 5 (term-ctor IntList Cons 9 (term-ctor IntList Cons 2 (term-ctor IntList Cons 6 (term-ctor IntList Cons 5 (term-ctor IntList Cons 3 (term-ctor IntList Cons 5 (term-ctor IntList Nil)))))))))))) (app print_list (app sort xs))))))
+6
View File
@@ -0,0 +1,6 @@
(module str_clone_cross_realisation
(fn main
(doc "Iter 24.1: cross-realisation invariant for str_clone. Clones a heap-Str (produced by int_to_str 42) AND a static-Str (literal `abc`); both clones produce fresh heap-Str slabs that print their input bytes. Pins the uniform-consumer-ABI claim — str_clone only reads the len-field at offset 0 plus the bytes plus the NUL; it never consults the (absent for static-Str) rc_header. RC stats under --alloc=rc: allocs == 3 (int_to_str output + two str_clone outputs), frees == 3, live == 0.")
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body (let src_heap (app int_to_str 42) (let c1 (app str_clone src_heap) (let c2 (app str_clone "abc") (let _a (do io/print_str c1) (do io/print_str c2))))))))
+6
View File
@@ -0,0 +1,6 @@
(module str_clone_drop_rc
(fn main
(doc "Iter 24.1: pin that `str_clone` participates in RC discipline AND emits correct bytes. Input is a static-Str literal; the clone produces a fresh heap-Str slab whose payload is byte-equal to the input. RC stats under --alloc=rc with AILANG_RC_STATS=1: allocs == 1 (the clone), frees == 1 (let-binder drop at scope close), live == 0.")
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body (let s (app str_clone "hello") (do io/print_str s)))))
+10
View File
@@ -0,0 +1,10 @@
(module str_field_in_adt_heap
(data Boxed
(doc "Iter hs.4 fixture: single-cell ADT around a Str field. Used to pin the drop discipline for ADTs that carry a heap-Str payload — both the ADT cell and the heap-Str inside it must round-trip through the RC walk.")
(ctor Box (con Str)))
(fn main
(doc "Iter hs.4: pin that `field_drop_call` for `Str` correctly dispatches to `ailang_rc_dec` when the Str payload is heap-Str. Construct `Box(int_to_str(42))`, bind, pattern-match to read the Str, print it. Under --alloc=rc with AILANG_RC_STATS=1: allocs >= 2 (ADT cell + heap-Str slab), allocs == frees, live == 0. drop.rs's Str-arm comment names the codegen-level elision that protects static-Str from this same path; here the Str is heap-allocated so rc_dec is the right (and only) consumer.")
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body (let b (term-ctor Boxed Box (app int_to_str 42)) (match b
(case (pat-ctor Box s) (do io/print_str s)))))))
+10
View File
@@ -0,0 +1,10 @@
(module sum
(fn sum
(doc "rekursive Summe 0..=n")
(type (fn-type (params (con Int)) (ret (con Int))))
(params n)
(body (if (app == n 0) 0 (app + n (app sum (app - n 1))))))
(fn main
(type (fn-type (params) (ret (con Unit)) (effects IO)))
(params)
(body (do io/print_int (app sum 10)))))
+8
View File
@@ -0,0 +1,8 @@
(module test_22b1_dup_a
(import test_22b1_dup_b)
(import test_22b1_dup_classmod)
(instance
(class test_22b1_dup_classmod.TShow)
(type (con test_22b1_dup_b.MyInt))
(method tshow
(body "from-a"))))
+9
View File
@@ -0,0 +1,9 @@
(module test_22b1_dup_b
(import test_22b1_dup_classmod)
(data MyInt
(ctor MkMyInt (con Int)))
(instance
(class test_22b1_dup_classmod.TShow)
(type (con MyInt))
(method tshow
(body "from-b"))))
+5
View File
@@ -0,0 +1,5 @@
(module test_22b1_dup_classmod
(class TShow
(param a)
(method tshow
(type (fn-type (params a) (ret (con Str)))))))

Some files were not shown because too many files have changed in this diff Show More