← board

A nested for in a parallel for body is a compile error

Repro

program nest;
uses palparallel;
function F(n: Integer): Int64;
var i, j: Integer; acc: Int64;
begin
  acc := 0;
  parallel(pdChunked) for i := 0 to n - 1 reduction(+: acc) do
  begin
    for j := 1 to 3 do acc := acc + 1;
  end;
  F := acc;
end;
begin WriteLn('acc=', F(1000)); end.
$ ./compiler/pascal26 -O2 --threadsafe nest.pas nest
pascal26:7: error: expected ':='
  in: ./compiler/../lib/rtl/palthread.pas
  near:  __pfhi  begin for j >>>

Compiler ba2efc846790. Two things are wrong at once: the construct is refused, and the diagnostic names an RTL file the user did not write, at a line number from their own file.

The boundary — measured, and it is narrow

Same harness, same body slot, one statement swapped each time:

body statement
acc := acc + 1; compiles
if i > 0 then acc := acc + 1; compiles
j := 0; while j < 3 do begin ... end; compiles
j := 0; repeat ... until j >= 3; compiles
case i mod 2 of 0: ...; else ...; end; compiles
begin acc := acc + 1; end; compiles
for j := 1 to 3 do acc := acc + 1; REFUSED
for j := 3 downto 1 do acc := acc + 1; REFUSED

So it is not nesting, not block structure, and not a second loop — while and repeat are both fine. It is specifically the for STATEMENT.

Suspected site — a hypothesis, not a diagnosis

The worker is built by replaying captured tokens rather than by lowering an AST:

procedure __pf_<n>(__pfctx: Pointer; __pflo, __pfhi: NativeInt);
var V: NativeInt; begin for V := __pflo to __pfhi do BODY end;

(pasparser_stmt.inc:3744, stashed via PFStash around 4195-4330.) The error text is what makes this the first place to look: the parser reports expected ':=' while positioned at for j, and the near: window shows __pfhi begin for j — the INNER loop's := is not where the parser expects it. The user's source plainly has j := 1, so the token that vanished or moved did so during capture or replay.

Do not take that as established. The window is a rendering of the synthesized stream and could equally be showing a do that went missing after __pfhi. Dump the replayed tokens before theorising further — this is the class of bug devdocs/dev/debugging-playbook.md says to print rather than reason about.

Why it matters

A loop nest is the ordinary shape of the work parallel for exists to speed up, and the workaround — hoisting the inner loop into a called function, which is what the regression test for bug-a-a-shared-ansistring-handle-in-a-parallel-loop-is-11x-slower had to do — changes the code rather than the schedule. while compiling is what makes this survivable and also what makes it confusing: the feature looks like it supports nested loops.

Gate

make compiler/pascal26 + the repro above compiling and printing acc=3000, plus the existing parallel for tests in the Makefile. Keep a downto case: both arms are refused today and only one is likely to be exercised.

Found while measuring the heap-handle half of [[bug-a-a-shared-ansistring-handle-in-a-parallel-loop-is-11x-slower]] — the integrity test wanted an inner loop and could not have one.

Resolved 2026-09-02 — the hypothesis was wrong, and the refusal was load-bearing

The ticket suspected the inner loop's := had gone missing from the token replay, and said not to take that as established. It had not. A ^ was ADDED. j is an enclosing local, so it is a capture, so every reference in the worker body gets a trailing ^ — including the one the for statement uses as its control variable. for j^ := 1 to 3 is not a for-loop the grammar has, and the parser reports expected ':=' pointing at that ^.

The discriminating experiment was one line: make the inner control variable a global, which is never captured, and the same program compiles. That separates "the for statement" from "the capture rewrite" in a single build, and it is what the boundary table could not do — every row in it used a local.

Then the global version hung. Which is the real finding: the two spellings that compiled are both broken, and the refused one was the only safe one.

spelling before why
for j := 1 to 3 (j local) compile error capture rewrite → for j^ :=
j := 0; while j < 3 (j local) 299674 / 299015 / 295718 of 300000 one j shared through a pointer by every worker
for g := 1 to 3 (g global) HANG a worker resetting g to 1 keeps the others' loops alive

The fix, and why it is not "allow for p^ :="

Widening the grammar to accept a dereference as a control variable would have made the reported program compile and then race exactly like the while one. The construct is wrong for a reason that has nothing to do with syntax: a loop's control variable is written by the loop, so N workers sharing one is a race by construction.

So the control variable of an inner for becomes a worker-private local, whatever its storage was outside — the rule OpenMP applies to loop variables inside a parallel region, and the only reading under which all three rows above come out right. A pre-scan over the captured body tokens collects every for <ident> :=, declares each in the worker's own var section (reusing the reduction accumulators' type-keyword rule, which exists because a bare tkIdent('Integer') does not resolve as a named type), and excludes them from the capture list, so the ^ is never appended.

Privatising a global control variable changes what that global holds after the loop. Under parallelism that value was never defined, and the measured alternative is a hang.

What is still broken, deliberately

A variable the body merely WRITES — the while shape — is still a shared capture and still racy. That is [[feature-a-a-private-clause-for-parallel-for]], raised from 25 to 50 with the numbers above: it was filed as "a class of body you cannot write", and it is really "a class of body that compiles and quietly returns a short answer".

Log