iter 22b.3.7: synthetic fixture — class+instance compiles, runs, prints 5
This commit is contained in:
@@ -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");
|
||||
}
|
||||
|
||||
@@ -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 } }]
|
||||
}]
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
Reference in New Issue
Block a user