← board

Member access on a procedural-variable call result is rejected

Measured

One program, three shapes, one run (2026-09-04, x86-64):

type TR = record a, b, c: Integer; end;
     TF = function(k: Integer): TR;
function Plain(k: Integer): TR;      { .a := k; .b := k*2; .c := k*3 }
type TB = class function M(k: Integer): TR; virtual; end;
var fp: TF; b: TB;
expression result
Plain(8).c 24 — accepted
b.M(8).c 308 — accepted
fp(8).c pascal26:8: error: expected ')' before '.'

So it is not member-access-on-a-temporary that is missing, and it is not indirect dispatch either — a virtual call is indirect and works. It is specifically the procedural-variable call.

Why

ApplyCallResultPtrSuffix (pasparser_lval.inc:4918) is, by its own forward declaration, "the ONE materialisation point for a suffix on a call RESULT". Its first statement is tk := Procs[procIdx].RetType — it takes a real proc index, and every one of its callers has one. A procedural-variable call has a signature proc (the IR carries it in IRIVal for IR_CALL_IND), but the AN_CALL_IND construction sites do not call the helper at all.

Why this is filed and not fixed

AllocNode(AN_CALL_IND) appears at FIVE sites in pasparser_lval.inc (72, 636, 809, 1811, 3024) plus one in pasparser_stmt.inc. Adding the suffix walk at the site that happens to produce the reported failure is precisely the second path that stays broken — the other four would each need finding again later, by someone reducing a different symptom.

The shape of the fix is the same one normalise-dont-special-case.md prescribes and that this helper's own comment already claims to be: make the materialisation point reachable from the indirect sites too, once, rather than five times. Whoever takes it should establish first whether all five sites have a usable signature proc in hand at that moment, because that is what decides between "call the existing helper" and "give the helper a signature-only entry point".

Guard when it closes

test/test_cross_indirect_aggregate_return.pas carries the row commented out with this slug beside it (fp(7).c). Un-comment it; the file is already wired on all six cross targets, so the row lands everywhere at once.

Log


2026-09-04 (frankH) — banked: why no signature-only entry point is needed, MEASURED rather than read

Banked at frankuser's request before going idle. It was a reading when it went into a peer message; it is a measurement now, and the two greps that settle it cost less than writing the caveat would have.

1. Every AN_CALL_IND construction site already carries its signature. All 18 AllocNode(AN_CALL_IND) sites in the tree set ASTIVal := <signature proc index> within three lines of the allocation — there is no site that builds the node and leaves the signature to be recovered later:

file sites field assigned
pasparser_lval.inc 5 (74, 667, 840, 1842, 3055) sigPi / fldSigPi
pasparser_stmt.inc 1 (7681) pvSig
cparser.inc 4 SymProcSig[idx] / sig
pyparser.inc 7 sigPi / fldSigPi
ir.inc 1 (3837) sig

The five in pasparser_lval.inc are the ones named in the boundary with frankA. The sixth Pascal site is in pasparser_stmt.inc and is easy to miss when the boundary is quoted as "the five".

2. ApplyCallResultPtrSuffix needs nothing from a signature but its columns. Every use of procIdx in the whole procedure (in pasparser_lval.inc; cited by NAME, not by line — see the re-check note below) is one of exactly two shapes — Procs[procIdx].RetType, once, and a ProcRet*[procIdx] column, everywhere else: ProcRetPtrElemTk, ProcRetPtrElemRec, ProcRetPtrDepth, ProcRetPtrBaseTk, ProcRetPtrBaseRec, ProcRetIsDynArray, ProcRetFixedArrBytes, ProcRetRecId, ProcRetElemTk, ProcRetElemRec, ProcRetDynDepth, ProcRetArrAi. Nothing reads a body, a parameter list, a scope or a name.

So the existing (node, procIdx) entry point IS the signature-only one. A new one would take the same two things and read the same columns.

The residual risk is NOT the entry point, and this is the part to carry forward

The plumbing being uniform says the signature always ARRIVES. It says nothing about whether the columns are FILLED, and they are not filled on every path — measured in this slice's own ProcRet* census: ParseSubroutine fills all 17 columns, while the three pasparser_decl.inc paths fill the same 11 and drop the same 6. ProcRetEnumId and ProcRetRecId were fixed there; the array columns (ProcRetIsDynArray, ProcRetFixedArrBytes, ProcRetElemTk, ProcRetElemRec) and ProcRetProcSig remain open as [[bug-p-a-procedural-type-cannot-return-an-array-or-another-procedural-type]].

A signature declared by ParseProcTypeSignature therefore reaches this function correctly and finds its array columns blank — which reads exactly like "this construct is not supported" rather than like a missing write. Whoever picks that ticket up should expect the symptom to appear HERE while the defect is in the writer, and should not go looking for a missing entry point.

Untested claim retired: nothing above is inferred from behaviour, and no repro was run for this note. It is a static census of construction sites and of one procedure's uses of one parameter, which is the whole of what it claims.

2026-09-05 — re-checked after 7095ca817, and the citation de-lined

7095ca817 (frankA, "a ^ after a FIELD of a call result derefs the FIELD, not the call") landed four hunks inside this procedure, +38 lines. The conclusion above is unchanged and was re-run, not assumed: every procIdx use is still Procs[procIdx].RetType once plus the same twelve ProcRet* columns. The new code works on the node/field side and adds no signature coupling at all, which is a good sign about the change rather than a lucky escape.

What DID break is the citation. This note originally said pasparser_lval.inc:5011-5434; the procedure is now at 5060-5529, so those numbers were wrong within a day of being written and a stale line number does not error -- it points somewhere. This repo already learned that once and acted on it: the Makefile:<n> citations in CLAUDE.md were replaced with recipe names after one drifted 142 lines in an evening, to fi; \, a real line that explains nothing. Same failure, same fix -- the procedure is cited by name here now, and anyone re-running this should bound it with a grep for its procedure line rather than trusting a number in a document.

The re-check itself is the reusable part, and it is one command:

sed -n "<start>,<end>p" compiler/pasparser_lval.inc \
  | grep -o "[A-Za-z_]*\[procIdx\]\|Procs\[procIdx\]\.[A-Za-z_]*" | sort -u

If that ever prints something outside Procs[].RetType and the ProcRet* family, the conclusion has expired and a signature-only entry point may be back on the table.