iter 22b.3.5: workspace fixpoint loop — dedup + synth-append
This commit is contained in:
@@ -49,25 +49,21 @@ fn monomorphise_workspace_is_identity_on_class_free_workspace() {
|
||||
}
|
||||
}
|
||||
|
||||
/// Property: the Task 1 skeleton's class-present branch is also a
|
||||
/// no-op. The fixture has a `Def::Class` and a `Def::Instance` (so
|
||||
/// the early-out does NOT fire) but no concrete class-method call
|
||||
/// sites yet wired through the pipeline; under the skeleton, the
|
||||
/// fall-through `Ok(ws.clone())` must preserve every module's
|
||||
/// canonical hash.
|
||||
/// Property: when a workspace has a class + instance but no concrete
|
||||
/// class-method call sites, the mono pass produces a hash-identical
|
||||
/// workspace — the early-out path is bypassed (typeclasses present)
|
||||
/// but the fixpoint loop terminates on round 1 with zero new targets,
|
||||
/// so no FnDefs are appended.
|
||||
///
|
||||
/// NOTE: this test will need updating in Task 5 once the skeleton
|
||||
/// is replaced with the real fixpoint — at that point synth fns
|
||||
/// will be appended to the `defining_module`, breaking module-hash
|
||||
/// equality on that module by design. The test as written
|
||||
/// guarantees the Task 1 contract and catches accidental partial
|
||||
/// rewrites landed before Task 5.
|
||||
/// The fixture (`test_22b3_no_call_sites.ail.json`) declares
|
||||
/// `class Show` + `instance Show Int` with a Lam-shaped instance
|
||||
/// body, but `main` is a literal — there is no `show <x>` 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`.
|
||||
#[test]
|
||||
fn monomorphise_workspace_is_identity_when_no_concrete_call_sites() {
|
||||
// `test_22b2_instance_present.ail.json` has `class Show` +
|
||||
// `instance Show Int` and typechecks cleanly; reused across
|
||||
// 22b.2 tests so its shape is stable.
|
||||
let entry = examples_dir().join("test_22b2_instance_present.ail.json");
|
||||
let entry = examples_dir().join("test_22b3_no_call_sites.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);
|
||||
@@ -82,14 +78,15 @@ fn monomorphise_workspace_is_identity_when_no_concrete_call_sites() {
|
||||
before.modules.keys().collect::<Vec<_>>(),
|
||||
"module set must be identical"
|
||||
);
|
||||
// Each module hashes byte-identical (skeleton clones unchanged).
|
||||
// Each module hashes byte-identical — no synth fns appended
|
||||
// because no call site triggered a target.
|
||||
for (mname, m) in &ws_after.modules {
|
||||
let before_m = &before.modules[mname];
|
||||
let h_after = ailang_core::module_hash(m);
|
||||
let h_before = ailang_core::module_hash(before_m);
|
||||
assert_eq!(
|
||||
h_after, h_before,
|
||||
"class-present module `{}` must hash-identical through Task 1 skeleton",
|
||||
"class-present-but-no-call-site module `{}` must hash-identical",
|
||||
mname
|
||||
);
|
||||
}
|
||||
@@ -366,3 +363,30 @@ fn synthesise_mono_fn_zero_arg_method_uses_body_verbatim() {
|
||||
f.body
|
||||
);
|
||||
}
|
||||
|
||||
/// 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
|
||||
/// 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 diags = ailang_check::check_workspace(&ws);
|
||||
assert!(diags.is_empty(), "fixture must typecheck: {:?}", diags);
|
||||
|
||||
let ws = ailang_check::monomorphise_workspace(&ws).expect("mono");
|
||||
|
||||
// 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" {
|
||||
count += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_eq!(count, 1, "exactly one show#Int across the workspace");
|
||||
}
|
||||
|
||||
@@ -74,10 +74,136 @@ pub fn monomorphise_workspace(ws: &Workspace) -> Result<Workspace> {
|
||||
return Ok(ws.clone());
|
||||
}
|
||||
|
||||
// Skeleton in Task 1: even with classes present, return the
|
||||
// workspace clone unchanged. Subsequent tasks fill in collection,
|
||||
// synthesis, and rewriting.
|
||||
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);
|
||||
|
||||
// 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.
|
||||
// Each round walks every fn body in every module — including
|
||||
// bodies appended by the previous round, which is what makes
|
||||
// the loop close on chained class-method calls (e.g.
|
||||
// `Eq.ne x y = not (eq x y)`).
|
||||
loop {
|
||||
let new_targets = collect_targets_workspace_wide(&ws_owned, &env)?;
|
||||
// Dedup within a round (multiple call sites of `show` at
|
||||
// the same type produce N copies of the same MonoTarget)
|
||||
// AND across rounds (already-synthesised keys filtered out).
|
||||
// Stable iteration order — preserve first-seen target so
|
||||
// any later debugging round-trips to a deterministic output.
|
||||
let mut seen_this_round: BTreeSet<(String, String, String)> = BTreeSet::new();
|
||||
let mut new: Vec<MonoTarget> = Vec::new();
|
||||
for t in new_targets {
|
||||
let k = mono_target_key(&t);
|
||||
if synthesised.contains(&k) || !seen_this_round.insert(k) {
|
||||
continue;
|
||||
}
|
||||
new.push(t);
|
||||
}
|
||||
if new.is_empty() {
|
||||
break;
|
||||
}
|
||||
|
||||
// Synthesise each new target; append its FnDef to the
|
||||
// target's `defining_module`. Mark the key as synthesised
|
||||
// so the next round won't re-collect.
|
||||
//
|
||||
// Iter 22b.3 uses ws_owned.registry as the source of
|
||||
// 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_));
|
||||
let entry = ws_owned
|
||||
.registry
|
||||
.entries
|
||||
.get(®istry_key)
|
||||
.ok_or_else(|| {
|
||||
crate::CheckError::Internal(format!(
|
||||
"monomorphise_workspace: target `{} {}` has no registry entry",
|
||||
t.class,
|
||||
ailang_core::pretty::type_to_string(&t.type_),
|
||||
))
|
||||
})?;
|
||||
let class_def = class_index.get(&t.class).ok_or_else(|| {
|
||||
crate::CheckError::Internal(format!(
|
||||
"monomorphise_workspace: class `{}` not found",
|
||||
t.class
|
||||
))
|
||||
})?;
|
||||
let f = synthesise_mono_fn(t, class_def, &entry.instance)?;
|
||||
let target_module = ws_owned
|
||||
.modules
|
||||
.get_mut(&t.defining_module)
|
||||
.ok_or_else(|| {
|
||||
crate::CheckError::Internal(format!(
|
||||
"monomorphise_workspace: defining module `{}` missing",
|
||||
t.defining_module
|
||||
))
|
||||
})?;
|
||||
target_module.defs.push(Def::Fn(f));
|
||||
synthesised.insert(key);
|
||||
}
|
||||
}
|
||||
|
||||
// Task 6 will splice the call-site rewrite here.
|
||||
Ok(ws_owned)
|
||||
}
|
||||
|
||||
/// Iter 22b.3: walk every fn / const body in the workspace,
|
||||
/// returning the union of [`collect_mono_targets`] outputs. Const
|
||||
/// bodies are wrapped in a synthetic zero-arg `FnDef` for the
|
||||
/// collection call — the residual gathering only depends on body
|
||||
/// shape, so the wrapping is harmless.
|
||||
fn collect_targets_workspace_wide(
|
||||
ws: &Workspace,
|
||||
env: &crate::Env,
|
||||
) -> Result<Vec<MonoTarget>> {
|
||||
let mut out: Vec<MonoTarget> = Vec::new();
|
||||
for (mname, m) in &ws.modules {
|
||||
for d in &m.defs {
|
||||
match d {
|
||||
Def::Fn(f) => {
|
||||
out.extend(collect_mono_targets(f, mname, env)?);
|
||||
}
|
||||
Def::Const(c) => {
|
||||
let pseudo = AstFnDef {
|
||||
name: c.name.clone(),
|
||||
ty: c.ty.clone(),
|
||||
params: Vec::new(),
|
||||
body: c.value.clone(),
|
||||
doc: None,
|
||||
suppress: Vec::new(),
|
||||
};
|
||||
out.extend(collect_mono_targets(&pseudo, mname, env)?);
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
/// Iter 22b.3: workspace-wide `class-name -> ClassDef` index.
|
||||
/// Used by the fixpoint to look up the matching class definition
|
||||
/// when synthesising a fn.
|
||||
fn build_class_index(ws: &Workspace) -> BTreeMap<String, ClassDef> {
|
||||
let mut idx = BTreeMap::new();
|
||||
for m in ws.modules.values() {
|
||||
for d in &m.defs {
|
||||
if let Def::Class(c) = d {
|
||||
idx.insert(c.name.clone(), c.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
idx
|
||||
}
|
||||
|
||||
/// Iter 22b.3: returns `true` iff any module in `ws` declares at
|
||||
|
||||
@@ -32,17 +32,19 @@ fn list_json_fixtures() -> Vec<PathBuf> {
|
||||
p.file_name()
|
||||
.and_then(|n| n.to_str())
|
||||
.map(|n| {
|
||||
// Iter 22b.1 / 22b.2: the `test_22b1_*` and
|
||||
// `test_22b2_*` fixtures exercise class/instance
|
||||
// defs at the workspace-load level, but the surface
|
||||
// (Form-B) parser/printer arms for those nodes are
|
||||
// deferred to 22b.4. Until that ships, including
|
||||
// these fixtures in the round-trip gate would block
|
||||
// 22b on a property the schema floor does not yet
|
||||
// Iter 22b.1 / 22b.2 / 22b.3: the `test_22b1_*`,
|
||||
// `test_22b2_*`, and `test_22b3_*` fixtures exercise
|
||||
// class/instance defs at the workspace-load and
|
||||
// monomorphisation levels, but the surface (Form-B)
|
||||
// parser/printer arms for those nodes are deferred
|
||||
// to 22b.4. Until that ships, including these
|
||||
// fixtures in the round-trip gate would block 22b
|
||||
// on a property the schema floor does not yet
|
||||
// promise. Skip them.
|
||||
n.ends_with(".ail.json")
|
||||
&& !n.starts_with("test_22b1_")
|
||||
&& !n.starts_with("test_22b2_")
|
||||
&& !n.starts_with("test_22b3_")
|
||||
})
|
||||
.unwrap_or(false)
|
||||
})
|
||||
|
||||
@@ -0,0 +1,55 @@
|
||||
{
|
||||
"schema": "ailang/v0",
|
||||
"name": "test_22b3_dup_call_sites",
|
||||
"imports": [],
|
||||
"defs": [
|
||||
{
|
||||
"kind": "class", "name": "Show", "param": "a",
|
||||
"methods": [{
|
||||
"name": "show",
|
||||
"type": {
|
||||
"k": "fn", "params": [{ "k": "var", "name": "a" }],
|
||||
"ret": { "k": "con", "name": "Str" }, "effects": []
|
||||
}
|
||||
}]
|
||||
},
|
||||
{
|
||||
"kind": "instance",
|
||||
"class": "Show",
|
||||
"type": { "k": "con", "name": "Int" },
|
||||
"methods": [{
|
||||
"name": "show",
|
||||
"body": {
|
||||
"t": "lam",
|
||||
"params": ["x"],
|
||||
"paramTypes": [{ "k": "var", "name": "a" }],
|
||||
"retType": { "k": "con", "name": "Str" },
|
||||
"body": { "t": "lit", "lit": { "kind": "str", "value": "n" } }
|
||||
}
|
||||
}]
|
||||
},
|
||||
{
|
||||
"kind": "fn",
|
||||
"name": "main",
|
||||
"type": {
|
||||
"k": "fn", "params": [], "ret": { "k": "con", "name": "Str" },
|
||||
"effects": []
|
||||
},
|
||||
"params": [],
|
||||
"body": {
|
||||
"t": "let",
|
||||
"name": "_a",
|
||||
"value": {
|
||||
"t": "app",
|
||||
"fn": { "t": "var", "name": "show" },
|
||||
"args": [{ "t": "lit", "lit": { "kind": "int", "value": 5 } }]
|
||||
},
|
||||
"body": {
|
||||
"t": "app",
|
||||
"fn": { "t": "var", "name": "show" },
|
||||
"args": [{ "t": "lit", "lit": { "kind": "int", "value": 7 } }]
|
||||
}
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"schema": "ailang/v0",
|
||||
"name": "test_22b3_no_call_sites",
|
||||
"imports": [],
|
||||
"defs": [
|
||||
{
|
||||
"kind": "class", "name": "Show", "param": "a",
|
||||
"methods": [{
|
||||
"name": "show",
|
||||
"type": {
|
||||
"k": "fn", "params": [{ "k": "var", "name": "a" }],
|
||||
"ret": { "k": "con", "name": "Str" }, "effects": []
|
||||
}
|
||||
}]
|
||||
},
|
||||
{
|
||||
"kind": "instance",
|
||||
"class": "Show",
|
||||
"type": { "k": "con", "name": "Int" },
|
||||
"methods": [{
|
||||
"name": "show",
|
||||
"body": {
|
||||
"t": "lam",
|
||||
"params": ["x"],
|
||||
"paramTypes": [{ "k": "var", "name": "a" }],
|
||||
"retType": { "k": "con", "name": "Str" },
|
||||
"body": { "t": "lit", "lit": { "kind": "str", "value": "n" } }
|
||||
}
|
||||
}]
|
||||
},
|
||||
{
|
||||
"kind": "fn",
|
||||
"name": "main",
|
||||
"type": {
|
||||
"k": "fn", "params": [], "ret": { "k": "con", "name": "Str" },
|
||||
"effects": []
|
||||
},
|
||||
"params": [],
|
||||
"body": { "t": "lit", "lit": { "kind": "str", "value": "no-call" } }
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user