Diagnostics — Stator

From docs/DIAGNOSTICS.md at 2f94229 View on GitHub

This file is the authoritative reference for all diagnostic codes emitted by Stator. Codes are never reused or renumbered — tests and external tools reference them by number, and renaming a shipped code breaks tooling.

Code ranges and classes

Stator divides diagnostics into six ranges, each with distinct semantics:

RangePurposeClassCharacteristic
STA0xxxCLI, configuration, toolchainerrorArgument parsing, missing files, compiler setup
STA10xxMode & typing policyneverBy-design rejections; permanent by mode choice (e.g., any in ts mode)
STA11xxRejected constructsneverUnsupported language features (v1 non-goals, ESM-only rules, etc.); permanent
STA12xxNot yetnot-yetPlanned features; message names the implementing phase
STA2xxxLowering / boundary errorserror, runtimeCast failures and unimplemented lowering operations are compile-time error; boundary-check failures are class runtime, raised by the emitted program rather than the compiler (STA2001, STA2002, STA2004, STA2006 abort; STA2008–STA2009 throw a catchable TypeError instead)
STA3xxxModule grapherrorCyclic imports, missing modules, resolution failures
STA4xxxInternal invariant violationsinternal errorAlways a compiler bug; message says so and asks for a report

Never vs. Not-yet: why this distinction matters

Never (STA10xx, STA11xx): By design, this will never compile in this mode/version. It is an explicit anti-goal. Tests and tooling use the code to enforce policy — for example, a test suite can assert "no code should hit STA1001 (any in ts mode)" as a style rule. A never code is shipped and stable.

Not-yet (STA12xx): Planned for a future phase. The code documents when it will be addressed. Tests use it to mark expected-fail cases before the feature exists. When the phase ships, not-yet tests flip to passing in the same commit, and the code remains stable.

Never reuse: once a code is shipped in a release, every variant (the message, the mode, the phase name) is frozen. Future refinements get new codes in the reserved ranges.

Output formats

Human format

file:line:col STA1234 [mode] message text

Example:

src/app.ts:42:10 STA1001 [ts] explicit 'any' is not allowed in ts mode; use 'unknown' instead

Fields:

  • file:line:col: source location (1-indexed); file uses the platform's separators (\ on Windows; docs/BUNDLER.md §5, "Paths")
  • STA1234: diagnostic code (5 chars, always)
  • [mode]: ts or js (braced, always)
  • message text: human-readable explanation (may name phases, hint fixes, etc.)

JSON format

--diagnostics=json turns the diagnostic stream into an array of diagnostic objects. This is not the same thing as stator explain --json, which reports a file verdict, every diagnostic of the stage that decided it, and per-function rows — that schema is specified in docs/MODES.md §6, and its verdict + code are what tests/subset/run.ts consumes. A diagnostic is "what went wrong"; an explain verdict is "how this construct would compile", and most constructs produce a verdict without producing any diagnostic at all.

[
  {
    "file": "src/app.ts",
    "span": { "start": 42, "length": 3 },
    "line": 42,
    "column": 10,
    "code": "STA1001",
    "class": "never",
    "mode": "ts",
    "message": "explicit 'any' is not allowed in ts mode; use 'unknown' instead"
  },
  {
    "file": "src/async.ts",
    "span": { "start": 105, "length": 5 },
    "line": 12,
    "column": 4,
    "code": "STA1201",
    "class": "not-yet",
    "mode": "ts",
    "message": "generators are not yet supported; planned for Phase 5 (the iterator protocol)",
    "phase": 5
  }
]

Schema:

  • file (string): source file path, with the platform's separators (as in the text form)
  • span (object): offset and length in the source, in UTF-16 code units (the units the TypeScript compiler API reports, so no re-encoding is needed)
    • start (number): 0-indexed offset
    • length (number): span length
  • line (number): 1-indexed line number
  • column (number): 1-indexed column number, in UTF-16 code units
  • code (string): the STA code — always present, on every diagnostic
  • class (string): one of error, never, not-yet, runtime, internal — the same value this document's tables give the code, so it is derivable from code alone and is included only to save consumers a lookup
  • mode (string): ts or js
  • message (string): human-readable text
  • phase (number): the delivering phase — present only when class is not-yet, and omitted (never null) otherwise, matching the explain schema's convention in docs/MODES.md §6

Note what is not here: construct and verdict. Those belong to explain, which reports on constructs whether or not anything is wrong with them. A diagnostic always means something is wrong, so its class already carries that information.


STA0xxx: CLI, config, toolchain

CodeMode(s)ClassMessage templateNotes
STA0002botherrorunknown mode "{mode}" (expected "ts" or "js") · unknown opt "{opt}"{origin} (expected 0, 1, 2, or 3)Triggered by an invalid --mode or --opt value. A bad STATOR_OPT names its origin ( in the environment variable STATOR_OPT), since the command line holds no --opt to blame (plan.md §9 Task 6.20, QA audit F11)
STA0003botherrorunknown command "{cmd}" (expected "build" or "explain")First non-flag argument is not build or explain
STA0004botherror{context} requires {missing} · {flag} requires {value}, not the flag "{next}" · {output} "{path}" is {input} "{path}" — refusing to overwrite an input · {output} "{path}" and {output} "{path}" name the same file — one output would overwrite the other · unit name "{name}" (--unit-name, config "unitName") may hold only letters, digits and _ · --emit=lib requires --emit-header=<h> (config "emitHeader") · --emit=lib requires -o lib<name>.a, not "{path}"Covers: entry file missing, -o path missing, build requires -o; --bundler (or the config's bundler) in ts mode: --bundler requires --mode=js (T12.1). Since Task 6.20 (QA audit F1, F10, F11): a value flag whose next argument starts with - (-o --emit=c is a forgotten path, not a file named --emit=c; a dash-led value takes the --flag=value spelling, e.g. --link=-lm); build() refusing any two of the entry, every program source file, -o, --emit-header and the --keep-c file that name one file (compared by inode when the file exists, else by canonical path), before anything is written, for in-process callers too; an explicit unit name outside ^[A-Za-z0-9_]+$, because sanitizing is not injective (my-lib and my_lib would share every symbol). The default unit, the entry basename, is still sanitized. Since Task 7.4 (plan-notes 342): --emit=lib without --emit-header (the header is the library's C surface), and with an -o not spelled lib<name>.a (-l<name> finds nothing else); its lib<name>.pc joins the outputs the aliasing check compares
STA0005botherrorunknown flag "{flag}" · flag "{flag}" does not apply to {command}Any unrecognized flag starting with -, and since Task 6.20 (QA audit F11) a known flag on the wrong command: explain takes --mode, --node, --json/--diagnostics, --bundler, --config and --no-config; build takes all of those except --json/--diagnostics, plus -o, --emit, --keep-c, --opt, --link, --emit-header and --unit-name. An inert flag would hide a typo in a script
STA0006botherrorunexpected argument "{arg}"Extra positional argument after entry file
STA0007botherrorentry file "{path}" does not existChecked before building the program, so the user gets a path error rather than a tsc "file not found"
STA0008botherrorC compiler "{cc}" not found — install clang (mise install, or macOS: xcode-select --install; Debian/Ubuntu: apt install clang) or set CCThe message must name a fix: a missing toolchain is the one build failure a user can always act on. The project pin is LLVM 21.1.8 via mise.toml; CC overrides the default clang
STA0009botherrorC compiler failed (exit {code}) — this is a compiler bug; keep the C with --keep-c and report it · C compiler "{cc}" exited 0 but wrote no "{out}" — check that CC names a working clangEmitted C that clang rejects is always Stator's fault, never the user's. Points at --keep-c because the C is the evidence. Since Task 6.21 (QA audit F4), a zero exit with no output file (a CC that is not clang, or a wrapper that swallows the call) is a failure too, checked after the object compile and the link
STA0011botherror{runtime|std} archive not found at {path} — run just -f {root}/justfile -d {root} {recipe}Linking needs packages/runtime/build/libjsrt.a, and a program whose module graph holds a std/* module also needs packages/std/build/libjsrt_std.a (recipe std, docs/STD.md §6); in a source checkout each is a build step the user has not run yet
STA0012botherror{tsc message}A diagnostic from the TypeScript checker itself, passed through with its location intact. One code for all of them on purpose: tsc already has a stable numbered error space (TS2345 and friends), and mirroring it into the STA space would create a second name for every message with nothing to keep the two in sync. In js mode a syntactic diagnostic or a JavaScript early error is always STA0012; a type-level refusal is STA0012 unless JS_MODE_RUNTIME_CODES lists it with a run-time answer, in which case it is dropped and the construct takes the dynamic path (docs/MODES.md §3 "Which checker diagnostics are fatal", plan-notes 297). In the bundler's vendor module a type-level complaint is never reported, except the four codes where Node throws at run time (TDZ reads, const assignment); docs/BUNDLER.md §6, plan-notes 320 Q4. Two checker answers on a std/… import are the std edge's refusals instead: TS2307 on a specifier (STA3002, or STA1214 for std/sync/std/thread), and TS2305/TS2724 on a std/fs Promise twin such as readTextAsync (STA1214, Phase 10; docs/STD.md §2, plan-notes 307)
STA0013botherrorthe TypeScript checker ran out of stack type-checking this program (plain tsc fails on it too) — return-type annotations on long chains of functions that infer their return types from each other shorten its inferencesrc/frontend/program.ts, around ts.getPreEmitDiagnostics; raised as a BuildError, so it ends build and explain with no span and no verdict, like STA0007. TypeScript infers an unannotated return type on demand, nesting one inference (about thirty JS frames) per link, and re-enters functions whose inference is still in progress, so a long chain exhausts the default V8 stack. Two known inputs: TypeScript 6.0.3's own _tsc.js in js mode (360 nested inferences inside one 1,285-function cycle of the checker; plan-notes 286, 287) and Test262's generator-prop-name-yield-expr.js (var yield plus a generator method keyed by [yield], an unbounded upstream recursion; plan-notes 213). Plain tsc dies on both. The checker is unusable after the throw, so nothing is reported past it. Matched on V8's call-stack message; any other exception from the checker is still STA4072. No decision test: like STA0007 it is a thrown error, not a per-construct verdict; packages/tests/unit/cli.test.ts pins it
STA0014jserrorbundler adapter '{name}' could not be loaded — install {package}, or build with --bundler=none ({reason})Emitted since T12.1 (plan-notes 296, 320; docs/BUNDLER.md §5). Raised by build and explain when the graph imports a package or holds a CommonJS project file, so the adapter is needed, and it cannot be had: --bundler (default vite, the vite-stator package) names a module that cannot be imported, or one whose default export (or named adapter) is not { name, bundle }. {reason} is Node's import error or the missing export. A graph that needs no bundling never loads the adapter and never raises it. A BuildError: no span, no verdict
STA0015jserrorthe bundle step failed: {bundler message}Emitted since T12.1 (plan-notes 296, 320; docs/BUNDLER.md §5). The adapter's bundle() rejected — an unresolvable package, a syntax error inside node_modules, a plugin failure — and the bundler's message is passed through unchanged, the STA0012 precedent: the bundler owns that error space. Also raised when bundle() answers something that is not a bundle (no code string, no version-3 map, no inputs list): the message names the missing part. A BuildError
STA0016botherror{path}:{line}:{column}: not valid JSON ({reason}) · {path}: cannot read the config file ({reason})stator.config.json (plan.md §9 Task 6.18, plan-notes 303; docs/CONFIG.md), raised by src/cli/config.ts. The file exists but cannot be read (a directory, no permission), or it is not strict JSON: comments and trailing commas are errors too. JSON.parse decides validity; jsonc-parser locates the failure, because V8's message omits the position for an unexpected token. A BuildError: no span, no verdict
STA0017botherror{path}: does not match the schema: {pointer}: {expected}; …The file parsed but fails packages/compiler/schema/stator.config.schema.json. Every violation is listed with its JSON pointer and what was expected (/mode: expected one of "ts", "js", /keepC: must be boolean). An unknown key is one too (/mdoe: unknown key, additionalProperties: false), so a typo cannot pass silently, and so is a key for --config, --no-config, --help or --version, which have none
STA0018botherror--config names a missing file: {path}--config=<path> (resolved against the current directory) does not exist. Without --config, a missing ./stator.config.json is not an error: Stator runs on flags and defaults
STA0019botherrorcannot write {output} "{path}": {errno} ({reason})plan.md §9 Task 6.20 (QA audit F2), src/cli/outputs.ts. A build output the user's file system refuses: ENOENT (the directory does not exist), ENOTDIR, EISDIR, EACCES, EPERM, EROFS, ENOSPC. Raised by the write of -o (--emit=c), --emit-header and the C file, and before clang runs, when -o's directory is missing or not writable or -o is a directory, so clang's own failure there is never blamed on the compiler (STA0009). Any other write error is still STA4072. A BuildError: no span, no verdict
STA0020botherror{step}: "{tool}" not found — install it (Linux: binutils; macOS: xcode-select --install) or set {CC|OBJCOPY|AR} · {step}: "{tool}" failed ({exit})plan.md §10 Task 7.4 (plan-notes 342), src/cli/library.ts. A tool the --emit=lib steps run after the unit's own compile: the prelink ($CC -r), the ELF symbol localization (objcopy) and the archiver (ar). The tool's own output is replayed before the diagnostic. Not STA0008 (that is the C compiler missing before any compile) and not STA0009 (the C it compiled is the runtime's and the unit's, already compiled once). A BuildError: no span, no verdict

STA4001 is also raised by the CLI (--version with an unreadable package.json), but it lives in the STA4xxx table below: the range is decided by what the code means — an internal invariant violation — not by which module happens to raise it.


STA10xx: Mode & typing policy (never)

These rejections are by design. They enforce the mode's philosophy: strict static typing in ts mode, flexibility in js mode.

CodeMode(s)ClassMessage templateNotes
STA1001tsneverexplicit 'any' is not allowed in ts mode; use 'unknown' insteadTriggered by bare any or as any cast in ts mode. Dynamic escape hatch; any in js mode compiles as dynamic value
STA1002tsnever.js files are not allowed in ts mode; use --mode=js or convert to .tsAttempt to load a .js file when mode is ts. Hint directs user to the right mode. allowJs is on in both modes so tsc keeps the file; the gate is what refuses it (plan-notes 141)
STA1003tsneverimplicit 'any' is not allowed in ts mode; add a type annotationUntyped variable, parameter, or expression in ts mode. unknown is the universal type if the type cannot be inferred

STA11xx: Rejected constructs (never)

These are language features that Stator does not support, either by design (ESM-only, v1 non-goal) or by architectural choice (no sloppy mode, no dynamic code generation).

CodeMode(s)ClassMessage templateNotes
STA1101tsnevereval() is not allowed in ts mode — it prevents static analysis and is a permanent design choiceeval in any form (bare call or globalThis.eval)
STA1103tsnevernew Function() is not allowed in ts mode — code generation is not supportedConstructor form of dynamic code generation
STA1104tsnevervar is not allowed in ts mode; use let or const insteadFunction-scoped hoisting is not supported; use block-scoped declarations. (js mode allows var)
STA1105tsneverarguments pseudo-variable is not allowed in ts mode; use rest parameters insteadarguments object does not exist in ts mode; use ...rest parameters
STA1106tsneverProxy is not supported in ts mode — metaprogramming is outside the static compilation modelnew Proxy() in any form
STA1107tsneverprototype mutation is not supported in ts mode — Object.setPrototypeOf, proto, and similar are incompatible with static shape analysisObject.setPrototypeOf(), assignment to __proto__, or prototype rewrites
STA1108tsneverdelete on class fields is not supported in ts mode — classes have fixed shape at compile timeRaised by gateDelete when the receiver of a delete lowers to a fixed-shape HObject. The class-field wording is exhaustive, not an example: TS2790 already requires the operand to be an OPTIONAL property, and an optional property is exactly what isDynamicShape uses to send an anonymous object type to the dynamic representation — so the only fixed shape a type-correct ts-mode delete can still name is a class instance, whose layout is the whole point of ts mode. js mode defers the same case instead (STA1205); an Unknown receiver that turns out fixed at run time aborts with STA2007
STA1109bothneverwith statement is not supported — ESM is strict; with is not allowed in strict modewith keyword
STA1110bothneverCommonJS {require() | module.exports | exports | __filename | __dirname} is not supported — {ts mode uses ES modules only | without --node, Stator uses ES modules only | under --node only a CommonJS file built through a bundler has it}A free read of one of Node's CommonJS bindings: a require no program declaration binds (a user's own function require is ordinary code), called or referenced; a free module.exports or exports (since plan-notes 315; typeof of either passes); a free __filename or __dirname (since plan-notes 316, replacing the retired STA1218). Narrowed by plan.md §11c T11.5 (re-scoped by T12.0, plan-notes 296, 312): in ts mode it is this code with or without --node, and in js mode without --node. Under --node a js-mode CommonJS project file goes to the bundler whole (T12.1, docs/BUNDLER.md §4) and never reaches this check: the bundle's require is createRequire(import.meta.url) and the frontend injects its __filename/__dirname (docs/MODES.md §6). The third message is a binding under --node in a file the bundler did not take (an ES module, where Node has none of them, or --bundler=none); an ES module's require comes from createRequire(import.meta.url). Fixtures subset_commonjs_require_*, subset_commonjs_file_*, subset_node_filename_*, subset_node_dirname_*
STA1111bothnever.tsx and .jsx files are not supported in v1 — JSX is a post-MVP goalAttempt to load .jsx or .tsx file
STA1112bothneverdecorators are not supported in v1Any @decorator on a class, method, accessor, property, or parameter. A v1 non-goal (plan §0 "Non-goals"), so it is a never code rather than a not-yet one: no phase promises to deliver it
STA1113bothnevera relative import must name the file's extension (./x.ts, ./x.js) — Node ESM does not resolve extensionless specifiersGate. The checker resolves Bundler-style (program.ts must, to be free of package.json metadata), which accepts ./x; Node, the differential ground truth, never does. Permanent because it is Node's own permanent rule, not a scheduling decision
STA1114bothneverunknown in an extern signature has no C representation; use a type from the ABI table (docs/FFI.md)Extern refusal: unknown would need boxing, and "no boxing for primitives" is only meaningful if the non-primitives are refused rather than silently boxed (plan §10 Task 7.1 step 2)
STA1115bothneverobject type in an extern signature has no C representation; use a branded pointer or CString (docs/FFI.md)Extern refusal: an object has no C layout the generated call could pass. A C struct the callee owns is a branded pointer; text is CString
STA1116bothneverarray type in an extern signature has no C representation; pass a pointer and a length as ABI types (docs/FFI.md)Extern refusal: a managed array is not a C buffer. The fix spells both halves in the signature; bytes are a Uint8Array, which has its own row and never reaches this code (plan.md §11c T11.3a)
STA1117bothneverfunction type in an extern signature has no C representation; v0 has no closure trampoline (docs/FFI.md)Extern refusal. C calls back in through Task 7.2's exported functions, never through a closure passed out (plan §10 out-of-scope table)
STA1118bothneverstring in an extern signature has no direct C mapping; use CString (borrow) or CStringOwned (transfer) (docs/FFI.md)Extern refusal: UTF-16 in, bytes out is a real conversion with a real allocation, so it is spelled at the declaration and never inferred. The two wrappers differ only in who owns the copy after return
STA1119bothnever{kind} in an extern signature is outside the ABI table (docs/FFI.md)Extern catch-all: struct by value, void as a parameter, CStringOwned or Uint8Array as a return (the Uint8Array row is parameter-only, plan.md §11c T11.3a), a void return combined with a value-signalling error convention, a malformed @statorLink pragma (or one in a file with no extern declaration, a second #include, a flag outside the link allowlist -l<name>/-L<dir>/-framework <name>/-Wl,-rpath,<dir> — named in the message — or a header path holding a ", a line break or NUL, which no #include can spell), and anything else the table does not name. (What the gate cannot see it does not claim: whether a void-mapped C symbol returns a value is unknowable from the declaration alone, so only the convention combination is refused.) A future task that widens the table splits a kind out with a NEW code, never by reusing this one
STA1120bothnevervariadic extern declaration is not supported — each call site is a different function type (docs/FFI.md)Extern refusal, permanent: printf-style varargs have no sound signature (plan §10 out-of-scope table)
STA1121bothneverextern declaration is only legal in a .d.ts file; move it there (docs/FFI.md)Extern placement: the marker (§1) is only read on declarations in declaration files, which is what makes Task 7.3's generator output a drop-in
STA1122bothneverexported {kind} '{name}' cannot be exposed to C in v0 — {reason} (docs/FFI.md)Export refusal (plan §10 Task 7.2 step 2): a class, a closure-valued const, a generic function, a rest/destructured parameter, an export default or re-export form, or any other exported shape that is not a non-generic function declaration. The kind names the shape; every variant is permanent for v0, so one code (unlike the extern table, whose kinds may split out as the table widens)
STA1123bothneverexported '{name}' is mutable or non-primitive state and cannot be exposed to C in v0 — {reason} (docs/FFI.md)Export refusal (plan §10 Task 7.2 step 2): let/var exports, destructured const lists, uninitialized consts, and consts of non-primitive type. Only const number/boolean (C type), CString (const char *), and string/null/undefined (jsrt_value) are exportable in v0
STA1125bothneveran Out<T> out-slot is used outside its contract (docs/FFI.md)Extern refusal: Out<T> needs a branded-pointer inner, lives only in locals, passes only to Out<T> parameters, and reads only through .value. Anything else — a non-brand inner, a T** spelled any other way, an Out flowing into a return, an object/array, a non-Out binding, or a dynamically-typed argument where only a proven slot is sound (an out-param WRITES through the pointer, so unlike a T* read it cannot trust bits) — is this code. Split out of STA1119 when the table widened to T** (docs/FFI.md §2)
STA1126bothneverexported '{name}' holds an Out<T> out-slot and cannot be exposed to C in v0 — out-slots are call-local rooted addresses (docs/FFI.md)Export refusal (plan §10 Task 7.2 step 2): an Out<T> value names a frame-local slot whose address dies with the call, so it has no C-observable meaning across the boundary
STA1130bothneverthe generator cannot express {kind} from {header}:{line} (docs/FFI.md)Generator scope refusal (plan §10 Task 7.3 step 5): macros/macro constants, enums, inline functions, globals, unions, and bitfields are rejected with construct + header line rather than silently skipped. Refusals that have a gate equivalent (out-params, int64, function pointers, varargs, …) cite the gate code instead
STA1124bothneverexported '{a}' collides with exported '{b}' on the C symbol '{symbol}' — rename one (docs/FFI.md)Export refusal (plan §10 Task 7.2 step 7): two exports mangling to one stator_<unit>_<name> is a compile error, never last-writer-wins

STA12xx: Not-yet (planned features)

These are language features on the roadmap. The message names the phase that will implement them.

CodeMode(s)ClassMessage templatePhaseNotes
STA1201bothnot-yetgenerators are not yet supported; planned for Phase 5 (the iterator protocol)5function* declarations and unnamed expressions landed with step 8 (plan-notes 152). The code now names the residue: generator methods, async generators, and for await. yield* is STA1214. User-iterable for-of landed (plan-notes 154)
STA1202jsnot-yetarguments pseudo-variable is not yet supported in js mode; planned for Phase 5 (stretch goal)5In js mode, arguments accesses require shape tables and runtime support. ts mode rejects it permanently (STA1105)
STA1203jsnot-yetProxy is not yet supported in js mode; planned for Phase 8 (dynamic tier)8Metaprogramming via proxies deferred to Phase-8 dynamic semantics
STA1204jsnot-yetprototype mutation is not yet supported in js mode; planned for Phase 8 (dynamic tier)8Shape-based optimizations and dynamic shape tables for prototype chains require Phase-8 support
STA1205jsnot-yetdelete on a statically-shaped object is not yet supported in js mode; planned for Phase 8 (dynamic tier)8NARROWED by plan §8 step 2a(c) (was "delete on properties"): the operator itself landed. On a dynamic-shape receiver delete is jsrt_delete, which answers the boolean by REBUILDING the object's shape chain from the root without the key — shapes are shared immortal metadata, so a node cannot be dropped in place, and replaying transitions lands two objects that deleted the same key on one shape. What still defers is a receiver whose shape is FIXED at compile time, which has no encoding for a missing slot; it lifts with the same dictionary-mode escape STA2004 names. A name that receiver's type does NOT declare is not this case: it lives in the overflow table, and delete o.grown compiles (docs/VALUE.md §4.24). ts mode refuses that case permanently (STA1108); an Unknown receiver that turns out fixed at run time aborts with STA2007
STA1206jsnot-yeteval() and new Function() are not yet supported in js mode; planned for Phase 8 (dynamic tier)8One code for both dynamic-code-generation constructs — they land together with the Phase-8 interpreter tier. ts mode rejects them permanently (STA1101, STA1103). Emitted since Phase 5 step 2; the interpreter that retires it is Phase 8
STA1207bothnot-yetimport() with a computed specifier is not yet supported; planned for Phase 8 (runtime module resolution)8Literal-specifier import() landed in Phase 5 step 10 (plan-notes 156): a namespace HObject whose field reads are the export globals, wrapped in Promise.resolve. The code stays allocated for the computed-specifier half and for a non-arity-1 call. Never reuse.
STA1208bothnot-yettop-level await is not yet supported: a module body has no resume point to suspend into5No longer emitted (Phase 5 step 9, plan-notes 155). The module body is an async unit. Kept allocated so the number is never reused.
STA1210bothnot-yet{member} needs the ICU feature build: rebuild with just runtime-intl and compile with STATOR_RUNTIME=intl · {member} is not yet supported; planned for Phase 5none / 5Date's RESIDUE code, the shape STA1211 has for RegExp: it names one MEMBER at a time rather than the class. Slice A landed 2026-09-01 (plan-notes 132) and slice B the same day (plan-notes 133), which leaves exactly the ICU-dependent string forms: toString and toTimeString, whose output appends the host zone's LONG display name ((Central European Summer Time)) that Node reads from ICU and libc's %Z cannot produce, and the three toLocale*. Those carry NO PHASE (2026-09-01, plan-notes 136): their blocker is the FEATURE BUILD, exactly the STA1215 model, and a phase number would tell the user to wait for something already shipped. The code's other sites -- arity and spread refusals on Date/Date.prototype/new Date -- are ordinary lowering work and name Phase 5, since Phase 4 owns nothing further here. toDateString is NOT among the residue: its output carries no zone name, so it landed with slice B. Date.now() and zero-argument new Date() are ACCEPTED, not deferred: nondeterminism is a proof problem, not an acceptance problem, and they prove through the determinism carve-out (tests/unit/date-clock.test.ts)
STA1211bothnot-yetRegExp.prototype.{member} is not yet supported; planned for Phase 88Moved from Phase 4 on 2026-09-01 (plan-notes 136). The array-with-properties blocker is CLOSED and the DATA PROPERTY surface landed with Task 4.2 (plan-notes 121), so Phase 4 owns nothing left under this code. Exactly two members remain, and neither is a builtin Phase 4 declined to write: compile is Annex B §B.2.4 legacy that RE-INITIALIZES an existing RegExp in place -- the mutate-a-built-object surface Phase 8 owns with STA1204 -- and unicodeSets is declared in lib.es2024 while tsconfig.json pins lib: ["es2023"], so the checker refuses the read before the gate is asked. String.prototype.matchAll landed with Phase 5 step 8 (plan-notes 151)
STA1212bothnot-yetSymbol is not yet supported; planned for Phase 55Well-known symbols as values, Symbol.for/Symbol.keyFor, and the global registry. Emitted for Symbol(...) and Symbol.iterator as a stored value. [Symbol.iterator]() as a class method name is admitted (plan-notes 154)
STA1213bothnot-yetBigInt is not yet supported; planned for Phase 55A second numeric type with its own arithmetic and no implicit coercion to number
STA1214bothnot-yet{construct} is not yet supported; planned for Phase {n}variesThe subset-boundary code. Emitted by the frontend gate for any construct outside the subset the compiler currently lowers — functions, for, classes, try, objects, arrays, template literals, &&/||, ==/!=, globals the compiler does not model (String, NaN, Math, globalThis, …), and the rest. The Node platform edge uses it too (plan.md §11c T11.5, docs/MODES.md §6): a Node built-in without --node names the flag and carries no phase, and under --node a module or member packages/node has not landed names Phase 11 (T11.6). Deliberately ONE code rather than one per construct: these are not deferred for different reasons, they are deferred for the same reason (the subset has not reached them yet) and they arrive together as the lowering ladder climbs. The construct is named in the message; the code names the boundary. Constructs that graduate move to static, not to their own code, so this row shrinks every phase and is deleted when the subset is complete

| STA1215 | both | not-yet | String.prototype.{op} needs the ICU feature build: rebuild with just runtime-intl and compile with STATOR_RUNTIME=intl | none | localeCompare, toLocaleLowerCase, toLocaleUpperCase. Not a scheduling deferral like the rest of this band and not permanent either: the code is written and the CHECK is a build configuration, so the message names the flag that turns it on rather than a phase. Collation and tailored casing are CLDR data (~10 MB), which the default runtime does not carry (Phase 4 Task 4.4). The diagnostic now carries NO phase field (2026-09-01, plan-notes 136): the row already said the message names a flag rather than a phase, but the object still set phase: 4, so the day Phase 4 closed it would have read as a promise about a finished release. Omitting the field is the no-phase sentinel (src/support/phases.ts) | | STA1216 | both | not-yet | Promise.prototype.{m} is not yet supported: use an async function, whose await and return do the same work / new Promise(executor) is not yet supported | 5 | then/catch/finally and arity-1 new Promise(executor) landed in Phase 5 step 11 (plan-notes 157). The code stays allocated for other prototype members and a constructor that is not arity 1. Never reuse. | | STA1217 | both | not-yet | using an extern function as a value is not yet supported (v0 has no C value representation for externs) | none | The FFI surface code (plan §10 Task 7.1 steps 1–2): emitted for @statorExtern-marked declarations the lowering does not cover. Since Phase 7 closed that is the extern-as-value position only — branded-pointer (T*) signatures compile borrow-only since step 6, header/link plumbing landed in step 7, and the optional call delivered at close-out (?. on an always-linked callee is a proven no-op) — never the call itself. No open phase owns a C value representation, so the code carries no phase (the src/support/phases.ts no-phase case). The eight STA1114–STA1121 refusals are never codes in the STA11xx table — design limits of the v0 surface, not schedule — so the ranges stay disjoint per §1.3 | | STA1218 | — | — | (retired) | — | Was "'{__filename|__dirname}' is not yet supported", the T12.1 step 7 placeholder until T11.5 gave the two names a meaning. Retired in plan-notes 316: under --node the vendor module's reads are injected, and every read that reaches the gate is STA1110, because an ES module has neither name in Node either. Never reuse this number | | STA1219 | both | not-yet | --emit=lib is not yet supported on Windows (docs/FFI.md §8) — use --emit-header for an object, linked with the runtime archive | none | plan.md §10 Task 7.4 (plan-notes 342). A Windows static library needs llvm-lib and a COFF symbol localization the Mach-O and ELF paths do not cover. Raised before compiling. No phase: Phase 7 has landed, and the Windows library has no task yet. A BuildError | | STA1220 | both | not-yet | unit "{unit}" has an export that crosses as jsrt_value, which --emit=lib does not support yet: the library's runtime is private, so a consumer has no runtime API to make or read one (docs/FFI.md §8) — keep plain C types, or use --emit-header without --emit=lib | none | plan.md §10 Task 7.4 (plan-notes 342). An export whose header needs jsrt_value.h (a parameter, result or const whose type is outside the export ABI table, such as unknown or a plain string, docs/FFI.md §8) under --emit=lib: the library hides every jsrt_* symbol, so the consumer could not call the accessors the type needs. Plain --emit-header still exposes it. A BuildError |

STA1209 is unallocated: STA1207/STA1208 are Phase 5 module features and STA1210+ are builtins whose residues now name Phase 5, Phase 8 or no phase at all, so the gap keeps room for a third module-level not-yet. Gaps are free; renumbering is not.


STA2xxx: Lowering & boundary errors

These errors occur during translation from TypeScript AST to HIR, or when checking type boundaries and FFI contracts.

CodeMode(s)ClassMessage templateNotes
STA2002bothruntimea write past the end of an array of a typed element would leave holes / an array length grown by assignmentThe second runtime-emitted diagnostic. Raised by jsrt_array_set — the write of an array whose element type is KNOWN — when the index is more than one past the end, and by the property-write entry when a.length = n names a larger length (plan-notes 310). ECMA-262 leaves the skipped indices genuinely ABSENT — console.log prints <2 empty items>, not undefined — and a typed element has no value that could stand for "absent", so it refuses loudly instead of filling with undefined and printing a different program's output. Narrowed by T11.4 step 4b (plan-notes 310, docs/VALUE.md §4.4): an array of an Unknown element now holds holes, so its indexed write (jsrt_array_set_sparse) and every untyped write (jsrt_dyn_index_set) fill the gap instead. In-range writes and the append idiom a[a.length] = v are unaffected. Not a compile error by construction: the index is a runtime value
STA2003botherrorgeneric instantiation is more than {n} deep at '{name}'; it does not terminateLowering. Monomorphization emits one function per concrete type tuple, so a generic that instantiates itself at a STRICTLY LARGER type — f<T> calling f<T[]> — has no fixed point and the queue never drains. The cap is what turns an infinite emit loop into a diagnostic; it is deliberately a user error, not an internal one, because the program really is not compilable ahead of time, and the depth (16) is far past any instantiation chain a person writes by hand
STA2001bothruntimeboundary check failed at {file}:{line}:{col} — expected {expected}, got {actual}The first runtime-emitted diagnostic, and since Task 3.5 a live one. Raised by jsrt_check_number/string/boolean when a value crossing a type boundary does not match the type the program claimed, and by jsrt_check_uint8array at every extern call that passes a Uint8Array (docs/FFI.md §2): a dynamic argument, or a js-mode value reaching a .ts parameter annotated Uint8Array, is checked before C reads the view's layout (plan.md §11c T11.3a). Spellings that reach it: a read of an unknown at a point the checker narrowed it (a typeof guard, an instanceof, an !== undefined); an as cast off an unknown; and a dynamic value reaching an annotated binding, parameter, or return — the mixed-graph edge (Phase 5 step 5). In js mode, also a value of ANOTHER concrete type reaching a .ts variable annotated number, string or boolean, at a declaration or assignment the checker refused with TS2322 (js mode suppresses that refusal but keeps the annotation; plan-notes 301), and the same at a call (TS2345 against a function, method or constructor parameter a .ts file annotated) or a return (TS2322 against a .ts function's annotated return; plan-notes 308). JSON.parse lands unknown and uses the same wrap at those sites. Not a compile error by construction: the boundary exists precisely because the type is unknowable statically (plan §0.2). The location is exact rather than reconstructed — the emitter bakes file:line:col in as a string literal, because it is the last party that still knows it. Failure ABORTS: everything emitted downstream of a check is entitled to trust the type completely, and a check that could be ignored would make that false
STA2004bothruntimea statically-shaped object cannot grow a new property; planned for Phase 8The third runtime-emitted diagnostic. NARROWED by plan.md §11c T11.4 step 7: a fixed-layout JSRTObject asked to grow a key its JSRTClass descriptor does not list no longer aborts — the key lands in the object's overflow table (docs/VALUE.md §4.24), so b.y = 2 against a layout that has no y grows it, and a frozen object throws Node's TypeError instead. Raised by jsrt_set_prop now only for the runtime's own layouts that share the JSRTObject prefix but have no table (a Map, Date, Promise, iterator, ...), and, worded "a class object cannot grow a new static", for a class object asked for a static it never declared. Not a compile error: whether a given value arrives fixed or dynamic is a runtime property. Lifts in Phase 8 with the dictionary-mode escape
STA2005bothruntime{builtin operation}; builtins cannot throw yet / Unicode case mapping is not yet supportedThe fourth runtime-emitted diagnostic, for a Task 4.2 builtin that meets machinery that has not landed. toUpperCase/toLowerCase on any code unit above 0x7F: real case mapping needs the Unicode tables that arrive with vendored libunicode (Task 4.3), and an ASCII-only mapping would be silently wrong for exactly the characters that made the string non-ASCII. Precedent: STA2002, loudly unimplemented beats wrong. (String.prototype.repeat/padStart/padEnd used to abort here too; since plan-notes 200 they join the pending-check protocol and throw a catchable RangeError matching Node — see step 2a(c).) console.log's placeholders use it too (plan.md §9 Task 6.24, plan-notes 337): a format string the gate could not read — not a literal — that meets a value the printer cannot render as Node does (%o of an object, %s of a function; since plan.md §9 Task 6.27 a user toString/valueOf and a Date are printed as Node prints them, while %s of an object whose inherited toString may come from a class named like a builtin, behind a capturing method, joins this list) aborts with STA2005: console.log … is not yet supported before anything is written; with a literal format the gate refuses the same cases as STA1214. Since plan.md §9 Task 6.23 it also names a native stack overflow the prologue guard cannot see, the runtime's own C recursion over deeply nested data: PANIC: STA2005 stack overflow, printed by the fault handler (docs/VALUE.md §4.25). Recursion in compiled code throws Node's catchable RangeError instead.
STA2006bothruntimecalling a non-function at {file}:{line}The fifth runtime-emitted diagnostic. Raised by jsrt_call_at when the callee is not a closure. The emitter bakes file:line in as a string literal (column is not on Span by design — docs/HIR.md BoundaryCheck). Builtin-internal calls still go through jsrt_call, which keeps the unlocated TypeError: callee is not a function. Until Phase 5 step 11 gives the runtime a catch around user code, the trap is fatal rather than a catchable TypeError. Arity padding is jsrt_arg (missing arguments are undefined); A dynamic method call (o.m(x) on an Unknown receiver) loads the name through the shape table (jsrt_get_prop, which finds hidden #method: slots and descriptor method tables, not just fields) and passes the receiver as argument zero when -- and only when -- the loaded closure declares one (has_receiver, docs/VALUE.md §4.16); a plain closure is called with user arguments only
STA2007bothruntimea statically-shaped object cannot lose a property; planned for Phase 8The sixth runtime-emitted diagnostic, and STA2004's mirror image: that one is a layout asked to GROW, this one is a layout asked to SHRINK, and both are the same absent encoding — a JSRTClass descriptor lists its fields at fixed offsets and has no way to spell one as missing. Raised by jsrt_delete when an Unknown receiver turns out at run time to be a fixed-shape object holding the key in a DECLARED slot; a key it grew lives in the overflow table, which can lose it (docs/VALUE.md §4.24, plan.md §11c T11.4 step 7). (An array element used to abort here too; since T11.4 step 4b it becomes a hole, docs/VALUE.md §4.4, and delete a.length throws Node's TypeError.) Where the receiver's type is known the gate refuses first, with STA1108 in ts mode and STA1205 in js. One fixed-shape delete does have a right answer and takes it instead: a FROZEN property is non-configurable, so delete on it raises Node's TypeError rather than aborting — the removal that cannot be represented is also the removal the spec forbids. Lifts with the same Phase 8 dictionary-mode escape STA2004 names
STA2008bothruntimeArray.prototype.{method} called on an incompatible receiver / Cannot read properties of {null|undefined} (reading '{method}')The seventh runtime-emitted diagnostic, and the first catchable one in this range: unlike STA2001/STA2002/STA2004/STA2006, which abort, this one throws a TypeError through the pending cell, so user try/catch observes it. Raised by jsrt_require_array when an Array.prototype entry receives a statically-array-typed value that is not an array at run time (var hoisting hands the op undefined; a lying annotation hands it anything) — the §0.2 boundary the dynamic representation exists to catch (plan.md §8 step 20). A nullish receiver throws Node's member-access wording, which is where Node throws for the same program; any other receiver throws the incompatible-receiver TypeError (Node's message names receiver source text, which compiled code no longer has). Reads and writes do not need a code of their own: a lying receiver degrades to the dynamic property path, which already throws Node's wording for nullish and misses to undefined otherwise
STA2009bothruntime{value} is not iterableThe eighth runtime-emitted diagnostic, catchable like STA2008. Raised by jsrt_require_array_iterable when a for-of over a statically-array-typed iterable meets a non-array at run time — the sync walk reads the array header directly instead of going through an entry, so the check is its own statement before the loop (plan.md §8 step 20). Node-exact for a nullish receiver; best-effort past it for STA2008's source-text reason. Only the array kind takes this path; every other static kind has its own walk and its own step

Everything else in this range is reserved; Phase 2+ populates it as lowering is implemented.


STA3xxx: Module graph

CodeMode(s)ClassMessage templateNotes
STA3001botherrorimport cycle detected: {cycle}Cyclic module imports. Includes the cycle path for diagnosis. Phase 2 implements module-graph scanning
STA3002botherrorunknown std module '{specifier}' — the std modules are {list}std/ is a reserved prefix, not a package (docs/STD.md §1): a name that is not one of packages/std/src/<name>.ts is a hard error, never a fall-through to a package lookup. std/sync and std/thread are not unknown — they are STA1214 naming Phase 10 (T10.2). Raised by the checker-diagnostic mapping (unresolved) or the gate (resolved to a non-module file such as std/internal/error), once either way. plan.md §11c T11.2
STA3003jserror'{name}' is ambiguous in {module}: two export * re-exports bind it differently, so it cannot be imported by nameAn import or re-export that names an AMBIGUOUS star export: two export * from of one module bind the name to two different bindings, and the module does not export it itself. ES drops such a name from the namespace and makes naming it a SyntaxError at link time (ResolveExport answers "ambiguous"); this is that refusal, at compile time. Reading the namespace (import * as ns) is fine, and ns simply has no such member. ts mode never reaches it: TypeScript reports the second export * as TS2308 (STA0012), which js mode drops so the ES answer can stand (plan-notes 302). plan.md §11c T11.5a
STA3004jserror{module} has no default export: it is an ES module ("type": "module") with no export statementA default import (import x from, import { default as x } from) or a re-export of default from a .js file that Node loads as an ES module but that has no import or export statement. Such a module has no exports, so ES raises a SyntaxError at link time (ResolveExport answers null, §16.2.1.6.3). TypeScript reports every other missing default itself (TS1192, STA0012), but it cannot tell this file from CommonJS and gives it a synthesized default. A .js file with no "type" and no module syntax is CommonJS to Node, whose default import is module.exports, so it is not refused here. ts mode never reaches it: a .ts file's missing default is TS1192. plan.md §9 Task 6.29 (plan-notes 350)
STA3005botherror'{eval|arguments}' cannot be an imported binding: a module is strict codeAn import declaration that binds eval or arguments (import { eval }, import { x as arguments }, import eval from, import * as eval from). A module is strict code, where either name as a binding identifier is an early SyntaxError (§13.1.1). TypeScript's binder checks every other declaration for this but not an imported binding, so the frontend raises it after the checker. It is a parse-phase error: it is reported before the gate and before any bundle step. plan.md §9 Task 6.29 (plan-notes 350)

STA4xxx: Internal errors (always a bug)

Every code here means the compiler contradicted itself. A user can do nothing about one except report it, so the message names the precise invariant that broke and asks for the input.

STA4002–STA4020 are the HIR verifier's (src/hir/verify.ts). The verifier runs on HIR the lowering just produced from source the gate just accepted, so a problem it reports always means the gate and the HIR disagree about the subset — see the invariant at the top of gateConstruct.

CodeMode(s)ClassMessage templateNotes
STA4001bothinternalpackage.json has no readable "version" fieldReading package.json to emit version failed
STA4002bothinternalidentifier '{name}' is not definedVerifier: reference with no binding in scope
STA4003bothinternalidentifier '{name}' assigned before declarationVerifier: assignment target never declared
STA4004bothinternalassignment target type {a} does not match value type {b}Verifier
STA4005——(retired)Was "if condition must be boolean". Retired once if applied ToBoolean: every value is truthy or falsy, so the rule rejected correct IR. Never reuse this number
STA4006——(retired)Was "while condition must be boolean". Retired with STA4005, same reason. Never reuse this number
STA4007bothinternalnumber-literal must have type 'number', got '{t}'Verifier
STA4008bothinternalstring-literal must have type 'string', got '{t}'Verifier
STA4009bothinternalboolean-literal must have type 'boolean', got '{t}'Verifier
STA4010bothinternalidentifier '{name}' has type '{a}' but is used as '{b}'Verifier
STA4011bothinternalarithmetic operand must be number, got {t}Verifier: left operand. unknown is accepted too -- the emitter wraps every arithmetic operand in ToNumber, which is defined on every value. What this catches is a KNOWN non-number
STA4012bothinternalarithmetic operand must be number, got {t}Verifier: right operand. Same rule as STA4011
STA4013bothinternalarithmetic operator result must be number, got {t}Verifier
STA4014——(retired)Was "comparison operands must have the same type". Retired at rung 2: Abstract Relational Comparison accepts any two primitives, and "10" < 9 is legal (and false). Never reuse this number
STA4015——(retired)Was "comparison operand must be number or string". Retired with STA4014: true < 2 is legal (and true). Never reuse this number
STA4016bothinternalcomparison result must be boolean, got {t}Verifier
STA4017——(retired)Was "strict equality operands must have the same type". Retired because null === undefined is legal code that answers false. Never reuse this number
STA4018bothinternalequality result must be boolean, got {t}Verifier
STA4019bothinternalconsole.log must have type 'undefined', got '{t}'Verifier
STA4020bothinternal{kind} node missing HTypeVerifier: a node reached the IR without a type. Allocated after STA4001 was already taken — the verifier's own first code, not a renumbering
STA4021bothinternallowering produced no module and no diagnosticRaised by stator explain when lowerSourceFile returns null without saying why. Reporting static in that state would be a false claim about a program that does not compile
STA4022bothinternalnull-literal must have type 'null', got '{t}'Verifier
STA4023bothinternalundefined-literal must have type 'undefined', got '{t}'Verifier
STA4024bothinternalbitwise operator result must be number, got '{t}'Verifier. The result is a number, never an integer type: >>> can exceed int32 range (docs/NUMERIC.md §4.2)
STA4025bothinternalunary operator '{op}' result must be {number|boolean}, got '{t}'Verifier. ! yields boolean; -, + and ~ yield number
STA4026bothinternaltemplate literal has {n} literal chunks for {m} substitutions; expected {m+1}Verifier. The chunks bracket the holes, so there is always exactly one more chunk than hole; a violation means the emitter would silently drop or duplicate text
STA4027bothinternaltemplate literal must have type 'string', got '{t}'Verifier
STA4028bothinternalstring length must have type 'number', got '{t}'Verifier. A count of UTF-16 code units (docs/VALUE.md §2)
STA4029bothinternal{break|continue} has no {enclosing|labelled '{l}'} {loop|loop or switch} to jump out ofVerifier. The last code in the verifier's original band. A jump whose target does not resolve would be emitted as a goto to a C label the emitter never wrote, so the failure would surface as a clang error against generated code instead of a diagnostic. Note the asymmetry the message carries: break may leave a loop or a switch, continue only a loop
STA4040bothinternalswitch has {n} default clauses; at most one is legalVerifier. First code of the verifier's second band. Unreachable from source today — TypeScript rejects a duplicate default before the gate sees it — but it is an invariant of the HIR, which later passes will also construct
STA4041bothinternalcallee has type '{t}', which is not callableVerifier. unknown is exempt: in js mode a call on an unresolved value is the whole point, and the check happens at runtime. A concrete non-function callee means the lowering built a call the checker would already have rejected
STA4042bothinternalreturn statement outside a functionVerifier. The emitter would compile it to a JSRT_FRAME_POP(); return in main, popping the globals frame that must outlive the program
STA4043bothinternalarray length must have type 'number', got '{t}'Verifier. The .length of an array is a number by construction; anything else means the lowering built the node with the operand's type or the element's
STA4044bothinternalindex target has type '{t}', which is not indexableVerifier. Covers a[i] as a read and as an assignment target. unknown is exempt for the same reason it is exempt from STA4041 — in js mode indexing an unresolved value is the point, and the runtime decides
STA4045bothinternalfor-of iterable has type '{t}', which is not an array, string, Map, Set or iteratorVerifier. Separate from STA4044 because the emitted shape differs: for-of over an array, a string, a Map or a Set compiles to a specialized loop (docs/VALUE.md §4.13); a user iterable is a MethodCall of [Symbol.iterator]() whose result is HIR iterator and walks the same boxed-iterator path
STA4046bothinternalfield '{f}' is not slot {n} of {C} / field target has type '{t}', which has no fieldsVerifier. This is the reason FieldAccess and FieldAssignment STORE a slot instead of letting the emitter recompute one: a recomputed index is correct by construction and therefore unfalsifiable, while a stored one can be checked against the layout it claims to index. A disagreement between the lowering's field order and the type's would otherwise be a silent read of the wrong field
STA4047bothinternalreceiver has type '{t}', not {C} / {C} has no method '{m}'Verifier. MethodCall names the class DECLARING the method so the emitter can call it directly rather than loading a closure per instance. For an inherited method that is an ancestor rather than the receiver's own class, so the check is that the receiver's type has it in its ancestry and that the method still exists — otherwise the emitted call runs another class's body
STA4048bothinternalnew {C} has type '{t}'Verifier. The allocated descriptor and the expression's type must be the same class; they are chosen at different points in the lowering
STA4049bothinternal{C}.{m} does not take {C} as its receiverVerifier. Every constructor and method is lowered as an ordinary function whose parameter zero is the instance. A member missing it would read fields out of whatever the caller happened to pass first
STA4030bothinternalinternal error during lowering: {detail}Lowering: an exception escaped. The catch-all that keeps a stack trace from reaching the user
STA4031bothinternalunexpected {statement|expression} kind: {kind}Lowering: a construct the gate accepted and the lowering cannot lower — the two disagree about the subset. See the invariant at the top of gateConstruct in src/frontend/gate.ts
STA4032bothinternal{empty declaration list | declaration without initializer}Lowering: a VariableStatement shape the gate should have rejected
STA4033bothinternalassignment target must be an identifierLowering. Two spellings name a binding — a plain identifier and C.count on a static — and both resolve here, which is what lets C.count += 1 reuse the identifier machinery. Anything else is a place the lowering has no binding for
STA4034bothinternalidentifier '{name}' assigned before declarationLowering
STA4035bothinternalidentifier '{name}' used before declarationLowering
STA4036bothinternalunsupported binary operator: {kind}Lowering
STA4037——(retired)Was "unsupported call expression". Retired at rung 4a: the lowering now lowers every call the gate accepts, so no call can reach it. A callee the gate should have rejected is a gate bug, and a non-callable callee is STA4041. Never reuse this number
STA4060bothinternalno field '{f}' on {t}Lowering. First code of the lowering's second band. The checker proved the name is declared and the gate proved the target is a class this subset lays out, so a miss means classTypeToHType and the gate disagree about what a class is — the load-bearing invariant, seen from the inside
STA4061bothinternalthis outside a class memberLowering. this lowers to a read of the receiver parameter, which only a constructor or method has; the gate rejects every other position
STA4050bothinternalinstanceof {C} has type '{t}'Verifier. The target is unconstrained on purpose (1 instanceof C is false, not an error), so the answer being a boolean is the only invariant left to check
STA4062bothinternalnew produced '{t}', which is not a class instance / receiver has type '{t}', which is not a class instance / not a class instance the type model describesLowering. One code for the three places the same invariant is checked — new, a method receiver, and a class declaration — because the failure is identical in all three: the gate accepted a class the type model then declined to describe
STA4063bothinternalinstanceof right operand is not a class the gate acceptedLowering. gateBinary admits instanceof only against an identifier resolving to a modelled class, so a miss here is the gate and the lowering disagreeing about what that means
STA4051bothinternalsuper call to '{C}' from a receiver of type '{t}'Verifier. A SuperCall names the BASE whose constructor runs, and the emitter calls it directly. The name is only safe while the receiver's type actually has that class in its ancestry — otherwise the emitted call runs a constructor against an instance whose layout it does not describe
STA4052bothinternalobject literal entry '{e}' is not at the slot its shape gives itVerifier. A literal's entries ARE its slots, in the order written, and every later read resolves a slot against the same shape. An entry out of position builds an object whose every read misindexes it — the construction-time twin of the check STA4046 runs on a field access
STA4064bothinternalsuper call outside a derived constructorLowering. gateClass admits super(...) only as the first statement of a constructor in a class with an extends clause, so reaching this means the receiver binding or its base list is missing where the gate proved both exist
STA4065bothinternalno class in the receiver's ancestry declares method '{m}'Lowering. MethodCall names the class that DECLARES the method, not the receiver's own, so that an inherited method resolves to the one function that exists. The gate proved the name is a method somewhere in the chain, so a miss is the gate and the lowering walking different chains
STA4066bothinternalstatic '{C.f}' read before its class declaration was loweredLowering. A static is a binding registered when its class is lowered, and a class is a statement in source order, so a read that resolves to a static the gate accepted must find the binding. A miss means the gate let a static reference past a class the lowering never reached
STA4067bothinternalmethod '{m}' has no slot in the layout of {t}Lowering. A method call resolves its slot against the receiver's static type, and the checker already proved the method exists on that type. A miss means the layout classTypeToHType built omits a method the checker lists — the same disagreement STA4060 catches for fields, one member kind over
STA4068bothinternalobject literal has no shape / member is not a name/value pairLowering. The gate admitted the literal only after tsTypeToHType gave it a shape and every key proved to be an identifier, so either miss means the gate and the type model disagree about what an object literal IS — the load-bearing invariant again, one construct over
STA4053bothinternalnew {map|set} has type '{t}' / {op} on a receiver of type '{t}' / '{op}' is not an operation of a {map|set} / {op} takes {n} arguments, not {m}Verifier. One code for the four ways a collection node can be malformed, because the consequence is one thing: each operation emits a call to a runtime function with a fixed C signature, and every jsrt_value argument has the same C type — so an add on a Map, or a set missing its value, is a call the C compiler cannot reject and the runtime reads past the arguments it was given
STA4054bothinternal{kind} has the unsubstituted type '{t}'Verifier. The HIR has no type parameters in its vocabulary: monomorphization happens AT the lowering, so a T reaching the verifier means a node was built outside a specialization's substitution. This is checked first, before every other rule, because a type parameter compares unequal to everything and would otherwise surface as an unrelated type mismatch several rules away from its cause
STA4069bothinternal'{name}' is not an operation of a {Map|Set}Lowering. gateCollectionCall admits only the names in its own table, so a name reaching the lowering that collectionOperation does not recognize is the gate and the lowering disagreeing about which operations exist
STA4055bothinternaltypeof result must be string, got '{t}'Verifier. typeof answers with one of seven strings and nothing else. A node typed otherwise means the lowering built it from something that is not a TypeOfExpression, and the emitted C would compare a boxed string against the wrong kind of value — silently taking the false branch of every guard rather than failing
STA4056bothinternalboundary check on a value of type '{t}', which is already concrete / boundary check narrows to unknown, which checks nothingVerifier. Both halves of a check that does nothing. The lowering inserts a check exactly where an unknown becomes concrete, so a check on a value that already has the checked type is a runtime cost with no soundness gain (a concrete value of ANOTHER type is allowed: that is the js-mode TS2322 edge, where the check is what fails, plan-notes 301), and one whose result is still unknown has narrowed nothing and left its consumer no better off. Either means the insertion rule and the type model disagree about what a boundary IS
STA4057bothinternaltry statement with neither catch nor finally / catch binding without a catch blockVerifier. Both halves of a TryStatement that no source can produce — TypeScript's grammar requires at least one of catch/finally, and a binding only exists inside a catch (e) clause — so either shape means a pass rebuilt the node and dropped a field. Caught here because the emitter dispatches on which blocks are present: a try with neither would emit its body and silently discard the whole protocol
STA4058——(retired)Was "dynamic property access on a non-object value". Retired in Phase 5 step 4: jsrt_get_prop/jsrt_set_prop/jsrt_has_prop now give each receiver a deliberate path (nullish → TypeError, fixed object → class-descriptor walk, string "length" → length, other primitives → undefined / TypeError on set). Never reuse this number
STA4059bothinternaldynamic node typed '{t}', not UnknownVerifier. The three dynamic nodes (dyn-object-literal, dyn-field-access, dyn-field-assignment, docs/HIR.md) carry Unknown BY DEFINITION: the shape table answers reads at run time, so any static type on one is a claim nothing checked — exactly the narrowing-out-of-nothing that rule 4 forbids. A concrete type here means the lowering picked the dynamic path for a receiver the fast path owns, or invented a result type for a read it cannot see
STA4080bothinternalMath.{m} takes {n} arguments, not {k} / Math.{m} must be number -> numberVerifier, first code of its THIRD band (STA4080–STA4099). The three claims the jsrt_math_* C signatures rest on and the C compiler cannot check across jsrt_value: the method is one the runtime exports, the arity is exact (the lowering folded variadic min/max into nested binary nodes, so a wrong count is a lowering bug, never a source property), and every operand and the result are typed number
STA4081bothinternalstring op {op} takes a string receiver / takes {n} arguments / must produce {t}Verifier. Polices the STRING_OPS table's three claims about a string-op node — string-typed receiver, exact post-padding arity (the lowering pads omitted optionals with undefined literals, so a short count is a lowering bug, never a source property), and the result type the table declares. Argument types are deliberately unchecked: the runtime coerces per spec (indexOf(1) is legal JavaScript), and pinning them here would re-refuse what the gate already admitted
STA4082bothinternalarray op {op} takes an array receiver / takes {n} arguments / must produce {t}Verifier. The ARRAY_OPS counterpart of STA4081: array-typed receiver, post-padding arity, and the table's result type — where self is the receiver's own array type (slice/concat/fill/reverse) and element is Unknown by the IndexAccess rule, so only the concrete result kinds are pinned. The arity is EXACT except for the variadic ops (plan.md §8 step 19): push/unshift/splice/concat hold any count from the table's minimum up, and lastIndexOf holds one or two — the emitter picks the runtime entry point by count. Argument types stay deliberately unchecked, for STA4081's reason
STA4083bothinternalObject.{m} results in {t}, not an arrayVerifier. Object.keys must be exactly string[]; values/entries must be SOME array — their element follows the checker's answer and degrades to Unknown for a mixed shape, so only the kind is pinned. The argument's type is deliberately unchecked: the gate restricted it to the two object layouts, which spell differently here
STA4084bothinternalObject.keys/values/entries on a non-object valueRaised by the RUNTIME (jsrt_object_ops.c), in the verifier's band for STA4058's reason: it polices the same argument contract from the other side. The gate restricts the argument to the two object layouts, so reaching this is a compiler bug, never a source property
STA4085bothinternalJSON.stringify results in {t}, not a stringVerifier. The lowering pins the single-argument form to string; anything else is a lowering bug. The argument is deliberately unchecked -- any serializable value is legal, and the two that are not (undefined, a function) were refused by the gate at the top level and abort loudly (STA2005 pattern) when an Unknown smuggles one in deeper
STA4086bothinternala regular-expression literal has type {t} / {op} on a receiver of type {t} / {op} takes 1 argument, not {n} / {op} results in {t}, not a booleanVerifier. The RegExpOperation counterpart of STA4081, and pinned harder than one: jsrt_regexp_test dereferences its receiver as a JSRTRegExp without asking, so a wrong receiver kind is memory corruption rather than a wrong answer. The literal's own claim is only its kind -- the pattern text is never read above the C boundary. The ARGUMENT is deliberately unchecked, for STA4081's reason: an untyped subject is the js-mode norm, and the runtime's tag check is the honest place to settle it
STA4070bothinternalno specialization '{name}' was collected for this call / generic call still mentions a type parameter after substitution: {t}Lowering. The specialization walk and the lowering walk visit the same calls in the same order, so every call the second one resolves must have been collected by the first. A miss means the two disagree about which calls exist or which substitution is in scope — and the emitted C would call a function that was never emitted
STA4087bothinternalawait of '{a}' results in '{b}', not '{c}'Verifier. The one claim that holds for every await: awaiting a PROMISE produces its value type. Awaiting anything else is legal (await 1 is a spec-sanctioned no-op that still yields to the queue) and its result is the operand's own type, so nothing is pinned there -- peeling unconditionally would get exactly that case wrong
STA4088bothinternalPromise.{m} results in '{t}', not a promise / Promise.{m} takes an array, not '{t}'Verifier. All three statics answer a promise unconditionally, and jsrt_promise_all reads its argument as an array WITHOUT asking -- a wrong receiver kind there is a wrong dereference, not a wrong answer. An Unknown argument is admitted for STA4081's reason: js mode's untyped array is the norm, and the runtime's own tag check is the honest place to settle it
STA4089bothinternal{field} on a receiver of type '{t}', not the unknown a match-or-null is / {field} results in '{t}', not '{want}'Verifier. A match read's receiver is Unknown BY CONSTRUCTION -- exec answers a match or null, a union the HIR does not model -- so a concrete receiver type here means the lowering built the node from something that is not a match. The result type is the field's, from MATCH_FIELDS: index and length are numbers, input a string, groups Unknown
STA4090bothinternal{field} on a receiver of type '{t}' / {field} results in '{t}', not '{want}'Verifier. The mirror of STA4089 with the receiver pinned the OTHER way: a RegExp field read's target is concretely typed -- a regexp is a regexp and the checker says so -- and the C accessors dereference a JSRTRegExp without asking, so a wrong kind here is memory corruption rather than a wrong answer. The result type is the field's, from REGEXP_FIELDS: source and flags are strings, lastIndex a number, the eight flag predicates booleans
STA4091bothinternala RegExp flag property the runtime does not knowRaised by the RUNTIME, STA4084-style, from the other side of the same contract: jsrt_regexp_flag takes the flag's LETTER because the LRE_FLAG_* constants belong to the vendored engine's header and generated C does not include it, so a letter outside dgimsuvy is the emitter having invented a property that is not in REGEXP_FIELDS
STA4092bothinternalnew Date results in '{t}', not a Date / {op} on a receiver of type '{t}' / {op} takes {n} arguments, not {k} / {op} results in '{t}', not a {want} / Date.{m} takes {n} arguments, not {k}Verifier. The Date counterpart of STA4086, pinned the same way and for the same reason: the jsrt_date_* accessors read a JSRTDate without a tag test, so a wrong receiver kind is memory corruption rather than a wrong answer. Arity is EXACT because the lowering pads omitted trailing components to the table's full count -- a short list is a lowering bug, never a source property. The CONSTRUCTOR's argument is deliberately unchecked: jsrt_date_from_value discriminates number/string/Date by tag and answers an Invalid Date for anything else, which is what the spec's ToPrimitive-then-ToNumber path amounts to
STA4093bothinternala Date operation on a value that is not a DateRaised by the RUNTIME, STA4084/STA4091-style, from the other side of STA4092's contract: every jsrt_date_* entry point asserts its receiver's class pointer, so a mis-emitted call aborts at the boundary instead of reading a double out of whatever object it was handed
STA4094bothinternalPromise.prototype.{m} on '{t}', not a promise / results in '{t}', not a promise / new Promise results in '{t}', not a promiseVerifier. The instance methods and new Promise answer a promise unconditionally; a wrong receiver kind at .then is a lowering/gate disagreement
STA4095bothinternalnew {Ctor} results in '{t}', not the {Ctor} layoutVerifier. The standard Error constructors. What is pinned is the LAYOUT, not merely the kind: jsrt_error_new writes slot 0 (name) and slot 1 (message) by index, and every e.message downstream resolves against the same two fields, so a type that disagreed with runtime/src/jsrt_error.c would be a wrong SLOT rather than a wrong answer. Comparing against errorHType checks the class name, the field order and the base chain at once, which is the only place the emitter's and the runtime's definitions can drift (plan.md §8 step 2a(c))
STA4096bothinternalreference-error must have type 'unknown', got '{t}' / reference-error must carry the name it failed to resolveVerifier. Reading a name nothing declares. unknown is the CONTRACT here rather than a fallback: the node stands where a value of an unknowable type would have been, and boundary insertion reads that type to decide whether a check is owed — typing it undefined (the C value the emitter hands back so the expression has one) would let const n: number = undeclared through as a static assignment of the wrong type instead of a boundary. The name is checked non-empty because it is the whole payload: the emitter spells it into jsrt_reference_error("x") and it is what makes the message match Node (plan.md §8 step 2a(c))
STA4097bothinternaldelete result must be boolean, got '{t}'Verifier. The delete operator's answer. A delete-prop node carries a target and a key, either of which could plausibly have supplied its type by mistake, but the operator has exactly one result type in the language — the boolean saying whether the key is gone — and never the deleted value's. Pinning it here is what entitles the emitter to wrap jsrt_delete in jsrt_bool unconditionally, and const-fold/DCE to treat the node as boolean-valued (plan.md §8 step 2a(c))
STA4098bothinternalextern call '{f}' takes {n} arguments, not {m} / argument {i} has type '{t}', not '{want}' / results in '{t}', not '{want}' / uses the {conv} convention on a '{ret}' returnVerifier. The four claims the extern-call direct C call rests on, and the C compiler cannot check any of them across the boundary: the arity is exact (the gate refuses a mismatch, so a wrong count is a lowering bug, never a source property), each argument unboxes to its declared ABI kind, the boxed result is the return kind's HType, and the @statorError convention reads what the return carries (plan.md §10 Task 7.1 steps 4–5)
STA4099bothinternalString.{m} must return string, got '{t}'Verifier. The two claims a string-static node rests on: the method is one of STRING_STATICS (today only fromCharCode), and the answer is a string by construction. Any argument count lands — the node is variadic and the emitter passes the count — and argument types stay deliberately unchecked: the runtime coerces each code with ToNumber then ToUint16 (plan.md §8 step 19)
STA4100bothinternalfunction length must have type 'number', got '{t}' / function length needs a function operand, got '{t}'Verifier. The two claims a function-length node rests on: the answer is a number by construction, and the operand is statically function-typed (jsrt_closure_arity reads the closure without asking). An Unknown-typed receiver never reaches this node -- it takes the dynamic path through jsrt_get_prop -- so a non-function operand here is a lowering bug (plan.md §8 step 21b)
STA4101bothinternal{op} on a receiver of type '{t}' / {op} takes {n} operands, not {k} / {op} results in '{t}', not '{r}'Verifier. The three claims a typed-op node rests on, all read from its TYPED_OPS row (plan.md §11c T11.1): the receiver's HType is the row's (uint8array or arraybuffer; a constructor row has none), the operand count is the row's arity plus the receiver (the lowering pads a missing optional argument with undefined, so a short list is a lowering bug), and the node's type is the row's result. The C entry points take jsrt_value and cannot check any of this across the call
STA4102bothinternal{f} takes {n} arguments, not {k} / {f} results in '{t}', not a {r}Verifier. The two claims a global-call node rests on, read from its GLOBAL_CALLS row (plan.md §11c T11.4): the operand count is the row's arity exactly (the lowering pads an omitted argument with undefined, so a short list is a lowering bug), and the node's type is the row's result (number for parseInt/parseFloat, boolean for isNaN/isFinite, regexp for RegExp, an array for Array). Argument types stay deliberately unchecked: every jsrt_global_* entry point coerces its operands itself
STA4103bothinternal{op} on '{t}' with {n} arguments, typed '{r}'Verifier. The claims a number-op node rests on (plan.md §11c T11.4, plan-notes 310): the receiver is a number (the jsrt_number_* entry points read it without a tag test), the one argument is present (the lowering pads an omitted one with undefined), and the result is a string. The argument's type stays unchecked: the runtime runs ToIntegerOrInfinity on it
STA4104bothinternalspread {kind} with {n} arguments, first typed '{t}', not one arrayVerifier. A spread call or dyn-method-call (plan.md §11c T11.4 step 5) passes ONE array holding every argument, which jsrt_call_spread_at reads as a JSRTArray without a tag test; the lowering folds the spread list into exactly that (docs/VALUE.md §4.23)
STA4071bothinternalIterator.{m} reached the lowering on a non-generator receiverLowering invariant: the gate admits return/throw only on a checker-typed Generator. Runtime receivers are validated independently and throw a catchable TypeError before casting; a lying JS annotation must not turn this compiler invariant into a runtime abort or invalid struct read (plan-notes 200)
STA4072bothinternalinternal error: {message} — this is a compiler bug; report it with the input that triggered itThe CLI's catch-all (src/cli/main.ts), raised for any exception that is neither a StatorError nor a BuildError: the last line of AGENTS.md's "a thrown exception reaching the CLI is a compiler bug", and the reason no input can reach a user as a raw Node stack trace. It sits in the lowering's second band for room — the STA4001 precedent, where the range is chosen by availability and the row names the raiser. Its first real case, the TypeScript checker's own stack overflow (plan-notes 213), is no longer one: plain tsc dies on those inputs too, so it is not a Stator bug and has its own code, STA0013 (plan-notes 287). A stack overflow anywhere in Stator's own code still lands here
STA4073bothinternalthe #brand-in-object test names no class the gate acceptedLowering. The gate admits #x in o only when the private name resolves to a declaring class, and the lowering desugars it to instanceof against that class — so an unresolvable brand here means the gate and the lowering disagree about which class a brand means (plan.md §8 step 12(d))

Adding a new diagnostic code

  1. Identify the range. Consult the ranges table above. Is this a CLI error (STA0), a never-reject (STA1x), a not-yet (STA12), a lowering error (STA2), module graph (STA3), or internal bug (STA4)?

  2. Pick the next free number. Scan the appropriate range in this file and choose the lowest unoccupied code.

  3. Add a row to the appropriate table with:

    • Code
    • Mode(s) affected (ts, js, or both)
    • Class (error, never, not-yet, runtime, internal)
    • Message template (may use {placeholders} for dynamic values)
    • Phase (for not-yet only)
    • Notes (explain when triggered, interactions with other codes, etc.)
  4. Add a decision test in tests/subset/ for error/not-yet codes (see AGENTS.md testing rules). The test must reference the new code in a // @code: STAxxxx directive.

  5. Update the emitter in src/. The code must be hardcoded in the throw site; never derive it from data.

  6. Commit together. The code, test, and emitter update land in one commit, with the code cited in the commit message.

Never:

  • Reuse a shipped code
  • Change a shipped code's message in any form
  • Move a code between ranges
  • Delete a code (if it became obsolete, leave it in the table and mark it reserved)

Reserved and not-yet-allocated

The following ranges are reserved for future phases:

  • STA0007–STA0009: CLI/toolchain (Phase 2+)
  • STA0021–STA0099: CLI/toolchain and config (STA0014–STA0015 are the bundler's since T12.1, plan-notes 296, 320; STA0016–STA0018 are the config file's, plan-notes 303; STA0019 is the unwritable output's, Task 6.20, plan-notes 331; STA0020 is the --emit=lib tools', Task 7.4, plan-notes 342)
  • STA1122–STA1199: Rejected constructs (Phase 1+ may add more as new anti-goals are clarified)
  • STA1209, STA1221–STA1299: Not-yet features (Phase 2+ will expand as phases land)
  • STA2010–STA2999: Lowering and boundary errors (Phase 2 onward)
  • STA3004–STA3999: Module graph errors (Phase 2+)
  • STA4038–STA4039, STA4074–STA4079, STA4105–STA4999: Internal errors (may grow as invariants are formalized). STA4002–STA4020, STA4022–STA4029 and STA4040–STA4059 are the HIR verifier's; STA4030–STA4037 and STA4060–STA4079 are the lowering's. The gap between the verifier's first two ranges was deliberate room for each to grow without renumbering. The verifier's first band is full (STA4029 was the last of it) and its second band, STA4040–STA4059, is now full too: STA4040–STA4057 and STA4059 are the verifier's, STA4058 sat in the band as a runtime panic and is now retired (Phase 5 step 4); the number stays listed so it is never reused. The verifier's third band is STA4080–STA4099, of which STA4080–STA4099 are taken (STA4084, STA4091 and STA4093 raised by the runtime, STA4058-style). STA4094 is the Promise instance/construct verifier, STA4095 the Error-constructor layout check, STA4096 the undeclared-name read, STA4097 the delete result type, STA4098 the extern-call direct-call claims and STA4099 the String namespace call (plan.md §8 step 19). STA4100 is the function-length verifier (plan.md §8 step 21b), STA4101 the typed-array op verifier (plan.md §11c T11.1), STA4102 the global-function call verifier, STA4103 the Number.prototype op verifier and STA4104 the spread-call verifier (plan.md §11c T11.4). The lowering's first band is also effectively full — STA4038–STA4039 are its only remaining room, and STA4037 is retired rather than reusable — so it has opened a second band, STA4060–STA4079, of which STA4060–STA4071 are taken and STA4072 is the CLI's catch-all (the STA4001 precedent: band by availability, raiser named in the row). Each is a new band, not a renumbering of an old one, which is why the free ranges above are discontiguous.

Each phase's task-list entries in plan.md will allocate from these ranges as features ship. Allocations are made in this file — never in source code comments or separate documents.


Retired codes — never reuse

CodeWasWhy retired
STA0010"{cmd}" is not implemented yet — plan.md Phase 2 delivers itThe placeholder build/explain emitted before Phase 2. Phase 2 implements both commands, so nothing can reach it. A missing entry file is now STA0007.
STA1102eval / new Function in js mode ("not yet", Phase 8)A not-yet code allocated inside the STA11xx never range, which breaks the disjoint-range invariant in plan §1.3 — a test could not tell design intent from schedule by looking at the number. Renumbered to STA1206 before anything shipped.
STA1218'{__filename|__dirname}' is not yet supported (not-yet, Phase 11)A placeholder for T11.5. plan-notes 316 injects the values in the vendor module under --node; every other read is an ES module's, which Node answers with a ReferenceError, so it is the never code STA1110.
STA4058dynamic property access on a non-object valueThe shape-table entry points used to panic on anything that was not a JSRTDynObject. Phase 5 step 4 gave each receiver a deliberate path, so the panic is unreachable.

A retired code is dead, not free: it stays listed here and is never assigned a new meaning. Retirement is only possible because nothing has shipped yet; after the first release, a mistaken code is fixed by adding a correct one, never by moving it.