Skip to content

fix(jq): round large int literals to 17 decimal digits before f64 arithmetic (#2906) - #2939

Merged
newhoggy merged 5 commits into
mainfrom
issue-2906-jq-large-int-round
Sep 14, 2026
Merged

fix(jq): round large int literals to 17 decimal digits before f64 arithmetic (#2906)#2939
newhoggy merged 5 commits into
mainfrom
issue-2906-jq-large-int-round

Conversation

@newhoggy

@newhoggy newhoggy commented Sep 14, 2026

Copy link
Copy Markdown
Contributor

Summary

Closes #2906.

succinctly jq -n '869389897822472004 + 944331' printed 869389897823416400; jq 1.7.1 prints 869389897823416300. The issue recorded a residual "no model explains ~4% of cases" bucket and shelved it as needing arbitrary-precision decimal arithmetic. It does not: the earlier analysis modelled the operands as exact integers.

Root cause (from jq 1.7.1 src/jv.c, not black-box fitting): every parsed number is kept as an exact decNumber literal, and the first time arithmetic reads it, jvp_literal_number_to_double runs decNumberReduce under a DECIMAL64 context with digits = 17 (round-half-even), then a correctly-rounded strtod. That double rounding lands on a different double than i64 as f64 for some 18/19-digit values: 869389897822472004869389897822472000 → nearest double …471936 (tie to even), whereas the exact integer's nearest double is …472064. Computed numbers are plain doubles and are never re-rounded. Modelled that way, jq's answer was reproduced on every one of 1200 random + - * / cases, 200 input-sourced cases (negatives, over-i64), 100 program-literal negatives, and — after the fix — 4148/4150 random differential cases spanning arithmetic, comparisons and the ordering builtins (the two remaining mismatches are >17-digit float literals, filed as #2936).

For an i64 the two conversions differ only at |n| >= 10^17 (18–19 digits); everything in [2^53, 10^17) was already right.

Scope: integers, jq mode only, both evaluators. yq mode's exact int64 arithmetic and plain-cast widening are untouched (pinned by a yq companion test; real yq v4.53.3 gives 869389897823416335).

Changes

  • jq_literal_int_to_f64 (src/jq/value.rs): the 17-digit half-even rounding with integer arithmetic only (identity below 10^17), gated by a new EvalSemantics::DECNUMBER_LITERALS (true for jq, false for yq) through int_to_f64::<S>.
  • Arithmetic: jq_checked_int_arith's fallback and the mixed Int/Float arms of + - * / widen through it; % follows jq's binop_mod (each operand's double truncated to intmax_t, result held as a double) — in-range operands stay a single wrapping_rem, the rest delegate to mod_floats. 869389897822472004 % 1000 is now 936 (jq) instead of the exact 4; 9007199254740993 % 2 is 0.
  • Comparison follows jvp_number_cmp's two rules via a new jq_numeric_cmp: a literal against a computed double widens through the rounding; two literals compare exactly as decimals (decNumberCompare) — doubles first (monotonic, so a strict order is exact), digit comparison only on a tie. jq-mode == (owned_value_eq::<S>) and ordering (compare_values, now generic over S, so sort/unique/group_by/min/max/bsearch/delpaths inherit it) go through it. The mode-blind PartialEq keeps the old plain widening — the yq presentation layer relies on it for comment alignment (a review round caught that routing it through the jq rounding moved yq comments onto the wrong elements). As a consequence 9007199254740993 == 9007199254740992.0 is now jq's false rather than a recorded divergence.
  • docs/compliance/jq/limitations.md: the jq: large-integer +/- arithmetic can round to a different f64 than real jq even when both operands are non-negative #2906 entry is rewritten as resolved, with the mechanism, what stays open, and two things the fix makes visible without changing (jq's dtoi UB at exactly 2^63 on x86_64; the pre-existing -x % y precedence gap).

Review round

/code-review high (8 angles) found two real regressions in the first comparison change — two literals must compare exactly, and the mode-blind PartialEq is what yq's comment alignment relies on — fixed in the second commit (jq_numeric_cmp; PartialEq back on the plain cast). The orchestrator's follow-up commit adds an Int/Int fast path to jq_numeric_cmp, reuses the module's sign/exponent helpers, and rewords the x86_64 dtoi note as unverified; the last commit clamps the saturated exponent that reuse introduced (a debug-build panic on 1e<45 digits> == 2e<45 digits>) and promotes the #2938 rows to positive expectations, since #2902 (now on main) makes the bridge hand a computed float back as a bare Float.

Out of scope, filed as follow-ups (pinned by a characterization test)

Test plan

  • New unit tests: the helper on boundary/tie/carry/i64::MIN/MAX vectors (bit-exact vs jq), a 20 000-sample seeded property test (string-oracle agreement via {:.16e}, sign symmetry, the "already a double ⇒ no-op" lemma, monotonicity, yq mode bit-identical), cmp_decimal_literals, and jq_numeric_cmp vs owned_value_eq::<JqSemantics>.
  • New CLI tests (tests/jq_cli_tests.rs, every expectation captured live from /usr/bin/jq 1.7.1): arithmetic incl. %, literal-vs-computed and literal-vs-literal comparisons through ==/</sort/unique/bsearch/index/IN/group_by/min, stdin/--argjson/tonumber/fromjson sources, must-not-change display/small-int/overflow/jq: unary minus in filter text preserves an exact literal where real jq collapses it to a double, masking divergences in the large-integer class #2357 rows, and the follow-up characterization test. yq companion in tests/yq_cli_tests.rs (captured from yq v4.53.3).
  • Differential fuzz vs jq 1.7.1: 5600/5600 (first round), 4148/4150 with literal-vs-literal pairs and float literals added (the 2 are jq: >17-significant-digit float and over-i64 integer literals must round to 17 digits before f64 (same mechanism as #2906) #2936).
  • cargo test --features cli --no-fail-fast, --no-default-features, default features; cargo clippy --all-targets --all-features -- -D warnings; cargo fmt --check; RUSTDOCFLAGS=-D warnings cargo doc --no-deps --all-features.
  • /code-review high (8 angles) — every finding applied (commits 2–5) or recorded as a follow-up.
  • CI perf-guard: compare_values is now generic over S (same shape as the existing owned_value_eq::<S>), which doubles its monomorphisation; no instruction-count change is expected on the guarded identity rows, but the release profile is codegen-sensitive there — watch that job.

@github-actions

github-actions Bot commented Sep 14, 2026

Copy link
Copy Markdown

Coverage

Total: 93.51% ⚪ 0.02 pp vs main

Comparing 950b8a5..2f95331 (merge-base → PR head)

File Before After Δ
src/jq/value.rs 99% 98.93% 🔴 -0.07 pp
🔇 0 ignored region(s), 91 tolerated region(s)

ignore removes the lines from both reports; tolerate keeps them in the reported percentage but scores them against the baseline, so a cross-run flip cannot move a delta. Regions are read from each revision's own source.

File Kind Lines Rev Reason
src/bin/succinctly/jq_runner.rs tolerate 572-584 both unreachable: try_parse_meta_op only fires under ParserMode::Yq (src/jq/parser.rs), and rewrite_namespaced_calls is only reached via ModuleProcessor::process_program, which jq_runner's own jq-mode run is the sole caller of -- so a MetaAssign node can never reach this function (#798)
src/bin/succinctly/jq_runner.rs tolerate 1539 both unreachable: widening the shadow-candidate set never rejects a program the first parse accepted -- a newly covered name only wraps an already-successful dedicated parse, and a failing one would have propagated its error in the first parse too, so the retry budget is charged at the identical sites in both (#2395)
src/bin/succinctly/jq_runner.rs tolerate 7404 both unreachable in a passing suite by design -- the fixed b\
src/bin/succinctly/yq_runner.rs tolerate 1638 both unreachable: bytes already parsed successfully by every caller (#1350)
src/bin/succinctly/yq_runner.rs tolerate 3299 both unreachable: path is always the raw output of the path(TARGET) builtin evaluated a few lines up in resolve_one_meta_assign -- path/1 is a jq/yq language invariant that always answers an array of path components (see Expr::Builtin(Builtin::PathNoArg) => Ok(Some(OwnedValue::Array(..))) in eval_generic.rs), never any other shape (#798)
src/bin/succinctly/yq_runner.rs tolerate 3399 both unreachable: resolve_meta_assign_writes runs expr through this before any evaluation begins (see its own doc comment), and Expr::Shared is never constructed by the parser -- only at eval time, by function-call argument substitution (substitute_func_param in eval.rs) -- so a pre-evaluation AST can never contain one here (#798)
src/bin/succinctly/yq_runner.rs tolerate 4028 both unreachable: every arm of the match result { .. } above that assigns docs (L3492-3622) constructs Ok(..) -- none ever produces Err, so this if let's implicit else can't be taken; symmetric to L1625's ? (#798)
src/jq/document.rs tolerate 1069-1076 both unreachable: both implementors (StandardJson, YamlValue) override this to decode once; the default exists as the contract a future implementor inherits, and is deliberately the two-call sequence it replaces (#965)
src/jq/eval.rs tolerate 1140 both unreachable: def is always a collect_alias_groups anchor path, which step_to_expr never fails on (#1351)
src/jq/eval.rs tolerate 1161 both unreachable: redirect_paths with Redirect::SINGLE always contributes exactly one output per input, so a 1-element paths always pops Some (#1351)
src/jq/eval.rs tolerate 1170 both unreachable: a concrete setpath/delpaths path's components are always Field/Index -- step_to_expr never produces another shape (#1351)
src/jq/eval.rs tolerate 1174 both unreachable: the map above never yields None, since it only ever matches Field/Index (#1351)
src/jq/eval.rs tolerate 4247 both unreachable: is_escape() is exactly `Error
src/jq/eval.rs tolerate 4248 both unreachable: see the if let above -- push_owned_values never answers None for an is_escape() result (#2180)
src/jq/eval.rs tolerate 5396 both unreachable today: to_owned's only failures are is_decode_failure()-tagged, and suppresses() answers false for those whatever optional is -- the same defensive-but-dead arm eval_generic's own Builtin::Path materialization documents under #2280 (#2908)
src/jq/eval.rs tolerate 7277 both unreachable: optional is never true here. eval_each is entered with a forced true at exactly one site (Expr::Optional over an IndexExpr/SliceExpr), and both of those evaluate their target (eval_index_expr) and their key (eval_each(key, .., false)) with a hardcoded false, so only the final index/slice step ever sees it -- nothing carries it down to an Expr::Object (#2180)
src/jq/eval.rs tolerate 10587 both unreachable: only ever constructed by builtin_sort_keys's own eval_update_no_vivify call, whose enclosing eval_update_impl already runs to_owned on the whole document up front (#2855) -- a decode failure anywhere raises there, before this filter ever sees a value to re-decode; confirmed live, sort_keys(.a)/sort_keys(..) on a document with a decode-failure subtree both raise from the outer to_owned
src/jq/eval.rs tolerate 21116 both unreachable: escape_with_prefix! sets terminal before Demand::Stop; already returned above (#2138)
src/jq/eval.rs tolerate 21485 both unreachable: escape! sets terminal before Demand::Stop; already returned above (#2546)
src/jq/eval.rs tolerate 29787 both unreachable today: postfix ? attaches to a single path element until #367 reopens it, so no path this resolver builds has depth() != 1 here (#2649)
src/jq/eval.rs tolerate 30147 both unreachable: is_primitive admits only Identity/Field/Index/Slice, and of those only a Slice's computed bounds can halt -- all four have their own arm in resolve_node_sink/resolve_node_eager, so none reaches this function. Pre-existing; #2694 only wrapped the return in Some (#2694)
src/jq/eval.rs tolerate 30181 both unreachable, as this arm's own comment above says: indexing or slicing a value yields zero or one result, so is_primitive never produces more than one -- kept as a named error rather than a panic. Pre-existing; #2694 only wrapped the enclosing return in Some (#2694)
src/jq/eval.rs tolerate 31826 both unreachable: PatternStep::component only ever builds Expr::Field/Expr::Index, and navigation_element answers Some for both (#2649)
src/jq/eval.rs tolerate 35407 both unreachable in a passing suite by design -- a panic-message format argument for the #682 single-valued-tail pin, evaluated only if that assert's own condition is false (#2190)
src/jq/eval.rs tolerate 35458 both unreachable: classify_static_component answers Field only for OwnedValue::Object, and it was handed this very value (#2190)
src/jq/eval.rs tolerate 35463 both unreachable: classify_static_component answers Index only for OwnedValue::Array, and it was handed this very value (#2190)
src/jq/eval.rs tolerate 35540 both unreachable: the only caller reaches this after classify_static_component answered Field for this same value, which it does only for an object (#2190)
src/jq/eval.rs tolerate 35584 both unreachable: both callers establish the container first -- navigate_static_component_ref via classify_static_component's Index arm, and walk_path's Expr::Iterate arm by matching on the container itself (#2190)
src/jq/eval.rs tolerate 39099 both unreachable: stop_with_escape's only write is slot.set(Some(control)) with the control it was handed, which is always the Control::Error built one line above (#2180)
src/jq/eval.rs tolerate 39946 both unreachable by construction: the per-fork match only ever hands stop_with_downstream a non-Exhausted flow, so terminal can never hold Exhausted. foreach_forks' identical arm is 0-hit for the same reason and is only unflagged because it predates this diff (#2899)
src/jq/eval.rs tolerate 52268 both unreachable in practice: a def with more than 64 parameters; the fallback exists so ScopeMask's one-bit-per-parameter u64 is a performance ceiling rather than a correctness limit (#2633)
src/jq/eval.rs tolerate 52280 both unreachable: bind_def_call only calls this for a non-empty params, and install_def_calls only builds a DefCall whose args.len() equals params.len(), so the zip is never empty here (#2560)
src/jq/eval.rs tolerate 52300 both unreachable in practice: needs more than 32 duplicated-name parameters; see the non-duplicate path's own note (#2633)
src/jq/eval.rs tolerate 52321 both unreachable: name was just read from params, so the zip over (params, args) has a matching pair unless args is shorter than params, which install_def_calls' own arity guard rules out (#2560)
src/jq/eval.rs tolerate 52338 both unreachable: params is non-empty here (bind_def_call's own guard) and its first entry is never skipped, so at least one substitution always ran (#2560)
src/jq/eval.rs tolerate 52399 both unreachable: same arity invariant as bind_def_call_params' own copy of this loop -- name came from params, so the zip has a matching pair unless args is shorter, which install_def_calls rules out (#2560)
src/jq/eval.rs tolerate 53717 both substitute_var_impl's FuncDef arm always returns FuncDef (#2283)
src/jq/eval.rs tolerate 53739 both substitute_var_impl's FuncDef arm always returns FuncDef (#2283)
src/jq/eval.rs tolerate 53757 both substitute_var_impl's FuncDef arm always returns FuncDef (#2283)
src/jq/eval.rs tolerate 53827 both substitute_var_impl's FuncDef arm always returns FuncDef (#2283)
src/jq/eval.rs tolerate 53841 both substitute_func_param_impl's FuncDef arm always returns FuncDef (#2555)
src/jq/eval.rs tolerate 60841 both unreachable in a passing suite by design -- this is the failure message for the assertion the test exists to make (#2190)
src/jq/eval.rs tolerate 89202 both unreachable in a passing suite by design -- every filter this helper is called with parses to an AsPattern (#2649)
src/jq/eval.rs tolerate 89234 both unreachable in a passing suite by design -- every call site passes the origin of a binding this same test already proved carries a marker (#2649)
src/jq/eval.rs tolerate 90114 both unreachable in a passing suite by design -- every row here is a shape jq accepts, confirmed live (#2649)
src/jq/eval.rs tolerate 90182 both unreachable in a passing suite by design -- every row here is a shape jq refuses, confirmed live (#2649)
src/jq/eval.rs tolerate 90546 both unreachable in a passing suite by design -- this is the panic message for the #2072 pin itself, only formatted if the let-else pattern fails to match (#2072)
src/jq/eval.rs tolerate 90557 both unreachable in a passing suite by design -- this is the panic message for the #2072 pin itself, only formatted if the let-else pattern fails to match (#2072)
src/jq/eval.rs tolerate 90578 both unreachable in a passing suite by design -- this is the panic message for the #2072 pin itself, only formatted if the match doesn't hit the expected arm above (#2072)
src/jq/eval.rs tolerate 90594 both unreachable in a passing suite by design -- this is the panic message for the #2072 pin itself, only formatted if the match doesn't hit the expected arm above (#2072)
src/jq/eval_generic.rs tolerate 3569 both unreachable: the sole remaining caller (retain_truthy_generic's Many arm) runs to_owned on an item before keeping it, so re-converting a kept item here cannot fail; the ManyCursor caller that made this reachable went with the truthiness walk (#2692, re-establishing #2661's premise)
src/jq/eval_generic.rs tolerate 6923 both unreachable in a passing suite by design -- this is the panic message for the #2368 pin itself, only formatted if the assert's own condition is false (#2368)
src/jq/eval_generic.rs tolerate 10664 both unreachable: every producer that reaches this empty-exprs tail (each_lazy_keys_iterate_sink's sorted/!sorted arms, each_lazy_index_range_iterate_sink, each_lazy_seq_iterate_sink) yields OneCursorValue/OneCursor/Owned, never a cursorless GenericItem::One -- so cursor is always Some here; kept for exhaustiveness/symmetry with the Some arm (#2103)
src/jq/eval_generic.rs tolerate 13771 both unreachable: escape_generic!/ensure_owned! set terminal before Demand::Stop; already returned above (#2138)
src/jq/eval_generic.rs tolerate 14125 both unreachable: escape! sets terminal before Demand::Stop; already returned above (#2546)
src/jq/eval_generic.rs tolerate 16578 both unreachable: len_checked and SliceBounds::resolve already bound every index in range to [0, len), so get_cursor cannot miss (#2168)
src/jq/eval_generic.rs tolerate 20236 both unreachable: malformed_object_member above already proved every key stringifies (the None half), and to_owned_cursor on an untagged key decoded_key_str decoded cannot fail (the Err half) (#2785)
src/jq/eval_generic.rs tolerate 20317 both unreachable: map(f) over an array emits exactly one array; kept so a future map shape produces no output rather than a panic (#2785)
src/jq/eval_generic.rs tolerate 20585 both unreachable by design -- eval_single's #2368 debug_assert forbids optional=true on Builtin::Reverse, so length never answers None here (#2730)
src/jq/eval_generic.rs tolerate 22444 both unreachable by construction: every shape either guard admits now has an arm above (#2771), and expr_dispatch_catchall_guards_default_conservatively_2549 pins both guards' _ => false defaults directly
src/jq/eval_generic.rs tolerate 22944 both unreachable in a passing suite by design -- owned_identity_rule maps a bare Expr::Var to Bound too (for the static gate, which sees a body before its as substitution runs), but every runtime dispatch that reaches this rule (owned_identity_after_stage/owned_identity_placed_by, from owned_identity_leaving_cursor's Bound arm) only ever sees a stage after eval_owned_identity_as's unconditional substitute_bound_var_from call, which always turns $x into Expr::TrackedVar before recursing -- confirmed by running the full suite with this arm replaced by a hard panic!(), which never fired (#2072)
src/jq/eval_generic.rs tolerate 24781 both unreachable in a passing suite by design -- the fixture's map(.+1) is always a LazySeq; this arm is the test's own diagnostic (#2666)
src/jq/eval_generic.rs tolerate 24809 both unreachable in a passing suite by design -- the fixture's only escape is Control::Error; this arm is the test's own diagnostic (#2666)
src/json/light.rs tolerate 6311 both unreachable in a passing suite by design -- this is a panic-message format argument for the #2072 pin itself, only evaluated if the assert's own condition is false (#2072)
src/json/light.rs tolerate 6322 both unreachable in a passing suite by design -- this is a panic-message format argument for the #2072 pin itself, only evaluated if the assert's own condition is false (#2072)
src/json/light.rs tolerate 6403 both unreachable in a passing suite by design -- this is a panic-message format argument for the #2072 pin itself, only evaluated if the assert's own condition is false (#2072)
src/yaml/index.rs tolerate 1268 both unreachable: YamlIndex::build always wraps the parsed document(s) in a virtual root Sequence, at TY index 0 (#798)
src/yaml/index.rs tolerate 1272 both unreachable: every fixture field_key_head_foot is called with in this test module is a top-level mapping (#798)
src/yaml/index.rs tolerate 1283 both unreachable: a mapping key is always emitted as YamlValue::String -- it is never type-inferred like a value (#222), so this if-let's pattern can never fail to match (#798)
src/yaml/index.rs tolerate 1285 both unreachable: every call to field_key_head_foot in this test module passes a key that the fixture's mapping actually has (#798)
src/yaml/index.rs tolerate 1295 both unreachable: YamlIndex::build always wraps the parsed document(s) in a virtual root Sequence, at TY index 0 (#798)
src/yaml/index.rs tolerate 1299 both unreachable: every fixture seq_item_head_foot is called with in this test module is a top-level sequence (#798)
src/yaml/index.rs tolerate 1322 both unreachable: YamlIndex::build always wraps the parsed document(s) in a virtual root Sequence, at TY index 0 (#798)
src/yaml/index.rs tolerate 1341 both unreachable: YamlIndex::build always wraps the parsed document(s) in a virtual root Sequence, at TY index 0 (#798)
src/yaml/index.rs tolerate 1364 both unreachable: YamlIndex::build always wraps the parsed document(s) in a virtual root Sequence, at TY index 0 (#798)
src/yaml/index.rs tolerate 1373 both unreachable: every fixture field_key_head_foot_in_doc is called with in this test module is a top-level mapping (#798)
src/yaml/index.rs tolerate 1384 both unreachable: a mapping key is always emitted as YamlValue::String -- it is never type-inferred like a value (#222), so this if-let's pattern can never fail to match (#798)
src/yaml/index.rs tolerate 1386 both unreachable: every call to field_key_head_foot_in_doc in this test module passes a key that the fixture's document actually has (#798)
src/yaml/index.rs tolerate 1397 both unreachable: YamlIndex::build always wraps the parsed document(s) in a virtual root Sequence, at TY index 0 (#798)
src/yaml/index.rs tolerate 1401 both unreachable: every fixture nested_key_head_foot is called with in this test module is a top-level mapping (#798)
src/yaml/index.rs tolerate 1405 both unreachable: a mapping key is always emitted as YamlValue::String -- it is never type-inferred like a value (#222), so this let-else's pattern can never fail to match (#798)
src/yaml/index.rs tolerate 1411 both unreachable: every fixture nested_key_head_foot is called with has a nested mapping under outer (#798)
src/yaml/index.rs tolerate 1422 both unreachable: a mapping key is always emitted as YamlValue::String -- it is never type-inferred like a value (#222), so this if-let's pattern can never fail to match (#798)
src/yaml/index.rs tolerate 1425 both unreachable: every call to nested_key_head_foot in this test module passes an outer.inner pair that the fixture actually has (#798)
src/yaml/light.rs tolerate 3364 both unreachable: an alias target is never None for a built index (#1374)
src/yaml/light.rs tolerate 15398 both unreachable in a passing suite by design -- this is a panic-message format argument for the #2072 pin itself, only evaluated if the assert's own condition is false (#2072)
src/yaml/light.rs tolerate 15409 both unreachable in a passing suite by design -- this is a panic-message format argument for the #2072 pin itself, only evaluated if the assert's own condition is false (#2072)
src/yaml/light.rs tolerate 15566 both unreachable in a passing suite by design -- this is a panic-message format argument for the #2072 pin itself, only evaluated if the assert's own condition is false (#2072)
src/yaml/parser.rs tolerate 1583 both unreachable: every block-sequence open registers a frame at its own depth before any item of it can be parsed (#1079)
src/yaml/parser.rs tolerate 1613 both unreachable: this function's sole caller (record_standalone_comment) only invokes it from inside a match on pending_head_lines.last(), so pending_head_lines is already known non-empty here (#798)
src/yaml/parser.rs tolerate 7999 both unreachable: every byte here already passed the [0-9.eE+-] charset check above, a strict subset of ASCII, so str::from_utf8 can never fail (#2778)

Patch coverage

Patch: 98.23% (445/453 new lines covered)

File Patch Uncovered new lines
src/jq/eval.rs 98.8% (82/83) 9797
src/jq/eval_generic.rs 92.31% (24/26) 23050-23051
src/jq/value.rs 98.55% (339/344) 2815, 2979, 3068, 3073, 4066
Uncovered new lines (8)
  • src/jq/eval.rs:9797
  • src/jq/eval_generic.rs:23050
  • src/jq/eval_generic.rs:23051
  • src/jq/value.rs:2815
  • src/jq/value.rs:2979
  • src/jq/value.rs:3068
  • src/jq/value.rs:3073
  • src/jq/value.rs:4066

📦 Full per-file coverage summary · run summary

@github-actions

github-actions Bot commented Sep 14, 2026

Copy link
Copy Markdown

Coverage

Total: 93.42% ⚪ 0.02 pp vs main

Comparing 950b8a5..2f95331 (merge-base → PR head)

File Before After Δ
src/jq/value.rs 99% 98.93% 🔴 -0.07 pp
🔇 0 ignored region(s), 92 tolerated region(s)

ignore removes the lines from both reports; tolerate keeps them in the reported percentage but scores them against the baseline, so a cross-run flip cannot move a delta. Regions are read from each revision's own source.

File Kind Lines Rev Reason
src/bin/succinctly/jq_runner.rs tolerate 572-584 both unreachable: try_parse_meta_op only fires under ParserMode::Yq (src/jq/parser.rs), and rewrite_namespaced_calls is only reached via ModuleProcessor::process_program, which jq_runner's own jq-mode run is the sole caller of -- so a MetaAssign node can never reach this function (#798)
src/bin/succinctly/jq_runner.rs tolerate 1539 both unreachable: widening the shadow-candidate set never rejects a program the first parse accepted -- a newly covered name only wraps an already-successful dedicated parse, and a failing one would have propagated its error in the first parse too, so the retry budget is charged at the identical sites in both (#2395)
src/bin/succinctly/jq_runner.rs tolerate 7404 both unreachable in a passing suite by design -- the fixed b\
src/bin/succinctly/yq_runner.rs tolerate 1638 both unreachable: bytes already parsed successfully by every caller (#1350)
src/bin/succinctly/yq_runner.rs tolerate 3299 both unreachable: path is always the raw output of the path(TARGET) builtin evaluated a few lines up in resolve_one_meta_assign -- path/1 is a jq/yq language invariant that always answers an array of path components (see Expr::Builtin(Builtin::PathNoArg) => Ok(Some(OwnedValue::Array(..))) in eval_generic.rs), never any other shape (#798)
src/bin/succinctly/yq_runner.rs tolerate 3399 both unreachable: resolve_meta_assign_writes runs expr through this before any evaluation begins (see its own doc comment), and Expr::Shared is never constructed by the parser -- only at eval time, by function-call argument substitution (substitute_func_param in eval.rs) -- so a pre-evaluation AST can never contain one here (#798)
src/bin/succinctly/yq_runner.rs tolerate 4028 both unreachable: every arm of the match result { .. } above that assigns docs (L3492-3622) constructs Ok(..) -- none ever produces Err, so this if let's implicit else can't be taken; symmetric to L1625's ? (#798)
src/jq/document.rs tolerate 1069-1076 both unreachable: both implementors (StandardJson, YamlValue) override this to decode once; the default exists as the contract a future implementor inherits, and is deliberately the two-call sequence it replaces (#965)
src/jq/eval.rs tolerate 1140 both unreachable: def is always a collect_alias_groups anchor path, which step_to_expr never fails on (#1351)
src/jq/eval.rs tolerate 1161 both unreachable: redirect_paths with Redirect::SINGLE always contributes exactly one output per input, so a 1-element paths always pops Some (#1351)
src/jq/eval.rs tolerate 1170 both unreachable: a concrete setpath/delpaths path's components are always Field/Index -- step_to_expr never produces another shape (#1351)
src/jq/eval.rs tolerate 1174 both unreachable: the map above never yields None, since it only ever matches Field/Index (#1351)
src/jq/eval.rs tolerate 4247 both unreachable: is_escape() is exactly `Error
src/jq/eval.rs tolerate 4248 both unreachable: see the if let above -- push_owned_values never answers None for an is_escape() result (#2180)
src/jq/eval.rs tolerate 5396 both unreachable today: to_owned's only failures are is_decode_failure()-tagged, and suppresses() answers false for those whatever optional is -- the same defensive-but-dead arm eval_generic's own Builtin::Path materialization documents under #2280 (#2908)
src/jq/eval.rs tolerate 7277 both unreachable: optional is never true here. eval_each is entered with a forced true at exactly one site (Expr::Optional over an IndexExpr/SliceExpr), and both of those evaluate their target (eval_index_expr) and their key (eval_each(key, .., false)) with a hardcoded false, so only the final index/slice step ever sees it -- nothing carries it down to an Expr::Object (#2180)
src/jq/eval.rs tolerate 10587 both unreachable: only ever constructed by builtin_sort_keys's own eval_update_no_vivify call, whose enclosing eval_update_impl already runs to_owned on the whole document up front (#2855) -- a decode failure anywhere raises there, before this filter ever sees a value to re-decode; confirmed live, sort_keys(.a)/sort_keys(..) on a document with a decode-failure subtree both raise from the outer to_owned
src/jq/eval.rs tolerate 21116 both unreachable: escape_with_prefix! sets terminal before Demand::Stop; already returned above (#2138)
src/jq/eval.rs tolerate 21485 both unreachable: escape! sets terminal before Demand::Stop; already returned above (#2546)
src/jq/eval.rs tolerate 29787 both unreachable today: postfix ? attaches to a single path element until #367 reopens it, so no path this resolver builds has depth() != 1 here (#2649)
src/jq/eval.rs tolerate 30147 both unreachable: is_primitive admits only Identity/Field/Index/Slice, and of those only a Slice's computed bounds can halt -- all four have their own arm in resolve_node_sink/resolve_node_eager, so none reaches this function. Pre-existing; #2694 only wrapped the return in Some (#2694)
src/jq/eval.rs tolerate 30181 both unreachable, as this arm's own comment above says: indexing or slicing a value yields zero or one result, so is_primitive never produces more than one -- kept as a named error rather than a panic. Pre-existing; #2694 only wrapped the enclosing return in Some (#2694)
src/jq/eval.rs tolerate 31826 both unreachable: PatternStep::component only ever builds Expr::Field/Expr::Index, and navigation_element answers Some for both (#2649)
src/jq/eval.rs tolerate 35407 both unreachable in a passing suite by design -- a panic-message format argument for the #682 single-valued-tail pin, evaluated only if that assert's own condition is false (#2190)
src/jq/eval.rs tolerate 35458 both unreachable: classify_static_component answers Field only for OwnedValue::Object, and it was handed this very value (#2190)
src/jq/eval.rs tolerate 35463 both unreachable: classify_static_component answers Index only for OwnedValue::Array, and it was handed this very value (#2190)
src/jq/eval.rs tolerate 35540 both unreachable: the only caller reaches this after classify_static_component answered Field for this same value, which it does only for an object (#2190)
src/jq/eval.rs tolerate 35584 both unreachable: both callers establish the container first -- navigate_static_component_ref via classify_static_component's Index arm, and walk_path's Expr::Iterate arm by matching on the container itself (#2190)
src/jq/eval.rs tolerate 39099 both unreachable: stop_with_escape's only write is slot.set(Some(control)) with the control it was handed, which is always the Control::Error built one line above (#2180)
src/jq/eval.rs tolerate 39946 both unreachable by construction: the per-fork match only ever hands stop_with_downstream a non-Exhausted flow, so terminal can never hold Exhausted. foreach_forks' identical arm is 0-hit for the same reason and is only unflagged because it predates this diff (#2899)
src/jq/eval.rs tolerate 52268 both unreachable in practice: a def with more than 64 parameters; the fallback exists so ScopeMask's one-bit-per-parameter u64 is a performance ceiling rather than a correctness limit (#2633)
src/jq/eval.rs tolerate 52280 both unreachable: bind_def_call only calls this for a non-empty params, and install_def_calls only builds a DefCall whose args.len() equals params.len(), so the zip is never empty here (#2560)
src/jq/eval.rs tolerate 52300 both unreachable in practice: needs more than 32 duplicated-name parameters; see the non-duplicate path's own note (#2633)
src/jq/eval.rs tolerate 52321 both unreachable: name was just read from params, so the zip over (params, args) has a matching pair unless args is shorter than params, which install_def_calls' own arity guard rules out (#2560)
src/jq/eval.rs tolerate 52338 both unreachable: params is non-empty here (bind_def_call's own guard) and its first entry is never skipped, so at least one substitution always ran (#2560)
src/jq/eval.rs tolerate 52399 both unreachable: same arity invariant as bind_def_call_params' own copy of this loop -- name came from params, so the zip has a matching pair unless args is shorter, which install_def_calls rules out (#2560)
src/jq/eval.rs tolerate 53717 both substitute_var_impl's FuncDef arm always returns FuncDef (#2283)
src/jq/eval.rs tolerate 53739 both substitute_var_impl's FuncDef arm always returns FuncDef (#2283)
src/jq/eval.rs tolerate 53757 both substitute_var_impl's FuncDef arm always returns FuncDef (#2283)
src/jq/eval.rs tolerate 53827 both substitute_var_impl's FuncDef arm always returns FuncDef (#2283)
src/jq/eval.rs tolerate 53841 both substitute_func_param_impl's FuncDef arm always returns FuncDef (#2555)
src/jq/eval.rs tolerate 60841 both unreachable in a passing suite by design -- this is the failure message for the assertion the test exists to make (#2190)
src/jq/eval.rs tolerate 89202 both unreachable in a passing suite by design -- every filter this helper is called with parses to an AsPattern (#2649)
src/jq/eval.rs tolerate 89234 both unreachable in a passing suite by design -- every call site passes the origin of a binding this same test already proved carries a marker (#2649)
src/jq/eval.rs tolerate 90114 both unreachable in a passing suite by design -- every row here is a shape jq accepts, confirmed live (#2649)
src/jq/eval.rs tolerate 90182 both unreachable in a passing suite by design -- every row here is a shape jq refuses, confirmed live (#2649)
src/jq/eval.rs tolerate 90546 both unreachable in a passing suite by design -- this is the panic message for the #2072 pin itself, only formatted if the let-else pattern fails to match (#2072)
src/jq/eval.rs tolerate 90557 both unreachable in a passing suite by design -- this is the panic message for the #2072 pin itself, only formatted if the let-else pattern fails to match (#2072)
src/jq/eval.rs tolerate 90578 both unreachable in a passing suite by design -- this is the panic message for the #2072 pin itself, only formatted if the match doesn't hit the expected arm above (#2072)
src/jq/eval.rs tolerate 90594 both unreachable in a passing suite by design -- this is the panic message for the #2072 pin itself, only formatted if the match doesn't hit the expected arm above (#2072)
src/jq/eval_generic.rs tolerate 3569 both unreachable: the sole remaining caller (retain_truthy_generic's Many arm) runs to_owned on an item before keeping it, so re-converting a kept item here cannot fail; the ManyCursor caller that made this reachable went with the truthiness walk (#2692, re-establishing #2661's premise)
src/jq/eval_generic.rs tolerate 6923 both unreachable in a passing suite by design -- this is the panic message for the #2368 pin itself, only formatted if the assert's own condition is false (#2368)
src/jq/eval_generic.rs tolerate 10664 both unreachable: every producer that reaches this empty-exprs tail (each_lazy_keys_iterate_sink's sorted/!sorted arms, each_lazy_index_range_iterate_sink, each_lazy_seq_iterate_sink) yields OneCursorValue/OneCursor/Owned, never a cursorless GenericItem::One -- so cursor is always Some here; kept for exhaustiveness/symmetry with the Some arm (#2103)
src/jq/eval_generic.rs tolerate 13771 both unreachable: escape_generic!/ensure_owned! set terminal before Demand::Stop; already returned above (#2138)
src/jq/eval_generic.rs tolerate 14125 both unreachable: escape! sets terminal before Demand::Stop; already returned above (#2546)
src/jq/eval_generic.rs tolerate 16578 both unreachable: len_checked and SliceBounds::resolve already bound every index in range to [0, len), so get_cursor cannot miss (#2168)
src/jq/eval_generic.rs tolerate 20236 both unreachable: malformed_object_member above already proved every key stringifies (the None half), and to_owned_cursor on an untagged key decoded_key_str decoded cannot fail (the Err half) (#2785)
src/jq/eval_generic.rs tolerate 20317 both unreachable: map(f) over an array emits exactly one array; kept so a future map shape produces no output rather than a panic (#2785)
src/jq/eval_generic.rs tolerate 20585 both unreachable by design -- eval_single's #2368 debug_assert forbids optional=true on Builtin::Reverse, so length never answers None here (#2730)
src/jq/eval_generic.rs tolerate 22444 both unreachable by construction: every shape either guard admits now has an arm above (#2771), and expr_dispatch_catchall_guards_default_conservatively_2549 pins both guards' _ => false defaults directly
src/jq/eval_generic.rs tolerate 22944 both unreachable in a passing suite by design -- owned_identity_rule maps a bare Expr::Var to Bound too (for the static gate, which sees a body before its as substitution runs), but every runtime dispatch that reaches this rule (owned_identity_after_stage/owned_identity_placed_by, from owned_identity_leaving_cursor's Bound arm) only ever sees a stage after eval_owned_identity_as's unconditional substitute_bound_var_from call, which always turns $x into Expr::TrackedVar before recursing -- confirmed by running the full suite with this arm replaced by a hard panic!(), which never fired (#2072)
src/jq/eval_generic.rs tolerate 24781 both unreachable in a passing suite by design -- the fixture's map(.+1) is always a LazySeq; this arm is the test's own diagnostic (#2666)
src/jq/eval_generic.rs tolerate 24809 both unreachable in a passing suite by design -- the fixture's only escape is Control::Error; this arm is the test's own diagnostic (#2666)
src/json/light.rs tolerate 6311 both unreachable in a passing suite by design -- this is a panic-message format argument for the #2072 pin itself, only evaluated if the assert's own condition is false (#2072)
src/json/light.rs tolerate 6322 both unreachable in a passing suite by design -- this is a panic-message format argument for the #2072 pin itself, only evaluated if the assert's own condition is false (#2072)
src/json/light.rs tolerate 6403 both unreachable in a passing suite by design -- this is a panic-message format argument for the #2072 pin itself, only evaluated if the assert's own condition is false (#2072)
src/util/simd/x86.rs tolerate 208-258 both CPU-gated: the avx512f early-return only executes on Zen 4+ / Skylake-X runners, and its absence changes which AMD/Intel branch below executes too (#2449)
src/yaml/index.rs tolerate 1268 both unreachable: YamlIndex::build always wraps the parsed document(s) in a virtual root Sequence, at TY index 0 (#798)
src/yaml/index.rs tolerate 1272 both unreachable: every fixture field_key_head_foot is called with in this test module is a top-level mapping (#798)
src/yaml/index.rs tolerate 1283 both unreachable: a mapping key is always emitted as YamlValue::String -- it is never type-inferred like a value (#222), so this if-let's pattern can never fail to match (#798)
src/yaml/index.rs tolerate 1285 both unreachable: every call to field_key_head_foot in this test module passes a key that the fixture's mapping actually has (#798)
src/yaml/index.rs tolerate 1295 both unreachable: YamlIndex::build always wraps the parsed document(s) in a virtual root Sequence, at TY index 0 (#798)
src/yaml/index.rs tolerate 1299 both unreachable: every fixture seq_item_head_foot is called with in this test module is a top-level sequence (#798)
src/yaml/index.rs tolerate 1322 both unreachable: YamlIndex::build always wraps the parsed document(s) in a virtual root Sequence, at TY index 0 (#798)
src/yaml/index.rs tolerate 1341 both unreachable: YamlIndex::build always wraps the parsed document(s) in a virtual root Sequence, at TY index 0 (#798)
src/yaml/index.rs tolerate 1364 both unreachable: YamlIndex::build always wraps the parsed document(s) in a virtual root Sequence, at TY index 0 (#798)
src/yaml/index.rs tolerate 1373 both unreachable: every fixture field_key_head_foot_in_doc is called with in this test module is a top-level mapping (#798)
src/yaml/index.rs tolerate 1384 both unreachable: a mapping key is always emitted as YamlValue::String -- it is never type-inferred like a value (#222), so this if-let's pattern can never fail to match (#798)
src/yaml/index.rs tolerate 1386 both unreachable: every call to field_key_head_foot_in_doc in this test module passes a key that the fixture's document actually has (#798)
src/yaml/index.rs tolerate 1397 both unreachable: YamlIndex::build always wraps the parsed document(s) in a virtual root Sequence, at TY index 0 (#798)
src/yaml/index.rs tolerate 1401 both unreachable: every fixture nested_key_head_foot is called with in this test module is a top-level mapping (#798)
src/yaml/index.rs tolerate 1405 both unreachable: a mapping key is always emitted as YamlValue::String -- it is never type-inferred like a value (#222), so this let-else's pattern can never fail to match (#798)
src/yaml/index.rs tolerate 1411 both unreachable: every fixture nested_key_head_foot is called with has a nested mapping under outer (#798)
src/yaml/index.rs tolerate 1422 both unreachable: a mapping key is always emitted as YamlValue::String -- it is never type-inferred like a value (#222), so this if-let's pattern can never fail to match (#798)
src/yaml/index.rs tolerate 1425 both unreachable: every call to nested_key_head_foot in this test module passes an outer.inner pair that the fixture actually has (#798)
src/yaml/light.rs tolerate 3364 both unreachable: an alias target is never None for a built index (#1374)
src/yaml/light.rs tolerate 15398 both unreachable in a passing suite by design -- this is a panic-message format argument for the #2072 pin itself, only evaluated if the assert's own condition is false (#2072)
src/yaml/light.rs tolerate 15409 both unreachable in a passing suite by design -- this is a panic-message format argument for the #2072 pin itself, only evaluated if the assert's own condition is false (#2072)
src/yaml/light.rs tolerate 15566 both unreachable in a passing suite by design -- this is a panic-message format argument for the #2072 pin itself, only evaluated if the assert's own condition is false (#2072)
src/yaml/parser.rs tolerate 1583 both unreachable: every block-sequence open registers a frame at its own depth before any item of it can be parsed (#1079)
src/yaml/parser.rs tolerate 1613 both unreachable: this function's sole caller (record_standalone_comment) only invokes it from inside a match on pending_head_lines.last(), so pending_head_lines is already known non-empty here (#798)
src/yaml/parser.rs tolerate 7999 both unreachable: every byte here already passed the [0-9.eE+-] charset check above, a strict subset of ASCII, so str::from_utf8 can never fail (#2778)

Patch coverage

Patch: 98.23% (445/453 new lines covered)

File Patch Uncovered new lines
src/jq/eval.rs 98.8% (82/83) 9797
src/jq/eval_generic.rs 92.31% (24/26) 23050-23051
src/jq/value.rs 98.55% (339/344) 2815, 2979, 3068, 3073, 4066
Uncovered new lines (8)
  • src/jq/eval.rs:9797
  • src/jq/eval_generic.rs:23050
  • src/jq/eval_generic.rs:23051
  • src/jq/value.rs:2815
  • src/jq/value.rs:2979
  • src/jq/value.rs:3068
  • src/jq/value.rs:3073
  • src/jq/value.rs:4066

📦 Full per-file coverage summary · run summary

@newhoggy
newhoggy force-pushed the issue-2906-jq-large-int-round branch from 2026aaf to 107c0de Compare September 14, 2026 03:37
…thmetic (#2906)

`succinctly jq -n '869389897822472004 + 944331'` printed 869389897823416400
where jq 1.7.1 prints 869389897823416300. #2631 routed past-2^53 results
through the f64 model but widened each operand with a bare `i64 as f64`;
jq instead converts a *literal* through `jvp_literal_number_to_double`
(src/jv.c): `decNumberReduce` under a DECIMAL64 context with `digits = 17`
(half-even), then a correctly-rounded strtod -- a double rounding that lands
on a different double for some 18/19-digit values. Modelled that way, jq's
answer was reproduced on 5600/5600 random differential cases; the earlier
"~4% unexplained" figure came from modelling the operands as exact integers.

Add `jq_literal_int_to_f64` (identity below 10^17, integer-only rounding
above) and an `EvalSemantics::INT_LITERAL_ROUNDS_TO_17_DIGITS` gate, and use
it wherever jq mode widens an `Int`: `jq_checked_int_arith`'s fallback, the
mixed arms of `+ - * /`, `%` (now jq's `binop_mod` truncate-the-double
model, so `869389897822472004 % 1000` is 936 and `9007199254740993 % 2` is
0), `numeric_repr_eq`, and ordering via `compare_values`, which becomes
generic over `S` so `sort`/`unique`/`min`/`max`/`bsearch`/`delpaths` inherit
the gate. yq mode keeps its exact int64 arithmetic and plain cast.

Out of scope, recorded in docs/compliance/jq/limitations.md: >17-digit float
and over-i64 literals, the math builtins' own widening, floor-family display
past 2^53, and the reindex bridge re-parsing a computed double as an integer
literal before `sort`/`unique`/`min`/`max`.
The out-of-scope surfaces the #2906 entry lists are now filed: >17-digit
float and over-i64 literals (#2936), the math builtins' widening and the
floor-family display past 2^53 (#2937), and the reindex bridge re-parsing a
computed double as an integer literal before sort/unique/min/max (#2938).
…ning (#2906)

Review of the #2906 fix found two regressions in its comparison half. jq's
`jvp_number_cmp` has two rules, not one: a literal against a *computed*
double widens the literal through the 17-digit rounding, but two *literals*
compare exactly as decimals (`decNumberCompare`). Rounding unconditionally
made `869389897822472004 == 869389897822471936.0` true (jq: false) and
`869389897822472004.0` unequal to its own integer spelling (jq: equal). And
`OwnedValue`'s mode-blind `PartialEq` -- which the yq presentation layer
uses to align comments across a `|=` -- had inherited the jq rounding, so a
yq-mode `Int` no longer matched the plain-cast double yq's own arithmetic
produces and comments moved onto the wrong elements.

Add `jq_numeric_cmp`: bare `Int` and `NumberLiteral` are literals, bare
`Float` is computed; two literals are ordered by their correctly-rounded
doubles (monotonic, so a strict order is exact) and only a tie falls through
to `cmp_decimal_literals`, an exact digit comparison of the two spellings.
jq-mode `==` (`owned_value_eq`) and ordering (`compare_values`) go through
it; `PartialEq`/`numeric_repr_eq`/`numeric_repr_cmp` go back to the plain
widening, so yq mode and its alignment hash are untouched. As a consequence
`9007199254740993 == 9007199254740992.0` is now jq's `false` rather than the
recorded divergence.

Also from the review: rename the gate to `DECNUMBER_LITERALS`; take `%`'s
in-range operands straight to `wrapping_rem` and delegate the rest to
`mod_floats` (one copy of `binop_mod`); let `jq_checked_int_arith`'s exact
arm use `jq_f64_backed_int`; keep direct `Int`/`Int` and `Float`/`Float`
arms in `compare_values`; derive `delete_paths_*`'s yq flag from `S`;
correct the `materialize_lazy_keys` comment; pin the three follow-up gaps
(#2936, #2937, #2938) with a characterization test; and note jq's `dtoi` UB
at 2^63 and the `-x % y` precedence gap it exposes.
Five findings from a high-effort /code-review of the #2906 branch:

- jq_numeric_cmp now fast-paths any Int/NumberLiteral(Int) pair to a
  plain i64 comparison before building a Literal/Side for either
  operand -- the common case for sort/unique/min/max/bsearch over
  ordinary parsed-JSON integer arrays, which previously always paid
  for the full decNumber-literal machinery even though it always
  resolved to the same x.cmp(&y).
- decompose_decimal_literal now reuses strip_leading_sign and
  parse_literal_exponent/ExpParse instead of a third independent
  sign-stripping and exponent-saturation implementation, matching the
  precedent #106/#1304 set for exactly this duplication pattern.
- The x86_64 dtoi/INT64_MIN claim in limitations.md and the matching
  eval.rs comment is reworded as unverified (no x86_64 jq 1.7.1 was
  available to capture it live), instead of stated as fact.
- materialize_lazy_keys's hardcoded compare_values::<JqSemantics> now
  cites the actual parse-time enforcement (try_parse_builtin's
  ParserMode::Yq check) it relies on, rather than an unexplained
  comment; threading S through the whole streaming lazy-materialization
  pipeline for an already parse-time-enforced invariant wasn't worth
  the additional plumbing.
- arith_mod's float-truncation gate is reordered so its condition is a
  single negation matching its own comment, instead of needing De
  Morgan's law to map one to the other.
…rows (#2906)

`decompose_decimal_literal` now reuses `parse_literal_exponent`, which
saturates an over-long exponent to `i128::MIN`/`MAX`; the fraction and
digit-count adjustments around that value then overflowed (a debug-build
panic on `1e<45 digits> == 2e<45 digits>`, reachable because both doubles
are infinite and the tie falls through to the decimal comparison). Clamp
the parsed exponent to +/-2^100 first, and pin three such spellings.

Rebasing onto main also picked up #2902, whose reindex bridge now hands a
computed float back as a bare `Float` instead of re-parsing its printed
digits as an integer literal -- which is exactly the gap #2938 was filed
for. Its `sort`/`unique`/`max`/`==` rows now match jq, so they move out of
the characterization test into the positive comparison rows, and the
limitations entry records #2938 as closed by #2902.
@newhoggy
newhoggy force-pushed the issue-2906-jq-large-int-round branch from 107c0de to 2f95331 Compare September 14, 2026 03:51
@newhoggy
newhoggy added this pull request to the merge queue Sep 14, 2026
Merged via the queue into main with commit efff64f Sep 14, 2026
30 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

jq: large-integer +/- arithmetic can round to a different f64 than real jq even when both operands are non-negative

1 participant