← board

What

Three spellings, each leaking every string, frees=0:

spelling before after
Cish('lit' + c) with Cish(s: PChar); cdeclimplicit, no cast in source 921 3
p := 'lit' + c with p: PChar 921 3
Pointer('lit' + c + 'x') 921 4

A pointer destination retains nothing — a PChar/Pointer parameter keeps an address, a pointer variable stores a bare handle, a pointer cast is a tag change. The +1 belonged to nobody and was never a symbol, so no scope-exit scan could find it.

Why this is one bug and not three

Sixth, seventh and eighth instance of one shape: a lowering hands a fresh managed string to a consumer that keeps a RAW POINTER. The five before were 88e1ab536 (Variant→AnsiString, twice), f42665459 (array of const element) and 7cd695c7d (the explicit PChar() cast).

The common cause is that ownership is decided by asking the argument's AST SHAPE at seven call-argument sites (ParamWantsManagedStrTemp plus an AN_IDENT/AN_FIELD/AN_INDEX/AN_DEREF exclusion). That is sound reasoning about SOURCE and wrong about a LOWERING, and for a POINTER parameter it is worse than wrong: ParamWantsManagedStrTemp is False by construction, so none of the seven ever fire.

So this commit does the structural half as well: one named helper, IRParkManagedStr, and all eight sites call it. The implicit-parameter arm goes at IRLowerCallArg's single tail — the funnel every call argument passes, where the width conversion already lives — rather than at the seven sites.

No oracle row for two of the three

FPC rejects the implicit spellings: Cish('lit' + c) is "Incompatible type for arg no. 1: Got AnsiString, expected PChar" and p := 'lit' + c is "Incompatible types: got AnsiString expected PChar". Only the Pointer(...) cast compiles there. pxx accepts all three deliberately — it is what lets a C binding take a Pascal string without PChar() boilerplate — and accepting what FPC rejects is not a defect. It does mean the ownership rule for those spellings is ours to define, and dropping the +1 was still wrong.

Measured

test/test_string_to_pointer_seam_leaks.pas: 3138 → 6 against a bound of 50, on fa002ef63d15 vs the fixed binary. Rejected by the pre-fix binary (rc=1). Identical on x86-64/i386/aarch64/arm32/riscv32. Clean under -dPXX_HEAP_DEBUG.

allocs rises 4274 → 4809 rather than staying put, and that is the one number that is not "same traffic": the unowned value used to let a concat be dropped. 4809 matches what the named-intermediate spelling (t := 'lit' + c + 'x'; q := Pointer(t)) reads on its own, so 4809 is the honest count and 4274 was a second symptom.

Log