Types
PXX implements a traditional Object Pascal type system: ordinals, real numbers, strings, enumerations, records, and arrays. Every example on this page compiles and runs on the pinned compiler.
This page is the tour. For exact sizes, array strides, record offsets and what
a file of T writes — and which of those you may rely on — see the
representation contract.
Ordinal types
Integers and the types built on them (Byte, Char, Boolean, enumerations).
Integer is 32-bit; Int64 is 64-bit.
| Type | Size | Range |
|---|---|---|
Byte |
1 | 0 … 255 |
ShortInt |
1 | -128 … 127 |
Word |
2 | 0 … 65535 |
SmallInt |
2 | -32768 … 32767 |
LongWord |
4 | 0 … 4294967295 |
Integer |
4 | -2147483648 … 2147483647 |
Int64 |
8 | signed 64-bit |
Boolean |
1 | False / True |
Char |
1 | a single byte |
Ordinal helpers: Ord, Succ, Pred, Inc, Dec, Low, High, Odd.
Real types
Single (4-byte), Double (8-byte), and Real. Write with a
field-width/precision suffix:
writeln(f:0:1); { 3.5 }
Real is the target's native float
Real is not a fixed alias for Double. It is the widest float the target
handles natively:
| Target | Real is |
SizeOf(Real) |
|---|---|---|
| x86-64, i386, aarch64, arm32 | Double |
8 |
| xtensa (ESP32), riscv32 (ESP32-C3) | Single |
4 |
This is deliberate and settled, not a gap waiting to be closed. The ESP class
has no hardware double, so a Double there is a software-emulated value that
costs both cycles and flash on parts that have little of either. Making Real
mean "the float this chip actually has" is the whole point of the name: code
written to Real gets the fast path everywhere, and code that genuinely needs
53 bits of mantissa says Double and gets it — emulated on ESP, but only where
it was actually asked for.
The practical consequences, on the ESP targets only:
SizeOf(Real)is 4, and anarray of Realstrides by 4.Realarithmetic carries about 7 decimal digits, so1.0/3.0is0.33333334, not0.33333333333333331.- A
Realwritten to a file or sent over a wire is 4 bytes. Anything shared with a host program must nameSingleorDoubleexplicitly rather thanReal, because the two ends disagree about whatRealmeans.
If you want the same width on every target, say Single or Double. Those two
names always mean exactly what they say.
This differs from FPC, where Real is Double on every supported platform.
It is one of the few places PXX deliberately parts company with FPC — see
FPC compatibility.
Strings
string is a managed, reference-counted, length-prefixed type — it grows
automatically and frees itself. Length, Copy, Pos, IntToStr, and +
concatenation all work on it. This is the default.
SizeOf(string) is the pointer width, because the variable is a handle rather
than the characters. A capacity-bounded string[N] is inline and fixed-width
instead — N+1 bytes for N up to 255, matching FPC exactly. The
representation contract has both,
including what happens above 255.
There is also an older frozen string ABI: a fixed-capacity inline buffer instead of a heap allocation, with no reference counting. Select it by undefining the managed-string symbol at compile time:
./pxx -uPXX_MANAGED_STRING hello.pas hello
The frozen ABI trades away automatic growth and copy-on-write for a smaller, simpler runtime footprint — useful for size-constrained targets or when you want to avoid heap traffic entirely. Prefer the default managed strings unless you have a specific reason to opt out.
Enumerations
type
TColor = (cRed, cGreen, cBlue);
Ord(cGreen) is 1. Enumerations are ordinals — usable in case, for, and
array indexing.
Records
type
TPoint = record
X, Y: Integer;
end;
Access fields with .. Records are value types — assignment copies the whole
record. Variant records (a case part sharing storage) are supported.
Advanced records
Records can also carry methods, visibility sections, constructors, and operator
overloads — they stay value types, but gain much of a class's surface without
heap allocation. Operators use the symbol form (class operator + (...)),
not Delphi's named form (class operator Add):
program advanced_record_demo;
type
TVec = record
X, Y: Integer;
constructor Create(ax, ay: Integer);
function Len2: Integer;
class operator + (const a, b: TVec): TVec;
end;
constructor TVec.Create(ax, ay: Integer);
begin
X := ax;
Y := ay;
end;
function TVec.Len2: Integer;
begin
Result := X * X + Y * Y;
end;
class operator TVec.+ (const a, b: TVec): TVec;
begin
Result.X := a.X + b.X;
Result.Y := a.Y + b.Y;
end;
var
a, b, c: TVec;
begin
a := TVec.Create(1, 2);
b := TVec.Create(3, 4);
c := a + b;
writeln(c.X, ',', c.Y, ' len2=', c.Len2); { 4,6 len2=52 }
end.
Arrays
Fixed arrays have a compile-time index range:
var fixed: array[1..3] of Integer;
Dynamic arrays start empty and are sized with SetLength; they are
0-indexed and managed:
var dyn: array of Integer;
...
SetLength(dyn, 2);
dyn[0] := 1;
writeln(Length(dyn)); { 2 }
Pointers and Typed Pointers
PXX supports low-level pointer operations, including typed pointers, address-of operations, and pointer arithmetic:
- Declaration: Declare a typed pointer using
^TwhereTis the target type. - Address-of (
@): Get the address of a variable or routine using the@operator. - Dereferencing (
^): Access the value pointed to by a pointer using the^suffix. - Pointer Arithmetic: Add or subtract integers to/from pointers to traverse memory.
- C Interop (
PChar): ThePChartype represents a pointer to a null-terminated string, useful for passing strings to C libraries. PXX automatically marshals Pascal strings to Cconst char*when calling C imports.
[!IMPORTANT] When writing portable code for both 32-bit and 64-bit targets, use
^NativeIntinstead of^Int64for pointer-sized integer storage. A write to^Int64is always 8 bytes and will overrun a 4-byte slot on 32-bit platforms.
var
x: Integer;
p: ^Integer;
begin
x := 42;
p := @x; { p points to x }
p^ := 100; { dereference and assign }
writeln(x); { prints 100 }
end;
Sets
Sets in PXX represent a collection of values of the same ordinal type (such as bytes, characters, or enumerations). A set is backed internally by a 32-byte bitset, supporting up to 256 elements. The width is 32 bytes whatever the declared bounds — see sets in the representation contract for the bit layout and how it lines up with FPC's narrower sets.
Set Operations
- Union (
+): Combines elements of both sets. - Difference (
-): Removes elements of the second set from the first. - Intersection (
*): Keeps only elements present in both sets. - Membership (
in): Checks if an element is in the set. - Comparisons (
<=,>=): Checks subset and superset relationships.
type
TCharSet = set of Char;
var
letters: TCharSet;
begin
letters := ['a', 'b', 'c'];
if 'b' in letters then
writeln('b is present');
letters := letters + ['d'] - ['a']; { ['b', 'c', 'd'] }
end;
Variants
PXX supports a built-in Variant type. A Variant can hold values of different types dynamically (such as integers, characters, real numbers, booleans, and strings) and can change its type at runtime through reassignment.
Key Characteristics
- Dynamic Typing: Assigning a value to a
Variantboxes the value and updates the variant's internal type tag. - Automatic Conversion: PXX automatically converts between compatible types during assignment or operations.
- Operations: You can perform arithmetic (
+,-,*,/,div,mod) and comparisons (=,<,>, etc.) directly onVariantvariables. The operation resolves dynamically based on the operand types at runtime. - String Support: Variants can hold managed Pascal strings (
AnsiString) and support concatenation and comparison.
[!NOTE] A boolean variant prints as
True/False, while a plainBooleanprints asTRUE/FALSE. That is not a PXX quirk — FPC prints the same six letters, for the same reason: the variant is rendered through its own string conversion, not throughWrite's boolean case.
var
v, w: Variant;
begin
v := 42; { v holds Integer }
writeln(v); { prints 42 }
v := 'Q'; { v now holds Char }
writeln(v); { prints Q }
v := 3.14; { v now holds Double }
writeln(v); { prints 3.14 }
v := 'hello ';
w := 'world';
writeln(v + w); { prints "hello world" }
end;
Converting a Variant to a scalar
Reading a Variant into a scalar converts it — it does not reinterpret the
stored bits. An assignment and a typecast are the same operation here, so
i := v and Int64(v) always agree.
Two rows of that conversion surprise people often enough to state outright.
A boolean variant converts to -1, not 1.
var v: Variant; b: Boolean;
begin
v := True;
writeln(Int64(v)); { -1 }
writeln(Byte(v)); { 255 }
writeln(Double(v):0:1); { -1.0 }
b := True;
writeln(Ord(True)); { 1 — unchanged }
writeln(Integer(b)); { 1 — unchanged }
end;
The -1 is OLE Automation's VARIANT_TRUE, which every COM consumer expects,
and it belongs to the variant conversion rather than to booleans in general
— hence the last two lines. FPC gives the same eight values.
Converting a Variant to Char is the one place PXX deliberately differs from
FPC. PXX answers Chr(n) for a numeric variant; FPC renders the variant to
its string form and takes character 1.
v |
Char(v) in PXX |
Char(v) in FPC, and in PXX under --strict-fpc |
|---|---|---|
65 |
A |
6 |
122 |
z |
1 |
2.5 |
#0 |
2 |
True |
#1 |
T |
'hi' |
h |
h |
This is the rare case where differing from FPC is the defensible side, so the reason is worth having when you port code:
- FPC contradicts itself here. The same numeric variant converts as a
number for
Byte,WordandInt64—Byte(v)is65— and as a string forChar. - The string rule comes from OLE Automation's
VARIANT, which has no character type at all, so Delphi defined aChartarget as "a string of length one" and FPC inherited it. - The intermediate is not something you could write by hand anyway:
c := someAnsiStringis a type error in FPC.
If you are porting code that relies on FPC's rule, --strict-fpc reproduces it
exactly, edges included — an empty-string variant yields #0 under both
compilers and both modes.
Putting it together
program types_demo;
type
TColor = (cRed, cGreen, cBlue);
TPoint = record
X, Y: Integer;
end;
TCharSet = set of Char;
var
i: Integer;
b: Byte;
f: Double;
c: TColor;
s: string;
fixed: array[1..3] of Integer;
dyn: array of Integer;
p: TPoint;
ptr: ^Integer;
letters: TCharSet;
v: Variant;
begin
i := -42;
b := 255;
f := 3.5;
c := cGreen;
s := 'pxx';
fixed[1] := 10; fixed[2] := 20; fixed[3] := 30;
SetLength(dyn, 2);
dyn[0] := 1; dyn[1] := 2;
p.X := 7; p.Y := 9;
// Pointer demo
ptr := @i;
ptr^ := 100;
// Set demo
letters := ['a', 'b', 'c'];
letters := letters + ['d'] - ['a'];
// Variant demo
v := 'variant string';
writeln(i, ' ', b, ' ', f:0:1, ' ', Ord(c));
writeln(s, ' len=', Length(s));
writeln(fixed[2], ' ', dyn[1], ' ', Length(dyn));
writeln(p.X, ',', p.Y);
writeln('ptr^: ', ptr^);
if 'b' in letters then writeln('b in set');
if not ('a' in letters) then writeln('a not in set');
writeln('v: ', v);
end.
Output:
100 255 3.5 1
pxx len=3
20 2 2
7,9
ptr^: 100
b in set
a not in set
v: variant string