The hybrid-boundary inversion is closed: a statically provable interface
violation now fails at COMPILE time instead of reaching wovm as an ICALL
that traps WO_T_BOUNDS at runtime.
- types.ml class_satisfies: the same rule emit.ml's `satisfies` builds
vtable rows from (instance method with matching name + parameter count
for every interface method; `static fn` never satisfies) — one rule, two
consumers, so the check and the vtable can never disagree.
- check_iface_boundary fires wherever a confidently class-typed value flows
into an interface-typed slot: call arguments against the callee's declared
parameters (free fns, methods off confident receivers, interface-method
sigs, statics — resolved exactly as confident_typ resolves returns),
annotated `let`s, and `return`s. Silent when underivable.
- The same per-argument pass extends the ?T boundary to CALL ARGUMENTS
(the previous slice covered stores/returns/operands): nil into a
non-nullable parameter is WO-E212, an unnarrowed ?T argument is WO-E211.
- tests/corpus/trap/unsatisfied-interface -> compile-fail/ with
fixture.code WO-E205, per the fixture's own standing instruction; its
header comment rewritten to the wired reality.
- The new arg checks caught a real mistyped signature in the sample:
log-watcher's rpc_error/rpc_result/call_tool declared `id: json.Value`
while every caller legitimately passes nil (JSON-RPC id-absent) — now
`?json.Value`; dispatch/call_tool/cron-row sites moved to the
bind-then-narrow idiom (including an `or`-guard narrowing:
`if spath == nil or spat == nil { return }`).
- Catalog: E205 gains its main-table row; the "owed gap" section is
rewritten as closed. Board known-gap struck through.
Verified: woc-test 540/0 + test_diag 14/0; oop-e2e 83/0 (fixture now
compile-fail, satisfying-class negative probe compiles clean); oop-accept
ALL MET; log-watcher 7/0; employee 8/0; gc-cycle clean.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
162 lines
27 KiB
Markdown
162 lines
27 KiB
Markdown
# The `woc` diagnostic catalog — normative reference
|
||
|
||
Every `WO-E###`/`WO-W###` code the `woc` front end (`compiler/`) actually
|
||
emits, as of plan 2 tasks 2–8 and plan 3 tasks 1–2. Code ranges are reserved
|
||
per stage (`compiler/src/diag.ml`): `WO-E0xx` lexing, `WO-E1xx` parsing,
|
||
`WO-E2xx` types, `WO-E3xx` ownership, `WO-E4xx` the bytecode emitter,
|
||
`WO-W2xx` warnings from the types stage. This
|
||
is an enumeration of codes already in use, not an archaeology dig — see
|
||
"Completeness method" below for how that was verified, "Reserved,
|
||
not yet emitted" for codes the source declares but no check yet raises,
|
||
and (until 2026-08-18) "Reachable but unenforced" for the one code (WO-E205) whose check
|
||
site the milestone grammar *does* exercise, unlike the codes above it —
|
||
see that section for why this is a live gap, not a scope boundary.
|
||
One code (WO-E214) is emitted by the driver (`compiler/bin/main.ml`),
|
||
not one of the four stage modules — a Task 8 review finding — see its
|
||
row in the types table below for why it still uses that range.
|
||
|
||
Every diagnostic renders as `file:line:col: <severity> <code>: <message>`,
|
||
the source line, and a caret under the column (`compiler/src/diag.ml`);
|
||
ownership errors (`WO-E3xx`) add a second, indented site for the other
|
||
half of the story ("moved here" / "borrowed here" / etc.).
|
||
|
||
## WO-E0xx — lexing (Task 3, `compiler/src/lexer.ml`)
|
||
|
||
| code | meaning | example message |
|
||
| --- | --- | --- |
|
||
| WO-E001 | an input byte the lexer doesn't recognize as the start of any token. Reported once per bad byte, which is then skipped — one bad byte never stops the whole file. | `unknown character '$'` |
|
||
| WO-E002 | a string literal's backslash escape is the last byte of the file, with no character left to escape (a plain unterminated string with no dangling backslash is *not* an error — rt parity). | `unterminated string escape` |
|
||
|
||
## WO-E1xx — parsing (Tasks 4–5, `compiler/src/parser.ml`; WO-E103 haxe-parity Task 2)
|
||
|
||
| code | meaning | example message |
|
||
| --- | --- | --- |
|
||
| WO-E101 | generic syntax error: an unexpected token where the grammar expected something else, including running off the end of the file inside an unclosed block/type/interface body. Declaration-level recovery syncs to the next top-level keyword so one bad declaration yields one diagnostic, not a cascade. | `expected ')' or ',', got NEWLINE` |
|
||
| WO-E102 | an invalid `@table(...)` configuration: `name` given twice, an `index` with no columns, or an argument key other than `name`/`index`. | `@table(name: ...) given twice` |
|
||
| WO-E103 | haxe-parity Task 2. `inline fn ...` — the haxe keyword verdict table's own reject half of the `inline` row (`const` values are the adopted half). The whole declaration is discarded by the usual top-level recovery, same as any other bad declaration. | `` `inline fn` is rejected — optimization is the compiler's job `` |
|
||
| WO-E104 | iteration 7b. `@gc` on a class — the annotation is gone: GC-ness is inferred (structural cycles + demand promotion; `woc --dump-gc`). The annotation is skipped for recovery and the class classifies by inference. | `` `@gc` is not a valid annotation: GC-ness is inferred by the compiler (run `woc --dump-gc`). Remove it. `` |
|
||
|
||
## WO-E2xx / WO-W2xx — types (Task 6, `compiler/src/types.ml`; WO-E214 Task 8, `compiler/bin/main.ml`; WO-E215 plan 3 Task 2, `compiler/src/types.ml`; WO-E210/E216–E218/W202 haxe-parity Task 1, `compiler/src/types.ml`; WO-E201 haxe-parity Task 2, `compiler/src/types.ml`; WO-E208 haxe-parity Task 3, `compiler/src/types.ml`; WO-E203 haxe-parity Task 4, `compiler/src/types.ml`)
|
||
|
||
| code | meaning | example message |
|
||
| --- | --- | --- |
|
||
| WO-E201 | haxe-parity Task 2. An `and`/`or` operand whose type is confidently known (the same narrow, "stay silent when underivable" deriver WO-E209 uses — `confident_typ`) and is not `Bool` — this language has no truthiness. Reserved since Task 6, its first real emission site. Haxe-parity Task 3 gave it two more sites: a `switch` expression's arms disagree on their yielded type (the switch's own type is fixed by the first arm — types.ml's "first wins" convention — every later arm is checked against it, via the regular `.typ` inference, not `confident_typ`; since haxe-parity Task 4 the comparison is *structural* — two typedef records with the same shape are the same type, `typ_equal`); and (review fix, Critical 2) a `case` value whose representation (`WO_K_TEXT` vs. `WO_K_SCALAR`) doesn't match the switch subject's — a real VM segfault if unchecked (a `Text` subject picks EQS, and EQS's `str_check` dereferences whatever sits in a mismatched `Int` case value's register), checked via `confident_typ`, silent when either side is unresolved. Haxe-parity Task 4 added the union-subject site: a `case` value over a confidently union-typed subject that does not name one of that union's variants (a misspelled variant, a variant of some other union, or a plain literal — union arms match variants, never values). Task 4's fix round 1 added three inverse/porosity sites, each a reviewer-reproduced silent-wrong-behavior hole: a variant-named `case` over a confidently **`?Union`** subject (it can never match — `switch` does not narrow `?T`; the message points at handling nil first, since forced handling is Task 6's), a variant-named `case` over a confidently **non-union** subject (`switch n { case Lo: }` over `n: Int` silently ordinal-matched), and a **cross-union `==`/`!=`** (`X == P` from two different bare unions was silently true whenever the ordinals matched; same-union comparison stays legal). Lexical scope wins at every one of these sites — a local sharing a variant's name is never misread as one. | `` `Wat` is not a variant of union `Kind` `` |
|
||
| WO-E203 | haxe-parity Task 4 (`typedef` records + enum payload variants). A payload variant's argument count doesn't match its declaration, at either of the two places payload fields are positional: a construction (`Failed("a", "b")` against `Failed(reason: Text)`) or a `switch` pattern (`case Failed(a, b):`). The pattern site also rejects a non-name argument (`case Failed("x"):` — payload fields are bound positionally, never matched by value) and a payload-binding pattern sharing its arm with other values (`case Failed(r), Pending:` — the binding would be meaningless on the other match). Reserved since plan 2 Task 6; these are its first real emission sites, scoped to variant payloads only — user `fn`/method call arity is still the emitter's WO-E403, unchanged (see "Reserved, not yet emitted" below for the history of that gap). | `` variant `Failed` of `Status` takes 1 payload argument(s), given 2 `` |
|
||
| WO-W201 *(retired, iteration 7b)* | Suggested `@gc` for a recursive/shared class. Retired: `@gc` no longer exists (WO-E104) and inference classifies exactly these classes as traced automatically, so the suggestion is obsolete. | *(no longer emitted)* |
|
||
| WO-E205 | iteration 5 strictness (2026-08-18). A confidently class-typed value used at an interface-typed position — a call argument, an annotated `let`, or a `return` — where the class does not structurally satisfy the interface (Go-style: an instance method of the same name and parameter count for every interface method; a `static fn` never satisfies). Provable statically, so it fails at compile time instead of `ICALL`'s no-vtable-entry `WO_T_BOUNDS` trap — the hybrid boundary restored. Silent when the value's type is underivable. | `` `Rock` does not satisfy interface `Priced`: no matching instance method `current_price` `` |
|
||
| WO-E202 | a `.field` access names a field that the base's class (a *declared* class — an unresolved/placeholder expression type never triggers this) doesn't have. | `unknown field \`price\` on \`Product\`` |
|
||
| WO-E206 | a constructor literal (`ClassName { ... }`) omits a field the class declares that is neither defaulted nor nullable. Haxe-parity Task 4 narrowed it from "omits any field with no default was already the rule, but defaults were unenforceable" to the real omittability rule: a field with a declared default is filled by the emitter (`TailState {}` — the sample's defaults-fill-in pattern), and a `?`-typed field omitted is nil (the zero word `NEW` already leaves), for classes and typedef records alike. | `missing field \`sku\` in constructor of \`Product\`` |
|
||
| WO-E207 | a constructor literal names a class that isn't declared anywhere in the (possibly multi-file) program. `typedef` records (haxe-parity Task 4) are classes to this check — a record name resolves here like any declared class. | `unknown type \`Widget\` in constructor` |
|
||
| WO-E208 | haxe-parity Task 3 (`switch` as expression); the union exhaustiveness rule is haxe-parity Task 4's. Two subject regimes: (1) a **union-typed** subject (derived via `confident_typ`, the "stay silent when underivable" deriver — an underivable subject falls to regime 2) needs no `default` exactly when every variant is covered by some arm; a gap fires this code and names the missing variants, in declaration order. A `default` always satisfies it. (2) every **other** subject (scalars, Text, and anything underivable) keeps Task 3's unconditional rule: no `default` arm is always this error. A `?Union` subject is deliberately regime 2 until Task 6's forced-handling work legalizes narrowing it. | `` switch over `Kind` has no `default` arm and does not cover: Mid, Hi `` |
|
||
| WO-E209 | hotfix (2026-08-11, round 2). A builtin call (`print`, `print_int`, `words`, `now`, `push`, `get`, `count`, `latest`, `set`, `has`, `multi_new`, `map_new`) given the wrong number of arguments, or an argument whose type is confidently known and does not match the builtin's signature (source of truth: [`08-builtin-surface.md`](08-builtin-surface.md)). Motivated by a real segfault: `print(7)` compiled clean and crashed `wovm` — `print` wants a `Text` (a heap-string pointer), and the VM's `str_check` dereferences whatever register it is handed as a `wo_str*` with no runtime tag to check first, so a bare `Int` was a wild pointer read. `types.ml`'s own `confident_typ` (deliberately narrower than the typechecker's regular `.typ` inference — see its doc comment) derives an argument's type from a literal, `self`, a parameter's declared type, a class field's declared type, a `Ctor` naming a real declared class, a `let` whose value was itself confidently typed, or (round 2 — round 1 missed this, a real second segfault repro: `print(takesSecret(box))` where `takesSecret` is declared `-> Int`) a `Call` whose declared signature is known: a free fn (resolved own-module-first-then-used-modules, never the flat whole-program symbol merge — the same Critical-1 bug shape haxe-parity Task 1 already fixed for the emitter), a class method off a confidently-typed receiver, an interface method's signature, or another builtin's own confident return type (`words`/`count` → `Int`, `now` → `Timestamp`, `has` → `Bool`, …). `ReqInt` accepts any non-`Text` builtin scalar (`Int`/`Bool`/`Timestamp`/`Id` all share the identical `WO_K_SCALAR` runtime representation — found as a real false positive against `print_int(has(...))` once builtin return-chasing went live). Anything still not chased (an unresolved name, an `Index`/`Binary` result, a qualified free-fn call through a `use` alias, an UNKNOWN-BUT-RESERVED stdlib call) is left unchecked rather than guessed at — see "Remaining unchecked surface" below. A user-declared free `fn` of the same name always wins over the builtin table (08-builtin-surface.md's shadowing rule; also resolved module-aware, not via the flat merge), so a shadowed name is never checked here either. Arity mismatches are also still caught later, at emission (`WO-E403`, unchanged) — this is an earlier, additional gate over the same contract, not a replacement. | `` builtin `print` expects Text, got `Int` `` |
|
||
| WO-E210 | haxe-parity Task 1 (modules). A `Ctor`/bare-call name resolves — it's declared, somewhere — but not in this file's own module and not through any `use` edge either; the module it actually lives in is named as a hint. Reserved by Task 6's own brief, genuinely blocked until a module concept existed at all (see the nullable-types-implementation.md handoff) — this is its first real emission site. | `` `helper` is declared in module `shared/util`, which is not `use`d here `` |
|
||
| WO-E211 | iteration 5 strictness (`?T` forced handling, 2026-08-18). A possibly-nil value (`?T`, or a literal `nil`) used where a plain value is required, un-narrowed: an arithmetic or `<`/`<=`/`>`/`>=` operand, an `and`/`or` operand (`?Bool`), an interpolation segment, a `for` iterable, or a `return` whose declared type is not nullable. Narrowing legalizes it: `if x != nil { … }` narrows the LOCAL `x` to `T` inside the branch; a diverging then-branch (`if x == nil { return … }`) narrows after the `if`; `x != nil and x.n > 3` narrows the right operand; `while x != nil` narrows the body. Field places (`a.next`) never narrow — bind to a local first. Env types here are declared or confidently inferred (the fallback placeholders are plain scalars, never `?T`), so this cannot false-positive off an underivable expression. | `` `x` is possibly nil (`?T`) and is used where a plain value is required — narrow it first (`if x != nil { ... }`) `` |
|
||
| WO-E212 | iteration 5 strictness. `nil` or a `?T` value stored across the boundary into a slot whose DECLARED type is not nullable: an annotated `let`, an assignment to a local whose type is confidently known (cenv, never the placeholder env) or to a resolvable class field. | `` `nil` cannot be stored in `x: Int` — the target is not nullable; declare it `?T` or narrow the value first `` |
|
||
| WO-E213 | iteration 5 strictness. Reaching through a possibly-nil value — a field read, an index — without a nil check. The deref twin of WO-E211: the base itself is `?T`. | `` field `next` is possibly nil (`?T`) — check it against `nil` before reaching through it `` |
|
||
| WO-E214 | a class or interface name is declared more than once across the files a directory discovers (one program, multiple files — Task 8). Reported at the *later*-discovered declaration (sorted by path), with the first declaration as the related site; the merged symbol table keeps the first one, so this is what stops that silent keep from also hiding a real shape conflict. Driver-level, not `types.ml` — reuses the `types_prefix` range because it's a symbol-table concern, not a lexing/parsing/ownership one. | `class \`Dup\` already declared in \`a_first.wo\`` |
|
||
| WO-E215 | a class, interface, free `fn`, or (haxe-parity Task 4) union name is declared more than once in the *same file* (`collect_declarations`'s own `StringMap.add` silently dropped the earlier one — Task 1 review, found while building the plan-3 emitter, fixed in Task 2). Task 4 also fires it for a *variant* name reused within a file's unions (inside one union or across two — variants share one flat value namespace, so a bare `Ok` reference could not otherwise pick a tag), reported at the reusing variant with the owning union as the related site. Reported at the later declaration, with the first as the related site — the same shape as WO-E214, one file instead of two; the symbol table keeps the first declaration. Class/interface names and free-fn names are separate namespaces, so a class and a fn sharing a name never collide here. | `class \`Dup\` already declared` |
|
||
| WO-E216 | haxe-parity Task 1 (modules). A `use <path>` names something that is neither one of the six reserved stdlib namespaces (`fs`, `proc`, `net`, `time`, `json`, `env`) nor a directory this program actually discovers. | `` unknown module `nosuchmodule` — not a discovered project module and not a reserved stdlib namespace `` |
|
||
| WO-E217 | haxe-parity Task 1 (modules). A qualified reference (`alias.name(...)`) names a real declaration in a real, `use`d module, but that declaration has no `pub` marker — private to its own module. | `` `hidden` is not `pub` in module `secret` `` |
|
||
| WO-E218 | haxe-parity Task 1 (modules). A bare (unqualified) name resolves as `pub` in *more than one* used module — "collisions diagnose rather than shadow silently" (the plan's own words): resolution never silently picks a winner among used modules, it fails loudly and names every alias that matched. | `` `thing` is ambiguous — exported `pub` by more than one used module (a, b) `` |
|
||
| WO-E225 | a field's declared type name isn't a builtin scalar, a declared class (typedef records included), a declared interface, or (haxe-parity Task 4) a declared union. Also checked, same shape, on a payload variant's field types. A dotted name whose head is a reserved stdlib namespace (`json.Value`) is accepted as UNKNOWN-BUT-RESERVED — the same plan-9 convention `fs.stat(...)` calls get; any other dotted name is as unknown as a misspelling. Checked once per field declaration, at the field's own position. | `unknown type \`Wdiget\`` |
|
||
| WO-W202 *(warning)* | haxe-parity Task 1 (modules). A file's own `use` clause is never actually referenced — neither a bare name resolving through it nor a qualified `alias.name(...)` call — anywhere in that file's surviving parse tree. | `` unused `use fs` `` |
|
||
| WO-W203 *(warning)* | haxe-parity Task 3 review fix (Critical 1). A `switch`'s `default` arm is not textually last — no longer a silent dead-code trap (`default` is lowered last regardless of source position, `Ast.switch_lowering_order`), but still surprising source; fired once per switch, at `default`'s own position. | `` `default` is not the last arm -- a `case` written after it still matches (this compiler evaluates `default` last regardless of source position), which reads as dead code `` |
|
||
|
||
### Reserved, not yet emitted
|
||
|
||
`unknown_fn_code` (WO-E204) is declared in `types.ml` — the range is
|
||
reserved — but nothing in the front end raises it yet. (WO-E211/E212/E213
|
||
left this list 2026-08-18: `?T` forced handling is enforced; see their rows
|
||
in the main table above.) `module_not_imported_code`
|
||
(WO-E210) — the one member of this list Task 6's brief named that a *later*
|
||
task, not a gap in Task 6's own shipped work, was blocking — has moved up
|
||
into the table above: haxe-parity Task 1 gave it its first real emission
|
||
site. `invalid_builtin_code` (WO-E209) has also since moved up into the
|
||
table above — a 2026-08-11 hotfix wired it (a real `print(7)` segfault
|
||
forced the issue; see its row above) — and `type_mismatch_code` (WO-E201)
|
||
has too: haxe-parity Task 2 wired it for `and`/`or`'s non-`Bool`-operand
|
||
check (see its row above). `non_exhaustive_switch_code` (WO-E208) has also
|
||
moved up: haxe-parity Task 3 (`switch` as expression) wired it — the exact
|
||
"genuinely blocked: no `switch` keyword exists yet" gap the nullable-types
|
||
dead-code register named is closed (see its row above). `bad_arity_code`
|
||
(WO-E203) has now moved up too: haxe-parity Task 4 wired it for variant
|
||
payload arity (construction and pattern sites — see its row above). That
|
||
wiring is deliberately narrower than the code's own name promises: a
|
||
user-declared `fn`/method call's arity is still ungated here and caught
|
||
only at emission (WO-E403), the same pre-existing gap the WO-E209 hotfix
|
||
note already disclosed for builtins. Four of the original ten remain dead
|
||
(WO-E204, WO-E211, WO-E212, WO-E213 — the last three are Task 6's own
|
||
forced-handling work). Listed here so a conformance fixture (plan 3) or a
|
||
future reader doesn't assume one of the remaining codes is reachable
|
||
today; move a code up into the table above in the same commit that wires
|
||
its first real emission site.
|
||
|
||
### Reachable but unenforced
|
||
|
||
**WO-E205 was wired 2026-08-18** (branch `type-enforcement`) — the owed gap
|
||
this section used to record is closed. Structural satisfaction (`types.ml`'s
|
||
`class_satisfies`, the same name+arity+non-static rule `emit.ml`'s
|
||
`satisfies` builds vtable rows from) is checked wherever a confidently
|
||
class-typed value flows into an interface-typed slot: a call argument
|
||
against the callee's declared parameter, an annotated `let`, and a `return`
|
||
against the declared return type. The canonical evidence program now fails
|
||
compile at the `quote(r)` call site, and the known-gap fixture moved to
|
||
`tests/corpus/compile-fail/unsatisfied-interface/` (`fixture.code WO-E205`)
|
||
in the same change, exactly as its own comment demanded. See the WO-E205 row
|
||
in the main table above.
|
||
|
||
## WO-E3xx — ownership / MVS (Task 7, `compiler/src/owner.ml`)
|
||
|
||
Every ownership diagnostic carries a second site (the module doc's
|
||
"two-site errors are the product") — the example messages below are the
|
||
primary message only; the related site's label (e.g. "`b` moved here")
|
||
renders indented beneath it.
|
||
|
||
| code | meaning | example message |
|
||
| --- | --- | --- |
|
||
| WO-E301 | a place is read (or moved again) after its value was already moved — by assignment, `take` argument passing, constructor field init, or `return`. | `use of \`b\` after it was moved` |
|
||
| WO-E302 | a place is moved while a live borrow of it, or of an overlapping place, still exists. | `cannot move \`bag.items\` while \`r\` is borrowed` |
|
||
| WO-E303 | two exclusive (`mut`) accesses of the same place, or two accesses the analysis can *prove* overlap, conflict in one region (e.g. two `mut` element accesses through the same provable index, or the same place borrowed and then mutated). Cases the analysis can't prove either way become a residual site for the VM to guard at runtime, not this diagnostic. | `cannot borrow \`bag.items[i]\` as \`mut\` twice in the same call` |
|
||
| WO-E304 | a borrow is returned or stored somewhere that outlives the scope it borrowed from. `@gc`-typed values are exempt (freely aliased by design). | `borrow of \`x\` returned — borrows cannot outlive their scope` |
|
||
|
||
## WO-E4xx — emitter (plan 3 Task 1, `compiler/src/emit.ml`)
|
||
|
||
The emitter's range covers the two boundaries nothing upstream can see:
|
||
the `.wob` format's own encoding limits, and the milestone-1 instruction
|
||
set's edge — surface the front end accepts but the VM has no operation
|
||
for. Both are reported, never worked around: an over-budget method is a
|
||
diagnostic rather than a truncated frame, and a construct with no
|
||
lowering is a diagnostic rather than invented bytecode. No image is
|
||
written when any of these fire (`woc --emit` writes nothing on exit 1).
|
||
|
||
| code | meaning | example message |
|
||
| --- | --- | --- |
|
||
| WO-E401 | the method needs more than 64 registers — the VM's register window (`runtime/src/wob.h` `WO_MAX_REGS`, enforced by the loader). Reported once per method, at the method's own position. | `` `wide` needs more than 64 registers — the VM's register window is 64 slots; split the method or reduce the number of live locals `` |
|
||
| WO-E402 | a value that does not fit an instruction field: a constant/class/method/interface-slot index above 65535 (`LOADK`/`NEW`/`CALL`/`ICALL` carry a 16-bit operand), a field index above 255 (`GETF`/`SETF` carry a byte), or a jump farther than the signed 16-bit displacement. | `field index 300 exceeds the 8-bit GETF/SETF field` |
|
||
| WO-E403 | a construct the v1 instruction set cannot express, or a call the emitter cannot lower correctly. The full source-surface contract is [`08-builtin-surface.md`](08-builtin-surface.md); the cases raised here are: an unresolved name; a call to something that is neither a declared `fn` nor a builtin; a wrong argument count (nothing upstream checks arity — WO-E203 is declared and never raised — and a mismatched call reserves a window the callee does not read, which the loader rejects); a field/method on a type that is not a declared class; `multi_new()`/`map_new()` with no destination of declared type (the element kinds are the container's runtime drop plan and cannot be guessed); an element write into a `multi` (v1 has `multi_push`/`multi_get`, no element store); a `for` over a `map` (v1 exposes no key enumeration); (haxe-parity Task 2) `break`/`continue` with no enclosing loop — nothing upstream tracks loop nesting to reject it earlier; (haxe-parity Task 2) a `"${expr}"` interpolant whose type is neither `Text` nor `Int`, or is unresolvable; (haxe-parity Task 4) a field default the emitter cannot lower when a constructor literal omits that field — defaults are opaque token spans by design (ast.ml), and the lowerable set is exactly the sample's own shapes: Int (optionally negated), Text, and Bool literals, `now()`, and `[]` (an empty container, kinds from the field's declared type). Several of these are cases the typechecker's placeholder types let through — the emitter is the first stage that must be exact. | `` `for` can only iterate a `multi` — the v1 builtins expose no key enumeration for a `map` `` |
|
||
| WO-E404 | an ownership-table entry the emitter could not honor: a residual borrow site whose operand has no register at the guarded region, a residual region no lowering wrapped at all (checked at the end of every compilation unit — owner.ml anchors regions on several different node kinds, and one nobody consumed would ship the aliasing check silently disabled), or a drop/rc site naming a local that has no register. Emitting such a region unguarded would drop the single enforcement a residual site exists for, so it fails instead. | `` residual borrow site in `shuffle` names an operand with no live register — the runtime guard cannot be placed `` |
|
||
| WO-E405 | the program entry (the zero-arg free fn `main`, selected by name) declares a return type other than `Int`. The systems-track spec (`docs/superpowers/specs/2026-08-01-systems-track-design.md:70`) makes the entry's return value the process exit code, so any other declared return type was never legal — this is the check that finally says so. `main` with no return annotation at all is unaffected (nothing declared to contradict `Int`); every other milestone-1 fixture uses that form. Reported once, at `main`'s own position. | `` entry `main` declares return type `Node` — the entry's return value is the process exit code, so it must return `Int` `` |
|
||
| WO-E406 | haxe-parity Task 1 (modules). A call through a reserved stdlib alias (`use fs`/`proc`/`net`/`time`/`json`/`env`) survived typechecking — `types.ml` accepts it as UNKNOWN-BUT-RESERVED, since the six namespaces' members arrive in plan 9 — and reached emission. There is nothing to lower it to yet; an unused `use fs` never reaches this code at all, only a call that actually goes through it. | `` stdlib module `fs` is not linked in this milestone (called as `fs.stat`) `` |
|
||
|
||
## Completeness method
|
||
|
||
Every code in this catalog was found the same way: grep every
|
||
`Diag.error`/`Diag.warning` call site across `compiler/src/*.ml` (lexer,
|
||
parser, types, owner — dump.ml never constructs a diagnostic) and
|
||
`compiler/bin/main.ml` (the driver — added after a Task 8 review found
|
||
WO-E214 living there, outside the original src/*.ml-only sweep), then
|
||
cross-reference each `~code:` argument back to the `let <name>_code = ...`
|
||
constant it names. Every constant with at least one such call site is
|
||
in the table above; every constant with zero call sites is listed under
|
||
"Reserved, not yet emitted" instead of silently omitted. `parser.ml`'s
|
||
`fail`/`unexpected`, `owner.ml`'s `report`/`escape`/`check_against_borrows`,
|
||
`emit.ml`'s `err`/`over_budget`/`check_bx`/`check_field_idx`, `main.ml`'s
|
||
`report_collision`, and `types.ml`'s `report_duplicate_decl` (plan 3 Task 2)
|
||
are the only indirection layers
|
||
between a bare `~code:` argument and the `Diag.error` call — each was
|
||
read to confirm which named constant ultimately reaches the collector,
|
||
not just the arity-generic wrapper name. This is why the catalog is an
|
||
enumeration, not a dig: `diag.ml` already reserves the numeric ranges,
|
||
so the only open question per stage was *which* reserved codes actually
|
||
fire — answered by exhaustive grep, not inspection of a handful of
|
||
samples.
|