A constant expression that overflows Int64 stays signed
DONE 2026-09-09 (frankD) — the PROMOTION half. The second, smaller half is NOT done.
high(int64)+100 > 0now takes the positive arm, and fpc's owntoperator6.ppburns:rc=2under the pin (theint64operator was selected,halt(2)),rc=0after, matching fpc. That row's whole subject is this promotion.I nearly reported it as non-discriminating. Both compilers compile
toperator6.ppbefore and after — the assertion is in its exit code, viahalt(1)/halt(2), and I compared compile success first. The corpus ticket warns that exit-clean is not correct because the runner compares exit codes; the inverse bites too, and a row whose verdict IS the exit code says nothing at all when you only build it.Where it went, and why it is small
ParseSimpleExpr's ordinal arm, beside the rule that was already there for the operand-tag case ("a non-negative literal paired with a QWord operand joins the unsigned domain"). The new arm reaches the same domain by VALUE: both operands non-negative constants and the Int64 sum carrying into bit 63, which for two Int64-representable non-negatives can only mean the true value is in[2^63, 2^64)— exactly a QWord, and it cannot have wrapped further. Ordered BELOW the two tag arms so an already-unsigned operand keeps taking them.The ticket's plan assumed this needed a signed/unsigned tag threaded through constant arithmetic, like FPC's
Tconstexprint.signed. It did not: the existingTypeArithmeticResultOp(tyUInt64, tyUInt64, op)path already produces the right type, so the whole change is deciding when to enter it.What is NOT done, stated so nobody reads this as the whole ticket
*is not covered. A product can wrap PAST 2^64, where the value is representable in neither reading andlv*rv < 0stops meaning "landed in the unsigned band".high(int64)*2is still signed. Narrower coverage, not an inconsistency — nothing promotes in one direction while failing to demote in the other, which is the pairing this ticket warned about.-needed nothing, and that is a measurement, not an assumption: two non-negative operands cannot subtract into the sign bit, and a left operand already taggedtyUInt64is carried by the QWord arm.high(int64)+100-200stays unsigned and positive; it is a fixture row.- THE SECOND, SMALLER HALF IS UNTOUCHED — the one this ticket's own summary names last:
FindOpConvToDestis handedASTTk[rhs], so a conversion operator ranks a literal by its STATIC kind andb := 200picks the Int64 overload where fpc picks the Byte one. That is a different site and a different rule (rank by VALUE, asLiteralIntKindalready does for call arguments). Filed as [[bug-p-conversion-operator-ranking-reads-a-literals-static-kind-not-its-value]] rather than left inside adone/ticket, soreadycan hand it to somebody — and carrying the parent's warning thatLiteralIntKindtakes anInt64and so cannot even be passed a value in the unsigned band. I have NOT re-measured that observable at HEAD; it is frankS's from 2026-09-06.Fixture
test/test_const_fold_overflows_into_qword.pas, 10 rows,.expectedis fpc's own output. Three rows fail under the pin and pass after; six are identical on both sides on purpose — includingstoredandlit, because the VALUE was always right and a fixture built from stores would have been green throughout.gate.sh quickGREEN,converged after 1 round(s), compiler2a9e5179428f. Landed as commit 4a602ebb2.
- Found: 2026-09-06 (frankS), on
toperator6.ppfrom the FPC-testsuite corpus ([[feature-pascal-corpus-fpc-testsuite]]). - Measured at compiler
4b22a668e6abagainst fpc 3.2.2.
The observable, without any operator overloading in sight
var q: qword;
begin
q := high(int64)+100;
Writeln(q); { both: 9223372036854775907 }
if (high(int64)+100) > 0 then Writeln('positive') else Writeln('negative');
end.
fpc: 9223372036854775907 / positive
pxx: 9223372036854775907 / negative
The VALUE is right and the TYPE is wrong, which is why this hides: every store of the constant into a QWord gives fpc's bytes, and only a question that asks about its type — a comparison, an overload — reads back the signed view.
Why the literal fix does not reach it
10e670503 tags a decimal LITERAL above High(Int64) as tyUInt64 at its
creation site (ParseFactor) and carries the same fact through the const
evaluator (CEOrdTk). high(int64)+100 is neither: it is a FOLD, and pxx has
nowhere to record that a fold went unsigned. ConstEval returns a bare Int64;
the expression path types the binop from the two operand KINDS
(tyInt64 + tyInteger = tyInt64) and never looks at the result value.
FPC does have somewhere: Tconstexprint carries a signed flag beside the
value and flips it on overflow. toperator6.pp is not a coincidence here — the
record it declares is a hand-rolled copy of exactly that type.
What a fix has to decide
- Where the tag lives.
CEOrdTkis the existing precedent on the const-eval side and already carriesTkIsUnsigned64throughConstEvalTerm/Add; the EXPRESSION path has no equivalent and is the larger half. + - *overflowing into the sign bit with both operands non-negative is the promotion;-going negative is the demotion. Getting one without the other is worse than neither.- Do NOT reach for
LiteralIntKind(symtab.inc): it takesv: Int64, so a value aboveHigh(Int64)cannot even be passed to it, and it caps attyInt64and never returnstyUInt64.
The second, smaller half — ranking reads the static kind
Independent of the fold, and fixable on its own. FindOpConvToDest is handed
ASTTk[rhs], so an integer literal ranks as its STATIC kind. FPC types the
literal BY VALUE first (the rule pxx already implements for CALL arguments, via
LiteralIntKind in pasparser_lval.inc) and ranks that. Measured, two
conversion operators to one destination:
fpc pxx
operator :=(integer) / :=(int64)
a := 10 integer integer
a := 200 integer integer
a := 5000000000 int64 int64
operator :=(byte) / :=(int64)
b := 10 int64 int64
b := 200 byte int64 <-- diverges
b := 300 int64 int64
One row, and it names the rule: 200 types as Byte by value, which is an EXACT
hit on the byte parameter; 10 types as ShortInt, which prefers the same-signedness
Int64 over the unsigned Byte; 300 types as Word, which the Byte parameter cannot
hold. So OpConvSourceRank (symtab.inc) needs the source kind BY VALUE, a
"parameter can hold the value" filter, and a narrower-wins tiebreak. Every other
row above already agrees, so the change is aimed at one row and must not move
the other five.
Not taken
toperator6.pp stays skip-listed; its skip reason names both halves. Banked
rather than microfixed — the fold half is a representation change, not a patch.