From e8018d48b68029d9ef2f9e69439c4cce64572fcd Mon Sep 17 00:00:00 2001 From: Brummel Date: Sat, 9 May 2026 20:59:43 +0200 Subject: [PATCH] =?UTF-8?q?iter=2022b.3.5:=20fix=20=E2=80=94=20quality-rev?= =?UTF-8?q?iew=20(chained-call=20test,=20hoist=20class=5Findex,=20drop=20n?= =?UTF-8?q?oise=20comments)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- crates/ail/tests/typeclass_22b3.rs | 52 ++++++++++++++++ crates/ailang-check/src/mono.rs | 8 +-- examples/test_22b3_chained_calls.ail.json | 73 +++++++++++++++++++++++ 3 files changed, 126 insertions(+), 7 deletions(-) create mode 100644 examples/test_22b3_chained_calls.ail.json diff --git a/crates/ail/tests/typeclass_22b3.rs b/crates/ail/tests/typeclass_22b3.rs index d6355eb..e874075 100644 --- a/crates/ail/tests/typeclass_22b3.rs +++ b/crates/ail/tests/typeclass_22b3.rs @@ -390,3 +390,55 @@ fn fixpoint_dedupes_repeated_calls_at_same_type() { } assert_eq!(count, 1, "exactly one show#Int across the workspace"); } + +/// Property: when a synthesised instance body itself calls another class +/// method (the canonical `Eq.ne x y = not (eq x y)` shape), the workspace +/// fixpoint closes over the chained call — Round 1 synthesises the outer +/// fn, Round 2 picks up the inner class-method residual on the freshly- +/// appended body and synthesises that. Without the multi-round loop, the +/// inner mono fn would be missing. +/// +/// Fixture (`test_22b3_chained_calls`): +/// class B a where b_method : (a) -> Int +/// class A a where a_method : (a) -> Int default \x. b_method x +/// instance B Int { b_method = \y. y } +/// 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 +/// 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 diags = ailang_check::check_workspace(&ws); + assert!(diags.is_empty(), "fixture must typecheck: {:?}", diags); + + let ws = ailang_check::monomorphise_workspace(&ws).expect("mono"); + + // Locate both synthesised fns workspace-wide. + let mut has_a_method_int = false; + let mut has_b_method_int = false; + 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" { + has_a_method_int = true; + } + if f.name == "b_method#Int" { + has_b_method_int = true; + } + } + } + } + assert!( + has_a_method_int, + "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 \ + — multi-round fixpoint property" + ); +} diff --git a/crates/ailang-check/src/mono.rs b/crates/ailang-check/src/mono.rs index 9a16276..28ecce3 100644 --- a/crates/ailang-check/src/mono.rs +++ b/crates/ailang-check/src/mono.rs @@ -74,14 +74,10 @@ pub fn monomorphise_workspace(ws: &Workspace) -> Result { return Ok(ws.clone()); } - // Take ownership of a mutable workspace clone; the pass - // appends synthesised defs to per-module def lists. let mut ws_owned: Workspace = ws.clone(); let env = build_workspace_env(&ws_owned); + let class_index = build_class_index(&ws_owned); - // Dedup set: every (class, method, type-hash) we have already - // synthesised. Once a key is here, future collection rounds - // skip it. let mut synthesised: BTreeSet<(String, String, String)> = BTreeSet::new(); // Fixpoint: keep collecting until a round adds nothing new. @@ -117,7 +113,6 @@ pub fn monomorphise_workspace(ws: &Workspace) -> Result { // truth for ClassDef + InstanceDef lookups. Both come // from the un-modified workspace registry — synthesis // never mutates the registry. - let class_index = build_class_index(&ws_owned); for t in &new { let key = mono_target_key(t); let registry_key = (t.class.clone(), ailang_core::canonical::type_hash(&t.type_)); @@ -153,7 +148,6 @@ pub fn monomorphise_workspace(ws: &Workspace) -> Result { } } - // Task 6 will splice the call-site rewrite here. Ok(ws_owned) } diff --git a/examples/test_22b3_chained_calls.ail.json b/examples/test_22b3_chained_calls.ail.json new file mode 100644 index 0000000..b117d57 --- /dev/null +++ b/examples/test_22b3_chained_calls.ail.json @@ -0,0 +1,73 @@ +{ + "schema": "ailang/v0", + "name": "test_22b3_chained_calls", + "imports": [], + "defs": [ + { + "kind": "class", "name": "B", "param": "a", + "methods": [{ + "name": "b_method", + "type": { + "k": "fn", "params": [{ "k": "var", "name": "a" }], + "ret": { "k": "con", "name": "Int" }, "effects": [] + } + }] + }, + { + "kind": "class", "name": "A", "param": "a", + "methods": [{ + "name": "a_method", + "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": "app", + "fn": { "t": "var", "name": "b_method" }, + "args": [{ "t": "var", "name": "x" }] + } + } + }] + }, + { + "kind": "instance", + "class": "B", + "type": { "k": "con", "name": "Int" }, + "methods": [{ + "name": "b_method", + "body": { + "t": "lam", + "params": ["y"], + "paramTypes": [{ "k": "var", "name": "a" }], + "retType": { "k": "con", "name": "Int" }, + "body": { "t": "var", "name": "y" } + } + }] + }, + { + "kind": "instance", + "class": "A", + "type": { "k": "con", "name": "Int" }, + "methods": [] + }, + { + "kind": "fn", + "name": "main", + "type": { + "k": "fn", "params": [], "ret": { "k": "con", "name": "Int" }, + "effects": [] + }, + "params": [], + "body": { + "t": "app", + "fn": { "t": "var", "name": "a_method" }, + "args": [{ "t": "lit", "lit": { "kind": "int", "value": 5 } }] + } + } + ] +}