step N clause in for loops advances the counter by an arbitrary positive amount on each iteration. Works with both to and downto, and with inline var counters.
Modeswitch: forstep, enabled by default in {$mode unleashed}; opt in elsewhere with {$modeswitch forstep}.
for i := 1 to 10 step 2 do
write(i, ' '); // 1 3 5 7 9
for i := 20 downto 1 step 3 do
write(i, ' '); // 20 17 14 11 8 5 2
for var k := 5 to 50 step 5 do
write(k, ' '); // 5 10 15 ... 50The step expression must be ordinal and positive. Use downto for descending loops - the step itself is always positive, and a non-positive constant is a parse error.
The step expression is evaluated once before the loop starts, so calls with side effects fire exactly one time - the same way the classic for-loop treats its upper bound:
for i := 0 to 12 step computeStep do
use(i); // computeStep called exactly oncestep is not a reserved token. The parser checks for it in one position only: right after the to / downto expression and before do. Anywhere else step stays an ordinary identifier, so existing code keeps compiling:
var step: integer = 5; // OK - a variable named step
function step: integer; // OK - a function named step
type TFoo = record step: integer end; // OK - a record field named step
for i := 1 to step do use(i); // OK - the upper bound is the variable
for i := 0 to step step 1 do use(i); // OK - first `step` is the upper bound, second is the keywordThe SynEdit highlighter in Unleashed Pascal IDE mirrors this: only the keyword position lights up.
A literal step 1 is dropped during simplify, so the loop reverts to the regular for-loop code path and keeps every existing optimization (range-check elision, unrolling, ...). Only step N for N >= 2, or a non-constant step, takes the dedicated path.
A surviving step lowers the for-loop to a while-loop with the increment at the start of each pass, guarded by a first-iteration flag:
steptemp := step; totemp := to; i := from; first := true;
while true do begin
if first then first := false
else i := i + steptemp; // - steptemp for downto
if i > totemp then break; // < totemp for downto
body; // continue lands on the next pass
end;
This shape lets every control-flow construct behave as in a regular for-loop:
continuejumps to the always-true while-condition; the next pass runs the increment, the range check, then the body. No infinite loop.breakexits cleanly; the counter holds the value at break time.exitandraisepropagate normally; no temporaries to clean up.
Cost: one boolean temp plus one branch per iteration.
Unleashed mode preserves the for-loop counter after the loop (see tweaks.md). With step, the guarantee is "last assigned value" - and because the lowering advances the counter before the range check, a natural exit leaves the first value past the bound, not the last one the body saw:
for i := 1 to 10 step 4 do ; // body sees 1, 5, 9
{ i = 13 after the loop (9 + step) }
for i := 20 downto 1 step 3 do ; // body sees 20, 17, ..., 2
{ i = -1 after the loop (2 - step) }
for i := 1 to 10 step 4 do
if i = 5 then break;
{ i = 5 - break keeps the exact value }When the post-loop value matters, prefer break (exact), or derive the last body value as i - step (i + step for downto) after a natural exit.
The step expression must be ordinal; the compiler picks the wider of the step type and the counter's range type for the increment arithmetic.
| Counter type | Step | Status |
|---|---|---|
integer of any width (byte ... int64) |
any positive integer expression | works |
| enum | positive integer (ordinal distance) | works: for var d := dMon to dFri step 2 do visits every other value |
char |
- | not supported: rejected with Illegal type conversion: "ShortString" to "AnsiChar" |
Note the inline-var inference gotcha: for var ch := 'a' to 'z' do infers a string counter (char literals promote to string under inference) - a char counter needs the explicit type, for var ch: char := 'a' to 'z' do, and takes no step.
| Message | Trigger |
|---|---|
Step value must be a positive integer |
constant step 0 or step -N |
Step expression must be of ordinal type |
step is a real / string / pointer expression |
Step is not allowed in for-in loops |
step after a for ... in collection |
With the modeswitch off ({$modeswitch forstep-}), step in a for-header is not recognized at all and the parser reports Syntax error, "DO" expected but "identifier STEP" found.
| Case | Behavior |
|---|---|
for i := 1 to 5 step 100 do |
body runs once (i = 1), then 1 + 100 > 5 exits |
for i := 10 to 1 step 2 do |
empty range, body never runs |
for i := 5 to 5 step 3 do |
single iteration (i = 5) |
step from a runtime variable holding 0 |
the counter never advances - infinite loop; no compile-time check is possible, prefer constants |
| nested for-step loops | each level keeps its own step and counter |
step also composes with for parallel loops - see parallelfor.md.
program for_step_demo;
{$mode unleashed}
type
TDay = (dMon, dTue, dWed, dThu, dFri);
var
calls: integer = 0;
function stride: integer;
begin
inc(calls);
result := 4;
end;
begin
// every third number: 3 + 6 + ... + 99
var sum := 0;
for var i := 3 to 99 step 3 do sum += i;
writeln($'sum of multiples of 3 up to 99: {sum}');
// countdown by 25
write('launch in:');
for var t := 100 downto 0 step 25 do write(' ', t);
writeln;
// enum counters advance by ordinal distance
write('every other day: ');
for var d: TDay := dMon to dFri step 2 do write(d, ' ');
writeln;
// the step expression is evaluated once, before the loop starts
var hits := 0;
for var i := 0 to 20 step stride do inc(hits);
writeln($'0..20 step stride(): {hits} iterations, stride evaluated {calls} time(s)');
{$ifdef WINDOWS}readln;{$endif}
end.Output:
sum of multiples of 3 up to 99: 1683
launch in: 100 75 50 25 0
every other day: dMon dWed dFri
0..20 step stride(): 6 iterations, stride evaluated 1 time(s)