← board

for x in F is refused when F returns a set

Repro

program f1;
type TM = (mA, mB, mC); TMs = set of TM;
function F: TMs; begin F := [mA, mC]; end;
var m: TM;
begin for m in F do Write(Ord(m), ' '); WriteLn; end.

pxx: error: for-in: not a generator, enum type, or iterable variable fpc 3.2.2: accepts, prints 0 2 .

The boundary, measured — this is the part not to re-derive

shape result
for m in F where F: TMs (set) refused
for i in F where F: array of Integer works, 7 9
for m in F.s where F: TR and TR.s: TMs works, 0 2
s := F; for m in s works, 0 2 — the workaround

So for-in over a CALL is not the problem; the dyn-array result goes through fine. It is the SET result specifically.

Why it is filed separately from the parameter bug

Its diagnostic comes from a different place. The parameter defect reached BuildForInSetLoop and was refused there by the element-kind test (set iteration supports set of <enum>, set of Char or an ordinal set constructor) because the symbol carried no element kind. This one never gets that far — the container dispatch does not recognise a call node with a set result as an iterable at all, and says so with the other message. Fixing the stamp did not move it, which is the evidence, not the reasoning.

Where to look

ParseForInSetAST takes a SYMBOL (setSym), and the qualified-lvalue path (ParseForInNodeAST, pasparser_stmt.inc:~1413) already handles a set reached through a node rather than a symbol — that is why the record-field row works. A call result is a third source with no arm. The element identity is available on the proc (ProcRetType and the set columns beside it) the same way the field path reads UFldSetEnumId / UFldSetElemTk.

Note the temp-assignment workaround already materialises the result into a symbol, so whatever the fix does, it must not evaluate F once per loop iteration — the membership scan runs the container expression's node, and a call with side effects would fire 256 times.

THE CARRIER CENSUS — frankB, 2026-09-06, and it is why this is not a one-line arm

A set's ELEMENT IDENTITY is carried once per CARRIER, and the carriers are independent parallel columns in four files:

carrier column
variable SymSetEnumId / SymSetElemTk (defs.inc:3286)
record field UFldSetEnumId / UFldSetElemTk (defs.inc:5894)
property UPropSetEnumId (defs.inc:5951)
parameter ProcParamSetEnumId (defs.inc:3524)
type alias AliasSetElemTk (defs.inc:6037)
function RESULT none — ProcRetEnumId exists, ProcRetSetEnumId does not

defs.inc:3525 already names this family in prose, in its own words — "the five-carrier set (symbol / record field / type alias / array element / param-return)" — for a DIFFERENT fact (managed-string element width). So the enumeration exists, as a comment, and the sixth carrier is missing anyway. That is a worked example of a written-down list failing to surface an omission, sitting in the file today. A list nothing is forced to walk is a comment, which is the same rule as: a precondition you do not branch on is a comment.

The order of work, and why the column is NOT being added first

The refusal today comes from the container DISPATCH (pasparser_stmt.inc:~2930), one layer earlier than any element-identity question — it would refuse a set-returning call even if the identity were present, because a call result is a third source of a set and only two have an arm (ParseForInSetAST takes a symbol, ParseForInNodeAST takes a node whose kind is AN_IDENT or AN_FIELD).

So: add the dispatch arm, re-measure, and add a carrier only if the compiler then says it does not know the element type. Adding two columns on a hypothesis is how a plausible explanation gets built into the tree, and a plausible explanation for a red is this repo's expensive failure mode.

The constraint the fix must satisfy, which is not about parsing

BuildForInSetLoop scans the set's DOMAIN and reads the container node per candidate. CloneAST(contNode) on a CALL node would therefore emit the call inside the loop — up to 256 evaluations for a set of Char, side effects and all. The container must be materialised into a hidden local ONCE, which is the move the dyn-array and fixed-array call-result arms in ParseForInNodeAST already make and document. fpc evaluates it once; test_for_in_over_a_set_valued_call row E asserts calls=2 and is the row a fix that merely stops the refusal still fails.

Log