aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/shirabe-php-shim
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-08-18 01:57:02 +0900
committernsfisis <nsfisis@gmail.com>2026-08-18 01:57:02 +0900
commit6aeda8b237fcbf7a56ca0e8c0fff415477d31d22 (patch)
tree13942695ae0e7c749cfcdb12d5824daf7134c008 /crates/shirabe-php-shim
parentfed0a6e7ac361af9b963c1f62411b1a85478230c (diff)
downloadphp-shirabe-6aeda8b237fcbf7a56ca0e8c0fff415477d31d22.tar.gz
php-shirabe-6aeda8b237fcbf7a56ca0e8c0fff415477d31d22.tar.zst
php-shirabe-6aeda8b237fcbf7a56ca0e8c0fff415477d31d22.zip
refactor(preg): make preg_match_all yield matches per occurrence
PHP's PREG_PATTERN_ORDER is column-oriented, but 7 of the 10 call sites read it row-wise, rebuilding each occurrence by indexing every column at the same offset. Return an iterator of PregMatches instead, which is also what the set-order and offset-capture variants were carrying, so the three functions collapse into one and PregMatchesAll, PregMatchesAllWithOffsets, CaptureKey and preg_match_map! all go away. The offset-capture call sites are served by the new PregMatches get_offset/name_offset accessors. The search stays eager: regex::Captures borrows only the subject, so the matches outlive the pattern resolved for the call, and PHP's preg_match_all is eager too. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Diffstat (limited to 'crates/shirabe-php-shim')
-rw-r--r--crates/shirabe-php-shim/src/preg.rs176
1 files changed, 19 insertions, 157 deletions
diff --git a/crates/shirabe-php-shim/src/preg.rs b/crates/shirabe-php-shim/src/preg.rs
index c4060a34..a9809205 100644
--- a/crates/shirabe-php-shim/src/preg.rs
+++ b/crates/shirabe-php-shim/src/preg.rs
@@ -16,62 +16,6 @@
use indexmap::IndexMap;
use std::sync::{Arc, LazyLock, Mutex};
-#[derive(Debug, Clone, PartialOrd, Ord, PartialEq, Eq, Hash)]
-pub enum CaptureKey {
- ByIndex(usize),
- ByName(String),
-}
-
-/// Defines a newtype over `IndexMap` for one of the `$matches` shapes the `preg_*` functions fill
-/// in.
-macro_rules! preg_match_map {
- ($(#[$attr:meta])* $vis:vis struct $name:ident($key:ty => $value:ty);) => {
- $(#[$attr])*
- #[derive(Debug, Default, Clone, PartialEq, Eq)]
- $vis struct $name(::indexmap::IndexMap<$key, $value>);
-
- impl $name {
- pub fn new() -> Self {
- Self(::indexmap::IndexMap::new())
- }
-
- pub fn clear(&mut self) {
- self.0.clear();
- }
-
- pub fn get<Q>(&self, key: &Q) -> Option<&$value>
- where
- Q: ?Sized + ::std::hash::Hash + ::indexmap::Equivalent<$key>,
- {
- self.0.get(key)
- }
-
- pub fn insert(&mut self, key: $key, value: $value) -> Option<$value> {
- self.0.insert(key, value)
- }
-
- pub fn iter(&self) -> ::indexmap::map::Iter<'_, $key, $value> {
- self.0.iter()
- }
- }
-
- impl IntoIterator for $name {
- type Item = ($key, $value);
- type IntoIter = ::indexmap::map::IntoIter<$key, $value>;
-
- fn into_iter(self) -> Self::IntoIter {
- self.0.into_iter()
- }
- }
-
- impl FromIterator<($key, $value)> for $name {
- fn from_iter<I: IntoIterator<Item = ($key, $value)>>(iter: I) -> Self {
- Self(iter.into_iter().collect())
- }
- }
- };
-}
-
/// A single match's `$matches`: the `regex::Captures` the search produced, read by either the named
/// or the numbered form of a capture group. `'h` is the lifetime of the searched subject, which the
/// group values borrow from.
@@ -96,35 +40,16 @@ impl<'h> PregMatches<'h> {
pub fn name(&self, name: &str) -> Option<&'h str> {
self.caps.name(name).map(|group| group.as_str())
}
-}
-
-preg_match_map! {
- /// `PREG_PATTERN_ORDER` `$matches`: one entry per capture group, holding that group's value
- /// across every match occurrence.
- pub struct PregMatchesAll(CaptureKey => Vec<Option<String>>);
-}
-preg_match_map! {
- /// `PREG_OFFSET_CAPTURE` counterpart of `PregMatchesAll`, pairing each value with the byte
- /// offset it was captured at (`-1` for a group that did not participate).
- pub struct PregMatchesAllWithOffsets(CaptureKey => Vec<(Option<String>, i64)>);
-}
-
-impl PregMatchesAll {
- /// The number PHP's `preg_match_all` returns: every column holds one entry per occurrence.
- pub fn occurrence_count(&self) -> usize {
- self.get(&CaptureKey::ByIndex(0))
- .expect("group 0 is always present")
- .len()
+ /// The byte offset the capture group at `index` starts at, under the same rules as `get`.
+ /// `PREG_OFFSET_CAPTURE` reports a non-participating group at offset `-1`.
+ pub fn get_offset(&self, index: usize) -> Option<usize> {
+ self.caps.get(index).map(|group| group.start())
}
-}
-impl PregMatchesAllWithOffsets {
- /// The number PHP's `preg_match_all` returns: every column holds one entry per occurrence.
- pub fn occurrence_count(&self) -> usize {
- self.get(&CaptureKey::ByIndex(0))
- .expect("group 0 is always present")
- .len()
+ /// The byte offset of the capture group called `name`, under the same rules as `get_offset`.
+ pub fn name_offset(&self, name: &str) -> Option<usize> {
+ self.caps.name(name).map(|group| group.start())
}
}
@@ -166,84 +91,21 @@ pub fn preg_match<'h>(pattern: impl PregPattern, subject: &'h str) -> Option<Pre
Some(PregMatches::new(caps))
}
-// The number of occurrences the caller would get from PHP's return value is the length of any
-// one column, as `PregMatchesAll::occurrence_count` reports it.
-pub fn preg_match_all(pattern: impl PregPattern, subject: &str) -> PregMatchesAll {
- let __resolved = pattern.resolve();
- let re = __resolved.regex();
- let group_count = re.captures_len();
- let names: Vec<Option<&str>> = re.capture_names().collect();
-
- // PREG_PATTERN_ORDER: one column per group, one row per match occurrence.
- let mut groups: Vec<Vec<Option<String>>> = vec![Vec::new(); group_count];
- for caps in re.captures_iter(subject) {
- for (g, column) in groups.iter_mut().enumerate() {
- let value = caps.get(g).map(|m| m.as_str().to_string());
- column.push(value);
- }
- }
-
- let mut matches = PregMatchesAll::new();
- for (g, column) in groups.into_iter().enumerate() {
- if let Some(Some(name)) = names.get(g) {
- matches.insert(CaptureKey::ByName((*name).to_string()), column.clone());
- }
- matches.insert(CaptureKey::ByIndex(g), column);
- }
-
- matches
-}
-
-// PREG_SET_ORDER: the outer vec is indexed by match occurrence, the inner by
-// capture group (a `$matches` row). A non-participating group is reported as
-// None.
-pub fn preg_match_all_set_order(
+// Every occurrence of the pattern in `subject`, in match order. The search runs eagerly, as PHP's
+// does: a match borrows `subject` alone, so the matches outlive the compiled pattern, which is only
+// resolved for the duration of this call.
+pub fn preg_match_all<'h>(
pattern: impl PregPattern,
- subject: &str,
-) -> Vec<Vec<Option<String>>> {
- let __resolved = pattern.resolve();
- let re = __resolved.regex();
- re.captures_iter(subject)
- .map(|caps| {
- (0..caps.len())
- .map(|g| caps.get(g).map(|m| m.as_str().to_string()))
- .collect()
- })
- .collect()
-}
-
-// A non-participating group is reported as None, at offset -1. The number of occurrences the
-// caller would get from PHP's return value is the length of any one column, as
-// `PregMatchesAllWithOffsets::occurrence_count` reports it.
-pub fn preg_match_all_offset_capture(
- pattern: impl PregPattern,
- subject: &str,
-) -> PregMatchesAllWithOffsets {
+ subject: &'h str,
+) -> impl Iterator<Item = PregMatches<'h>> {
let __resolved = pattern.resolve();
- let re = __resolved.regex();
- let group_count = re.captures_len();
- let names: Vec<Option<&str>> = re.capture_names().collect();
-
- let mut groups: Vec<Vec<(Option<String>, i64)>> = vec![Vec::new(); group_count];
- for caps in re.captures_iter(subject) {
- for (g, column) in groups.iter_mut().enumerate() {
- let entry = match caps.get(g) {
- Some(m) => (Some(m.as_str().to_string()), m.start() as i64),
- None => (None, -1),
- };
- column.push(entry);
- }
- }
-
- let mut matches = PregMatchesAllWithOffsets::new();
- for (g, column) in groups.into_iter().enumerate() {
- if let Some(Some(name)) = names.get(g) {
- matches.insert(CaptureKey::ByName((*name).to_string()), column.clone());
- }
- matches.insert(CaptureKey::ByIndex(g), column);
- }
+ let matches: Vec<PregMatches<'h>> = __resolved
+ .regex()
+ .captures_iter(subject)
+ .map(PregMatches::new)
+ .collect();
- matches
+ matches.into_iter()
}
pub fn preg_grep<T: AsRef<str>>(