File types#

A file's extension says what it holds: Nupp source, an interface somebody else implements, or plain Lua the toolchain runs unchanged. One project mixes all three, and a module's name never includes the extension.

module models

export record Point
    x: number
    y: number
end

export function scale(p: Point, k: number): Point
    return new Point(x = p.x * k, y = p.y * k)
end

File extensions#

Extension Holds
.nupp Nupp source; unknown variables and untyped exports are errors
.d.nupp declarations for an interface somebody else implements
.lua plain Lua syntax; recognized type comments are imported

Unknown variables and untyped exports#

Every .nupp file is held to two rules beyond ordinary checking. An unknown variable is reported instead of typing as any, and an exported declaration without an annotation is reported too, so nothing untyped crosses a module boundary.

module models

export function double(n)
    return n * factr
end
nupp check models.nupp
error: NUPP2106: an exported function must write its public result type
error: NUPP2106: an exported function must write every public parameter type
error: NUPP2105: unknown variable "factr"

Inside the file nothing more is asked. An unannotated local takes the type its initializer settles and an unannotated parameter is any, as Inference describes.

Plain Lua files#

The toolchain requires, builds, and runs a .lua file unchanged. Nupp syntax is refused there because the runtime file must remain Lua:

local function double(n: integer): integer
    return n * 2
end
nupp check helpers.lua
error: NUPP1006: a type annotation is not plain Lua

LuaCATS, EmmyLua, and typed LuaDoc comments are always imported as checker facts, so a Lua file can state the same contract without changing its runtime syntax:

---@param n integer
---@return integer
local function double(n)
    return n * 2
end

Malformed or unsupported comment types recover locally to any and report a NUPP1008 warning. An unknown name in a .lua file is not reported, since it may be a global the program sets at run time. See LuaCATS for the comment forms and how nupp migrate turns them into Nupp.

Declaration files#

A .d.nupp file describes an interface somebody else implements, and may write any where that is the type the interface actually has. LuaJIT's string.buffer encode(v: any): string does take any Lua value, and no annotation written here changes what LuaJIT accepts. A declaration file cannot declare a source module.

Constructs that aren't erased#

A struct has a checked fixed layout. Native LuaJIT targets lower it to FFI cdata, while portable targets may select the table-backed struct provider. A C header imports as checked declarations that load native symbols. Everything else is ordinary Lua at run time: the types are gone, and what remains is what you would have written by hand.

local struct Vec2
    x: float
    y: float
end
const __nuppMt_Vec2 = {__index = {}}
const Vec2 = ffi.metatype(ffi.typeof("struct { float x; float y; }"), __nuppMt_Vec2)

See Structs for the layout rules that lowering follows.

FAQ#

Does existing Lua need conversion?#

No. A Lua file stays valid in a .lua file, and .nupp modules require it and are required by it like any other module. Move a file to .nupp when it needs typed syntax; nupp migrate writes annotated Lua to a .nupp destination.

Do file extensions change module identity?#

No. The extension is not part of the module name, so require("models") resolves models.nupp or models.lua alike, and nothing that requires a file changes when it moves from one to the other. See Canonical names for how a module's name is derived from its path.

Do types exist at runtime?#

Annotations, generics, interfaces, affine policies, and most other checked constructs erase when source lowers to Lua. A struct retains a runtime representation because its checked layout is the feature, and C declarations remain runtime bindings because they load native symbols. Ordinary typed code acquires no type registry or runtime checker.