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
14 changes: 14 additions & 0 deletions crates/perry-codegen/src/codegen/helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -391,6 +391,7 @@ pub(super) fn init_static_fields_early(
// built-in error subclass, register this class's id.
let mut cur: Option<String> = c.extends_name.clone();
let mut extends_error = false;
let mut extends_data_view = false;
let mut depth = 0usize;
while let Some(name) = cur {
if matches!(
Expand All @@ -407,6 +408,10 @@ pub(super) fn init_static_fields_early(
extends_error = true;
break;
}
if name == "DataView" {
extends_data_view = true;
break;
}
// Walk user-defined ancestor chain.
if let Some(parent) = ctx.classes.get(&name) {
cur = parent.extends_name.clone();
Expand All @@ -427,6 +432,15 @@ pub(super) fn init_static_fields_early(
);
}
}
if extends_data_view {
if let Some(&cid) = ctx.class_ids.get(&c.name) {
let cid_str = cid.to_string();
ctx.block().call_void(
"js_register_class_extends_data_view",
&[(crate::types::I32, &cid_str)],
);
}
}
}
// Well-known symbol class hooks: HIR lifts `static [Symbol.hasInstance]`
// and `get [Symbol.toStringTag]` to top-level functions with the
Expand Down
28 changes: 28 additions & 0 deletions crates/perry-codegen/src/expr/property_get.rs
Original file line number Diff line number Diff line change
Expand Up @@ -672,6 +672,34 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
&[(I64, &ctor_handle), (I64, &key_raw)],
));
}
// #4033: `ArrayBuffer.isView` must also work as a value
// (`const isView = ArrayBuffer.isView; isView(view)`). Bare
// builtin receivers are collapsed to `GlobalGet(0)`, so recover
// the populated constructor closure and read the reified static.
if property == "isView" {
let ctor_idx = ctx.strings.intern("ArrayBuffer");
let ctor_bytes_global =
format!("@{}", ctx.strings.entry(ctor_idx).bytes_global);
let ctor_len = "ArrayBuffer".len().to_string();
let ctor = ctx.block().call(
DOUBLE,
"js_get_global_this_builtin_value",
&[(PTR, &ctor_bytes_global), (I64, &ctor_len)],
);
let key_idx = ctx.strings.intern(property);
let key_handle_global =
format!("@{}", ctx.strings.entry(key_idx).handle_global);
let blk = ctx.block();
let ctor_handle = unbox_to_i64(blk, &ctor);
let key_box = blk.load(DOUBLE, &key_handle_global);
let key_bits = blk.bitcast_double_to_i64(&key_box);
let key_raw = blk.and(I64, &key_bits, POINTER_MASK_I64);
return Ok(blk.call(
DOUBLE,
"js_object_get_field_by_name_f64",
&[(I64, &ctor_handle), (I64, &key_raw)],
));
}
if property == "f16round" {
let math_idx = ctx.strings.intern("Math");
let math_bytes_global =
Expand Down
14 changes: 5 additions & 9 deletions crates/perry-codegen/src/lower_call/builtin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -119,23 +119,19 @@ pub(super) fn lower_builtin_new(
// Uint8Array — i.e. ArrayBuffers — are aliased rather than
// copied). SharedArrayBuffer uses the same storage allocation with a
// separate runtime registry so util.types can distinguish it.
// Non-numeric arg shapes (`new ArrayBuffer(undefined)` etc.) coerce to
// 0 — matches Node/bun's `ToIndex(length)` step for the typical
// undefined-arg case.
"ArrayBuffer" | "SharedArrayBuffer" => {
let size_box = if !args.is_empty() {
lower_expr(ctx, &args[0])?
} else {
double_literal(0.0)
};
let blk = ctx.block();
let size_i32 = blk.fptosi(DOUBLE, &size_box, I32);
let runtime = if class_name == "SharedArrayBuffer" {
"js_shared_array_buffer_new"
"js_shared_array_buffer_new_value"
} else {
"js_array_buffer_new"
"js_array_buffer_new_value"
};
let handle = blk.call(I64, runtime, &[(I32, &size_i32)]);
let handle = blk.call(I64, runtime, &[(DOUBLE, &size_box)]);
Ok(Some(nanbox_pointer_inline(blk, &handle)))
}
"Uint8Array" if args.len() >= 2 => {
Expand All @@ -157,8 +153,8 @@ pub(super) fn lower_builtin_new(
}
// Minimal DataView support for BufferSource consumers such as
// StringDecoder: Perry models ArrayBuffer/Uint8Array storage as a
// BufferHeader, so `new DataView(buffer)` can alias the same backing
// pointer for byte-extraction call sites.
// BufferHeader, so `new DataView(buffer)` can create a registered view
// over that backing store for byte-extraction call sites.
"DataView" => {
// Pass the raw NaN-boxed arguments (undefined when absent) so the
// runtime can apply the spec's ToIndex/range validation and throw
Expand Down
1 change: 1 addition & 0 deletions crates/perry-codegen/src/runtime_decls/strings.rs
Original file line number Diff line number Diff line change
Expand Up @@ -977,6 +977,7 @@ pub fn declare_phase_b_strings(module: &mut LlModule) {
// is a runtime expression (function arg holding class ref).
module.declare_function("js_instanceof_dynamic", DOUBLE, &[DOUBLE, DOUBLE]);
module.declare_function("js_register_class_extends_error", VOID, &[I32]);
module.declare_function("js_register_class_extends_data_view", VOID, &[I32]);
module.declare_function("js_register_class_id", VOID, &[I32]);
// #1021 NestJS: surface Perry class names to V8 so `metatype.name`
// is non-empty. Codegen emits one call per registered class id at
Expand Down
2 changes: 2 additions & 0 deletions crates/perry-codegen/src/runtime_decls/strings_part2.rs
Original file line number Diff line number Diff line change
Expand Up @@ -525,6 +525,8 @@ pub(crate) fn declare_phase_b_strings_part2(module: &mut LlModule) {
// bytes that subsequent `new Uint8Array(ab)` views ALIAS via shared pointer.
module.declare_function("js_array_buffer_new", I64, &[I32]);
module.declare_function("js_shared_array_buffer_new", I64, &[I32]);
module.declare_function("js_array_buffer_new_value", I64, &[DOUBLE]);
module.declare_function("js_shared_array_buffer_new_value", I64, &[DOUBLE]);
// JSON full-featured stringify/parse (replacer + indent + reviver).
module.declare_function("js_json_stringify_full", I64, &[DOUBLE, DOUBLE, DOUBLE]);
module.declare_function("js_json_parse_with_reviver", I64, &[I64, I64]);
Expand Down
47 changes: 34 additions & 13 deletions crates/perry-runtime/src/buffer/from.rs
Original file line number Diff line number Diff line change
Expand Up @@ -588,6 +588,25 @@ fn zeroed_array_buffer_storage(size: i32) -> *mut BufferHeader {
buf
}

fn throw_array_buffer_range_error() -> ! {
crate::fs::validate::throw_range_error_with_code("Invalid array buffer length")
}

fn array_buffer_to_index(value: f64) -> i32 {
let jv = crate::value::JSValue::from_bits(value.to_bits());
if jv.is_undefined() {
return 0;
}
let n = jv.to_number();
if n.is_nan() {
return 0;
}
if n < 0.0 || n > i32::MAX as f64 {
throw_array_buffer_range_error();
}
n.trunc() as i32
}

/// `new ArrayBuffer(size)` — allocate a zero-filled buffer of `size` bytes.
/// Issue #579: pre-fix, `ArrayBuffer` had no constructor handler so it fell
/// through to the empty-ObjectHeader placeholder path, and `new Uint8Array(ab)`
Expand All @@ -604,6 +623,11 @@ pub extern "C" fn js_array_buffer_new(size: i32) -> *mut BufferHeader {
buf
}

#[no_mangle]
pub extern "C" fn js_array_buffer_new_value(size_value: f64) -> *mut BufferHeader {
js_array_buffer_new(array_buffer_to_index(size_value))
}

/// `new SharedArrayBuffer(size)` — same BufferHeader backing store as
/// ArrayBuffer, tracked in a distinct side registry for util.types predicates.
#[no_mangle]
Expand All @@ -613,6 +637,11 @@ pub extern "C" fn js_shared_array_buffer_new(size: i32) -> *mut BufferHeader {
buf
}

#[no_mangle]
pub extern "C" fn js_shared_array_buffer_new_value(size_value: f64) -> *mut BufferHeader {
js_shared_array_buffer_new(array_buffer_to_index(size_value))
}

/// `Type(buffer) is not Object` → `TypeError` (DataView spec step 2). Also
/// raised when `buffer` is a non-buffer pointer (Symbol, plain object, …) —
/// Perry only models ArrayBuffer/SharedArrayBuffer storage as a registered
Expand Down Expand Up @@ -656,11 +685,10 @@ fn dataview_to_index(value: f64, what: &str) -> i64 {
}

/// `new DataView(buffer, byteOffset?, byteLength?)` — Perry models a DataView
/// as a `BufferHeader` over the backing `ArrayBuffer`. With `byteOffset == 0`
/// and no explicit length the view aliases the backing pointer directly
/// (zero-copy; writes are visible through sibling `Uint8Array` views). A
/// non-zero offset or explicit length produces a registered slice view so the
/// numeric accessors index relative to the view start (#2878).
/// as a `BufferHeader` view over the backing `ArrayBuffer`. Even a full-span
/// `new DataView(buffer)` gets its own registered view header so brand checks
/// can distinguish the DataView receiver from the backing ArrayBuffer while
/// preserving write propagation through the shared view registry.
///
/// `offset_value` / `length_value` are NaN-boxed JS values (the raw arguments,
/// `undefined` when absent) so the spec's `ToIndex`/range validation can run
Expand Down Expand Up @@ -702,14 +730,7 @@ pub extern "C" fn js_data_view_new(value: f64, offset_value: f64, length_value:
requested
};

// Fast path: full-buffer zero-offset view aliases the backing pointer
// directly (zero-copy; preserves the existing StringDecoder consumer).
if offset == 0 && view_len == total_len {
mark_as_data_view(addr);
return value;
}

// Otherwise build a registered slice view so the numeric accessors index
// Build a registered view so the numeric accessors index
// relative to the view start and `.byteOffset`/`.byteLength`/`.buffer`
// report the right values — including the zero-length edge cases
// (`offset == total_len`) that `js_buffer_slice` would otherwise collapse
Expand Down
13 changes: 7 additions & 6 deletions crates/perry-runtime/src/buffer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -41,12 +41,13 @@ pub use header::{

// ---- Re-exports: Buffer.from / alloc / concat (FFI) ----
pub use from::{
js_array_buffer_new, js_buffer_alloc, js_buffer_alloc_fill_value, js_buffer_alloc_unsafe,
js_buffer_concat, js_buffer_concat_with_length, js_buffer_fill, js_buffer_fill_range,
js_buffer_fill_value_range, js_buffer_from_array, js_buffer_from_arraybuffer_slice,
js_buffer_from_string, js_buffer_from_value, js_data_view_new, js_encoding_tag_from_value,
js_shared_array_buffer_new, js_uint8array_alloc, js_uint8array_from_array, js_uint8array_new,
js_uint8array_view,
js_array_buffer_new, js_array_buffer_new_value, js_buffer_alloc, js_buffer_alloc_fill_value,
js_buffer_alloc_unsafe, js_buffer_concat, js_buffer_concat_with_length, js_buffer_fill,
js_buffer_fill_range, js_buffer_fill_value_range, js_buffer_from_array,
js_buffer_from_arraybuffer_slice, js_buffer_from_string, js_buffer_from_value,
js_data_view_new, js_encoding_tag_from_value, js_shared_array_buffer_new,
js_shared_array_buffer_new_value, js_uint8array_alloc, js_uint8array_from_array,
js_uint8array_new, js_uint8array_view,
};

// ---- Re-exports: predicates / byteLength (FFI) ----
Expand Down
41 changes: 41 additions & 0 deletions crates/perry-runtime/src/object/data_view_registry.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
use super::*;

/// Global registry of class IDs that extend the built-in DataView class.
static EXTENDS_DATA_VIEW_REGISTRY: RwLock<Option<std::collections::HashSet<u32>>> =
RwLock::new(None);

/// Mark a user-defined class as extending the built-in DataView class.
#[no_mangle]
pub extern "C" fn js_register_class_extends_data_view(class_id: u32) {
let mut registry = EXTENDS_DATA_VIEW_REGISTRY.write().unwrap();
if registry.is_none() {
*registry = Some(std::collections::HashSet::new());
}
registry.as_mut().unwrap().insert(class_id);
}

/// Check if a class id extends the built-in DataView class.
pub(crate) fn extends_builtin_data_view(class_id: u32) -> bool {
let registry = EXTENDS_DATA_VIEW_REGISTRY.read().unwrap();
if let Some(reg) = registry.as_ref() {
if reg.contains(&class_id) {
return true;
}
let mut current = class_id;
let parent_reg = super::CLASS_REGISTRY.read().unwrap();
if let Some(pr) = parent_reg.as_ref() {
for _ in 0..32 {
match pr.get(&current).copied() {
Some(parent) if parent != 0 => {
if reg.contains(&parent) {
return true;
}
current = parent;
}
_ => break,
}
}
}
}
false
}
Loading