Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
22 changes: 22 additions & 0 deletions crates/perry-codegen/src/lower_call/console_promise.rs
Original file line number Diff line number Diff line change
Expand Up @@ -797,6 +797,28 @@ pub fn try_lower_native_method_str_dispatch(
return Ok(Some(materialized));
}
}
// #6386 fast path: `dv.getFloat64(off, le)` / `dv.setInt32(off, v)`
// lowers to one `js_data_view_{get,set}_direct` call instead of
// the generic dispatch tower. Fires for a statically-typed
// DataView receiver AND for an unknown-typed receiver (a mutable
// `var v = new DataView(b)` is widened to Any by the local-type
// fixpoint) whose method name matches the accessor family — the
// runtime entry re-validates the receiver against the DataView
// registry and re-enters `js_native_call_method` otherwise, so a
// non-DataView receiver that happens to share the method name
// keeps its generic dispatch semantics (same #5525 guarded-
// fast-path shape as typed-array index access).
if matches!(class_name_opt.as_deref(), Some("DataView") | None) {
if let Some(reg) = super::dataview_intrinsic::try_emit_data_view_accessor(
ctx,
object,
property,
args,
call_byte_offset,
)? {
return Ok(Some(reg));
}
}
Comment on lines +811 to +821

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🎯 Functional Correctness | 🟠 Major | ⚡ Quick win

Statically-typed DataView receivers bypass the fast path.

The fast path is placed inside the if !skip_native block. However, if class_name_opt is Some("DataView"), the skip_native condition evaluates to true (since DataView is typically known to codegen and lacks an explicit carve-out like is_buffer_class). Consequently, the fast path is skipped for statically-typed DataView receivers and only fires for untyped (Any / None) receivers.

To fix this, DataView must be explicitly excluded from skip_native upstream so it is allowed to enter the block.

// Apply this logic outside the selected line range (around line 775):

        let is_dataview_class = matches!(
            class_name_opt.as_deref(),
            Some("DataView")
        );
        
        let skip_native = matches!(object.as_ref(), Expr::GlobalGet(_))
            || matches!(object.as_ref(), Expr::NativeModuleRef(_))
            || (class_name_opt.is_some()
                && !is_buffer_class
                && !is_dataview_class // <-- ADDED CARVE-OUT
                && !class_unknown_to_codegen
                && !is_well_known_proto_method
                && !is_collection_subclass_method
                && !is_array_subclass_method);
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@crates/perry-codegen/src/lower_call/console_promise.rs` around lines 811 -
821, Update the skip_native calculation in the surrounding call-lowering logic
to define an is_dataview_class check for Some("DataView") and exclude it from
the typed-class skip condition, alongside the existing is_buffer_class
carve-out. Preserve all other skip_native conditions so statically typed
DataView receivers enter the block containing try_emit_data_view_accessor.

let recv_box = lower_expr(ctx, object)?;
let mut lowered_args: Vec<String> = Vec::with_capacity(args.len());
for a in args {
Expand Down
114 changes: 114 additions & 0 deletions crates/perry-codegen/src/lower_call/dataview_intrinsic.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,114 @@
//! #6386: direct lowering for DataView accessor method calls.
//!
//! `dv.getFloat64(off, le)` / `dv.setInt32(off, v)` on a receiver whose
//! STATIC type is `DataView` previously lowered to the fully generic
//! `js_typed_feedback_native_call_method_by_id` tower — per call: a method-id
//! resolution, a typed-feedback observation, an args `Vec` + handle-scope
//! setup, then the buffer-registry dispatch ladder (`is_registered_buffer` →
//! own-prop shadow probe → `is_data_view` → suffix re-parse). This lowers the
//! same calls to one `js_data_view_{get,set}_direct` call carrying the
//! pre-resolved element-kind code.
//!
//! The runtime entry re-checks the receiver (a variable whose static type
//! was violated at runtime, a shadowed method, exotica) and falls back to the
//! generic dispatcher, so this is a pure fast path — semantics unchanged.

use anyhow::Result;
use perry_hir::Expr;

use crate::expr::{lower_expr, FnCtx};
use crate::types::{DOUBLE, I32};

/// Classify a DataView accessor method name: `Some((is_set, kind_code))` for
/// the `get*`/`set*` numeric family. `kind_code` is the ABI contract with
/// `DataViewKind` in `perry-runtime/src/buffer/dataview.rs` (`repr(i32)`
/// discriminants) — keep the two in sync.
fn classify_data_view_accessor(method: &str) -> Option<(bool, i32)> {
let (is_set, suffix) = if let Some(s) = method.strip_prefix("get") {
(false, s)
} else if let Some(s) = method.strip_prefix("set") {
(true, s)
} else {
return None;
};
let kind_code = match suffix {
"Int8" => 0,
"Uint8" => 1,
"Int16" => 2,
"Uint16" => 3,
"Int32" => 4,
"Uint32" => 5,
"Float32" => 6,
"Float64" => 7,
"BigInt64" => 8,
"BigUint64" => 9,
_ => return None,
};
Some((is_set, kind_code))
}

/// Try to lower `object.<property>(args)` as a direct DataView accessor call.
/// Returns `Ok(None)` when the method/arity doesn't match the direct form —
/// the generic dispatch path then handles it (missing REQUIRED arguments stay
/// on the generic path so its argument-defaulting behavior is preserved
/// exactly; extra arguments beyond the accessor's arity likewise).
pub(super) fn try_emit_data_view_accessor(
ctx: &mut FnCtx<'_>,
object: &Expr,
property: &str,
args: &[Expr],
call_byte_offset: u32,
) -> Result<Option<String>> {
let Some((is_set, kind_code)) = classify_data_view_accessor(property) else {
return Ok(None);
};
let (min_args, max_args) = if is_set { (2, 3) } else { (1, 2) };
if args.len() < min_args || args.len() > max_args {
return Ok(None);
}
let recv = lower_expr(ctx, object)?;
let mut lowered: Vec<String> = Vec::with_capacity(args.len());
for a in args {
lowered.push(lower_expr(ctx, a)?);
}
// Absent littleEndian lowers to undefined — the runtime evaluates its
// truthiness exactly like the generic path's `truthy(args[2])`.
let undef = ctx
.block()
.bitcast_i64_to_double(crate::nanbox::TAG_UNDEFINED_I64);
// The accessors can throw (RangeError on an out-of-bounds offset, the
// offset's `valueOf`) — record the call location for the error message.
crate::expr::calls::emit_call_location_at(ctx, call_byte_offset);
let argc = args.len().to_string();
let kind = kind_code.to_string();
let blk = ctx.block();
let result = if is_set {
let little = lowered.get(2).unwrap_or(&undef);
blk.call(
DOUBLE,
"js_data_view_set_direct",
&[
(DOUBLE, &recv),
(DOUBLE, &lowered[0]),
(DOUBLE, &lowered[1]),
(DOUBLE, little),
(I32, &kind),
(I32, &argc),
],
)
} else {
let little = lowered.get(1).unwrap_or(&undef);
blk.call(
DOUBLE,
"js_data_view_get_direct",
&[
(DOUBLE, &recv),
(DOUBLE, &lowered[0]),
(DOUBLE, little),
(I32, &kind),
(I32, &argc),
],
)
};
Ok(Some(result))
}
1 change: 1 addition & 0 deletions crates/perry-codegen/src/lower_call/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ mod builtin_table_gate;
mod capture_writeback;
mod closure_analysis;
mod console_promise;
mod dataview_intrinsic;
mod early_branches;
mod event_target;
mod extern_func;
Expand Down
13 changes: 13 additions & 0 deletions crates/perry-codegen/src/runtime_decls/strings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -765,6 +765,19 @@ pub fn declare_phase_b_strings(module: &mut LlModule) {
module.declare_function("js_util_types_is_map_iterator", DOUBLE, &[DOUBLE]);
module.declare_function("js_util_types_is_set_iterator", DOUBLE, &[DOUBLE]);
module.declare_function("js_data_view_new", DOUBLE, &[DOUBLE, DOUBLE, DOUBLE]);
// #6386: direct DataView accessor entries for statically-typed receivers
// (kind codes = `DataViewKind` repr(i32) discriminants; trailing i32 is
// the source-level argc, forwarded for the generic-dispatch fallback).
module.declare_function(
"js_data_view_get_direct",
DOUBLE,
&[DOUBLE, DOUBLE, DOUBLE, I32, I32],
);
module.declare_function(
"js_data_view_set_direct",
DOUBLE,
&[DOUBLE, DOUBLE, DOUBLE, DOUBLE, I32, I32],
);
module.declare_function("js_getenv", I64, &[I64]);
module.declare_function("js_getenv_value", DOUBLE, &[I64]);
// #1344: process.env.X = v / delete process.env.X.
Expand Down
Loading
Loading