plum

#treesitter#compiler#wasm

git clone https://git.pyrossh.dev/plum

A statically typed, imperative programming language inspired by rust, python


b03ab64 — Peter John 2026-11-02T20:32:36+05:30
docs(plans): traits machine plan and run log
docs/plans/2026-11-02-traits.md ADDED
@@ -0,0 +1,253 @@
1
+ # Traits with fields (embedding)
2
+
3
+ **Review plan:** docs/plans/2026-11-02-traits.review.md
4
+ **Goal:** `trait Name(fields) = methods`; a `record` embeds traits by listing them first in its field list, gains their fields and default methods, and converts to each trait wherever one is expected.
5
+ **Architecture:** Before `resolveMembers`, `lowerTraits` copies trait fields and missing defaults into each embedding record, adds accessors `__get_<f>`/`__set_<f>` per field, and replaces each trait with a union enum (`isTrait: true`). Conversion, dispatch and narrowing reuse unions; trait field reads/writes become accessor dispatches in `union_lower`.
6
+ **Tech stack:** Rust workspace, tree-sitter grammar (regenerate with `tree-sitter generate --disable-optimizations`), Plum std/examples.
7
+ **Spec:** docs/plans/2026-11-02-traits.review.md
8
+ **Detail:** contracts
9
+ **Target-min:** 30
10
+ **Budget-usd:** 40
11
+
12
+ Global constraints:
13
+ - Language Rust; fast tests `cargo test -p <crate> --test <file>`; full suite in `.superlite/test-cmd.json`.
14
+ - `touch` an edited .rs before trusting `cargo build --quiet` (stale-build gotcha).
15
+ - Grammar edits: regenerate with `--disable-optimizations`, then `tree-sitter test` must pass the whole corpus.
16
+ - Identifiers are camelCase in Rust (repo style, `#![allow(non_snake_case)]`).
17
+ - Accessor names are exactly `__get_<field>` and `__set_<field>`; a trait union is an `Enum` with `isTrait: true` and one hidden member variant per embedding record (`Entity__House`), in declaration order.
18
+ - v1 exclusions: generic traits, traits embedding traits, `fun*` in traits, struct embedding, `==` on trait values.
19
+ - `plum format <file>` rewrites in place: run it only on copies or on the new example.
20
+
21
+ ```mermaid
22
+ flowchart LR
23
+ SRC[source] --> P["T1 grammar + parser"]
24
+ P --> L["loader merge"]
25
+ L --> TL["T2 lowerTraits"]
26
+ TL --> RM["resolveMembers (exists)"]
27
+ RM --> CK["T4 checker rules"]
28
+ CK --> FA["T5 trait field plan"]
29
+ FA --> UL["union_lower (exists + T5)"]
30
+ UL --> CG["T6 codegen e2e"]
31
+ CK --> LSP["T7 editor"]
32
+ ```
33
+
34
+ File structure: each task's Owns line. Spec coverage: What you will get (T1-T7), What will be built (T1-T9), How it fits together and How it gets built (diagrams), Decisions you can change (defaults taken), Risks (Review Focus), Out of scope (v1 exclusions), How we will know it works (T6, T8, T9), Rough time and cost (Budget-usd).
35
+
36
+ ## Task graph
37
+ ```mermaid
38
+ flowchart LR
39
+ T1[grammar parser]
40
+ T2[lowering]
41
+ T3[formatter]
42
+ T4[checker rules]
43
+ T5[field access]
44
+ T6[codegen e2e]
45
+ T7[editor]
46
+ T8[docs example]
47
+ T9[scene actor]
48
+ %% taskgraph
49
+ T1 --> T2
50
+ T1 --> T3
51
+ T2 --> T4
52
+ T2 --> T5
53
+ T4 --> T6
54
+ T5 --> T6
55
+ T2 --> T7
56
+ T6 --> T8
57
+ T8 --> T9
58
+ ```
59
+
60
+ ```mermaid
61
+ %% schedule
62
+ gantt
63
+ title Predicted schedule (mm:ss)
64
+ dateFormat X
65
+ axisFormat %M:%S
66
+ section lane 1
67
+ T1 grammar and parser :t1, 0, 147s
68
+ T2 trait lowering :t2, 147, 153s
69
+ T4 checker trait rules :t4, 300, 142s
70
+ T6 codegen and run tests :t6, 442, 145s
71
+ T8 docs and examples :t8, 588, 144s
72
+ T9 Scene on trait Actor :t9, 731, 202s
73
+ section lane 2
74
+ T3 formatter and highlighting :t3, 147, 100s
75
+ T5 field access through a trait :t5, 300, 140s
76
+ section lane 3
77
+ T7 editor support :t7, 300, 105s
78
+ ```
79
+
80
+ critical path: T1 → T2 → T4 → T6 → T8 → T9 (15.6 min) · peak width 3 · predicted build 15.6 min · predicted total ≈ 19.1 min (target ✓)
81
+
82
+ ## Package A: syntax
83
+
84
+ **Probe entry:** call=plum_core::parser::parse
85
+
86
+ ### T1 grammar and parser · Complexity 6 · Est. context 50k
87
+ **Owns:** plum-tooling/tree-sitter-plum/grammar.js, plum-tooling/tree-sitter-plum/src/, plum-tooling/tree-sitter-plum/test/corpus/trait.txt, plum-core/src/ast.rs, plum-core/src/parser.rs, plum-core/tests/trait_parse_test.rs, plum-core/src/forms.rs, plum-core/src/loader.rs, plum-core/src/builtin_usage.rs, plum-lsp/src/completion.rs, plum-checker/src/unions.rs, plum-core/src/unions.rs
88
+ **Components:** 2
89
+ **LOC owned:** 260
90
+ **Tests:** fast
91
+ **Kind:** design
92
+ **Model:** sonnet
93
+ **Est. time:** 5 min
94
+ **Consumes:** none
95
+ **Produces:** ast::Item::Trait(ast::TraitDecl) · ast::TraitDecl { name: String, line: usize, column: usize, fields: Vec<ast::EnumParam>, methods: Vec<ast::Fn>, required: Vec<ast::Fn> } · ast::Enum.embeds: Vec<String> · ast::Enum.isTrait: bool
96
+ e.g. parse("trait E(z: Int) =\n fun up(self)\n fun g(self) -> Int = self.z") -> Item::Trait(TraitDecl{name:"E", fields:[z: Int], required:[up], methods:[g]})
97
+ e.g. parse("record H(E, k: Int)") -> Enum{name:"H", isRecord:true, embeds:["E"], params/fields:[k]}
98
+ **Review Focus:** `trait_bodiless_method_only_in_trait`: `fun f(self)` with no `=` at top level is still an extern-less error path as today (no new acceptance outside traits); `record_embed_after_field_rejected`: `record H(k: Int, E)` is a syntax error; `struct_embed_parses`: `struct S(E, x: Int)` parses with embeds ["E"] (T2 reports the error); corpus all green.
99
+ **Test code:** named tests: `parses_trait_fields_required_and_defaults` (above e.g.), `parses_record_with_two_embeds` (`record H(E, C, k: Int)` -> embeds ["E","C"]), `parses_record_embeds_only` (`record H(E)` -> embeds ["E"], no own fields), `trait_methods_have_self_and_type_param` (each trait Fn has has_self and type_param = Some("E")), `trait_without_fields` (`trait T =\n fun a(self)` parses, fields empty).
100
+ **Steps:** 1 failing tests, 2 grammar `trait` rule (optional `(fields)`, `=` indent body of `fn`), record/struct field list `commaSep1` with leading `type_identifier` embeds, 3 regenerate `--disable-optimizations`, add corpus, 4 parser + AST (`Enum` gains `embeds`, `isTrait` default false everywhere it is constructed), 5 PASS + `tree-sitter test`.
101
+ **Notes:** required methods are `Fn` with `body: FnBody::Extern` and `is_extern: false`. New `Item::Trait` arms: add to every exhaustive `match` on `Item` across the workspace (`cargo build --workspace` must pass); downstream crates may treat it as unreachable after lowering.
102
+
103
+ ### T3 formatter and highlighting · Complexity 3 · Est. context 30k
104
+ **Owns:** plum-tooling/tree-sitter-plum/queries/plum/format.scm, plum-tooling/tree-sitter-plum/queries/plum/highlights.scm, plum-tooling/tree-sitter-plum/queries/plum/tags.scm, plum-core/tests/formatter_test.rs
105
+ **Components:** 1
106
+ **LOC owned:** 80
107
+ **Tests:** fast
108
+ **Kind:** design
109
+ **Model:** sonnet
110
+ **Est. time:** 2.5 min
111
+ **Consumes:** ast::Item::Trait(ast::TraitDecl) (T1)
112
+ **Produces:** none
113
+ **Review Focus:** `format_trait_idempotent`: formatting a trait with fields, a required method, a default with block body and a comment, twice, gives identical output; `format_record_embeds`: `record H(E,C,k:Int)` -> `record H(E, C, k: Int)`.
114
+ **Test code:** named tests `format_trait_idempotent`, `format_record_embeds` as above; `trait` highlighted as `@keyword`, trait name as `@type.definition`, tags.scm emits a definition for traits.
115
+ **Steps:** 1 failing tests, 2 run FAIL, 3 queries, 4 PASS.
116
+
117
+ ## Package B: lowering and checking
118
+
119
+ **Probe entry:** call=plum_core::traits::lowerTraits
120
+
121
+ ### T2 trait lowering · Complexity 7 · Est. context 55k
122
+ **Owns:** plum-core/src/traits.rs, plum-core/src/lib.rs, plum-core/src/loader.rs, plum-core/tests/traits_test.rs, plum-core/src/ast.rs, plum-core/src/parser.rs, plum-wasm-codegen/src/lib.rs
123
+ **Components:** 2
124
+ **LOC owned:** 300
125
+ **Tests:** fast
126
+ **Kind:** design
127
+ **Model:** sonnet
128
+ **Est. time:** 5.5 min
129
+ **Consumes:** ast::TraitDecl (T1) · ast::Enum.embeds: Vec<String> (T1)
130
+ **Produces:** plum_core::traits::lowerTraits(source: &mut Source) -> Vec<String> · plum_core::traits::getter(field: &str) -> String · plum_core::traits::setter(field: &str) -> String · ast::Source.traits: Vec<ast::TraitDecl>
131
+ e.g. getter("pos") -> "__get_pos"; setter("pos") -> "__set_pos"
132
+ e.g. lowerTraits on `trait E(z: Int) = fun up(self)` + `record H(E, k: Int) = fun up(self) = ()` -> [] errors; items gain `enum E = | H` with isTrait true; H fields become [z, k]; H gains `__get_z(self) -> Int`, `__set_z(self, v: Int)`; no Item::Trait left; source.traits holds E.
133
+ e.g. record H(E) without `up` -> ["record H embeds E but does not define required method up"]
134
+ **Review Focus:** state: idempotent (second call is a no-op, like resolveMembers); errors at the record, never at a copy: `missing_required` (above), `field_clash` (`trait A(x: Int)`, `trait B(x: Int)`, `record R(A, B)` -> "record R gets field x from both A and B"; own field same name -> "record R field x is already declared by trait A"), `default_clash` (A and B both default `f`, R does not define `f` -> "record R gets default method f from both A and B; define R.f"), `embed_not_trait` (`record R(Vec2)` -> "R embeds Vec2, which is not a trait"), `struct_embed` (`struct S(E, x: Int)` -> "struct S cannot embed trait E; only records embed traits"); `self_recursive_field`: `trait A(children: List[A])` copies the field as `List[A]`; a record that defines a default method itself keeps its own; a trait with no embedders still lowers to an empty union enum.
135
+ **Test code:** named tests for every e.g. and Review Focus case, asserting exact strings; `loader_runs_lowerTraits` (a two-file program via loader with the trait in one module and the record in the other lowers).
136
+ **Steps:** 1 failing tests, 2 FAIL, 3 implement; call from loader immediately before `resolveMembers`, 4 PASS.
137
+ **Notes:** copied defaults are deep clones with `type_param = Some(record)`; insert the union enum where the trait item was; accessor Fns carry the record's file and the trait's line.
138
+
139
+ ### T4 checker trait rules · Complexity 6 · Est. context 60k
140
+ **Owns:** plum-checker/src/lib.rs, plum-checker/src/traits.rs, plum-checker/tests/traits_tests.rs, plum-checker/tests/trait_fields_tests.rs
141
+ **Components:** 2
142
+ **LOC owned:** 220
143
+ **Tests:** fast
144
+ **Kind:** design
145
+ **Model:** sonnet
146
+ **Est. time:** 5 min
147
+ **Consumes:** plum_core::traits::lowerTraits(source: &mut Source) -> Vec<String> (T2) · plum_checker::unions::traitField(obj: &PlumType, field: &str, exprAddr: usize, write: bool, ctx: &CheckCtx) -> Option<Result<PlumType, String>> (T5)
148
+ **Produces:** plum_checker::traits::isTrait(name: &str, ctx: &CheckCtx) -> bool · plum_checker::traits::traitMethods(name: &str, ctx: &CheckCtx) -> Vec<String>
149
+ e.g. traitMethods("Entity") -> ["update", "draw", "moveBy"] (required then defaults, declaration order)
150
+ **Review Focus:** `only_trait_methods`: `e.onClick()` on `e: Entity` where House defines onClick -> "trait Entity has no method onClick"; `match_trait_needs_wildcard`: `match e | House(k) -> 1` -> "match on trait Entity needs a `_` case"; with `_` it narrows and binds House's fields; plain union enums keep exhaustive matches and shared-method dispatch (regression); a record also listed in a union enum converts to the expected type only (no double wrap).
151
+ **Test code:** named tests for each Review Focus case plus `trait_param_accepts_record` (`fun f(e: Entity)` called with `House(...)` type-checks), `default_self_call_checks` (default `moveBy` calling `self.draw()` checks in each record). Tests call `lowerTraits` then `resolveMembers` after parse; parse helper asserts no syntax error.
152
+ **Steps:** 1 failing tests, 2 FAIL, 3 implement (CheckCtx learns trait names from `Enum.isTrait`; at field-access and field-assignment sites on a trait type, call `unions::traitField`), 4 PASS.
153
+ **Notes:** until T5 commits, `traitField` is the Wave 0 stub returning None; field-access tests live in T5.
154
+
155
+ ### T5 field access through a trait · Complexity 6 · Est. context 55k
156
+ **Owns:** plum-checker/src/unions.rs, plum-core/src/unions.rs, plum-core/src/union_lower.rs, plum-checker/tests/trait_fields_tests.rs
157
+ **Components:** 2
158
+ **LOC owned:** 200
159
+ **Tests:** fast
160
+ **Kind:** design
161
+ **Model:** sonnet
162
+ **Est. time:** 4.5 min
163
+ **Consumes:** plum_core::traits::getter(field: &str) -> String (T2) · plum_core::traits::setter(field: &str) -> String (T2)
164
+ **Produces:** plum_checker::unions::traitField(obj: &PlumType, field: &str, exprAddr: usize, write: bool, ctx: &CheckCtx) -> Option<Result<PlumType, String>> · plum_core::unions::recordFieldRead(exprAddr: usize, variants: Vec<String>) · plum_core::unions::recordFieldWrite(stmtAddr: usize, variants: Vec<String>)
165
+ e.g. traitField(TNamed("Entity"), "pos", a, false, ctx) -> Some(Ok(TNamed("Vec2"))) and records a read dispatch of `__get_pos`
166
+ e.g. traitField(TNamed("Entity"), "nope", a, false, ctx) -> Some(Err("trait Entity has no field nope"))
167
+ e.g. traitField(TNamed("Shape"), "radius", a, false, ctx) -> None (not a trait)
168
+ **Review Focus:** `nested_write_rejected`: `e.pos.x = 1.0` on a struct-typed trait field -> "assign the whole field: e.pos = ..."; `write_then_read`: after `e.pos = p` both `e.pos` and the House value see p; UnionPlan gains `fieldReads`/`fieldWrites`, `union_lower` rewrites `Attribute(e, f)` to `e.__get_f()` and `Assign([Field(e,f)], [v])` to `e.__set_f(v)` with the dispatch synthesized like method dispatch; compound assign (`e.z += 1`) lowers to `__set_z(__get_z() + 1)`.
169
+ **Test code:** named tests for each e.g. and Review Focus case (checker-level, through `typedSource` and the lowered copy).
170
+ **Steps:** 1 failing tests, 2 FAIL, 3 implement, 4 PASS.
171
+
172
+ ## Package C: integration
173
+
174
+ **Probe entry:** run=./target/debug/plum test plum-examples/traits.plum
175
+
176
+ ### T6 codegen and run tests · Complexity 5 · Est. context 60k
177
+ **Owns:** plum-wasm-codegen/src/lib.rs, plum-wasm-codegen/tests/traits_e2e_test.rs
178
+ **Components:** 1
179
+ **LOC owned:** 150
180
+ **Tests:** slow
181
+ **Kind:** design
182
+ **Model:** sonnet
183
+ **Est. time:** 4 min
184
+ **Consumes:** plum_checker::traits::isTrait(name: &str, ctx: &CheckCtx) -> bool (T4) · plum_checker::unions::traitField(obj: &PlumType, field: &str, exprAddr: usize, write: bool, ctx: &CheckCtx) -> Option<Result<PlumType, String>> (T5)
185
+ **Produces:** none
186
+ **Review Focus:** `opening_example_runs` (review plan's example: List[Entity] of House and Unit, update and draw dispatch, default moveBy changes pos seen via House and Entity); `default_calls_override`; `three_level_actor_tree` (`trait A(children: List[A])`, mixed records, depth-first sum); `trait_without_embedders_compiles`; `record_in_trait_and_union_enum`; `match_trait_narrows`.
187
+ **Test code:** each named test compiles a Plum source and asserts its printed output.
188
+ **Steps:** 1 failing tests, 2 FAIL, 3 fix codegen gaps found, 4 PASS, full suite in background.
189
+
190
+ ### T7 editor support · Complexity 4 · Est. context 45k
191
+ **Owns:** plum-lsp/src/main.rs, plum-lsp/src/completion.rs
192
+ **Components:** 1
193
+ **LOC owned:** 120
194
+ **Tests:** fast
195
+ **Kind:** design
196
+ **Model:** sonnet
197
+ **Est. time:** 3 min
198
+ **Consumes:** ast::Source.traits: Vec<ast::TraitDecl> (T2) · plum_core::traits::lowerTraits(source: &mut Source) -> Vec<String> (T2)
199
+ **Produces:** none
200
+ **Review Focus:** `hover_embedded_field` (hover `h.pos` -> `pos: Vec2 (from Entity)`), `complete_trait_value` (completion on `e.` with `e: Entity` lists trait fields and methods only, no `__get_`/`__set_` names), `goto_embedded_field` (jumps to the trait's field), `goto_trait_name`.
201
+ **Test code:** the four named tests in plum-lsp's existing test module style.
202
+ **Steps:** 1 failing tests, 2 FAIL, 3 implement, 4 PASS.
203
+
204
+ ### T8 docs and examples · Complexity 2 · Est. context 30k
205
+ **Owns:** README.md, plum-examples/traits.plum, plum-examples/trait_scene.plum
206
+ **Components:** 1
207
+ **LOC owned:** 140
208
+ **Tests:** slow
209
+ **Kind:** design
210
+ **Model:** sonnet
211
+ **Est. time:** 2.5 min
212
+ **Consumes:** none
213
+ **Produces:** none
214
+ **Review Focus:** `traits_example_tests` (`plum test plum-examples/traits.plum` green, covers each error-free feature); `trait_scene_example` (a Scene of Entity values updated and drawn, House and Unit); `format_idempotent` (format a copy twice, no diff).
215
+ **Test code:** `test` blocks inside the two example files.
216
+ **Steps:** write examples, run `plum test`, README "Traits" section after union enums.
217
+
218
+ ### T9 Scene on trait Actor · Complexity 7 · Est. context 70k
219
+ **Owns:** plum-std/game/Scene.plum, plum-examples/scene.plum, plum-game/web-test/fixtures/, plum-game/web-test/BENCHMARK.md, README.md
220
+ **Components:** 2
221
+ **LOC owned:** 400
222
+ **Tests:** slow
223
+ **Kind:** design
224
+ **Model:** sonnet
225
+ **Est. time:** 6 min
226
+ **Consumes:** none
227
+ **Produces:** none
228
+ **Review Focus:** `scene_tests_green` (Scene's z-index, sortChildren, culling, hit tests in Scene.plum stay green); `walker_record` (`record Walker(Actor, speed: Float)` overriding act/draw works in a Stage); browser check of scene.plum passes; BENCHMARK.md records 5k and 20k culled for closure and trait versions; >10% slower -> report DONE_WITH_CONCERNS.
229
+ **Test code:** existing Scene tests ported plus `walker_record`.
230
+ **Steps:** benchmark closure version first, migrate `Node` to `trait Actor(x, y, width, height, originX, originY, scaleX, scaleY, rotation, color, visible, children: List[Actor], ...)` with default act/draw/hit, port example and fixtures, re-benchmark, README Scene section.
231
+
232
+ ### T10 box-free trait layout (wasm-gc subtyping) · Complexity 8 · Est. context 35k
233
+ **Owns:** plum-wasm-codegen/src/lib.rs, plum-wasm-codegen/tests/traits_e2e_test.rs, plum-core/src/union_lower.rs, plum-checker/src/unions.rs, plum-game/web-test/BENCHMARK.md, plum-core/src/traits.rs, plum-core/src/ast.rs, plum-checker/tests/trait_fields_tests.rs, plum-core/src/parser.rs, plum-core/tests/traits_test.rs
234
+ **Components:** 2
235
+ **LOC owned:** 350
236
+ **Tests:** slow
237
+ **Kind:** design
238
+ **Model:** sonnet
239
+ **Est. time:** 6 min
240
+ **Consumes:** none
241
+ **Produces:** none
242
+ **Review Focus:** `subtyped_field_read_direct` (a field read on an Actor value compiles to one `struct.get` on the trait base type, no call); `conversion_is_identity` (pushing a record into List[Actor] allocates nothing; `a == b` on the same record converted twice is True); `second_trait_stays_boxed` (a record's second embedded trait keeps the box and still works); all traits_e2e, Scene and examples tests green; BENCHMARK.md stress_world within 10% of the closure version.
243
+ **Test code:** the three named tests plus re-running T6's six e2e tests.
244
+ **Steps:** a trait is subtyped iff every embedder lists it FIRST; emit a non-final base struct type with its fields in declaration order; each embedder's struct type declares it as supertype (lowering already puts trait fields first); trait value = ref to base; wrap = no-op upcast; trait field read/write = struct.get/set on base (skip accessor dispatch); hidden-variant match/narrowing = ref.test/ref.cast on the record type; method dispatch = cast chain (or a kind tag + br_table). Others keep the union box.
245
+ **Notes:** triggered by T9's benchmark (+92% on stress_world from accessor dispatch in the y-sort); approved by the review plan's "more than 10% slower" rule.
246
+
247
+ ## Review Focus
248
+ - Package A (parser): deep (nesting traits in bodies), encoding (grammar ambiguity: bare name vs `name: Type`).
249
+ - Package B (compiler pass): state (idempotent lowering, plan addresses after rewrite), deep (recursive trait field), protocol (union rules must not leak into traits and back).
250
+ - Package C (integration): state (writes through trait visible on record), numeric (benchmark threshold 10%).
251
+
252
+ ## Self-review
253
+ Coverage: every review-plan bullet maps to T1-T9. Edges are execution dependencies only; T4 and T5 share no file (T4 owns checker lib.rs, T5 checker unions.rs). README: T8 then T9 (edge).
docs/plans/2026-11-02-traits.progress.md ADDED
@@ -0,0 +1,13 @@
1
+ - start 6b983a7e16fccc5ac27e50a54b779a462e41e87e
2
+ Ruling: archived .superlite run state of the finished game-host run to .superlite/archive/game-host-run - its T1..T9 ledger collided with this plan - cost if wrong: none, files kept
3
+ Ruling: orchestrator switched to AI by the user (`! sl orchestrator set ai`) - the approval reply hook never logged "approve" - cost if wrong: none
4
+ Ruling: prime referenced by path instead of pasted - same bytes, saves coordinator context - cost if wrong: agent skips reading it
5
+ Ruling: T0 stubs carried by T1's commit - hook blocks coordinator git commit - cost if wrong: none
6
+ Ruling: T1/T2/T4/T10 Owns extended post-dispatch with cross-crate initializer/match-arm edits - commit_task stages only Owns - cost if wrong: none, edits were sequential
7
+ Ruling: fresh agents get prime path + task in one message - saves a READY round trip - cost if wrong: none
8
+ Ruling: review low-tier items (R(A,A) wording, required-vs-default across traits reported missing, non-record wording) left open - errors still fire - cost if wrong: confusing messages
9
+ Ruling: T10 subtyping added - T9 stress_world +92% exceeds the review plan's 10% rule - a trait is subtyped only if every embedder lists it first, others keep the box - cost if wrong: Clickable-style second traits stay slow
10
+ Ruling: boxed-trait == compares wrappers - == on trait values is out of scope in v1; subtyped traits compare equal - cost if wrong: surprising false for Clickable-style traits
11
+ - commits: T1 134a034, T3 5bb1d4c, T2 d83c36b f50415c, T7 818e0d0 670cb84, T5 ed7f16c 4306b97, T4 fe4959d add63b7, T6 808db73, T8 a1704af 7ae62f5, T9 3c9dcee, T10 538b1ee aa923a9, vscode trait ee042db
12
+ - tests full (final): cargo test --workspace --no-fail-fast 396 passed 0 failed; plum test plum-std plum-examples 446 passed (57 files); plum-game 20 passed
13
+ - benchmark (avg ms): stress_scene closure 1.60 / boxed 1.69 / box-free 1.68; stress_world closure 1.09 / boxed 2.09 / box-free 1.10