Skip to content

fix(codegen): a literal birth mints its shape with the plain prototype - #11839

Merged
proggeramlug merged 2 commits into
mainfrom
fix/literal-birth-names-plain-prototype
Oct 3, 2026
Merged

proggeramlug merged 2 commits into
mainfrom
fix/literal-birth-names-plain-prototype

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Symptom

The natively compiled OpenCode v1.18.30 TUI aborts at startup:

Perry internal error: the static ShapeId 0x80015cb7 (proto 0x0) was refused by the shape mint
(it is outside the static band or already names other facts in this agent); a static id must name one shape

A conditional breakpoint on js_object_shape_id_for_class_keys_static for that id (the fifth argument) shows two requests on the main thread:

module key_count live class_id rep
first, accepted ajv/dist/compile/resolve.js literal chunk 1 1 25667 0
second, refused json5/lib/parse.js literal chunk 1 1 40819 0

Both 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_birth maps __AnonShape_* to BirthProto::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 both Literal. 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 is PROTO_ID_CLASS | vtable_class(id) when the id has a vtable and PROTO_ID_DEFAULT otherwise. Per-module class ids collide across modules — the root cause #9133 mitigated for getPrototypeOf but 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 proto 0x0. 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). ClassBirth now carries the literal flag that class_birth already decides, so the content definition and the emitter stay one derivation. js_build_class_keys_array still 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 declared Point, then reads the class-id argument of every shape-mint call:

  • lazy route: the literal's mint names class id 0, and Point's names its own non-zero id, so the fix cannot over-reach;
  • static route: with the driver's id assigned to the literal's content, the literal's static mint names class id 0. The test first asserts that the id actually reached that mint, so it cannot pass vacuously.

With the fix it passes. With string_pool.rs and static_shape_ids.rs reverted 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 -- --check is clean. A full OpenCode build with this plus #11817, #11819 and #11833 is running, and I will post whether the TUI renders.

Ralph Küpper added 2 commits October 3, 2026 22:33
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.
@coderabbitai

coderabbitai Bot commented Oct 3, 2026

Copy link
Copy Markdown

Review in Change Stack →

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
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 71d0f312-97eb-48df-9a64-48ee83829fc0
📥 Commits

Reviewing files that changed from the base of the PR and between 2026ecf and 1b1bcaa.

📒 Files selected for processing (3)
  • crates/perry-codegen/src/codegen/static_shape_ids.rs
  • crates/perry-codegen/src/codegen/static_shape_ids_tests.rs
  • crates/perry-codegen/src/codegen/string_pool.rs
 ____________________
< I dream in semver. >
 --------------------
  \
   \   (\__/)
       (•ㅅ•)
       /   づ
✨ Finishing Touches
📝 Generate docstrings
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autopilot · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@proggeramlug
proggeramlug merged commit 5b06d69 into main Oct 3, 2026
32 of 35 checks passed
@proggeramlug
proggeramlug deleted the fix/literal-birth-names-plain-prototype branch October 3, 2026 20:36
@proggeramlug

Copy link
Copy Markdown
Contributor Author

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 { "": v }. That cause is #11858. With both applied, the startup abort is gone.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant