feat(const): fold floating-point constant expressions at compile time (#108)
Constant declarations now accept arithmetic expressions involving float literals, named float constants, and the '/' operator. The semantic pass detects float-containing expressions and folds them to a single Double value at compile time, storing the result as a string — the same format used for bare float literals. The '/' operator always yields a float result even with integer operands, matching Delphi/FPC semantics (e.g. const X = 10 / 4 produces 2.5). Parser: extended ConstRhsStartsIntExpr to detect float-led and slash-containing expressions; added tkSlash to IsConstExprOp. Semantic: added IsFloatConstExpr (detects float leaves or '/' usage) and EvalConstFloatExpr (folds via libc strtod/snprintf to avoid a known self-hosting ABI issue with the Blaise StrToDouble built-in). Tests: 8 IR unit tests + 4 E2E tests (both backends). Grammar and language rationale updated.
This commit is contained in:
parent
f6d488bca4
commit
38c77c8a93
|
|
@ -777,7 +777,7 @@ end;
|
|||
function TParser.IsConstExprOp(AKind: TTokenKind): Boolean;
|
||||
begin
|
||||
Result := (AKind = tkPlus) or (AKind = tkMinus) or (AKind = tkStar)
|
||||
or (AKind = tkDiv) or (AKind = tkMod)
|
||||
or (AKind = tkSlash) or (AKind = tkDiv) or (AKind = tkMod)
|
||||
or (AKind = tkOr) or (AKind = tkAnd) or (AKind = tkXor)
|
||||
or (AKind = tkShl) or (AKind = tkShr);
|
||||
end;
|
||||
|
|
@ -789,16 +789,22 @@ begin
|
|||
Result := True
|
||||
else if Check(tkIntLit) and Self.IsConstExprOp(PeekKind()) then
|
||||
Result := True
|
||||
else if Check(tkFloatLit) and Self.IsConstExprOp(PeekKind()) then
|
||||
Result := True
|
||||
else if Check(tkMinus) and (PeekKind() = tkIntLit)
|
||||
and Self.IsConstExprOp(PeekKind2()) then
|
||||
Result := True
|
||||
else if Check(tkMinus) and (PeekKind() = tkFloatLit)
|
||||
and Self.IsConstExprOp(PeekKind2()) then
|
||||
Result := True
|
||||
{ Ident-led integer expression (e.g. BASE * 2). Only the unambiguously
|
||||
numeric operators trigger this — '+' is left to the string path so that
|
||||
string-concat consts ('a' + b) keep their existing behaviour, and the
|
||||
legacy ident-bitop chain handles or/and/xor/shl/shr starting on an ident. }
|
||||
else if Check(tkIdent) and
|
||||
((PeekKind() = tkStar) or (PeekKind() = tkDiv) or
|
||||
(PeekKind() = tkMod) or (PeekKind() = tkMinus)) then
|
||||
((PeekKind() = tkStar) or (PeekKind() = tkSlash) or
|
||||
(PeekKind() = tkDiv) or (PeekKind() = tkMod) or
|
||||
(PeekKind() = tkMinus)) then
|
||||
Result := True;
|
||||
end;
|
||||
|
||||
|
|
|
|||
|
|
@ -163,6 +163,8 @@ type
|
|||
references are resolved against the symbol table, so this must run after
|
||||
the referenced consts are declared. }
|
||||
function EvalConstIntExpr(AExpr: TASTExpr; ALine, ACol: Integer): Int64;
|
||||
function EvalConstFloatExpr(AExpr: TASTExpr; ALine, ACol: Integer): string;
|
||||
function IsFloatConstExpr(AExpr: TASTExpr): Boolean;
|
||||
procedure AnalyseTypeDecls(ABlock: TBlock);
|
||||
procedure LinkClassMethodImpls(ABlock: TBlock);
|
||||
procedure LinkGenericClassMethodImpls(ABlock: TBlock);
|
||||
|
|
@ -455,6 +457,22 @@ type
|
|||
|
||||
implementation
|
||||
|
||||
function _strtod(S: PChar; EndPtr: Pointer): Double; external name 'strtod';
|
||||
function _snprintf(Buf: PChar; Size: Int64; Fmt: PChar; V: Double): Integer; external name 'snprintf';
|
||||
|
||||
function RawDoubleToStr(V: Double): string;
|
||||
var
|
||||
Buf: array[0..63] of Byte;
|
||||
begin
|
||||
_snprintf(PChar(@Buf[0]), 64, PChar('%.15g'), V);
|
||||
Result := string(PChar(@Buf[0]));
|
||||
end;
|
||||
|
||||
function RawStrToDouble(const S: string): Double;
|
||||
begin
|
||||
Result := _strtod(PChar(S), nil);
|
||||
end;
|
||||
|
||||
function TSemanticAnalyser.GetSymbolTable: TSymbolTable;
|
||||
begin
|
||||
Result := FTable;
|
||||
|
|
@ -3664,6 +3682,99 @@ begin
|
|||
'Constant expression is not a compile-time integer', ALine, ACol);
|
||||
end;
|
||||
|
||||
function TSemanticAnalyser.IsFloatConstExpr(AExpr: TASTExpr): Boolean;
|
||||
var
|
||||
Bin: TBinaryExpr;
|
||||
Sym: TSymbol;
|
||||
begin
|
||||
Result := False;
|
||||
if AExpr = nil then Exit;
|
||||
if AExpr is TFloatLiteral then
|
||||
Exit(True);
|
||||
if AExpr is TIdentExpr then
|
||||
begin
|
||||
Sym := FTable.Lookup(TIdentExpr(AExpr).Name);
|
||||
if (Sym <> nil) and (Sym.Kind = skConstant) and
|
||||
(Sym.TypeDesc <> nil) and Sym.TypeDesc.IsFloat() then
|
||||
Exit(True);
|
||||
Exit;
|
||||
end;
|
||||
if AExpr is TBinaryExpr then
|
||||
begin
|
||||
Bin := TBinaryExpr(AExpr);
|
||||
if Bin.Op = boSlash then
|
||||
Exit(True);
|
||||
if IsFloatConstExpr(Bin.Left) or IsFloatConstExpr(Bin.Right) then
|
||||
Exit(True);
|
||||
end;
|
||||
end;
|
||||
|
||||
function TSemanticAnalyser.EvalConstFloatExpr(AExpr: TASTExpr;
|
||||
ALine, ACol: Integer): string;
|
||||
var
|
||||
Bin: TBinaryExpr;
|
||||
IdSym: TSymbol;
|
||||
L, R: Double;
|
||||
LStr: string;
|
||||
RStr: string;
|
||||
begin
|
||||
Result := '0';
|
||||
if AExpr = nil then Exit;
|
||||
|
||||
if AExpr is TFloatLiteral then
|
||||
Exit(TFloatLiteral(AExpr).Value);
|
||||
|
||||
if AExpr is TIntLiteral then
|
||||
Exit(IntToStr(TIntLiteral(AExpr).Value));
|
||||
|
||||
if AExpr is TIdentExpr then
|
||||
begin
|
||||
IdSym := FTable.Lookup(TIdentExpr(AExpr).Name);
|
||||
if (IdSym = nil) or (IdSym.Kind <> skConstant) then
|
||||
begin
|
||||
SemanticError(Format(
|
||||
'Constant expression references ''%s'', which is not a constant',
|
||||
[TIdentExpr(AExpr).Name]), ALine, ACol);
|
||||
Exit;
|
||||
end;
|
||||
if (IdSym.TypeDesc <> nil) and IdSym.TypeDesc.IsFloat() then
|
||||
Exit(IdSym.ConstString)
|
||||
else
|
||||
Exit(IntToStr(IdSym.ConstValue));
|
||||
end;
|
||||
|
||||
if AExpr is TBinaryExpr then
|
||||
begin
|
||||
Bin := TBinaryExpr(AExpr);
|
||||
LStr := EvalConstFloatExpr(Bin.Left, ALine, ACol);
|
||||
RStr := EvalConstFloatExpr(Bin.Right, ALine, ACol);
|
||||
L := RawStrToDouble(LStr);
|
||||
R := RawStrToDouble(RStr);
|
||||
case Bin.Op of
|
||||
boAdd: L := L + R;
|
||||
boSub: L := L - R;
|
||||
boMul: L := L * R;
|
||||
boSlash, boDiv:
|
||||
begin
|
||||
if R = 0.0 then
|
||||
begin
|
||||
SemanticError('Division by zero in constant expression', ALine, ACol);
|
||||
Exit;
|
||||
end;
|
||||
L := L / R;
|
||||
end;
|
||||
else
|
||||
SemanticError(
|
||||
'Unsupported operator in floating-point constant expression', ALine, ACol);
|
||||
Exit;
|
||||
end;
|
||||
Exit(RawDoubleToStr(L));
|
||||
end;
|
||||
|
||||
SemanticError(
|
||||
'Constant expression is not a compile-time float', ALine, ACol);
|
||||
end;
|
||||
|
||||
procedure TSemanticAnalyser.AnalyseSetConstDecl(ACD: TConstDecl);
|
||||
var
|
||||
I: Integer;
|
||||
|
|
@ -3837,9 +3948,13 @@ begin
|
|||
named-constant values in scope. }
|
||||
if CD.IntExprTokens <> nil then
|
||||
CD.IntVal := FoldConstBitOpExpr(CD.IntExprTokens, CD.Line, CD.Col);
|
||||
{ Compile-time integer expression (arithmetic/bitwise with precedence):
|
||||
fold the AST now that prior consts in scope are resolvable. }
|
||||
if CD.IntValueExpr <> nil then
|
||||
if (CD.IntValueExpr <> nil) and IsFloatConstExpr(CD.IntValueExpr) then
|
||||
begin
|
||||
CD.StrVal := EvalConstFloatExpr(CD.IntValueExpr, CD.Line, CD.Col);
|
||||
CD.IsFloat := True;
|
||||
CD.IsString := False;
|
||||
end
|
||||
else if CD.IntValueExpr <> nil then
|
||||
CD.IntVal := EvalConstIntExpr(CD.IntValueExpr, CD.Line, CD.Col);
|
||||
{ Set-valued constants reference enum members, which are not registered
|
||||
until AnalyseTypeDecls runs — so they are resolved in the second pass
|
||||
|
|
|
|||
|
|
@ -132,6 +132,16 @@ type
|
|||
procedure TestConstExpr_UnaryMinus_Folds;
|
||||
procedure TestConstExpr_MixedArithAndBitwise_Folds;
|
||||
procedure TestArrayConst_OrChainElement_FoldsToFinalInteger;
|
||||
|
||||
{ Compile-time floating-point constant expressions (issue #108) }
|
||||
procedure TestConstExpr_FloatMultiply_Folds;
|
||||
procedure TestConstExpr_FloatDivide_Folds;
|
||||
procedure TestConstExpr_FloatAddSub_Folds;
|
||||
procedure TestConstExpr_FloatParens_Folds;
|
||||
procedure TestConstExpr_FloatMixedIntFloat_Folds;
|
||||
procedure TestConstExpr_FloatNamedConst_Folds;
|
||||
procedure TestConstExpr_NegativeFloat_Folds;
|
||||
procedure TestConstExpr_IntSlash_ProducesFloat;
|
||||
end;
|
||||
|
||||
implementation
|
||||
|
|
@ -1540,6 +1550,124 @@ begin
|
|||
AssertTrue('99 also in $T data item', IRContains(IR, 'w 99'));
|
||||
end;
|
||||
|
||||
{ ------------------------------------------------------------------ }
|
||||
{ Compile-time floating-point constant expressions (issue #108) }
|
||||
{ ------------------------------------------------------------------ }
|
||||
|
||||
procedure TConstTests.TestConstExpr_FloatMultiply_Folds;
|
||||
var IR: string;
|
||||
begin
|
||||
IR := GenIR(
|
||||
'''
|
||||
program P;
|
||||
const X = 3.0 * 2.0;
|
||||
var V: Double;
|
||||
begin V := X end.
|
||||
''');
|
||||
AssertTrue('IR non-empty', IR <> '');
|
||||
AssertTrue('folded to 6', IRContains(IR, 'd_6'));
|
||||
end;
|
||||
|
||||
procedure TConstTests.TestConstExpr_FloatDivide_Folds;
|
||||
var IR: string;
|
||||
begin
|
||||
IR := GenIR(
|
||||
'''
|
||||
program P;
|
||||
const X = 10.0 / 4.0;
|
||||
var V: Double;
|
||||
begin V := X end.
|
||||
''');
|
||||
AssertTrue('IR non-empty', IR <> '');
|
||||
AssertTrue('folded to 2.5', IRContains(IR, 'd_2.5'));
|
||||
end;
|
||||
|
||||
procedure TConstTests.TestConstExpr_FloatAddSub_Folds;
|
||||
var IR: string;
|
||||
begin
|
||||
IR := GenIR(
|
||||
'''
|
||||
program P;
|
||||
const X = 1.5 + 2.5;
|
||||
var V: Double;
|
||||
begin V := X end.
|
||||
''');
|
||||
AssertTrue('IR non-empty', IR <> '');
|
||||
AssertTrue('folded to 4', IRContains(IR, 'd_4'));
|
||||
end;
|
||||
|
||||
procedure TConstTests.TestConstExpr_FloatParens_Folds;
|
||||
var IR: string;
|
||||
begin
|
||||
IR := GenIR(
|
||||
'''
|
||||
program P;
|
||||
const X = (1.0 + 2.0) * 3.0;
|
||||
var V: Double;
|
||||
begin V := X end.
|
||||
''');
|
||||
AssertTrue('IR non-empty', IR <> '');
|
||||
AssertTrue('folded to 9', IRContains(IR, 'd_9'));
|
||||
end;
|
||||
|
||||
procedure TConstTests.TestConstExpr_FloatMixedIntFloat_Folds;
|
||||
var IR: string;
|
||||
begin
|
||||
IR := GenIR(
|
||||
'''
|
||||
program P;
|
||||
const X = 2 * 1.5;
|
||||
var V: Double;
|
||||
begin V := X end.
|
||||
''');
|
||||
AssertTrue('IR non-empty', IR <> '');
|
||||
AssertTrue('folded to 3', IRContains(IR, 'd_3'));
|
||||
end;
|
||||
|
||||
procedure TConstTests.TestConstExpr_FloatNamedConst_Folds;
|
||||
var IR: string;
|
||||
begin
|
||||
IR := GenIR(
|
||||
'''
|
||||
program P;
|
||||
const
|
||||
PI = 3.14;
|
||||
TAU = PI * 2.0;
|
||||
var V: Double;
|
||||
begin V := TAU end.
|
||||
''');
|
||||
AssertTrue('IR non-empty', IR <> '');
|
||||
AssertTrue('folded to 6.28', IRContains(IR, 'd_6.28'));
|
||||
end;
|
||||
|
||||
procedure TConstTests.TestConstExpr_NegativeFloat_Folds;
|
||||
var IR: string;
|
||||
begin
|
||||
IR := GenIR(
|
||||
'''
|
||||
program P;
|
||||
const X = -1.5 * 2.0;
|
||||
var V: Double;
|
||||
begin V := X end.
|
||||
''');
|
||||
AssertTrue('IR non-empty', IR <> '');
|
||||
AssertTrue('folded to -3', IRContains(IR, 'd_-3'));
|
||||
end;
|
||||
|
||||
procedure TConstTests.TestConstExpr_IntSlash_ProducesFloat;
|
||||
var IR: string;
|
||||
begin
|
||||
IR := GenIR(
|
||||
'''
|
||||
program P;
|
||||
const X = 10 / 4;
|
||||
var V: Double;
|
||||
begin V := X end.
|
||||
''');
|
||||
AssertTrue('IR non-empty', IR <> '');
|
||||
AssertTrue('folded to 2.5', IRContains(IR, 'd_2.5'));
|
||||
end;
|
||||
|
||||
initialization
|
||||
RegisterTest(TConstTests);
|
||||
|
||||
|
|
|
|||
|
|
@ -155,6 +155,12 @@ type
|
|||
{ Integer arguments to float builtins + float typecasts }
|
||||
procedure TestRun_Trig_IntegerArgs;
|
||||
procedure TestRun_FloatCast_FromInteger;
|
||||
|
||||
{ Float constant expressions (issue #108) }
|
||||
procedure TestRun_FloatConstExpr_MulDiv;
|
||||
procedure TestRun_FloatConstExpr_MixedIntFloat;
|
||||
procedure TestRun_FloatConstExpr_NamedRef;
|
||||
procedure TestRun_FloatConstExpr_IntSlash;
|
||||
end;
|
||||
|
||||
implementation
|
||||
|
|
@ -1535,6 +1541,63 @@ begin
|
|||
''', '32' + LE + '64' + LE + '1000' + LE, 0);
|
||||
end;
|
||||
|
||||
{ ------------------------------------------------------------------ }
|
||||
{ Float constant expressions (issue #108) }
|
||||
{ ------------------------------------------------------------------ }
|
||||
|
||||
procedure TE2EMathTests.TestRun_FloatConstExpr_MulDiv;
|
||||
begin
|
||||
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end;
|
||||
AssertRunsOnAll('''
|
||||
program P;
|
||||
const
|
||||
X = 3.0 * 2.0;
|
||||
Y = 10.0 / 4.0;
|
||||
begin
|
||||
WriteLn(Round(X));
|
||||
WriteLn(Y)
|
||||
end.
|
||||
''', '6' + LE + '2.5' + LE, 0);
|
||||
end;
|
||||
|
||||
procedure TE2EMathTests.TestRun_FloatConstExpr_MixedIntFloat;
|
||||
begin
|
||||
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end;
|
||||
AssertRunsOnAll('''
|
||||
program P;
|
||||
const X = 2 * 1.5;
|
||||
begin
|
||||
WriteLn(X)
|
||||
end.
|
||||
''', '3' + LE, 0);
|
||||
end;
|
||||
|
||||
procedure TE2EMathTests.TestRun_FloatConstExpr_NamedRef;
|
||||
begin
|
||||
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end;
|
||||
AssertRunsOnAll('''
|
||||
program P;
|
||||
const
|
||||
PI = 3.14;
|
||||
TAU = PI * 2.0;
|
||||
begin
|
||||
WriteLn(TAU)
|
||||
end.
|
||||
''', '6.28' + LE, 0);
|
||||
end;
|
||||
|
||||
procedure TE2EMathTests.TestRun_FloatConstExpr_IntSlash;
|
||||
begin
|
||||
if not ToolchainAvailable() then begin Ignore('toolchain unavailable'); Exit; end;
|
||||
AssertRunsOnAll('''
|
||||
program P;
|
||||
const X = 10 / 4;
|
||||
begin
|
||||
WriteLn(X)
|
||||
end.
|
||||
''', '2.5' + LE, 0);
|
||||
end;
|
||||
|
||||
initialization
|
||||
RegisterTest(TE2EMathTests);
|
||||
|
||||
|
|
|
|||
|
|
@ -515,32 +515,45 @@ ConstArrayValue
|
|||
{ COMMA ( ConstArrayValue | ConstExpr ) } RPAREN
|
||||
;
|
||||
|
||||
(* A constant value position. A scalar const may be a string/float literal or
|
||||
* a compile-time INTEGER expression: integer literals, named-constant
|
||||
* references, the binary operators ( + - * div mod and or xor shl shr ), unary
|
||||
* minus, and parentheses, with the usual operator precedence. The integer
|
||||
* expression is folded to a single value by the semantic pass (which resolves
|
||||
* named-constant references), so it may appear anywhere a constant is required. *)
|
||||
(* A constant value position. A scalar const may be a string/float literal, a
|
||||
* compile-time integer expression, or a compile-time float expression.
|
||||
*
|
||||
* INTEGER expressions: integer literals, named integer-constant references,
|
||||
* binary operators ( + - * div mod and or xor shl shr ), unary minus, and
|
||||
* parentheses, with the usual operator precedence.
|
||||
*
|
||||
* FLOAT expressions: any expression that contains at least one float literal,
|
||||
* a named float constant, or uses the '/' operator (which always yields a
|
||||
* float result, even with integer operands — matching Delphi/FPC semantics).
|
||||
* Supports the arithmetic operators + - * / div, unary minus, named-constant
|
||||
* references, and parentheses.
|
||||
*
|
||||
* Both integer and float expressions are folded to a single value by the
|
||||
* semantic pass, which resolves named-constant references. *)
|
||||
ConstExpr
|
||||
= ConstIntExpr
|
||||
| [ MINUS ] FloatLit
|
||||
= ConstArithExpr
|
||||
| StrLit { PLUS StrLit } (* string constant, with concatenation *)
|
||||
| IDENT (* named constant reference, e.g. True / False *)
|
||||
;
|
||||
|
||||
ConstIntExpr
|
||||
= ConstIntTerm { ( PLUS | MINUS | OR | XOR ) ConstIntTerm }
|
||||
(* Arithmetic constant expression — covers both integer and float. The
|
||||
* semantic pass inspects the AST: if any leaf is a float literal, a named
|
||||
* float constant, or the '/' operator is used, the expression is folded as
|
||||
* a float; otherwise it is folded as an integer. *)
|
||||
ConstArithExpr
|
||||
= ConstArithTerm { ( PLUS | MINUS | OR | XOR ) ConstArithTerm }
|
||||
;
|
||||
|
||||
ConstIntTerm
|
||||
= ConstIntFactor { ( STAR | DIV | MOD | AND | SHL | SHR ) ConstIntFactor }
|
||||
ConstArithTerm
|
||||
= ConstArithFactor { ( STAR | SLASH | DIV | MOD | AND | SHL | SHR ) ConstArithFactor }
|
||||
;
|
||||
|
||||
ConstIntFactor
|
||||
ConstArithFactor
|
||||
= IntLit
|
||||
| IDENT (* named integer constant *)
|
||||
| MINUS ConstIntFactor (* unary minus *)
|
||||
| LPAREN ConstIntExpr RPAREN
|
||||
| FloatLit
|
||||
| IDENT (* named integer or float constant *)
|
||||
| MINUS ConstArithFactor (* unary minus *)
|
||||
| LPAREN ConstArithExpr RPAREN
|
||||
;
|
||||
|
||||
(* ConstDecl supports untyped, scalar-typed, array-typed, and set-typed forms:
|
||||
|
|
|
|||
|
|
@ -4676,3 +4676,50 @@ serialised and re-instantiated later.
|
|||
are tracked separately (see `docs/future-improvements.adoc`); because range
|
||||
expansion is base-type-agnostic, `[1..3]` will work automatically once they
|
||||
land.
|
||||
|
||||
== Floating-Point Constant Expressions
|
||||
|
||||
=== Decision
|
||||
|
||||
Constant declarations accept compile-time floating-point expressions, not only
|
||||
bare float literals. The following forms are supported:
|
||||
|
||||
[source,pascal]
|
||||
----
|
||||
const
|
||||
Pi = 3.14159; { bare literal — already supported }
|
||||
Tau = Pi * 2.0; { named const × literal }
|
||||
Half = 10 / 4; { integer / integer → float (2.5) }
|
||||
Mixed = 2 * 1.5 + 0.5; { integer × float + float → float }
|
||||
Neg = -1.5 * 2.0; { unary minus propagates through float }
|
||||
----
|
||||
|
||||
The `/` operator (real division) always produces a float result, even when both
|
||||
operands are integers. This matches Delphi and FPC semantics and matches the
|
||||
existing runtime behaviour of the `/` operator on variables. Integer division
|
||||
is expressed via `div`.
|
||||
|
||||
=== Rationale
|
||||
|
||||
Before this change, any expression involving a float literal or the `/` operator
|
||||
in a constant declaration was rejected — the constant folding path only handled
|
||||
integer arithmetic. Users had to inline magic numbers or break them into
|
||||
separate `var` initialisations, losing the self-documenting benefit of named
|
||||
constants.
|
||||
|
||||
The semantic pass detects a float expression (at least one float literal, a
|
||||
named float constant, or use of `/`) and dispatches to a float-specific
|
||||
constant folder. The folder performs arithmetic using the host's IEEE 754
|
||||
`Double` type and stores the result as a string in the constant declaration,
|
||||
exactly as bare float literals are stored.
|
||||
|
||||
=== Alternatives considered
|
||||
|
||||
* *Defer to runtime evaluation.* Rejected. Constants must be folded at compile
|
||||
time; emitting runtime arithmetic for `const X = 3.14 * 2.0` would violate
|
||||
the constant contract and prevent their use in contexts requiring true constants
|
||||
(e.g. array bounds, case labels).
|
||||
* *Support only the four arithmetic operators.* Adopted. Bitwise operators
|
||||
(`and`, `or`, `xor`, `shl`, `shr`) have no meaningful float semantics. The
|
||||
`mod` operator could theoretically map to `fmod`, but there is no demonstrated
|
||||
need. Integer-only operators remain available in integer constant expressions.
|
||||
|
|
|
|||
Loading…
Reference in a new issue