From 13a8a39af01ca4303fbe7d15b17a16a0f31c9de3 Mon Sep 17 00:00:00 2001 From: Graeme Geldenhuys Date: Sat, 20 Jun 2026 02:04:58 +0100 Subject: [PATCH] fix(compiler): incremental unit-mode crash (nil Prog.SymbolTable) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Compiling a UNIT (top source is a `unit`, so the pipeline runs in unit-mode and Prog stays nil) via the default incremental path segfaulted: the incremental worker setup unconditionally read `Prog.SymbolTable` to seed each dep worker, dereferencing nil. In unit-mode the symbol table comes from the semantic pass, not a program node. This is exactly the invocation the runtime Makefile drives (`blaise --source X.pas --output X.o` per RTL unit), so it broke `make` in runtime/ once incremental became the default; it was a latent pre-existing bug in the opt-in incremental path (reproduces at de0ea5d with --incremental). Fix: seed the worker symbol table from Semantic.GetSymbolTable() when Prog is nil, matching the non-incremental unit-mode path. Also pin the runtime Makefile to --no-incremental. That Makefile is itself a hand-managed separate-compilation system (one object per unit + an explicit archive list); the compiler's incremental mode would additionally write per-dependency side-effect objects and skip inlining a used unit's bodies, but those side-effect objects are not in the archive list — so a cross-unit symbol (e.g. typeinfo_TRtlPlatform, referenced by the derived TRtlPlatformPosix in a different unit) was left undefined at link. Whole-program per unit keeps each unit object self-contained. Regression test (cp.test.e2e.sepcompile.pas): compile a unit that uses another unit in unit-mode via the default incremental path, then build and run a program over the emitted object. --- compiler/src/main/pascal/Blaise.pas | 9 +- .../test/pascal/cp.test.e2e.sepcompile.pas | 91 +++++++++++++++++++ runtime/Makefile | 10 +- 3 files changed, 108 insertions(+), 2 deletions(-) diff --git a/compiler/src/main/pascal/Blaise.pas b/compiler/src/main/pascal/Blaise.pas index 9744fb6..647a370 100644 --- a/compiler/src/main/pascal/Blaise.pas +++ b/compiler/src/main/pascal/Blaise.pas @@ -789,7 +789,14 @@ begin Worker := TCompileWorker.Create(True); Worker.WorkUnit := TUnit(Units.Items[I]); Worker.Iface := TUnitInterface(UnitIfaces.Items[I]); - Worker.SymTable := Prog.SymbolTable; + { In unit-mode (the top source is a `unit`) Prog is nil — the symbol + table comes from the semantic pass, not a program node. Using + Prog.SymbolTable there dereferences nil and crashes the incremental + worker setup. } + if Prog <> nil then + Worker.SymTable := Prog.SymbolTable + else + Worker.SymTable := Semantic.GetSymbolTable(); Worker.OPath := UnitOPath; Worker.Driver := WorkerDriver; Worker.Opts := Opts; diff --git a/compiler/src/test/pascal/cp.test.e2e.sepcompile.pas b/compiler/src/test/pascal/cp.test.e2e.sepcompile.pas index 1e4e185..2c9d382 100644 --- a/compiler/src/test/pascal/cp.test.e2e.sepcompile.pas +++ b/compiler/src/test/pascal/cp.test.e2e.sepcompile.pas @@ -49,6 +49,13 @@ type procedure TestDuplicateExternalAcrossUnits_Compiles; procedure TestNativeIncremental_MultiUnitClass_Compiles; procedure TestNativeNoIncremental_MultiUnitClass_Compiles; + { Regression: compiling a UNIT (unit-mode, top source is a `unit`) that + USES another unit, with the default incremental path, segfaulted — the + incremental worker setup read Prog.SymbolTable, but in unit-mode Prog is + nil (the table comes from the semantic pass). This is the exact path the + runtime Makefile drives (every RTL unit is compiled `--source X.pas + --output X.o`), so it broke `make` in runtime/. } + procedure TestNativeIncremental_UnitUsesUnit_Compiles; end; implementation @@ -579,6 +586,90 @@ begin AssertEquals('use_shapes_ni stdout', 'shape:box' + #10, Captured) end; +procedure TSepCompileTests.TestNativeIncremental_UnitUsesUnit_Compiles; +{ Compile a UNIT that uses another unit, in unit-mode (--source .pas + --output .o), via the default incremental path. This is the runtime + Makefile's exact invocation and used to segfault (nil Prog.SymbolTable in the + incremental worker setup). A program that uses the compiled unit must then + build and run, proving the per-unit object is correct. } +const + BaseSrc = + ''' + unit BaseU; + interface + type TFoo = record A: Integer; end; + function MakeFoo(N: Integer): TFoo; + implementation + function MakeFoo(N: Integer): TFoo; + begin Result.A := N end; + end. + '''; + DerivedSrc = + ''' + unit DerivedU; + interface + uses BaseU; + function Doubled(N: Integer): Integer; + implementation + function Doubled(N: Integer): Integer; + var F: TFoo; + begin F := MakeFoo(N); Result := F.A * 2 end; + end. + '''; + ProgSrc = + ''' + program UseDerived; + uses DerivedU; + begin + WriteLn(Doubled(21)) + end. + '''; +var + BasePas, DerivedPas, DerivedObj, ProgPas, ProgBin: string; + Captured: string; + Rc: Integer; +begin + if not ToolchainAvailable() then + begin + Fail('toolchain missing — qbe or RTL not found'); + Exit + end; + if not FileExists(BlaisePath()) then + begin + Fail('blaise binary missing at ' + BlaisePath()); + Exit + end; + + BasePas := FScratch + '/BaseU.pas'; + DerivedPas := FScratch + '/DerivedU.pas'; + DerivedObj := FScratch + '/derivedu.o'; + ProgPas := FScratch + '/use_derived.pas'; + ProgBin := FScratch + '/use_derived'; + + WriteFile(BasePas, BaseSrc); + WriteFile(DerivedPas, DerivedSrc); + DeleteFile(DerivedObj); + + { Unit-mode incremental compile: this is the call that crashed. } + Rc := RunBlaise(['--source', DerivedPas, '--output', DerivedObj, + '--backend', 'native', + '--unit-path', FScratch], Captured); + AssertEquals('blaise(DerivedU unit) exit code (out: ' + Captured + ')', 0, Rc); + AssertTrue('DerivedU.o exists', FileExists(DerivedObj)); + + { The emitted unit object must be usable: build + run a program over it. } + WriteFile(ProgPas, ProgSrc); + Rc := RunBlaise(['--source', ProgPas, '--output', ProgBin, + '--backend', 'native', + '--unit-path', FScratch], Captured); + AssertEquals('blaise(use_derived) exit code (out: ' + Captured + ')', 0, Rc); + AssertTrue('use_derived exists', FileExists(ProgBin)); + + Rc := RunBinary(ProgBin, Captured); + AssertEquals('use_derived exit code', 0, Rc); + AssertEquals('use_derived stdout', '42' + #10, Captured) +end; + initialization RegisterTest(TSepCompileTests); diff --git a/runtime/Makefile b/runtime/Makefile index bc21285..d140272 100644 --- a/runtime/Makefile +++ b/runtime/Makefile @@ -25,7 +25,15 @@ LIB = $(OBJ_DIR)/blaise_rtl.a # driver finds it automatically next to itself. COMPILER_BIN = ../compiler/target BLAISE = $(COMPILER_BIN)/blaise -BLAISE_FLAGS = +# This Makefile IS a hand-managed separate-compilation system: it compiles each +# RTL unit to its own object and archives an explicit object list. The +# compiler's own incremental mode (now the default) would, on top of that, write +# per-dependency side-effect .o files next to each output and skip inlining a +# used unit's bodies — but those side-effect objects are not in the archive's +# object list, so a cross-unit symbol (e.g. typeinfo_TRtlPlatform, referenced by +# the derived TRtlPlatformPosix in another unit) ends up undefined at link. +# Force whole-program compilation per unit so every unit object is self-contained. +BLAISE_FLAGS = --no-incremental # Assembly sources — platform-specific, no C compiler needed. ASM_OBJS = $(OBJ_DIR)/blaise_setjmp_x86_64.o \