diff --git a/crates/ail/tests/typeclass_22b3.rs b/crates/ail/tests/typeclass_22b3.rs index 4b116ea..e10d1ae 100644 --- a/crates/ail/tests/typeclass_22b3.rs +++ b/crates/ail/tests/typeclass_22b3.rs @@ -687,3 +687,110 @@ fn rewrite_replaces_class_method_var_with_mono_symbol_same_module() { }; assert_eq!(callee_name, "show__Int"); } + +/// Helper: build & run a workspace via `ail run` and capture stdout +/// + stderr. The 22b.3 e2e tests below all funnel through this so the +/// failure-mode reporting is uniform. +fn ail_run(fixture_name: &str) -> (std::process::Output, String) { + 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(fixture_name); + + let tmp = manifest_dir.join("target").join(format!( + "test_22b3_e2e_{}_{}", + fixture_name.replace('.', "_"), + std::process::id() + )); + std::fs::create_dir_all(&tmp).expect("mkdir temp"); + + let output = std::process::Command::new(&ail_bin) + .arg("run") + .arg(&fixture) + .current_dir(&tmp) + .output() + .expect("ail run"); + let stdout = String::from_utf8_lossy(&output.stdout).to_string(); + (output, stdout) +} + +/// Property: when a single workspace declares two distinct instances of +/// the SAME class (`R Int` and `R Bool`), and `main` calls the class +/// method `r` at BOTH types, the binary picks the correct +/// monomorphised body for each call site. Coherence is preserved +/// end-to-end: instance `R Int { r _ = 11 }` reaches the `r 0` call, +/// instance `R Bool { r _ = 22 }` reaches the `r True` call. A +/// regression where coherence collapses (e.g. both call sites get +/// rewritten to the same `r__`, or one synth body shadows the other) +/// would surface here as `11\n11\n`, `22\n22\n`, or build/runtime +/// failure — all observable at stdout. Defends mono-pass invariant 3. +#[test] +fn coherence_two_instances_pick_correct_body_at_runtime() { + let (output, stdout) = ail_run("test_22b3_coherence_two_instances.ail.json"); + assert!( + output.status.success(), + "ail run failed: stdout={}, stderr={}", + stdout, + String::from_utf8_lossy(&output.stderr), + ); + assert_eq!( + stdout.trim_end_matches('\n'), + "11\n22", + "stdout = `{stdout}`, stderr = `{}`", + String::from_utf8_lossy(&output.stderr), + ); +} + +/// Property: a class method whose body comes from the class-level +/// `default` clause (the instance omits the override) is reachable +/// end-to-end through codegen and run. `class D a where dval : (a) -> +/// Int default \x. 99` + empty `instance D Int {}` + `main` calling +/// `dval 0` must produce `99\n`. Currently only `synthesise_mono_fn`'s +/// unit test covers the default-fallback branch; this fixture pins +/// down the *binary-level* property — the synthesised fn must lower +/// and link, the call site must be rewritten to `dval__Int`, the +/// constant 99 must reach stdout. Defends mono-pass invariant 4. +#[test] +fn class_default_method_runs_and_prints_default_value() { + let (output, stdout) = ail_run("test_22b3_default_e2e.ail.json"); + assert!( + output.status.success(), + "ail run failed: stdout={}, stderr={}", + stdout, + String::from_utf8_lossy(&output.stderr), + ); + assert_eq!( + stdout.trim_end(), + "99", + "stdout = `{stdout}`, stderr = `{}`", + String::from_utf8_lossy(&output.stderr), + ); +} + +/// Property: a class+instance declared in module A, called from module +/// B's `main`, links and runs end-to-end. The mono pass appends the +/// synthesised fn to module A (the instance's defining module), the +/// call-site rewrite in module B emits the qualified name +/// `.`, and the linker resolves the reference across +/// module boundaries. The AST-level property is already protected by +/// `rewrite_uses_qualified_name_for_cross_module_call_site`, but the +/// emit + link + run path is otherwise untested at the binary level. +/// A regression that left the synthesised fn unemitted or the call +/// site unqualified would surface as a link error or wrong stdout. +/// Defends mono-pass invariant 6. +#[test] +fn cross_module_class_method_runs_and_prints_correct_value() { + let (output, stdout) = ail_run("test_22b3_xmod_e2e_main.ail.json"); + assert!( + output.status.success(), + "ail run failed: stdout={}, stderr={}", + stdout, + String::from_utf8_lossy(&output.stderr), + ); + assert_eq!( + stdout.trim_end(), + "7", + "stdout = `{stdout}`, stderr = `{}`", + String::from_utf8_lossy(&output.stderr), + ); +} diff --git a/examples/test_22b3_coherence_two_instances.ail.json b/examples/test_22b3_coherence_two_instances.ail.json new file mode 100644 index 0000000..4b77dfa --- /dev/null +++ b/examples/test_22b3_coherence_two_instances.ail.json @@ -0,0 +1,77 @@ +{ + "schema": "ailang/v0", + "name": "test_22b3_coherence_two_instances", + "imports": [], + "defs": [ + { + "kind": "class", "name": "R", "param": "a", + "methods": [{ + "name": "r", + "type": { + "k": "fn", "params": [{ "k": "var", "name": "a" }], + "ret": { "k": "con", "name": "Int" }, "effects": [] + } + }] + }, + { + "kind": "instance", + "class": "R", + "type": { "k": "con", "name": "Int" }, + "methods": [{ + "name": "r", + "body": { + "t": "lam", + "params": ["x"], + "paramTypes": [{ "k": "var", "name": "a" }], + "retType": { "k": "con", "name": "Int" }, + "body": { "t": "lit", "lit": { "kind": "int", "value": 11 } } + } + }] + }, + { + "kind": "instance", + "class": "R", + "type": { "k": "con", "name": "Bool" }, + "methods": [{ + "name": "r", + "body": { + "t": "lam", + "params": ["b"], + "paramTypes": [{ "k": "var", "name": "a" }], + "retType": { "k": "con", "name": "Int" }, + "body": { "t": "lit", "lit": { "kind": "int", "value": 22 } } + } + }] + }, + { + "kind": "fn", + "name": "main", + "type": { + "k": "fn", "params": [], "ret": { "k": "con", "name": "Unit" }, + "effects": ["IO"] + }, + "params": [], + "body": { + "t": "seq", + "lhs": { + "t": "do", + "op": "io/print_int", + "args": [{ + "t": "app", + "fn": { "t": "var", "name": "r" }, + "args": [{ "t": "lit", "lit": { "kind": "int", "value": 0 } }] + }] + }, + "rhs": { + "t": "do", + "op": "io/print_int", + "args": [{ + "t": "app", + "fn": { "t": "var", "name": "r" }, + "args": [{ "t": "lit", "lit": { "kind": "bool", "value": true } }] + }] + } + } + } + ] +} diff --git a/examples/test_22b3_default_e2e.ail.json b/examples/test_22b3_default_e2e.ail.json new file mode 100644 index 0000000..67e92c5 --- /dev/null +++ b/examples/test_22b3_default_e2e.ail.json @@ -0,0 +1,48 @@ +{ + "schema": "ailang/v0", + "name": "test_22b3_default_e2e", + "imports": [], + "defs": [ + { + "kind": "class", "name": "D", "param": "a", + "methods": [{ + "name": "dval", + "type": { + "k": "fn", "params": [{ "k": "var", "name": "a" }], + "ret": { "k": "con", "name": "Int" }, "effects": [] + }, + "default": { + "t": "lam", + "params": ["x"], + "paramTypes": [{ "k": "var", "name": "a" }], + "retType": { "k": "con", "name": "Int" }, + "body": { "t": "lit", "lit": { "kind": "int", "value": 99 } } + } + }] + }, + { + "kind": "instance", + "class": "D", + "type": { "k": "con", "name": "Int" }, + "methods": [] + }, + { + "kind": "fn", + "name": "main", + "type": { + "k": "fn", "params": [], "ret": { "k": "con", "name": "Unit" }, + "effects": ["IO"] + }, + "params": [], + "body": { + "t": "do", + "op": "io/print_int", + "args": [{ + "t": "app", + "fn": { "t": "var", "name": "dval" }, + "args": [{ "t": "lit", "lit": { "kind": "int", "value": 0 } }] + }] + } + } + ] +} diff --git a/examples/test_22b3_xmod_e2e_classmod.ail.json b/examples/test_22b3_xmod_e2e_classmod.ail.json new file mode 100644 index 0000000..7072912 --- /dev/null +++ b/examples/test_22b3_xmod_e2e_classmod.ail.json @@ -0,0 +1,32 @@ +{ + "schema": "ailang/v0", + "name": "test_22b3_xmod_e2e_classmod", + "imports": [], + "defs": [ + { + "kind": "class", "name": "XR", "param": "a", + "methods": [{ + "name": "xr", + "type": { + "k": "fn", "params": [{ "k": "var", "name": "a" }], + "ret": { "k": "con", "name": "Int" }, "effects": [] + } + }] + }, + { + "kind": "instance", + "class": "XR", + "type": { "k": "con", "name": "Int" }, + "methods": [{ + "name": "xr", + "body": { + "t": "lam", + "params": ["x"], + "paramTypes": [{ "k": "var", "name": "a" }], + "retType": { "k": "con", "name": "Int" }, + "body": { "t": "lit", "lit": { "kind": "int", "value": 7 } } + } + }] + } + ] +} diff --git a/examples/test_22b3_xmod_e2e_main.ail.json b/examples/test_22b3_xmod_e2e_main.ail.json new file mode 100644 index 0000000..985a846 --- /dev/null +++ b/examples/test_22b3_xmod_e2e_main.ail.json @@ -0,0 +1,25 @@ +{ + "schema": "ailang/v0", + "name": "test_22b3_xmod_e2e_main", + "imports": [{ "module": "test_22b3_xmod_e2e_classmod" }], + "defs": [ + { + "kind": "fn", + "name": "main", + "type": { + "k": "fn", "params": [], "ret": { "k": "con", "name": "Unit" }, + "effects": ["IO"] + }, + "params": [], + "body": { + "t": "do", + "op": "io/print_int", + "args": [{ + "t": "app", + "fn": { "t": "var", "name": "xr" }, + "args": [{ "t": "lit", "lit": { "kind": "int", "value": 0 } }] + }] + } + } + ] +}