Skip to content

fix: reclaim the tuple-literal argument box, and tag nested argument literals - #810

Merged
kacy merged 1 commit into
mainfrom
tag-nested-argument-literals
Aug 19, 2026
Merged

fix: reclaim the tuple-literal argument box, and tag nested argument literals#810
kacy merged 1 commit into
mainfrom
tag-nested-argument-literals

Conversation

@kacy

@kacy kacy commented Aug 19, 2026

Copy link
Copy Markdown
Owner

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 on ir_rc_kind returning 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_type now runs collection_literal_coerces for 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

  • probes at 300k rounds, before → after (peak RSS): 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 after
  • new tests/cases/test_argument_literal_ownership.pith golden: 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 elements
  • new tests/leaks/leak_argument_literal gate 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)
  • 8 consecutive make sitegen-check runs clean — the golden-output gate that catches over-releases
  • make check-invalid-only: 51 passed, 0 failed; fmt + lint clean; bootstrap seed regenerated

…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
@kacy
kacy merged commit 74229fb into main Aug 19, 2026
1 of 2 checks passed
@kacy
kacy deleted the tag-nested-argument-literals branch August 19, 2026 06:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant