summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--compile.c21
-rw-r--r--compile.h1
-rw-r--r--docs/content/3.manual/manual.yml23
-rw-r--r--execute.c48
-rw-r--r--lexer.l2
-rw-r--r--main.c6
-rw-r--r--opcode_list.h1
-rw-r--r--parser.y16
-rw-r--r--tests/all.test5
9 files changed, 108 insertions, 15 deletions
diff --git a/compile.c b/compile.c
index 597fa6f1..788b8d73 100644
--- a/compile.c
+++ b/compile.c
@@ -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);
diff --git a/compile.h b/compile.h
index 531e853e..1343ab02 100644
--- a/compile.h
+++ b/compile.h
@@ -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
diff --git a/execute.c b/execute.c
index 860e46b7..f384420e 100644
--- a/execute.c
+++ b/execute.c
@@ -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;
diff --git a/lexer.l b/lexer.l
index 22d1d94d..a66867d0 100644
--- a/lexer.l
+++ b/lexer.l
@@ -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; }
diff --git a/main.c b/main.c
index 468f12ae..80497e77 100644
--- a/main.c
+++ b/main.c
@@ -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)
diff --git a/parser.y b/parser.y
index b546e2bb..bfd09d35 100644
--- a/parser.y
+++ b/parser.y
@@ -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"]