Skip to content
Closed
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
275 changes: 217 additions & 58 deletions crates/perry-ext-events/src/lib.rs

Large diffs are not rendered by default.

13 changes: 13 additions & 0 deletions crates/perry-ffi/src/handle.rs
Original file line number Diff line number Diff line change
Expand Up @@ -198,6 +198,19 @@ pub fn register_handle<T: 'static + Send + Sync>(value: T) -> Handle {
handle
}

/// Register `value` under a caller-selected handle id.
///
/// This is for in-tree wrappers that must reserve a disjoint handle band for
/// generic runtime dispatch. External wrappers should prefer
/// [`register_handle`].
pub fn register_handle_with_id<T: 'static + Send + Sync>(value: T, handle: Handle) -> Handle {
if handle <= INVALID_HANDLE {
panic!("perry-ffi handle id must be positive");
}
HANDLES.insert(handle, Box::new(value));
handle
}

fn next_handle_id() -> Handle {
let handle = NEXT_HANDLE.fetch_add(1, Ordering::SeqCst);
if handle >= FFI_HANDLE_ID_END {
Expand Down
4 changes: 2 additions & 2 deletions crates/perry-ffi/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -65,8 +65,8 @@ pub use handle::gc_register_root_scanner;
pub use handle::{
drop_handle, gc_register_mutable_root_scanner, gc_register_mutable_root_scanner_named,
get_handle, get_handle_mut, handle_exists, iter_handle_ids_of, iter_handles_of,
iter_handles_of_mut, register_handle, take_handle, with_handle, with_handle_mut,
GcMutableRootScanner, GcRootVisitor, Handle, INVALID_HANDLE,
iter_handles_of_mut, register_handle, register_handle_with_id, take_handle, with_handle,
with_handle_mut, GcMutableRootScanner, GcRootVisitor, Handle, INVALID_HANDLE,
};

mod jsvalue;
Expand Down
3 changes: 2 additions & 1 deletion crates/perry-hir/src/lower/expr_new.rs
Original file line number Diff line number Diff line change
Expand Up @@ -159,6 +159,7 @@ fn is_url_encoding_constructor_name(name: &str) -> bool {

fn module_constructor_name(module_name: &str, method_name: Option<&str>) -> Option<&'static str> {
match (module_name, method_name) {
("events", Some("EventEmitter")) => Some("EventEmitter"),
("events", Some("EventEmitterAsyncResource")) => Some("EventEmitterAsyncResource"),
("url", Some("URL")) => Some("URL"),
("url", Some("URLSearchParams")) => Some("URLSearchParams"),
Expand Down Expand Up @@ -623,7 +624,7 @@ pub(super) fn lower_new(ctx: &mut LoweringContext, new_expr: &ast::NewExpr) -> R
let class_name = prop_ident.sym.as_ref();
if matches!(
(module_name, class_name),
("events", "EventEmitterAsyncResource")
("events", "EventEmitter" | "EventEmitterAsyncResource")
| ("async_hooks", "AsyncLocalStorage" | "AsyncResource")
| ("sqlite", "DatabaseSync" | "Session" | "StatementSync")
) {
Expand Down
40 changes: 33 additions & 7 deletions crates/perry-runtime/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@
//!
//! Provides the built-in Error class and its subclasses.

use crate::string::{js_string_from_bytes, StringHeader};
use crate::string::{js_string_from_bytes, js_string_from_bytes_with_capacity, StringHeader};

/// Object type tag for runtime type discrimination
pub const OBJECT_TYPE_REGULAR: u32 = 1;
Expand Down Expand Up @@ -84,12 +84,38 @@ pub struct ErrorHeader {
unsafe fn make_stack(name: &str, message: &str) -> *mut StringHeader {
// Build a simple "<name>: <message>\n at <anonymous>" string.
// Real stack traces are not implemented; the test only checks `.includes(message)`.
let s = if message.is_empty() {
format!("{}\n at <anonymous>", name)
} else {
format!("{}: {}\n at <anonymous>", name, message)
};
js_string_from_bytes(s.as_ptr(), s.len() as u32)
const SEP: &[u8] = b": ";
const SUFFIX: &[u8] = b"\n at <anonymous>";

let name_bytes = name.as_bytes();
let message_bytes = message.as_bytes();
let sep_len = if message.is_empty() { 0 } else { SEP.len() };
let total_len = name_bytes.len() + sep_len + message_bytes.len() + SUFFIX.len();
if total_len > u32::MAX as usize {
return js_string_from_bytes(std::ptr::null(), 0);
}

let ptr = js_string_from_bytes_with_capacity(std::ptr::null(), 0, total_len as u32);
let mut out = (ptr as *mut u8).add(std::mem::size_of::<StringHeader>());
std::ptr::copy_nonoverlapping(name_bytes.as_ptr(), out, name_bytes.len());
out = out.add(name_bytes.len());
if !message.is_empty() {
std::ptr::copy_nonoverlapping(SEP.as_ptr(), out, SEP.len());
out = out.add(SEP.len());
std::ptr::copy_nonoverlapping(message_bytes.as_ptr(), out, message_bytes.len());
out = out.add(message_bytes.len());
}
std::ptr::copy_nonoverlapping(SUFFIX.as_ptr(), out, SUFFIX.len());

(*ptr).byte_len = total_len as u32;
(*ptr).utf16_len = (name.encode_utf16().count()
+ if message.is_empty() {
0
} else {
SEP.len() + message.encode_utf16().count()
}
+ SUFFIX.len()) as u32;
ptr
}

unsafe fn alloc_error(
Expand Down
18 changes: 13 additions & 5 deletions crates/perry-runtime/src/gc/barrier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -761,12 +761,12 @@ pub(super) fn heap_word_candidate_addr(bits: u64) -> Option<usize> {
let tag = bits & TAG_MASK;
if tag == POINTER_TAG || tag == STRING_TAG || tag == BIGINT_TAG {
let ptr = (bits & POINTER_MASK) as usize;
return (ptr != 0).then_some(ptr);
return (ptr >= MIN_HEAP_POINTER as usize).then_some(ptr);
}
if tag >= 0x7FF8_0000_0000_0000 {
return None;
}
if (0x1000..=0x0000_FFFF_FFFF_FFFF).contains(&bits) {
if (MIN_HEAP_POINTER..=0x0000_FFFF_FFFF_FFFF).contains(&bits) {
Some(bits as usize)
} else {
None
Expand Down Expand Up @@ -801,7 +801,7 @@ pub(super) fn current_heap_header_for_user_ptr(
user_ptr: usize,
valid_ptrs: Option<&ValidPointerSet>,
) -> Option<*mut GcHeader> {
if user_ptr < GC_HEADER_SIZE + 0x1000 {
if user_ptr < MIN_HEAP_POINTER as usize {
return None;
}
if valid_ptrs.is_some_and(|ptrs| ptrs.contains(&user_ptr)) {
Expand Down Expand Up @@ -1039,7 +1039,7 @@ pub(super) fn barrier_parent_needs_remembering(parent_addr: usize, external_slot

#[inline]
pub(super) fn malloc_gc_parent_addr(parent_addr: usize) -> bool {
if parent_addr < GC_HEADER_SIZE + 0x1000 {
if parent_addr < MIN_HEAP_POINTER as usize {
return false;
}
unsafe {
Expand All @@ -1063,12 +1063,20 @@ pub(super) fn malloc_gc_parent_addr(parent_addr: usize) -> bool {
pub(super) fn decode_heap_addr(bits: u64) -> usize {
let tag = bits & TAG_MASK;
if tag == POINTER_TAG || tag == STRING_TAG || tag == BIGINT_TAG {
(bits & POINTER_MASK) as usize
let addr = (bits & POINTER_MASK) as usize;
if addr >= MIN_HEAP_POINTER as usize {
addr
} else {
0
}
} else if tag < 0x7FF8_0000_0000_0000 {
// Possible raw pointer. Accept only if the arena side metadata
// recognizes it as a heap address; ordinary f64 payload bits
// miss the metadata table and remain non-pointers.
let addr = bits as usize;
if addr < MIN_HEAP_POINTER as usize {
return 0;
}
if matches!(
crate::arena::classify_heap_generation(addr),
crate::arena::HeapGeneration::Unknown
Expand Down
6 changes: 3 additions & 3 deletions crates/perry-runtime/src/gc/copying.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,15 +127,15 @@ impl CopyingPointerSet {

#[inline]
pub(super) fn raw_pointer_candidate(bits: u64) -> bool {
(0x1000..=POINTER_MASK).contains(&bits) && bits & 0x7 == 0
(MIN_HEAP_POINTER..=POINTER_MASK).contains(&bits) && bits & 0x7 == 0
}

#[inline]
pub(super) fn decode_bits(&self, bits: u64) -> Option<(usize, bool, u64)> {
let tag = bits & TAG_MASK;
if tag == POINTER_TAG || tag == STRING_TAG || tag == BIGINT_TAG {
let addr = (bits & POINTER_MASK) as usize;
return (addr != 0).then_some((addr, true, tag));
return (addr >= MIN_HEAP_POINTER as usize).then_some((addr, true, tag));
}
if tag >= 0x7FF8_0000_0000_0000 {
return None;
Expand All @@ -155,7 +155,7 @@ impl CopyingPointerSet {
let tag = bits & TAG_MASK;
if tag == POINTER_TAG || tag == STRING_TAG || tag == BIGINT_TAG {
let addr = (bits & POINTER_MASK) as usize;
if addr == 0 {
if addr < MIN_HEAP_POINTER as usize {
return Ok(None);
}
return self
Expand Down
4 changes: 2 additions & 2 deletions crates/perry-runtime/src/gc/layout.rs
Original file line number Diff line number Diff line change
Expand Up @@ -271,12 +271,12 @@ pub(super) fn strip_nanbox_user_ptr(bits: u64) -> usize {
pub(super) fn layout_pointer_bearing_bits(bits: u64) -> bool {
let tag = bits & TAG_MASK;
if tag == POINTER_TAG || tag == STRING_TAG || tag == BIGINT_TAG {
return bits & POINTER_MASK != 0;
return bits & POINTER_MASK >= MIN_HEAP_POINTER;
}
if tag >= 0x7FF8_0000_0000_0000 {
return false;
}
(0x1000..=POINTER_MASK).contains(&bits) && (bits & 0x7) == 0
(MIN_HEAP_POINTER..=POINTER_MASK).contains(&bits) && (bits & 0x7) == 0
}

#[inline]
Expand Down
19 changes: 12 additions & 7 deletions crates/perry-runtime/src/gc/roots.rs
Original file line number Diff line number Diff line change
Expand Up @@ -518,7 +518,7 @@ pub(super) fn try_mark_value_or_raw(word: u64, valid_ptrs: &ValidPointerSet) ->
// and plain integers. Valid heap pointers are in the lower 48-bit address space and
// won't have NaN-boxing tags in upper bits (already rejected above).
let raw_ptr_u64 = word;
if !(0x1000..=0x0000_FFFF_FFFF_FFFF).contains(&raw_ptr_u64) {
if !(MIN_HEAP_POINTER..=0x0000_FFFF_FFFF_FFFF).contains(&raw_ptr_u64) {
return false; // Too small (null/invalid) or has upper bits set (NaN tag or non-address)
}
let raw_ptr = raw_ptr_u64 as usize;
Expand Down Expand Up @@ -904,9 +904,11 @@ impl<'a> RuntimeRootVisitor<'a> {
RuntimeRootVisitMode::Copy { mark } => {
let tag = bits & TAG_MASK;
if tag == POINTER_TAG || tag == STRING_TAG || tag == BIGINT_TAG {
(*mark)(f64::from_bits(bits));
if bits & POINTER_MASK >= MIN_HEAP_POINTER {
(*mark)(f64::from_bits(bits));
}
} else if tag < 0x7FF8_0000_0000_0000
&& (0x1000..=0x0000_FFFF_FFFF_FFFF).contains(&bits)
&& (MIN_HEAP_POINTER..=0x0000_FFFF_FFFF_FFFF).contains(&bits)
{
(*mark)(f64::from_bits(POINTER_TAG | (bits & POINTER_MASK)));
}
Expand All @@ -917,7 +919,7 @@ impl<'a> RuntimeRootVisitor<'a> {

#[inline]
pub(super) fn visit_tagged_raw_addr(&mut self, addr: usize, copy_tag: u64) -> Option<usize> {
if addr == 0 {
if addr < MIN_HEAP_POINTER as usize {
return None;
}
match &mut self.mode {
Expand Down Expand Up @@ -1458,7 +1460,7 @@ pub(super) fn mark_mutable_root_slots_step(
pub(super) fn shadow_slot_pointer_root(bits: u64) -> bool {
let tag = bits & TAG_MASK;
let addr = bits & POINTER_MASK;
addr != 0 && (tag == POINTER_TAG || tag == STRING_TAG || tag == BIGINT_TAG)
addr >= MIN_HEAP_POINTER && (tag == POINTER_TAG || tag == STRING_TAG || tag == BIGINT_TAG)
}

#[inline]
Expand All @@ -1477,7 +1479,7 @@ pub(super) fn mutable_slot_points_to_valid_root(bits: u64, valid_ptrs: &ValidPoi
return valid_ptrs.contains(&addr);
}
let raw_ptr = bits as usize;
raw_ptr != 0 && valid_ptrs.contains(&raw_ptr)
raw_ptr >= MIN_HEAP_POINTER as usize && valid_ptrs.contains(&raw_ptr)
}

#[inline]
Expand Down Expand Up @@ -1567,7 +1569,10 @@ pub(super) fn nanboxed_root_header(
return None;
}
let ptr_val = (value_bits & POINTER_MASK) as usize;
if ptr_val == 0 || !valid_ptrs.maybe_contains(ptr_val) || !valid_ptrs.contains(&ptr_val) {
if ptr_val < MIN_HEAP_POINTER as usize
|| !valid_ptrs.maybe_contains(ptr_val)
|| !valid_ptrs.contains(&ptr_val)
{
return None;
}
Some(unsafe { header_from_user_ptr(ptr_val as *const u8) })
Expand Down
13 changes: 13 additions & 0 deletions crates/perry-runtime/src/gc/tests/barrier.rs
Original file line number Diff line number Diff line change
Expand Up @@ -299,6 +299,13 @@ fn test_write_barrier_non_pointer_child_skipped() {
0,
"number child must not enter remembered set"
);
// Small native handles use pointer-like NaN-box tags but are not heap objects.
js_write_barrier(POINTER_TAG | (old as u64), POINTER_TAG | 0xE0000);
assert_eq!(
remembered_set_size(),
0,
"small native-handle child must not enter remembered set"
);
}

#[test]
Expand All @@ -312,6 +319,12 @@ fn test_write_barrier_non_pointer_parent_skipped() {
0,
"non-pointer parent must not dirty remembered pages"
);
js_write_barrier_slot(POINTER_TAG | 0xE0000, 0, POINTER_TAG | young as u64);
assert_eq!(
remembered_set_size(),
0,
"small native-handle parent must not dirty remembered pages"
);
}

#[test]
Expand Down
21 changes: 21 additions & 0 deletions crates/perry-runtime/src/gc/tests/layout_trace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1502,3 +1502,24 @@ fn test_trace_closure_uses_pointer_layout_mask() {
clear_marks();
clear_mark_seeds();
}

#[test]
fn test_layout_mask_ignores_small_pointer_tagged_native_handles() {
clear_marks();
clear_mark_seeds();

let closure = crate::closure::js_closure_alloc(layout_mask_test_closure as *const u8, 2);
let native_handle_bits = POINTER_TAG | 0xE0000;
crate::closure::js_closure_set_capture_f64(closure, 0, f64::from_bits(native_handle_bits));
crate::closure::js_closure_set_capture_ptr(closure, 1, 0xE0000);

assert_eq!(test_layout_pointer_slot_count(closure as usize, 2), Some(0));
let slots = unsafe { test_heap_child_slots_for_user(closure as *mut u8) };
assert!(matches!(
slots.as_slice(),
[HeapChildSlot::PointerFreeRange(range)] if range.slot_count() == 2
));

clear_marks();
clear_mark_seeds();
}
28 changes: 28 additions & 0 deletions crates/perry-runtime/src/gc/tests/runtime_roots.rs
Original file line number Diff line number Diff line change
Expand Up @@ -936,6 +936,34 @@ fn test_transient_runtime_handle_scope_drop_removes_roots() {
}
}

#[test]
fn test_transient_runtime_handle_scope_ignores_small_native_handle_root() {
clear_marks();
clear_mark_seeds();

let live = crate::arena::arena_alloc_gc(64, 8, GC_TYPE_OBJECT);
let valid_ptrs = build_valid_pointer_set();
let native_handle_bits = POINTER_TAG | 0xE0000;
{
let scope = RuntimeHandleScope::new();
let native = scope.root_nanbox_f64(f64::from_bits(native_handle_bits));
let heap = scope.root_nanbox_u64(ptr_bits(live as usize));
assert_eq!(native.get_nanbox_u64(), native_handle_bits);
assert_eq!(heap.get_nanbox_u64(), ptr_bits(live as usize));

let mut marker = RuntimeRootVisitor::for_mark(&valid_ptrs);
scan_runtime_handle_roots_mut(&mut marker);
}

unsafe {
assert_ne!((*header_from_user_ptr(live)).gc_flags & GC_FLAG_MARKED, 0);
}
assert_eq!(RuntimeHandleScope::active_len_for_tests(), 0);

clear_marks();
clear_mark_seeds();
}

#[test]
fn test_set_gc_field_rewrite_reindexes_elements() {
clear_marks();
Expand Down
6 changes: 3 additions & 3 deletions crates/perry-runtime/src/gc/trace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -388,7 +388,7 @@ pub(super) fn try_mark_value(value_bits: u64, valid_ptrs: &ValidPointerSet) -> b
return false;
}
let ptr_val = (value_bits & POINTER_MASK) as usize;
if ptr_val == 0 {
if ptr_val < MIN_HEAP_POINTER as usize {
return false;
}

Expand Down Expand Up @@ -458,7 +458,7 @@ pub(super) unsafe fn mark_field_into_worklist(
let tag = val_bits & TAG_MASK;
let ptr_val: usize = if tag == POINTER_TAG || tag == STRING_TAG || tag == BIGINT_TAG {
let p = (val_bits & POINTER_MASK) as usize;
if p == 0 {
if p < MIN_HEAP_POINTER as usize {
return false;
}
p
Expand All @@ -468,7 +468,7 @@ pub(super) unsafe fn mark_field_into_worklist(
// user-address range. f64 numbers have the exponent bits set,
// which puts them well above 0x0000_FFFF_FFFF_FFFF — they're
// rejected here.
if !(0x1000..=0x0000_FFFF_FFFF_FFFF).contains(&val_bits) {
if !(MIN_HEAP_POINTER..=0x0000_FFFF_FFFF_FFFF).contains(&val_bits) {
return false;
}
val_bits as usize
Expand Down
1 change: 1 addition & 0 deletions crates/perry-runtime/src/gc/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -784,3 +784,4 @@ pub(super) const STRING_TAG: u64 = 0x7FFF_0000_0000_0000;
pub(super) const BIGINT_TAG: u64 = 0x7FFA_0000_0000_0000;
pub(super) const POINTER_MASK: u64 = 0x0000_FFFF_FFFF_FFFF;
pub(super) const TAG_MASK: u64 = 0xFFFF_0000_0000_0000;
pub(super) const MIN_HEAP_POINTER: u64 = 0x100000;
Loading