If you have only seen MATLAB from the outside, it is easy to think of it as a matrix-oriented scripting language: arrays, plots, numerical routines, and a prompt where engineers try things quickly. That is all true, and it is also why MATLAB is still relevant: it is one of the languages engineers and scientists are taught to think in when they learn applied math, simulation, controls, signal processing, optimization, and numerical computing. A lot of teams keep using it because their models, tools, habits, and domain knowledge are already there.
MATLAB is also a large language and runtime environment for scientific and engineering
programs. Real MATLAB code is often split across many files. It uses package folders,
class folders, private helper folders, function handles, dynamically resolved calls,
workspace state, object-oriented classdef classes, multiple return values, overloaded
indexing, and interactive execution. A lot of long-lived engineering code depends on
those semantics.
RunMat is a Rust runtime and compiler for that MATLAB-family code.
As a result, it is not only trying to evaluate simple expressions like A + B. It needs
to execute real-world programs that look more like this, while preserving the semantics
that the original program depends on:
model = SignalModel(samples);
[filtered, stats] = filters.lowpass(model.samples, 30);
model.history{end + 1} = stats;
plot(filtered);To run that correctly, RunMat makes several language questions explicit during compilation and execution:
- Is
SignalModela variable, a function, a class constructor, or an unresolved external name? If it is a variable, is this an attempted array/object call rather than construction? - Does
filters.lowpassrefer to a package function, a static method, or a field access followed by a call? - How many outputs did the caller request from
lowpass, and can the callee observe that throughnargout? - Is
model.samplesa stored property, a dependent property, or an overloaded member access? - What does
endmean insidemodel.history{end + 1}? - Should
plot(filtered)return a value, update figure state, or suppress ordinary output?
Those are not parsing questions. They are semantic questions. For a team trying to run existing engineering code, they are also compatibility questions. If the runtime answers them too late, or answers them differently in the interpreter, language server, JIT, and GPU planner, the system becomes hard to reason about.
RunMat resolves those decisions through a staged compiler pipeline:
source
-> AST
-> semantic HIR
-> MIR
-> MIR analysis
-> VM layout + bytecode
-> runtime/providersThat staging is the useful connection to Rust: source is lowered into progressively more explicit compiler products before execution. Names, functions, classes, bindings, effects, output counts, indexing contexts, and runtime layout become facts that later stages can reuse.
This post explains why MATLAB needs that kind of pipeline and how RunMat's runtime is structured today.
Why MATLAB needs semantic resolution
MATLAB syntax is compact because a lot of meaning is supplied by context. That is part of why the language is productive for numerical work. It is also why MATLAB is hard to execute correctly (and ideally statically): the same surface syntax can mean several different things, and which one is intended depends on the context.
Return to the opening example:
SignalModel(samples)might be a class constructor, a function call, a variable being indexed, or an unresolved external name.filters.lowpassmight be a package function, a static method, or a field access followed by a call.[filtered, stats] = ...requests two outputs, and that output count can affect how the callee behaves throughnargoutorvarargout.model.samplesmight be a stored property, a dependent property, or overloaded member access.model.history{end + 1}is not only indexing; it is an assignment target whoseenddepends on the current value ofhistory.plot(filtered)is a call with figure and display effects.
The same surface syntax can therefore mean several different things. RunMat's compiler records which role the program resolved to, so later runtime paths can share the same answer.
This is why we chose to put a semantic stage in the pipeline. A direct AST-to-bytecode runtime was possible (and was our previous approach), but it tended to scatter these decisions across the interpreter, runtime dispatch, editor tooling, JIT, and acceleration planner. RunMat as of 0.5+ now resolves the source into shared language facts first. That gives the rest of the system one place to ask what the program means, and it lets those facts match what existing MATLAB users expect their code to do.
Pipeline overview
The current pipeline is:
MATLAB source
-> parser AST
-> semantic HIR assembly
-> MIR assembly
-> MIR analysis store
-> VM assembly layout
-> bytecode
-> interpreter/JIT/runtime providersEach stage has a different job.
The AST preserves syntactic structure. Semantic HIR says what the source means as a MATLAB program: which functions exist, which names bind to which language entities, which classes are defined, which calls request which outputs, and which bindings should be visible in the workspace. MIR makes control flow, places, calls, and effects easier to analyze and compile. Analysis attaches facts. VM layout maps semantic bindings into executable frame slots. Bytecode is what the interpreter and JIT consume. Runtime providers handle concrete execution services such as builtin dispatch, plotting, filesystem access, workspace materialization, and acceleration.
The docs version of those layers starts with the Compilation Pipeline overview, then goes deeper on High-Level IR (HIR), Mid-Level IR (MIR), MIR & Static Analysis, and Bytecode Compilation.
The benefit of this separation is that language decisions become reusable. The interpreter can execute them, the language server can explain them, the JIT can compile through them, and the acceleration planner can use them as inputs. The compiler does not have to rediscover from VM stack shape that a call was a two-output call, that an index was a deletion target, or that a function was a nested capture.
The bytecode product also becomes more useful. It is VM bytecode, not platform-specific machine code, so it can be consumed by the interpreter, the JIT, snapshots, and tooling. That is also the direction that makes future ahead-of-time compilation or static binary packaging plausible: the source can lower into stable semantic and bytecode products before a later target decides how to execute or package them. Static binaries would still need to carry or link the relevant RunMat runtime services, and highly dynamic MATLAB features would still need explicit policies, but the architecture no longer ties execution to reinterpreting source text.
Semantic HIR: the language product
RunMat's semantic HIR is the compiler's record of what the MATLAB program means after source layout and names have been resolved. It is represented as an assembly.
An assembly owns tables for:
- modules
- entrypoints
- functions
- classes
- bindings
These are semantic IDs, not VM slots. That distinction is important for MATLAB because
users think in terms of variables, functions, files, classes, packages, and workspaces,
not storage offsets. A binding like total in a nested function has a language identity
before the VM decides where it lives in a frame. A class method belongs to a class before
the runtime decides how to dispatch it. A function call can refer to a builtin, bound
function, imported path, dynamic name, or external boundary before bytecode is emitted.
A HIR function carries its MATLAB ABI:
- fixed inputs
varargin- fixed outputs
varargout- implicit
nargin - implicit
nargout - captures
- parent function
- enclosing class, if any
That lets RunMat represent the opening example as language structure instead of VM stack
shape: SignalModel can be resolved as a constructor, filters.lowpass can be resolved
as a package function or method-like call, the call can carry an exact requested output
count of two, model.history{end + 1} can be marked as an indexed assignment target, and
plot(filtered) can be treated as an effectful plotting call.
The same structure also covers other MATLAB function behavior that engineers rely on:
local functions, nested functions that share parent scope, anonymous functions, function
handles, nargin, nargout, varargin, and varargout. If the compiler loses those
relationships, the code may still parse, but it will not behave like MATLAB.
Calls also carry semantic identity. A call target can be a bound function, builtin,
imported path, dynamic expression, super constructor, super method, or unresolved
qualified name. The call also carries requested output count and source syntax. That is
how later stages know the difference between direct calls, method syntax, dotted calls,
feval, and output-expanding calls.
Indexing is also semantic. HIR records the index kind and result context: paren or brace, read or assignment, deletion target or comma-list expansion, function-argument expansion or ordinary single read.
That context becomes critical later because the same surface syntax can mean read, write, deletion, expansion, or overloaded object dispatch.
MIR: control flow and compiler facts
HIR is still close to MATLAB language structure. MIR is lower-level. It is the form where RunMat turns language meaning into explicit control flow and assignable places.
RunMat's MIR assembly contains bodies keyed by function. A MIR body has locals and basic blocks. Blocks contain statements and a terminator. Places represent assignable locations. Rvalues represent computed values.
MIR can represent:
- local and binding places
- member places
- dynamic member places
- indexed places
- assignments
- multi-assignments
- expression statements
- workspace effects
- environment effects
- branches
- loops
- switch
- try/catch
- return
- await
- future creation
- spawn
- tensor, cell, struct, and object literals
- calls with requested output counts
This gives the compiler a form that is easier to analyze. For example, initialization analysis is more natural on MIR blocks than on a source syntax tree. So is spawn-safety analysis, fusion candidate detection, and validation that an indexed assignment target was lowered with an assignment context.
MIR also gives RunMat a place to keep "place" and "value" roles separate. A read from
A(i) and a write to A(i) share source syntax but have different roles. MIR can
preserve that difference explicitly.
Analysis: facts that feed execution
RunMat's MIR analysis stores facts that later stages can use.
Examples include:
- whether a local is unassigned, maybe assigned, or definitely assigned
- simple type facts
- shape facts
- value-flow facts
- async/future facts
- spawn boundary metadata
- diagnostics
- fusion eligibility signals
These facts are not only for error messages. They feed the runtime architecture. The point is not to make MATLAB statically typed; it is to give the runtime enough shared facts that execution, tooling, and acceleration do not each invent their own approximate model of the program.
For acceleration, semantic and MIR facts give the planner more to work with than a bytecode scan. The compiler can identify MIR regions that are semantically pure enough or structurally suitable enough to be fusion candidates. Runtime planning can then reconcile those candidates with the actual acceleration graph, provider capabilities, and concrete residency of values.
That split is deliberate. The compiler provides semantic facts. The runtime and provider own physical execution details. This is especially important for numerical code because the same source may run on host arrays, GPU arrays, or a mix of both depending on the session and provider state.
VM layout: semantic bindings become frame slots
HIR and MIR keep semantic identity separate from VM slot numbers. MATLAB programs have
workspace-visible variables, local variables, persistent variables, global variables, and
implicit values like ans; keeping those identities intact makes it easier to preserve
the user's model of the program while still compiling to efficient execution storage.
Instead, RunMat derives a VM layout after semantic lowering and MIR construction. The layout maps:
- function frame ABI fields to slots
- semantic bindings to slots
- MIR locals to slots
- captures to capture slots
- entrypoint exports to workspace-visible names
- global and persistent bindings to storage bindings
This keeps language semantics separate from execution storage.
For example, a script entrypoint may export top-level bindings into the workspace. A
function entrypoint may return outputs instead. A local binding may be hidden. An implicit
ans binding may have special workspace visibility. Those are semantic/workspace
policies first. VM layout later decides which frame slot holds each value.
This is the same general principle that makes compiler IRs useful: the program's meaning can stay independent from the storage scheme chosen by the final execution engine. For RunMat, that means a workspace variable remains a workspace variable even though the VM eventually stores it in a frame slot.
Bytecode and callable descriptors
After layout, RunMat compiles MIR into VM bytecode. This is the point where the program becomes directly executable, but it is not the point where RunMat first learns what the program means.
The VM compiler emits instructions for arithmetic, calls, indexing, stores, object operations, returns, async descriptors, and runtime effects. But by the time bytecode is emitted, many hard semantic decisions have already been made. As a result, the VM is doing a fairly minimal amount of work to translate simple bytecode into machine code.
Calls are compiled from typed callable identities and fallback policies. That means the runtime can distinguish:
- a bound semantic function
- a builtin
- a dynamic name
- an external qualified name
- a method identity
- a super call
- a
fevaltarget
This is especially important for function handles. MATLAB users pass functions around as values in optimizers, callbacks, event handlers, array operations, and plotting workflows. A function handle should preserve what it points to when possible. If it refers to a semantic function, RunMat can preserve that semantic identity instead of degrading the handle into a string and hoping name lookup finds the same thing later.
RunMat's callable descriptor path keeps the identity,
arguments, requested output count, and fallback policy together. That lets feval,
closures, dynamic names, method handles, and external names share one routing model.
Project manifests and composition
The semantic pipeline becomes much more useful when RunMat knows the shape of the project
it is compiling. As of the 0.5 release, that project boundary is explicit. A
runmat.toml project manifest can name the
package, declare source roots and local dependencies, and define runnable entrypoints.
The manifest shape is intentionally small and should feel familiar to modern programmers:
[package]
name = "signal-demo"
version = "0.1.0"
[sources]
roots = ["src", "examples"]
[dependencies]
filters = { path = "deps/filters", version = "0.1.0" }
[entrypoints.main]
path = "src/main"
[entrypoints.demo]
path = "examples/demo"That manifest can sit next to an existing MATLAB-style source tree:
signal-demo/
runmat.toml
src/
main.m
@SignalModel/
SignalModel.m
private/
normalizeSamples.m
examples/
demo.m
deps/
filters/
runmat.toml
src/
+filters/
lowpass.mThe MATLAB parts remain MATLAB parts. @SignalModel is still the class folder.
+filters is still the package folder. private/normalizeSamples.m is still a private
helper. main.m can still be a normal script or function file. RunMat is not asking the
project to become a Rust crate or a new module syntax.
The added runmat.toml gives that source tree a stable compiler boundary. [sources]
tells RunMat which folders are part of the project source index. [dependencies] makes
the local filters project available to name resolution. [entrypoints.main] and
[entrypoints.demo] give users and hosts named workflows instead of relying on whichever
file path happened to be executed first.
The Module Composition docs go deeper on the source index, composition graph, package folders, class folders, private helpers, dependencies, and entrypoint resolution behind that behavior.
From the CLI, those entrypoints are now stable commands:
cd signal-demo
runmat run main
runmat run demoThat is the difference between path execution and project composition. Without a manifest, RunMat can still execute a file by path and discover nearby companion source where the runtime can do so safely. With a manifest, the compiler knows the intended source roots, dependency projects, and runnable entrypoints up front.
For the opening example, that means SignalModel(samples) can resolve against the
@SignalModel class under src, while filters.lowpass(...) can resolve through the
declared filters dependency. The same composition graph is available to the interpreter,
bytecode compiler, LSP, JIT boundary, and acceleration planner.
import still has its MATLAB job: it controls which names are visible and convenient
inside source files. The dependency declaration makes the filters project available; an
import can then make its package functions convenient inside main.m:
import filters.*
model = SignalModel(samples);
[filtered, stats] = lowpass(model.samples, 30);Dependencies have a different job: they declare which external projects are available to the resolver. Keeping those responsibilities separate lets existing MATLAB code keep its source conventions while giving RunMat a reproducible project graph.
Builtins as runtime metadata
RunMat's builtins are not only function pointers. For MATLAB
users, builtins are the standard library surface: sum, plot, size, zeros, fft,
table, struct, feval, and hundreds of others. Their signatures, output behavior,
errors, and editor documentation are part of the language experience.
Builtins can carry descriptors for:
- signatures
- input parameters
- output parameters
- fixed vs requested-output-dependent behavior
- completion policy
- stable errors
This makes runtime behavior, editor tooling, and generated documentation point at the same source of truth.
For example, an LSP signature-help request can use builtin descriptors instead of a separate hand-maintained list. Runtime errors can be thrown from descriptor-backed rows instead of duplicating stable identifiers in several files. Generated reference docs can stay aligned with the same metadata that the runtime uses.
That does not remove the implementation complexity of each builtin. It does make the public contract of a builtin more explicit, and it gives runtime, docs, and tooling the same contract to read from.
It also makes each builtin an isolated unit of behavior. Each builtin has its own suite of tests, documentation, and error messages that define, constrain, and enforce their contracts.
Classes and objects
MATLAB classes add another layer of semantic resolution. If your
picture of MATLAB is only scripts and matrices, classdef can look secondary. In real
engineering code, classes are often how teams package models, hardware abstractions,
simulations, and domain-specific APIs.
A classdef source file defines class metadata: name, superclass, kind, properties,
methods, events, enumerations, attributes, defaults, access rules, and method bodies.
RunMat lowers that into semantic HIR and registers runtime class metadata during
compilation/execution.
In the opening example, SignalModel(samples) may be constructing an object from a
source classdef. RunMat records whether SignalModel is a value class or a handle
class, which properties it defines, which methods are static or instance methods, which
access rules apply, and whether model.history{end + 1} = stats mutates stored object
state or routes through overloaded indexing. Those are language semantics, not bytecode
conveniences. If they are wrong, the program may run but produce the wrong model state.
Custom indexing adds another example. If a class defines subsref or subsasgn, ordinary
member/index syntax may dispatch through that protocol. RunMat's object indexing
descriptor path keeps those operations explicit so overloaded
indexing can coexist with normal property access.
Acceleration and provider boundaries
RunMat's acceleration model separates semantic eligibility from concrete residency.
The compiler can identify candidate groups and semantic operation kinds. Builtin metadata can mark operations as elementwise, reductions, shape transforms, sinks, or effectful. MIR analysis can indicate whether a region is structurally suitable for fusion.
But the runtime/provider decides whether values are actually resident on a GPU, whether a provider supports an operation, whether data must be gathered, whether a fallback is required, and which concrete kernel or host path should run.
That separation prevents a common mistake: assuming source syntax alone determines GPU execution. In RunMat, compiler facts guide acceleration, but provider state owns physical execution. The user-facing goal is straightforward: preserve MATLAB behavior first, then use acceleration when the runtime can prove the provider path is appropriate.
JIT status
RunMat has a JIT path through Turbine and Cranelift, but the JIT is not complete yet and is not the normal execution path for most programs. Today, most bytecode patterns still fall back to the interpreter. That is the right tradeoff at this stage: the 0.5 work was mostly about making the compiler and runtime agree on MATLAB semantics before compiling more of those semantics to native code.
The important change is that the JIT now has the right boundary to build on. Semantic function identity, requested output counts, expanded arguments, and non-scalar runtime values can cross through a tagged value ABI instead of being flattened into f64-only lanes or string lookups. That means Turbine can compile from the same bytecode and semantic facts as the interpreter.
From here, broader Cranelift lowering is mostly compiler engineering rather than a new language-semantics project. More JIT coverage is planned before 1.0, with the interpreter remaining the fallback whenever a bytecode pattern is not compiled yet.
What this unlocks
MATLAB remains useful because engineering teams have years of models, scripts, classes, and workflows built around its semantics. That is also what makes a new runtime hard. Workspaces, functions, classes, output counts, indexing, packages, dynamic calls, builtins, GPU residency, and interactive execution all interact.
Trying to handle those interactions directly in an interpreter works for small cases, but it becomes brittle as the language surface grows. Every missing semantic product turns into a runtime heuristic. Every runtime heuristic becomes harder for tooling, diagnostics, JIT, and acceleration to share. For users, that usually shows up as incompatibility: something works in one context, fails in another, or behaves differently once the code moves out of the prompt and into a project.
With the 0.5 runtime architecture, RunMat can treat more of that code as a real project instead of isolated snippets. Source roots, package folders, class folders, private helpers, function handles, workspace-visible bindings, object metadata, requested output counts, and display effects now have places in the runtime model.
That means several paths get better at the same time:
- Multi-file MATLAB-family code can resolve companion functions, classes, packages, and private helpers through project-aware source discovery.
- The interpreter, language server, JIT, and acceleration planner can share semantic facts instead of rediscovering names, output counts, and indexing behavior independently.
- Hosts such as notebooks, REPLs, desktop integrations, and WebAssembly embeddings can use structured execution outcomes for workspace changes, display events, diagnostics, and figures.
- Future native execution work has a cleaner target: bytecode plus semantic products, rather than source text plus runtime guesses.
You can now use RunMat to execute real project-shaped code: folders with helper functions, package directories, classes, callbacks, plotting, and ordinary workspace-driven workflows. Those are exactly the cases this release was built to start handling systematically.
For the release-focused view of what changed in 0.5, see the 0.5 release post.
Related docs
- Compilation Pipeline, High-Level IR (HIR), Mid-Level IR (MIR), and MIR & Static Analysis for the compiler stages.
- Projects and Module Composition for
runmat.toml, source roots, dependencies, packages, classes, private helpers, and entrypoints. - Bytecode Compilation, Callable Resolution & Function Dispatch, and Indexing Subsystem for VM lowering and execution boundaries.
- Builtins, Execution Requests, and Editor Features for the metadata shared by runtime execution and tooling.
- Fusion Engine & Residency Management and JIT Pipeline for acceleration and Turbine status.
We're excited to see what you build with RunMat!
Related posts

Introducing RunMat Desktop: a high-performance MATLAB alternative
RunMat Desktop is a local, GPU-accelerated workspace for MATLAB-syntax code: editor, plots, variables, notebooks, run history, and an agent that can inspect your project.

RunMat Runtime 0.5 is out: multi-file project support, and resolving MATLAB language semantics like Rust's compiler
RunMat Runtime 0.5 adds manifest-backed multi-file projects, named entrypoints, stronger MATLAB function/class/indexing semantics, and a shared compiler foundation for tooling, acceleration, and JIT work.

Open-Source MATLAB Alternatives 2026: Speed, Compatibility & Ease of Use
Benchmarks, compatibility, and setup time for four free MATLAB alternatives. Which runs your .m files, which needs a rewrite, and which can you try in 5 seconds?
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.