feb941363a
`(app print x)` under --alloc=rc leaked the heap-Str allocated by
`show x` in print's body: the let-binder `s` in
`(let s (app show x) (do io/print_str s))` was never flagged
trackable, so the drop site at scope-close emitted no
`ailang_rc_dec(s)`. Minimal repro `(body (app print 42))` produced
`allocs=1 frees=0 live=1`.
Root cause is two-layer:
1. examples/prelude.ail.json declared the Show class method `show`
without an explicit `ret_mode` on the return type, so serde
defaulted to ParamMode::Implicit. Fix: add `"ret_mode": "own"`
on the Show.show method, parallel to how the heap-Str-producing
builtins (int_to_str, bool_to_str, float_to_str, str_clone,
str_concat) declare it. examples/prelude.ail regenerated via
`ail render` to stay parse-isomorphic with the JSON.
2. crates/ailang-check/src/lib.rs `substitute_rigids` was silently
stripping `param_modes` and `ret_mode` whenever it rebuilt a
`Type::Fn`, so even after the prelude fix, the mono-synthesised
`show__Int / Bool / Str / Float` lost the `Own` annotation
during rigid substitution. Fix: preserve both fields through
the substitution.
RED pin: crates/ail/tests/print_no_leak_pin.rs (test
`alloc_rc_print_int_does_not_leak_show_result_str`) + fixture
examples/print_int_no_leak_pin.ail asserts allocs == frees and
live == 0 for `(body (app print 42))` under --alloc=rc.
Ten mono-body hash pins re-recorded (eq__Int/Bool/Str,
compare__Int/Bool/Str, show__Int/Bool/Str/Float, eq__IntBox) —
the bodies are semantically identical; the canonical JSON now
carries the previously-stripped mode metadata, so the body hash
moves. Re-pinning is bookkeeping for the intentional drift, not
a workaround.
Surfaced 2026-05-14 during the rpe.1 BLOCKED orchestrator run
(Cat A). Existed in latent form since iter 24.3 (when Show + print
shipped); only became user-observable when `(app print ...)` joined
the corpus. cargo test --workspace: 565 / 0 / 3.
Open follow-up flagged for next /audit: grep for `Type::Fn { ..., .. }`
field-spread sites — any other shape that drops modes during
rebuild is a latent instance of the same bug class. Out of scope
for this minimal fix.
110 lines
4.3 KiB
Rust
110 lines
4.3 KiB
Rust
//! Regression pin for primitive Eq/Ord mono-symbol body hashes.
|
|
//!
|
|
//! Locks the six primitive mono symbols (`eq__Int`, `eq__Bool`, `eq__Str`,
|
|
//! `compare__Int`, `compare__Bool`, `compare__Str`) to their pre-iter-23.4
|
|
//! body hashes. The unified mono pass MUST produce bit-identical bodies.
|
|
|
|
use ailang_core::ast::Def;
|
|
use ailang_core::hash::def_hash;
|
|
use ailang_surface::load_workspace;
|
|
use std::path::PathBuf;
|
|
|
|
fn fixture_path() -> PathBuf {
|
|
let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
d.pop();
|
|
d.pop();
|
|
d.join("examples").join("mono_hash_pin_smoke.ail")
|
|
}
|
|
|
|
fn show_fixture_path() -> PathBuf {
|
|
let mut d = PathBuf::from(env!("CARGO_MANIFEST_DIR"));
|
|
d.pop();
|
|
d.pop();
|
|
d.join("examples").join("show_mono_pin_smoke.ail")
|
|
}
|
|
|
|
#[test]
|
|
fn primitive_eq_ord_mono_symbol_hashes_stay_bit_identical() {
|
|
let ws = load_workspace(&fixture_path()).expect("workspace loads");
|
|
let diags = ailang_check::check_workspace(&ws);
|
|
assert!(diags.is_empty(), "typecheck diagnostics: {diags:?}");
|
|
let post_mono = ailang_check::monomorphise_workspace(&ws).expect("mono green");
|
|
|
|
// Mono symbols for class methods are appended to the instance's
|
|
// `defining_module` — i.e. the prelude module, since all six
|
|
// primitive Eq/Ord instances live there.
|
|
let prelude_mod = post_mono
|
|
.modules
|
|
.get("prelude")
|
|
.expect("prelude module present");
|
|
|
|
// 2026-05-14 bugfix-print-leak-show-ret-mode: hashes re-pinned
|
|
// after `substitute_rigids` started preserving `param_modes` and
|
|
// `ret_mode` through rigid substitution (was: hard-reset to
|
|
// Implicit / `[]`). Eq/Ord class methods declare
|
|
// `param_modes: ["borrow", "borrow"]` in `prelude.ail.json`, so
|
|
// the mono-synthesised `eq__T` / `compare__T` symbols now carry
|
|
// those modes in their `Type::Fn` — canonical-JSON bytes drift,
|
|
// bodies are semantically identical.
|
|
let pins: &[(&str, &str)] = &[
|
|
("eq__Int", "8bf7bec502487a57"),
|
|
("eq__Bool", "e46d7e6cd2ec459c"),
|
|
("eq__Str", "2a92935174182d2f"),
|
|
("compare__Int", "6d6c20520766368b"),
|
|
("compare__Bool", "02b64e8fadc913eb"),
|
|
("compare__Str", "9645929d53cd3cc9"),
|
|
];
|
|
|
|
for (sym, pin) in pins {
|
|
let def = prelude_mod
|
|
.defs
|
|
.iter()
|
|
.find(|d| matches!(d, Def::Fn(f) if f.name == *sym))
|
|
.unwrap_or_else(|| panic!("mono symbol {sym} not found in prelude module"));
|
|
let h = def_hash(def);
|
|
assert_eq!(&h, pin, "{sym} body hash drifted; captured: {h}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn primitive_show_mono_symbol_hashes_stay_bit_identical() {
|
|
// 24.2 hash-stability pin for the four primitive `show__T` mono
|
|
// symbols synthesised from `prelude.Show {Int|Bool|Str|Float}`
|
|
// instance bodies. Loaded from a separate fixture
|
|
// (`show_mono_pin_smoke.ail.json`) so the Eq/Ord pin above stays
|
|
// structurally isolated.
|
|
let ws = load_workspace(&show_fixture_path()).expect("workspace loads");
|
|
let diags = ailang_check::check_workspace(&ws);
|
|
assert!(diags.is_empty(), "typecheck diagnostics: {diags:?}");
|
|
let post_mono = ailang_check::monomorphise_workspace(&ws).expect("mono green");
|
|
|
|
let prelude_mod = post_mono
|
|
.modules
|
|
.get("prelude")
|
|
.expect("prelude module present");
|
|
|
|
// 2026-05-14 bugfix-print-leak-show-ret-mode: hashes re-pinned
|
|
// after the Show class method declaration in `prelude.ail.json`
|
|
// added `ret_mode: "own"` AND `substitute_rigids` started
|
|
// preserving `param_modes`/`ret_mode`. The mono-synthesised
|
|
// `show__T` symbols now carry `param_modes: ["borrow"]` and
|
|
// `ret_mode: Own` in their `Type::Fn` — bodies are semantically
|
|
// identical.
|
|
let pins: &[(&str, &str)] = &[
|
|
("show__Int", "9229ce65fdf11f61"),
|
|
("show__Bool", "ade621119184351f"),
|
|
("show__Str", "93fb52834427598c"),
|
|
("show__Float", "49874c7ed31c9d15"),
|
|
];
|
|
|
|
for (sym, pin) in pins {
|
|
let def = prelude_mod
|
|
.defs
|
|
.iter()
|
|
.find(|d| matches!(d, Def::Fn(f) if f.name == *sym))
|
|
.unwrap_or_else(|| panic!("mono symbol {sym} not found in prelude module"));
|
|
let h = def_hash(def);
|
|
assert_eq!(&h, pin, "{sym} body hash drifted; captured: {h}");
|
|
}
|
|
}
|