use std::collections::{HashMap, HashSet};
use std::hash::BuildHasherDefault;
use fnv::FnvHasher;
use crate::graph::GraphStorage;
use crate::traversal::context::tick_deadline;
use crate::value::glob_path::GlobPattern;
use crate::value::{PathRef, SackRef};
use crate::*;
pub(crate) mod reason {
pub(crate) const MISSING_KEY: &str = "glob_path() needs a first by() naming the match key";
pub(crate) const LABELS_AS_KEY: &str =
"the first by() is the match key; the vertex-label list goes into the second by()";
pub(crate) const KEY_KIND: &str = "the match key must be a property key, T.id or T.label";
pub(crate) const JPATH_KEY: &str = "the match key must be a plain property name, not a jpath(..) path; \
glob_path() matches the stored value of one property, use by(\"name\") instead";
pub(crate) const FILTER_KIND: &str = "the second by() must be a list of vertex labels";
pub(crate) const EMPTY_LABELS: &str = "the vertex-label list is empty";
pub(crate) const TOO_MANY: &str = "glob_path() takes at most two by() modulators";
}
pub(crate) const MAX_DEPTH_OPTION: &str = "glob_path.max_depth";
#[derive(Debug, Clone, PartialEq, Eq)]
enum MatchKey {
Property(CompactString),
Id,
Label,
}
#[derive(Debug, Clone, PartialEq, Eq)]
enum GlobBy {
Property(CompactString),
Id,
Label,
Labels(Vec<CompactString>),
Other(&'static str),
}
fn write_quoted(f: &mut std::fmt::Formatter<'_>, s: &str) -> std::fmt::Result {
f.write_str(&crate::value::literal::rhai_string(s))
}
impl fmt::Display for GlobBy {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
GlobBy::Property(key) => write_quoted(f, key),
GlobBy::Id => write!(f, "T.id"),
GlobBy::Label => write!(f, "T.label"),
GlobBy::Labels(labels) => {
write!(f, "[")?;
for (i, label) in labels.iter().enumerate() {
if i > 0 {
write!(f, ",")?;
}
write_quoted(f, label)?;
}
write!(f, "]")
}
GlobBy::Other(kind) => write!(f, "{kind}"),
}
}
}
#[non_exhaustive]
pub struct GlobPathOptions<'g, S: GraphStorage + 'g> {
max_depth: Option<i64>,
prune: Option<AnonymousTraversal<'g, S>>,
}
impl<'g, S: GraphStorage + 'g> Default for GlobPathOptions<'g, S> {
fn default() -> Self {
Self {
max_depth: None,
prune: None,
}
}
}
impl<'g, S: GraphStorage + 'g> Clone for GlobPathOptions<'g, S> {
fn clone(&self) -> Self {
Self {
max_depth: self.max_depth,
prune: self.prune.clone(),
}
}
}
impl<'g, S: GraphStorage + 'g> fmt::Debug for GlobPathOptions<'g, S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("GlobPathOptions")
.field("max_depth", &self.max_depth)
.field("prune", &self.prune)
.finish()
}
}
impl<'g, S: GraphStorage + 'g> GlobPathOptions<'g, S> {
pub fn max_depth(mut self, depth: u32) -> Self {
self.max_depth = Some(i64::from(depth));
self
}
#[cfg_attr(not(feature = "script"), allow(dead_code))]
pub(crate) fn max_depth_raw(mut self, depth: i64) -> Self {
self.max_depth = Some(depth);
self
}
pub fn prune(mut self, traversal: AnonymousTraversal<'g, S>) -> Self {
self.prune = Some(traversal);
self
}
}
struct WalkBuffers<V> {
stack: WalkStack<V>,
visited: WalkVisited<V>,
depth_slots: HashMap<V, usize, BuildHasherDefault<FnvHasher>>,
depths: Vec<u32>,
}
impl<V> Default for WalkBuffers<V> {
fn default() -> Self {
Self {
stack: Vec::new(),
visited: HashMap::default(),
depth_slots: HashMap::default(),
depths: Vec::new(),
}
}
}
pub(crate) struct GlobPathScratch<V> {
walk: WalkBuffers<V>,
emitted: HashSet<V, BuildHasherDefault<FnvHasher>>,
}
impl<V> Default for GlobPathScratch<V> {
fn default() -> Self {
Self {
walk: WalkBuffers::default(),
emitted: HashSet::default(),
}
}
}
struct StepGlobPath<'g, S: GraphStorage + 'g> {
raw: CompactString,
pattern: Result<GlobPattern, (usize, String)>,
by: Vec<GlobBy>,
options: GlobPathOptions<'g, S>,
}
impl<'g, S: GraphStorage + 'g> Clone for StepGlobPath<'g, S> {
fn clone(&self) -> Self {
Self {
raw: self.raw.clone(),
pattern: self.pattern.clone(),
by: self.by.clone(),
options: self.options.clone(),
}
}
}
fn write_child<'g, S: GraphStorage + 'g>(
f: &mut std::fmt::Formatter<'_>,
traversal: &AnonymousTraversal<'g, S>,
) -> std::fmt::Result {
write!(f, "{traversal}")
}
impl<'g, S: GraphStorage + 'g> fmt::Display for StepGlobPath<'g, S> {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "glob_path(")?;
write_quoted(f, &self.raw)?;
let GlobPathOptions { max_depth, prune } = &self.options;
if max_depth.is_some() || prune.is_some() {
write!(f, ", {{")?;
if let Some(depth) = max_depth {
write!(f, "max_depth: {depth}")?;
}
if let Some(prune) = prune {
if max_depth.is_some() {
write!(f, ", ")?;
}
write!(f, "prune: ")?;
write_child(f, prune)?;
}
write!(f, "}}")?;
}
write!(f, ")")?;
for by in &self.by {
write!(f, ".by({by})")?;
}
Ok(())
}
}
impl<'g, S: GraphStorage + 'g> StepGlobPath<'g, S> {
fn new(pattern: &str, options: GlobPathOptions<'g, S>) -> Self {
let compiled = GlobPattern::compile(pattern).map_err(|err| match err {
ValueError::InvalidGlobPattern {
position, reason, ..
} => (position, reason),
other => (0, other.to_string()),
});
Self {
raw: CompactString::new(pattern),
pattern: compiled,
by: Vec::new(),
options,
}
}
fn pattern(&self) -> TraverserResult<&GlobPattern> {
match &self.pattern {
Ok(pattern) => Ok(pattern),
Err((position, reason)) => Err(Box::new(TraverserError::Value(
ValueError::InvalidGlobPattern {
pattern: self.raw.to_string(),
position: *position,
reason: reason.clone(),
},
))),
}
}
fn config(&self) -> TraverserResult<(MatchKey, Option<&[CompactString]>)> {
let invalid = |position: usize,
reason: &str|
-> TraverserResult<(MatchKey, Option<&[CompactString]>)> {
Err(Box::new(TraverserError::InvalidModulator {
step: "glob_path".to_string(),
position,
modulator: crate::traversal::by_reason::MODULATOR,
reason: reason.to_string(),
}))
};
let key = match self.by.first() {
None => return invalid(1, reason::MISSING_KEY),
Some(GlobBy::Property(key)) => MatchKey::Property(key.clone()),
Some(GlobBy::Id) => MatchKey::Id,
Some(GlobBy::Label) => MatchKey::Label,
Some(GlobBy::Labels(_)) => return invalid(1, reason::LABELS_AS_KEY),
Some(GlobBy::Other("<json_path>")) => return invalid(1, reason::JPATH_KEY),
Some(GlobBy::Other(_)) => return invalid(1, reason::KEY_KIND),
};
let filter = match self.by.get(1) {
None => None,
Some(GlobBy::Labels(labels)) if labels.is_empty() => {
return invalid(2, reason::EMPTY_LABELS);
}
Some(GlobBy::Labels(labels)) => Some(labels.as_slice()),
Some(GlobBy::Property(_) | GlobBy::Id | GlobBy::Label | GlobBy::Other(_)) => {
return invalid(2, reason::FILTER_KIND);
}
};
if self.by.len() > 2 {
return invalid(3, reason::TOO_MANY);
}
Ok((key, filter))
}
fn max_depth(&self) -> TraverserResult<Option<u32>> {
let Some(depth) = self.options.max_depth else {
return Ok(None);
};
let reason = if depth < 1 {
format!("must be at least 1, got {depth}")
} else {
match u32::try_from(depth) {
Ok(depth) => return Ok(Some(depth)),
Err(_) => format!("must be at most {}, got {depth}", u32::MAX),
}
};
Err(Box::new(TraverserError::InvalidOption {
key: MAX_DEPTH_OPTION.to_string(),
reason,
}))
}
}
const VERTEX_INPUT: &[&str] = &["vertex", "vertexProperty", "computed (linked to vertex)"];
fn start_vertex<S: GraphStorage>(value: &TraverserValue<S>) -> TraverserResult<S::VertexId> {
let vertex = match value {
TraverserValue::Vertex(vertex) | TraverserValue::VertexProperty(_, vertex, _) => {
Some(*vertex)
}
TraverserValue::Computed(_, vertex) => *vertex,
TraverserValue::Path(_)
| TraverserValue::Edge(_)
| TraverserValue::EdgeProperty(_, _, _)
| TraverserValue::PathProperty(_)
| TraverserValue::Entry(_)
| TraverserValue::Object(_)
| TraverserValue::Record { .. }
| TraverserValue::Array(_)
| TraverserValue::Schema(_)
| TraverserValue::Graph(_) => None,
};
vertex.ok_or_else(|| Box::new(TraverserError::cast_exception(VERTEX_INPUT, value)))
}
fn transition<S: GraphStorage>(
storage: &S,
pattern: &GlobPattern,
key: &MatchKey,
mask: u64,
child: S::VertexId,
) -> u64 {
if mask & pattern.needs_key() == 0 {
return pattern.step(mask, None);
}
match key {
MatchKey::Property(key) => {
let value = storage
.vertex_properties(child)
.and_then(|properties| properties.get(key.as_str()));
if let Some(ElementProperty::Compressed(text)) = value {
return text.with_text(|text| pattern.step(mask, Some(text)));
}
let value = value.and_then(|value| match value {
ElementProperty::String(value) => Some(value.as_str()),
ElementProperty::Compressed(_)
| ElementProperty::Null
| ElementProperty::Number(_)
| ElementProperty::Boolean(_)
| ElementProperty::Uuid(_)
| ElementProperty::Object(_)
| ElementProperty::Map(_)
| ElementProperty::Array(_)
| ElementProperty::Vertex(_)
| ElementProperty::Edge(_) => None,
});
pattern.step(mask, value)
}
MatchKey::Id => pattern.step(mask, storage.vertex_id(child)),
MatchKey::Label => {
let labels = storage.vertex_labels(child);
if labels.is_empty() {
return pattern.step(mask, None);
}
labels.iter().fold(0u64, |next, label| {
next | pattern.step(mask, Some(label.as_str()))
})
}
}
}
type WalkStack<V> = Vec<(V, u64, u32)>;
type WalkVisited<V> = HashMap<V, u64, BuildHasherDefault<FnvHasher>>;
struct Walk<'a, 'g, S: GraphStorage + 'g> {
pattern: &'a GlobPattern,
key: MatchKey,
filter: Option<&'a [CompactString]>,
start: S::VertexId,
max_depth: Option<u32>,
prune: Option<&'a AnonymousTraversal<'g, S>>,
path: PathRef,
sack: SackRef,
}
impl<'g, S: GraphStorage + 'g> Walk<'_, 'g, S> {
fn run(
&self,
context: &mut TraverserContext<'_, S>,
buffers: &mut WalkBuffers<S::VertexId>,
mut on_match: impl FnMut(S::VertexId),
) -> TraverserResult<bool> {
let Self {
pattern,
ref key,
filter,
start,
max_depth,
prune,
path,
sack,
} = *self;
let WalkBuffers {
stack,
visited,
depth_slots,
depths,
} = buffers;
let live_states = pattern.accept_bit() - 1;
let slots = pattern.n() + 1;
let mut reexpanded = false;
stack.clear();
visited.clear();
depth_slots.clear();
depths.clear();
if max_depth.is_some() {
depth_slots.insert(start, 0);
depths.resize(slots, u32::MAX);
for_each_state(pattern.initial(), |state| {
if let Some(depth) = depths.get_mut(state) {
*depth = 0;
}
});
} else {
visited.insert(start, pattern.initial());
}
stack.push((start, pattern.initial(), 0));
while let Some((vertex, mask, depth)) = stack.pop() {
if let (Some(prune), true) = (prune, depth > 0) {
let probe = Traverser::with_parts(TraverserValue::Vertex(vertex), path, 1, sack);
if prune.exists(context, &probe)? {
continue;
}
}
let child_depth = depth.saturating_add(1);
if max_depth.is_some_and(|max| child_depth > max) {
continue;
}
let expandable = max_depth.is_none_or(|max| child_depth < max);
let storage: &S = &*context.storage;
for edge in storage.out_edges(vertex) {
let Some((_, child)) = storage.edge_endpoints(edge) else {
continue;
};
tick_deadline(&mut context.deadline)?;
if let Some(labels) = filter
&& !labels
.iter()
.any(|label| storage.vertex_has_label(child, label))
{
continue;
}
let mut next = transition(storage, pattern, key, mask, child);
if next == 0 {
continue;
}
if child == start || storage.in_edges(child).nth(1).is_some() {
if max_depth.is_some() {
let (slot, revisit) = match depth_slots.get(&child) {
Some(&slot) => (slot, true),
None => {
let slot = depths.len();
depths.resize(slot + slots, u32::MAX);
depth_slots.insert(child, slot);
(slot, false)
}
};
let mut new = 0u64;
for_each_state(next, |state| {
if let Some(seen) = depths.get_mut(slot + state)
&& *seen > child_depth
{
*seen = child_depth;
new |= 1u64 << state;
}
});
if new == 0 {
continue;
}
reexpanded |= revisit;
next = new;
} else {
match visited.get_mut(&child) {
Some(seen) => {
let new = next & !*seen;
if new == 0 {
continue;
}
*seen |= next;
next = new;
reexpanded = true;
}
None => {
visited.insert(child, next);
}
}
}
}
if pattern.accepts(next) && child != start {
on_match(child);
}
if expandable && next & live_states != 0 {
stack.push((child, next, child_depth));
}
}
}
Ok(reexpanded)
}
}
fn for_each_state(mut mask: u64, mut f: impl FnMut(usize)) {
while mask != 0 {
f(mask.trailing_zeros() as usize);
mask &= mask - 1;
}
}
impl<'g, S: GraphStorage + 'g> StepGlobPath<'g, S> {
fn prepare<'a>(&'a self, source: &Traverser<S>) -> TraverserResult<Walk<'a, 'g, S>> {
let pattern = self.pattern()?;
let (key, filter) = self.config()?;
let max_depth = self.max_depth()?;
Ok(Walk {
pattern,
key,
filter,
start: start_vertex(&source.value)?,
max_depth,
prune: self.options.prune.as_ref(),
path: source.path,
sack: source.sack,
})
}
}
fn dedup_emitted<S: GraphStorage>(
emitted: &mut HashSet<S::VertexId, BuildHasherDefault<FnvHasher>>,
output: &mut Vec<Traverser<S>>,
before: usize,
) {
emitted.clear();
let mut write = before;
for read in before..output.len() {
let keep = match output.get(read).map(|traverser| &traverser.value) {
Some(TraverserValue::Vertex(vertex)) => emitted.insert(*vertex),
Some(
TraverserValue::Computed(..)
| TraverserValue::VertexProperty(..)
| TraverserValue::Edge(_)
| TraverserValue::EdgeProperty(..)
| TraverserValue::Path(_)
| TraverserValue::Record { .. }
| TraverserValue::Object(_)
| TraverserValue::PathProperty(_)
| TraverserValue::Entry(_)
| TraverserValue::Array(_)
| TraverserValue::Schema(_)
| TraverserValue::Graph(_),
)
| None => true,
};
if keep {
output.swap(write, read);
write += 1;
}
}
output.truncate(write);
}
fn with_scratch<S: GraphStorage, T>(
context: &mut TraverserContext<'_, S>,
f: impl FnOnce(
&mut TraverserContext<'_, S>,
&mut GlobPathScratch<S::VertexId>,
) -> TraverserResult<T>,
) -> TraverserResult<T> {
let mut scratch = std::mem::take(&mut context.glob_path);
let result = f(context, &mut scratch);
context.glob_path = scratch;
result
}
impl<'g, S: GraphStorage + 'g> GraphTraversalSource<'g, S> {
pub fn glob_path(self, pattern: &str) -> Self {
self.glob_path_with(pattern, GlobPathOptions::default())
}
pub fn glob_path_with(self, pattern: &str, options: GlobPathOptions<'g, S>) -> Self {
self.push_step(GraphStep::FlatMap(
"glob_path",
Box::new(StepGlobPath::new(pattern, options)),
))
}
}
impl __ {
pub fn glob_path<'g, S: GraphStorage + 'g>(pattern: &str) -> AnonymousTraversal<'g, S> {
AnonymousTraversal::new().glob_path(pattern)
}
pub fn glob_path_with<'g, S: GraphStorage + 'g>(
pattern: &str,
options: GlobPathOptions<'g, S>,
) -> AnonymousTraversal<'g, S> {
AnonymousTraversal::new().glob_path_with(pattern, options)
}
}
impl<'g, S: GraphStorage + 'g> AnonymousTraversal<'g, S> {
pub fn glob_path(self, pattern: &str) -> Self {
self.glob_path_with(pattern, GlobPathOptions::default())
}
pub fn glob_path_with(self, pattern: &str, options: GlobPathOptions<'g, S>) -> Self {
self.push_step(GraphStep::FlatMap(
"glob_path",
Box::new(StepGlobPath::new(pattern, options)),
))
}
}
impl<'g, S: GraphStorage + 'g> GraphStepFlatMap<'g, S> for StepGlobPath<'g, S> {
fn process(
&self,
context: &mut TraverserContext<'_, S>,
source: Traverser<S>,
output: &mut Vec<Traverser<S>>,
) -> TraverserResult<()> {
let walk = self.prepare(&source)?;
let before = output.len();
let path = source.path;
let bulk = source.bulk;
let sack = source.sack;
with_scratch(context, |context, scratch| {
let reexpanded = walk.run(context, &mut scratch.walk, |vertex| {
output.push(Traverser::with_parts(
TraverserValue::Vertex(vertex),
path,
bulk,
sack,
))
})?;
if reexpanded {
dedup_emitted(&mut scratch.emitted, output, before);
}
Ok(())
})
}
fn count(
&self,
context: &mut TraverserContext<'_, S>,
source: Traverser<S>,
) -> TraverserResult<u64> {
let walk = self.prepare(&source)?;
let bulk = source.bulk;
with_scratch(context, |context, scratch| {
let GlobPathScratch {
walk: buffers,
emitted,
} = scratch;
emitted.clear();
if walk.prune.is_some() {
walk.run(context, buffers, |vertex| {
emitted.insert(vertex);
})?;
return crate::traversal::bulk::mul_bulk(emitted.len() as u64, bulk, "glob_path");
}
let mut count = 0usize;
let reexpanded = walk.run(context, buffers, |_| count += 1)?;
if !reexpanded {
return crate::traversal::bulk::mul_bulk(count as u64, bulk, "glob_path");
}
walk.run(context, buffers, |vertex| {
emitted.insert(vertex);
})?;
crate::traversal::bulk::mul_bulk(emitted.len() as u64, bulk, "glob_path")
})
}
fn push_modulator_by(&mut self, args: By<'g, S>, _modulator_name: &str) -> ByModulation {
self.by.push(match args {
By::Property(key) => GlobBy::Property(key),
By::Id => GlobBy::Id,
By::Label => GlobBy::Label,
By::Labels(labels) => GlobBy::Labels(labels),
By::Traversal(_) => GlobBy::Other("<traversal>"),
By::Order(_) => GlobBy::Other("<order>"),
By::Value => GlobBy::Other("Value"),
By::Identity => GlobBy::Other("Identity"),
By::Key => GlobBy::Other("Key"),
By::Count => GlobBy::Other("Count"),
By::JsonPath(_) => GlobBy::Other("<json_path>"),
By::Column(_) => GlobBy::Other("<column>"),
});
ByModulation::Accepted
}
fn child_traversals_mut(&mut self) -> Vec<&mut crate::traversal::AnonymousTraversal<'g, S>> {
self.options.prune.as_mut().into_iter().collect()
}
fn analyze_nested_paths(
&mut self,
analysis: &crate::traversal::optimizer::path_analysis::PathAnalysis,
_downstream: &crate::traversal::optimizer::path_analysis::PathRequirement,
) -> crate::traversal::optimizer::path_analysis::PathRequirement {
match &mut self.options.prune {
Some(prune) => prune.analyze_paths(
analysis,
crate::traversal::optimizer::path_analysis::PathRequirement::None,
),
None => crate::traversal::optimizer::path_analysis::PathRequirement::None,
}
}
fn clone_box(&self) -> Box<dyn GraphStepFlatMap<'g, S> + 'g> {
Box::new(self.clone())
}
}
#[cfg(test)]
mod tests {
use super::*;
type Step = StepGlobPath<'static, &'static TraversalGraph>;
fn by_names(step: &Step) -> String {
step.to_string()
}
#[test]
fn renders_pattern_and_modulators() {
let mut step = Step::new("**/\"q\".rs", GlobPathOptions::default());
step.by.push(GlobBy::Property(CompactString::new("name")));
step.by.push(GlobBy::Labels(vec![
CompactString::new("directory"),
CompactString::new("file"),
]));
assert_eq!(
by_names(&step),
r#"glob_path("**/\"q\".rs").by("name").by(["directory","file"])"#
);
}
#[test]
fn renders_only_the_options_that_are_set() {
let step = Step::new("**", GlobPathOptions::default().max_depth(3));
assert_eq!(by_names(&step), r#"glob_path("**", {max_depth: 3})"#);
let step = Step::new(
"**",
GlobPathOptions::default().prune(__::has("name", "author")),
);
assert_eq!(
by_names(&step),
r#"glob_path("**", {prune: __.has("name", "author")})"#
);
let mut step = Step::new(
"**/*.rs",
GlobPathOptions::default()
.max_depth_raw(-1)
.prune(__::glob_path("*").by("name")),
);
step.by.push(GlobBy::Property(CompactString::new("name")));
assert_eq!(
by_names(&step),
r#"glob_path("**/*.rs", {max_depth: -1, prune: __.glob_path("*").by("name")}).by("name")"#
);
}
#[test]
fn max_depth_is_validated_on_execution() {
for (depth, expected) in [
(0, Some("must be at least 1, got 0")),
(-1, Some("must be at least 1, got -1")),
(1, None),
(i64::from(u32::MAX), None),
(
i64::from(u32::MAX) + 1,
Some("must be at most 4294967295, got 4294967296"),
),
] {
let step = Step::new("*", GlobPathOptions::default().max_depth_raw(depth));
match (step.max_depth().map_err(|err| *err), expected) {
(Ok(Some(actual)), None) => assert_eq!(i64::from(actual), depth),
(Err(TraverserError::InvalidOption { key, reason }), Some(expected)) => {
assert_eq!(key, MAX_DEPTH_OPTION);
assert_eq!(reason, expected);
}
(other, _) => panic!("max_depth {depth}: unexpected {other:?}"),
}
}
}
#[test]
fn config_rejects_by_modulators_by_position() {
let cases: Vec<(Vec<GlobBy>, usize, &str)> = vec![
(vec![], 1, reason::MISSING_KEY),
(
vec![GlobBy::Labels(vec![CompactString::new("a")])],
1,
reason::LABELS_AS_KEY,
),
(vec![GlobBy::Other("<traversal>")], 1, reason::KEY_KIND),
(vec![GlobBy::Other("<json_path>")], 1, reason::JPATH_KEY),
(vec![GlobBy::Id, GlobBy::Label], 2, reason::FILTER_KIND),
(
vec![GlobBy::Id, GlobBy::Labels(vec![])],
2,
reason::EMPTY_LABELS,
),
(
vec![
GlobBy::Id,
GlobBy::Labels(vec![CompactString::new("a")]),
GlobBy::Id,
],
3,
reason::TOO_MANY,
),
];
for (by, expected_position, expected_reason) in cases {
let mut step = Step::new("*", GlobPathOptions::default());
step.by = by;
match step.config().map(|_| ()).map_err(|err| *err) {
Err(TraverserError::InvalidModulator {
position, reason, ..
}) => {
assert_eq!(position, expected_position);
assert_eq!(reason, expected_reason);
}
other => panic!("expected InvalidModulator, got {other:?}"),
}
}
}
}