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 spelling

Text 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 clause

That 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.
  • file and code, 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 NUPP2119

Every 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, with ok set and equal to the status being 0.
  • ok false always comes with at least one error in diagnostics that 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#

  1. Run nupp check --json, and read ok first.
  2. Apply a complete fix from diagnostics[].fixes when its title matches the intended repair. Never take individual edits out of one.
  3. Read docs on a diagnostic, or run nupp 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, and nupp reference --section <anchor> takes the docs pointer itself. Either is a few hundred words where the chapter is thousands.
  4. Inspect related locations before changing cross-file declarations or ownership transfers.
  5. Use nupp lsp inspect, nupp lsp definition, and nupp lsp references when more semantic context is needed.
  6. Re-run the check after each edit group, then nupp test --json before committing, which reports each failing test's name, message, file, and line rather than a wall of progress text.
  7. Read timing.compiledModules and timing.slowest when a check of an unchanged project is slower than expected. compiledModules = 0 is the authoritative answer that nothing was redone, and slowest ranks 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.

Reported
local s = 'oops
return 1

A 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.

Accepted
local s = 'oops'
return s

Related: NUPP1002, NUPP1005. Open the reported program in the playground.

NUPP1002#

A required token is missing.

Reported
local function f(): integer
    if true then
        return 1
end

A 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.

Accepted
local function f(): integer
    if true then
        return 1
    end
    return 0
end

Related: NUPP1004. Open the reported program in the playground.

NUPP1003#

A required name is missing.

Reported
local record Point
    x: number
end

local function show(p: Point): number
    return p.
end

return show

Certain 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.

Accepted
local record Point
    x: number
end

local function show(p: Point): number
    return p.x
end

return show

Related: NUPP1002, NUPP1004. Open the reported program in the playground.

NUPP1004#

A required expression is missing.

Reported
local record Point
    x: integer
end

local bare = new Point

return bare

An 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().

Accepted
local record Point
    x: integer
end

local made = new Point(x = 1)

return made

Related: NUPP1002, NUPP1003. Open the reported program in the playground.

NUPP1005#

Another syntax or recovery constraint failed.

Reported
return 1
local x = 2

The 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.

Accepted
local x = 2
return x

Related: 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.

Related: NUPP2105, NUPP2106.

NUPP1007#

A docblock parameter does not exist.

Reported
--- @param valeu the value
local function keep(value: integer): integer
    return value
end
return keep(1)

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.

Accepted
--- @param value the value
local function keep(value: integer): integer
    return value
end
return keep(1)

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.

Reported
--- Reads a name.
--- @raises when name is empty
local function read(name: string): string
    if name == "" then error("empty name") end
    return name
end
return read

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.

Accepted
--- Reads a name.
--- @raises string when name is empty
local function read(name: string): string
    if name == "" then error("empty name") end
    return name
end
return read

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.

Reported
local count: integer = "twelve"
return count

An annotated binding accepts only values of that type. The annotation is the claim; the initializer has to keep it.

Accepted
local count: integer = 12
return count

Related: NUPP2002, NUPP2006. Open the reported program in the playground.

NUPP2002#

A returned value does not fit the declared result.

Reported
local function length(): integer
    return "long"
end

return length

A 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.

Accepted
local function length(): integer
    return 4
end

return length

Related: 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.

Reported
local lpeg = require("lpeg")
local pattern = lpeg.P("a") ^ "two"
return pattern

Arithmetic, 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.

Accepted
local lpeg = require("lpeg")
local pattern = lpeg.P("a") ^ 2
return pattern

Related: NUPP2005, NUPP2006. Open the reported program in the playground.

NUPP2004#

The field does not exist on that type.

Reported
local record Point
    x: number
    y: number
end

local function show(p: Point): number
    return p.z
end

return show

A 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.

Accepted
local record Point
    x: number
    y: number
end

local function show(p: Point): number
    return p.x
end

return show

Related: NUPP2005, NUPP2119. Open the reported program in the playground.

NUPP2005#

A call's callee has no callable type or __call contract.

Reported
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.

Accepted
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.

Reported
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 draw

Named 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.

Accepted
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 draw

Related: 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.

Reported
local function keep(value: integer): integer
    return value
end
return keep(1, 2)

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.

Accepted
local function keep(value: integer): integer
    return value
end
return keep(1)

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.

Reported
const count = 1
count = 2
return count

The const modifier on a local or a module field is a promise that nothing after its initializer changes it. An ordinary assignment or compound assignment to that name is refused, whether the binding is a local declared const, a builtin the prelude declares constant, or a field a module recorded as const on first assignment.

Accepted
local count = 1
count = 2
return count

Related: NUPP2001, NUPP2602. Reference: docs/learn/language/types/primitives.md#const. Open the reported program in the playground.

NUPP2009#

A property view does not grant the requested access.

Reported
local out: {@writeonly value: string} = {}
local value = out.value
return value

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.

Accepted
local out: {@writeonly value: string} = {}
out.value = "ready"
return out

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.

Reported
local value = select(0, 1, 2)
return value

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.

Accepted
local first, second = select(1, 1, 2)
return first, second

Related: 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.

Reported
local input: number = 0.1
local value: float = input as float
return value

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.

Accepted
local input: number = 0.1
local value: float = nupp.math.f32.narrow(input)
return value

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.

Reported
local byte: uint8 = 1
return byte

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.

Accepted
local byte: uint32 = 1
return byte

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.

Reported
local type Small = nupp.types.range(integer, 0, 10)
local function of(v: integer): integer
    return nupp.admit(v)
end
return of

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.

Accepted
local type Small = nupp.types.range(integer, 0, 10)
local function of(v: integer): Small
    return nupp.admit(v)
end
return of

Related: NUPP2011, NUPP2422. Open the reported program in the playground.

NUPP2101#

A type name cannot be resolved.

Reported
local count: Count = 1
return count

A type name must be a built-in, a declaration visible in this file, or a member reached through its module. Declare the type, require the module that exports it, or correct its spelling.

Accepted
local type Count = integer
local count: Count = 1
return count

Related: 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.

Related: NUPP2104, NUPP2101.

NUPP2104#

Two project globals declare the same value name.

A global record or global struct declaration puts a value in _G as well as a type in the project's global type namespace. When two files each declare a global of the same name whose value side collides, an unqualified use of that name as a value cannot know which declaration it means, and both declaring locations are reported. Like NUPP2102, this is a whole-project condition: a file that declares the name resolves its own declaration directly and never reaches this check, so a single isolated file cannot exhibit it on its own.

Related: NUPP2102, NUPP2120.

NUPP2105#

A name is not any value the checker or project can account for.

Reported
local function shout(text: string): string
    return txet .. "!"
end
return shout

A 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.

Accepted
local function shout(text: string): string
    return text .. "!"
end
return shout

Related: NUPP2120, NUPP2101. Open the reported program in the playground.

NUPP2106#

An exported declaration needs a type annotation.

Reported
local m = {}

function m.double(n)
    return n * 2
end

return m

What 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.

Accepted
local m = {}

function m.double(n: integer): integer
    return n * 2
end

return m

Related: NUPP2119. Open the reported program in the playground.

NUPP2107#

A dispatch leaves members of a closed set unhandled.

Reported
local type Color = 'red' | 'green' | 'blue'

local function name(c: Color): string
    if c == 'red' then return "red" end
    return "other"
end

return name

When 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.

Accepted
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 name

Reported 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.

Reported
@allow(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.

Accepted
local count: integer = 1
return count

Related: NUPP2001, NUPP2112. Reference: docs/reference/lints.md#local-suppressions. Open the reported program in the playground.

NUPP2109#

A name written to select a parameter or captured value resolves to nothing.

Reported
local record R
    x: integer
end

local function f(a: R): b is R
    return true
end

return f

Several 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.

Accepted
local record R
    x: integer
end

local function f(a: R): a is R
    return true
end

return f

Related: 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.

Reported
local record R
    x: integer
end

local function f(a: string): a is R
    return true
end

return f

A 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.

Accepted
local record R
    x: integer
end

local function f(a: R): a is R
    return true
end

return f

Related: 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.

Reported
@inline
local function f(): nil
end

return f

An @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.

Accepted
local function f(): nil
end

return f

Related: 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.

Reported
@jit
local x = 1

return x

A 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.

Accepted
@jit
local function f(): nil
end

return f

Related: NUPP2111, NUPP2115. Reference: docs/reference/annotations.md#attachment-targets. Open the reported program in the playground.

NUPP2113#

A reserved built-in annotation is applied before its behavior is implemented.

A built-in definition may carry a reservation naming the feature it is waiting on, ahead of the feature existing. Applying that name still parses and resolves, since the definition is real, but is refused with what it is reserved for rather than silently accepted and doing nothing. No shipped built-in currently carries a reservation -- the mechanism exists for a name registered ahead of its implementation, as @jit itself once was.

Related: NUPP2111, NUPP2112.

NUPP2114#

@annotationValue or @ref is used outside an annotation-definition field, or a definition's own shape is invalid.

Reported
local record Plain
    @annotationValue
    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.

Accepted
@annotation(targets = {"record"})
local record Plain
    @annotationValue
    x: string
end

Related: 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.

Reported
@syntax(42)
local value = 1

return value

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.

Accepted
@syntax("json")
local value = 1

return value

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.

Reported
local interface Named
    name: string
end

local interface Tagged
    associated type Tag is Named = integer
end

return Tagged

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.

Accepted
local interface Named
    name: string
end

local interface Tagged
    associated type Tag is Named = Named
end

return Tagged

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.

Reported
local record Other
end

local record Bad is Other
end

return Bad

Only 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.

Accepted
local interface Other
end

local record Bad is Other
end

return Bad

Related: 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.

Reported
local record Pair
    first: string
    first: integer
end

return Pair

A 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.

Accepted
local record Pair
    first: string
    second: integer
end

return Pair

Related: NUPP2004, NUPP2123. Open the reported program in the playground.

NUPP2119#

A declaration does not say where it lives.

Reported
record Loose
    id: integer
end

return Loose

A 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.

Accepted
local record Loose
    id: integer
end

return Loose

Related: NUPP2106, NUPP2120. Open the reported program in the playground.

NUPP2120#

A project module is used without being required.

Reported
local answer: number = mathutil.double(21)
return answer

A 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.

Accepted
local mathutil = require("mathutil")
local answer: number = mathutil.double(21)
return answer

Reported 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.

Reported
local value: (number, string)
return value

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.

Accepted
local function pair(): (number, string)
    return 1, "one"
end
return pair

Related: NUPP2010. Reference: docs/learn/language/types/packs.md#syntax. Open the reported program in the playground.

NUPP2122#

A refinement cannot be enforced.

Reported
local interface Circle
    kind: string

    satisfies |self| -> tostring(self.kind) == "circle"
end

return Circle

A 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.

Accepted
local interface Circle
    kind: string

    satisfies |self| -> self.kind == "circle"
end

return Circle

Related: 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.

Reported
local record I64
    v: integer
    metamethod __add: function(self: I64, other: I64): I64
end

local x = new I64(v = 1)
setmetatable(x, {__add = "not a function"})

return x

A 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.

Accepted
local record I64
    v: integer
    metamethod __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 x

Related: 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.

Reported
local type Impossible = string & number
return Impossible

A 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.

Accepted
local type NamedValue = {name: string} & {value: number}
return NamedValue

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.

Reported
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.

Accepted
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.

Reported
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.

Accepted
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.

Reported
local interface Reader
    associated type Item
end

local record Lines is Reader
end

return Lines

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.

Accepted
local interface Reader
    associated type Item
end

local record Lines is Reader
    associated type Item = string
end

return Lines

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.

Reported
local record Box
    associated type Item
end

return Box

An 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.

Accepted
local record Box
    type Item = string
end

return Box

Related: 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.

Reported
local interface Reader
    type Item = string
    associated type Item
end

return Reader

A 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.

Accepted
local interface Reader
    type Unit = string
    associated type Item
end

return Reader

Related: 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.

Reported
local function read(value: {name: string}.['missing']): string
    return value
end

return read

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.

Accepted
local function read(value: {name: string}.['name']): string
    return value
end

return read

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.

Reported
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.

Accepted
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.

Reported
local function read(value: `${string}`): string
    return value
end

return read

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.

Accepted
local type Choice = 'a' | 'b'
local function read(value: `${Choice}`): string
    return value
end

return read

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.

Reported
local type Loop<T> = Loop<T>
return Loop

Type 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.

Accepted
@comptime
local function DeepElement(T: type): type
    while nupp.types.kind(T) == 'array' do
        T = nupp.types.elements(T)[1]
    end
    return T
end
local value: DeepElement({{integer}}) = 1
return value

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.

Reported
local function first<T>(x: T): T.Item
    return nil as any
end

return first

T.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.

Accepted
local interface Reader
    associated type Item
end

local function first<T is Reader>(x: T): T.Item
    return nil as any
end

return first

Related: 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.

Reported
local interface Holds
    associated type Value
end

local record Direct is Holds
    associated type Value = Direct.Value
end

return Direct

An 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.

Accepted
local interface Holds
    associated type Value
end

local record Direct is Holds
    associated type Value = string
end

return Direct

Related: 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.

Reported
local spans = require("nupp.mem.span")

local record Forged is spans.Span<int32>
end

return Forged

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.

Accepted
local spans = require("nupp.mem.span")

local storage = ffi.new<int32[4]>()
local view = spans.fromFixedCarray(storage, 4)

return #view

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.

Reported
local n: integer = 2
local value = switch n do
    case 1 + 1 -> 'two'
    else -> 'other'
end
return value

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.

Accepted
local n: integer = 2
local value = switch n do
    case 2 -> 'two'
    else -> 'other'
end
return value

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.

Reported
local n: number = 1
local value = switch n do
    case 1, 1.0 -> 'one'
    else -> 'other'
end
return value

Static cases compare Lua values, not source spellings. Numerically equivalent literals such as 1 and 1.0 are therefore duplicates.

Accepted
local n: number = 1
local value = switch n do
    case 1 -> 'one'
    else -> 'other'
end
return value

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.

Reported
local n: 1 = 1
local value = switch n do
    case 2 -> 'two'
    else -> 'other'
end
return value

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.

Accepted
local n: number = 1
local value = switch n do
    case 2 -> 'two'
    else -> 'other'
end
return value

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.

Reported
local type Mode = 'read' | 'write'
local mode: Mode = 'read'
local value = switch mode do case 'read' -> 1 end
return value

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.

Accepted
local type Mode = 'read' | 'write'
local mode: Mode = 'read'
local value = switch mode do
    case 'read' -> 1
    case 'write' -> 2
end
return value

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.

Reported
local value = do local answer = 1 end
return value

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.

Accepted
local value = do local answer = 1; yield answer end
return value

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 =.

Reported
module geom.shapes

local shapes = {}

export record Point
    x: number
end

export = shapes

A 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.

Accepted
module geom.shapes

local shapes = {}

record shapes.Point
    x: number
end

export = shapes

Related: 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.

Reported
local record Box<string>
    value: string
end

return Box

Builtin 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.

Accepted
local record Box<T>
    value: T
end

return Box

Related: 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.

Reported
local record Pair<A, B>
    a: A
    b: B
end

local half: Pair<integer> = nil as any

return half

An 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.

Accepted
local record Pair<A, B>
    a: A
    b: B
end

local both: Pair<integer, string> = nil as any

return both

Related: 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.

Reported
local record Box
    value: integer
end

local function held(v: Box | string): boolean
    return not v is Box
end

return held

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.

Accepted
local record Box
    value: integer
end

local function held(v: Box | string): boolean
    return not (v is Box)
end

return held

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.

Reported
local function make<T>(): T
    error('not reached')
end

local value = make()

return value

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.

Accepted
local function make<T>(): T
    error('not reached')
end

local value = make<string>()

return value

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.

Reported
local function name(): string
    return "x"
end
local function first(): string?
    local value = name() or return
    return value
end
return first()

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.

Accepted
local function name(): string?
    return "x"
end
local function first(): string?
    local value = name() or return
    return value
end
return first()

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.

Reported
local struct Loop
    me: Loop
    n: int32
end
return Loop

A 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.

Accepted
local struct Loop
    me: Loop*?
    n: int32
end
return Loop

Related: 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'.

Reported
local record Point
    x: integer
end

local p = Point{x = 1}

return p

Records 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.

Accepted
local record Point
    x: integer
end

local p = new Point(x = 1)

return p

Related: 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.

Reported
cdef function process(values: {number}): int32
return process

A 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.

Accepted
cdef function process(values: voidptr): int32
return process

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.

Reported
local struct P
    {integer}
    x: float
end
return P

The 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.

Accepted
local struct P
    x: float[4]
end
return P

Related: NUPP2201, NUPP2205. Open the reported program in the playground.

NUPP2205#

An array part is not written as a single element type.

Reported
local record R
    {x: integer}
    y: integer
end
return R

The 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.

Accepted
local record R
    {integer}
    y: integer
end
return R

Related: NUPP2204, NUPP2201. Open the reported program in the playground.

NUPP2206#

Only a record or a struct can be constructed.

Reported
local interface Named
    name: string
end

local n = new Named(name = "ada")

return n

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.

Accepted
local interface Named
    name: string
end

local record User is Named
    name: string
end

local n = new User(name = "ada")

return n

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.

Reported
local record Point
    x: integer
end

local p: Point

return p.x

local 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.

Accepted
local record Point
    x: integer
end

local p: Point = new Point(x = 0)

return p.x

Related: 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.

Reported
local record Account
    name: string
    balance: number

    constructor(self, name: string)
        self.name = name
    end
end

return Account

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.

Accepted
local record Account
    name: string
    balance: number

    constructor(self, name: string)
        self.name = name
        self.balance = 0
    end
end

return Account

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.

Reported
local h = require('nupp.mem.heap')
local a = h.allocate(ffi.typeof<int32>(), 1)
return a.pointer

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.

Accepted
local h = require('nupp.mem.heap')
local a = h.allocate(ffi.typeof<int32>(), 1)
return a:read()[1]

Related: NUPP2004, NUPP2202. Reference: docs/learn/language/types/records-and-structs.md#private-fields. Open the reported program in the playground.

NUPP2301#

cheader's arguments are not the literal form it takes.

Reported
local mini = cheader("mini" .. ".h")
return mini

cheader 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.

Accepted
local mini = cheader("mini.h")
return mini

Related: 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.

Reported
local mini = cheader("missing.h")
return mini

cheader 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.

Reported
local ffi = require("ffi")
ffi.cdef[[ this is not C ]]
return 0

A 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.

Accepted
local ffi = require("ffi")
ffi.cdef[[ int mini_add(int a, int b); ]]
return 0

Related: 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.

Reported
local ffi = require("ffi")
ffi.cdef[[ struct CstA { int x; }; ]]
local p = ffi.new("struct CstNoSuch")
return p

ffi.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.

Accepted
local ffi = require("ffi")
ffi.cdef[[ struct CstA { int x; }; ]]
local p = ffi.new("struct CstA")
return p

Related: 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.

Reported
local record Point
    x: number
    y: number
end

local points = carray(Point, 4)
return points

carray(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.

Accepted
local struct Point
    x: float
    y: float
end

local points = carray(Point, 4)
return points

Related: NUPP2402, NUPP2201. Reference: docs/learn/runtime/c-interop/index.md#typed-ffi-operations. Open the reported program in the playground.

NUPP2402#

layoutof was asked about something with no C layout.

Reported
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.

Accepted
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.

Reported
local soa = require("nupp.mem.soa")
local ffi = require("ffi")

local rows = soa.allocate(ffi.typeof<int32>(), 4)
return rows

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.

Accepted
local soa = require("nupp.mem.soa")
local ffi = require("ffi")

local struct Point
    x: float
    y: float
end

local rows = soa.allocate(ffi.typeof<Point>(), 4)
rows:close()

Related: NUPP2402, NUPP2009. Open the reported program in the playground.

NUPP2410#

A comptime block reads or writes a binding it cannot reach.

Reported
local n = 5
local value: integer = comptime do
    return n
end
print(value)

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.

Accepted
local value: integer = comptime do
    return 5
end
print(value)

Related: NUPP2411, NUPP2412. Open the reported program in the playground.

NUPP2411#

A construct or library member is unavailable at comptime.

Reported
local value: number = comptime do
    return math.random()
end
print(value)

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.

Accepted
local value: number = comptime do
    return math.floor(7 / 2)
end
print(value)

Related: NUPP2410, NUPP2412. Open the reported program in the playground.

NUPP2412#

A comptime evaluation exceeded a bound or used a value it cannot.

Reported
local value: string = comptime do
    return tostring({})
end
print(value)

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.

Accepted
local value: string = comptime do
    return tostring(5)
end
print(value)

Related: NUPP2410, NUPP2411. Open the reported program in the playground.

NUPP2413#

A comptime result has no literal spelling.

Reported
local value: {{integer}} = comptime do
    local shared = {1}
    return {shared, shared}
end
print(value)

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.

Accepted
local value: {{integer}} = comptime do
    return {{1}, {1}}
end
print(value)

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.

Reported
local record Position
    x: number
    y: number
end

const codec = comptime 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.

Accepted
local record Position
    x: number
    y: number
end

const codec: nupp.reflect.FieldCodec<Position> = comptime 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.

Reported
@comptime
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.

Accepted
@comptime
local function answer(): integer return 42 end
return comptime do return answer() end

Related: NUPP2414, NUPP2416. Open the reported program in the playground.

NUPP2416#

A comptime result or worker message exceeds its bound.

Reported
return comptime do
    local values = {}
    for index = 1, 12000 do values[index] = index end
    return values
end

Comptime 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.

Accepted
return comptime do
    local values = {}
    for index = 1, 10 do values[index] = index end
    return values
end

Related: NUPP2415, NUPP2412. Open the reported program in the playground.

NUPP2417#

A PEG intrinsic call or finalized grammar is malformed.

Reported
const Bad: nupp.peg.Peg<integer> = comptime do
    return nupp.peg.compile("('')*")
end
return Bad

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.

Accepted
const Good: nupp.peg.Peg<integer> = comptime do
    return nupp.peg.compile("('x')*")
end
return Good

Related: NUPP2415, NUPP2416. Open the reported program in the playground.

NUPP2418#

nupp.reflect was applied to something other than a resolvable type.

Reported
local value = 1
const Bad: nupp.reflect.FieldCodec<any> = comptime do
    return nupp.reflect.fieldCodec(nupp.reflect(value))
end
return Bad, value

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.

Accepted
local record Item
    value: integer
end
const Good: nupp.reflect.FieldCodec<Item> = comptime do
    return nupp.reflect.fieldCodec(nupp.reflect(Item))
end
return Good

Related: NUPP2415, NUPP2416. Open the reported program in the playground.

NUPP2419#

A layout intrinsic has no target, a bad shape, or an unknown field.

Reported
local struct Value
    n: int32
end
return comptime do return nupp.sizeof(Value) end

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.

Accepted
local struct Value
    n: int32
end
local size = ffi.sizeof<Value>()
return size

Related: NUPP2410, NUPP2416. Open the reported program in the playground.

NUPP2420#

A comptime type function rejected its own application.

Reported
@comptime
local function Positive(): type
    return nupp.types.error("always fails")
end
local value: Positive() = 1
return value

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.

Accepted
@comptime
local function Positive(): type
    return nupp.types.number
end
local value: Positive() = 1
return value

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.

Reported
local leaked = nupp.types.string
return leaked

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.

Accepted
@comptime
local function Self(): type
    return nupp.types.string
end
local value: Self() = "ok"
return value

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.

Reported
local type Empty = nupp.types.range(integer, 10, 0)
local value: Empty = 5
return value

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.

Accepted
local type Small = nupp.types.range(integer, 0, 10)
local value: Small = 5
return value

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.

Reported
local pointer = ffi.cast<cstring>('a' .. 'b')
print(pointer)

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.

Accepted
local text = 'a' .. 'b'
local pointer = ffi.cast<cstring>(text)
print(pointer)

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.

Reported
@unsafe do
    local callback = function()
    end
    local pointer = ffi.cast<voidptr>(callback)
    local handle = nupp.pin(pointer, callback)
end

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.

Accepted
@unsafe do
    local pointer = ffi.cast<voidptr>(8)
    print(pointer)
end

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.

Reported
local ready = true
local pending = !ready
return pending

The 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.

Accepted
local ready = true
local pending = not ready
return pending

Reported 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.

Reported
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 run

The loop-invariant-closure lint reports a function literal built inside a loop body that reads nothing from the iteration, so every built copy is the same function and building one afresh each time buys nothing. LuaJIT has no recorder for the bytecode that builds a function, so a loop containing one aborts trace recording every time it is tried and is eventually blacklisted, running interpreted from then on however hot it gets. Declare the function once above the loop and pass its name. Default level is warning; the sibling jit-loop-closure reports the case where the function does read the iteration and cannot be lifted.

Accepted
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 run

Reported by the loop-invariant-closure lint, category suspicious, at warning by default. Configure it by name or by category.

Related: NUPP2515, NUPP2707. Reference: docs/reference/lints.md#loop-invariant-closure. Open the reported program in the playground.

NUPP2506#

A documented function can raise without saying when.

Reported
--- Reads a name.
--- @param name the name to read
local function read(name: string): string
    if name == "" then error("empty name") end
    return name
end

return read

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.

Accepted
--- Reads a name.
--- @param name the name to read
--- @raises string when name is empty
local function read(name: string): string
    if name == "" then error("empty name") end
    return name
end

return read

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.

Reported
local function shout(text: string): string
    local prefix = "> "
    return text .. "!"
end

return shout

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.

Accepted
local function shout(text: string): string
    local prefix = "> "
    return prefix .. text .. "!"
end

return shout

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.

Reported
local function double(value: number): number
    return value * 2
end

double(21)

return double

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.

Accepted
local function double(value: number): number
    return value * 2
end

local answer = double(21)
print(answer)

return double

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.

Reported
local function classify(primary: boolean, fallback: boolean): nil
    if primary then
        print("primary")
    else
        if fallback then
            print("fallback")
        end
    end
end

return classify

The 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.

Accepted
local function classify(primary: boolean, fallback: boolean): nil
    if primary then
        print("primary")
    elseif fallback then
        print("fallback")
    end
end

return classify

Reported 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.

Reported
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 erased

A projection is checked as any when inference never worked out what answers it, which is the feature declining to say anything rather than saying the call is right. Reported once per call and member, where the erasure happened, because a parameter erased that way accepts whatever it is given.

Accepted
local interface Holds
    associated type Item
end

local record Lines is Holds
    associated type Item = string
end

local function held<T is Holds>(x: T): T.Item
    return nil as any
end

local kept = held(new Lines())

return kept

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.

Reported
local record Point
    x: integer
    y: integer
end

local p = new Point(1, 2)

return p

A 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").

Accepted
local record Point
    x: integer
    y: integer
end

local p = new Point(x = 1, y = 2)

return p

Reported 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.

Reported
local function current(): integer return 1 end

@deprecated(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.

Accepted
local function current(): integer return 1 end

@deprecated(replacement = "current")
local function legacy(): integer return current() end

return current()

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.

Reported
cdef function printf(format: cstring, ...): int32

local function report(value: int32): nil
    printf("%d", value)
end

return report

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.

Accepted
cdef function printf(format: cstring, ...): int32

local function coldReport(value: int32): nil
    printf("%d", value)
end
jit.off(coldReport)

local function report(value: int32): nil
    coldReport(value)
end

return report

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.

Reported
module geom.shapes

local record Coordinate
    x: number
end

export record Point
    coordinate: Coordinate
end

The 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.

Accepted
module geom.shapes

export record Coordinate
    x: number
end

export record Point
    coordinate: Coordinate
end

Reported 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.

Reported
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.

Accepted
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.

Reported
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 useTwice

An 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.

Accepted
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 useOnce

Related: NUPP2602, NUPP2603. Open the reported program in the playground.

NUPP2602#

An ownership operation is invalid for the value's current state.

Reported
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.

Accepted
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.

Reported
cdef function begin_request(): affine(voidptr)
local request = begin_request()
return 0

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.

Accepted
cdef function begin_request(): affine(voidptr)
cdef function submit_request(takes request: voidptr)
local request = begin_request()
submit_request(request)
return 0

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.

Reported
cdef struct resource
    value: int32
end

local function advance(borrows base: resource*): resource*
    return base + 1
end

return advance

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.

Accepted
cdef struct resource
    value: int32
end

local function advance(borrows base: resource*): resource* borrows (base)
    return @unsafe (base + 1)
end

return advance

Related: NUPP2001, NUPP2004, NUPP2602. Open the reported program in the playground.

NUPP2605#

Adjusting a value pack would discard an affine value.

Reported
local record Resource
end
local function release(takes value: Resource): nil
end
local function acquire(): affine(Resource, release)
    return new Resource()
end
acquire()
return acquire

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.

Accepted
local record Resource
end
local function release(takes value: Resource): nil
end
local function acquire(): affine(Resource, release)
    return new Resource()
end
local value = acquire()
release(value)
return acquire

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.

Reported
local function forward<T>(borrows value: T): T preserves value
    return value
end
return forward

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.

Accepted
local function forward<T>(takes value: T): T preserves value
    return value
end
return forward

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.

Reported
local function pair(exclusive a: table, exclusive b: table): nil end
local value = {}
pair(value, value)

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.

Accepted
local function pair(exclusive a: table, exclusive b: table): nil end
pair({}, {})

Related: NUPP2602, NUPP2608, NUPP2609. Reference: docs/learn/runtime/ownership/borrowing.md#regions-and-loop-carried-capabilities. Open the reported program in the playground.

NUPP2608#

A rooted value escapes its permitted lifetime.

Reported
local function leak(borrows value: table): table
    return borrow(value)
end
return leak

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.

Accepted
local function view(borrows value: table): table borrows (value)
    return borrow(value)
end
return view

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.

Reported
local record Resource
end
local function close(takes value: Resource): nil
end
local function open(): affine(Resource, close) return new Resource() end
local function run(again: boolean): nil
    local value = open()
    while again do
        nupp.drop(value)
    end
end
return run

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.

Accepted
local record Resource
end
local function close(takes value: Resource): nil
end
local function open(): affine(Resource, close) return new Resource() end
local function run(again: boolean): nil
    local value = open()
    if again then
        print('once')
    end
    nupp.drop(value)
end
return run

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.

Reported
local m = {}
function m.forward<T>(value: T): T
    return value
end
return m

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.

Accepted
local m = {}
function m.forward<T>(takes value: T): T preserves value
    return value
end
return m

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.

Reported
local function erase(value: affine(table)): any
    return value
end
return erase

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.

Accepted
local function erase(takes value: affine(table)): any
    return @unsafe nupp.release(value)
end
return erase

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.

Reported
local record Resource
end
local function begin(): affine(Resource) return new Resource() end
local cell = nupp.manage(begin())

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.

Accepted
local record Resource
end
local function close(takes value: Resource): nil
end
local function open(): affine(Resource, close) return new Resource() end
local cell = nupp.manage(open())

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.

Reported
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.

Accepted
local record File is nupp.Closeable
    function flush(exclusive self): nil
end
    function close(takes self): nil
end
end
local owner = nupp.manage(new File())
local erased: any = owner:alias()
local recovered = nupp.recoverAlias(erased)
local file = recovered and recovered:downcast<File>()

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.

Reported
local record Resource
end
local record Other
end
local function release(takes value: Other): nil
end
local function acquire(): affine(Resource, release)
    return new Resource()
end
return acquire

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.

Accepted
local record Resource
end
local record Other
end
local function release(takes value: Resource): nil
end
local function acquire(): affine(Resource, release)
    return new Resource()
end
return acquire

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.

Reported
local record Buffer
    value: string
end

local function closeBuffer(takes value: Buffer): nil
end

local function bad(borrows source: Buffer): affine(Buffer, closeBuffer)
    return source
end

return bad

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.

Accepted
local record Buffer
    value: string
end

local function ok(borrows source: Buffer): Buffer borrows (source)
    return source
end

return ok

Related: 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.

Reported
local record Buffer
    value: string
end

local function bad(takes value: Buffer): Buffer borrows (value)
    return value
end

return bad

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.

Accepted
local record Buffer
    value: string
end

local function ok(borrows value: Buffer): Buffer borrows (value)
    return value
end

return ok

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.

Reported
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.

Accepted
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.

Reported
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 bad

A 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.

Accepted
local record Buffer
    value: string
end

local function ok(borrows left: Buffer, borrows right: Buffer): (Buffer borrows (left, right), integer)
    return left, #right.value
end

return ok

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.

Reported
local function release(takes id: integer): nil end
local function owned(): affine(integer, release) return 0 end
local function step(_state: nil, _control: integer?): integer? return nil end
for n in step, nil, owned() do end

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.

Accepted
local function step(_state: nil, _control: integer?): integer? return nil end
for n in step, nil, 0 do end

Related: 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.

Reported
cdef function visit(
    borrows values: const int32* countedBy(missing), count: uint64
)

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.

Accepted
cdef function visit(
    borrows values: const int32* countedBy(count), count: 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.

Reported
local function wait(): nil
    coroutine.yield()
end

@nosuspend do
    wait()
end

A @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.

Accepted
local function finish(): nil
end

@nosuspend do
    finish()
end

Related: 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.

Reported
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.

Accepted
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.

Reported
goto inside
with owner = acquire() do
    ::inside::
end

Entering 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.

Accepted
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.

Reported
cdef function printf(format: cstring, ...): int32

@jit
local function hot(): nil
    printf('%d', 1)
end

@jit promises that a function contains no variadic FFI call and does not pass a Lua callback into C. Move the boundary into a function disabled with jit.off, or remove @jit when tracing is not required.

Accepted
cdef function printf(format: cstring, ...): int32

local function cold(): nil
    printf('%d', 1)
end

jit.off(cold)

Related: NUPP2502, NUPP2514. Reference: docs/learn/runtime/c-interop/index.md#jit-sensitive-c-boundaries. Open the reported program in the playground.

NUPP2710#

A non-allocating region can reach allocation.

Reported
local function build(): nil
    local values = {}
end

@noalloc do
    build()
end

A @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.

Accepted
local function update(): nil
    local n = 1
end

@noalloc do
    update()
end

Related: 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.

Reported
local function fail(): nil
    error('failed')
end

@noraise do
    fail()
end

A @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.

Accepted
local function finish(): nil
end

@noraise do
    finish()
end

Related: NUPP2112, NUPP2710. Reference: docs/learn/language/effects.md#allocation-and-raising-regions. Open the reported program in the playground.

NUPP2801#

A derive provider name is unknown or duplicated.

Each @derive argument names one resolved exported comptime provider, and an identity may occur only once across every derive annotation on the record.

Related: NUPP2113, NUPP2802.

NUPP2802#

A generated derive member conflicts with the declaration.

A derive never overrides a written instance or static member. Remove the written member or remove the provider so the behavior has one owner.

Related: NUPP2801, NUPP2115.

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.

Reported
@jit
@aot
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.

Accepted
@aot
local function hot(scale: number): number
    return scale * 2.0
end

Related: 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.

Reported
local record Point
    x: number
    @aot
    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.

Accepted
@aot
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
end

Related: 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.

Reported
@aot
local function total(scale: number): number
    local function double(x: number): number
        return x * 2.0
    end

    return double(scale)
end

The body of an @aot function is ordinary Nupp, but only the part of it the AOT IR can represent. The IR has no closure, table, interpolated string, vararg, goto, dynamic call, or unsafe operation, so a body containing one cannot compile ahead of time and says so here rather than failing in a backend. Rewrite the body without it, or remove @aot and let the function run as ordinary Nupp.

Accepted
local function double(x: number): number
    return x * 2.0
end

@aot
local function total(scale: number): number
    return double(scale)
end

Related: 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.

Reported
@aot
local function sign(value: number): number
    if value then
        return 1.0
    end
    return 0.0
end

return {sign = sign}

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.

Accepted
@aot
local function sign(value: number): number
    if value > 0 then
        return 1.0
    end
    return 0.0
end

return {sign = sign}

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.

Reported
local array = require("nupp.mem.array")
local span = require("nupp.mem.span")
local simd = require("nupp.simd")

@aot
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.

Accepted
local array = require("nupp.mem.array")
local span = require("nupp.mem.span")
local simd = require("nupp.simd")

@aot
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.

Reported
local span = require("nupp.mem.span")

@aot(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.

Accepted
local span = require("nupp.mem.span")

@aot(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.

Reported
@aot
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.

Accepted
@aot
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.

Reported
local array = require("nupp.mem.array")
local simd = require("nupp.simd")

@aot
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.

Accepted
local array = require("nupp.mem.array")
local span = require("nupp.mem.span")
local simd = require("nupp.simd")

@aot
local function twice(value: simd.Vector<float, simd.Preferred>): simd.Vector<float, simd.Preferred>
    return value + value
end

@aot
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.

Reported
local simd = require("nupp.simd")

local struct Pair
    re: simd.Vector<float, simd.Preferred>
    im: simd.Vector<float, simd.Preferred>
end

@aot
local function conjugate(z: Pair): Pair
    z.im = -z.im
    return z
end

return {conjugate = conjugate, Pair = Pair}

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.

Accepted
local simd = require("nupp.simd")

local struct Pair
    re: simd.Vector<float, simd.Preferred>
    im: simd.Vector<float, simd.Preferred>
end

@aot
local function conjugate(z: Pair): Pair
    return new Pair(z.re, -z.im)
end

return {conjugate = conjugate, Pair = Pair}

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.

Reported
local interface Drawable
    width: number
end

local record Sprite is Drawable
    width: number
end

local unknown: any = new Sprite(width = 1)

return unknown is Drawable

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.

Accepted
local interface Drawable
    kind: "drawable"
    width: number
end

local record Sprite is Drawable
    kind: "drawable"
    width: number
end

local unknown: any = new Sprite(kind = "drawable", width = 1)

return unknown is Drawable

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.

Reported
local record Widget
    id: integer
end

local struct Holder
    items: Widget*[3]
end

return Holder

Every 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.

Accepted
cdef struct Widget
    id: integer
end

local struct Holder
    items: Widget*
    count: int32
end

return Holder

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.

Reported
local record Widget
    id: integer
end

cdef function inspect(target: Widget*)

return inspect

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.

Accepted
cdef struct Widget
    id: integer
end

cdef function inspect(target: Widget*)

return inspect

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.

Reported
local record Widget
    id: integer
end

local w = ffi.new<Widget>()
return w

ffi.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.

Accepted
local struct Widget
    id: integer
end

local w = ffi.new<Widget>()
return w

Related: 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.