← board

A METHOD returning a freshly built string leaks it; the same def does not

A method whose result is a managed string leaks roughly 32 bytes per call when the returned value is FRESH (a literal, a str(...), a concatenation). Returning a FIELD — a borrow — is flat, and a plain top-level def returning the same fresh string is flat too. So the divergence is method-vs-def, not string-vs-object.

Measured (RSS slope, 20k vs 320k calls)

body of label(self) -> str 20k 320k
return "lit" 0.94 MB 10.3 MB
return str(self.v) 0.94 MB 10.3 MB
return self.k (field, a borrow) 0.30 MB 0.30 MB — flat
same body as a plain def label(k, v) -> str 0.30 MB 0.30 MB — flat

Driver holds ONE receiver outside the loop and calls len(x.label()), so the receiver's own lifetime is not in the measurement.

First measurement — the two bodies' IR, side by side

Same program, return "lit" in both:

Node.label                              flabel
0: zero_sym  a=264            tk=23     (none)
1: const_str a=41 b=3         tk=4      0: const_str a=53 b=3   tk=4
2: store_sym a=264 b=1        tk=4      1: store_sym a=263 b=0  tk=4

tk=23 is tyAnsiString (managed), tk=4 is tyString (frozen). The method's $pyresult is a MANAGED slot — it gets the prologue zero_sym — and the frozen literal is then stored into it with a raw store_sym, not through the ARC assign path. The def's $pyresult gets no zero_sym at all, i.e. the two paths do not agree on the result slot's storage class. That disagreement, not the literal, is the thing to chase.

Where to look

The method body path allocates $pyresult in a different place from the def path (pyparser.inc: the method arm uses Procs[procIdx].RetType, the def arm uses the header's retType), and the two differ in how a managed result is retained/released. Diff the two allocation sites first, and dump the emitted IR of both bodies (PXXDBG=a.ir:Node.label vs PXXDBG=a.ir:label) before theorising — the def version is the known-good oracle here, which is what makes this cheap to localise.

Found while measuring [[bug-nilpy-object-reclamation-disabled-inside-py-modules]]; independent of it (reproduces on the main-program path with the pre-fix compiler).

Gate

make test-nilpy + self-host byte-identical, plus the table above going flat.

Log