fix: reclaim the tuple-literal argument box, and tag nested argument literals - #810
Merged
Conversation
…literals The last two measured leaks from docs/ownership.md's argument family. A tuple literal written straight into a call — `f((a, b))` — stranded one box per call (~64 bytes): the kind alone cannot justify a release, since an optional and a result lower to the same shell, but a SYNTACTIC `(a, b)` in the source is unambiguous — the box is one only this call ever saw, and a storing callee takes its own count. The owned-argument release now fires for exactly that node shape; every other tuple-typed register keeps the old bounded leak rather than risking a double free. A nested empty literal — `f([[]])` — left the inner list untagged (~30 bytes a round once strings were pushed into it): a literal WITH elements types itself, but an empty literal nested inside leaves the whole thing error-typed, and an error-typed argument skipped the coercion path the mismatch check would have run. The propagation helper now runs collection_literal_coerces for that shape, which validates the elements honestly and records the declared type through every nested literal. A genuinely wrong element still fails the coercion and leaves the literal exactly as it was. docs/ownership.md updates both entries. ## what was tested - probes at 300k rounds, before -> after (peak rss): f((i, i+1)) 20.8mb -> 2.2mb; f([[]]) with a string push per round 11.2mb -> 2.2mb; outputs identical before and after - new tests/cases/test_argument_literal_ownership.pith golden: the released tuple, a callee that STORES the tuple and reads it back after the caller-side release (no dangle), the tagged nested empty literal, and a nested literal with elements - new tests/leaks/leak_argument_literal gate case: flat at 200k vs 800k rounds (2656 vs 2696 kb peak) - make bootstrap-verify: bootstrap ir fixed point verified (includes self regressions + std tests) - 8 consecutive make sitegen-check runs clean (the over-release gate) - make check-invalid-only: 51 passed, 0 failed; fmt + lint clean; bootstrap seed regenerated
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
The last two measured leaks from docs/ownership.md's argument-position family, both probe-confirmed live on main before fixing.
the tuple-literal argument box (~64 B/call)
f((a, b))stranded one box per call. The kind alone can never justify a release — an optional and a result lower to the same tuple shell, so a tuple-TYPED register might be a wrapper whose count the extraction paths still hand on — but a syntactic(a, b)in the source is unambiguous: the box is one only this call ever saw, and a storing callee takes its own count. The owned-argument release (ir_release_owned_string_operand) now fires for exactly that node shape; any other tuple-typed register keeps the old bounded leak rather than risking a double free. (The first cut of this gated onir_rc_kindreturning empty for "tuple" — it doesn't, it returns "tuple", which would have released every wrapper in argument position. Caught before it ever built; the landed gate is on the AST node kind inside the caller.)the nested argument literal (
f([[]]), ~30 B/round)A literal WITH elements types itself, but an empty literal nested inside leaves the whole expression error-typed — and an error-typed argument skips the coercion path the type-mismatch check would have run, so the inner list stayed untagged and never owned the strings pushed into it.
propagate_empty_argument_collection_typenow runscollection_literal_coercesfor that shape: the element walk qualifies the literal honestly and records the declared type through every nested literal. A genuinely wrong element still fails the coercion and reports as before.docs/ownership.md updates both entries; with these merged, every leak that file documented with a measured per-round figure in the argument family is closed.
what was tested
f((i, i+1))20.8 MB → 2.2 MB;f([[]])with a string push per round 11.2 MB → 2.2 MB; outputs identical before and aftertests/cases/test_argument_literal_ownership.pithgolden: the released tuple, a callee that stores the tuple into a struct and reads it back after the caller-side release (no dangle), the tagged nested empty literal, and a nested literal with elementstests/leaks/leak_argument_literalgate case, measured with the gate's own sequence: flat at 200k vs 800k rounds (2656 vs 2696 kb peak)make bootstrap-verify: bootstrap ir fixed point verified (includes self regressions + std tests)make sitegen-checkruns clean — the golden-output gate that catches over-releasesmake check-invalid-only: 51 passed, 0 failed; fmt + lint clean; bootstrap seed regenerated