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=31425B  data=104B  bss=4217B  procs=42]
$ ./hello
Hello, world!

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.

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:

./pxx hello.c hello_c
./pxx hello.npy hello_npy   # or hello.py — same frontend, either extension

See 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 ver.npy ver && ./ver
3045001

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. Nothing was written by hand to make that call reachable.

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 -Ilib/crtl/include -Ilib/crtl/src -Ipath/to/sqlite prog.c prog

with the C side pulling the implementation in directly:

#include "sqlite3.c"

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.

Next