diff options
-rw-r--r-- | compile.c | 21 | ||||
-rw-r--r-- | compile.h | 1 | ||||
-rw-r--r-- | docs/content/3.manual/manual.yml | 23 | ||||
-rw-r--r-- | execute.c | 48 | ||||
-rw-r--r-- | lexer.l | 2 | ||||
-rw-r--r-- | main.c | 6 | ||||
-rw-r--r-- | opcode_list.h | 1 | ||||
-rw-r--r-- | parser.y | 16 | ||||
-rw-r--r-- | tests/all.test | 5 |
9 files changed, 108 insertions, 15 deletions
@@ -443,6 +443,27 @@ block gen_cond(block cond, block iftrue, block iffalse) { BLOCK(gen_op_simple(POP), iffalse))); } +block gen_try(block exp, block handler) { + /* + * Produce: + * FORK_OPT <address of handler> + * <exp> + * JUMP <end of handler> + * <handler> + * + * The handler will only execute if we backtrack to the FORK_OPT with + * an error (exception). If <exp> produces no value then FORK_OPT + * will backtrack (propagate the `empty`, as it were. If <exp> + * produces a value then we'll execute whatever bytecode follows this + * sequence. + */ + if (!handler.first && !handler.last) + // A hack to deal with `.` as the handler; we could use a real NOOP here + handler = BLOCK(gen_op_simple(DUP), gen_op_simple(POP), handler); + exp = BLOCK(exp, gen_op_target(JUMP, handler)); + return BLOCK(gen_op_target(FORK_OPT, exp), exp, handler); +} + block gen_cbinding(const struct cfunction* cfunctions, int ncfunctions, block code) { for (int cfunc=0; cfunc<ncfunctions; cfunc++) { inst* i = inst_new(CLOSURE_CREATE_C); @@ -40,6 +40,7 @@ block gen_or(block a, block b); block gen_var_binding(block var, const char* name, block body); block gen_cond(block cond, block iftrue, block iffalse); +block gen_try(block exp, block handler); block gen_cbinding(const struct cfunction* functions, int nfunctions, block b); diff --git a/docs/content/3.manual/manual.yml b/docs/content/3.manual/manual.yml index 7225b7dc..4d3ac45f 100644 --- a/docs/content/3.manual/manual.yml +++ b/docs/content/3.manual/manual.yml @@ -1453,6 +1453,7 @@ sections: - program: '.[] == 1' input: '[1, 1.0, "1", "banana"]' output: ['true', 'true', 'false', 'false'] + - title: if-then-else body: | @@ -1561,6 +1562,28 @@ sections: input: '{}' output: [42] + - title: try-catch + body: | + + Errors can be caught by using `try EXP catch EXP`. The first + expression is executed, and if it fails then the second is + executed with the error message. The output of the handler, + if any, is output as if it had been the output of the + expression to try. + + The `try EXP` form uses `empty` as the exception handler. + + examples: + - program: 'try .a catch ". is not an object"' + input: 'true' + output: [". is not an object"] + - program: '[.[]|try .a]' + input: '[{}, true, {"a":1}' + output: ['[null, 1]'] + - program: 'try error("some exception") catch .' + input: 'true' + output: ['"some exception"'] + - title: Advanced features body: | Variables are an absolute necessity in most programming languages, but @@ -23,6 +23,7 @@ struct jq_state { jq_err_cb err_cb; void *err_cb_data; + jv error; struct stack stk; stack_ptr curr_frame; @@ -241,6 +242,8 @@ static void jq_reset(jq_state *jq) { assert(jq->fork_top == 0); assert(jq->curr_frame == 0); stack_reset(&jq->stk); + jv_free(jq->error); + jq->error = jv_null(); if (jv_get_kind(jq->path) != JV_KIND_INVALID) jv_free(jq->path); @@ -250,21 +253,12 @@ static void jq_reset(jq_state *jq) { static void print_error(jq_state *jq, jv value) { assert(!jv_is_valid(value)); - jv msg = jv_invalid_get_msg(value); - jv msg2; - if (jv_get_kind(msg) == JV_KIND_STRING) - msg2 = jv_string_fmt("jq: error: %s", jv_string_value(msg)); - else - msg2 = jv_string_fmt("jq: error: <unknown>"); - jv_free(msg); - - if (jq->err_cb) - jq->err_cb(jq->err_cb_data, msg2); - else if (jv_get_kind(msg2) == JV_KIND_STRING) - fprintf(stderr, "%s\n", jv_string_value(msg2)); - else - fprintf(stderr, "jq: error: %s\n", strerror(ENOMEM)); - jv_free(msg2); + if (jq->err_cb) { + // callback must jv_free() its jv argument + jq->err_cb(jq->err_cb_data, jv_invalid_get_msg(jv_copy(value))); + } + jv_free(jq->error); + jq->error = value; } #define ON_BACKTRACK(op) ((op)+NUM_OPCODES) @@ -278,6 +272,7 @@ jv jq_next(jq_state *jq) { int backtracking = !jq->initial_execution; jq->initial_execution = 0; + assert(jv_get_kind(jq->error) == JV_KIND_NULL); while (1) { uint16_t opcode = *pc; @@ -312,6 +307,8 @@ jv jq_next(jq_state *jq) { if (backtracking) { opcode = ON_BACKTRACK(opcode); backtracking = 0; + if (!jv_is_valid(jq->error) && opcode != ON_BACKTRACK(FORK_OPT)) + goto do_backtrack; } pc++; @@ -606,18 +603,36 @@ jv jq_next(jq_state *jq) { case BACKTRACK: { pc = stack_restore(jq); if (!pc) { + if (!jv_is_valid(jq->error)) { + jv error = jq->error; + jq->error = jv_null(); + return error; + } return jv_invalid(); } backtracking = 1; break; } + case FORK_OPT: case FORK: { stack_save(jq, pc - 1, stack_get_pos(jq)); pc++; // skip offset this time break; } + case ON_BACKTRACK(FORK_OPT): { + if (jv_is_valid(jq->error)) { + // `try EXP ...` backtracked here (no value, `empty`), so we backtrack more + jv_free(stack_pop(jq)); + goto do_backtrack; + } + // `try EXP ...` exception caught in EXP + jv_free(stack_pop(jq)); // free the input + stack_push(jq, jv_invalid_get_msg(jq->error)); // push the error's message + jq->error = jv_null(); + /* fallthru */ + } case ON_BACKTRACK(FORK): { uint16_t offset = *pc++; pc += offset; @@ -644,6 +659,8 @@ jv jq_next(jq_state *jq) { case 3: top = ((func_3)function->fptr)(in[0], in[1], in[2]); break; case 4: top = ((func_4)function->fptr)(in[0], in[1], in[2], in[3]); break; case 5: top = ((func_5)function->fptr)(in[0], in[1], in[2], in[3], in[4]); break; + // FIXME: a) up to 7 arguments (input + 6), b) should assert + // because the compiler should not generate this error. default: return jv_invalid_with_msg(jv_string("Function takes too many arguments")); } @@ -739,6 +756,7 @@ jq_state *jq_init(void) { jq->stk_top = 0; jq->fork_top = 0; jq->curr_frame = 0; + jq->error = jv_null(); jq->err_cb = NULL; jq->err_cb_data = NULL; @@ -52,6 +52,8 @@ struct lexer_param; "end" { return END; } "reduce" { return REDUCE; } "//" { return DEFINEDOR; } +"try" { return TRY; } +"catch" { return CATCH; } "|=" { return SETPIPE; } "+=" { return SETPLUS; } "-=" { return SETMINUS; } @@ -124,6 +124,12 @@ static int process(jq_state *jq, jv value, int flags) { if (options & UNBUFFERED_OUTPUT) fflush(stdout); } + if (jv_invalid_has_msg(jv_copy(result))) { + // Uncaught jq exception + jv msg = jv_invalid_get_msg(jv_copy(result)); + fprintf(stderr, "jq: error: %s\n", jv_string_value(msg)); + jv_free(msg); + } jv_free(result); return ret; } diff --git a/opcode_list.h b/opcode_list.h index c4c48deb..b3b0f2e5 100644 --- a/opcode_list.h +++ b/opcode_list.h @@ -10,6 +10,7 @@ OP(INDEX_OPT, NONE, 2, 1) OP(EACH, NONE, 1, 1) OP(EACH_OPT, NONE, 1, 1) OP(FORK, BRANCH, 0, 0) +OP(FORK_OPT, BRANCH, 0, 0) OP(JUMP, BRANCH, 0, 0) OP(JUMP_F,BRANCH, 1, 0) OP(BACKTRACK, NONE, 0, 0) @@ -66,6 +66,8 @@ struct lexer_param; %token END "end" %token AND "and" %token OR "or" +%token TRY "try" +%token CATCH "catch" %token SETPIPE "|=" %token SETPLUS "+=" %token SETMINUS "-=" @@ -240,6 +242,20 @@ Term "as" '$' IDENT '|' Exp { $$ = $2; } | +"try" Exp "catch" Exp { + //$$ = BLOCK(gen_op_target(FORK_OPT, $2), $2, $4); + $$ = gen_try($2, $4); +} | +"try" Exp { + //$$ = BLOCK(gen_op_target(FORK_OPT, $2), $2, gen_op_simple(BACKTRACK)); + $$ = gen_try($2, gen_op_simple(BACKTRACK)); +} | +"try" Exp "catch" error { + FAIL(@$, "Possibly unterminated 'try' statement"); + $$ = $2; +} | + + Exp '=' Exp { $$ = gen_call("_assign", BLOCK(gen_lambda($1), gen_lambda($3))); } | diff --git a/tests/all.test b/tests/all.test index e50b9250..f5c7bd68 100644 --- a/tests/all.test +++ b/tests/all.test @@ -665,6 +665,11 @@ def inc(x): x |= .+1; inc(.[].a) {} [true, true, false] +# Try/catch and general `?` operator +[.[]|try if . == 0 then error("foo") elif . == 1 then .a elif . == 2 then empty else . end catch .] +[0,1,2,3] +["foo","Cannot index number with string \"a\"",3] + # string operations [.[]|startswith("foo")] ["fo", "foo", "barfoo", "foobar", "barfoob"] |