From 77b28ad64db4f461702bdca5f334e63384b2c94a Mon Sep 17 00:00:00 2001 From: Brummel Date: Wed, 13 May 2026 11:12:48 +0200 Subject: [PATCH] iter form-a.1 (Tasks 1-5): additive phase + relocation MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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/.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. --- Cargo.lock | 3 + .../2026-05-13-iter-form-a.1.json | 30 ++ .../tests/codegen_import_map_fallback_pin.rs | 4 +- crates/ail/tests/ct1_check_cli.rs | 2 +- crates/ail/tests/e2e.rs | 138 ++++---- crates/ail/tests/eq_float_noinstance.rs | 4 +- crates/ail/tests/eq_ord_e2e.rs | 8 +- crates/ail/tests/floats_e2e.rs | 2 +- crates/ail/tests/ir_snapshot.rs | 10 +- crates/ail/tests/mono_hash_stability.rs | 6 +- crates/ail/tests/mono_recursive_fn.rs | 2 +- crates/ail/tests/mono_unification.rs | 14 +- crates/ail/tests/mono_xmod_ctor_pattern.rs | 4 +- crates/ail/tests/mono_xmod_qualified_ref.rs | 2 +- crates/ail/tests/mq3_multi_class_e2e.rs | 6 +- .../tests/polyfn_dot_qualified_branch_pin.rs | 4 +- crates/ail/tests/prelude_free_fns.rs | 12 +- crates/ail/tests/print_mono_body_shape.rs | 4 +- crates/ail/tests/roundtrip_cli.rs | 124 +++++++ crates/ail/tests/show_mono_synthesis.rs | 4 +- crates/ail/tests/show_no_instance_e2e.rs | 4 +- crates/ail/tests/show_print_e2e.rs | 4 +- crates/ail/tests/typeclass_22b2.rs | 48 +-- crates/ail/tests/typeclass_22b3.rs | 42 +-- crates/ail/tests/typeclass_22c.rs | 2 +- .../tests/env_construction_pin.rs | 4 +- .../tests/method_collision_pin.rs | 6 +- .../ailang-check/tests/show_dispatch_pin.rs | 6 +- crates/ailang-check/tests/workspace.rs | 10 +- crates/ailang-codegen/Cargo.toml | 3 + crates/ailang-codegen/src/lib.rs | 96 +----- .../ailang-codegen/tests/eq_primitives_pin.rs | 82 +++++ crates/ailang-core/Cargo.toml | 1 + crates/ailang-core/src/hash.rs | 309 +----------------- crates/ailang-core/src/workspace.rs | 284 ++-------------- .../ailang-core/tests/carve_out_inventory.rs | 50 +++ .../ailang-core/tests/ctt2_registry_rekey.rs | 4 +- crates/ailang-core/tests/hash_pin.rs | 261 +++++++++++++++ crates/ailang-core/tests/workspace_pin.rs | 283 ++++++++++++++++ crates/ailang-prose/Cargo.toml | 3 + crates/ailang-prose/tests/snapshot.rs | 16 +- crates/ailang-surface/tests/round_trip.rs | 25 ++ docs/journals/2026-05-13-iter-form-a.1.md | 187 +++++++++++ examples/bench_mono_dispatch.ail | 23 ++ examples/bool_to_str_drop_rc.ail | 6 + examples/bool_to_str_smoke_false.ail | 6 + examples/closure.ail | 11 + examples/cmp_max_smoke.ail | 11 + examples/compare_primitives_smoke.ail | 32 ++ examples/ctt2_collision_cls.ail | 5 + examples/ctt2_collision_lib.ail | 9 + examples/ctt2_collision_main.ail | 10 + examples/eq_float_noinstance.ail | 5 + examples/eq_ord_polymorphic.ail | 10 + examples/eq_ord_user_adt.ail | 24 ++ examples/eq_primitives_smoke.ail | 10 + examples/float_to_str_smoke.ail | 6 + examples/floats.ail | 5 + examples/ge_at_int_smoke.ail | 5 + examples/gt_at_bool_smoke.ail | 5 + examples/heap_str_repeated_print_borrow.ail | 6 + examples/hof.ail | 15 + examples/int_to_print_int_borrow.ail | 6 + examples/int_to_str_drop_rc.ail | 6 + examples/int_to_str_smoke.ail | 6 + examples/le_at_str_smoke.ail | 5 + examples/list.ail | 17 + examples/list_map.ail | 24 ++ examples/lt_at_int_smoke.ail | 5 + examples/max3.ail | 14 + examples/maybe_int.ail | 17 + examples/mono_hash_pin_smoke.ail | 12 + examples/mq1_xmod_constraint_class.ail | 7 + examples/mq1_xmod_constraint_class_dep.ail | 5 + examples/mq3_class_eq_vs_fn_eq.ail | 8 + examples/mq3_class_eq_vs_fn_eq_classmod.ail | 10 + examples/mq3_class_eq_vs_fn_eq_fnmod.ail | 6 + examples/mq3_two_show_ambiguous.ail | 8 + examples/mq3_two_show_ambiguous_a.ail | 10 + examples/mq3_two_show_ambiguous_b.ail | 10 + examples/mq3_two_show_qualified.ail | 8 + examples/ne_at_int_smoke.ail | 5 + examples/ordering_match.ail | 9 + examples/poly_apply.ail | 13 + examples/poly_id.ail | 9 + examples/rc_box_drop.ail | 10 + examples/rc_list_drop.ail | 17 + examples/rc_list_drop_borrow.ail | 12 + examples/show_mono_pin_smoke.ail | 5 + examples/show_no_instance.ail | 5 + examples/show_print_smoke.ail | 5 + examples/show_user_adt.ail | 13 + examples/sort.ail | 30 ++ examples/str_clone_cross_realisation.ail | 6 + examples/str_clone_drop_rc.ail | 6 + examples/str_field_in_adt_heap.ail | 10 + examples/sum.ail | 10 + examples/test_22b1_dup_a.ail | 8 + examples/test_22b1_dup_b.ail | 9 + examples/test_22b1_dup_classmod.ail | 5 + examples/test_22b1_dup_entry.ail | 4 + examples/test_22b1_dup_same_module.ail | 15 + examples/test_22b1_missing_method.ail | 12 + examples/test_22b1_orphan_class.ail | 10 + examples/test_22b1_orphan_third.ail | 7 + examples/test_22b1_orphan_third_classmod.ail | 5 + examples/test_22b2_class_method_lookup.ail | 10 + examples/test_22b2_constraint_declared.ail | 9 + ...2b2_constraint_declared_via_superclass.ail | 14 + examples/test_22b2_instance_present.ail | 14 + ...22b2_method_name_collision_class_class.ail | 9 + ...st_22b2_method_name_collision_class_fn.ail | 9 + examples/test_22b2_missing_constraint.ail | 9 + .../test_22b2_missing_constraint_subclass.ail | 14 + .../test_22b2_missing_superclass_instance.ail | 15 + examples/test_22b2_no_instance.ail | 9 + examples/test_22b2_overriding_nonexistent.ail | 12 + .../test_22b2_two_fns_missing_constraint.ail | 13 + examples/test_22b2_xmod_classmod.ail | 10 + examples/test_22b2_xmod_classmod_noinst.ail | 5 + examples/test_22b2_xmod_instance_present.ail | 6 + .../test_22b2_xmod_missing_constraint.ail | 6 + examples/test_22b2_xmod_no_instance.ail | 6 + examples/test_22b3_chained_calls.ail | 22 ++ .../test_22b3_coherence_two_instances.ail | 19 ++ examples/test_22b3_default_e2e.ail | 13 + examples/test_22b3_dup_call_sites.ail | 14 + examples/test_22b3_mono_synthetic.ail | 14 + examples/test_22b3_no_call_sites.ail | 14 + examples/test_22b3_shadow_class_method.ail | 23 ++ examples/test_22b3_xmod_e2e_classmod.ail | 10 + examples/test_22b3_xmod_e2e_main.ail | 6 + examples/test_22c_user_class_e2e.ail | 17 + examples/test_mono_ctor_listmod.ail | 5 + examples/test_mono_ctor_main.ail | 22 ++ examples/test_mono_imports_classmod.ail | 14 + examples/test_mono_imports_main.ail | 6 + examples/test_mono_recursive_fn_bug.ail | 18 + examples/ws_broken.ail | 7 + examples/ws_lib.ail | 6 + examples/ws_main.ail | 7 + examples/ws_unknown_module.ail | 6 + 142 files changed, 2314 insertions(+), 840 deletions(-) create mode 100644 bench/orchestrator-stats/2026-05-13-iter-form-a.1.json create mode 100644 crates/ailang-codegen/tests/eq_primitives_pin.rs create mode 100644 crates/ailang-core/tests/carve_out_inventory.rs create mode 100644 crates/ailang-core/tests/hash_pin.rs create mode 100644 crates/ailang-core/tests/workspace_pin.rs create mode 100644 docs/journals/2026-05-13-iter-form-a.1.md create mode 100644 examples/bench_mono_dispatch.ail create mode 100644 examples/bool_to_str_drop_rc.ail create mode 100644 examples/bool_to_str_smoke_false.ail create mode 100644 examples/closure.ail create mode 100644 examples/cmp_max_smoke.ail create mode 100644 examples/compare_primitives_smoke.ail create mode 100644 examples/ctt2_collision_cls.ail create mode 100644 examples/ctt2_collision_lib.ail create mode 100644 examples/ctt2_collision_main.ail create mode 100644 examples/eq_float_noinstance.ail create mode 100644 examples/eq_ord_polymorphic.ail create mode 100644 examples/eq_ord_user_adt.ail create mode 100644 examples/eq_primitives_smoke.ail create mode 100644 examples/float_to_str_smoke.ail create mode 100644 examples/floats.ail create mode 100644 examples/ge_at_int_smoke.ail create mode 100644 examples/gt_at_bool_smoke.ail create mode 100644 examples/heap_str_repeated_print_borrow.ail create mode 100644 examples/hof.ail create mode 100644 examples/int_to_print_int_borrow.ail create mode 100644 examples/int_to_str_drop_rc.ail create mode 100644 examples/int_to_str_smoke.ail create mode 100644 examples/le_at_str_smoke.ail create mode 100644 examples/list.ail create mode 100644 examples/list_map.ail create mode 100644 examples/lt_at_int_smoke.ail create mode 100644 examples/max3.ail create mode 100644 examples/maybe_int.ail create mode 100644 examples/mono_hash_pin_smoke.ail create mode 100644 examples/mq1_xmod_constraint_class.ail create mode 100644 examples/mq1_xmod_constraint_class_dep.ail create mode 100644 examples/mq3_class_eq_vs_fn_eq.ail create mode 100644 examples/mq3_class_eq_vs_fn_eq_classmod.ail create mode 100644 examples/mq3_class_eq_vs_fn_eq_fnmod.ail create mode 100644 examples/mq3_two_show_ambiguous.ail create mode 100644 examples/mq3_two_show_ambiguous_a.ail create mode 100644 examples/mq3_two_show_ambiguous_b.ail create mode 100644 examples/mq3_two_show_qualified.ail create mode 100644 examples/ne_at_int_smoke.ail create mode 100644 examples/ordering_match.ail create mode 100644 examples/poly_apply.ail create mode 100644 examples/poly_id.ail create mode 100644 examples/rc_box_drop.ail create mode 100644 examples/rc_list_drop.ail create mode 100644 examples/rc_list_drop_borrow.ail create mode 100644 examples/show_mono_pin_smoke.ail create mode 100644 examples/show_no_instance.ail create mode 100644 examples/show_print_smoke.ail create mode 100644 examples/show_user_adt.ail create mode 100644 examples/sort.ail create mode 100644 examples/str_clone_cross_realisation.ail create mode 100644 examples/str_clone_drop_rc.ail create mode 100644 examples/str_field_in_adt_heap.ail create mode 100644 examples/sum.ail create mode 100644 examples/test_22b1_dup_a.ail create mode 100644 examples/test_22b1_dup_b.ail create mode 100644 examples/test_22b1_dup_classmod.ail create mode 100644 examples/test_22b1_dup_entry.ail create mode 100644 examples/test_22b1_dup_same_module.ail create mode 100644 examples/test_22b1_missing_method.ail create mode 100644 examples/test_22b1_orphan_class.ail create mode 100644 examples/test_22b1_orphan_third.ail create mode 100644 examples/test_22b1_orphan_third_classmod.ail create mode 100644 examples/test_22b2_class_method_lookup.ail create mode 100644 examples/test_22b2_constraint_declared.ail create mode 100644 examples/test_22b2_constraint_declared_via_superclass.ail create mode 100644 examples/test_22b2_instance_present.ail create mode 100644 examples/test_22b2_method_name_collision_class_class.ail create mode 100644 examples/test_22b2_method_name_collision_class_fn.ail create mode 100644 examples/test_22b2_missing_constraint.ail create mode 100644 examples/test_22b2_missing_constraint_subclass.ail create mode 100644 examples/test_22b2_missing_superclass_instance.ail create mode 100644 examples/test_22b2_no_instance.ail create mode 100644 examples/test_22b2_overriding_nonexistent.ail create mode 100644 examples/test_22b2_two_fns_missing_constraint.ail create mode 100644 examples/test_22b2_xmod_classmod.ail create mode 100644 examples/test_22b2_xmod_classmod_noinst.ail create mode 100644 examples/test_22b2_xmod_instance_present.ail create mode 100644 examples/test_22b2_xmod_missing_constraint.ail create mode 100644 examples/test_22b2_xmod_no_instance.ail create mode 100644 examples/test_22b3_chained_calls.ail create mode 100644 examples/test_22b3_coherence_two_instances.ail create mode 100644 examples/test_22b3_default_e2e.ail create mode 100644 examples/test_22b3_dup_call_sites.ail create mode 100644 examples/test_22b3_mono_synthetic.ail create mode 100644 examples/test_22b3_no_call_sites.ail create mode 100644 examples/test_22b3_shadow_class_method.ail create mode 100644 examples/test_22b3_xmod_e2e_classmod.ail create mode 100644 examples/test_22b3_xmod_e2e_main.ail create mode 100644 examples/test_22c_user_class_e2e.ail create mode 100644 examples/test_mono_ctor_listmod.ail create mode 100644 examples/test_mono_ctor_main.ail create mode 100644 examples/test_mono_imports_classmod.ail create mode 100644 examples/test_mono_imports_main.ail create mode 100644 examples/test_mono_recursive_fn_bug.ail create mode 100644 examples/ws_broken.ail create mode 100644 examples/ws_lib.ail create mode 100644 examples/ws_main.ail create mode 100644 examples/ws_unknown_module.ail diff --git a/Cargo.lock b/Cargo.lock index 2c0a966..2c135bf 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -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]] diff --git a/bench/orchestrator-stats/2026-05-13-iter-form-a.1.json b/bench/orchestrator-stats/2026-05-13-iter-form-a.1.json new file mode 100644 index 0000000..dfe0499 --- /dev/null +++ b/bench/orchestrator-stats/2026-05-13-iter-form-a.1.json @@ -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 + } +} diff --git a/crates/ail/tests/codegen_import_map_fallback_pin.rs b/crates/ail/tests/codegen_import_map_fallback_pin.rs index 52c231d..c056728 100644 --- a/crates/ail/tests/codegen_import_map_fallback_pin.rs +++ b/crates/ail/tests/codegen_import_map_fallback_pin.rs @@ -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] diff --git a/crates/ail/tests/ct1_check_cli.rs b/crates/ail/tests/ct1_check_cli.rs index 77537a8..a12ccb7 100644 --- a/crates/ail/tests/ct1_check_cli.rs +++ b/crates/ail/tests/ct1_check_cli.rs @@ -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() diff --git a/crates/ail/tests/e2e.rs b/crates/ail/tests/e2e.rs index 7c693fb..538cd65 100644 --- a/crates/ail/tests/e2e.rs +++ b/crates/ail/tests/e2e.rs @@ -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!["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}"); diff --git a/crates/ail/tests/eq_float_noinstance.rs b/crates/ail/tests/eq_float_noinstance.rs index 6fecd70..5370942 100644 --- a/crates/ail/tests/eq_float_noinstance.rs +++ b/crates/ail/tests/eq_float_noinstance.rs @@ -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"); diff --git a/crates/ail/tests/eq_ord_e2e.rs b/crates/ail/tests/eq_ord_e2e.rs index 8efc4aa..b64a569 100644 --- a/crates/ail/tests/eq_ord_e2e.rs +++ b/crates/ail/tests/eq_ord_e2e.rs @@ -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"); diff --git a/crates/ail/tests/floats_e2e.rs b/crates/ail/tests/floats_e2e.rs index e7fb3bc..fcebda0 100644 --- a/crates/ail/tests/floats_e2e.rs +++ b/crates/ail/tests/floats_e2e.rs @@ -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. diff --git a/crates/ail/tests/ir_snapshot.rs b/crates/ail/tests/ir_snapshot.rs index 246ec52..7fdfe3f 100644 --- a/crates/ail/tests/ir_snapshot.rs +++ b/crates/ail/tests/ir_snapshot.rs @@ -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"); } diff --git a/crates/ail/tests/mono_hash_stability.rs b/crates/ail/tests/mono_hash_stability.rs index 30e9acb..8f9fa90 100644 --- a/crates/ail/tests/mono_hash_stability.rs +++ b/crates/ail/tests/mono_hash_stability.rs @@ -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] diff --git a/crates/ail/tests/mono_recursive_fn.rs b/crates/ail/tests/mono_recursive_fn.rs index 13e4e5f..5d45e44 100644 --- a/crates/ail/tests/mono_recursive_fn.rs +++ b/crates/ail/tests/mono_recursive_fn.rs @@ -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() diff --git a/crates/ail/tests/mono_unification.rs b/crates/ail/tests/mono_unification.rs index ca95ecf..ec64480 100644 --- a/crates/ail/tests/mono_unification.rs +++ b/crates/ail/tests/mono_unification.rs @@ -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:?}"); diff --git a/crates/ail/tests/mono_xmod_ctor_pattern.rs b/crates/ail/tests/mono_xmod_ctor_pattern.rs index 9f96170..61f015f 100644 --- a/crates/ail/tests/mono_xmod_ctor_pattern.rs +++ b/crates/ail/tests/mono_xmod_ctor_pattern.rs @@ -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. diff --git a/crates/ail/tests/mono_xmod_qualified_ref.rs b/crates/ail/tests/mono_xmod_qualified_ref.rs index 4efa515..63ef993 100644 --- a/crates/ail/tests/mono_xmod_qualified_ref.rs +++ b/crates/ail/tests/mono_xmod_qualified_ref.rs @@ -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() diff --git a/crates/ail/tests/mq3_multi_class_e2e.rs b/crates/ail/tests/mq3_multi_class_e2e.rs index e989a7e..10b9899 100644 --- a/crates/ail/tests/mq3_multi_class_e2e.rs +++ b/crates/ail/tests/mq3_multi_class_e2e.rs @@ -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={}", diff --git a/crates/ail/tests/polyfn_dot_qualified_branch_pin.rs b/crates/ail/tests/polyfn_dot_qualified_branch_pin.rs index cb6c5e9..626a8e8 100644 --- a/crates/ail/tests/polyfn_dot_qualified_branch_pin.rs +++ b/crates/ail/tests/polyfn_dot_qualified_branch_pin.rs @@ -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] diff --git a/crates/ail/tests/prelude_free_fns.rs b/crates/ail/tests/prelude_free_fns.rs index 3b51dab..78f1694 100644 --- a/crates/ail/tests/prelude_free_fns.rs +++ b/crates/ail/tests/prelude_free_fns.rs @@ -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 { #[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:#?}" diff --git a/crates/ail/tests/print_mono_body_shape.rs b/crates/ail/tests/print_mono_body_shape.rs index 15caec7..24b23eb 100644 --- a/crates/ail/tests/print_mono_body_shape.rs +++ b/crates/ail/tests/print_mono_body_shape.rs @@ -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] diff --git a/crates/ail/tests/roundtrip_cli.rs b/crates/ail/tests/roundtrip_cli.rs index 9e2039e..6c88967 100644 --- a/crates/ail/tests/roundtrip_cli.rs +++ b/crates/ail/tests/roundtrip_cli.rs @@ -45,6 +45,23 @@ fn list_json_fixtures() -> Vec { paths } +fn list_ail_fixtures() -> Vec { + let dir = examples_dir(); + let mut paths: Vec = 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) -> Vec { 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 render | ail parse` is byte-identical +/// to direct `ail parse `. 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::::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"); +} diff --git a/crates/ail/tests/show_mono_synthesis.rs b/crates/ail/tests/show_mono_synthesis.rs index a1bb513..d71c50f 100644 --- a/crates/ail/tests/show_mono_synthesis.rs +++ b/crates/ail/tests/show_mono_synthesis.rs @@ -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] diff --git a/crates/ail/tests/show_no_instance_e2e.rs b/crates/ail/tests/show_no_instance_e2e.rs index cf3a499..d9e288d 100644 --- a/crates/ail/tests/show_no_instance_e2e.rs +++ b/crates/ail/tests/show_no_instance_e2e.rs @@ -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!( diff --git a/crates/ail/tests/show_print_e2e.rs b/crates/ail/tests/show_print_e2e.rs index 324010e..df740bd 100644 --- a/crates/ail/tests/show_print_e2e.rs +++ b/crates/ail/tests/show_print_e2e.rs @@ -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:?}"); diff --git a/crates/ail/tests/typeclass_22b2.rs b/crates/ail/tests/typeclass_22b2.rs index 6e75650..ba8edd1 100644 --- a/crates/ail/tests/typeclass_22b2.rs +++ b/crates/ail/tests/typeclass_22b2.rs @@ -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 diff --git a/crates/ail/tests/typeclass_22b3.rs b/crates/ail/tests/typeclass_22b3.rs index 14556dc..e120b78 100644 --- a/crates/ail/tests/typeclass_22b3.rs +++ b/crates/ail/tests/typeclass_22b3.rs @@ -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::>(), @@ -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::>(), before.modules.keys().collect::>(), @@ -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); diff --git a/crates/ail/tests/typeclass_22c.rs b/crates/ail/tests/typeclass_22c.rs index 63b0d74..fb22318 100644 --- a/crates/ail/tests/typeclass_22c.rs +++ b/crates/ail/tests/typeclass_22c.rs @@ -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"); } diff --git a/crates/ailang-check/tests/env_construction_pin.rs b/crates/ailang-check/tests/env_construction_pin.rs index 671eca7..f79ec4d 100644 --- a/crates/ailang-check/tests/env_construction_pin.rs +++ b/crates/ailang-check/tests/env_construction_pin.rs @@ -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); diff --git a/crates/ailang-check/tests/method_collision_pin.rs b/crates/ailang-check/tests/method_collision_pin.rs index 2902790..ff069a1 100644 --- a/crates/ailang-check/tests/method_collision_pin.rs +++ b/crates/ailang-check/tests/method_collision_pin.rs @@ -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"); } diff --git a/crates/ailang-check/tests/show_dispatch_pin.rs b/crates/ailang-check/tests/show_dispatch_pin.rs index b7e6527..436f747 100644 --- a/crates/ailang-check/tests/show_dispatch_pin.rs +++ b/crates/ailang-check/tests/show_dispatch_pin.rs @@ -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:#?}"); diff --git a/crates/ailang-check/tests/workspace.rs b/crates/ailang-check/tests/workspace.rs index ff12030..46c15ce 100644 --- a/crates/ailang-check/tests/workspace.rs +++ b/crates/ailang-check/tests/workspace.rs @@ -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 diff --git a/crates/ailang-codegen/Cargo.toml b/crates/ailang-codegen/Cargo.toml index 5c3845b..0470604 100644 --- a/crates/ailang-codegen/Cargo.toml +++ b/crates/ailang-codegen/Cargo.toml @@ -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" } diff --git a/crates/ailang-codegen/src/lib.rs b/crates/ailang-codegen/src/lib.rs index 845893a..6ea6f29 100644 --- a/crates/ailang-codegen/src/lib.rs +++ b/crates/ailang-codegen/src/lib.rs @@ -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`). diff --git a/crates/ailang-codegen/tests/eq_primitives_pin.rs b/crates/ailang-codegen/tests/eq_primitives_pin.rs new file mode 100644 index 0000000..1bbf423 --- /dev/null +++ b/crates/ailang-codegen/tests/eq_primitives_pin.rs @@ -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}" + ); +} diff --git a/crates/ailang-core/Cargo.toml b/crates/ailang-core/Cargo.toml index d1c678f..a8e1371 100644 --- a/crates/ailang-core/Cargo.toml +++ b/crates/ailang-core/Cargo.toml @@ -13,3 +13,4 @@ indexmap.workspace = true [dev-dependencies] tempfile.workspace = true +ailang-surface = { path = "../ailang-surface" } diff --git a/crates/ailang-core/src/hash.rs b/crates/ailang-core/src/hash.rs index 037865b..292ed5e 100644 --- a/crates/ailang-core/src/hash.rs +++ b/crates/ailang-core/src/hash.rs @@ -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 {} diff --git a/crates/ailang-core/src/workspace.rs b/crates/ailang-core/src/workspace.rs index a9f4f4b..50b2348 100644 --- a/crates/ailang-core/src/workspace.rs +++ b/crates/ailang-core/src/workspace.rs @@ -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::>() - ); - 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::>() - ); - } + // `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 diff --git a/crates/ailang-core/tests/carve_out_inventory.rs b/crates/ailang-core/tests/carve_out_inventory.rs new file mode 100644 index 0000000..4d97dd2 --- /dev/null +++ b/crates/ailang-core/tests/carve_out_inventory.rs @@ -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 = 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", + ); +} diff --git a/crates/ailang-core/tests/ctt2_registry_rekey.rs b/crates/ailang-core/tests/ctt2_registry_rekey.rs index b0a1e49..b5945c2 100644 --- a/crates/ailang-core/tests/ctt2_registry_rekey.rs +++ b/crates/ailang-core/tests/ctt2_registry_rekey.rs @@ -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"); diff --git a/crates/ailang-core/tests/hash_pin.rs b/crates/ailang-core/tests/hash_pin.rs new file mode 100644 index 0000000..2bc2608 --- /dev/null +++ b/crates/ailang-core/tests/hash_pin.rs @@ -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/.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/.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"); +} diff --git a/crates/ailang-core/tests/workspace_pin.rs b/crates/ailang-core/tests/workspace_pin.rs new file mode 100644 index 0000000..d4cac73 --- /dev/null +++ b/crates/ailang-core/tests/workspace_pin.rs @@ -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::>() + ); + 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::>() + ); + // 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")); +} diff --git a/crates/ailang-prose/Cargo.toml b/crates/ailang-prose/Cargo.toml index b79c1e2..0a31a22 100644 --- a/crates/ailang-prose/Cargo.toml +++ b/crates/ailang-prose/Cargo.toml @@ -6,3 +6,6 @@ license.workspace = true [dependencies] ailang-core.workspace = true + +[dev-dependencies] +ailang-surface = { path = "../ailang-surface" } diff --git a/crates/ailang-prose/tests/snapshot.rs b/crates/ailang-prose/tests/snapshot.rs index adb2801..9c9f896 100644 --- a/crates/ailang-prose/tests/snapshot.rs +++ b/crates/ailang-prose/tests/snapshot.rs @@ -1,12 +1,12 @@ //! Iter 20a snapshot tests. //! -//! For each `examples/.ail.json`, the renderer's output is -//! compared against the committed `examples/.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/.ail`, the renderer's output is compared +//! against the committed `examples/.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 .ail.json > .prose.txt` +//! `cargo run -p ail -- prose .ail > .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) diff --git a/crates/ailang-surface/tests/round_trip.rs b/crates/ailang-surface/tests/round_trip.rs index 0746fb1..522502c 100644 --- a/crates/ailang-surface/tests/round_trip.rs +++ b/crates/ailang-surface/tests/round_trip.rs @@ -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"); +} diff --git a/docs/journals/2026-05-13-iter-form-a.1.md b/docs/journals/2026-05-13-iter-form-a.1.md new file mode 100644 index 0000000..32027e1 --- /dev/null +++ b/docs/journals/2026-05-13-iter-form-a.1.md @@ -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/.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/.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/.ail` rendered via `ail render` (T2) + +## Stats + +bench/orchestrator-stats/2026-05-13-iter-form-a.1.json diff --git a/examples/bench_mono_dispatch.ail b/examples/bench_mono_dispatch.ail new file mode 100644 index 0000000..e1f7b49 --- /dev/null +++ b/examples/bench_mono_dispatch.ail @@ -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))))) diff --git a/examples/bool_to_str_drop_rc.ail b/examples/bool_to_str_drop_rc.ail new file mode 100644 index 0000000..2febb14 --- /dev/null +++ b/examples/bool_to_str_drop_rc.ail @@ -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))))) diff --git a/examples/bool_to_str_smoke_false.ail b/examples/bool_to_str_smoke_false.ail new file mode 100644 index 0000000..69df911 --- /dev/null +++ b/examples/bool_to_str_smoke_false.ail @@ -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))))) diff --git a/examples/closure.ail b/examples/closure.ail new file mode 100644 index 0000000..f45c9f6 --- /dev/null +++ b/examples/closure.ail @@ -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)))))) diff --git a/examples/cmp_max_smoke.ail b/examples/cmp_max_smoke.ail new file mode 100644 index 0000000..3d78897 --- /dev/null +++ b/examples/cmp_max_smoke.ail @@ -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))))) diff --git a/examples/compare_primitives_smoke.ail b/examples/compare_primitives_smoke.ail new file mode 100644 index 0000000..6ba562c --- /dev/null +++ b/examples/compare_primitives_smoke.ail @@ -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)))))))))))))) diff --git a/examples/ctt2_collision_cls.ail b/examples/ctt2_collision_cls.ail new file mode 100644 index 0000000..b703ef5 --- /dev/null +++ b/examples/ctt2_collision_cls.ail @@ -0,0 +1,5 @@ +(module ctt2_collision_cls + (class MyC + (param a) + (method op + (type (fn-type (params a) (ret (con Int))))))) diff --git a/examples/ctt2_collision_lib.ail b/examples/ctt2_collision_lib.ail new file mode 100644 index 0000000..3ac7207 --- /dev/null +++ b/examples/ctt2_collision_lib.ail @@ -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)))))) diff --git a/examples/ctt2_collision_main.ail b/examples/ctt2_collision_main.ail new file mode 100644 index 0000000..f47ff7a --- /dev/null +++ b/examples/ctt2_collision_main.ail @@ -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)))))) diff --git a/examples/eq_float_noinstance.ail b/examples/eq_float_noinstance.ail new file mode 100644 index 0000000..51791a2 --- /dev/null +++ b/examples/eq_float_noinstance.ail @@ -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))))) diff --git a/examples/eq_ord_polymorphic.ail b/examples/eq_ord_polymorphic.ail new file mode 100644 index 0000000..51b31c3 --- /dev/null +++ b/examples/eq_ord_polymorphic.ail @@ -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))))))) diff --git a/examples/eq_ord_user_adt.ail b/examples/eq_ord_user_adt.ail new file mode 100644 index 0000000..b2b7eeb --- /dev/null +++ b/examples/eq_ord_user_adt.ail @@ -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)))))) diff --git a/examples/eq_primitives_smoke.ail b/examples/eq_primitives_smoke.ail new file mode 100644 index 0000000..3e50361 --- /dev/null +++ b/examples/eq_primitives_smoke.ail @@ -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"))))))))))) diff --git a/examples/float_to_str_smoke.ail b/examples/float_to_str_smoke.ail new file mode 100644 index 0000000..c4127a0 --- /dev/null +++ b/examples/float_to_str_smoke.ail @@ -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))))) diff --git a/examples/floats.ail b/examples/floats.ail new file mode 100644 index 0000000..59e7917 --- /dev/null +++ b/examples/floats.ail @@ -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))))))) diff --git a/examples/ge_at_int_smoke.ail b/examples/ge_at_int_smoke.ail new file mode 100644 index 0000000..483338e --- /dev/null +++ b/examples/ge_at_int_smoke.ail @@ -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))))) diff --git a/examples/gt_at_bool_smoke.ail b/examples/gt_at_bool_smoke.ail new file mode 100644 index 0000000..48f4f22 --- /dev/null +++ b/examples/gt_at_bool_smoke.ail @@ -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))))) diff --git a/examples/heap_str_repeated_print_borrow.ail b/examples/heap_str_repeated_print_borrow.ail new file mode 100644 index 0000000..551cdd3 --- /dev/null +++ b/examples/heap_str_repeated_print_borrow.ail @@ -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)))))) diff --git a/examples/hof.ail b/examples/hof.ail new file mode 100644 index 0000000..6d19849 --- /dev/null +++ b/examples/hof.ail @@ -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))))) diff --git a/examples/int_to_print_int_borrow.ail b/examples/int_to_print_int_borrow.ail new file mode 100644 index 0000000..c96b9a8 --- /dev/null +++ b/examples/int_to_print_int_borrow.ail @@ -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))))) diff --git a/examples/int_to_str_drop_rc.ail b/examples/int_to_str_drop_rc.ail new file mode 100644 index 0000000..6752f4a --- /dev/null +++ b/examples/int_to_str_drop_rc.ail @@ -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))))) diff --git a/examples/int_to_str_smoke.ail b/examples/int_to_str_smoke.ail new file mode 100644 index 0000000..0638a49 --- /dev/null +++ b/examples/int_to_str_smoke.ail @@ -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))))) diff --git a/examples/le_at_str_smoke.ail b/examples/le_at_str_smoke.ail new file mode 100644 index 0000000..9ad893a --- /dev/null +++ b/examples/le_at_str_smoke.ail @@ -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))))) diff --git a/examples/list.ail b/examples/list.ail new file mode 100644 index 0000000..82cd107 --- /dev/null +++ b/examples/list.ail @@ -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)))))) diff --git a/examples/list_map.ail b/examples/list_map.ail new file mode 100644 index 0000000..34a2b38 --- /dev/null +++ b/examples/list_map.ail @@ -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)))))) diff --git a/examples/lt_at_int_smoke.ail b/examples/lt_at_int_smoke.ail new file mode 100644 index 0000000..f37163e --- /dev/null +++ b/examples/lt_at_int_smoke.ail @@ -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))))) diff --git a/examples/max3.ail b/examples/max3.ail new file mode 100644 index 0000000..2962999 --- /dev/null +++ b/examples/max3.ail @@ -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))))) diff --git a/examples/maybe_int.ail b/examples/maybe_int.ail new file mode 100644 index 0000000..48a3014 --- /dev/null +++ b/examples/maybe_int.ail @@ -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)))))) diff --git a/examples/mono_hash_pin_smoke.ail b/examples/mono_hash_pin_smoke.ail new file mode 100644 index 0000000..1138b96 --- /dev/null +++ b/examples/mono_hash_pin_smoke.ail @@ -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"))))))))))) diff --git a/examples/mq1_xmod_constraint_class.ail b/examples/mq1_xmod_constraint_class.ail new file mode 100644 index 0000000..7e6ba91 --- /dev/null +++ b/examples/mq1_xmod_constraint_class.ail @@ -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)))) diff --git a/examples/mq1_xmod_constraint_class_dep.ail b/examples/mq1_xmod_constraint_class_dep.ail new file mode 100644 index 0000000..0b03c43 --- /dev/null +++ b/examples/mq1_xmod_constraint_class_dep.ail @@ -0,0 +1,5 @@ +(module mq1_xmod_constraint_class_dep + (class Show + (param a) + (method show + (type (fn-type (params a) (ret (con Str))))))) diff --git a/examples/mq3_class_eq_vs_fn_eq.ail b/examples/mq3_class_eq_vs_fn_eq.ail new file mode 100644 index 0000000..da4664f --- /dev/null +++ b/examples/mq3_class_eq_vs_fn_eq.ail @@ -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))))) diff --git a/examples/mq3_class_eq_vs_fn_eq_classmod.ail b/examples/mq3_class_eq_vs_fn_eq_classmod.ail new file mode 100644 index 0000000..ba9da46 --- /dev/null +++ b/examples/mq3_class_eq_vs_fn_eq_classmod.ail @@ -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)))))) diff --git a/examples/mq3_class_eq_vs_fn_eq_fnmod.ail b/examples/mq3_class_eq_vs_fn_eq_fnmod.ail new file mode 100644 index 0000000..a6b573f --- /dev/null +++ b/examples/mq3_class_eq_vs_fn_eq_fnmod.ail @@ -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))) diff --git a/examples/mq3_two_show_ambiguous.ail b/examples/mq3_two_show_ambiguous.ail new file mode 100644 index 0000000..0fcd468 --- /dev/null +++ b/examples/mq3_two_show_ambiguous.ail @@ -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))))) diff --git a/examples/mq3_two_show_ambiguous_a.ail b/examples/mq3_two_show_ambiguous_a.ail new file mode 100644 index 0000000..2fed436 --- /dev/null +++ b/examples/mq3_two_show_ambiguous_a.ail @@ -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))))))) diff --git a/examples/mq3_two_show_ambiguous_b.ail b/examples/mq3_two_show_ambiguous_b.ail new file mode 100644 index 0000000..16f1950 --- /dev/null +++ b/examples/mq3_two_show_ambiguous_b.ail @@ -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))))))) diff --git a/examples/mq3_two_show_qualified.ail b/examples/mq3_two_show_qualified.ail new file mode 100644 index 0000000..754b1b4 --- /dev/null +++ b/examples/mq3_two_show_qualified.ail @@ -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))))) diff --git a/examples/ne_at_int_smoke.ail b/examples/ne_at_int_smoke.ail new file mode 100644 index 0000000..2fe0a0e --- /dev/null +++ b/examples/ne_at_int_smoke.ail @@ -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))))) diff --git a/examples/ordering_match.ail b/examples/ordering_match.ail new file mode 100644 index 0000000..71db9cb --- /dev/null +++ b/examples/ordering_match.ail @@ -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)))))) diff --git a/examples/poly_apply.ail b/examples/poly_apply.ail new file mode 100644 index 0000000..aa2ba02 --- /dev/null +++ b/examples/poly_apply.ail @@ -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))))) diff --git a/examples/poly_id.ail b/examples/poly_id.ail new file mode 100644 index 0000000..0a67d55 --- /dev/null +++ b/examples/poly_id.ail @@ -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)))))) diff --git a/examples/rc_box_drop.ail b/examples/rc_box_drop.ail new file mode 100644 index 0000000..80f144d --- /dev/null +++ b/examples/rc_box_drop.ail @@ -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))))))) diff --git a/examples/rc_list_drop.ail b/examples/rc_list_drop.ail new file mode 100644 index 0000000..322b212 --- /dev/null +++ b/examples/rc_list_drop.ail @@ -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)))))) diff --git a/examples/rc_list_drop_borrow.ail b/examples/rc_list_drop_borrow.ail new file mode 100644 index 0000000..c0e842d --- /dev/null +++ b/examples/rc_list_drop_borrow.ail @@ -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))))))) diff --git a/examples/show_mono_pin_smoke.ail b/examples/show_mono_pin_smoke.ail new file mode 100644 index 0000000..8a71d03 --- /dev/null +++ b/examples/show_mono_pin_smoke.ail @@ -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)))))))) diff --git a/examples/show_no_instance.ail b/examples/show_no_instance.ail new file mode 100644 index 0000000..45cd53a --- /dev/null +++ b/examples/show_no_instance.ail @@ -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))))) diff --git a/examples/show_print_smoke.ail b/examples/show_print_smoke.ail new file mode 100644 index 0000000..4dfe1b1 --- /dev/null +++ b/examples/show_print_smoke.ail @@ -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))))) diff --git a/examples/show_user_adt.ail b/examples/show_user_adt.ail new file mode 100644 index 0000000..8943e99 --- /dev/null +++ b/examples/show_user_adt.ail @@ -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))))) diff --git a/examples/sort.ail b/examples/sort.ail new file mode 100644 index 0000000..bd265c9 --- /dev/null +++ b/examples/sort.ail @@ -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)))))) diff --git a/examples/str_clone_cross_realisation.ail b/examples/str_clone_cross_realisation.ail new file mode 100644 index 0000000..16c4172 --- /dev/null +++ b/examples/str_clone_cross_realisation.ail @@ -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)))))))) diff --git a/examples/str_clone_drop_rc.ail b/examples/str_clone_drop_rc.ail new file mode 100644 index 0000000..2d1208f --- /dev/null +++ b/examples/str_clone_drop_rc.ail @@ -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))))) diff --git a/examples/str_field_in_adt_heap.ail b/examples/str_field_in_adt_heap.ail new file mode 100644 index 0000000..beb8039 --- /dev/null +++ b/examples/str_field_in_adt_heap.ail @@ -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))))))) diff --git a/examples/sum.ail b/examples/sum.ail new file mode 100644 index 0000000..b8a8df7 --- /dev/null +++ b/examples/sum.ail @@ -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))))) diff --git a/examples/test_22b1_dup_a.ail b/examples/test_22b1_dup_a.ail new file mode 100644 index 0000000..aa00d2b --- /dev/null +++ b/examples/test_22b1_dup_a.ail @@ -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")))) diff --git a/examples/test_22b1_dup_b.ail b/examples/test_22b1_dup_b.ail new file mode 100644 index 0000000..d806ff4 --- /dev/null +++ b/examples/test_22b1_dup_b.ail @@ -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")))) diff --git a/examples/test_22b1_dup_classmod.ail b/examples/test_22b1_dup_classmod.ail new file mode 100644 index 0000000..b1e87b2 --- /dev/null +++ b/examples/test_22b1_dup_classmod.ail @@ -0,0 +1,5 @@ +(module test_22b1_dup_classmod + (class TShow + (param a) + (method tshow + (type (fn-type (params a) (ret (con Str))))))) diff --git a/examples/test_22b1_dup_entry.ail b/examples/test_22b1_dup_entry.ail new file mode 100644 index 0000000..6ff2e95 --- /dev/null +++ b/examples/test_22b1_dup_entry.ail @@ -0,0 +1,4 @@ +(module test_22b1_dup_entry + (import test_22b1_dup_a) + (import test_22b1_dup_b) + (import test_22b1_dup_classmod)) diff --git a/examples/test_22b1_dup_same_module.ail b/examples/test_22b1_dup_same_module.ail new file mode 100644 index 0000000..67b8487 --- /dev/null +++ b/examples/test_22b1_dup_same_module.ail @@ -0,0 +1,15 @@ +(module test_22b1_dup_same_module + (class TShow + (param a) + (method tshow + (type (fn-type (params a) (ret (con Str)))))) + (instance + (class TShow) + (type (con Int)) + (method tshow + (body "first"))) + (instance + (class TShow) + (type (con Int)) + (method tshow + (body "second")))) diff --git a/examples/test_22b1_missing_method.ail b/examples/test_22b1_missing_method.ail new file mode 100644 index 0000000..33e2723 --- /dev/null +++ b/examples/test_22b1_missing_method.ail @@ -0,0 +1,12 @@ +(module test_22b1_missing_method + (class TEq + (param a) + (method teq + (type (fn-type (params a a) (ret (con Bool))))) + (method tne + (type (fn-type (params a a) (ret (con Bool)))))) + (instance + (class TEq) + (type (con Int)) + (method tne + (body false)))) diff --git a/examples/test_22b1_orphan_class.ail b/examples/test_22b1_orphan_class.ail new file mode 100644 index 0000000..9ac6e43 --- /dev/null +++ b/examples/test_22b1_orphan_class.ail @@ -0,0 +1,10 @@ +(module test_22b1_orphan_class + (class TShow + (param a) + (method tshow + (type (fn-type (params a) (ret (con Str)))))) + (instance + (class TShow) + (type (con Int)) + (method tshow + (body "")))) diff --git a/examples/test_22b1_orphan_third.ail b/examples/test_22b1_orphan_third.ail new file mode 100644 index 0000000..f16c4b2 --- /dev/null +++ b/examples/test_22b1_orphan_third.ail @@ -0,0 +1,7 @@ +(module test_22b1_orphan_third + (import test_22b1_orphan_third_classmod) + (instance + (class test_22b1_orphan_third_classmod.TShow) + (type (con Int)) + (method tshow + (body "")))) diff --git a/examples/test_22b1_orphan_third_classmod.ail b/examples/test_22b1_orphan_third_classmod.ail new file mode 100644 index 0000000..8f5f6bd --- /dev/null +++ b/examples/test_22b1_orphan_third_classmod.ail @@ -0,0 +1,5 @@ +(module test_22b1_orphan_third_classmod + (class TShow + (param a) + (method tshow + (type (fn-type (params a) (ret (con Str))))))) diff --git a/examples/test_22b2_class_method_lookup.ail b/examples/test_22b2_class_method_lookup.ail new file mode 100644 index 0000000..701a34e --- /dev/null +++ b/examples/test_22b2_class_method_lookup.ail @@ -0,0 +1,10 @@ +(module test_22b2_class_method_lookup + (class TShow + (param a) + (method tshow + (type (fn-type (params a) (ret (con Str)))))) + (instance + (class TShow) + (type (con Int)) + (method tshow + (body "n")))) diff --git a/examples/test_22b2_constraint_declared.ail b/examples/test_22b2_constraint_declared.ail new file mode 100644 index 0000000..5374144 --- /dev/null +++ b/examples/test_22b2_constraint_declared.ail @@ -0,0 +1,9 @@ +(module test_22b2_constraint_declared + (class TShow + (param a) + (method tshow + (type (fn-type (params a) (ret (con Str)))))) + (fn describe + (type (forall (vars a) (constraints (constraint TShow a)) (fn-type (params a) (ret (con Str))))) + (params x) + (body (app tshow x)))) diff --git a/examples/test_22b2_constraint_declared_via_superclass.ail b/examples/test_22b2_constraint_declared_via_superclass.ail new file mode 100644 index 0000000..07b73a2 --- /dev/null +++ b/examples/test_22b2_constraint_declared_via_superclass.ail @@ -0,0 +1,14 @@ +(module test_22b2_constraint_declared_via_superclass + (class TEq + (param a) + (method teq + (type (fn-type (params a a) (ret (con Bool)))))) + (class TOrd + (param a) + (superclass (class TEq) (type a)) + (method tlt + (type (fn-type (params a a) (ret (con Bool)))))) + (fn uses_eq_via_ord + (type (forall (vars a) (constraints (constraint TOrd a)) (fn-type (params a a) (ret (con Bool))))) + (params x y) + (body (app teq x y)))) diff --git a/examples/test_22b2_instance_present.ail b/examples/test_22b2_instance_present.ail new file mode 100644 index 0000000..5dfbe55 --- /dev/null +++ b/examples/test_22b2_instance_present.ail @@ -0,0 +1,14 @@ +(module test_22b2_instance_present + (class TShow + (param a) + (method tshow + (type (fn-type (params a) (ret (con Str)))))) + (instance + (class TShow) + (type (con Int)) + (method tshow + (body (lam (params (typed x a)) (ret (con Str)) (body "n"))))) + (fn main + (type (fn-type (params) (ret (con Str)))) + (params) + (body (app tshow 42)))) diff --git a/examples/test_22b2_method_name_collision_class_class.ail b/examples/test_22b2_method_name_collision_class_class.ail new file mode 100644 index 0000000..be26c0a --- /dev/null +++ b/examples/test_22b2_method_name_collision_class_class.ail @@ -0,0 +1,9 @@ +(module test_22b2_method_name_collision_class_class + (class A + (param a) + (method foo + (type (fn-type (params a) (ret (con Unit)))))) + (class B + (param b) + (method foo + (type (fn-type (params b) (ret (con Unit))))))) diff --git a/examples/test_22b2_method_name_collision_class_fn.ail b/examples/test_22b2_method_name_collision_class_fn.ail new file mode 100644 index 0000000..0debe67 --- /dev/null +++ b/examples/test_22b2_method_name_collision_class_fn.ail @@ -0,0 +1,9 @@ +(module test_22b2_method_name_collision_class_fn + (class Greet + (param a) + (method greet + (type (fn-type (params a) (ret (con Str)))))) + (fn greet + (type (fn-type (params (con Int)) (ret (con Str)))) + (params x) + (body "hi"))) diff --git a/examples/test_22b2_missing_constraint.ail b/examples/test_22b2_missing_constraint.ail new file mode 100644 index 0000000..53b3792 --- /dev/null +++ b/examples/test_22b2_missing_constraint.ail @@ -0,0 +1,9 @@ +(module test_22b2_missing_constraint + (class TShow + (param a) + (method tshow + (type (fn-type (params a) (ret (con Str)))))) + (fn describe + (type (forall (vars a) (fn-type (params a) (ret (con Str))))) + (params x) + (body (app tshow x)))) diff --git a/examples/test_22b2_missing_constraint_subclass.ail b/examples/test_22b2_missing_constraint_subclass.ail new file mode 100644 index 0000000..72dc343 --- /dev/null +++ b/examples/test_22b2_missing_constraint_subclass.ail @@ -0,0 +1,14 @@ +(module test_22b2_missing_constraint_subclass + (class TEq + (param a) + (method teq + (type (fn-type (params a a) (ret (con Bool)))))) + (class TOrd + (param a) + (superclass (class TEq) (type a)) + (method tlt + (type (fn-type (params a a) (ret (con Bool)))))) + (fn needs_ord_but_only_has_eq + (type (forall (vars a) (constraints (constraint TEq a)) (fn-type (params a a) (ret (con Bool))))) + (params x y) + (body (app tlt x y)))) diff --git a/examples/test_22b2_missing_superclass_instance.ail b/examples/test_22b2_missing_superclass_instance.ail new file mode 100644 index 0000000..3b051a1 --- /dev/null +++ b/examples/test_22b2_missing_superclass_instance.ail @@ -0,0 +1,15 @@ +(module test_22b2_missing_superclass_instance + (class TEq + (param a) + (method teq + (type (fn-type (params a a) (ret (con Bool)))))) + (class TOrd + (param a) + (superclass (class TEq) (type a)) + (method tlt + (type (fn-type (params a a) (ret (con Bool)))))) + (instance + (class TOrd) + (type (con Int)) + (method tlt + (body true)))) diff --git a/examples/test_22b2_no_instance.ail b/examples/test_22b2_no_instance.ail new file mode 100644 index 0000000..487184d --- /dev/null +++ b/examples/test_22b2_no_instance.ail @@ -0,0 +1,9 @@ +(module test_22b2_no_instance + (class TShow + (param a) + (method tshow + (type (fn-type (params a) (ret (con Str)))))) + (fn main + (type (fn-type (params) (ret (con Str)))) + (params) + (body (app tshow 42)))) diff --git a/examples/test_22b2_overriding_nonexistent.ail b/examples/test_22b2_overriding_nonexistent.ail new file mode 100644 index 0000000..d0d8309 --- /dev/null +++ b/examples/test_22b2_overriding_nonexistent.ail @@ -0,0 +1,12 @@ +(module test_22b2_overriding_nonexistent + (class TEq + (param a) + (method teq + (type (fn-type (params a a) (ret (con Bool)))))) + (instance + (class TEq) + (type (con Int)) + (method teq + (body true)) + (method ne + (body false)))) diff --git a/examples/test_22b2_two_fns_missing_constraint.ail b/examples/test_22b2_two_fns_missing_constraint.ail new file mode 100644 index 0000000..79414f1 --- /dev/null +++ b/examples/test_22b2_two_fns_missing_constraint.ail @@ -0,0 +1,13 @@ +(module test_22b2_two_fns_missing_constraint + (class TShow + (param a) + (method tshow + (type (fn-type (params a) (ret (con Str)))))) + (fn describe_first + (type (forall (vars a) (fn-type (params a) (ret (con Str))))) + (params x) + (body (app tshow x))) + (fn describe_second + (type (forall (vars b) (fn-type (params b) (ret (con Str))))) + (params y) + (body (app tshow y)))) diff --git a/examples/test_22b2_xmod_classmod.ail b/examples/test_22b2_xmod_classmod.ail new file mode 100644 index 0000000..2a2ccb9 --- /dev/null +++ b/examples/test_22b2_xmod_classmod.ail @@ -0,0 +1,10 @@ +(module test_22b2_xmod_classmod + (class TShow + (param a) + (method tshow + (type (fn-type (params a) (ret (con Str)))))) + (instance + (class TShow) + (type (con Int)) + (method tshow + (body (lam (params (typed x a)) (ret (con Str)) (body "n")))))) diff --git a/examples/test_22b2_xmod_classmod_noinst.ail b/examples/test_22b2_xmod_classmod_noinst.ail new file mode 100644 index 0000000..f6ecc8a --- /dev/null +++ b/examples/test_22b2_xmod_classmod_noinst.ail @@ -0,0 +1,5 @@ +(module test_22b2_xmod_classmod_noinst + (class TShow + (param a) + (method tshow + (type (fn-type (params a) (ret (con Str))))))) diff --git a/examples/test_22b2_xmod_instance_present.ail b/examples/test_22b2_xmod_instance_present.ail new file mode 100644 index 0000000..e7cc00d --- /dev/null +++ b/examples/test_22b2_xmod_instance_present.ail @@ -0,0 +1,6 @@ +(module test_22b2_xmod_instance_present + (import test_22b2_xmod_classmod) + (fn main + (type (fn-type (params) (ret (con Str)))) + (params) + (body (app tshow 42)))) diff --git a/examples/test_22b2_xmod_missing_constraint.ail b/examples/test_22b2_xmod_missing_constraint.ail new file mode 100644 index 0000000..4c2c237 --- /dev/null +++ b/examples/test_22b2_xmod_missing_constraint.ail @@ -0,0 +1,6 @@ +(module test_22b2_xmod_missing_constraint + (import test_22b2_xmod_classmod_noinst) + (fn describe + (type (forall (vars a) (fn-type (params a) (ret (con Str))))) + (params x) + (body (app tshow x)))) diff --git a/examples/test_22b2_xmod_no_instance.ail b/examples/test_22b2_xmod_no_instance.ail new file mode 100644 index 0000000..bfa451d --- /dev/null +++ b/examples/test_22b2_xmod_no_instance.ail @@ -0,0 +1,6 @@ +(module test_22b2_xmod_no_instance + (import test_22b2_xmod_classmod_noinst) + (fn main + (type (fn-type (params) (ret (con Str)))) + (params) + (body (app tshow 42)))) diff --git a/examples/test_22b3_chained_calls.ail b/examples/test_22b3_chained_calls.ail new file mode 100644 index 0000000..bbd10d8 --- /dev/null +++ b/examples/test_22b3_chained_calls.ail @@ -0,0 +1,22 @@ +(module test_22b3_chained_calls + (class B + (param a) + (method b_method + (type (fn-type (params a) (ret (con Int)))))) + (class A + (param a) + (method a_method + (type (fn-type (params a) (ret (con Int)))) + (default (lam (params (typed x a)) (ret (con Int)) (body (app b_method x)))))) + (instance + (class B) + (type (con Int)) + (method b_method + (body (lam (params (typed y a)) (ret (con Int)) (body y))))) + (instance + (class A) + (type (con Int))) + (fn main + (type (fn-type (params) (ret (con Int)))) + (params) + (body (app a_method 5)))) diff --git a/examples/test_22b3_coherence_two_instances.ail b/examples/test_22b3_coherence_two_instances.ail new file mode 100644 index 0000000..c7d2015 --- /dev/null +++ b/examples/test_22b3_coherence_two_instances.ail @@ -0,0 +1,19 @@ +(module test_22b3_coherence_two_instances + (class R + (param a) + (method r + (type (fn-type (params a) (ret (con Int)))))) + (instance + (class R) + (type (con Int)) + (method r + (body (lam (params (typed x a)) (ret (con Int)) (body 11))))) + (instance + (class R) + (type (con Bool)) + (method r + (body (lam (params (typed b a)) (ret (con Int)) (body 22))))) + (fn main + (type (fn-type (params) (ret (con Unit)) (effects IO))) + (params) + (body (seq (do io/print_int (app r 0)) (do io/print_int (app r true)))))) diff --git a/examples/test_22b3_default_e2e.ail b/examples/test_22b3_default_e2e.ail new file mode 100644 index 0000000..6e35280 --- /dev/null +++ b/examples/test_22b3_default_e2e.ail @@ -0,0 +1,13 @@ +(module test_22b3_default_e2e + (class D + (param a) + (method dval + (type (fn-type (params a) (ret (con Int)))) + (default (lam (params (typed x a)) (ret (con Int)) (body 99))))) + (instance + (class D) + (type (con Int))) + (fn main + (type (fn-type (params) (ret (con Unit)) (effects IO))) + (params) + (body (do io/print_int (app dval 0))))) diff --git a/examples/test_22b3_dup_call_sites.ail b/examples/test_22b3_dup_call_sites.ail new file mode 100644 index 0000000..f48da0a --- /dev/null +++ b/examples/test_22b3_dup_call_sites.ail @@ -0,0 +1,14 @@ +(module test_22b3_dup_call_sites + (class TShow + (param a) + (method tshow + (type (fn-type (params a) (ret (con Str)))))) + (instance + (class TShow) + (type (con Int)) + (method tshow + (body (lam (params (typed x a)) (ret (con Str)) (body "n"))))) + (fn main + (type (fn-type (params) (ret (con Str)))) + (params) + (body (let _a (app tshow 5) (app tshow 7))))) diff --git a/examples/test_22b3_mono_synthetic.ail b/examples/test_22b3_mono_synthetic.ail new file mode 100644 index 0000000..101bf85 --- /dev/null +++ b/examples/test_22b3_mono_synthetic.ail @@ -0,0 +1,14 @@ +(module test_22b3_mono_synthetic + (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 i a)) (ret (con Int)) (body i))))) + (fn main + (type (fn-type (params) (ret (con Unit)) (effects IO))) + (params) + (body (do io/print_int (app foo 5))))) diff --git a/examples/test_22b3_no_call_sites.ail b/examples/test_22b3_no_call_sites.ail new file mode 100644 index 0000000..912fccd --- /dev/null +++ b/examples/test_22b3_no_call_sites.ail @@ -0,0 +1,14 @@ +(module test_22b3_no_call_sites + (class TShow + (param a) + (method tshow + (type (fn-type (params a) (ret (con Str)))))) + (instance + (class TShow) + (type (con Int)) + (method tshow + (body (lam (params (typed x a)) (ret (con Str)) (body "n"))))) + (fn main + (type (fn-type (params) (ret (con Str)))) + (params) + (body "no-call"))) diff --git a/examples/test_22b3_shadow_class_method.ail b/examples/test_22b3_shadow_class_method.ail new file mode 100644 index 0000000..c654146 --- /dev/null +++ b/examples/test_22b3_shadow_class_method.ail @@ -0,0 +1,23 @@ +(module test_22b3_shadow_class_method + (class TShow + (param a) + (method tshow + (type (fn-type (params a) (ret (con Str)))))) + (class Foo + (param a) + (method foo + (type (fn-type (params a) (ret (con Str)))))) + (instance + (class TShow) + (type (con Int)) + (method tshow + (body (lam (params (typed x a)) (ret (con Str)) (body "s"))))) + (instance + (class Foo) + (type (con Int)) + (method foo + (body (lam (params (typed x a)) (ret (con Str)) (body "f"))))) + (fn main + (type (fn-type (params) (ret (con Str)))) + (params) + (body (let _t (app tshow 5) (let tshow "shadow" (let _u tshow (app foo 7))))))) diff --git a/examples/test_22b3_xmod_e2e_classmod.ail b/examples/test_22b3_xmod_e2e_classmod.ail new file mode 100644 index 0000000..03119de --- /dev/null +++ b/examples/test_22b3_xmod_e2e_classmod.ail @@ -0,0 +1,10 @@ +(module test_22b3_xmod_e2e_classmod + (class XR + (param a) + (method xr + (type (fn-type (params a) (ret (con Int)))))) + (instance + (class XR) + (type (con Int)) + (method xr + (body (lam (params (typed x a)) (ret (con Int)) (body 7)))))) diff --git a/examples/test_22b3_xmod_e2e_main.ail b/examples/test_22b3_xmod_e2e_main.ail new file mode 100644 index 0000000..75ff552 --- /dev/null +++ b/examples/test_22b3_xmod_e2e_main.ail @@ -0,0 +1,6 @@ +(module test_22b3_xmod_e2e_main + (import test_22b3_xmod_e2e_classmod) + (fn main + (type (fn-type (params) (ret (con Unit)) (effects IO))) + (params) + (body (do io/print_int (app xr 0))))) diff --git a/examples/test_22c_user_class_e2e.ail b/examples/test_22c_user_class_e2e.ail new file mode 100644 index 0000000..77091e1 --- /dev/null +++ b/examples/test_22c_user_class_e2e.ail @@ -0,0 +1,17 @@ +(module test_22c_user_class_e2e + (data IntBox + (ctor MkIntBox (con Int))) + (class Foo + (param a) + (method foo + (type (fn-type (params (borrow a)) (ret (con Int)))))) + (instance + (class Foo) + (type (con IntBox)) + (method foo + (body (lam (params (typed b a)) (ret (con Int)) (body (match b + (case (pat-ctor MkIntBox v) v))))))) + (fn main + (type (fn-type (params) (ret (con Unit)) (effects IO))) + (params) + (body (do io/print_int (app foo (term-ctor IntBox MkIntBox 42)))))) diff --git a/examples/test_mono_ctor_listmod.ail b/examples/test_mono_ctor_listmod.ail new file mode 100644 index 0000000..b1e29cf --- /dev/null +++ b/examples/test_mono_ctor_listmod.ail @@ -0,0 +1,5 @@ +(module test_mono_ctor_listmod + (data List (vars a) + (doc "Minimal polymorphic list for the mono-pass cross-module ctor-pattern bug fixture.") + (ctor Nil) + (ctor Cons a (con List a)))) diff --git a/examples/test_mono_ctor_main.ail b/examples/test_mono_ctor_main.ail new file mode 100644 index 0000000..d8d434a --- /dev/null +++ b/examples/test_mono_ctor_main.ail @@ -0,0 +1,22 @@ +(module test_mono_ctor_main + (import test_mono_ctor_listmod) + (class Trivial + (param a) + (method trivial + (type (fn-type (params a) (ret (con Int)))))) + (instance + (class Trivial) + (type (con Int)) + (method trivial + (body (lam (params (typed x (con Int))) (ret (con Int)) (body x))))) + (fn head_or_zero + (doc "Pattern-matches Cons from an imported module's List. Bug repro: with a class+instance present in the workspace, the mono pass walks this body and synth's local-flat ctor_index resolves Cons to bare \"List\", which mismatches the qualified scrutinee type \"test_mono_ctor_listmod.List\".") + (type (fn-type (params (con test_mono_ctor_listmod.List (con Int))) (ret (con Int)))) + (params xs) + (body (match xs + (case (pat-ctor Cons h _) h) + (case (pat-ctor Nil) 0)))) + (fn main + (type (fn-type (params) (ret (con Unit)) (effects IO))) + (params) + (body (do io/print_int (app head_or_zero (term-ctor test_mono_ctor_listmod.List Cons 7 (term-ctor test_mono_ctor_listmod.List Nil))))))) diff --git a/examples/test_mono_imports_classmod.ail b/examples/test_mono_imports_classmod.ail new file mode 100644 index 0000000..8738dba --- /dev/null +++ b/examples/test_mono_imports_classmod.ail @@ -0,0 +1,14 @@ +(module test_mono_imports_classmod + (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 x))))) + (fn helper + (type (fn-type (params (con Int)) (ret (con Int)))) + (params n) + (body (app + n 100)))) diff --git a/examples/test_mono_imports_main.ail b/examples/test_mono_imports_main.ail new file mode 100644 index 0000000..5d78256 --- /dev/null +++ b/examples/test_mono_imports_main.ail @@ -0,0 +1,6 @@ +(module test_mono_imports_main + (import test_mono_imports_classmod) + (fn main + (type (fn-type (params) (ret (con Unit)) (effects IO))) + (params) + (body (do io/print_int (app test_mono_imports_classmod.helper 42))))) diff --git a/examples/test_mono_recursive_fn_bug.ail b/examples/test_mono_recursive_fn_bug.ail new file mode 100644 index 0000000..db79b64 --- /dev/null +++ b/examples/test_mono_recursive_fn_bug.ail @@ -0,0 +1,18 @@ +(module test_mono_recursive_fn_bug + (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 x))))) + (fn loop + (type (fn-type (params (con Int) (con Int)) (ret (con Int)))) + (params acc i) + (body (if (app == i 0) acc (tail-app loop (app + acc i) (app - i 1))))) + (fn main + (type (fn-type (params) (ret (con Unit)) (effects IO))) + (params) + (body (do io/print_int (app loop 0 5))))) diff --git a/examples/ws_broken.ail b/examples/ws_broken.ail new file mode 100644 index 0000000..92ede35 --- /dev/null +++ b/examples/ws_broken.ail @@ -0,0 +1,7 @@ +(module ws_broken + (import ws_lib) + (fn main + (doc "Iter 5b negative fixture: ruft ws_lib.bogus auf — Modul existiert, Def nicht. Triggert `unknown-import`.") + (type (fn-type (params) (ret (con Int)))) + (params) + (body (app ws_lib.bogus 1 2)))) diff --git a/examples/ws_lib.ail b/examples/ws_lib.ail new file mode 100644 index 0000000..c262c32 --- /dev/null +++ b/examples/ws_lib.ail @@ -0,0 +1,6 @@ +(module ws_lib + (fn add + (doc "Iter 5a fixture: einfache Add-Funktion, die ws_main importiert.") + (type (fn-type (params (con Int) (con Int)) (ret (con Int)))) + (params a b) + (body (app + a b)))) diff --git a/examples/ws_main.ail b/examples/ws_main.ail new file mode 100644 index 0000000..373f43f --- /dev/null +++ b/examples/ws_main.ail @@ -0,0 +1,7 @@ +(module ws_main + (import ws_lib) + (fn main + (doc "Iter 5b fixture: ruft ws_lib.add auf, druckt das Ergebnis. So ist Cross-Module-Typcheck am Beispiel beobachtbar.") + (type (fn-type (params) (ret (con Unit)) (effects IO))) + (params) + (body (do io/print_int (app ws_lib.add 2 3))))) diff --git a/examples/ws_unknown_module.ail b/examples/ws_unknown_module.ail new file mode 100644 index 0000000..3c33a5c --- /dev/null +++ b/examples/ws_unknown_module.ail @@ -0,0 +1,6 @@ +(module ws_unknown_module + (fn main + (doc "Iter 5b negative fixture: kein Import, aber qualifizierter Verweis `nope.x`. Triggert `unknown-module`.") + (type (fn-type (params) (ret (con Int)))) + (params) + (body nope.x)))