fix(compiler): incremental unit-mode crash (nil Prog.SymbolTable)
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.
This commit is contained in:
parent
47ca10d7d3
commit
13a8a39af0
|
|
@ -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;
|
||||
|
|
|
|||
|
|
@ -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 <unit>.pas
|
||||
--output <unit>.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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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 \
|
||||
|
|
|
|||
Loading…
Reference in a new issue