Coming from Free Pascal

If you write Free Pascal, you already write the language PXX compiles. PXX aims at the Object Pascal that FPC accepts, in objfpc and Delphi mode. It does not reproduce FPC's world around the language: its RTL and packages, its command line, and its unit and object files. This page lists what carries over unchanged, what the mode lines do, where the two compilers give different answers, and how to build a small FPC program.

What was checked. Every program on this page was compiled with pin v447 (compiler sha256 fad87004e4e8) and with FPC 3.2.2 (fpc -O1) on x86-64 Linux on 2026-09-28. Both binaries were run. Where the output is the same, the page shows it once; where it differs, it shows both. Every program was run again with pin v450 (sha256 c19cc2d531e4) on 2026-09-28, with the flags the page names (--strict-fpc, --mimic-fpc, fpc -Sa), and printed what the page shows.

What compiles unchanged

Ordinary FPC programs are the best fit: records, arrays, classes and inheritance, properties, generics, exceptions, for ... in, units, and the common parts of SysUtils and Classes. This program uses all of them and prints the same thing under both compilers:

program shapes;
{$mode objfpc}{$H+}
uses SysUtils, Classes;

type
  EEmptyStack = class(Exception);

  TShape = class
  private
    FName: string;
  public
    constructor Create(const AName: string);
    function Area: Double; virtual; abstract;
    property Name: string read FName;
  end;

  TCircle = class(TShape)
  private
    FR: Double;
  public
    constructor Create(AR: Double);
    function Area: Double; override;
  end;

  TRectangle = class(TShape)
  private
    FW, FH: Double;
  public
    constructor Create(AW, AH: Double);
    function Area: Double; override;
  end;

  generic TStack<T> = class
  private
    FItems: array of T;
  public
    procedure Push(const V: T);
    function Pop: T;
    function Count: Integer;
  end;

  TIntStack = specialize TStack<Integer>;

constructor TShape.Create(const AName: string);
begin
  FName := AName;
end;

constructor TCircle.Create(AR: Double);
begin
  inherited Create('circle');
  FR := AR;
end;

function TCircle.Area: Double;
begin
  Result := Pi * FR * FR;
end;

constructor TRectangle.Create(AW, AH: Double);
begin
  inherited Create('rectangle');
  FW := AW; FH := AH;
end;

function TRectangle.Area: Double;
begin
  Result := FW * FH;
end;

procedure TStack.Push(const V: T);
begin
  SetLength(FItems, Length(FItems) + 1);
  FItems[High(FItems)] := V;
end;

function TStack.Pop: T;
begin
  if Length(FItems) = 0 then
    raise EEmptyStack.Create('pop from an empty stack');
  Result := FItems[High(FItems)];
  SetLength(FItems, Length(FItems) - 1);
end;

function TStack.Count: Integer;
begin
  Result := Length(FItems);
end;

var
  all: array of TShape;
  s: TShape;
  names: TStringList;
  st: TIntStack;
  i: Integer;
begin
  SetLength(all, 3);
  all[0] := TCircle.Create(1.5);
  all[1] := TRectangle.Create(2, 3.5);
  all[2] := TCircle.Create(0.5);
  for s in all do
    WriteLn(Format('%-10s %8.3f', [s.Name, s.Area]));

  names := TStringList.Create;
  try
    names.CommaText := 'pear,apple,fig,banana';
    names.Sort;
    WriteLn(names.CommaText, ' (', names.Count, ')');
    WriteLn(UpperCase(names[0]), ' ', Pos('an', names[1]), ' ', Copy(names[3], 1, 2));
  finally
    names.Free;
  end;

  st := TIntStack.Create;
  for i := 1 to 3 do st.Push(i * i);
  while st.Count > 0 do Write(st.Pop, ' ');
  WriteLn;
  try
    st.Pop;
  except
    on E: EEmptyStack do WriteLn('caught ', E.ClassName, ': ', E.Message);
  end;
  st.Free;
  for s in all do s.Free;
end.
circle        7.069
rectangle     7.000
circle        0.785
apple,banana,fig,pear (4)
APPLE 2 pe
9 4 1
caught EEmptyStack: pop from an empty stack

The 9 4 1 line ends with a space, because the loop writes one after each number (Write(st.Pop, ' ')). FPC 3.2.2 prints the same bytes.

On a larger scale, PXX runs a curated 550 programs of FPC 3.2.2's own test suite (tools/run_pascal_conformance.sh). With pin v441, 427 pass and 0 fail, and the same with v451 (2026-09-29); 50 do not apply here, and 73 are skipped, each with a written reason (see the examples showcase).

Mode lines

PXX has one dialect. {$mode objfpc}, {$mode fpc}, {$mode tp}, {$mode delphiunicode} and -Mobjfpc are accepted and change nothing. {$mode delphi} changes two things:

Delphi's generic syntax, with no generic or specialize keyword, is accepted in that mode as in FPC:

program delphimode;
{$mode delphi}
type
  TIntFunc = function(x: Integer): Integer;

  TPair<T> = record
    A, B: T;
    function Swapped: TPair<T>;
  end;

function Twice(x: Integer): Integer;
begin
  Result := 2 * x;
end;

function TPair<T>.Swapped: TPair<T>;
begin
  Result.A := B;
  Result.B := A;
end;

var
  f: TIntFunc;
  p: TPair<Integer>;
begin
  f := Twice;          { no @ in delphi mode }
  p.A := 1; p.B := 2;
  p := p.Swapped;
  WriteLn(f(21), ' ', p.A, ' ', p.B);
end.

It prints 42 2 1 under both. {$mode iso} and {$mode extendedpascal} give a warning, and {$mode macpas} is an error. The whole model, including the strictness switches, is on Compiler modes.

To tell the two compilers apart in source, test PXX. PXX does not define FPC:

program ifdefs;
begin
{$ifdef PXX}
  WriteLn('built by pxx');
{$endif}
{$ifdef FPC}
  WriteLn('built by fpc');
{$endif}
end.

What gives a different answer

These are deliberate. Each was decided rather than left as a bug, and each compiles under both compilers. The outputs are from the programs as shown.

Shifts on a 32-bit Integer happen at full width. FPC masks the shift count to 5 bits and truncates to 32 bits; PXX does not lose bits. --strict-fpc gives FPC's answers:

program shifts;
{$mode objfpc}
var a, b: Integer;
begin
  a := -8;
  WriteLn(a shr 1);
  a := 8; b := 40;
  WriteLn(a shl b);
  a := 1;
  WriteLn(a shl 31);
end.
FPC 3.2.2 PXX PXX --strict-fpc
a shr 1, a = -8 2147483644 9223372036854775804 2147483644
a shl b, 8 and 40 2048 8796093022208 2048
a shl 31, a = 1 -2147483648 2147483648 -2147483648

Assert is on by default. FPC ignores Assert unless you build with -Sa; PXX checks it. With fpc -Sa both print assert raised:

program asserts;
{$mode objfpc}
uses SysUtils;
begin
  try
    Assert(1 + 1 = 3, 'arithmetic is broken');
    WriteLn('assert skipped');
  except
    on E: EAssertionFailed do WriteLn('assert raised');
  end;
end.

FPC prints assert skipped, and PXX prints assert raised.

A pointer difference always counts elements. When one side is an untyped Pointer, FPC counts bytes. Cast to say which you mean, and both agree:

program ptrdiff;
{$mode objfpc}
type PInt = ^Integer;
var a: array[0..7] of Integer; p, p0: PInt; u: Pointer;
begin
  p0 := @a[0]; p := @a[2]; u := @a[0];
  WriteLn(p - p0);                    { 2 in both }
  WriteLn(p - u);                     { FPC 8 (bytes), PXX 2 (elements) }
  WriteLn(p - PInt(u));               { 2 in both }
  WriteLn(PtrUInt(p) - PtrUInt(u));   { 8 in both }
end.

Null and Unassigned are one value. A Variant holding either is empty, so VarIsNull and VarIsEmpty are both true for both, and neither raises when printed:

program variantnull;
{$mode objfpc}
uses Variants;
var v: Variant;
begin
  v := Null;
  WriteLn(VarType(v), ' ', VarIsNull(v), ' ', VarIsEmpty(v));
  v := Unassigned;
  WriteLn(VarType(v), ' ', VarIsNull(v), ' ', VarIsEmpty(v));
end.
FPC 3.2.2 PXX
after v := Null 1 TRUE FALSE 0 TRUE TRUE
after v := Unassigned 0 FALSE TRUE 0 TRUE TRUE

A program that uses VarIsNull to tell a database NULL from an unset variant cannot do so here.

Nested comments in {$mode tp}. FPC 3.2.2 nests { } comments in its default and objfpc modes, with a "Comment level 2 found" warning, and does not nest them in delphi and tp mode. PXX nests them in every mode but delphi, so { a {b} c } compiles under PXX in tp mode and is a syntax error under FPC there.

Random gives a different sequence for the same seed. PXX has its own random-number generator, not FPC's Mersenne Twister, so a program that relies on the numbers a seed produces under FPC gets others:

program rnd;
var i: Integer;
begin
  RandSeed := 42;
  for i := 1 to 5 do Write(Random(100), ' ');
  WriteLn;
end.

FPC prints 37 79 95 18 73, and PXX prints 20 80 2 66 15.

On the ESP32, Real is Single. FPC makes Real a Double everywhere; PXX uses the target's native float width. See Types.

The full list, with the reasoning for each, is kept for the compiler's developers in devdocs/dev/pascal-dialect-divergences.md. Problems that are not deliberate are on Known issues.

The RTL is PXX's own

PXX ships its own units under FPC's names: SysUtils, Classes, Math, StrUtils, DateUtils, Variants, TypInfo, Contnrs, SyncObjs, BaseUnix and Unix, among others. They are written from scratch and cover the common part of FPC's, not all of it. Some units FPC programs often use do not exist: fgl, Generics.Collections, Crt, Dos, IniFiles, Process, fpjson, jsonparser, RegExpr and StreamIO. A missing unit stops the compile at the uses line:

pascal26:3: error: uses: unit source not found: fgl

Within the units that exist, a few common routines are missing. Checked 2026-09-28 with pin v447, against 28 routines FPC programs use often, and the three missing ones and the ten named below again with v450: FormatFloat is not in SysUtils, and TStringList has no LoadFromFile or SaveToFile. The other 25 compiled, among them Format, FloatToStrF, StrToIntDef, StringReplace, FileExists, ExtractFileName, Now, FormatDateTime, SplitString and Power. Use Format for FormatFloat. For a string list, go through a stream, which both compilers accept:

program slfile;
{$mode objfpc}{$H+}
uses SysUtils, Classes;
var sl: TStringList; fs: TFileStream;
begin
  sl := TStringList.Create;
  sl.Add('first');
  sl.Add('second');
  fs := TFileStream.Create('list.txt', fmCreate);
  try
    sl.SaveToStream(fs);      { instead of sl.SaveToFile('list.txt') }
  finally
    fs.Free;
  end;
  sl.Clear;
  fs := TFileStream.Create('list.txt', fmOpenRead);
  try
    sl.LoadFromStream(fs);    { instead of sl.LoadFromFile('list.txt') }
  finally
    fs.Free;
  end;
  WriteLn(sl.Count, ' lines, last: ', sl[sl.Count - 1]);
  sl.Free;
end.

It prints 2 lines, last: second under both.

UnicodeString is the byte string in PXX's default build, so UTF8Decode returns its input unchanged: a string with non-ASCII characters keeps one element per byte. This is by design, part of PXX's move to UTF-16 strings. Define PXX_WIDE_PAYLOAD to get FPC's UTF-16 UnicodeString, where UTF8Decode decodes as FPC does:

program utf8len;
{$mode objfpc}{$H+}
{$define PXX_WIDE_PAYLOAD}   { without this line PXX prints 3 }
uses SysUtils;
var u: UnicodeString;
begin
  u := UTF8Decode('h'#$C3#$A9);   { 'hé' as UTF-8 bytes }
  WriteLn(Length(u));
end.

It prints 2 under both. Without the define line, PXX prints 3. ASCII text gives the same result either way.

FPC RTL coverage lists the 150 routines and classes FPC's own code uses most, and says which PXX has.

Building a small FPC program

A program with one unit in src/:

unit geometry;
{$mode objfpc}{$H+}
interface

type
  TPoint2 = record
    X, Y: Double;
  end;

function Distance(const A, B: TPoint2): Double;
function Describe(const P: TPoint2): string;

implementation

uses SysUtils, Math;

function Distance(const A, B: TPoint2): Double;
begin
  Result := Hypot(B.X - A.X, B.Y - A.Y);
end;

function Describe(const P: TPoint2): string;
begin
  Result := Format('(%.1f, %.1f)', [P.X, P.Y]);
end;

end.
program main;
{$mode objfpc}{$H+}
uses SysUtils, geometry;
var a, b: TPoint2;
begin
  a.X := 1; a.Y := 2;
  b.X := 4; b.Y := 6;
  WriteLn(Describe(a), ' to ', Describe(b), ' is ', Format('%.3f', [Distance(a, b)]));
end.

The two command lines side by side:

fpc -Fusrc main.pas              # FPC: writes ./main, plus .o and .ppu files
./pxx -Fusrc main.pas main       # PXX: the output name is the second argument

Both print (1.0, 2.0) to (4.0, 6.0) is 5.000. PXX writes one static executable and no .o or .ppu files; it compiles the units again on every build. -Fu and -Fi work as in FPC, but most other FPC switches do not: the command line is PXX's own. See Command line.

For library code that chooses a branch by asking {$ifdef FPC}, --mimic-fpc turns on a curated set of FPC's defines, FPC among them: built with it, the ifdefs program above prints both lines. Use it only for such code; see FPC compatibility.

Next