Skip to content

Commit db93945

Browse files
committed
Fix try/catch catches more than it should #1859
Close #1885, #2140, #2011, #2220, #2485, 2073 Rename the FORK_OPT opcode to TRY_BEGIN, add a TRY_END opcode, and wrap errors when raising through a TRY_END so that they will not be caught by the matching TRY_BEGIN. Now a `try exp catch handler` expression generates code like: TRY_BEGIN handler <exp> TRY_END JUMP past_handler handler: <handler> past_handler: ... On backtrack through TRY_BEGIN it just backtracks. If anything past the whole thing raises when <exp> produced a value, then the TRY_END will catch the error, wrap it in another, and backtrack. The TRY_BEGIN will see a wrapped error and then it will unwrap and re-raise the error. If <exp> raises, then TRY_BEGIN will catch the error and jump to the handler, but the TRY_BEGIN will not stack_save() in that case, so on raise/backtrack the TRY_BEGIN will not execute again (nor will the TRY_END).
1 parent c9c45d7 commit db93945

File tree

7 files changed

+468
-368
lines changed

7 files changed

+468
-368
lines changed

src/compile.c

Lines changed: 28 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -1034,43 +1034,40 @@ block gen_cond(block cond, block iftrue, block iffalse) {
10341034
BLOCK(gen_op_simple(POP), iffalse)));
10351035
}
10361036

1037-
block gen_try_handler(block handler) {
1038-
// Quite a pain just to hide jq's internal errors.
1039-
return gen_cond(// `if type=="object" and .__jq
1040-
gen_and(gen_call("_equal",
1041-
BLOCK(gen_lambda(gen_const(jv_string("object"))),
1042-
gen_lambda(gen_call("type", gen_noop())))),
1043-
BLOCK(gen_subexp(gen_const(jv_string("__jq"))),
1044-
gen_noop(),
1045-
gen_op_simple(INDEX))),
1046-
// `then error`
1047-
gen_call("error", gen_noop()),
1048-
// `else HANDLER end`
1049-
handler);
1050-
}
1051-
10521037
block gen_try(block exp, block handler) {
10531038
/*
1054-
* Produce something like:
1055-
* FORK_OPT <address of handler>
1039+
* Produce:
1040+
*
1041+
* TRY_BEGIN handler
10561042
* <exp>
1057-
* JUMP <end of handler>
1058-
* <handler>
1043+
* TRY_END
1044+
* JUMP past_handler
1045+
* handler: <handler>
1046+
* past_handler:
10591047
*
1060-
* If this is not an internal try/catch, then catch and re-raise
1061-
* internal errors to prevent them from leaking.
1048+
* If <exp> backtracks then TRY_BEGIN will backtrack.
10621049
*
1063-
* The handler will only execute if we backtrack to the FORK_OPT with
1064-
* an error (exception). If <exp> produces no value then FORK_OPT
1065-
* will backtrack (propagate the `empty`, as it were. If <exp>
1066-
* produces a value then we'll execute whatever bytecode follows this
1067-
* sequence.
1050+
* If <exp> produces a value then we'll execute whatever bytecode follows
1051+
* this sequence. If that code raises an exception, then TRY_END will wrap
1052+
* and re-raise that exception, and TRY_BEGIN will unwrap and re-raise the
1053+
* exception (see jq_next()).
1054+
*
1055+
* If <exp> raises then the TRY_BEGIN will see a non-wrapped exception and
1056+
* will jump to the handler (note the TRY_END will not execute in this case),
1057+
* and if the handler produces any values, then we'll execute whatever
1058+
* bytecode follows this sequence. Note that TRY_END will not execute in
1059+
* this case, so if the handler raises an exception, or code past the handler
1060+
* raises an exception, then that exception won't be wrapped and re-raised,
1061+
* and the TRY_BEGIN will not catch it because it does not stack_save() when
1062+
* it branches to the handler.
10681063
*/
1069-
if (!handler.first && !handler.last)
1070-
// A hack to deal with `.` as the handler; we could use a real NOOP here
1071-
handler = BLOCK(gen_op_simple(DUP), gen_op_simple(POP), handler);
1072-
exp = BLOCK(exp, gen_op_target(JUMP, handler));
1073-
return BLOCK(gen_op_target(FORK_OPT, exp), exp, handler);
1064+
1065+
if (block_is_noop(handler))
1066+
handler = BLOCK(gen_op_simple(DUP), gen_op_simple(POP));
1067+
1068+
block jump = gen_op_target(JUMP, handler);
1069+
return BLOCK(gen_op_target(TRY_BEGIN, jump), exp, gen_op_simple(TRY_END),
1070+
jump, handler);
10741071
}
10751072

10761073
block gen_label(const char *label, block exp) {

src/compile.h

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -59,7 +59,6 @@ block gen_destructure(block var, block matcher, block body);
5959
block gen_destructure_alt(block matcher);
6060

6161
block gen_cond(block cond, block iftrue, block iffalse);
62-
block gen_try_handler(block handler);
6362
block gen_try(block exp, block handler);
6463
block gen_label(const char *label, block exp);
6564

src/execute.c

Lines changed: 54 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -346,6 +346,7 @@ jv jq_next(jq_state *jq) {
346346

347347
int raising;
348348
int backtracking = !jq->initial_execution;
349+
349350
jq->initial_execution = 0;
350351
assert(jv_get_kind(jq->error) == JV_KIND_NULL);
351352
while (1) {
@@ -806,15 +807,66 @@ jv jq_next(jq_state *jq) {
806807
break;
807808
}
808809

809-
case FORK_OPT:
810+
case TRY_BEGIN:
811+
stack_save(jq, pc - 1, stack_get_pos(jq));
812+
pc++; // skip handler offset this time
813+
break;
814+
815+
case TRY_END:
816+
stack_save(jq, pc - 1, stack_get_pos(jq));
817+
break;
818+
819+
case ON_BACKTRACK(TRY_BEGIN): {
820+
if (!raising) {
821+
/*
822+
* `try EXP ...` -- EXP backtracked (e.g., EXP was `empty`), so we
823+
* backtrack more:
824+
*/
825+
jv_free(stack_pop(jq));
826+
goto do_backtrack;
827+
}
828+
829+
/*
830+
* Else `(try EXP ... ) | EXP2` raised an error.
831+
*
832+
* If the error was wrapped in another error, then that means EXP2 raised
833+
* the error. We unwrap it and re-raise it as it wasn't raised by EXP.
834+
*
835+
* See commentary in gen_try().
836+
*/
837+
jv e = jv_invalid_get_msg(jv_copy(jq->error));
838+
if (!jv_is_valid(e) && jv_invalid_has_msg(jv_copy(e))) {
839+
set_error(jq, e);
840+
goto do_backtrack;
841+
}
842+
jv_free(e);
843+
844+
/*
845+
* Else we caught an error containing a non-error value, so we jump to
846+
* the handler.
847+
*
848+
* See commentary in gen_try().
849+
*/
850+
uint16_t offset = *pc++;
851+
jv_free(stack_pop(jq)); // free the input
852+
stack_push(jq, jv_invalid_get_msg(jq->error)); // push the error's message
853+
jq->error = jv_null();
854+
pc += offset;
855+
break;
856+
}
857+
case ON_BACKTRACK(TRY_END):
858+
// Wrap the error so the matching TRY_BEGIN doesn't catch it
859+
if (raising)
860+
set_error(jq, jv_invalid_with_msg(jv_copy(jq->error)));
861+
goto do_backtrack;
862+
810863
case DESTRUCTURE_ALT:
811864
case FORK: {
812865
stack_save(jq, pc - 1, stack_get_pos(jq));
813866
pc++; // skip offset this time
814867
break;
815868
}
816869

817-
case ON_BACKTRACK(FORK_OPT):
818870
case ON_BACKTRACK(DESTRUCTURE_ALT): {
819871
if (jv_is_valid(jq->error)) {
820872
// `try EXP ...` backtracked here (no value, `empty`), so we backtrack more

src/opcode_list.h

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,10 @@ OP(STORE_GLOBAL, GLOBAL, 0, 0)
1111
OP(INDEX, NONE, 2, 1)
1212
OP(INDEX_OPT, NONE, 2, 1)
1313
OP(EACH, NONE, 1, 1)
14-
OP(EACH_OPT, NONE, 1, 1)
14+
OP(EACH_OPT, NONE, 1, 1)
1515
OP(FORK, BRANCH, 0, 0)
16-
OP(FORK_OPT, BRANCH, 0, 0)
16+
OP(TRY_BEGIN, BRANCH, 0, 0)
17+
OP(TRY_END, NONE, 0, 0)
1718
OP(JUMP, BRANCH, 0, 0)
1819
OP(JUMP_F,BRANCH, 1, 0)
1920
OP(BACKTRACK, NONE, 0, 0)

0 commit comments

Comments
 (0)