Nupp

LuaJIT with static guarantees.

Nupp gives LuaJIT precise types, checked C interop, deterministic ownership, scheduler-neutral suspension, isolated workers, and self-contained builds—without hiding the Lua underneath.

A nuppeppo in a moonlit forest

Try Nupp#

Nupp Features

-- models.g.nupp: type syntax, gradual checks
local function scale(point, factor)
    return {x = point.x * factor, y = point.y * factor}
end

-- models.nupp: the same code, now a checked boundary
local function scale(point: Point, factor: number): Point
    return new Point {x = point.x * factor, y = point.y * factor}
end

Add types without leaving Lua behind

Start with a LuaJIT program that already runs. Add annotations where they earn their keep, then tighten a file to strict Nupp when it is ready.

local record User
    name: string
    online: boolean
end

local struct Vec2
    x: float
    y: float
end

Model real data precisely

Records stay flexible Lua tables. Structs become compact FFI cdata with a fixed C layout. Use the representation your data actually needs.

local function first<V>(items: {V}): V?
    return items[1]
end

local function label(value: string | number): string
    if value is string then return value:upper() end
    return string.format("%.2f", value)
end

Write expressive, checked APIs

Generics, interfaces, unions, overloads, and control-flow narrowing make contracts useful without making Lua feel heavy.

cdef struct timeval
    tv_sec: int64
    tv_usec: int32
end

cdef function gettimeofday(tv: timeval*, tz: voidptr?): int32

Make FFI contracts visible

Declare native functions with their real C-compatible types. Nupp checks every call while LuaJIT still does the fast work.

# Generate typed bindings you can commit and edit.

nupp import-c native/library.h --out src/library.d.nupp

# Or use a header directly while compiling.
local native = cheader("native/library.h")

Import headers instead of transcribing them

Turn a C header into a typed Nupp declaration module, then keep the generated boundary reviewed and versioned with the code that calls it.

do
    local file = resources.openFile("report.txt", "r")
    local contents = file:read("*a")
    send(borrows contents)
end -- the file is closed on every structured exit

Give resources a lifetime the checker can see

Ownership, borrowing, pinning, and deterministic cleanup make the important rules at a C boundary explicit—and make leaks and use-after-move errors reportable.

local process = require("nupp.io.process")

local function compilerVersion(): string
    local child = assert(process.new({args = {"cc", "--version"}}))
    local result = assert(child:communicate())
    child:close()
    return result.output
end

local function printVersion(): nil
    print(compilerVersion())
end

printVersion()

Suspend without coloring the call graph

A suspension-aware call returns its ordinary value. It blocks without a handler and parks its coroutine under a host scheduler. The checker tracks that effect separately from the return type. Follow the call from function to scheduler.

local workers = require("nupp.workers")

do
    local hasher = workers.spawn("workers.hash")
    local answer = hasher:call({
        name = "level1",
        bytes = contents,
    })
end

Use every core without sharing the heap

Workers run fresh LuaJIT states on native threads and exchange bounded, serialized messages. Calls read like functions, failures cross back, and ownership guarantees every worker is joined.

cdef function send(borrows bytes: cstring): int32

@owned(free)
cdef function malloc(size: uint64): voidptr
cdef function free(takes value: voidptr)

Capture what native calls are allowed to do

Effect contracts describe whether a call borrows, takes, or returns ownership. The compiler infers those facts for Nupp code and checks them at module boundaries.

local name = "Nupp"
local status = ready ? "go" : "wait"
print(`Hello, ${name}: ${status}`)

Keep the LuaJIT you already know

Every valid LuaJIT program is valid Nupp. Keep Lua's small, direct model, then opt into interpolation, typed declarations, and the rest of Nupp where they help.

local source = nupp.io.Path.new("src", "main.nupp")

-- Path support is selected for this target. Workers, URI
-- support, UUID generation, and unrelated native code are absent.

Pay only for the native runtime you use

The compiler follows resolved standard-library uses and builds exactly their Rust or C providers. Paths, workers, and every other native facility disappear completely when the program does not use them.

nupp build --target dist
nupp fixpoint --binary

Ship a deterministic, self-contained program

Build modules, one-file Lua bundles, or executables with a feature-matched LuaJIT host. Sorted payloads and content-addressed inputs make identical source produce byte-identical output.

local record Vec2
    x: number
    y: number
    expands (x, y)
end

update(
    ...entity.body.position,
    ...entity.body.velocity,
    delta
)
-- entity.body is read once; update receives x, y, x, y, delta.

Optimize what the JIT can't infer

Nupp leaves hot loops to LuaJIT's tracer and uses types where the tracer cannot: declared call projections share stable table paths and become flat positional arguments without tables, varargs, or closures.

nupp.log.debug("spawn at %d,%d", x, y) -- unevaluated when filtered

local zone = require("nupp.zone")
zone.push("physics")
stepWorld()
zone.pop() -- inlined against the zone stack, not called

Compiler intrinsics

A logged line and a zone marker are calls the compiler knows enough to remove. A filtered nupp.log severity evaluates none of its arguments, and a zone push or discarded pop on the module nupp.zone returns generates inline--no call left for a hot path to pay for.

const CRC32 = comptime do
    const entries = {}
    for byte = 0, 255 do
        local acc = byte
        for _ = 1, 8 do
            acc = acc & 1 ~= 0 and 0xedb88320 ~ (acc >> 1) or acc >> 1
        end
        entries[byte + 1] = acc
    end
    return entries
end

-- The generated Lua holds the table, not the loop that built it.

Compute what you can before the program runs

comptime do ... end runs ordinary Nupp while the file is compiled and writes the answer into the output as a literal. Deterministic and sandboxed: no clock, no files, no randomness -- and no macros, because it produces data rather than code.

nupp check          # type-check the project
nupp fmt            # apply Nupp's fixed style
nupp test           # build and run the configured suite
nupp run --profile  # write a speedscope-compatible profile
nupp lsp            # start the language server

Carry the whole workflow in one toolchain

Check, format, build, test, profile, generate documentation, explain errors, and power an editor from the same language-aware compiler. No glue scripts required.

Getting started#

  • Installation — requirements, a checkout, and a first project.
  • A tour of Nupp — the whole language in one pass.
  • Suspension waits with or without a scheduler, composes concurrent work, and checks where suspension is forbidden.
  • Workers run CPU work in isolated LuaJIT states and communicate through bounded copied messages.
  • Nupp syntax — the syntax, and what LuaJIT 2.1 carries.

API docs#

  • The language reference — every construct and the codes that report getting it wrong, generated from the compiler.
  • Type system — gradual typing, records, structs, interfaces, generics, and narrowing.
  • Ownership — resources that are hard to leak.
  • Effect contracts — what calls may observe, change, or expose.
  • Suspension covers scheduler-neutral waiting, checked suspension effects, cancellation, and structured concurrency.
  • Tooling — the checker, build system, formatter, language server, documentation generator, and profiler.
  • The nupp standard library — JSON, UTF-8, buffers, readers, writers, paths, URIs, identifiers, hashes, checksums, math and vectors.
  • The language reference — every construct and the codes that report getting it wrong, generated from the compiler.

The generated API reference for the compiler's own modules follows.

Modules