Skip to content

Commit b737697

Browse files
Ralph Küpperproggeramlug
authored andcommitted
perf(intl): access view cursor numbers through fixed slots
1 parent b5d502f commit b737697

2 files changed

Lines changed: 123 additions & 24 deletions

File tree

crates/perry-runtime/src/intl/segments_view.rs

Lines changed: 117 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -93,6 +93,7 @@ fn cursor_ptr(value: f64) -> Option<*mut ObjectHeader> {
9393
Some(obj)
9494
}
9595

96+
#[cfg(test)]
9697
#[inline(always)]
9798
fn num_field(obj: *mut ObjectHeader, index: u32) -> usize {
9899
let bits = crate::object::js_object_get_field(obj, index);
@@ -104,6 +105,49 @@ fn num_field(obj: *mut ObjectHeader, index: u32) -> usize {
104105
}
105106
}
106107

108+
/// Direct view of the cursor's fixed inline payload. `cursor_ptr` has already
109+
/// established the class id, and every cursor is allocated with exactly
110+
/// `CURSOR_FIELDS`, so these indexed reads need neither a shape-table lookup
111+
/// nor an overflow check.
112+
#[derive(Clone, Copy)]
113+
struct CursorFields(*mut JSValue);
114+
115+
impl CursorFields {
116+
#[inline(always)]
117+
unsafe fn from_cursor(cursor: *mut ObjectHeader) -> Self {
118+
Self((cursor as *mut u8).add(std::mem::size_of::<ObjectHeader>()) as *mut JSValue)
119+
}
120+
121+
#[inline(always)]
122+
unsafe fn value(self, index: u32) -> JSValue {
123+
*self.0.add(index as usize)
124+
}
125+
126+
#[inline(always)]
127+
unsafe fn number(self, index: u32) -> usize {
128+
let n = self.value(index).to_number();
129+
if n.is_finite() && n >= 0.0 {
130+
n as usize
131+
} else {
132+
0
133+
}
134+
}
135+
136+
/// Store a value whose representation is provably an IEEE number. Cursor
137+
/// slots 1..=4 are initialized as numbers and no writer stores any other
138+
/// kind, so there is no child edge for the write barrier to remember.
139+
#[inline(always)]
140+
unsafe fn set_number(self, index: u32, value: usize) {
141+
debug_assert!((F_BYTE_START..=F_UTF16_LEN).contains(&index));
142+
// GC_STORE_AUDIT(NUMBER): `JSValue::number` cannot carry a heap edge;
143+
// `cursor_position_fields_are_never_pointer_typed` pins the invariant
144+
// across every product writer.
145+
self.0
146+
.add(index as usize)
147+
.write(JSValue::number(value as f64));
148+
}
149+
}
150+
107151
#[inline(always)]
108152
fn set_num_field(obj: *mut ObjectHeader, index: u32, value: usize) {
109153
crate::object::js_object_set_field(obj, index, JSValue::number(value as f64));
@@ -136,8 +180,11 @@ fn set_num_field(obj: *mut ObjectHeader, index: u32, value: usize) {
136180
/// A `debug_assert` re-checks it in debug builds, which is where a future
137181
/// fourth writer to the slot would be caught.
138182
#[inline]
139-
fn with_input<R>(cursor: *mut ObjectHeader, f: impl FnOnce(&str) -> R) -> Option<R> {
140-
let value = crate::object::js_object_get_field(cursor, F_INPUT);
183+
fn with_input<R>(fields: CursorFields, f: impl FnOnce(&str) -> R) -> Option<R> {
184+
// SAFETY: `fields` was derived at entry from a branded cursor, and slot 0
185+
// is the traced input value. Reading it here is the §9a re-derivation; no
186+
// address derived from it is retained beyond this call.
187+
let value = unsafe { fields.value(F_INPUT) };
141188
let mut sso = [0u8; crate::value::SHORT_STRING_MAX_LEN];
142189
let bytes =
143190
unsafe { crate::string::js_string_key_bytes(JSValue::from_bits(value.bits()), &mut sso) }?;
@@ -249,27 +296,31 @@ pub extern "C" fn js_segments_view_open(segmenter: f64, input: f64) -> f64 {
249296
}
250297

251298
/// Advance to the next grapheme boundary. `1.0` if a segment is now current,
252-
/// `0.0` at the end. **Allocation-free by contract**: three integer field
299+
/// `0.0` at the end. **Allocation-free by contract**: four integer field
253300
/// writes and a UAX #29 boundary scan, no arena allocation, no owned `String`,
254301
/// no descriptor insert — which is also why it cannot collect.
255302
#[no_mangle]
256303
pub extern "C" fn js_segments_view_next(cursor: f64) -> f64 {
257304
let Some(c) = cursor_ptr(cursor) else {
258305
return 0.0;
259306
};
260-
let from = num_field(c, F_BYTE_END);
261-
let utf16_start = num_field(c, F_UTF16_START) + num_field(c, F_UTF16_LEN);
262-
let step = with_input(c, |text| {
307+
// SAFETY: `cursor_ptr` proved the fixed cursor layout.
308+
let fields = unsafe { CursorFields::from_cursor(c) };
309+
let from = unsafe { fields.number(F_BYTE_END) };
310+
let utf16_start = unsafe { fields.number(F_UTF16_START) + fields.number(F_UTF16_LEN) };
311+
let step = with_input(fields, |text| {
263312
next_boundary(text, from).map(|next| (next, super::segmenter::utf16_len(&text[from..next])))
264313
})
265314
.flatten();
266315
let Some((next, seg_u16)) = step else {
267316
return 0.0;
268317
};
269-
set_num_field(c, F_BYTE_START, from);
270-
set_num_field(c, F_UTF16_START, utf16_start);
271-
set_num_field(c, F_BYTE_END, next);
272-
set_num_field(c, F_UTF16_LEN, seg_u16 as usize);
318+
unsafe {
319+
fields.set_number(F_BYTE_START, from);
320+
fields.set_number(F_UTF16_START, utf16_start);
321+
fields.set_number(F_BYTE_END, next);
322+
fields.set_number(F_UTF16_LEN, seg_u16 as usize);
323+
}
273324
bump(&NEXTS);
274325
1.0
275326
}
@@ -291,14 +342,16 @@ pub extern "C" fn js_segments_view_code_point_at(cursor: f64, k: f64) -> f64 {
291342
if !k.is_finite() || k < 0.0 || k.fract() != 0.0 {
292343
return undef;
293344
}
345+
// SAFETY: `cursor_ptr` proved the fixed cursor layout.
346+
let fields = unsafe { CursorFields::from_cursor(c) };
294347
let k = k as usize;
295-
if k >= num_field(c, F_UTF16_LEN) {
348+
if k >= unsafe { fields.number(F_UTF16_LEN) } {
296349
return undef;
297350
}
298-
let start = num_field(c, F_BYTE_START);
299-
let end = num_field(c, F_BYTE_END);
351+
let start = unsafe { fields.number(F_BYTE_START) };
352+
let end = unsafe { fields.number(F_BYTE_END) };
300353
bump(&CODE_POINT_ATS);
301-
with_input(c, |text| {
354+
with_input(fields, |text| {
302355
let seg = &text[start..end];
303356
let mut utf16_pos = 0usize;
304357
for ch in seg.chars() {
@@ -329,14 +382,16 @@ pub extern "C" fn js_segments_view_segment(cursor: f64) -> f64 {
329382
let Some(c) = cursor_ptr(cursor) else {
330383
return undef;
331384
};
332-
let start = num_field(c, F_BYTE_START);
333-
let end = num_field(c, F_BYTE_END);
385+
// SAFETY: `cursor_ptr` proved the fixed cursor layout.
386+
let fields = unsafe { CursorFields::from_cursor(c) };
387+
let start = unsafe { fields.number(F_BYTE_START) };
388+
let end = unsafe { fields.number(F_BYTE_END) };
334389
bump(&MATERIALISE_SEGMENT);
335390
// The allocation happens INSIDE the borrow, so the borrow must not outlive
336391
// it: take the bytes out first, then allocate from a copy on the stack path
337392
// `js_string_from_bytes` performs. Nothing derived from the input survives
338393
// this call.
339-
let made = with_input(c, |text| {
394+
let made = with_input(fields, |text| {
340395
let seg = &text[start..end];
341396
crate::string::js_string_from_bytes(seg.as_ptr(), seg.len() as u32)
342397
});
@@ -398,9 +453,11 @@ pub extern "C" fn js_segments_view_regexp_test(cursor: f64, regex: f64) -> f64 {
398453
bump(&REGEXP_TEST_DECLINED);
399454
return undef;
400455
}
401-
let start = num_field(c, F_BYTE_START);
402-
let end = num_field(c, F_BYTE_END);
403-
let verdict = with_input(c, |text| {
456+
// SAFETY: `cursor_ptr` proved the fixed cursor layout.
457+
let fields = unsafe { CursorFields::from_cursor(c) };
458+
let start = unsafe { fields.number(F_BYTE_START) };
459+
let end = unsafe { fields.number(F_BYTE_END) };
460+
let verdict = with_input(fields, |text| {
404461
crate::regex::regexp_test_str_bounded(re, &text[start..end])
405462
})
406463
.flatten();
@@ -618,6 +675,40 @@ mod view_mode_tests {
618675
);
619676
}
620677

678+
/// SABOTAGE-SHAPED: slots 1..=4 are the proof that `_next` may bypass the
679+
/// generic JSValue store barrier. A future writer that puts a pointer in
680+
/// any position slot makes this fail at the exact step where the invariant
681+
/// is broken; slot 0 is intentionally excluded because it is the traced
682+
/// input string.
683+
#[test]
684+
fn cursor_position_fields_are_never_pointer_typed() {
685+
let cursor = js_segments_view_open(
686+
grapheme_segmenter(),
687+
js_string("a\u{301}b\u{1f469}\u{200d}\u{1f4bb}cd"),
688+
);
689+
assert!(cursor != 0.0);
690+
let c = cursor_ptr(cursor).expect("branded cursor");
691+
let fields = unsafe { CursorFields::from_cursor(c) };
692+
let mut steps = 0usize;
693+
loop {
694+
for index in F_BYTE_START..=F_UTF16_LEN {
695+
let value = unsafe { fields.value(index) };
696+
assert!(
697+
value.is_number() && !value.is_pointer(),
698+
"cursor position slot {index} must be number-only before step {steps}"
699+
);
700+
}
701+
if js_segments_view_next(cursor) != 1.0 {
702+
break;
703+
}
704+
steps += 1;
705+
}
706+
assert!(
707+
steps >= 4,
708+
"the invariant must be checked across real steps"
709+
);
710+
}
711+
621712
/// The rooting obligation of §9e, exercised rather than asserted: `open`
622713
/// allocates the cursor while holding the input, so a collection landing in
623714
/// that window must not leave a dead value in the traced slot. Force a
@@ -850,10 +941,12 @@ mod view_mode_tests {
850941
(0..count)
851942
.find(|&i| {
852943
let key = crate::array::js_array_get_f64(keys, i);
853-
crate::string::js_string_key_matches_bytes(
854-
JSValue::from_bits(key.to_bits()),
855-
b"test",
856-
)
944+
unsafe {
945+
crate::string::js_string_key_matches_bytes(
946+
JSValue::from_bits(key.to_bits()),
947+
b"test",
948+
)
949+
}
857950
})
858951
.expect("test key") as u32
859952
})

scripts/gc_runtime_root_holders.json

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -589,6 +589,12 @@
589589
"verdict": "test_only",
590590
"why": "#[cfg(test)] diagnostic trace for the bound-method moving-GC regression: records the (before, after) addresses a test-forced minor produced so the test can assert the relocation happened. The addresses are compared as integers, never dereferenced, and the cell is dead in a shipped binary."
591591
},
592+
{
593+
"file": "crates/perry-runtime/src/object/regex_proto_thunks.rs",
594+
"name": "REGEXP_PROTOTYPE_TEST_WALKS",
595+
"verdict": "not_a_gc_pointer",
596+
"why": "View-mode diagnostic tally of install-time RegExp.prototype.test walks. A plain AtomicU64 incremented once by record_canonical_test_site and read as a count by tests; it never stores an address or NaN-boxed value. The actual prototype and closure roots live together in REGEXP_PROTOTYPE_TEST_SITE and are visited by scan_canonical_test_site_roots_mut."
597+
},
592598
{
593599
"file": "crates/perry-runtime/src/object/read_stub.rs",
594600
"name": "READ_STUB",

0 commit comments

Comments
 (0)