Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
88 commits
Select commit Hold shift + click to select a range
5db7d13
Add isolated QuickJS bytecode interpreter kernel
dannote Jul 10, 2026
e810933
Add resumable frame stacks and closure cells
dannote Jul 10, 2026
7d53aa0
Add JavaScript exception unwinding
dannote Jul 10, 2026
09aa0cd
Add process-owned object heap
dannote Jul 10, 2026
148edd8
Add async BEAM handlers and Promise scheduling
dannote Jul 10, 2026
e6f59ad
Fix nested package import rewriting
dannote Jul 10, 2026
5ca74e0
Add Preact SSR runtime slice
dannote Jul 10, 2026
9e694b1
Add VM memory containment
dannote Jul 10, 2026
14d24bf
Add source-mapped VM JavaScript errors
dannote Jul 10, 2026
9aab5d7
Tune VM process heap containment
dannote Jul 10, 2026
14b5c46
Stabilize VM containment tests
dannote Jul 10, 2026
5bab7b2
Update npm lockfile for SSR dependencies
dannote Jul 10, 2026
caecfa8
Add detached async frames and Promise reactions
dannote Jul 10, 2026
b84803c
Document VM internals without suppressing docs
dannote Jul 10, 2026
10b29ea
Harden VM object and string semantics
dannote Jul 10, 2026
ce65322
Add resumable property accessor semantics
dannote Jul 10, 2026
5fba5ed
Add pinned Test262 conformance gate
dannote Jul 10, 2026
8c90520
Decode Test262 metadata with JSONCodec
dannote Jul 10, 2026
bc5f83e
Document prototype extraction audit
dannote Jul 10, 2026
32e9f53
Add JavaScript error hierarchy and close Test262 baseline
dannote Jul 10, 2026
b92d039
Extract canonical VM property semantics
dannote Jul 13, 2026
445e652
Extract canonical VM invocation semantics
dannote Jul 13, 2026
aa943be
Extract canonical VM async semantics
dannote Jul 13, 2026
9e6687d
Extract canonical VM exception semantics
dannote Jul 13, 2026
ba457f2
Consolidate canonical VM value semantics
dannote Jul 13, 2026
6b24c43
Split stack and value opcode families
dannote Jul 13, 2026
c16dba1
Split control and local opcode families
dannote Jul 13, 2026
273f448
Split object and property opcode family
dannote Jul 13, 2026
44b1ac1
Split invocation opcode family
dannote Jul 13, 2026
de92f45
Preserve sparse array holes in native callbacks
dannote Jul 13, 2026
ba2f72a
Add declarative VM builtin DSL
dannote Jul 13, 2026
c3ba297
Migrate resumable builtins to declarative DSL
dannote Jul 13, 2026
c8e5ff7
Migrate Object and Array builtins to DSL
dannote Jul 13, 2026
4355e1c
Migrate String and Number builtins to DSL
dannote Jul 13, 2026
e6ebf2b
Polish declarative builtin DSL
dannote Jul 13, 2026
097b6ab
Migrate Promise builtins to declarative DSL
dannote Jul 13, 2026
c4658ac
Add resumable Symbol iterator semantics
dannote Jul 13, 2026
4613232
Retire legacy builtin method dispatch
dannote Jul 13, 2026
6e2065b
Make constructor bootstrap declarative
dannote Jul 13, 2026
e7c6315
Expand pinned ECMAScript conformance gate
dannote Jul 13, 2026
cb1e0e5
Serialize native resource shutdown
dannote Jul 13, 2026
c1bbcc3
Add Vue and Svelte SSR acceptance
dannote Jul 13, 2026
d49e0ab
Serialize third-party addon lifecycle tests
dannote Jul 13, 2026
f40a698
Discover builtin registry from app manifest
dannote Jul 13, 2026
2366a9f
Add bounded VM mutation corpus
dannote Jul 13, 2026
548fec8
Guard native addon reinitialization
dannote Jul 13, 2026
b8132d0
Publish BEAM VM SSR measurements
dannote Jul 13, 2026
e277739
Define bounded BEAM compiler contract
dannote Jul 13, 2026
73a579d
Add bounded compiler module pool
dannote Jul 14, 2026
0dcec02
Add structured generated module backend
dannote Jul 15, 2026
48303b4
Add first bounded pure compiler lowering
dannote Jul 15, 2026
10bb81c
Specialize pure compiler block forms
dannote Jul 15, 2026
cad3931
Add compiler orchestration and scheduler gates
dannote Jul 15, 2026
a703c92
Publish compiler SSR compatibility gate
dannote Jul 15, 2026
85d7a67
Add bounded nested compiler re-entry
dannote Jul 15, 2026
041fd18
Add guarded scalar compiler lowering
dannote Jul 15, 2026
bc1aaf5
Add scalar compiler actions and cached host templates
dannote Jul 16, 2026
0a332b8
Add bounded compiler coverage counters and globals
dannote Jul 16, 2026
dbf6768
Measure bounded compiler region admission
dannote Jul 16, 2026
76b1e43
Remove redundant array allocation
dannote Jul 16, 2026
d616207
Compact default array descriptors
dannote Jul 16, 2026
0ed8c35
Document BEAM object allocation findings
dannote Jul 16, 2026
dff7429
Compact ordinary object descriptors
dannote Jul 16, 2026
8e5ada3
Measure compact object allocation
dannote Jul 16, 2026
4689d57
Measure bounded object shapes
dannote Jul 16, 2026
416f874
Measure object literal allocation plans
dannote Jul 16, 2026
81d7270
Attribute interpreter allocation hotspots
dannote Jul 16, 2026
8f498a5
Measure closure allocation fast paths
dannote Jul 16, 2026
d8e01ad
Add bounded shared VM programs
dannote Jul 16, 2026
971f6c2
Rename shared programs to pinned programs
dannote Jul 17, 2026
57e87e2
Harden pinned program residency
dannote Jul 17, 2026
3155f41
Prepare BEAM VM release candidate
dannote Jul 17, 2026
5d53f6c
Merge branch 'master' into beam-vm-release-candidate
dannote Jul 17, 2026
11f656d
Remove VM review artifacts and regex parsers
dannote Jul 17, 2026
a791fda
Normalize BEAM VM module namespaces
dannote Jul 17, 2026
e958cf2
Use complete OTP 27 release in CI
dannote Jul 17, 2026
b4d5252
Stabilize the BEAM VM public API
dannote Jul 17, 2026
53177f9
Decouple VM runtime from compiler internals
dannote Jul 17, 2026
8cdebe1
Quarantine the internal VM compiler facade
dannote Jul 17, 2026
42c5774
Compare native and BEAM VM SSR latency
dannote Jul 17, 2026
95a229f
Reduce compiler overhead on interpreted SSR
dannote Jul 17, 2026
e640238
Expose bounded scalar eligibility diagnostics
dannote Jul 17, 2026
07573df
Harden scalar generated register liveness
dannote Jul 17, 2026
2c3213b
Add isolated VM global calls
dannote Jul 17, 2026
2d90c0e
Exclude generated Zig from Hex packages
dannote Jul 17, 2026
c3e0b8d
Narrow generated Zig package exclusion
dannote Jul 17, 2026
0cdbbcf
Harden and polish BEAM VM implementation
dannote Jul 17, 2026
7822771
Use fixed VM ABI atom vocabulary
dannote Jul 17, 2026
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
21 changes: 20 additions & 1 deletion .formatter.exs
Original file line number Diff line number Diff line change
@@ -1,4 +1,23 @@
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs,zig}"],
plugins: [Zig.Formatter]
plugins: [Zig.Formatter],
locals_without_parens: [
builtin: 2,
builtin: 3,
static: 1,
static: 2,
method: 1,
method: 2,
prototype: 1,
prototype: 2,
static_value: 2,
static_value: 3,
prototype_value: 2,
prototype_value: 3,
constant: 2,
getter: 1,
getter: 2,
accessor: 2,
static_accessor: 2
]
]
19 changes: 15 additions & 4 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ jobs:
runs-on: ubuntu-latest
strategy:
matrix:
otp: ['27.0']
otp: ['27.3']
elixir: ['1.18']
env:
MIX_ENV: test
Expand Down Expand Up @@ -61,6 +61,17 @@ jobs:
if [ $rc -ne 0 ] && grep -q "0 failures" /tmp/ci.log; then exit 0; fi
exit $rc

- name: Checkout pinned Test262 corpus
run: |
git clone --filter=blob:none --sparse https://github.com/tc39/test262.git /tmp/quickbeam-test262
git -C /tmp/quickbeam-test262 checkout d1d583db95a521218f3eb8341a887fd63eda8ff1
git -C /tmp/quickbeam-test262 sparse-checkout set harness test

- name: BEAM VM copied and pinned conformance gates
env:
TEST262_PATH: /tmp/quickbeam-test262
run: mix test test/vm/test262_test.exs --include test262

ubsan:
name: UBSan + Zig Debug
runs-on: ubuntu-latest
Expand All @@ -74,7 +85,7 @@ jobs:

- uses: erlef/setup-beam@v1
with:
otp-version: '27.0'
otp-version: '27.3'
elixir-version: '1.18'

- uses: goto-bus-stop/setup-zig@v2
Expand All @@ -87,8 +98,8 @@ jobs:
path: |
deps
_build
key: ${{ runner.os }}-ubsan-27.0-1.18-${{ hashFiles('mix.lock') }}-${{ hashFiles('lib/quickbeam/*.zig') }}
restore-keys: ${{ runner.os }}-ubsan-27.0-1.18-
key: ${{ runner.os }}-ubsan-27.3-1.18-${{ hashFiles('mix.lock') }}-${{ hashFiles('lib/quickbeam/*.zig') }}
restore-keys: ${{ runner.os }}-ubsan-27.3-1.18-

- run: mix deps.get
- run: mix npm.get
Expand Down
34 changes: 34 additions & 0 deletions .github/workflows/vm-fuzz.yml
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
name: VM mutation corpus

on:
workflow_dispatch:
schedule:
- cron: '17 3 * * 1'

jobs:
decoder-verifier:
runs-on: ubuntu-latest
timeout-minutes: 20
env:
MIX_ENV: test

steps:
- uses: actions/checkout@v4

- uses: erlef/setup-beam@v1
with:
otp-version: '27.3'
elixir-version: '1.18'

- name: Cache dependencies
uses: actions/cache@v4
with:
path: |
deps
_build
key: ${{ runner.os }}-vm-fuzz-27.3-1.18-${{ hashFiles('mix.lock') }}
restore-keys: ${{ runner.os }}-vm-fuzz-27.3-1.18-

- run: mix deps.get
- run: mix compile --warnings-as-errors
- run: mix quickbeam.vm.fuzz --iterations 100000 --seed 5325389
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,12 @@

## Unreleased

- Add `QuickBEAM.VM`, an isolated BEAM interpreter for verified QuickJS v26 bytecode with async/await, asynchronous `Beam.call` handlers, JavaScript errors, explicit resource limits, deterministic measurements, and bounded Test262 and SSR coverage. `QuickBEAM.VM.call/4` and `measure_call/4` initialize a fresh heap and invoke a named global with a shape matching the native call API.
- Add explicit immutable program pinning through `QuickBEAM.VM.pin/1` and `QuickBEAM.VM.unpin/1`, with fixed slots, single-flight admission, monitored evaluation leases, restart restoration, and decoded-residency limits.
- Add an internal bounded BEAM compiler benchmark tier with fixed generated-module slots and explicit deoptimization. Compiler execution is not exposed through the public VM facade and remains release-quarantined.
- Compact default object and array descriptors while preserving property ordering, reflection, and deterministic memory accounting.
- Serialize native addon initialization and reject unsafe cross-runtime or post-reset reuse unless `allow_reinitialization: true` is explicitly selected.

## 0.10.20

- Add grapheme-only `Intl.Segmenter` support through `unicode-segmenter` 0.17.0.
Expand Down
58 changes: 58 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,64 @@ QuickBEAM.eval(rt, "function greet(name) { return 'hi ' + name }")
QuickBEAM.stop(rt)
```

## Isolated BEAM execution

`QuickBEAM.VM` decodes QuickJS bytecode and evaluates it with an isolated BEAM
interpreter. Each evaluation owns its JavaScript heap, globals, jobs, handlers,
and resource accounting. Long-lived SSR bundles can be pinned explicitly so
request processes copy only a lightweight handle:

```elixir
{:ok, source} = File.read("priv/server.js")
{:ok, program} = QuickBEAM.VM.compile(source, filename: "server.js")
{:ok, pinned} = QuickBEAM.VM.pin(program)

try do
QuickBEAM.VM.call(pinned, "render", [%{title: "Catalog"}],
profile: :ssr,
handlers: %{"load_props" => fn [] -> %{title: "Catalog"} end},
max_steps: 20_000_000,
memory_limit: 64 * 1024 * 1024,
timeout: 2_000
)
after
QuickBEAM.VM.unpin(pinned)
end
```

The program store is fixed-capacity and has no implicit eviction or fallback.
Applications should have one supervised lifecycle owner pin each bundle during
startup and unpin it during replacement or normal shutdown.

`QuickBEAM.VM.call/4` deliberately mirrors `QuickBEAM.call/4`, but their
lifecycles differ: native calls reuse persistent JavaScript state, while every
VM call initializes a fresh heap from the immutable program before invoking the
global function.

### Isolated VM limitations

- Programs and serialized bytecode are locked to the exact vendored QuickJS
bytecode version and ABI fingerprint. Recompile programs when upgrading.
- Source compilation uses a short-lived native QuickJS compiler; execution is
performed by the BEAM interpreter.
- Each `eval` or `call` owns fresh globals, heap objects, jobs, and handlers.
Mutations never persist into another evaluation.
- The BEAM interpreter implements a bounded, tested JavaScript subset rather
than every native QuickJS feature. Unsupported bytecode and semantics fail
explicitly without falling back to native execution.
- `memory_limit` bounds deterministic logical VM allocation. It is distinct
from endpoint BEAM process memory, which is reported by `measure/2` and
`measure_call/4`. Each evaluation permits at most 64 concurrently outstanding
asynchronous `Beam.call` handler operations.
- `:core` and `:ssr` select fixed builtin profiles. They do not expose the full
browser, Node.js, DOM, WASM, or native-addon surfaces of a native runtime.
- Pinned storage has fixed capacity and residency budgets, no implicit eviction,
and no stale-handle fallback.

The bounded BEAM compiler remains an internal, release-quarantined benchmark
and conformance tier. It is not selectable through the public `QuickBEAM.VM`
evaluation options; production evaluation always uses the interpreter.

## BEAM integration

JS can call Elixir functions and access OTP libraries:
Expand Down
23 changes: 23 additions & 0 deletions bench/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -19,8 +19,31 @@ MIX_ENV=bench mix run bench/call_with_data.exs
MIX_ENV=bench mix run bench/beam_call.exs
MIX_ENV=bench mix run bench/startup.exs
MIX_ENV=bench mix run bench/concurrent.exs

# Isolated BEAM VM SSR and resource gates
MIX_ENV=bench mix run bench/vm_ssr.exs --pinned-programs

# Native QuickJS versus pinned interpreter/compiler SSR latency
MIX_ENV=bench mix run bench/vm_ssr_compare.exs --samples 50 --warmup 10

# Single-scheduler fairness and timeout containment
ERL_FLAGS='+S 1:1' MIX_ENV=bench \
mix run bench/vm_scheduler_probe.exs --pinned-programs

# Interpreter sampling and bounded compiler performance
ERL_FLAGS='+S 1:1 +JPperf true' MIX_ENV=bench \
mix run bench/vm_interpreter_perf.exs --mode pinned
MIX_ENV=bench mix run bench/vm_compiler_perf.exs
```

The VM runners accept explicit interpreter or compiler profiles. Compiler
execution remains release-quarantined; the interpreter is the default. The SSR
comparison reports both a persistent native runtime and a bare native runtime
created per request, because only the latter shares the BEAM VM's isolated-heap
lifecycle. The interpreter runner uses the public `QuickBEAM.VM.call/4` shape;
internal compiler runners use an equivalent request wrapper. Compilation and
service initialization are excluded from warm latency.

## Results

Apple M1 Pro, Elixir 1.18.4, OTP 27, Zig 0.15.2 (ReleaseFast).
Expand Down
85 changes: 85 additions & 0 deletions bench/vm_compiler_perf.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,85 @@
Mix.Task.run("app.start")

alias QuickBEAM.VM.Compiler
alias QuickBEAM.VM.Runtime.Engine

iterations =
case Integer.parse(System.get_env("COMPILER_PERF_ITERATIONS", "500")) do
{value, ""} when value > 0 -> value
_invalid -> raise "COMPILER_PERF_ITERATIONS must be a positive integer"
end

workloads = [
{"arithmetic_loop", "(function(n){let s=0; for(let i=0;i<n;i++) s=s+i*2; return s})(100)"},
{"branch_loop",
"(function(n){let s=0; for(let i=0;i<n;i++){if((i&1)===0)s+=i;else s-=i} return s})(100)"},
{"local_arithmetic",
"(function(n){let a=1,b=2,c=3; for(let i=0;i<n;i++){a=b+c+i;b=a+c;c=a+b} return c})(100)"},
{"array_sum",
"(function(arr){let s=0;for(let i=0;i<arr.length;i++)s+=arr[i];return s})([1,2,3,4,5,6,7,8,9,10,11,12,13,14,15,16,17,18,19,20])"},
{"object_property_loop",
"(function(obj,n){let s=0;for(let i=0;i<n;i++)s+=obj.x;return s})({x:3},100)"}
]

average_us = fn fun ->
started = System.monotonic_time()
Enum.each(1..iterations, fn _iteration -> fun.() end)
elapsed = System.monotonic_time() - started
System.convert_time_unit(elapsed, :native, :microsecond) / iterations
end

{:ok, compiler} = Compiler.start_link(capacity: 32)
{:ok, initialization_program} = QuickBEAM.VM.compile("0")

{runtime_init_us, {:ok, 0}} =
:timer.tc(fn ->
Engine.eval(initialization_program, isolation: :caller, max_steps: 1_000_000)
end)

IO.puts("COMPILER_PERF runtime_initialization_us=#{runtime_init_us} profile=core")

try do
Enum.each(workloads, fn {name, source} ->
{:ok, program} = QuickBEAM.VM.compile(source)
interpreter_opts = [isolation: :caller, max_steps: 1_000_000]

compiler_opts = [
engine: :compiler,
compiler_profile: :scalar_v1,
isolation: :caller,
max_steps: 1_000_000
]

{cold_us, compiled_result} = :timer.tc(fn -> Engine.eval(program, compiler_opts) end)
interpreted_result = Engine.eval(program, interpreter_opts)

{:ok, _raw_value, raw_execution} =
Compiler.start(program, compiler_profile: :scalar_v1, max_steps: 1_000_000)

compiled_functions =
Enum.count(raw_execution.compiler_context.decisions, fn {_id, decision} ->
match?({:compile, _, _}, decision) or match?({:cached, _}, decision)
end)

if compiled_result != interpreted_result do
raise "#{name} mismatch: compiler=#{inspect(compiled_result)} interpreter=#{inspect(interpreted_result)}"
end

compiler_us =
average_us.(fn -> ^compiled_result = Engine.eval(program, compiler_opts) end)

interpreter_us =
average_us.(fn -> ^interpreted_result = Engine.eval(program, interpreter_opts) end)

speedup = interpreter_us / compiler_us

IO.puts(
"COMPILER_PERF workload=#{name} compiled_functions=#{compiled_functions} " <>
"cold_us=#{cold_us} " <>
"compiler_us=#{Float.round(compiler_us, 2)} " <>
"interpreter_us=#{Float.round(interpreter_us, 2)} speedup=#{Float.round(speedup, 3)}"
)
end)
after
GenServer.stop(compiler)
end
94 changes: 94 additions & 0 deletions bench/vm_interpreter_perf.exs
Original file line number Diff line number Diff line change
@@ -0,0 +1,94 @@
defmodule QuickBEAM.Bench.VMInterpreterPerf do
@moduledoc "Workload driver for non-instrumented BeamAsm/perf sampling."

def run(args) do
{opts, positional, invalid} =
OptionParser.parse(args,
strict: [mode: :string, iterations: :integer, warmup: :integer]
)

if positional != [] or invalid != [],
do: raise(ArgumentError, "invalid arguments: #{inspect(positional ++ invalid)}")

mode = mode!(Keyword.get(opts, :mode, "pinned"))
iterations = positive!(Keyword.get(opts, :iterations, 500), :iterations)
warmup = non_negative!(Keyword.get(opts, :warmup, 10), :warmup)
{:ok, source} = bundle()
{:ok, decoded_program} = QuickBEAM.VM.compile(source, filename: fixture())

program =
if mode == :pinned do
{:ok, pinned_program} = QuickBEAM.VM.pin(decoded_program)
pinned_program
else
decoded_program
end

options = [
profile: :ssr,
handlers: %{"load_props" => fn [] -> props() end},
max_steps: 50_000_000,
memory_limit: 256_000_000,
timeout: 5_000
]

run = operation(mode, program, options)
Enum.each(1..warmup//1, fn _iteration -> successful!(run.()) end)

started = System.monotonic_time()
Enum.each(1..iterations, fn _iteration -> successful!(run.()) end)
elapsed = System.monotonic_time() - started
elapsed_ms = System.convert_time_unit(elapsed, :native, :millisecond)

IO.puts("mode=#{mode} iterations=#{iterations} elapsed_ms=#{elapsed_ms}")

if mode == :pinned, do: QuickBEAM.VM.unpin(program)
end

defp operation(:caller, program, options),
do: fn -> QuickBEAM.VM.eval(program, [isolation: :caller] ++ options) end

defp operation(_isolated, program, options),
do: fn -> QuickBEAM.VM.eval(program, options) end

defp successful!({:ok, _result}), do: :ok
defp successful!(result), do: raise("unexpected evaluation result: #{inspect(result)}")

defp fixture, do: "test/fixtures/vm/vue_ssr.js"

defp bundle do
QuickBEAM.JS.bundle_file(fixture(),
format: :esm,
minify: true,
define: %{
"__VUE_OPTIONS_API__" => "true",
"__VUE_PROD_DEVTOOLS__" => "false",
"__VUE_PROD_HYDRATION_MISMATCH_DETAILS__" => "false",
"process.env.NODE_ENV" => ~s("production")
}
)
end

defp props do
%{
"title" => "Profile",
"products" => [
%{"id" => 1, "name" => "Product 1", "inStock" => true, "priceCents" => 1299}
]
}
end

defp mode!("caller"), do: :caller
defp mode!("copied"), do: :copied
defp mode!("pinned"), do: :pinned
defp mode!(mode), do: raise(ArgumentError, "invalid mode: #{inspect(mode)}")

defp positive!(value, _name) when is_integer(value) and value > 0, do: value
defp positive!(value, name), do: raise(ArgumentError, "invalid #{name}: #{inspect(value)}")
defp non_negative!(value, _name) when is_integer(value) and value >= 0, do: value

defp non_negative!(value, name),
do: raise(ArgumentError, "invalid #{name}: #{inspect(value)}")
end

QuickBEAM.Bench.VMInterpreterPerf.run(System.argv())
Loading
Loading