Back to Blog

Compiling MATLAB Code to Native Executables with RunMat

Published08/27/2026
14 min read

RunMat can now compile a MATLAB-syntax program into a native executable:

runmat compile simulation.m -o simulation
./simulation

On Windows, RunMat can compile to a native .exe:

runmat compile simulation.m -o simulation.exe
.\simulation.exe

The 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 executable

Ahead-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 ────────────▶ executable

All 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 executor

The 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 tooling

The 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 object

Compiled 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 simulation

The 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 simulation

Closed-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:REF

Closed-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:

PolicyProgram codeRuntime bindingBest fit
native-specializedRetained functions compiled nativelyBroad runtime discovery retainedGeneral statically composed projects
closed-worldRetained functions compiled nativelyExact proven builtin bindings onlyAuditable 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 simulation

The 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 simulation

The 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-fast

speed 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:

PlatformTarget
macOS Apple Siliconaarch64-apple-darwin
macOS Intelx86_64-apple-darwin
Linux x86_64x86_64-unknown-linux-gnu
Windows x86_64x86_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.

CoverageWhat it established
Native IR and object validationMalformed schemas, digests, targets, functions, data sections, and ABI identities are rejected before execution
Runtime archive validationCompiler/runtime/catalog/target mismatches, corrupt payloads, invalid link tokens, and unsupported capability profiles cannot link
Reachability and closed-world testsUnknown calls and noncanonical bindings fail; exact builtin symbols are selected deterministically with no global fallback
Architecture guardsThe AOT runtime dependency graph cannot regain the VM, JIT, Cranelift compiler, or frontend-only native-codegen features
Physical binary inspectionClosed-world binaries match their JSON builtin plan and omit compiler, parser, VM, JIT, and object-emission symbols
Shared-executor conformanceLinked entrypoints use the same call, exception, workspace, root, and continuation contracts as native JIT execution
Release smoke executionThe 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.m

Then compile and launch it:

runmat compile signal-energy.m -o signal-energy
./signal-energy

To inspect the composition decision:

runmat compile signal-energy.m \
  --explain-link \
  --link-plan-json signal-energy.link.json \
  --force \
  -o signal-energy

runmat 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.

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.