feat(compiler): is_gc_class is inference-first (7b Phase 2a)
GC-ness now comes from the inference pass, not only the annotation. A class is traced if the structural SCC put it in `syms.traced`, OR (temporary bridge until demand-promotion lands) it still carries `@gc`. - Types.symbols gains a `traced : StringSet.t`; is_gc_class reads it (union'd with the surviving @gc annotation). All symbols literals + both merges carry the field. - typecheck_all injects the classification once (Gcinfer.classify -> traced) into the merged table AND every module table, before typecheck/owner/emit. - emit.ml routes the class gc-flag and the union/drop decision through is_gc_class instead of the raw `.is_gc`, so structurally-inferred gc classes get the runtime flag. Field-kind derivation already routed through is_gc_class. - gcinfer.traced_names exposes the traced set for injection. Effect: docs/examples/gc-cycle now COMPILES with no annotation (the WO-E301 use-after-move at the ring-closing store is gone) — traced classes alias freely. Bytecode is byte-identical to writing `@gc class Node`. Verified: woc-test 566/0 (goldens unchanged — every current @gc class stays gc via the annotation branch, and no golden has a structural-gc-non-annotated class); oop-e2e 79/0 (gc corpus green). Not in this slice: demand-promotion (the acyclic-aliased PriceCache case still needs the @gc bridge) and @gc-in-source-as-error (Phase 2b); the ring RUNNING (the RC runtime doesn't implement nullable-gcref `?Node` fields — Phase 3). WO-W201 still fires on gc-cycle (retired in Phase 4). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This commit is contained in:
parent
3777234cdc
commit
484a3259b5
5 changed files with 52 additions and 21 deletions
|
|
@ -244,10 +244,12 @@ let merge_symbols (syms_list : Woc_lib.Types.symbols list) : Woc_lib.Types.symbo
|
|||
typedefs = SM.union keep_first acc.typedefs s.typedefs;
|
||||
unions = SM.union keep_first acc.unions s.unions;
|
||||
modules = acc.modules @ s.modules;
|
||||
traced = Woc_lib.Types.StringSet.union acc.traced s.traced;
|
||||
})
|
||||
Woc_lib.Types.{
|
||||
classes = SM.empty; interfaces = SM.empty; free_fns = SM.empty;
|
||||
typedefs = SM.empty; unions = SM.empty; modules = [];
|
||||
traced = Woc_lib.Types.StringSet.empty;
|
||||
}
|
||||
syms_list
|
||||
|
||||
|
|
@ -358,6 +360,14 @@ let typecheck_all (collector : Woc_lib.Diag.Collector.t) ~(root : string)
|
|||
(* haxe-parity Task 5: the predeclared `Error` record joins the merged
|
||||
table only — see Types.with_builtin_records for why not per-file. *)
|
||||
let syms = Woc_lib.Types.with_builtin_records (merge_symbols (List.map snd per_file_syms)) in
|
||||
(* iteration 7b Phase 2: classify GC-ness once (structural SCC over the class
|
||||
graph, union'd with surviving @gc annotations) and inject the traced set
|
||||
into the merged table AND every module table, so Types.is_gc_class answers
|
||||
from inference everywhere (field kinds, owner exemptions, the class flag). *)
|
||||
let traced = Woc_lib.Gcinfer.traced_names (Woc_lib.Gcinfer.classify syms) in
|
||||
let syms = { syms with Woc_lib.Types.traced } in
|
||||
Hashtbl.fold (fun k v acc -> (k, { v with Woc_lib.Types.traced }) :: acc) module_syms []
|
||||
|> List.iter (fun (k, v) -> Hashtbl.replace module_syms k v);
|
||||
(* `~file_syms` (hotfix, multi-file double-report): `per_file_syms` and
|
||||
`parsed` are both `List.map`s over the same original file list, in
|
||||
the same order, so pairing them positionally is exact -- each
|
||||
|
|
|
|||
|
|
@ -3255,7 +3255,7 @@ and call_window (p : pctx) (f : fstate) (v : views) (e : Ast.expr) ~(recv : Ast.
|
|||
| Some u -> u.Types.u_has_payload
|
||||
| None -> (
|
||||
match Types.StringMap.find_opt n p.p_syms.Types.classes with
|
||||
| Some (ci : Types.class_info) -> not ci.Types.is_gc
|
||||
| Some (ci : Types.class_info) -> not (Types.is_gc_class p.p_syms ci.Types.name)
|
||||
| None -> false))
|
||||
| _ -> false)
|
||||
| None -> false)
|
||||
|
|
@ -4454,7 +4454,7 @@ let emit ~(syms : Types.symbols) ~(module_of : string -> string)
|
|||
cols)
|
||||
cfg.Ast.indexes
|
||||
| None -> ());
|
||||
{ cr_name = c.name; cr_gc = c.is_gc;
|
||||
{ cr_name = c.name; cr_gc = Types.is_gc_class syms c.name;
|
||||
cr_fields =
|
||||
Array.of_list
|
||||
(List.filter_map
|
||||
|
|
|
|||
|
|
@ -75,6 +75,11 @@ type result = {
|
|||
order : string list; (* all class names, sorted — deterministic dump order *)
|
||||
}
|
||||
|
||||
(* the traced class names as a set, for injection into `Types.symbols.traced`
|
||||
(what `Types.is_gc_class` consults). *)
|
||||
let traced_names (r : result) : Types.StringSet.t =
|
||||
SMap.fold (fun k _ acc -> Types.StringSet.add k acc) r.traced Types.StringSet.empty
|
||||
|
||||
let classify (syms : Types.symbols) : result =
|
||||
let classes = syms.Types.classes in
|
||||
let nodes =
|
||||
|
|
|
|||
|
|
@ -125,6 +125,9 @@ and symbols = {
|
|||
typedefs : typedef_info StringMap.t;
|
||||
unions : union_info StringMap.t; (* haxe-parity Task 4 *)
|
||||
modules : string list;
|
||||
traced : StringSet.t;
|
||||
(* iteration 7b: classes inferred `gc` (Gcinfer). Injected after
|
||||
declaration collection; is_gc_class reads it. Empty until then. *)
|
||||
}
|
||||
|
||||
(* Variant lookup by bare name, across every union in scope — how a
|
||||
|
|
@ -327,11 +330,13 @@ and has_recursive_structure_type (ty : Ast.field_ty) (cls_name : string) : bool
|
|||
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
|
||||
pass put it in `syms.traced` (structural cycle, or a Phase-2 demand
|
||||
promotion), OR — as a temporary bridge until demand promotion lands — it
|
||||
still carries the `@gc` annotation for the acyclic-but-aliased case. *)
|
||||
let is_gc_class (syms : symbols) name =
|
||||
try
|
||||
let cls = StringMap.find name syms.classes in
|
||||
cls.is_gc
|
||||
with Not_found -> false
|
||||
StringSet.mem name syms.traced
|
||||
|| (try (StringMap.find name syms.classes).is_gc with Not_found -> false)
|
||||
|
||||
let gc_suggestion_code = Diag.warning_prefix ^ "201" (* WO-W201 *)
|
||||
|
||||
|
|
@ -604,7 +609,8 @@ let collect_declarations ~file (prog : program) (collector : Diag.Collector.t) :
|
|||
) prog.decls;
|
||||
|
||||
{ classes = !classes; interfaces = !interfaces; free_fns = !free_fns;
|
||||
typedefs = !typedefs; unions = !unions; modules = !modules }
|
||||
typedefs = !typedefs; unions = !unions; modules = !modules;
|
||||
traced = StringSet.empty }
|
||||
|
||||
(* ============================================================
|
||||
Pass 2: Body Typechecking
|
||||
|
|
@ -2126,9 +2132,11 @@ let merge_syms_for_module (syms_list : symbols list) : symbols =
|
|||
typedefs = StringMap.union keep_first acc.typedefs s.typedefs;
|
||||
unions = StringMap.union keep_first acc.unions s.unions;
|
||||
modules = acc.modules @ s.modules;
|
||||
traced = StringSet.union acc.traced s.traced;
|
||||
})
|
||||
{ classes = StringMap.empty; interfaces = StringMap.empty; free_fns = StringMap.empty;
|
||||
typedefs = StringMap.empty; unions = StringMap.empty; modules = [] }
|
||||
typedefs = StringMap.empty; unions = StringMap.empty; modules = [];
|
||||
traced = StringSet.empty }
|
||||
syms_list
|
||||
|
||||
(* Generic structural walk over every Ctor/Call site in a program's
|
||||
|
|
|
|||
|
|
@ -183,23 +183,31 @@ inferred). The developer writes no memory annotations for either.
|
|||
|
||||
## Run status
|
||||
|
||||
**This is a target sample for iteration 7b, which is not yet implemented.** It
|
||||
does **not** build on today's toolchain — and the compile error is precisely
|
||||
the motivation. `woc --emit docs/examples/gc-cycle` today reports:
|
||||
Iteration 7b is landing in phases (plan:
|
||||
[`../../superpowers/plans/2026-08-18-inferred-gc-mark-sweep.md`](../../superpowers/plans/2026-08-18-inferred-gc-mark-sweep.md)).
|
||||
|
||||
**Phase 1 (landed).** The inference pass classifies each class; `woc --dump-gc
|
||||
docs/examples/gc-cycle` prints:
|
||||
|
||||
```
|
||||
main.wo: error WO-E301: use of `a` after it was moved
|
||||
c.next = a; <- `a` moved here
|
||||
Node gc (cycle Node -> Node)
|
||||
Segment owned
|
||||
```
|
||||
|
||||
Under the current model a class instance is `owned` and single-owner, so storing
|
||||
`b` into `a.next` **moves** it and closing the ring with `c.next = a` re-uses a
|
||||
moved value. Iteration 7b classifies `Node` as **traced** — and *traced classes
|
||||
alias freely* (spec §3 rule 5), so the ring becomes legal and the collector,
|
||||
not ownership, reclaims it. The pieces still to build: the inference pass
|
||||
(`gcinfer.ml`), `--dump-gc`, the `.wob` opcode-27/28 retirement, the sweep list,
|
||||
and the incremental collector. Today's runtime still uses RC + a Bacon–Rajan
|
||||
cycle collector behind an explicit `@gc` annotation (`runtime/src/gc.c`).
|
||||
**Phase 2a (landed).** `Types.is_gc_class` is now inference-first, so `Node` is
|
||||
traced with **no annotation** and *traced classes alias freely* — the ring
|
||||
**compiles** (the old `WO-E301: use of \`a\` after it was moved` at `c.next = a`
|
||||
is gone), and its bytecode is byte-identical to writing `@gc class Node`.
|
||||
|
||||
**Not yet: the ring runs.** On today's runtime (RC + Bacon–Rajan, `gc.c`) a
|
||||
**nullable single-reference gc field** (`next: ?Node`) store/read is
|
||||
unimplemented — even a one-hop `a.next = b; print(a.next.label)` traps
|
||||
`null receiver` (the existing gc corpus only exercises `multi` gcref fields,
|
||||
which do work). Running the ring, and reclaiming it, is **Phase 3**: the
|
||||
incremental mark-sweep collector + full gcref field paths, the `.wob`
|
||||
opcode-27/28 retirement, and the sweep list. Remaining front-end work
|
||||
(**Phase 2b**): demand-promotion for the acyclic-but-aliased case, then
|
||||
`@gc`-in-source becomes an error and the annotation bridge is removed.
|
||||
|
||||
When 7b lands, the acceptance is:
|
||||
- `woc --dump-gc docs/examples/gc-cycle` classifies `Node gc (cycle …)` /
|
||||
|
|
|
|||
Loading…
Reference in a new issue