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):
adts_and_matching— structs, enums, exhaustivematch, guard clauses.option_resultcoversOption/Resultcombinators for error handling with no null anywhere.json_and_filesbuilds aJsonValue, stringify it, round-trip it through a real file.concurrencycoversspawn/.join(), channels,send/recv.generics_and_assoc_fnscovers generic structs andType.func(args)associated functions on your own types.shared_mutability—Ref[T], the opt-in escape hatch for state that’s genuinely shared or mutated in place.contractscoversrequire/ensurefunction contracts: preconditions and postconditions checked at the call boundary, contrasted withoption_result’sResult-based handling for genuinely expected failure.curryingcovers partial application at call sites: an under-applied call becomes a real function value over the remaining parameters, composing with ordinary closures and higher-order functions for free.asteroidsis a full playable Asteroids clone against real raylib, the one example that links native C (native/raylib_shim.cbridges raylib’s real ABI (32-bitfloat/unsigned charfields) acrossextern "C"’s closed, ABI-safe type surface; see that file’s own doc comment). Build/run withmake/make runinside the example’s own directory (needs raylib installed and on your linker path, notplum run/plum builddirectly; see its own README for install/build steps and controls). The most complete demonstration of functional game-state-as-value-not-mutation in the whole repo.
Generated from RUNNING.md in the repository.