diff --git a/docs/stdlib.dj b/docs/stdlib.dj index 0599204ed..0019cfc01 100644 --- a/docs/stdlib.dj +++ b/docs/stdlib.dj @@ -2222,6 +2222,13 @@ $ jaq -n '1, halt(42), 2'; echo $? `jq` does not implement `halt($exit_code)`, only `halt`. ::: +::: 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)` diff --git a/jaq-all/examples/main.rs b/jaq-all/examples/main.rs index 1cc93bd0b..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,7 +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| Error::new(ErrorKind::Other, e.to_string())); + 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-all/src/data.rs b/jaq-all/src/data.rs index 361488599..12b4d4e28 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::{data, DataT, Lut, Vars}; use jaq_fmts::write::Writer; -use jaq_json::{Val, ValR}; +use jaq_json::{Val, ValX}; use jaq_std::input::{self, Inputs, RcIter}; /// Filter for given kind of data. @@ -93,7 +93,7 @@ pub fn run( vars: Vars, inputs: impl Iterator>, fi: impl Fn(String) -> E, - mut f: impl FnMut(ValR) -> Result<(), 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 +109,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..ec8faa78a 100644 --- a/jaq-core/src/exn.rs +++ b/jaq-core/src/exn.rs @@ -6,10 +6,10 @@ 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. +/// 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::val::unwrap_valr`] to convert a [`crate::val::ValX`] to an error. +/// 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>); @@ -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(i32), } #[derive(Clone, Debug)] @@ -48,12 +49,27 @@ 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> { + pub fn get_err(self) -> Result, Self> { match self.0 { 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), + } + } + + /// 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)) + } } impl From> for Exn<'_, V> { diff --git a/jaq-core/src/val.rs b/jaq-core/src/val.rs index 86cb50630..d6a6e1d60 100644 --- a/jaq-core/src/val.rs +++ b/jaq-core/src/val.rs @@ -30,8 +30,20 @@ pub type ValXs<'a, T, V = T> = BoxIter<'a, ValX<'a, T, V>>; /// /// 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 { - v.map_err(|e| e.get_err().ok().unwrap()) + #[cfg(feature = "std")] + let exit = |exit_code| std::process::exit(exit_code); + #[cfg(not(feature = "std"))] + let exit = |exit_code| panic!("halt({})", exit_code); + + 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 16bbd0940..f5b5b47ef 100644 --- a/jaq-play/src/lib.rs +++ b/jaq-play/src/lib.rs @@ -6,8 +6,9 @@ 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, 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 +135,7 @@ impl Settings { enum Error { Hifijson(String), Jaq(json::Error), + Halt(i32), } #[wasm_bindgen] @@ -146,8 +148,10 @@ 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 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), @@ -166,6 +170,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-std/src/lib.rs b/jaq-std/src/lib.rs index 3a7a2c1c3..84a8684c1 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) } @@ -442,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)))) + }), ]) } @@ -467,10 +469,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_isize(); - bome(exit_code.map(|exit_code| std::process::exit(exit_code as i32))) - }), ]) } diff --git a/jaq/src/filter.rs b/jaq/src/filter.rs index 5eef58a2b..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(Error::Jaq)?; + 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/main.rs b/jaq/src/main.rs index 3328a1273..31dae8114 100644 --- a/jaq/src/main.rs +++ b/jaq/src/main.rs @@ -228,6 +228,7 @@ enum Error { Report(Vec>), Parse(String), Jaq(jaq_all::json::Error), + Halt(i32), FalseOrNull, NoOutput, } @@ -249,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 { @@ -277,6 +278,8 @@ impl Termination for Error { Self::Report(_) => 3, Self::NoOutput => 4, Self::Parse(_) | Self::Jaq(_) => 5, + // ExitCode ~= u8, but exit_code: i32 + Self::Halt(exit_code) => std::process::exit(exit_code), }) } } diff --git a/jaq/src/tests.rs b/jaq/src/tests.rs index c858fa73c..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(Error::Jaq)?); + 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(()) })?;