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.

Next