Skip to content

fix(runtime): the empty-string key survives a packed key list - #11858

Merged
proggeramlug merged 2 commits into
mainfrom
fix/empty-string-literal-key
Oct 3, 2026
Merged

proggeramlug merged 2 commits into
mainfrom
fix/empty-string-literal-key

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Oct 3, 2026 •

Copy link
Copy Markdown
Contributor

Symptom

Any object literal whose key is the empty string aborts:

const holder = { "": 42 }
// perry:  Perry internal error: refusing to publish invalid object shape facts   (abort)
// node:   holder[""] === 42, Object.keys(holder) is [""]

Across modules the same bug surfaces as a different abort. This is what killed the natively compiled OpenCode v1.18.30 TUI at startup:

Perry internal error: the static ShapeId 0x80015cb7 (proto 0x0) was refused by the shape mint

json5's lib/parse.js builds the standard JSON reviver holder { '': root }, and ajv's compile/resolve.js has an equal one-key literal.

Cause

Codegen packs a literal's key names as name\0 per name (codegen/mod.rs, expr/object_literal.rs, lower_call/new_alloc.rs), so the key "" is the single byte \0. The encoding is lossless. Every reader split the packed names on NUL and then dropped empty segments (.filter(|s| !s.is_empty())), which deletes the empty key:

  • in one module, js_build_class_keys_array built a keys array shorter than its count (0 keys for key_count = 1), and the mint refused the facts;
  • in two modules, the equal literals are one compile-time content, so the driver gives them one static ShapeId. Each module then built its own fresh empty array, the records' keys differed under that one id, and the second module's static mint was refused.

A temporary diagnostic at the refusal printed both records' facts. Every identity field was equal except keys, and both requests passed an empty keys array with key_count = 1.

Fix

One helper, object::packed_key_names, keeps empty names and drops only the empty segment after the final terminator. It replaces all seven readers: four in object/alloc.rs, the two literal seeds in object/static_shapes.rs, and the JSON shape hint in json/parse_api.rs. It is identical to the old behaviour for every non-empty name, and it also accepts an unterminated last name. Codegen is unchanged.

Tests

  • object::packed_key_names_tests covers "a\0b\0", "\0" (one empty key), leading and trailing empty names, an unterminated last name, and empty input.
  • crates/perry/tests/empty_string_literal_key.rs compiles a two-module program end to end and matches node 26.5.1's output byte for byte. It covers holder[""], Object.keys, in, a mixed { "": 1, a: 2 }, a JSON reviver, a second module's own { "": v } (the static-id path), and a literal built inside a function.

Both arms were run as real commits with the runtime libraries rebuilt from each:

  • with the fix: both tests pass;
  • with the four runtime files reverted: the end-to-end test fails with the production symptom, Perry internal error: refusing to publish invalid object shape facts.

On the full OpenCode build, a stamp-pinned relink with this fix (on top of #11817, #11819, #11833 and #11839) removes the startup abort. The TUI now enters its alternate screen like bun, and then stops on a separate module-loading error, which I am investigating.

cargo fmt --all -- --check is clean.

Ralph Küpper added 2 commits October 4, 2026 01:27
Codegen packs key names as name\0 per name, so the key "" is the single
byte \0. Every runtime reader split the packed names on NUL and dropped empty
segments, so an object literal { "": v } lost its key: the keys array came out
shorter than its count and the shape mint refused the facts, aborting at the
literal ("refusing to publish invalid object shape facts"). Across two
modules the equal contents share one static ShapeId while each module built
its own empty array, so the second module's static mint was refused -- the
natively compiled OpenCode v1.18.30 TUI died this way at startup, on json5's
JSON reviver holder { '': root }.

Read packed names through one helper that keeps empty names and drops only
the segment after the final terminator, at all seven readers.
@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: 02fd1912-6f8f-410d-adb0-da8cd778c9b3
📥 Commits

Reviewing files that changed from the base of the PR and between 586c194 and 759878c.

📒 Files selected for processing (5)
  • crates/perry-runtime/src/json/parse_api.rs
  • crates/perry-runtime/src/object/alloc.rs
  • crates/perry-runtime/src/object/mod.rs
  • crates/perry-runtime/src/object/static_shapes.rs
  • crates/perry/tests/empty_string_literal_key.rs
 _________________________________________________________________________________________
< The three laws of code review: 1. Find bugs. 2. Find more bugs. 3. Find even more bugs. >
 -----------------------------------------------------------------------------------------
  \
   \   \
        \ /\
        ( )
      .( o ).
✨ 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 e9575d8 into main Oct 3, 2026
22 of 23 checks passed
@proggeramlug
proggeramlug deleted the fix/empty-string-literal-key branch October 3, 2026 23:56
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