l3i owns its Luau build. Luau is the luau/ git submodule, pinned at release 0.742;
build.rs compiles it and the binder’s C++ additions with cc, in parallel, from
the checkout alone. There is no network access at build time and no external Lua crate. Rust 1.92
or newer, edition 2024.
git clone --recurse-submodules https://github.com/DreamWeave-MP/l3i
# or, in an existing checkout
git submodule update --init
cargo build
build.rs fails early with that git submodule line when luau/VM/include/lua.h is missing.
What build.rs compiles#
Every Luau component the crate uses is one cc library: Common, Ast, Bytecode, Inliner,
Compiler, Config, Require and VM always; CodeGen under jit; Analysis under analysis. The
binder’s own C additions are csrc/extra.cpp; the shims over the C++-only APIs are
csrc/codegen.cpp (jit) and csrc/analysis.cpp (analysis).
| Define or flag | Value | Why |
|---|---|---|
LUAI_MAXCSTACK | 8000 | Luau’s default, OpenMW’s too; LUA_REGISTRYINDEX in the FFI derives from it |
LUA_VECTOR_SIZE | 3 | Three-component vectors, as OpenMW |
LUA_UTAG_LIMIT | 254 | Every tag Luau can address; l3i::TAG_LIMIT mirrors it, and LUAU_CXXFLAGS cannot lower it |
LUA_API, LUALIB_API, and the per-library *_API | extern "C" | The C ABI Rust declares by hand in src/raw/ffi.rs |
LUAU_ENABLE_ASSERT | Set when Rust’s debug assertions are on | Luau’s internal api_check assertions in debug builds; a release build with debuginfo keeps the release VM |
-fno-math-errno | Release only | Lets the compiler lower sqrt to one instruction |
-std=c++17, Cargo’s optimisation level | -O3 in release, no -march | The binary stays portable |
-fexceptions -fwasm-exceptions | emscripten | cc adds -fno-exceptions for wasm32; Luau needs exceptions in the ABI Rust uses |
/EHs | MSVC targets | Luau raises errors as C++ exceptions out of its extern "C" API too |
-flto=thin | The verified toolchain, except Apple | Bitcode objects, so the linker’s LTO sees Luau and the Rust thunks as one module |
Hosts may append compiler flags through LUAU_CXXFLAGS. Under jit, build.rs also parses
Luau/IrData.h and Luau/CodeGenOptions.h and writes ir_enums.rs into OUT_DIR, the mirrors
of IrCmd, IrCondition, IrBlockKind and HostMetamethod for that exact Luau build. It exports
LUAU_VERSION (0.742) and L3I_TAG_LIMIT to the crate.
Compiled chunks default to optimisation level 2 and debug level 1, and runtime plans turn type information on so native code sees the userdata types.
The value-layout mirror#
The binder reads argument slots straight from Luau’s 16-byte TValue layout. csrc/extra.cpp
pins every offset at compile time:
static_assert(sizeof(TValue) == 16, "l3i mirrors a 16-byte TValue");
static_assert(offsetof(TValue, value) == 0, "l3i mirrors the value union at offset 0");
static_assert(offsetof(TValue, extra) == 8, "l3i mirrors extra at offset 8");
static_assert(offsetof(TValue, tt) == 12, "l3i mirrors the tag at offset 12");
static_assert(LUA_VECTOR_SIZE == 3, "l3i mirrors three-component vectors");
static_assert(offsetof(Udata, tag) == 3, "l3i mirrors the userdata tag at offset 3");
static_assert(offsetof(Udata, data) == 16, "l3i mirrors the userdata payload at offset 16");
Each runtime proves the mirror against the API once at creation, so a Luau bump that moves a
byte fails at once rather than misreading slots. Bumping the submodule means bumping
LUAU_VERSION in build.rs, re-auditing src/raw/ffi.rs against the new headers, and
re-checking the flag policy.
Features#
[dependencies]
l3i = { version = "2", features = ["jit"] }| Feature | Default | Adds |
|---|---|---|
jit | off | Luau’s CodeGen library and csrc/codegen.cpp: l3i::native_code, the lowering hooks, quat.math(), and the lowered paths of raster.math(), soft.vertices(), bytes.math() and an annotated intern Pool’s intern and find. Not supported on emscripten |
analysis | off | Luau’s Analysis library and csrc/analysis.cpp: l3i::analysis (type checker, linter, autocomplete, parser) and RuntimePlan::check_definitions |
soft-render | off | The dream-soft-render dependency as the dream.soft_render extension |
bytes | off | The dream.bytes extension (l3i::bytes) and memchr for its searches |
bytes-codecs | off | miniz_oxide, lz4_flex, ruzstd, lzma-rs and crc32fast: inflate, deflate, LZ4, zstd, LZMA/XZ |
bytes-digests | off | crc32fast, adler2, xxhash-rust, md-5, sha1, sha2, blake3: the checksums and digests |
bytes-text | off | encoding_rs: text decoding and encoding for every WHATWG label |
bytes-regex | off | regex: bytes.regex, regular expressions over bytes, many ranges of a buffer in one call |
intern | off | The dream.intern extension (l3i::intern); no dependencies |
intl | off | The dream.intl extension (l3i::intl) with ICU4X’s icu_locale_core, icu_plurals and icu_decimal and their compiled CLDR data, fixed_decimal and writeable: BCP 47 locales, plural rules, decimal formatting |
syntax | off | The dream.luau extension (l3i::syntax) and csrc/syntax.cpp, which builds Luau’s parse tree as tables; Luau’s Ast library is in every build, so no dependencies |
fs | off | The dream.fs extension (l3i::fs) with memmap2, walkdir and same-file |
process | off | The dream.process extension (l3i::process); no dependencies |
tcp | off | The dream.tcp extension (l3i::tcp) with mio (epoll, kqueue, IOCP) and socket2 (backlog, address reuse, Apple’s SO_NOSIGPIPE): TCP listeners, streams and a readiness poller |
dns | off | The dream.dns extension (l3i::dns) with idna (over ICU4X’s normalizer) and, on Unix, libc for getaddrinfo: host name resolution on a bounded worker pool |
tls | off | The dream.tls extension (l3i::tls), turning on tcp, with rustls (ring provider; ring’s C and assembly built by cc with clang), rustls-platform-verifier, zeroize and idna: verified TLS client and server sessions |
The game transport is not a feature: dream-net is a normal dependency and the dream.udp
bridge is in every plan. Raw TCP is the optional tcp feature.
The toolchain rule#
l3i builds only with clang, cross-language thin LTO, and lld. build.rs refuses anything else,
naming the missing piece, so a partial configuration fails up front instead of at the final link
or by building silently slow.
| Side | Requirement | Where it is set |
|---|---|---|
C++ (Luau, csrc/) | clang++; build.rs adds -flto=thin itself | CXX=clang++ (.cargo/config.toml [env]) |
| C in dependencies (BLAKE3’s kernels, ring) | clang, never the system cc, which is GCC on many Linux hosts | CC=clang (.cargo/config.toml [env]) |
| Rust | -Clinker-plugin-lto -Clinker=clang -Clink-arg=-fuse-ld=lld, all three checked by build.rs | .cargo/config.toml [target.*] rustflags |
| Both | clang and rustc on the same LLVM major (clang++ --version, rustc -vV) | checked by build.rs |
| MSVC targets | clang-cl compiles (CXX_x86_64-pc-windows-msvc), rustc links with -Clinker=lld-link | .cargo/config.toml |
cc crate | parallel feature | Cargo.toml |
The lld flag is not optional: without it clang hands the bitcode objects to ld.bfd, which fails
with bad -plugin-opt option. -Clinker-plugin-lto without a clang-built Luau links fine but
forfeits the gain, and plain clang without the LTO half is slower than gcc at runtime, so the rule
enforces both. Linux is the measured configuration; the Windows entries follow the same shape and
are what CI runs.
Apple targets are the vetted exception: rustc passes -Clinker-plugin-lto’s GNU -plugin-opt
arguments to the linker and ld64.lld rejects them (found in dream-ini’s CI). There clang still
drives ld64.lld and rustc’s own release thin LTO covers the Rust side, build.rs builds Luau as
plain objects rather than bitcode, and the cross-language inlining is forfeited; macOS pays a few
percent on the binder’s hot paths, which is the price of keeping the platform.
Two exemptions:
L3I_UNVERIFIED_TOOLCHAIN=1turns the refusal into a warning, for hosts that cannot meet the requirement; expect slower binder hot paths.- docs.rs, which sets
DOCS_RS, is exempt automatically: it only renders documentation and cannot be handed a linker configuration.
l3i::VERIFIED_TOOLCHAIN says which kind of build a binary is: true exactly when build.rs
found nothing missing, whether or not L3I_UNVERIFIED_TOOLCHAIN was set. A host that publishes
performance numbers checks it, since an exempted build is slower by no fixed amount.
Installing the tools: Fedora dnf install clang lld; Debian and Ubuntu
apt install clang-<N> lld-<N> where <N> is rustc’s LLVM major. Apple’s clang is not upstream
LLVM; use a Homebrew or nightly LLVM whose major matches. The measurements behind the rule are
the toolchain campaign: every binder path gains
8 to 15 percent from the cross-language inlining, plain clang without it is slower than GCC, and
the verified configuration is also the fastest clean build.
GCC is not supported, for Luau or for anything else in the tree. Do not build, compile-check or
test with it; the standalone C++ suites under csrc/ are clang-only too.
What a dependent crate copies#
Cargo does not inherit a dependency’s configuration, so a crate that depends on l3i copies the
[env] and rustflags lines into its own .cargo/config.toml. This repository’s file, in full:
# The verified toolchain (https://DreamWeave-MP.github.io/l3i/docs/building/#the-toolchain-rule):
# clang compiles Luau, the Rust side emits bitcode, and
# lld runs one thin LTO over both languages. build.rs refuses anything else unless
# L3I_UNVERIFIED_TOOLCHAIN is set (docs.rs is exempt). Environment variables override every
# entry here. Linux is the measured configuration; the Windows entries follow the same shape
# (clang-cl plus lld-link) and are what CI runs. Apple targets are the vetted exception below.
[env]
CXX = "clang++"
CXX_x86_64-pc-windows-msvc = "clang-cl"
CXX_aarch64-pc-windows-msvc = "clang-cl"
# C dependencies (BLAKE3's SIMD kernels, ring under `tls`) go through the same compiler: the
# system `cc` is GCC on many Linux hosts, and l3i is never built with GCC.
CC = "clang"
CC_x86_64-pc-windows-msvc = "clang-cl"
CC_aarch64-pc-windows-msvc = "clang-cl"
[target.x86_64-unknown-linux-gnu]
rustflags = ["-Clinker-plugin-lto", "-Clinker=clang", "-Clink-arg=-fuse-ld=lld"]
[target.aarch64-unknown-linux-gnu]
rustflags = ["-Clinker-plugin-lto", "-Clinker=clang", "-Clink-arg=-fuse-ld=lld"]
# No -Clinker-plugin-lto on Apple targets: rustc passes that flag's GNU `-plugin-opt` arguments
# to the linker, and ld64.lld rejects them (vetted in dream-ini's CI). clang and lld still link,
# the release profile's thin LTO runs in rustc instead, and the cross-language inlining the
# binder's hot paths gain from is forfeited there; macOS pays a few percent, portability does not.
[target.x86_64-apple-darwin]
rustflags = ["-Clinker=clang", "-Clink-arg=-fuse-ld=lld"]
[target.aarch64-apple-darwin]
rustflags = ["-Clinker=clang", "-Clink-arg=-fuse-ld=lld"]
[target.x86_64-pc-windows-msvc]
rustflags = ["-Clinker-plugin-lto", "-Clinker=lld-link"]
[target.aarch64-pc-windows-msvc]
rustflags = ["-Clinker-plugin-lto", "-Clinker=lld-link"]
Environment variables override every entry, so CXX and RUSTFLAGS set in a shell take
precedence over the file.
Checking a build#
cargo test and cargo test --all-features run the ported C++ test contracts plus the runtime,
sandbox, watchdog, native code, analysis and renderer suites. The integration tests under
tests/ link as one binary, because each binary pays a full link-time codegen under
cross-language LTO, so one file’s tests run with cargo test <file>::. The clean configuration is
cargo clippy --all-targets --all-features -- -W clippy::pedantic -D warnings and
cargo fmt --check.
The C++ suites#
The surface frontend, the lowering and the source map under csrc/ have standalone C++ tests
that run outside Cargo, so they can run under clang’s ASan and UBSan and probe allocation
failure, which the Rust host must never see. Run them after any change under csrc/. They link
against the archives build.rs already built: LUAU_OUT is the build script’s OUT_DIR
(target/debug/build/l3i-*/out, or what cargo build -vv prints) and OUT any scratch
directory. clang only; add -fsanitize=address,undefined for the sanitized run.
The frontend’s own tests, and the lowering and provenance tests, link the frontend, the syntax module and the source map with Luau’s Ast, Common and VM archives:
clang++ -std=c++17 -O1 -fuse-ld=lld -Wall -Wextra -Werror -pedantic \
-isystem luau/Ast/include -isystem luau/Common/include -Icsrc \
csrc/surface_frontend.cpp csrc/surface_syntax.cpp csrc/source_map.cpp csrc/surface_frontend_test.cpp \
-Wl,--start-group "$LUAU_OUT/libluauast.a" "$LUAU_OUT/libluaucommon.a" \
"$LUAU_OUT/libluauvm.a" -Wl,--end-group \
-o "$OUT"/l3i-frontend-test
"$OUT"/l3i-frontend-test
clang++ -std=c++17 -O1 -fuse-ld=lld -Wall -Wextra -Werror -pedantic \
-isystem luau/Ast/include -isystem luau/Common/include -Icsrc \
csrc/surface_frontend.cpp csrc/surface_syntax.cpp csrc/source_map.cpp csrc/surface_syntax_test.cpp \
-Wl,--start-group "$LUAU_OUT/libluauast.a" "$LUAU_OUT/libluaucommon.a" \
"$LUAU_OUT/libluauvm.a" -Wl,--end-group \
-o "$OUT"/l3i-surface-test
"$OUT"/l3i-surface-test
The remapper skips subtrees whose coordinates the lowering left in place. A second suite proves that every location it produces is the one a full walk produces:
clang++ -std=c++17 -O1 -fuse-ld=lld -Wall -Wextra -Werror -pedantic \
-isystem luau/Ast/include -isystem luau/Common/include -Icsrc \
csrc/source_locations_test.cpp csrc/source_locations.cpp csrc/source_map.cpp \
csrc/surface_frontend.cpp csrc/surface_syntax.cpp \
-Wl,--start-group "$LUAU_OUT/libluauast.a" "$LUAU_OUT/libluaucommon.a" -Wl,--end-group \
-o "$OUT"/l3i-locations-test
"$OUT"/l3i-locations-test
The allocation-failure probe fails every allocation in turn through parse-error and
compile-error serialization for both C entry points, and checks that the outer exception barrier
returns null and a zero size without allocating, so nothing ever throws across the C ABI. It
overrides the global operator new. Never link it into the Rust host.
clang++ -std=c++17 -O1 -fuse-ld=lld -Wall -Wextra -Werror -pedantic \
-isystem luau/Ast/include -isystem luau/Common/include \
-isystem luau/Compiler/include -isystem luau/Bytecode/include -Icsrc \
csrc/compiler_failure_test.cpp csrc/bytecode.cpp csrc/surface_frontend.cpp csrc/surface_syntax.cpp \
csrc/source_map.cpp csrc/source_locations.cpp -Wl,--start-group \
"$LUAU_OUT/libluaucompiler.a" "$LUAU_OUT/libluauast.a" \
"$LUAU_OUT/libluaubytecode.a" "$LUAU_OUT/libluaucommon.a" \
"$LUAU_OUT/libluauvm.a" -Wl,--end-group \
-o "$OUT"/l3i-compiler-failure-test
"$OUT"/l3i-compiler-failure-test
Make any script that chains these stop on the first failure. A pipe into grep hides a failing
suite, and a failing suite is the whole point.