feat(compiler): remove @gc from the language — GC-ness is fully inferred (7b)

`@gc` is no longer part of the language: a developer never writes or mentions
it. GC-ness is decided entirely by inference (structural cycles + demand
promotion), which the earlier 7b commits made complete and precise.

- parser: `@gc` on a class is now WO-E104 ("GC-ness is inferred; run
  `woc --dump-gc`. Remove it."). `is_gc` stays false; the class classifies by
  inference. No `.wo` in the repo carries `@gc` anymore.
- types.ml: retired the WO-W201 machinery (suggest_gc_annotation,
  has_recursive_structure(_type), has_unique_field, gc_suggestion_code) — it
  suggested `@gc`, now obsolete since inference traces exactly those classes.
- runner.ml: deleted the 8 WO-W201 gc-suggestion test blocks; the @gc-exemption
  test's `Cache` is made self-referential so inference classifies it gc without
  an annotation.
- fixtures: dropped `@gc` from rc.wo (Cache demand-promotes via its escape),
  elision.wo (Cache given a self-ref to stay structurally gc for the rc-elision
  dump), pricing-demo.wo (PriceCache doesn't escape -> now owned), and the
  abandoned-cycle/budget-steps corpus (Node is structurally gc). rc.wo keeps a
  placeholder comment line so its line-indexed rc assertions hold. Goldens
  re-blessed.
- docs: error catalog gains WO-E104 and marks WO-W201 retired; gc-cycle README
  records the keyword removal.

Verified: woc-test 553/0 (was 566 minus the 13 retired WO-W201 checks),
test_diag 14/0, oop-e2e 79/0, employee 8/0, log-watcher 7/0. `git grep '@gc'`
finds only comments — success criterion 1 met.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
shoney.arickathil 2026-08-18 19:19:09 +02:00
parent 1cedbda0bb
commit 2379a95016
14 changed files with 50 additions and 169 deletions

1
.gitignore vendored
View file

@ -101,3 +101,4 @@ prototypes/wo-db/
__pycache__/ __pycache__/
*.pyc *.pyc
dist/ dist/
docs/examples/*/target/

View file

@ -126,6 +126,7 @@ let fail (st : state) (site : Ast.pos) (code : string) (message : string) : 'a =
let syntax_code = Diag.parsing_prefix ^ "01" (* WO-E101: generic syntax error *) let syntax_code = Diag.parsing_prefix ^ "01" (* WO-E101: generic syntax error *)
let table_code = Diag.parsing_prefix ^ "02" (* WO-E102: invalid @table(...) configuration *) let table_code = Diag.parsing_prefix ^ "02" (* WO-E102: invalid @table(...) configuration *)
let gc_removed_code = Diag.parsing_prefix ^ "04" (* WO-E104: `@gc` — GC-ness is inferred *)
(* haxe-parity Task 2: the haxe keyword verdict table's `inline` row — (* haxe-parity Task 2: the haxe keyword verdict table's `inline` row —
"adopt (values): const compile-time values; inline *functions* "adopt (values): const compile-time values; inline *functions*
@ -332,10 +333,19 @@ let parse_type_annotations (st : state) : type_annotations =
let table = ref None in let table = ref None in
while peek st = Token.At do while peek st = Token.At do
ignore (advance st); ignore (advance st);
let at_pos = peek_pos st in
let name = expect_ident st "annotation name" in let name = expect_ident st "annotation name" in
(match name with (match name with
| "gc" -> | "gc" ->
is_gc := true; (* iteration 7b: `@gc` is not part of the language — GC-ness is inferred
(structural cycles + demand promotion; see `woc --dump-gc`). Reject it
rather than accept a meaningless annotation. `is_gc` stays false. *)
Diag.Collector.add st.collector
(Diag.error ~code:gc_removed_code ~file:st.file ~line:at_pos.Ast.line
~col:at_pos.Ast.col
~message:
"`@gc` is not a valid annotation: GC-ness is inferred by the \
compiler (run `woc --dump-gc`). Remove it." ());
skip_paren_args st skip_paren_args st
| "table" -> table := Some (parse_table_cfg st) | "table" -> table := Some (parse_table_cfg st)
| _ -> skip_paren_args st); | _ -> skip_paren_args st);

View file

@ -302,34 +302,6 @@ let with_builtin_records (syms : symbols) : symbols =
{ acc with classes = StringMap.add name info acc.classes }) { acc with classes = StringMap.add name info acc.classes })
syms predeclared_records syms predeclared_records
let rec has_recursive_structure (cls : class_info) : bool =
List.exists (fun (_, ty, _, _) ->
match ty with
| Ast.Scalar name -> name = cls.name (* direct self-reference *)
| Ast.Ref name -> name = cls.name
| Ast.Multi name -> name = cls.name (* multi Self *)
| Ast.Map (k, v) -> k = cls.name || v = cls.name (* map<_, Self> / map<Self, _> *)
| Ast.Backlink _ -> false
| Ast.Nullable inner -> has_recursive_structure_type inner cls.name
) cls.fields
and has_recursive_structure_type (ty : Ast.field_ty) (cls_name : string) : bool =
match ty with
| Ast.Scalar name -> name = cls_name
| Ast.Ref name -> name = cls_name
| Ast.Multi name -> name = cls_name
| Ast.Map (k, v) -> k = cls_name || v = cls_name
| Ast.Backlink _ -> false (* a computed inverse holds no owned structure *)
| Ast.Nullable inner -> has_recursive_structure_type inner cls_name
(* @unique field -> persistent identity (plan's "When NOT to emit": a
class with a @unique field should not get the @gc suggestion even if
it also has recursive/shared structure). Annotation *names* only, per
Ast.field's own doc comment -- "unique" is what parse_field stores for
a bare `@unique`. *)
let has_unique_field (cls : class_info) : bool =
List.exists (fun (_, _, _, anns) -> List.mem "unique" anns) cls.fields
(* iteration 7b: GC-ness is inference-first. A class is traced if the inference (* iteration 7b: GC-ness is inference-first. A class is traced if the inference
pass put it in `syms.traced` (structural cycle, or a Phase-2 demand pass put it in `syms.traced` (structural cycle, or a Phase-2 demand
promotion), OR — as a temporary bridge until demand promotion lands — it promotion), OR — as a temporary bridge until demand promotion lands — it
@ -338,15 +310,6 @@ let is_gc_class (syms : symbols) name =
StringSet.mem name syms.traced StringSet.mem name syms.traced
|| (try (StringMap.find name syms.classes).is_gc with Not_found -> false) || (try (StringMap.find name syms.classes).is_gc with Not_found -> false)
let gc_suggestion_code = Diag.warning_prefix ^ "201" (* WO-W201 *)
let suggest_gc_annotation ~file (cls : class_info) (collector : Diag.Collector.t) : unit =
if not cls.is_gc && Option.is_none cls.table && not (has_unique_field cls)
&& has_recursive_structure cls then
Diag.Collector.add collector
(Diag.warning ~code:gc_suggestion_code ~file ~line:cls.pos.line ~col:cls.pos.col
~message:(Printf.sprintf "%s has recursive/shared structure that borrow checker cannot prove. Consider adding @gc if this is an ephemeral in-memory cache. If this maps to a database table, keep owned (default)." cls.name) ())
(* Ast.field_ty -> the internal resolved typ. Hoisted out of (* Ast.field_ty -> the internal resolved typ. Hoisted out of
typecheck_program (where it was a local closure) so the .wob emitter typecheck_program (where it was a local closure) so the .wob emitter
can reach the same mapping instead of keeping a second copy of it; can reach the same mapping instead of keeping a second copy of it;
@ -515,9 +478,7 @@ let collect_declarations ~file (prog : program) (collector : Diag.Collector.t) :
| Some (existing : class_info) -> | Some (existing : class_info) ->
report_duplicate_decl collector ~file ~kind:"class" ~name:c.name ~pos:c.pos report_duplicate_decl collector ~file ~kind:"class" ~name:c.name ~pos:c.pos
~first_pos:existing.pos ~first_pos:existing.pos
| None -> | None -> classes := StringMap.add c.name info !classes)
classes := StringMap.add c.name info !classes;
suggest_gc_annotation ~file info collector)
| Ast.Interface i -> | Ast.Interface i ->
let methods = List.map (fun (m : Ast.method_sig) -> let methods = List.map (fun (m : Ast.method_sig) ->
{ name = m.name; { name = m.name;

View file

@ -14,11 +14,11 @@
22:5 ASSIGN self.prices = amount 22:5 ASSIGN self.prices = amount
25:3 METHOD adopt(take other: Product) -> Product 25:3 METHOD adopt(take other: Product) -> Product
26:5 RETURN other 26:5 RETURN other
31:1 CLASS PriceCache @gc 30:1 CLASS PriceCache
32:3 FIELD entries: map<Text, Int> 31:3 FIELD entries: map<Text, Int>
35:1 TYPE Note 34:1 TYPE Note
36:3 FIELD id: Id 35:3 FIELD id: Id
37:3 FIELD body: Text 36:3 FIELD body: Text
38:3 FIELD created: Timestamp = now() 37:3 FIELD created: Timestamp = now()
41:1 METHOD discount(mut amount: Int, take pct: Int) -> Int 40:1 METHOD discount(mut amount: Int, take pct: Int) -> Int
42:3 RETURN amount 41:3 RETURN amount

View file

@ -27,7 +27,6 @@ class Product {
} }
} }
@gc
class PriceCache { class PriceCache {
entries: map<Text, Int> entries: map<Text, Int>
} }

View file

@ -2,14 +2,15 @@
k0 TEXT "Cache" k0 TEXT "Cache"
k1 TEXT "Holder" k1 TEXT "Holder"
k2 TEXT "hits" k2 TEXT "hits"
k3 TEXT "cache" k3 TEXT "peer"
k4 TEXT "read" k4 TEXT "cache"
k5 TEXT "proven" k5 TEXT "read"
k6 TEXT "main" k6 TEXT "proven"
k7 INT 41 k7 TEXT "main"
k8 INT 1 k8 INT 41
k9 INT 1
== CLASSES == == CLASSES ==
c0 Cache flags=gc fields=[hits:SCALAR] c0 Cache flags=gc fields=[hits:SCALAR, peer:GCREF]
c1 Holder flags=- fields=[cache:GCREF] c1 Holder flags=- fields=[cache:GCREF]
== INTERFACES == == INTERFACES ==
== VTABLES == == VTABLES ==
@ -33,7 +34,7 @@ m2 main args=0 regs=6 [free fn] [ENTRY]
drops: pc 14 owned={} gc={r0} drops: pc 14 owned={} gc={r0}
drops: pc 15 owned={} gc={} drops: pc 15 owned={} gc={}
0000 NEW r0, c0 0000 NEW r0, c0
0001 LOADK r1, k7 0001 LOADK r1, k8
0002 SETF r0, f0, r1 0002 SETF r0, f0, r1
0003 NEW r1, c1 0003 NEW r1, c1
0004 MOVE r2, r0 0004 MOVE r2, r0
@ -42,7 +43,7 @@ m2 main args=0 regs=6 [free fn] [ENTRY]
0007 MOVE r4, r1 0007 MOVE r4, r1
0008 CALL r4, m1 0008 CALL r4, m1
0009 MOVE r3, r4 0009 MOVE r3, r4
0010 LOADK r5, k8 0010 LOADK r5, k9
0011 ADD r2, r3, r5 0011 ADD r2, r3, r5
0012 BUILTIN r2, r2, print_int 0012 BUILTIN r2, r2, print_int
0013 DROP r1 0013 DROP r1

View file

@ -7,9 +7,9 @@
-- the emitted body must contain NO borrow op and NO rc op at all — the -- the emitted body must contain NO borrow op and NO rc op at all — the
-- disassembly below is the evidence. `main` is the contrast: an escape -- disassembly below is the evidence. `main` is the contrast: an escape
-- into a field is a KEPT acquire, so RC_INC does appear there. -- into a field is a KEPT acquire, so RC_INC does appear there.
@gc
class Cache { class Cache {
hits: Int hits: Int
peer: ?Cache
} }
class Holder { class Holder {

View file

@ -27,7 +27,6 @@ class Product {
} }
} }
@gc
class PriceCache { class PriceCache {
entries: map<Text, Int> entries: map<Text, Int>
} }

View file

@ -1,4 +1,4 @@
@gc -- Cache is inferred `gc`: it escapes (returned out of `escaping`), so demand promotion traces it.
class Cache { class Cache {
n: Int n: Int
} }

View file

@ -1231,98 +1231,6 @@ let () =
check "Float is not a builtin scalar" (not (Types.is_builtin_scalar "Float")); check "Float is not a builtin scalar" (not (Types.is_builtin_scalar "Float"));
check "Timestamp is a builtin scalar" (Types.is_builtin_scalar "Timestamp") check "Timestamp is a builtin scalar" (Types.is_builtin_scalar "Timestamp")
let () =
(* class Node { next: Node } -- direct self-reference, no @gc, no
@table, no @unique field: WO-W201 must fire, at the class's own
(real) file/line/col, and a warning-only run must still exit 0
(Diag.Collector's severity-keyed exit-code contract). *)
let path = "node.wo" in
let _, collector = typecheck_str ~file:path "class Node {\n next: Node\n}\n" in
let diags = Diag.Collector.diagnostics collector in
check_eq "gc-suggestion: exactly one diagnostic (WO-W201)" ~expected:1
~actual:(List.length diags) string_of_int;
(match diags with
| [ d ] ->
check "gc-suggestion: code is WO-W201" (d.Diag.code = "WO-W201");
check "gc-suggestion: severity is Warning" (d.Diag.severity = Diag.Warning);
check "gc-suggestion: real file/line/col (node.wo:1:1, the `class` token)"
(d.Diag.site.Diag.file = path && d.Diag.site.Diag.line = 1 && d.Diag.site.Diag.col = 1)
| _ -> check "gc-suggestion: exactly one diagnostic" false);
check_eq "gc-suggestion: a warning-only run exits 0, not 1" ~expected:0
~actual:(Diag.Collector.exit_code collector) string_of_int
let () =
(* @gc class Cache { next: Cache } -- same recursive shape as above,
but already @gc: WO-W201 must NOT fire. *)
let _, collector = typecheck_str ~file:"cache.wo" "@gc\nclass Cache {\n next: Cache\n}\n" in
check_eq "gc-suggestion: @gc class reports nothing" ~expected:0
~actual:(List.length (Diag.Collector.diagnostics collector)) string_of_int
let () =
(* @table(...) class Node2 { next: Node2 } -- recursive, but DB-backed
via @table: WO-W201 must NOT fire (plan: "@table -> must be owned"). *)
let _, collector =
typecheck_str ~file:"node2.wo"
"@table(name: \"nodes\")\nclass Node2 {\n next: Node2\n}\n"
in
check_eq "gc-suggestion: @table class reports nothing" ~expected:0
~actual:(List.length (Diag.Collector.diagnostics collector)) string_of_int
let () =
(* class Node3 { id: Id @unique; next: Node3 } -- recursive, but has a
@unique field (persistent identity): WO-W201 must NOT fire (plan's
"When NOT to emit" list, second bullet). *)
let _, collector =
typecheck_str ~file:"node3.wo"
"class Node3 {\n id: Id @unique\n next: Node3\n}\n"
in
check_eq "gc-suggestion: class with a @unique field reports nothing" ~expected:0
~actual:(List.length (Diag.Collector.diagnostics collector)) string_of_int
let () =
(* class Point { x: Int; y: Int } -- a plain data struct, no
recursive/shared fields: WO-W201 must NOT fire either. *)
let _, collector =
typecheck_str ~file:"point.wo" "class Point {\n x: Int\n y: Int\n}\n"
in
check_eq "gc-suggestion: simple data struct reports nothing" ~expected:0
~actual:(List.length (Diag.Collector.diagnostics collector)) string_of_int
let () =
(* class Calc { items: multi Item } (golden/ast/body-statements.wo's own
shape) -- a `multi` field of an UNRELATED type, not `multi Self`.
has_recursive_structure must key off self-reference, not "any multi
field": a bare `Ast.Multi _ -> true` would spuriously fire WO-W201
on every plain data class that merely holds a collection. *)
let _, collector =
typecheck_str ~file:"calc.wo" "class Calc {\n items: multi Item\n}\n"
in
check_eq "gc-suggestion: unrelated `multi Item` field reports nothing" ~expected:0
~actual:(List.length (Diag.Collector.diagnostics collector)) string_of_int
let () =
(* class Bucket { entries: map<Text, Item> } -- same over-trigger risk
for `map`, neither side self-referential. *)
let _, collector =
typecheck_str ~file:"bucket.wo" "class Bucket {\n entries: map<Text, Item>\n}\n"
in
check_eq "gc-suggestion: unrelated `map<Text, Item>` field reports nothing" ~expected:0
~actual:(List.length (Diag.Collector.diagnostics collector)) string_of_int
let () =
(* class Tree { children: multi Tree } -- `multi Self` must still fire
(the plan's own literal example of the heuristic). *)
let path = "tree.wo" in
let _, collector =
typecheck_str ~file:path "class Tree {\n children: multi Tree\n}\n"
in
let diags = Diag.Collector.diagnostics collector in
check_eq "gc-suggestion: `multi Self` still fires WO-W201" ~expected:1
~actual:(List.length diags) string_of_int;
match diags with
| [ d ] -> check "gc-suggestion: `multi Self` diagnostic is WO-W201" (d.Diag.code = "WO-W201")
| _ -> check "gc-suggestion: `multi Self` exactly one diagnostic" false
let () = let () =
(* class BadExample { code: INVALID_TYPE } -- INVALID_TYPE is not a (* class BadExample { code: INVALID_TYPE } -- INVALID_TYPE is not a
builtin, class, or interface: WO-E225 must fire, at builtin, class, or interface: WO-E225 must fire, at
@ -1955,13 +1863,15 @@ let () =
(Option.is_some (find_substring ~needle:"previous loop iteration" d.Diag.message))) (Option.is_some (find_substring ~needle:"previous loop iteration" d.Diag.message)))
let () = let () =
(* @gc is exempt from all of it (spec rule 5): the exact shape that is (* A traced (gc) class is exempt from all of it (spec rule 5): the exact
WO-E301 above is silent here, and produces no move-table entries shape that is WO-E301 above is silent here, and produces no move-table
either — a @gc transfer is an rc site, not a move. *) entries either — a gc transfer is an rc site, not a move. `Cache` is
self-referential (`peer: ?Cache`), so inference classifies it gc without
any annotation (iteration 7b). *)
let src = let src =
"@gc\n\ "class Cache {\n\
class Cache {\n\
\ n: Int\n\ \ n: Int\n\
\ peer: ?Cache\n\
}\n\ }\n\
\n\ \n\
fn keep(take c: Cache) -> Int {\n\ fn keep(take c: Cache) -> Int {\n\

View file

@ -209,13 +209,14 @@ opcode-27/28 retirement, and the sweep list.
**Phase 2b (landed).** Demand promotion: the ownership pass, run in collect **Phase 2b (landed).** Demand promotion: the ownership pass, run in collect
mode, promotes any class whose value *must escape* (returned, stored where it mode, promotes any class whose value *must escape* (returned, stored where it
outlives its scope) — the acyclic-but-aliased case (`PriceCache`) inference by outlives its scope) — the acyclic-but-aliased case inference by structure cannot
structure cannot see. So GC-ness is now fully inferred (cycles by structure + see, targeting the escaping projection's type precisely. So GC-ness is fully
aliasing by demand), and `@gc` is redundant everywhere. What remains is inferred: cycles by structure + aliasing by demand.
*removing the `@gc` keyword itself* — the parser rejecting it, and the RC/`@gc`
golden + `test_diag` assertions being rewritten — which is coupled to Phase 3 **`@gc` removed from the language (landed).** The keyword is now a hard error
(the RC machinery those tests cover is deleted there), so the keyword removal (`WO-E104`): a developer never writes or mentions it; `woc --dump-gc` shows what
lands with Phase 3. inference decided. WO-W201 (which suggested `@gc`) is retired. No `.wo` in the
repo carries `@gc`. This is the front-end half of iteration 7b, complete.
When 7b lands, the acceptance is: When 7b lands, the acceptance is:
- `woc --dump-gc docs/examples/gc-cycle` classifies `Node gc (cycle …)` / - `woc --dump-gc docs/examples/gc-cycle` classifies `Node gc (cycle …)` /

View file

@ -34,6 +34,7 @@ half of the story ("moved here" / "borrowed here" / etc.).
| 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-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-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-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`) ## 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`)
@ -41,7 +42,7 @@ half of the story ("moved here" / "borrowed here" / etc.).
| --- | --- | --- | | --- | --- | --- |
| 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-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-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 *(warning)* | a class has recursive/shared structure (a field, directly or through `ref`/`multi`/`map`/`?`, refers back to its own class) that the ownership pass cannot prove disjoint, has no `@table`, and has no `@unique` field — suggests `@gc`. | `Node has recursive/shared structure that borrow checker cannot prove. Consider adding @gc if this is an ephemeral in-memory cache. If this maps to a database table, keep owned (default).` | | 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-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-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-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-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` |

View file

@ -4,7 +4,6 @@
-- same trick runtime/test/test_cycle.c uses (BIG=130) so ASan can prove -- same trick runtime/test/test_cycle.c uses (BIG=130) so ASan can prove
-- a Node is actually freed, not just recycled inside the arena's own -- a Node is actually freed, not just recycled inside the arena's own
-- freelist where a sanitizer can never see it. -- freelist where a sanitizer can never see it.
@gc
class Node { class Node {
peers: multi Node peers: multi Node
p1: Int p1: Int

View file

@ -4,7 +4,6 @@
-- same trick runtime/test/test_cycle.c uses (BIG=130) so ASan can prove -- same trick runtime/test/test_cycle.c uses (BIG=130) so ASan can prove
-- a Node is actually freed, not just recycled inside the arena's own -- a Node is actually freed, not just recycled inside the arena's own
-- freelist where a sanitizer can never see it. -- freelist where a sanitizer can never see it.
@gc
class Node { class Node {
peers: multi Node peers: multi Node
p1: Int p1: Int