Inline loop variables — for var i := 0 to N / for var x in coll (Delphi 10.3 Rio)
- Type: feature (language / parser) — Track A
- Status: done — counted form
for var i := a to bDONE (2026-06-30); for-in inlinefor var x in collDONE (2026-07-02, pin v142) for array/ string/record/set containers (plain var, implicit-Self field, qualified lvalue) and enum types. GetEnumerator/generator containers and an implicit-Self record-array field are explicitly out of scope (own clear error) — see "Remaining" below, kept as a narrower follow-on rather than blocking the common cases. - Opened: 2026-06-30
- Origin: carved out of the
--auto-localsidea (feature-implicit-identifier- binding-strictness-switch) after a design pass. This is the typed, standard, forward-compatible layer; the sloppy layer is [[feature-implicit-locals-sloppy-switch]].
Idea
Allow declaring the loop variable inline in the for header, matching Delphi
10.3 Rio's inline-variable syntax:
for var i := 0 to N do ... { i is a fresh loop-scoped Integer (inferred) }
for var i := N downto 0 do ...
for var x in coll do ... { x's type inferred = the iterable's element type }
The type is inferred, not written — for var i := 0 makes i an ordinal
from the bounds; for var x in arr makes x the element type. This is the same
inference pxx already does for the inline-var-statement form (below), just in
the loop header. var here plays the role of auto.
Why
Eliminate the redundant separate var i: Integer; boilerplate for throwaway loop
counters — the single most common source of it. Forward-compatible: every
existing for i := ... with a pre-declared i keeps working unchanged; this only
adds a new spelling. Standard-ish (Delphi 10.3 set the precedent; pxx already
leans Delphi-compatible elsewhere).
Already in place (most of the machinery)
tyAuto(defs.inc:540, "statically typed variable with inferred type") + the inference path: an inlinevar x := exprstatement in a body already works (var s := 'abc'; writeln(s)→abc 3;var x := 7→7). Gated onEnableAutoVar(default True;--no-auto-vardisables). See parser.inc ~7512, ~7550 (the var-decl-with-init + auto-resolve), ~4223 ("use of auto variable before type is inferred").for x in collover a dynarray/enum/iterable already works for a pre-declaredx.
Scope (the actual gap)
ParseForStatementAST (parser.inc ~6811) rejects var in the header
(for: expected variable). Teach it:
- On
for, if the next token isvar, consume it, read the counter name, and declare a loop-scoped local for it instead ofFindSym-ing an existing one.- Counted form (
:= a to b): declare it ordinal (Integer, or inferred from the bound expression's type as the existing counter path does). inform: declare ittyAutoand let the existing for-in element-type inference fill it (mirror the inline-var := exprresolve).
- Counted form (
- Scope: the inline counter is visible only in the loop body (Rio semantics). Simplest acceptable v1: a normal routine-local (function-scoped) like other pxx locals — loop-only scoping is a refinement, not a blocker.
- Optionally accept an explicit type too (
for var i: Int64 := ...) — cheap once thevar-in-header path exists; skip if it complicates v1.
Acceptance
- [x]
for var i := 0 to N doandfor var x in coll docompile + run,i/xcorrectly typed; existing pre-declaredforunchanged;--no-auto-varstill compiles pre-declared loops (only the inline form is gated). Self-host byte-identical. Tests for counted + for-in inline forms. Done, pin v142 (array/string/record/set/enum-type containers); GetEnumerator/generator/ implicit-Self-record-array-field explicitly out of scope, own clear error.
v1 done (2026-06-30, Track A) — counted form
for var i := a to b / downto now declares a fresh Integer routine-local
counter (gated on EnableAutoVar; classic pre-declared for unchanged).
ParseForStatementAST (parser.inc): on var after for, AllocVar(name, tyInteger) instead of FindSym. Nested for var works. Self-host byte-identical;
make test green (test/test_for_var_inline.pas, oracle 10 / 6).
for-in inline done (2026-07-02, pin v142)
ParseForStatementAST now allocates the loop var tyAuto (instead of
rejecting) when the header is for var x in .... Each ParseForIn*AST
variant that can resolve the iterable's element type ahead of the body now
does so before calling ParseStatementAST and backfills
Syms[varIdx].TypeKind (+ RecName for a record element) when it's
tyAuto, exactly as the root-cause note above predicted:
ParseForInVarAST/ParseForInFieldAST/ParseForInNodeAST— array (incl. array-of-record) and string containers, plain var / implicit-Self field / qualified lvalue.ParseForInSetAST(+ the qualified-set branch insideParseForInNodeAST) — set-of-enum / set-of-Char membership scan.ParseForInEnumTypeAST—for var x in TEnum; enums have no distinct type kind (ordinal/Integer-sized perParseTypeKind), so this is just backfillingtyInteger.
Explicitly out of scope, own clear error instead of silent mishandling:
ParseForInGeneratorAST/ParseForInEnumeratorAST(GetEnumerator structural enumerator) — both need a further property/yield-type lookup this pass didn't do; atyAutoloop var into either now errors "inline loop variable not yet supported here" rather than miscompiling.- An implicit-Self record-array field (
ParseForInFieldAST/ParseForInNodeAST'sAN_FIELDbranch) — no element-record lookup is wired for that source shape (unlike a plain record-array var'sSyms[].ElemRecName); also errors explicitly rather than leavingRecNameunset.
Verified: identical output between the new inline form and the existing
pre-declared-var form across array/string/record/set containers; the two
guarded-rejection paths give a clean compile error, not a crash. Front-end
only, self-host byte-identical, cross-target identical (i386/arm32/
aarch64), full make test green. test/test_for_var_inline.pas extended
to cover all four working container kinds.