diff --git a/CLAUDE.md b/CLAUDE.md index a3d019fcab..d4ceaeab29 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.1582 +**Current Version:** 0.5.1583 ## TypeScript Parity Status diff --git a/Cargo.lock b/Cargo.lock index f9c816f59c..395a1bc50a 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -5617,13 +5617,13 @@ checksum = "9b4f627cb1b25917193a259e49bdad08f671f8d9708acfd5fe0a8c1455d87220" [[package]] name = "perex" -version = "0.1.4" +version = "0.1.7" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "1542e48011813fbdf3c075da4a4ed53ee93c816eef62e36eb5064a6fd2be10a5" +checksum = "fc61f41aef38c94e922057977bcb33bf185ab42242188719991ecfdc0fa1fe6b" [[package]] name = "perry" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "anyhow", "base64 0.22.1", @@ -5687,7 +5687,7 @@ dependencies = [ [[package]] name = "perry-api-manifest" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "perry-dispatch", "serde", @@ -5695,7 +5695,7 @@ dependencies = [ [[package]] name = "perry-audio-miniaudio" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "cc", "libc", @@ -5704,7 +5704,7 @@ dependencies = [ [[package]] name = "perry-codegen" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "aho-corasick", "anyhow", @@ -5721,7 +5721,7 @@ dependencies = [ [[package]] name = "perry-codegen-arkts" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "anyhow", "perry-hir", @@ -5729,7 +5729,7 @@ dependencies = [ [[package]] name = "perry-codegen-glance" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "anyhow", "perry-hir", @@ -5737,7 +5737,7 @@ dependencies = [ [[package]] name = "perry-codegen-js" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "anyhow", "perry-dispatch", @@ -5746,7 +5746,7 @@ dependencies = [ [[package]] name = "perry-codegen-swiftui" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "anyhow", "perry-hir", @@ -5754,7 +5754,7 @@ dependencies = [ [[package]] name = "perry-codegen-wasm" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "anyhow", "base64 0.22.1", @@ -5766,7 +5766,7 @@ dependencies = [ [[package]] name = "perry-codegen-wear-tiles" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "anyhow", "perry-hir", @@ -5774,7 +5774,7 @@ dependencies = [ [[package]] name = "perry-container-compose" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "async-trait", "clap", @@ -5798,14 +5798,14 @@ dependencies = [ [[package]] name = "perry-container-e2e" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "anyhow", ] [[package]] name = "perry-diagnostics" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "serde", "serde_json", @@ -5813,7 +5813,7 @@ dependencies = [ [[package]] name = "perry-dispatch" -version = "0.5.1582" +version = "0.5.1583" [[package]] name = "perry-doc-fixture-my-bindings" @@ -5824,7 +5824,7 @@ dependencies = [ [[package]] name = "perry-doc-tests" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "anyhow", "clap", @@ -5839,7 +5839,7 @@ dependencies = [ [[package]] name = "perry-ext-ads" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "block2", "objc2", @@ -5849,7 +5849,7 @@ dependencies = [ [[package]] name = "perry-ext-argon2" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "argon2", "perry-ffi", @@ -5858,7 +5858,7 @@ dependencies = [ [[package]] name = "perry-ext-axios" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "perry-ffi", "reqwest", @@ -5867,7 +5867,7 @@ dependencies = [ [[package]] name = "perry-ext-bcrypt" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "bcrypt", "perry-ffi", @@ -5875,7 +5875,7 @@ dependencies = [ [[package]] name = "perry-ext-better-sqlite3" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "perry-ffi", "rusqlite", @@ -5883,7 +5883,7 @@ dependencies = [ [[package]] name = "perry-ext-cheerio" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "perry-ffi", "scraper", @@ -5891,7 +5891,7 @@ dependencies = [ [[package]] name = "perry-ext-commander" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "perry-ffi", "perry-runtime", @@ -5899,7 +5899,7 @@ dependencies = [ [[package]] name = "perry-ext-cron" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "chrono", "cron", @@ -5909,7 +5909,7 @@ dependencies = [ [[package]] name = "perry-ext-dayjs" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "chrono", "perry-ffi", @@ -5917,7 +5917,7 @@ dependencies = [ [[package]] name = "perry-ext-decimal" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "perry-ffi", "rust_decimal", @@ -5925,7 +5925,7 @@ dependencies = [ [[package]] name = "perry-ext-dotenv" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "perry-ffi", "serde_json", @@ -5933,7 +5933,7 @@ dependencies = [ [[package]] name = "perry-ext-ethers" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "perry-ffi", "rand 0.10.2", @@ -5941,7 +5941,7 @@ dependencies = [ [[package]] name = "perry-ext-events" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "perry-ffi", "perry-runtime", @@ -5949,14 +5949,14 @@ dependencies = [ [[package]] name = "perry-ext-exponential-backoff" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "perry-ffi", ] [[package]] name = "perry-ext-fastify" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "bytes", "http-body-util", @@ -5973,7 +5973,7 @@ dependencies = [ [[package]] name = "perry-ext-fetch" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "bytes", "lazy_static", @@ -5986,7 +5986,7 @@ dependencies = [ [[package]] name = "perry-ext-http" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "base64 0.22.1", "bytes", @@ -6018,7 +6018,7 @@ dependencies = [ [[package]] name = "perry-ext-ioredis" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "lazy_static", "perry-ffi", @@ -6028,7 +6028,7 @@ dependencies = [ [[package]] name = "perry-ext-jsonwebtoken" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "base64 0.22.1", "jsonwebtoken", @@ -6039,7 +6039,7 @@ dependencies = [ [[package]] name = "perry-ext-lru-cache" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "lru", "perry-ffi", @@ -6048,7 +6048,7 @@ dependencies = [ [[package]] name = "perry-ext-moment" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "chrono", "perry-ffi", @@ -6056,7 +6056,7 @@ dependencies = [ [[package]] name = "perry-ext-mongodb" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "bson", "futures-util", @@ -6068,7 +6068,7 @@ dependencies = [ [[package]] name = "perry-ext-mysql2" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "chrono", "perry-ffi", @@ -6080,7 +6080,7 @@ dependencies = [ [[package]] name = "perry-ext-nanoid" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "nanoid", "perry-ffi", @@ -6089,7 +6089,7 @@ dependencies = [ [[package]] name = "perry-ext-net" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "bytes", "perry-ffi", @@ -6104,7 +6104,7 @@ dependencies = [ [[package]] name = "perry-ext-node-forge" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "const-oid 0.10.2", "der 0.8.2", @@ -6123,7 +6123,7 @@ dependencies = [ [[package]] name = "perry-ext-nodemailer" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "lettre", "perry-ffi", @@ -6133,7 +6133,7 @@ dependencies = [ [[package]] name = "perry-ext-parcel-watcher" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "notify", "perry-ffi", @@ -6145,7 +6145,7 @@ dependencies = [ [[package]] name = "perry-ext-pdf" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "perry-ffi", "printpdf", @@ -6153,7 +6153,7 @@ dependencies = [ [[package]] name = "perry-ext-pg" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "perry-ffi", "sqlx", @@ -6162,7 +6162,7 @@ dependencies = [ [[package]] name = "perry-ext-qs" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "perry-ffi", "perry-runtime", @@ -6171,7 +6171,7 @@ dependencies = [ [[package]] name = "perry-ext-ratelimit" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "governor", "perry-ffi", @@ -6179,7 +6179,7 @@ dependencies = [ [[package]] name = "perry-ext-sharp" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "fast_image_resize", "image", @@ -6190,7 +6190,7 @@ dependencies = [ [[package]] name = "perry-ext-streams" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "lazy_static", "perry-ffi", @@ -6199,7 +6199,7 @@ dependencies = [ [[package]] name = "perry-ext-typescript" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "anyhow", "perry-ffi", @@ -6219,7 +6219,7 @@ dependencies = [ [[package]] name = "perry-ext-undici" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "perry-ffi", "perry-runtime", @@ -6228,7 +6228,7 @@ dependencies = [ [[package]] name = "perry-ext-uuid" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "perry-ffi", "uuid", @@ -6236,7 +6236,7 @@ dependencies = [ [[package]] name = "perry-ext-validator" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "perry-ffi", "perry-validation", @@ -6245,7 +6245,7 @@ dependencies = [ [[package]] name = "perry-ext-ws" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "futures-util", "lazy_static", @@ -6258,7 +6258,7 @@ dependencies = [ [[package]] name = "perry-ext-zlib" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "brotli", "flate2", @@ -6268,7 +6268,7 @@ dependencies = [ [[package]] name = "perry-ffi" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "dashmap 6.2.1", "once_cell", @@ -6278,7 +6278,7 @@ dependencies = [ [[package]] name = "perry-hir" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "anyhow", "perry-api-manifest", @@ -6298,11 +6298,11 @@ dependencies = [ [[package]] name = "perry-native-registration" -version = "0.5.1582" +version = "0.5.1583" [[package]] name = "perry-parser" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "anyhow", "perry-diagnostics", @@ -6315,7 +6315,7 @@ dependencies = [ [[package]] name = "perry-perex" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "perex", "regex", @@ -6323,7 +6323,7 @@ dependencies = [ [[package]] name = "perry-runtime" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "ahash", "base64 0.22.1", @@ -6381,14 +6381,14 @@ dependencies = [ [[package]] name = "perry-runtime-static" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "perry-runtime", ] [[package]] name = "perry-stdlib" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "aes 0.8.4", "aes 0.9.1", @@ -6477,21 +6477,21 @@ dependencies = [ [[package]] name = "perry-stdlib-static" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "perry-stdlib", ] [[package]] name = "perry-transform" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "perry-hir", ] [[package]] name = "perry-ui" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "dirs", "perry-ffi", @@ -6501,7 +6501,7 @@ dependencies = [ [[package]] name = "perry-ui-android" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "base64 0.22.1", "jni", @@ -6516,7 +6516,7 @@ dependencies = [ [[package]] name = "perry-ui-geisterhand" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "rand 0.10.2", "serde", @@ -6526,7 +6526,7 @@ dependencies = [ [[package]] name = "perry-ui-gtk4" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "base64 0.22.1", "cairo-rs 0.22.9", @@ -6549,7 +6549,7 @@ dependencies = [ [[package]] name = "perry-ui-ios" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "base64 0.22.1", "block2", @@ -6566,7 +6566,7 @@ dependencies = [ [[package]] name = "perry-ui-macos" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "base64 0.22.1", "block2", @@ -6583,7 +6583,7 @@ dependencies = [ [[package]] name = "perry-ui-model" -version = "0.5.1582" +version = "0.5.1583" [[package]] name = "perry-ui-test" @@ -6594,11 +6594,11 @@ dependencies = [ [[package]] name = "perry-ui-testkit" -version = "0.5.1582" +version = "0.5.1583" [[package]] name = "perry-ui-tvos" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "base64 0.22.1", "block2", @@ -6615,7 +6615,7 @@ dependencies = [ [[package]] name = "perry-ui-visionos" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "base64 0.22.1", "block2", @@ -6632,7 +6632,7 @@ dependencies = [ [[package]] name = "perry-ui-watchos" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "block2", "libc", @@ -6646,7 +6646,7 @@ dependencies = [ [[package]] name = "perry-ui-windows" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "base64 0.22.1", "libc", @@ -6665,7 +6665,7 @@ dependencies = [ [[package]] name = "perry-ui-windows-winui" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "base64 0.22.1", "libc", @@ -6678,7 +6678,7 @@ dependencies = [ [[package]] name = "perry-updater" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "anyhow", "base64 0.22.1", @@ -6693,7 +6693,7 @@ dependencies = [ [[package]] name = "perry-validation" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "idna", "regex", @@ -6703,7 +6703,7 @@ dependencies = [ [[package]] name = "perry-wasm-host" -version = "0.5.1582" +version = "0.5.1583" dependencies = [ "wasmi", ] diff --git a/Cargo.toml b/Cargo.toml index 3506d2a1da..4dee45285d 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -338,7 +338,7 @@ codegen-units = 1 codegen-units = 1 [workspace.package] -version = "0.5.1582" +version = "0.5.1583" edition = "2021" license = "MIT" repository = "https://github.com/PerryTS/perry" @@ -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.4" +perex = "0.1.7" hex = "0.4" tempfile = "3" itoa = "1.0" diff --git a/changelog.d/10372-perex-017-lent-scratch.md b/changelog.d/10372-perex-017-lent-scratch.md new file mode 100644 index 0000000000..bc0ade8b62 --- /dev/null +++ b/changelog.d/10372-perex-017-lent-scratch.md @@ -0,0 +1,7 @@ +### Faster + +- `RegExp` calls no longer build their match scratch per call. A `.test()` on a short string costs about a third fewer instructions, and `exec` about a quarter fewer, because the thread lends one scratch cell to each search instead of constructing and moving a fresh one (#10166). + +### Changed + +- The regex engine moves from perex 0.1.4 to 0.1.7. The `v` flag is complete: string members (`[\q{abc|de}]`), the properties of strings such as `\p{RGI_Emoji}`, and set operators such as `[a--b]` all compile and match. A start-anchored pattern now tries only one start. diff --git a/crates/perry-runtime/src/gc/tests/runtime_roots/perex_execution.rs b/crates/perry-runtime/src/gc/tests/runtime_roots/perex_execution.rs index a0f002ebdb..572fa5b9f6 100644 --- a/crates/perry-runtime/src/gc/tests/runtime_roots/perex_execution.rs +++ b/crates/perry-runtime/src/gc/tests/runtime_roots/perex_execution.rs @@ -319,9 +319,32 @@ fn perex_host_failures_release_scratch_and_preserve_consumed_work() { } assert_eq!(memory.live_bytes(), 0); assert_eq!(external_side_live_bytes(), before); - // `[a]` under `v` compiles now that the engine implements the union - // grammar; its set *operators* are what remain unimplemented. - for (pattern, flags, work) in [("(", "", 100_000), ("[a--b]", "v", 100_000), ("a", "", 0)] { + // perex 0.1.7 completes the `v` grammar: set operators compile too, so + // `[a--b]` is a successful difference rather than an Unsupported witness. + { + let program = compile(&scope, "[a--b]", "v"); + let input = subject(&scope, b"ba"); + let result = host::find( + &program, + &input, + 0, + CaptureMode::All, + &mut Budget::new(100_000), + &memory, + 1, + &mut host::poll, + ) + .unwrap() + .unwrap(); + // `a` minus `b`: the `b` at 0 is not a member, the `a` at 1 is. + assert_eq!(result.full, Span::new(1, 2).unwrap()); + } + assert_eq!(memory.live_bytes(), 0); + assert_eq!(external_side_live_bytes(), before); + // Compile failures still release scratch and keep the work they charged. + // Nothing in the `v` grammar reports Unsupported any more, so the arms + // left are a syntax error and an exhausted work budget. + for (pattern, flags, work) in [("(", "", 100_000), ("a", "", 0)] { let input = subject(&scope, pattern.as_bytes()); let mut budget = Budget::new(work); let result = host::compile( @@ -341,14 +364,6 @@ fn perex_host_failures_release_scratch_and_preserve_consumed_work() { result, Err(EngineError::Compile(CompileError::WorkLimit)) )); - } else if flags == "v" { - assert!(matches!( - result, - Err(EngineError::Compile(CompileError::Unsupported { - feature: "Unicode sets", - .. - })) - )); } else { assert!(matches!( result, diff --git a/crates/perry-runtime/src/regex/perex_runtime.rs b/crates/perry-runtime/src/regex/perex_runtime.rs index 0efc08964e..dac658fa58 100644 --- a/crates/perry-runtime/src/regex/perex_runtime.rs +++ b/crates/perry-runtime/src/regex/perex_runtime.rs @@ -159,10 +159,15 @@ impl std::ops::DerefMut for Slots<'_, T, N> { } } -/// 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; +/// Registers a program can have and still search without allocating on the +/// owned path, which builds its buffers per call. Most programs need far +/// fewer: `/^[a-z]+_[0-9]+$/` needs 2. Frames and undo entries start empty and +/// only grow through `rebuffer`, so they are never inline. +const INLINE_REGISTERS: usize = 8; +/// Registers the lent cell holds. This array is allocated once per thread, not +/// per call, so it is sized for the programs a search may bring rather than +/// for what is cheap to move. +const LENT_REGISTERS: usize = 32; /// Capture spans an `exec` result can have and still be read without /// allocating. const INLINE_CAPTURES: usize = 16; @@ -196,6 +201,55 @@ impl ScratchOwner for MatchBuffers<'_> { } } +/// Scratch a thread lends to one search at a time, instead of building an +/// owner per call (#10166). +/// +/// `find_near` built a `MatchBuffers` for every call: a 32-register inline +/// array zeroed and then moved by value into `Search`, which disassembled to a +/// 336-byte `memcpy` at every call and measured as a third of a short +/// `.test()`. Nothing in that scratch depends on the subject, and a search +/// initializes its own live state, so one cell serves every call on the +/// thread. The arrays keep whatever size an earlier call needed, so a loop +/// reaches a steady state that grows nothing and constructs nothing. +/// +/// No GC pointer is ever stored here: registers are subject offsets, and +/// frames and undo entries are the engine's own opaque scratch, exactly as in +/// the owned buffers this replaces (see this module's header). +struct ScratchCell { + registers: [usize; LENT_REGISTERS], + frames: Vec, + undo: Vec, +} + +crate::perry_thread_local! { + /// Borrowed for one search. A nested regex — a replacer callback that runs + /// its own match, or a poll that re-enters — finds the cell borrowed and + /// takes the owned path, so two searches never share slots. This is the + /// runtime half of the guarantee perex's `ScratchOwner for &mut O` makes at + /// compile time within a single frame. + /// + /// `perry_thread_local!` rather than the raw macro (#7469): every search on + /// this thread reads it, so the address belongs in the hot cache instead of + /// costing a `_tlv_get_addr` call — the opposite of what this change is for. + static LENT_SCRATCH: std::cell::RefCell = const { + std::cell::RefCell::new(ScratchCell { + registers: [0; LENT_REGISTERS], + frames: Vec::new(), + undo: Vec::new(), + }) + }; +} + +/// What a lent attempt produced: an answer, or a reason to run the owned path. +enum Lent<'mem> { + Done(Option>, Position), + /// The cell was already borrowed, or the search asked for more frames or + /// undo entries than it holds. The cell has been grown to the requested + /// size, so the next call starts big enough; this call runs the owned path + /// from its entry budget, exactly as it would have without lending. + Fallback, +} + #[derive(Clone, Copy)] pub(crate) enum CaptureMode { /// Test/search need only the full match, without allocating output slots. @@ -243,6 +297,105 @@ pub(crate) fn find<'mem, S: ImmutableSubject>( /// `near` must come from a search or reader over this same binding. Another /// string with an identical layout cannot be detected and would give wrong /// answers, so callers keep a position only as long as the binding it came from. +/// One search over lent scratch. Returns `Fallback` without an answer when the +/// scratch cannot serve this search; the caller then runs the owned path. +#[allow(clippy::too_many_arguments)] +fn find_near_lent<'mem, S: ImmutableSubject>( + resources: &BoundResources<'_, GcProgram<'_>, S>, + registers: usize, + start: usize, + near: Option, + mode: CaptureMode, + budget: &mut Budget, + memory: &'mem MemoryBudget, + quantum: usize, + poll: &mut impl FnMut() -> Result<(), EngineError>, +) -> Result, EngineError> { + LENT_SCRATCH.with(|cell| { + let Ok(mut cell) = cell.try_borrow_mut() else { + return Ok(Lent::Fallback); + }; + let cell = &mut *cell; + // Charged exactly like the owner it replaces: the operation's limit + // sees the slots a search may use, whether or not they were allocated + // for it. The thread keeps the memory; the operation only borrows it. + let bytes = registers + .checked_mul(std::mem::size_of::()) + .and_then(|n| { + n.checked_add( + cell.frames + .len() + .checked_mul(std::mem::size_of::())?, + ) + }) + .and_then(|n| n.checked_add(cell.undo.len().checked_mul(std::mem::size_of::())?)) + .ok_or(StorageError::Limit)?; + let _charge = Charge::new(memory, bytes)?; + let scratch = Scratch { + registers: &mut cell.registers[..registers], + frames: &mut cell.frames[..], + undo: &mut cell.undo[..], + }; + poll()?; + let mut search = match near { + Some(near) => Search::new_near(resources, start, near, scratch, *budget), + None => Search::new(resources, start, scratch, *budget), + } + .map_err(search_error)?; + loop { + let result = search.advance(quantum); + *budget = Budget::new(search.remaining_work()); + match result { + Ok(Progress::NoMatch) => return Ok(Lent::Done(None, search.position())), + Ok(Progress::Matched) => { + let full = search + .capture(0) + .map_err(EngineError::Execution)? + .ok_or(EngineError::Execution(ExecError::InvalidProgram))?; + let captures = match mode { + CaptureMode::Full => None, + CaptureMode::All => { + poll()?; + let mut output = Slots::new(memory, search.capture_count())?; + search + .copy_captures(&mut output) + .map_err(EngineError::Execution)?; + Some(output) + } + }; + let position = search.position(); + return Ok(Lent::Done(Some(Match { full, captures }), position)); + } + Ok(Progress::Pending) => poll()?, + Err(SearchError::Execution(ExecError::Frames | ExecError::Undo)) => { + // Grow the cell for the next call and let this one run the + // owned path, which charges the whole search once from the + // caller's entry budget. Rebuffering in place would need a + // second borrow of the cell the search already holds. + let required = search.required_scratch(); + drop(search); + if crate::hot_diag::regex_on() { + crate::hot_diag::regex_with(|d| d.perex_scratch_grows += 1); + } + let frames = required + .frames + .max(cell.frames.len().saturating_mul(2)) + .max(8); + let undo = required.undo.max(cell.undo.len().saturating_mul(2)).max(16); + if frames > cell.frames.len() { + cell.frames.resize(frames, Frame::default()); + } + if undo > cell.undo.len() { + cell.undo.resize(undo, Undo::default()); + } + return Ok(Lent::Fallback); + } + Err(error) => return Err(search_error(error)), + } + } + }) +} + pub(crate) fn find_near<'mem, S: ImmutableSubject>( program: &BoundProgram>, subject: &BoundSubject, @@ -269,6 +422,19 @@ pub(crate) fn find_near<'mem, S: ImmutableSubject>( frames: 0, undo: 0, }; + // Lend the thread's scratch first: a search that fits it constructs and + // moves nothing (#10166). Anything the cell cannot serve falls through to + // the owned buffers below with the budget it entered on. + if registers <= LENT_REGISTERS { + let entry = *budget; + match find_near_lent( + &resources, registers, start, near, mode, budget, memory, quantum, poll, + )? { + Lent::Done(found, position) => return Ok((found, position)), + Lent::Fallback => *budget = entry, + } + } + poll()?; let buffers = MatchBuffers::new(memory, size)?; let mut search = match near { diff --git a/scripts/gc_runtime_root_holders.json b/scripts/gc_runtime_root_holders.json index 2d8de2811a..749d68732e 100644 --- a/scripts/gc_runtime_root_holders.json +++ b/scripts/gc_runtime_root_holders.json @@ -955,6 +955,12 @@ "verdict": "test_only", "why": "#10164: #[cfg(test)] Cell counting how many searches resumed from a recorded cross-call position, so tests can tell the position was used. A count, never an address, and absent from shipped binaries." }, + { + "file": "crates/perry-runtime/src/regex/perex_runtime.rs", + "name": "LENT_SCRATCH", + "verdict": "not_a_gc_pointer", + "why": "Per-thread match scratch lent to one search at a time (#10166): registers are subject offsets in UTF-16 units, frames and undo entries are perex's own opaque evaluator scratch. No GC pointer is ever stored, exactly as for the per-call MatchBuffers it replaces (this module's header and perex_memory.rs). The cell is borrowed for one search and holds no program, subject or result: those stay in GcProgram/HeapSubject roots and in the operation's MemoryBudget-charged slots." + }, { "file": "crates/perry-runtime/src/regex/perex_split.rs", "name": "FORWARD_SPLITS",