A member read off a CONSTRUCTOR result silently yields garbage
- Type: bug — Track P (Pascal frontend, shared
compiler/parser.inc) - Status: done
- Opened: 2026-08-05
- Found by: writing
test/test_procedure_as_value_ok.pas. The line meant to prove constructors stay usable as values printed junk, which is how this surfaced at all. - Pre-existing: reproduced identically with the pinned stable
(
stable_linux_amd64/default, VERSION 243), so it is not a regression from the no-result-call check landed the same night.
Repro
program mem;
type
TThing = class
n: Integer;
constructor Create(k: Integer);
function Val: Integer;
end;
constructor TThing.Create(k: Integer); begin n := k; end;
function TThing.Val: Integer; begin Result := n; end;
function Make(k: Integer): TThing; begin Result := TThing.Create(k); end;
var a, b, c, d: Integer;
begin
a := TThing.Create(2).n;
b := TThing.Create(3).Val;
c := Make(4).n;
d := Make(5).Val;
writeln(a, '|', b, '|', c, '|', d);
end.
| output | |
|---|---|
| FPC | 2|3|4|5 |
| pxx | -801112056|-801112032|4|5 |
What the split says
Make(4).n and Make(5).Val — a member off an ordinary function result —
are correct. Only the constructor result is wrong, for both a field and
a method. So the machinery for "bind a member to a call result" works; the
constructor path is not producing (or not keeping) the instance pointer the
member access then reads through.
Two more facts worth having before touching it:
- The same expression in a different position is a parse error, not garbage:
writeln(TThing.Create(2).Val)givesExpected: ), but got: ... unexpected token. Two positions, two different wrong answers, which points at the postfix-after-a-call handling being duplicated per position rather than shared — the same smell as [[compat-pascal-index-a-function-call-result]], which is that family's parse-error half. t := TThing.Create(10); t.nis fine. Only the un-named intermediate is lost.
Relationship to the neighbours
- [[compat-pascal-index-a-function-call-result]] —
Copy(s,2,3)[1],b.ArrP(3)[0]. Same "postfix off a call result" family, but those fail LOUDLY (parse error / IR_UNSUPPORTED). This one is silent, which is why it is filed as abug-in its own right per CLAUDE.md's escape rule rather than folded into the compat ticket.
Root cause (measured, FIXED)
PXXDBG=a.ir:<proc> on the two shapes side by side said it in six lines:
ViaFunc (Make(4).n, correct) ViaCtor (TThing.Create(2).n, wrong)
2: call a=90 4: call a=-45 (-Ord(tkGetMem))
3: field a=2 ival=8 [offset=8] 5: store_sym [sym=Result]
4: load_mem a=3
5: store_sym [sym=Result]
The field + load_mem pair is simply absent. The expression-position
constructor branch in ParseFactorCore built the GetMem node and did
CurASTNode := node; Exit without ever looking at what followed, so the
selector chain was dropped on the floor and the store received the instance
POINTER. No reasoning needed once the IR was printed — this is the
debugging-playbook case exactly.
The fix is the ParseClassRecordSelectors call every other class-valued path
already makes, at that exit.
One trap on the way in: idx there is already a full rec id
(REC_UCLASS_BASE + ci, assigned a few lines above with the arity check),
despite ASTRight's "preserve created user-class id" comment reading like a
bare class index. Passing REC_UCLASS_BASE + idx lands on an unrelated record
and every member comes back "no such member".
Verification
2|3|4|5, = FPC. Also correct now, all previously broken or unparseable:
writeln(TThing.Create(2).Val) (this was the parse-error half),
TThing.Create(4).Twice + 1, TThing.Create(5).ClassName,
TStringList.Create.Count.
- self-host fixedpoint converged in one round
tools/gate.sh quickGREEN incl. the FPC seed canary,tools/gate.sh libGREENmake demos34/34 with the changed compilertools/fpc_diff_probe.shfiring[known]set byte-identical to the pre-change run; themember-off-a-constructor-resultcase is untagged and must stay greentest/test_ctor_result_member.paswired intomake test
Not fixed by this
[[compat-pascal-index-a-function-call-result]] — Copy(s,2,3)[1] and Make[1]
still do not parse, and b.ArrP(3)[0] still hits IR_UNSUPPORTED. Those are the
loud half of the family and a different code path.
Gate
Track P: make test + self-host fixedpoint (byte-identical).
Log
- 2026-08-05 — resolved, commit 8464f0a9f.