RunMat can now compile a MATLAB-syntax program into a native executable:
runmat compile simulation.m -o simulation
./simulationOn Windows, RunMat can compile to a native .exe:
runmat compile simulation.m -o simulation.exe
.\simulation.exeThe output contains target-native program code and its matching RunMat execution runtime. Running it requires neither the RunMat CLI nor the project source, parser, compiler, VM, JIT, or a separately installed RunMat SDK.
A numerical program depends on services outside its compiled machine instructions. The executable needs array storage, indexing, function calls, multiple outputs, errors, workspace behavior, builtin implementations, file and plotting services, and memory management. Each service must follow the same language rules as an interactive RunMat session.
runmat compile starts from the analyzed MIR and composed project used for VM and JIT execution. It emits native code from the MIR and links the resulting object with a compiler-free build of the RunMat runtime.
MATLAB-syntax source + project/package closure
│
▼
parser → HIR → MIR → analysis
│
▼
immutable executable unit
│
▼
verified Native IR → native object
│
├──────────────┐
▼ ▼
program code matching RunMat runtime archive
│ │
└──────┬───────┘
▼
system linker
│
▼
standalone executableAhead-of-time compilation and the runtime
RunMat executes programs in three forms. The VM starts from portable bytecode. The JIT observes a running program and compiles hot functions or loops. Ahead-of-time compilation, or AOT, produces native code before the program is launched.
VM source → portable bytecode ───────────────▶ execute
JIT source → MIR → execute and observe → native code
AOT source → native object → link ────────────▶ executableAll three forms preserve the same language behavior. A(2, :), nargout, a nested function capture, and a call to svd keep their meaning when execution moves between them.
Duplicating indexing, builtin dispatch, or workspace behavior inside the AOT compiler would create a second numerical runtime. MATLAB syntax makes such duplication especially fragile: the result of a call can depend on requested output count, workspace state, dynamic dispatch, overloaded indexing, and runtime object behavior.
The language operations live in runmat-runtime. Native JIT and AOT functions call them through a versioned interface. Both kinds of entrypoint run through runmat-native-executor, which owns invocation state, roots, calls, exceptions, cancellation, workspace publication, and continuation points.
The AOT compiler emits native control flow and data movement. Indexing, builtin calls, workspace mutation, and other runtime operations cross the shared native interface.
Replacing Turbine with the shared native compiler
Native compilation also replaced RunMat's original JIT, called Turbine. Turbine was a provisional native tier built around VM bytecode. It counted executions by bytecode hash, lowered a supported subset of VM instructions directly to Cranelift, cached the resulting function pointer, and returned execution to the interpreter for unsupported operations. Its implemented fast paths covered scalar arithmetic, local variables, simple control flow, and a selected set of runtime calls.
The VM bytecode boundary limited Turbine. By that stage, source-level control flow and analysis results had been discarded or encoded as VM instructions. Extending Turbine meant recovering program structure from those instructions and maintaining another lowering path alongside MIR. Supporting lexical closures, structured exceptions, suspension, workspace mutation, exact deoptimization, and loop entry required specialized bridges or interpreter fallback.
RunMat now builds both adaptive and ahead-of-time native code from the same immutable executable unit:
MIR + analysis
│
▼
immutable executable unit → verified Native IR → Cranelift lowering
│
┌─────────────────┴────────────────┐
▼ ▼
JIT machine code AOT native object
│ │
publish into session link into executable
└──────────────────┬─────────────────┘
▼
shared native executorThe adaptive JIT adds policy around that compiler substrate. It records function and loop heat, builds bounded runtime profiles, compiles in the background, publishes guarded specializations, enters hot loops at verified headers, invalidates code when dependencies change, and retires old code generations safely. The VM remains available for cold execution and exact continuation.
AOT applies a different policy to the same compiler output. It selects a closed program and builtin set, emits relocatable objects, links the matching runtime archive, and verifies the result as a process image. The finished executable contains no profiler, background compiler, executable-memory manager, or adaptive-tiering policy.
Both paths execute through runmat-native-executor and the same runtime ABI. A correction to Native IR lowering or call semantics therefore applies to interactive native execution and compiled executables together. The JIT publishes machine-code entrypoints into a running session; AOT links native objects into an executable.
Sharing compiler output across execution targets
runmat compile, runmat run, the REPL, Desktop, runmat check, and the JIT share project composition and compiler stages.
The parser produces an AST. Semantic HIR and MIR then resolve program functions, bindings, requested output counts, control flow, call identities, effects, and shapes. Core packages the MIR, analysis facts, bytecode, and source maps into an immutable executable unit bound to the program and environment revision.
Native compilation consumes that unit directly. No source is reparsed, no functions are reconstructed from bytecode, and no separate type-analysis pass runs. The MIR compiler documentation describes the boundary in more detail.
┌──────────────▶ VM bytecode
source → HIR → MIR ────┼──────────────▶ adaptive JIT
├──────────────▶ native AOT object
└──────────────▶ analysis and editor toolingThe native executable retains the compiler's function identities and call graph. Link explanations use the compiler's catalog and reachability facts; unresolved-source diagnostics come directly from program analysis.
Core composes the project before native compilation. The executable closure includes functions from the entry file, configured source roots, and locked package dependencies. Stable cross-directory source belongs under [sources].roots in runmat.toml:
[sources]
roots = ["src", "lib"]A live RunMat session can use addpath(...) to change function lookup. A linked executable contains a fixed source closure, so addpath(...) cannot introduce another source file. Programs designed to load new source during execution should continue to use runmat run.
Native IR as a contract for native code generation
MIR contains more compiler information than an executable needs. RunMat lowers the executable unit into a smaller, bounded Native IR before target-specific object emission.
Native IR assigns stable identities to functions, blocks, values, calls, runtime sites, guards, and continuation points. RunMat verifies it before code generation and again when the linked executable starts. The Native IR manifest contains canonical program metadata and content digests that bind the native object to the corresponding Native IR.
The object emitter lowers verified Native IR through Cranelift into the current host's relocatable format:
macOS Mach-O object
Linux ELF object
Windows COFF objectCompiled native functions exchange opaque, generation-checked value references with a versioned host table. The native ABI excludes Rust's Value layout; the runtime owns values and allocations. The runtime can change Value's internal representation without breaking compiled programs at an undocumented memory-layout boundary.
Before object emission, whole-program reachability removes program functions that cannot be called. Unused helper functions are omitted from the object. At startup, the process checks that every function in the manifest has one native entrypoint and rejects both missing and unexpected entries.
The runtime included in a standalone executable
Consider a small numerical program:
t = linspace(0, 20, 20001);
response = exp(-0.08 .* t) .* sin(2 .* pi .* 3 .* t);
energy = sum(response .* response);
fprintf("Signal energy: %.6f\n", energy);The compiled native functions implement the program's loops, branches, and reads and writes of local variables. Calls to linspace, exp, sin, sum, and fprintf enter the RunMat runtime, which also provides numeric allocation, output handling, and error propagation. The builtin implementations are the same ones used during VM and JIT sessions.
runmat-aot-runtime packages Value, the runtime, garbage collection, and the native executor as a static archive. Source parsing, HIR/MIR lowering, static analysis, Core composition, object emission, the VM, and the adaptive JIT stay out of that archive.
RunMat's release build validates and compresses the archive, adds a manifest, and embeds both in the runmat CLI. The manifest records:
- the native target and object format
- the RunMat and Native IR schema versions
- the runtime and builtin-catalog fingerprints
- the archive and compressed-payload digests
- the exact native linker requirements
- the runtime capabilities supported by the archive
At compile time, RunMat compares the archive manifest with the compiler, host target, runtime identity, native ABI, schemas, and builtin catalog. Any mismatch stops the link. An archive copied from another RunMat build cannot be substituted.
The compiler and runtime archive ship together, with no mutable SDK directory beside the CLI. The two-phase release build creates and identifies the archive first, then embeds that archive in the CLI that will consume it.
The finished program needs no RunMat installation or SDK. It can depend on operating-system libraries and on native libraries enabled in the platform build. Creating it requires a system linker and the target's native link dependencies.
Native-specialized and closed-world linking
MATLAB programs vary in how much work they defer to runtime dispatch. RunMat currently provides two native composition policies.
The default is native-specialized:
runmat compile simulation.m --policy=native-specialized -o simulationThe native-specialized policy compiles the composed program functions to native code and retains runtime builtin discovery. Projects with a known source closure can continue to resolve builtins through the runtime registry. The linker force-loads the runtime archive to retain that registry.
The closed-world policy is selected with:
runmat compile simulation.m --policy=closed-world -o simulationClosed-world compilation requires a bounded set of program and builtin targets. Each reachable catalog-backed builtin contributes a stable native binding symbol. The linked program installs that immutable binding set for its invocation. A lookup outside the set fails instead of falling back to the process-global registry.
The system linker can then extract only referenced archive members and apply platform dead stripping:
macOS archive extraction + -dead_strip
Linux archive extraction + --gc-sections
Windows archive extraction + /OPT:REFClosed-world compilation rejects unknown call targets, unbounded dynamic calls such as feval, legacy builtins without canonical native bindings, and target-conditional bindings whose variant remains unresolved. The compiler reports the unsupported edge instead of silently switching to native-specialized.
The policies differ as follows:
| Policy | Program code | Runtime binding | Best fit |
|---|---|---|---|
native-specialized | Retained functions compiled natively | Broad runtime discovery retained | General statically composed projects |
closed-world | Retained functions compiled natively | Exact proven builtin bindings only | Auditable programs with a fully bounded call graph |
The CLI reserves dynamic-runtime and portable for future workflows. dynamic-runtime requires a source loader and embedded frontend. portable produces a target-independent artifact. Neither is implemented in this workflow, and selecting either returns a capability diagnostic.
Explaining what reached the executable
--explain-link prints each reachable program or runtime node and the edge that retained it:
runmat compile simulation.m --explain-link -o simulationThe report distinguishes direct, finite-dynamic, and unknown reachability. It names retained functions, builtins, classes, provider or extension boundaries, artifact dependencies, and runtime families. A closed-world report also lists every selected builtin binding and native symbol.
For automated inspection, the link plan can be written as deterministic JSON:
runmat compile simulation.m \
--policy=closed-world \
--link-plan-json simulation.link.json \
-o simulationThe JSON document includes source and program graph digests, the target, runtime archive digest, catalog fingerprint, capabilities, reachability, builtin bindings, and runtime-family decisions. CI can inspect it directly or retain it with other build artifacts.
MIR analysis supplies reachability. The builtin catalog supplies binding and extension contracts. AOT orchestration combines those records with the runtime manifest, and the CLI formats the completed plan.
Linking on three operating systems
The final step uses a system linker driver: cc, clang, or gcc on Unix-family systems, and link.exe or lld-link.exe on Windows. --linker or RUNMAT_LINKER can select a specific driver.
RunMat writes the object, archive, and linker arguments into a private temporary directory and passes validated arguments through a response file. Temporary inputs are deleted after the link unless --keep-temps is supplied for diagnosis.
RunMat publishes the output transactionally. It refuses to overwrite an existing executable unless --force is present. Even with --force, the old executable stays in place until the replacement links successfully, so a failed link leaves no partial binary at the requested path.
Three object-optimization levels are available:
runmat compile simulation.m --optimization=none -o simulation-debug
runmat compile simulation.m --optimization=size -o simulation-small
runmat compile simulation.m --optimization=speed -o simulation-fastspeed is the default. The setting affects native object optimization and leaves language semantics and runtime selection unchanged.
WebAssembly and the portable compiler boundary
HIR, MIR, analysis facts, executable identities, and the Native IR schema are portable across RunMat's native and browser builds. The web product consumes those representations without redefining the source language.
Object emission and process linking currently run only on native hosts. A browser cannot launch a Mach-O, ELF, or PE executable, and the wasm32-unknown-unknown build cannot allocate host-native executable memory. Browser programs execute through the portable VM and browser runtime, while the WASM build validates portable executable manifests and the Native IR schema.
The reserved portable policy will need a target-independent distribution and execution contract of its own. The current runmat compile command produces an executable for the host target.
What native compilation supports today
runmat compile accepts a .m entrypoint and produces an executable for the current host. Packaged support covers:
| Platform | Target |
|---|---|
| macOS Apple Silicon | aarch64-apple-darwin |
| macOS Intel | x86_64-apple-darwin |
| Linux x86_64 | x86_64-unknown-linux-gnu |
| Windows x86_64 | x86_64-pc-windows-msvc |
Compilation is currently host-native. The object, runtime archive, linker, ABI, and enabled native libraries must all describe the host target.
Entrypoints may be scripts or zero-input functions. The AOT process launcher cannot currently supply arguments to a function entrypoint. Every source file must be reachable from the entry file, configured source roots, or locked package closure. Runtime-only source loading and the reserved portable and dynamic-runtime policies are not yet available in this workflow.
RunMat stops compilation when it cannot resolve every source and runtime dependency required by the selected policy. The error appears on the build machine, where the compiler and source tree are available, instead of appearing later when the executable is deployed.
Verifying the executable
A successfully emitted object does not test archive creation, linker discovery, system libraries, process startup, runtime binding, output forwarding, or platform packaging. Our tests cover those later stages separately.
| Coverage | What it established |
|---|---|
| Native IR and object validation | Malformed schemas, digests, targets, functions, data sections, and ABI identities are rejected before execution |
| Runtime archive validation | Compiler/runtime/catalog/target mismatches, corrupt payloads, invalid link tokens, and unsupported capability profiles cannot link |
| Reachability and closed-world tests | Unknown calls and noncanonical bindings fail; exact builtin symbols are selected deterministically with no global fallback |
| Architecture guards | The AOT runtime dependency graph cannot regain the VM, JIT, Cranelift compiler, or frontend-only native-codegen features |
| Physical binary inspection | Closed-world binaries match their JSON builtin plan and omit compiler, parser, VM, JIT, and object-emission symbols |
| Shared-executor conformance | Linked entrypoints use the same call, exception, workspace, root, and continuation contracts as native JIT execution |
| Release smoke execution | The packaged CLI compiles a real .m program and the produced executable runs with the expected output |
The release workflow also compiles this program for an end-to-end smoke test:
x = 2 + 3;
disp(x);For each native release target, the packaged runmat binary compiles the source and launches the generated program. The test requires output of exactly 5. The final packages passed on Apple Silicon macOS, Intel macOS, x86_64 Linux, and x86_64 Windows.
A separate closed-world verifier reads the link plan and inspects the physical executable. It rejects extra builtin bindings and any retained compiler, frontend, VM, JIT, or Cranelift symbols. The check compares the file on disk with the policy recorded by the planner.
WASM and browser tests validate the portable executable and Native IR contracts independently. WASM builds stop before native object emission, which tests the portability boundary without relying on the native linker.
Try it
Save this as signal-energy.m:
t = linspace(0, 20, 20001);
response = exp(-0.08 .* t) .* sin(2 .* pi .* 3 .* t);
energy = sum(response .* response);
fprintf("Signal energy: %.6f\n", energy);Run it normally first:
runmat signal-energy.mThen compile and launch it:
runmat compile signal-energy.m -o signal-energy
./signal-energyTo inspect the composition decision:
runmat compile signal-energy.m \
--explain-link \
--link-plan-json signal-energy.link.json \
--force \
-o signal-energyrunmat signal-energy.m and the compiled executable call the same runtime implementations. runmat compile performs native code generation and runtime linking before the program is launched.
See the runmat compile CLI documentation for policies, optimization levels, linker selection, source-root behavior, and platform requirements.
RunMat is an independent, modern runtime for MATLAB-syntax source code. We are not affiliated with MathWorks.
Related posts
Adding and Extending Integer Support for MATLAB Code in RunMat
How RunMat added native fixed-width integer storage, extended integer support across its builtin surface, and preserved typed values through compilation, GPU execution, and persistence.
RunMat Turns One: What We Built and What Comes Next
RunMat's first year: how v0.0.1 grew from a CLI with 87 built-in functions into a GPU-accelerated runtime for the browser, RunMat Cloud, and Desktop.
Switching from MATLAB to Julia
A practical guide to switching from MATLAB to Julia, covering syntax, arrays, performance, packages, Simulink, team training, deployment, and validation.
Enjoyed this post? Join the newsletter
Monthly updates on RunMat internals, development, and performance tips.
Download RunMat
Download RunMat for full performance, or use RunMat in your browser for zero setup.