diff --git a/crates/perry-codegen/src/collectors/pointer_locals.rs b/crates/perry-codegen/src/collectors/pointer_locals.rs index 5b5abf5c5..f991b596e 100644 --- a/crates/perry-codegen/src/collectors/pointer_locals.rs +++ b/crates/perry-codegen/src/collectors/pointer_locals.rs @@ -192,6 +192,7 @@ pub fn collect_pointer_typed_locals( | Expr::BufferFrom { .. } | Expr::BufferFromArrayBuffer { .. } | Expr::BufferConcat(_) + | Expr::BufferConcatWithLength { .. } | Expr::Uint8ArrayNew(_) | Expr::Uint8ArrayFrom(_) | Expr::TextEncoderEncode(_) => Some(Type::Named("Uint8Array".into())), diff --git a/crates/perry-codegen/src/expr/array_methods.rs b/crates/perry-codegen/src/expr/array_methods.rs index bad0f883e..86afcc358 100644 --- a/crates/perry-codegen/src/expr/array_methods.rs +++ b/crates/perry-codegen/src/expr/array_methods.rs @@ -143,6 +143,18 @@ pub(crate) fn lower(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { let buf_handle = blk.call(I64, "js_buffer_concat", &[(I64, &arr_handle)]); Ok(nanbox_pointer_inline(blk, &buf_handle)) } + Expr::BufferConcatWithLength { list, total_length } => { + let arr_box = lower_expr(ctx, list)?; + let total_box = lower_expr(ctx, total_length)?; + let blk = ctx.block(); + let arr_handle = unbox_to_i64(blk, &arr_box); + let buf_handle = blk.call( + I64, + "js_buffer_concat_with_length", + &[(I64, &arr_handle), (DOUBLE, &total_box)], + ); + Ok(nanbox_pointer_inline(blk, &buf_handle)) + } // #1177: `buf.slice(start?, end?)` on a statically buffer-producing // receiver — emitted by the HIR fold at `expr_call/mod.rs:5396` when diff --git a/crates/perry-codegen/src/expr/calls.rs b/crates/perry-codegen/src/expr/calls.rs index 5194191e7..2ddea5799 100644 --- a/crates/perry-codegen/src/expr/calls.rs +++ b/crates/perry-codegen/src/expr/calls.rs @@ -60,6 +60,7 @@ fn hash_input_is_buffer(ctx: &FnCtx<'_>, e: &Expr) -> bool { | Expr::BufferAlloc { .. } | Expr::BufferAllocUnsafe(_) | Expr::BufferConcat(_) + | Expr::BufferConcatWithLength { .. } | Expr::CryptoRandomBytes(_) ) { return true; diff --git a/crates/perry-codegen/src/expr/mod.rs b/crates/perry-codegen/src/expr/mod.rs index 6f994018e..f4f83c5d1 100644 --- a/crates/perry-codegen/src/expr/mod.rs +++ b/crates/perry-codegen/src/expr/mod.rs @@ -1745,6 +1745,7 @@ pub(crate) fn lower_expr(ctx: &mut FnCtx<'_>, expr: &Expr) -> Result { | Expr::AggregateErrorNew { .. } | Expr::RegExpLastIndex(..) | Expr::BufferConcat(..) + | Expr::BufferConcatWithLength { .. } | Expr::BufferSlice { .. } | Expr::BufferIsBuffer(..) | Expr::BufferIsEncoding(..) diff --git a/crates/perry-codegen/src/runtime_decls/stdlib_ffi.rs b/crates/perry-codegen/src/runtime_decls/stdlib_ffi.rs index 55aee1bc6..fdf5d2d67 100644 --- a/crates/perry-codegen/src/runtime_decls/stdlib_ffi.rs +++ b/crates/perry-codegen/src/runtime_decls/stdlib_ffi.rs @@ -406,6 +406,7 @@ pub fn declare_stdlib_ffi(module: &mut LlModule) { module.declare_function("js_buffer_byte_length", I32, &[I64]); module.declare_function("js_buffer_byte_length_value", I32, &[DOUBLE, DOUBLE]); module.declare_function("js_buffer_concat", I64, &[I64]); + module.declare_function("js_buffer_concat_with_length", I64, &[I64, DOUBLE]); module.declare_function("js_buffer_copy", I32, &[I64, I64, I32, I32, I32]); module.declare_function("js_buffer_equals", I32, &[I64, I64]); module.declare_function("js_buffer_fill", I64, &[I64, I32]); diff --git a/crates/perry-codegen/src/type_analysis.rs b/crates/perry-codegen/src/type_analysis.rs index 49cd56704..57e39c863 100644 --- a/crates/perry-codegen/src/type_analysis.rs +++ b/crates/perry-codegen/src/type_analysis.rs @@ -230,6 +230,7 @@ pub(crate) fn refine_type_from_init(ctx: &FnCtx<'_>, init: &Expr) -> Option Some(HirType::Named("Uint8Array".into())), // Compare results are now NaN-boxed booleans (TAG_TRUE/FALSE). // Type-refining the local as Boolean lets is_numeric_expr diff --git a/crates/perry-hir/src/analysis.rs b/crates/perry-hir/src/analysis.rs index 1762805cf..5a6cc2470 100644 --- a/crates/perry-hir/src/analysis.rs +++ b/crates/perry-hir/src/analysis.rs @@ -748,6 +748,10 @@ pub(crate) fn collect_assigned_locals_expr(expr: &Expr, assigned: &mut Vec { collect_assigned_locals_expr(expr, assigned); } + Expr::BufferConcatWithLength { list, total_length } => { + collect_assigned_locals_expr(list, assigned); + collect_assigned_locals_expr(total_length, assigned); + } Expr::BufferByteLength { data, encoding } => { collect_assigned_locals_expr(data, assigned); if let Some(enc) = encoding { diff --git a/crates/perry-hir/src/ir/expr.rs b/crates/perry-hir/src/ir/expr.rs index ecb0db462..541f80ad9 100644 --- a/crates/perry-hir/src/ir/expr.rs +++ b/crates/perry-hir/src/ir/expr.rs @@ -982,8 +982,13 @@ pub enum Expr { }, BufferAllocUnsafe(Box), // Buffer.allocUnsafe(size) -> Buffer BufferConcat(Box), // Buffer.concat(list) -> Buffer - BufferIsBuffer(Box), // Buffer.isBuffer(obj) -> boolean - BufferIsEncoding(Box), // Buffer.isEncoding(encoding) -> boolean + BufferConcatWithLength { + // Buffer.concat(list, totalLength) -> Buffer + list: Box, + total_length: Box, + }, + BufferIsBuffer(Box), // Buffer.isBuffer(obj) -> boolean + BufferIsEncoding(Box), // Buffer.isEncoding(encoding) -> boolean BufferByteLength { data: Box, encoding: Option>, diff --git a/crates/perry-hir/src/lower/expr_call/array_only_methods.rs b/crates/perry-hir/src/lower/expr_call/array_only_methods.rs index ecae2a548..f49e847bd 100644 --- a/crates/perry-hir/src/lower/expr_call/array_only_methods.rs +++ b/crates/perry-hir/src/lower/expr_call/array_only_methods.rs @@ -463,6 +463,7 @@ pub(super) fn try_array_only_methods( if matches!( &array_expr, Expr::BufferConcat(_) + | Expr::BufferConcatWithLength { .. } | Expr::BufferFrom { .. } | Expr::BufferSlice { .. } ) { diff --git a/crates/perry-hir/src/lower/expr_call/module_static.rs b/crates/perry-hir/src/lower/expr_call/module_static.rs index addd0847c..db9f9c248 100644 --- a/crates/perry-hir/src/lower/expr_call/module_static.rs +++ b/crates/perry-hir/src/lower/expr_call/module_static.rs @@ -1038,9 +1038,15 @@ pub(super) fn try_module_static_methods( } "concat" => { if !args.is_empty() { - return Ok(Ok(Expr::BufferConcat(Box::new( - args.into_iter().next().unwrap(), - )))); + let mut args_iter = args.into_iter(); + let list = args_iter.next().unwrap(); + if let Some(total_length) = args_iter.next() { + return Ok(Ok(Expr::BufferConcatWithLength { + list: Box::new(list), + total_length: Box::new(total_length), + })); + } + return Ok(Ok(Expr::BufferConcat(Box::new(list)))); } } "of" => { diff --git a/crates/perry-hir/src/lower/expr_call/native_module.rs b/crates/perry-hir/src/lower/expr_call/native_module.rs index 35cf1102e..2c7ad355e 100644 --- a/crates/perry-hir/src/lower/expr_call/native_module.rs +++ b/crates/perry-hir/src/lower/expr_call/native_module.rs @@ -462,6 +462,12 @@ pub(super) fn try_native_module_methods( } "concat" => { let list = args.first().cloned().unwrap_or(Expr::Array(vec![])); + if let Some(total_length) = args.get(1).cloned() { + return Ok(Ok(Expr::BufferConcatWithLength { + list: Box::new(list), + total_length: Box::new(total_length), + })); + } return Ok(Ok(Expr::BufferConcat(Box::new(list)))); } "of" => { diff --git a/crates/perry-hir/src/lower/expr_call/url_date_instance.rs b/crates/perry-hir/src/lower/expr_call/url_date_instance.rs index 323087deb..f0eb75a33 100644 --- a/crates/perry-hir/src/lower/expr_call/url_date_instance.rs +++ b/crates/perry-hir/src/lower/expr_call/url_date_instance.rs @@ -133,6 +133,7 @@ pub(super) fn try_url_date_weakref_instance( | Expr::BufferAlloc { .. } | Expr::BufferAllocUnsafe(_) | Expr::BufferConcat(_) + | Expr::BufferConcatWithLength { .. } ) { return Ok(Ok(Expr::Call { callee: Box::new(Expr::PropertyGet { diff --git a/crates/perry-hir/src/stable_hash/expr.rs b/crates/perry-hir/src/stable_hash/expr.rs index 475f2287e..5a523df2b 100644 --- a/crates/perry-hir/src/stable_hash/expr.rs +++ b/crates/perry-hir/src/stable_hash/expr.rs @@ -274,6 +274,7 @@ impl SH for Expr { Expr::BufferAlloc { size, fill, encoding } => { tag(h, 216); size.as_ref().hash(h); fill.hash(h); encoding.hash(h); } Expr::BufferAllocUnsafe(e) => { tag(h, 217); e.as_ref().hash(h); } Expr::BufferConcat(e) => { tag(h, 218); e.as_ref().hash(h); } + Expr::BufferConcatWithLength { list, total_length } => { tag(h, 11222); list.as_ref().hash(h); total_length.as_ref().hash(h); } Expr::BufferIsBuffer(e) => { tag(h, 219); e.as_ref().hash(h); } Expr::BufferIsEncoding(e) => { tag(h, 11219); e.as_ref().hash(h); } Expr::BufferByteLength { data, encoding } => { tag(h, 220); data.as_ref().hash(h); encoding.hash(h); } diff --git a/crates/perry-hir/src/walker/expr_mut.rs b/crates/perry-hir/src/walker/expr_mut.rs index 71232f3d3..176fc98dd 100644 --- a/crates/perry-hir/src/walker/expr_mut.rs +++ b/crates/perry-hir/src/walker/expr_mut.rs @@ -307,6 +307,10 @@ where | Expr::TemplateRaw(v) => { f(v); } + Expr::BufferConcatWithLength { list, total_length } => { + f(list); + f(total_length); + } Expr::UrlCanParseWithBase { input, base } => { f(input); diff --git a/crates/perry-hir/src/walker/expr_ref.rs b/crates/perry-hir/src/walker/expr_ref.rs index 1ed231a0e..6945da8ca 100644 --- a/crates/perry-hir/src/walker/expr_ref.rs +++ b/crates/perry-hir/src/walker/expr_ref.rs @@ -308,6 +308,10 @@ where | Expr::TemplateRaw(v) => { f(v); } + Expr::BufferConcatWithLength { list, total_length } => { + f(list); + f(total_length); + } Expr::UrlCanParseWithBase { input, base } => { f(input); diff --git a/crates/perry-runtime/src/buffer/from.rs b/crates/perry-runtime/src/buffer/from.rs index 0f4132dde..2b3b77d7b 100644 --- a/crates/perry-runtime/src/buffer/from.rs +++ b/crates/perry-runtime/src/buffer/from.rs @@ -853,9 +853,53 @@ pub extern "C" fn js_buffer_alloc_unsafe(size: i32) -> *mut BufferHeader { buf } -/// Concatenate multiple buffers -#[no_mangle] -pub extern "C" fn js_buffer_concat(arr_ptr: *const ArrayHeader) -> *mut BufferHeader { +fn throw_buffer_concat_invalid_arg_type(index: usize) -> ! { + static REGISTER_TYPE_ERROR: std::sync::Once = std::sync::Once::new(); + REGISTER_TYPE_ERROR.call_once(|| { + crate::object::js_register_class_extends_error(crate::error::CLASS_ID_TYPE_ERROR); + }); + + let obj = crate::object::js_object_alloc(crate::error::CLASS_ID_TYPE_ERROR, 4); + unsafe { + let set = |key: &[u8], value: f64| { + let key_ptr = crate::string::js_string_from_bytes(key.as_ptr(), key.len() as u32); + crate::object::js_object_set_field_by_name(obj, key_ptr, value); + }; + let str_val = |s: &[u8]| -> f64 { + let ptr = crate::string::js_string_from_bytes(s.as_ptr(), s.len() as u32); + f64::from_bits(crate::JSValue::string_ptr(ptr).bits()) + }; + let message = + format!("The \"list[{index}]\" argument must be an instance of Buffer or Uint8Array"); + set(b"name", str_val(b"TypeError")); + set(b"code", str_val(b"ERR_INVALID_ARG_TYPE")); + set(b"message", str_val(message.as_bytes())); + } + crate::exception::js_throw(crate::value::js_nanbox_pointer(obj as i64)) +} + +fn normalize_buffer_concat_total_length(total_length: f64) -> Option { + let jsval = crate::JSValue::from_bits(total_length.to_bits()); + if jsval.is_undefined() { + return None; + } + if jsval.is_int32() { + return Some((jsval.as_int32().max(0)) as usize); + } + if jsval.is_bool() { + return Some(if jsval.as_bool() { 1 } else { 0 }); + } + if jsval.is_null() || total_length.is_nan() || !total_length.is_finite() || total_length <= 0.0 + { + return Some(0); + } + Some((total_length.trunc() as usize).min(u32::MAX as usize)) +} + +fn js_buffer_concat_impl( + arr_ptr: *const ArrayHeader, + requested_total_length: Option, +) -> *mut BufferHeader { // Strip NaN-boxing tags if present let arr_ptr = { let bits = arr_ptr as u64; @@ -884,34 +928,55 @@ pub extern "C" fn js_buffer_concat(arr_ptr: *const ArrayHeader) -> *mut BufferHe } }; - // Calculate total size - let mut total_size: usize = 0; + let mut actual_total_size: usize = 0; for i in 0..len { let raw_bits = strip_nanbox((*arr_data.add(i)).to_bits()); - let buf_ptr = raw_bits as *const BufferHeader; - if !buf_ptr.is_null() && raw_bits >= 0x1000 { - total_size += (*buf_ptr).length as usize; + if raw_bits < 0x1000 || !is_registered_buffer(raw_bits as usize) { + throw_buffer_concat_invalid_arg_type(i); } + let buf_ptr = raw_bits as *const BufferHeader; + actual_total_size = actual_total_size.saturating_add((*buf_ptr).length as usize); } + let total_size = requested_total_length.unwrap_or(actual_total_size); + let total_size = total_size.min(u32::MAX as usize); // Allocate result buffer let result = buffer_alloc(total_size as u32); (*result).length = total_size as u32; + ptr::write_bytes(buffer_data_mut(result), 0, total_size); // Copy data let mut offset: usize = 0; for i in 0..len { let raw_bits = strip_nanbox((*arr_data.add(i)).to_bits()); let buf_ptr = raw_bits as *const BufferHeader; - if !buf_ptr.is_null() && raw_bits >= 0x1000 { - let buf_len = (*buf_ptr).length as usize; - let src_data = buffer_data(buf_ptr); - let dst_data = buffer_data_mut(result).add(offset); - ptr::copy_nonoverlapping(src_data, dst_data, buf_len); - offset += buf_len; + let buf_len = (*buf_ptr).length as usize; + let remaining = total_size.saturating_sub(offset); + if remaining == 0 { + break; } + let copy_len = buf_len.min(remaining); + let src_data = buffer_data(buf_ptr); + let dst_data = buffer_data_mut(result).add(offset); + ptr::copy_nonoverlapping(src_data, dst_data, copy_len); + offset += copy_len; } result } } + +/// Concatenate multiple buffers. +#[no_mangle] +pub extern "C" fn js_buffer_concat(arr_ptr: *const ArrayHeader) -> *mut BufferHeader { + js_buffer_concat_impl(arr_ptr, None) +} + +/// Concatenate multiple buffers using Node's optional totalLength semantics. +#[no_mangle] +pub extern "C" fn js_buffer_concat_with_length( + arr_ptr: *const ArrayHeader, + total_length: f64, +) -> *mut BufferHeader { + js_buffer_concat_impl(arr_ptr, normalize_buffer_concat_total_length(total_length)) +} diff --git a/crates/perry-runtime/src/buffer/mod.rs b/crates/perry-runtime/src/buffer/mod.rs index ba1c09b17..6b9712933 100644 --- a/crates/perry-runtime/src/buffer/mod.rs +++ b/crates/perry-runtime/src/buffer/mod.rs @@ -38,8 +38,9 @@ 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_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_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, }; diff --git a/crates/perry-runtime/src/object/native_module_dispatch.rs b/crates/perry-runtime/src/object/native_module_dispatch.rs index dd11ad195..3c51bd5e9 100644 --- a/crates/perry-runtime/src/object/native_module_dispatch.rs +++ b/crates/perry-runtime/src/object/native_module_dispatch.rs @@ -283,7 +283,12 @@ pub(crate) unsafe fn dispatch_native_module_method( } ("buffer.Buffer", "concat") => { let arr = ptr_addr(arg(0)) as *const crate::array::ArrayHeader; - ptr_to_f64(crate::buffer::js_buffer_concat(arr) as *const u8) + let buf = if args_len >= 2 { + crate::buffer::js_buffer_concat_with_length(arr, arg(1)) + } else { + crate::buffer::js_buffer_concat(arr) + }; + ptr_to_f64(buf as *const u8) } ("buffer.Buffer", "of") => { let arr = pack_args();