Diagnostics#
A diagnostic is a stable code, a severity, a source range, and a message, and every one the compiler reports carries all four whether a person or a program is reading. This is what one looks like on standard error:
src/main.nupp:8:13: error: NUPP2004: no field "horizonal" in Point
8 | print(point.horizonal)
| ^~~~~~~~~
help: use the suggested field spellingText report#
The first line keeps the conventional compiler form, so a build tool that already reads file:line:column: severity: message needs no adapter for Nupp. Under it come the quoted source line and a caret run marking the primary range, then one note line per related location, then help.
Lines, columns, and offsets are 1-based byte positions. They are 0 when the file could not be read at all.
Lint names#
A lint carries its name after the code, so the thing a project would configure is visible in the line it printed:
src/main.nupp:4:5: warning: NUPP2107 exhaustiveness: every branch returns, so this handles "blue" | "green" | "red" and leaves "red" unhandled
help: add branches for "red" or add an else clauseThat name is what an @allow suppression or a nupp.lua entry writes. See Lints for the built-in lints, their categories, and their default levels.
Color#
Written to a terminal the same report is colored: the severity and the caret run share a color, the code and lint name are dimmed, the file position and the message have their own, and so does the rail beside the quoted source. Written anywhere else it is exactly the text above, byte for byte.
--color=always and --color=never answer without looking at the stream. Failing an explicit flag, NO_COLOR refuses escapes, CLICOLOR_FORCE demands them, and failing both the stream is asked whether it is a terminal that understands them, which TERM=dumb answers no for.
Dive deeper
Color never changes the bytes. A styled report is the plain report with escapes wrapped around spans of it, so a tool that reads compiler output has nothing to strip and no second format to learn, and a person who pipes a failing build into a file gets the same thing they saw.
That constraint is why NO_COLOR is one-way. The convention gives a user a way to refuse escapes and no way to ask for them, so the variable that forces them on is a separate one, and a terminal that has announced itself as incapable through TERM=dumb is still asked last rather than first.
Diagnostic fields#
Every diagnostic carries severity, message, and range, plus fixes, notes, and related, which are always present and may be empty. The rest appear when there is something to say:
fileandcode, on anything with a source position and a code of its own.help: a concrete repair direction, in one sentence.related: labeled secondary ranges, including ranges in other files.fixes: titled edit sets a tool can apply without reading the prose.notes: context that points at no source, such as the trace classification a JIT trace check attaches to its finding.lint: the lint name, when the code names one.docs: the reference section covering the code, as a path and an anchor.
The language server converts the same data to UTF-16 LSP ranges, relatedInformation, diagnostic data, and code actions, so an editor and a terminal disagree about presentation and about nothing else.
Explaining a code#
nupp explain prints the rule behind a code, a program that reports it, the same program corrected, related codes, and a reference into these documents:
nupp explain NUPP2119Every code with an entry can be looked up this way, which is faster than searching for the number. The diagnostic index below is the same content rendered as a page.
Code families#
The leading digit is the family, and every code falls in one, so a code with no entry of its own can still be resolved this far.
| Codes | Meaning |
|---|---|
NUPP0001 |
Source input could not be read. |
NUPP1001 |
Invalid or unterminated lexical input. |
NUPP1002 |
A required token is missing. |
NUPP1003 |
A required name is missing. |
NUPP1004 |
A required expression is missing. |
NUPP1005 |
Another syntax or recovery constraint failed. |
NUPP1006 |
Typed Nupp syntax appeared in plain Lua. |
NUPP1007 |
A docblock names a parameter that does not exist. |
NUPP1008 |
An annotated Lua type was recovered with reduced precision. |
NUPP2xxx |
Type, declaration, lint, FFI, or ownership diagnostics. |
NUPP3xxx |
Code generation cannot represent a checked construct. |
NUPP4001 |
Formatting could not safely produce the requested result. |
NUPP5xxx |
A development-time change requires a restart. |
OPT-n |
An optimization pass reporting what it did or declined to do. |
OPT-n is the one family that does not describe a problem. A pass reports one to say that it rewrote something, or that it looked at something and could not. The severity is always note, so a remark is reported and stepped over and never fails a build.
Remarks are off unless --remarks is passed, and they come from nupp build and nupp run rather than nupp check, which does not optimize. The code is stable across a pass being renamed, split, or merged, so it can be cited in a bug report or passed to -Zno-opt. See the performance guide for what each pass reports.
Dive deeper
An optimizer is the one part of the compiler whose silence is ambiguous. A pass that declined to inline a call and a pass that was never reached both produce a program with the call still in it, and nothing in the output says which happened. OPT-n is the answer a language owes its user when a declared intention did not reach the generated code.
Giving remarks codes rather than free text is what makes them usable twice. The same identifier that names a remark in a bug report turns the pass off through -Zno-opt, and it survives the pass being renamed or split, so neither use breaks when the optimizer is rearranged.
Codes by area#
A code is explained in full by the diagnostic index below, and in context by the page that owns the rule. This is the map from an area to that page:
| Area | Page | Codes |
|---|---|---|
| Affine types | Affine types | NUPP2606 |
| Ahead-of-time compilation | ahead-of-time.md | NUPP2901, NUPP2902, NUPP2903 |
| Annotations | Annotations | NUPP2108, NUPP2112, NUPP2113, NUPP2119, NUPP2707, NUPP2901, NUPP2902, NUPP2903 |
| Associated types | Associated types | NUPP2127, NUPP2128, NUPP2129, NUPP2134, NUPP2135, NUPP2511 |
| C interop | c-interop.md | NUPP2201, NUPP2402, NUPP2403, NUPP2904 |
| Checked spans | nupp.mem.span |
NUPP2001, NUPP2004, NUPP2602, NUPP2604 |
| Comptime | Comptime | NUPP2410 through NUPP2416, NUPP2419, NUPP2420, NUPP2421 |
| Comptime types | type-level-computation.md | NUPP2001 |
| Derives | nupp.derive | NUPP2810 |
| Effect contracts | Effect contracts | NUPP2112, NUPP2710, NUPP2711 |
| Files | nupp.io.files |
NUPP2701 |
| Formatter | fmt.md | NUPP4001 |
| Generics | Generics | NUPP2003, NUPP2116, NUPP2122 |
| Gradual typing | strictness.md | NUPP1006, NUPP1008, NUPP2105, NUPP2106 |
| Hot reload | Hot reload | NUPP5001 |
| Interfaces | Interfaces | NUPP2116, NUPP2117, NUPP2118, NUPP2136, NUPP3001 |
| Intersections and overloads | Intersections | NUPP2124, NUPP2125, NUPP2126, NUPP2208 |
| Lints | Lints | NUPP2107, NUPP2120, NUPP2501, NUPP2502, NUPP2504 through NUPP2515 |
| Logging | nupp.log |
NUPP2006 |
| LuaJIT trace checking | LuaJIT trace checking | NUPP2502, NUPP2505, NUPP2514, NUPP2515, NUPP2707, NUPP2904 |
| Math | nupp.math |
NUPP2011, NUPP2012 |
| Metamethods | Metamethods | NUPP2003, NUPP2005, NUPP2006, NUPP2007, NUPP2116, NUPP2117, NUPP2118 |
| Modules | Modules | NUPP1002, NUPP2004, NUPP2101, NUPP2105, NUPP2119 |
| Named and plucked arguments | calls.md | NUPP2004, NUPP2006, NUPP2125 |
| Narrowing | Narrowing | NUPP2002, NUPP2109, NUPP2110 |
| Overloads and overrides | Overloads and overrides | NUPP2118, NUPP2125, NUPP2126, NUPP2208 |
| Ownership | ownership.md | NUPP2601, NUPP2602, NUPP2603, NUPP2606 through NUPP2615, NUPP2620 |
| Primitive types | Primitive types | NUPP2001, NUPP2002, NUPP2004, NUPP2006, NUPP2106, NUPP2115 |
| Property capabilities | Property capabilities | NUPP2009, NUPP2118 |
| Records and structs | records.md | NUPP2118, NUPP2201, NUPP2202, NUPP2204, NUPP2205 |
| Reflection | Reflection | NUPP2414, NUPP2415, NUPP2416, NUPP2418 |
| Refinements | Refinements | NUPP2122 |
| Structure-of-arrays storage | Structure-of-arrays storage | NUPP2009, NUPP2403 |
| Suspension | Suspension | NUPP2603, NUPP2701, NUPP2702, NUPP2706 |
| Switch expressions | Switch expressions | NUPP2137 through NUPP2142, NUPP3001 |
| Target capability profiles | Build system | NUPP2904 |
| Type packs | Type packs | NUPP2007, NUPP2010, NUPP2121, NUPP2605 |
| Type system | overview.md | NUPP2001, NUPP2004, NUPP2011, NUPP2012, NUPP2105, NUPP2106 |
| Unions | Unions | NUPP2001, NUPP2107, NUPP2138, NUPP2139, NUPP2140 |
A code appears in several rows when several rules can report it. NUPP2004 is one code for "that field is not there", and which page explains it depends on what was being reached for.
Repairs#
A fix is a title and a set of byte-ranged edits into one file, and applying it is all-or-nothing. Only unambiguous rewrites qualify: where a message lists alternatives, each alternative is its own fix rather than a guess between them.
The checker offers fixes for misspelled variables, type names, fields, methods, and metamethods; missing module qualifications and require statements; declaration visibility; a customary operator replaced with Lua's word; explicit casts for intended lossy narrowing; and the conversion that establishes a fixed-width value.
A diagnostic gives help rather than an edit when the compiler cannot choose a program on the author's behalf. Enum exhaustiveness cannot invent branch bodies, an ambiguous global cannot decide which public declaration should change visibility, and a resource without cleanup metadata cannot guess which function owns that responsibility.
Machine-readable output#
Every command whose result is data rather than a side effect takes --format json, with --json as its shorthand, and every command that takes it also takes --schema, which prints the JSON Schema of what --json writes and exits. The schema is declared beside the code that writes it and a test validates real output against it, so the two cannot drift.
| Command | --json reports |
|---|---|
aot |
what every @aot function in one file lowered to |
ast |
the lossless syntax tree |
bc |
the bytecode of one file, instruction by instruction |
build |
diagnostics, the target, and every path written |
check |
diagnostics, and where the check's time went |
clean |
the paths removed, or that would be |
coverage |
the aggregate coverage summary |
doc |
the resolved format, the output, and every path written |
explain |
a code's rule and worked examples |
export-c |
the header written and the declarations in it |
fixpoint |
whether it reproduced, and why not |
fmt |
unformatted, written, and failed, kept apart |
import-c |
the module written and any warnings |
init |
what the template resolved to and what it wrote |
lints |
every lint, its level here, and its default |
lsp |
per operation; each has its own schema |
ownership-audit |
foreign pointer contracts and unsafe assertion sites |
reference |
the reference, section by section |
tasks |
the task list, or one task's configuration |
test |
totals and a record per test, with file and line |
nupp run is the exception. Its --json writes the --jit-aborts record as JSON instead of CSV, because the program's own output is the run's output.
Read ok before diagnostics. An empty list means the project is clean only when ok is true, since a run that could not use the manifest never reached a file and reports the same empty list.
Agent workflow#
- Run
nupp check --json --strict, and readokfirst. - Apply a complete fix from
diagnostics[].fixeswhen its title matches the intended repair. Never take individual edits out of one. - Read
docson a diagnostic, or runnupp explain <code> --json, when the message alone does not say what the rule is. For the surrounding prose rather than the rule,nupp reference --for <code>prints the sections that cover it, andnupp reference --section <anchor>takes thedocspointer itself. Either is a few hundred words where the chapter is thousands. - Inspect
relatedlocations before changing cross-file declarations or ownership transfers. - Use
nupp lsp inspect,nupp lsp definition, andnupp lsp referenceswhen more semantic context is needed. - Re-run the check after each edit group, then
nupp test --jsonbefore committing, which reports each failing test's name, message, file, and line rather than a wall of progress text. - Read
timing.compiledModulesandtiming.slowestwhen a check of an unchanged project is slower than expected.compiledModules = 0is the authoritative answer that nothing was redone, andslowestranks modules by time actually spent either way, since confirming that a cache entry is still valid costs time too.
FAQ#
How do I stop the checker reporting a lint I do not want?#
Set the lint to off by name or by category in the lints table in nupp.lua, or decorate the one statement with @allow("lint-name"). See Lints for the resolution order, which runs registry default, then category, then name, then the @allow.
Why did a diagnostic give me help and no fix?#
Because more than one program would satisfy the rule and the compiler will not pick one. An unhandled enum member needs a branch body only the author knows, so the message says what is missing and stops. See Repairs for the repairs that are unambiguous enough to be offered as edits.
Why is diagnostics empty when the command still failed?#
The run never reached a file. A manifest that could not be used ends a check before anything is parsed, and that answers with the same empty list a clean project does, which is why ok is a separate field. See Machine-readable output.
Why did @allow not silence an error?#
@allow reaches lints and nothing else, so naming a type error in one leaves the error standing and reports NUPP2108 for the name. See Lints for what counts as a lint.
Diagnostic index#
Every code the compiler reports specifically, with the rule it enforces and, where one is known, the program that reports it beside the same program corrected. nupp explain <code> prints the same thing in a terminal.
A code that resolves only through its family is not here. The families are the leading digit, and each one answers for every code under it.
Input#
NUPP0001#
A source file could not be read.
The path named on the command line, or reached from a require, could not be opened. Check the spelling and that the file is readable from the directory the compiler was run in.
Reference: docs/reference/diagnostics.md#code-families.
Syntax#
NUPP1001#
The lexer could not read a token to completion.
local s = 'oops
return 1A string, long string, long comment, interpolation, or number must close or resolve within the input it is given: a quote must find its matching quote before the line ends, a long bracket must find its matching close, and a numeral must match the shape of a number. A byte that starts none of the lexer's tokens is reported the same way. Recovery marks the malformed span as an error token and resumes lexing from the next line.
local s = 'oops'
return sRelated: NUPP1002, NUPP1005. Reference: docs/reference/diagnostics.md#code-families. Open the reported program in the playground.
NUPP1002#
A required token is missing.
local function f(): integer
if true then
return 1
endA construct was opened and not closed. The position reported is where the closing token was expected, which is the end of the file when nothing closed it at all.
local function f(): integer
if true then
return 1
end
return 0
endRelated: NUPP1004. Reference: docs/reference/diagnostics.md#code-families. Open the reported program in the playground.
NUPP1003#
A required name is missing.
local record Point
x: number
end
local function show(p: Point): number
return p.
end
return showCertain tokens are always followed by a name: '.', 'function', 'for', 'as', '@', 'metamethod', and other positions where a declaration or reference names something. When the next token is not a name, the parser reports it there and inserts a zero-width name placeholder so parsing, and the source round trip, can continue.
local record Point
x: number
end
local function show(p: Point): number
return p.x
end
return showRelated: NUPP1002, NUPP1004. Reference: docs/reference/diagnostics.md#code-families. Open the reported program in the playground.
NUPP1004#
A required expression is missing.
local record Point
x: integer
end
local bare = new Point
return bareAn expression position, whether a parenthesized operand, a call argument, or the whole of a value, must actually contain one; a token that cannot begin an expression is reported there and replaced with a zero-width placeholder. new is held to the same requirement in its own way: it takes a suffixed expression that must end in a call, since a bare new T would be a second spelling of new T().
local record Point
x: integer
end
local made = new Point(x = 1)
return madeRelated: NUPP1002, NUPP1003. Reference: docs/reference/diagnostics.md#code-families. Open the reported program in the playground.
NUPP1005#
Another syntax or recovery constraint failed.
return 1
local x = 2The catch-all syntax code, used for a structural violation that is not a missing token, name, or expression: a return that is not the last statement in its block, a const path that is not static, and similar constraints the parser enforces directly. Each diagnostic's message names the specific rule that failed.
local x = 2
return xRelated: NUPP1002, NUPP1004. Reference: docs/reference/diagnostics.md#code-families. Open the reported program in the playground.
NUPP1006#
The typed layer appears in a plain Lua file.
A .lua file is Lua. The toolchain will require, build and run one unchanged, so typed Nupp syntax cannot be written into it. Rename the file .nupp to mean it, or .g.nupp to keep the gradual layer with typed syntax available. Type comments are separate: recognized LuaCATS, EmmyLua, and typed LuaDoc comments are always imported.
Related: NUPP2105, NUPP2106. Reference: docs/reference/diagnostics.md#code-families.
NUPP1007#
A docblock parameter does not exist.
An @param line documents one parameter of the declaration below it. Spell that parameter's source name exactly, or remove the line; otherwise generated documentation promises an argument the function does not accept.
Related: NUPP2506. Reference: docs/learn/tooling/documentation.md#doc-comments. Open the reported program in the playground.
NUPP1008#
An annotated Lua type lost precision during import.
Type comments in a .lua file are always read. A malformed, ambiguous, unsupported, or ownership-incomplete foreign type recovers at the smallest affected position, usually as any, and checking continues.
Related: NUPP1006, NUPP2101. Reference: docs/learn/projects/integrations/luacats.md#recovery.
NUPP1009#
A type dependency's declaration file was not imported.
A kind = "types" dependency is somebody else's source, pinned at a revision this project chose but did not write. A file in that tree the importer cannot read, parse, or check is skipped and named here; the declarations beside it are still imported, so one unreadable corner of an API does not cost the project the rest of it. Pin a revision this compiler can read, or report the construct that stopped it.
Related: NUPP1008, NUPP1006. Reference: docs/learn/projects/integrations/luacats.md#recovery.
Types, declarations, and rules#
NUPP2001#
A value does not fit the type it is bound to.
An annotated binding accepts only values of that type. The annotation is the claim; the initializer has to keep it.
Related: NUPP2002, NUPP2006. Reference: docs/reference/diagnostics.md#code-families. Open the reported program in the playground.
NUPP2002#
A returned value does not fit the declared result.
local function length(): integer
return "long"
end
return lengthA function's result annotation is a promise to every caller. Each returned value must fit its corresponding result type, and a function that declares several results must return the complete sequence.
local function length(): integer
return 4
end
return lengthRelated: NUPP2001, NUPP2010. Reference: docs/learn/language/types/primitives.md#function-declarations. Open the reported program in the playground.
NUPP2003#
An operator's operand types cannot support what it does.
local lpeg = require("lpeg")
local pattern = lpeg.P("a") ^ "two"
return patternArithmetic, comparison, concatenation, length, and LPeg's pattern algebra each accept a fixed set of operand shapes or a metamethod contract that stands in for one. LPeg's Pattern operators are compiler-provided rather than ordinary metamethods: *, +, and - need operands whose capture shapes can be combined, ^ needs an integer exponent, / needs a string, integer, function, or table transform, and % needs a function accumulator. An operand outside what the operator accepts is reported rather than run to see what happens.
local lpeg = require("lpeg")
local pattern = lpeg.P("a") ^ 2
return patternRelated: NUPP2005, NUPP2006. Reference: docs/reference/diagnostics.md#diagnostic-index. Open the reported program in the playground.
NUPP2004#
The field does not exist on that type.
local record Point
x: number
y: number
end
local function show(p: Point): number
return p.z
end
return showA field read has to name a field the receiver's type actually has. When the name is close to a real one the diagnostic carries a fix that spells it correctly.
local record Point
x: number
y: number
end
local function show(p: Point): number
return p.x
end
return showRelated: NUPP2005, NUPP2119. Reference: docs/reference/diagnostics.md#diagnostic-index. Open the reported program in the playground.
NUPP2005#
A call's callee has no callable type or __call contract.
A call needs a function type, an intersection with at least one function member, or a receiver whose type contracts for __call. any is exempt, since a gradual value's runtime shape is not yet known; anything else that is called is reported once, and its argument expressions are still checked so one mistake does not hide another.
local function callable(): integer
return 5
end
return callable()Related: NUPP2003, NUPP2006. Reference: docs/reference/diagnostics.md#diagnostic-index. Open the reported program in the playground.
NUPP2006#
A call's arguments are not arranged in a way it can be given.
local record Vec3
x: number
y: number
z: number
end
local function draw(x: number, y: number): nil
print(x, y)
end
local function make(): Vec3
return new Vec3(x = 1, y = 2, z = 3)
end
draw({x, y} = make())
return drawNamed and plucked arguments follow every positional argument, so nothing positional may come after one. A plucked operand must be a name or dotted field path: each name it fills reads one field of that path, and confining it to a path is what lets those reads be unordered and the path be evaluated once. Bind a call or a computed index to a local and pluck from the local.
local record Vec3
x: number
y: number
z: number
end
local function draw(x: number, y: number): nil
print(x, y)
end
local function make(): Vec3
return new Vec3(x = 1, y = 2, z = 3)
end
local position = make()
draw({x, y} = position)
return drawRelated: NUPP2004, NUPP2125. Reference: docs/learn/language/named-arguments.md. Open the reported program in the playground.
NUPP2007#
A call passes more arguments than its callee accepts.
A function that takes fewer parameters is usable where more are supplied, because ignoring trailing arguments is ordinary Lua; the reverse is an error unless the callee's parameter pack has a variadic tail to absorb the extras.
Related: NUPP2006, NUPP2010. Reference: docs/learn/language/types/primitives.md#function-declarations. Open the reported program in the playground.
NUPP2008#
A const binding or const module field is assigned to.
The const modifier on a local or a module field is a promise that nothing after its initializer changes it. An ordinary assignment or compound assignment to that name is refused, whether the binding is a local declared const, a builtin the prelude declares constant, or a field a module recorded as const on first assignment.
Related: NUPP2001, NUPP2602. Reference: docs/learn/language/types/primitives.md#const. Open the reported program in the playground.
NUPP2009#
A property view does not grant the requested access.
A read-only property may be read but not assigned, and a write-only property may be assigned but not read. Use a view that declares the needed capability.
Related: NUPP2004, NUPP2008. Reference: docs/learn/language/types/properties.md#access-diagnostics. Open the reported program in the playground.
NUPP2010#
A complete value pack does not fit the required sequence.
Calls, returns, assignments, selection, and coroutine transfers compare complete Lua value sequences. Their fixed slots, optional tails, and correlated alternatives must agree; use parentheses only when intentionally projecting one ordinary value.
local first, second = select(1, 1, 2)
return first, secondRelated: NUPP2002, NUPP2007, NUPP2121. Reference: docs/learn/language/types/packs.md#pack-compatibility. Open the reported program in the playground.
NUPP2011#
A fixed-width value was claimed without being established.
float, int32, and uint32 are checked refinements of Lua numbers. A fitting literal, a checked refined source, a reified load, or the explicit conversion establishes the claim; an erased as does not.
Related: NUPP2001, NUPP2002, NUPP2006, NUPP2012. Reference: nupp.math. Open the reported program in the playground.
NUPP2012#
A physical storage width was used as an ordinary value type.
int8, int16, uint8, and uint16 describe physical layout. Use them for struct fields, C arrays and pointers, cdefs, or span elements; use int32 or uint32 for locals, parameters, results, and record fields.
Related: NUPP2011, NUPP2201, NUPP2203. Reference: docs/learn/language/types/primitives.md#numbers. Open the reported program in the playground.
NUPP2101#
A type name cannot be resolved.
A type name must be a built-in, a declaration visible in this file, or a member reached through its module. Declare the type, require the module that exports it, or correct its spelling.
Related: NUPP2105, NUPP2120. Reference: docs/reference/diagnostics.md#diagnostic-index. Open the reported program in the playground.
NUPP2102#
Two project globals declare the same type name.
A global declaration is reachable project-wide with no require, so globals share one flat namespace across every file. When two files each declare a global type of the same name, an unqualified use of that name cannot know which declaration it means, and both declaring locations are reported. This is inherently a whole-project condition: within the file that declares a name, its own declaration is already in scope directly and this check is not reached, so a single isolated file cannot exhibit it on its own.
Related: NUPP2104, NUPP2101. Reference: docs/reference/diagnostics.md#diagnostic-index.
NUPP2104#
Two project globals declare the same value name.
A global record or global struct declaration puts a value in _G as well as a type in the project's global type namespace. When two files each declare a global of the same name whose value side collides, an unqualified use of that name as a value cannot know which declaration it means, and both declaring locations are reported. Like NUPP2102, this is a whole-project condition: a file that declares the name resolves its own declaration directly and never reaches this check, so a single isolated file cannot exhibit it on its own.
Related: NUPP2102, NUPP2120. Reference: docs/reference/diagnostics.md#diagnostic-index.
NUPP2105#
A name is not any value the checker or project can account for.
local function shout(text: string): string
return txet .. "!"
end
return shoutAn unknown name is gradual by default: it types as any and stays quiet, because it might resolve at runtime. Under --strict, or in any file the strict floor already applies to, an unknown name that is not a project module's basename is reported instead, with a spelling fix offered when a visible name is close enough to be the one meant.
This applies only under --strict, or in a .nupp file.
local function shout(text: string): string
return text .. "!"
end
return shoutRelated: NUPP2120, NUPP2101. Reference: docs/reference/diagnostics.md#diagnostic-index. Open the reported program in the playground.
NUPP2106#
An exported declaration needs a type annotation.
local m = {}
function m.double(n)
return n * 2
end
return mWhat a module returns is its interface, and an interface is written down rather than inferred. A function attached to the module table needs its parameter and result types. This is a strict rule: it is reported only under --strict, or when the manifest sets strict = true.
This applies only under --strict, or in a .nupp file.
local m = {}
function m.double(n: integer): integer
return n * 2
end
return mRelated: NUPP2119. Reference: docs/reference/diagnostics.md#diagnostic-index. Open the reported program in the playground.
NUPP2107#
A dispatch leaves members of a closed set unhandled.
local type Color = 'red' | 'green' | 'blue'
local function name(c: Color): string
if c == 'red' then return "red" end
return "other"
end
return nameWhen every branch returns, the dispatch is exhaustive or it is not, and the checker can tell which. Add the missing branches, or an else that says the rest are deliberately alike.
local type Color = 'red' | 'green' | 'blue'
local function name(c: Color): string
if c == 'red' then
return "red"
elseif c == 'green' then
return "green"
else
return "blue"
end
end
return nameReported by the exhaustiveness lint, category correctness, at warning by default. Configure it by name or by category.
Related: NUPP2004. Reference: docs/reference/lints.md. Open the reported program in the playground.
NUPP2108#
A type error is named as though it were a lint.
@allow suppresses lint judgements, by lint name or code. A type error says the program cannot keep its own type claims, so it cannot be configured or silenced; correct that error instead.
Related: NUPP2001, NUPP2112. Reference: docs/reference/lints.md#local-suppressions. Open the reported program in the playground.
NUPP2109#
A name written to select a parameter or captured value resolves to nothing.
local record R
x: integer
end
local function f(a: R): b is R
return true
end
return fSeveral forms name a parameter or a value already in scope rather than introduce one: a predicate's x is T result names the tested parameter, and a closure's takes (...) or borrows (...) clause names values it captures. Each such name must resolve to an actual parameter of the enclosing function, or an actual value visible where the closure is written; a name that answers to nothing there is reported rather than silently ignored.
local record R
x: integer
end
local function f(a: R): a is R
return true
end
return fRelated: NUPP2110. Reference: docs/learn/language/types/narrowing.md#predicate-functions. Open the reported program in the playground.
NUPP2110#
A predicate's tested parameter could never hold the narrowed type.
local record R
x: integer
end
local function f(a: string): a is R
return true
end
return fA predicate's return type param is T is trusted rather than verified for what the body proves, but the checker still confirms the shape is coherent: the named parameter's declared type must be able to hold T. When the two types are disjoint the predicate can never truthfully hold, so it is refused rather than compiled as narrowing that could never fire.
local record R
x: integer
end
local function f(a: R): a is R
return true
end
return fRelated: NUPP2109. Reference: docs/learn/language/types/narrowing.md#predicate-functions. Open the reported program in the playground.
NUPP2111#
An annotation name does not resolve to a definition.
local function f(): nil
end
return fAn @name application resolves name against a definition: a compiler built-in, or a project declaration reached with @annotation. Applying a name nothing defines, or one a project's own resolution reports an error for, is refused rather than treated as an inert comment.
local function f(): nil
end
return fRelated: NUPP2112, NUPP2114. Reference: docs/reference/annotations.md#defining-an-annotation. Open the reported program in the playground.
NUPP2112#
An annotation is applied where its definition forbids it, or with arguments outside its declared shape.
local x = 1
return xA definition's targets set is the complete list of constructs an application may decorate, and its argument policy (none, names, effects, warnings, typed) is the complete shape its arguments may take. An application outside either is refused rather than silently accepted.
local function f(): nil
end
return fRelated: NUPP2111, NUPP2115. Reference: docs/reference/annotations.md#attachment-targets. Open the reported program in the playground.
NUPP2113#
A reserved built-in annotation is applied before its behavior is implemented.
A built-in definition may carry a reservation naming the feature it is waiting on, ahead of the feature existing. Applying that name still parses and resolves, since the definition is real, but is refused with what it is reserved for rather than silently accepted and doing nothing. No shipped built-in currently carries a reservation -- the mechanism exists for a name registered ahead of its implementation, as @jit itself once was.
Related: NUPP2111, NUPP2112. Reference: docs/reference/diagnostics.md#diagnostic-index.
NUPP2114#
@annotationValue or @ref is used outside an annotation-definition field, or a definition's own shape is invalid.
local record Plain
x: string
end@annotationValue and @ref are meaningful only on a field inside a record or struct itself marked @annotation, and @annotation takes exactly one named targets argument: a non-empty array of target-name string literals. Any of these written where that context is missing describes a definition that cannot exist.
(targets = {"record"})
local record Plain
x: string
endRelated: NUPP2115, NUPP2112. Reference: docs/reference/annotations.md#single-value-applications. Open the reported program in the playground.
NUPP2115#
A typed annotation's argument does not fit its member, or is not a compile-time constant.
A typed-argument annotation checks each supplied argument against the member it names: the value must fit the member's declared type, a @ref member must resolve to a compatible type reference, no member may be supplied twice, and every required member must be supplied. Only the member marked @annotationValue may be supplied positionally.
Related: NUPP2114, NUPP2112. Reference: docs/reference/annotations.md#built-in-annotations. Open the reported program in the playground.
NUPP2116#
A type argument, or an associated-type answer, does not fit its declared bound.
A T is Bound constraint, wherever it is written -- a generic type parameter or an associated type's own bound -- is checked where the argument is supplied: at a generic's instantiation, or at the default or answer that fills the associated type. An any argument skips the check; any other concrete type must actually satisfy the bound.
Related: NUPP2117, NUPP2003. Reference: docs/learn/language/types/generics.md#constraints-use-is. Open the reported program in the playground.
NUPP2117#
A declaration names something other than an interface after is.
local record Other
end
local record Bad is Other
end
return BadOnly an interface may appear after is on a record or struct declaration, because that is the only declaration kind able to state a contract for another to inherit. A record, struct, alias, or any other named value is refused there, even when its shape happens to match.
local interface Other
end
local record Bad is Other
end
return BadRelated: NUPP2116, NUPP2136. Reference: docs/learn/language/types/interfaces.md#is-is-a-claim-not-a-proof. Open the reported program in the playground.
NUPP2118#
A declaration contains an invalid or conflicting member.
local record Pair
first: string
first: integer
end
return PairA declaration has one member of a given name and capability. Duplicate fields, methods with the same parameter pack, conflicting static and instance members, and unknown metamethods cannot describe one coherent contract.
local record Pair
first: string
second: integer
end
return PairRelated: NUPP2004, NUPP2123. Reference: docs/reference/diagnostics.md#diagnostic-index. Open the reported program in the playground.
NUPP2119#
A declaration does not say where it lives.
record Loose
id: integer
end
return LooseA declaration is file-local (local), a member of a table (record m.R), or a project global (global). Plain Lua would have made it a global silently; Nupp asks instead, because a name that means one thing here and another elsewhere is worth one word to prevent.
local record Loose
id: integer
end
return LooseRelated: NUPP2106, NUPP2120. Reference: docs/reference/diagnostics.md#diagnostic-index. Open the reported program in the playground.
NUPP2120#
A project module is used without being required.
local answer: number = mathutil.double(21)
return answerA project file does not put its basename into every other file's scope. Bind the module with require before reading its members; the diagnostic names the require call that makes the program valid.
local mathutil = require("mathutil")
local answer: number = mathutil.double(21)
return answerReported by the missing-require lint, category correctness, at error by default. Configure it by name or by category.
Related: NUPP2101, NUPP2105. Reference: docs/learn/language/modules.md#naming-a-member-from-another-file. Open the reported program in the playground.
NUPP2121#
A type pack is used where only one value type can appear.
A pack binder must follow every ordinary type binder, and a pack may appear only in a sequence position: function parameters or results, a coroutine protocol, a pack-union arm, or an explicit pack argument.
local function pair(): (number, string)
return 1, "one"
end
return pairRelated: NUPP2010. Reference: docs/learn/language/types/packs.md#syntax. Open the reported program in the playground.
NUPP2122#
A refinement cannot be enforced.
local interface Circle
kind: string
satisfies |self| -> tostring(self.kind) == "circle"
end
return CircleA satisfies declaration names the runtime test that decides whether a value is one of these, and x is T compiles to it. That test has to run wherever is is written, so it reads the declaration's own fields through self and nothing else: comparisons against literals, type() tests, and, or, not. A call, arithmetic, or a name from outside the subject cannot be evaluated there. A refinement that always answers the same way is also refused, since always true identifies every value and always false leaves the type uninhabited, and so is one on a struct, which ffi.istype already answers exactly.
A declaration is also held to the refinements of the interfaces it declares. record C is Shape is a claim the checker proves, and Shape's refinement is what is Shape runs, so fields that make that test fail would leave the two disagreeing about the same value. Only a provable failure is reported: a refinement whose answer no declaration settles is not evidence of a mistake.
local interface Circle
kind: string
satisfies |self| -> self.kind == "circle"
end
return CircleRelated: NUPP2116. Reference: docs/learn/language/types/refinements.md. Open the reported program in the playground.
NUPP2123#
A metatable value does not fit the key it is written under.
local record I64
v: integer
__add: function(self: I64, other: I64): I64
end
local x = new I64(v = 1)
setmetatable(x, {__add = "not a function"})
return xA metamethod declaration is a contract, and a metatable literal is where the value fulfilling it is written, so the value is held to it, with self specialized to the receiver, through a bounded type parameter as readily as through a concrete type. Where the declaration contracts for nothing, LuaJIT still says what it will do with the key: __mode is read as a string, __index and __newindex are a table to defer to or a function to run, and everything else it knows is called.
Only a literal is checked. What a function returns cannot be seen from here, so a computed metatable stays gradual.
local record I64
v: integer
__add: function(self: I64, other: I64): I64
end
local x = new I64(v = 1)
setmetatable(x, {__add = function(a: I64, b: I64): I64
return new I64(v = a.v + b.v)
end})
return xRelated: NUPP2118, NUPP2006. Reference: docs/learn/language/metamethods.md. Open the reported program in the playground.
NUPP2124#
An intersection is provably uninhabited.
local type Impossible = string & number
return ImpossibleA value of A & B must satisfy both contracts. Nupp reports only finite proofs of impossibility, such as distinct runtime categories, literal tags, concrete nominal identities, or incompatible required fields; an intersection it cannot disprove remains legal.
Related: NUPP2125, NUPP2126. Reference: docs/learn/language/types/intersections.md#provable-emptiness. Open the reported program in the playground.
NUPP2125#
No overload accepts a call.
local type F = function(integer): string & function(string): boolean
local f: F = nil as any
return f(true)A callable intersection probes every function member against the same adjusted argument pack. This diagnostic lists why each candidate was rejected; no candidate's ownership or borrowing effects are applied.
local type F = function(integer): string & function(string): boolean
local f: F = nil as any
return f(1)Related: NUPP2006, NUPP2126. Reference: docs/learn/language/types/intersections.md#overload-selection. Open the reported program in the playground.
NUPP2126#
Several overloads accept a call.
local type F = function(integer): string & function(number): boolean
local f: F = nil as any
return f(1)Overloads have no ranking or declaration-order tie breaker. A call must leave exactly one candidate; use distinguishable parameter packs or narrow a gradual argument before calling.
local type F = function(integer): string & function(string): boolean
local f: F = nil as any
return f(1)Related: NUPP2125, NUPP2208. Reference: docs/learn/language/types/intersections.md#overload-selection. Open the reported program in the playground.
NUPP2127#
A declaration does not answer an associated type it is owed.
An interface may state a type it does not name, and whatever takes that contract has to name it. A default on the interface answers it, so only a requirement nothing named is reported -- as is one that several contracts default differently, or that an implementor answers otherwise than a == equality fixes, since neither leaves one answer to take.
Related: NUPP2128, NUPP2116. Reference: docs/learn/language/types/associated-types.md. Open the reported program in the playground.
NUPP2128#
An associated type member cannot mean anything where it is written.
local record Box
associated type Item
end
return BoxAn interface states a requirement, and = on one states a default. Anything else answers, so it needs the =, may not restate the bound -- what may answer is the contract's to say -- and may not use ==, which fixes a type on the contract that states it. Answering a name no contract declares means the name is private, which a plain nested type alias already is.
local record Box
type Item = string
end
return BoxRelated: NUPP2127, NUPP2117. Reference: docs/learn/language/types/associated-types.md. Open the reported program in the playground.
NUPP2129#
An associated type collides with another type member.
local interface Reader
type Item = string
associated type Item
end
return ReaderA declaration has one type namespace. An associated type and a nested alias or nested declaration share it, so two of a name is one namespace saying two things. Fields are a separate namespace and may share the spelling.
local interface Reader
type Unit = string
associated type Item
end
return ReaderRelated: NUPP2128. Reference: docs/learn/language/types/associated-types.md. Open the reported program in the playground.
NUPP2130#
A type-level reduction cannot resolve the operator it is given.
keyof, T.[K], writeof, mapped shapes, and template construction each reduce a neutral type-level term to a concrete type. A term whose operand is not the shape the operator needs -- chief among them a member projection naming a key the subject does not have -- fails to reduce, and is reported where it was written rather than left as an opaque type.
Related: NUPP2132, NUPP2133. Reference: docs/learn/language/types/comptime-types.md#direct-finite-operators. Open the reported program in the playground.
NUPP2131#
A const type parameter cannot be given the value it needs.
local function same<const S: string>(left: S, right: S): nil
end
same('x', 'y')A const type parameter's value must be inferable from a use that names it directly -- an argument, or another const parameter already bound to the same one -- and every use inferring the same parameter must agree on exactly the same value. A parameter nothing binds, two arguments that disagree, or a value outside the parameter's declared const domain are each refused.
local function same<const S: string>(left: S, right: S): nil
end
same('x', 'x')Related: NUPP2101, NUPP2130. Reference: docs/learn/language/types/comptime-types.md#const-parameters. Open the reported program in the playground.
NUPP2132#
A template type's hole cannot reduce to a finite string literal.
A backtick template type's ${...} holes must each reduce to a finite union of string or numeric literals, so the product of every combination is itself finite. A hole naming an unbounded type such as string, or one that is not a valid compile-time constant expression at all, cannot be enumerated and is refused.
Related: NUPP2130, NUPP2133. Reference: docs/learn/language/types/comptime-types.md#direct-finite-operators. Open the reported program in the playground.
NUPP2133#
A recursive type alias is not supported.
local type Loop<T> = Loop<T>
return LoopType aliases describe finite type expressions and cannot refer to themselves, directly or mutually. Write an ordinary recursive comptime function that returns type or typepack when the computation needs control flow.
Related: NUPP2130, NUPP2132. Reference: docs/learn/language/types/comptime-types.md#closed-and-generic-calls. Open the reported program in the playground.
NUPP2134#
A projection names something that cannot be projected.
local function first<T>(x: T): T.Item
return nil as any
end
return firstT.Item is the associated type Item as whatever T answers it with, so it is a type only where some contract states that name. An unbounded binder has no contract to project through, a bounded one may not state the name, and a union states it only when every alternative does. A projection takes no type arguments.
Related: NUPP2127. Reference: docs/learn/language/types/associated-types.md. Open the reported program in the playground.
NUPP2135#
An associated type answers through itself.
local interface Holds
associated type Value
end
local record Direct is Holds
associated type Value = Direct.Value
end
return DirectAn answer has to be a type that does not reach the member it answers. The graph is read once every answer and default is installed, so naming a member declared later in the body is ordinary and only a closed loop is reported. A cyclic default stays latent on the interface that states it and surfaces on the first concrete implementor.
local interface Holds
associated type Value
end
local record Direct is Holds
associated type Value = string
end
return DirectRelated: NUPP2127. Reference: docs/learn/language/types/associated-types.md. Open the reported program in the playground.
NUPP2136#
A sealed interface is implemented outside its owning module.
local spans = require("nupp.mem.span")
local record Forged is spans.Span<int32>
end
return ForgedA sealed interface carries hidden invariants that its public shape cannot prove. Only a declaration in the interface's own module may explicitly name it after is; structural lookalikes do not satisfy it. Obtain the value from that module's constructors instead of declaring another implementation.
local spans = require("nupp.mem.span")
local storage = ffi.new<int32[4]>()
local view = spans.fromFixedCarray(storage, 4)
return #viewRelated: NUPP2001, NUPP2117. Reference: docs/learn/language/types/interfaces.md#sealed-interfaces. Open the reported program in the playground.
NUPP2137#
A switch pattern is not static or cannot bind what it names.
Static switch cases are finite scalar literals or names whose checked type is one exact scalar. Type destructuring binds direct fields only, and every bound name and field must exist once in that arm.
Related: NUPP2138, NUPP2140. Reference: docs/learn/language/switch-expressions.md#switch-expressions. Open the reported program in the playground.
NUPP2138#
A static switch value is handled more than once.
Static cases compare Lua values, not source spellings. Numerically equivalent literals such as 1 and 1.0 are therefore duplicates.
Related: NUPP2137, NUPP2139. Reference: docs/learn/language/switch-expressions.md#switch-expressions. Open the reported program in the playground.
NUPP2139#
A switch case cannot be selected.
Cases are checked in source order against the selector type left by earlier cases. A value outside that type and an arm reached after the residue is empty are unreachable.
Related: NUPP2138, NUPP2140. Reference: docs/learn/language/switch-expressions.md#switch-expressions. Open the reported program in the playground.
NUPP2140#
A switch leaves possible selector values unhandled.
A switch is an expression and must produce a value whenever its selector returns. Cover every member of a finite checked type, or add else when the remaining set is open. A union declared with nupp.types.nonExhaustive() is always open, so it always needs else.
Related: NUPP2139, NUPP2141. Reference: docs/learn/language/switch-expressions.md#switch-expressions. Open the reported program in the playground.
NUPP2141#
A block switch arm does not yield on every completing path.
A block arm produces the switch value with contextual yield value. Every path that does not return from the enclosing function must reach one, and no statement may follow it in the same path.
Related: NUPP2140, NUPP2142. Reference: docs/learn/language/switch-expressions.md#switch-expressions. Open the reported program in the playground.
NUPP2142#
A switch is in a conditionally evaluated expression position.
The first lexical lowering lifts switches from statement-root eager expressions. Lifting one out of and, or, ??, a ternary arm, or guarded safe-navigation work would run it when the source does not, so that placement waits for general expression normalization. Bind the switch in a preceding statement instead.
Related: NUPP2140. Reference: docs/learn/language/switch-expressions.md#switch-expressions.
NUPP2143#
An export carrying a runtime value sits beside export =.
module geom.shapes
local shapes = {}
export record Point
x: number
end
export = shapesA module either exports its declarations, and the compiler builds the table, or it names its value with export =, and that value is the module. A declaration that carries a runtime value cannot do both: there is no table of the compiler's for it to land on, and the named value is the author's to fill. Put it on that value instead. An interface or type alias is erased and stays legal beside one, because it contributes a type and no value.
module geom.shapes
local shapes = {}
record shapes.Point
x: number
end
export = shapesRelated: NUPP2119. Reference: docs/learn/language/modules.md#migrating-a-table-shaped-module. Open the reported program in the playground.
NUPP2144#
An internal module is imported outside its package namespace.
A leading @!internal or an internal module segment restricts static imports to modules with the same first canonical namespace segment. An internal init.nupp restricts its descendants too. Import the package's public module instead. This applies to require, qualified values and qualified types; dynamic Lua loading remains gradual and is not an access-control sandbox.
Reference: docs/learn/language/modules.md#internal-modules.
NUPP2201#
A struct field has no C-representable layout.
local struct Loop
me: Loop
n: int32
end
return LoopA struct is laid out in C memory, so every field needs a fixed size and offset. A field type has to be reifiable: a numeric primitive, another struct held by value, a pointer, or a fixed C array. A struct cannot hold itself by value, because the layout would then have to contain a copy of itself and so would have no size; hold self-reference through a pointer instead.
local struct Loop
me: Loop*?
n: int32
end
return LoopRelated: NUPP2204, NUPP2205, NUPP2402. Reference: docs/learn/language/types/records-and-structs.md#pointing-at-itself. Open the reported program in the playground.
NUPP2202#
A declaration is built with 'new'.
local record Point
x: integer
end
local p = Point{x = 1}
return pRecords and structs are constructed with new. Calling a declaration used to be the construction, which meant the call was never the program's own: a record's __call was shadowed whenever the argument was a table, and a struct's ctype call was claimed outright. Saying construction in the source gives those hooks back. A record that does declare a __call contract is called, not reported.
local record Point
x: integer
end
local p = new Point(x = 1)
return pRelated: NUPP2206. Reference: docs/learn/language/types/records-and-structs.md#records. Open the reported program in the playground.
NUPP2203#
A C declaration uses a type C cannot represent.
cdef function process(values: {number}): int32
return processA cdef signature and struct layout cross the C ABI, so their types must have a C representation. Lua tables, records, and other garbage-collected values need an explicit pointer or handle boundary.
cdef function process(values: voidptr): int32
return processRelated: NUPP2201, NUPP2602. Reference: docs/learn/runtime/c-interop/index.md#hand-write-a-small-binding. Open the reported program in the playground.
NUPP2204#
A struct declares a Lua array part.
local struct P
{integer}
x: float
end
return PThe array part {T} makes a record's own instances behave as a one-based Lua sequence, which only makes sense for a table-backed value. A struct has no Lua table underneath it, so it cannot declare one; use a fixed C array field T[N] for an inline vector instead.
local struct P
x: float[4]
end
return PRelated: NUPP2201, NUPP2205. Reference: docs/reference/diagnostics.md#diagnostic-index. Open the reported program in the playground.
NUPP2205#
An array part is not written as a single element type.
local record R
{x: integer}
y: integer
end
return RThe array part is spelled {T}: braces around exactly the element type. Anything else inside the braces, such as a shape with named fields, resolves to a different kind of type and is not a valid array part.
local record R
{integer}
y: integer
end
return RRelated: NUPP2204, NUPP2201. Reference: docs/reference/diagnostics.md#diagnostic-index. Open the reported program in the playground.
NUPP2206#
Only a record or a struct can be constructed.
new names a type and builds a value of it, so the operand has to be a declaration with something to build. An interface declares a contract and has no runtime table to stamp; an enum value is one of its declared strings, written directly. The operand is answered as a type rather than through whatever value stands under the name, because an interface binds none.
Related: NUPP2202. Reference: docs/learn/language/types/records-and-structs.md#records. Open the reported program in the playground.
NUPP2207#
A binding is read before it holds a value.
local record Point
x: integer
end
local p: Point
return p.xlocal v: Vec2 used to construct one where it was declared, which was a construction the source did not say. It no longer does, so the binding holds nil until something assigns to it, and reading it before that indexes nil at run time rather than yielding a value of the declared type. Strict code holds every annotation that does not admit nil to this, and a binding is assigned only once every path reaching the read assigned it: an assignment on one arm of an if, or inside a loop that may not run, is not one. Assign it first, or declare it optional if it is meant to start empty. A declaration file states what exists elsewhere and assigns nothing, so it is exempt.
local record Point
x: integer
end
local p: Point = new Point(x = 0)
return p.xRelated: NUPP2202, NUPP2206. Reference: docs/learn/language/types/records-and-structs.md#records. Open the reported program in the playground.
NUPP2208#
A constructor does not hold up its declaration.
A constructor(self, ...) body is what new T(...) runs. The instance is made before it and returned after it, so its whole job is to fill the fields in, and every field that cannot hold nil has to be filled, or the value handed back does not match the declaration it claims. That guarantee is the reason to prefer a constructor over a literal, so declaring one closes the literal form for that declaration. An interface builds nothing and cannot carry one. Constructors may be overloaded by parameter pack. A result annotation is the successful construction policy: it must be one value that erases to the record itself, such as affine(T, T.destroy), because the body still returns the instance it allocated.
Related: NUPP2202, NUPP2207. Reference: docs/learn/language/types/records-and-structs.md#constructors-and-result-policies. Open the reported program in the playground.
NUPP2209#
A private record field is used outside its module.
local h = require('nupp.mem.heap')
local a = h.allocate(ffi.typeof<int32>(), 1)
return a.pointerA private field is representation available only while checking the canonical module that declares its record. Expose a public method when another module needs an operation without revealing that representation.
local h = require('nupp.mem.heap')
local a = h.allocate(ffi.typeof<int32>(), 1)
return a:read()[1]Related: NUPP2004, NUPP2202. Reference: docs/learn/language/types/records-and-structs.md#private-fields. Open the reported program in the playground.
NUPP2301#
cheader's header path is not a literal.
local mini = cheader("mini" .. ".h")
return minicheader reads the named header at compile time, so its path has to be known to the compiler before the program runs. It accepts only a literal string as its first argument, never a computed or run-time expression.
local mini = cheader("mini.h")
return miniRelated: NUPP2302. Reference: docs/learn/runtime/c-interop/index.md#type-the-header-in-place. Open the reported program in the playground.
NUPP2302#
A cheader path cannot be read.
local mini = cheader("missing.h")
return minicheader resolves its literal path relative to the file, then as written, then against each project root, and reads whichever candidate exists first. When none can be opened, the diagnostic reports why, the same way an unreadable source path is reported.
Related: NUPP2301, NUPP0001. Reference: docs/learn/runtime/c-interop/index.md#type-the-header-in-place. Open the reported program in the playground.
NUPP2303#
A literal ffi.cdef block does not parse as C.
local ffi = require("ffi")
ffi.cdef[[ this is not C ]]
return 0A literal string given directly to ffi.cdef declares to the checker as well as to LuaJIT at runtime, so it has to be declarations LuaJIT's own C parser accepts. A string built at runtime, or reached only through a guard, is not read this way and so cannot be checked.
local ffi = require("ffi")
ffi.cdef[[ int mini_add(int a, int b); ]]
return 0Related: NUPP2301, NUPP2304. Reference: docs/learn/runtime/c-interop/index.md#hand-write-a-small-binding. Open the reported program in the playground.
NUPP2304#
A typed FFI operation's type string does not name a C type.
local ffi = require("ffi")
ffi.cdef[[ struct CstA { int x; }; ]]
local p = ffi.new("struct CstNoSuch")
return pffi.new, ffi.cast, ffi.typeof, ffi.istype, ffi.sizeof, and ffi.alignof read a literal string argument through LuaJIT's own type-string parser when one is given, so that string has to name a type already declared, through a cdef or cheader the checker has already seen.
local ffi = require("ffi")
ffi.cdef[[ struct CstA { int x; }; ]]
local p = ffi.new("struct CstA")
return pRelated: NUPP2303, NUPP2203. Reference: docs/learn/runtime/c-interop/index.md#typed-ffi-operations. Open the reported program in the playground.
NUPP2401#
carray's arguments are not a struct type and an element count.
local record Point
x: number
y: number
end
local points = carray(Point, 4)
return pointscarray(T, n) allocates a zero-based C array of n elements of T. T must resolve to a reified struct type -- a record is a table with no C layout to array up -- and n must be a number, or the gradual any, usable as an element count.
Related: NUPP2402, NUPP2201. Reference: docs/learn/runtime/c-interop/index.md#typed-ffi-operations. Open the reported program in the playground.
NUPP2402#
layoutof was asked about something with no C layout.
local record Point
x: number
y: number
end
return layoutof(Point)layoutof(T) answers how a reified struct sits in memory: its fields in declaration order with their offsets, sizes and padding, the whole size, and a fingerprint over all of it. Only a struct has one -- a record is a table, not C memory -- and a struct with no fields has nothing to lay out.
Related: NUPP2401, NUPP2201. Reference: docs/learn/runtime/c-interop/index.md#read-a-structs-layout. Open the reported program in the playground.
NUPP2403#
An SoA allocation or field projection has no resolved stored field.
local soa = require("nupp.mem.soa")
local ffi = require("ffi")
local rows = soa.allocate(ffi.typeof<int32>(), 4)
return rowsStructure-of-arrays storage accepts only a reified struct whose top-level fields have fixed C storage; a record, union, interface, GC-managed field, owned field, borrowed field, or variable-size field is not SoA-eligible. A row's field projection must likewise name one such stored field of that struct, not a dynamic name or an unknown one.
Related: NUPP2402, NUPP2009. Reference: docs/reference/diagnostics.md#diagnostic-index. Open the reported program in the playground.
NUPP2410#
A comptime block reads or writes a binding it cannot reach.
A comptime block evaluates in a scope of its own: its own locals and parameters, comptime-declared functions, and the allowlisted compile-time environment. A runtime local, upvalue, module-level binding, global, or an ambient library the environment does not expose -- io, os, require, debug among them -- cannot be read, and the same set cannot be assigned.
Related: NUPP2411, NUPP2412. Reference: docs/reference/diagnostics.md#diagnostic-index. Open the reported program in the playground.
NUPP2411#
A construct or library member is unavailable at comptime.
The comptime evaluator supports a fixed subset of Nupp -- literals, control flow, operators over ordinary values, and calls into an explicit allowlist of library members -- and names what falls outside it: a nested function declaration, a multi-value result, a logical operator observing an opaque value, or a library member the allowlist leaves out because it is nondeterministic, such as math.random.
Related: NUPP2410, NUPP2412. Reference: docs/reference/diagnostics.md#diagnostic-index. Open the reported program in the playground.
NUPP2412#
A comptime evaluation exceeded a bound or used a value it cannot.
Comptime evaluation is bounded so a block always finishes, and defined only over values its operations actually support: a step count, a helper call's frame depth, and an allocation size are each checked against a fixed limit, a comptime function must return and must be called with the argument count its parameters declare, and indexing, calling, or printing a value in a way its runtime kind does not support fails the same way.
Related: NUPP2410, NUPP2411. Reference: docs/reference/diagnostics.md#diagnostic-index. Open the reported program in the playground.
NUPP2413#
A comptime result has no literal spelling.
A block's result is quoted into the generated Lua as a literal: nil, booleans, numbers that read back unchanged, strings, and acyclic tables of those with no metatable. NaN, the infinities, functions, and cdata have no such spelling, and a table reachable by more than one path is refused rather than quoted twice into two tables where the block built one.
Related: NUPP2414, NUPP2416. Reference: docs/learn/language/comptime.md#values-checked-at-their-destination. Open the reported program in the playground.
NUPP2414#
An opaque comptime result did not reach a materialization boundary.
local record Position
x: number
y: number
end
const codec = do
return nupp.reflect.fieldCodec(nupp.reflect(Position))
end
print(codec)A sealed provider -- reflection's field-codec builder among them -- may return a description with no literal spelling. It materializes only when the comptime block directly initializes a declaration whose annotated type the provider owns; an inferred binding, or an opaque value nested inside an ordinary table, gives it nowhere to materialize into.
local record Position
x: number
y: number
end
const codec: nupp.reflect.FieldCodec<Position> = do
return nupp.reflect.fieldCodec(nupp.reflect(Position))
end
print(codec)Related: NUPP2415, NUPP2416. Reference: docs/learn/language/reflection.md#comptime-reflection. Open the reported program in the playground.
NUPP2415#
A comptime-only value failed validation where it landed.
local function answer(): integer return 42 end
local escaped = answer
return escaped()A comptime function's own identity, a type or const-function argument crossing into the isolated comptime worker, and an opaque provider result reaching a materialization boundary are all checked against exactly where they land. A comptime function read as an ordinary runtime value has nowhere to run there, an argument that cannot import into the worker cannot be evaluated, and a declared type with no registered materialization relation for what a provider produced cannot receive it.
local function answer(): integer return 42 end
return do return answer() endRelated: NUPP2414, NUPP2416. Reference: docs/reference/diagnostics.md#diagnostic-index. Open the reported program in the playground.
NUPP2416#
A comptime result or worker message exceeds its bound.
return do
local values = {}
for index = 1, 12000 do values[index] = index end
return values
endComptime evaluation runs in an isolated worker with fixed step, memory, and result-graph limits, and talks to the compiler over a bounded protocol. A finalized value graph over the item limit, and a worker request or response over the protocol byte limit, are reported here instead of being silently truncated or left to crash the worker.
return do
local values = {}
for index = 1, 10 do values[index] = index end
return values
endRelated: NUPP2415, NUPP2412. Reference: docs/reference/diagnostics.md#diagnostic-index. Open the reported program in the playground.
NUPP2417#
A PEG intrinsic call or finalized grammar is malformed.
Every nupp.peg intrinsic is checked against the pattern arguments it needs, and a finalized grammar is checked against PEG's own rules once assembled. Repetition is possessive and must consume at least one byte whenever it succeeds, so a nullable repetition such as ('')* is refused, and direct or indirect left recursion is refused because a top-down parser cannot re-enter a rule at the position it started.
Related: NUPP2415, NUPP2416. Reference: nupp.peg. Open the reported program in the playground.
NUPP2418#
nupp.reflect was applied to something other than a resolvable type.
nupp.reflect(T) resolves T in type position; it does not accept an expression that merely holds a runtime value, or one that fails to name a type at all. nupp.reflect.fieldCodec further requires that descriptor to describe a record, since only a record has present declared fields for the codec to copy.
local record Item
value: integer
end
const Good: nupp.reflect.FieldCodec<Item> = do
return nupp.reflect.fieldCodec(nupp.reflect(Item))
end
return GoodRelated: NUPP2415, NUPP2416. Reference: docs/reference/diagnostics.md#diagnostic-index. Open the reported program in the playground.
NUPP2419#
A layout intrinsic has no target, a bad shape, or an unknown field.
local struct Value
n: int32
end
return do return nupp.sizeof(Value) endnupp.sizeof, nupp.alignof, and nupp.offsetof run only inside a comptime block, against the actual nupp global, with one concrete type argument (plus a compile-time-known field name for offsetof). Once the call's shape is right, it still needs the selected build's layoutTarget configured and a type with a runtime C layout to measure; a record or an unmodeled type has none. A program that only needs the host's own layout should measure it at runtime with ffi.sizeof instead.
local struct Value
n: int32
end
local size = ffi.sizeof<Value>()
return sizeRelated: NUPP2410, NUPP2416. Reference: docs/reference/diagnostics.md#diagnostic-index. Open the reported program in the playground.
NUPP2420#
A comptime type function rejected its own application.
nupp.types.error(message) lets a comptime type function refuse a specific application deliberately, distinct from an evaluator crash or a result of the wrong kind. The authored message is reported at the application together with the bounded comptime call trace, so a computed type contract can say why an argument it inspected does not qualify.
Related: NUPP2415, NUPP2421. Reference: docs/learn/language/types/comptime-types.md#inspection-and-construction. Open the reported program in the playground.
NUPP2421#
A compiler-only member or type-position call was used where it cannot run.
Type handles, comptime type aliases, and members typed comptime type or comptime function(...) are opaque and exist only for the comptime evaluator. Reading one into an ordinary runtime binding, naming a comptime alias in a runtime annotation, or writing a type-position call whose callee, argument, or result does not resolve to a checked comptime function signature is refused because none has anything to erase to at runtime.
Related: NUPP2133, NUPP2420. Reference: docs/learn/language/types/comptime-types.md#inspection-and-construction. Open the reported program in the playground.
NUPP2501#
A cast to a C pointer type has no lifetime root to hold the string alive.
The string-pointer lint reports a ffi.cast<T>(value) to a pointer type whose argument is a Lua string but is not itself a named binding or a preserved parameter: the pointer aliases the string's bytes, and a fresh string is scratch storage LuaJIT is free to collect once the expression that produced it goes unread. Bind the string to a local, or thread it through a preserving parameter, before casting a pointer out of it. Default level is warning, and a project may reconfigure or suppress it like any other lint.
Reported by the string-pointer lint, category suspicious, at warning by default. Configure it by name or by category.
Related: NUPP2608, NUPP2603. Reference: docs/reference/lints.md#string-pointer. Open the reported program in the playground.
NUPP2502#
A Lua function is cast to a C callback.
The jit-callback lint reports an unsafe cast of a Lua function to a C-derived callback type. The cast is legal only inside unsafe with an explicit retained owner, but LuaJIT registers the function as a permanent C callback once it happens, and no trace can compile through that call boundary for as long as it stays registered. Call jit.off on the registering function when the callback is intentional, or pass a plain pointer instead. Default level is warning, and a project may reconfigure or suppress it like any other lint.
Reported by the jit-callback lint, category suspicious, at warning by default. Configure it by name or by category.
Related: NUPP2604, NUPP2603. Reference: docs/reference/lints.md#jit-callback. Open the reported program in the playground.
NUPP2504#
An operator uses its customary C-style spelling.
local ready = true
local pending = !ready
return pendingThe C-style spellings !, &&, ||, and != are accepted, but the style lint prefers Lua's not, and, or, and ~=. The spelling changes; the expression's meaning does not.
local ready = true
local pending = not ready
return pendingReported by the customary-operator lint, category style, at warning by default. Configure it by name or by category.
Related: NUPP2108. Reference: docs/reference/lints.md#customary-operator. Open the reported program in the playground.
NUPP2505#
A loop builds the same non-capturing function on every iteration.
The loop-invariant-closure lint reports a function literal built inside a loop body that reads nothing from the iteration, so every built copy is the same function and building one afresh each time buys nothing. LuaJIT has no recorder for the bytecode that builds a function, so a loop containing one aborts trace recording every time it is tried and is eventually blacklisted, running interpreted from then on however hot it gets. Declare the function once above the loop and pass its name. Default level is warning; the sibling jit-loop-closure reports the case where the function does read the iteration and cannot be lifted.
Reported by the loop-invariant-closure lint, category suspicious, at warning by default. Configure it by name or by category.
Related: NUPP2515, NUPP2707. Reference: docs/reference/lints.md#loop-invariant-closure. Open the reported program in the playground.
NUPP2506#
A documented function can raise without saying when.
A function with a docblock that calls error must document the condition with @raises. Callers reading generated documentation then see the exceptional exit beside the ordinary result.
Reported by the undocumented-raise lint, category suspicious, at warning by default. Configure it by name or by category.
Related: NUPP2507. Reference: docs/reference/lints.md#undocumented-raise. Open the reported program in the playground.
NUPP2507#
A local is declared and nothing reads it.
local function shout(text: string): string
local prefix = "> "
return text .. "!"
end
return shoutA binding nothing reads is a leftover from an edit, or a use that went to another name. Delete it, or name it _ when the value is deliberately not wanted. A parameter and a loop variable are bound by a signature and an iterator rather than by their author, so neither is judged.
local function shout(text: string): string
local prefix = "> "
return prefix .. text .. "!"
end
return shoutReported by the unused-binding lint, category suspicious, at warning by default. Configure it by name or by category.
Related: NUPP2120, NUPP2603. Reference: docs/reference/lints.md. Open the reported program in the playground.
NUPP2508#
A call that does nothing but return had its result dropped.
A call written as a statement is made for what it does. When the callee is proved to do nothing but produce a value, and the statement drops that value, the whole statement does nothing. Use the result, or delete the call.
Reported by the discarded-result lint, category suspicious, at warning by default. Configure it by name or by category.
Related: NUPP2112, NUPP2603. Reference: docs/reference/lints.md. Open the reported program in the playground.
NUPP2509#
A record's fields would all fit in C memory.
The reifiable-record lint reports a record declaration whose every member -- fields, constructors, methods -- is one a struct would also accept: every field's type reifies, and nothing else in the body (an indexer, a Lua array part, a declaration-only metamethod, a nested declaration, a property capability, generics, or a declared supertype) rules struct out. Declaring it struct instead moves instances into C memory, off the collector's graph, at the cost of a fixed layout and giving up pairs/table-serializer support without an explicit hook. This is a suggestion rather than a correctness rule, so it defaults to off; a project turns it on with lints = {["reifiable-record"] = "note"} or by raising the whole performance category.
Reported by the reifiable-record lint, category performance, at off by default. Configure it by name or by category.
Related: NUPP2201, NUPP2012. Reference: docs/reference/lints.md#reifiable-record.
NUPP2510#
An else contains only a nested if.
local function classify(primary: boolean, fallback: boolean): nil
if primary then
print("primary")
else
if fallback then
print("fallback")
end
end
end
return classifyThe else-if lint reports an else block whose only statement is an unannotated if, which is the long way to write elseif; it also recognizes adjacent if statements that compare the same local to different literals when the first body does not reassign it. Write elseif (or fold the adjacent ifs into one chain) so the chain reads as one dispatch rather than nested blocks. Default level is warning, and a project may reconfigure or suppress it like any other lint.
local function classify(primary: boolean, fallback: boolean): nil
if primary then
print("primary")
elseif fallback then
print("fallback")
end
end
return classifyReported by the else-if lint, category style, at warning by default. Configure it by name or by category.
Related: NUPP2107. Reference: docs/reference/lints.md#else-if. Open the reported program in the playground.
NUPP2511#
An associated type was erased because inference did not reach its head.
A projection is checked as any when inference never worked out what answers it, which is the feature declining to say anything rather than saying the call is right. Reported once per call and member, where the erasure happened, because a parameter erased that way accepts whatever it is given.
Reported by the gradual-projection lint, category suspicious, at warning by default. Configure it by name or by category.
Related: NUPP2134. Reference: docs/learn/language/types/associated-types.md. Open the reported program in the playground.
NUPP2512#
A record is built by field order rather than by naming its fields.
local record Point
x: integer
y: integer
end
local p = new Point(1, 2)
return pA record without a declared constructor may be built either way, and both build the same table. Naming the fields says at the call site which value lands where; leaving it to the order says it in the declaration, so a reader has to go there, and adding a field silently changes what an existing call means. A struct is exempt: it is its C layout, and that order is the layout's rather than the program's to name. Turn it off by name or by its style category, or write @allow("positional-record-construction").
local record Point
x: integer
y: integer
end
local p = new Point(x = 1, y = 2)
return pReported by the positional-record-construction lint, category style, at warning by default. Configure it by name or by category.
Related: NUPP2202, NUPP2208. Reference: docs/reference/lints.md. Open the reported program in the playground.
NUPP2513#
An API marked deprecated is used.
local function current(): integer return 1 end
(replacement = "current")
local function legacy(): integer return current() end
return legacy()@deprecated keeps an API available while telling callers to move away from it. The optional reason explains why and the replacement names what to use instead. The annotation changes tooling only: it reports this suppressible lint at use sites and emits no runtime behavior.
Reported by the deprecated lint, category suspicious, at warning by default. Configure it by name or by category.
Related: NUPP2115. Reference: docs/reference/lints.md. Open the reported program in the playground.
NUPP2514#
A variadic FFI call is reachable from a compiled trace.
The jit-boundary lint reports a call to a cdef function declared with a ... variadic tail from code LuaJIT may try to compile: a variadic C call cannot safely execute on a trace. Move the call into a helper disabled with jit.off, so the interpreter always runs it, rather than leaving the boundary reachable from hot code. Default level is warning; inside an @jit function the same boundary is instead the non-suppressible NUPP2707 contract error.
Reported by the jit-boundary lint, category suspicious, at warning by default. Configure it by name or by category.
Related: NUPP2707, NUPP2502. Reference: docs/reference/lints.md#jit-boundary. Open the reported program in the playground.
NUPP2515#
A loop builds a function and so never compiles.
LuaJIT has no recording for the bytecode that builds a function, so a loop containing one aborts trace recording, is blacklisted after enough attempts, and then runs interpreted however hot it gets. A function that reads nothing from the iteration is loop-invariant-closure and lifts out unchanged; this is the other one, which reads the iteration and cannot. The way out where there is one is to declare a function above the loop that takes what varies as arguments, so the loop calls one rather than builds one.
Nothing about the program's answers changes either way, which is why this is off until a project asks for it. @jit is where it is not optional: that annotation promises the function compiles, so the same hazard inside one is reported as NUPP2707, and jit.off on the enclosing function silences it. nupp bc --check finds the same loops in any file without running it.
Reported by the jit-loop-closure lint, category performance, at off by default. Configure it by name or by category.
Related: NUPP2505, NUPP2707. Reference: docs/reference/lints.md.
NUPP2516#
An export exposes a private nominal type.
module geom.shapes
local record Coordinate
x: number
end
export record Point
coordinate: Coordinate
endThe private-export-type lint reports a private record, interface, or struct reached through an exported field, function signature, container, generic argument, or other public type position. The boundary remains valid: callers can infer and forward values of the hidden type, but cannot name or independently construct it. Export the nominal or an alias when callers should have a public name, or suppress the lint at the export when that opacity is intentional. Transparent aliases and private record fields do not expose a nominal identity.
module geom.shapes
export record Coordinate
x: number
end
export record Point
coordinate: Coordinate
endReported by the private-export-type lint, category suspicious, at warning by default. Configure it by name or by category.
Related: NUPP2004. Reference: docs/reference/lints.md#private-export-type. Open the reported program in the playground.
NUPP2518#
a closed no-input function repeats work that comptime can erase.
A lint, reported at off by default and configurable by name or by category. The program parses but does not mean something the checker can accept, or means something its author probably did not intend.
This code has no example pair of its own. The rule above is its family's, which is what the compiler knows about it.
Reported by the prefer-comptime lint, category performance, at off by default. Configure it by name or by category.
Reference: docs/reference/lints.md.
NUPP2601#
A moved-from owner is used again.
cdef struct resource
value: int32
end
cdef function resource_create(): resource*
cdef function resource_free(takes value: resource*)
local function resource_new(): affine(resource*, resource_free)
return resource_create()
end
local function useTwice(): nil
local value = resource_new()
drop(value)
drop(value)
end
return useTwiceAn affine binding or affine field is discharged by exactly one move, drop, or return. The name stays in scope after that, but its value is gone: reading it, moving it a second time, or dropping it again all reuse a transfer that already happened.
cdef struct resource
value: int32
end
cdef function resource_create(): resource*
cdef function resource_free(takes value: resource*)
local function resource_new(): affine(resource*, resource_free)
return resource_create()
end
local function useOnce(): nil
local value = resource_new()
drop(value)
end
return useOnceRelated: NUPP2602, NUPP2603. Reference: docs/reference/diagnostics.md#diagnostic-index. Open the reported program in the playground.
NUPP2602#
An ownership operation is invalid for the value's current state.
cdef struct resource
value: int32
end
cdef function resource_create(): resource*
cdef function resource_free(takes value: resource*)
local function resource_new(): affine(resource*, resource_free)
return resource_create()
end
local value = resource_new()
do
local view = borrow(value)
resource_free(value)
end
resource_free(value)A capability-qualified value only accepts the operations its current state permits: a value with a live borrow cannot itself move or be consumed until that borrow ends, a partially moved record cannot move or drop as a whole, and a plain value cannot be transferred as though it carried an affine or pinned capability. NUPP2602 covers whichever one of those the attempted operation violated.
cdef struct resource
value: int32
end
cdef function resource_create(): resource*
cdef function resource_free(takes value: resource*)
local function resource_new(): affine(resource*, resource_free)
return resource_create()
end
local value = resource_new()
do
local view = borrow(value)
print(view.value)
end
resource_free(value)Related: NUPP2601, NUPP2608, NUPP2615. Reference: docs/reference/diagnostics.md#diagnostic-index. Open the reported program in the playground.
NUPP2603#
An ownership obligation is not discharged or cannot escape.
An affine value must be consumed, dropped, returned under a matching ownership contract, or explicitly released in unsafe. Borrows likewise stay within the lifetime and suspension boundaries their provenance permits.
Related: NUPP2601, NUPP2602, NUPP2605. Reference: docs/learn/runtime/ownership/borrowing.md#consumption-and-lexical-destruction. Open the reported program in the playground.
NUPP2604#
Raw pointer arithmetic lacks proof or an unsafe boundary.
cdef struct resource
value: int32
end
local function advance(borrows base: resource*): resource*
return base + 1
end
return advanceOffsetting a raw C pointer with + or - needs a borrowed, rooted pointer to charge the access to, and needs an explicit unsafe do acknowledging that nothing checks the resulting address against a bound. Use a checked span instead when the bound should be proven rather than asserted.
cdef struct resource
value: int32
end
local function advance(borrows base: resource*): resource* borrows (base)
unsafe do
return base + 1
end
end
return advanceRelated: NUPP2001, NUPP2004, NUPP2602. Reference: docs/reference/diagnostics.md#diagnostic-index. Open the reported program in the playground.
NUPP2605#
Adjusting a value pack would discard an affine value.
Lua may truncate extra results, but Nupp cannot silently lose an owned or pinned slot. Bind and discharge every affine result, or forward the complete generic pack to a matching result or parameter.
Related: NUPP2602, NUPP2603, NUPP2010. Reference: docs/learn/language/types/packs.md#ownership-and-provenance. Open the reported program in the playground.
NUPP2606#
A preservation relation would lose or duplicate capability.
preserves source conserves the source's complete capability. A source that may carry a cleanup, transfer-only obligation, pin, or foreign retention must be taken, and each movable part must reach exactly one related result.
Related: NUPP2602, NUPP2603, NUPP2605. Reference: docs/learn/runtime/ownership/borrowing.md#generic-preservation. Open the reported program in the playground.
NUPP2607#
Shared and exclusive regions overlap incompatibly.
A live exclusive region must be disjoint from every other live shared or exclusive region. Fixed sibling fields and distinct proven indexes are disjoint; a parent, unknown index, or unproved range overlaps conservatively.
local function pair(exclusive a: table, exclusive b: table): nil end
pair({}, {})Related: NUPP2602, NUPP2608, NUPP2609. Reference: docs/learn/runtime/ownership/borrowing.md#regions-and-loop-carried-capabilities. Open the reported program in the playground.
NUPP2608#
A rooted value escapes its permitted lifetime.
A borrowed or scoped value may leave its scope only through an explicit result relation whose named roots outlive the result. It may not be returned, stored, captured, or erased without that relation.
Related: NUPP2603, NUPP2607, NUPP2611. Reference: docs/learn/runtime/ownership/borrowing.md#borrowing-and-pinning. Open the reported program in the playground.
NUPP2609#
A loop back edge changes capability state unsafely.
Every repeatable back edge must restore the loop header's live obligations and region loans. An iteration-local borrow must end before the edge, and an outer affine value cannot be consumed only on a possibly repeating path.
Related: NUPP2601, NUPP2603, NUPP2607. Reference: docs/learn/runtime/ownership/borrowing.md#regions-and-loop-carried-capabilities. Open the reported program in the playground.
NUPP2610#
A public capability contract is implicit.
An exported parameter or result that may carry a cleanup, pin, retention, or rooted view must state its mode or result relation. Ordinary public values remain unannotated.
Related: NUPP2606, NUPP2611. Reference: docs/learn/runtime/ownership/borrowing.md#public-capability-contracts. Open the reported program in the playground.
NUPP2611#
A dynamic boundary would erase a live capability.
A value carrying an obligation, root, exclusive loan, pin, or foreign retention cannot be converted to any or passed to untyped Lua. Keep the boundary typed, move a self-contained owner into nupp.manage, or use an explicit unsafe release.
Related: NUPP2603, NUPP2608, NUPP2612. Reference: docs/learn/runtime/ownership/borrowing.md#dynamic-boundaries. Open the reported program in the playground.
NUPP2612#
A managed value is not self-contained.
nupp.manage accepts only a capability it can discharge without outside state. Transfer-only obligations, external roots, outstanding exclusive loans, and unmatched foreign retentions cannot be managed.
Related: NUPP2608, NUPP2611, NUPP2614. Reference: docs/learn/runtime/ownership/borrowing.md#dynamic-boundaries. Open the reported program in the playground.
NUPP2613#
A managed alias has the wrong type policy.
Downcasting an alias requires an exactly droppable target and compares its canonical representation and cleanup policy with the policy recorded by nupp.manage. Recovery from any separately validates the runtime brand.
Related: NUPP2611, NUPP2614. Reference: docs/learn/runtime/ownership/borrowing.md#dynamic-boundaries. Open the reported program in the playground.
NUPP2614#
A managed alias is invalid or its cell is terminal.
An alias must carry the runtime brand of one managed cell. Closing or taking the payload leaves that cell as a permanent tombstone, and every copied alias fails before reading the payload.
Related: NUPP2612, NUPP2613. Reference: docs/learn/runtime/ownership/borrowing.md#dynamic-boundaries.
NUPP2615#
An affine value names an invalid terminal.
The terminal must be visible and have the exact shape nosuspend function(takes Representation): nil. An affine interface declares exactly one such terminal and cannot inherit a competing one.
Related: NUPP2602, NUPP2603. Reference: docs/learn/runtime/ownership/affine-types.md#cleanup-and-transfer-only-forms. Open the reported program in the playground.
NUPP2616#
A function declaring an owning result returns an input borrow.
A result annotated with an owning (affine) type promises every caller a value they will eventually discharge themselves. Returning a value borrowed from a parameter breaks that promise, since the caller would receive something whose lifetime it never actually controls. Declare the result borrows (param) instead of an owning type when the function only ever forwards a borrow.
local record Buffer
value: string
end
local function ok(borrows source: Buffer): Buffer borrows (source)
return source
end
return okRelated: NUPP2608, NUPP2619. Reference: docs/learn/runtime/ownership/borrowing.md#borrowing-and-pinning. Open the reported program in the playground.
NUPP2618#
A result declares a borrow from a parameter the function consumes.
borrows (param) on a result ties the result's lifetime to a live argument at the call site. A takes parameter is moved into the function and its argument is gone once the call returns, so nothing in the result can still be borrowing from it; name a borrows parameter instead, or drop the result's borrow annotation.
Related: NUPP2109, NUPP2616. Reference: docs/learn/runtime/ownership/borrowing.md#borrowing-and-pinning. Open the reported program in the playground.
NUPP2619#
A borrowed value's provenance does not reach its declared root.
local record Buffer
value: string
end
local function view(borrows source: Buffer): Buffer borrows (source)
return source
end
local record Cursor
source: Buffer
bytes: Buffer borrows (source)
end
local left = new Buffer(value = "left")
local right = new Buffer(value = "right")
local cursor = new Cursor(source = left, bytes = view(right))
print(cursor.bytes.value)A declared borrow names where its value comes from, and the value has to be provably derived from there, sharing the provenance chain rather than merely equalling or coincidentally matching. The same code reports each place that promise is checked: a field declared T borrows (source) constructed from a different root than the sibling passed as source; a result declared T borrows (param) returned from something that cannot be traced to that parameter; a call whose borrowed result has no rooted argument to borrow from; and a C output declared borrowed without a named lifetime source. Derive the value from the root the declaration names, or name the root it is derived from.
local record Buffer
value: string
end
local function view(borrows source: Buffer): Buffer borrows (source)
return source
end
local record Cursor
source: Buffer
bytes: Buffer borrows (source)
end
local left = new Buffer(value = "left")
local cursor = new Cursor(source = left, bytes = view(left))
print(cursor.bytes.value)Related: NUPP2602, NUPP2203, NUPP2707. Reference: docs/learn/runtime/ownership/borrowing.md#borrowing-and-pinning. Open the reported program in the playground.
NUPP2620#
A managed cell has a conflicting runtime borrow.
Shared alias callbacks exclude an exclusive callback, and an exclusive callback excludes every other borrow. Close and take likewise require no active callback borrow. The runtime releases a borrow before propagating a callback error.
Related: NUPP2607, NUPP2614. Reference: docs/learn/runtime/ownership/borrowing.md#dynamic-boundaries.
NUPP2621#
More than one result declares a borrow.
local record Buffer
value: string
end
local function bad(borrows left: Buffer, borrows right: Buffer): (Buffer borrows (left), Buffer borrows (right))
return left, right
end
return badA callable carries one borrow relation, and it states the result it is about: borrows (param) may be written on any result, but only one of them. Two results borrowing from two different parameters would be two relations, which the callable has nowhere to record and a call site nowhere to attach; return the second value owned, or hand the caller the borrowed one and let it derive the rest.
Related: NUPP2608, NUPP2618, NUPP2109. Reference: docs/learn/runtime/ownership/borrowing.md#borrowing-and-pinning. Open the reported program in the playground.
NUPP2630#
A counted C pointer does not match its physical count parameter.
countedBy(count) replaces a call-duration borrowed pointer/count pair with a checked span. The qualified value must be a pointer, and the named plain count must currently be uint64.
Related: NUPP2203, NUPP2602. Reference: docs/learn/runtime/c-interop/index.md#counted-pointer-adapters. Open the reported program in the playground.
NUPP2701#
A non-suspending region can reach suspension.
local function wait(): nil
coroutine.yield()
end
do
wait()
endA nosuspend region and every cleanup contract must finish without parking the current coroutine. Remove the yielding call, move it before the protected region, or call an operation whose type or visible body proves that it cannot suspend.
local function finish(): nil
end
do
finish()
endRelated: NUPP2602, NUPP2603. Reference: docs/learn/runtime/concurrency/suspension.md#non-suspending-regions. Open the reported program in the playground.
NUPP2702#
A non-yieldable C callback can reach suspension.
table.sort({2, 1}, function(a, b): boolean
coroutine.yield()
return a < b
end)LuaJIT cannot yield through every C frame. Make the callback and every call it reaches non-suspending, or invoke it from a yieldable Lua boundary.
table.sort({2, 1}, function(a, b): boolean
return a < b
end)Related: NUPP2701. Reference: docs/learn/runtime/concurrency/suspension.md#c-call-boundaries. Open the reported program in the playground.
NUPP2706#
Control cannot jump into a handled suspension region.
goto inside
handle suspension with handler do
::inside::
endEntering a handle suspension body from outside would bypass handler installation and its cleanup obligation. Move the label outside the region or move the jump inside it. Structured exits from the region are allowed.
handle suspension with handler do
end
::outside::Related: NUPP2701, NUPP2702. Reference: docs/learn/runtime/concurrency/suspension.md#handler-scope-follows-the-coroutine. Open the reported program in the playground.
NUPP2707#
A function required to compile crosses an unsupported JIT boundary.
cdef function printf(format: cstring, ...): int32
local function hot(): nil
printf('%d', 1)
end@jit promises that a function contains no variadic FFI call and does not pass a Lua callback into C. Move the boundary into a function disabled with jit.off, or remove @jit when tracing is not required.
cdef function printf(format: cstring, ...): int32
local function cold(): nil
printf('%d', 1)
end
jit.off(cold)Related: NUPP2502, NUPP2514. Reference: docs/learn/runtime/c-interop/index.md. Open the reported program in the playground.
NUPP2710#
A non-allocating region can reach allocation.
local function build(): nil
local values = {}
end
noalloc do
build()
endA noalloc region may only call operations whose visible bodies or exact exported guarantees prove they perform no managed allocation. Remove the construction, move it outside the region, or give an unseen declaration a truthful trusted @effects contract.
local function update(): nil
local n = 1
end
noalloc do
update()
endRelated: NUPP2112, NUPP2711. Reference: docs/learn/language/effects.md#allocation-and-raising-regions. Open the reported program in the playground.
NUPP2711#
A non-raising region can reach an error path.
local function fail(): nil
error('failed')
end
noraise do
fail()
endA noraise region may only call operations whose visible bodies or exact exported guarantees prove they cannot raise a catchable language error. Validate the precondition first, remove the error path, or move the operation outside the region.
local function finish(): nil
end
noraise do
finish()
endRelated: NUPP2112, NUPP2710. Reference: docs/learn/language/effects.md#allocation-and-raising-regions. Open the reported program in the playground.
NUPP2801#
A derive provider name is unknown or duplicated.
Each @derive argument names one resolved exported comptime provider, and an identity may occur only once across every derive annotation on the record.
Related: NUPP2113, NUPP2802. Reference: docs/reference/derives.md.
NUPP2802#
A generated derive member conflicts with the declaration.
A derive never overrides a written instance or static member. Remove the written member or remove the provider so the behavior has one owner.
Related: NUPP2801, NUPP2115. Reference: docs/reference/derives.md.
NUPP2803#
A field cannot participate in derived Debug.
Every visible Debug field needs a supported scalar or container type, another Debug record, an exact nupp.Debug contract, or the dynamic any fallback. A field cannot be both skipped and redacted.
Related: NUPP2802. Reference: docs/reference/derives.md#debug.
NUPP2806#
A record does not describe a supported JSON schema.
Derived JSON needs a closed supported field graph and consistent JSON options. Rename duplicate keys, default omitted fields, avoid erased generic records and unsupported values, and do not use int64 or uint64 as JSON numbers.
Related: NUPP2001. Reference: docs/reference/derives.md#json.
NUPP2807#
A derive dependency cycle has no valid lowering.
Recursive Debug and JSON graphs are supported. A package provider dependency cycle must still reduce to a closed recipe rather than require another unfinished provider result.
Related: NUPP2803, NUPP2806, NUPP2810. Reference: docs/reference/derives.md.
NUPP2808#
A derive exceeds a compiler generation limit.
Generated recipes, expressions, locals, upvalues, and emitted output are bounded compiler resources. Reduce the derived declaration or split the schema rather than depending on an unbounded generated function.
Related: NUPP2807. Reference: docs/reference/derives.md.
NUPP2809#
A comptime derive provider declaration or reference is invalid.
A public derive provider is an exported, nongeneric comptime function with the exact shape function(nupp.derive.Info): nupp.derive.Result, where I is one existing interface. @derive must name that resolved export.
Related: NUPP2411, NUPP2801. Reference: docs/reference/derives.md#package-providers.
NUPP2810#
A comptime derive provider failed or returned an invalid blueprint.
Providers execute in the bounded comptime worker and must return only nupp.derive.implement or nupp.derive.error. Closed builders reject foreign, cyclic, malformed, stale, and over-limit recipe graphs.
Related: NUPP2412, NUPP2808. Reference: docs/reference/derives.md#package-providers.
NUPP2811#
A derive recipe declares an invalid generated member.
A bare forward fills a bodyless callable requirement of Result. A member recipe may instead provide a bounded function signature. Neither form may replace written members or interface defaults.
Related: NUPP2118, NUPP2802. Reference: docs/reference/derives.md#closed-forwarding-recipes.
NUPP2812#
A forwarding argument does not exist on the generated method.
A forwarding recipe may pass its receiver, a named method parameter, a readable stored field, a bounded frozen constant, or a fresh array of those. Every name is resolved against the written owner and interface requirement.
Related: NUPP2004, NUPP2811. Reference: docs/reference/derives.md#closed-forwarding-recipes.
NUPP2813#
A runtime forwarding helper does not satisfy the generated call.
The helper must be an ordinary exported Nupp function. Forwarded argument types must fit its parameters, its result pack must satisfy the interface requirement, and it may not suspend where the requirement cannot.
Related: NUPP2001, NUPP2701, NUPP2812. Reference: docs/reference/derives.md#runtime-helpers.
NUPP2901#
One function is promised to two compilers.
@jit promises a function compiles as a LuaJIT trace. @aot reserves that body for ahead-of-time compilation. Stacking them asks two compilers for the same code, so one of the two has to go: keep @aot when the function should compile ahead of time, and @jit when it should trace.
Related: NUPP2707, NUPP2902. Reference: docs/neps/0009-ahead-of-time-compilation.md. Open the reported program in the playground.
NUPP2902#
@aot is attached to something that is not a whole function.
local record Point
x: number
constructor(self, x: number)
self.x = x
end
end@aot compiles one complete function and puts a wrapper in front of the function value callers see. A constructor and an inline interface requirement are neither: the compiler generates code around both, and neither is a value the wrapper can stand in front of. A function nested inside another function is a fresh value on every call of the one holding it, which is not something one wrapper can stand in front of either. Move the work into an ordinary top-level function and annotate that, then call it from wherever it was.
local function scaled(x: number): number
return x * 2.0
end
local record Point
x: number
constructor(self, x: number)
self.x = scaled(x)
end
endRelated: NUPP2901, NUPP2903. Reference: docs/neps/0009-ahead-of-time-compilation.md. Open the reported program in the playground.
NUPP2903#
An @aot body contains a construct with no AOT IR representation.
The body of an @aot function is ordinary Nupp, but only the part of it the AOT IR can represent. The IR has no closure, table, interpolated string, vararg, goto, dynamic call, or unsafe operation, so a body containing one cannot compile ahead of time and says so here rather than failing in a backend. Rewrite the body without it, or remove @aot and let the function run as ordinary Nupp.
Related: NUPP2901, NUPP2902. Reference: docs/neps/0009-ahead-of-time-compilation.md. Open the reported program in the playground.
NUPP2904#
A construct needs a capability the selected target does not have.
A target profile states what a destination admits: a dynamic loader, a tracing JIT, FFI callbacks, and a way to resolve a default-namespace symbol out of a static archive. @jit on a target with no trace compiler asserts a contract nothing there can meet, and a cdef ... from "name" on a target with no loader asks the platform to load something it cannot; both are refused where they are written rather than on the device. Reach the same C through the default namespace, which a static link puts in the process image, and drop @jit where nothing traces. Which target is selected is what decides this, so no isolated file exhibits it on its own: a target is described by the built-in profile for its triple or by a verified descriptor in a compiler pack, and a target nothing describes refuses nothing.
Related: NUPP2707, NUPP2901. Reference: docs/learn/projects/build.md.
Code generation#
NUPP3001#
is has nothing to test against this type.
A record is identified by the metatable it stamps and a struct by its ctype, so both answer is exactly. An interface has neither, by design, being conformance rather than provenance, so something has to stand in for one.
Three things can. A literal-typed field is a tag, and the test is read off it with nothing written. A satisfies declaration says the test outright, for a shape no tag describes. And a subject whose own type declares the interface needs no test at all: the declaration already answered, so the is compiles to true. An alias has none of these and never will.
Related: NUPP2122. Reference: docs/learn/language/types/interfaces.md. Open the reported program in the playground.
NUPP3002#
A struct field's type has no C declarator the generator can emit.
local record Widget
id: integer
end
local struct Holder
items: Widget*[3]
end
return HolderEvery field of a reified struct lowers to a literal C declarator inside the ffi.typeof string the record compiles to. The checker admits any pointer or fixed array uniformly as a reifiable field, but the generator still has to spell each concrete shape syntactically, and some admitted shapes have no case in that spelling, chief among them an array whose element is itself a pointer. Such a field is refused at generation rather than at check.
Related: NUPP2201, NUPP3003, NUPP3004. Reference: docs/learn/runtime/c-interop/index.md#type-mapping. Open the reported program in the playground.
NUPP3003#
A cdef struct field or function signature has no C spelling.
local record Widget
id: integer
end
cdef function inspect(target: Widget*)
return inspectcdef struct and cdef function declare literal C, and every field and parameter type must render as one. A pointer to an ordinary Nupp record, one with no cdef struct layout of its own, passes the checker's general rule that any pointer is reifiable, but has no C declarator: only cdef-declared aggregates, primitives, cstring, voidptr, and pointers built from those actually render. Point at a cdef struct instead of a plain record.
cdef struct Widget
id: integer
end
cdef function inspect(target: Widget*)
return inspectRelated: NUPP2203, NUPP3002, NUPP3004. Reference: docs/learn/runtime/c-interop/index.md#type-mapping. Open the reported program in the playground.
NUPP3004#
An FFI operation's explicit type argument has no C spelling.
local record Widget
id: integer
end
local w = ffi.new<Widget>()
return wffi.new<T>, ffi.cast<T>, ffi.typeof<T> and their siblings take T as a literal C type argument to LuaJIT, not an ordinary Nupp value type. The checker resolves any named type here without verifying it is renderable; only a cdef struct, a Nupp struct, a primitive, or a pointer built from those actually is. Naming a plain record fails at generation instead.
local struct Widget
id: integer
end
local w = ffi.new<Widget>()
return wRelated: NUPP2203, NUPP3002, NUPP3003. Reference: docs/learn/runtime/c-interop/index.md#type-mapping. Open the reported program in the playground.
NUPP3005#
Generated code that a Lua VM will not load.
The generator writes Lua and this is that Lua refusing to parse. Almost always it is one limit: a function may capture at most sixty names from around it, and one that reaches past that cannot be loaded at all. A function reading that many things from its scope is usually reading a record it could take as one argument instead. Pass what varies, or gather what it reads into one value and capture that.
Any other spelling of this is a bug in the compiler rather than in the program, and nupp bc FILE shows the code it wrote. It is reported where the file is built rather than where the module is first required, because the line a VM would name belongs to generated text and the line here is the one that was written.
Related: NUPP3004. Reference: docs/reference/diagnostics.md#code-families.
NUPP3006#
The selected dialect cannot represent a required language capability.
A dialect fixes the language representations and operations an artifact may use. Choose a target that supports the operation, or express it with a portable contract. Runtime provider selection cannot add a compiler representation.
Related: NUPP3005. Reference: docs/learn/projects/portability/libraries.md.
NUPP3009#
The portable dialect cannot lower an authored jump.
An authored label or goto can jump across arbitrary lexical regions. Lowering a function containing one to Lua 5.1 would require a state machine, changing stack frames and error sites. Use structured control flow instead. Compiler-generated cleanup and continue transfers are structured and are lowered separately.
Related: NUPP3006. Reference: docs/learn/projects/portability/libraries.md.
NUPP3010#
A resolved prelude identity is outside the portable runtime surface.
The lua51 dialect runs on Lua 5.1 through 5.4 and LuaJIT, so an ambient prelude identity is available only when it has the same contract throughout that matrix or the compiler lowers it explicitly. A same-spelled local or ordinary table field is a different definition and remains application code.
Related: NUPP3006, NUPP3009. Reference: docs/learn/projects/portability/libraries.md.
Formatting#
NUPP4001#
The formatter's own output would change the token stream.
Formatting may only move whitespace, reflow comments, add or remove a schema-proven single-value annotation label, and parenthesize a method call's sugar-form arguments. Before writing its result the formatter re-lexes it and compares the token fingerprint against the input; a mismatch means the rewrite it just performed would have changed the program's meaning, so it discards the rewrite and leaves the file untouched instead of risking that change.
Reference: docs/reference/diagnostics.md#code-families.
Development runtime#
NUPP5001#
A running program cannot apply a change without a restart.
Hot reload replaces a watched module's callable bodies in place, but cannot rebind top-level executable statements or initializers, add, remove, rename or move a named declaration, change a callable's signature or capture set, change a record, struct, native or component layout, or repatch a function that took ownership of a cleanup-bearing capture. Loaded FFI declarations, native libraries, and values already built from them are likewise fixed for the life of the process. Any such edit is rejected with NUPP5001 and the previous generation keeps running until the process restarts.
Reference: docs/learn/projects/hot-reload.md#changes-that-require-restart.