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, message, and range, plus fixes, notes, and related, which are always present and may be empty. The rest appear when there is something to say:

  • file and code, on anything with a source position and a code of its own.
  • help: a concrete repair direction, in one sentence.
  • related: labeled secondary ranges, including ranges in other files.
  • fixes: titled edit sets a tool can apply without reading the prose.
  • notes: context that points at no source, such as the trace classification a JIT trace check attaches to its finding.
  • lint: the lint name, when the code names one.
  • docs: the reference section covering the code, as a path and an anchor.

The language server converts the same data to UTF-16 LSP ranges, relatedInformation, diagnostic data, and code actions, so an editor and a terminal disagree about presentation and about nothing else.

Explaining a code#

nupp explain prints the rule behind a code, a program that reports it, the same program corrected, related codes, and a reference into these documents:

nupp explain 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 Source input could not be read.
NUPP1001 Invalid or unterminated lexical input.
NUPP1002 A required token is missing.
NUPP1003 A required name is missing.
NUPP1004 A required expression is missing.
NUPP1005 Another syntax or recovery constraint failed.
NUPP1006 Typed Nupp syntax appeared in plain Lua.
NUPP1007 A docblock names a parameter that does not exist.
NUPP1008 An annotated Lua type was recovered with reduced precision.
NUPP2xxx Type, declaration, lint, FFI, or ownership diagnostics.
NUPP3xxx Code generation cannot represent a checked construct.
NUPP4001 Formatting could not safely produce the requested result.
NUPP5xxx A development-time change requires a restart.
OPT-n An optimization pass reporting what it did or declined to do.

OPT-n is the one family that does not describe a problem. A pass reports one to say that it rewrote something, or that it looked at something and could not. The severity is always note, so a remark is reported and stepped over and never fails a build.

Remarks are off unless --remarks is passed, and they come from nupp build and nupp run rather than nupp check, which does not optimize. The code is stable across a pass being renamed, split, or merged, so it can be cited in a bug report or passed to -Zno-opt. See the performance guide for what each pass reports.

Dive deeper

An optimizer is the one part of the compiler whose silence is ambiguous. A pass that declined to inline a call and a pass that was never reached both produce a program with the call still in it, and nothing in the output says which happened. OPT-n is the answer a language owes its user when a declared intention did not reach the generated code.

Giving remarks codes rather than free text is what makes them usable twice. The same identifier that names a remark in a bug report turns the pass off through -Zno-opt, and it survives the pass being renamed or split, so neither use breaks when the optimizer is rearranged.

Codes by area#

A code is explained in full by the diagnostic index below, and in context by the page that owns the rule. This is the map from an area to that page:

Area Page Codes
Affine types Affine types NUPP2606
Ahead-of-time compilation ahead-of-time.md NUPP2901, NUPP2902, NUPP2903
Annotations Annotations NUPP2108, NUPP2112, NUPP2113, NUPP2119, NUPP2707, NUPP2901, NUPP2902, NUPP2903
Associated types Associated types NUPP2127, NUPP2128, NUPP2129, NUPP2134, NUPP2135, NUPP2511
C interop c-interop.md NUPP2201, NUPP2402, NUPP2403, NUPP2904
Checked spans nupp.mem.span NUPP2001, NUPP2004, NUPP2602, NUPP2604
Comptime Comptime NUPP2410 through NUPP2416, NUPP2419, NUPP2420, NUPP2421
Comptime types type-level-computation.md NUPP2001
Derives nupp.derive NUPP2810
Effect contracts Effect contracts NUPP2112, NUPP2710, NUPP2711
Files nupp.io.files NUPP2701
Formatter fmt.md NUPP4001
Generics Generics NUPP2003, NUPP2116, NUPP2122
Gradual typing strictness.md NUPP1006, NUPP1008, NUPP2105, NUPP2106
Hot reload Hot reload NUPP5001
Interfaces Interfaces NUPP2116, NUPP2117, NUPP2118, NUPP2136, NUPP3001
Intersections and overloads Intersections NUPP2124, NUPP2125, NUPP2126, NUPP2208
Lints Lints NUPP2107, NUPP2120, NUPP2501, NUPP2502, NUPP2504 through NUPP2515
Logging nupp.log NUPP2006
LuaJIT trace checking LuaJIT trace checking NUPP2502, NUPP2505, NUPP2514, NUPP2515, NUPP2707, NUPP2904
Math nupp.math NUPP2011, NUPP2012
Metamethods Metamethods NUPP2003, NUPP2005, NUPP2006, NUPP2007, NUPP2116, NUPP2117, NUPP2118
Modules Modules NUPP1002, NUPP2004, NUPP2101, NUPP2105, NUPP2119
Named and plucked arguments calls.md NUPP2004, NUPP2006, NUPP2125
Narrowing Narrowing NUPP2002, NUPP2109, NUPP2110
Overloads and overrides Overloads and overrides NUPP2118, NUPP2125, NUPP2126, NUPP2208
Ownership ownership.md NUPP2601, NUPP2602, NUPP2603, NUPP2606 through NUPP2615, NUPP2620
Primitive types Primitive types NUPP2001, NUPP2002, NUPP2004, NUPP2006, NUPP2106, NUPP2115
Property capabilities Property capabilities NUPP2009, NUPP2118
Records and structs records.md NUPP2118, NUPP2201, NUPP2202, NUPP2204, NUPP2205
Reflection Reflection NUPP2414, NUPP2415, NUPP2416, NUPP2418
Refinements Refinements NUPP2122
Structure-of-arrays storage Structure-of-arrays storage NUPP2009, NUPP2403
Suspension Suspension NUPP2603, NUPP2701, NUPP2702, NUPP2706
Switch expressions Switch expressions NUPP2137 through NUPP2142, NUPP3001
Target capability profiles Build system NUPP2904
Type packs Type packs NUPP2007, NUPP2010, NUPP2121, NUPP2605
Type system overview.md NUPP2001, NUPP2004, NUPP2011, NUPP2012, NUPP2105, NUPP2106
Unions Unions NUPP2001, NUPP2107, NUPP2138, NUPP2139, NUPP2140

A code appears in several rows when several rules can report it. NUPP2004 is one code for "that field is not there", and which page explains it depends on what was being reached for.

Repairs#

A fix is a title and a set of byte-ranged edits into one file, and applying it is all-or-nothing. Only unambiguous rewrites qualify: where a message lists alternatives, each alternative is its own fix rather than a guess between them.

The checker offers fixes for misspelled variables, type names, fields, methods, and metamethods; missing module qualifications and require statements; declaration visibility; a customary operator replaced with Lua's word; explicit casts for intended lossy narrowing; and the conversion that establishes a fixed-width value.

A diagnostic gives help rather than an edit when the compiler cannot choose a program on the author's behalf. Enum exhaustiveness cannot invent branch bodies, an ambiguous global cannot decide which public declaration should change visibility, and a resource without cleanup metadata cannot guess which function owns that responsibility.

Machine-readable output#

Every command whose result is data rather than a side effect takes --format json, with --json as its shorthand, and every command that takes it also takes --schema, which prints the JSON Schema of what --json writes and exits. The schema is declared beside the code that writes it and a test validates real output against it, so the two cannot drift.

Command --json reports
aot what every @aot function in one file lowered to
ast the lossless syntax tree
bc the bytecode of one file, instruction by instruction
build diagnostics, the target, and every path written
check diagnostics, and where the check's time went
clean the paths removed, or that would be
coverage the aggregate coverage summary
doc the resolved format, the output, and every path written
explain a code's rule and worked examples
export-c the header written and the declarations in it
fixpoint whether it reproduced, and why not
fmt unformatted, written, and failed, kept apart
import-c the module written and any warnings
init what the template resolved to and what it wrote
lints every lint, its level here, and its default
lsp per operation; each has its own schema
ownership-audit foreign pointer contracts and unsafe assertion sites
reference the reference, section by section
tasks the task list, or one task's configuration
test totals and a record per test, with file and line

nupp run is the exception. Its --json writes the --jit-aborts record as JSON instead of CSV, because the program's own output is the run's output.

Read ok before diagnostics. An empty list means the project is clean only when ok is true, since a run that could not use the manifest never reached a file and reports the same empty list.

Agent workflow#

  1. Run nupp check --json --strict, 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 is diagnostics empty when the command still failed?#

The run never reached a file. A manifest that could not be used ends a check before anything is parsed, and that answers with the same empty list a clean project does, which is why ok is a separate field. See Machine-readable output.

Why did @allow not silence an error?#

@allow reaches lints and nothing else, so naming a type error in one leaves the error standing and reports NUPP2108 for the name. See Lints for what counts as a lint.

Diagnostic index#

Every code the compiler reports specifically, with the rule it enforces and, where one is known, the program that reports it beside the same program corrected. nupp explain <code> prints the same thing in a terminal.

A code that resolves only through its family is not here. The families are the leading digit, and each one answers for every code under it.

Input#

NUPP0001#

A source file could not be read.

The path named on the command line, or reached from a require, could not be opened. Check the spelling and that the file is readable from the directory the compiler was run in.

Reference: docs/reference/diagnostics.md#code-families.

Syntax#

NUPP1001#

The lexer could not read a token to completion.

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. Reference: docs/reference/diagnostics.md#code-families. 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. Reference: docs/reference/diagnostics.md#code-families. 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. Reference: docs/reference/diagnostics.md#code-families. 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. Reference: docs/reference/diagnostics.md#code-families. 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, 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. Reference: docs/reference/diagnostics.md#code-families. Open the reported program in the playground.

NUPP1006#

The typed layer appears in a plain Lua file.

A .lua file is Lua. The toolchain will require, build and run one unchanged, so typed Nupp syntax cannot be written into it. Rename the file .nupp to mean it, or .g.nupp to keep the gradual layer with typed syntax available. Type comments are separate: recognized LuaCATS, EmmyLua, and typed LuaDoc comments are always imported.

Related: NUPP2105, NUPP2106. Reference: docs/reference/diagnostics.md#code-families.

NUPP1007#

A docblock parameter does not exist.

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: NUPP2506. Reference: docs/learn/tooling/documentation.md#doc-comments. Open the reported program in the playground.

NUPP1008#

An annotated Lua type lost precision during import.

Type comments in a .lua file are always read. A malformed, ambiguous, unsupported, or ownership-incomplete foreign type recovers at the smallest affected position, usually as any, and checking continues.

Related: NUPP1006, NUPP2101. Reference: docs/learn/projects/integrations/luacats.md#recovery.

NUPP1009#

A type dependency's declaration file was not imported.

A kind = "types" dependency is somebody else's source, pinned at a revision this project chose but did not write. A file in that tree the importer cannot read, parse, or check is skipped and named here; the declarations beside it are still imported, so one unreadable corner of an API does not cost the project the rest of it. Pin a revision this compiler can read, or report the construct that stopped it.

Related: NUPP1008, NUPP1006. Reference: docs/learn/projects/integrations/luacats.md#recovery.

Types, declarations, and rules#

NUPP2001#

A value does not fit the type it is bound to.

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. Reference: docs/reference/diagnostics.md#code-families. 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. Reference: docs/reference/diagnostics.md#diagnostic-index. 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. Reference: docs/reference/diagnostics.md#diagnostic-index. Open the reported program in the playground.

NUPP2005#

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

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 a gradual value's runtime shape is not yet known; anything else that is called is reported once, and its argument expressions are still checked so one mistake does not hide another.

Accepted
local function callable(): integer
    return 5
end
return callable()

Related: NUPP2003, NUPP2006. Reference: docs/reference/diagnostics.md#diagnostic-index. Open the reported program in the playground.

NUPP2006#

A call's arguments are not arranged in a way it can be given.

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. 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: nupp.math. 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.

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. Reference: docs/reference/diagnostics.md#diagnostic-index. Open the reported program in the playground.

NUPP2102#

Two project globals declare the same type name.

A global declaration is reachable project-wide with no require, so globals share one flat namespace across every file. When two files each declare a global type of the same name, an unqualified use of that name cannot know which declaration it means, and both declaring locations are reported. This is inherently a whole-project condition: within the file that declares a name, its own declaration is already in scope directly and this check is not reached, so a single isolated file cannot exhibit it on its own.

Related: NUPP2104, NUPP2101. Reference: docs/reference/diagnostics.md#diagnostic-index.

NUPP2104#

Two project globals declare the same value name.

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

Related: NUPP2102, NUPP2120. Reference: docs/reference/diagnostics.md#diagnostic-index.

NUPP2105#

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

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

An unknown name is gradual by default: it types as any and stays quiet, because it might resolve at runtime. Under --strict, or in any file the strict floor already applies to, an unknown name that is not a project module's basename is reported instead, with a spelling fix offered when a visible name is close enough to be the one meant.

This applies only under --strict, or in a .nupp file.

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

Related: NUPP2120, NUPP2101. Reference: docs/reference/diagnostics.md#diagnostic-index. 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. This is a strict rule: it is reported only under --strict, or when the manifest sets strict = true.

This applies only under --strict, or in a .nupp file.

Accepted
local m = {}

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

return m

Related: NUPP2119. Reference: docs/reference/diagnostics.md#diagnostic-index. Open the reported program in the playground.

NUPP2107#

A dispatch leaves members of a closed set unhandled.

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. 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. Reference: docs/reference/diagnostics.md#diagnostic-index.

NUPP2114#

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

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 something other than an 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 or struct declaration, because that is the only declaration kind able to state a contract for another to inherit. A record, struct, alias, or any other named value is refused there, even when its shape happens to match.

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, and unknown metamethods cannot describe one coherent contract.

Accepted
local record Pair
    first: string
    second: integer
end

return Pair

Related: NUPP2004, NUPP2123. Reference: docs/reference/diagnostics.md#diagnostic-index. Open the reported program in the playground.

NUPP2119#

A declaration does not say where it lives.

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. Reference: docs/reference/diagnostics.md#diagnostic-index. 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. 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 gradual.

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. 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 a gradual 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. 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. 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. 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
local comptime 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. 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. 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 block switch arm does not yield on every completing path.

Reported
local value = switch 1 do
    else -> do
        local answer = 1
    end
end
return value

A block arm produces the switch value with contextual yield value. Every path that does not return from the enclosing function must reach one, and no statement may follow it in the same path.

Accepted
local value = switch 1 do
    else -> do
        local answer = 1
        yield answer
    end
end
return value

Related: NUPP2140, NUPP2142. Reference: docs/learn/language/switch-expressions.md#switch-expressions. Open the reported program in the playground.

NUPP2142#

A switch is in a conditionally evaluated expression position.

The first lexical lowering lifts switches from statement-root eager expressions. Lifting one out of and, or, ??, a ternary arm, or guarded safe-navigation work would run it when the source does not, so that placement waits for general expression normalization. Bind the switch in a preceding statement instead.

Related: NUPP2140. Reference: docs/learn/language/switch-expressions.md#switch-expressions.

NUPP2143#

An export carrying a runtime value sits beside export =.

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 gradual and is not an access-control sandbox.

Reference: docs/learn/language/modules.md#internal-modules.

NUPP2201#

A struct field has no C-representable layout.

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. Calling a declaration used to be the construction, which meant the call was never the program's own: a record's __call was shadowed whenever the argument was a table, and a struct's ctype call was claimed outright. Saying construction in the source gives those hooks back. A record that does declare a __call contract is called, not reported.

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. Reference: docs/reference/diagnostics.md#diagnostic-index. Open the reported program in the playground.

NUPP2205#

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

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. Reference: docs/reference/diagnostics.md#diagnostic-index. 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 used to construct one where it was declared, which was a construction the source did not say. It no longer does, so the binding holds nil until something assigns to it, and reading it before that indexes nil at run time rather than yielding a value of the declared type. Strict code holds every annotation that does not admit nil to this, and a binding is assigned only once every path reaching the read assigned it: an assignment on one arm of an if, or inside a loop that may not run, is not one. Assign it first, or declare it optional if it is meant to start empty. A declaration file states what exists elsewhere and assigns nothing, so it is exempt.

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 header path is not a literal.

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

cheader reads the named header at compile time, so its path has to be known to the compiler before the program runs. It accepts only a literal string as its first argument, never a computed or run-time expression.

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 the gradual 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 field 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's field 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. Reference: docs/reference/diagnostics.md#diagnostic-index. Open the reported program in the playground.

NUPP2410#

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

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. Reference: docs/reference/diagnostics.md#diagnostic-index. 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. Reference: docs/reference/diagnostics.md#diagnostic-index. Open the reported program in the playground.

NUPP2412#

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

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. Reference: docs/reference/diagnostics.md#diagnostic-index. 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
local comptime 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
local comptime function answer(): integer return 42 end
return comptime do return answer() end

Related: NUPP2414, NUPP2416. Reference: docs/reference/diagnostics.md#diagnostic-index. Open the reported program in the playground.

NUPP2416#

A comptime result or worker message exceeds its bound.

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. Reference: docs/reference/diagnostics.md#diagnostic-index. 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. Reference: nupp.peg. 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. Reference: docs/reference/diagnostics.md#diagnostic-index. Open the reported program in the playground.

NUPP2419#

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

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. Reference: docs/reference/diagnostics.md#diagnostic-index. Open the reported program in the playground.

NUPP2420#

A comptime type function rejected its own application.

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

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 an unsafe cast of a Lua function to a C-derived callback type. The cast is legal only inside unsafe with an explicit retained owner, but LuaJIT registers the function as a permanent C callback once it happens, and no trace can compile through that call boundary for as long as it stays registered. Call jit.off on the registering function when the callback is intentional, or pass a plain pointer instead. Default level is warning, and a project may reconfigure or suppress it like any other lint.

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. Callers reading generated documentation then see the exceptional exit beside the ordinary result.

Accepted
--- Reads a name.
--- @param name the name to read
--- @raises 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: 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. 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. 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 gradual-projection lint, category suspicious, at warning by default. Configure it by name or by category.

Related: NUPP2134. Reference: docs/learn/language/types/associated-types.md. Open the reported program in the playground.

NUPP2512#

A record is built by field order rather than by naming its fields.

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

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.

NUPP2518#

a closed no-input function repeats work that comptime can erase.

A lint, reported at off by default and configurable by name or by category. The program parses but does not mean something the checker can accept, or means something its author probably did not intend.

This code has no example pair of its own. The rule above is its family's, which is what the compiler knows about it.

Reported by the prefer-comptime lint, category performance, at off by default. Configure it by name or by category.

Reference: docs/reference/lints.md.

NUPP2601#

A moved-from owner is used again.

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()
    drop(value)
    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()
    drop(value)
end

return useOnce

Related: NUPP2602, NUPP2603. Reference: docs/reference/diagnostics.md#diagnostic-index. Open the reported program in the playground.

NUPP2602#

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

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. Reference: docs/reference/diagnostics.md#diagnostic-index. 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 in unsafe. 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 to charge the access to, and needs an explicit unsafe do acknowledging that nothing checks the resulting address against a bound. Use a checked span instead when the bound should be proven rather than asserted.

Accepted
cdef struct resource
    value: int32
end

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

return advance

Related: NUPP2001, NUPP2004, NUPP2602. Reference: docs/reference/diagnostics.md#diagnostic-index. Open the reported program in the playground.

NUPP2605#

Adjusting a value pack would discard an affine value.

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

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

Related: NUPP2603, NUPP2608, NUPP2612. Reference: docs/learn/runtime/ownership/borrowing.md#dynamic-boundaries. Open the reported program in the playground.

NUPP2612#

A managed value is not self-contained.

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. 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. Open the reported program in the playground.

NUPP2614#

A managed alias is invalid or its cell is terminal.

An alias must carry the runtime brand of one managed cell. Closing or taking the payload leaves that cell as a permanent tombstone, and every copied alias fails before reading the payload.

Related: NUPP2612, NUPP2613. Reference: docs/learn/runtime/ownership/borrowing.md#dynamic-boundaries.

NUPP2615#

An affine value names an invalid terminal.

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 nosuspend function(takes Representation): nil. An affine interface declares exactly one such terminal 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.

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.

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
handle suspension with handler do
    ::inside::
end

Entering a handle suspension body from outside would bypass handler installation and its cleanup obligation. Move the label outside the region or move the jump inside it. Structured exits from the region are allowed.

Accepted
handle suspension with handler do
end
::outside::

Related: NUPP2701, NUPP2702. Reference: docs/learn/runtime/concurrency/suspension.md#handler-scope-follows-the-coroutine. Open the reported program in the playground.

NUPP2707#

A function required to compile crosses an unsupported JIT boundary.

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. 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. Reference: docs/reference/derives.md.

NUPP2802#

A generated derive member conflicts with the declaration.

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

Related: NUPP2801, NUPP2115. Reference: docs/reference/derives.md.

NUPP2803#

A field cannot participate in derived Debug.

Every visible Debug field needs a supported scalar or container type, another Debug record, an exact nupp.Debug contract, or the dynamic any fallback. A field cannot be both skipped and redacted.

Related: NUPP2802. Reference: docs/reference/derives.md#debug.

NUPP2806#

A record does not describe a supported JSON schema.

Derived JSON needs a closed supported field graph and consistent JSON options. Rename duplicate keys, default omitted fields, avoid erased generic records and unsupported values, and do not use int64 or uint64 as JSON numbers.

Related: NUPP2001. Reference: docs/reference/derives.md#json.

NUPP2807#

A derive dependency cycle has no valid lowering.

Recursive Debug and JSON graphs are supported. A package provider dependency cycle must still reduce to a closed recipe rather than require another unfinished provider result.

Related: NUPP2803, NUPP2806, NUPP2810. Reference: docs/reference/derives.md.

NUPP2808#

A derive exceeds a compiler generation limit.

Generated recipes, expressions, locals, upvalues, and emitted output are bounded compiler resources. Reduce the derived declaration or split the schema rather than depending on an unbounded generated function.

Related: NUPP2807. Reference: docs/reference/derives.md.

NUPP2809#

A comptime derive provider declaration or reference is invalid.

A public derive provider is an exported, nongeneric comptime function with the exact shape function(nupp.derive.Info): nupp.derive.Result, where I is one existing interface. @derive must name that resolved export.

Related: NUPP2411, NUPP2801. Reference: docs/reference/derives.md#package-providers.

NUPP2810#

A comptime derive provider failed or returned an invalid blueprint.

Providers execute in the bounded comptime worker and must return only nupp.derive.implement or nupp.derive.error. Closed builders reject foreign, cyclic, malformed, stale, and over-limit recipe graphs.

Related: NUPP2412, NUPP2808. Reference: docs/reference/derives.md#package-providers.

NUPP2811#

A derive recipe declares an invalid generated member.

A bare forward fills a bodyless callable requirement of Result. A member recipe may instead provide a bounded function signature. Neither form may replace written members or interface defaults.

Related: NUPP2118, NUPP2802. Reference: docs/reference/derives.md#closed-forwarding-recipes.

NUPP2812#

A forwarding argument does not exist on the generated method.

A forwarding recipe may pass its receiver, a named method parameter, a readable stored field, a bounded frozen constant, or a fresh array of those. Every name is resolved against the written owner and interface requirement.

Related: NUPP2004, NUPP2811. Reference: docs/reference/derives.md#closed-forwarding-recipes.

NUPP2813#

A runtime forwarding helper does not satisfy the generated call.

The helper must be an ordinary exported Nupp function. Forwarded argument types must fit its parameters, its result pack must satisfy the interface requirement, and it may not suspend where the requirement cannot.

Related: NUPP2001, NUPP2701, NUPP2812. Reference: docs/reference/derives.md#runtime-helpers.

NUPP2901#

One function is promised to two compilers.

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/neps/0009-ahead-of-time-compilation.md. Open the reported program in the playground.

NUPP2902#

@aot is attached to something that is not a whole function.

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/neps/0009-ahead-of-time-compilation.md. Open the reported program in the playground.

NUPP2903#

An @aot body contains a construct with no AOT IR representation.

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/neps/0009-ahead-of-time-compilation.md. Open the reported program in the playground.

NUPP2904#

A construct needs a capability the selected target does not have.

A target profile states what a destination admits: a dynamic loader, a tracing JIT, FFI callbacks, and a way to resolve a default-namespace symbol out of a static archive. @jit on a target with no trace compiler asserts a contract nothing there can meet, and a cdef ... from "name" on a target with no loader asks the platform to load something it cannot; both are refused where they are written rather than on the device. Reach the same C through the default namespace, which a static link puts in the process image, and drop @jit where nothing traces. Which target is selected is what decides this, so no isolated file exhibits it on its own: a target is described by the built-in profile for its triple or by a verified descriptor in a compiler pack, and a target nothing describes refuses nothing.

Related: NUPP2707, NUPP2901. Reference: docs/learn/projects/build.md.

Code generation#

NUPP3001#

is has nothing to test against this type.

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.

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. 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 limit: a function may capture at most sixty names from around it, and one that reaches past that cannot be loaded at all. A function reading that many things from its scope is usually reading a record it could take as one argument instead. Pass what varies, or gather what it reads into one value and capture that.

Any other spelling of this is a bug in the compiler rather than in the program, and nupp bc FILE shows the code it wrote. It is reported where the file is built rather than where the module is first required, because the line a VM would name belongs to generated text and the line here is the one that was written.

Related: NUPP3004. Reference: docs/reference/diagnostics.md#code-families.

NUPP3006#

The selected dialect cannot represent a required language capability.

A dialect fixes the language representations and operations an artifact may use. Choose a target that supports the operation, or express it with a portable contract. Runtime provider selection cannot add a compiler representation.

Related: NUPP3005. Reference: docs/learn/projects/portability/libraries.md.

NUPP3009#

The portable dialect cannot lower an authored jump.

An authored label or goto can jump across arbitrary lexical regions. Lowering a function containing one to Lua 5.1 would require a state machine, changing stack frames and error sites. Use structured control flow instead. Compiler-generated cleanup and continue transfers are structured and are lowered separately.

Related: NUPP3006. Reference: docs/learn/projects/portability/libraries.md.

NUPP3010#

A resolved prelude identity is outside the portable runtime surface.

The lua51 dialect runs on Lua 5.1 through 5.4 and LuaJIT, so an ambient prelude identity is available only when it has the same contract throughout that matrix or the compiler lowers it explicitly. A same-spelled local or ordinary table field is a different definition and remains application code.

Related: NUPP3006, NUPP3009. Reference: docs/learn/projects/portability/libraries.md.

Formatting#

NUPP4001#

The formatter's own output would change the token stream.

Formatting may only move whitespace, reflow comments, add or remove a schema-proven single-value annotation label, and parenthesize a method call's sugar-form arguments. Before writing its result the formatter re-lexes it and compares the token fingerprint against the input; a mismatch means the rewrite it just performed would have changed the program's meaning, so it discards the rewrite and leaves the file untouched instead of risking that change.

Reference: docs/reference/diagnostics.md#code-families.

Development runtime#

NUPP5001#

A running program cannot apply a change without a restart.

Hot reload replaces a watched module's callable bodies in place, but cannot rebind top-level executable statements or initializers, add, remove, rename or move a named declaration, change a callable's signature or capture set, change a record, struct, native or component layout, or repatch a function that took ownership of a cleanup-bearing capture. Loaded FFI declarations, native libraries, and values already built from them are likewise fixed for the life of the process. Any such edit is rejected with NUPP5001 and the previous generation keeps running until the process restarts.

Reference: docs/learn/projects/hot-reload.md#changes-that-require-restart.