Commit graph

4 commits

Author SHA1 Message Date
de1592c02c refactor(compiler): inference is one library pass, Gcinfer.infer
Extract the structural+demand classification out of main.ml's typecheck_all
into `Gcinfer.infer : parsed -> symbols -> symbols`, so a single library entry
point runs the whole pass. The driver calls it once (before typecheck); the
unit-test helpers can call the same function, so is_gc_class classifies
identically in the binary and in tests (prerequisite for removing the @gc
keyword, whose tests read the golden fixtures through the library directly).

- dune: gcinfer moved after owner (it now runs ownership in collect mode).
- main.ml typecheck_all: the split structural-inject + post-typecheck demand
  loop become one `Gcinfer.infer parsed syms` call.
- Documented the known demand-promotion imprecision (promotes the escaping root
  local's class, over-promoting the container) to refine with Phase 3.

Behavior-neutral: woc-test 566/0, test_diag 14/0, oop-e2e 79/0.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 19:00:11 +02:00
26771e6aa4 feat(compiler): demand promotion — GC-ness fully inferred (7b Phase 2b core)
Structural inference (2a) covers cyclic classes; this adds the DEMAND half for
the acyclic-but-aliased case, so @gc is now redundant everywhere.

- owner.ml: a `promote` sink on ctx. In collect mode, a class value that would
  raise WO-E304 (escape) records its class instead of erroring — because a
  class that MUST escape cannot be owned (second-class borrows can't be stored
  or returned), so it must be traced. `class_of_ty` extracts the class from the
  escaping local's type. analyze/analyze_fn take ?promote.
- main.ml typecheck_all: after structural injection, a fixpoint runs ownership
  in collect mode over every file, unioning promotions into syms.traced (and
  every module table), before owner/emit see it. Terminates (promotions only
  grow, bounded by class count).
- gcinfer.render_final: --dump-gc now reads the authoritative is_gc_class
  (structural + demand + the remaining @gc bridge), with the reason.

Effect: a class that escapes is inferred `gc` with no annotation — e.g. `Cache`
(rc.wo sans @gc) shows `gc (alias escape (demand))`; `Box` returned out of
`leak` is promoted and the program is valid. No false positives: employee's
Department/Employee stay owned; the 566 goldens + 14 test_diag unchanged.

Corpus: compile-fail/borrow-escape-return reclassified to run/ (prints 1) —
returning a borrowed class is now legal under demand promotion; the fixture
encoded pre-7b behavior.

Verified: woc-test 566/0 + test_diag 14/0; oop-e2e 79/0; employee 8/0;
log-watcher 7/0.

NOT in this slice: removing the `@gc` KEYWORD (parser rejection + rewriting the
RC/@gc golden + test_diag assertions + moving the inference injection into the
library so unit tests see it) — coupled to Phase 3, which deletes the RC
machinery those tests cover. The ring still needs Phase 3 to RUN (nullable
`?Node` gcref path).

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 18:53:08 +02:00
f130999018 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>
2026-08-18 16:51:06 +02:00
fadf52cd65 feat(compiler): inferred GC classification + --dump-gc (7b Phase 1)
Structural half of the inference pass, additive — it backs `woc --dump-gc`
and does NOT yet feed field-kind derivation (that is Phase 2 at the
Types.is_gc_class seam), so no emitted bytecode or golden changes.

- compiler/src/gcinfer.ml: build the class-reference graph (edges from
  Scalar/Multi/Map fields, unwrapping ?; ref and backlink contribute NO edge),
  run Tarjan SCC, classify any class in a non-trivial SCC or with a self-loop
  as `gc`, else `owned`; carry a cycle-path reason.
- --dump-gc mode in bin/main.ml (mirrors --dump-owner) + usage line + dune.

Verified:
- docs/examples/gc-cycle -> `Node gc (cycle Node -> Node)`, `Segment owned`.
- docs/examples/employee -> Department/Employee both `owned` (ref/backlink
  make no edge, so no false cycle) — the load-bearing correctness case.
- just woc-test 566/0 (goldens untouched, build clean both flavors).

Remaining Phase 1: a --dump-gc golden fixture (deferred — verified manually to
avoid golden-harness churn this slice). Phases 2-4 per the plan.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-08-18 16:37:31 +02:00