fix(codegen): three bugs compiling units with generics + float code (#107)

Compiling the issue's waveCalcs unit surfaced three independent codegen
bugs, two of which the reporter saw directly and a third uncovered while
verifying the fix on the native backend.

1. QBE: a generic FUNCTION declared in a unit but never instantiated was
   code-generated as a template — its T-typed locals have no resolved
   type, so codegen raised "Variable 'MaxValue' has no resolved type —
   semantic pass required". EmitStandaloneDefs skipped templates, but the
   unit-emission path (GenerateUnit/AppendUnit -> EmitFuncDef) did not.
   Guard centrally in EmitFuncDef: skip decls with TypeParams <> nil; only
   concrete instances (from GenericFuncInstances) are emitted.

2. native: a float comparison nested inside a short-circuit and/or — e.g.
   (P > 1.0) or (P < -1.0) — reaches EmitExprToEax rather than
   EmitCondBranch. The integer comparison path there emits its operands via
   EmitExprToEax, which has no TFloatLiteral handler, so it raised
   "unsupported expression form TFloatLiteral". Add a float-comparison
   branch to EmitExprToEax that materialises the 0/1 result via ucomisd +
   setcc, mirroring EmitCondBranch.

3. native: a local float const (const TwoPi = 6.28; inside a function) was
   lowered as a load from a symbol named after the const, which is never
   emitted — link error "undefined reference to TwoPi". The const has no
   storage; inline its value from ConstString via .rodata in
   EmitExprToXmm0, as a TFloatLiteral already does.

Regression tests:
- TestUninstantiatedGenericFunc_InUnit_Compiles (sepcompile e2e)
- TestRun_Native_FloatCompareInOrAnd (runs on both backends)
- TestRun_Native_LocalFloatConst (runs on both backends)

FIXPOINT_OK + NATIVE_FIXPOINT_OK; full suite OK (3185 tests).
This commit is contained in:
Graeme Geldenhuys 2026-06-15 16:22:29 +01:00
parent 267e8db702
commit bf63d7419f
4 changed files with 219 additions and 0 deletions

View file

@ -4215,6 +4215,35 @@ begin
if AExpr is TIdentExpr then
begin
{ Float-typed named constant (const X = 6.28; ...): inline its literal
value. The const has no storage — its ConstString holds the source
text — so loading from a symbol named after it (VarOperand) would
reference an undefined label. Emit the value via .rodata exactly as
a TFloatLiteral does. }
if TIdentExpr(AExpr).IsConstant and IsFloatFamily(AExpr.ResolvedType) then
begin
IsS := (AExpr.ResolvedType <> nil) and
(AExpr.ResolvedType.Kind = tySingle);
if IsS then
begin
Self.Emit(Format(#9'movss .LF%s(%%rip), %%xmm0', [IntToStr(FLabelCount)]));
Self.Emit('.section .rodata');
Self.Emit('.balign 4');
Self.Emit(Format('.LF%s:', [IntToStr(FLabelCount)]));
Self.Emit(Format(#9'.float %s', [TIdentExpr(AExpr).ConstString]));
end
else
begin
Self.Emit(Format(#9'movsd .LF%s(%%rip), %%xmm0', [IntToStr(FLabelCount)]));
Self.Emit('.section .rodata');
Self.Emit('.balign 8');
Self.Emit(Format('.LF%s:', [IntToStr(FLabelCount)]));
Self.Emit(Format(#9'.double %s', [TIdentExpr(AExpr).ConstString]));
end;
Self.Emit('.text');
Inc(FLabelCount);
Exit;
end;
Ty := AExpr.ResolvedType;
if (Ty = nil) and Self.IsLocal(TIdentExpr(AExpr).Name) then
Ty := Self.LocalType(TIdentExpr(AExpr).Name);
@ -5783,6 +5812,49 @@ begin
if AExpr is TBinaryExpr then
begin
BE := TBinaryExpr(AExpr);
{ Float comparison evaluated for its boolean value (0/1 in %rax).
EmitCondBranch handles float comparisons that are themselves the
branch condition, but when a float comparison is a sub-expression —
nested in `and`/`or`, assigned to a Boolean, or passed as an
argument — it reaches EmitExprToEax and must materialise the
result. The integer comparison path below cannot, because its
operand emission (EmitExprToEax) has no TFloatLiteral handler. }
if (BE.Op in [boEQ, boNE, boLT, boGT, boLE, boGE]) and
(IsFloatFamily(BE.Left.ResolvedType) or
IsFloatFamily(BE.Right.ResolvedType)) then
begin
IsS := (BE.Left.ResolvedType <> nil) and
(BE.Left.ResolvedType.Kind = tySingle);
Self.EmitExprToXmm0(BE.Left);
Self.EmitXmm0WidthAdjust(BE.Left.ResolvedType, IsS);
Self.Emit(#9'subq $8, %rsp');
if IsS then Self.Emit(#9'movss %xmm0, (%rsp)')
else Self.Emit(#9'movsd %xmm0, (%rsp)');
Self.EmitExprToXmm0(BE.Right);
Self.EmitXmm0WidthAdjust(BE.Right.ResolvedType, IsS);
if IsS then
begin
Self.Emit(#9'movss (%rsp), %xmm1');
Self.Emit(#9'addq $8, %rsp');
Self.Emit(#9'ucomiss %xmm0, %xmm1'); { %xmm1 - %xmm0 }
end
else
begin
Self.Emit(#9'movsd (%rsp), %xmm1');
Self.Emit(#9'addq $8, %rsp');
Self.Emit(#9'ucomisd %xmm0, %xmm1');
end;
case BE.Op of
boEQ: Self.Emit(#9'sete %al');
boNE: Self.Emit(#9'setne %al');
boLT: Self.Emit(#9'setb %al'); { below (CF set) }
boGT: Self.Emit(#9'seta %al'); { above }
boLE: Self.Emit(#9'setbe %al');
boGE: Self.Emit(#9'setae %al');
end;
Self.Emit(#9'movzbl %al, %eax');
Exit;
end;
{ String concatenation (boAdd on tyString): call _StringConcat(left, right). }
if (BE.Op = boAdd) and
(BE.Left.ResolvedType <> nil) and

View file

@ -7683,6 +7683,12 @@ var
begin
if ADecl.IsExternal then Exit; { no body to emit for external declarations }
if ADecl.Body = nil then Exit; { forward declaration — impl appears elsewhere }
{ Generic template — its body has unbound type parameters (locals typed `T`
never get a ResolvedType). Only concrete instances are emitted, via the
GenericFuncInstances list. EmitStandaloneDefs already skips templates at
the program level; unit emission (GenerateUnit/AppendUnit) reaches here
without that guard, so enforce it centrally. }
if ADecl.TypeParams <> nil then Exit;
{ Emit any nested procedures declared inside this function's body before
emitting this function itself. Each nested proc gets a mangled name

View file

@ -101,6 +101,8 @@ type
procedure TestRun_Native_Single_GlobalReadWrite;
procedure TestRun_Native_Double_FuncParam;
procedure TestRun_Native_Double_FuncReturn;
procedure TestRun_Native_FloatCompareInOrAnd;
procedure TestRun_Native_LocalFloatConst;
{ M7c — string operations }
procedure TestRun_Native_String_WriteLnLiteral;
@ -1271,6 +1273,45 @@ const
end.
''';
{ Issue #107: a float comparison nested inside a short-circuit and/or
reaches EmitExprToEax (not EmitCondBranch), whose integer path could
not lower the TFloatLiteral operand — "unsupported expression form
TFloatLiteral". Exercise both `or` and `and`, both branches. }
SrcFloatCompareInOrAnd = '''
program Prg;
procedure Check(P: Double);
begin
if (P > 1.0) or (P < -1.0) then WriteLn('out') else WriteLn('in')
end;
procedure Both(P: Double);
begin
if (P > 0.0) and (P < 10.0) then WriteLn('mid') else WriteLn('off')
end;
begin
Check(2.0);
Check(0.5);
Check(-3.0);
Both(5.0);
Both(20.0)
end.
''';
{ Issue #107: a local float `const` was emitted as a reference to an
undefined symbol on the native backend — "undefined reference to
precalc". Inline its value instead. }
SrcLocalFloatConst = '''
program Prg;
function Circ(R: Double): Double;
const
TwoPi = 6.28318530718;
begin
Result := R * TwoPi
end;
begin
WriteLn(Circ(2.0))
end.
''';
{ Every test below runs its source through BOTH backends (beQBE, beNative)
and asserts identical stdout/exit on each — the native backend's whole
correctness model is parity with QBE on the same source, so this exercises
@ -1736,6 +1777,20 @@ begin
AssertRunsOnAll(SrcDoubleFuncReturn, '3.5' + LE, 0);
end;
procedure TE2ENativeTests.TestRun_Native_FloatCompareInOrAnd;
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end;
AssertRunsOnAll(SrcFloatCompareInOrAnd,
'out' + LE + 'in' + LE + 'out' + LE + 'mid' + LE + 'off' + LE, 0);
end;
procedure TE2ENativeTests.TestRun_Native_LocalFloatConst;
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end;
{ Circ(2.0) = 2.0 * 6.28318530718 = 12.56637061436 }
AssertRunsOnAll(SrcLocalFloatConst, '12.56637061436' + LE, 0);
end;
{ ------------------------------------------------------------------ }
{ M7c — string operations }
{ ------------------------------------------------------------------ }

View file

@ -45,6 +45,7 @@ type
published
procedure TestFreeRoutine_RoundTrip_WithoutSource;
procedure TestGenericClass_RoundTrip_WithoutSource;
procedure TestUninstantiatedGenericFunc_InUnit_Compiles;
end;
implementation
@ -270,6 +271,91 @@ begin
AssertEquals('use_box stdout', 'Box = 42' + #10, Captured)
end;
{ Regression for issue #107: a generic FUNCTION declared in a unit but never
instantiated was code-generated as a template, whose `T`-typed locals have
no resolved type — codegen raised "Variable 'MaxValue' has no resolved type
— semantic pass required". EmitStandaloneDefs skipped templates, but the
unit-emission path (GenerateUnit/AppendUnit → EmitFuncDef) did not. The
unit must compile to an .o even when nothing uses the generic. }
procedure TSepCompileTests.TestUninstantiatedGenericFunc_InUnit_Compiles;
const
DepSrc =
'''
unit MyMaxUnit;
interface
function Doubled(N: Integer): Integer;
function FindMaxValue<T>(arr: array of T): T;
implementation
function Doubled(N: Integer): Integer;
begin
Result := N * 2
end;
function FindMaxValue<T>(arr: array of T): T;
var
MaxValue: T;
x: UInt64;
begin
x := Low(arr);
MaxValue := arr[0];
while x <= High(arr) do
begin
if arr[x] > MaxValue then
MaxValue := arr[x];
Inc(x)
end;
Result := MaxValue
end;
end.
''';
ProgSrc =
'''
program UseMyMax;
uses MyMaxUnit;
begin
{ Use only the non-generic routine — the generic stays uninstantiated. }
WriteLn('Doubled(21) = ', Doubled(21))
end.
''';
var
DepPas, DepObj, 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;
DepPas := FScratch + '/MyMaxUnit.pas';
DepObj := FScratch + '/MyMaxUnit.o';
ProgPas := FScratch + '/use_mymax.pas';
ProgBin := FScratch + '/use_mymax';
{ Step 1: compile the unit. The uninstantiated generic template must be
skipped by codegen — this is the step that previously crashed. }
WriteFile(DepPas, DepSrc);
Rc := RunBlaise(['--source', DepPas, '--output', DepObj], Captured);
AssertEquals('blaise(MyMaxUnit) exit code 0' + #10 + Captured, 0, Rc);
AssertTrue('MyMaxUnit.o exists', FileExists(DepObj));
{ Step 2: a consumer that never instantiates the generic links and runs. }
WriteFile(ProgPas, ProgSrc);
Rc := RunBlaise(['--source', ProgPas, '--output', ProgBin,
'--unit-path', FScratch], Captured);
AssertEquals('blaise(use_mymax) exit code 0' + #10 + Captured, 0, Rc);
AssertTrue('use_mymax exists', FileExists(ProgBin));
Rc := RunBinary(ProgBin, Captured);
AssertEquals('use_mymax exit code', 0, Rc);
AssertEquals('use_mymax stdout', 'Doubled(21) = 42' + #10, Captured)
end;
initialization
RegisterTest(TSepCompileTests);