bugfix: print leak — propagate ret_mode through rigid substitution
`(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.
This commit is contained in:
@@ -0,0 +1,85 @@
|
||||
# iter bugfix-print-leak-show-ret-mode — print() leaks heap-Str under --alloc=rc; Show class method's ret_mode stripped twice
|
||||
|
||||
**Date:** 2026-05-14
|
||||
**Started from:** 301cbc33a0b2fed18f465c6c17041211ff7da35e
|
||||
**Status:** DONE
|
||||
**Tasks completed:** 1 of 1
|
||||
|
||||
## Summary
|
||||
|
||||
Two-layer bug: (i) the `Show` class method declaration in
|
||||
`examples/prelude.ail.json` omitted the `ret_mode` key, so serde
|
||||
defaulted it to `ParamMode::Implicit`; (ii) even with `ret_mode: own`
|
||||
declared, `substitute_rigids` in `crates/ailang-check/src/lib.rs`
|
||||
was hard-resetting `param_modes` to `vec![]` and `ret_mode` to
|
||||
`Implicit` in its `Type::Fn` arm — silently stripping the mode
|
||||
metadata during class-method mono synthesis (`show__Int`, `eq__T`,
|
||||
`compare__T`, user-instance `eq__<UserType>`, etc.). Codegen's
|
||||
`is_rc_heap_allocated` App-arm requires the callee's `ret_mode == Own`
|
||||
to mark the let-binder trackable, so print's `let s = show x` binder
|
||||
was never `ailang_rc_dec`'d at let-scope-close. The fix declares
|
||||
`ret_mode: "own"` on the Show class method (parallel to the
|
||||
`int_to_str`/`bool_to_str`/`float_to_str`/`str_clone`/`str_concat`
|
||||
builtins which already carry `ret_mode: Own`) and repairs
|
||||
`substitute_rigids`'s `Type::Fn` arm to clone `param_modes` and
|
||||
preserve `ret_mode` through rigid substitution. The Form-A sibling
|
||||
`examples/prelude.ail` is regenerated via `ail render` to stay in
|
||||
lock-step per spec §C4 (b). Ten pinned mono-body hashes
|
||||
(`eq__Int/Bool/Str`, `compare__Int/Bool/Str`, `show__Int/Bool/Str/Float`,
|
||||
plus `eq__IntBox`) are re-pinned to the new GREEN baselines because
|
||||
the mono bodies now carry the previously-stripped `param_modes` /
|
||||
`ret_mode` metadata; bodies are semantically identical.
|
||||
|
||||
## Per-task notes
|
||||
|
||||
- bugfix.1: applied the fix.
|
||||
- `examples/prelude.ail.json`: Show class method now has
|
||||
`"ret_mode": "own"`.
|
||||
- `examples/prelude.ail`: regenerated via
|
||||
`cargo run -p ail -- render examples/prelude.ail.json
|
||||
> examples/prelude.ail`; round-trips parse-isomorphic.
|
||||
- `crates/ailang-check/src/lib.rs` `substitute_rigids` Type::Fn
|
||||
arm: pattern-match all five fields explicitly; clone
|
||||
`param_modes` and pass `*ret_mode` through (`ParamMode: Copy`).
|
||||
Comment cites the bugfix iter id and the cause.
|
||||
- `crates/ail/tests/mono_hash_stability.rs`: re-pinned 10 hashes
|
||||
across the Eq/Ord and Show test fns; both bodies now report
|
||||
`captured: {h}` on assertion failure (the Eq/Ord arm was
|
||||
missing the captured-suffix; small in-place tidy folded into
|
||||
the re-pin since the assertion-message string was being
|
||||
rewritten anyway).
|
||||
- `crates/ail/tests/eq_ord_e2e.rs`: re-pinned `eq__IntBox`
|
||||
body hash to `3c4cf040cb4e8bb2`.
|
||||
|
||||
## Concerns
|
||||
|
||||
The debugger's cause analysis (carrier) pointed only at the JSON
|
||||
omission. The JSON-only fix leaves the test RED — `substitute_rigids`
|
||||
was silently stripping modes too. Suggest: at the next /audit pass,
|
||||
verify there are no *other* call sites that build a `Type::Fn` with
|
||||
explicit `..` field-spread and end up dropping modes. The pattern
|
||||
`Type::Fn { ..., .. }` is the smell.
|
||||
|
||||
## Known debt
|
||||
|
||||
None — the fix is local. The wider question of "should `Type::Fn`'s
|
||||
mode fields be field-init-by-name everywhere to prevent this class
|
||||
of bug at the compiler level" is out of scope for this minimal fix.
|
||||
|
||||
## Files touched
|
||||
|
||||
- `examples/prelude.ail.json` — Show class: added `ret_mode: own`.
|
||||
- `examples/prelude.ail` — regenerated Form-A sibling.
|
||||
- `crates/ailang-check/src/lib.rs` — `substitute_rigids` Type::Fn
|
||||
arm: preserve `param_modes` + `ret_mode`.
|
||||
- `crates/ail/tests/mono_hash_stability.rs` — re-pinned 10 hashes
|
||||
(6 Eq/Ord + 4 Show); added cause comments.
|
||||
- `crates/ail/tests/eq_ord_e2e.rs` — re-pinned `eq__IntBox` hash.
|
||||
- `crates/ail/tests/print_no_leak_pin.rs` (from /debug) — RED-pin
|
||||
for the leak; now GREEN.
|
||||
- `examples/print_int_no_leak_pin.ail` (from /debug) — fixture for
|
||||
the RED-pin; now GREEN.
|
||||
|
||||
## Stats
|
||||
|
||||
bench/orchestrator-stats/2026-05-14-iter-bugfix-print-leak-show-ret-mode.json
|
||||
Reference in New Issue
Block a user