iter 22b.3.7: synthetic fixture — class+instance compiles, runs, prints 5

This commit is contained in:
2026-05-09 21:28:41 +02:00
parent 7f260b82ad
commit b59820b737
3 changed files with 152 additions and 41 deletions
+89 -37
View File
@@ -60,7 +60,7 @@ fn monomorphise_workspace_is_identity_on_class_free_workspace() {
/// 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`.
/// (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 <body>`.
@@ -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");
}
+13 -4
View File
@@ -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 `<method>#<surface-name>` for diagnostic and
/// produce `<method>__<surface-name>` for diagnostic and
/// ABI legibility. All other types — parameterised cons,
/// user-defined ADTs, function types — fall to
/// `<method>#<8-hex-prefix-of-canonical-type-hash>`. The hash
/// `<method>__<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
@@ -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 } }]
}]
}
}
]
}