From 676dcb1cb6ed2e39df8da43f6615abe936bd2095 Mon Sep 17 00:00:00 2001 From: Ralph Date: Sat, 18 Jul 2026 00:47:18 -0700 Subject: [PATCH] fix: complete DisposableStack `.disposed` getter and `instanceof SuppressedError` (#6364) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit `test_gap_disposablestack_2875.ts` was an untriaged gap-suite failure with two distinct defects — both structurally invisible to CI because DisposableStack / SuppressedError are Node 24+ and the old node-22 pin classified the test `node_fail` and dropped it from the gate. 1. `stack.disposed` read `undefined`. A bare native-instance member read on a `__disposable__`-tagged instance had no arm in `is_native_dispatch_member`, so it fell through to a plain `PropertyGet` and never reached the existing `js_disposable_stack_disposed` FFI getter. Add the arm so `disposed` dispatches as a 0-arg `NativeMethodCall` (the runtime helper and the codegen `NativeModSig` row already existed); every other member stays a `PropertyGet` bound-method read. 2. `err instanceof SuppressedError` returned `false` while `instanceof Error` worked. Two compounding causes: - `CLASS_ID_SUPPRESSED_ERROR` was `0xFFFF_003B`, colliding with `CLASS_ID_FLOAT16_ARRAY`. Move it to the free `0xFFFF_003E`, out of the `0xFFFF0030..=0xFFFF003B` typed-array reserved range. - `SuppressedError` was absent from the codegen name->class-id tables, so the RHS resolved to id 0 and `js_instanceof(v, 0)` short-circuits to false. Register it in both the `lower_instanceof` match and `builtin_parent_reserved_class_id`. Output now matches `node --experimental-strip-types` byte-for-byte. Regression-checked: `Float16Array` instanceof and using/dispose stay identical. Co-Authored-By: Claude Opus 4.8 (1M context) --- crates/perry-codegen/src/expr/instance_misc1.rs | 10 ++++++++++ .../perry-hir/src/lower/expr_member/native_dispatch.rs | 9 +++++++++ crates/perry-runtime/src/disposable.rs | 9 ++++++++- 3 files changed, 27 insertions(+), 1 deletion(-) diff --git a/crates/perry-codegen/src/expr/instance_misc1.rs b/crates/perry-codegen/src/expr/instance_misc1.rs index 3f68d3aa34..e23691e367 100644 --- a/crates/perry-codegen/src/expr/instance_misc1.rs +++ b/crates/perry-codegen/src/expr/instance_misc1.rs @@ -63,6 +63,10 @@ pub(crate) fn builtin_parent_reserved_class_id(name: &str) -> Option { "AggregateError" => 0xFFFF0014, "EvalError" => 0xFFFF0015, "URIError" => 0xFFFF0016, + // #6364 — keep in sync with the `lower_instanceof` match above and + // `CLASS_ID_SUPPRESSED_ERROR` so `class X extends SuppressedError {}` + // walks the parent edge and `new X() instanceof SuppressedError` holds. + "SuppressedError" => 0xFFFF003E, "Date" => 0xFFFF0020, "RegExp" => 0xFFFF0021, "Map" => 0xFFFF0022, @@ -377,6 +381,12 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { "AggregateError" => 0xFFFF0014u32, "EvalError" | "globalThis.EvalError" => 0xFFFF0015u32, "URIError" | "globalThis.URIError" => 0xFFFF0016u32, + // #6364 — SuppressedError (TC39 explicit-resource-management). + // Must match `CLASS_ID_SUPPRESSED_ERROR` in + // perry-runtime/src/disposable.rs. The instance is a + // GC_TYPE_OBJECT carrying this class id, so the runtime + // class-id chain fast-path matches it (see instanceof.rs). + "SuppressedError" => 0xFFFF003Eu32, // Uint8Array / Buffer — runtime detects these via a // thread-local buffer registry (see buffer.rs). The // TextEncoder path registers its ArrayHeader result diff --git a/crates/perry-hir/src/lower/expr_member/native_dispatch.rs b/crates/perry-hir/src/lower/expr_member/native_dispatch.rs index 89852700c9..89243c544e 100644 --- a/crates/perry-hir/src/lower/expr_member/native_dispatch.rs +++ b/crates/perry-hir/src/lower/expr_member/native_dispatch.rs @@ -127,6 +127,15 @@ pub(crate) fn is_native_dispatch_member(module: &str, class: &str, prop: &str) - // user own-property surface in the bundle walls, so keep dispatching // for any member to preserve existing behaviour. "events" | "net" => true, + // #6364 — DisposableStack / AsyncDisposableStack: `disposed` is the + // only native data getter (its value comes from the FFI helper + // `js_disposable_stack_disposed`), so a bare read must dispatch as a + // 0-arg `NativeMethodCall` through the `__disposable__` NativeModSig + // row. Every other member (`use`/`adopt`/`defer`/`dispose`/ + // `disposeAsync`/`move`) is a method: a method CALL arrives via the + // call-expression path, and a bare method-VALUE read must stay a plain + // PropertyGet (a bound-method read), never a 0-arg invoking dispatch. + "__disposable__" => prop == "disposed", // Other native modules historically routed every uncovered member to // the dispatching fallback. They have no observed user-own-property // surface, so preserve that: dispatch any member not handled by the diff --git a/crates/perry-runtime/src/disposable.rs b/crates/perry-runtime/src/disposable.rs index a98a3ba4cb..289f355c0b 100644 --- a/crates/perry-runtime/src/disposable.rs +++ b/crates/perry-runtime/src/disposable.rs @@ -129,7 +129,14 @@ pub const CLASS_ID_DISPOSABLE_STACK: u32 = 0xFFFF_003C; pub const CLASS_ID_ASYNC_DISPOSABLE_STACK: u32 = 0xFFFF_003D; /// Reserved class id for a `SuppressedError` instance. Registered as an /// Error subclass at first construction so `instanceof Error` holds. -pub const CLASS_ID_SUPPRESSED_ERROR: u32 = 0xFFFF_003B; +/// +/// #6364 — must stay OUT of the typed-array reserved range +/// (`0xFFFF0030..=0xFFFF003B`, Int8Array..Float16Array). This previously read +/// `0xFFFF_003B`, colliding with `CLASS_ID_FLOAT16_ARRAY`, which made +/// `err instanceof Float16Array` (and the reverse) true once `SuppressedError` +/// was wired into the codegen `instanceof` table. `0xFFFF_003E` is the first +/// free id after the DisposableStack/AsyncDisposableStack block below. +pub const CLASS_ID_SUPPRESSED_ERROR: u32 = 0xFFFF_003E; const FIELD_DISPOSERS: u32 = 0; const FIELD_DISPOSED: u32 = 1;