From 6858b77e3fa6a994aeed77c60a48f957883d6e47 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 13 Sep 2026 10:06:02 +0000 Subject: [PATCH 1/3] perf(regex): bind a RegExp's program and subject in constant work across calls (#10166) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A search per JavaScript call (an exec or test loop, matchAll's next(), search) bound its subject and program from scratch every time: binding a subject decodes the entire string and binding a program revalidates every word. A loop over one string therefore did O(n) binding work per call and O(n²) overall, and `.test()` paid a program validation per call. Perex 0.1.3 adds constant-work rebinding for what the host already validated: BoundSubject::new_counted(storage, utf16_len) and a ProgramWitness from BoundProgram::witness() that BoundProgram::new_witnessed checks against the program's length and header. Perry now keeps both, as plain data, with no allocation and nothing new traced: - Program: ProgramCell gains `witness: Option` in its pointer-free prefix, beside the words it describes. The first validating bind records it; later binds use new_witnessed, falling back to validation (recording a fresh witness) if it does not match. A recompile emits a new cell that starts with none, so a witness can never describe other words. RegExpHeader stays 56 bytes. Debug builds assert no witness is written while a view of the cell's words is live. - Subject: StringHeader gains STRING_FLAG_WTF8_VALIDATED. Perry strings are not all valid WTF-8 (raw Buffer/FFI payloads reach regex operations), so nothing is assumed: the first bind decodes, and only if that succeeds and the decoded UTF-16 length equals the header's is the header marked; later binds use new_counted. A validated header is already shared, so its payload is never mutated in place. init_string_header strips the bit from every constructed string, and the in-place writers (js_string_append, js_string_append_chain) clear it, so it never reaches another string; concat's memo check ignores it. Deliberately excluded: a cross-call lastIndex position hint for non-ASCII exec loops. Recognising the same string across calls without a traced reference would need a heap generation counter bumped on every free and move path, and one missed path gives silent wrong answers. The remaining cost is one seek from the nearer end of the subject per call on non-ASCII subjects; it is charged but uncapped (#10176), so those loops finish, but are not linear. Tests: - string::tests_validated_flag: slice, substring, trim, concat, repeat, padStart, toUpperCase, join, string_copy_range and js_string_from_bytes_known_utf16 (passed the source's whole flags word) never inherit the bit; both in-place append paths clear it. - gc::tests::runtime_roots::perex_cross_call: a program cell records its witness on first bind and a recompiled program's new cell has none; a foreign equal-header witness (x(b) on x(a)) still answers with the cell's own words; a mismatched witness falls back and is replaced; a subject is marked only with an exact length, never with a corrupted utf16_len or malformed bytes; marks survive moving collections; a witness write under a live view is caught. - perex_reuse's accounting now expects one validation per program. Fault injection, each confirmed to fail its test: marking without the length check; never recording a witness; a mismatch without fallback; removing the view guard; keeping the whole flags word in init_string_header; keeping the bit in the in-place writers. Requires perex 0.1.3 (crates.io checksum 060b4682849d20ebcba05d68f9584a1bba20af4b7838c688cfb37af572f562f4), resolved once with CARGO_RESOLVER_INCOMPATIBLE_PUBLISH_AGE=allow with the maintainer's approval; ordinary --locked builds use it without the override. Claude-Session: https://claude.ai/code/session_01Da12JXeG5XuVBma5yWp5C9 --- Cargo.lock | 4 +- Cargo.toml | 2 +- .../src/gc/tests/runtime_roots.rs | 2 + .../tests/runtime_roots/perex_cross_call.rs | 227 ++++++++++++++++++ .../src/gc/tests/runtime_roots/perex_reuse.rs | 14 +- crates/perry-runtime/src/regex/perex_api.rs | 75 +++++- .../src/regex/perex_match_search.rs | 6 +- crates/perry-runtime/src/regex/perex_owner.rs | 85 ++++++- crates/perry-runtime/src/string/append.rs | 2 +- crates/perry-runtime/src/string/concat.rs | 5 +- crates/perry-runtime/src/string/mod.rs | 17 +- .../src/string/tests_validated_flag.rs | 101 ++++++++ 12 files changed, 514 insertions(+), 26 deletions(-) create mode 100644 crates/perry-runtime/src/gc/tests/runtime_roots/perex_cross_call.rs create mode 100644 crates/perry-runtime/src/string/tests_validated_flag.rs diff --git a/Cargo.lock b/Cargo.lock index ae90e9b1d7..09767ca4f1 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5684,9 +5684,9 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perex" -version = "0.1.2" +version = "0.1.3" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "21df239ee18f99de6abff50953f6f15be1b5ebd11e6ae9661acdd93026e983db" +checksum = "060b4682849d20ebcba05d68f9584a1bba20af4b7838c688cfb37af572f562f4" [[package]] name = "perry" diff --git a/Cargo.toml b/Cargo.toml index b3a2237c9a..db4cd20d6f 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -399,7 +399,7 @@ chrono = "0.4" regex = "1.12" aho-corasick = "1.1" # The single regular-expression engine, through crates/perry-perex. -perex = "0.1.2" +perex = "0.1.3" hex = "0.4" tempfile = "3" itoa = "1.0" diff --git a/crates/perry-runtime/src/gc/tests/runtime_roots.rs b/crates/perry-runtime/src/gc/tests/runtime_roots.rs index a5e4bf025f..627516524d 100644 --- a/crates/perry-runtime/src/gc/tests/runtime_roots.rs +++ b/crates/perry-runtime/src/gc/tests/runtime_roots.rs @@ -19,6 +19,8 @@ mod old_defrag_contract; #[cfg(feature = "regex-engine")] mod perex_construction; #[cfg(feature = "regex-engine")] +mod perex_cross_call; +#[cfg(feature = "regex-engine")] mod perex_dispatch; #[cfg(feature = "regex-engine")] mod perex_execution; diff --git a/crates/perry-runtime/src/gc/tests/runtime_roots/perex_cross_call.rs b/crates/perry-runtime/src/gc/tests/runtime_roots/perex_cross_call.rs new file mode 100644 index 0000000000..4f8811386e --- /dev/null +++ b/crates/perry-runtime/src/gc/tests/runtime_roots/perex_cross_call.rs @@ -0,0 +1,227 @@ +//! Cross-call rebinding (#10166): a program cell keeps a plain-data witness of +//! its validated words, and a string header remembers that its payload +//! validated, so a search per JavaScript call binds both in constant work. +use super::*; +use crate::regex::perex_api as api; +use crate::regex::perex_owner::{cell_witness, set_cell_witness, GcProgram}; +use crate::regex::perex_runtime::EngineError; +use crate::regex::RegExpHeader; +use crate::string::{StringHeader, STRING_FLAG_WTF8_VALIDATED}; +use crate::value::js_nanbox_string; +use perex::binding::ImmutableProgram; + +fn text<'s>(scope: &'s RuntimeHandleScope, bytes: &[u8]) -> RuntimeHandle<'s> { + scope.root_string_ptr(crate::string::js_string_from_bytes( + bytes.as_ptr(), + bytes.len() as u32, + )) +} + +fn regex<'s>(scope: &'s RuntimeHandleScope, pattern: &str) -> RuntimeHandle<'s> { + let pattern = text(scope, pattern.as_bytes()); + let flags = text(scope, b""); + scope.root_raw_mut_ptr(pattern.with_const_ptr::(|pattern| { + flags.with_const_ptr::(|flags| crate::regex::js_regexp_new(pattern, flags)) + })) +} + +/// One non-global search: the full match's UTF-16 span, or None. +fn search( + re: &RuntimeHandle<'_>, + input: &RuntimeHandle<'_>, +) -> Result, EngineError> { + re.with_mut_ptr::(|re| { + input.with_const_ptr::(|input| { + api::execute(re, input, false, &mut || Ok(())) + .map(|found| found.map(|m| (m.full.start(), m.full.end()))) + }) + }) +} + +/// The witness in the RegExp's current program cell. +fn witness(re: &RuntimeHandle<'_>) -> Option { + re.with_const_ptr::(|re| unsafe { cell_witness((*re).perex_program) }) +} + +fn set_witness(re: &RuntimeHandle<'_>, value: Option) { + re.with_const_ptr::(|re| unsafe { + set_cell_witness((*re).perex_program, value) + }); +} + +fn recompile(scope: &RuntimeHandleScope, re: &RuntimeHandle<'_>, pattern: &str) { + let pattern = text(scope, pattern.as_bytes()); + let flags = text(scope, b""); + let p = pattern.with_const_ptr::(|p| js_nanbox_string(p as i64)); + let f = flags.with_const_ptr::(|f| js_nanbox_string(f as i64)); + re.with_mut_ptr::(|re| crate::regex::js_regexp_compile_value(re, p, f)); +} + +fn validated(s: &RuntimeHandle<'_>) -> bool { + s.with_const_ptr::(|s| unsafe { (*s).flags & STRING_FLAG_WTF8_VALIDATED != 0 }) +} + +#[test] +fn perex_program_witness_lives_with_its_cell_and_a_recompile_starts_without_one() { + let _guard = CopyingNurseryTestGuard::new(0); + let _triggers = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + super::perex_public::register_host_roots(); + let scope = RuntimeHandleScope::new(); + let input = text(&scope, b"xb xa"); + + let re = regex(&scope, "x(a)"); + assert_eq!( + witness(&re), + None, + "a freshly compiled program has no witness" + ); + assert_eq!(search(&re, &input).unwrap(), Some((3, 5))); + let wa = witness(&re).expect("the first validating bind records the witness"); + + recompile(&scope, &re, "x(b)"); + assert_eq!( + witness(&re), + None, + "a recompile emits a new cell, which starts without a witness" + ); + assert_eq!(search(&re, &input).unwrap(), Some((0, 2))); + let wb = witness(&re).expect("the new cell records its own witness"); + + // Precondition for the next step: these programs share length and header. + assert_eq!( + wa, wb, + "precondition: x(a) and x(b) must share length and header" + ); + // Even a foreign witness over an equal header binds the cell's CURRENT + // words, which Perex emitted: the answer is this program's. + recompile(&scope, &re, "x(a)"); + set_witness(&re, Some(wb)); + assert_eq!(search(&re, &input).unwrap(), Some((3, 5))); +} + +#[test] +fn perex_program_witness_that_does_not_match_falls_back_to_validation() { + let _guard = CopyingNurseryTestGuard::new(0); + let _triggers = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + super::perex_public::register_host_roots(); + let scope = RuntimeHandleScope::new(); + let input = text(&scope, b"ba"); + let a = regex(&scope, "a"); + let b = regex(&scope, "b"); + assert_eq!(search(&a, &input).unwrap(), Some((1, 2))); + assert_eq!(search(&b, &input).unwrap(), Some((0, 1))); + let (wa, wb) = (witness(&a).unwrap(), witness(&b).unwrap()); + assert_ne!( + wa, wb, + "precondition: a and b must differ in length or header" + ); + + set_witness(&a, Some(wb)); + assert_eq!(search(&a, &input).unwrap(), Some((1, 2))); + assert_eq!( + witness(&a), + Some(wa), + "a mismatched witness is replaced by validation" + ); +} + +#[test] +fn perex_subject_is_marked_only_after_it_validates_with_its_exact_length() { + let _guard = CopyingNurseryTestGuard::new(0); + let _triggers = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + super::perex_public::register_host_roots(); + let scope = RuntimeHandleScope::new(); + let digits = regex(&scope, r"\d+"); + + let good = text(&scope, "ä1 b22 c333 longer than inline".as_bytes()); + assert!(!validated(&good)); + assert_eq!(search(&digits, &good).unwrap(), Some((1, 2))); + assert!( + validated(&good), + "a valid payload with an exact length is marked" + ); + assert_eq!( + search(&digits, &good).unwrap(), + Some((1, 2)), + "the counted bind answers the same" + ); + + let wrong_length = text(&scope, b"abcdef1 and longer than inline"); + wrong_length.with_const_ptr::(|s| unsafe { + (*(s as *mut StringHeader)).utf16_len = 3; + }); + let _ = search(&digits, &wrong_length); + assert!( + !validated(&wrong_length), + "a header whose length disagrees with its payload must never be trusted" + ); + + let malformed = text(&scope, b"\xff\xfe 1 malformed and longer than inline"); + let result = search(&digits, &malformed); + assert!( + result.is_err(), + "malformed WTF-8 is rejected as before: {result:?}" + ); + assert!( + !validated(&malformed), + "a payload that failed to validate is never marked" + ); +} + +#[test] +fn perex_cross_call_marks_survive_moving_collections() { + let _guard = CopyingNurseryTestGuard::new(0); + let _scan = ConservativeScanDisabledGuard::new(); + let _triggers = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + let _force = ForcedEvacuationTestGuard::on(); + super::perex_public::register_host_roots(); + let scope = RuntimeHandleScope::new(); + let re = regex(&scope, r"\d+"); + let input = text( + &scope, + "ö7 and some text after it, beyond inline".as_bytes(), + ); + assert_eq!(search(&re, &input).unwrap(), Some((1, 2))); + let (w, marked) = (witness(&re), validated(&input)); + assert!(w.is_some() && marked); + let (re_before, input_before) = ( + re.with_const_ptr::(|p| p as usize), + input.with_const_ptr::(|p| p as usize), + ); + let cycles = copying_minor_cycles(); + gc_collect_minor(); + gc_collect_minor(); + assert!(copying_minor_cycles() > cycles); + assert_ne!( + input.with_const_ptr::(|p| p as usize), + input_before, + "the string must move" + ); + assert_ne!( + re.with_const_ptr::(|p| p as usize), + re_before, + "the RegExp must move" + ); + assert_eq!(witness(&re), w, "the witness moves with its RegExp"); + assert!(validated(&input), "the mark moves with its string"); + assert_eq!(search(&re, &input).unwrap(), Some((1, 2))); +} + +/// Debug builds prove a witness is never written under a live view of the +/// words it describes (#10166). +#[test] +#[cfg(debug_assertions)] +#[should_panic(expected = "must not be written while a view of its words is live")] +fn perex_program_witness_write_under_a_live_view_is_caught() { + let _guard = CopyingNurseryTestGuard::new(0); + let _triggers = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + super::perex_public::register_host_roots(); + let scope = RuntimeHandleScope::new(); + let re = regex(&scope, "q+"); + let input = text(&scope, b"qq"); + assert_eq!(search(&re, &input).unwrap(), Some((0, 2))); + let witness = witness(&re).expect("validated"); + let owner = unsafe { GcProgram::from_receiver(&scope, &re) }.unwrap(); + let root = owner.root(); + let _ = owner.with_words(|_words| GcProgram::record_witness(&root, witness)); +} diff --git a/crates/perry-runtime/src/gc/tests/runtime_roots/perex_reuse.rs b/crates/perry-runtime/src/gc/tests/runtime_roots/perex_reuse.rs index e6fdd12f94..3eddd46247 100644 --- a/crates/perry-runtime/src/gc/tests/runtime_roots/perex_reuse.rs +++ b/crates/perry-runtime/src/gc/tests/runtime_roots/perex_reuse.rs @@ -140,15 +140,17 @@ fn perex_reuse_serves_a_whole_global_loop_across_moving_collections() { let fresh = regex(&scope, PATTERN, "gu"); let (fresh_matches, fresh_work) = global_loop(&fresh, &fresh_input, None); assert_eq!(fresh_matches, expected); - // Six searches (five matches and the final miss). Binding per search charges - // program validation six times; reuse charged it once, in `setup`. Reuse - // also resumes each search from the previous one instead of seeking from - // an end of this non-ASCII subject, so it saves at least the validations. + // Both loops validate their program once: the witness in each program cell + // lets later searches bind it without validating (#10166). The reused loop + // paid its validation in `setup`, and additionally resumes each search from + // the previous one instead of seeking from an end of this non-ASCII subject, + // so it must charge strictly less than the fresh loop minus one validation. + // Without reuse the two differ by exactly that one validation. let validation = api::WORK - setup.remaining(); assert!(validation > 0); assert!( - fresh_work >= reused_work + 6 * validation, - "reuse must save at least six program validations: fresh {fresh_work}, reused {reused_work}, one validation {validation}" + fresh_work > reused_work + validation, + "reuse must save its seeks as well as the validation: fresh {fresh_work}, reused {reused_work}, one validation {validation}" ); } diff --git a/crates/perry-runtime/src/regex/perex_api.rs b/crates/perry-runtime/src/regex/perex_api.rs index d467743e99..2175921db9 100644 --- a/crates/perry-runtime/src/regex/perex_api.rs +++ b/crates/perry-runtime/src/regex/perex_api.rs @@ -106,7 +106,72 @@ pub(crate) fn program<'s>( ) -> Result>, EngineError> { let owner = unsafe { GcProgram::from_receiver(scope, receiver) } .map_err(|e| EngineError::Subject(perex::binding::SubjectError::Resource(e)))?; - BoundProgram::new(owner, budget).map_err(|e| EngineError::Program(e.error)) + bind_program(owner, budget) +} + +/// Bind a program, in constant work when a binding validated this same cell +/// before (#10166): the witness lives beside the words in the program cell, so +/// it cannot describe other words. A witness that does not match falls back to +/// validation, which records a fresh one. No allocation and nothing traced. +pub(crate) fn bind_program<'s>( + owner: GcProgram<'s>, + budget: &mut Budget, +) -> Result>, EngineError> { + let root = owner.root(); + let owner = match owner.witness() { + Some(witness) => match BoundProgram::new_witnessed(owner, witness) { + Ok(bound) => return Ok(bound), + Err(failed) => failed.storage, + }, + None => owner, + }; + let bound = BoundProgram::new(owner, budget).map_err(|e| EngineError::Program(e.error))?; + GcProgram::record_witness(&root, bound.witness()); + Ok(bound) +} + +/// Bind a whole heap string, in constant work when this header was validated +/// before (#10166). The first binding decodes it; if that succeeds and its +/// UTF-16 length matches the header, the header is marked +/// `STRING_FLAG_WTF8_VALIDATED` and later bindings use `new_counted`. +/// +/// Perry strings are not all valid WTF-8 (raw Buffer and FFI payloads reach +/// here too), so validity is never assumed: a string that fails to validate is +/// never marked and keeps failing exactly as before. `HeapSubject::new` has +/// already marked the header shared, so a marked payload is never mutated in +/// place, and the mark is never copied to another string (see the flag). +pub(crate) fn bind_heap_subject( + input: RuntimeHandle<'_>, +) -> Result>, EngineError> { + use crate::string::STRING_FLAG_WTF8_VALIDATED; + let (utf16_len, validated) = input.with_const_ptr::(|s| unsafe { + ( + (*s).utf16_len as usize, + (*s).flags & STRING_FLAG_WTF8_VALIDATED != 0, + ) + }); + let owner = unsafe { HeapSubject::new(input) } + .map_err(|e| EngineError::Subject(perex::binding::SubjectError::Resource(e)))?; + let owner = if validated { + match BoundSubject::new_counted(owner, utf16_len) { + Ok(bound) => return Ok(bound), + Err(failed) => failed.storage, + } + } else { + owner + }; + let bound = BoundSubject::new(owner).map_err(|e| EngineError::Subject(e.error))?; + let decoded = bound + .with_view(|view| view.len_utf16()) + .map_err(EngineError::Subject)?; + // An empty string has nothing to decode. `HeapSubject::new` already wrote + // this header's refcount, so it is writable. + if decoded == utf16_len && utf16_len > 0 { + input.with_const_ptr::(|s| unsafe { + (*(s as *mut StringHeader)).flags |= STRING_FLAG_WTF8_VALIDATED; + }); + } + Ok(bound) } /// Bindings one compound operation reuses across its searches (#10165). @@ -162,7 +227,7 @@ impl<'b, 's> Reuse<'b, 's> { let receiver = scope.root_raw_const_ptr(re); let cell = scope.root_raw_const_ptr(unsafe { (*re).perex_program }); let owner = unsafe { GcProgram::from_receiver(scope, &receiver) }.ok()?; - let bound = BoundProgram::new(owner, budget).ok()?; + let bound = bind_program(owner, budget).ok()?; Some(ReusedProgram { receiver, cell, @@ -339,11 +404,7 @@ pub(crate) fn execute_with_resources( let subject = match reused_subject { Some(subject) => subject, None => { - fresh_subject = - BoundSubject::new(unsafe { HeapSubject::new(input) }.map_err(|e| { - EngineError::Subject(perex::binding::SubjectError::Resource(e)) - })?) - .map_err(|e| EngineError::Subject(e.error))?; + fresh_subject = bind_heap_subject(input)?; &fresh_subject } }; diff --git a/crates/perry-runtime/src/regex/perex_match_search.rs b/crates/perry-runtime/src/regex/perex_match_search.rs index c09a371025..3222675382 100644 --- a/crates/perry-runtime/src/regex/perex_match_search.rs +++ b/crates/perry-runtime/src/regex/perex_match_search.rs @@ -53,11 +53,7 @@ pub(crate) fn flags(receiver: f64) -> Result<*mut StringHeader, EngineError> { pub(super) fn subject( input: RuntimeHandle<'_>, ) -> Result>, EngineError> { - BoundSubject::new( - unsafe { HeapSubject::new(input) } - .map_err(|e| EngineError::Subject(perex::binding::SubjectError::Resource(e)))?, - ) - .map_err(|e| EngineError::Subject(e.error)) + api::bind_heap_subject(input) } fn match_flags( diff --git a/crates/perry-runtime/src/regex/perex_owner.rs b/crates/perry-runtime/src/regex/perex_owner.rs index 5606175edc..05a7188fe3 100644 --- a/crates/perry-runtime/src/regex/perex_owner.rs +++ b/crates/perry-runtime/src/regex/perex_owner.rs @@ -13,6 +13,12 @@ use perex::compiler::{CompileError, Prepared}; #[repr(C)] struct ProgramCell { word_count: usize, + /// What validating these words established, so later bindings of this same + /// cell skip validation (#10166). Plain data beside the words it describes: + /// the words never change, and a recompile emits a new cell that starts + /// with none, so it cannot describe other words. Stored by the first + /// validating bind; the cell stays a pointer-free leaf. + witness: Option, // Immediately followed by word_count initialized u32 words. } @@ -74,7 +80,10 @@ impl<'scope> GcProgram<'scope> { // finalizer or a leaked external owner. No GC call occurs in this scope. unsafe { // GC_STORE_AUDIT(POINTER_FREE): the program cell is a leaf of u32 words; its prefix is a count. - cell.write(ProgramCell { word_count: words }); + cell.write(ProgramCell { + word_count: words, + witness: None, + }); let output = cell.add(1).cast::(); output.write_bytes(0, words); let output = std::slice::from_raw_parts_mut(output, words); @@ -106,6 +115,39 @@ impl<'scope> GcProgram<'scope> { }); } + /// The witness stored beside this program's words, if a binding validated + /// them before (#10166). + pub(crate) fn witness(&self) -> Option { + self.root + .with_const_ptr::(|cell| unsafe { (*cell).witness }) + } + + /// Record what validating this program established. `witness` must come + /// from a binding of this same cell. + pub(crate) fn record_witness( + root: &RuntimeHandle<'_>, + witness: perex::binding::ProgramWitness, + ) { + // The prefix lies outside the word slice, but the write still goes + // through the cell's own pointer and never under a live view of its + // words (a binding holds none between calls). + #[cfg(debug_assertions)] + debug_assert_eq!( + PROGRAM_VIEWS.with(std::cell::Cell::get), + 0, + "a program cell's witness must not be written while a view of its words is live" + ); + // A plain-data store into a pointer-free leaf: no allocation, no barrier. + root.with_const_ptr::(|cell| unsafe { + (*(cell as *mut ProgramCell)).witness = Some(witness); + }); + } + + /// This program's registered root, which survives consuming the owner. + pub(crate) fn root(&self) -> RuntimeHandle<'scope> { + self.root + } + /// Establish a separate operation root, so reentrant receiver recompilation /// cannot replace the immutable program of an already-running operation. /// @@ -127,6 +169,46 @@ impl<'scope> GcProgram<'scope> { } } +/// The witness stored in the program cell at `program` (a RegExp's +/// `perex_program`), for tests. +#[cfg(test)] +pub(crate) unsafe fn cell_witness(program: *const u8) -> Option { + unsafe { (*(program as *const ProgramCell)).witness } +} + +/// Overwrite the witness stored in the program cell at `program`, for tests. +#[cfg(test)] +pub(crate) unsafe fn set_cell_witness( + program: *const u8, + witness: Option, +) { + unsafe { (*(program as *mut ProgramCell)).witness = witness }; +} + +// How many `with_words` views of any program cell are live on this thread, so +// debug builds can prove a witness is never written under one (#10166). +#[cfg(debug_assertions)] +thread_local! { + static PROGRAM_VIEWS: std::cell::Cell = const { std::cell::Cell::new(0) }; +} + +struct ProgramView; + +impl ProgramView { + fn open() -> Self { + #[cfg(debug_assertions)] + PROGRAM_VIEWS.with(|views| views.set(views.get() + 1)); + ProgramView + } +} + +impl Drop for ProgramView { + fn drop(&mut self) { + #[cfg(debug_assertions)] + PROGRAM_VIEWS.with(|views| views.set(views.get() - 1)); + } +} + impl ImmutableProgram for GcProgram<'_> { type Error = OwnerError; @@ -149,6 +231,7 @@ impl ImmutableProgram for GcProgram<'_> { // Only emit creates these cells; no mutable word access escapes. // Binding validation is separate, once per immutable owner. This // getter neither allocates nor polls and always reacquires the base. + let _view = ProgramView::open(); Ok(f(std::slice::from_raw_parts(cell.add(1).cast(), count))) }) } diff --git a/crates/perry-runtime/src/string/append.rs b/crates/perry-runtime/src/string/append.rs index 789f3dff1b..5c51849197 100644 --- a/crates/perry-runtime/src/string/append.rs +++ b/crates/perry-runtime/src/string/append.rs @@ -127,7 +127,7 @@ pub extern "C" fn js_string_append( ); (*dest).byte_len = new_blen; (*dest).utf16_len += (*src).utf16_len; - (*dest).flags |= flag_bits; + (*dest).flags = ((*dest).flags | flag_bits) & !STRING_FLAG_WTF8_VALIDATED; return if boundary_pair { // Merge the straddling pair (usually returns a new, smaller // string; rare, so the in-place win still holds in general). diff --git a/crates/perry-runtime/src/string/concat.rs b/crates/perry-runtime/src/string/concat.rs index 800d942489..9fae0db487 100644 --- a/crates/perry-runtime/src/string/concat.rs +++ b/crates/perry-runtime/src/string/concat.rs @@ -806,7 +806,7 @@ pub extern "C" fn js_string_concat_value( let memoizable = total_blen <= CONCAT_MEMO_MAX_BYTES as usize && is_valid_string_ptr(prefix) && prefix_u16 == prefix_blen - && unsafe { (*prefix).flags == 0 } + && unsafe { (*prefix).flags & !STRING_FLAG_WTF8_VALIDATED == 0 } && bytes_all_ascii(string_data(prefix), prefix_blen) && concat_memo_should_probe(); let mut memo_buf = [0u8; CONCAT_MEMO_MAX_BYTES as usize]; @@ -1073,7 +1073,8 @@ fn append_chain_all_heap_strings( } (*dest).byte_len = total_blen; (*dest).utf16_len = total_u16; - (*dest).flags |= piece_flags; + // The destination's payload just changed; no piece's validation carries over. + (*dest).flags = ((*dest).flags | piece_flags) & !STRING_FLAG_WTF8_VALIDATED; return if piece_flags & STRING_FLAG_HAS_LONE_SURROGATES != 0 { canonicalize_surrogate_pairs(dest) } else { diff --git a/crates/perry-runtime/src/string/mod.rs b/crates/perry-runtime/src/string/mod.rs index 24fb6f64e4..fceb76cd36 100644 --- a/crates/perry-runtime/src/string/mod.rs +++ b/crates/perry-runtime/src/string/mod.rs @@ -135,6 +135,8 @@ mod trim_tests; /// end of an exact-sized payload. Unix-only (needs `mmap` + `mprotect`). #[cfg(all(test, unix))] mod tests_guard_page; +#[cfg(test)] +mod tests_validated_flag; // Explicit named re-exports — preserve the original `crate::string::*` // surface 1:1. NO glob re-exports. @@ -262,6 +264,17 @@ pub const STRING_FLAG_HAS_LONE_SURROGATES: u32 = 1; /// byte-level escape scan. String-producing mutations do not propagate this /// provenance bit unless they independently prove the resulting payload. pub(crate) const STRING_FLAG_JSON_ESCAPE_FREE: u32 = 1 << 1; +/// This exact header's payload was fully validated as generalized WTF-8 and its +/// `utf16_len` found exact, so a RegExp can bind it again in constant work +/// (`perex::binding::BoundSubject::new_counted`) instead of decoding it (#10166). +/// +/// Only the regex subject binding sets it, after a full validation succeeds. It +/// describes one payload, so it must never reach another string: +/// `init_string_header` strips it from every constructed string, and the +/// in-place writers (`js_string_append`, `js_string_append_chain`) clear it on +/// the destination they change. A validated header is already shared +/// (`js_string_addref`), so it is never itself mutated in place afterwards. +pub(crate) const STRING_FLAG_WTF8_VALIDATED: u32 = 1 << 2; /// A static empty string that can be used as a safe fallback for null pointers. /// Has utf16_len=0, byte_len=0, capacity=0, refcount=0, flags=0 (shared). @@ -812,7 +825,9 @@ pub(crate) unsafe fn init_string_header( (*ptr).byte_len = byte_len; (*ptr).capacity = capacity; (*ptr).refcount = refcount; - (*ptr).flags = flags; + // A new header never inherits its source's validation (#10166), however a + // caller computed `flags`. + (*ptr).flags = flags & !STRING_FLAG_WTF8_VALIDATED; } #[inline] diff --git a/crates/perry-runtime/src/string/tests_validated_flag.rs b/crates/perry-runtime/src/string/tests_validated_flag.rs new file mode 100644 index 0000000000..aa91667edc --- /dev/null +++ b/crates/perry-runtime/src/string/tests_validated_flag.rs @@ -0,0 +1,101 @@ +//! `STRING_FLAG_WTF8_VALIDATED` describes one header's payload (#10166). A +//! RegExp trusts it to bind in constant work without decoding, so it must never +//! reach any other string: a wrong bit gives wrong answers or a panic. +use super::*; + +fn heap(text: &str) -> *mut StringHeader { + js_string_from_bytes(text.as_ptr(), text.len() as u32) +} + +/// A shared heap string marked the way the regex binding marks one. +fn validated(text: &str) -> *mut StringHeader { + let s = heap(text); + unsafe { + (*s).refcount = 0; + (*s).flags |= STRING_FLAG_WTF8_VALIDATED; + } + s +} + +fn carries(s: *const StringHeader) -> bool { + unsafe { (*s).flags & STRING_FLAG_WTF8_VALIDATED != 0 } +} + +fn assert_not_inherited(name: &str, source: *const StringHeader, derived: *const StringHeader) { + if derived != source { + assert!( + !carries(derived), + "{name} copied the validation of its source" + ); + } +} + +#[test] +fn string_validated_flag_is_never_inherited_by_a_derived_string() { + let v = validated("abcdef"); + let w = validated("xyz"); + assert!(carries(v) && carries(w)); + assert_not_inherited("slice", v, js_string_slice(v, 1, 4)); + assert_not_inherited("substring", v, js_string_substring(v, 0, 2)); + assert_not_inherited("trim", v, js_string_trim(validated(" abc "))); + assert_not_inherited("concat", v, js_string_concat(v, w)); + assert_not_inherited("repeat", v, js_string_repeat(v, 3.0)); + assert_not_inherited("padStart", v, js_string_pad_start(v, 12.0, w)); + assert_not_inherited("toUpperCase", v, crate::string::js_string_to_upper_case(v)); + let array = crate::array::js_array_alloc(2); + let array = crate::array::js_array_push_f64(array, crate::value::js_nanbox_string(v as i64)); + let array = crate::array::js_array_push_f64(array, crate::value::js_nanbox_string(w as i64)); + assert_not_inherited( + "join", + v, + crate::array::js_array_join(array, validated(",")), + ); + // Constructors that take a caller-computed flags word, passed the source's + // whole word on purpose: the funnel must still strip the validation. + let whole = unsafe { (*v).flags }; + assert_not_inherited("string_copy_range", v, string_copy_range(v, 0, 3, 3, whole)); + assert_not_inherited( + "js_string_from_bytes_known_utf16", + v, + js_string_from_bytes_known_utf16(b"abc".as_ptr(), 3, 3, whole), + ); +} + +#[test] +fn string_in_place_append_clears_the_destinations_validation() { + // A unique destination with spare capacity is appended in place. It cannot + // normally be validated (validation happens on shared strings); mark it + // anyway to prove the writers clear the bit when the payload changes. + let dest = js_string_from_bytes_with_capacity(b"ab".as_ptr(), 2, 64); + let piece = validated("cd"); + unsafe { + (*dest).refcount = 1; + (*dest).flags |= STRING_FLAG_WTF8_VALIDATED; + } + let appended = js_string_append(dest, piece); + assert_eq!(appended, dest, "the test needs the in-place path"); + assert!( + !carries(appended), + "js_string_append kept a stale validation" + ); + + let chain_dest = js_string_from_bytes_with_capacity(b"ab".as_ptr(), 2, 64); + unsafe { + (*chain_dest).refcount = 1; + (*chain_dest).flags |= STRING_FLAG_WTF8_VALIDATED; + } + let parts = [ + crate::value::js_nanbox_string(chain_dest as i64), + crate::value::js_nanbox_string(validated("cd") as i64), + crate::value::js_nanbox_string(validated("ef") as i64), + ]; + let chained = js_string_append_chain(parts.as_ptr(), 3); + assert_eq!( + chained, chain_dest, + "the test needs the in-place chain path" + ); + assert!( + !carries(chained), + "js_string_append_chain kept a stale validation" + ); +} From ca29ea48837bba3ccd85fed14be0a73e303958f6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 13 Sep 2026 10:18:57 +0000 Subject: [PATCH 2/3] changelog: add fragment for #10183 Claude-Session: https://claude.ai/code/session_01Da12JXeG5XuVBma5yWp5C9 --- changelog.d/10183-regex-cross-call-rebinding.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 changelog.d/10183-regex-cross-call-rebinding.md diff --git a/changelog.d/10183-regex-cross-call-rebinding.md b/changelog.d/10183-regex-cross-call-rebinding.md new file mode 100644 index 0000000000..ad30bdc592 --- /dev/null +++ b/changelog.d/10183-regex-cross-call-rebinding.md @@ -0,0 +1,3 @@ +### Performance + +- **A RegExp search per JavaScript call binds its program and subject in constant work** (#10166). JS-level `exec` and `test` loops, `matchAll` iteration and `search` used to decode the whole string and revalidate the whole program on every call, so a loop over one string did quadratic work and each `.test()` paid a full program validation. A compiled program now keeps a small witness of its validation, and a string remembers that its bytes validated, both as plain data with no extra garbage-collector work. Non-ASCII subjects still seek from the nearer end once per call. Requires `perex` 0.1.3. From acfd56dd955577c71ec7a631c5b8589c20d9e3e6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 13 Sep 2026 11:29:18 +0000 Subject: [PATCH 3/3] chore: bump workspace version to 0.5.1552 Claude-Session: https://claude.ai/code/session_01Da12JXeG5XuVBma5yWp5C9 --- CLAUDE.md | 2 +- Cargo.lock | 162 ++++++++++++++++++++++++++--------------------------- Cargo.toml | 2 +- 3 files changed, 83 insertions(+), 83 deletions(-) diff --git a/CLAUDE.md b/CLAUDE.md index b434e7c262..7bec8e9cd0 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -8,7 +8,7 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co Perry is a native TypeScript compiler written in Rust that compiles TypeScript source code directly to native executables. It uses SWC for TypeScript parsing and LLVM for code generation. -**Current Version:** 0.5.1551 +**Current Version:** 0.5.1552 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index 09767ca4f1..c55de3dccc 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5690,7 +5690,7 @@ checksum = "060b4682849d20ebcba05d68f9584a1bba20af4b7838c688cfb37af572f562f4" [[package]] name = "perry" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "anyhow", "base64 0.22.1", @@ -5754,7 +5754,7 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "perry-dispatch", "serde", @@ -5762,7 +5762,7 @@ dependencies = [ [[package]] name = "perry-audio-miniaudio" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "cc", "libc", @@ -5771,7 +5771,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "aho-corasick", "anyhow", @@ -5789,7 +5789,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "anyhow", "perry-hir", @@ -5797,7 +5797,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "anyhow", "perry-hir", @@ -5805,7 +5805,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "anyhow", "perry-dispatch", @@ -5814,7 +5814,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "anyhow", "perry-hir", @@ -5822,7 +5822,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "anyhow", "base64 0.22.1", @@ -5834,7 +5834,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "anyhow", "perry-hir", @@ -5842,7 +5842,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "anyhow", "async-trait", @@ -5870,14 +5870,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "serde", "serde_json", @@ -5885,7 +5885,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1551" +version = "0.5.1552" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5896,7 +5896,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "anyhow", "clap", @@ -5911,7 +5911,7 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "block2", "objc2", @@ -5921,7 +5921,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "argon2", "perry-ffi", @@ -5930,7 +5930,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "perry-ffi", "reqwest", @@ -5939,7 +5939,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "bcrypt", "perry-ffi", @@ -5947,7 +5947,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "perry-ffi", "rusqlite", @@ -5955,7 +5955,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "perry-ffi", "scraper", @@ -5963,7 +5963,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "perry-ffi", "perry-runtime", @@ -5971,7 +5971,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "chrono", "cron", @@ -5981,7 +5981,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "chrono", "perry-ffi", @@ -5989,7 +5989,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "perry-ffi", "rust_decimal", @@ -5997,7 +5997,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "perry-ffi", "serde_json", @@ -6005,7 +6005,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "perry-ffi", "rand 0.10.2", @@ -6013,7 +6013,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "perry-ffi", "perry-runtime", @@ -6021,14 +6021,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "bytes", "http-body-util", @@ -6046,7 +6046,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "bytes", "lazy_static", @@ -6059,7 +6059,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "base64 0.22.1", "bytes", @@ -6091,7 +6091,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "lazy_static", "perry-ffi", @@ -6101,7 +6101,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "base64 0.22.1", "jsonwebtoken", @@ -6112,7 +6112,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "lru", "perry-ffi", @@ -6121,7 +6121,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "chrono", "perry-ffi", @@ -6129,7 +6129,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "bson", "futures-util", @@ -6141,7 +6141,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "chrono", "perry-ffi", @@ -6153,7 +6153,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "nanoid", "perry-ffi", @@ -6162,7 +6162,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "bytes", "perry-ffi", @@ -6177,7 +6177,7 @@ dependencies = [ [[package]] name = "perry-ext-node-forge" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "const-oid 0.10.2", "der 0.8.1", @@ -6196,7 +6196,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "lettre", "perry-ffi", @@ -6206,7 +6206,7 @@ dependencies = [ [[package]] name = "perry-ext-parcel-watcher" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "notify", "perry-ffi", @@ -6218,7 +6218,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "perry-ffi", "printpdf", @@ -6226,7 +6226,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "perry-ffi", "sqlx", @@ -6235,7 +6235,7 @@ dependencies = [ [[package]] name = "perry-ext-qs" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "perry-ffi", "perry-runtime", @@ -6244,7 +6244,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "governor", "perry-ffi", @@ -6252,7 +6252,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "fast_image_resize", "image", @@ -6263,7 +6263,7 @@ dependencies = [ [[package]] name = "perry-ext-streams" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "lazy_static", "perry-ffi", @@ -6272,7 +6272,7 @@ dependencies = [ [[package]] name = "perry-ext-typescript" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "anyhow", "perry-ffi", @@ -6292,7 +6292,7 @@ dependencies = [ [[package]] name = "perry-ext-undici" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "perry-ffi", "perry-runtime", @@ -6301,7 +6301,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "perry-ffi", "uuid", @@ -6309,7 +6309,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "perry-ffi", "perry-validation", @@ -6318,7 +6318,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "futures-util", "lazy_static", @@ -6331,7 +6331,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "brotli", "flate2", @@ -6341,7 +6341,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "dashmap 6.2.1", "once_cell", @@ -6351,7 +6351,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "anyhow", "perry-api-manifest", @@ -6372,11 +6372,11 @@ dependencies = [ [[package]] name = "perry-native-registration" -version = "0.5.1551" +version = "0.5.1552" [[package]] name = "perry-parser" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "anyhow", "perry-diagnostics", @@ -6390,7 +6390,7 @@ dependencies = [ [[package]] name = "perry-perex" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "perex", "regex", @@ -6398,7 +6398,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "ahash", "anyhow", @@ -6458,14 +6458,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "aes 0.8.4", "aes 0.9.1", @@ -6560,14 +6560,14 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "anyhow", "perry-hir", @@ -6576,7 +6576,7 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "perry-ffi", "perry-ui-model", @@ -6584,7 +6584,7 @@ dependencies = [ [[package]] name = "perry-ui-android" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "base64 0.22.1", "itoa", @@ -6602,7 +6602,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "rand 0.10.2", "serde", @@ -6612,7 +6612,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "base64 0.22.1", "cairo-rs 0.22.9", @@ -6635,7 +6635,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "base64 0.22.1", "block2", @@ -6652,7 +6652,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "base64 0.22.1", "block2", @@ -6669,7 +6669,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1551" +version = "0.5.1552" [[package]] name = "perry-ui-test" @@ -6680,11 +6680,11 @@ dependencies = [ [[package]] name = "perry-ui-testkit" -version = "0.5.1551" +version = "0.5.1552" [[package]] name = "perry-ui-tvos" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "base64 0.22.1", "block2", @@ -6701,7 +6701,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "base64 0.22.1", "block2", @@ -6718,7 +6718,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "block2", "libc", @@ -6732,7 +6732,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "base64 0.22.1", "libc", @@ -6751,7 +6751,7 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "base64 0.22.1", "libc", @@ -6764,7 +6764,7 @@ dependencies = [ [[package]] name = "perry-updater" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "anyhow", "base64 0.22.1", @@ -6780,7 +6780,7 @@ dependencies = [ [[package]] name = "perry-validation" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "idna", "regex", @@ -6790,7 +6790,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1551" +version = "0.5.1552" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index db4cd20d6f..81fc773085 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -338,7 +338,7 @@ codegen-units = 1 codegen-units = 1 [workspace.package] -version = "0.5.1551" +version = "0.5.1552" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry"