diff --git a/jaq-core/src/compile.rs b/jaq-core/src/compile.rs index ff9521f4..13ede0fd 100644 --- a/jaq-core/src/compile.rs +++ b/jaq-core/src/compile.rs @@ -1,7 +1,7 @@ //! Program compilation. use crate::load::{self, lex, parse}; -use crate::{ops, Bind, Filter}; +use crate::{ops, Bind as Arg, Filter}; use alloc::collections::{BTreeMap, BTreeSet}; use alloc::{boxed::Box, string::String, vec::Vec}; @@ -9,7 +9,6 @@ type NativeId = usize; type ModId = usize; type VarId = usize; type VarSkip = usize; -type LabelSkip = usize; type Arity = usize; /// Index of a term in the look-up table. @@ -81,15 +80,14 @@ pub(crate) enum Term { /// Singleton object (`{f: g}`) ObjSingle(T, T), - /// Bound variable (`$x`) or filter argument (`a`) - Var(VarId, LabelSkip), + /// Bound variable (`$x`), label (`label $x`), or filter argument (`a`) + Var(VarId), /// Call to a filter (`filter`, `filter(…)`) - CallDef(TermId, Box<[Bind]>, VarSkip, Option), - Native(NativeId, Box<[Bind]>), + CallDef(TermId, Box<[Arg]>, VarSkip, Option), + Native(NativeId, Box<[Arg]>), + /// Binding of a break label (`label $x | f`) Label(T), - Break(usize), - /// Negation operation (`-f`) Neg(T), /// Variable binding (`f as $x | g`) if identifier (`x`) is given, otherwise @@ -233,22 +231,22 @@ impl Default for Compiler { } #[derive(Clone, Debug)] -struct Sig { +struct Sig { name: S, // TODO: we could analyse for each argument whether it is TR, and // use this when converting args at callsite args: Box<[A]>, } -fn bind_from(s: &str, x: T) -> Bind { +fn bind_from(s: &str, x: T) -> Arg { if s.starts_with('$') { - Bind::Var(x) + Arg::Var(x) } else { - Bind::Fun(x) + Arg::Fun(x) } } -fn binds(binds: &[Bind], args: &[U]) -> Box<[Bind]> { +fn binds(binds: &[Arg], args: &[U]) -> Box<[Arg]> { assert!(binds.len() == args.len()); let args = binds.iter().zip(args); args.map(|(bind, id)| bind.as_ref().map(|_| *id)).collect() @@ -270,7 +268,7 @@ impl Sig { } impl Def { - fn call(&self, args: Box<[Bind]>, vars: usize) -> Term { + fn call(&self, args: Box<[Arg]>, vars: usize) -> Term { // we pretend that the function call is tail-recursive, // and at the very end of compilation, we will correct calls // to non-tail-recursive functions @@ -279,12 +277,20 @@ impl Def { } /// Store a map of vectors plus the sum of the lengths of all vectors. -#[derive(Default)] struct MapVecLen { bound: MapVec, total: usize, } +impl Default for MapVecLen { + fn default() -> Self { + Self { + bound: MapVec::default(), + total: 0, + } + } +} + impl MapVecLen { fn push(&mut self, name: S) { self.total += 1; @@ -302,22 +308,40 @@ impl MapVecLen { } enum Fun { - // number of labels - Arg(usize), - Parent(Box<[Bind]>, Def), + Arg, + Parent(Box<[Arg]>, Def), // Tr is for tail-rec allowed funs - Sibling(Box<[Bind]>, Def, Tr), + Sibling(Box<[Arg]>, Def, Tr), +} + +/// Single binding. +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] +pub(crate) enum Bind { + /// binding to a variable + Var(V), + /// binding to a break label + Label(L), + /// binding to a filter + Fun(F), } -#[derive(Default)] struct Locals { // usize = number of vars funs: MapVec<(S, Arity), (Fun, usize)>, - vars: MapVecLen, - labels: MapVecLen, + vars: MapVecLen>, parents: Tr, } +impl Default for Locals { + fn default() -> Self { + Self { + funs: MapVec::default(), + vars: MapVecLen::default(), + parents: Tr::default(), + } + } +} + struct MapVec(BTreeMap>); impl Default for MapVec { @@ -354,7 +378,7 @@ impl MapVec { } impl Locals { - fn push_sibling(&mut self, name: S, args: Box<[Bind]>, def: Def) { + fn push_sibling(&mut self, name: S, args: Box<[Arg]>, def: Def) { let tr = self.parents.clone(); self.funs.push( (name, args.len()), @@ -362,7 +386,7 @@ impl Locals { ); } - fn pop_sibling(&mut self, name: S, arity: Arity) -> (Box<[Bind]>, Def, Tr) { + fn pop_sibling(&mut self, name: S, arity: Arity) -> (Box<[Arg]>, Def, Tr) { let (y, vars) = match self.funs.pop(&(name, arity)) { Some((Fun::Sibling(args, def, tr), vars)) => ((args, def, tr), vars), _ => panic!(), @@ -371,15 +395,15 @@ impl Locals { y } - fn push_parent(&mut self, name: S, args: Box<[Bind]>, def: Def) { + fn push_parent(&mut self, name: S, args: Box<[Arg]>, def: Def) { self.parents.insert(def.id); let vars = self.vars.total; for arg in args.iter() { match arg { - Bind::Var(v) => self.vars.push(*v), - Bind::Fun(f) => self.push_arg(*f), + Arg::Var(v) => self.vars.push(Bind::Var(*v)), + Arg::Fun(f) => self.push_arg(*f), } } @@ -394,8 +418,8 @@ impl Locals { }; for arg in args.iter().rev() { match arg { - Bind::Var(v) => self.vars.pop(v), - Bind::Fun(f) => self.pop_arg(*f), + Arg::Var(v) => self.vars.pop(&Bind::Var(*v)), + Arg::Fun(f) => self.pop_arg(*f), } } assert_eq!(self.vars.total, vars); @@ -404,26 +428,22 @@ impl Locals { } fn push_arg(&mut self, name: S) { - self.vars.total += 1; - self.funs - .push((name, 0), (Fun::Arg(self.labels.total), self.vars.total)); + self.vars.push(Bind::Fun(name)); + self.funs.push((name, 0), (Fun::Arg, self.vars.total)); } fn pop_arg(&mut self, name: S) { - let (labels, vars) = match self.funs.pop(&(name, 0)) { - Some((Fun::Arg(labels), vars)) => (labels, vars), + let vars = match self.funs.pop(&(name, 0)) { + Some((Fun::Arg, vars)) => vars, _ => panic!(), }; - assert_eq!(self.labels.total, labels); assert_eq!(self.vars.total, vars); - self.vars.total -= 1; + self.vars.pop(&Bind::Fun(name)); } fn call(&mut self, name: S, args: &[TermId], tr: &Tr) -> Option { Some(match self.funs.get_last_mut(&(name, args.len()))? { - (Fun::Arg(labels), vars) => { - Term::Var(self.vars.total - *vars, self.labels.total - *labels) - } + (Fun::Arg, vars) => Term::Var(self.vars.total - *vars), (Fun::Sibling(args_, def, tr_), vars) => { // we are at a position that may only call `tr` tail-recursively and // we call a sibling that may only call `tr_` tail-recursively, @@ -447,10 +467,7 @@ impl Locals { } fn is_empty(&self) -> bool { - self.funs.is_empty() - && self.vars.is_empty() - && self.labels.is_empty() - && self.parents.is_empty() + self.funs.is_empty() && self.vars.is_empty() && self.parents.is_empty() } } @@ -460,7 +477,7 @@ type Tr = BTreeSet; impl<'s, F> Compiler<&'s str, F> { /// Supply functions with given signatures. - pub fn with_funs(mut self, funs: impl IntoIterator, F)>) -> Self { + pub fn with_funs(mut self, funs: impl IntoIterator, F)>) -> Self { self.lut.funs = funs .into_iter() .map(|(name, args, f)| (Sig { name, args }, f)) @@ -533,16 +550,19 @@ impl<'s, F> Compiler<&'s str, F> { } fn with_label(&mut self, label: &'s str, f: impl FnOnce(&mut Self) -> T) -> T { - self.locals.labels.push(label); + self.locals.vars.push(Bind::Label(label)); let y = f(self); - self.locals.labels.pop(&label); + self.locals.vars.pop(&Bind::Label(label)); y } fn with_vars(&mut self, vars: &[&'s str], f: impl FnOnce(&mut Self) -> T) -> T { - vars.iter().for_each(|v| self.locals.vars.push(v)); + vars.iter() + .for_each(|v| self.locals.vars.push(Bind::Var(v))); let y = f(self); - vars.iter().rev().for_each(|v| self.locals.vars.pop(v)); + vars.iter() + .rev() + .for_each(|v| self.locals.vars.pop(&Bind::Var(v))); y } @@ -583,7 +603,7 @@ impl<'s, F> Compiler<&'s str, F> { } /// Compile a placeholder sibling with its corresponding definition. - fn def_post(&mut self, d: parse::Def<&'s str>) -> (Sig<&'s str, Bind>, Def) { + fn def_post(&mut self, d: parse::Def<&'s str>) -> (Sig<&'s str, Arg>, Def) { let (args, def, mut tr) = self.locals.pop_sibling(d.name, d.args.len()); let tid = def.id; self.locals.push_parent(d.name, args, def); @@ -808,19 +828,19 @@ impl<'s, F> Compiler<&'s str, F> { fn var(&mut self, x: &'s str) -> Term { let mut i = self.locals.vars.total; - if let Some(v) = self.locals.vars.bound.get_last(&x) { - return Term::Var(i - v, 0); + if let Some(v) = self.locals.vars.bound.get_last(&Bind::Var(x)) { + return Term::Var(i - v); } for (x_, mid) in self.imported_vars.iter().rev() { if x == *x_ && *mid == self.mod_map.len() { - return Term::Var(i, 0); + return Term::Var(i); } else { i += 1; } } for x_ in self.global_vars.iter().rev() { if x == *x_ { - return Term::Var(i, 0); + return Term::Var(i); } else { i += 1; } @@ -829,8 +849,8 @@ impl<'s, F> Compiler<&'s str, F> { } fn break_(&mut self, x: &'s str) -> Term { - if let Some(l) = self.locals.labels.bound.get_last(&x) { - return Term::Break(self.locals.labels.total - l); + if let Some(l) = self.locals.vars.bound.get_last(&Bind::Label(x)) { + return Term::Var(self.locals.vars.total - l); } self.fail(x, Undefined::Label) } diff --git a/jaq-core/src/exn.rs b/jaq-core/src/exn.rs index aa989fa2..126fea67 100644 --- a/jaq-core/src/exn.rs +++ b/jaq-core/src/exn.rs @@ -17,7 +17,7 @@ pub(crate) enum Inner<'a, V> { /// /// This is used internally to execute tail-recursive filters. /// If this can be observed by users, then this is a bug. - TailCall(&'a crate::compile::TermId, crate::Vars<'a, V>, V), + TailCall(&'a crate::compile::TermId, crate::filter::Vars<'a, V>, V), Break(usize), } diff --git a/jaq-core/src/filter.rs b/jaq-core/src/filter.rs index 6474919b..d9800b14 100644 --- a/jaq-core/src/filter.rs +++ b/jaq-core/src/filter.rs @@ -1,13 +1,13 @@ +//! Filter execution. + use crate::box_iter::{self, box_once, flat_map_then, flat_map_then_with, flat_map_with, map_with}; -use crate::compile::{Fold, Lut, Pattern, Tailrec, Term as Ast}; +use crate::compile::{Bind, Fold, Lut, Pattern, Tailrec, Term as Ast, TermId as Id}; use crate::fold::fold; use crate::val::{ValT, ValX, ValXs}; -use crate::{exn, rc_lazy_list, Bind, Ctx, Error, Exn}; +use crate::{exn, rc_lazy_list, Bind as Arg, Error, Exn, Inputs, RcList}; use alloc::boxed::Box; use dyn_clone::DynClone; -pub(crate) use crate::compile::TermId as Id; - // we can unfortunately not make a `Box` // that is why we have to go through the pain of making a new trait here pub trait Update<'a, V>: Fn(V) -> ValXs<'a, V> + DynClone {} @@ -18,31 +18,129 @@ dyn_clone::clone_trait_object!(<'a, V> Update<'a, V>); type BoxUpdate<'a, V> = Box + 'a>; -type Results<'a, T, V> = crate::box_iter::Results<'a, T, Exn<'a, V>>; +type Results<'a, T, V> = box_iter::Results<'a, T, Exn<'a, V>>; + +/// List of bindings. +#[derive(Clone, Debug, PartialEq, Eq)] +pub(crate) struct Vars<'a, V>(RcList>); + +impl<'a, V> Vars<'a, V> { + fn get(&self, i: usize) -> Option<&Bind> { + self.0.get(i) + } +} + +/// Filter execution context. +#[derive(Clone)] +pub struct Ctx<'a, V> { + vars: Vars<'a, V>, + /// Number of bound labels at the current path + /// + /// This is used to create fresh break IDs. + labels: usize, + inputs: &'a Inputs<'a, V>, +} + +impl<'a, V> Ctx<'a, V> { + /// Construct a context. + pub fn new(vars: impl IntoIterator, inputs: &'a Inputs<'a, V>) -> Self { + Self { + vars: Vars(RcList::new().extend(vars.into_iter().map(Bind::Var))), + labels: 0, + inputs, + } + } + + /// Add a new variable binding. + fn cons_var(mut self, x: V) -> Self { + self.vars.0 = self.vars.0.cons(Bind::Var(x)); + self + } + + /// Add a new filter binding. + fn cons_fun(mut self, (f, ctx): (&'a Id, Self)) -> Self { + self.vars.0 = self.vars.0.cons(Bind::Fun((f, ctx.vars))); + self + } + + fn cons_label(mut self) -> Self { + self.labels += 1; + self.vars.0 = self.vars.0.cons(Bind::Label(self.labels)); + self + } + + /// Remove the `skip` most recent variable bindings. + fn skip_vars(mut self, skip: usize) -> Self { + if skip > 0 { + self.vars.0 = self.vars.0.skip(skip).clone(); + } + self + } + + /// Replace variables in context with given ones. + fn with_vars(&self, vars: Vars<'a, V>) -> Self { + Self { + vars, + labels: self.labels, + inputs: self.inputs, + } + } + + /// Return remaining input values. + pub fn inputs(&self) -> &'a Inputs<'a, V> { + self.inputs + } +} + +impl<'a, V: Clone> Ctx<'a, V> { + /// Remove the latest bound variable from the context. + /// + /// This is useful for writing [`Native`] filters. + pub fn pop_var(&mut self) -> V { + let (head, tail) = match core::mem::take(&mut self.vars.0).pop() { + Some((Bind::Var(head), tail)) => (head, tail), + _ => panic!(), + }; + self.vars.0 = tail; + head + } + + /// Remove the latest bound function from the context. + /// + /// This is useful for writing [`Native`] filters. + pub fn pop_fun(&mut self) -> (&'a Id, Self) { + let ((id, vars), tail) = match core::mem::take(&mut self.vars.0).pop() { + Some((Bind::Fun(head), tail)) => (head, tail), + _ => panic!(), + }; + self.vars.0 = tail; + (id, self.with_vars(vars)) + } +} /// Enhance the context `ctx` with variables bound to the outputs of `args` executed on `cv`, /// and return the enhanced contexts together with the original value of `cv`. /// /// This is used when we call filters with variable arguments. fn bind_vars<'a, F: FilterT>( - args: &'a [Bind], + args: &'a [Arg], lut: &'a Lut, ctx: Ctx<'a, F::V>, cv: Cv<'a, F::V>, ) -> Results<'a, Cv<'a, F::V>, F::V> { match args.split_first() { - Some((Bind::Var(arg), [])) => { + Some((Arg::Var(arg), [])) => { map_with(arg.run(lut, cv.clone()), (ctx, cv.1), |y, (ctx, v)| { Ok((ctx.cons_var(y?), v)) }) } - Some((Bind::Fun(arg), [])) => box_once(Ok((ctx.cons_fun((arg, cv.0)), cv.1))), - Some((Bind::Var(arg), rest)) => { + Some((Arg::Fun(arg), [])) => box_once(Ok((ctx.cons_fun((arg, cv.0)), cv.1))), + Some((Arg::Var(arg), rest)) => { flat_map_then_with(arg.run(lut, cv.clone()), (ctx, cv), |y, (ctx, cv)| { bind_vars(rest, lut, ctx.cons_var(y), cv) }) } - Some((Bind::Fun(arg), rest)) => bind_vars(rest, lut, ctx.cons_fun((arg, cv.0.clone())), cv), + Some((Arg::Fun(arg), rest)) => bind_vars(rest, lut, ctx.cons_fun((arg, cv.0.clone())), cv), None => box_once(Ok((ctx, cv.1))), } } @@ -105,16 +203,6 @@ where Box::new(fold(xs, init, f, |_| (), |_, _| None, Some)) } -fn label_skip<'a, V: 'a>(ys: ValXs<'a, V>, skip: usize) -> ValXs<'a, V> { - if skip == 0 { - return ys; - } - Box::new(ys.map(move |y| match y { - Err(Exn(exn::Inner::Break(n))) => Err(Exn(exn::Inner::Break(n + skip))), - y => y, - })) -} - fn lazy I>(f: F) -> impl Iterator { core::iter::once_with(f).flatten() } @@ -301,15 +389,18 @@ impl> FilterT for Id { }) } - Ast::Var(v, skip) => match cv.0.vars.get(*v).unwrap() { + Ast::Var(v) => match cv.0.vars.get(*v).unwrap() { Bind::Var(v) => box_once(Ok(v.clone())), - Bind::Fun((id, vars)) => { - label_skip(id.run(lut, (cv.0.with_vars(vars.clone()), cv.1)), *skip) - } + Bind::Fun((id, vars)) => id.run(lut, (cv.0.with_vars(vars.clone()), cv.1)), + Bind::Label(l) => box_once(Err(Exn(exn::Inner::Break(*l)))), }, Ast::CallDef(id, args, skip, tailrec) => { use core::ops::ControlFlow; - let inputs = cv.0.inputs; + let with_vars = move |vars| Ctx { + vars, + labels: cv.0.labels, + inputs: cv.0.inputs, + }; let cvs = bind_vars(args, lut, cv.0.clone().skip_vars(*skip), cv); match tailrec { None => flat_map_then(cvs, |cv| id.run(lut, cv)), @@ -317,7 +408,7 @@ impl> FilterT for Id { [flat_map_then(cvs, |cv| id.run(lut, cv))].into(), move |r| match r { Err(Exn(exn::Inner::TailCall(id_, vars, v))) if id == id_ => { - ControlFlow::Continue(id.run(lut, (Ctx { vars, inputs }, v))) + ControlFlow::Continue(id.run(lut, (with_vars(vars), v))) } Ok(_) | Err(_) => ControlFlow::Break(r), }, @@ -331,13 +422,14 @@ impl> FilterT for Id { let cvs = bind_vars(args, lut, Ctx::new([], cv.0.inputs), cv); flat_map_then(cvs, |cv| lut.funs[*id].run(lut, cv)) } - Ast::Label(id) => Box::new(id.run(lut, cv).map_while(|y| match y { - Err(Exn(exn::Inner::Break(n))) => { - n.checked_sub(1).map(|m| Err(Exn(exn::Inner::Break(m)))) - } - y => Some(y), - })), - Ast::Break(skip) => box_once(Err(Exn(exn::Inner::Break(*skip)))), + Ast::Label(id) => { + let ctx = cv.0.cons_label(); + let labels = ctx.labels; + Box::new(id.run(lut, (ctx, cv.1)).map_while(move |y| match y { + Err(Exn(exn::Inner::Break(b))) if b == labels => None, + y => Some(y), + })) + } } } @@ -357,7 +449,7 @@ impl> FilterT for Id { // jq implements updates on `try ... catch` and `label`, but // I do not see how to implement this in jaq // folding, however, could be done, even if jq does not support it - Ast::TryCatch(..) | Ast::Label(_) | Ast::Fold(..) => err, + Ast::TryCatch(..) | Ast::Label(..) | Ast::Fold(..) => err, Ast::Id => f(cv.1), Ast::Path(l, path) => { @@ -396,12 +488,10 @@ impl> FilterT for Id { if some_true { l } else { r }.update(lut, cv, f) } - Ast::Var(v, skip) => match cv.0.vars.get(*v).unwrap() { + Ast::Var(v) => match cv.0.vars.get(*v).unwrap() { Bind::Var(_) => err, - Bind::Fun(l) => label_skip( - l.0.update(lut, (cv.0.with_vars(l.1.clone()), cv.1), f), - *skip, - ), + Bind::Fun(l) => l.0.update(lut, (cv.0.with_vars(l.1.clone()), cv.1), f), + Bind::Label(l) => box_once(Err(Exn(exn::Inner::Break(*l)))), }, Ast::CallDef(id, args, skip, _tailrec) => { let init = cv.1.clone(); @@ -415,7 +505,6 @@ impl> FilterT for Id { lut.funs[*id].update(lut, (cv.0, v), f.clone()) }) } - Ast::Break(skip) => box_once(Err(Exn(exn::Inner::Break(*skip)))), } } } diff --git a/jaq-core/src/lib.rs b/jaq-core/src/lib.rs index fbb0ac0c..f1334fec 100644 --- a/jaq-core/src/lib.rs +++ b/jaq-core/src/lib.rs @@ -67,7 +67,7 @@ pub mod val; pub use compile::Compiler; pub use exn::{Error, Exn}; -pub use filter::{Cv, FilterT, Native, RunPtr, UpdatePtr}; +pub use filter::{Ctx, Cv, FilterT, Native, RunPtr, UpdatePtr}; pub use rc_iter::RcIter; pub use val::{ValR, ValT, ValX, ValXs}; @@ -75,9 +75,6 @@ use alloc::string::String; use rc_list::List as RcList; use stack::Stack; -/// Variable bindings. -#[derive(Clone, Debug, PartialEq, Eq)] -struct Vars<'a, V>(RcList>); type Inputs<'i, V> = RcIter> + 'i>; /// Argument of a definition, such as `$v` or `f` in `def foo($v; f): ...`. @@ -121,85 +118,6 @@ impl Bind { } } -impl<'a, V> Vars<'a, V> { - fn get(&self, i: usize) -> Option<&Bind> { - self.0.get(i) - } -} - -/// Filter execution context. -#[derive(Clone)] -pub struct Ctx<'a, V> { - vars: Vars<'a, V>, - inputs: &'a Inputs<'a, V>, -} - -impl<'a, V> Ctx<'a, V> { - /// Construct a context. - pub fn new(vars: impl IntoIterator, inputs: &'a Inputs<'a, V>) -> Self { - let vars = Vars(RcList::new().extend(vars.into_iter().map(Bind::Var))); - Self { vars, inputs } - } - - /// Add a new variable binding. - fn cons_var(mut self, x: V) -> Self { - self.vars.0 = self.vars.0.cons(Bind::Var(x)); - self - } - - /// Add a new filter binding. - fn cons_fun(mut self, (f, ctx): (&'a filter::Id, Self)) -> Self { - self.vars.0 = self.vars.0.cons(Bind::Fun((f, ctx.vars))); - self - } - - /// Remove the `skip` most recent variable bindings. - fn skip_vars(mut self, skip: usize) -> Self { - if skip > 0 { - self.vars.0 = self.vars.0.skip(skip).clone(); - } - self - } - - /// Replace variables in context with given ones. - fn with_vars(&self, vars: Vars<'a, V>) -> Self { - let inputs = self.inputs; - Self { vars, inputs } - } - - /// Return remaining input values. - pub fn inputs(&self) -> &'a Inputs<'a, V> { - self.inputs - } -} - -impl<'a, V: Clone> Ctx<'a, V> { - /// Remove the latest bound variable from the context. - /// - /// This is useful for writing [`Native`] filters. - pub fn pop_var(&mut self) -> V { - let (head, tail) = match core::mem::take(&mut self.vars.0).pop() { - Some((Bind::Var(head), tail)) => (head, tail), - _ => panic!(), - }; - self.vars.0 = tail; - head - } - - /// Remove the latest bound function from the context. - /// - /// This is useful for writing [`Native`] filters. - pub fn pop_fun(&mut self) -> (&'a filter::Id, Self) { - let ((id, vars), tail) = match core::mem::take(&mut self.vars.0).pop() { - Some((Bind::Fun(head), tail)) => (head, tail), - _ => panic!(), - }; - let inputs = self.inputs; - self.vars.0 = tail; - (id, Self { vars, inputs }) - } -} - /// Function from a value to a stream of value results. #[derive(Debug, Clone)] pub struct Filter(compile::TermId, compile::Lut); diff --git a/jaq-core/tests/tests.rs b/jaq-core/tests/tests.rs index 88429cda..ab631a58 100644 --- a/jaq-core/tests/tests.rs +++ b/jaq-core/tests/tests.rs @@ -235,6 +235,12 @@ yields!( [1, 2] ); +yields!( + label_break_common, + "[label $x | (def x: break $x; (label $y | x), 0)]", + json!([]) +); + yields!( try_catch_short_circuit, "[try (\"1\", \"2\", {}[0], \"4\") catch .]",