A ground-up BEAM virtual machine written in Rust, targeting Gleam as its primary source language. Compile your Gleam code with gleam build, then run the resulting .beam bytecode directly on beamr — no Erlang/OTP runtime required.
Built for Meridian workflow execution, where Gleam's type system provides compile-time validation: if the workflow compiles, the types are correct.
If you are on any version below 0.16.3, upgrade. Three classes of
silent memory-safety defect were fixed across 0.16.2 and 0.16.3. None of
them produce an error or a crash — corrupted or freed data is read as valid —
so a passing test suite proves nothing about your exposure.
This is not a 0.16.x problem. The as_bytes borrow-across-allocation
class fixed in 0.16.3 is present in every version from at least 0.4.4:
crates/beamr/src/native/stdlib_stubs/string_bifs.rs is byte-identical
(blob d405462) across all 29 tags from v0.4.4 to v0.15.2 and at the
0.16.2 commit, with all five affected BIFs (trim, split, find, pad,
slice) present throughout. The two classes fixed in 0.16.2 have not
had their introduction point measured — treat them the same way until they
have.
Your options are 0.17.0 or 0.16.3. Both carry the fixes. 0.17.0 is
the current line and the one to prefer; note it is a breaking change
(spawn_link_dirty is removed). 0.16.3 is the last patch on the 0.16.x
line if something holds you there.
A correction to what the earlier advisories told you. The 0.16.2 and
0.16.3 notes described the remaining JIT-reachable sites as "reachable
only under the optional jit feature", which reads as a mitigation you
could apply. It is not one. jit cannot be disabled in any build that
retains threads — such a build does not compile. The word was wrong in the
direction that matters, and it was wrong for 0.16.2 and 0.16.3 exactly as
published. This is a defect under repair, not an intended property.
So neither 0.16.3 nor 0.17.0 is a clean bill of health, and the only
configuration that removes that surface is dropping the threads feature
— not dropping jit. The remaining JIT sites are owned and are not fixed in
any released version to date.
Full mechanics — the affected classes, the exact failing build command, how
to test a git base for the fixes, and what remains open — are in
CHANGELOG.md,
under "Advisory — silent memory-safety defects in every version below 0.16.3" and
"Correction to the 0.16.2 and 0.16.3 advisories". Site-level detail is in
AMENDMENT 3 of
AUDIT.md.
This note points rather than restates, on purpose: two copies of a safety fact drift apart, and the copy a reader happens to find is the one that misleads them. That is exactly how the word optional survived as long as it did.
# Build the CLI
cargo build --release -p beamr-cli
# Run a compiled Gleam module
./target/release/beamr my_module.beam
# Run a specific function with arguments
./target/release/beamr proof.beam proof:factorial/1 -- 10
# => 3628800beamr loads OTP 26 format .beam bytecode files and executes them on a preemptive scheduler with work-stealing, generational garbage collection, and a full BEAM term representation. Gleam compiles to Erlang bytecode, so beamr runs Gleam programs by implementing the subset of the BEAM instruction set and standard library that Gleam actually uses.
The key insight: Gleam doesn't use the full breadth of BEAM/OTP. It generates a predictable, well-structured subset of bytecode. beamr targets that subset precisely, implementing native Rust stubs for the Erlang and Gleam stdlib functions that compiled Gleam code calls rather than loading the original Erlang .beam implementations.
# Run a module's main/0 function (default entry point)
beamr my_module.beam
# Run a specific function with arguments
beamr proof.beam proof:factorial/1 -- 12
# => 479001600
# Alternative --entry flag syntax
beamr proof.beam --entry proof:fibonacci/1 -- 30
# => 832040
# Load dependencies from directories before running
beamr my_app.beam my_app:run/1 --dir ./build/dev/erlang/my_app/ebin -- hello
# Multiple dependency directories
beamr my_app.beam --dir ./deps/gleam_stdlib/ebin --dir ./deps/gleam_otp/ebin
# Check what imports a module needs
beamr imports my_module.beambeamr imports lists everything the module needs that beamr does not provide natively — both unresolved BIFs and module dependencies that must be supplied with --dir. Empty output means all imports resolve to built-in functions, so the module runs standalone.
// proof.gleam
pub fn factorial(n) {
case n {
0 -> 1
_ -> n * factorial(n - 1)
}
}
pub fn fibonacci(n) {
case n {
0 -> 0
1 -> 1
_ -> fibonacci(n - 1) + fibonacci(n - 2)
}
}$ gleam build
$ beamr proof.beam proof:factorial/1 -- 20
2432902008176640000
$ beamr proof.beam proof:fibonacci/1 -- 30
832040
beamr loads entire Gleam projects by pointing --dir at the compiled .beam output. All modules in the directory are loaded into the module registry before the entry module runs, so cross-module calls resolve normally.
# After gleam build, the .beam files are in build/dev/erlang/<project>/ebin/
beamr build/dev/erlang/my_app/ebin/my_app.beam --dir build/dev/erlang/my_app/ebinbeamr can run complete Gleam workflows that read files, execute shell commands, and write output — the full path from source to execution that Meridian uses for workflow orchestration.
// sample_workflow.gleam — reads input, runs a command, writes output
import gleam/io
import gleam/string
@external(erlang, "meridian_ffi", "read_file")
pub fn read_file(path: String) -> String
@external(erlang, "meridian_ffi", "write_file")
pub fn write_file(path: String, content: String) -> Nil
@external(erlang, "meridian_ffi", "run_command")
pub fn run_command(cmd: String, args: List(String)) -> String
pub fn main() {
let input = read_file("input.txt")
let result = run_command("echo", ["processed: " <> input])
write_file("output.txt", string.trim(result))
}The meridian_ffi functions are native Rust BIFs registered in beamr — they're not Erlang code. This is how Meridian exposes host capabilities to Gleam workflows while keeping the type-safe boundary.
crates/
beamr/ Core VM library (~108k lines of Rust)
src/
atom/ Atom table — interned strings with fast integer lookup
gc/ Generational copying garbage collector (minor + major)
interpreter/ Bytecode interpreter, opcode dispatch, pattern matching
loader/ .beam file parser, decoder, module loader
mailbox/ Lock-free process mailboxes with selective receive
native/ 200+ BIF implementations across:
bifs Core erlang BIFs (arithmetic, comparison, type checks)
gate3_bifs Extended erlang BIFs (type conversion, bitwise, math)
gleam_ffi Gleam-specific FFI functions
otp_stubs OTP module stubs (gleam_erlang, gleam_otp)
stdlib_stubs Standard library BIFs (collections, strings, IO, encoding)
process_bifs Process management BIFs (spawn, link, monitor)
process/ Process state, heap, stack, registry
scheduler/ Preemptive scheduler with work-stealing and dirty schedulers
supervision/ OTP-style links, monitors, exit signal propagation
term/ Tagged term representation (integers, atoms, binaries,
tuples, lists, maps, pids, floats, closures)
jit/ Cranelift-backed JIT (+ AOT cache, profiler, safepoints)
distribution/ Distributed-Erlang support (OTP 23+ handshake, control
messages, remote links/monitors, process groups)
etf/ External Term Format (ETF) encode/decode
ets/ ETS in-memory tables (set, ordered_set, bag, match specs)
io/ I/O backend with an io_uring ring on Linux
replay/ Replay driver + step debugger (recorder unwired — see Features)
telemetry/ OpenTelemetry-style spans, metrics, lifecycle events
capability/ Capability-based security layer (sandbox + audit)
tests/ Integration tests (OTP loading, GC, supervision, e2e)
beamr-cli/ Command-line .beam runner
The Meridian integration layer (beamr-meridian) lives in the yggdrasil repository, where it wires MeridianRuntime, the async NIF bridge, and run_workflow into the Meridian orchestration engine.
- Bytecode execution: OTP 26 format
.beamloading and execution, covering the instruction subset Gleam generates - Term representation: Full BEAM term system — small integers, atoms, heap binaries, tuples, lists (cons cells), maps, pids, floats, closures — all using a 64-bit tagged pointer scheme
- Preemptive scheduling: Configurable thread pool with work-stealing (crossbeam deques), reduction counting, dirty scheduler pool for long-running BIFs
- Garbage collection: Generational copying GC wired to the interpreter via
test_heapinstructions, Fibonacci heap growth - Process primitives: Spawn, link, monitor, exit signals, process registry — the core of OTP's actor model
- Supervision: Start-link, restart-on-crash, exit signal propagation through supervision trees
- Mailboxes: Lock-free process mailboxes with selective receive (the
select/1pattern Gleam uses) - 200+ native BIFs: Covering erlang, lists, maps, string, binary, io, math, unicode, rand, uri, and all Gleam stdlib FFI modules
- JSON: Native OTP 27
jsonmodule (decode/1,encode/1,encode_integer/1,encode_float/1,encode_binary/1) sogleam_jsonworks out of the box; Term toserde_json::Valuebridging remains behind thejsonfeature flag - Async NIF support:
wake_with_resultfor suspending a BEAM process and delivering results from host-side async operations - JIT compilation: Cranelift-backed JIT with hot-function profiling, an AOT code cache, and safepoint-based deoptimization
- Distribution (beamr-to-beamr): OTP-style distribution — a handshake in the OTP 23+ shape, control messages carrying OTP's opcode numbers, remote links/monitors, and process groups. beamr peers only: the framing is beamr-private and there is no EPMD, so beamr nodes do not interoperate with real Erlang/OTP nodes (the shared opcode vocabulary is a deliberate door, not a compatibility claim)
- ETF: External Term Format encode/decode for term serialization and distribution
- ETS: In-memory tables (
set,ordered_set,bag) with match-spec query support - io_uring I/O: I/O backend with an io_uring ring on Linux (with a portable fallback)
- Record/replay (incomplete): replay-side machinery, a versioned on-disk
log format, and a step debugger. The recording half is not wired:
ReplayRecorderhas no callers, sobeamr recordwrites a log carrying the run's transcript and no events. The log format also records no module identity, so a recorded run cannot be reconstructed —beamr replayrefuses rather than reprinting the recorded transcript, and no record→replay round trip exists. - Telemetry: OpenTelemetry-style spans, metrics, and process lifecycle events
- Capability security: Capability-based sandbox and audit layer enforced through native BIF dispatch
- Zero unresolved imports: All
gleam_otp.beammodules load cleanly
cargo test --workspace # 1,500+ testsKey architectural choices are documented as ADRs in docs/adr/:
| ADR | Decision |
|---|---|
| 001 | Loader lives inside core crate (not separate) |
| 002 | Global atom table in core |
| 003 | No async in the scheduler — synchronous reduction loop |
| 004 | Low-bit term tagging (3-bit tag in low bits of u64) |
| 005 | Only implement opcodes Gleam generates |
| 006 | BIFs are demand-driven via import table |
| 007 | Supervision is a library layer, not baked into scheduler |
| 008 | Message passing copies terms between process heaps |
| 009 | Reduction boundary hook is a registration point |
| 010 | Dirty scheduler pool for long-running operations |
| 011 | Lock-free mailbox implementation |
Several major subsystems — the JIT, distribution, record/replay, the io_uring I/O backend, and the capability security layer — post-date the current ADRs and are not yet covered by an ADR.
See CONTRIBUTING.md for build instructions, repo layout, and development workflow.
Licensed under the Apache License, Version 2.0. See LICENSE for details.