Skip to content

fix(runtime): arm the buffer dispatch bucket when the global Buffer is minted (#6924) - #7422

Merged
proggeramlug merged 2 commits into
mainfrom
fix/6924-native-subclass-static-read
Aug 5, 2026
Merged

fix(runtime): arm the buffer dispatch bucket when the global Buffer is minted (#6924)#7422
proggeramlug merged 2 commits into
mainfrom
fix/6924-native-subclass-static-read

Conversation

@proggeramlug

@proggeramlug proggeramlug commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Fixes #6924.

Symptom

class MyBuf extends Buffer {}
console.log(typeof (MyBuf as any).from);       // function
console.log((MyBuf as any).from("ab").length); // perry: TypeError (from() returned undefined) — node: 2

Not just the subclass: every Buffer static reached as a value misdispatched in a program without a buffer import —

const B: any = Buffer;
B.from("cd");            // undefined
const f = B.from; f("ab"); // undefined

Root cause

Buffer's statics are BOUND_METHOD closures that dispatch by name through the "buffer.Buffer" namespace, and that dispatch resolves via the per-module devirtualization registry (nm_dispatch_lookup, #5256). The registry's soundness rule — a bound export exists only after its module's js_nm_install_* ran — is upheld by codegen for imported modules (install calls are emitted at import/namespace sites), but Buffer is a global: buffer_constructor_value() mints all the bound statics when the global is materialized, with no import anywhere. Nothing armed the bucket, nm_dispatch_lookup("buffer.Buffer") returned None, and the dispatch fall-through silently returned undefined (trace-verified: module="buffer.Buffer" method="from" lookup_hit=false).

The read side of #6924 (the typeof = undefined originally reported) resolves on current main via the class-registry parent-closure walk — what remains broken is that the resolved bound value (and the fused static-call path nm_static_buffer_proto_chain, which dispatches through the same registry) evaporates on invocation.

Fix

buffer_constructor_value() calls js_nm_install_buffer() at the mint — the single choke point every Buffer bound static flows through (global value read, extends Buffer, globalThis.Buffer, the buffer namespace's .Buffer property). This mirrors the install_native_module_vtable() call the same mint path already makes for the same reason. Link-size cost is one bucket fn whose crate::buffer::* callees are already pinned by the direct Buffer.* lowerings.

Tests

  • Unit (per-PR gate): buffer_constructor_mint_arms_the_buffer_dispatch_bucket. It must defeat the cfg(test) lazy js_nm_install_all() fallback in the lookup fns — the mint's nm_attach_lookup otherwise self-heals the registry in test builds and the assertion is vacuously green. Added an RAII-guarded test-only toggle (NM_TEST_DISABLE_LAZY_INSTALL) for that. Sabotage-verified: with the install call removed, the test goes red; with it, green.
  • Gap suite (tag-gated): test-files/test_gap_6924_extends_buffer_statics.ts — byte-for-byte vs Node 26.5.1, verified locally on both the auto-optimized and PERRY_NO_AUTO_OPTIMIZE=1 runtime arms (Windows).

Validation notes

Out of scope (pre-existing, found while probing)

  • const h: any = Buffer.from; h("ef") — the statically-typed Buffer.from member read lowers through the GlobalGet sentinel path and yields a non-callable (TypeError: value is not a function). Different lowering, unaffected by (and predating) this fix.
  • js_globalthis_seed_async_local_storage mints an async_hooks bound export the same importless way; if its nm_attach decoration matters on a no-import path it has the same hole shape.

No version bump per the contributor-PR convention (maintainer bumps at merge).

Summary by CodeRabbit

  • Bug Fixes

    • Improved global Buffer behavior so inherited, direct, and captured static methods dispatch correctly without requiring an explicit buffer import.
    • Fixed compatibility for Buffer subclass statics, including from, alloc, isBuffer, and concat.
  • Tests

    • Added regression coverage for global Buffer initialization and inherited static method behavior.

Ralph Kuepper added 2 commits August 5, 2026 09:09
…s minted (#6924)

The statics Buffer carries (from/alloc/isBuffer/concat/...) are BOUND_METHOD
closures that dispatch by name through the "buffer.Buffer" namespace, and
that dispatch resolves via the per-module registry (nm_dispatch_lookup).
The registry's soundness rule - a bound export exists only after its module's
js_nm_install_* ran - is upheld by codegen for imported modules, but Buffer
is a GLOBAL: buffer_constructor_value() mints the bound statics with no
buffer import anywhere, so nothing armed the bucket and every inherited or
value-captured static silently returned undefined:

  class MyBuf extends Buffer {}
  (MyBuf as any).from("ab")      // undefined (node: <Buffer 61 62>)
  const B: any = Buffer; B.from   // typeof "function", but calls -> undefined

Fix: buffer_constructor_value() calls js_nm_install_buffer() at the mint,
mirroring the install_native_module_vtable() call the mint path already
makes for the same reason.

The new unit test suppresses the cfg(test) lazy js_nm_install_all() fallback
(new RAII-guarded toggle) while asserting - without that, the lookup
self-heals in test builds and the assertion is vacuously green
(sabotage-verified: removing the install call turns the test red).

Gap-suite twin: test_gap_6924_extends_buffer_statics.ts, byte-for-byte
against node 26.5.1 on both the auto-optimized and full runtime arms.
@coderabbitai

coderabbitai Bot commented Aug 5, 2026

Copy link
Copy Markdown

Review Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: defaults

Review profile: CHILL

Plan: Pro Plus

Run ID: 2cb3a1b9-b078-4230-8014-bf5d296b4942

📥 Commits

Reviewing files that changed from the base of the PR and between fc9683d and bb7c703.

📒 Files selected for processing (4)
  • changelog.d/7422-buffer-global-dispatch-arming.md
  • crates/perry-runtime/src/object/native_module/callable_exports.rs
  • crates/perry-runtime/src/object/native_module_registry.rs
  • test-files/test_gap_6924_extends_buffer_statics.ts

📝 Walkthrough

Walkthrough

Global Buffer construction now initializes the native module registry before creating bound static exports. Tests disable lazy installation to verify registry arming and validate inherited and captured Buffer statics.

Changes

Buffer static exports

Layer / File(s) Summary
Explicit Buffer module installation
crates/perry-runtime/src/object/native_module/callable_exports.rs
buffer_constructor_value installs the Buffer module before creating the global constructor and bound static methods.
Lookup control and regression coverage
crates/perry-runtime/src/object/native_module_registry.rs, test-files/test_gap_6924_extends_buffer_statics.ts, changelog.d/7422-buffer-global-dispatch-arming.md
Native-module lookups can disable lazy installation during tests. Regression tests verify Buffer registry arming, inherited static methods, and captured static values. The changelog records the fix and test coverage.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Suggested labels: bug

Suggested reviewers: thehypnoo

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title clearly identifies the primary runtime fix: arming the Buffer dispatch bucket when the global Buffer is created.
Description check ✅ Passed The description provides detailed context, implementation changes, linked issue, tests, validation notes, and scope boundaries, despite omitting the template checklist.
Linked Issues check ✅ Passed The changes address [#6924] by enabling inherited and value-captured Buffer statics to dispatch correctly without a buffer import.
Out of Scope Changes check ✅ Passed The changelog, runtime fix, regression tests, and test-only registry controls directly support the linked issue and stated objectives.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
✨ Finishing Touches
📝 Generate docstrings
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/6924-native-subclass-static-read

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 4143c2a into main Aug 5, 2026
8 of 11 checks passed
@proggeramlug
proggeramlug deleted the fix/6924-native-subclass-static-read branch August 5, 2026 07:36
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.

class X extends Buffer: inherited statics missing (MyBuf.from undefined) — native-base subclass family

1 participant