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 and message, plus fixes, notes, and related, which are always present and may be empty. The rest appear when there is something to say:
range, on anything with a place in source. A diagnostic about something with none -- a file that could not be read, a manifest that could not be used -- leaves it out rather than pointing at a line 0 no file has.fileandcode, on anything with a file 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, always as a documentation path and an anchor: the prose section that owns the rule, or the code's own entry in the diagnostic index where no prose section does.
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 |
A file could not be read or written. |
NUPP0002 |
The project manifest could not be used. |
NUPP0003 |
A name the command was given names nothing here. |
NUPP0004 |
A tool the command depends on failed. |
NUPP0005 |
A type dependency's declaration file was not imported. |
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. |
NUPP1010 |
A docblock @raises line does not name a type. |
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. |
NUPP4002 |
A file is not formatted the way nupp fmt writes it. |
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, NUPP2905 through NUPP2911 |
| 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 |
| Constrained types | Constrained types | NUPP2001, NUPP2013, NUPP2422 |
| Derives | nupp.derive | NUPP2810 |
| Effect contracts | Effect contracts | NUPP2112, NUPP2710, NUPP2711 |
| File types | File types | NUPP1006, NUPP1008, NUPP2105, NUPP2106 |
| Files | nupp.io.files |
NUPP2701 |
| Formatter | fmt.md | NUPP4001 |
| Generics | Generics | NUPP2003, NUPP2116, NUPP2122, NUPP2146, NUPP2148 |
| 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 NUPP2518 |
| 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 |
| Do expressions | Do expressions | NUPP2141, NUPP2142 |
| Exit suffixes | Exit suffixes | NUPP2002, NUPP2149, NUPP2517, NUPP2605 |
| Switch expressions | Switch expressions | NUPP2137 through NUPP2142, NUPP3001 |
| Target capability profiles | Build system | NUPP2904 |
| Type packs | Type packs | NUPP2007, NUPP2010, NUPP2121, NUPP2148, 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 |
doc |
what kind of documentation was produced, 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 |
migrate |
the migration plans and why any failed |
ownership-audit |
foreign pointer contracts and unsafe assertion sites |
reference |
the reference, section by section |
bench |
one run with every fork of every benchmark, or with --list the benchmarks |
task |
the task list, or one task's configuration |
test |
totals and a record per test, with file and line |
version |
the compiler's version and the interpreter under it |
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.
Every command holds its --json to one contract, whichever way it ends:
- Exit status 2 is a usage error: an argument the command cannot use, decided from the arguments and the names compiled into the compiler. Nothing is written to stdout, even under
--json; stderr has the usage message. - Exit status 0 or 1 writes exactly one document to stdout, valid against the command's
--schema, withokset and equal to the status being 0. okfalse always comes with at least oneerrorindiagnosticsthat carries a code. A failure outside source is coded too; see Failures outside source.- Nothing on stderr is needed to understand the result. It carries progress, when progress was asked for.
nupp test answers a failure before its tests started -- a manifest it could not use, a build under test that failed -- with the runner's own document: one failed record under a synthetic ID such as nupp/<build>.
The documents are not versioned. Their version is the compiler's, which nupp
version --json reports, and --schema is the contract: it describes what this compiler writes, and a field may change between releases before 1.0. Artifacts another process reads back -- a coverage report, an AOT units manifest, a bench baseline -- are a different matter and carry a schemaVersion checked on read; see Versions.
The output is always valid UTF-8. A byte that is not UTF-8, such as one a diagnostic quotes from the source, is written as U+FFFD.
Failures outside source#
A command can stop before it has a file and a position to point at. Each such failure still has a code, so a caller reading --json can tell them apart without reading the message, and none has a range:
| Code | The run stopped because |
|---|---|
NUPP0001 |
a file could not be read or written |
NUPP0002 |
the project manifest exists and could not be used |
NUPP0003 |
a target, task, platform, triple, function or position it was given names nothing here |
NUPP0004 |
a tool it depends on failed: LuaRocks, the LLVM kit, Cargo, a C preprocessor, a stage compiler |
A directory with no nupp.lua is a supported configuration for the commands that can work without one, and reports nothing. One that has a manifest which does not load is NUPP0002, since answering without it would answer for a different project.
Agent workflow#
- Run
nupp check --json, 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 does a failed run have a diagnostic with no range?#
The run never reached a file. A manifest that could not be used ends a check before anything is parsed, so the reason is a coded diagnostic about the run -- NUPP0002 -- rather than about a place in source. See Failures outside source.
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 file could not be read or written.
The path named on the command line, reached from a require, or written as output could not be opened. Check the spelling and that the file is readable, or its directory writable, from the directory the compiler was run in. The diagnostic has no position: the run never got inside the file.
Related: NUPP0002. Reference: docs/reference/diagnostics.md#failures-outside-source.
NUPP0002#
The project manifest could not be used.
A nupp.lua that exists has to load and describe a project this compiler can build. One that raises, returns something other than a table, misspells a key, or asks for a combination the build cannot honour stops the command before any source is read, since answering without it would answer for a different project. A directory with no nupp.lua at all is a supported configuration and reports nothing.
Related: NUPP0001, NUPP0003. Reference: docs/reference/diagnostics.md#failures-outside-source.
NUPP0003#
A name the command was given names nothing here.
A build target, task, platform or triple is looked up in the manifest or among the targets this compiler models, and a function, position or kind of input in the file the command was pointed at. The name was well formed and names nothing there. nupp task --list lists the targets and tasks a project defines.
Related: NUPP0002. Reference: docs/reference/diagnostics.md#failures-outside-source.
NUPP0004#
A tool the command depends on failed.
Some commands hand part of their work to another program or component: LuaRocks installs a dependency, the LLVM kit compiles native code, Cargo builds a native provider, a C preprocessor reads a header, a stage compiler rebuilds the compiler. When that step fails, the message carries what it said. The source may be fine; what failed is the step.
Related: NUPP0002. Reference: docs/reference/diagnostics.md#failures-outside-source.
NUPP0005#
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.
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. 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. 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. 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. 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, expressions, blocks or types nested more than a thousand levels deep, and similar constraints the parser enforces directly. Each diagnostic's message names the specific rule that failed.
local x = 2
return xRelated: NUPP1002, NUPP1004. 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. Type comments are separate: recognized LuaCATS, EmmyLua, and typed LuaDoc comments are always imported.
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: NUPP1010, 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.
NUPP1010#
A docblock raise does not name a type.
An @raises line is written @raises Type the condition. The type is the value error is called with, so a caller reading the page knows what a pcall around the call hands back. Name a type this file can resolve, or string where the raise is an ordinary Lua message.
Related: NUPP1007, NUPP2506. Reference: docs/learn/tooling/documentation.md#doc-comments. Open the reported program in the playground.
Types, declarations, and rules#
NUPP2001#
A value does not fit the type it is bound to.
local count: integer = "twelve"
return countAn annotated binding accepts only values of that type. The annotation is the claim; the initializer has to keep it.
local count: integer = 12
return countRelated: NUPP2002, NUPP2006. 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. 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. Open the reported program in the playground.
NUPP2005#
A call's callee has no callable type or __call contract.
local count: integer = 5
return count()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 it opts the value out of checking; 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. 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#rejected-arrangements. 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.
const count = 1
count = 2
return countThe 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.
local count = 1
count = 2
return countRelated: 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: docs/learn/language/types/primitives.md#value-refinements. 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.
NUPP2013#
nupp.admit was written without a constrained destination or with the wrong value.
nupp.admit(value) takes the constrained type it admits into from where its result goes: an annotated binding, a declared result, or a parameter. A destination that is not constrained gives it nothing to establish, and a value that is not already the constraint's base is a conversion rather than an admission, which this does not do.
Related: NUPP2011, NUPP2422. Open the reported program in the playground.
NUPP2101#
A type name cannot be resolved.
local count: Count = 1
return countA 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.
local type Count = integer
local count: Count = 1
return countRelated: NUPP2105, NUPP2120. 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.
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.
NUPP2105#
A name is not any value the checker or project can account for.
local function shout(text: string): string
return txet .. "!"
end
return shoutA name the checker cannot account for is reported unless it is a project module's basename, with a spelling fix offered when a visible name is close enough to be the one meant. A .lua file reports none, since an unknown name there may be a Lua global set at run time.
Related: NUPP2120, NUPP2101. 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, and a signature mentioning any counts as unwritten. A .d.nupp file may write any, since it describes code somebody else implements.
local m = {}
function m.double(n: integer): integer
return n * 2
end
return mRelated: NUPP2119. 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#exhaustiveness. Open the reported program in the playground.
NUPP2108#
A type error is named as though it were a lint.
(NUPP2001)
local count: integer = "one"
return count@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.
local count: integer = 1
return countRelated: 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.
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 an invalid interface after is.
local record Other
end
local record Bad is Other
end
return BadOnly an interface may appear after is on a record, struct, or interface declaration, because that is the only declaration kind able to state a contract for another to inherit. An interface cannot implement itself, directly or through other interfaces: the declared hierarchy must be acyclic. 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, unknown metamethods, and incompatible interface overrides cannot describe one coherent contract. A method overriding a child interface's inherited slot must say @override; a concrete implementor needs it when replacing inherited default behavior.
local record Pair
first: string
second: integer
end
return PairRelated: NUPP2004, NUPP2123. 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. 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#tests-read-self-and-nothing-else. 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 unchecked.
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#checking-a-metatable-literal. 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 an any 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#answering-a-requirement. 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#defaults-and-fixed-equalities. 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#associated-types-are-not-nested-aliases. 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.
local interface Reader
associated type Item
end
local function first<T is Reader>(x: T): T.Item
return nil as any
end
return firstRelated: NUPP2127. Reference: docs/learn/language/types/associated-types.md#reaching-an-associated-type. 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#cycles-report-once-per-component. Open the reported program in the playground.
NUPP2136#
A sealed interface is implemented outside its owning module.
A 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.
Related: 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 do expression does not yield on every completing path.
A do expression produces one value with contextual yield value. Every path reaching its end must yield, and no statement may follow an unconditional yield in the same path. Return, break and continue retain their enclosing function or loop targets.
Related: NUPP2140, NUPP2142. Reference: docs/learn/language/do-expressions.md#do-expressions. Open the reported program in the playground.
NUPP2142#
A block expression is in an unsupported lowering position.
Do and switch expressions need a lexical statement context for their setup. The compiler reports positions its lowering cannot yet preserve. Bind the expression to a preceding local in the same conditional region when that preserves the intended evaluation order.
Related: NUPP2141. Reference: docs/learn/language/do-expressions.md#do-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 unchecked and is not an access-control sandbox.
Reference: docs/learn/language/modules.md#internal-modules.
NUPP2145#
A type parameter conflicts with a builtin type name.
local record Box<string>
value: string
end
return BoxBuiltin type names always denote their primitive types in a type position. Using one for an ordinary generic parameter would create a parameter the declaration cannot refer to. Choose a name that is not a builtin type.
local record Box<T>
value: T
end
return BoxRelated: NUPP2101, NUPP2121. Reference: docs/learn/language/types/generics.md#parameter-positions. Open the reported program in the playground.
NUPP2146#
A generic declaration is applied to the wrong number of type arguments.
local record Pair<A, B>
a: A
b: B
end
local half: Pair<integer> = nil as any
return halfAn application writes as many arguments as the declaration binds parameters. A trailing parameter the declaration defaulted may be left out; nothing past the last parameter may be written, and a bare name is an application with none. Left unchecked the result was a type whose parameters nothing had bound, which fits no value the program can build and surfaces instead as a mismatch between two types that print the same. Type<Box> and metatable<Box> name the declaration rather than one of its values and so take no arguments, and inside its own body a declaration means itself applied to its own parameters.
local record Pair<A, B>
a: A
b: B
end
local both: Pair<integer, string> = nil as any
return bothRelated: NUPP2101, NUPP2116, NUPP2121. Reference: docs/learn/language/types/generics.md#instantiation. Open the reported program in the playground.
NUPP2147#
An is test's subject can never hold the type it tests.
A test whose subject and its target share no value can only ever answer false, so the branch it guards is one no run reaches. It is refused where it is written rather than compiled. Only what can be settled for certain counts: any, a type parameter, and a structural fit the checker could not decide all leave the test alone, and as remains the way to say the value is something its declaration does not admit. is sits at the comparison level, below unary not, so not v is T is (not v) is T -- a boolean tested for the identity of something else -- and not (v is T) is the test that narrows.
Related: NUPP2110, NUPP3001. Reference: docs/learn/language/types/narrowing.md#type-tests. Open the reported program in the playground.
NUPP2148#
A result type parameter cannot be inferred.
A type or type-pack parameter used by a call's result needs evidence from an explicit type argument, an argument value, a destination type, an any input, or a declared default. Without any of those, substituting any would silently erase the result's type. A parameter absent from the result does not need evidence.
Related: NUPP2116, NUPP2131, NUPP2146. Reference: docs/learn/language/types/generics.md#inference-at-a-call-site. Open the reported program in the playground.
NUPP2149#
An operand's results cannot be read by an exit suffix.
or return, or break and or continue read the operand's first result as both the discriminator and the value. That needs a fixed result count, results that are not a correlated pack union, and a first result that can be nil or false and can also be a value. pcall and xpcall are refused by the union rule, because they lead with the discriminator rather than with a result; nupp.util.try and nupp.util.tryWith answer the conventional order instead.
Related: NUPP2002, NUPP2605. Reference: docs/learn/language/exit-suffixes.md#exit-suffixes. Open the reported program in the playground.
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, so calling a declaration is never a construction and the call stays the program's own: a record's __call is not shadowed when the argument is a table, and a struct's ctype call is not claimed. 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.
Related: 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. 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. 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 declares a binding and constructs nothing, 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. The checker 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.
A 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.
Related: NUPP2004, NUPP2202. Reference: docs/learn/language/types/records-and-structs.md#private-fields. Open the reported program in the playground.
NUPP2301#
cheader's arguments are not the literal form it takes.
local mini = cheader("mini" .. ".h")
return minicheader reads the named header at compile time, so everything it is given has to be known to the compiler before the program runs. It takes a literal header path, then optionally a literal library name, then optionally the literal "preprocess", and nothing more. A computed header path or library name, a third argument other than "preprocess", and any argument after the third are each refused.
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 any, usable as an element count.
local struct Point
x: float
y: float
end
local points = carray(Point, 4)
return pointsRelated: 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.
local struct Point
x: float
y: float
end
return layoutof(Point)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 column projection has no resolved stored field.
Structure-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 view's dot or literal bracket projection must likewise name one such stored field of that struct, not a dynamic name or an unknown one.
Related: NUPP2402, NUPP2009. 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. 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. 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. 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. 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. 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. 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. Open the reported program in the playground.
NUPP2419#
A layout intrinsic has no target, a bad shape, or an unknown field.
nupp.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. 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.
NUPP2422#
A constrained type declaration names an unsupported base or empty bounds.
nupp.types.range narrows integer, int32 or uint32, and nupp.types.length narrows string; either may also narrow a constrained alias of one, which intersects the two constraints. Both bounds are inclusive whole numbers in ascending order, and both must lie inside what the base itself admits, so a declaration that admits nothing is reported where it is written rather than compiled into a test no value passes.
Related: NUPP2013, NUPP2421. 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 a cast of a Lua function to a C-derived callback type. The cast requires @unsafe permission and 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.
local function register(item: any, callback: function(): boolean): nil
print(item, callback())
end
local function run(items: {any}): nil
for _, item in ipairs(items) do
register(item, function(): boolean
return true
end)
end
end
return runThe 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.
local function register(item: any, callback: function(): boolean): nil
print(item, callback())
end
local function alwaysTrue(): boolean
return true
end
local function run(items: {any}): nil
for _, item in ipairs(items) do
register(item, alwaysTrue)
end
end
return runReported 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 Type the condition. Callers reading generated documentation then see the exceptional exit, and what it hands back, beside the ordinary result.
Reported by the undocumented-raise lint, category suspicious, at warning by default. Configure it by name or by category.
Related: NUPP1010, NUPP2507. Reference: docs/reference/lints.md#undocumented-raise. Open the reported program in the playground.
NUPP2507#
A local is declared and nothing reads it.
A 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.
Reported 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#unused-binding. 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#discarded-result. 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.
local interface Holds
associated type Item
end
local function held<T is Holds>(x: T): T.Item
return nil as any
end
local erased = held(nil as any)
return erasedA 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 erased-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#erased-projections. 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#positional-record-construction. 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#deprecated. 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#jit-loop-closure.
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.
NUPP2517#
A binding list takes more than one value from an exit suffix.
local function pair(): (integer?, integer?)
return 1, 2
end
local function both(): (integer?, integer?)
local left, right = pair() or return
return left, right
end
return both()An exit suffix supplies its operand's first result and nothing else, however wide the operand is. A second name bound from one is therefore always nil. Bind the operand's results first and test the one that decides when the other results are wanted.
local function pair(): (integer?, integer?)
return 1, 2
end
local function both(): (integer?, integer?)
local left, right = pair()
if left == nil then
return nil, nil
end
return left, right
end
return both()Reported by the exit-suffix-binding lint, category suspicious, at warning by default. Configure it by name or by category.
Related: NUPP2149. Reference: docs/learn/language/exit-suffixes.md#exit-suffixes. Open the reported program in the playground.
NUPP2518#
A closed no-input function repeats work that comptime can erase.
A function with no parameters and no runtime captures that does deterministic construction and iteration repeats the same work every time it is called. When the bounded comptime evaluator proves that work reduces to a materially smaller scalar literal, wrapping the body in comptime do makes that choice explicit and keeps the function's runtime API: the offered fix does exactly that, and the function then returns the literal.
Methods, functions with parameters or runtime captures, nondeterministic or unsupported operations, table results, trivial literal bodies, and work that does not become materially smaller are not reported. The lint is off until a project enables the performance category or the lint by name.
Reported by the prefer-comptime lint, category performance, at off by default. Configure it by name or by category.
Related: NUPP2410, NUPP2411. Reference: docs/reference/lints.md#prefer-comptime.
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()
nupp.drop(value)
nupp.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()
nupp.drop(value)
end
return useOnceRelated: NUPP2602, NUPP2603. 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. 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 with @unsafe nupp.release. 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.
Offsetting a raw C pointer with + or - needs a borrowed, rooted pointer and an explicit @unsafe (base + offset) acknowledging that nothing checks the resulting address against a bound. The parentheses include the arithmetic in the annotation. Use a checked span when the bound should be checked.
Related: NUPP2001, NUPP2004, NUPP2602. 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.
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 nupp.release call.
Related: NUPP2603, NUPP2608, NUPP2612. Reference: docs/learn/runtime/ownership/borrowing.md#dynamic-boundaries-and-managed-cells. 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-and-managed-cells. Open the reported program in the playground.
NUPP2613#
A managed alias has the wrong type policy.
local record Plain
end
local erased: any = {}
local recovered = nupp.recoverAlias(erased)
local plain = recovered and recovered:downcast<Plain>()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-and-managed-cells. 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-and-managed-cells.
NUPP2615#
An affine value names an invalid terminal.
The terminal must be visible and have the exact shape function(takes Representation): nil. A declared nupp.Affine claim selects one cleanup function 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-and-managed-cells.
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.
NUPP2622#
A generic for control value cannot be affine.
Generic for replaces its control value with the first result of every iterator call. An affine initial control would lose its cleanup obligation on the first step. Use an ordinary control value; a newly produced affine iterator or state is held and closed by the loop itself.
local function step(_state: nil, _control: integer?): integer? return nil end
for n in step, nil, 0 do endRelated: NUPP2603, NUPP2615. Reference: docs/learn/runtime/ownership/affine-types.md#cleanup-and-transfer-only-forms. 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
with owner = acquire() do
::inside::
endEntering a with body from outside would bypass owner acquisition and its cleanup obligation. Move the label outside the scope or move the jump inside it. Structured exits from the scope are allowed.
with owner = acquire() 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.
@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.
Related: NUPP2502, NUPP2514. Reference: docs/learn/runtime/c-interop/index.md#jit-sensitive-c-boundaries. Open the reported program in the playground.
NUPP2710#
A non-allocating region can reach allocation.
local function build(): nil
local values = {}
end
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
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
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
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.
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.
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 retired JSON derive rejected its schema.
This diagnostic belonged to the retired JSON derive. Select an explicit serialization binding and codec; use nupp migrate for supported local declarations.
Related: NUPP2001. Reference: docs/learn/runtime/data/serde.md#migration.
NUPP2807#
A derive dependency cycle has no valid lowering.
Recursive Debug 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#package-providers.
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.
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.
local function hot(scale: number): number
return scale * 2.0
end@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.
local function hot(scale: number): number
return scale * 2.0
endRelated: NUPP2707, NUPP2902. Reference: docs/learn/performance/ahead-of-time/index.md#annotation-guarantees. 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/learn/performance/ahead-of-time/index.md#annotation-guarantees. Open the reported program in the playground.
NUPP2903#
An @aot body contains a construct with no AOT IR representation.
local function total(scale: number): number
local function double(x: number): number
return x * 2.0
end
return double(scale)
endThe 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.
local function double(x: number): number
return x * 2.0
end
local function total(scale: number): number
return double(scale)
endRelated: NUPP2901, NUPP2902. Reference: docs/learn/performance/ahead-of-time/index.md#annotation-guarantees. 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#target-capability-profiles.
NUPP2905#
An @aot body uses a value or operation lowering cannot admit.
An @aot body lowers to typed native code, which decides at compile time what the Lua version decides as it runs: a branch condition has to be a boolean rather than any truthy value, a call has to reach an admitted intrinsic or helper, a span read has to be proved in bounds, and a parameter has to have a type the native ABI can carry. The message names the construct and what it would take. Rewrite that construct, or remove @aot and let the function run as ordinary Nupp.
This applies only to a build target whose aot policy compiles @aot functions, which is every policy but off.
Related: NUPP2903, NUPP2906. Reference: docs/learn/performance/ahead-of-time/index.md#annotation-guarantees. Open the reported program in the playground.
NUPP2906#
The selected feature tier cannot carry an explicit vector operation.
local array = require("nupp.mem.array")
local span = require("nupp.mem.span")
local simd = require("nupp.simd")
local function lane(borrows input: span.Span<float>): number
if species = simd.vectors(array.float) then
if species.lanes <= #input then
return species:load(input, 1):extract(5)
end
end
return 0.0
end
return {lane = lane}Explicit SIMD code is legalized for the tier it is compiled for, and every tier a build selects compiles the same body. A constant lane index past the lanes the tier's species has names a lane that does not exist there. Index within species.lanes, or keep the lane-specific code inside the branch that tested the species.
This applies only to a build target whose aot policy compiles @aot functions, which is every policy but off.
local array = require("nupp.mem.array")
local span = require("nupp.mem.span")
local simd = require("nupp.simd")
local function lane(borrows input: span.Span<float>): number
if species = simd.vectors(array.float) then
if species.lanes <= #input then
return species:load(input, 1):extract(1)
end
end
return 0.0
end
return {lane = lane}Related: NUPP2905. Reference: docs/learn/performance/ahead-of-time/simd-programming.md#prefix-scans. Open the reported program in the playground.
NUPP2907#
A GPU kernel is outside the subset the GPU backend compiles.
local span = require("nupp.mem.span")
(target = "gpu")
local function scale(
exclusive output: span.WriteSpan<float>,
borrows input: span.Span<float>,
enabled: boolean
): nil
assert(#output == #input, "length mismatch")
for index = 1, #output do
output[index] = enabled and input[index] or 0.0
end
end
return {scale = scale}A kernel that runs on the GPU lowers to a shader, which has no call stack, no dynamic allocation and a narrower set of types than a CPU kernel. The message names the rule the kernel broke. Keep the kernel to the map and workgroup forms the GPU page describes, or run it on the CPU.
This applies only to a build target whose aot policy compiles @aot functions, which is every policy but off.
local span = require("nupp.mem.span")
(target = "gpu")
local function scale(
exclusive output: span.WriteSpan<float>,
borrows input: span.Span<float>,
factor: float
): nil
assert(#output == #input, "length mismatch")
for index = 1, #output do
output[index] = input[index] * factor
end
end
return {scale = scale}Related: NUPP2905. Reference: docs/learn/performance/ahead-of-time/gpu.md#inspection-and-limits. Open the reported program in the playground.
NUPP2908#
A const-specialized family cannot be compiled.
local function repeated<const N: integer>(value: number, count: N): number
local answer = value
for _ = 1, count as integer do
answer = answer + value
end
return answer
end
return repeated(1.0, 1) + repeated(1.0, 2) + repeated(1.0, 3) + repeated(1.0, 4) + repeated(1.0, 5)
+ repeated(1.0, 6) + repeated(1.0, 7) + repeated(1.0, 8) + repeated(1.0, 9)A const-generic @aot function compiles one body per closed set of constant arguments its callers use, from a checked source, and a module may demand only so many of them. Pass fewer distinct constants, or make the varying argument an ordinary parameter.
This applies only to a build target whose aot policy compiles @aot functions, which is every policy but off.
local function repeated(value: number, count: integer): number
local answer = value
for _ = 1, count do
answer = answer + value
end
return answer
end
return repeated(1.0, 1) + repeated(1.0, 2) + repeated(1.0, 3) + repeated(1.0, 4) + repeated(1.0, 5)
+ repeated(1.0, 6) + repeated(1.0, 7) + repeated(1.0, 8) + repeated(1.0, 9)Related: NUPP2905. Reference: docs/learn/performance/ahead-of-time/index.md#const-specialized-families. Open the reported program in the playground.
NUPP2909#
A file given to AOT compilation has no @aot function.
nupp aot inspects what the @aot functions in one file compile to, so a file without one has nothing to show. A build or a check that merely finds the word @aot in a file's text does not report this, which is why there is no example program here: only nupp aot reports it.
Related: NUPP2905. Reference: docs/learn/performance/ahead-of-time/index.md#annotation-guarantees.
NUPP2910#
A native-only @aot entry is used where Lua would execute it.
local array = require("nupp.mem.array")
local simd = require("nupp.simd")
local function twice(value: simd.Vector<float, simd.Preferred>): simd.Vector<float, simd.Preferred>
return value + value
end
local function fromLua(): simd.Vector<float, simd.Preferred>
local species = simd.species(array.float)
return twice(species:splat(1.0))
end
return {twice = twice, fromLua = fromLua}An @aot function whose signature carries a simd.Vector or a simd.Mask is native-only: a register holds such a value and Lua has nothing to hand through the parameter or receive from the result, so the build gives it no Lua wrapper and it is called only from another @aot function. Every use outside one -- a call, an argument, a return, a field store -- is refused where it is written, for a target whose policy compiles. The member the module exports it under and the binding another module imports it through are admitted, because the native call resolves through them without reading a Lua value. Move the call into an @aot function, or give the entry a span and scalar signature.
This applies only to a build target whose aot policy compiles @aot functions, which is every policy but off.
local array = require("nupp.mem.array")
local span = require("nupp.mem.span")
local simd = require("nupp.simd")
local function twice(value: simd.Vector<float, simd.Preferred>): simd.Vector<float, simd.Preferred>
return value + value
end
local function apply(exclusive output: span.WriteSpan<float>, borrows input: span.Span<float>): nil
assert(#output == #input, "length mismatch")
local species = simd.species(array.float)
for at, active in species:over(#input) do
species:store(output, at, twice(species:load(input, at, active)), active)
end
end
return {twice = twice, apply = apply}Related: NUPP2905. Reference: docs/learn/performance/ahead-of-time/simd-programming.md#native-only-entries. Open the reported program in the playground.
NUPP2911#
A field of a native-only aggregate is assigned.
A struct holding a simd.Vector or a simd.Mask is a native-only aggregate: a value the register file holds, as the vectors in it are. It has no field stores, in native code or as ordinary Lua, so the two never disagree about who sees a write. Construct a new aggregate with the field changed.
Related: NUPP2910. Reference: docs/learn/performance/ahead-of-time/simd-programming.md#native-only-aggregates. Open the reported program in the playground.
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. An erased generic parameter also carries no runtime identity, even when its bound does.
Related: NUPP2122. Reference: docs/learn/language/types/interfaces.md#is-at-runtime. 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.
cdef 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.
Related: 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 of two limits, and the message says which. A function may capture at most sixty names from around it: one reading that many things from its scope is usually reading a record it could take as one argument instead, so pass what varies, or gather what it reads into one value and capture that. And a function, the file's top level included, may hold at most two hundred locals at once: past that, gather related values into a table, or split the function or the module in two. A declared module whose records, structs, interfaces, cdef items and exported functions do not fit is generated with those moved into a table, so only the locals it wrote count against its top level, and nupp check reports a top level with more of those than fit.
Any other spelling of this is a bug in the compiler rather than in the program. nupp lsp artifact --kind lua FILE prints the Lua it wrote even when that Lua does not load, which is where the reason can be read. 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.
NUPP3006#
The compiler or host cannot provide a required language capability.
The compiler building an artifact and the host it targets fix the language representations and runtime modules it may use: the in-memory browser compiler has no native layout provider, and a browser host has no native runtime modules. Choose a target that supports the operation, or express it with a portable contract. Runtime provider selection cannot add a compiler representation.
Related: NUPP3005.
NUPP3013#
Authored syntax is outside the Lua 5.1 compatibility profile.
The project-wide compat=lua51 requirement checks source before optimization. Types can erase, but runtime const, operators, jumps and literals specific to LuaJIT are rejected even in dead code. Use ordinary Lua 5.1 syntax; this profile does not invoke a lowerer.
Reference: docs/learn/projects/portability/libraries.md#lua-51-source-compatibility.
NUPP3014#
A runtime identity or representation requires LuaJIT.
Compatibility follows resolved standard-library identities through aliases and checks the stock Lua 5.1 surface. Native bit, FFI, exact-width representations, string.buffer and VM table helpers are unavailable. Explicitly checked pure-Lua libraries remain usable; no automatic provider substitution occurs.
Reference: docs/learn/projects/portability/libraries.md#lua-51-source-compatibility.
NUPP3015#
A runtime dependency or calling contract cannot satisfy compatibility.
Every runtime import needs checked source. A declaration alone, an opaque native module or non-enumerable dynamic code cannot certify compatibility. Literal loadstring source is checked recursively without execution. Cleanup cannot depend on yieldable protected calls or variadic xpcall arguments. Follow the reported dependency to an explicit Lua 5.1 implementation.
Reference: docs/learn/projects/portability/libraries.md#lua-51-source-compatibility.
NUPP3016#
Ordinary generated output does not satisfy Lua 5.1 syntax.
The final compatibility guard checks generated helpers as well as authored code. A rejected construct needs shared portable emission before this profile can accept it. Changing the compatibility flag never selects the legacy Lua 5.1 lowerer.
Reference: docs/learn/projects/portability/libraries.md#lua-51-source-compatibility.
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.
NUPP4002#
A file is not formatted the way nupp fmt writes it.
There is one formatted spelling of a file, and nupp fmt --check and a project-wide nupp fmt report every file that differs from it. Nothing is wrong with the program; run nupp fmt --write on the file to produce the spelling.
Related: NUPP4001. Reference: docs/learn/tooling/formatter.md#formatter-modes.
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.