Skip to content

Commit 9249824

Browse files
authored
feat: array slice selector
- Simple slicing: forward step and positive bounds. Includes an overhaul to how array transitions are compiled. Ref: #152
1 parent 9884d6c commit 9249824

18 files changed

+1386
-335
lines changed

.vscode/settings.json

+2
Original file line numberDiff line numberDiff line change
@@ -14,10 +14,12 @@
1414
"cmpeq",
1515
"codegen",
1616
"color_eyre",
17+
"complementation",
1718
"cvtsi",
1819
"datavalue",
1920
"dealloc",
2021
"Deque",
22+
"determinization",
2123
"docsrs",
2224
"ebnf",
2325
"Elem",

Cargo.lock

+28-22
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

crates/rsonpath-lib/src/automaton.rs

+155-9
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
//! Automaton representations of a JSONPath query.
2+
mod array_transition_set;
23
pub mod error;
34
mod minimizer;
45
mod nfa;
@@ -21,8 +22,23 @@ pub struct Automaton<'q> {
2122

2223
/// Transition when a JSON member name matches a [`JsonString`]i.
2324
pub type MemberTransition<'q> = (&'q JsonString, State);
24-
/// Transition on the n-th element of an array, with n specified by a [`JsonUInt`].
25-
pub type ArrayTransition<'q> = (JsonUInt, State);
25+
26+
/// Transition on elements of an array with indices specified by either a single index
27+
/// or a simple slice expression.
28+
#[derive(Debug, PartialEq, Eq)]
29+
pub struct ArrayTransition {
30+
label: ArrayTransitionLabel,
31+
target: State,
32+
}
33+
34+
/// Represent the distinct methods of moving on a match between states.
35+
#[derive(Debug, Copy, PartialEq, Clone, Eq)]
36+
pub(super) enum ArrayTransitionLabel {
37+
/// Transition on the n-th element of an array, with n specified by a [`JsonUInt`].
38+
Index(JsonUInt),
39+
/// Transition on elements of array matched by a slice expression - bounds and a step.
40+
Slice(SimpleSlice),
41+
}
2642

2743
/// A transition table of a single [`State`] of an [`Automaton`].
2844
///
@@ -32,10 +48,17 @@ pub type ArrayTransition<'q> = (JsonUInt, State);
3248
pub struct StateTable<'q> {
3349
attributes: StateAttributes,
3450
member_transitions: SmallVec<[MemberTransition<'q>; 2]>,
35-
array_transitions: SmallVec<[ArrayTransition<'q>; 2]>,
51+
array_transitions: SmallVec<[ArrayTransition; 2]>,
3652
fallback_state: State,
3753
}
3854

55+
#[derive(Debug, Copy, PartialEq, Clone, Eq)]
56+
pub(crate) struct SimpleSlice {
57+
start: JsonUInt,
58+
end: Option<JsonUInt>,
59+
step: JsonUInt,
60+
}
61+
3962
impl<'q> Default for StateTable<'q> {
4063
#[inline]
4164
fn default() -> Self {
@@ -76,6 +99,47 @@ impl<'q> Index<State> for Automaton<'q> {
7699
}
77100
}
78101

102+
impl ArrayTransition {
103+
pub(crate) fn new(label: ArrayTransitionLabel, target: State) -> Self {
104+
Self { label, target }
105+
}
106+
107+
#[inline(always)]
108+
pub(crate) fn target_state(&self) -> State {
109+
self.target
110+
}
111+
112+
#[inline(always)]
113+
pub(crate) fn matches(&self, index: JsonUInt) -> bool {
114+
self.label.matches(index)
115+
}
116+
}
117+
118+
impl ArrayTransitionLabel {
119+
pub(crate) fn matches(&self, index: JsonUInt) -> bool {
120+
match self {
121+
Self::Index(i) => index.eq(i),
122+
Self::Slice(s) => s.contains(index),
123+
}
124+
}
125+
}
126+
127+
impl From<JsonUInt> for ArrayTransitionLabel {
128+
#[must_use]
129+
#[inline(always)]
130+
fn from(index: JsonUInt) -> Self {
131+
Self::Index(index)
132+
}
133+
}
134+
135+
impl From<SimpleSlice> for ArrayTransitionLabel {
136+
#[must_use]
137+
#[inline(always)]
138+
fn from(slice: SimpleSlice) -> Self {
139+
Self::Slice(slice)
140+
}
141+
}
142+
79143
impl<'q> Automaton<'q> {
80144
/// Convert a [`JsonPathQuery`] into a minimal deterministic automaton.
81145
///
@@ -172,7 +236,7 @@ impl<'q> Automaton<'q> {
172236
#[must_use]
173237
#[inline(always)]
174238
pub fn has_any_array_item_transition(&self, state: State) -> bool {
175-
self[state].attributes.has_array_index_transition()
239+
self[state].attributes.has_array_transition()
176240
}
177241

178242
/// Returns whether the given state is accepting the first item in a list.
@@ -219,11 +283,11 @@ impl<'q> Automaton<'q> {
219283
#[inline(always)]
220284
pub fn has_array_index_transition_to_accepting(&self, state: State, match_index: &JsonUInt) -> bool {
221285
let state = &self[state];
222-
state.attributes.has_array_index_transition_to_accepting()
286+
state.attributes.has_array_transition_to_accepting()
223287
&& state
224288
.array_transitions()
225289
.iter()
226-
.any(|(i, s)| i.eq(match_index) && self.is_accepting(*s))
290+
.any(|trans| self.is_accepting(trans.target) && trans.matches(*match_index))
227291
}
228292

229293
/// Returns whether the given state has any transitions
@@ -303,7 +367,7 @@ impl<'q> StateTable<'q> {
303367
/// to the contained [`State`].
304368
#[must_use]
305369
#[inline(always)]
306-
pub fn array_transitions(&self) -> &[ArrayTransition<'q>] {
370+
pub fn array_transitions(&self) -> &[ArrayTransition] {
307371
&self.array_transitions
308372
}
309373

@@ -318,6 +382,22 @@ impl<'q> StateTable<'q> {
318382
}
319383
}
320384

385+
impl Display for ArrayTransitionLabel {
386+
#[inline(always)]
387+
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
388+
match self {
389+
Self::Index(index) => write!(f, "{}", index.as_u64()),
390+
Self::Slice(slice) => {
391+
if let Some(end) = slice.end {
392+
write!(f, "[{}:{}:{}]", slice.start, end, slice.step)
393+
} else {
394+
write!(f, "[{}::{}]", slice.start, slice.step)
395+
}
396+
}
397+
}
398+
}
399+
}
400+
321401
impl<'q> Display for Automaton<'q> {
322402
#[inline]
323403
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
@@ -348,8 +428,35 @@ impl<'q> Display for Automaton<'q> {
348428
}
349429

350430
for (i, transitions) in self.states.iter().enumerate() {
351-
for (label, state) in &transitions.array_transitions {
352-
writeln!(f, " {i} -> {} [label=\"[{}]\"]", state.0, label.as_u64())?
431+
for array_transition in &transitions.array_transitions {
432+
match array_transition.label {
433+
ArrayTransitionLabel::Index(label) => writeln!(
434+
f,
435+
" {i} -> {} [label=\"[{}]\"]",
436+
array_transition.target.0,
437+
label.as_u64()
438+
)?,
439+
ArrayTransitionLabel::Slice(label) => {
440+
if let Some(end) = label.end {
441+
writeln!(
442+
f,
443+
" {i} -> {} [label=\"[{}:{}:{}]\"]",
444+
array_transition.target.0,
445+
label.start.as_u64(),
446+
end.as_u64(),
447+
label.step.as_u64()
448+
)?
449+
} else {
450+
writeln!(
451+
f,
452+
" {i} -> {} [label=\"[{}::{}]\"]",
453+
array_transition.target.0,
454+
label.start.as_u64(),
455+
label.step.as_u64()
456+
)?
457+
}
458+
}
459+
}
353460
}
354461
for (label, state) in &transitions.member_transitions {
355462
writeln!(f, " {i} -> {} [label=\"{}\"]", state.0, label.unquoted())?
@@ -360,3 +467,42 @@ impl<'q> Display for Automaton<'q> {
360467
Ok(())
361468
}
362469
}
470+
471+
impl SimpleSlice {
472+
fn new(start: JsonUInt, end: Option<JsonUInt>, step: JsonUInt) -> Self {
473+
Self { start, end, step }
474+
}
475+
476+
#[inline(always)]
477+
#[must_use]
478+
fn contains(&self, index: JsonUInt) -> bool {
479+
if index < self.start {
480+
return false;
481+
}
482+
let offset = index.as_u64() - self.start.as_u64();
483+
if let Some(end) = self.end {
484+
index < end && offset % self.step.as_u64() == 0
485+
} else {
486+
offset % self.step.as_u64() == 0
487+
}
488+
}
489+
}
490+
491+
#[cfg(test)]
492+
mod tests {
493+
use super::SimpleSlice;
494+
use rsonpath_syntax::num::JsonUInt;
495+
use test_case::test_case;
496+
497+
#[test_case(0.into(), None, 1.into(), 0.into() => true)]
498+
#[test_case(2.into(), None, 1.into(), 3.into() => true)]
499+
#[test_case(2.into(), None, 2.into(), 3.into() => false)]
500+
#[test_case(3.into(), None, 2.into(), 3.into() => true)]
501+
#[test_case(2.into(), None, 2.into(), 4.into() => true)]
502+
#[test_case(2.into(), Some(6.into()), 2.into(), 2.into() => true)]
503+
#[test_case(2.into(), Some(6.into()), 2.into(), 6.into() => false)]
504+
fn simple_slice_containment(start: JsonUInt, end: Option<JsonUInt>, step: JsonUInt, idx: JsonUInt) -> bool {
505+
let slice = SimpleSlice::new(start, end, step);
506+
slice.contains(idx)
507+
}
508+
}

0 commit comments

Comments
 (0)