fix(codegen): a literal birth mints its shape with the plain prototype - #11839
Conversation
The driver gives every anonymous object-literal content (BirthProto::Literal) one static ShapeId, and the startup literal seed mints it with class id 0. Each module's own birth mint passed the __AnonShape_* class id instead, which the runtime turns into a prototype fact. That was equivalent only while the id had no vtable class, but per-module class ids collide: an anonymous shape's id can also be another module's DECLARED class. The mint then derived that class's prototype for a plain literal, so two modules' equal literal contents -- one static id -- reached the mint with different facts and the second aborted: "the static ShapeId ... was refused by the shape mint". OpenCode's TUI died this way at startup (ajv's and json5's one-key literals). Pass class id 0 to the static and lazy shape mints of a literal birth, as the seed does. The keys array still carries the real class id.
|
Navigate logical layers of code changes, visualize relationships, and explore their blast radius. Note Currently processing new changes in this PR. This may take a few minutes, please wait... ⚙️ Run configuration
📒 Files selected for processing (3)
✨ Finishing Touches📝 Generate docstrings
🧪 Generate unit tests (beta)
Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out. Comment |
|
An honest scope note after following this through to the end. This PR fixes a real bug: an anonymous literal's colliding class id let a plain object literal be minted with another module's class prototype. It is not, by itself, what stopped OpenCode's TUI. With it applied, the TUI still aborted on the same static ShapeId, and a diagnostic at the refusal showed every identity fact equal except the keys: both requests passed an empty keys array for |
Symptom
The natively compiled OpenCode v1.18.30 TUI aborts at startup:
A conditional breakpoint on
js_object_shape_id_for_class_keys_staticfor that id (the fifth argument) shows two requests on the main thread:ajv/dist/compile/resolve.jsliteral chunkjson5/lib/parse.jsliteral chunkBoth are anonymous one-key object literals. json5 is reached lazily:
tui.ts→plugin/tui/runtime.ts→ @opentui/solid's Babel plugin →babel-plugin-module-resolver→find-babel-config→json5.Cause
The driver names a birth's content without its class id when it is an anonymous object-literal class:
class_birthmaps__AnonShape_*toBirthProto::Literal. It gives equal contents one static id; the probing assignment guarantees distinct contents distinct ids. So the two requests above carry the same compile-time content: same keys,key_count,live,rep, and bothLiteral. The startup literal seed,js_shape_seed_plain, mints that content with class id 0, and its doc gives the reason: "an anonymous literal class has no vtable class, so class id 0 names the same plain prototype."Each module's own birth mint, though, passes the anonymous class's id. The runtime turns it into the prototype fact,
class_proto_id(class_id), which isPROTO_ID_CLASS | vtable_class(id)when the id has a vtable andPROTO_ID_DEFAULTotherwise. Per-module class ids collide across modules — the root cause #9133 mitigated forgetPrototypeOfbut left in place — so an__AnonShape_*id can also be another module's declared class.is_anon_shape_class_id(25667)is true at the first request, and the refused request reports proto0x0. Every other fact is equal by construction, so ajv's literal was minted with a class prototype: different facts under one static id, and the second request aborts.The abort is the loud half. The quiet half is that a plain object literal can be minted with another module's class prototype.
Fix
A literal birth names the plain prototype, so its shape mints pass class id 0, exactly as the seed does, on both routes: the static mint and the lazy mints (
js_object_shape_id_for_class_keys/_live).ClassBirthnow carries theliteralflag thatclass_birthalready decides, so the content definition and the emitter stay one derivation.js_build_class_keys_arraystill receives the real class id. Declared classes are unchanged: their prototype is a shape fact.This does not fix the per-module class-id collision itself. It stops literal shapes from depending on it, which is what the static-id design already assumes.
Test
a_literal_birth_mints_its_shape_with_the_plain_prototype_on_both_routes(static_shape_ids_tests.rs) compiles a module holding a{ a }literal and a declaredPoint, then reads the class-id argument of every shape-mint call:Point's names its own non-zero id, so the fix cannot over-reach;With the fix it passes. With
string_pool.rsandstatic_shape_ids.rsreverted it fails at the intended assertion:the literal's mint must name class id 0: [["%r1", "1", "1", "0"], ["%r1", "1", "2", "0"]].cargo fmt --all -- --checkis clean. A full OpenCode build with this plus #11817, #11819 and #11833 is running, and I will post whether the TUI renders.