Declarations and modules#
A typed declaration says where it lives, using the spelling Lua already has for saying where a definition lives. There is no fourth rule to learn and no default to remember.
form value side (plain Lua) type side
───────────────────── ────────────────────── ────────────────
local record R file-local file-local
record M.R a member of M a member of M
global record R a _G global a project globalThe parallel is exact: record M.Point is to record what function M.f is to function. A dot puts the thing on a table; local keeps it in the file; neither makes it global.
Module forms#
A declaration that names none of the three is refused:
Plain Lua would have made Loose a global. Rather than reuse that silence for a different meaning, Nupp asks. This is the one place the language declines to inherit a Lua default, because the cost of guessing wrong is a name that means one thing here and another thing elsewhere.
Naming a member from another file#
A member is reached through the module it was attached to, so every name in a file shows where it came from:
A module path also names a type directly, without a runtime require:
The module itself is a value like any other, so it has to be required before its name means anything — a file's basename is not in scope elsewhere:
An unknown name is any, as it is anywhere else, so the first line would otherwise say nothing at all until it ran. Because a project file is named after it, the checker knows better, and says so once per name:
error: NUPP2120: "mathutil" names a project module; require("mathutil") to use itA build refuses it. The program does not work — mathutil is nil when it runs — and a compiler that can see why should not hand you a binary that fails later. An editor reports the same thing as a warning: a file you are typing into is half-written by definition, and the require is usually the next thing you add.
This is the one diagnostic where a name being merely unknown is not the end of it, so it is not @allow-able advice. If a project genuinely has an undeclared global sharing a file's basename, the file or the global has to be renamed.
Only a global is reachable without saying where it came from. There is no project-wide search for an unqualified name, so adding a Point — or a mathutil.nupp — to one corner of a project cannot change what a name means in another.
Returned module tables#
A module's type is whatever the file returned — nothing is merged in behind it. A declaration that carries a runtime value puts itself on its table, which is an ordinary assignment in the generated Lua:
type and interface have no runtime value and emit nothing; they still name a member on the type side. Records and structs are values too, which is what lets a dependent construct one:
Which local is the module is read off the return statement, so wrapping it still works:
One table deep, so the name a declaration binds under and the field it is assigned to stay the same thing — record m.sub.Deep is refused. A record body is where types nest, and it reaches through the table its owner sits on:
Attaching to any other table is not an export. It is a perfectly good way to group types privately:
Naming itself#
Inside its own body a declaration answers to its simple name, so a recursive field does not repeat the table it sits on:
The binding lives and dies with the body. Outside it, the member is shapes.Path like any other.
Methods#
Prefer implementing a record's methods inline, as Path.count above. Inline methods keep behavior beside the fields and contracts it relies on, declare self first, and are still emitted as ordinary shapes.Path methods. Use a separate qualified method only when adapting a type outside its declaration.
Conventions#
None of this is enforced — there is no naming lint — but it is what the compiler's own sources and the generated documentation assume.
Use camelCase for functions, methods, locals, parameters, fields, and module filenames. Use PascalCase for nominal types: User, HttpClient, ReadBuffer. Names imported from C keep the spelling of the C API, because those identify ABI symbols; a camelCase local holding the module (local miniApi = require("native.mini")) marks the boundary without disguising the foreign name.
For a module, keep helpers and internal aliases local, attach the exported records, structs, functions, and values to the module table, and return that table once at the end. Reserve global for a contract that genuinely belongs to the whole project.
Annotate exported parameters and returns; let obvious locals infer. That keeps public contracts stable without making bodies noisy.
Leading underscores mark privacy to the documentation generator: members beginning with _, source files beginning with _, and files under internal/ are omitted unless the docs target opts into private output.
Mutual recursion across files#
Type resolution runs over declarations, not over loaded modules: a declaration is nameable as soon as its header is parsed, before any body is checked. Two modules may therefore refer to each other's types freely. The runtime require is a separate matter and follows ordinary Lua rules, so a genuine load-time cycle — two modules constructing each other's records while loading — is still a genuine cycle, and is reported as one.
This is also what keeps rebuilds cheap: a file's interface is derived from its declaration headers, so editing a function body cannot change it, and dependents are not rechecked.
Declaration files#
A .d.nupp file describes an interface it does not own and returns no table of its own, so there is nothing for a declaration to attach to. A bare declaration there is the interface being described, and is allowed:
In a declaration file local is not privacy either; it marks the bindings the described module exports. See the documentation section of the README.
Annotation definitions#
An annotation definition is registered project-wide under its own name, since applications spell an unqualified @name and there is no table to reach it through. It is the other exemption from NUPP2119. See annotations.md.
Worked example#
src/geom/shapes.nupp:
src/app/main.nupp:
Diagnostics#
- NUPP2119 — a declaration names no visibility and no table to attach to. Write
local,global, or a qualified name. Also raised when a modifier sits beside a qualified name, which says where it lives twice. - NUPP2101 — unknown type name. An unqualified name that is a member of some module reports this rather than resolving: name the module.
- NUPP2102 / NUPP2104 — two project globals share a type or value name. Globals are one flat namespace, so one of them has to become a member.
- NUPP2120 — a project module is used without being required. An error in a build, a warning in an editor. Given once per name, and it replaces NUPP2105 for that name.
- NUPP2105 (strict files) — unknown variable, for names no project file answers to. Reported in a
.nuppfile, and in any file under--strict. - NUPP1006 — the typed layer written in a
.luafile, which is plain Lua.
NUPP2119, NUPP2101 and NUPP2120 carry machine-applicable fixes, which the LSP server offers as quick fixes. Each way out a message names is its own fix rather than a choice made for the author: NUPP2119 offers local, global, and — where the file returns a table — attaching to it; NUPP2101 offers the qualified spelling through each module exporting that name, adding the require in the same edit when the file has none; NUPP2120 offers one require per candidate module. A fix that would bind over a name already in scope is not offered at all.