Skip to content

Commit c45fb13

Browse files
olwangclaude
andcommitted
docs: ground rewrite roadmap with closure-value probe result
Probe confirms owned Fn is gated to direct parameter position (RS0015 'unsupported owned position'); closures aren't first-class storable values from the surface, but the VmClosure/CallClosure + Box<dyn FnMut> substrate already exists. So L1 is lifting a frontend gate (owned-only storable, sound by move-ownership), not building closures. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 060afe8 commit c45fb13

1 file changed

Lines changed: 32 additions & 11 deletions

File tree

docs/planning/declarative-rewrite-roadmap.md

Lines changed: 32 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -41,20 +41,41 @@ target parameter is `noescape Fn()`"). This is a deliberate review-first
4141
restriction (an escaping closure that captures is a retention/aliasing concern).
4242

4343
So the precise need is **owned, escaping closures storable in a collection**
44-
(`List<Fn(UOp) -> UOp>` of rules). The VM already represents them (a `Closure` is
45-
an `Rc`, trivially storable); the gap is the **checker's escape rule** plus a
46-
**storable `Fn` value type** in signatures. Relaxing this is the one real language
47-
decision — and it must clear RSScript's admission bar: an escaping+capturing
48-
closure has to *phrase as a reviewer question* (what it captured, whether it can
49-
retain), e.g. via an explicit `own`/move capture annotation and the `Fn` type
50-
surfacing in the signature, so nothing is implicit.
44+
(`List<owned Fn(UOp) -> UOp>` of rules).
45+
46+
**Probe result (confirmed empirically).** RSScript already has `owned Fn(...)`
47+
(escaping, move-captures owned values, lowers to `Box<dyn FnMut>`), but it is
48+
gated: a `List<owned Fn(Int)->Int>` field / generic arg is rejected with
49+
`RS0015`*"`owned Fn(...)` is only supported as a direct function parameter
50+
type" / "unsupported owned position"* — and consequently a closure literal in a
51+
value position is an "unsupported expression", a struct can't hold one, and a
52+
list-fetched closure `f(x)` "does not resolve". So closures are usable **only
53+
inline at the call site of a function with an `Fn` parameter** — not as
54+
first-class values. The gap is a **frontend restriction (parser + checker)**, NOT
55+
a missing capability: the runtime (`VmClosure`/`MakeClosure`/`CallClosure`) and
56+
the lowering (`Box<dyn FnMut>`, `move` capture) substrate already exist.
57+
58+
So L1 is "lift a deliberate gate", not "build closures." The sound rule:
59+
**storable ⇒ `owned` ⇒ move-captures owned values ⇒ no aliasing**, with the
60+
`owned Fn` type visible wherever it's stored. That preserves review-first by the
61+
same argument that already makes `owned` parameters sound (ownership moved in, no
62+
shared mutable state). `noescape`/`read Fn` stay parameter-only (storing a
63+
borrow-capturing closure would let a borrow escape — unsound), so the relaxation
64+
is narrow and one-directional.
5165

5266
## Layered plan (priority order)
5367

54-
1. **L1 — language (keystone): owned/escaping storable closures.** Allow a
55-
closure that captures owned values to escape into a value of a first-class
56-
`Fn(args) -> ret` type and be stored in containers. Make the capture + escape
57-
explicit in the signature (review-first). Everything else rides on this.
68+
1. **L1 — language (keystone): make `owned Fn` a first-class value.** Lift the
69+
"parameter-position-only" gate so `owned Fn(args) -> ret` is allowed as a
70+
generic type argument (`List<owned Fn ...>`), a struct field, and a
71+
`let`/`local` binding; accept a closure literal as a value expression; and
72+
resolve a call on a closure-typed binding (`f(x)`). Keep `noescape`/`read Fn`
73+
parameter-only. Work is **parser + checker** (type a closure literal as
74+
`owned Fn`; allow it in storable positions; resolve closure-value calls) plus a
75+
**lowering** glue step (emit closure values + `Box<dyn FnMut>` in non-parameter
76+
positions, call closure-typed loads) — all on the existing
77+
`MakeClosure`/`CallClosure` + `Box<dyn FnMut>` substrate. Parity-gated like any
78+
feature. Everything else rides on this.
5879
2. **L2 — library/runtime: PatternMatcher + graph_rewrite.** With L1, build a
5980
`UOp`/`UPat` type and a native `graph_rewrite(sink, rules, bottom_up, name)`
6081
driver (fixpoint + memoization) plus a gated `toposort(gate: Fn(UOp)->Bool)`

0 commit comments

Comments
 (0)