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 declaration

Keywords 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 = 5

as 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 = 5

One 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  /  1LL

Four 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 goto that automatic cleanup lowering emits;
  • require("ffi") is injected for any struct, cdef, ffi.* call, carray, or cheader;
  • require("table.new") or require("table.clear") is injected when its builtin is used, and table.clone injects its own definition; presizing also uses the table.new binding.

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#