fix: 64-bit integer constants — no truncation, accept full bit pattern (issue #133)

Two related defects in integer constant handling:

- A large hex literal that needs more than 32 bits (e.g. $080808080808 =
  8830587504648) was silently TRUNCATED to 32 bits (printed 134744072).  An
  untyped integer constant was unconditionally typed as Integer regardless of
  its magnitude, so codegen treated it as a 32-bit word.

- A literal above High(Int64) (e.g. $8080808080808080) was REJECTED at parse
  time with "Integer literal exceeds Int64 range", even though it is a valid
  64-bit bit pattern and is accepted by FPC/Delphi.

Fixes:

- uParser: the bare-integer-literal const path now uses ParseIntOrUInt64Literal
  (which already existed for the tolerant case) instead of ParseIntLiteral, so a
  value in the UInt64 range is captured as a bit pattern and flagged via a new
  TConstDecl.IsUInt64.

- uSemantic (AnalyseConstDecls): an untyped integer constant now picks its type
  by magnitude — Integer when it fits in signed 32-bit, Int64 when larger, and
  UInt64 when the value exceeds High(Int64) (matching Delphi, which prints
  $8080808080808080 as 9259542123273814144).  A typed const (e.g. A: Int64 =
  $8080808080808080) keeps its declared type and stores the bit pattern, so it
  prints -9187201950435737472.

Tests (cp.test.e2e.misc.pas, dual-backend): large hex not truncated, typed
Int64 bit pattern, typed UInt64 bit pattern, and untyped-above-Int64 widening
to UInt64.

Verified: FIXPOINT_OK, NATIVE_FIXPOINT_OK, NATIVE_INTERNAL_OK; full suite green
on both the QBE-built and native-built runners (3704 tests).
This commit is contained in:
Graeme Geldenhuys 2026-06-20 18:30:12 +01:00
parent fe5460f79b
commit c5d49ba03e
4 changed files with 77 additions and 2 deletions

View file

@ -574,7 +574,8 @@ type
public
Name: string;
TypeName: string; { non-empty when a type annotation was written }
IntVal: Int64; { used when kind = integer }
IntVal: Int64; { used when kind = integer (bit pattern when IsUInt64) }
IsUInt64: Boolean; { value exceeded High(Int64): IntVal is the UInt64 bit pattern }
StrVal: string; { used when IsString = True or IsFloat = True (raw text) }
IsString: Boolean;
IsFloat: Boolean; { set when the rhs is a float literal }

View file

@ -1395,13 +1395,18 @@ begin
end
else if Check(tkIntLit) then
begin
CD.IntVal := ParseIntLiteral(FCurrent.Value);
{ Accept the full 64-bit unsigned range as a bit pattern (e.g.
$8080808080808080): ParseIntOrUInt64Literal yields the bit pattern and
flags when the value exceeded High(Int64). This is the Int64/UInt64
hex-constant case from issue #133. }
ParseIntOrUInt64Literal(FCurrent.Value, CD.IntVal, CD.IsUInt64);
CD.IsString := False;
Advance();
if CurrentIsConstBitOp() then
begin
CD.IntExprTokens := CollectConstBitOpExpr(IntToStr(CD.IntVal), False);
CD.IntVal := 0;
CD.IsUInt64 := False;
end;
end
else if Check(tkIdent) and (PeekKind() = tkLParen)

View file

@ -4282,6 +4282,14 @@ begin
TD := FTable.TypeString
else if CD.IsFloat then
TD := FTable.TypeDouble
{ Untyped integer constant: pick the width by magnitude so a value that does
not fit in 32 bits is NOT silently truncated to Integer (issue #133). A
UInt64-range bit pattern (above High(Int64)) types as UInt64; a value
outside the signed 32-bit range types as Int64; otherwise Integer. }
else if CD.IsUInt64 then
TD := FTable.TypeUInt64
else if (CD.IntVal > 2147483647) or (CD.IntVal < -2147483648) then
TD := FTable.TypeInt64
else
TD := FTable.TypeInteger;
Sym := TSymbol.Create(CD.Name, skConstant, TD);

View file

@ -122,6 +122,15 @@ type
{ forward; in a program decl section (issue #130 bug2): the forward decl
used to swallow the following implementation as a nested-proc body. }
procedure TestRun_Forward_MutualRecursion;
{ Large / 64-bit integer constants (issue #133): a hex value above 32 bits
was truncated (inferred Integer); a value above High(Int64) was rejected.
Untyped consts now widen by magnitude; typed Int64/UInt64 accept the full
64-bit bit pattern. }
procedure TestRun_Const_LargeHex_NotTruncated;
procedure TestRun_Const_Int64BitPattern;
procedure TestRun_Const_UInt64BitPattern;
procedure TestRun_Const_UntypedAboveInt64_IsUInt64;
end;
implementation
@ -1224,6 +1233,58 @@ begin
AssertRunsOnAll(Src, '200 -7 250' + LE, 0);
end;
procedure TE2EMiscTests.TestRun_Const_LargeHex_NotTruncated;
const
{ $080808080808 = 8830587504648, needs 64 bits — must not truncate to 32. }
Src = '''
program P;
const DECI = $080808080808;
begin WriteLn(DECI) end.
''';
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end;
AssertRunsOnAll(Src, '8830587504648' + LE, 0);
end;
procedure TE2EMiscTests.TestRun_Const_Int64BitPattern;
const
{ $8080808080808080 as Int64 is the bit pattern -9187201950435737472. }
Src = '''
program P;
const A: Int64 = $8080808080808080;
begin WriteLn(A) end.
''';
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end;
AssertRunsOnAll(Src, '-9187201950435737472' + LE, 0);
end;
procedure TE2EMiscTests.TestRun_Const_UInt64BitPattern;
const
{ Same bits as UInt64 = 9259542123273814144. }
Src = '''
program P;
const A: UInt64 = $8080808080808080;
begin WriteLn(A) end.
''';
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end;
AssertRunsOnAll(Src, '9259542123273814144' + LE, 0);
end;
procedure TE2EMiscTests.TestRun_Const_UntypedAboveInt64_IsUInt64;
const
{ An untyped literal above High(Int64) types as UInt64 (matches Delphi). }
Src = '''
program P;
const A = $8080808080808080;
begin WriteLn(A) end.
''';
begin
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end;
AssertRunsOnAll(Src, '9259542123273814144' + LE, 0);
end;
procedure TE2EMiscTests.TestRun_Forward_MutualRecursion;
const
{ Mutually-recursive routines via a forward; declaration in the program's