From a205c22136c8da4eca9ddd2966c8edb469cbbd37 Mon Sep 17 00:00:00 2001 From: Graeme Geldenhuys Date: Sun, 28 Jun 2026 09:34:25 +0100 Subject: [PATCH] fix(codegen): empty loop body no longer crashes the compiler (#150) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `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. --- .../pascal/blaise.codegen.native.x86_64.pas | 6 +++ .../src/main/pascal/blaise.codegen.qbe.pas | 5 ++- .../test/pascal/cp.test.e2e.controlflow.pas | 39 +++++++++++++++++++ 3 files changed, 49 insertions(+), 1 deletion(-) diff --git a/compiler/src/main/pascal/blaise.codegen.native.x86_64.pas b/compiler/src/main/pascal/blaise.codegen.native.x86_64.pas index 4d36321..6d70eaa 100644 --- a/compiler/src/main/pascal/blaise.codegen.native.x86_64.pas +++ b/compiler/src/main/pascal/blaise.codegen.native.x86_64.pas @@ -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 diff --git a/compiler/src/main/pascal/blaise.codegen.qbe.pas b/compiler/src/main/pascal/blaise.codegen.qbe.pas index daf53ed..79fb681 100644 --- a/compiler/src/main/pascal/blaise.codegen.qbe.pas +++ b/compiler/src/main/pascal/blaise.codegen.qbe.pas @@ -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); ' diff --git a/compiler/src/test/pascal/cp.test.e2e.controlflow.pas b/compiler/src/test/pascal/cp.test.e2e.controlflow.pas index 448c0ae..ec057d0 100644 --- a/compiler/src/test/pascal/cp.test.e2e.controlflow.pas +++ b/compiler/src/test/pascal/cp.test.e2e.controlflow.pas @@ -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