Getting started
Start here after installing PXX. This section walks through the smallest useful program, then points to language, library, and target docs.
Your first program
hello.pas:
program hello;
begin
writeln('Hello, world!');
end.
Compile and run:
$ ./pxx hello.pas hello
ok: hello [code=600B data=416B bss=34600B procs=37 codeseg=3808B]
$ ./hello
Hello, world!
The figures are what this build happened to produce; they move as the runtime
does, so different numbers do not mean something is wrong. The line to check is
ok: and the name of the file it wrote. The whole executable is about 4.5 KB.
The compiler also writes hello.map, a symbol map for debugging; you can
ignore it.
PXX writes a complete Linux ELF executable directly — no as, no ld.
See First program for the same example with a short explanation of the source layout and compiler arguments.
If that build instead says unit source not found, the compiler is looking in
the wrong place rather than the program being wrong. Run ./pxx --where: it
prints every root it resolves and marks the missing ones, and it needs no source
file. See checking an install.
Debugging with gdb
Add -g to emit DWARF debug info:
$ ./pxx -g hello.pas hello
$ gdb ./hello
(gdb) break hello.pas:3
(gdb) run
Breakpoint 1, hello () at hello.pas:3
3 writeln('Hello, world!');
Line stepping, breakpoints, backtraces (bt), and print of locals/globals all
work — on x86-64, i386, aarch64, and arm32.
Cross-compiling
Pass --target= to build for another CPU:
./pxx --target=aarch64 hello.pas hello.a64
./pxx --target=i386 hello.pas hello.i386
./pxx --target=arm32 hello.pas hello.arm
Run cross binaries under QEMU user-mode (see the repo's tools/run_target.sh).
Other frontends
The same compiler also accepts .c and .npy source directly — the frontend
is chosen by extension, no separate tool. hello.c:
#include <stdio.h>
int main(void)
{
printf("Hello, world!\n");
return 0;
}
hello.npy:
print("Hello, world!")
./pxx hello.c hello_c
./pxx hello.npy hello_npy # or hello.py — same frontend, either extension
For C, start with Getting started with C. For the ESP32, start with Getting started on the ESP32. See also C Frontend and Nil Python.
What you just built: a binary that needs nothing
hello above is not linked against libc. It is a static ELF whose only runtime
dependency is the Linux kernel:
$ ldd hello
not a dynamic executable
That is the default, for every frontend — not a --static flag you remember to
pass. PXX ships its own runtime, so a Pascal, C, or Nil Python program compiles
to something you can copy to another machine of the same architecture and run.
Importing across languages
Frontends share one backend, one symbol table, and one import resolver, so an import does not care which language the thing it finds was written in. There is no wrapper to generate, no FFI block to hand-write, no IDL.
The clearest case is a Python file reaching a C library through the header your distribution already installed:
import sqlite3
print(sqlite3_libversion_number())
$ ./pxx --no-shims ver.npy ver && ./ver
3046001
import sqlite3 found sqlite3.h, read the real declarations out of it, and
linked libsqlite3.so.0. The version printed is whatever the host has (this
needs the SQLite development headers, for example libsqlite3-dev). Nothing was
written by hand to make that call reachable.
--no-shims is what sends the import to the C header. Without it,
import sqlite3 means what it means in Python: PXX's own implementation of
Python's sqlite3 module (connect, execute, iterating over rows). That
implementation covers part of the module, not all of it. Imports resolve PXX's
own modules first so that the same line always means the same thing.
The same resolver is why uses gtk3_c; in Pascal reads a plain C header, and
why import re in Nil Python lands on a genuine Pascal unit
(lib/rtl/re.pas). See Cross languages for the
whole model, including the parts that do not work yet.
Two ways to get a library, and what each costs
Importing a system library is the one thing that gives up the property above —
ver needs libsqlite3.so.0 at runtime, so it is a normal dynamic binary. That
is a deliberate trade, not a limitation, and you can decline it: point the
compiler at the library's source and it compiles that in instead.
./pxx -DSQLITE_THREADSAFE=0 -Ilib/crtl/include -Ilib/crtl/src -Ipath/to/sqlite prog.c prog
with the C side pulling the implementation in directly:
#include "sqlite3.c"
-DSQLITE_THREADSAFE=0 is for a single-threaded program. A program that uses
SQLite from several threads builds with --threadsafe instead;
test/csqlite_thread_test.c does that, with several threads on one connection
and on one connection each, and printed all OK with pin v441 and with v450.
Now SQLite is part of your binary, dead-code-eliminated with everything else,
and ldd again reports no dynamic dependencies. Both routes are exercised by
the same gate: make test-sqlite-parity builds one program against the system
libsqlite3.so.0 and another over the self-compiled amalgamation, runs the
identical workload through both, and requires their output to match byte for
byte.
So the two headline properties are not in tension. Zero-dependency static output is the default; reaching into the system's shared libraries is available when you want it, and costs exactly the dependency you asked for.