From 268ddd667cbd13e12e269cb6e1a12b40388abca5 Mon Sep 17 00:00:00 2001 From: Andrew Lees Date: Wed, 29 Apr 2026 23:12:53 +0100 Subject: [PATCH 01/14] `halt` no longer directly calls `std::process::exit` Modifies the `halt` builtin to return a kind of `Exn` that, by default, exits the process when unwrapped. This allows embedders (library users) to handle `halt` calls themselves to e.g. return a custom error when running a filter, rather than taking down the whole process. --- jaq-all/src/data.rs | 21 +++++++++++++++++++-- jaq-core/src/exn.rs | 21 +++++++++++++++++++++ jaq-core/src/val.rs | 9 ++++++++- jaq-std/src/lib.rs | 6 +++++- 4 files changed, 53 insertions(+), 4 deletions(-) diff --git a/jaq-all/src/data.rs b/jaq-all/src/data.rs index 361488599..51be4067e 100644 --- a/jaq-all/src/data.rs +++ b/jaq-all/src/data.rs @@ -2,7 +2,7 @@ use crate::{compile_with, FileReports, Fun}; use jaq_core::{data, unwrap_valr, DataT, Lut, Vars}; use jaq_fmts::write::Writer; -use jaq_json::{Val, ValR}; +use jaq_json::{Val, ValR, ValX}; use jaq_std::input::{self, Inputs, RcIter}; /// Filter for given kind of data. @@ -85,6 +85,9 @@ pub fn compile(code: &str) -> Result> { /// Run a filter with given input values and run `f` for every value output. /// +/// This will call [`std::process::exit`] if the filter calls `halt/1` - use [`run_exns`] and manually check +/// [`Exn::exit_code`](jaq_core::Exn::exit_code) to change this behaviour. +/// /// This function cannot return an `Iterator` because it creates an `RcIter`. /// This is most unfortunate. We should think about how to simplify this ... pub fn run( @@ -94,6 +97,20 @@ pub fn run( inputs: impl Iterator>, fi: impl Fn(String) -> E, mut f: impl FnMut(ValR) -> Result<(), E>, +) -> Result<(), E> { + run_exns(runner, filter, vars, inputs, fi, |r| f(unwrap_valr(r))) +} + +/// Run a filter with given input values and run `f` for every value output, handling exceptions manually. +/// +/// You will likely need to use the [`unwrap_valr`] function to convert [`ValX`] to [`ValR`]. +pub fn run_exns( + runner: &Runner, + filter: &Filter, + vars: Vars, + inputs: impl Iterator>, + fi: impl Fn(String) -> E, + mut f: impl FnMut(ValX) -> Result<(), E>, ) -> Result<(), E> { let inputs = Box::new(inputs.map(|r| r.map_err(|e| e.to_string()))); let null = Box::new(core::iter::once(Ok(Val::Null))); @@ -109,7 +126,7 @@ pub fn run( let outputs = |x| filter.id.run((ctx.clone(), x)); (if runner.null_input { null } else { data.inputs }).try_for_each(|x| match x { - Ok(x) => outputs(x).try_for_each(|y| f(unwrap_valr(y))), + Ok(x) => outputs(x).try_for_each(&mut f), Err(e) => Err(fi(e)), }) } diff --git a/jaq-core/src/exn.rs b/jaq-core/src/exn.rs index 5e2a07ade..74f5c50dd 100644 --- a/jaq-core/src/exn.rs +++ b/jaq-core/src/exn.rs @@ -22,6 +22,7 @@ pub(crate) enum Inner<'a, V> { /// If this can be observed by users, then this is a bug. TailCall(Box<(&'a TermId, Vars, CallInput)>), Break(usize), + Halt(isize), } #[derive(Clone, Debug)] @@ -54,6 +55,26 @@ impl Exn<'_, V> { _ => Err(self), } } + + /// Create an exception intended to halt filter execution, such as for the + /// `halt/1` filter. + /// + /// See [`Self::exit_code`] for more details. + pub fn halt(exit_code: isize) -> Self { + Self(Inner::Halt(exit_code)) + } + + /// Returns the exit code passed to [`Self::halt`], if any. + /// + /// Exceptions with a `Some(_)` return value are precisely those that would cause + /// [`unwrap_valr`](crate::val::unwrap_valr) to call [`exit`](std::process::exit). + pub fn exit_code(&self) -> Option { + if let Inner::Halt(exit_code) = self.0 { + Some(exit_code) + } else { + None + } + } } impl From> for Exn<'_, V> { diff --git a/jaq-core/src/val.rs b/jaq-core/src/val.rs index 86cb50630..556f0a56e 100644 --- a/jaq-core/src/val.rs +++ b/jaq-core/src/val.rs @@ -27,11 +27,18 @@ pub type ValXs<'a, T, V = T> = BoxIter<'a, ValX<'a, T, V>>; /// /// This should always succeed when called on results of a main filter. /// For any other filter, this may not succeed, i.e. panic. +/// Note that this will call [`std::process::exit`] if passed an exception +/// produced by [`Exn::halt`](crate::Exn::halt). /// /// If you are writing a native filter, e.g. `f(f1; ...; fn)`, /// do not use this function on outputs of `fi`! pub fn unwrap_valr(v: ValX) -> ValR { - v.map_err(|e| e.get_err().ok().unwrap()) + v.map_err(|e| { + if let Some(exit_code) = e.exit_code() { + std::process::exit(exit_code as i32) + } + e.get_err().ok().unwrap() + }) } /// Range of options, used for iteration operations. diff --git a/jaq-std/src/lib.rs b/jaq-std/src/lib.rs index 3a7a2c1c3..f91bbd310 100644 --- a/jaq-std/src/lib.rs +++ b/jaq-std/src/lib.rs @@ -469,7 +469,11 @@ where ("now", v(0), |_| bome(now().map(D::V::from))), ("halt", v(1), |mut cv| { let exit_code = cv.0.pop_var().try_as_isize(); - bome(exit_code.map(|exit_code| std::process::exit(exit_code as i32))) + box_once( + exit_code + .map_err(Exn::from) + .and_then(|exit_code| Err(Exn::halt(exit_code))), + ) }), ]) } From d5ae99c414ab0574f89487521b42f09ae03080b4 Mon Sep 17 00:00:00 2001 From: Andrew Lees Date: Fri, 1 May 2026 12:39:57 +0100 Subject: [PATCH 02/14] Use `i32` instead of `isize` for valid `halt` exit codes Also update the docs to mention platform-specific limitations on process exit codes. --- docs/stdlib.dj | 5 +++++ jaq-core/src/exn.rs | 6 +++--- jaq-core/src/val.rs | 2 +- jaq-std/src/lib.rs | 2 +- 4 files changed, 10 insertions(+), 5 deletions(-) diff --git a/docs/stdlib.dj b/docs/stdlib.dj index 0599204ed..1971778d0 100644 --- a/docs/stdlib.dj +++ b/docs/stdlib.dj @@ -2222,6 +2222,11 @@ $ jaq -n '1, halt(42), 2'; echo $? `jq` does not implement `halt($exit_code)`, only `halt`. ::: +Note that not all values for `$exit_code` will work as intended on all platforms: see the +[Rust documentation on `std::process::exit`](https://doc.rust-lang.org/stable/std/process/fn.exit.html#platform-specific-behavior) +for more information. As a general rule of thumb, the 8-bit signed integers (-128 to 127 inclusive) are safe to use and +anything outside of that range _may_ be silently truncated by the operating system. + {#halt_error} ### `halt_error`, `halt_error($exit_code)` diff --git a/jaq-core/src/exn.rs b/jaq-core/src/exn.rs index 74f5c50dd..9f0f32ab9 100644 --- a/jaq-core/src/exn.rs +++ b/jaq-core/src/exn.rs @@ -22,7 +22,7 @@ pub(crate) enum Inner<'a, V> { /// If this can be observed by users, then this is a bug. TailCall(Box<(&'a TermId, Vars, CallInput)>), Break(usize), - Halt(isize), + Halt(i32), } #[derive(Clone, Debug)] @@ -60,7 +60,7 @@ impl Exn<'_, V> { /// `halt/1` filter. /// /// See [`Self::exit_code`] for more details. - pub fn halt(exit_code: isize) -> Self { + pub fn halt(exit_code: i32) -> Self { Self(Inner::Halt(exit_code)) } @@ -68,7 +68,7 @@ impl Exn<'_, V> { /// /// Exceptions with a `Some(_)` return value are precisely those that would cause /// [`unwrap_valr`](crate::val::unwrap_valr) to call [`exit`](std::process::exit). - pub fn exit_code(&self) -> Option { + pub fn exit_code(&self) -> Option { if let Inner::Halt(exit_code) = self.0 { Some(exit_code) } else { diff --git a/jaq-core/src/val.rs b/jaq-core/src/val.rs index 556f0a56e..e8446c9b8 100644 --- a/jaq-core/src/val.rs +++ b/jaq-core/src/val.rs @@ -35,7 +35,7 @@ pub type ValXs<'a, T, V = T> = BoxIter<'a, ValX<'a, T, V>>; pub fn unwrap_valr(v: ValX) -> ValR { v.map_err(|e| { if let Some(exit_code) = e.exit_code() { - std::process::exit(exit_code as i32) + std::process::exit(exit_code) } e.get_err().ok().unwrap() }) diff --git a/jaq-std/src/lib.rs b/jaq-std/src/lib.rs index f91bbd310..df158e37b 100644 --- a/jaq-std/src/lib.rs +++ b/jaq-std/src/lib.rs @@ -468,7 +468,7 @@ where }), ("now", v(0), |_| bome(now().map(D::V::from))), ("halt", v(1), |mut cv| { - let exit_code = cv.0.pop_var().try_as_isize(); + let exit_code = cv.0.pop_var().try_as_i32(); box_once( exit_code .map_err(Exn::from) From 895bda10b6805e32eeca3598dfb11ef2067aefb9 Mon Sep 17 00:00:00 2001 From: Andrew Lees Date: Tue, 5 May 2026 22:03:18 +0100 Subject: [PATCH 03/14] Remove the `unwrap_valr` free function in favour of an `Exn::handle` method This removes the call to `std::process::exit` within `jaq-core`, which would not be compatibl with `#![no_std]`. Incidentally, this also makes the web playground print a message with the exit code passed to `halt/1`, if any, as explicitly handling halt/error cases is required to pass typecheck. --- jaq-all/examples/main.rs | 7 ++++++- jaq-all/src/data.rs | 21 ++------------------- jaq-core/examples/repl.rs | 10 ++++++++-- jaq-core/src/exn.rs | 39 +++++++++++++++++---------------------- jaq-core/src/lib.rs | 14 ++++++++++---- jaq-core/src/val.rs | 20 +------------------- jaq-play/src/lib.rs | 8 +++++--- jaq/src/filter.rs | 2 +- jaq/src/main.rs | 6 ++++++ jaq/src/tests.rs | 2 +- 10 files changed, 57 insertions(+), 72 deletions(-) diff --git a/jaq-all/examples/main.rs b/jaq-all/examples/main.rs index 1cc93bd0b..0b1ba9b45 100644 --- a/jaq-all/examples/main.rs +++ b/jaq-all/examples/main.rs @@ -18,7 +18,12 @@ fn main() -> io::Result<()> { let fi = |e| Error::new(ErrorKind::InvalidData, e); data::run(&runner, &filter, vars, inputs, fi, |v| { - let v = v.map_err(|e| Error::new(ErrorKind::Other, e.to_string())); + let v = v.map_err(|e| { + e.handle( + |e| Error::new(ErrorKind::Other, e.to_string()), + |exit_code| std::process::exit(exit_code), + ) + }); fmts::write::write(stdout, &runner.writer, &v?) }) } diff --git a/jaq-all/src/data.rs b/jaq-all/src/data.rs index 51be4067e..9222e25c9 100644 --- a/jaq-all/src/data.rs +++ b/jaq-all/src/data.rs @@ -1,8 +1,8 @@ //! Commonly used data for filter compilation & execution. use crate::{compile_with, FileReports, Fun}; -use jaq_core::{data, unwrap_valr, DataT, Lut, Vars}; +use jaq_core::{DataT, Lut, Vars, data}; use jaq_fmts::write::Writer; -use jaq_json::{Val, ValR, ValX}; +use jaq_json::{Val, ValX}; use jaq_std::input::{self, Inputs, RcIter}; /// Filter for given kind of data. @@ -85,26 +85,9 @@ pub fn compile(code: &str) -> Result> { /// Run a filter with given input values and run `f` for every value output. /// -/// This will call [`std::process::exit`] if the filter calls `halt/1` - use [`run_exns`] and manually check -/// [`Exn::exit_code`](jaq_core::Exn::exit_code) to change this behaviour. -/// /// This function cannot return an `Iterator` because it creates an `RcIter`. /// This is most unfortunate. We should think about how to simplify this ... pub fn run( - runner: &Runner, - filter: &Filter, - vars: Vars, - inputs: impl Iterator>, - fi: impl Fn(String) -> E, - mut f: impl FnMut(ValR) -> Result<(), E>, -) -> Result<(), E> { - run_exns(runner, filter, vars, inputs, fi, |r| f(unwrap_valr(r))) -} - -/// Run a filter with given input values and run `f` for every value output, handling exceptions manually. -/// -/// You will likely need to use the [`unwrap_valr`] function to convert [`ValX`] to [`ValR`]. -pub fn run_exns( runner: &Runner, filter: &Filter, vars: Vars, diff --git a/jaq-core/examples/repl.rs b/jaq-core/examples/repl.rs index 72ba8517b..8ec28209e 100644 --- a/jaq-core/examples/repl.rs +++ b/jaq-core/examples/repl.rs @@ -9,7 +9,7 @@ //! rlwrap cargo run --example repl use jaq_core::load::{Arena, File, Loader}; -use jaq_core::{data, unwrap_valr, Compiler, Ctx, Vars}; +use jaq_core::{Compiler, Ctx, Vars, data}; use jaq_json::{write, Val}; use std::io::{stdin, stdout, Write}; @@ -29,7 +29,13 @@ fn eval_print(code: &str) -> std::io::Result<()> { let mut stdout = stdout().lock(); // iterator over the output values - for y in filter.id.run((ctx, Val::default())).map(unwrap_valr) { + for y in filter.id.run((ctx, Val::default())).map(|v| { + v.map_err(|x| { + x.handle(std::convert::identity, |exit_code| { + std::process::exit(exit_code) + }) + }) + }) { write::write(&mut stdout, &write::Pp::default(), 0, &y.unwrap())?; writeln!(stdout)?; } diff --git a/jaq-core/src/exn.rs b/jaq-core/src/exn.rs index 9f0f32ab9..21ec30f7b 100644 --- a/jaq-core/src/exn.rs +++ b/jaq-core/src/exn.rs @@ -6,10 +6,8 @@ use core::fmt::{self, Display}; /// Exception. /// -/// This is either an error or control flow data internal to jaq. -/// Users should only be able to observe errors. -/// -/// Use [`crate::val::unwrap_valr`] to convert a [`crate::val::ValX`] to an error. +/// This is either an error, a runtime halt, or control flow data internal to jaq. +/// Users should only be able to observe the first two cases. #[derive(Clone, Debug)] pub struct Exn<'a, V>(pub(crate) Inner<'a, V>); @@ -48,33 +46,30 @@ impl CallInput { } impl Exn<'_, V> { - /// If the exception is an error, yield it, else yield the exception. - pub(crate) fn get_err(self) -> Result, Self> { + /// Handle the exception kinds that can be returned from executing a main filter. + /// + /// For any other filter, this may not succeed, i.e. panic. + /// + /// If you are writing a native filter, e.g. `f(f1; ...; fn)`, + /// do not use this method on outputs of `fi`! + pub fn handle(self, err: impl FnOnce(Error) -> T, halt: impl FnOnce(i32) -> T) -> T { match self.0 { - Inner::Err(e) => Ok(*e), - _ => Err(self), + Inner::Err(e) => err(*e), + Inner::Halt(exit_code) => halt(exit_code), + Inner::TailCall(_) => { + panic!("tried to handle an internal exception variant: TailCall") + } + Inner::Break(_) => { + panic!("tried to handle an internal exception variant: Break") + } } } /// Create an exception intended to halt filter execution, such as for the /// `halt/1` filter. - /// - /// See [`Self::exit_code`] for more details. pub fn halt(exit_code: i32) -> Self { Self(Inner::Halt(exit_code)) } - - /// Returns the exit code passed to [`Self::halt`], if any. - /// - /// Exceptions with a `Some(_)` return value are precisely those that would cause - /// [`unwrap_valr`](crate::val::unwrap_valr) to call [`exit`](std::process::exit). - pub fn exit_code(&self) -> Option { - if let Inner::Halt(exit_code) = self.0 { - Some(exit_code) - } else { - None - } - } } impl From> for Exn<'_, V> { diff --git a/jaq-core/src/lib.rs b/jaq-core/src/lib.rs index b607a19f7..2c3d994f1 100644 --- a/jaq-core/src/lib.rs +++ b/jaq-core/src/lib.rs @@ -7,7 +7,7 @@ //! more complex use cases, such as lazy JSON file loading, error handling etc. //! //! ~~~ -//! use jaq_core::{data, unwrap_valr, Compiler, Ctx, Vars}; +//! use jaq_core::{data, Compiler, Ctx, Vars}; //! use jaq_core::load::{Arena, File, Loader}; //! use jaq_json::{read, Val}; //! @@ -34,7 +34,7 @@ //! // context for filter execution //! let ctx = Ctx::>::new(&filter.lut, Vars::new([])); //! // iterator over the output values -//! let mut out = filter.id.run((ctx, input)).map(unwrap_valr); +//! let mut out = filter.id.run((ctx, input)).map(|r| r.map_err(|x| x.handle(std::convert::identity, |exit_code| panic!("halt({exit_code})")))); //! //! assert_eq!(out.next(), Some(Ok(Val::from("Hello".to_owned()))));; //! assert_eq!(out.next(), Some(Ok(Val::from("world".to_owned()))));; @@ -68,7 +68,7 @@ pub mod val; pub use data::DataT; pub use exn::{Error, Exn}; pub use filter::{Ctx, Cv, Native, PathsPtr, RunPtr, UpdatePtr, Vars}; -pub use val::{unwrap_valr, ValR, ValT, ValX, ValXs}; +pub use val::{ValR, ValT, ValX, ValXs}; use rc_list::List as RcList; use stack::Stack; @@ -127,7 +127,13 @@ impl Filter> { /// This is for testing purposes. pub fn yields(&self, x: V, ys: impl Iterator>) { let ctx = Ctx::>::new(&self.lut, Vars::new([])); - let out = self.id.run((ctx, x)).map(unwrap_valr); + let out = self.id.run((ctx, x)).map(|valx| { + valx.map_err(|x| { + x.handle(std::convert::identity, |exit_code| { + panic!("tests shouldn't call halt/1 (code {exit_code})") + }) + }) + }); assert!(out.eq(ys)); } } diff --git a/jaq-core/src/val.rs b/jaq-core/src/val.rs index e8446c9b8..bbbf8d0f9 100644 --- a/jaq-core/src/val.rs +++ b/jaq-core/src/val.rs @@ -18,29 +18,11 @@ pub type ValR = Result>; pub type ValRs<'a, T, V = T> = BoxIter<'a, ValR>; /// Value or eXception. /// -/// Use [`unwrap_valr`] to convert to [`ValR`]. +/// Use [`Exn::handle`](crate::Exn::handle) to extract the [`Error`](crate::Error) from an [`Exn`](crate::Exn). pub type ValX<'a, T, V = T> = Result>; /// Stream of values and eXceptions. pub type ValXs<'a, T, V = T> = BoxIter<'a, ValX<'a, T, V>>; -/// Convert a value exception [`ValX`] into a value result [`ValR`]. -/// -/// This should always succeed when called on results of a main filter. -/// For any other filter, this may not succeed, i.e. panic. -/// Note that this will call [`std::process::exit`] if passed an exception -/// produced by [`Exn::halt`](crate::Exn::halt). -/// -/// If you are writing a native filter, e.g. `f(f1; ...; fn)`, -/// do not use this function on outputs of `fi`! -pub fn unwrap_valr(v: ValX) -> ValR { - v.map_err(|e| { - if let Some(exit_code) = e.exit_code() { - std::process::exit(exit_code) - } - e.get_err().ok().unwrap() - }) -} - /// Range of options, used for iteration operations. pub type Range = core::ops::Range>; diff --git a/jaq-play/src/lib.rs b/jaq-play/src/lib.rs index 16bbd0940..ac2b6acc5 100644 --- a/jaq-play/src/lib.rs +++ b/jaq-play/src/lib.rs @@ -7,7 +7,7 @@ use core::fmt::{self, Debug, Display, Formatter}; use jaq_all::data::{self, compile, Runner}; use jaq_all::fmts::{read, write}; use jaq_all::json::write::{Pp, Styles}; -use jaq_all::json::{self, bstr, style, write_bytes, write_utf8, Val, ValR}; +use jaq_all::json::{self, bstr, style, write_bytes, write_utf8, Val, ValX}; use jaq_all::load::{Color, FileReportsDisp}; use wasm_bindgen::prelude::{wasm_bindgen, JsValue}; use web_sys::DedicatedWorkerGlobalScope as Scope; @@ -134,6 +134,7 @@ impl Settings { enum Error { Hifijson(String), Jaq(json::Error), + Halt(i32), } #[wasm_bindgen] @@ -146,8 +147,8 @@ pub fn run(filter: &str, input: &str, settings: &JsValue, scope: &Scope) { let runner = &settings.runner(); let post = |s: String| scope.post_message(&s.into()).unwrap(); - let post_value = |y: ValR| { - let y = y.map_err(Error::Jaq)?; + let post_value = |y: ValX| { + let y = y.map_err(|x| x.handle(Error::Jaq, Error::Halt))?; let s = FormatterFn(|f: &mut Formatter| match &y { Val::TStr(s) | Val::BStr(s) if settings.raw_output => bstr(&escape_bytes(s)).fmt(f), y => fmt_json(f, &runner.writer.pp, 0, y), @@ -166,6 +167,7 @@ pub fn run(filter: &str, input: &str, settings: &JsValue, scope: &Scope) { Ok(()) => (), Err(Error::Hifijson(e)) => post(format!("Parse error: {e}")), Err(Error::Jaq(e)) => post(format!("Error: {e}")), + Err(Error::Halt(exit_code)) => post(format!("Exited with code {exit_code}")), }, } diff --git a/jaq/src/filter.rs b/jaq/src/filter.rs index 5eef58a2b..dbc83b12b 100644 --- a/jaq/src/filter.rs +++ b/jaq/src/filter.rs @@ -51,7 +51,7 @@ pub(crate) fn run( ) -> Result, Error> { let mut last = None; jaq_all::data::run(runner, filter, vars, inputs, Error::Parse, |v| { - let v = v.map_err(Error::Jaq)?; + let v = v.map_err(|x| x.handle(Error::Jaq, Error::Halt))?; last = Some(v.as_bool()); f(v).map_err(Into::into) })?; diff --git a/jaq/src/main.rs b/jaq/src/main.rs index 3328a1273..3290faf96 100644 --- a/jaq/src/main.rs +++ b/jaq/src/main.rs @@ -61,6 +61,9 @@ fn main() -> io::Result { } } else { real_main(&cli).or_else(|e| { + if let Error::Halt(exit_code) = e { + std::process::exit(exit_code); + } write!(err, "{}", ErrorColor::new(&e, cli.color_errors()))?; Ok(e.report()) }) @@ -228,6 +231,7 @@ enum Error { Report(Vec>), Parse(String), Jaq(jaq_all::json::Error), + Halt(i32), FalseOrNull, NoOutput, } @@ -265,6 +269,7 @@ impl fmt::Display for ErrorColor<'_> { }), Error::Parse(e) => writeln!(f, "Error: failed to parse: {e}"), Error::Jaq(e) => writeln!(f, "Error: {e}"), + Error::Halt(exit_code) => writeln!(f, "Exited with code {exit_code}"), } } } @@ -277,6 +282,7 @@ impl Termination for Error { Self::Report(_) => 3, Self::NoOutput => 4, Self::Parse(_) | Self::Jaq(_) => 5, + Self::Halt(_) => 101, // this branch should never be encountered in practice }) } } diff --git a/jaq/src/tests.rs b/jaq/src/tests.rs index c858fa73c..d9ad93e0f 100644 --- a/jaq/src/tests.rs +++ b/jaq/src/tests.rs @@ -51,7 +51,7 @@ fn run_test(test: Test) -> Result<(Val, Val), Error> { let vars = Default::default(); let input = core::iter::once(parse_single(test.input.as_bytes()).map_err(|e| e.to_string())); run(runner, &filter, vars, input, Error::Parse, |v| { - obtain.push(v.map_err(Error::Jaq)?); + obtain.push(v.map_err(|x| x.handle(Error::Jaq, Error::Halt))?); Ok(()) })?; From ef5680124eca09bb63ad58d91987af7a084382f4 Mon Sep 17 00:00:00 2001 From: Andrew Lees Date: Tue, 5 May 2026 22:35:31 +0100 Subject: [PATCH 04/14] Move `halt/1` into `jaq-core` As `jaq-core` is fully generic over the value type, this requires moving `i32` extraction into `jaq-std`. --- jaq-all/examples/main.rs | 3 ++- jaq-core/examples/repl.rs | 2 +- jaq-core/src/exn.rs | 6 +++--- jaq-core/src/funs.rs | 6 +++++- jaq-play/src/lib.rs | 2 +- jaq-std/src/lib.rs | 23 +++++++++++++---------- jaq/src/filter.rs | 2 +- jaq/src/tests.rs | 2 +- 8 files changed, 27 insertions(+), 19 deletions(-) diff --git a/jaq-all/examples/main.rs b/jaq-all/examples/main.rs index 0b1ba9b45..36162787b 100644 --- a/jaq-all/examples/main.rs +++ b/jaq-all/examples/main.rs @@ -19,7 +19,8 @@ fn main() -> io::Result<()> { data::run(&runner, &filter, vars, inputs, fi, |v| { let v = v.map_err(|e| { - e.handle( + jaq_std::handle_exn_i32( + e, |e| Error::new(ErrorKind::Other, e.to_string()), |exit_code| std::process::exit(exit_code), ) diff --git a/jaq-core/examples/repl.rs b/jaq-core/examples/repl.rs index 8ec28209e..6740d56f2 100644 --- a/jaq-core/examples/repl.rs +++ b/jaq-core/examples/repl.rs @@ -31,7 +31,7 @@ fn eval_print(code: &str) -> std::io::Result<()> { // iterator over the output values for y in filter.id.run((ctx, Val::default())).map(|v| { v.map_err(|x| { - x.handle(std::convert::identity, |exit_code| { + jaq_std::handle_exn_i32(x, std::convert::identity, |exit_code| { std::process::exit(exit_code) }) }) diff --git a/jaq-core/src/exn.rs b/jaq-core/src/exn.rs index 21ec30f7b..bc72fc845 100644 --- a/jaq-core/src/exn.rs +++ b/jaq-core/src/exn.rs @@ -20,7 +20,7 @@ pub(crate) enum Inner<'a, V> { /// If this can be observed by users, then this is a bug. TailCall(Box<(&'a TermId, Vars, CallInput)>), Break(usize), - Halt(i32), + Halt(V), } #[derive(Clone, Debug)] @@ -52,7 +52,7 @@ impl Exn<'_, V> { /// /// If you are writing a native filter, e.g. `f(f1; ...; fn)`, /// do not use this method on outputs of `fi`! - pub fn handle(self, err: impl FnOnce(Error) -> T, halt: impl FnOnce(i32) -> T) -> T { + pub fn handle(self, err: impl FnOnce(Error) -> T, halt: impl FnOnce(V) -> T) -> T { match self.0 { Inner::Err(e) => err(*e), Inner::Halt(exit_code) => halt(exit_code), @@ -67,7 +67,7 @@ impl Exn<'_, V> { /// Create an exception intended to halt filter execution, such as for the /// `halt/1` filter. - pub fn halt(exit_code: i32) -> Self { + pub fn halt(exit_code: V) -> Self { Self(Inner::Halt(exit_code)) } } diff --git a/jaq-core/src/funs.rs b/jaq-core/src/funs.rs index 02ad9e9a8..96144da47 100644 --- a/jaq-core/src/funs.rs +++ b/jaq-core/src/funs.rs @@ -1,4 +1,4 @@ -use crate::box_iter::BoxIter; +use crate::box_iter::{BoxIter, box_once}; use crate::native::{bome, v, Filter, RunPathsPtr}; use crate::{Bind, DataT, Error, Exn, RunPtr, ValT, ValX}; use alloc::{boxed::Box, vec::Vec}; @@ -41,6 +41,10 @@ where let f = |(k, v)| [k, v].into_iter().collect(); bome(cv.1.key_values().map(|kv| kv.map(f)).collect()) }), + ("halt", v(1), |mut cv| { + let exit_code = cv.0.pop_var(); + box_once(Err(Exn::halt(exit_code))) + }), ]) } diff --git a/jaq-play/src/lib.rs b/jaq-play/src/lib.rs index ac2b6acc5..0443a7ba4 100644 --- a/jaq-play/src/lib.rs +++ b/jaq-play/src/lib.rs @@ -148,7 +148,7 @@ pub fn run(filter: &str, input: &str, settings: &JsValue, scope: &Scope) { let post = |s: String| scope.post_message(&s.into()).unwrap(); let post_value = |y: ValX| { - let y = y.map_err(|x| x.handle(Error::Jaq, Error::Halt))?; + let y = y.map_err(|x| jaq_all::jaq_std::handle_exn_i32(x, Error::Jaq, Error::Halt))?; let s = FormatterFn(|f: &mut Formatter| match &y { Val::TStr(s) | Val::BStr(s) if settings.raw_output => bstr(&escape_bytes(s)).fmt(f), y => fmt_json(f, &runner.writer.pp, 0, y), diff --git a/jaq-std/src/lib.rs b/jaq-std/src/lib.rs index df158e37b..26b20004b 100644 --- a/jaq-std/src/lib.rs +++ b/jaq-std/src/lib.rs @@ -154,8 +154,6 @@ trait ValTx: ValT + Sized { .ok_or_else(|| Error::typ(self.clone(), "integer")) } - #[cfg(feature = "math")] - /// Use as an i32 to be given as an argument to a libm function. fn try_as_i32(&self) -> Result> { self.try_as_isize()?.try_into().map_err(Error::str) } @@ -236,6 +234,19 @@ trait ValTx: ValT + Sized { } impl ValTx for T {} +/// Convenience function around [`Exn::handle`] for cases where you want the +/// exit code to strictly be an [`i32`], such as when calling [`std::process::exit`]. +pub fn handle_exn_i32( + x: Exn<'_, T>, + err: impl Fn(Error) -> R, + halt: impl FnOnce(i32) -> R, +) -> R { + x.handle(&err, |exit_val| match exit_val.try_as_i32() { + Ok(exit_code) => halt(exit_code), + Err(e) => err(e), + }) +} + /// Sort array by the given function. fn sort_by<'a, V: ValT>(xs: &mut [V], f: impl Fn(V) -> ValXs<'a, V>) -> Result<(), Exn<'a, V>> { // Some(e) iff an error has previously occurred @@ -467,14 +478,6 @@ where )) }), ("now", v(0), |_| bome(now().map(D::V::from))), - ("halt", v(1), |mut cv| { - let exit_code = cv.0.pop_var().try_as_i32(); - box_once( - exit_code - .map_err(Exn::from) - .and_then(|exit_code| Err(Exn::halt(exit_code))), - ) - }), ]) } diff --git a/jaq/src/filter.rs b/jaq/src/filter.rs index dbc83b12b..08549ccaf 100644 --- a/jaq/src/filter.rs +++ b/jaq/src/filter.rs @@ -51,7 +51,7 @@ pub(crate) fn run( ) -> Result, Error> { let mut last = None; jaq_all::data::run(runner, filter, vars, inputs, Error::Parse, |v| { - let v = v.map_err(|x| x.handle(Error::Jaq, Error::Halt))?; + let v = v.map_err(|x| jaq_all::jaq_std::handle_exn_i32(x, Error::Jaq, Error::Halt))?; last = Some(v.as_bool()); f(v).map_err(Into::into) })?; diff --git a/jaq/src/tests.rs b/jaq/src/tests.rs index d9ad93e0f..1130ebcdd 100644 --- a/jaq/src/tests.rs +++ b/jaq/src/tests.rs @@ -51,7 +51,7 @@ fn run_test(test: Test) -> Result<(Val, Val), Error> { let vars = Default::default(); let input = core::iter::once(parse_single(test.input.as_bytes()).map_err(|e| e.to_string())); run(runner, &filter, vars, input, Error::Parse, |v| { - obtain.push(v.map_err(|x| x.handle(Error::Jaq, Error::Halt))?); + obtain.push(v.map_err(|x| jaq_all::jaq_std::handle_exn_i32(x, Error::Jaq, Error::Halt))?); Ok(()) })?; From b351798ac523ccb9177f12e7015364674797516d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20F=C3=A4rber?= <01mf02@gmail.com> Date: Mon, 11 May 2026 17:07:06 +0200 Subject: [PATCH 05/14] Documentation. --- docs/stdlib.dj | 8 +++++--- 1 file changed, 5 insertions(+), 3 deletions(-) diff --git a/docs/stdlib.dj b/docs/stdlib.dj index 1971778d0..0019cfc01 100644 --- a/docs/stdlib.dj +++ b/docs/stdlib.dj @@ -2222,10 +2222,12 @@ $ jaq -n '1, halt(42), 2'; echo $? `jq` does not implement `halt($exit_code)`, only `halt`. ::: -Note that not all values for `$exit_code` will work as intended on all platforms: see the -[Rust documentation on `std::process::exit`](https://doc.rust-lang.org/stable/std/process/fn.exit.html#platform-specific-behavior) -for more information. As a general rule of thumb, the 8-bit signed integers (-128 to 127 inclusive) are safe to use and +::: Advanced +Note that not all integer values for `$exit_code` work as intended on all platforms; see +[`std::process::exit`](https://doc.rust-lang.org/stable/std/process/fn.exit.html#platform-specific-behavior). +As a general rule of thumb, the 8-bit signed integers (-128 to 127 inclusive) are safe to use and anything outside of that range _may_ be silently truncated by the operating system. +::: {#halt_error} ### `halt_error`, `halt_error($exit_code)` From 6e331f4d0b21a4eed5e6c3e84fa2ab040363262d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20F=C3=A4rber?= <01mf02@gmail.com> Date: Mon, 11 May 2026 17:38:09 +0200 Subject: [PATCH 06/14] Refactoring. --- jaq-all/src/data.rs | 2 +- jaq-core/examples/repl.rs | 2 +- jaq-core/src/exn.rs | 24 ++++++++++++------------ jaq-core/src/funs.rs | 6 +----- jaq-core/src/lib.rs | 4 ++-- jaq-play/src/lib.rs | 2 +- jaq-std/src/lib.rs | 17 ++++------------- jaq/src/filter.rs | 2 +- jaq/src/tests.rs | 2 +- 9 files changed, 24 insertions(+), 37 deletions(-) diff --git a/jaq-all/src/data.rs b/jaq-all/src/data.rs index 9222e25c9..12b4d4e28 100644 --- a/jaq-all/src/data.rs +++ b/jaq-all/src/data.rs @@ -1,6 +1,6 @@ //! Commonly used data for filter compilation & execution. use crate::{compile_with, FileReports, Fun}; -use jaq_core::{DataT, Lut, Vars, data}; +use jaq_core::{data, DataT, Lut, Vars}; use jaq_fmts::write::Writer; use jaq_json::{Val, ValX}; use jaq_std::input::{self, Inputs, RcIter}; diff --git a/jaq-core/examples/repl.rs b/jaq-core/examples/repl.rs index 6740d56f2..d4b0d2202 100644 --- a/jaq-core/examples/repl.rs +++ b/jaq-core/examples/repl.rs @@ -9,7 +9,7 @@ //! rlwrap cargo run --example repl use jaq_core::load::{Arena, File, Loader}; -use jaq_core::{Compiler, Ctx, Vars, data}; +use jaq_core::{data, Compiler, Ctx, Vars}; use jaq_json::{write, Val}; use std::io::{stdin, stdout, Write}; diff --git a/jaq-core/src/exn.rs b/jaq-core/src/exn.rs index bc72fc845..3eef48fc3 100644 --- a/jaq-core/src/exn.rs +++ b/jaq-core/src/exn.rs @@ -20,7 +20,7 @@ pub(crate) enum Inner<'a, V> { /// If this can be observed by users, then this is a bug. TailCall(Box<(&'a TermId, Vars, CallInput)>), Break(usize), - Halt(V), + Halt(i32), } #[derive(Clone, Debug)] @@ -52,22 +52,22 @@ impl Exn<'_, V> { /// /// If you are writing a native filter, e.g. `f(f1; ...; fn)`, /// do not use this method on outputs of `fi`! - pub fn handle(self, err: impl FnOnce(Error) -> T, halt: impl FnOnce(V) -> T) -> T { + pub fn unwrap_err_or_halt( + self, + fail: impl FnOnce(Error) -> T, + halt: impl FnOnce(i32) -> T, + ) -> T { match self.0 { - Inner::Err(e) => err(*e), + Inner::Err(e) => fail(*e), Inner::Halt(exit_code) => halt(exit_code), - Inner::TailCall(_) => { - panic!("tried to handle an internal exception variant: TailCall") - } - Inner::Break(_) => { - panic!("tried to handle an internal exception variant: Break") - } + Inner::TailCall(_) | Inner::Break(_) => panic!(), } } - /// Create an exception intended to halt filter execution, such as for the - /// `halt/1` filter. - pub fn halt(exit_code: V) -> Self { + /// Create an exception intended to halt filter execution. + /// + /// This is used by the `halt/1` filter. + pub fn halt(exit_code: i32) -> Self { Self(Inner::Halt(exit_code)) } } diff --git a/jaq-core/src/funs.rs b/jaq-core/src/funs.rs index 96144da47..02ad9e9a8 100644 --- a/jaq-core/src/funs.rs +++ b/jaq-core/src/funs.rs @@ -1,4 +1,4 @@ -use crate::box_iter::{BoxIter, box_once}; +use crate::box_iter::BoxIter; use crate::native::{bome, v, Filter, RunPathsPtr}; use crate::{Bind, DataT, Error, Exn, RunPtr, ValT, ValX}; use alloc::{boxed::Box, vec::Vec}; @@ -41,10 +41,6 @@ where let f = |(k, v)| [k, v].into_iter().collect(); bome(cv.1.key_values().map(|kv| kv.map(f)).collect()) }), - ("halt", v(1), |mut cv| { - let exit_code = cv.0.pop_var(); - box_once(Err(Exn::halt(exit_code))) - }), ]) } diff --git a/jaq-core/src/lib.rs b/jaq-core/src/lib.rs index 2c3d994f1..a46c83124 100644 --- a/jaq-core/src/lib.rs +++ b/jaq-core/src/lib.rs @@ -129,8 +129,8 @@ impl Filter> { let ctx = Ctx::>::new(&self.lut, Vars::new([])); let out = self.id.run((ctx, x)).map(|valx| { valx.map_err(|x| { - x.handle(std::convert::identity, |exit_code| { - panic!("tests shouldn't call halt/1 (code {exit_code})") + x.unwrap_err_or_halt(core::convert::identity, |exit_code| { + panic!("test called halt (code {exit_code})") }) }) }); diff --git a/jaq-play/src/lib.rs b/jaq-play/src/lib.rs index 0443a7ba4..76f078089 100644 --- a/jaq-play/src/lib.rs +++ b/jaq-play/src/lib.rs @@ -148,7 +148,7 @@ pub fn run(filter: &str, input: &str, settings: &JsValue, scope: &Scope) { let post = |s: String| scope.post_message(&s.into()).unwrap(); let post_value = |y: ValX| { - let y = y.map_err(|x| jaq_all::jaq_std::handle_exn_i32(x, Error::Jaq, Error::Halt))?; + let y = y.map_err(|x| x.unwrap_err_or_halt(Error::Jaq, Error::Halt))?; let s = FormatterFn(|f: &mut Formatter| match &y { Val::TStr(s) | Val::BStr(s) if settings.raw_output => bstr(&escape_bytes(s)).fmt(f), y => fmt_json(f, &runner.writer.pp, 0, y), diff --git a/jaq-std/src/lib.rs b/jaq-std/src/lib.rs index 26b20004b..84a8684c1 100644 --- a/jaq-std/src/lib.rs +++ b/jaq-std/src/lib.rs @@ -234,19 +234,6 @@ trait ValTx: ValT + Sized { } impl ValTx for T {} -/// Convenience function around [`Exn::handle`] for cases where you want the -/// exit code to strictly be an [`i32`], such as when calling [`std::process::exit`]. -pub fn handle_exn_i32( - x: Exn<'_, T>, - err: impl Fn(Error) -> R, - halt: impl FnOnce(i32) -> R, -) -> R { - x.handle(&err, |exit_val| match exit_val.try_as_i32() { - Ok(exit_code) => halt(exit_code), - Err(e) => err(e), - }) -} - /// Sort array by the given function. fn sort_by<'a, V: ValT>(xs: &mut [V], f: impl Fn(V) -> ValXs<'a, V>) -> Result<(), Exn<'a, V>> { // Some(e) iff an error has previously occurred @@ -453,6 +440,10 @@ where .map(|s| ValT::from_utf8_bytes(s.replace(b"'", b"'\\''"))), ) }), + ("halt", v(1), |mut cv| { + let exit_code = cv.0.pop_var().try_as_i32().map_err(Exn::from); + box_once(exit_code.and_then(|exit_code| Err(Exn::halt(exit_code)))) + }), ]) } diff --git a/jaq/src/filter.rs b/jaq/src/filter.rs index 08549ccaf..b87732c9b 100644 --- a/jaq/src/filter.rs +++ b/jaq/src/filter.rs @@ -51,7 +51,7 @@ pub(crate) fn run( ) -> Result, Error> { let mut last = None; jaq_all::data::run(runner, filter, vars, inputs, Error::Parse, |v| { - let v = v.map_err(|x| jaq_all::jaq_std::handle_exn_i32(x, Error::Jaq, Error::Halt))?; + let v = v.map_err(|x| x.unwrap_err_or_halt(Error::Jaq, Error::Halt))?; last = Some(v.as_bool()); f(v).map_err(Into::into) })?; diff --git a/jaq/src/tests.rs b/jaq/src/tests.rs index 1130ebcdd..b61387fb8 100644 --- a/jaq/src/tests.rs +++ b/jaq/src/tests.rs @@ -51,7 +51,7 @@ fn run_test(test: Test) -> Result<(Val, Val), Error> { let vars = Default::default(); let input = core::iter::once(parse_single(test.input.as_bytes()).map_err(|e| e.to_string())); run(runner, &filter, vars, input, Error::Parse, |v| { - obtain.push(v.map_err(|x| jaq_all::jaq_std::handle_exn_i32(x, Error::Jaq, Error::Halt))?); + obtain.push(v.map_err(|x| x.unwrap_err_or_halt(Error::Jaq, Error::Halt))?); Ok(()) })?; From 31f1a1768c1342c4825170f1c380a08a81c32408 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20F=C3=A4rber?= <01mf02@gmail.com> Date: Mon, 11 May 2026 18:18:03 +0200 Subject: [PATCH 07/14] Make tests compile again. --- jaq-all/examples/main.rs | 3 +-- jaq-core/examples/repl.rs | 12 ++++-------- jaq-core/src/lib.rs | 2 +- 3 files changed, 6 insertions(+), 11 deletions(-) diff --git a/jaq-all/examples/main.rs b/jaq-all/examples/main.rs index 36162787b..e5b4d48fe 100644 --- a/jaq-all/examples/main.rs +++ b/jaq-all/examples/main.rs @@ -19,8 +19,7 @@ fn main() -> io::Result<()> { data::run(&runner, &filter, vars, inputs, fi, |v| { let v = v.map_err(|e| { - jaq_std::handle_exn_i32( - e, + e.unwrap_err_or_halt( |e| Error::new(ErrorKind::Other, e.to_string()), |exit_code| std::process::exit(exit_code), ) diff --git a/jaq-core/examples/repl.rs b/jaq-core/examples/repl.rs index d4b0d2202..f9afafe96 100644 --- a/jaq-core/examples/repl.rs +++ b/jaq-core/examples/repl.rs @@ -10,7 +10,7 @@ use jaq_core::load::{Arena, File, Loader}; use jaq_core::{data, Compiler, Ctx, Vars}; -use jaq_json::{write, Val}; +use jaq_json::{write, Val, ValX}; use std::io::{stdin, stdout, Write}; fn eval_print(code: &str) -> std::io::Result<()> { @@ -28,14 +28,10 @@ fn eval_print(code: &str) -> std::io::Result<()> { let mut stdout = stdout().lock(); + let unwrap_valx = + |x: ValX| x.map_err(|e| e.unwrap_err_or_halt(|e| e, |code| std::process::exit(code))); // iterator over the output values - for y in filter.id.run((ctx, Val::default())).map(|v| { - v.map_err(|x| { - jaq_std::handle_exn_i32(x, std::convert::identity, |exit_code| { - std::process::exit(exit_code) - }) - }) - }) { + for y in filter.id.run((ctx, Val::default())).map(unwrap_valx) { write::write(&mut stdout, &write::Pp::default(), 0, &y.unwrap())?; writeln!(stdout)?; } diff --git a/jaq-core/src/lib.rs b/jaq-core/src/lib.rs index a46c83124..d9fe79753 100644 --- a/jaq-core/src/lib.rs +++ b/jaq-core/src/lib.rs @@ -34,7 +34,7 @@ //! // context for filter execution //! let ctx = Ctx::>::new(&filter.lut, Vars::new([])); //! // iterator over the output values -//! let mut out = filter.id.run((ctx, input)).map(|r| r.map_err(|x| x.handle(std::convert::identity, |exit_code| panic!("halt({exit_code})")))); +//! let mut out = filter.id.run((ctx, input)).map(|r| r.map_err(|x| x.unwrap_err_or_halt(std::convert::identity, |exit_code| panic!("halt({exit_code})")))); //! //! assert_eq!(out.next(), Some(Ok(Val::from("Hello".to_owned()))));; //! assert_eq!(out.next(), Some(Ok(Val::from("world".to_owned()))));; From d9395457a93f967d52800b9459c52d4d818689c6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20F=C3=A4rber?= <01mf02@gmail.com> Date: Fri, 15 May 2026 16:43:14 +0200 Subject: [PATCH 08/14] Restore `unwrap_valr`. --- jaq-core/src/val.rs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/jaq-core/src/val.rs b/jaq-core/src/val.rs index bbbf8d0f9..1bca80bec 100644 --- a/jaq-core/src/val.rs +++ b/jaq-core/src/val.rs @@ -23,6 +23,28 @@ pub type ValX<'a, T, V = T> = Result>; /// Stream of values and eXceptions. pub type ValXs<'a, T, V = T> = BoxIter<'a, ValX<'a, T, V>>; +/// Convert a value exception [`ValX`] into a value result [`ValR`]. +/// +/// This should always succeed when called on results of a main filter. +/// For any other filter, this may not succeed, i.e. panic. +/// +/// If you are writing a native filter, e.g. `f(f1; ...; fn)`, +/// do not use this function on outputs of `fi`! +/// +/// This function will exit the current process if +/// the value exception results from a call to the filter `halt`. +/// If the `std` feature is disabled, this function panics instead. +/// In a future jaq 3.0, this function should only be provided if +/// the `std` feature is enabled. +pub fn unwrap_valr(v: ValX) -> ValR { + #[cfg(feature = "std")] + let exit = |exit_code| std::process::exit(exit_code); + #[cfg(not(feature = "std"))] + let exit = |exit_code| panic!("halt({})", exit_code); + + v.map_err(|e| e.unwrap_err_or_halt(core::convert::identity, exit)) +} + /// Range of options, used for iteration operations. pub type Range = core::ops::Range>; From edd7d003b689a668b89e05f482fe91ee3d53f1ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20F=C3=A4rber?= <01mf02@gmail.com> Date: Fri, 15 May 2026 17:03:41 +0200 Subject: [PATCH 09/14] More idiomatic handling of `halt`. --- jaq/src/main.rs | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/jaq/src/main.rs b/jaq/src/main.rs index 3290faf96..31dae8114 100644 --- a/jaq/src/main.rs +++ b/jaq/src/main.rs @@ -61,9 +61,6 @@ fn main() -> io::Result { } } else { real_main(&cli).or_else(|e| { - if let Error::Halt(exit_code) = e { - std::process::exit(exit_code); - } write!(err, "{}", ErrorColor::new(&e, cli.color_errors()))?; Ok(e.report()) }) @@ -253,7 +250,7 @@ impl fmt::Display for ErrorColor<'_> { fn fmt(&self, f: &mut Formatter) -> fmt::Result { let Self(error, color) = self; match error { - Error::FalseOrNull | Error::NoOutput => Ok(()), + Error::FalseOrNull | Error::NoOutput | Error::Halt(_) => Ok(()), Error::Io(prefix, e) => { write!(f, "Error: ")?; if let Some(p) = prefix { @@ -269,7 +266,6 @@ impl fmt::Display for ErrorColor<'_> { }), Error::Parse(e) => writeln!(f, "Error: failed to parse: {e}"), Error::Jaq(e) => writeln!(f, "Error: {e}"), - Error::Halt(exit_code) => writeln!(f, "Exited with code {exit_code}"), } } } @@ -282,7 +278,8 @@ impl Termination for Error { Self::Report(_) => 3, Self::NoOutput => 4, Self::Parse(_) | Self::Jaq(_) => 5, - Self::Halt(_) => 101, // this branch should never be encountered in practice + // ExitCode ~= u8, but exit_code: i32 + Self::Halt(exit_code) => std::process::exit(exit_code), }) } } From 9b145a22c02cd01bf4e75bfd439e0f4c75aa9822 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20F=C3=A4rber?= <01mf02@gmail.com> Date: Fri, 15 May 2026 17:04:28 +0200 Subject: [PATCH 10/14] Re-introduce `unwrap_valr`. --- jaq-core/src/lib.rs | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/jaq-core/src/lib.rs b/jaq-core/src/lib.rs index d9fe79753..b607a19f7 100644 --- a/jaq-core/src/lib.rs +++ b/jaq-core/src/lib.rs @@ -7,7 +7,7 @@ //! more complex use cases, such as lazy JSON file loading, error handling etc. //! //! ~~~ -//! use jaq_core::{data, Compiler, Ctx, Vars}; +//! use jaq_core::{data, unwrap_valr, Compiler, Ctx, Vars}; //! use jaq_core::load::{Arena, File, Loader}; //! use jaq_json::{read, Val}; //! @@ -34,7 +34,7 @@ //! // context for filter execution //! let ctx = Ctx::>::new(&filter.lut, Vars::new([])); //! // iterator over the output values -//! let mut out = filter.id.run((ctx, input)).map(|r| r.map_err(|x| x.unwrap_err_or_halt(std::convert::identity, |exit_code| panic!("halt({exit_code})")))); +//! let mut out = filter.id.run((ctx, input)).map(unwrap_valr); //! //! assert_eq!(out.next(), Some(Ok(Val::from("Hello".to_owned()))));; //! assert_eq!(out.next(), Some(Ok(Val::from("world".to_owned()))));; @@ -68,7 +68,7 @@ pub mod val; pub use data::DataT; pub use exn::{Error, Exn}; pub use filter::{Ctx, Cv, Native, PathsPtr, RunPtr, UpdatePtr, Vars}; -pub use val::{ValR, ValT, ValX, ValXs}; +pub use val::{unwrap_valr, ValR, ValT, ValX, ValXs}; use rc_list::List as RcList; use stack::Stack; @@ -127,13 +127,7 @@ impl Filter> { /// This is for testing purposes. pub fn yields(&self, x: V, ys: impl Iterator>) { let ctx = Ctx::>::new(&self.lut, Vars::new([])); - let out = self.id.run((ctx, x)).map(|valx| { - valx.map_err(|x| { - x.unwrap_err_or_halt(core::convert::identity, |exit_code| { - panic!("test called halt (code {exit_code})") - }) - }) - }); + let out = self.id.run((ctx, x)).map(unwrap_valr); assert!(out.eq(ys)); } } From d4f0fd3d91225094fb29a5b7c8de7ae0d6ed581b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20F=C3=A4rber?= <01mf02@gmail.com> Date: Fri, 15 May 2026 17:11:38 +0200 Subject: [PATCH 11/14] More simplification. --- jaq-all/examples/main.rs | 8 ++------ jaq-core/examples/repl.rs | 8 +++----- 2 files changed, 5 insertions(+), 11 deletions(-) diff --git a/jaq-all/examples/main.rs b/jaq-all/examples/main.rs index e5b4d48fe..9050c474c 100644 --- a/jaq-all/examples/main.rs +++ b/jaq-all/examples/main.rs @@ -1,4 +1,5 @@ use jaq_all::{data, fmts, load}; +use jaq_core::unwrap_valr; use std::io::{self, Error, ErrorKind}; fn main() -> io::Result<()> { @@ -18,12 +19,7 @@ fn main() -> io::Result<()> { let fi = |e| Error::new(ErrorKind::InvalidData, e); data::run(&runner, &filter, vars, inputs, fi, |v| { - let v = v.map_err(|e| { - e.unwrap_err_or_halt( - |e| Error::new(ErrorKind::Other, e.to_string()), - |exit_code| std::process::exit(exit_code), - ) - }); + let v = unwrap_valr(v).map_err(|e| Error::new(ErrorKind::Other, e.to_string())); fmts::write::write(stdout, &runner.writer, &v?) }) } diff --git a/jaq-core/examples/repl.rs b/jaq-core/examples/repl.rs index f9afafe96..72ba8517b 100644 --- a/jaq-core/examples/repl.rs +++ b/jaq-core/examples/repl.rs @@ -9,8 +9,8 @@ //! rlwrap cargo run --example repl use jaq_core::load::{Arena, File, Loader}; -use jaq_core::{data, Compiler, Ctx, Vars}; -use jaq_json::{write, Val, ValX}; +use jaq_core::{data, unwrap_valr, Compiler, Ctx, Vars}; +use jaq_json::{write, Val}; use std::io::{stdin, stdout, Write}; fn eval_print(code: &str) -> std::io::Result<()> { @@ -28,10 +28,8 @@ fn eval_print(code: &str) -> std::io::Result<()> { let mut stdout = stdout().lock(); - let unwrap_valx = - |x: ValX| x.map_err(|e| e.unwrap_err_or_halt(|e| e, |code| std::process::exit(code))); // iterator over the output values - for y in filter.id.run((ctx, Val::default())).map(unwrap_valx) { + for y in filter.id.run((ctx, Val::default())).map(unwrap_valr) { write::write(&mut stdout, &write::Pp::default(), 0, &y.unwrap())?; writeln!(stdout)?; } From 6b752e90ba18a92bdc1705057e0f4099e2187c40 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20F=C3=A4rber?= <01mf02@gmail.com> Date: Fri, 15 May 2026 17:19:23 +0200 Subject: [PATCH 12/14] Panic less in core. --- jaq-core/src/exn.rs | 15 +++++---------- jaq-core/src/val.rs | 2 +- jaq-play/src/lib.rs | 2 +- jaq/src/filter.rs | 2 +- jaq/src/tests.rs | 2 +- 5 files changed, 9 insertions(+), 14 deletions(-) diff --git a/jaq-core/src/exn.rs b/jaq-core/src/exn.rs index 3eef48fc3..04a753e99 100644 --- a/jaq-core/src/exn.rs +++ b/jaq-core/src/exn.rs @@ -47,20 +47,15 @@ impl CallInput { impl Exn<'_, V> { /// Handle the exception kinds that can be returned from executing a main filter. - /// - /// For any other filter, this may not succeed, i.e. panic. - /// - /// If you are writing a native filter, e.g. `f(f1; ...; fn)`, - /// do not use this method on outputs of `fi`! - pub fn unwrap_err_or_halt( + pub fn err_or_halt( self, fail: impl FnOnce(Error) -> T, halt: impl FnOnce(i32) -> T, - ) -> T { + ) -> Result { match self.0 { - Inner::Err(e) => fail(*e), - Inner::Halt(exit_code) => halt(exit_code), - Inner::TailCall(_) | Inner::Break(_) => panic!(), + Inner::Err(e) => Ok(fail(*e)), + Inner::Halt(exit_code) => Ok(halt(exit_code)), + Inner::TailCall(_) | Inner::Break(_) => Err(self), } } diff --git a/jaq-core/src/val.rs b/jaq-core/src/val.rs index 1bca80bec..48a1ac111 100644 --- a/jaq-core/src/val.rs +++ b/jaq-core/src/val.rs @@ -42,7 +42,7 @@ pub fn unwrap_valr(v: ValX) -> ValR { #[cfg(not(feature = "std"))] let exit = |exit_code| panic!("halt({})", exit_code); - v.map_err(|e| e.unwrap_err_or_halt(core::convert::identity, exit)) + v.map_err(|e| e.err_or_halt(core::convert::identity, exit).ok().unwrap()) } /// Range of options, used for iteration operations. diff --git a/jaq-play/src/lib.rs b/jaq-play/src/lib.rs index 76f078089..0890a3826 100644 --- a/jaq-play/src/lib.rs +++ b/jaq-play/src/lib.rs @@ -148,7 +148,7 @@ pub fn run(filter: &str, input: &str, settings: &JsValue, scope: &Scope) { let post = |s: String| scope.post_message(&s.into()).unwrap(); let post_value = |y: ValX| { - let y = y.map_err(|x| x.unwrap_err_or_halt(Error::Jaq, Error::Halt))?; + let y = y.map_err(|x| x.err_or_halt(Error::Jaq, Error::Halt).ok().unwrap())?; let s = FormatterFn(|f: &mut Formatter| match &y { Val::TStr(s) | Val::BStr(s) if settings.raw_output => bstr(&escape_bytes(s)).fmt(f), y => fmt_json(f, &runner.writer.pp, 0, y), diff --git a/jaq/src/filter.rs b/jaq/src/filter.rs index b87732c9b..361f6d394 100644 --- a/jaq/src/filter.rs +++ b/jaq/src/filter.rs @@ -51,7 +51,7 @@ pub(crate) fn run( ) -> Result, Error> { let mut last = None; jaq_all::data::run(runner, filter, vars, inputs, Error::Parse, |v| { - let v = v.map_err(|x| x.unwrap_err_or_halt(Error::Jaq, Error::Halt))?; + let v = v.map_err(|x| x.err_or_halt(Error::Jaq, Error::Halt).ok().unwrap())?; last = Some(v.as_bool()); f(v).map_err(Into::into) })?; diff --git a/jaq/src/tests.rs b/jaq/src/tests.rs index b61387fb8..52033c89d 100644 --- a/jaq/src/tests.rs +++ b/jaq/src/tests.rs @@ -51,7 +51,7 @@ fn run_test(test: Test) -> Result<(Val, Val), Error> { let vars = Default::default(); let input = core::iter::once(parse_single(test.input.as_bytes()).map_err(|e| e.to_string())); run(runner, &filter, vars, input, Error::Parse, |v| { - obtain.push(v.map_err(|x| x.unwrap_err_or_halt(Error::Jaq, Error::Halt))?); + obtain.push(v.map_err(|x| x.err_or_halt(Error::Jaq, Error::Halt).ok().unwrap())?); Ok(()) })?; From 21b3599f9601455fd97cc93e48e4b9ddceb1acd2 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20F=C3=A4rber?= <01mf02@gmail.com> Date: Mon, 18 May 2026 08:40:00 +0200 Subject: [PATCH 13/14] Split `err_or_halt` back into two functions. --- jaq-core/src/exn.rs | 21 ++++++++++++--------- jaq-core/src/val.rs | 3 ++- jaq-play/src/lib.rs | 5 ++++- jaq/src/filter.rs | 6 ++++-- jaq/src/tests.rs | 4 +++- 5 files changed, 25 insertions(+), 14 deletions(-) diff --git a/jaq-core/src/exn.rs b/jaq-core/src/exn.rs index 04a753e99..c62ebe24d 100644 --- a/jaq-core/src/exn.rs +++ b/jaq-core/src/exn.rs @@ -46,16 +46,19 @@ impl CallInput { } impl Exn<'_, V> { - /// Handle the exception kinds that can be returned from executing a main filter. - pub fn err_or_halt( - self, - fail: impl FnOnce(Error) -> T, - halt: impl FnOnce(i32) -> T, - ) -> Result { + /// If the exception is an error, yield it, else yield the exception. + pub fn get_err(self) -> Result, Self> { match self.0 { - Inner::Err(e) => Ok(fail(*e)), - Inner::Halt(exit_code) => Ok(halt(exit_code)), - Inner::TailCall(_) | Inner::Break(_) => Err(self), + Inner::Err(e) => Ok(*e), + _ => Err(self), + } + } + + /// If the exception halts, yield the exit code, else yield the exception. + pub fn get_halt(self) -> Result { + match self.0 { + Inner::Halt(code) => Ok(code), + _ => Err(self), } } diff --git a/jaq-core/src/val.rs b/jaq-core/src/val.rs index 48a1ac111..2d8676794 100644 --- a/jaq-core/src/val.rs +++ b/jaq-core/src/val.rs @@ -42,7 +42,8 @@ pub fn unwrap_valr(v: ValX) -> ValR { #[cfg(not(feature = "std"))] let exit = |exit_code| panic!("halt({})", exit_code); - v.map_err(|e| e.err_or_halt(core::convert::identity, exit).ok().unwrap()) + let halt = |e: crate::Exn<_>| exit(e.get_halt().ok().unwrap()); + v.map_err(|e| e.get_err().unwrap_or_else(halt)) } /// Range of options, used for iteration operations. diff --git a/jaq-play/src/lib.rs b/jaq-play/src/lib.rs index 0890a3826..f5b5b47ef 100644 --- a/jaq-play/src/lib.rs +++ b/jaq-play/src/lib.rs @@ -6,6 +6,7 @@ use alloc::{boxed::Box, string::String, vec::Vec}; use core::fmt::{self, Debug, Display, Formatter}; use jaq_all::data::{self, compile, Runner}; use jaq_all::fmts::{read, write}; +use jaq_all::jaq_core::Exn; use jaq_all::json::write::{Pp, Styles}; use jaq_all::json::{self, bstr, style, write_bytes, write_utf8, Val, ValX}; use jaq_all::load::{Color, FileReportsDisp}; @@ -148,7 +149,9 @@ pub fn run(filter: &str, input: &str, settings: &JsValue, scope: &Scope) { let post = |s: String| scope.post_message(&s.into()).unwrap(); let post_value = |y: ValX| { - let y = y.map_err(|x| x.err_or_halt(Error::Jaq, Error::Halt).ok().unwrap())?; + let halt = |e: Exn<_>| Error::Halt(e.get_halt().ok().unwrap()); + let y = y.map_err(|e| e.get_err().map_or_else(halt, Error::Jaq))?; + let s = FormatterFn(|f: &mut Formatter| match &y { Val::TStr(s) | Val::BStr(s) if settings.raw_output => bstr(&escape_bytes(s)).fmt(f), y => fmt_json(f, &runner.writer.pp, 0, y), diff --git a/jaq/src/filter.rs b/jaq/src/filter.rs index 361f6d394..52fa529ca 100644 --- a/jaq/src/filter.rs +++ b/jaq/src/filter.rs @@ -1,7 +1,7 @@ //! Filter parsing, compilation, and execution. use crate::{funs, read, Error, Runner, Val}; use jaq_all::data::Filter; -use jaq_all::jaq_core::{compile, load, ValT, Vars}; +use jaq_all::jaq_core::{compile, load, Exn, ValT, Vars}; use jaq_all::load::{compile_errors, load_errors, FileReports}; use std::{io, path::PathBuf}; @@ -51,7 +51,9 @@ pub(crate) fn run( ) -> Result, Error> { let mut last = None; jaq_all::data::run(runner, filter, vars, inputs, Error::Parse, |v| { - let v = v.map_err(|x| x.err_or_halt(Error::Jaq, Error::Halt).ok().unwrap())?; + let halt = |e: Exn<_>| Error::Halt(e.get_halt().ok().unwrap()); + let v = v.map_err(|e| e.get_err().map_or_else(halt, Error::Jaq))?; + last = Some(v.as_bool()); f(v).map_err(Into::into) })?; diff --git a/jaq/src/tests.rs b/jaq/src/tests.rs index 52033c89d..e304d4256 100644 --- a/jaq/src/tests.rs +++ b/jaq/src/tests.rs @@ -2,6 +2,7 @@ use crate::Error; use alloc::vec::Vec; +use jaq_all::jaq_core::Exn; use jaq_all::json::Val; use std::io::{self, BufRead, Write}; use std::process::ExitCode; @@ -51,7 +52,8 @@ fn run_test(test: Test) -> Result<(Val, Val), Error> { let vars = Default::default(); let input = core::iter::once(parse_single(test.input.as_bytes()).map_err(|e| e.to_string())); run(runner, &filter, vars, input, Error::Parse, |v| { - obtain.push(v.map_err(|x| x.err_or_halt(Error::Jaq, Error::Halt).ok().unwrap())?); + let halt = |e: Exn<_>| Error::Halt(e.get_halt().ok().unwrap()); + obtain.push(v.map_err(|e| e.get_err().map_or_else(halt, Error::Jaq))?); Ok(()) })?; From 10af6733251a637158f6faaf762d08bcc8258575 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Michael=20F=C3=A4rber?= <01mf02@gmail.com> Date: Tue, 26 May 2026 06:19:17 +0200 Subject: [PATCH 14/14] Documentation. --- jaq-core/src/exn.rs | 2 ++ jaq-core/src/val.rs | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/jaq-core/src/exn.rs b/jaq-core/src/exn.rs index c62ebe24d..ec8faa78a 100644 --- a/jaq-core/src/exn.rs +++ b/jaq-core/src/exn.rs @@ -8,6 +8,8 @@ use core::fmt::{self, Display}; /// /// This is either an error, a runtime halt, or control flow data internal to jaq. /// Users should only be able to observe the first two cases. +/// +/// Use [`crate::unwrap_valr`] to convert a [`crate::ValX`] to an error. #[derive(Clone, Debug)] pub struct Exn<'a, V>(pub(crate) Inner<'a, V>); diff --git a/jaq-core/src/val.rs b/jaq-core/src/val.rs index 2d8676794..d6a6e1d60 100644 --- a/jaq-core/src/val.rs +++ b/jaq-core/src/val.rs @@ -18,7 +18,7 @@ pub type ValR = Result>; pub type ValRs<'a, T, V = T> = BoxIter<'a, ValR>; /// Value or eXception. /// -/// Use [`Exn::handle`](crate::Exn::handle) to extract the [`Error`](crate::Error) from an [`Exn`](crate::Exn). +/// Use [`unwrap_valr`] to convert to [`ValR`]. pub type ValX<'a, T, V = T> = Result>; /// Stream of values and eXceptions. pub type ValXs<'a, T, V = T> = BoxIter<'a, ValX<'a, T, V>>;