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:
- programs and units
- constants, variables, records, arrays, sets, and strings
- procedures, functions,
var/const/outparameters, overloads, and operators - classes, inheritance, virtual dispatch, constructors, properties, and RTTI
- interfaces,
is/as, exceptions, and generics - conditional compilation with
{$ifdef},{$ifndef},{$if},{$else},{$elseif}, and{$endif}
PXX-specific or early surface
Some features are project-specific or still early:
PXXis predefined when compiling Pascal input.-dNAMEand-uNAMEdefine and undefine conditional symbols.--threadsafeenables atomic reference counts for managed strings and arrays.--no-lazy-vardisables PXX's inline variable declarations, and--no-auto-vartheautokeyword and inline loop variables.--target=ARCHselects the output CPU target..cand.npyinputs route to the C and Nil Python frontends, which are mainline and gated alongside Pascal;.basand.rsroute to the experimental BASIC, Rust and Zig research frontends.
Inline and auto-typed variables
PXX introduces two modern conveniences to the Pascal language surface, both of which are enabled by default:
- Inline variables: You can declare a variable using the
varkeyword anywhere inside abegin ... endblock, rather than only in a routine's top-levelvarsection. Inline variables are scoped to the block in which they are declared. - 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
autokeyword.
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;
--no-lazy-var (or -fno-lazy-var) turns off inline var declarations,
including the inferred var i := 0. --no-auto-var (or -fno-auto-var)
turns off the auto keyword and inline loop variables (for var i := …); a
plain var i := 0 in a block still compiles under it (pin v447).
FPC 3.2.2 has neither feature. It stops at the first inline var with
"Illegal expression", so code that must also build with FPC declares its
variables in a var section.
Calling conventions
A calling convention in PXX is mostly a property of the target, not of a
routine. PXX's own routines use an internal convention, and calls into C use
the target's C ABI. There is one per-routine choice, cdecl, and only two
places where it changes anything: on a procedural type (see below), and on a
routine with a body whose address goes into a cdecl procedural type. On
x86-64 it gives that routine a System V prologue, and since pin v449 the
routine and the type must agree on x86-64, i386, aarch64 and arm32.
On an external, 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 an external |
on a routine with a body | on a procedural type | on a method declaration |
|---|---|---|---|---|
cdecl |
accepted | meaningful on x86-64: the routine gets a System V prologue, so C, or a cdecl procedural type, can call it. Accepted elsewhere. Since v449 it must match the procedural type the routine is stored in, on x86-64, i386, aarch64 and arm32 |
meaningful — see below | accepted |
register, stdcall, safecall, pascal, mwpascal |
accepted | 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. Since pin v449 PXX checks the cdecl
pairing. Here Twice lacks the cdecl its procedural type
has:
program cd2;
type TF = function(x: Double): Double; cdecl;
function Twice(x: Double): Double; begin Twice := 2 * x; end;
var f: TF;
begin f := @Twice; WriteLn(f(21.0):0:1); end.
FPC refuses the assignment ("Incompatible types"), and so does PXX v449 on x86-64, i386, aarch64 and arm32:
pascal26:5: error: incompatible types: @Twice uses the Pascal convention but the procedural type uses cdecl -- declare both the same way
The commit that added the check (7d105ec21c) says it runs wherever a
routine's address meets a procedural type (an assignment to a variable, a
field or an element, and an argument), in both directions, and that
external routines are exempt, because their convention is the library's;
this page measured only the assignment above. On riscv32 cdecl on a bodied routine selects
nothing, so there is nothing to mismatch and the program compiles. On xtensa
and wasm32 this example is refused for a different reason: a Pascal routine
with a by-value float parameter cannot be called through a cdecl pointer
there yet.
Before v449 PXX compiled the program, and on some targets the call gave a
wrong answer. With v448 (sha256 b2b325036c3b) it printed 0.0 on x86-64
and i386, and 42.0 on aarch64, arm32 and riscv32. With cdecl on Twice
too, v448 and v449 print 42.0 on all five. Measured 2026-09-28 under QEMU
user mode, v449 sha256 0ded1e5d04c8. Write cdecl on the routine
whenever you take its address for a cdecl type. For the other
conventions, code FPC accepts compiles here, and 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, and on every Pascal routine you store in it. On an
external routine the marker is documentation. On a bodied routine, leaving
it off while storing the routine in a cdecl type is refused on x86-64,
i386, aarch64 and arm32 since v449.
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, and on a bodied routine stored in one, are the exceptions |
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
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: 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. So is varargs, since
2026-09-03: on a cdecl external it lets a Pascal program call a variadic C
function with extra arguments, in either of FPC's two orders.
function sprintf(buf, fmt: PChar): Integer; cdecl; varargs;
external 'libc.so.6' name 'sprintf';
You can call libc's own printf this way without flushing it yourself. A PXX
program does not exit through libc, so a program that imports from libc calls
fflush(NULL) for you after the unit finalizations, on every exit that runs
them: the end of the program, Halt, and a runtime error. Output is still
lost if the program is killed by a signal. Because libc buffers and PXX's own
Write does not, printf output and WriteLn output can appear in a
different order from the order of the calls when stdout is a pipe or a file.
Call fflush(nil) between them if the order matters.
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". 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 weak import does not make the program depend on its library. A library
reached only through weak imports gets no DT_NEEDED entry, so its symbols
read nil even when the library is installed. A program whose imports are all
weak is linked statically, with no interpreter, and every weak symbol in it
reads nil. This is by design. To have the weak symbol resolve when the
library is present, declare at least one ordinary external from that same
library: the program is then linked dynamically against it, and @f is
non-nil wherever the symbol exists. With an ordinary external getpid from
libc.so.6 beside the weak pthread_create, @pthread_create is non-nil.
Without it, @pthread_create is nil, and so is a weak cbrt from
libm.so.6 when the program has no ordinary import from libm.
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: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
There is no cdecl line: on x86-64, cdecl on P selects the System V
prologue, so it is not ignored. Built with --target=i386, the same program
also warns that cdecl is ignored (pin v447). v449 and v450 print that
warning on i386, aarch64 and arm32 too, including for a routine stored in a
cdecl procedural type. There the warning is wrong: without the cdecl, the
program in Calling conventions is refused by v449,
and on i386 v448 printed 0.0 instead of 42.0. Keep the cdecl. After
v450 (6b92d83493, in no pin yet) the warning is gone on those three
targets and still given on riscv32, where cdecl selects nothing. Measured
on 2026-09-28 with v450 (c19cc2d531e4) and the compiler built at
947170c4cb (1634f6483109).
It covers cdecl off x86-64, 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.