Skip to content

S0: private Luau interpreter feasibility

Status: first S0 implementation, based on Ludus d13b4f8. Production adoption remains gated. This experiment implements no public scripting SDK, behavior provider, language-neutral manifest, graph editor, C# runtime, or live reload. See the architecture for those subsequent contracts.

Scope and reproducibility

LUDUS_BUILD_LUAU_PROBE defaults to OFF. Ordinary configure, build, install and SDK consumers neither download nor require Luau. Enabling the option explicitly requires prepared sources and cooked fixtures; missing, edited or stale inputs fail configuration and the probe's build dependency. Luau targets have no Ludus install/export rules.

The toolchain record pins compiler and VM to 1eca9fda3e4753a1592000f6cfdf659aaa778b7d, archive SHA-256 5b93ecef638a485065f042be1f48436e2d835deb9cdea7ccdccbfaa467e74a48. The experimental profile is s0-interpreter-longjmp-loader-lifetime-v2. The compiler uses optimization level 1 and debug level 2. Fixture --!strict annotations document intent; this spike does not yet implement generated API definitions or a type-checking acceptance gate.

./init.sh --cli linux-clang-development --preset-only --locked --with-tests --no-system-install
./scripts/luau-probe bootstrap
./scripts/luau-probe cook
./scripts/luau-probe run --preset linux-clang-development
./scripts/luau-probe check --preset linux-clang-development

./init.sh --cli linux-clang-asan-ubsan --preset-only --locked --with-tests --no-system-install
./scripts/luau-probe run --preset linux-clang-asan-ubsan
python3 -m unittest discover -s tools/luau-probe -p 'test_*.py' -v

Bootstrap is the explicit network action. It verifies the archive, reconstructs the pinned source plus the checked loader, assertion-helper and analyzer-include edits, and builds upstream luau-compile in a separate host build. Upstream compiler/CLI exception handling stays in that process; no Ludus code is compiled there. The engine probe links VM and Common only, both using the Ludus no-exception and sanitizer policy. No compiler, checker, require implementation or native code generator is linked into it.

Cook generates private bytecode arrays and a manifest under out/luau-probe. Source, pin and generated header fingerprints invalidate stale inputs. Verify compares every extracted upstream file with the verified archive plus the exact profile transformations. This detects accidental edits; these local fingerprints are not an authenticity boundary against a developer controlling the checkout. Only trusted bytecode from the paired compiler enters the VM.

The host analyzer's pinned Analysis/src/TableLiteralInference.cpp uses integer std::abs without including its declaring header. Bootstrap adds <cstdlib> at one checked include anchor so the analyzer builds with libc++ 23. Thanks to Roblox/Luau's source at the pinned revision. The source body and upstream license are preserved. Changed anchors, an already patched upstream file, and extracted bytes differing from the checked transform are rejected. This host-tool include correction keeps the v2 VM profile.

For Web, prepare the SDK pinned in config/web_toolchain.json using the existing browser bootstrap workflow, then run:

./scripts/luau-probe run --preset web-emscripten-development
./scripts/luau-probe run --preset web-emscripten-release
# After npm ci and Playwright Chromium installation in tools/web-browser-tests:
node tools/luau-probe/browser.mjs out/build/web-emscripten-release/tools/luau-probe

The run command disables unrelated Web smoke/probe targets in that build cache. Reconfigure those targets with their ordinary scripts when returning to them. It enables Info logging in Release/Profile probe configurations so verification can observe each case; this is a diagnostic probe configuration, not the engine's ordinary Release logging profile. The wasm profile uses SUPPORT_LONGJMP=wasm; its implementation requires wasm exception-handling support, while every VM/host C++ translation unit still uses -fno-exceptions. The VM does not use Luau's stock exception-enabled Web demo. The SDK's Node execution and actual Chromium execution are separate checks. Windows, macOS, Android and iOS have no acceptance evidence from this slice.

Lifetime audit and loader profile

Thanks to Roblox Corporation and Lua.org/PUC-Rio for Luau's MIT-licensed VM, particularly ldo.cpp, lapi.cpp, lstate.cpp, lvmload.cpp, and the debugger examples in tests/Conformance.test.cpp at that revision. The host probe adapts the documented protection/debugger contracts; it does not copy an engine-object binding implementation. Both inherited MIT notices are preserved in acquired sources and copied beside probe artifacts.

The C++ nonlocal-jump rule forbids crossing automatic objects whose nontrivial destruction would be required by equivalent exception unwinding. The audit is therefore static as well as dynamic: sanitizers and an allocation counter cannot alone prove this language rule.

Region Ownership and recovery decision
luaD_rawrunprotected Explicit longjmp mode uses a jump buffer and volatile status; never compile the exception branch.
lua_newstate Upstream protects its allocating initialization and closes partially created state. Sweep rejection during this path.
lua_cpcall Protects stack growth and C-closure creation before entering the host callback; use it for setup and ordinary execution.
Initialize, Execute, HostAdd, Interrupt Only scalars, pointers and trivially destructible contexts. No locks, containers, scope guards, logging, native resource owners or reentrant engine calls.
luau_load outer scope ScopedSetGCThreshold and the original string/prototype TempBuffer owners sit outside its internal protected call, so normal return destroys them. Pre-load GC is covered by the outer host protection.
Constant-table deserialization Original TempBuffer<int32_t> nilKeys sits inside loadsafe, below the protected boundary. An allocation failure can exit its lifetime by longjmp. Move it into LoadContext, pass a reference into loadsafe, and release/reset it before another table. Its destructor then runs after protected recovery.
Debugger callbacks and resumes POD-only callbacks, a registry-rooted coroutine, bounded local inspection, and resume status handling. No callback-owned native resources.
Allocator and destruction Fallible C realloc/free, explicit byte accounting and an 8 MiB cap. Rejected realloc preserves the original block. No Lua calls in the allocator; no custom userdata finalizers in this profile. Always close a failed VM.
Common and VM helpers Reviewed VM nontrivial owner declarations and STL usage; the loader owners above are the relevant crossed-frame finding. Common time-trace RAII is disabled. Future libraries, userdata callbacks and native execution require a new audit.

scripts/luau-probe applies seven exact, checked substitutions to the loader; an upstream pin change fails closed rather than applying an approximate patch. It preserves the upstream notices and constructor/destructor ownership model. This is a source-audit finding, not a claim that ASan diagnosed longjmp UB or that every unmodified allocation fail point necessarily leaks. The unmodified pin is not approved for production. Before S1 adoption, decide whether to obtain an upstream fix, change the reviewed pin, or explicitly maintain this small profile.

Sanitizer function-prefix profile

Q0 fatal UBSan exposed a Clang 18 Mach-O prefix/coalescing problem at VM close. The callback in lgc.cpp and indirect call in lmem.cpp have matching signatures and object-file type hashes. The preceding shared noinline assertCallHandler is emitted as a coalesced inline function; its atom also contains the following local callback's sanitizer prefix. Selecting another translation unit's helper can therefore give the callback a different linked prefix and a false function-type-mismatch finding. Both Apple ld and LLVM 18 ld64.lld reproduced it.

Thanks to Roblox/Luau's Common.h at the pinned revision for the existing assertion contract. The v2 profile gives only assertCallHandler internal linkage with static inline; its body, noinline attribute, shared handler storage, licenses and diagnostics are preserved. scripts/luau-probe applies one exact checked substitution and source verification includes the transformed header. The compiler/VM revision and archive checksum remain pinned; the profile fingerprint invalidates old cooked evidence.

A Clang 18 UBSan-only VM create/close reproducer aborted before this edit and exited zero afterward. Required macOS Behavior and safety acceptance exercises VM close with ASan/UBSan; no sanitizer check or recovery policy is disabled.

Executed contracts

The harness runs the same cooked fixtures in native and wasm interpreters:

  • Checked scalar binding, result 42, and absent os/io/debug/require/print access.
  • Protected script error and invalid binding argument; neither escapes the VM.
  • Recursive script stack overflow and an infinite loop stopped at 64 interpreter safepoints. This is a cooperative safepoint count, not an instruction count or a hard wall-clock limit; a long native call is not preempted.
  • Allocation rejection starting at every allocator call in a successful baseline, spanning state creation, library setup, bytecode loading and table allocation. Each rejection must report failure and leave zero bytes after close.
  • Two constant-table templates exercising the loader scratch-owner path.
  • A source breakpoint at line 3, local value 20, static generated-node mapping to node 1001, single-step to line 4/value 42, and successful continuation to 42. This proves debugger primitives, not a DAP server or general graph source map.
  • A 10,000-call scalar dispatch batch and a successful fresh VM after failures.

The error policy for this first experiment destroys each failed VM. Reusing a failed VM, preserving game state, staging commands, generation-checked entity handles and provider fault isolation belong to subsequent integration slices. No hard sandbox claim follows from exposing only a subset of base functions. In particular, production cancellation must address script-protected calls and native callback costs, and numeric bindings need their declared finite/range rules.

Every semantics run writes evidence.json beside the executable, including pin, host, output, executable/wasm byte counts and hashes. Output includes allocator fail-point counts, heap peaks and dispatch-batch time. Native file size includes debug symbols and FoundationLogging; Web file size includes its runtime glue and Foundation dependencies. Neither is an incremental production VM-size result. Batch time includes VM creation, setup, loading and teardown; it is not a pure call benchmark or evidence that a game meets the proposed 0.83 ms tick budget.

CI adds these checks to native Development, ASan/UBSan and the existing browser workflow. Browser artifacts retain licenses and evidence; the Chromium job executes the exact Release probe output. Tooling tests cover corrupt archives, path traversal/symlinks, source edits and stale fixture/pin invalidation.

Remaining S0 acceptance work

The first implementation makes the feasibility question reproducible. S0 remains in progress until the pin/profile decision is accepted, useful workload and incremental-size budgets are measured on reference hardware, target device builds are recorded, and the production library/callback surface receives its own audit. S1 then introduces the smallest real Luau/native-equivalent interaction using the shared gameplay manifest. C# and a visual editor remain separate deliveries.

The scalar argument checks also adopt the boundary-validation idea from Waldemar Celes, Luiz Henrique de Figueiredo and Roberto Ierusalimschy, “Binding C/C++ Objects to Lua”, Game Programming Gems 6, §4.2, pp. 341–355, as recorded in the source review. Unlike that chapter's ownership examples, this probe exposes no raw engine-object pointer or GC-owned entity.