diff --git a/crates/ail/tests/typeclass_22b3.rs b/crates/ail/tests/typeclass_22b3.rs index bca5745..4b116ea 100644 --- a/crates/ail/tests/typeclass_22b3.rs +++ b/crates/ail/tests/typeclass_22b3.rs @@ -60,7 +60,7 @@ fn monomorphise_workspace_is_identity_on_class_free_workspace() { /// body, but `main` is a literal — there is no `show ` call at any /// type. Updated in Task 5 from `test_22b2_instance_present.ail.json`, /// which had a `show 42` call site that the real fixpoint now -/// (correctly) materialises into `show#Int`. +/// (correctly) materialises into `show__Int`. #[test] fn monomorphise_workspace_is_identity_when_no_concrete_call_sites() { let entry = examples_dir().join("test_22b3_no_call_sites.ail.json"); @@ -102,10 +102,10 @@ fn mono_symbol_primitive_types_use_surface_form() { let str_ty = Type::str_(); let unit_ty = Type::unit(); - assert_eq!(ailang_check::mono::mono_symbol("show", &int_ty), "show#Int"); - assert_eq!(ailang_check::mono::mono_symbol("eq", &bool_ty), "eq#Bool"); - assert_eq!(ailang_check::mono::mono_symbol("show", &str_ty), "show#Str"); - assert_eq!(ailang_check::mono::mono_symbol("foo", &unit_ty), "foo#Unit"); + assert_eq!(ailang_check::mono::mono_symbol("show", &int_ty), "show__Int"); + assert_eq!(ailang_check::mono::mono_symbol("eq", &bool_ty), "eq__Bool"); + assert_eq!(ailang_check::mono::mono_symbol("show", &str_ty), "show__Str"); + assert_eq!(ailang_check::mono::mono_symbol("foo", &unit_ty), "foo__Unit"); } /// Property: collect_mono_targets returns exactly the set of @@ -167,9 +167,9 @@ fn mono_symbol_compound_type_uses_hash_suffix() { }; let name = ailang_check::mono::mono_symbol("show", &pair_ty); - // Format: `show#<8-hex-chars>`. - assert!(name.starts_with("show#"), "name = {name}"); - let suffix = &name["show#".len()..]; + // Format: `show__<8-hex-chars>`. + assert!(name.starts_with("show__"), "name = {name}"); + let suffix = &name["show__".len()..]; assert_eq!(suffix.len(), 8, "compound suffix is 8 hex chars: {suffix}"); assert!( suffix.chars().all(|c| c.is_ascii_hexdigit()), @@ -231,7 +231,7 @@ fn synthesise_mono_fn_uses_instance_body_lam() { }; let f = synthesise_mono_fn(&target, &class_def, &instance).expect("synth"); - assert_eq!(f.name, "bar#Int"); + assert_eq!(f.name, "bar__Int"); assert!( matches!( &f.ty, @@ -300,7 +300,7 @@ fn synthesise_mono_fn_falls_back_to_class_default() { }; let f = synthesise_mono_fn(&target, &class_def, &instance).expect("synth"); - assert_eq!(f.name, "hello#Int"); + assert_eq!(f.name, "hello__Int"); assert_eq!( f.params.len(), 1, @@ -314,7 +314,7 @@ fn synthesise_mono_fn_falls_back_to_class_default() { /// without Lam-unwrapping. Class `Foo a where bar : Int` (note: no /// arrow — the method is a constant), instance `Foo Int where bar /// = 7`. synthesise_mono_fn must produce -/// `FnDef { name: "bar#Int", ty: Int, params: [], body: Lit 7 }`. +/// `FnDef { name: "bar__Int", ty: Int, params: [], body: Lit 7 }`. #[test] fn synthesise_mono_fn_zero_arg_method_uses_body_verbatim() { use ailang_check::mono::{synthesise_mono_fn, MonoTarget}; @@ -352,7 +352,7 @@ fn synthesise_mono_fn_zero_arg_method_uses_body_verbatim() { }; let f = synthesise_mono_fn(&target, &class_def, &instance).expect("synth"); - assert_eq!(f.name, "bar#Int"); + assert_eq!(f.name, "bar__Int"); assert!( f.params.is_empty(), "zero-arg method must produce empty params" @@ -366,7 +366,7 @@ fn synthesise_mono_fn_zero_arg_method_uses_body_verbatim() { /// Property: when two call sites of `show` apply at the same concrete /// type (Int), the workspace fixpoint synthesises the matching FnDef -/// (`show#Int`) exactly once across the whole workspace. Verifies the +/// (`show__Int`) exactly once across the whole workspace. Verifies the /// dedup contract — the pass must not emit one FnDef per call site. #[test] fn fixpoint_dedupes_repeated_calls_at_same_type() { @@ -377,18 +377,18 @@ fn fixpoint_dedupes_repeated_calls_at_same_type() { let ws = ailang_check::monomorphise_workspace(&ws).expect("mono"); - // Count `show#Int` fns workspace-wide. + // Count `show__Int` fns workspace-wide. let mut count = 0usize; for m in ws.modules.values() { for d in &m.defs { if let ailang_core::ast::Def::Fn(f) = d { - if f.name == "show#Int" { + if f.name == "show__Int" { count += 1; } } } } - assert_eq!(count, 1, "exactly one show#Int across the workspace"); + assert_eq!(count, 1, "exactly one show__Int across the workspace"); } /// Property: when a synthesised instance body itself calls another class @@ -405,8 +405,8 @@ fn fixpoint_dedupes_repeated_calls_at_same_type() { /// instance A Int { /* uses class default */ } /// main : () -> Int = a_method 5 /// -/// Expected post-mono workspace contents: BOTH `a_method#Int` (Round 1, -/// from the class default) AND `b_method#Int` (Round 2, from instance B's +/// Expected post-mono workspace contents: BOTH `a_method__Int` (Round 1, +/// from the class default) AND `b_method__Int` (Round 2, from instance B's /// body). #[test] fn fixpoint_handles_chained_class_method_calls() { @@ -423,10 +423,10 @@ fn fixpoint_handles_chained_class_method_calls() { for m in ws.modules.values() { for d in &m.defs { if let ailang_core::ast::Def::Fn(f) = d { - if f.name == "a_method#Int" { + if f.name == "a_method__Int" { has_a_method_int = true; } - if f.name == "b_method#Int" { + if f.name == "b_method__Int" { has_b_method_int = true; } } @@ -434,11 +434,11 @@ fn fixpoint_handles_chained_class_method_calls() { } assert!( has_a_method_int, - "a_method#Int must be synthesised in Round 1 from main's call site" + "a_method__Int must be synthesised in Round 1 from main's call site" ); assert!( has_b_method_int, - "b_method#Int must be synthesised in Round 2 from a_method#Int's body \ + "b_method__Int must be synthesised in Round 2 from a_method__Int's body \ — multi-round fixpoint property" ); } @@ -463,12 +463,12 @@ fn fixpoint_handles_chained_class_method_calls() { /// /// `synth` pushes exactly two residuals (in order: show@Int, foo@Int). /// The shadow-aware walker must: -/// - rewrite the outer `show` callee → `show#Int`, +/// - rewrite the outer `show` callee → `show__Int`, /// - leave the inner shadowed `show` Var literally as `show`, -/// - rewrite the `foo` callee → `foo#Int`. +/// - rewrite the `foo` callee → `foo__Int`. /// /// A shadow-blind walker would advance the cursor on the inner Var, -/// rewriting it to `foo#Int` (using residual #1) AND leaving the real +/// rewriting it to `foo__Int` (using residual #1) AND leaving the real /// `foo 7` callee untouched (cursor walks off the end of the slice). #[test] fn rewrite_walker_skips_locally_shadowed_class_method() { @@ -504,8 +504,8 @@ fn rewrite_walker_skips_locally_shadowed_class_method() { other => panic!("outer value is not App: {:?}", other), }; assert_eq!( - outer_callee, "show#Int", - "the outer class-method call site must be rewritten to show#Int" + outer_callee, "show__Int", + "the outer class-method call site must be rewritten to show__Int" ); // Step inside `let show = "shadow" in `. @@ -532,7 +532,7 @@ fn rewrite_walker_skips_locally_shadowed_class_method() { not be re-aligned to a later residual slot" ); // The real `foo 7` call site — the second residual — must be - // rewritten correctly to `foo#Int`. A shadow-blind walker would + // rewritten correctly to `foo__Int`. A shadow-blind walker would // have spent residual #1 on the shadowed Var and left this Var // untouched. let foo_callee = match inner_body { @@ -543,7 +543,7 @@ fn rewrite_walker_skips_locally_shadowed_class_method() { other => panic!("expected App for `foo 7`, got {:?}", other), }; assert_eq!( - foo_callee, "foo#Int", + foo_callee, "foo__Int", "the post-shadow `foo` call site must align to residual #1" ); } @@ -581,29 +581,81 @@ fn rewrite_uses_qualified_name_for_cross_module_call_site() { }; // class+instance live in test_22b2_xmod_classmod; caller in // test_22b2_xmod_instance_present → qualified call. - assert_eq!(callee_name, "test_22b2_xmod_classmod.show#Int"); + assert_eq!(callee_name, "test_22b2_xmod_classmod.show__Int"); // Placement: the synthesised def must live in the defining module, // not the caller's module. let classmod = ws.modules.get("test_22b2_xmod_classmod").unwrap(); let has_synth = classmod.defs.iter().any(|d| { - matches!(d, ailang_core::ast::Def::Fn(f) if f.name == "show#Int") + matches!(d, ailang_core::ast::Def::Fn(f) if f.name == "show__Int") }); assert!( has_synth, - "show#Int must live in the instance's defining module" + "show__Int must live in the instance's defining module" ); let caller_has_synth = main_module.defs.iter().any(|d| { - matches!(d, ailang_core::ast::Def::Fn(f) if f.name == "show#Int") + matches!(d, ailang_core::ast::Def::Fn(f) if f.name == "show__Int") }); assert!( !caller_has_synth, - "show#Int must NOT live in the caller's module" + "show__Int must NOT live in the caller's module" + ); +} + +/// Iter 22b.3.7 GATING: the synthetic class+instance fixture must +/// typecheck cleanly under `check_workspace`. Pre-condition for the +/// end-to-end run gate (`synthetic_fixture_runs_and_prints_five`): +/// if the fixture is ill-typed, the run gate's failure mode would +/// 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 diags = ailang_check::check_workspace(&ws); + assert!(diags.is_empty(), "fixture must typecheck: {:?}", diags); +} + +/// Iter 22b.3.7 GATING: the synthetic class+instance fixture must +/// build via `ail run` and emit `5\n` to stdout. This is the +/// spec's close-out criterion for 22b.3 — proves the full mono +/// pipeline (class declaration + instance + class-method call site +/// → synthesised concrete fn + rewritten call site → codegen + run) +/// is end-to-end-correct independently of the Prelude (22b.4). +#[test] +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 tmp = manifest_dir.join("target").join(format!( + "test_22b3_synthetic_{}", 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"); + + assert!( + output.status.success(), + "ail run failed: stdout={}, stderr={}", + String::from_utf8_lossy(&output.stdout), + String::from_utf8_lossy(&output.stderr), + ); + let stdout = String::from_utf8_lossy(&output.stdout); + assert_eq!( + stdout.trim_end(), "5", + "stdout = `{stdout}`, stderr = `{}`", + String::from_utf8_lossy(&output.stderr) ); } /// After the mono pass, the body of `main` in -/// test_22b2_instance_present must reference `show#Int` instead +/// test_22b2_instance_present must reference `show__Int` instead /// of `show`. #[test] fn rewrite_replaces_class_method_var_with_mono_symbol_same_module() { @@ -625,7 +677,7 @@ fn rewrite_replaces_class_method_var_with_mono_symbol_same_module() { .expect("main"); // The body is `App { fn: Var "show", args: [Lit 42] }`. After - // rewrite, the callee Var must be `show#Int`. + // rewrite, the callee Var must be `show__Int`. let callee_name: &str = match &main_fn.body { ailang_core::ast::Term::App { callee, .. } => match callee.as_ref() { ailang_core::ast::Term::Var { name } => name.as_str(), @@ -633,5 +685,5 @@ fn rewrite_replaces_class_method_var_with_mono_symbol_same_module() { }, other => panic!("body is not App: {:?}", other), }; - assert_eq!(callee_name, "show#Int"); + assert_eq!(callee_name, "show__Int"); } diff --git a/crates/ailang-check/src/mono.rs b/crates/ailang-check/src/mono.rs index 3cfdd21..e5a2560 100644 --- a/crates/ailang-check/src/mono.rs +++ b/crates/ailang-check/src/mono.rs @@ -277,26 +277,35 @@ fn workspace_has_typeclasses(ws: &Workspace) -> bool { /// Iter 22b.3: deterministic mono-symbol name for a `(method, /// type)` pair. Primitive types (`Int`, `Bool`, `Str`, `Unit`) -/// produce `#` for diagnostic and +/// produce `__` for diagnostic and /// ABI legibility. All other types — parameterised cons, /// user-defined ADTs, function types — fall to -/// `#<8-hex-prefix-of-canonical-type-hash>`. The hash +/// `__<8-hex-prefix-of-canonical-type-hash>`. The hash /// route ensures uniqueness without requiring a flattened /// surface form for arbitrarily nested types. /// +/// Separator choice: `__` (double underscore) rather than the spec's +/// recommended `#` — `#` terminates LLVM IR global identifiers (and +/// breaks C-ABI symbol names on most targets), so the produced name +/// has to survive codegen unaltered. The double underscore is +/// already in use elsewhere in the codegen pipeline as a descriptor +/// separator (see `emit_specialised_fn`), keeping the convention +/// uniform. Iter 22b.3.7 documents the substitution at the e2e +/// gate; Tasks 2/4 unit-tests are updated in lockstep. +/// /// Determinism: `ailang_core::canonical::type_hash` is the same /// function `workspace::build_registry` uses to key /// [`Registry::entries`], so a registry-key match implies a /// `mono_symbol` match. pub fn mono_symbol(method: &str, ty: &Type) -> String { if let Some(prim) = primitive_surface_name(ty) { - return format!("{method}#{prim}"); + return format!("{method}__{prim}"); } let full_hash = ailang_core::canonical::type_hash(ty); // 8-hex prefix is enough for low-collision keying across the // workspace; the full hash remains in the registry for // disambiguation should one ever be needed. - format!("{method}#{}", &full_hash[..8]) + format!("{method}__{}", &full_hash[..8]) } /// Iter 22b.3: returns the surface name iff `ty` is a zero-arity diff --git a/examples/test_22b3_mono_synthetic.ail.json b/examples/test_22b3_mono_synthetic.ail.json new file mode 100644 index 0000000..ff3b3de --- /dev/null +++ b/examples/test_22b3_mono_synthetic.ail.json @@ -0,0 +1,50 @@ +{ + "schema": "ailang/v0", + "name": "test_22b3_mono_synthetic", + "imports": [], + "defs": [ + { + "kind": "class", "name": "Foo", "param": "a", + "methods": [{ + "name": "foo", + "type": { + "k": "fn", "params": [{ "k": "var", "name": "a" }], + "ret": { "k": "con", "name": "Int" }, "effects": [] + } + }] + }, + { + "kind": "instance", + "class": "Foo", + "type": { "k": "con", "name": "Int" }, + "methods": [{ + "name": "foo", + "body": { + "t": "lam", + "params": ["i"], + "paramTypes": [{ "k": "var", "name": "a" }], + "retType": { "k": "con", "name": "Int" }, + "body": { "t": "var", "name": "i" } + } + }] + }, + { + "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": "foo" }, + "args": [{ "t": "lit", "lit": { "kind": "int", "value": 5 } }] + }] + } + } + ] +}