use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[derive(Debug, Clone, PartialEq)]
pub enum Expr {
Integer(i64),
Number(f64),
Variable(String),
Operator(std::sync::Arc<ExpressionNode>),
}
impl Expr {
pub fn operator(node: ExpressionNode) -> Expr {
Expr::Operator(crate::intern::intern_node(node))
}
pub fn node_mut(&mut self) -> Option<&mut ExpressionNode> {
match self {
Expr::Operator(rc) => Some(std::sync::Arc::make_mut(rc)),
_ => None,
}
}
}
impl From<ExpressionNode> for Expr {
fn from(node: ExpressionNode) -> Expr {
Expr::operator(node)
}
}
impl<'de> Deserialize<'de> for Expr {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: serde::Deserializer<'de>,
{
deserializer.deserialize_any(ExprVisitor)
}
}
struct ExprVisitor;
impl<'de> serde::de::Visitor<'de> for ExprVisitor {
type Value = Expr;
fn expecting(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str("an expression: a number, a variable name, or an operator node")
}
fn visit_i64<E: serde::de::Error>(self, v: i64) -> Result<Self::Value, E> {
Ok(Expr::Integer(v))
}
fn visit_u64<E: serde::de::Error>(self, v: u64) -> Result<Self::Value, E> {
Ok(match i64::try_from(v) {
Ok(i) => Expr::Integer(i),
Err(_) => Expr::Number(v as f64),
})
}
fn visit_f64<E: serde::de::Error>(self, v: f64) -> Result<Self::Value, E> {
Ok(Expr::Number(v))
}
fn visit_str<E: serde::de::Error>(self, v: &str) -> Result<Self::Value, E> {
Ok(Expr::Variable(v.to_owned()))
}
fn visit_string<E: serde::de::Error>(self, v: String) -> Result<Self::Value, E> {
Ok(Expr::Variable(v))
}
fn visit_map<A>(self, map: A) -> Result<Self::Value, A::Error>
where
A: serde::de::MapAccess<'de>,
{
ExpressionNode::deserialize(serde::de::value::MapAccessDeserializer::new(map))
.map(Expr::operator)
}
fn visit_seq<A>(self, seq: A) -> Result<Self::Value, A::Error>
where
A: serde::de::SeqAccess<'de>,
{
ExpressionNode::deserialize(serde::de::value::SeqAccessDeserializer::new(seq))
.map(Expr::operator)
}
}
impl Serialize for Expr {
fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
match self {
Expr::Integer(i) => serializer.serialize_i64(*i),
Expr::Number(n) => serialize_canonical_f64(*n, serializer),
Expr::Variable(v) => serializer.serialize_str(v),
Expr::Operator(node) => node.serialize(serializer),
}
}
}
pub(crate) fn serialize_canonical_f64<S: serde::Serializer>(
n: f64,
serializer: S,
) -> Result<S::Ok, S::Error> {
if n.is_finite() && n.fract() == 0.0 && n.abs() < 9_223_372_036_854_775_808.0 {
serializer.serialize_i64(n as i64)
} else {
serializer.serialize_f64(n)
}
}
fn serialize_canonical_eps<S: serde::Serializer>(
eps: &Option<f64>,
serializer: S,
) -> Result<S::Ok, S::Error> {
match eps {
Some(n) => serialize_canonical_f64(*n, serializer),
None => serializer.serialize_none(),
}
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
#[serde(untagged)]
pub enum RegionBound {
Int(i64),
Expr(Expr),
}
impl RegionBound {
#[must_use]
pub fn as_i64(&self) -> Option<i64> {
match self {
RegionBound::Int(i) => Some(*i),
RegionBound::Expr(Expr::Integer(i)) => Some(*i),
RegionBound::Expr(Expr::Number(n))
if n.fract() == 0.0 && n.abs() < 9_223_372_036_854_775_808.0 =>
{
Some(*n as i64)
}
RegionBound::Expr(_) => None,
}
}
}
impl From<i64> for RegionBound {
fn from(i: i64) -> Self {
RegionBound::Int(i)
}
}
#[must_use]
pub fn region_bounds(pair: &[RegionBound; 2]) -> Option<[i64; 2]> {
Some([pair[0].as_i64()?, pair[1].as_i64()?])
}
#[derive(Debug, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[serde(untagged)]
pub enum RangeSpec {
Interval([i64; 2]),
Strided([i64; 3]),
IndexSetRef {
from: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
of: Option<Vec<String>>,
},
RaggedDyn {
offsets: String,
of: Vec<String>,
},
DerivedDyn {
from_faq: String,
},
}
impl RangeSpec {
pub fn bounds(&self) -> Option<[i64; 2]> {
match self {
RangeSpec::Interval(iv) => Some(*iv),
RangeSpec::Strided(iv) => Some([iv[0], iv[1]]),
RangeSpec::IndexSetRef { .. }
| RangeSpec::RaggedDyn { .. }
| RangeSpec::DerivedDyn { .. } => None,
}
}
pub fn ragged(&self) -> Option<(&str, &[String])> {
match self {
RangeSpec::RaggedDyn { offsets, of } => Some((offsets.as_str(), of.as_slice())),
_ => None,
}
}
pub fn derived(&self) -> Option<&str> {
match self {
RangeSpec::DerivedDyn { from_faq } => Some(from_faq.as_str()),
_ => None,
}
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct JoinClause {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub on: Vec<[String; 2]>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub overlap: Option<OverlapClause>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub syms: Option<[String; 2]>,
#[serde(skip)]
pub on_gate: Option<crate::join::OnGate>,
}
#[derive(Debug, Clone, PartialEq, Serialize, Deserialize)]
pub struct OverlapClause {
pub src_env: Vec<String>,
pub tgt_env: Vec<String>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
serialize_with = "serialize_canonical_eps"
)]
pub eps: Option<f64>,
#[serde(skip)]
pub sym_src: Option<String>,
#[serde(skip)]
pub sym_tgt: Option<String>,
}
mod output_idx_serde {
use serde::de::Error as _;
use serde::ser::SerializeSeq;
use serde::{Deserialize, Deserializer, Serializer};
use serde_json::Value;
pub(super) fn serialize<S: Serializer>(
value: &Option<Vec<String>>,
serializer: S,
) -> Result<S::Ok, S::Error> {
let items = value
.as_ref()
.expect("serialize_with is only reached when the field is Some");
let mut seq = serializer.serialize_seq(Some(items.len()))?;
for item in items {
match item.parse::<i64>() {
Ok(n) if n.to_string() == *item => seq.serialize_element(&n)?,
_ => seq.serialize_element(item)?,
}
}
seq.end()
}
pub(super) fn deserialize<'de, D: Deserializer<'de>>(
deserializer: D,
) -> Result<Option<Vec<String>>, D::Error> {
let Some(items) = Option::<Vec<Value>>::deserialize(deserializer)? else {
return Ok(None);
};
let mut out = Vec::with_capacity(items.len());
for v in items {
match v {
Value::String(s) => out.push(s),
Value::Number(n) if n.is_i64() || n.is_u64() => out.push(n.to_string()),
other => {
return Err(D::Error::custom(format!(
"output_idx entries must be an index name (string) or an integer, got {other}"
)));
}
}
}
Ok(Some(out))
}
}
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
pub struct ExpressionNode {
pub op: String,
pub args: Vec<Expr>,
#[serde(skip_serializing_if = "Option::is_none")]
pub wrt: Option<String>,
#[serde(skip_serializing_if = "Option::is_none")]
pub dim: Option<String>,
#[serde(default, rename = "var", skip_serializing_if = "Option::is_none")]
pub int_var: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub lower: Option<Box<Expr>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub upper: Option<Box<Expr>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expr: Option<Box<Expr>>,
#[serde(
default,
skip_serializing_if = "Option::is_none",
with = "output_idx_serde"
)]
pub output_idx: Option<Vec<String>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub ranges: Option<HashMap<String, RangeSpec>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub reduce: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub semiring: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub join: Option<Vec<JoinClause>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub filter: Option<Box<Expr>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub regions: Option<Vec<Vec<[RegionBound; 2]>>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub values: Option<Vec<Expr>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub shape: Option<Vec<i64>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub perm: Option<Vec<i64>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub axis: Option<i64>,
#[serde(default, rename = "fn", skip_serializing_if = "Option::is_none")]
pub broadcast_fn: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub label: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub value: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub units: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub table: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub axes: Option<HashMap<String, Expr>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub output: Option<serde_json::Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub manifold: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub bindings: Option<HashMap<String, Expr>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub arg: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub distinct: Option<bool>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub key: Option<Box<Expr>>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub expect_cadence: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub attrs: Option<serde_json::Value>,
}
const CHILD_ARRAY: u8 = 0;
const CHILD_SCALAR: u8 = 1; const CHILD_MAP: u8 = 2;
const fn child_keys<const N: usize>(tag: u8) -> [&'static str; N] {
let mut out = [""; N];
let mut i = 0;
let mut j = 0;
while i < EXPR_CHILD_FIELDS.len() {
if EXPR_CHILD_FIELDS[i].1 == tag {
out[j] = EXPR_CHILD_FIELDS[i].0;
j += 1;
}
i += 1;
}
assert!(
j == N,
"child-key count does not match the declared array length"
);
out
}
macro_rules! expr_child_visit {
(@each array, $node:tt, $f:ident, $field:ident) => {
for a in &$node.$field {
$f(a);
}
};
(@each opt_array, $node:tt, $f:ident, $field:ident) => {
if let Some(vs) = &$node.$field {
for v in vs {
$f(v);
}
}
};
(@each scalar, $node:tt, $f:ident, $field:ident) => {
if let Some(e) = $node.$field.as_deref() {
$f(e);
}
};
(@each map, $node:tt, $f:ident, $field:ident) => {
if let Some(m) = &$node.$field {
let mut keys: Vec<&String> = m.keys().collect();
keys.sort();
for k in keys {
$f(&m[k]);
}
}
};
(@each_mut array, $node:tt, $f:ident, $field:ident) => {
for a in &mut $node.$field {
$f(a);
}
};
(@each_mut opt_array, $node:tt, $f:ident, $field:ident) => {
if let Some(vs) = &mut $node.$field {
for v in vs {
$f(v);
}
}
};
(@each_mut scalar, $node:tt, $f:ident, $field:ident) => {
if let Some(e) = $node.$field.as_deref_mut() {
$f(e);
}
};
(@each_mut map, $node:tt, $f:ident, $field:ident) => {
if let Some(m) = &mut $node.$field {
let mut keys: Vec<String> = m.keys().cloned().collect();
keys.sort();
for k in keys {
if let Some(v) = m.get_mut(&k) {
$f(v);
}
}
}
};
(@any array, $node:tt, $f:ident, $field:ident) => {
$node.$field.iter().any(&mut *$f)
};
(@any opt_array, $node:tt, $f:ident, $field:ident) => {
$node
.$field
.as_ref()
.is_some_and(|vs| vs.iter().any(&mut *$f))
};
(@any scalar, $node:tt, $f:ident, $field:ident) => {
$node.$field.as_deref().is_some_and(|e| $f(e))
};
(@any map, $node:tt, $f:ident, $field:ident) => {
$node
.$field
.as_ref()
.is_some_and(|m| m.values().any(&mut *$f))
};
(@map array, $node:tt, $out:ident, $f:ident, $field:ident) => {
$out.$field = $node.$field.iter().map(&mut *$f).collect();
};
(@map opt_array, $node:tt, $out:ident, $f:ident, $field:ident) => {
$out.$field = $node
.$field
.as_ref()
.map(|vs| vs.iter().map(&mut *$f).collect());
};
(@map scalar, $node:tt, $out:ident, $f:ident, $field:ident) => {
$out.$field = $node.$field.as_deref().map(|e| Box::new($f(e)));
};
(@map map, $node:tt, $out:ident, $f:ident, $field:ident) => {
$out.$field = $node
.$field
.as_ref()
.map(|m| m.iter().map(|(k, v)| (k.clone(), $f(v))).collect());
};
(@tag array) => {
CHILD_ARRAY
};
(@tag opt_array) => {
CHILD_ARRAY
};
(@tag scalar) => {
CHILD_SCALAR
};
(@tag map) => {
CHILD_MAP
};
}
macro_rules! expr_children {
($( $field:ident : $shape:ident ),* $(,)?) => {
const EXPR_CHILD_FIELDS: &[(&str, u8)] = &[
$( (stringify!($field), expr_child_visit!(@tag $shape)) ),*
];
impl ExpressionNode {
pub fn for_each_child<'a>(&'a self, f: &mut impl FnMut(&'a Expr)) {
$( expr_child_visit!(@each $shape, self, f, $field); )*
}
pub fn for_each_child_mut(&mut self, f: &mut impl FnMut(&mut Expr)) {
$( expr_child_visit!(@each_mut $shape, self, f, $field); )*
}
pub fn any_child(&self, f: &mut impl FnMut(&Expr) -> bool) -> bool {
$( if expr_child_visit!(@any $shape, self, f, $field) { return true; } )*
false
}
pub fn map_children(&self, f: &mut impl FnMut(&Expr) -> Expr) -> ExpressionNode {
let mut out = self.clone();
$( expr_child_visit!(@map $shape, self, out, f, $field); )*
out
}
}
};
}
expr_children! {
args: array,
lower: scalar,
upper: scalar,
expr: scalar,
filter: scalar,
values: opt_array,
axes: map,
key: scalar,
bindings: map,
}
impl ExpressionNode {
pub fn try_for_each_child<'a, E>(
&'a self,
f: &mut impl FnMut(&'a Expr) -> Result<(), E>,
) -> Result<(), E> {
let mut first_err: Option<E> = None;
self.for_each_child(&mut |child| {
if first_err.is_none()
&& let Err(e) = f(child)
{
first_err = Some(e);
}
});
match first_err {
Some(e) => Err(e),
None => Ok(()),
}
}
pub fn try_for_each_child_mut<E>(
&mut self,
f: &mut impl FnMut(&mut Expr) -> Result<(), E>,
) -> Result<(), E> {
let mut first_err: Option<E> = None;
self.for_each_child_mut(&mut |child| {
if first_err.is_none()
&& let Err(e) = f(child)
{
first_err = Some(e);
}
});
match first_err {
Some(e) => Err(e),
None => Ok(()),
}
}
}
pub(crate) const EXPR_SCALAR_CHILD_KEYS: [&str; 5] = child_keys::<5>(CHILD_SCALAR);
pub(crate) const EXPR_ARRAY_CHILD_KEYS: [&str; 2] = child_keys::<2>(CHILD_ARRAY);
pub(crate) const EXPR_MAP_CHILD_KEYS: [&str; 2] = child_keys::<2>(CHILD_MAP);
#[cfg(test)]
mod expr_deserialize_token_mapping_tests {
use super::*;
fn parse(src: &str) -> Result<Expr, serde_json::Error> {
serde_json::from_str::<Expr>(src)
}
fn parse_via_value(src: &str) -> Result<Expr, serde_json::Error> {
let v: serde_json::Value = serde_json::from_str(src).expect("fixture is JSON");
serde_json::from_value::<Expr>(v)
}
#[test]
fn integer_tokens_bind_to_integer_and_float_tokens_to_number() {
for (src, want) in [
("0", Expr::Integer(0)),
("-7", Expr::Integer(-7)),
("9223372036854775807", Expr::Integer(i64::MAX)),
("-9223372036854775808", Expr::Integer(i64::MIN)),
("0.0", Expr::Number(0.0)),
("9.0", Expr::Number(9.0)),
("1e3", Expr::Number(1e3)),
("-104.52369275835723", Expr::Number(-104.52369275835723)),
] {
assert_eq!(parse(src).expect(src), want, "from_str {src}");
assert_eq!(parse_via_value(src).expect(src), want, "from_value {src}");
}
}
#[test]
fn unsigned_tokens_above_i64_max_become_number() {
let src = "9223372036854775808";
let want = Expr::Number(9223372036854775808u64 as f64);
assert_eq!(parse(src).expect(src), want);
assert_eq!(parse_via_value(src).expect(src), want);
}
#[test]
fn string_tokens_bind_to_variable() {
assert_eq!(parse(r#""theta""#).unwrap(), Expr::Variable("theta".into()));
assert_eq!(
parse_via_value(r#""theta""#).unwrap(),
Expr::Variable("theta".into())
);
assert_eq!(parse(r#""12""#).unwrap(), Expr::Variable("12".into()));
}
#[test]
fn objects_bind_to_operator_nodes() {
let src = r#"{"op":"+","args":[1,2.5,"x"]}"#;
let Expr::Operator(node) = parse(src).expect(src) else {
panic!("expected an operator node");
};
assert_eq!(node.op, "+");
assert_eq!(
node.args,
vec![
Expr::Integer(1),
Expr::Number(2.5),
Expr::Variable("x".into())
]
);
assert_eq!(parse_via_value(src).expect(src), Expr::Operator(node));
}
#[test]
fn non_expression_tokens_are_rejected() {
for src in [
"null",
"true",
"false",
"[]",
"[1,2]",
"{}",
r#"{"op":"x"}"#,
] {
assert!(parse(src).is_err(), "{src} must not parse as an Expr");
assert!(
parse_via_value(src).is_err(),
"{src} must not parse as an Expr via Value"
);
}
}
#[test]
fn nested_expressions_recurse_through_every_child_slot() {
let src = r#"{
"op":"faq","args":[],"output_idx":["i"],
"expr":{"op":"*","args":[{"op":"-","args":["a",1]},2.0]},
"filter":{"op":">","args":["i",0]},
"axes":{"z":{"op":"+","args":["b",3]}}
}"#;
let Expr::Operator(node) = parse(src).expect("nested fixture parses") else {
panic!("expected an operator node");
};
let Some(Expr::Operator(inner)) = node.expr.as_deref() else {
panic!("expr slot must hold an operator node");
};
assert_eq!(inner.op, "*");
assert_eq!(inner.args[1], Expr::Number(2.0));
let Expr::Operator(lhs) = &inner.args[0] else {
panic!("nested arg must be an operator node");
};
assert_eq!(lhs.args, vec![Expr::Variable("a".into()), Expr::Integer(1)]);
assert!(node.filter.is_some());
assert!(node.axes.as_ref().is_some_and(|m| m.contains_key("z")));
}
#[test]
fn parse_emit_round_trip_is_byte_identical() {
for src in [
r#"{"op":"+","args":[1,"x"]}"#,
r#"{"op":"/","args":[{"op":"sin","args":["t"]},2.5]}"#,
] {
let expr = parse(src).expect(src);
assert_eq!(serde_json::to_string(&expr).unwrap(), src);
}
}
}
#[cfg(test)]
mod expr_child_spec_tests {
use super::*;
#[test]
fn child_key_constants_match_pinned_sets() {
assert_eq!(EXPR_ARRAY_CHILD_KEYS, ["args", "values"]);
assert_eq!(
EXPR_SCALAR_CHILD_KEYS,
["lower", "upper", "expr", "filter", "key"]
);
assert_eq!(EXPR_MAP_CHILD_KEYS, ["axes", "bindings"]);
let bucketed =
EXPR_ARRAY_CHILD_KEYS.len() + EXPR_SCALAR_CHILD_KEYS.len() + EXPR_MAP_CHILD_KEYS.len();
assert_eq!(bucketed, EXPR_CHILD_FIELDS.len());
assert!(EXPR_CHILD_FIELDS.iter().all(|(k, _)| !k.is_empty()));
}
#[test]
fn walkers_cover_all_child_fields_in_contract_order() {
let mut node = ExpressionNode {
op: "probe".into(),
args: vec![Expr::Variable("args0".into())],
lower: Some(Box::new(Expr::Variable("lower".into()))),
upper: Some(Box::new(Expr::Variable("upper".into()))),
expr: Some(Box::new(Expr::Variable("expr".into()))),
filter: Some(Box::new(Expr::Variable("filter".into()))),
values: Some(vec![Expr::Variable("values0".into())]),
key: Some(Box::new(Expr::Variable("key".into()))),
..Default::default()
};
let mut axes = HashMap::new();
axes.insert("z_axis".to_string(), Expr::Variable("axes_z".into()));
axes.insert("a_axis".to_string(), Expr::Variable("axes_a".into()));
node.axes = Some(axes);
let mut bindings = HashMap::new();
bindings.insert("z_bind".to_string(), Expr::Variable("bind_z".into()));
bindings.insert("a_bind".to_string(), Expr::Variable("bind_a".into()));
node.bindings = Some(bindings);
let mut seen = Vec::new();
node.for_each_child(&mut |c| {
if let Expr::Variable(v) = c {
seen.push(v.clone());
}
});
assert_eq!(
seen,
vec![
"args0", "lower", "upper", "expr", "filter", "values0", "axes_a",
"axes_z", "key", "bind_a", "bind_z", ]
);
let mut count_mut = 0usize;
node.for_each_child_mut(&mut |_c| count_mut += 1);
assert_eq!(count_mut, seen.len());
assert!(node.any_child(&mut |c| matches!(c, Expr::Variable(v) if v == "filter")));
assert!(node.any_child(&mut |c| matches!(c, Expr::Variable(v) if v == "bind_z")));
assert!(!node.any_child(&mut |c| matches!(c, Expr::Variable(v) if v == "absent")));
let rebuilt = node.map_children(&mut |c| c.clone());
let mut seen_rebuilt = Vec::new();
rebuilt.for_each_child(&mut |c| {
if let Expr::Variable(v) = c {
seen_rebuilt.push(v.clone());
}
});
assert_eq!(seen, seen_rebuilt);
let mut seen_try = Vec::new();
let ok: Result<(), ()> = node.try_for_each_child(&mut |c| {
if let Expr::Variable(v) = c {
seen_try.push(v.clone());
}
Ok(())
});
assert_eq!(ok, Ok(()));
assert_eq!(seen, seen_try);
let mut before_err = Vec::new();
let failed = node.try_for_each_child(&mut |c| {
if let Expr::Variable(v) = c {
before_err.push(v.clone());
if v == "expr" {
return Err(format!("stopped at {v}"));
}
}
Ok(())
});
assert_eq!(failed, Err("stopped at expr".to_string()));
assert_eq!(before_err, vec!["args0", "lower", "upper", "expr"]);
let mut count_try_mut = 0usize;
let failed_mut: Result<(), ()> = node.try_for_each_child_mut(&mut |_c| {
count_try_mut += 1;
if count_try_mut == 2 { Err(()) } else { Ok(()) }
});
assert_eq!(failed_mut, Err(()));
assert_eq!(count_try_mut, 2);
}
}