Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions crates/bashkit/docs/jq.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,16 @@ shim adds or overrides:
| `input` / `inputs` | Pull from the same stream as the main loop; with `-n` the whole stream is theirs. `input` fails with `No more inputs` at the end. |
| Most other 1.7/1.8 stdlib filters | Forwarded from `jaq-std` (`getpath`, `paths`, `to_entries`, `group_by`, `ltrimstr`/`rtrimstr`, `splits`, `test`, `now`, `debug`, `limit`, etc.). |

## Numbers

Computed numbers print like jq: `pow(2;10)` is `1024`, `1e17*1` is `1e+17`,
`0.00001*1` is `1e-05`, NaN is `null` and infinities print as the largest
double. Input literals print as read until arithmetic changes them (`1.0`
stays `1.0`). Division by zero is an error, `%` truncates its operands to
integers, a fractional array index is truncated, and `gamma` is the log-gamma
function, all as in jq. Math and index errors use jq's wording, so
`try ("a"+1) catch .` gives `string ("a") and number (1) cannot be added`.

## Errors

Filter compile failures exit `3`. A runtime error is reported as
Expand Down
8 changes: 6 additions & 2 deletions crates/bashkit/src/builtins/jq/convert.rs
Original file line number Diff line number Diff line change
Expand Up @@ -184,8 +184,12 @@ pub(super) fn jq_to_val(v: &JqJson) -> Val {
{
return Val::from(i);
}
if let Ok(f) = s.parse::<f64>() {
return Val::from(f);
// Other literals keep their token (`1.0` prints `1.0`, as in jq)
// until arithmetic turns them into floats.
if s.parse::<f64>().is_ok() {
return Val::Num(super::jaq_json::Num::Dec(super::jaq_json::Rc::new(
s.clone(),
)));
}
Val::from(0isize)
}
Expand Down
22 changes: 22 additions & 0 deletions crates/bashkit/src/builtins/jq/jaq_json/defs.jq
Original file line number Diff line number Diff line change
Expand Up @@ -15,3 +15,25 @@ def rindex($i): indices($i)[-1];
# Formatting
def @json: tojson;


# BASHKIT PATCH: jq refuses to divide by zero, so jaq-std's `0 / 0` and
# `1 / 0` definitions (and the filters built on them) are redefined.
# `gamma` is jq's log-gamma (C gamma()).
def infinite: 1e1000 * 1;
def nan: infinite - infinite;
def isnan: . < nan and nan < .;
def isinfinite: . == infinite or . == -infinite;
def isfinite: isnumber and (isinfinite | not);
def isnormal: isnumber and ((. == 0 or isnan or isinfinite) | not);
def logb:
if . == 0.0 then -infinite
elif isinfinite then infinite
elif isnan then .
else ilogb | . + 0.0 end;
def significand:
if isinfinite or isnan then .
elif . == 0.0 then 0.0
else scalbln(.; ilogb | -1 * .) end;
def finites: select(isfinite);
def normals: select(isnormal);
def gamma: lgamma;
87 changes: 87 additions & 0 deletions crates/bashkit/src/builtins/jq/jaq_json/jqmsg.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,87 @@
// BASHKIT PATCH: bashkit-only module (not from upstream jaq-json).
//! jq's wording for value errors, so `try ... catch .` sees the same text as
//! jq (`string ("a") and number (1) cannot be added`).

use super::{Num, Val};
use jaq_core::{ops, Error};
use std::string::{String, ToString};

fn type_name(v: &Val) -> &'static str {
match v {
Val::Null => "null",
Val::Bool(_) => "boolean",
Val::Num(_) => "number",
Val::BStr(_) | Val::TStr(_) => "string",
Val::Arr(_) => "array",
Val::Obj(_) => "object",
}
}

/// jq dumps an operand into a 15-byte buffer: longer dumps keep 11 bytes
/// and end in `...`.
fn dump_trunc(v: &Val) -> String {
let mut s = String::new();
// Render at most a little past the cut, whatever the value's size.
for c in v.to_string().chars() {
s.push(c);
if s.len() > 14 {
break;
}
}
if s.len() > 14 {
let mut end = 11;
while !s.is_char_boundary(end) {
end -= 1;
}
s.truncate(end);
s.push_str("...");
}
s
}

fn described(v: &Val) -> String {
std::format!("{} ({})", type_name(v), dump_trunc(v))
}

/// `l op r` failed; `zero` marks a zero divisor.
pub(crate) fn math(l: Val, op: ops::Math, r: Val, zero: bool) -> Error<Val> {
let verb = match op {
ops::Math::Add => "added",
ops::Math::Sub => "subtracted",
ops::Math::Mul => "multiplied",
ops::Math::Div => "divided",
ops::Math::Rem => "divided (remainder)",
};
let why = if zero { " because the divisor is zero" } else { "" };
Error::str(std::format!(
"{} and {} cannot be {verb}{why}",
described(&l),
described(&r)
))
}

/// `l[r]` is not defined.
pub(crate) fn index(l: Val, r: Val) -> Error<Val> {
let right = match &r {
Val::BStr(_) | Val::TStr(_) => std::format!("string {}", r),
other => type_name(other).to_string(),
};
Error::str(std::format!("Cannot index {} with {right}", type_name(&l)))
}

pub(crate) fn is_zero(v: &Val) -> bool {
matches!(v, Val::Num(n) if n.as_f64() == 0.0)
}

/// jq's `%` works on integers: fractional operands are truncated
/// (saturating at the 64-bit range).
pub(crate) fn trunc_int(n: &Num) -> Num {
if n.is_int() {
return n.clone();
}
let f = n.as_f64();
if f.is_nan() {
return Num::Int(0);
}
Num::Int(f.trunc() as i64 as isize)
}
44 changes: 36 additions & 8 deletions crates/bashkit/src/builtins/jq/jaq_json/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,8 @@


mod funs;
// BASHKIT PATCH: jq's wording for math and index errors.
mod jqmsg;
// BASHKIT PATCH: live-memory meter (TM-DOS-110).
pub mod meter;
mod num;
Expand Down Expand Up @@ -526,14 +528,30 @@ impl Val {
let indices = iw.filter_map(|(i, w)| (w == ***y).then_some(i));
Some(indices.map(Val::from).collect())
}
// BASHKIT PATCH: jq truncates a fractional array index.
(Val::Arr(a), Val::Num(n @ (Num::Float(_) | Num::Dec(_)))) => {
let f = n.as_f64();
if !f.is_finite() {
None
} else {
let i = Num::Int(f.trunc() as isize);
i.as_pos_usize()
.and_then(|i| abs_index(i, a.len()))
.map(|i| a[i].clone())
}
}
// BASHKIT PATCH: jq objects have string keys only.
(s @ Val::Obj(_), i) if !matches!(i, Val::TStr(_) | Val::BStr(_)) => {
return Err(jqmsg::index(s, index.clone()))
}
(Val::Obj(o), i) => o.get(i).cloned(),
(v @ (Val::BStr(_) | Val::TStr(_) | Val::Arr(_)), Val::Obj(o)) => {
use jaq_core::ValT;
let start = o.get(&Val::utf8_str("start"));
let end = o.get(&Val::utf8_str("end"));
return v.range(start..end).map(Some);
}
(s, _) => return Err(Error::index(s, index.clone())),
(s, _) => return Err(jqmsg::index(s, index.clone())), // BASHKIT PATCH
})
}
}
Expand Down Expand Up @@ -627,7 +645,7 @@ impl core::ops::Add for Val {
o.resync();
Ok(Obj(l))
}
(l, r) => Err(Error::math(l, ops::Math::Add, r)),
(l, r) => Err(jqmsg::math(l, ops::Math::Add, r, false)), // BASHKIT PATCH
}
}
}
Expand All @@ -645,7 +663,7 @@ impl core::ops::Sub for Val {
a.resync(); // BASHKIT PATCH (TM-DOS-110)
Ok(Self::Arr(l))
}
(l, r) => Err(Error::math(l, ops::Math::Sub, r)),
(l, r) => Err(jqmsg::math(l, ops::Math::Sub, r, false)), // BASHKIT PATCH
}
}
}
Expand Down Expand Up @@ -691,7 +709,7 @@ impl core::ops::Mul for Val {
obj_merge(&mut l, r);
Ok(Obj(l))
}
(l, r) => Err(Error::math(l, ops::Math::Mul, r)),
(l, r) => Err(jqmsg::math(l, ops::Math::Mul, r, false)), // BASHKIT PATCH
}
}
}
Expand All @@ -718,10 +736,14 @@ impl core::ops::Div for Val {
split(&x, &y).map(|s| into(x.slice_ref(s))).collect()
};
match (self, rhs) {
// BASHKIT PATCH: jq refuses to divide by zero.
(l @ Self::Num(_), r @ Self::Num(_)) if jqmsg::is_zero(&r) => {
Err(jqmsg::math(l, ops::Math::Div, r, true))
}
(Self::Num(x), Self::Num(y)) => Ok(Self::Num(x / y)),
(Self::TStr(x), Self::TStr(y)) => Ok(fs(x.into_bytes(), y.into_bytes(), Val::utf8_str)),
(Self::BStr(x), Self::BStr(y)) => Ok(fs(x.into_bytes(), y.into_bytes(), Val::byte_str)),
(l, r) => Err(Error::math(l, ops::Math::Div, r)),
(l, r) => Err(jqmsg::math(l, ops::Math::Div, r, false)), // BASHKIT PATCH
}
}
}
Expand All @@ -731,10 +753,16 @@ impl core::ops::Rem for Val {
fn rem(self, rhs: Self) -> Self::Output {
meter::tick(); // BASHKIT PATCH (TM-DOS-110)
match (self, rhs) {
(Self::Num(x), Self::Num(y)) if !(x.is_int() && y.is_int() && y == Num::Int(0)) => {
Ok(Self::Num(x % y))
// BASHKIT PATCH: jq truncates both operands to integers and
// refuses a zero divisor.
(Self::Num(x), Self::Num(y)) => {
let (l, r) = (jqmsg::trunc_int(&x), jqmsg::trunc_int(&y));
match (l, r) {
(_, Num::Int(0)) => Err(jqmsg::math(Self::Num(x), ops::Math::Rem, Self::Num(y), true)),
(l, r) => Ok(Self::Num(l % r)),
}
}
(l, r) => Err(Error::math(l, ops::Math::Rem, r)),
(l, r) => Err(jqmsg::math(l, ops::Math::Rem, r, false)), // BASHKIT PATCH
}
}
}
Expand Down
72 changes: 68 additions & 4 deletions crates/bashkit/src/builtins/jq/jaq_json/num.rs
Original file line number Diff line number Diff line change
Expand Up @@ -398,11 +398,75 @@ impl fmt::Display for Num {
match self {
Self::Int(i) => write!(f, "{i}"),
Self::BigInt(i) => write!(f, "{i}"),
Self::Float(x) if x.is_nan() => write!(f, "NaN"),
Self::Float(f64::INFINITY) => write!(f, "Infinity"),
Self::Float(f64::NEG_INFINITY) => write!(f, "-Infinity"),
Self::Float(x) => ryu::Buffer::new().format_finite(*x).fmt(f),
// BASHKIT PATCH: print floats like jq 1.7 (`1024`, `1e+17`,
// `1e-05`, `-0`; NaN as null, infinities as the largest double).
Self::Float(x) => f.write_str(&jq_format_f64(*x)),
Self::Dec(n) => write!(f, "{n}"),
}
}
}

/// BASHKIT PATCH: jq's double formatting (`jvp_dtoa_fmt`): shortest
/// round-trip digits, plain notation unless the decimal exponent is below
/// -3 or more than 15 past the last digit, then `d.ddde±XX`.
pub(crate) fn jq_format_f64(x: f64) -> String {
if x.is_nan() {
return "null".into();
}
let x = if x.is_infinite() { f64::MAX.copysign(x) } else { x };
let sign = if x.is_sign_negative() { "-" } else { "" };
if x == 0.0 {
return format!("{sign}0");
}
let sci = format!("{:e}", x.abs());
let (mant, exp) = sci.split_once('e').unwrap_or((&sci, "0"));
let digits: String = mant.chars().filter(|c| *c != '.').collect();
let nd = digits.len() as i64;
let decpt = exp.parse::<i64>().unwrap_or(0) + 1;
if decpt <= -4 || decpt > nd + 15 {
let e = decpt - 1;
let (head, tail) = digits.split_at(1);
let frac = if tail.is_empty() { String::new() } else { format!(".{tail}") };
let esign = if e < 0 { '-' } else { '+' };
format!("{sign}{head}{frac}e{esign}{:02}", e.abs())
} else if decpt <= 0 {
format!("{sign}0.{}{digits}", "0".repeat((-decpt) as usize))
} else if decpt >= nd {
format!("{sign}{digits}{}", "0".repeat((decpt - nd) as usize))
} else {
let (a, b) = digits.split_at(decpt as usize);
format!("{sign}{a}.{b}")
}
}

#[test]
fn jq_format_f64_matches_jq() {
let cases: &[(f64, &str)] = &[
(1e17, "1e+17"),
(1e16, "1e+16"),
(1e15 + 1.0, "1000000000000001"),
(1e19, "1e+19"),
(123456789012345678.0, "123456789012345680"),
(0.0001, "0.0001"),
(0.00001, "1e-05"),
(3.0, "3"),
(1.0 / 3.0, "0.3333333333333333"),
(1e300, "1e+300"),
(1.5e300, "1.5e+300"),
(0.1 + 0.2, "0.30000000000000004"),
(1e20, "1e+20"),
(12345678901234567890123.0, "12345678901234568000000"),
(f64::MAX, "1.7976931348623157e+308"),
(5e-324, "5e-324"),
(1e15 + 0.5, "1000000000000000.5"),
(-2.5, "-2.5"),
(-0.0, "-0"),
(0.0, "0"),
(f64::INFINITY, "1.7976931348623157e+308"),
(f64::NEG_INFINITY, "-1.7976931348623157e+308"),
(f64::NAN, "null"),
];
for (x, want) in cases {
assert_eq!(jq_format_f64(*x), *want, "{x}");
}
}
Loading
Loading