Magnesium

Fast without a JIT.

An embeddable scripting language written from scratch in C11. Register bytecode, a generational garbage collector, and a verifier that makes untrusted bytecode safe to load.

  • v1.2.0
  • MIT license
  • C11
  • Bindings for Rust, C++ and C#
  • Needs only libc, libm, libdl and pthreads
// hello.mg
let name = input("Name: ")
print(_"Hello, {name}!")

$ ./magnesium hello.mg

Name: Ada

Hello, Ada!

$

The example from the README, run in a terminal.

Under the hood

One function, five stages.

Follow three lines of Magnesium from source text to a running register machine.

fn total(price, qty, tax)
    return price * qty + tax
end

A table-free scanner turns text into tokens, matching keywords with a hand-coded trie.

  • fn
  • total
  • (
  • price
  • ,
  • qty
  • ,
  • tax
  • )
  • return
  • price
  • *
  • qty
  • +
  • tax
  • end

keywordnameoperatorpunctuation

A recursive-descent parser with Pratt-style precedence climbing builds the tree, so * binds tighter than +.

fn total(3) return + * tax price qty

The compiler allocates registers and emits register bytecode. The parameters land in R1, R2 and R3.

  1. 0000MULA=4 B=1 C=2R4 = price * qty
  2. 0001ADDA=5 B=4 C=3R5 = R4 + tax
  3. 0002RETURNA=5 B=1return R5
  4. 0003LOADNILA=6implicit
  5. 0004RETURNA=6 B=1return null

The actual output of Magnesium’s disassembler for this function. The last two instructions are the return null every function ends with.

magnesium build saves the bytecode as a .mgc file. Loading one runs the verifier over every instruction before any of it executes:

  • Every opcode is one of the 109
  • Every register is inside the function’s frame
  • Every constant index exists, and holds a name where one is expected
  • Every jump lands on an instruction of the same function
  • Register counts and arity stay within limits

verified ready to run

Running source directly skips the file: the compiler hands its bytecode straight to the VM.

A computed-goto loop dispatches each instruction. Calling total(4, 3, 1):

  1. MULR4 = R1 * R2
  2. ADDR5 = R4 + R3
  3. RETURNR5
R1
4
R2
3
R3
1
R4
12
R5
13

→ 13

Call it from top-level code and total is never called at all: the compiler inlines it, so the caller runs the MUL and ADD itself, with no call and no new frame.

Benchmarks

16 races. 16 wins.

The same 16 programs in Magnesium, Lua 5.4 and CPython. Magnesium is the fastest on every one, from source and from bytecode.

vs Lua 5.4
1.6×
vs CPython 3
3.7×
  1. fib35recursive Fibonacci

  2. binary_treesdepth-18 GC pressure

  3. sieve1M-element array

  4. mandelbrot400 × 400

  5. dict_bench250k insert and lookup

  6. arith_looparithmetic loop

  7. call_loop10M function calls

  8. closure_loop5M upvalue mutations

  9. array_looparray loop

  10. object_fields1M struct-field dispatches

  11. gc_alloc50k × 50 arrays

  12. string_concatstring building

  13. control_flowbranches and loops

  14. fallible_lookup3M ? operator lookups

  15. native_len5M native calls

  16. coroutine_switch200k resume and yield

Every run: averages, medians and all five timings, from source and from bytecode
From source, in seconds
BenchmarkRuntimeAverageMedianvs MgRuns
fib35Magnesium0.3900.3961.00×0.376 0.384 0.396 0.399 0.396
Lua 5.40.6250.6171.56×0.617 0.638 0.656 0.614 0.600
CPython 30.9981.0012.53×1.003 1.005 1.001 0.991 0.990
binary_treesMagnesium0.0810.0791.00×0.089 0.079 0.079 0.081 0.079
Lua 5.40.1990.1932.43×0.187 0.193 0.234 0.193 0.189
CPython 30.5270.5146.49×0.585 0.563 0.487 0.486 0.514
sieveMagnesium0.0920.0941.00×0.084 0.091 0.095 0.094 0.096
Lua 5.40.1120.1111.19×0.119 0.111 0.104 0.122 0.106
CPython 30.1960.1922.05×0.202 0.192 0.187 0.213 0.188
mandelbrotMagnesium0.0810.0821.00×0.082 0.083 0.082 0.079 0.080
Lua 5.40.0890.0881.08×0.087 0.090 0.088 0.088 0.091
CPython 30.3800.3414.18×0.363 0.326 0.341 0.333 0.537
dict_benchMagnesium0.1210.1201.00×0.108 0.120 0.130 0.118 0.130
Lua 5.40.2070.2051.71×0.190 0.228 0.216 0.205 0.193
CPython 30.1840.1811.51×0.181 0.175 0.203 0.190 0.170
arith_loopMagnesium0.0890.0891.00×0.087 0.088 0.092 0.089 0.089
Lua 5.40.1070.1071.20×0.106 0.109 0.107 0.107 0.104
CPython 30.6790.6707.56×0.711 0.666 0.670 0.684 0.665
call_loopMagnesium0.1490.1501.00×0.147 0.152 0.150 0.146 0.151
Lua 5.40.2370.2331.55×0.251 0.234 0.232 0.233 0.233
CPython 31.1471.1397.57×1.194 1.131 1.139 1.139 1.130
closure_loopMagnesium0.1080.1071.00×0.111 0.106 0.110 0.105 0.107
Lua 5.40.1280.1281.20×0.128 0.129 0.128 0.125 0.129
CPython 30.4640.4594.28×0.484 0.468 0.459 0.456 0.452
array_loopMagnesium0.0750.0751.00×0.075 0.076 0.075 0.076 0.074
Lua 5.40.1080.1071.42×0.106 0.108 0.107 0.110 0.107
CPython 30.4560.4536.00×0.480 0.453 0.463 0.440 0.444
object_fieldsMagnesium0.0620.0631.00×0.059 0.064 0.063 0.064 0.059
Lua 5.40.0730.0721.14×0.072 0.071 0.072 0.074 0.075
CPython 30.1540.1512.39×0.151 0.151 0.151 0.154 0.161
gc_allocMagnesium0.0650.0661.00×0.066 0.067 0.061 0.066 0.066
Lua 5.40.0920.0921.39×0.092 0.091 0.095 0.092 0.090
CPython 30.2110.2113.19×0.207 0.205 0.216 0.215 0.211
string_concatMagnesium0.0370.0371.00×0.039 0.036 0.041 0.034 0.037
Lua 5.40.1010.0992.68×0.097 0.107 0.105 0.099 0.098
CPython 30.0770.0762.08×0.081 0.074 0.076 0.075 0.078
control_flowMagnesium0.0580.0581.00×0.060 0.058 0.063 0.055 0.055
Lua 5.40.0720.0711.22×0.071 0.066 0.068 0.073 0.080
CPython 30.3210.3215.55×0.321 0.323 0.322 0.319 0.320
fallible_lookupMagnesium0.0510.0511.00×0.052 0.049 0.053 0.051 0.051
Lua 5.40.0620.0611.19×0.060 0.062 0.061 0.059 0.067
CPython 30.2430.2444.75×0.248 0.244 0.236 0.243 0.244
native_lenMagnesium0.0730.0741.00×0.074 0.072 0.071 0.075 0.074
Lua 5.40.1390.1361.84×0.137 0.136 0.151 0.136 0.134
CPython 30.4320.4295.79×0.445 0.428 0.433 0.429 0.425
coroutine_switchMagnesium0.0520.0521.00×0.052 0.052 0.052 0.052 0.053
Lua 5.40.2530.2544.88×0.255 0.256 0.253 0.249 0.254
CPython 30.0890.0891.70×0.089 0.092 0.089 0.087 0.089
From bytecode, in seconds
BenchmarkRuntimeAverageMedianvs MgRuns
fib35Magnesium0.3710.3721.00×0.373 0.367 0.370 0.372 0.372
Lua 5.40.6260.6151.65×0.662 0.613 0.615 0.597 0.641
CPython 31.0521.0582.85×1.020 1.044 1.058 1.061 1.079
binary_treesMagnesium0.0730.0721.00×0.075 0.072 0.071 0.074 0.070
Lua 5.40.1870.1822.51×0.172 0.179 0.182 0.214 0.187
CPython 30.5400.5447.52×0.557 0.570 0.534 0.496 0.544
sieveMagnesium0.0870.0871.00×0.087 0.089 0.085 0.087 0.086
Lua 5.40.0920.0921.05×0.088 0.096 0.090 0.092 0.094
CPython 30.2100.2072.37×0.192 0.215 0.204 0.207 0.231
mandelbrotMagnesium0.0820.0801.00×0.080 0.079 0.080 0.083 0.086
Lua 5.40.0930.0921.14×0.096 0.089 0.092 0.090 0.096
CPython 30.3690.3714.63×0.348 0.371 0.345 0.394 0.385
dict_benchMagnesium0.1190.1171.00×0.109 0.123 0.117 0.116 0.129
Lua 5.40.2120.2001.71×0.200 0.219 0.191 0.196 0.251
CPython 30.1800.1801.54×0.180 0.179 0.183 0.176 0.182
arith_loopMagnesium0.0860.0861.00×0.087 0.086 0.085 0.086 0.086
Lua 5.40.1070.1061.24×0.110 0.105 0.106 0.106 0.106
CPython 30.6810.6837.96×0.682 0.685 0.685 0.683 0.668
call_loopMagnesium0.1520.1541.00×0.154 0.150 0.154 0.154 0.147
Lua 5.40.2340.2341.52×0.231 0.235 0.235 0.234 0.233
CPython 31.1531.1507.48×1.143 1.155 1.144 1.172 1.150
closure_loopMagnesium0.1050.1051.00×0.105 0.105 0.105 0.106 0.106
Lua 5.40.1290.1281.21×0.135 0.128 0.128 0.125 0.127
CPython 30.4700.4714.48×0.470 0.472 0.474 0.462 0.471
array_loopMagnesium0.0740.0741.00×0.075 0.072 0.074 0.074 0.077
Lua 5.40.1070.1071.44×0.107 0.108 0.105 0.107 0.108
CPython 30.4530.4516.06×0.465 0.451 0.432 0.471 0.447
object_fieldsMagnesium0.0650.0651.00×0.063 0.067 0.062 0.065 0.066
Lua 5.40.0730.0721.11×0.072 0.071 0.070 0.077 0.072
CPython 30.1520.1522.34×0.149 0.158 0.152 0.152 0.151
gc_allocMagnesium0.0660.0671.00×0.067 0.065 0.069 0.061 0.068
Lua 5.40.0920.0911.36×0.090 0.091 0.096 0.090 0.094
CPython 30.2130.2163.23×0.218 0.207 0.216 0.200 0.226
string_concatMagnesium0.0390.0391.00×0.039 0.037 0.038 0.040 0.039
Lua 5.40.1170.1132.89×0.111 0.111 0.138 0.114 0.113
CPython 30.0770.0771.98×0.076 0.076 0.079 0.078 0.077
control_flowMagnesium0.0600.0611.00×0.060 0.056 0.062 0.062 0.061
Lua 5.40.0700.0711.16×0.070 0.071 0.068 0.071 0.071
CPython 30.3270.3275.35×0.333 0.329 0.325 0.324 0.327
fallible_lookupMagnesium0.0480.0471.00×0.047 0.048 0.047 0.047 0.049
Lua 5.40.0600.0591.25×0.059 0.058 0.060 0.061 0.059
CPython 30.2560.2575.41×0.266 0.257 0.247 0.269 0.244
native_lenMagnesium0.0710.0721.00×0.067 0.072 0.074 0.073 0.070
Lua 5.40.1380.1371.91×0.137 0.145 0.138 0.137 0.134
CPython 30.4350.4346.05×0.434 0.445 0.447 0.420 0.430
coroutine_switchMagnesium0.0500.0511.00×0.053 0.051 0.046 0.051 0.050
Lua 5.40.2510.2524.98×0.252 0.254 0.252 0.248 0.251
CPython 30.0890.0871.72×0.087 0.094 0.086 0.091 0.085

The release gate

Every release runs all 16 benchmarks from source and from bytecode, checks that the three languages print the same output, then times five runs after a warmup. If Magnesium’s median is not strictly faster than both Lua 5.4 and CPython on every benchmark, the build fails. No benchmark-only opcodes, no special cases.

Medians of five runs, in seconds, lower is better. Magnesium v1.2.0 built with Clang -O3, Lua 5.4.6 and CPython 3.12.10, on an Intel Core i7-11700KF running Windows 11, October 2026. Averages are geometric means. Run it yourself with make bench.

Bytecode verifier

Damage one byte. Watch it refuse.

Compiled .mgc files are checked before a single instruction runs, so a host can load bytecode it didn’t compile. The test suite damages valid files one field at a time, and the loader must reject every variant.

  • vm_set_deadlineA time limit for each script
  • vm_request_cancelStop it from any thread
  • vm_set_memory_limitA memory cap the collector enforces
  • vm_set_sandboxNo files, environment, spawning or stray imports

$ ./magnesium game.mgc

13

Real bytes from game.mgc, built with magnesium build from the function above plus print(total(4, 3, 1)). Each damage is one of the malformed-bytecode tests, and the message is the loader’s own.

Implementation

Every part, written from scratch.

No parser generator, no JSON library, no runtime dependencies. Just C11.

  • 109opcodes
  • 4inline caches
  • 64bits per value, NaN-boxed
  • 14size classes in the slab allocator
  • vm.c

    VM

    Computed-goto dispatch, 109 opcodes, four inline caches, the standard library, native and FFI calls, threads and coroutines.

  • compiler.c

    Compiler

    AST to register bytecode: register allocation, peephole fusion, a leaf-function inliner and control-flow lowering.

  • gc.c

    Garbage collector

    Generational tri-colour mark-and-sweep with write barriers and a per-VM memory limit.

  • parser.c

    Parser

    Recursive descent with Pratt precedence and panic-mode recovery.

  • typecheck.c

    Type checker

    Opt-in. Aliases, structural shapes, forward references.

  • serialize.c

    Serializer

    The .mgc format and its verifier.

  • lsp.c

    Language server

    LSP over stdio, framing written by hand.

  • object.c

    Objects

    Strings with ropes, arrays, dicts, functions.

  • lexer.c

    Lexer

    Table-free scanner, keyword trie.

  • mt.c

    mt

    Installs the runtime, bindings and editor extension.

Embedding

Drop it into your engine.

A small C API, with safe bindings on top. Scripts declare what the host provides, so the checker and the language server know about it too.

  • RustA safe crate over magnesium-sys; vendored builds by default
  • C++17Header-only, built with CMake
  • C#.NET 8 through P/Invoke
  • CThe low-level boundary under all three
!strict

// Host-provided symbols are declared, not defined.
extern fn read_score(player: string): number
extern const MAX_SCORE: number

// Multi-return errors, with ? propagation.
fn get_score(scores, name)
    return scores<name>?
end

let scores = &< Ada = 10 >
let value, err = get_score(scores, "Grace")
if err then print(err) end
use magnesium::Vm;

let mut vm = Vm::new();
vm.try_set_global_number("MAX_SCORE", 100.0)?;
vm.try_interpret_named(r#"
!strict
extern const MAX_SCORE: number
print(MAX_SCORE)
"#, "game.mg")?;
typedef Value (*NativeFn)(VM *vm, int arg_count, Value *args);

void vm_register_native(
    VM *vm, const char *name, NativeFn function, int arity,
    void *userdata, void (*userdata_finalizer)(void *)
);
fn best(scores)
    let top = 0
    for name, score in scores
        if score > top then top = scroe end
    end
    return top
end
Illustration of the VS Code extension. Undeclared names are compile errors, so they show up as you type.

Tooling

A language server in the binary.

magnesium --lsp starts an LSP server over stdio, with no JSON library. The VS Code extension adds a grammar, auto-indent and the Magnesium Dark theme.

  • completion
  • hover
  • signatureHelp
  • definition
  • rename
  • formatting
  • documentSymbol
  • semanticTokens
  • diagnostics

The language server and extension are still a work in progress and not yet at parity with the core language.

Build it in a minute.

You need git, make and GCC or Clang. On Windows, use LLVM-MinGW.

terminal
git clone https://github.com/EmeraldStudiosG/magnesium-showcase.git
cd magnesium-showcase
make
./magnesium --version
mt, the toolchain manager
mt install          # runtime, library, headers, VS Code extension
mt install rust     # Rust bindings
mt install cpp      # C++17 header-only bindings
mt install csharp   # .NET 8 bindings
Flow, the Magnesium mascot: a navy bird in sunglasses, grinning

Meet Flow.

MIT licensed.