Funxy supports compilation to bytecode and building self-contained native binaries.
The build command creates a single executable that includes your script and the Funxy runtime:
# Build a standalone binary
funxy build script.lang # creates ./script
funxy build script.lang -o myapp # custom output name
# Run without Funxy installed
./myappSelf-contained binaries also work as full Funxy interpreters. Pass $ as the first argument to switch — the $ is stripped and the rest is processed normally:
./myapp # runs embedded bundle
./myapp --port 8080 # runs embedded bundle (flags go to your app via sysArgs)
./myapp $ other.lang # acts as Funxy interpreter
./myapp $ -e 'print(42)' # eval mode
./myapp $ -pe '1 + 2' # eval with auto-print
./myapp $ --help # shows helpThis lets tools like the Playground invoke themselves to run user code. Use sysExePath() from lib/sys to get the executable path, and sysScriptDir() to resolve paths relative to the script:
import "lib/sys" (sysExePath, sysExec, sysScriptDir)
result = sysExec(sysExePath(), ["$", userScript]) // invoke self as interpreter
dir = sysScriptDir() // script dir (standalone) or "" (compiled binary)
Note:
sysScriptDir()returns""in compiled binaries. UsepathJoin([sysScriptDir(), "file"])for portable code — in bundle modepathJoin(["", "file"])gives"file"which matches the embed key.
Bundle static files (HTML templates, configs, images, data files) into the binary:
# Embed a directory of templates
funxy build server.lang --embed templates -o server
# Multiple directories (two equivalent forms)
funxy build app.lang --embed static --embed config -o app
funxy build app.lang --embed static,config -o app
# Glob patterns
funxy build app.lang --embed "templates/*.html" -o app
funxy build app.lang --embed "assets/*.png,config/*.toml" -o app
# Embed a single file
funxy build tool.lang --embed data/schema.json -o toolMultiple paths can be comma-separated within a single --embed, and glob patterns (*, ?, [...]) are supported.
Embedded files are accessible via the standard fileRead, fileReadBytes, fileExists, and fileSize functions — the binary checks embedded resources first, then falls back to the filesystem. No code changes needed:
import "lib/io" (fileRead)
// Works the same whether interpreted or built as binary
html = fileRead("templates/index.html") |>> \x -> x
Embed keys are determined by the --embed argument itself — the argument IS the key prefix. --embed templates → keys start with templates/. Paths are normalized: fileRead("./templates/index.html") works.
@alias@ syntax (directories only) customizes the key prefix:
# Physical dir "assets/tpl", script sees "templates/..."
funxy build app.lang --embed assets/tpl@templates@
# Alias "." strips prefix — keys are just filenames
funxy build app.lang --embed static/@.@
# With glob filter — only .html files
funxy build app.lang --embed static/@.@*.htmlBundle multiple scripts into a single binary. Each script becomes a named command, dispatched by the first argument or by argv[0] (symlink):
# Build a multi-command binary
funxy build api.lang worker.lang cron.lang -o myserver
# Run commands
./myserver api --port 8080 # runs api.lang
./myserver worker # runs worker.lang
./myserver cron # runs cron.lang
./myserver # prints usage with available commandsCommand names are derived from filenames: api.lang → api, worker.lang → worker. Duplicate names are an error.
If the binary's argv[0] basename matches a command name, that command runs directly — no subcommand argument needed:
ln -s myserver api
ln -s myserver worker
./api --port 8080 # runs api.lang (dispatched by argv[0])
./worker # runs worker.langThis is the BusyBox pattern: one binary, multiple symlinks, each behaves as a standalone tool.
--embed resources are shared across all commands:
funxy build api.lang worker.lang --embed static --embed config.json -o myserverBoth api and worker can call fileRead("static/index.html") or fileRead("config.json") — they see the same embedded files.
Resources are copied to each sub-bundle at dispatch time, so mutating one command's resources never affects the parent or other commands.
sysArgs() does not include the command name. A script behaves the same whether it runs standalone or as a subcommand:
# Standalone: ./api --port 8080 → sysArgs() = ["./api", "--port", "8080"]
# Subcommand: ./myserver api --port 8080 → sysArgs() = ["./myserver", "--port", "8080"]
# Symlink: ./api --port 8080 → sysArgs() = ["./api", "--port", "8080"]The $ escape hatch works with multi-command binaries too:
./myserver $ -e 'print(42)' # interpreter modeUse --up to build a multi-command binary that acts as a standard interpreter by default, while exposing the bundled scripts as subcommands:
# Build an extended interpreter
funxy build fmt.lang lint.lang --up -o myfunxy
# Run subcommands
./myfunxy fmt # runs fmt.lang
./myfunxy lint # runs lint.lang
# Still works as an interpreter
./myfunxy script.lang # runs an external script
./myfunxy -pe '1+2' # eval modeSymlink dispatch is disabled in --up mode to avoid conflicts if the binary name matches a command name.
To build for a different OS or architecture, provide a pre-built Funxy binary for the target platform via --host:
# Build for Linux (from macOS or any other OS)
funxy build script.lang --host release-bin/funxy-linux-amd64 -o myapp
# Build for Windows
funxy build script.lang --host release-bin/funxy-windows-amd64.exe -o myapp.exe
# Build for macOS Intel (from ARM Mac)
funxy build script.lang --host release-bin/funxy-darwin-amd64 -o myapp-intel
# Build for FreeBSD
funxy build script.lang --host release-bin/funxy-freebsd-amd64 -o myapp-bsdThe bytecode is platform-independent — only the host binary determines the target. The --host flag requires an explicit path; there are no default targets.
- Your script(s) and all user module dependencies are compiled to bytecode
- The bytecode is serialized into a Bundle (v2 format), including any
--embedresources - For multi-command: each script becomes a named sub-bundle inside a parent Bundle
- The Bundle is appended to the host binary (own executable or
--host) - On startup, the binary detects the embedded bundle and executes it (or dispatches to a sub-command)
The resulting binary includes:
- The full Funxy VM runtime
- Your script's bytecode (or multiple scripts' bytecodes for multi-command)
- All user module dependencies (pre-compiled)
- Pre-compiled trait default methods
- Embedded static files (if
--embedwas used), shared across all commands
Virtual modules (lib/*) are resolved at runtime from the built-in standard library.
For pre-compiling without creating a full binary:
# Compile to bytecode bundle
funxy -c script.lang # creates script.fbc
# Run compiled bytecode
funxy -r script.fbcYou can dynamically load and execute compiled bytecode (.fbc files) from within a running Funxy script using runBytecode from lib/io. This is especially useful for plugin systems or dynamic service loading (e.g. in the Funxy VMM architecture). In sandbox mode (e.g. VMM workers), lib/io capability is required.
import "lib/io" (runBytecode)
// Load and run a compiled bytecode bundle
match runBytecode("service.fbc") {
Ok(result) -> print("Execution success:", result)
Fail(err) -> print("Execution failed:", err)
}The runBytecode function executes the loaded bundle and returns its final evaluated expression as a string representation wrapped in a Result.
The v2 bundle format replaces the legacy single-chunk v1 format:
- Magic:
FXYB(4 bytes) - Version:
0x02(1 byte) - Payload: Gob-encoded
Bundlestruct containing:MainChunk: compiled bytecode for the entry script (single-command mode)Modules: map of absolute path → pre-compiledBundledModuleTraitDefaults: pre-compiled trait default methodsResources: embedded static files (--embed)Commands: map of command name → sub-Bundle(multi-command mode)
Single-command mode: MainChunk is set, Commands is empty.
Multi-command mode: MainChunk is nil, Commands maps names to sub-bundles. Each sub-bundle has its own MainChunk, Modules, and TraitDefaults. Resources are shared from the parent.
Each BundledModule contains:
Chunk: compiled bytecodePendingImports: the module's own import dependenciesExports: list of exported symbol namesTraitDefaults: module-level trait defaults
The v1 format (single Chunk with FXYB + version 0x01) is still supported for backwards compatibility.
After deserialization, bundles are validated:
- Single-command:
MainChunkmust be present with non-empty bytecode - Multi-command: Each sub-bundle must have a
MainChunkwith non-empty bytecode
Invalid bundles are rejected with clear errors (e.g. "single-command bundle has nil MainChunk", "command \"api\" has empty bytecode").
If the bytecode version is not supported, the error shows the supported range:
unsupported bytecode version: 3 (this binary supports versions 1–2; upgrade Funxy to run newer bytecode)
[Host Binary][Bundle Data][8-byte size LE][4-byte "FXYS" magic]
- The host binary is the Funxy runtime itself
- Bundle data is a serialized v2 Bundle
- The 12-byte footer contains the bundle size and magic marker
- On macOS, the binary is re-signed with ad-hoc signature after creation
- Faster startup: No parsing or semantic analysis needed
- Bundled dependencies: All user modules pre-compiled
- Zero-dependency distribution: Single binary, no Funxy installation needed
- UPX compatible: Output binaries can be compressed with UPX for smaller size