From 08413d0e9db9708691aefaee97d65480cab246a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 13 Sep 2026 17:23:51 +0200 Subject: [PATCH 1/3] perf(regex): skip the unobservable exec lookup, keep small match scratch inline, copy ASCII captures in one pass (#10166) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Instruction attribution on the #10166 probes put a hoisted short-string `RegExp.prototype.test` at 23.7k instructions per call and `exec` with captures at 40.2k, with the engine itself about 12% and 7% of those. - `perex_dispatch::execute` performed `Get(R, "exec")` through the generic property path on every call, about half of each `test`. When the receiver is a RegExp and `regexp_view_uses_builtin` proves its own properties, prototype and `exec` are the untouched builtins, that Get reaches the builtin without running anything, so it is skipped. Any other receiver takes the Get. - `find_near` heap-allocated match registers per call and noted them to the collector inside a try frame, about a tenth of each `test`. `Slots` holds up to 32 registers and 16 capture spans inline; frames and undo start empty and still grow through `rebuffer` onto heap buffers. Inline slots are charged to the operation's memory limit exactly as a buffer of the same count is, so the limit and peak accounting are unchanged. - `copy_span_near` decoded each capture unit by unit through `BoundSpan` and re-encoded it, twice. On an ASCII subject UTF-16 offsets are byte offsets and the bytes are already the output encoding, so the span is copied as one byte range. Other subjects keep the existing path. No Perex change is needed. Tests: `perex_dispatch_skips_the_exec_lookup_only_when_nothing_can_observe_it` counts lookups — none for an untouched RegExp after its first call, and a lookup that runs the override for an own `exec`, a reparented RegExp and a replaced `RegExp.prototype.exec`. `perex_public_exec_captures_agree_across_ inline_and_heap_slots_and_storage` checks every group for inline and heap slot counts, a backtracking alternation that grows frames, unset and empty groups, behind an ASCII and a non-ASCII prefix, under forced evacuation. Three injected faults are caught: dropping the builtin-view check (four dispatch and search tests), truncating large programs into inline slots, and an off-by-one ASCII copy (three capture tests). `perex_` and `regex::` suites: 168 passed. Claude-Session: https://claude.ai/code/session_01RJkA4Fhqz9J5F5fzDk5HWv --- changelog.d/regex-per-call.md | 6 ++ .../gc/tests/runtime_roots/perex_dispatch.rs | 70 ++++++++++++++++ .../gc/tests/runtime_roots/perex_public.rs | 56 +++++++++++++ .../perry-runtime/src/regex/perex_dispatch.rs | 67 ++++++++++++---- .../perry-runtime/src/regex/perex_memory.rs | 22 ++++++ .../perry-runtime/src/regex/perex_runtime.rs | 79 ++++++++++++++++--- .../perry-runtime/src/regex/perex_strings.rs | 73 +++++++++++++++++ scripts/gc_runtime_root_holders.json | 6 ++ 8 files changed, 353 insertions(+), 26 deletions(-) create mode 100644 changelog.d/regex-per-call.md diff --git a/changelog.d/regex-per-call.md b/changelog.d/regex-per-call.md new file mode 100644 index 0000000000..0a5b89580f --- /dev/null +++ b/changelog.d/regex-per-call.md @@ -0,0 +1,6 @@ +Made each `RegExp.prototype.test` and `exec` call cheaper. When a RegExp, its +prototype and `exec` are the untouched builtins, the unobservable `exec` +property lookup is skipped. Match scratch for programs of up to 32 registers +and 16 captures is held inline instead of heap-allocated per call, and +captures of an ASCII string are copied as one byte range instead of being +decoded and re-encoded twice. diff --git a/crates/perry-runtime/src/gc/tests/runtime_roots/perex_dispatch.rs b/crates/perry-runtime/src/gc/tests/runtime_roots/perex_dispatch.rs index 2c4438effd..d1988a7019 100644 --- a/crates/perry-runtime/src/gc/tests/runtime_roots/perex_dispatch.rs +++ b/crates/perry-runtime/src/gc/tests/runtime_roots/perex_dispatch.rs @@ -460,3 +460,73 @@ fn perex_dispatch_proxy_apply_getter_and_nested_trap_survive_movement() { ); } } + +#[test] +fn perex_dispatch_skips_the_exec_lookup_only_when_nothing_can_observe_it() { + // The guard holds the global side-table lock, which the prototype edits + // below need; taking it again here would deadlock. + let _guard = CopyingNurseryTestGuard::new(0); + let _triggers = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + super::perex_public::register_host_roots(); + let scope = RuntimeHandleScope::new(); + let miss = text(&scope, b"x"); + let hit = text(&scope, b"NEVER"); + let lookups = || dispatch::EXEC_LOOKUPS.with(Cell::get); + let yes = function(&scope, return_this as *const u8, 1); + + // Untouched: the builtin runs with no lookup, and still answers. The + // realm records RegExp.prototype's canonical site on first use, so the + // first call on a thread may take the lookup; none after it does. + let plain = regex(&scope); + assert!(!test(&plain, &miss)); + let before = lookups(); + for _ in 0..10 { + assert!(!test(&plain, &miss)); + assert!(test(&plain, &hit)); + } + assert_eq!( + lookups(), + before, + "an untouched RegExp needs no exec lookup" + ); + + // An own exec is found by the lookup, and runs. + let own = regex(&scope); + put(&own, b"exec", &yes); + let before = lookups(); + assert!(test(&own, &miss), "an own exec override must run"); + assert!(lookups() > before); + + // A reparented RegExp resolves exec on its new prototype. + let reparented = regex(&scope); + let parent = object(&scope); + put(&parent, b"exec", &yes); + assert_eq!( + crate::proxy::js_reflect_set_prototype_of( + reparented.get_nanbox_f64(), + parent.get_nanbox_f64() + ) + .to_bits(), + crate::value::TAG_TRUE + ); + let before = lookups(); + assert!( + test(&reparented, &miss), + "the new prototype's exec must run" + ); + assert!(lookups() > before); + + // Replacing RegExp.prototype.exec reaches every RegExp, including a fresh + // one; restoring it restores the skipped lookup. + let proto = scope.root_nanbox_f64(crate::object::builtin_prototype_value("RegExp")); + let original = scope.root_nanbox_f64(api::finish(dispatch::get(&proto, b"exec"))); + put(&proto, b"exec", &yes); + let fresh = regex(&scope); + let before = lookups(); + assert!(test(&fresh, &miss), "a replaced prototype exec must run"); + assert!(lookups() > before); + put(&proto, b"exec", &original); + let before = lookups(); + assert!(!test(&fresh, &miss)); + assert_eq!(lookups(), before); +} diff --git a/crates/perry-runtime/src/gc/tests/runtime_roots/perex_public.rs b/crates/perry-runtime/src/gc/tests/runtime_roots/perex_public.rs index a4f1cf2165..1b9dd2922e 100644 --- a/crates/perry-runtime/src/gc/tests/runtime_roots/perex_public.rs +++ b/crates/perry-runtime/src/gc/tests/runtime_roots/perex_public.rs @@ -348,3 +348,59 @@ fn perex_public_nonglobal_test_propagates_lastindex_coercion_throw() { assert_eq!(RuntimeHandleScope::active_len_for_tests(), roots); assert_ne!(address::(&input), before); } + +#[test] +fn perex_public_exec_captures_agree_across_inline_and_heap_slots_and_storage() { + let _guard = CopyingNurseryTestGuard::new(0); + let _scan = ConservativeScanDisabledGuard::new(); + let _triggers = GcTriggerThresholdTestGuard::suppress_automatic_triggers(); + let _force = ForcedEvacuationTestGuard::on(); + register_host_roots(); + let twenty_groups = "(a)".repeat(20); + let twenty_a = "a".repeat(20); + // A few captures fit the inline slots. Twenty groups need 42 registers and + // 21 capture spans, past both inline limits. The alternation backtracks, + // growing frames through a rebuffer. `None` is an unset group. + let cases: [(&str, &str, Vec>); 4] = [ + ("(a)(b)?c", "acz", vec![Some("ac"), Some("a"), None]), + (&twenty_groups, &twenty_a, { + let mut all = vec![Some(twenty_a.as_str())]; + all.extend(std::iter::repeat_n(Some("a"), 20)); + all + }), + ( + "(a|ab)(c|bcd)(d*)", + "abcd", + vec![Some("abcd"), Some("a"), Some("bcd"), Some("")], + ), + ("(x*)$", "abc", vec![Some(""), Some("")]), + ]; + for (pattern, tail, expected) in cases { + // The same match behind an ASCII prefix and a non-ASCII one, so both + // the byte copy and the unit-by-unit copy produce these captures. + for prefix in ["!", "\u{e9}"] { + let scope = RuntimeHandleScope::new(); + let receiver = regex(&scope, pattern, ""); + let subject = format!("{prefix}{tail}"); + let input = text(&scope, subject.as_bytes()); + let result = exec(&receiver, &input); + assert!(!result.is_null(), "/{pattern}/ must match {subject:?}"); + let result = scope.root_raw_mut_ptr(result); + for (index, want) in expected.iter().enumerate() { + let value = item(&result, index as u32); + match want { + None => assert_eq!( + value.to_bits(), + TAG_UNDEFINED, + "/{pattern}/ over {subject:?}: group {index} must be unset" + ), + Some(want) => assert_eq!( + bytes(value), + want.as_bytes(), + "/{pattern}/ over {subject:?}: group {index}" + ), + } + } + } + } +} diff --git a/crates/perry-runtime/src/regex/perex_dispatch.rs b/crates/perry-runtime/src/regex/perex_dispatch.rs index e6f17c5a43..9be7cf0f25 100644 --- a/crates/perry-runtime/src/regex/perex_dispatch.rs +++ b/crates/perry-runtime/src/regex/perex_dispatch.rs @@ -94,10 +94,17 @@ pub(crate) fn call_one( result } +#[cfg(test)] +thread_local! { + /// `Get(R, "exec")` lookups `execute` performed on this thread. + pub(crate) static EXEC_LOOKUPS: std::cell::Cell = const { std::cell::Cell::new(0) }; +} + /// RegExpExec with operation-owned limits. Lookup happens on every iteration; /// a callback may replace exec or recompile the receiver before the next one. /// Only the known builtin may omit materialization for a boolean test. -/// `reuse` is consulted only on the builtin path, after the observable lookup. +/// `reuse` is consulted only on the builtin path, after the lookup or after +/// proving the lookup would reach the builtin without running anything. pub(crate) fn execute( receiver: &RuntimeHandle<'_>, input: &RuntimeHandle<'_>, @@ -111,7 +118,44 @@ pub(crate) fn execute( require_object(receiver.get_nanbox_f64())?; input.with_mut_ptr::(|input| crate::string::js_string_addref(input)); let scope = RuntimeHandleScope::new(); - let method = scope.root_nanbox_f64(get(receiver, b"exec")?); + // A RegExp whose own properties, prototype and `exec` are the untouched + // builtins reaches the builtin exec without running any code, so the Get + // is unobservable and is skipped. Through the generic property path it was + // about half of every `test` call (#10166). Anything else takes the Get. + let receiver_ptr = + crate::value::js_nanbox_get_pointer(receiver.get_nanbox_f64()) as *mut RegExpHeader; + let known_builtin = super::is_valid_regex_ptr(receiver_ptr) + && crate::object::regex_proto_thunks::regexp_view_uses_builtin(receiver.get_nanbox_f64()); + if !known_builtin { + #[cfg(test)] + EXEC_LOOKUPS.with(|lookups| lookups.set(lookups.get() + 1)); + let method = scope.root_nanbox_f64(get(receiver, b"exec")?); + if let Some(result) = execute_override(&scope, &method, receiver, input)? { + return Ok(result); + } + } + let re = crate::value::js_nanbox_get_pointer(receiver.get_nanbox_f64()) as *mut RegExpHeader; + if !super::is_valid_regex_ptr(re) { + return Err(EngineError::Type( + "RegExp builtin exec requires a RegExp receiver", + )); + } + // `execute_with_resources` roots both before it allocates. + input + .with_const_ptr::(|input| { + api::execute_with_resources(re, input, materialize, budget, memory, poll, reuse) + }) + .map(|result| result.map(ExecResult::Builtin)) +} + +/// The observable half of RegExpExec: call a looked-up `exec` that is not the +/// builtin, and check what it returns. `Ok(None)` means the builtin runs. +fn execute_override( + scope: &RuntimeHandleScope, + method: &RuntimeHandle<'_>, + receiver: &RuntimeHandle<'_>, + input: &RuntimeHandle<'_>, +) -> Result>, EngineError> { let callable = crate::proxy::proxy_wraps_callable(method.get_nanbox_f64()); let builtin = crate::object::regex_proto_thunks::is_builtin_regexp_exec(method.get_nanbox_f64()); @@ -119,29 +163,18 @@ pub(crate) fn execute( let argument = scope.root_nanbox_f64( input.with_const_ptr::(|input| js_nanbox_string(input as i64)), ); - let value = call_one(&method, receiver, &argument)?; + let value = call_one(method, receiver, &argument)?; if value.to_bits() == TAG_NULL { - return Ok(None); + return Ok(Some(None)); } if !crate::proxy::reflect_value_is_object(value) { return Err(EngineError::Type( "RegExp exec method must return an object or null", )); } - return Ok(Some(ExecResult::Override(value))); - } - let re = crate::value::js_nanbox_get_pointer(receiver.get_nanbox_f64()) as *mut RegExpHeader; - if !super::is_valid_regex_ptr(re) { - return Err(EngineError::Type( - "RegExp builtin exec requires a RegExp receiver", - )); + return Ok(Some(Some(ExecResult::Override(value)))); } - // `execute_with_resources` roots both before it allocates. - input - .with_const_ptr::(|input| { - api::execute_with_resources(re, input, materialize, budget, memory, poll, reuse) - }) - .map(|result| result.map(ExecResult::Builtin)) + Ok(None) } pub(crate) fn to_string(value: &RuntimeHandle<'_>) -> Result<*mut StringHeader, EngineError> { diff --git a/crates/perry-runtime/src/regex/perex_memory.rs b/crates/perry-runtime/src/regex/perex_memory.rs index 2280608bd5..5a72bbedcc 100644 --- a/crates/perry-runtime/src/regex/perex_memory.rs +++ b/crates/perry-runtime/src/regex/perex_memory.rs @@ -47,6 +47,28 @@ impl MemoryBudget { } } +/// Bytes charged to an operation's limit for storage it holds without +/// allocating, such as match slots kept inline. The limit and peak see them as +/// they would a buffer's; the collector is not told, since nothing is on its +/// heap or the native heap. +pub(crate) struct Charge<'a> { + budget: &'a MemoryBudget, + bytes: usize, +} +impl<'a> Charge<'a> { + pub(crate) fn new(budget: &'a MemoryBudget, bytes: usize) -> Result { + let live = budget.check(bytes)?; + budget.live.set(live); + budget.peak.set(budget.peak.get().max(live)); + Ok(Self { budget, bytes }) + } +} +impl Drop for Charge<'_> { + fn drop(&mut self) { + self.budget.live.set(self.budget.live.get() - self.bytes); + } +} + /// Account a stable native allocation whose GC-bearing slots are separately /// registered with the host's mutable root scanner before this can collect. pub(super) struct Reservation<'a> { diff --git a/crates/perry-runtime/src/regex/perex_runtime.rs b/crates/perry-runtime/src/regex/perex_runtime.rs index 802b2c98b8..1f97ce41ff 100644 --- a/crates/perry-runtime/src/regex/perex_runtime.rs +++ b/crates/perry-runtime/src/regex/perex_runtime.rs @@ -2,7 +2,7 @@ //! is a UTF-16 span. Collection and cancellation occur outside resource views. use super::flags::CanonicalFlags; -use super::perex_memory::{Buffer, MemoryBudget, StorageError}; +use super::perex_memory::{Buffer, Charge, MemoryBudget, StorageError}; use super::perex_owner::{BuildError, GcProgram, OwnerError}; use crate::gc::RuntimeHandleScope; use perex::binding::{ @@ -106,18 +106,79 @@ pub(crate) fn compile<'scope, S: ImmutableSubject>( } } +/// Match slots a search per call needs, held inline when they fit: such a call +/// allocates nothing and notes no external bytes. A heap buffer was about a +/// tenth of every short `test` (#10166). Inline slots are still charged to the +/// operation's limit, exactly as a buffer of the same count is. Past `N` +/// slots, and for any growth, they are a heap buffer as before. +pub(crate) enum Slots<'a, T: Copy + Default, const N: usize> { + /// The slots, how many are in use, and their charge to the limit, which + /// is released when they are dropped. + Inline { + slots: [T; N], + count: usize, + _charge: Charge<'a>, + }, + Heap(Buffer<'a, T>), +} + +impl<'a, T: Copy + Default, const N: usize> Slots<'a, T, N> { + fn new(memory: &'a MemoryBudget, count: usize) -> Result { + if count <= N { + let bytes = count + .checked_mul(std::mem::size_of::()) + .ok_or(StorageError::Limit)?; + let charge = Charge::new(memory, bytes)?; + Ok(Self::Inline { + slots: [T::default(); N], + count, + _charge: charge, + }) + } else { + Buffer::new(memory, count).map(Self::Heap) + } + } +} + +impl std::ops::Deref for Slots<'_, T, N> { + type Target = [T]; + fn deref(&self) -> &[T] { + match self { + Self::Inline { slots, count, .. } => &slots[..*count], + Self::Heap(buffer) => buffer, + } + } +} + +impl std::ops::DerefMut for Slots<'_, T, N> { + fn deref_mut(&mut self) -> &mut [T] { + match self { + Self::Inline { slots, count, .. } => &mut slots[..*count], + Self::Heap(buffer) => buffer, + } + } +} + +/// Registers a program can have and still search without allocating. Frames +/// and undo entries start empty and only grow through `rebuffer`, so they are +/// never inline. +const INLINE_REGISTERS: usize = 32; +/// Capture spans an `exec` result can have and still be read without +/// allocating. +const INLINE_CAPTURES: usize = 16; + struct MatchBuffers<'a> { - registers: Buffer<'a, usize>, - frames: Buffer<'a, Frame>, - undo: Buffer<'a, Undo>, + registers: Slots<'a, usize, INLINE_REGISTERS>, + frames: Slots<'a, Frame, 0>, + undo: Slots<'a, Undo, 0>, } impl<'a> MatchBuffers<'a> { fn new(memory: &'a MemoryBudget, size: ScratchRequirements) -> Result { Ok(Self { - registers: Buffer::new(memory, size.registers)?, - frames: Buffer::new(memory, size.frames)?, - undo: Buffer::new(memory, size.undo)?, + registers: Slots::new(memory, size.registers)?, + frames: Slots::new(memory, size.frames)?, + undo: Slots::new(memory, size.undo)?, }) } } @@ -142,7 +203,7 @@ pub(crate) enum CaptureMode { pub(crate) struct Match<'a> { pub(crate) full: Span, /// None under Full. All retains unset groups and includes group zero. - pub(crate) captures: Option>>, + pub(crate) captures: Option, INLINE_CAPTURES>>, } fn search_error(error: SearchError>) -> EngineError { @@ -225,7 +286,7 @@ pub(crate) fn find_near<'mem, S: ImmutableSubject>( CaptureMode::Full => None, CaptureMode::All => { poll()?; - let mut output = Buffer::new(memory, search.capture_count())?; + let mut output = Slots::new(memory, search.capture_count())?; search .copy_captures(&mut output) .map_err(EngineError::Execution)?; diff --git a/crates/perry-runtime/src/regex/perex_strings.rs b/crates/perry-runtime/src/regex/perex_strings.rs index bdc0c0c70e..110be27f42 100644 --- a/crates/perry-runtime/src/regex/perex_strings.rs +++ b/crates/perry-runtime/src/regex/perex_strings.rs @@ -151,6 +151,9 @@ pub(crate) fn copy_span_near( } .map_err(|e| read_error(e, |never| match never {})) }; + if let Some(output) = copy_ascii_span(subject, span, budget, max_output_bytes, poll)? { + return Ok(output); + } let mut readers = [reader()?, reader()?]; copy_units( Some(span.len()), @@ -166,6 +169,76 @@ pub(crate) fn copy_span_near( ) } +/// The span as one byte copy, when the subject is ASCII: its UTF-16 offsets +/// are then byte offsets, and its bytes are already the output's encoding. +/// Decoding and re-encoding it unit by unit, twice, was most of materializing +/// an `exec` result's captures (#10166). `None` for any other subject. +fn copy_ascii_span( + subject: &BoundSubject>, + span: Span, + budget: &mut Budget, + max_output_bytes: usize, + poll: &mut impl FnMut() -> Result<(), EngineError>, +) -> Result, EngineError> { + let Some(length) = subject + .with_view(|input| input.ascii_bytes().map(|bytes| bytes.len())) + .map_err(EngineError::Subject)? + else { + return Ok(None); + }; + if span.end() > length { + return Err(EngineError::InvalidSpan); + } + let units = span.len(); + let limit = max_output_bytes.min( + u32::MAX as usize - crate::gc::GC_HEADER_SIZE - std::mem::size_of::() - 7, + ); + if units > limit || units > crate::string::MAX_STRING_LENGTH { + return Err(StorageError::Limit.into()); + } + // The same charge a unit-by-unit read of the span makes. + super::perex_runtime::charge(budget, units)?; + poll()?; + let scope = RuntimeHandleScope::new(); + let capacity = units as u32; + let (output, _) = crate::string::string_storage_alloc(capacity); + // The header publishes an empty prefix until the copy below completes. No + // GC occurs before the root. + unsafe { + crate::string::init_string_header(output, 0, 0, capacity, 0, 0); + } + let output = scope.root_string_ptr(output); + output.with_mut_ptr::(|header| { + // Reacquire both bases inside one scope that neither allocates nor + // collects. + subject + .with_view(|input| { + let bytes = input.ascii_bytes().ok_or(EngineError::InvalidSpan)?; + let source = bytes + .get(span.start()..span.end()) + .ok_or(EngineError::InvalidSpan)?; + let data = unsafe { + std::slice::from_raw_parts_mut( + crate::string::string_data(header) as *mut MaybeUninit, + capacity as usize, + ) + }; + for (slot, &byte) in data.iter_mut().zip(source) { + // GC_STORE_AUDIT(POINTER_FREE): ASCII payload bytes of a string under construction. + slot.write(byte); + } + unsafe { + crate::string::init_string_header(header, capacity, capacity, capacity, 0, 0); + } + Ok::<(), EngineError>(()) + }) + .map_err(EngineError::Subject)? + })?; + Ok(Some( + output.with_mut_ptr::(|output| output), + )) +} + /// Two reusable original-string cursors for a sequence of final substrings. /// Each pass retains its own position, so adjacent split pieces do not seek /// repeatedly from the beginning of a non-ASCII input. Only offsets survive GC. diff --git a/scripts/gc_runtime_root_holders.json b/scripts/gc_runtime_root_holders.json index c3ebab9f30..1d4eba15bf 100644 --- a/scripts/gc_runtime_root_holders.json +++ b/scripts/gc_runtime_root_holders.json @@ -823,6 +823,12 @@ "scanner": "regex::regex_header_moved_for_gc (called from gc/types.rs on relocation), regex::regex_header_finalize_for_gc (gc/types.rs per-object finalize), regex::finalize_dead_copied_minor_from_space_regexps (gc/copying_phase.rs) and regex::collect_dead_registered_regexps_post_trace / finalize_collected_dead_regexp (gc/oldgen.rs)", "why": "Address-KEYED owner set, not a root, and the successor of REGEX_POINTERS under the single engine: its map is `usize` header address -> `RegexMetadata { registered_owner: bool }`, so the VALUE holds no heap address at all (source and flags live only in the header's traced string edges, and the compiled program is a traced GC child of the header). The key is rekeyed by `regex_header_moved_for_gc` when a RegExpHeader moves and removed on death by the finalize hook, the copying-minor from-space walk and the full-cycle post-trace walk; it never keeps a header alive. Reached from those GC hooks rather than a registered scanner, so the walk misses it." }, + { + "file": "crates/perry-runtime/src/regex/perex_dispatch.rs", + "name": "EXEC_LOOKUPS", + "verdict": "test_only", + "why": "#[cfg(test)] Cell counting Get(R, \"exec\") lookups taken by `execute`, so a test can assert the lookup is skipped only when unobservable (#10166). It stores a count, never an address, and is absent from shipped binaries." + }, { "file": "crates/perry-runtime/src/regex/perex_position_hint.rs", "name": "HINT_USES", From 905e556347501ae6241026946ac30c1a8547ea76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 13 Sep 2026 17:32:33 +0200 Subject: [PATCH 2/3] changelog: name the fragment for #10212 Claude-Session: https://claude.ai/code/session_01RJkA4Fhqz9J5F5fzDk5HWv --- changelog.d/{regex-per-call.md => 10212-regex-per-call.md} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename changelog.d/{regex-per-call.md => 10212-regex-per-call.md} (100%) diff --git a/changelog.d/regex-per-call.md b/changelog.d/10212-regex-per-call.md similarity index 100% rename from changelog.d/regex-per-call.md rename to changelog.d/10212-regex-per-call.md From 8ff9373d073237f7ca4c76c3bf2824d3c3dd880f Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ralph=20K=C3=BCpper?= Date: Sun, 13 Sep 2026 15:46:50 +0000 Subject: [PATCH 3/3] chore: bump workspace version to 0.5.1558 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 2a074af4fa..d29ed8825d 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.1557 +**Current Version:** 0.5.1558 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index e91294a5ab..f40f875f4f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5690,7 +5690,7 @@ checksum = "1542e48011813fbdf3c075da4a4ed53ee93c816eef62e36eb5064a6fd2be10a5" [[package]] name = "perry" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "anyhow", "base64 0.22.1", @@ -5754,7 +5754,7 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "perry-dispatch", "serde", @@ -5762,7 +5762,7 @@ dependencies = [ [[package]] name = "perry-audio-miniaudio" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "cc", "libc", @@ -5771,7 +5771,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "aho-corasick", "anyhow", @@ -5789,7 +5789,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "anyhow", "perry-hir", @@ -5797,7 +5797,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "anyhow", "perry-hir", @@ -5805,7 +5805,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "anyhow", "perry-dispatch", @@ -5814,7 +5814,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "anyhow", "perry-hir", @@ -5822,7 +5822,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "anyhow", "base64 0.22.1", @@ -5834,7 +5834,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "anyhow", "perry-hir", @@ -5842,7 +5842,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "anyhow", "async-trait", @@ -5870,14 +5870,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "serde", "serde_json", @@ -5885,7 +5885,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1557" +version = "0.5.1558" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5896,7 +5896,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "anyhow", "clap", @@ -5911,7 +5911,7 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "block2", "objc2", @@ -5921,7 +5921,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "argon2", "perry-ffi", @@ -5930,7 +5930,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "perry-ffi", "reqwest", @@ -5939,7 +5939,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "bcrypt", "perry-ffi", @@ -5947,7 +5947,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "perry-ffi", "rusqlite", @@ -5955,7 +5955,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "perry-ffi", "scraper", @@ -5963,7 +5963,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "perry-ffi", "perry-runtime", @@ -5971,7 +5971,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "chrono", "cron", @@ -5981,7 +5981,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "chrono", "perry-ffi", @@ -5989,7 +5989,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "perry-ffi", "rust_decimal", @@ -5997,7 +5997,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "perry-ffi", "serde_json", @@ -6005,7 +6005,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "perry-ffi", "rand 0.10.2", @@ -6013,7 +6013,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "perry-ffi", "perry-runtime", @@ -6021,14 +6021,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "bytes", "http-body-util", @@ -6046,7 +6046,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "bytes", "lazy_static", @@ -6059,7 +6059,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "base64 0.22.1", "bytes", @@ -6091,7 +6091,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "lazy_static", "perry-ffi", @@ -6101,7 +6101,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "base64 0.22.1", "jsonwebtoken", @@ -6112,7 +6112,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "lru", "perry-ffi", @@ -6121,7 +6121,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "chrono", "perry-ffi", @@ -6129,7 +6129,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "bson", "futures-util", @@ -6141,7 +6141,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "chrono", "perry-ffi", @@ -6153,7 +6153,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "nanoid", "perry-ffi", @@ -6162,7 +6162,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "bytes", "perry-ffi", @@ -6177,7 +6177,7 @@ dependencies = [ [[package]] name = "perry-ext-node-forge" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "const-oid 0.10.2", "der 0.8.1", @@ -6196,7 +6196,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "lettre", "perry-ffi", @@ -6206,7 +6206,7 @@ dependencies = [ [[package]] name = "perry-ext-parcel-watcher" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "notify", "perry-ffi", @@ -6218,7 +6218,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "perry-ffi", "printpdf", @@ -6226,7 +6226,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "perry-ffi", "sqlx", @@ -6235,7 +6235,7 @@ dependencies = [ [[package]] name = "perry-ext-qs" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "perry-ffi", "perry-runtime", @@ -6244,7 +6244,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "governor", "perry-ffi", @@ -6252,7 +6252,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "fast_image_resize", "image", @@ -6263,7 +6263,7 @@ dependencies = [ [[package]] name = "perry-ext-streams" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "lazy_static", "perry-ffi", @@ -6272,7 +6272,7 @@ dependencies = [ [[package]] name = "perry-ext-typescript" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "anyhow", "perry-ffi", @@ -6292,7 +6292,7 @@ dependencies = [ [[package]] name = "perry-ext-undici" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "perry-ffi", "perry-runtime", @@ -6301,7 +6301,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "perry-ffi", "uuid", @@ -6309,7 +6309,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "perry-ffi", "perry-validation", @@ -6318,7 +6318,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "futures-util", "lazy_static", @@ -6331,7 +6331,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "brotli", "flate2", @@ -6341,7 +6341,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "dashmap 6.2.1", "once_cell", @@ -6351,7 +6351,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "anyhow", "perry-api-manifest", @@ -6372,11 +6372,11 @@ dependencies = [ [[package]] name = "perry-native-registration" -version = "0.5.1557" +version = "0.5.1558" [[package]] name = "perry-parser" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "anyhow", "perry-diagnostics", @@ -6390,7 +6390,7 @@ dependencies = [ [[package]] name = "perry-perex" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "perex", "regex", @@ -6398,7 +6398,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "ahash", "anyhow", @@ -6458,14 +6458,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "aes 0.8.4", "aes 0.9.1", @@ -6560,14 +6560,14 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "anyhow", "perry-hir", @@ -6576,7 +6576,7 @@ dependencies = [ [[package]] name = "perry-ui" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "perry-ffi", "perry-ui-model", @@ -6584,7 +6584,7 @@ dependencies = [ [[package]] name = "perry-ui-android" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "base64 0.22.1", "itoa", @@ -6602,7 +6602,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "rand 0.10.2", "serde", @@ -6612,7 +6612,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "base64 0.22.1", "cairo-rs 0.22.9", @@ -6635,7 +6635,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "base64 0.22.1", "block2", @@ -6652,7 +6652,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "base64 0.22.1", "block2", @@ -6669,7 +6669,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1557" +version = "0.5.1558" [[package]] name = "perry-ui-test" @@ -6680,11 +6680,11 @@ dependencies = [ [[package]] name = "perry-ui-testkit" -version = "0.5.1557" +version = "0.5.1558" [[package]] name = "perry-ui-tvos" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "base64 0.22.1", "block2", @@ -6701,7 +6701,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "base64 0.22.1", "block2", @@ -6718,7 +6718,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "block2", "libc", @@ -6732,7 +6732,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "base64 0.22.1", "libc", @@ -6751,7 +6751,7 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "base64 0.22.1", "libc", @@ -6764,7 +6764,7 @@ dependencies = [ [[package]] name = "perry-updater" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "anyhow", "base64 0.22.1", @@ -6780,7 +6780,7 @@ dependencies = [ [[package]] name = "perry-validation" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "idna", "regex", @@ -6790,7 +6790,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1557" +version = "0.5.1558" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index 9b3dfc8fa6..33d27e8f9e 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -338,7 +338,7 @@ codegen-units = 1 codegen-units = 1 [workspace.package] -version = "0.5.1557" +version = "0.5.1558" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry"