fix(codegen): empty loop body no longer crashes the compiler (#150)

`for x := 0 to N do;` and `while C do;` have an empty statement as their
body, which the parser represents as a nil statement (its convention for
"no statement here" — every statement-list builder already filters nil).
A loop's single Body field is not filtered, so the nil reached EmitStmt:

  * Native backend: EmitStmt fell through every `is` check to the
    unsupported-statement fallback, which dereferenced AStmt.ClassName on
    nil — segfaulting the compiler (issue #150 was reported against the
    default native backend).
  * QBE backend: EmitStmt raised "EmitStmt called with nil statement",
    rejecting a legal empty body.

Both backends now treat a nil statement as a no-op, matching the parser's
nil-as-empty convention and the way AnalyseStmt already tolerates it. An
empty loop body compiles to a loop that does nothing.

E2E regression in cp.test.e2e.controlflow.pas asserts both `for ... do;`
and `while ... do;` compile + run as a no-op on every backend.
This commit is contained in:
Graeme Geldenhuys 2026-06-28 09:34:25 +01:00
parent f517f5148a
commit a205c22136
3 changed files with 49 additions and 1 deletions

View file

@ -10811,6 +10811,12 @@ var
PCHTotal: Integer;
AliasBuf: Integer;
begin
{ An empty statement (e.g. the body of `for x := 0 to N do;`) parses to a nil
statement — the parser's convention for "no statement here". It is a valid,
do-nothing body, so emit nothing. Without this guard the unsupported-
statement fallback at the tail dereferences AStmt.ClassName and segfaults. }
if AStmt = nil then
Exit;
Self.DbgStmtLabel(AStmt);
if AStmt is TAsmStmt then
begin

View file

@ -2572,8 +2572,11 @@ procedure TCodeGenQBE.EmitStmt(AStmt: TASTStmt);
var
DeadLbl: string;
begin
{ An empty statement (e.g. the body of `for x := 0 to N do;`) parses to a nil
statement — the parser's convention for "no statement here". It is a valid,
do-nothing body, so emit nothing rather than rejecting it. }
if AStmt = nil then
raise ECodeGenError.Create('EmitStmt called with nil statement');
Exit;
if AStmt is TAsmStmt then
raise ECodeGenError.Create(
'inline asm blocks require the native backend (--backend native); '

View file

@ -24,6 +24,8 @@ type
procedure TestRun_For_Upward_PrintsRange;
procedure TestRun_For_Downto_PrintsRange;
procedure TestRun_While_PrintsRange;
procedure TestRun_For_EmptyBody_NoCrash;
procedure TestRun_While_EmptyBody_NoCrash;
procedure TestRun_Repeat_PrintsRange;
procedure TestRun_For_BreakExitsEarly;
procedure TestRun_For_ContinueSkipsIteration;
@ -105,6 +107,29 @@ const
end.
''';
{ Empty loop body — `for ... do;` / `while ... do;` parse to a nil body.
Regression for issue #150: the native backend segfaulted (nil.ClassName in
the unsupported-statement fallback) and the QBE backend rejected it; both
must now treat the empty body as a valid no-op. }
SrcForEmptyBody = '''
program P;
var I: Integer;
begin
for I := 0 to 9 do;
WriteLn('done')
end.
''';
SrcWhileEmptyBody = '''
program P;
var I: Integer;
begin
I := 0;
while I < 0 do;
WriteLn('done')
end.
''';
SrcRepeat = '''
program P;
var I: Integer;
@ -202,6 +227,20 @@ begin
AssertEquals('1 2 3', '1' + LE + '2' + LE + '3' + LE, Output);
end;
procedure TE2EControlFlowTests.TestRun_For_EmptyBody_NoCrash;
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end;
{ Issue #150: an empty `for` body must compile + run as a no-op on both
backends (native previously segfaulted the compiler). }
AssertRunsOnAll(SrcForEmptyBody, 'done' + LE, 0);
end;
procedure TE2EControlFlowTests.TestRun_While_EmptyBody_NoCrash;
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end;
AssertRunsOnAll(SrcWhileEmptyBody, 'done' + LE, 0);
end;
procedure TE2EControlFlowTests.TestRun_Repeat_PrintsRange;
var Output: string; RCode: Integer;
begin