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:

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:

[!IMPORTANT] When writing portable code for both 32-bit and 64-bit targets, use ^NativeInt instead of ^Int64 for pointer-sized integer storage. A write to ^Int64 is 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

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

[!NOTE] A boolean variant prints as True / False, while a plain Boolean prints as TRUE / 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 through Write'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:

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

Next