← board

An array of const integer arm picks the Int64 overload

Measured — differential, against the FPC oracle, on fcl-json's own sources

Same fpjson.pp, compiled both ways (pxx with --mimic-fpc):

                            pxx                    fpc
CreateJSON(Longint)      -> TJSONIntegerNumber     TJSONIntegerNumber   agree
CreateJSON(1)            -> TJSONIntegerNumber     TJSONIntegerNumber   agree
CreateJSONArray([1])[0]  -> TJSONInt64Number       TJSONIntegerNumber   DIVERGE

The direct overload resolution is right. Only the value routed through array of const comes out as Int64.

Where

fpjson.pp, VariantToJSON:

With Element do
  case VType of
    vtInteger : Result := CreateJSON(VInteger);   { -> TJSONIntegerNumber }
    ...
    vtInt64   : Result := CreateJSON(vInt64^);    { -> TJSONInt64Number   }

fpjson declares CreateJSON(Data: Integer), CreateJSON(Data: Int64), CreateJSON(Data: QWord) and CreateJSON(Data: NativeInt) as four separate overloads, and picks the result class from which one binds. So the question is either

  1. the TVarRec we build for a plain integer literal is tagged vtInt64 rather than vtInteger (wrong arm taken), or
  2. the arm is right and CreateJSON(VInteger) — a Longint field read through with — binds the Int64 (or NativeInt) overload.

Measure which before theorising; they are one case apart and lead to completely different fixes. A standalone Show([1]) printing VType gives 0 (vtInteger), which points at (2), but that probe did not go through fpjson's overload set.

Note CreateJSON(Data: NativeInt) is a live confounder on 64-bit: it is a third candidate that is layout-identical to Int64.

Repro

tools/install_lib_candidates.sh fcl-json
# stage the suite flat (see the test-fpjson recipe in the Makefile), then:
#   probe.pp:  uses fpjson; writeln(CreateJSONArray([1]).Items[0].ClassName)
# pxx --mimic-fpc  -> TJSONInt64Number
# fpc             -> TJSONIntegerNumber

Or just run the suite: TTestFactory.ArrayCreateInteger and TTestFactory.ObjectCreateInteger fail with "Correct class" expected: <TMyInteger> but was: <TMyInt64>.

Why prio 55

It is the entire remaining gap between the fpjson rung and its recorded 203/203 — the suite is at 201/203 — and it is a silent wrong type in a real library's public factory, not a diagnostic nicety. Below the segfault tier, above ordinary parity work.

Gate

make compiler/pascal26 + the fpjson suite reaching run: 203 failures: 0 errors: 0 + tools/gate.sh quick.

[[feature-pascal-corpus-fpjson]] · [[bug-a-the-fpjson-suite-overflows-the-fixed-4096-entry-data-ptr-fixup-table]]

Measured (frank1, 2026-08-26) — it was (2), then one layer further down

The ticket's own two candidates, resolved by probe against fpjson's real overload set rather than a standalone Show([1]):

Mimic([1]):
  VType         = 0        <- the TAG IS RIGHT, so not (1)
  arm taken     = vtInteger
  CreateJSON -> TJSONInt64Number
direct CreateJSON(Longint var)   -> TJSONIntegerNumber   <- binding is right
CreateJSON(E2.VInteger)          -> TJSONInt64Number     <- the field is not

So (2), and the with is innocent — a plain E.VInteger field read fails the same way. The cause is one line of our own RTL: builtinheap.TVarRec declared VInteger: NativeInt where FPC declares Longint. FixupTVarRecLayout right-sizes the record regardless, so the width bought no storage; all it did was pick a different overload at every reader. The ticket's third confounder (CreateJSON(NativeInt)) is exactly the one it handed the argument to.

Two errors were cancelling, which is why neither showed alone. AN_VARREC_ARRAY tagged a pointer-wide native int vtInteger and then stored eight bytes under a tag that promises four — harmless only while VInteger was NativeInt and read all eight back. Narrowing the field alone would have truncated Format('%d', [NativeInt]). FPC has no such pair: on a 64-bit target NativeInt IS Int64 and the element goes in as vtInt64, so both halves move together.

Suite: run: 203 failures: 0 errors: 0 — the recorded 203/203, reached.

Log