Close Implicit on returns so heap-returning fns stop leaking #54
Reference in New Issue
Block a user
Delete Branch "%!s()"
Deleting a branch is permanent. Although the deleted branch may continue to exist for a short time before it actually gets removed, it CANNOT be undone in most cases. Continue?
Symptom
A fn whose return type is an RC-heap value (
Str, or a boxed ADT) butwhose signature omits the return mode carries
ret_mode == ParamMode::Implicit— the parser's silent default, which nothinginfers post-parse. Codegen reads
Implicitfaithfully: the caller doesnot
decthe returned slab, so the owned allocation leaks(
live = 1underAILANG_RC_STATS).This is currently documented as intentional under the
Implicitback-compat lane:
examples/rc_let_implicit_returning_app.ailassertslive = 1asthe correct outcome (see its header comment).
design/contracts/0008-memory-model.md("ret_mode—let-binder trackability"): an
Implicit-returning call isnon-trackable for scope-close drop, by contrast with an
Own-returning call.crates/ailang-codegen/src/drop.rs:469— onlyret_mode == Ownhands a fresh heap allocation to the caller andenables the dec.
The leak is the symptom; the cause is that an ownership default exists
on the return position at all.
ParamMode::Implicitis treated asOwnby the typechecker (mode_eq,crates/ailang-core/src/ast.rs)yet read separately by codegen, which omits the caller
dec— the samefn-type read two ways.
Minimal fix
Scoped deliberately below the full ownership-totality direction in
design/models/0008-ownership-totality.md, which a critical designreview on 2026-06-01 found is not yet shippable as one atomic cutover
(borrowed returns are refcount-invisible and can outlive their source —
a use-after-free that needs a liveness/escape axis the model does not
yet have; see §1 and §8 of that whitepaper):
Impliciton the return position: requireownorborrowon every fn return. Parameter-side modes and value-typeuniformity stay out of scope here.
ownreturn needs only first-order freshness verification("is this return actually fresh?"), not the reachability /
liveness pass.
borrowreturns in this first cut. They are thepositions that need the unresolved liveness/escape axis (a
borrowed return aliasing a parameter can dangle once the caller
drops the source; RC does not back-stop it because a borrow does
no inc/dec). Re-enabling them is separate, later work.
Cost flagged
Closing
Impliciton returns shifts the canonical JSON hash of everyfixture whose return mode was defaulted (
ret_modeis omitted from thehash only while
Implicit—skip_serializing_if = "is_implicit",crates/ailang-core/src/ast.rs). The hash-pin tests reset once. Thisis the one irreversible step and is signed off deliberately.
Acceptance
checkerror, not asilent leak.
examples/rc_let_implicit_returning_app.ailis migrated to anexplicit
ownreturn and assertslive = 0.borrowreturn is rejected with a diagnostic naming theliveness gap.
Closing: this targets the symptom (the return-position leak), not the
cause. Replaced by #55.
The whitepaper section 2 (design/models/0008-ownership-totality.md) names
the cause: an ownership default exists at all. #54 removes
Implicitonly on the return position and explicitly leaves it on parameters —
which is exactly the partial default-removal section 2 calls incoherent
("the same default surviving on the input side").
Two substantive problems, beyond incompleteness:
Two irreversible hash resets. The canonical-JSON hash shifts once
when
Implicitleaves a serialized position (section 6: "the onegenuinely irreversible consequence"). #54 spends that reset on returns
now; closing the parameter side later — the rest of totality — spends
a second one. One reset suffices for the whole surface.
It defers the already-finished half. The parameter side is
already verified —
consume-while-borrowed,over-strict-mode, andper-param
consume_count(crates/ailang-check) police it today, andsection 5 notes the param mode is compiler-derivable from that
analysis. There is no soundness reason to hold parameters out of the
cutover; only the "returns are the hard half" framing pushed #54 to
exclude the easy, done half.
#55 keeps #54's one correct move (forbid borrow-returns behind the
unbuilt liveness axis) and drops the rest: it deletes
Implicitas aconcept in one hash reset, instead of patching it per-position in two.