← board

A for-in enumerator whose MoveNext takes arguments is accepted, on both arms

Measured 2026-09-08, compiler 70dffa8a0e51, against fpc 3.2.2 -Mobjfpc.

type
  TBad = class
    F: Integer;
    function MoveNext(a: Integer): Boolean;   { NOT the protocol's MoveNext }
    property Current: Integer read F;
  end;
operator enumerator(a: Integer): TBad; ...
var i, v: Integer;
begin v := 1; for i in v do WriteLn(i); end.
arm pxx before fpc
operator enumerator returning TBad compiles, exit 0 Cannot find a "MoveNext" method in enumerator "TBad" + Impossible operator overload, at the operator DECLARATION
GetEnumerator returning TBad compiles, exit 0 same, at the loop

GenMakeMethodCallM passes the instance and no argument list, so the call was built for a parameterless method and the parameter slot was simply never written.

Why it was invisible, and it is not the usual reason

It is not that nobody wrote a test. tforin22.pp is exactly this program, is a %FAIL row, and was passing — because pxx refused it at a for-in gate that required the container's first token to be tkIdent, and its container is the literal 1. The row asserted "this must be rejected", pxx rejected it, and the rejection had nothing to do with the enumerator.

So the conformance suite reported a green that was correct about something else. Closing [[bug-p-a-for-in-container-must-start-with-an-identifier-token]] turned that row red in the same run, which is the only reason this was found.

A %FAIL row cannot tell you WHY the compiler refused, and that is structural rather than a gap in this one test: it asserts a non-zero exit, and every refusal produces one. Any %FAIL row whose program would also be refused for an unrelated reason is carrying the same risk, and there is no instrument in the harness that would distinguish them.

The fix, and why it is in the resolver

EnumeratorMoveNextMeth (pasparser_stmt.inc) answers -1 when the method it resolves takes more than one parameter. Its own header already stated the governing rule — "Both the check and the lowering must ask the same question, or a loop is accepted by one rule and lowered by another" — and both readers (ParseForInEnumeratorAST, BuildForInOpEnumeratorLoop) already Error on -1, so both arms refuse with one edit and one message.

ParamCount > 1, not <> 1, and the direction is deliberate: ParamCount counts Self for a class method (the interface-signature check in pasparser_decl.inc prints ParamCount - 1 as the user-visible arity), while an interface method's row need not carry the Self slot — tforin9.pp's IMyIterator, which nominates its MoveNext through the enumerator MoveNext directive, is that shape and still compiles. Erring toward accepting is right here: the failure being closed is ACCEPTING a malformed enumerator, so a missed interface case leaves it exactly where it was rather than refusing a loop that works.

Verification

test/test_a_for_in_enumerator_needs_a_parameterless_movenext_operator.pas and ..._getenumerator.pas, both MUST-NOT-COMPILE, both wired into test-core. Two files because there are two arms and one resolver — a fix at either call site would leave the other accepting, and these two rows are what says the rule is in the resolver rather than beside one caller.

Positive controls, all green after the change: the seven test/test_for_in_* fixtures and tforin9.pp.

Log