From 3b0bcf3f65db75e00763620e5a24a365e36ebbe2 Mon Sep 17 00:00:00 2001 From: Brummel Date: Sun, 10 May 2026 01:06:18 +0200 Subject: [PATCH] test: red for mono-pass unknown identifier on recursive fn in class workspace --- crates/ail/tests/mono_recursive_fn.rs | 73 ++++++++++++ examples/test_mono_recursive_fn_bug.ail.json | 111 +++++++++++++++++++ 2 files changed, 184 insertions(+) create mode 100644 crates/ail/tests/mono_recursive_fn.rs create mode 100644 examples/test_mono_recursive_fn_bug.ail.json diff --git a/crates/ail/tests/mono_recursive_fn.rs b/crates/ail/tests/mono_recursive_fn.rs new file mode 100644 index 0000000..13e4e5f --- /dev/null +++ b/crates/ail/tests/mono_recursive_fn.rs @@ -0,0 +1,73 @@ +//! Bug regression: the workspace-monomorphisation pass must seed +//! `env.globals` for the current module before re-running `synth` on +//! a fn body, otherwise self-recursive top-level fns are rejected with +//! `unknown identifier` whenever the workspace also contains a +//! `Def::Class` (which makes the mono pass actually traverse bodies +//! instead of identity-cloning the workspace). +//! +//! Surfaced by the 2026-05-10 mono-vs-vdisp micro-benchmark: the +//! original bench fixture had a self-recursive `loop` fn alongside a +//! `Foo` class, and `ail build` failed at the mono pass with +//! `monomorphise_workspace: unknown identifier: \`loop\``. The bench +//! worked around it by also wrapping the loop in a class method, +//! masking but not fixing the defect. + +use std::path::Path; +use std::process::Command; + +fn ail_bin() -> &'static str { + env!("CARGO_BIN_EXE_ail") +} + +/// Property: a workspace that mixes a `Def::Class` / `Def::Instance` +/// with a self-recursive top-level `Def::Fn` (one whose body refers +/// to its own name) must build and run end-to-end. The mono pass +/// re-runs `synth` on every fn body to recover residual class +/// constraints; that env must agree with the env the main typecheck +/// path used, including `env.globals` for the current module. +/// +/// Pre-fix: `ail build` fails at the mono pass with +/// `monomorphise_workspace: unknown identifier: \`loop\`` because +/// `mono::build_workspace_env` populates `env.module_globals` but +/// never seeds `env.globals` from the current module's fns — +/// `synth`'s `Term::Var` lookup at `lib.rs:1678` falls through to +/// the qualified-cross-module branch and finally to `UnknownIdent`. +/// The analogous gap for ADTs / ctors was fixed in commit 5c5180f +/// (iter 22c); this is the sibling gap for plain top-level fns. +#[test] +fn mono_pass_handles_recursive_fn_in_class_workspace() { + let manifest_dir = env!("CARGO_MANIFEST_DIR"); + let workspace = Path::new(manifest_dir).parent().unwrap().parent().unwrap(); + let src = workspace + .join("examples") + .join("test_mono_recursive_fn_bug.ail.json"); + let tmp = std::env::temp_dir().join(format!( + "ailang_mono_recursive_fn_{}", + std::process::id() + )); + std::fs::create_dir_all(&tmp).unwrap(); + let out = tmp.join("bin"); + + let build = Command::new(ail_bin()) + .args(["build", src.to_str().unwrap(), "-o"]) + .arg(&out) + .output() + .expect("ail build failed to run"); + assert!( + build.status.success(), + "ail build failed: stderr={}", + String::from_utf8_lossy(&build.stderr) + ); + + let run = Command::new(&out).output().expect("execute binary"); + assert!( + run.status.success(), + "binary {} exited non-zero", + out.display() + ); + // loop(0, 5) = 0 + 5 + 4 + 3 + 2 + 1 = 15 + assert_eq!( + String::from_utf8(run.stdout).expect("stdout utf8").trim(), + "15" + ); +} diff --git a/examples/test_mono_recursive_fn_bug.ail.json b/examples/test_mono_recursive_fn_bug.ail.json new file mode 100644 index 0000000..1859501 --- /dev/null +++ b/examples/test_mono_recursive_fn_bug.ail.json @@ -0,0 +1,111 @@ +{ + "schema": "ailang/v0", + "name": "test_mono_recursive_fn_bug", + "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": ["x"], + "paramTypes": [{ "k": "var", "name": "a" }], + "retType": { "k": "con", "name": "Int" }, + "body": { "t": "var", "name": "x" } + } + } + ] + }, + { + "kind": "fn", + "name": "loop", + "type": { + "k": "fn", + "params": [{ "k": "con", "name": "Int" }, { "k": "con", "name": "Int" }], + "ret": { "k": "con", "name": "Int" }, + "effects": [] + }, + "params": ["acc", "i"], + "body": { + "t": "if", + "cond": { + "t": "app", + "fn": { "t": "var", "name": "==" }, + "args": [ + { "t": "var", "name": "i" }, + { "t": "lit", "lit": { "kind": "int", "value": 0 } } + ] + }, + "then": { "t": "var", "name": "acc" }, + "else": { + "t": "app", + "tail": true, + "fn": { "t": "var", "name": "loop" }, + "args": [ + { + "t": "app", + "fn": { "t": "var", "name": "+" }, + "args": [ + { "t": "var", "name": "acc" }, + { "t": "var", "name": "i" } + ] + }, + { + "t": "app", + "fn": { "t": "var", "name": "-" }, + "args": [ + { "t": "var", "name": "i" }, + { "t": "lit", "lit": { "kind": "int", "value": 1 } } + ] + } + ] + } + } + }, + { + "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": "loop" }, + "args": [ + { "t": "lit", "lit": { "kind": "int", "value": 0 } }, + { "t": "lit", "lit": { "kind": "int", "value": 5 } } + ] + } + ] + } + } + ] +}