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:
parent
267e8db702
commit
bf63d7419f
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
{ ------------------------------------------------------------------ }
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue