A tour of Nupp#
This walks the whole language in one pass. Nothing here is a preview of a feature described properly later — it is the short version of each thing, with a link to the long one.
It starts as Lua#
Every valid LuaJIT program is a valid Nupp program. Rename a .lua file to .nupp and it parses, round-trips byte for byte, and checks with no diagnostics. The compiler's test suite pins that against a large body of real-world Lua.
Unannotated code is any and checks silently. Annotations are what turn checking on, one declaration at a time.
There is one place the two dialects disagree, and it is documented rather than discovered: Lua reads local type Alias = 5 as two statements, and Nupp reads it as a type alias. A newline or semicolon after type picks the Lua meaning.
Records are tables#
A record is a Lua table with a known shape and a nominal name. Methods go inline, beside the fields they use.
new Point(...) lowers to setmetatable({x = 3, y = 4}, Point). The runtime shape is what you would have written by hand.
Structs are cdata#
A struct is the same declaration syntax over FFI memory: fixed layout, real C field widths, no hash lookups.
That becomes ffi.metatype(ffi.typeof("struct { float x; float y; }"), ...). Fields must be C-representable, so a string or a {T} field is refused — this is the trade you make for the layout. A struct binding is never nil and zero-initializes, so local v: Vec2 is complete on its own.
Records and structs covers when to reach for each.
Interfaces are satisfied by shape#
A type satisfies an interface by carrying its members. The is clause is not required for that — it is a claim the checker trusts without re-proving, which is what lets a runtime registrar install the surface later. Interfaces erase completely and have no runtime value.
A union of literals is an enum#
There is no enum declaration; a string literal is a type, so a union of them is a closed set. A member is a string subtype, so a bare literal lands in it. Drop the else and the checker says which members you left out:
warning: NUPP2107 exhaustiveness: every branch returns, so this
handles "green" | "red" and leaves "green" unhandledUnions, optionals, and narrowing#
T? is T | nil. Inside the if, s is string.
When the alternatives carry data, give each record a literal-typed field and compare it — that is a tagged union, and the comparison narrows to the one record that declares the tag:
Narrowing reads is, == nil, truthiness, discriminant fields, and ffi.istype. It does not read type(x) == "string" — that is an ordinary call returning an ordinary string, and the checker has no way to tie it back to x. Write x is string.
Narrowing has the full list of what proves what.
Generics#
Type arguments are inferred from the call. Constraints use is: <T is Callable>. There is no explicit type-argument syntax at a call site — f<number>(x) parses as two comparisons, the way it does in Lua.
Ownership#
A value with a cleanup obligation carries it in its type, and the checker will not let you drop it.
A producer declares the obligation, and any type can carry one:
@drop marks the operation that consumes the resource; @owned marks the function that produces one. An ordinary local is destroyed automatically:
Cleanup runs on fallthrough, return, break, continue, a goto leaving the block, and an error raised anywhere inside. Moving, returning, or explicitly dropping the owner deactivates its automatic cleanup exactly once.
Ownership starts from here; the ownership reference has the whole model.
Waiting does not change a function's shape#
Nupp has no async function, await, future return type, or async half of the standard library. A suspension-aware operation is an ordinary call with an ordinary result:
With no scheduler, communicate blocks by driving the registered readiness sources. Under an installed scheduler, the same call parks the current coroutine so other work can run. A ready operation does neither.
Whether a function may suspend is an inferred effect. Use nosuspend do where control must not park; the compiler follows calls to the possible suspension and reports the path. Libraries subscribe through nupp.suspension, suspension handlers own scheduling policy, and all, gather, race, and batch compose several waiting operations without promises.
Suspension explains the runtime paths, cancellation contract, coroutine inheritance, and concurrent combinators.
Calling C#
That emits ffi.cdef and an ffi.load lookup. Parameter modes — borrows, takes, exclusive, retains, releases — say what C does with a pointer, which a header cannot. None of them change the ABI.
For a whole header there are two routes: nupp import-c writes a committed, hand-editable module, and cheader("mini.h") types the header at compile time with no generated file. C interop covers both.
Everything is one binary#
nupp check # type-check the project
nupp build # compile to Lua
nupp run app.nupp # compile and run
nupp fmt # format
nupp doc # generate this site
nupp lsp serve # language server
nupp explain NUPP2119The checker, formatter, documentation generator, language server, build system, profiler, and C importer are the same executable, built from the same parse. Tooling is the guided version of that list.
What to read next#
- Reasons to use Nupp — the case for each of the pieces above.
- Nupp syntax — the syntax in one place, including what LuaJIT 2.1 does and does not carry.
- Suspension covers ordinary calls that block or park according to their context.
- Type system — gradual typing, and what the checker proves.