use crate::{AssertionId, CaptureId};
use std::collections::VecDeque;
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ExtensionKind {
Backreference(CaptureId),
VariableWidthAssertion(AssertionId),
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub struct ExtensionLimits {
pub max_capture_units: usize,
pub max_work_items: usize,
}
#[derive(Clone, Copy, Debug, Default, PartialEq, Eq)]
pub struct ExtensionReceipt {
pub capture_units: usize,
pub work_items: usize,
}
#[derive(Clone, Copy, Debug, PartialEq, Eq)]
pub enum ExtensionRefusal {
Unsupported(ExtensionKind),
CaptureUnits,
WorkItems,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub struct ExtensionWork<T> {
pub payload: T,
pub capture_units: usize,
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ExtensionOutcome {
Match(ExtensionReceipt),
NoMatch(ExtensionReceipt),
Refused {
reason: ExtensionRefusal,
receipt: ExtensionReceipt,
},
}
#[derive(Clone, Debug, PartialEq, Eq)]
pub enum ExtensionStep<T> {
Match,
NoMatch,
Continue(Vec<ExtensionWork<T>>),
}
pub trait BoundedExtension {
type Work;
fn start(&self, kind: ExtensionKind) -> Option<Vec<ExtensionWork<Self::Work>>>;
fn step(&self, work: Self::Work) -> ExtensionStep<Self::Work>;
}
pub fn execute_extension<X: BoundedExtension>(
extension: &X,
kind: ExtensionKind,
limits: ExtensionLimits,
) -> ExtensionOutcome {
let Some(initial) = extension.start(kind) else {
return ExtensionOutcome::Refused {
reason: ExtensionRefusal::Unsupported(kind),
receipt: ExtensionReceipt::default(),
};
};
let mut queue = VecDeque::from(initial);
let mut receipt = ExtensionReceipt::default();
while let Some(work) = queue.pop_front() {
if receipt.work_items == limits.max_work_items {
return refused(ExtensionRefusal::WorkItems, receipt);
}
if work.capture_units
> limits
.max_capture_units
.saturating_sub(receipt.capture_units)
{
return refused(ExtensionRefusal::CaptureUnits, receipt);
}
receipt.work_items += 1;
receipt.capture_units += work.capture_units;
match extension.step(work.payload) {
ExtensionStep::Match => return ExtensionOutcome::Match(receipt),
ExtensionStep::NoMatch => {}
ExtensionStep::Continue(next) => queue.extend(next),
}
}
ExtensionOutcome::NoMatch(receipt)
}
fn refused(reason: ExtensionRefusal, receipt: ExtensionReceipt) -> ExtensionOutcome {
ExtensionOutcome::Refused { reason, receipt }
}
#[cfg(test)]
mod tests {
use super::*;
struct Backreference;
impl BoundedExtension for Backreference {
type Work = usize;
fn start(&self, kind: ExtensionKind) -> Option<Vec<ExtensionWork<Self::Work>>> {
matches!(kind, ExtensionKind::Backreference(_)).then(|| {
vec![ExtensionWork {
payload: 0,
capture_units: 3,
}]
})
}
fn step(&self, offset: usize) -> ExtensionStep<Self::Work> {
if offset == 3 {
ExtensionStep::Match
} else {
ExtensionStep::Continue(vec![ExtensionWork {
payload: offset + 1,
capture_units: 3,
}])
}
}
}
#[test]
fn backreference_exhausts_capture_budget_deterministically() {
let limits = ExtensionLimits {
max_capture_units: 6,
max_work_items: 8,
};
let expected = ExtensionOutcome::Refused {
reason: ExtensionRefusal::CaptureUnits,
receipt: ExtensionReceipt {
capture_units: 6,
work_items: 2,
},
};
for _ in 0..3 {
assert_eq!(
execute_extension(
&Backreference,
ExtensionKind::Backreference(CaptureId(1)),
limits
),
expected
);
}
}
#[test]
fn unsupported_extensions_are_typed() {
assert!(matches!(
execute_extension(
&Backreference,
ExtensionKind::VariableWidthAssertion(AssertionId(4)),
ExtensionLimits {
max_capture_units: 10,
max_work_items: 10
}
),
ExtensionOutcome::Refused {
reason: ExtensionRefusal::Unsupported(ExtensionKind::VariableWidthAssertion(
AssertionId(4)
)),
..
}
));
}
}