PXX dialect

PXX is an Object Pascal dialect. It deliberately follows FPC behavior for many implemented features, but it does not aim for full FPC compatibility and it also has PXX-specific extensions.

Common supported surface

PXX supports a tested Object Pascal subset including:

PXX-specific or early surface

Some features are project-specific or still early:

Inline and auto-typed variables

PXX introduces two modern conveniences to the Pascal language surface, both of which are enabled by default:

  1. Inline variables: You can declare a variable using the var keyword anywhere inside a begin ... end block, rather than only in a routine's top-level var section. Inline variables are scoped to the block in which they are declared.
  2. Auto-typed variables: If a variable declaration includes an initializer, the compiler can automatically infer its type. You can either omit the type entirely or use the auto keyword.

For example:

begin
  var i := 0;              { inferred Integer, declared inline }
  var name: auto := 'PXX'; { explicit auto keyword }
  var x: Double := 3.14;   { inline, explicit type }
  
  for i := 1 to 10 do
    writeln(name, ' count: ', i);
end;

These features can be disabled by passing --no-auto-var and --no-lazy-var (or -fno-auto-var and -fno-lazy-var) to the compiler.

Calling conventions

A calling convention in PXX is a property of the target, not of a routine. Each platform has exactly one, and the compiler uses it: the System V AMD64 ABI on 64-bit Linux, the target's own C ABI when cross-compiling. There is no per-routine choice to make.

So on a routine — a definition, a forward declaration, an external, or a method — a convention directive is decoration. These compile to the same code:

function sq(x: Double): Double; external 'libm.so.6' name 'sqrt';
function sq(x: Double): Double; cdecl; external 'libm.so.6' name 'sqrt';

Both call the C function correctly. The second is not more correct, only more explicit for a human reader: the call uses the platform C ABI because the routine is external, not because cdecl is written.

Every spelling is accepted in every position that can carry one, and exactly one combination means anything:

directive on a routine or external on a procedural type on a method declaration
cdecl accepted meaningful — see below accepted
register, stdcall, safecall, pascal, mwpascal accepted accepted accepted

Note that register on a procedural type is not the exception cdecl is: register is FPC's own convention, not C's, so marking a signature with it would be marking it the wrong way.

FPC also type-checks the pairing — it refuses to assign a register routine to a stdcall procedural variable. PXX does not, because a convention it does not model cannot make two signatures incompatible. Code FPC accepts compiles here; code PXX accepts may need the conventions matched up before FPC will take it back.

The exception: cdecl on a procedural type

On a procedural type — a function-pointer type — cdecl is not decoration. It marks the signature as C-ABI so that an indirect call through a value of that type marshals its arguments the C way. Leave it off a pointer to a real C function and the call is compiled with PXX's internal convention: it will build, run, and produce a wrong answer.

type
  TPlain = function(x: Double): Double;
  TCdecl = function(x: Double): Double; cdecl;

Given a dlsym'd pointer to a C double dtwice(double), calling it through TPlain yields 21.0 for an argument of 21.0 — the argument never reaches the register the C function reads — while TCdecl yields the correct 42.0.

The rule of thumb: if you are writing a type for a pointer to a C function, write cdecl on it. Everywhere else the marker is documentation.

Routine directives

Calling conventions are not the only decorators, and the rest are not uniformly inert either. A directive here falls into one of three groups, and it is worth knowing which — an inert one is free to write, a load-bearing one changes what is compiled, and a rejected one stops the build.

Load-bearing

These change the routine. Several refuse to compile when they cannot be honored, which is deliberate: silently ignoring them would produce a working build that does the wrong thing.

directive effect
assembler the body is inline assembly; an ordinary begin body is an error
generator the routine becomes a generator, and must be a function (its result type is the yielded element type)
async asynchronous routine
stackless selects the stackless strategy for an async routine
interrupt raw hardware-vector ISR. Implemented for riscv32 (ESP32-C3) and xtensa Call0 (ESP32-S3) only; on any other target it is an error, naming iram as the alternative for an IDF-registered ISR
flexcolumn call arguments accept write-style :width[:decimals] modifiers
external the routine is a dynamically-linked import
virtual, override, dynamic, abstract method dispatch (on a class member declaration)
static on a class method declaration: no Self

Accepted and ignored

Written for the reader, or for FPC source compatibility. They parse and do nothing.

directive why it is inert
cdecl, register, stdcall, safecall, pascal, mwpascal the calling convention is the target's — see above. cdecl on a procedural type is the exception
inline the optimizer decides. At -O2 it inlines any routine that qualifies (a function, scalar result, at most six scalar by-value parameters, not external or a generator) whether or not you wrote inline, and never inlines one that does not qualify because you did
stackful the default async strategy; accepted so it can be stated explicitly
static, reintroduce on a plain routine (static is meaningful on a class method)
iram on targets other than xtensa and riscv32, where there is no IRAM to place anything in
deprecated, platform, experimental, unimplemented, library hint directives — see FPC compatibility. No usage warning is emitted yet
noreturn, noinline hints about a decision the compiler makes for itself
nostackframe an optimization PXX does not make; the frame is emitted anyway
far, near 16-bit memory models. There is nothing to address far
local FPC's unit-private visibility. PXX is whole-program and has no separate unit output, so a routine is local exactly when nothing else names it

Rather than consulting this table, ask the compiler: --warn-ignored-directives names the inert ones at the point of use, with the reason — see below.

overload is a case of its own: inert by default, required under --strict-overload. PXX resolves overloads without it; the flag makes the missing directive an error, the way FPC has it.

Not accepted

varargs, public, export, alias, compilerproc, internproc, rtlproc, hardfloat and softfloat are refused, and that is a decision rather than a gap. Each of them means something: varargs changes how a call marshals, the linkage group changes what --emit-obj produces, compilerproc and its neighbours mark a routine the compiler itself supplies, and hardfloat/softfloat select a float ABI — which is a real choice on the arm32 and riscv32 targets. Accepting one and ignoring it would compile something other than what was written, which is worse than refusing it. FPC sources using them need the directive removed, or the behaviour implemented.

weakexternal was on that list until 2026-09-14 and is now accepted, by the second route that sentence offers: it is implemented.

weakexternal — an optional import

weakexternal takes the same clause as external and emits the symbol as an undefined weak dynamic symbol:

function pthread_create(t, attr, fn, arg: Pointer): Integer; cdecl;
  weakexternal 'libc.so.6' name 'pthread_create';

A symbol declared external that the loader cannot resolve is a fatal symbol lookup error before main runs. One declared weakexternal is not: the loader leaves its GOT slot at zero, the program starts, and @f reads nil. The caller must test for nil — PXX inserts no check, exactly as FPC inserts none:

if @pthread_create <> nil then { use it } else { fall back };

It is for "use this C facility if the program already has it", not for avoiding a dependency you actually took: a weak import still contributes its library to DT_NEEDED. What it buys is that one unit can reach a symbol that may be absent without every program including that unit failing to start. A fully static link with no interpreter bakes the same zero.

Finding out which ones are inert here: --warn-ignored-directives

The table above tells you which directives are inert in general. The compiler will tell you which are inert at a particular declaration, with the reason, under an opt-in flag:

program x;
procedure P; cdecl;
begin end;
procedure R; iram;
begin end;
function Big(a,b,c,d,e,f,g: Integer): Integer; inline;
begin Big := a+g; end;
begin P; R; WriteLn(Big(1,2,3,4,5,6,7)); end.
$ pxx -O2 --warn-ignored-directives x.pas x
pascal26:3: warning: directive 'cdecl' ignored here: the calling convention is the target's and is not selectable per routine, so P already uses it; the marker is documentation only
pascal26:5: warning: directive 'iram' ignored here: IRAM placement exists on the ESP targets (xtensa, riscv32) only; this target has no separate instruction RAM to place R in
pascal26:7: warning: directive 'inline' ignored here: the inliner takes at most six by-value scalar parameters and Big has 7

It covers cdecl, register, iram off the ESP targets, stackful, reintroduce, and inline when the routine cannot be inlined. Default behaviour is unchanged and silent — this reports, it never changes what is compiled.

Hint directives (deprecated, platform, …) are deliberately excluded. They are meant to be inert until usage warnings exist, so warning on them would fire on ordinary FPC source.

What the inline reason does not claim. It reports only causes knowable at the declaration — optimisation level, procedure vs function, assembler/generator/async/stackless, and more than six parameters. It never says the body is too complex, because the body has not been parsed at that point. So silence about a routine is not a promise that it will be inlined.

interrupt remains the one directive that is an error rather than a warning where it cannot be honored, because ignoring it would produce a working build that does the wrong thing.

Source compatibility posture

Prefer ordinary Object Pascal where possible. Use {$ifdef PXX} only for code that intentionally depends on PXX behavior.

Do not use {$ifdef FPC} to mean "Object Pascal compiler". PXX does not define FPC; that symbol belongs to Free Pascal.

Next

Async & coroutines

A pxx dialect extension with no FPC equivalent: a built-in cooperative, single-threaded coroutine scheduler and an async networking reactor, so one OS thread can drive thousands of concurrent tasks and connections without thread overhead. Coroutines run until they voluntarily yield.

uses scheduler;

procedure Worker(arg: Pointer);
begin
  writeln('tick'); CoSleep(100);   { non-blocking — scheduler runs others }
  writeln('tock'); CoYield;
end;

begin
  Spawn(@Worker, nil);             { SpawnSized for a custom stack (ESP32) }
  RunUntilDone;
end.

Full API — Spawn/SpawnSized, CoYield, CoSleep, RunUntilDone, and the async socket reactor — in Coroutines & Async.

Wrapper-free C import

A pxx unit can uses a plain C header directly — the compiler parses the .h and exposes its functions, types and #define constants to Pascal with no hand-written binding. This is how lib/pcl talks to GTK and OpenGL today (uses gtk3_c;, uses gl_c;). Input strings marshal to NUL-terminated C strings automatically; integer #define macros arrive as ordinary constants.

program users;
uses sqlite3;          { imports sqlite3.h directly — zero wrapper code }

var
  db, stmt: Pointer;
begin
  sqlite3_open('/tmp/users.db', @db);
  sqlite3_prepare_v2(db, 'select id, name from users', -1, @stmt, nil);

  while sqlite3_step(stmt) = SQLITE_ROW do   { SQLITE_ROW: a #define from the header }
  begin
    var id   := sqlite3_column_int(stmt, 0);    { auto-typed inline var: Integer }
    var name := sqlite3_column_text(stmt, 1);   { auto-typed: managed string }
    writeln(id, ' ', name);
  end;

  sqlite3_finalize(stmt);
  sqlite3_close(db);
end.

The nilpy frontend takes this further: with no address-of operator, it return-lifts trailing T** out-parameters automatically, so db = sqlite3_open(path) just returns the handle. See also the C frontend and its magic-link model.


Related: full dialect doc · FPC compatibility notes · compliance overview.