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
5 changes: 5 additions & 0 deletions benchmarks/compiler_output/workloads.toml
Original file line number Diff line number Diff line change
Expand Up @@ -487,6 +487,7 @@ kind = "native_rep_benchmark"
allow_hot_loop_conversions = true
allowed_hot_loop_runtime_calls = [
"js_buffer_get",
"js_buffer_index_get_value",
"js_buffer_set",
"js_shadow_slot_set",
]
Expand Down Expand Up @@ -520,12 +521,14 @@ allow_dynamic_property_runtime = true
allow_hot_loop_conversions = true
allowed_hot_loop_runtime_calls = [
"js_buffer_get",
"js_buffer_index_get_value",
"js_buffer_set",
"js_closure_call1",
"js_dyn_index_get",
"js_dynamic_string_or_number_add",
"js_number_coerce",
"js_uint8array_get",
"js_uint8array_index_get_value",
"js_uint8array_set",
"js_value_length_f64",
]
Expand Down Expand Up @@ -1956,6 +1959,7 @@ allowed_hot_loop_runtime_calls = [
"js_array_length",
"js_array_push_f64",
"js_buffer_get",
"js_buffer_index_get_value",
"js_buffer_set",
"js_handle_object_get_property",
"js_boxed_number_new",
Expand All @@ -1972,6 +1976,7 @@ allowed_hot_loop_runtime_calls = [
"js_shadow_slot_bind",
"js_shadow_slot_set",
"js_uint8array_get",
"js_uint8array_index_get_value",
"js_uint8array_set",
"js_write_barrier_root_nanbox",
]
Expand Down
39 changes: 33 additions & 6 deletions crates/perry-codegen/src/expr/arrays_finds.rs
Original file line number Diff line number Diff line change
Expand Up @@ -771,14 +771,41 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
&[(I64, &handle), (DOUBLE, &key)],
));
}
let value = lower_uint8array_get_i32(ctx, array, index)?;
let reason = buffer_access_materialization_reason(ctx, array);
Ok(materialize_js_value(ctx, value, reason))
// #6088: a proven non-negative integer key whose value is NOT
// proven in bounds (the inline load above bailed). The native i32
// accessor returns the `0` byte-sentinel for an out-of-range read;
// a JS-value `u8[i]` must instead read `undefined` (ECMAScript
// IntegerIndexedExotic `[[Get]]`). In-range reads still return the
// byte as a number.
let a = lower_expr(ctx, array)?;
let idx_i32 = lower_index_i32(ctx, index)?;
let blk = ctx.block();
let handle = unbox_to_i64(blk, &a);
Ok(blk.call(
DOUBLE,
"js_uint8array_index_get_value",
&[(I64, &handle), (I32, &idx_i32)],
))
}
Expr::BufferIndexGet { buffer, index } => {
let value = lower_buffer_index_get_i32(ctx, buffer, index)?;
let reason = buffer_access_materialization_reason(ctx, buffer);
Ok(materialize_js_value(ctx, value, reason))
// Proven-bounds inline load keeps the native fast path.
if let Some(value) =
lower_buffer_load(ctx, buffer, index, BufferAccessSpec::buffer_index_get())?
{
let reason = buffer_access_materialization_reason(ctx, buffer);
return Ok(materialize_js_value(ctx, value, reason));
}
// #6088: out-of-range → `undefined`, not the `0` byte-sentinel the
// native `js_buffer_get` accessor is forced to return.
let a = lower_expr(ctx, buffer)?;
let idx_i32 = lower_index_i32(ctx, index)?;
let blk = ctx.block();
let handle = unbox_to_i64(blk, &a);
Ok(blk.call(
DOUBLE,
"js_buffer_index_get_value",
&[(I64, &handle), (I32, &idx_i32)],
))
}
Expr::Uint8ArraySet {
array,
Expand Down
22 changes: 18 additions & 4 deletions crates/perry-codegen/src/expr/index_get.rs
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,6 @@ use crate::type_analysis::{
#[allow(unused_imports)]
use crate::types::{DOUBLE, F32, I1, I16, I32, I64, I8, PTR};

use super::arrays_finds::lower_buffer_index_get_i32;
#[allow(unused_imports)]
use super::{
array_kind_fact, buffer_access_materialization_reason, buffer_alias_metadata_suffix,
Expand Down Expand Up @@ -1401,9 +1400,24 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result<String> {
&[(I64, &arr_i64), (DOUBLE, &key_box)],
));
}
let value = lower_buffer_index_get_i32(ctx, object, index)?;
let reason = buffer_access_materialization_reason(ctx, object);
return Ok(materialize_js_value(ctx, value, reason));
// #6088: the index is a proven non-negative i32 key, but the
// inline load above bailed, so its value is NOT proven in
// bounds. The native `js_uint8array_get` accessor returns the
// `0` byte-sentinel for an out-of-range read; a JS-value
// `buf[i]` / `uint8array[i]` must instead read `undefined`
// (ECMAScript IntegerIndexedExotic `[[Get]]`). Route the
// unproven-bounds slow path through the JS-value getter (robust
// for both a real Uint8Array and a Buffer-backed receiver) —
// in-range reads still return the byte as a number.
let arr_box = lower_expr(ctx, object)?;
let idx_i32 = lower_expr_as_i32(ctx, index)?;
let blk = ctx.block();
let handle = unbox_to_i64(blk, &arr_box);
return Ok(blk.call(
DOUBLE,
"js_uint8array_index_get_value",
&[(I64, &handle), (I32, &idx_i32)],
));
}
// Scalar-replaced array literal: `arr[k]` where arr was bound to
// `[...]` and never escaped, and k is a compile-time index in
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 @@ -111,6 +111,7 @@ pub(crate) fn declare_phase_b_strings_part2(module: &mut LlModule) {
&[I64, DOUBLE, DOUBLE],
);
module.declare_function("js_uint8array_get", I32, &[I64, I32]);
module.declare_function("js_uint8array_index_get_value", DOUBLE, &[I64, I32]);
module.declare_function("js_uint8array_set", VOID, &[I64, I32, I32]);
module.declare_function("js_native_arena_alloc", I64, &[I64]);
module.declare_function("js_native_arena_view", I64, &[I64, I32, I64, I64]);
Expand Down Expand Up @@ -800,6 +801,7 @@ pub(crate) fn declare_phase_b_strings_part2(module: &mut LlModule) {
module.declare_function("js_buffer_from_arraybuffer_slice", I64, &[I64, I32, I32]);
module.declare_function("js_buffer_length", I32, &[I64]);
module.declare_function("js_buffer_get", I32, &[I64, I32]);
module.declare_function("js_buffer_index_get_value", DOUBLE, &[I64, I32]);
module.declare_function("js_native_buffer_data_ptr", PTR, &[DOUBLE]);
module.declare_function("js_native_buffer_byte_len", I64, &[DOUBLE]);
// console.time/count runtime functions.
Expand Down
79 changes: 56 additions & 23 deletions crates/perry-runtime/src/buffer/access.rs
Original file line number Diff line number Diff line change
Expand Up @@ -171,34 +171,67 @@ unsafe fn collect_buffer_set_bytes(source: BufferSetSource, source_len: usize) -
bytes
}

/// Get a byte at the specified index
#[no_mangle]
pub extern "C" fn js_buffer_get(buf_ptr: *const BufferHeader, index: i32) -> i32 {
if buf_ptr.is_null() || index < 0 {
return 0;
/// Read the byte at `index`, resolving a registered view to its ultimate
/// backing buffer. Returns `None` for a null receiver or an out-of-range
/// index (`index < 0` or `index >= length`). Shared by the native i32
/// accessor (`js_buffer_get`) and the JS-value accessor
/// (`js_buffer_index_get_value`) so their read semantics never drift.
#[inline]
unsafe fn read_buffer_byte(buf_ptr: *const BufferHeader, index: i32) -> Option<u8> {
if buf_ptr.is_null() || index < 0 || index as u32 >= (*buf_ptr).length {
return None;
}
unsafe {
if index as u32 >= (*buf_ptr).length {
return 0;
// Issue #1205: if the receiver is a registered view, read from
// the ultimate backing buffer — otherwise the view's local
// snapshot can lag any direct-fast-path write made to the
// backing through codegen.
let buf_addr = buf_ptr as usize;
if let Some(info) = super::view::lookup(buf_addr) {
let back_off = info.offset + index as u32;
let backing_ptr = info.backing as *const BufferHeader;
if !backing_ptr.is_null() && back_off < (*backing_ptr).length {
let back_data = buffer_data(backing_ptr);
return Some(*back_data.add(back_off as usize));
}
// Issue #1205: if the receiver is a registered view, read from
// the ultimate backing buffer — otherwise the view's local
// snapshot can lag any direct-fast-path write made to the
// backing through codegen.
let buf_addr = buf_ptr as usize;
if let Some(info) = super::view::lookup(buf_addr) {
let back_off = info.offset + index as u32;
let backing_ptr = info.backing as *const BufferHeader;
if !backing_ptr.is_null() && back_off < (*backing_ptr).length {
let back_data = buffer_data(backing_ptr);
return *back_data.add(back_off as usize) as i32;
}
}
let data = buffer_data(buf_ptr);
*data.add(index as usize) as i32
}
let data = buffer_data(buf_ptr);
Some(*data.add(index as usize))
}

/// Get a byte at the specified index. Native i32 accessor: an out-of-range
/// index yields the `0` sentinel because every caller here has proven the
/// index in bounds or consumes the byte in a native integer context. A
/// JS-value `buf[i]` read must instead yield `undefined` for out-of-range —
/// use `js_buffer_index_get_value` for that (#6088).
#[no_mangle]
pub extern "C" fn js_buffer_get(buf_ptr: *const BufferHeader, index: i32) -> i32 {
unsafe { read_buffer_byte(buf_ptr, index).map_or(0, |byte| byte as i32) }
}

/// `buf[i]` / `uint8array[i]` read as a JS value. Perry's `Uint8Array` is
/// Buffer-backed, so both share this accessor. An out-of-range canonical
/// integer index reads `undefined` — the ECMAScript IntegerIndexedExotic
/// `[[Get]]` semantics — NOT the `0` byte-sentinel of the native
/// `js_buffer_get` (#6088). Negative and fractional keys never reach here
/// (codegen routes them to the dynamic-key helper); an in-range read returns
/// the byte as a plain (non-NaN) f64, which is its own NaN-boxed JS number.
#[no_mangle]
pub extern "C" fn js_buffer_index_get_value(buf_ptr: *const BufferHeader, index: i32) -> f64 {
match unsafe { read_buffer_byte(buf_ptr, index) } {
Some(byte) => byte as f64,
None => f64::from_bits(crate::value::TAG_UNDEFINED),
}
}

// #6088: force-keep the JS-value buffer index getter under LTO /
// auto-optimize. It has zero internal Rust callers — codegen emits the only
// call (in `perry-codegen/src/expr/index_get.rs`), so a whole-program bitcode
// link is otherwise free to internalize and dead-strip it. The `#[used]`
// anchor pins it (mirrors `KEEP_JS_TYPED_ARRAY_INDEX_GET_DYNAMIC`).
#[used]
static KEEP_JS_BUFFER_INDEX_GET_VALUE: extern "C" fn(*const BufferHeader, i32) -> f64 =
js_buffer_index_get_value;

/// Set a byte at the specified index
#[no_mangle]
pub extern "C" fn js_buffer_set(buf_ptr: *mut BufferHeader, index: i32, value: i32) {
Expand Down
36 changes: 35 additions & 1 deletion crates/perry-runtime/src/buffer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@ pub use u8_codec::{

// ---- Re-exports: indexed access / slice / Uint8Array.set ----
pub use access::{
js_buffer_get, js_buffer_set, js_buffer_set_from, js_buffer_set_from_value, js_buffer_slice,
js_buffer_get, js_buffer_index_get_value, js_buffer_set, js_buffer_set_from,
js_buffer_set_from_value, js_buffer_slice,
};

// ---- Re-exports: DataView numeric accessors (#2878) ----
Expand Down Expand Up @@ -325,6 +326,39 @@ mod tests {
assert_eq!(js_buffer_get(buf, 2), 0x42);
}

/// #6088: the JS-value accessor reads `undefined` for an out-of-range
/// canonical index (IntegerIndexedExotic `[[Get]]`), unlike the native
/// `js_buffer_get` which returns the `0` byte-sentinel. In-range reads
/// still return the byte as a plain (non-NaN) f64 number.
#[test]
fn test_buffer_index_get_value_oob_is_undefined() {
let buf = js_buffer_alloc(3, 0);
js_buffer_set(buf, 0, 5);
js_buffer_set(buf, 1, 6);
js_buffer_set(buf, 2, 7);
let undef = f64::from_bits(crate::value::TAG_UNDEFINED);

// In-range: the byte value as a number (not undefined).
assert_eq!(js_buffer_index_get_value(buf, 0), 5.0);
assert_eq!(js_buffer_index_get_value(buf, 2), 7.0);

// Out-of-range and negative: undefined, NOT the 0 sentinel.
assert_eq!(js_buffer_index_get_value(buf, 3).to_bits(), undef.to_bits());
assert_eq!(js_buffer_index_get_value(buf, 9).to_bits(), undef.to_bits());
assert_eq!(
js_buffer_index_get_value(buf, -1).to_bits(),
undef.to_bits()
);
// The native accessor keeps its 0-for-OOB contract for its callers.
assert_eq!(js_buffer_get(buf, 9), 0);

// Null receiver: undefined.
assert_eq!(
js_buffer_index_get_value(std::ptr::null(), 0).to_bits(),
undef.to_bits()
);
}

#[test]
fn test_hex_encode_decode() {
let original = b"Hello";
Expand Down
37 changes: 37 additions & 0 deletions crates/perry-runtime/src/typedarray/access.rs
Original file line number Diff line number Diff line change
Expand Up @@ -454,6 +454,43 @@ pub extern "C" fn js_uint8array_get(target: *const TypedArrayHeader, index: i32)
}
}

/// `uint8array[i]` / `buffer[i]` read as a JS value. Mirrors the dispatch of
/// the native i32 accessor `js_uint8array_get` (a real Uint8Array
/// `TypedArrayHeader`, or a Buffer-backed registered buffer), but an
/// out-of-range canonical integer index reads `undefined` — the ECMAScript
/// IntegerIndexedExotic `[[Get]]` semantics — NOT the `0` byte-sentinel the
/// i32 accessor is forced to return (#6088). `js_typed_array_get` and
/// `js_buffer_index_get_value` both already yield `undefined` for out-of-range,
/// so each arm forwards directly.
#[no_mangle]
pub extern "C" fn js_uint8array_index_get_value(
target: *const TypedArrayHeader,
index: i32,
) -> f64 {
let undefined = f64::from_bits(crate::value::TAG_UNDEFINED);
let addr = strip_nanbox(target as u64);
if addr < 0x1000 || index < 0 {
return undefined;
}
if let Some(kind) = lookup_typed_array_kind(addr) {
if !matches!(kind, KIND_UINT8 | KIND_UINT8_CLAMPED) {
return undefined;
}
js_typed_array_get(addr as *const TypedArrayHeader, index)
} else if crate::buffer::is_registered_buffer(addr) {
crate::buffer::js_buffer_index_get_value(addr as *const crate::buffer::BufferHeader, index)
} else {
undefined
}
}

// #6088: force-keep the JS-value Uint8Array index getter under LTO /
// auto-optimize — it has zero internal Rust callers (codegen emits the only
// call), so a whole-program bitcode link is otherwise free to dead-strip it.
#[used]
static KEEP_JS_UINT8ARRAY_INDEX_GET_VALUE: extern "C" fn(*const TypedArrayHeader, i32) -> f64 =
js_uint8array_index_get_value;

#[no_mangle]
pub extern "C" fn js_uint8array_set(target: *mut TypedArrayHeader, index: i32, value: i32) {
let addr = strip_nanbox(target as u64);
Expand Down
Loading
Loading