Skip to content
jess
guide

Luau Style

How jess's own Luau, the templates, and every Luau example in these docs are written, and @jess/style, the checker that holds them to it.

The built-in modules and templates are some of the first real Jess code people read, and whatever they find there is what they copy. So the Luau owned by this repo, the jess init template, the test fixtures, and every block on this site follows one style, and a test fails when it drifts.

@jess/style is the repository's enforced Luau style gate: a checker written in Luau, over Luau's own parser, in the built-in package jess carries. It owns the structural rules below and runs as part of the Rust test suite. There is no separate formatter and no formatter configuration, deliberately: the checker is the formatter-and-style gate for every repository that builds on Jess.

The style§

Blank lines are paragraphs. One logical thought per paragraph. Setup and the action it sets up stay together; a new phase gets a blank line.

Compound statements get room. A multi-line if, for, while, repeat, do, function, or table constructor is always set apart by a blank line from a compound statement next to it, one-line or not. Two multi-line ifs in a row, a loop after an if, a table after a loop: blank line, every time.

local function hasFlag(flags: string?, flag: string): boolean
  if not flags then return false end

  for name in string.gmatch(flags, '[^|%s]+') do
    if name == flag then return true end
  end

  return false
end

Guard clauses keep the happy path flat. Return early, continue early, and keep the interesting code at the lowest indentation it can have.

for _, module in modules do
  if not module.active then continue end

  local layer = module.layer
  if not layer or layer == '' then continue end

  table.insert(groups[layer], module)
end

Declare things where they're used. No local Next, Type, Format = next, type, string.format at the top of the file. That's a LuaJIT habit, and Luau doesn't need it. Constants that belong to the module are PascalCase (PackageName, FlagBits); everything else is camelCase.

Truthiness, unless false means something. if not package then, value or default. Write == nil only where false is a real value that has to survive, like a TOML boolean or an option that turns a setting off, and say so in a comment when it isn't obvious.

Group declarations. Same-kind locals initialize on one line; each multi-line table gets its own paragraph.

local report, summary

local merged, usedLayers, failed = {}, {}, {}

local defaults = {
  output = 'report.toml',
  checks = { 'all' },
}

Names say what they are. Full words: record, not rec; left and right, not a and b. No single letters anywhere, i, x, and k included: a loop runs over an index, a grid over a column and a row. A name that needs a comment to explain it is the wrong name.

Comments are for things code can't say. External contracts, surprising behavior, the reason something weird is weird, licenses. Not narration. If a comment restates the next line, delete the comment and fix the line.

Errors§

Script code that breaks an invariant calls error and stops. It doesn't wrap its own functions in pcall to rewrap the message; pass the context in and raise the final message where the problem is found.

pcall goes around a call into an API that can legitimately throw, and nowhere else: bytes.regex on a user's pattern, @jess/json's decode on a document that came from disk. Every one gets a comment naming what throws and why the script carries on.

-- `bytes.regex` throws on a pattern Rust's regex crate rejects. Every bad pattern is reported
-- before the run stops, so each is caught here and collected.
local compiled, regex = pcall(bytes.regex, pattern, CaseInsensitive)

A pcall without that comment is a bug report waiting to happen, and the checker treats it as one: it's an error, in capitals. The comment goes right above the statement holding the call, or above the statement that holds that one from its first line (a function whose first statement is the pcall, an if whose body is). One comment above a run of pcalls with no blank line between them speaks for the whole run, which is how a test that throws on purpose five times in a row reads. A ---@type annotation or a --! directive isn't a comment for this purpose; it says nothing about what throws.

Types§

Every module starts with --!strict. --!native is a claim, not a default: a module carries it only when its package's bench shows a workload that runs faster with it, and the package records that workload beside the code. Jess compiles natively only what is marked (NativeCodeMode::Annotated), so an unmarked module costs its bytecode compile at load and nothing more. A module that parses arguments, reads a config, or builds tables once is never marked. The same goes for one function: @native before a function in an unmarked module compiles that function and nothing else, which is the cheaper claim when one loop in a large module is the hot one.

Jess's own Luau follows the same rule. The package bootstrap, @jess/package, @jess/jpk, the workspace, and @jess/log load as bytecode: a run spends almost no time in them, and compiling them natively was measured to cost every start more than it ever gave back. @jess/test is native, since every suite runs through it, and the style checker and the analysis carry @native on the functions a check spends its time in. The formats are the exception: every module under the built-in package's src/data/ (@jess/json, @jess/yaml, @jess/data/toml, @jess/data/value) is native whole, because a codec can't know whether its caller hands it a manifest of a few hundred bytes or a document of megabytes, and on documents of any size the native code won by a wide margin on a verified build.

Everything is typed, and nothing is any. unknown is fine where the API really hands you an unknown (a decoded JSON or TOML document), and nowhere else. The test suite type-checks the built-in package and the shader corpus against the generated definitions; nothing type-checks the modules under src/lua/stdlib or the jess init template yet, so when you change one, check it with common::type_check from tests/lua/common/mod.rs in a test you don't commit.

The l3i dialect§

Jess's Luau is l3i's dialect: comprehensions, reducers, sinks, slices, and record patterns, each of which lowers to the loop you would have written. The style holds them to the rules it holds that loop to.

local ids = [for entity in entities if entity.active => entity.id]
local heaviest = max[for body in bodies => body.mass]
local {health, position: {x: column, y: row}} = entity

A binding is a local. A generator's binding, a pattern's field, and a pattern standing where a parameter would are named like any other local: full words, camelCase. A shorthand field binds a local of the field's own name, so a record with an x is renamed where it's bound, {x: column}.

A comprehension over several lines is a compound statement, like a table over several lines, and gets the same blank line from its neighbours.

A filter is a condition. if value ~= nil in a comprehension is held to the truthiness rule exactly as it is in an if, and so is what any and all reduce.

An element kind is a string. terrain[1:count, 'f32'] and into(samples, 'f32@16'), single-quoted like the rest.

Use it where it says the thing. One list built from another, a count, a sum, a test, a few fields pulled out of a record. A loop with real work in its body, a walk over a map, or anything that leaves early is still a loop, and a comprehension that needed a helper invented to fit its projection was a loop too. It isn't free everywhere either: a comprehension checks every projection for nil, and next to a loop that already sizes its table and stores by index that check is measurable on a trivial projection. The hot loop that was already written well stays.

The checker§

@jess/style is the style above, as code. It's the first module of jess's built-in package, jpk/builtin, and it's Luau end to end: one parse with @dream/luau, Luau's own parser, the tree and its token stream as tables; one pass over the tokens for the layout of every line; one walk over the tree for everything else. No regexes over source text, no guessing where a long string ends.

jess run --capability filesystem.read check-style.jsl src tools jpk/builtin
--!strict
-- check-style.jsl: every finding under the folders given, and a count.
local style = require '@jess/style'
local errors, warnings = style.run { ... }

print(`{errors} errors, {warnings} warnings`)

style.run prints one line per finding, path:line:column: severity[rule]: message. style.check(source, options) returns the findings as tables (rule, severity, message, and the span: line, column, endLine, endColumn), and style.checkFile reads a file first, checking a Markdown page's ```luau blocks one by one. style.checkPackage runs the whole thing over a Package Source, with its payload held to strict and no any. The API page has the rest.

RuleSeverityWhat it wants
syntaxerrorLuau that parses.
pcall.commenterrorA comment right above every pcall and xpcall, naming what throws.
layout.indentationerrorSpaces, two per level. Text inside a long string or comment keeps its own layout.
layout.compounderrorA blank line between adjacent compound statements when either spans several lines. A statement holding a multi-line table, function, or comprehension counts as compound.
quotes.doubleerrorSingle quotes, unless the string holds one. The element kind of a typed slice or a buffer sink is a string too.
quotes.backtickerrorBackticks only around interpolation.
hoistingerrorNo local that only renames a Luau global or library function. math.huge and friends are values, not functions, so naming one is fine.
naming.caseerrorcamelCase locals; PascalCase only for module constants. IDENTITY and light_count are neither.
naming.constantwarningA module-level local holding a literal that nothing reassigns (or, for a table, nothing changes or hands to a function) is a constant, and PascalCase.
naming.wordswarningFull words. No single-letter name in any position: locals, parameters, loop variables, the bindings of a comprehension or a record pattern, and the keys you write in a table constructor; _ is the only one-character name. No abbreviations either: record, not rec.
truthinesswarningif not name for a value that can't be false. Only when the checker can prove it: a local annotated with a type that has no boolean in it (aliases in the file followed), a literal, or a library call like string.find.
locals.groupwarningAdjacent same-kind locals (empty tables of the same type) on one line.
locals.mixedwarningNo local starting nil in a statement that initializes others. A call, ..., or a buffer sink (the buffer and the count it wrote) may fill more than one.
guard.elsewarningNo else after a branch that returns, breaks, continues, or errors.
guard.nestedwarningA function or loop body that's one condition wrapped around three or more statements becomes a guard clause and the work.
directives.strict, directives.nativeerror--!strict and --!native at the top, where the caller requires them. The repository holds its own Luau and every package to strict only; native is earned, never required.
types.anyerrorNo any, where the caller requires it.

It costs more than a line-based check would, and checks more: about two thirds of its time is Luau's parser building the tree as tables, and the rules are the other third.

There's no comment that turns a rule off, for a line or a file. The style says to fix the code, not to quiet the checker; when the checker is wrong, fix the checker.

Options choose what a file is held to. directives = { 'strict' } and noAny = true are for the built-in modules; only = { ... } limits a check to some rules; declarations says whether a file is a definitions file, which the checker otherwise works out; and profile = 'lua' is for plain Lua projects, where hoisting globals into PascalCase locals (local Next, Type = next, type) is the style rather than a mistake and LuaLS ---@param types feed the truthiness rule.

The style test§

cargo test --test integration lua::luau_style::

tests/lua/luau_style.rs activates the built-in package from its source, which also checks that it's the copy the binary embeds, and runs @jess/style over every repository-owned .jsl file under src/, tools/, and jpk/builtin, over every ```luau block on this site, and over jess luau-types' definitions. Any finding, error or warning, fails it. The built-in modules are held to --!strict and no any; the jess init template to --!strict.

The same test runs the checker's own suite (jess package test jpk/builtin, from the source and packed) and type-checks the package against the types folder.