Plum

Running programs

Building, running, cross-compiling, and the worked examples.

Running a program

Plum programs live in a project directory. A directory is a module (see “Modules” below). plum new scaffolds a minimal starter project:

plum new myapp
myapp/
  main.plum
// myapp/main.plum
let main (): Unit = println("hello, plum")

Compile and run it in one step:

plum run myapp

run caches validated LLVM IR. With a supported default Clang toolchain, it also caches compiled objects when it can fingerprint the compiler and its loaded libraries. C inputs are preprocessed again so header edits and changes in include search paths are reflected. Every run links a fresh temporary executable and inherits your arguments, working directory and terminal streams. Other platforms and custom PLUM_CC commands reuse IR and compile native inputs normally.

Object reuse supports Linux’s default Clang/loader and the x86-64 MSYS2 MINGW64 Clang setup used by Windows CI. Other Windows toolchains, custom PLUM_CC commands, and macOS retain IR reuse and compile native objects afresh.

The Windows adapter requires MSYSTEM=MINGW64, the GNU Windows target, and an ASCII executable spelling ending in mingw64/bin/clang.exe. It launches a short, bounded loader probe and hashes the actual executable and DLL files selected by Windows. Private DLLs must come from that toolchain’s bin directory and have no delayed imports; system DLLs must come from Windows’ system directory. Unknown images, unreadable files, unsupported layouts, and failed probes bypass object reuse. This contract covers the standard compiler without compile-time plugins, not arbitrary DLL-loading compiler extensions. Compiler files must remain stable during a command.

Certified Windows object compilation uses the exact probed executable and an integrated Clang frontend. The effective command is checked against Clang’s configuration-free command; an external assembler/compiler, plugin, profile input, or unsupported configuration triggers ordinary compilation. C is still preprocessed afresh, objects are integrity-checked, and every invocation links a fresh executable. Windows validates emitted C LLVM IR rather than materializing large Windows-header JSON syntax trees; assembly and quoted assembler symbols conservatively bypass C object reuse. Normal builds keep their existing compiler selection and linking behavior.

The cache is under PLUM_CACHE/build/v1, or the same default cache root used for packages (XDG_CACHE_HOME/plum, then ~/.cache/plum). Source contents, discovered files, dependencies, compile-time embeds, compiler contents and build modes determine reuse. A failed edit never runs the previous successful program. Cache I/O failures fall back to compilation.

PLUM_NO_BUILD_CACHE=1 plum run myapp  # bypass all build artifacts
PLUM_NO_OBJECT_CACHE=1 plum run myapp # reuse IR, compile native inputs
PLUM_CACHE_TRACE=1 plum run myapp     # cache decisions on stderr
PLUM_PROFILE=1 plum run myapp         # phase timings/counts on stderr
PLUM_BUILD_CACHE_MAX_BYTES=536870912 plum run myapp  # 512 MiB budget
plum cache list build
plum cache clean build

Build artifacts have a default 1 GiB budget. After cached compilation, oldest-published IR, object and shim entries are removed until the cache fits the budget. Set PLUM_BUILD_CACHE_MAX_BYTES to a nonnegative byte limit to override it; zero retains no completed entries. Invalid values use the default. Cleanup is best effort: concurrent publishers can briefly exceed the budget, and cache I/O errors never fail a build. Staging directories older than 24 hours are removed only when their recorded owner process has exited; live or unknown owners are preserved. plum cache clean build atomically moves the existing build cache aside, then deletes it. Runs started concurrently may populate a new cache. These commands leave fetched packages alone; the existing plum cache list and plum cache clean continue to manage packages. Generated C shim sources also live in the build cache so their paths stay stable between object builds. No executable runs from the cache.

Use a cache directory trusted by your user on this machine. Shared writable or network cache roots are unsupported: integrity hashes detect corruption, but do not authenticate artifacts against another writer. Cached LLVM IR can retain earlier @embed_file contents until eviction or explicit cleaning.

Shared shims are verified and published as complete directories, so concurrent builds cannot read another writer’s partially extracted header.

Object validation has a cost: it hashes the native toolchain and runs C preprocessing even on a hit. Initial cache population can be slower than an uncached run. PLUM_NO_OBJECT_CACHE=1 keeps IR reuse while avoiding native-object validation/population. See the measured results for the local tradeoff.

Build inputs and the toolchain must remain stable during one compilation, as they must for an ordinary build. Observed source changes prevent cache publication. Corrupt entries are treated as misses and repaired.

Use plum run myapp/main.plum to run a single file. Both file and project forms use the build cache. The self-hosted compiler requires the run subcommand; a bare plum myapp invocation prints usage.

Or keep the binary:

plum build myapp -o myapp/out
./myapp/out

plum build’s -o/--output is optional; if omitted, the output binary is named after the project directory itself (written to the current working directory), mirroring go build/cargo build --bin.

Building for another platform

plum build takes a --target <triple>:

PLUM_CC="zig cc" plum build myapp --target aarch64-linux-musl

Plum does not implement cross-compilation so much as get out of its way. The IR it emits carries no target triple and no datalayout, so clang --target= retargets it directly, and Os.platform() is compiled in by the C compiler’s own #ifdef, so it reports the target’s platform, not the build machine’s, with no work on our part.

What Plum does not ship is a sysroot. clang can emit code for any target but cannot link one without that target’s libc, so --target needs a C driver that has one. PLUM_CC is where you name it; zig cc above, or a corporate cross toolchain. This is the same arrangement Rust’s ecosystem settled on with cargo-zigbuild, and it is why a native build still needs nothing but clang: nobody pays for a feature they do not use. See DESIGN.md’s cross-compilation section for the measurements behind that choice (bundling Zig’s sysroot would be a ~240x increase on a 712 KB archive).

Two conveniences: a Windows target gets a .exe suffix when you do not pass -o, and a non-64-bit target is refused outright rather than built. The second is not politeness: cell layout assumes 8-byte slots, so a 32-bit target would not fail to link, it would silently miscompile.

plum run and plum test are always native. They execute what they build, and this machine cannot run a foreign binary.

run and build are the SAME path: both compile to a native binary and differ only in whether it is kept. Nothing can behave one way under run and another under build, because there is no second engine for it to behave differently in.

Both use the same main entry point: a single Unit-typed parameter (let main (): ... = ..., invoked by the CLI itself, not called from your own source) returning Unit or any printable value (the CLI prints whatever main returns).

plum help (also --help and -h) prints usage. With no arguments at all, plum does the same. Extra arguments after help are ignored rather than treated as a file to compile.

Errors from either path point at real source locations:

error: type error: operator: type mismatch: expected Str, found Int
  --> <root>:3:15
  |
3 |     let bad = "hello" + 1;
  |               ^^^^^^^^^^^

Examples

Real, runnable projects under examples/, one per theme — each with its output recorded in expected.txt and checked by bootstrap/example-sweep (except asteroids, which opens a window and is only built, not run; see its own entry below):

Generated from RUNNING.md in the repository.