larvae logo
The Luau toolchain, in Rust · v0.4.0

Format,lint,andshipLuau.

One parallel Rust binary. Requires that cannot break, style that cannot drift, and worms that teach it languages beyond Luau. It starts with the require rewriting nothing else in the ecosystem does.

lpm tool add larvae

Fast by architecture

Parallelism, one parse per file, a constant number of transform passes, and incremental builds. Each one is a multiplier that darklua can't retrofit.

Parallel by architecture

Every file is processed across all cores with rayon. darklua is single threaded and has been since its parallelize issue was filed in 2021.

Splices, not reprints

Rewrites are byte range splices into the original source, so untouched bytes are never printed again. No second AST either.

Native string requires

Expand @alias imports into require("@game/...") paths the Roblox engine understands natively. No Instance chains, output stays diff-able.

Realm-aware validation

check catches requires that pass on disk but fail in a running game: server/client realm violations, Starter-container clones, and wait semantics.

Incremental & watch

The cache key covers the whole resolution environment, not just file bytes, so it is never stale. Watch mode keeps the last good output when a save fails to lex.

One Rojo project file

default.project.json stays the source of truth and larvae derives the build project from it, instead of the two hand synced files darklua needs.

The gap nobody fills

Roblox ships native string requires but no custom aliases. larvae expands your aliases at build time, and no other tool can rewrite into that form.

config

# larvae.toml
[aliases]
pkg = '@game/ReplicatedStorage/Packages'

Aliases merge over your .luaurc, so projects that already define them there need no config at all.

const Signal = require('@pkg/signal')
const Config = require('./config')
const Util = require('@self/util')

You write clean alias imports, backed by larvae.toml or your .luaurc.

Where it pulls ahead

These are structural differences. The numbers below are the receipts.

 larvaedarklua
ParallelismAll cores, every stageSingle-threaded since 2021 (#55)
RewritingByte range splices, no second treeTwo tree representations, double allocation
Rojo project filesOne, the build project is derivedTwo, kept in sync by hand
Incremental buildsResolution-aware cache + watch modeNone (#228 open)
Runtime validationRealm and clone aware, refuses broken requiresNone, disk resolution only
Native string requiresFirst-class output target with aliasesInstance chains from a sourcemap only
Rule coverage36 shipped, 27 of darklua's names plus 9 it does not have32 rules today
Formatting & lintingBuilt in, with stylua and selene paritySeparate tools
Custom transformsWorms: Luau, wasm, or a native binaryFork the project

Measured, not claimed

The headline is the matched comparison, both tools cold on the same tree running the same 10 rules. 16 cores, 7 runs per cell, median reported, larvae 0.1.0 against darklua 0.19.0.

the three workloads

same rules
both tools run 10 matching rules with retain_lines
parse only
darklua runs no rules, larvae still rewrites every require
darklua default
darklua’s own default stack plus its dense generator, unmatched on larvae’s side

3,000 files

synthetic project

14.0x faster

same rules, both cold

larvae
35 ms
darklua
493 ms
parse only 24 ms vs 430 ms 17.9xdarklua default 571 ms

5,000 files

synthetic project

14.0x faster

same rules, both cold

larvae
57 ms
darklua
799 ms
parse only 40 ms vs 703 ms 17.5xdarklua default 938 ms

1 large file

single 3.5 MB module

10.3x faster

same rules, both cold

larvae
140 ms
darklua
1450 ms
parse only 16 ms vs 1206 ms 75.3xdarklua default 1605 ms

Rules cost far more here than require rewriting does, 140 ms against 16 ms, because one enormous module gives the thread pool nothing to split. It is the workload larvae wins by the smallest margin once rules are on.

incremental, ours alone

darklua has no cache, so every darklua run is a cold run and these columns have no counterpart.

filescoldwarmone editcheck
3,000 files24 ms14 ms13 ms21 ms
5,000 files40 ms22 ms22 ms37 ms
1 large file16 ms3 ms3 ms80 ms

the linter, against selene

matched holds both tools to the 30 lints both implement, defaults runs what each enables: 49 lints for larvae, 33 for selene. 16 cores, median of 7.

5.5–5.9x faster

matched, both lint sets equal

filesworkloadlarvaeselenespeedup
3,000 filesmatched22 ms121 ms5.5x
3,000 filesdefaults26 ms121 ms4.6x
5,000 filesmatched34 ms201 ms5.9x
5,000 filesdefaults37 ms202 ms5.4x

One 1 MB module lands at 279x, and it is deliberately not the headline: selene’s cost per byte climbs with file size while larvae stays linear, so that row measures parser throughput, not a build. The matched row also favours selene, it holds 3 of selene’s lints off against 19 of larvae’s, so read the 5.5–5.9x as a floor on the gap.

On the matched rows larvae is also doing require rewriting that darklua is not. darklua can convert requires from a rojo sourcemap, but that needs a separate rojo run, so it is left off rather than timed unfairly. No speedup is claimed against darklua’s default stack, because that run is not matched rule for rule.

Running in three steps

1

Install

pick one
lpm tool add larvae
# or
lpx larvae
# or
cargo install larvae

Three separate routes to the same binary, not commands to run in order. lpm adds it as a project tool, lpx runs it once and installs nothing, and cargo pulls the published crate. Prebuilt binaries ship on GitHub releases.

2

Initialize

larvae init

Writes a starter larvae.toml with every fmt option and lint at its default as comments, offers to add .larvae/ and dist/ to your .gitignore, and turns Luau's own lints off so nothing reports twice.

3

Process, then serve

larvae process
rojo serve .larvae/build.project.json

larvae writes dist/ and keeps the derived project file fresh, then you point rojo at it. It never runs rojo for you, serving is rojo's job.

the full toolkit

larvae process   # rewrite requires, run rules; -w rebuilds on change
larvae check     # the whole-project analysis, the CI gate
larvae fmt       # format in place, stylua parity
larvae lint      # 51 lints with selene's spellings
larvae bundle    # the project as one file, modules load on demand
larvae lsp       # diagnostics and formatting for the editor
larvae init      # write a starter larvae.toml
larvae worm      # develop an extension
larvae self      # install, update, uninstall, code