]> granicus.if.org Git - jq/commitdiff
Add `try EXP catch EXP`
authorNicolas Williams <nico@cryptonector.com>
Sun, 6 Jul 2014 01:54:42 +0000 (20:54 -0500)
committerNicolas Williams <nico@cryptonector.com>
Sun, 6 Jul 2014 06:29:42 +0000 (01:29 -0500)
compile.c
compile.h
docs/content/3.manual/manual.yml
execute.c
lexer.l
main.c
opcode_list.h
parser.y
tests/all.test

index 597fa6f1456bb2be296475c19b210f96d2a6bf21..788b8d73fe0eecb07d4203457f4e35653a01f8d3 100644 (file)
--- 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);
index 531e853e0e3cd27b9216c94b36f45346f36c4c5a..1343ab0266dd4720ed7dfefac53fc2cb652c0763 100644 (file)
--- 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);
 
index 7225b7dcb1d67d2999dfb36f930624faa49b7e5d..4d3ac45f3abaaf09003a2673415e235a711b4bc7 100644 (file)
@@ -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
index 860e46b7904eaa72924d609418feaf9f717e49d2..f384420e5733ea67634b0300a96f22265f48c7dc 100644 (file)
--- 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 22d1d94d81862cf2d6d6f2d85f601a35c5b86cf1..a66867d0bc800121386dd8d857754fb6e04561f3 100644 (file)
--- 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 468f12ae2c17bc6a0d16230a2f6219616a575fd7..80497e779bc274af78a64675252548390e56b932 100644 (file)
--- 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;
 }
index c4c48debba3dc9411da8d47bde7fa242c067b2d4..b3b0f2e50e6b9d04ed4659ced3a560779f34bd97 100644 (file)
@@ -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)
index b546e2bbc328359cbcf96983a648d2f071a0fda4..bfd09d35943b77572b79a7df3bc3ee546986ae33 100644 (file)
--- 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)));
 } |
index e50b9250c2ccc164786ebd0dc4d0f74e59dfe178..f5c7bd68e1e2823cab548e9cc4aca583ccebf3c9 100644 (file)
@@ -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"]