Nupp syntax#
Nupp's grammar has two layers. Level 0 is LuaJIT's Lua dialect, including every LuaJIT 3.0 syntax extension. Level 1 is the typed layer on top. Both are implemented, and the normative definition is the ABNF grammar.
Level 0: the untyped base#
This is LuaJIT 3.0's dialect (the syntax-extension umbrella issue), in full:
Nupp adds three things here and takes nothing away: interpolated strings (`a is ${a}`), ??=, and type annotations on short-function parameters.
Level 1: the typed layer#
Type syntax:
Form Means
──────────────── ──────────────────────────────────────────
T? T or nil
T* pointer to T
T*? pointer that may be NULL
T[4] / T[?] C array, zero-based
{T} Lua array, one-based
{T, U} tuple
{[K]: V} map
{x: T, y: U} inline shape
A | B union
const T read-only view
function(A): B function type
Box<T> generic application
self the receiver, inside a declarationKeywords are contextual#
None of the level-1 introducers is reserved. type, record, interface, struct, const, cdef, from, unsafe, continue, global, as, is, metamethod, takes, borrows, exclusive, retains, releases, and out all keep their Lua meaning wherever a declaration cannot start:
That is what lets existing Lua keep compiling.
Compatibility with Lua and LuaJIT#
Plain Lua is valid Nupp#
Every valid LuaJIT program is a valid Nupp program. The compiler's test suite pins this against a large body of real-world Lua: it must parse with no errors, round-trip byte for byte, and check with no diagnostics.
There is exactly one deliberate overlap. Lua reads
local type Alias = 5as the two adjacent statements local type and Alias = 5, while Nupp reads it as a type alias. Put a newline or a semicolon after type to select the Lua meaning:
local type
Alias = 5One more thing changes without breaking: a || b now parses, as a or b, where Lua 5.1 rejected it. It raises the customary-operator lint, which is a house-style judgement a project can turn off.
What the generated code needs#
Generated Lua targets LuaJIT 2.1.1784535649 or newer — the first build carrying the backported syntax extensions. bin/nupp checks luajit -v and names the required build rather than letting a run fail on a line nobody wrote.
Most level-0 syntax is written straight through, because 2.1 backported it. A native ?. is one branch where the equivalent lowering would be a closure call, so passing it through is both shorter and faster:
Written Generated
─────────────────── ─────────────────────────────────────
a & b a & b
a >> 1 a >> 1
a > b ? "x" : "y" a > b ? "x" : "y"
x ?? "fallback" x ?? "fallback"
t?.x t?.x
a += 1 a += 1
|v| -> v + 1 |v| -> (v)
continue continue
const X = 1 const X = 1
1_000 / 1LL 1_000 / 1LLFour constructs are lowered, because 2.1 did not take them:
Written Generated
──────────────── ──────────────────────────────────────────────
a // b math.floor((a) / (b))
a //= b a = math.floor((a) / (b))
x ??= "set" if x == nil then x = "set" end
function(...xs) function(...) const xs = {n = select("#", ...), ...}
`a is ${a}` ("a is " .. tostring(a))Everything in level 1 erases: annotations, as, generics, unsafe do (which becomes do), and the interface and type declarations, which have no runtime value at all.
Generated code never changes the line count. A cursor only inserts newlines forward, so a traceback points at the line you wrote with no source map.
LuaJIT's table.new and table.clear, and Nupp's own table.clone, are available directly in Nupp source. Each generated module binds a used builtin once on its first line; no source require is needed. Recognition follows the prelude definition, so a local named table is left alone.
table.clone copies one level: the keys the table holds directly, plus its metatable. A value that is itself a table stays shared, and __index is not consulted, so the copy holds what next would have walked and inherits the rest the same way the original did.
Stock Lua 5.1#
Generated code does not run on stock Lua 5.1 in general. Three things stop it:
- any passed-through extension is a 5.1 parse error, as is the
gotothat automatic cleanup lowering emits; require("ffi")is injected for any struct,cdef,ffi.*call,carray, orcheader;require("table.new")orrequire("table.clear")is injected when its builtin is used, andtable.cloneinjects its own definition; presizing also uses thetable.newbinding.
A file that uses none of those, and whose typed layer erases cleanly, does generate plain 5.1 Lua. There is no flag that guarantees it.
Next#
- Type system covers what the annotations mean.
- The grammar for the normative ABNF.
- Declarations and modules for visibility and module rules.