Part of epic #712.
Problem
local@<DOMAIN>:<SLOT> ("generate no RPC boilerplate for this slot") is per-slot only. A survey of every use shows the annotation is modeling the wrong concept at the wrong granularity.
Usage today:
| class |
local slots |
| api_yplot |
13/13 |
| yview |
9/9 |
| ynet capture |
14/14 |
| ymap |
16/16 |
| ymusic |
6/6 |
| yflame |
16/16 |
| ycircuit |
12/12 |
| ychrome |
6/6 |
| yjupyter (client + protocol) |
27/27 |
| ynotebook |
33/33 |
| yapp |
3/3 |
| yfigure base slots |
9/9 |
| ytermsink sink |
5/5 |
| yplatform (window, platform, clipboard, window-chrome) |
~25/25 |
| yscene |
1/15 (only set_registry) |
| ygui widget |
3/9 (widget_paint, widget_emit_container, widget_emit_body) |
| yrich document |
3/14 (render, apply_op, destroy) |
~190 annotation lines restating a per-class fact slot by slot, plus a prose comment per file saying the same thing ("Every slot is local@ — never proxied over RPC").
The conceptual bug
Locality in yclass is already a property of the object reference, resolved at runtime: the generated stub dispatches directly when ctx->session == NULL and marshals over the wire when a session is set. An object is local or remote; its methods are not individually one or the other.
What a per-slot local@ actually produces on a proxy is a method whose stub has no RPC branch — calling it dispatches "locally" into an object with no implementation on this side. It is not "a method that runs locally"; it is a method absent from the remote interface. local@ is therefore encoding interface membership as a per-method attribute — a workaround for the model having no way to say "this class is not remotable" / "this base class is host-side."
The three "mixed" classes are each two interfaces fused into one class:
- the remote API — callable through a proxy (yscene's 14
node_* slots, yrich document's scalar editing slots, ygui widget's manipulation methods);
- the host SPI — virtual slots the host process invokes on the real object (the compositor calls
render, the input statemachine calls process_input, the terminal implements the ytermsink callbacks). Never called through a proxy; from the remote side they do not exist.
The codebase already separates these structurally: yfigure:figure (all slots local) is the host SPI base; yscene:scene derives from it and adds the remote API. ytermsink:sink is a pure host-callback base the terminal derives from. The per-slot annotation is redundant with a structure the class graph already expresses.
A second consequence of the current default: wire exposure is what you get by forgetting an annotation. A new slot silently joins the RPC surface (stub RPC branch + skel entry) unless someone remembers local@. The wire surface is a compatibility and security contract; it must be opt-in.
Proposal: class-level locality, no per-slot markers
- Locality is declared per class, not per slot.
- Default: local. No RPC branch, no skel entries, nothing on the wire. (Naming: an explicit class-level
local@/norpc@ marker is unnecessary once local is the default; rpc@ is the only marker. "local" is preferred over "norpc" anywhere a negative form is needed transitionally, since it names what the thing is rather than what it lacks.)
rpc@<DOMAIN>:<CLASS> on the class struct (next to class@): every slot introduced by this class is wire-exposed.
- Inherited slots keep the base class's locality. A remotable class deriving from a local base is exactly the "host SPI + remote API" composition: figure's
render/process_input stay host-side; scene's node_* go on the wire. No slot-level exceptions.
- Marshallability is validation, not annotation. An
rpc@ class introducing a slot with unmarshallable arguments (raw pointers, in-process carriers) is a hard codegen error at that slot — not a silent local fallback. Today's local@ exists largely to dodge this; under the flip it becomes the guard rail.
oneway@ folds into the rpc surface (a variant/flag of the wire form; only meaningful on rpc classes). The current "oneway@ + local@ is an error" check disappears structurally.
- Per-slot
local@ is retired, not kept as an escape hatch. The three current stragglers are misplaced methods, not evidence for the mechanism:
yscene:set_registry binds a borrowed in-process registry pointer from the framework — host-side construction wiring; move it to the host create/configure path (or a host-only helper), off the class's remote surface.
ygui widget_paint / widget_emit_container / widget_emit_body — host paint SPI; move to a local base/mixin, same pattern as figure's render.
yrich document_render / document_apply_op — figure-side render contract; same treatment.
End state: the entire RPC surface of the codebase is the set of rpc@ classes — one greppable, auditable declaration per remotable class. Everything else generates zero wire code. ~190 per-slot lines and all the "every slot is local@" comments are deleted.
Migration sketch
- Introduce class-level
rpc@<DOMAIN>:<CLASS> and inherited-locality resolution; keep per-slot local@ recognized. Annotate yscene, yrich document, ygui widget as rpc@. Generated output must be byte-identical for all-local classes; for the rpc classes it differs only where a per-slot annotation was missing/wrong — which is the audit.
- Move the three host-SPI/wiring stragglers off the remote classes (scene
set_registry → host construction; widget paint/emit slots → local base/mixin; document render/apply_op → figure side).
- Flip the default to local for unannotated classes; per-slot
local@ becomes a codegen error with a pointer at the class-level form; delete all per-slot lines.
- Fold
oneway@ into the rpc form.
Part of epic #712.
Problem
local@<DOMAIN>:<SLOT>("generate no RPC boilerplate for this slot") is per-slot only. A survey of every use shows the annotation is modeling the wrong concept at the wrong granularity.Usage today:
set_registry)widget_paint,widget_emit_container,widget_emit_body)render,apply_op,destroy)~190 annotation lines restating a per-class fact slot by slot, plus a prose comment per file saying the same thing ("Every slot is local@ — never proxied over RPC").
The conceptual bug
Locality in yclass is already a property of the object reference, resolved at runtime: the generated stub dispatches directly when
ctx->session == NULLand marshals over the wire when a session is set. An object is local or remote; its methods are not individually one or the other.What a per-slot
local@actually produces on a proxy is a method whose stub has no RPC branch — calling it dispatches "locally" into an object with no implementation on this side. It is not "a method that runs locally"; it is a method absent from the remote interface.local@is therefore encoding interface membership as a per-method attribute — a workaround for the model having no way to say "this class is not remotable" / "this base class is host-side."The three "mixed" classes are each two interfaces fused into one class:
node_*slots, yrich document's scalar editing slots, ygui widget's manipulation methods);render, the input statemachine callsprocess_input, the terminal implements the ytermsink callbacks). Never called through a proxy; from the remote side they do not exist.The codebase already separates these structurally:
yfigure:figure(all slots local) is the host SPI base;yscene:scenederives from it and adds the remote API.ytermsink:sinkis a pure host-callback base the terminal derives from. The per-slot annotation is redundant with a structure the class graph already expresses.A second consequence of the current default: wire exposure is what you get by forgetting an annotation. A new slot silently joins the RPC surface (stub RPC branch + skel entry) unless someone remembers
local@. The wire surface is a compatibility and security contract; it must be opt-in.Proposal: class-level locality, no per-slot markers
local@/norpc@marker is unnecessary once local is the default;rpc@is the only marker. "local" is preferred over "norpc" anywhere a negative form is needed transitionally, since it names what the thing is rather than what it lacks.)rpc@<DOMAIN>:<CLASS>on the class struct (next toclass@): every slot introduced by this class is wire-exposed.render/process_inputstay host-side; scene'snode_*go on the wire. No slot-level exceptions.rpc@class introducing a slot with unmarshallable arguments (raw pointers, in-process carriers) is a hard codegen error at that slot — not a silent local fallback. Today'slocal@exists largely to dodge this; under the flip it becomes the guard rail.oneway@folds into the rpc surface (a variant/flag of the wire form; only meaningful on rpc classes). The current "oneway@+local@is an error" check disappears structurally.local@is retired, not kept as an escape hatch. The three current stragglers are misplaced methods, not evidence for the mechanism:yscene:set_registrybinds a borrowed in-process registry pointer from the framework — host-side construction wiring; move it to the host create/configure path (or a host-only helper), off the class's remote surface.yguiwidget_paint/widget_emit_container/widget_emit_body— host paint SPI; move to a local base/mixin, same pattern as figure'srender.yrichdocument_render/document_apply_op— figure-side render contract; same treatment.End state: the entire RPC surface of the codebase is the set of
rpc@classes — one greppable, auditable declaration per remotable class. Everything else generates zero wire code. ~190 per-slot lines and all the "every slot is local@" comments are deleted.Migration sketch
rpc@<DOMAIN>:<CLASS>and inherited-locality resolution; keep per-slotlocal@recognized. Annotate yscene, yrich document, ygui widget asrpc@. Generated output must be byte-identical for all-local classes; for the rpc classes it differs only where a per-slot annotation was missing/wrong — which is the audit.set_registry→ host construction; widget paint/emit slots → local base/mixin; document render/apply_op → figure side).local@becomes a codegen error with a pointer at the class-level form; delete all per-slot lines.oneway@into the rpc form.