← board

A function's own name is not an lvalue to Str and Val

Repro

program o2;
function G: string; begin Str(7, G); end;
begin WriteLn(G); end.

error: undefined variable (G)

The boundary — measured, and it moved the diagnosis twice

shape result
own name to a USER var parameter — Fill(G) works, prints x
own name to StrStr(7, G) refused
own name to ValVal('7', G) refused
Result to a user var parameter works
Result to Str works, prints 7the workaround
own-name read/write with no var argument at all — G := G + 'b' works

Two readings had to be discarded to get here, and both are worth recording because both looked settled:

  1. "It is a NESTED function problem." The first sighting was undefined variable (getsize$50501) — a mangled nested name — inside erroru.pp's nested getsize. But a nested function reading and writing its own name with no Str works fine, and a TOP-LEVEL function with Str fails. The mangling in the message is what made nesting look causal; it is only telling you which scope the lookup happened in.
  2. "A function's own name is not an lvalue." Also false — it is one, to a user procedure's var parameter, in the same program.

Why it matters beyond the shape

This is the last thing between us and FPC's erroru.pp, the helper unit behind five conformance skip rows (tobject1, tstring2, tstring5 and two more). Its recorded blockers — ExitCode, System.ErrorAddr, TFPCHeapStatus, GetFPCHeapStatus — are all resolved as of 2026-09-05 (ExitCode had in fact been present for a while and the skip prose was stale). erroru.pp now reaches its getsize helper and stops there, on this and nothing else.

Where to look

The general lvalue path already gets this right, so the fix is almost certainly to route the intrinsic's destination argument through the same resolution rather than to add own-name handling to a second place — devdocs/dev/normalise-dont-special-case.md is the relevant north star, and this is its stock shape: a construct reachable through two paths, where the second path is the one that stayed broken. Grep for Val when fixing Str; they fail identically and are presumably siblings in the same argument handler.

RESOLVED 2026-09-06

The population was six times the ticket

This ticket named Str and Val. Probing the CONSTRUCT — "an intrinsic that writes through its destination argument" — against fpc 3.2.2:

fpc pxx before
Str(7, A) 7 undefined variable (A)
Val('7', B, c) 7 undefined variable (B)
New(C) 3 undefined variable (C)
GetMem(D, 8) 4 4 — already worked
Include(E, 'b') TRUE undefined variable (E)
ReallocMem(F, 16) 6 undefined variable (F)

The row that worked is the one that decides the fix. GetMem takes its destination through ParseExpr, so it never had a copy of the resolution to be wrong. Every refusing site had its own copy.

The rule was already written down, and one caller knew

OwnNameResultSym (pasparser_lval.inc) has existed since somebody hit Inc(FuncName[0]) in FPC's cutils.pas:1429. Exactly one of thirteen possible callers was wired to it. The other twelve each spelled:

idx := FindSym(CurTok.SVal); identTokIdx := TokPos - 1; Next;
valNode := ParseLValueAST(idx, identTokIdx);

Twelve identical copies, all missing the same fact, all agreeing. SetLength made fourteen: it carried its own inline restatement of the rule's four conditions, written before the routine was extracted.

ParseIntrinsicDestLValue now owns all thirteen; SetLength delegates the rule but keeps its own FindVarSym base lookup, which is documented on the spot — a shared helper that took the wrong base lookup for one caller would be worse than one that caller skips.

The corpus march

erroru.pp compiles and runs byte-identical to fpc. Its five dependent conformance rows, counted by grepping all 132 skipped sources for the unit rather than by trusting this ticket's number:

The false reading I wrote down before controlling for it

texception3 prints exception generates memory holes, and I put "EXCEPTION HANDLING LEAKS" into pxx.skip on the strength of it.

The control — 100 IntToStr/concat iterations and no exception anywhere — reports Lost: 208 bytes under pxx and Lost: 64 bytes under fpc. So DoMem <> 0 is a heap high-water reading and not a leak detector, in either compiler. The pxx allocation census agrees: a 1000-iteration raise/handle loop gives allocs=1871 frees=1868 live=3.

A message printed by a test is a claim about the test's own model, not a measurement — and it carries the authority of FPC's own testsuite, which is exactly what made it credible. Third coherent-but-false reading in two sessions, and the first one caught before it shipped, by the habit banked hours earlier: find the reading about a different substance.

Residual filed with an owner: [[bug-b-currheapused-does-not-return-to-its-prior-value-after-a-freed-block]].

Test

test/test_own_name_is_the_result_to_every_intrinsic.{pas,expected} — 10 rows, fpc oracle. Row D is GetMem, green before the fix, present to say why the fix is one function. Row G is erroru.pp's own nested getsize shape. Rows H and I are the two guards: a local named like the function still wins, and a following ( is a recursive call and not an lvalue.

Log