← board

UnicodeChar is 4 bytes here and 2 in FPC

Found 2026-08-31 by the full builtin-scalar audit in [[bug-p-sizeof-extended-disagrees-with-the-storage-extended-gets]] — a by-product, not its subject.

Measured (x86-64, binary cf1d5398838a, vs fpc 3.2.2 -Mobjfpc)

name pxx SizeOf(T) pxx SizeOf(v) fpc
WideChar 2 2 2
UCS4Char 4 4 4
UnicodeChar 4 4 2

pxx is self-consistent: both its paths say 4. This is not the two-table split the parent ticket was about — it is one table with one entry to argue about.

Where

compiler/pasparser_lval.inc, BuiltinScalarTypeKind:

else if CaseEqual(nm, 'ucs4char') or CaseEqual(nm, 'unicodechar') then Result := tyUCS4Char

The two names share a line. ucs4char on that line is right and must not move: UCS4Char really is a code POINT (FPC: UCS4Char = type LongWord), and the note in defs.inc explains why it has its own kind — it converts to a string as UTF-8, unlike WideChar. Only unicodechar looks misplaced. In FPC UnicodeChar = WideChar, a UTF-16 code UNIT.

Why it is a bug and not a compat item

The two differ in KIND, not just width. A UnicodeChar here holds any code point and stringifies as UTF-8; in FPC it is one UTF-16 unit, and a character above the BMP takes two of them. So a record or array of UnicodeChar has both a different stride and different contents than the same declaration under FPC, and code that walks a UTF-16 buffer through the name is reading the wrong width with no diagnostic — the ordinary silent-wrong-values shape.

Why it is filed rather than fixed

The one-word change (unicodechar -> tyWideChar) is not obviously right, and that is the whole reason this is a ticket:

The grep is done, and the answer is zero. Measured 2026-08-31 over lib/, examples/, compiler/ (.pas and .inc): no declaration anywhere in the tree uses the name: UnicodeChar and of UnicodeChar both return nothing. So the first bullet above is not a live risk in-tree, and the change is cheap here. What it cannot tell us is what OUT-of-tree code expects, which is the whole content of the decision: FPC's answer is 2, and matching it is the default unless someone wants the code-point meaning kept under this spelling.

Leaving that as an instruction to the next reader would have been the wrong shape — it is one command, and a ticket that asks its reader to run a measurement its author could have run is a ticket that will be read and not acted on.

Gate

make compiler/pascal26, plus the row above added to test/test_sizeof_builtin_type_names.pas, which already pins the other two.

FIXED 2026-09-04 (frankB) — unicodechar moves to the widechar line

compiler/pasparser_lval.inc, both tables: OrdinalNameToTk and BuiltinScalarTypeKind. ucs4char stays where it was and keeps tyUCS4Char, which is the constraint this ticket was most careful about.

Measured after, against fpc 3.2.2 -Mobjfpc, every row identical:

pxx before pxx after fpc
SizeOf(UnicodeChar) / SizeOf(v) 4 / 4 2 / 2 2 / 2
SizeOf(array[0..3] of UnicodeChar) 16 8 8
s := uc (stringify) A A A
SizeOf(WideChar), SizeOf(UCS4Char) 2, 4 2, 4 2, 4

Two corrections to this ticket's own text

"the row above added to test/test_sizeof_builtin_type_names.pas, which already pins the other two" — it pinned NEITHER. That file had no WideChar and no UCS4Char row before today; its only near miss was PWideChar. All three rows are added now, both halves each (type name and variable), plus a widths line asserting 2 4 2. The pre-fix answer was 2 4 4, so the row is aimed.

"one entry that is probably wrong, sharing a line with ucs4char" — TWO entries, one in each of the two tables, on a line each. The ticket's larger point survives and is the reason this was cheap: they AGREED with each other, so this was never the two-table split its parent audit was about. It is one decision spelled twice.

The pin cannot be the control here

stable_linux_amd64/default/pinned cannot compile test_sizeof_builtin_type_names.pas at all — it predates bug-p-sizeof-of-a-type-name-is-settled-against-a-kind-that-cannot-express-the-size and answers SizeOf: unknown type or variable at line 86. The before-number 2 4 4 is from this session's own pre-change build, not from a revert cycle.

Controls run on the kind that must NOT move

test_ucs4char, test_variant_widechar_store (whose u-emoji row stores UCS4Char($1F600) into a Variant and still prints 😀 — the above-BMP code point this ticket said must keep working), test_pwidechar_cast, test_widechar_no_cast_in_program, test_widechar_to_utf8_b319, test_widechar_var_concat, test_widechar_var_to_string and test_widechar_var_to_string_arg. All green.

The RTL-pull prescan in pasparser_prog.inc needed no change: it pulls builtin on any mention of unicodechar, and widechar is on the same list for the same helpers.

Gate

make compiler/pascal26 converged; tools/gate.sh quick GREEN with the FPC seed canary CONCURRENT.

Log