From df24981307658868759faaea76b817cc62dd6be0 Mon Sep 17 00:00:00 2001 From: itchyny Date: Sat, 19 Sep 2026 15:21:29 +0900 Subject: [PATCH] Fix reduce to preserve its input across init backtracking (#3633) `reduce f as $x (init; update)` evaluated `f` against a stack slot that forkpoints created while evaluating `init` restore. Both the `DUPN` opening the reduction loop and the `LOADVN` retrieving the result pop that slot destructively, nulling it out, so an `init` that backtracks (e.g. `0, 0`) made `f` see `null` and fail with "Cannot iterate over null". Duplicate the input before the `FORK` instead of inside the loop, and drop the leftover copy with `POP` once the loop is exhausted. The slot left live at the `FORK` is then a block pushed after `init` completed, which no earlier forkpoint can reach, so nulling it is harmless. This is the same approach `gen_foreach` has used since c70ae1a. `DUPN` had no other user, so remove the opcode. --- src/compile.c | 5 +++-- src/execute.c | 7 ------- src/opcode_list.h | 1 - tests/jq.test | 8 +++++++- 4 files changed, 10 insertions(+), 11 deletions(-) diff --git a/src/compile.c b/src/compile.c index 80b723c1..b925020a 100644 --- a/src/compile.c +++ b/src/compile.c @@ -832,8 +832,7 @@ static block bind_alternation_matchers(block matchers, block body) { block gen_reduce(block source, block matcher, block init, block body) { block res_var = gen_op_var_fresh(STOREV, "reduce"); - block loop = BLOCK(gen_op_simple(DUPN), - source, + block loop = BLOCK(source, bind_alternation_matchers(matcher, BLOCK(gen_op_bound(LOADVN, res_var), body, @@ -842,8 +841,10 @@ block gen_reduce(block source, block matcher, block init, block body) { return BLOCK(gen_op_simple(DUP), init, res_var, + gen_op_simple(DUP), gen_op_target(FORK, loop), loop, + gen_op_simple(POP), gen_op_bound(LOADVN, res_var)); } diff --git a/src/execute.c b/src/execute.c index 7cd41eda..02d6ae17 100644 --- a/src/execute.c +++ b/src/execute.c @@ -428,13 +428,6 @@ jv jq_next(jq_state *jq) { break; } - case DUPN: { - jv v = stack_popn(jq); - stack_push(jq, jv_copy(v)); - stack_push(jq, v); - break; - } - case DUP2: { jv keep = stack_pop(jq); jv v = stack_pop(jq); diff --git a/src/opcode_list.h b/src/opcode_list.h index 85a8a580..c9a81895 100644 --- a/src/opcode_list.h +++ b/src/opcode_list.h @@ -1,6 +1,5 @@ OP(LOADK, CONSTANT, 1, 1) OP(DUP, NONE, 1, 2) -OP(DUPN, NONE, 1, 2) OP(DUP2, NONE, 2, 3) OP(PUSHK_UNDER, CONSTANT, 1, 2) OP(POP, NONE, 1, 0) diff --git a/tests/jq.test b/tests/jq.test index ff3c625b..39f8368a 100644 --- a/tests/jq.test +++ b/tests/jq.test @@ -2571,7 +2571,13 @@ try ["OK", setpath([[1]]; 1)] catch ["KO", .] [] ["KO","Cannot update field at array index of array"] -# regression test for #3227 +# regression test for #3227 (reduce) +[] | reduce .[] as $x (0, 1; .) +null +0 +1 + +# regression test for #3227 (foreach) foreach .[] as $x (0, 1; . + $x) [1, 2] 1