luna
A Lua runtime in pure Rust. Five dialects — 5.1 / 5.2 / 5.3 / 5.4 / 5.5 — plus MacroLua in a single binary, with a zero-dependency interpreter core, a Cranelift-backed trace JIT, ahead-of-time native compilation, and a sandbox built for embedding untrusted scripts.
luna.golia.jp · docs · crates.io · docs.rs
use ;
let mut vm = new;
let v = vm.eval?; // [Int(42)]
Status
v3.1.0 shipped 2026-09-24: a parity release with no breaking change.
luna was compared against stock PUC 5.1–5.5 function by function, every
difference found was fixed or is listed as deliberate in
docs/compatibility.md, and the differential
corpus grew to 805 fixtures. v3.0.0 (2026-08-14) closed the v2.x
maturity arc. See CHANGELOG.md.
Install
Seven crates are published; three of them are ones you depend on directly.
# Most embedders — interpreter + Cranelift JIT + the C ABI.
[]
= "3"
# Minimum surface — pure interpreter, zero third-party deps, wasm-friendly.
[]
= "3"
cargo tree -p luna-core prints exactly one crate: itself. A CI gate
enforces that on every commit, so the audit surface stays small and the
wasm32 target needs no RWX mapping. luna-jit adds Cranelift.
For a standalone native binary from a Lua source file, luna-aot is a
build-time tool — not a runtime dependency of what it produces. See
docs/aot.md.
The CLI:
Dialects
One build hosts every mainline dialect, selected per-Vm at
construction; a single process can run several concurrently without
interference. Feature availability is driven by capability predicates in
crates/luna-core/src/version.rs, and luna emits per-dialect bytecode
in PUC's format, so PUC-compiled .luac files load directly.
MacroLua is a sixth dialect: the 5.4 surface plus compile-time
@macro(...) expansion. It sits between Lua54 and Lua55 in the
version enum so it inherits every 5.4-and-earlier capability predicate.
Full matrix: docs/compatibility.md.
Embedding
use Lua;
use LuaVersion;
let mut lua = sandbox
.open_base
.open_math
.open_string
.with_instr_budget
.with_memory_cap
.build;
let add = lua.create_function;
lua.set_global?;
let result: i64 = lua.eval?;
assert_eq!;
The host owns the security boundary: curated stdlib whitelisting, an instruction budget, an approximate memory cap, and bytecode loading off by default. Every capability a script can see was opted into from Rust.
No API an embedder touches requires an unsafe block.
Cookbook: docs/embedding.md. Threat model and
what is explicitly not contained:
docs/security.md.
Looking for embedders. luna wants production users and the feedback that comes with them. See
docs/embedder-recruitment.mdfor what it offers, what it does not, and how to try it.
Correctness
Compatibility here is a measurement, not a claim.
- 805 differential fixtures run against stock PUC interpreters built
from source — 5.1.5, 5.2.4, 5.3.6, 5.4.9, 5.5.1 — and must match
byte for byte on stdout, stderr and exit code, with zero skips, before
a commit is green; each also runs as PUC bytecode compiled by that
version's
luac. CI asserts the 5.4 and 5.5 references are exactly 5.4.9 and 5.5.1, so the basis cannot drift with a runner image. - PUC's own test suite runs end-to-end across all five dialects with matching assert-count instrumentation.
- AddressSanitizer over that suite nightly; Miri for provenance and UB; cross-allocator runs on glibc, jemalloc, mimalloc and Apple malloc.
- Seven fuzz targets weekly, behind a gate that fails the run if any target leaves a crash artifact.
- Soak runs bounded on second-half RSS drift under 1%.
- The cross-platform matrix — ubuntu / macos / windows / ubuntu-arm × stable, plus wasm32 — runs per push.
One lesson from the arc is worth repeating: a green check is not evidence that something ran. v2.17 found three gates that had never executed, and v2.20 two CI jobs that had never completed, all of them reporting success throughout.
Performance
luna deliberately does not publish a headline ratio. A single "N× faster
than X" collapses two independent axes — luna_jit vs LuaJIT_jit and
luna_interp vs LuaJIT_interp — and the number moves with the corpus it
was measured on. docs/performance.md gives the
methodology, what is measured today, and the per-release perf gate,
which compares HEAD against a pinned reference commit on the same
runner rather than against fixed nanosecond baselines that drift with
CI hardware.
Threading
Vm is !Send + !Sync — pin one per OS thread, or per single-threaded
Tokio worker. For async hosts use the current_thread flavour or a
LocalSet. docs/threading.md has the three
canonical patterns and the reasoning behind the constraint.
Standalone CLI
Options, the arg table, error reports and exit status follow the
selected dialect's lua.c; see
docs/compatibility.md.
Linking from C
luna-jit builds a cdylib / staticlib exposing a lua.h-compatible
subset (crates/luna-jit/src/capi.rs), for C and C++ hosts that want a
drop-in replacement for PUC.
Build
Architecture
crates/
├── luna-core/ # 0 third-party deps — lexer, parser, per-dialect
│ # compiler, dispatcher, stdlib, NaN-boxed values,
│ # intrusive mark-sweep GC, PUC pattern engine,
│ # and the JIT trait surface
├── luna-jit/ # Cranelift backend, lua.h-compatible C ABI,
│ # the `luna` CLI, the Lua embedding facade
├── luna-jit-derive/ # #[derive(LuaUserdata)] — kept separate so
│ # luna-core stays zero-dependency
├── luna-jit-helpers/ # shared extern-C helpers and JIT TLS discipline,
│ # single-sourced across both backends
├── luna-jit-llvm/ # alternative LLVM 18 backend (LUNA_JIT_BACKEND=llvm)
├── luna-runtime-helpers/ # staticlib linked into AOT-produced binaries
└── luna-aot/ # build-time: Lua source → standalone native binary
The JIT plugs in behind a trait, so the interpreter path never depends
on a backend and removing one does not touch the core API.
docs/architecture.md has the full breakdown.
Documentation
docs/embedding.md |
the cookbook |
docs/compatibility.md |
per-dialect feature matrix |
docs/architecture.md |
crate boundaries, JIT pipeline |
docs/security.md |
sandbox boundaries and threat model |
docs/threading.md |
async and multi-thread patterns |
docs/aot.md |
ahead-of-time compilation |
docs/deploy.md |
shipping something that embeds luna |
docs/performance.md |
methodology and measurements |
docs/binary-size.md |
what the binary costs |
docs/unsafe-accounting.md |
every unsafe site, justified |
docs/migration-v1-to-v2.md |
v1.x → v2.x migration |
docs/release-checklist.md |
how a release is cut |
CHANGELOG.md |
release notes |
Rendered API reference: docs.rs/luna-jit and
docs.rs/luna-core, or cargo doc --open.
Every public item is documented — deny(missing_docs) enforces it.
License
Dual MIT / Apache-2.0 (see LICENSE-MIT and
LICENSE-APACHE).