Repository: jqlang/jq

Problem statement:
Fold modulo operator on constant values and raise zero remainder error quickly
Currently constant division is folded on parsing and zero division raises `Division by zero?` error quickly. There is `Remainder by zero?` error message found in `parser.y` but this will never be thrown because `constant_fold` does not fold the modulo operator. So I added folding on constant modulo and make `jq -n "1%0"` throws the `Remainder by zero?` error and exits with `3` (just like `jq -n "1/0"`). The infinity is caught by [`block_is_const_inf`](https://github.com/stedolan/jq/blob/a17dd3248a666d01be75f6b16be37e80e20b0954/src/parser.y#L464-L468). Returning infinity here may look wired (zero modulo is not infinity) but it applies to division as well; zero division in jq is not infinity but it's ok because the infinity is filtered out by `block_is_const_inf`. This follows the commit b9c2a326bae085a27b5bd01ca15c3c42c7b726a3.
Hardcode 0/0 => NaN instead of letting C divide 0 by 0
In C, division by 0 is unspecified in all cases, so we can't expect a C compiler to always generate code that makes a / b yield NaN if both a and b are 0.

Fixup from 886a9b18b10e4e31b8c746d1f243043d2b7ea234

---
Ref: https://github.com/jqlang/jq/pull/2253#discussion_r1276708144


Hints:

[![Coverage Status](https://coveralls.io/builds/36549283/badge)](https://coveralls.io/builds/36549283)

Coverage increased (+0.002%) to 84.136% when pulling **3442d219eba06b68ad56fa27c0d481a168382bef on itchyny:fold-modulo-constant** into **80052e5275ae8c45b20411eecdd49c945a64a412 on stedolan:master**.

Rebased to run tests.
I also updated the constant folding tests.
> Maybe parts of your commit message could be a code comment on why infinity is returned?

Oh, hmm, maybe there's a better way to deal with division by zero / modulo zero in this function: if the divisor is zero then return a no-op block so that the caller will not constant-fold this so that the jq program can get a run-time error instead of infinity.  Or maybe we should try to make this a compile-time error.  We'd need some way for `constant_fold()` to indicate "error" and then have the callers of `gen_binop()` `FAIL()`.

What do you think, @itchyny?
I agree, `100 % 0` 
should not be folded to infinity
, it should either not be folded, or generate a compile time error

EDIT: I didn't realise `INFINITY` is used to signal errors to the caller when I wrote this comment, but yes, making `consant_fold()` return a no-op block sounds more sensible than using INFINITY, also because the way we are currently detecting division by zero with `INFINITY` relies on undefined behaviour.
Also (maybe it does not matter much for constant folding since you can't have a constant nan, infinity, etc) things like `jv_number_value(a) % jv_number_value(b)` implicitly convert `double` values to integer values, which is undefined behaviour if converting nan and some other values.
Since in C, division/modulo by 0 is unspecified for all values, and casting double to int is unspecified for some values, we should probably have `DIV()` and `MOD()` macros or functions that handle all the edge cases, and use them everywhere instead of bare `/` and `%` when dealing with values returned by `jv_number_value()`.
Ahh wait, zero division is undefined behavior in C? I didn't know that.
I didn't know either! ^^

I only learned it a few days ago because OSS Fuzz tried to compile `0/0`, and the constant folding triggered an UBSAN warning.

I wasn't able to reproduce the UBSAN warning locally for some reason, but I looked at the C standard, and apparently division/modulo with 0 as rhs is unspecified behaviour for all types.
> Since in C, division/modulo by 0 is unspecified for all values, and casting double to int is unspecified for some values, we should probably have `DIV()` and `MOD()` macros or functions that handle all the edge cases, and use them everywhere instead of bare `/` and `%` when dealing with values returned by `jv_number_value()`.

Another alternative solution I like is to somehow export the `f_plus`, `f_mod`, `f_divide`, etc functions from `src/builtin.c` and use those for constant folding. Those functions should already handle all the edge cases, and do type checking by themselves, so if you use them we don't even have to duplicate the type checking and error messages parts. We can just have a look up table that calls `f_plus` for `op == '+'`, `f_divide` for `op == '/'`, etc. and then check if the return value is invalid, and if it is use that as error message.
My preference is that `constant_fold()` return `gen_noop()` whenever it's asked to divide by zero.  This will push the handling of division by literal zero into run-time if this is all we do.

We could also then handle this as a compile-time error at the call-sites for `gen_binop()`, though that would be a bit more complex since we would need a convention for `constant_fold()` to return an error that `gen_binop()` can return and that the call-sites can check for.  Since we're close to a release, I think I'd rather do the simple thing that is pushing the error to run-time by having `constant_fold()` return `gen_noop()` when the divisor is zero.
I am not sure if this is the right fix, but it at least removes the undefined behaviour keeping the existing intended behaviour.

Do we really want to make `0/0` return NaN instead of throwing an division by 0 error? Why are we making `0/0` return Nan, but `1/0` throw an error instead of returning positive infinity? Do we want to make the arithmetic operators IEEE 754 compliant?
> Do we really want to make 0/0 return NaN instead of throwing an division by 0 error? Why are we making 0/0 return Nan, but 1/0 throw an error instead of returning positive infinity?

I guess @itchyny made `f_divide` return NaN for `a==0.0 && b==0.0` because constant-folded `0/0` would yield NaN, while `0 | ./0` would throw an error, and wanted to make things more consistent.

Constant-folding considers an error any expression that returns infinity (positive or negative), while `f_divide` used to only consider expressions with `b==0.0` errors; now it considers an error any expression with `b==0.0&&a!=0.0` which is still not exactly the same as constant-folding, but it's closer.

Another reason why we should centralise how we do division.

Patch:
diff --git a/src/builtin.c b/src/builtin.c
index 09ffc04183..098e4667e7 100644
--- a/src/builtin.c
+++ b/src/builtin.c
@@ -383,7 +383,7 @@ static jv f_multiply(jq_state *jq, jv input, jv a, jv b) {
 static jv f_divide(jq_state *jq, jv input, jv a, jv b) {
   jv_free(input);
   if (jv_get_kind(a) == JV_KIND_NUMBER && jv_get_kind(b) == JV_KIND_NUMBER) {
-    if (jv_number_value(b) == 0.0 && jv_number_value(a) != 0.0)
+    if (jv_number_value(b) == 0.0)
       return type_error2(a, b, "cannot be divided because the divisor is zero");
     jv r = jv_number(jv_number_value(a) / jv_number_value(b));
     jv_free(a);
@@ -396,14 +396,22 @@ static jv f_divide(jq_state *jq, jv input, jv a, jv b) {
   }
 }
 
+#define dtoi(n) ((n) < INTMAX_MIN ? INTMAX_MIN : -(n) < INTMAX_MIN ? INTMAX_MAX : (intmax_t)(n))
 static jv f_mod(jq_state *jq, jv input, jv a, jv b) {
   jv_free(input);
   if (jv_get_kind(a) == JV_KIND_NUMBER && jv_get_kind(b) == JV_KIND_NUMBER) {
-    intmax_t bi = (intmax_t)jv_number_value(b);
+    double na = jv_number_value(a);
+    double nb = jv_number_value(b);
+    if (isnan(na) || isnan(nb)) {
+      jv_free(a);
+      jv_free(b);
+      return jv_number(NAN);
+    }
+    intmax_t bi = dtoi(nb);
     if (bi == 0)
       return type_error2(a, b, "cannot be divided (remainder) because the divisor is zero");
     // Check if the divisor is -1 to avoid overflow when the dividend is INTMAX_MIN.
-    jv r = jv_number(bi == -1 ? 0 : (intmax_t)jv_number_value(a) % bi);
+    jv r = jv_number(bi == -1 ? 0 : dtoi(na) % bi);
     jv_free(a);
     jv_free(b);
     return r;
@@ -411,6 +419,7 @@ static jv f_mod(jq_state *jq, jv input, jv a, jv b) {
     return type_error2(a, b, "cannot be divided (remainder)");
   }
 }
+#undef dtoi
 
 static jv f_equal(jq_state *jq, jv input, jv a, jv b) {
   jv_free(input);
diff --git a/src/compile.c b/src/compile.c
index 4af74ff641..492340b172 100644
--- a/src/compile.c
+++ b/src/compile.c
@@ -172,12 +172,6 @@ int block_is_const(block b) {
   return (block_is_single(b) && (b.first->op == LOADK || b.first->op == PUSHK_UNDER));
 }
 
-int block_is_const_inf(block b) {
-  return (block_is_single(b) && b.first->op == LOADK &&
-          jv_get_kind(b.first->imm.constant) == JV_KIND_NUMBER &&
-          isinf(jv_number_value(b.first->imm.constant)));
-}
-
 jv_kind block_const_kind(block b) {
   assert(block_is_const(b));
   return jv_get_kind(b.first->imm.constant);
diff --git a/src/compile.h b/src/compile.h
index f690adc240..03cb54f568 100644
--- a/src/compile.h
+++ b/src/compile.h
@@ -22,7 +22,6 @@ block gen_op_simple(opcode op);
 block gen_const(jv constant);
 block gen_const_global(jv constant, const char *name);
 int block_is_const(block b);
-int block_is_const_inf(block b);
 jv_kind block_const_kind(block b);
 jv block_const(block b);
 block gen_op_target(opcode op, block target);
diff --git a/src/parser.c b/src/parser.c
index 541cef84e5..fc1a978c53 100644
--- a/src/parser.c
+++ b/src/parser.c
@@ -497,7 +497,16 @@ static block constant_fold(block a, block b, int op) {
     case '+': res = jv_number(na + nb); break;
     case '-': res = jv_number(na - nb); break;
     case '*': res = jv_number(na * nb); break;
-    case '/': res = jv_number(na / nb); break;
+    case '/':
+      if (nb == 0.0) return gen_noop();
+      res = jv_number(na / nb);
+      break;
+    case '%':
+#define is_unsafe_double(n) (isnan(n) || (n) < INTMAX_MIN || -(n) < INTMAX_MIN)
+      if (is_unsafe_double(na) || is_unsafe_double(nb) || (intmax_t)nb == 0) return gen_noop();
+#undef is_unsafe_double
+      res = jv_number((intmax_t)na % (intmax_t)nb);
+      break;
     case EQ:  res = (cmp == 0 ? jv_true() : jv_false()); break;
     case NEQ: res = (cmp != 0 ? jv_true() : jv_false()); break;
     case '<': res = (cmp < 0 ? jv_true() : jv_false()); break;
@@ -574,7 +583,7 @@ static block gen_loc_object(location *loc, struct locfile *locations) {
 }
 
 
-#line 578 "src/parser.c"
+#line 587 "src/parser.c"
 
 
 #ifdef short
@@ -964,24 +973,24 @@ static const yytype_int8 yytranslate[] =
 /* YYRLINE[YYN] -- Source line where rule number YYN was defined.  */
 static const yytype_int16 yyrline[] =
 {
-       0,   313,   313,   316,   321,   324,   339,   342,   347,   350,
-     355,   359,   362,   366,   370,   374,   377,   382,   385,   388,
-     393,   400,   404,   408,   412,   416,   420,   424,   428,   432,
-     436,   440,   444,   448,   452,   456,   460,   464,   470,   476,
-     480,   484,   488,   492,   496,   500,   504,   508,   513,   516,
-     533,   542,   549,   557,   568,   573,   579,   582,   587,   591,
-     598,   603,   607,   607,   614,   617,   620,   626,   629,   632,
-     637,   640,   643,   649,   652,   655,   663,   667,   670,   673,
-     676,   679,   682,   685,   688,   691,   695,   701,   704,   707,
-     710,   713,   716,   719,   722,   725,   728,   731,   734,   737,
-     740,   743,   746,   749,   752,   755,   758,   761,   783,   787,
-     791,   794,   806,   811,   812,   813,   814,   817,   820,   825,
-     830,   833,   838,   841,   846,   850,   853,   858,   861,   866,
-     869,   874,   877,   880,   883,   886,   889,   897,   903,   906,
-     909,   912,   915,   918,   921,   924,   927,   930,   933,   936,
-     939,   942,   945,   948,   951,   954,   959,   962,   963,   964,
-     967,   970,   973,   976,   980,   985,   989,   993,   997,  1001,
-    1009
+       0,   322,   322,   325,   330,   333,   348,   351,   356,   359,
+     364,   368,   371,   375,   379,   383,   386,   391,   394,   397,
+     402,   409,   413,   417,   421,   425,   429,   433,   437,   441,
+     445,   449,   453,   457,   461,   465,   469,   473,   477,   481,
+     485,   489,   493,   497,   501,   505,   509,   513,   518,   521,
+     538,   547,   554,   562,   573,   578,   584,   587,   592,   596,
+     603,   608,   612,   612,   619,   622,   625,   631,   634,   637,
+     642,   645,   648,   654,   657,   660,   668,   672,   675,   678,
+     681,   684,   687,   690,   693,   696,   700,   706,   709,   712,
+     715,   718,   721,   724,   727,   730,   733,   736,   739,   742,
+     745,   748,   751,   754,   757,   760,   763,   766,   788,   792,
+     796,   799,   811,   816,   817,   818,   819,   822,   825,   830,
+     835,   838,   843,   846,   851,   855,   858,   863,   866,   871,
+     874,   879,   882,   885,   888,   891,   894,   902,   908,   911,
+     914,   917,   920,   923,   926,   929,   932,   935,   938,   941,
+     944,   947,   950,   953,   956,   959,   964,   967,   968,   969,
+     972,   975,   978,   981,   985,   990,   994,   998,  1002,  1006,
+    1014
 };
 #endif
 
@@ -2195,199 +2204,199 @@ yydestruct (const char *yymsg,
     case YYSYMBOL_IDENT: /* IDENT  */
 #line 36 "src/parser.y"
             { jv_free(((*yyvaluep).literal)); }
-#line 2199 "src/parser.c"
+#line 2208 "src/parser.c"
         break;
 
     case YYSYMBOL_FIELD: /* FIELD  */
 #line 36 "src/parser.y"
             { jv_free(((*yyvaluep).literal)); }
-#line 2205 "src/parser.c"
+#line 2214 "src/parser.c"
         break;
 
     case YYSYMBOL_BINDING: /* BINDING  */
 #line 36 "src/parser.y"
             { jv_free(((*yyvaluep).literal)); }
-#line 2211 "src/parser.c"
+#line 2220 "src/parser.c"
         break;
 
     case YYSYMBOL_LITERAL: /* LITERAL  */
 #line 36 "src/parser.y"
             { jv_free(((*yyvaluep).literal)); }
-#line 2217 "src/parser.c"
+#line 2226 "src/parser.c"
         break;
 
     case YYSYMBOL_FORMAT: /* FORMAT  */
 #line 36 "src/parser.y"
             { jv_free(((*yyvaluep).literal)); }
-#line 2223 "src/parser.c"
+#line 2232 "src/parser.c"
         break;
 
     case YYSYMBOL_QQSTRING_TEXT: /* QQSTRING_TEXT  */
 #line 36 "src/parser.y"
             { jv_free(((*yyvaluep).literal)); }
-#line 2229 "src/parser.c"
+#line 2238 "src/parser.c"
         break;
 
     case YYSYMBOL_Module: /* Module  */
 #line 37 "src/parser.y"
             { block_free(((*yyvaluep).blk)); }
-#line 2235 "src/parser.c"
+#line 2244 "src/parser.c"
         break;
 
     case YYSYMBOL_Imports: /* Imports  */
 #line 37 "src/parser.y"
             { block_free(((*yyvaluep).blk)); }
-#line 2241 "src/parser.c"
+#line 2250 "src/parser.c"
         break;
 
     case YYSYMBOL_FuncDefs: /* FuncDefs  */
 #line 37 "src/parser.y"
             { block_free(((*yyvaluep).blk)); }
-#line 2247 "src/parser.c"
+#line 2256 "src/parser.c"
         break;
 
     case YYSYMBOL_Exp: /* Exp  */
 #line 37 "src/parser.y"
             { block_free(((*yyvaluep).blk)); }
-#line 2253 "src/parser.c"
+#line 2262 "src/parser.c"
         break;
 
     case YYSYMBOL_Import: /* Import  */
 #line 37 "src/parser.y"
             { block_free(((*yyvaluep).blk)); }
-#line 2259 "src/parser.c"
+#line 2268 "src/parser.c"
         break;
 
     case YYSYMBOL_ImportWhat: /* ImportWhat  */
 #line 37 "src/parser.y"
             { block_free(((*yyvaluep).blk)); }
-#line 2265 "src/parser.c"
+#line 2274 "src/parser.c"
         break;
 
     case YYSYMBOL_ImportFrom: /* ImportFrom  */
 #line 37 "src/parser.y"
             { block_free(((*yyvaluep).blk)); }
-#line 2271 "src/parser.c"
+#line 2280 "src/parser.c"
         break;
 
     case YYSYMBOL_FuncDef: /* FuncDef  */
 #line 37 "src/parser.y"
             { block_free(((*yyvaluep).blk)); }
-#line 2277 "src/parser.c"
+#line 2286 "src/parser.c"
         break;
 
     case YYSYMBOL_Params: /* Params  */
 #line 37 "src/parser.y"
             { block_free(((*yyvaluep).blk)); }
-#line 2283 "src/parser.c"
+#line 2292 "src/parser.c"
         break;
 
     case YYSYMBOL_Param: /* Param  */
 #line 37 "src/parser.y"
             { block_free(((*yyvaluep).blk)); }
-#line 2289 "src/parser.c"
+#line 2298 "src/parser.c"
         break;
 
     case YYSYMBOL_NoFormat: /* NoFormat  */
 #line 36 "src/parser.y"
             { jv_free(((*yyvaluep).literal)); }
-#line 2295 "src/parser.c"
+#line 2304 "src/parser.c"
         break;
 
     case YYSYMBOL_String: /* String  */
 #line 37 "src/parser.y"
             { block_free(((*yyvaluep).blk)); }
-#line 2301 "src/parser.c"
+#line 2310 "src/parser.c"
         break;
 
     case YYSYMBOL_QQString: /* QQString  */
 #line 37 "src/parser.y"
             { block_free(((*yyvaluep).blk)); }
-#line 2307 "src/parser.c"
+#line 2316 "src/parser.c"
         break;
 
     case YYSYMBOL_ElseBody: /* ElseBody  */
 #line 37 "src/parser.y"
             { block_free(((*yyvaluep).blk)); }
-#line 2313 "src/parser.c"
+#line 2322 "src/parser.c"
         break;
 
     case YYSYMBOL_ExpD: /* ExpD  */
 #line 37 "src/parser.y"
             { block_free(((*yyvaluep).blk)); }
-#line 2319 "src/parser.c"
+#line 2328 "src/parser.c"
         break;
 
     case YYSYMBOL_Term: /* Term  */
 #line 37 "src/parser.y"
             { block_free(((*yyvaluep).blk)); }
-#line 2325 "src/parser.c"
+#line 2334 "src/parser.c"
         break;
 
     case YYSYMBOL_Args: /* Args  */
 #line 37 "src/parser.y"
             { block_free(((*yyvaluep).blk)); }
-#line 2331 "src/parser.c"
+#line 2340 "src/parser.c"
         break;
 
     case YYSYMBOL_Arg: /* Arg  */
 #line 37 "src/parser.y"
             { block_free(((*yyvaluep).blk)); }
-#line 2337 "src/parser.c"
+#line 2346 "src/parser.c"
         break;
 
     case YYSYMBOL_RepPatterns: /* RepPatterns  */
 #line 37 "src/parser.y"
             { block_free(((*yyvaluep).blk)); }
-#line 2343 "src/parser.c"
+#line 2352 "src/parser.c"
         break;
 
     case YYSYMBOL_Patterns: /* Patterns  */
 #line 37 "src/parser.y"
             { block_free(((*yyvaluep).blk)); }
-#line 2349 "src/parser.c"
+#line 2358 "src/parser.c"
         break;
 
     case YYSYMBOL_Pattern: /* Pattern  */
 #line 37 "src/parser.y"
             { block_free(((*yyvaluep).blk)); }
-#line 2355 "src/parser.c"
+#line 2364 "src/parser.c"
         break;
 
     case YYSYMBOL_ArrayPats: /* ArrayPats  */
 #line 37 "src/parser.y"
             { block_free(((*yyvaluep).blk)); }
-#line 2361 "src/parser.c"
+#line 2370 "src/parser.c"
         break;
 
     case YYSYMBOL_ObjPats: /* ObjPats  */
 #line 37 "src/parser.y"
             { block_free(((*yyvaluep).blk)); }
-#line 2367 "src/parser.c"
+#line 2376 "src/parser.c"
         break;
 
     case YYSYMBOL_ObjPat: /* ObjPat  */
 #line 37 "src/parser.y"
             { block_free(((*yyvaluep).blk)); }
-#line 2373 "src/parser.c"
+#line 2382 "src/parser.c"
         break;
 
     case YYSYMBOL_Keyword: /* Keyword  */
 #line 36 "src/parser.y"
             { jv_free(((*yyvaluep).literal)); }
-#line 2379 "src/parser.c"
+#line 2388 "src/parser.c"
         break;
 
     case YYSYMBOL_MkDict: /* MkDict  */
 #line 37 "src/parser.y"
             { block_free(((*yyvaluep).blk)); }
-#line 2385 "src/parser.c"
+#line 2394 "src/parser.c"
         break;
 
     case YYSYMBOL_MkDictPair: /* MkDictPair  */
 #line 37 "src/parser.y"
             { block_free(((*yyvaluep).blk)); }
-#line 2391 "src/parser.c"
+#line 2400 "src/parser.c"
         break;
 
       default:
@@ -2691,31 +2700,31 @@ YYLTYPE yylloc = yyloc_default;
   switch (yyn)
     {
   case 2: /* TopLevel: Module Imports Exp  */
-#line 313 "src/parser.y"
+#line 322 "src/parser.y"
                    {
   *answer = BLOCK((yyvsp[-2].blk), (yyvsp[-1].blk), gen_op_simple(TOP), (yyvsp[0].blk));
 }
-#line 2699 "src/parser.c"
+#line 2708 "src/parser.c"
     break;
 
   case 3: /* TopLevel: Module Imports FuncDefs  */
-#line 316 "src/parser.y"
+#line 325 "src/parser.y"
                         {
   *answer = BLOCK((yyvsp[-2].blk), (yyvsp[-1].blk), (yyvsp[0].blk));
 }
-#line 2707 "src/parser.c"
+#line 2716 "src/parser.c"
     break;
 
   case 4: /* Module: %empty  */
-#line 321 "src/parser.y"
+#line 330 "src/parser.y"
        {
   (yyval.blk) = gen_noop();
 }
-#line 2715 "src/parser.c"
+#line 2724 "src/parser.c"
     break;
 
   case 5: /* Module: "module" Exp ';'  */
-#line 324 "src/parser.y"
+#line 333 "src/parser.y"
                  {
   if (!block_is_const((yyvsp[-1].blk))) {
     FAIL((yyloc), "Module metadata must be constant");
@@ -2729,364 +2738,360 @@ YYLTYPE yylloc = yyloc_default;
     (yyval.blk) = gen_module((yyvsp[-1].blk));
   }
 }
-#line 2733 "src/parser.c"
+#line 2742 "src/parser.c"
     break;
 
   case 6: /* Imports: %empty  */
-#line 339 "src/parser.y"
+#line 348 "src/parser.y"
        {
   (yyval.blk) = gen_noop();
 }
-#line 2741 "src/parser.c"
+#line 2750 "src/parser.c"
     break;
 
   case 7: /* Imports: Import Imports  */
-#line 342 "src/parser.y"
+#line 351 "src/parser.y"
                {
   (yyval.blk) = BLOCK((yyvsp[-1].blk), (yyvsp[0].blk));
 }
-#line 2749 "src/parser.c"
+#line 2758 "src/parser.c"
     break;
 
   case 8: /* FuncDefs: %empty  */
-#line 347 "src/parser.y"
+#line 356 "src/parser.y"
        {
   (yyval.blk) = gen_noop();
 }
-#line 2757 "src/parser.c"
+#line 2766 "src/parser.c"
     break;
 
   case 9: /* FuncDefs: FuncDef FuncDefs  */
-#line 350 "src/parser.y"
+#line 359 "src/parser.y"
                  {
   (yyval.blk) = block_join((yyvsp[-1].blk), (yyvsp[0].blk));
 }
-#line 2765 "src/parser.c"
+#line 2774 "src/parser.c"
     break;
 
   case 10: /* Exp: FuncDef Exp  */
-#line 355 "src/parser.y"
+#line 364 "src/parser.y"
                           {
   (yyval.blk) = block_bind_referenced((yyvsp[-1].blk), (yyvsp[0].blk), OP_IS_CALL_PSEUDO);
 }
-#line 2773 "src/parser.c"
+#line 2782 "src/parser.c"
     break;
 
   case 11: /* Exp: Term "as" Patterns '|' Exp  */
-#line 359 "src/parser.y"
+#line 368 "src/parser.y"
                            {
   (yyval.blk) = gen_destructure((yyvsp[-4].blk), (yyvsp[-2].blk), (yyvsp[0].blk));
 }
-#line 2781 "src/parser.c"
+#line 2790 "src/parser.c"
     break;
 
   case 12: /* Exp: "reduce" Term "as" Patterns '(' Exp ';' Exp ')'  */
-#line 362 "src/parser.y"
+#line 371 "src/parser.y"
                                                 {
   (yyval.blk) = gen_reduce((yyvsp[-7].blk), (yyvsp[-5].blk), (yyvsp[-3].blk), (yyvsp[-1].blk));
 }
-#line 2789 "src/parser.c"
+#line 2798 "src/parser.c"
     break;
 
   case 13: /* Exp: "foreach" Term "as" Patterns '(' Exp ';' Exp ';' Exp ')'  */
-#line 366 "src/parser.y"
+#line 375 "src/parser.y"
                                                          {
   (yyval.blk) = gen_foreach((yyvsp[-9].blk), (yyvsp[-7].blk), (yyvsp[-5].blk), (yyvsp[-3].blk), (yyvsp[-1].blk));
 }
-#line 2797 "src/parser.c"
+#line 2806 "src/parser.c"
     break;
 
   case 14: /* Exp: "foreach" Term "as" Patterns '(' Exp ';' Exp ')'  */
-#line 370 "src/parser.y"
+#line 379 "src/parser.y"
                                                  {
   (yyval.blk) = gen_foreach((yyvsp[-7].blk), (yyvsp[-5].blk), (yyvsp[-3].blk), (yyvsp[-1].blk), gen_noop());
 }
-#line 2805 "src/parser.c"
+#line 2814 "src/parser.c"
     break;
 
   case 15: /* Exp: "if" Exp "then" Exp ElseBody  */
-#line 374 "src/parser.y"
+#line 383 "src/parser.y"
                              {
   (yyval.blk) = gen_cond((yyvsp[-3].blk), (yyvsp[-1].blk), (yyvsp[0].blk));
 }
-#line 2813 "src/parser.c"
+#line 2822 "src/parser.c"
     break;
 
   case 16: /* Exp: "if" Exp "then" error  */
-#line 377 "src/parser.y"
+#line 386 "src/parser.y"
                       {
   FAIL((yyloc), "Possibly unterminated 'if' statement");
   (yyval.blk) = (yyvsp[-2].blk);
 }
-#line 2822 "src/parser.c"
+#line 2831 "src/parser.c"
     break;
 
   case 17: /* Exp: "try" Exp "catch" Exp  */
-#line 382 "src/parser.y"
+#line 391 "src/parser.y"
                       {
   (yyval.blk) = gen_try((yyvsp[-2].blk), (yyvsp[0].blk));
 }
-#line 2830 "src/parser.c"
+#line 2839 "src/parser.c"
     break;
 
   case 18: /* Exp: "try" Exp  */
-#line 385 "src/parser.y"
+#line 394 "src/parser.y"
           {
   (yyval.blk) = gen_try((yyvsp[0].blk), gen_op_simple(BACKTRACK));
 }
-#line 2838 "src/parser.c"
+#line 2847 "src/parser.c"
     break;
 
   case 19: /* Exp: "try" Exp "catch" error  */
-#line 388 "src/parser.y"
+#line 397 "src/parser.y"
                         {
   FAIL((yyloc), "Possibly unterminated 'try' statement");
   (yyval.blk) = (yyvsp[-2].blk);
 }
-#line 2847 "src/parser.c"
+#line 2856 "src/parser.c"
     break;
 
   case 20: /* Exp: "label" BINDING '|' Exp  */
-#line 393 "src/parser.y"
+#line 402 "src/parser.y"
                         {
   jv v = jv_string_fmt("*label-%s", jv_string_value((yyvsp[-2].literal)));
   (yyval.blk) = gen_location((yyloc), locations, gen_label(jv_string_value(v), (yyvsp[0].blk)));
   jv_free((yyvsp[-2].literal));
   jv_free(v);
 }
-#line 2858 "src/parser.c"
+#line 2867 "src/parser.c"
     break;
 
   case 21: /* Exp: Exp '?'  */
-#line 400 "src/parser.y"
+#line 409 "src/parser.y"
         {
   (yyval.blk) = gen_try((yyvsp[-1].blk), gen_op_simple(BACKTRACK));
 }
-#line 2866 "src/parser.c"
+#line 2875 "src/parser.c"
     break;
 
   case 22: /* Exp: Exp '=' Exp  */
-#line 404 "src/parser.y"
+#line 413 "src/parser.y"
             {
   (yyval.blk) = gen_call("_assign", BLOCK(gen_lambda((yyvsp[-2].blk)), gen_lambda((yyvsp[0].blk))));
 }
-#line 2874 "src/parser.c"
+#line 2883 "src/parser.c"
     break;
 
   case 23: /* Exp: Exp "or" Exp  */
-#line 408 "src/parser.y"
+#line 417 "src/parser.y"
              {
   (yyval.blk) = gen_or((yyvsp[-2].blk), (yyvsp[0].blk));
 }
-#line 2882 "src/parser.c"
+#line 2891 "src/parser.c"
     break;
 
   case 24: /* Exp: Exp "and" Exp  */
-#line 412 "src/parser.y"
+#line 421 "src/parser.y"
               {
   (yyval.blk) = gen_and((yyvsp[-2].blk), (yyvsp[0].blk));
 }
-#line 2890 "src/parser.c"
+#line 2899 "src/parser.c"
     break;
 
   case 25: /* Exp: Exp "//" Exp  */
-#line 416 "src/parser.y"
+#line 425 "src/parser.y"
              {
   (yyval.blk) = gen_definedor((yyvsp[-2].blk), (yyvsp[0].blk));
 }
-#line 2898 "src/parser.c"
+#line 2907 "src/parser.c"
     break;
 
   case 26: /* Exp: Exp "//=" Exp  */
-#line 420 "src/parser.y"
+#line 429 "src/parser.y"
               {
   (yyval.blk) = gen_definedor_assign((yyvsp[-2].blk), (yyvsp[0].blk));
 }
-#line 2906 "src/parser.c"
+#line 2915 "src/parser.c"
     break;
 
   case 27: /* Exp: Exp "|=" Exp  */
-#line 424 "src/parser.y"
+#line 433 "src/parser.y"
              {
   (yyval.blk) = gen_call("_modify", BLOCK(gen_lambda((yyvsp[-2].blk)), gen_lambda((yyvsp[0].blk))));
 }
-#line 2914 "src/parser.c"
+#line 2923 "src/parser.c"
     break;
 
   case 28: /* Exp: Exp '|' Exp  */
-#line 428 "src/parser.y"
+#line 437 "src/parser.y"
             {
   (yyval.blk) = block_join((yyvsp[-2].blk), (yyvsp[0].blk));
 }
-#line 2922 "src/parser.c"
+#line 2931 "src/parser.c"
     break;
 
   case 29: /* Exp: Exp ',' Exp  */
-#line 432 "src/parser.y"
+#line 441 "src/parser.y"
             {
   (yyval.blk) = gen_both((yyvsp[-2].blk), (yyvsp[0].blk));
 }
-#line 2930 "src/parser.c"
+#line 2939 "src/parser.c"
     break;
 
   case 30: /* Exp: Exp '+' Exp  */
-#line 436 "src/parser.y"
+#line 445 "src/parser.y"
             {
   (yyval.blk) = gen_binop((yyvsp[-2].blk), (yyvsp[0].blk), '+');
 }
-#line 2938 "src/parser.c"
+#line 2947 "src/parser.c"
     break;
 
   case 31: /* Exp: Exp "+=" Exp  */
-#line 440 "src/parser.y"
+#line 449 "src/parser.y"
              {
   (yyval.blk) = gen_update((yyvsp[-2].blk), (yyvsp[0].blk), '+');
 }
-#line 2946 "src/parser.c"
+#line 2955 "src/parser.c"
     break;
 
   case 32: /* Exp: '-' Exp  */
-#line 444 "src/parser.y"
+#line 453 "src/parser.y"
         {
   (yyval.blk) = BLOCK((yyvsp[0].blk), gen_call("_negate", gen_noop()));
 }
-#line 2954 "src/parser.c"
+#line 2963 "src/parser.c"
     break;
 
   case 33: /* Exp: Exp '-' Exp  */
-#line 448 "src/parser.y"
+#line 457 "src/parser.y"
             {
   (yyval.blk) = gen_binop((yyvsp[-2].blk), (yyvsp[0].blk), '-');
 }
-#line 2962 "src/parser.c"
+#line 2971 "src/parser.c"
     break;
 
   case 34: /* Exp: Exp "-=" Exp  */
-#line 452 "src/parser.y"
+#line 461 "src/parser.y"
              {
   (yyval.blk) = gen_update((yyvsp[-2].blk), (yyvsp[0].blk), '-');
 }
-#line 2970 "src/parser.c"
+#line 2979 "src/parser.c"
     break;
 
   case 35: /* Exp: Exp '*' Exp  */
-#line 456 "src/parser.y"
+#line 465 "src/parser.y"
             {
   (yyval.blk) = gen_binop((yyvsp[-2].blk), (yyvsp[0].blk), '*');
 }
-#line 2978 "src/parser.c"
+#line 2987 "src/parser.c"
     break;
 
   case 36: /* Exp: Exp "*=" Exp  */
-#line 460 "src/parser.y"
+#line 469 "src/parser.y"
              {
   (yyval.blk) = gen_update((yyvsp[-2].blk), (yyvsp[0].blk), '*');
 }
-#line 2986 "src/parser.c"
+#line 2995 "src/parser.c"
     break;
 
   case 37: /* Exp: Exp '/' Exp  */
-#line 464 "src/parser.y"
+#line 473 "src/parser.y"
             {
   (yyval.blk) = gen_binop((yyvsp[-2].blk), (yyvsp[0].blk), '/');
-  if (block_is_const_inf((yyval.blk)))
-    FAIL((yyloc), "Division by zero?");
 }
-#line 2996 "src/parser.c"
+#line 3003 "src/parser.c"
     break;
 
   case 38: /* Exp: Exp '%' Exp  */
-#line 470 "src/parser.y"
+#line 477 "src/parser.y"
             {
   (yyval.blk) = gen_binop((yyvsp[-2].blk), (yyvsp[0].blk), '%');
-  if (block_is_const_inf((yyval.blk)))
-    FAIL((yyloc), "Remainder by zero?");
 }
-#line 3006 "src/parser.c"
+#line 3011 "src/parser.c"
     break;
 
   case 39: /* Exp: Exp "/=" Exp  */
-#line 476 "src/parser.y"
+#line 481 "src/parser.y"
              {
   (yyval.blk) = gen_update((yyvsp[-2].blk), (yyvsp[0].blk), '/');
 }
-#line 3014 "src/parser.c"
+#line 3019 "src/parser.c"
     break;
 
   case 40: /* Exp: Exp "%=" Exp  */
-#line 480 "src/parser.y"
+#line 485 "src/parser.y"
                {
   (yyval.blk) = gen_update((yyvsp[-2].blk), (yyvsp[0].blk), '%');
 }
-#line 3022 "src/parser.c"
+#line 3027 "src/parser.c"
     break;
 
   case 41: /* Exp: Exp "==" Exp  */
-#line 484 "src/parser.y"
+#line 489 "src/parser.y"
              {
   (yyval.blk) = gen_binop((yyvsp[-2].blk), (yyvsp[0].blk), EQ);
 }
-#line 3030 "src/parser.c"
+#line 3035 "src/parser.c"
     break;
 
   case 42: /* Exp: Exp "!=" Exp  */
-#line 488 "src/parser.y"
+#line 493 "src/parser.y"
              {
   (yyval.blk) = gen_binop((yyvsp[-2].blk), (yyvsp[0].blk), NEQ);
 }
-#line 3038 "src/parser.c"
+#line 3043 "src/parser.c"
     break;
 
   case 43: /* Exp: Exp '<' Exp  */
-#line 492 "src/parser.y"
+#line 497 "src/parser.y"
             {
   (yyval.blk) = gen_binop((yyvsp[-2].blk), (yyvsp[0].blk), '<');
 }
-#line 3046 "src/parser.c"
+#line 3051 "src/parser.c"
     break;
 
   case 44: /* Exp: Exp '>' Exp  */
-#line 496 "src/parser.y"
+#line 501 "src/parser.y"
             {
   (yyval.blk) = gen_binop((yyvsp[-2].blk), (yyvsp[0].blk), '>');
 }
-#line 3054 "src/parser.c"
+#line 3059 "src/parser.c"
     break;
 
   case 45: /* Exp: Exp "<=" Exp  */
-#line 500 "src/parser.y"
+#line 505 "src/parser.y"
              {
   (yyval.blk) = gen_binop((yyvsp[-2].blk), (yyvsp[0].blk), LESSEQ);
 }
-#line 3062 "src/parser.c"
+#line 3067 "src/parser.c"
     break;
 
   case 46: /* Exp: Exp ">=" Exp  */
-#line 504 "src/parser.y"
+#line 509 "src/parser.y"
              {
   (yyval.blk) = gen_binop((yyvsp[-2].blk), (yyvsp[0].blk), GREATEREQ);
 }
-#line 3070 "src/parser.c"
+#line 3075 "src/parser.c"
     break;
 
   case 47: /* Exp: Term  */
-#line 508 "src/parser.y"
+#line 513 "src/parser.y"
      {
   (yyval.blk) = (yyvsp[0].blk);
 }
-#line 3078 "src/parser.c"
+#line 3083 "src/parser.c"
     break;
 
   case 48: /* Import: ImportWhat ';'  */
-#line 513 "src/parser.y"
+#line 518 "src/parser.y"
                {
   (yyval.blk) = (yyvsp[-1].blk);
 }
-#line 3086 "src/parser.c"
+#line 3091 "src/parser.c"
     break;
 
   case 49: /* Import: ImportWhat Exp ';'  */
-#line 516 "src/parser.y"
+#line 521 "src/parser.y"
                    {
   if (!block_is_const((yyvsp[-1].blk))) {
     FAIL((yyloc), "Module metadata must be constant");
@@ -3102,11 +3107,11 @@ YYLTYPE yylloc = yyloc_default;
     (yyval.blk) = gen_import_meta((yyvsp[-2].blk), (yyvsp[-1].blk));
   }
 }
-#line 3106 "src/parser.c"
+#line 3111 "src/parser.c"
     break;
 
   case 50: /* ImportWhat: "import" ImportFrom "as" BINDING  */
-#line 533 "src/parser.y"
+#line 538 "src/parser.y"
                                  {
   jv v = block_const((yyvsp[-2].blk));
   // XXX Make gen_import take only blocks and the int is_data so we
@@ -3116,11 +3121,11 @@ YYLTYPE yylloc = yyloc_default;
   jv_free((yyvsp[0].literal));
   jv_free(v);
 }
-#line 3120 "src/parser.c"
+#line 3125 "src/parser.c"
     break;
 
   case 51: /* ImportWhat: "import" ImportFrom "as" IDENT  */
-#line 542 "src/parser.y"
+#line 547 "src/parser.y"
                                {
   jv v = block_const((yyvsp[-2].blk));
   (yyval.blk) = gen_import(jv_string_value(v), jv_string_value((yyvsp[0].literal)), 0);
@@ -3128,22 +3133,22 @@ YYLTYPE yylloc = yyloc_default;
   jv_free((yyvsp[0].literal));
   jv_free(v);
 }
-#line 3132 "src/parser.c"
+#line 3137 "src/parser.c"
     break;
 
   case 52: /* ImportWhat: "include" ImportFrom  */
-#line 549 "src/parser.y"
+#line 554 "src/parser.y"
                      {
   jv v = block_const((yyvsp[0].blk));
   (yyval.blk) = gen_import(jv_string_value(v), NULL, 0);
   block_free((yyvsp[0].blk));
   jv_free(v);
 }
-#line 3143 "src/parser.c"
+#line 3148 "src/parser.c"
     break;
 
   case 53: /* ImportFrom: String  */
-#line 557 "src/parser.y"
+#line 562 "src/parser.y"
        {
   if (!block_is_const((yyvsp[0].blk))) {
     FAIL((yyloc), "Import path must be constant");
@@ -3153,183 +3158,183 @@ YYLTYPE yylloc = yyloc_default;
     (yyval.blk) = (yyvsp[0].blk);
   }
 }
-#line 3157 "src/parser.c"
+#line 3162 "src/parser.c"
     break;
 
   case 54: /* FuncDef: "def" IDENT ':' Exp ';'  */
-#line 568 "src/parser.y"
+#line 573 "src/parser.y"
                         {
   (yyval.blk) = gen_function(jv_string_value((yyvsp[-3].literal)), gen_noop(), (yyvsp[-1].blk));
   jv_free((yyvsp[-3].literal));
 }
-#line 3166 "src/parser.c"
+#line 3171 "src/parser.c"
     break;
 
   case 55: /* FuncDef: "def" IDENT '(' Params ')' ':' Exp ';'  */
-#line 573 "src/parser.y"
+#line 578 "src/parser.y"
                                        {
   (yyval.blk) = gen_function(jv_string_value((yyvsp[-6].literal)), (yyvsp[-4].blk), (yyvsp[-1].blk));
   jv_free((yyvsp[-6].literal));
 }
-#line 3175 "src/parser.c"
+#line 3180 "src/parser.c"
     break;
 
   case 56: /* Params: Param  */
-#line 579 "src/parser.y"
+#line 584 "src/parser.y"
       {
   (yyval.blk) = (yyvsp[0].blk);
 }
-#line 3183 "src/parser.c"
+#line 3188 "src/parser.c"
     break;
 
   case 57: /* Params: Params ';' Param  */
-#line 582 "src/parser.y"
+#line 587 "src/parser.y"
                  {
   (yyval.blk) = BLOCK((yyvsp[-2].blk), (yyvsp[0].blk));
 }
-#line 3191 "src/parser.c"
+#line 3196 "src/parser.c"
     break;
 
   case 58: /* Param: BINDING  */
-#line 587 "src/parser.y"
+#line 592 "src/parser.y"
         {
   (yyval.blk) = gen_param_regular(jv_string_value((yyvsp[0].literal)));
   jv_free((yyvsp[0].literal));
 }
-#line 3200 "src/parser.c"
+#line 3205 "src/parser.c"
     break;
 
   case 59: /* Param: IDENT  */
-#line 591 "src/parser.y"
+#line 596 "src/parser.y"
       {
   (yyval.blk) = gen_param(jv_string_value((yyvsp[0].literal)));
   jv_free((yyvsp[0].literal));
 }
-#line 3209 "src/parser.c"
+#line 3214 "src/parser.c"
     break;
 
   case 60: /* NoFormat: %empty  */
-#line 598 "src/parser.y"
+#line 603 "src/parser.y"
        {
   (yyval.literal) = jv_string("text");
 }
-#line 3217 "src/parser.c"
+#line 3222 "src/parser.c"
     break;
 
   case 61: /* String: QQSTRING_START NoFormat QQString QQSTRING_END  */
-#line 603 "src/parser.y"
+#line 608 "src/parser.y"
                                               {
   (yyval.blk) = (yyvsp[-1].blk);
   jv_free((yyvsp[-2].literal));
 }
-#line 3226 "src/parser.c"
+#line 3231 "src/parser.c"
     break;
 
   case 62: /* @1: %empty  */
-#line 607 "src/parser.y"
+#line 612 "src/parser.y"
                       { (yyval.literal) = (yyvsp[-1].literal); }
-#line 3232 "src/parser.c"
+#line 3237 "src/parser.c"
     break;
 
   case 63: /* String: FORMAT QQSTRING_START @1 QQString QQSTRING_END  */
-#line 607 "src/parser.y"
+#line 612 "src/parser.y"
                                                                   {
   (yyval.blk) = (yyvsp[-1].blk);
   jv_free((yyvsp[-2].literal));
 }
-#line 3241 "src/parser.c"
+#line 3246 "src/parser.c"
     break;
 
   case 64: /* QQString: %empty  */
-#line 614 "src/parser.y"
+#line 619 "src/parser.y"
        {
   (yyval.blk) = gen_const(jv_string(""));
 }
-#line 3249 "src/parser.c"
+#line 3254 "src/parser.c"
     break;
 
   case 65: /* QQString: QQString QQSTRING_TEXT  */
-#line 617 "src/parser.y"
+#line 622 "src/parser.y"
                        {
   (yyval.blk) = gen_binop((yyvsp[-1].blk), gen_const((yyvsp[0].literal)), '+');
 }
-#line 3257 "src/parser.c"
+#line 3262 "src/parser.c"
     break;
 
   case 66: /* QQString: QQString QQSTRING_INTERP_START Exp QQSTRING_INTERP_END  */
-#line 620 "src/parser.y"
+#line 625 "src/parser.y"
                                                        {
   (yyval.blk) = gen_binop((yyvsp[-3].blk), gen_format((yyvsp[-1].blk), jv_copy((yyvsp[-4].literal))), '+');
 }
-#line 3265 "src/parser.c"
+#line 3270 "src/parser.c"
     break;
 
   case 67: /* ElseBody: "elif" Exp "then" Exp ElseBody  */
-#line 626 "src/parser.y"
+#line 631 "src/parser.y"
                                {
   (yyval.blk) = gen_cond((yyvsp[-3].blk), (yyvsp[-1].blk), (yyvsp[0].blk));
 }
-#line 3273 "src/parser.c"
+#line 3278 "src/parser.c"
     break;
 
   case 68: /* ElseBody: "else" Exp "end"  */
-#line 629 "src/parser.y"
+#line 634 "src/parser.y"
                  {
   (yyval.blk) = (yyvsp[-1].blk);
 }
-#line 3281 "src/parser.c"
+#line 3286 "src/parser.c"
     break;
 
   case 69: /* ElseBody: "end"  */
-#line 632 "src/parser.y"
+#line 637 "src/parser.y"
       {
   (yyval.blk) = gen_noop();
 }
-#line 3289 "src/parser.c"
+#line 3294 "src/parser.c"
     break;
 
   case 70: /* ExpD: ExpD '|' ExpD  */
-#line 637 "src/parser.y"
+#line 642 "src/parser.y"
               {
   (yyval.blk) = block_join((yyvsp[-2].blk), (yyvsp[0].blk));
 }
-#line 3297 "src/parser.c"
+#line 3302 "src/parser.c"
     break;
 
   case 71: /* ExpD: '-' ExpD  */
-#line 640 "src/parser.y"
+#line 645 "src/parser.y"
          {
   (yyval.blk) = BLOCK((yyvsp[0].blk), gen_call("_negate", gen_noop()));
 }
-#line 3305 "src/parser.c"
+#line 3310 "src/parser.c"
     break;
 
   case 72: /* ExpD: Term  */
-#line 643 "src/parser.y"
+#line 648 "src/parser.y"
      {
   (yyval.blk) = (yyvsp[0].blk);
 }
-#line 3313 "src/parser.c"
+#line 3318 "src/parser.c"
     break;
 
   case 73: /* Term: '.'  */
-#line 649 "src/parser.y"
+#line 654 "src/parser.y"
     {
   (yyval.blk) = gen_noop();
 }
-#line 3321 "src/parser.c"
+#line 3326 "src/parser.c"
     break;
 
   case 74: /* Term: ".."  */
-#line 652 "src/parser.y"
+#line 657 "src/parser.y"
     {
   (yyval.blk) = gen_call("recurse", gen_noop());
 }
-#line 3329 "src/parser.c"
+#line 3334 "src/parser.c"
     break;
 
   case 75: /* Term: "break" BINDING  */
-#line 655 "src/parser.y"
+#line 660 "src/parser.y"
               {
   jv v = jv_string_fmt("*label-%s", jv_string_value((yyvsp[0].literal)));     // impossible symbol
   (yyval.blk) = gen_location((yyloc), locations,
@@ -3338,263 +3343,263 @@ YYLTYPE yylloc = yyloc_default;
   jv_free(v);
   jv_free((yyvsp[0].literal));
 }
-#line 3342 "src/parser.c"
+#line 3347 "src/parser.c"
     break;
 
   case 76: /* Term: "break" error  */
-#line 663 "src/parser.y"
+#line 668 "src/parser.y"
             {
   FAIL((yyloc), "break requires a label to break to");
   (yyval.blk) = gen_noop();
 }
-#line 3351 "src/parser.c"
+#line 3356 "src/parser.c"
     break;
 
   case 77: /* Term: Term FIELD '?'  */
-#line 667 "src/parser.y"
+#line 672 "src/parser.y"
                {
   (yyval.blk) = gen_index_opt((yyvsp[-2].blk), gen_const((yyvsp[-1].literal)));
 }
-#line 3359 "src/parser.c"
+#line 3364 "src/parser.c"
     break;
 
   case 78: /* Term: FIELD '?'  */
-#line 670 "src/parser.y"
+#line 675 "src/parser.y"
           {
   (yyval.blk) = gen_index_opt(gen_noop(), gen_const((yyvsp[-1].literal)));
 }
-#line 3367 "src/parser.c"
+#line 3372 "src/parser.c"
     break;
 
   case 79: /* Term: Term '.' String '?'  */
-#line 673 "src/parser.y"
+#line 678 "src/parser.y"
                     {
   (yyval.blk) = gen_index_opt((yyvsp[-3].blk), (yyvsp[-1].blk));
 }
-#line 3375 "src/parser.c"
+#line 3380 "src/parser.c"
     break;
 
   case 80: /* Term: '.' String '?'  */
-#line 676 "src/parser.y"
+#line 681 "src/parser.y"
                {
   (yyval.blk) = gen_index_opt(gen_noop(), (yyvsp[-1].blk));
 }
-#line 3383 "src/parser.c"
+#line 3388 "src/parser.c"
     break;
 
   case 81: /* Term: Term FIELD  */
-#line 679 "src/parser.y"
+#line 684 "src/parser.y"
                         {
   (yyval.blk) = gen_index((yyvsp[-1].blk), gen_const((yyvsp[0].literal)));
 }
-#line 3391 "src/parser.c"
+#line 3396 "src/parser.c"
     break;
 
   case 82: /* Term: FIELD  */
-#line 682 "src/parser.y"
+#line 687 "src/parser.y"
                    {
   (yyval.blk) = gen_index(gen_noop(), gen_const((yyvsp[0].literal)));
 }
-#line 3399 "src/parser.c"
+#line 3404 "src/parser.c"
     break;
 
   case 83: /* Term: Term '.' String  */
-#line 685 "src/parser.y"
+#line 690 "src/parser.y"
                              {
   (yyval.blk) = gen_index((yyvsp[-2].blk), (yyvsp[0].blk));
 }
-#line 3407 "src/parser.c"
+#line 3412 "src/parser.c"
     break;
 
   case 84: /* Term: '.' String  */
-#line 688 "src/parser.y"
+#line 693 "src/parser.y"
                         {
   (yyval.blk) = gen_index(gen_noop(), (yyvsp[0].blk));
 }
-#line 3415 "src/parser.c"
+#line 3420 "src/parser.c"
     break;
 
   case 85: /* Term: '.' error  */
-#line 691 "src/parser.y"
+#line 696 "src/parser.y"
           {
   FAIL((yyloc), "try .[\"field\"] instead of .field for unusually named fields");
   (yyval.blk) = gen_noop();
 }
-#line 3424 "src/parser.c"
+#line 3429 "src/parser.c"
     break;
 
   case 86: /* Term: '.' IDENT error  */
-#line 695 "src/parser.y"
+#line 700 "src/parser.y"
                 {
   jv_free((yyvsp[-1].literal));
   FAIL((yyloc), "try .[\"field\"] instead of .field for unusually named fields");
   (yyval.blk) = gen_noop();
 }
-#line 3434 "src/parser.c"
+#line 3439 "src/parser.c"
     break;
 
   case 87: /* Term: Term '[' Exp ']' '?'  */
-#line 701 "src/parser.y"
+#line 706 "src/parser.y"
                      {
   (yyval.blk) = gen_index_opt((yyvsp[-4].blk), (yyvsp[-2].blk));
 }
-#line 3442 "src/parser.c"
+#line 3447 "src/parser.c"
     break;
 
   case 88: /* Term: Term '[' Exp ']'  */
-#line 704 "src/parser.y"
+#line 709 "src/parser.y"
                               {
   (yyval.blk) = gen_index((yyvsp[-3].blk), (yyvsp[-1].blk));
 }
-#line 3450 "src/parser.c"
+#line 3455 "src/parser.c"
     break;
 
   case 89: /* Term: Term '.' '[' Exp ']' '?'  */
-#line 707 "src/parser.y"
+#line 712 "src/parser.y"
                          {
   (yyval.blk) = gen_index_opt((yyvsp[-5].blk), (yyvsp[-2].blk));
 }
-#line 3458 "src/parser.c"
+#line 3463 "src/parser.c"
     break;
 
   case 90: /* Term: Term '.' '[' Exp ']'  */
-#line 710 "src/parser.y"
+#line 715 "src/parser.y"
                                   {
   (yyval.blk) = gen_index((yyvsp[-4].blk), (yyvsp[-1].blk));
 }
-#line 3466 "src/parser.c"
+#line 3471 "src/parser.c"
     break;
 
   case 91: /* Term: Term '[' ']' '?'  */
-#line 713 "src/parser.y"
+#line 718 "src/parser.y"
                  {
   (yyval.blk) = block_join((yyvsp[-3].blk), gen_op_simple(EACH_OPT));
 }
-#line 3474 "src/parser.c"
+#line 3479 "src/parser.c"
     break;
 
   case 92: /* Term: Term '[' ']'  */
-#line 716 "src/parser.y"
+#line 721 "src/parser.y"
                           {
   (yyval.blk) = block_join((yyvsp[-2].blk), gen_op_simple(EACH));
 }
-#line 3482 "src/parser.c"
+#line 3487 "src/parser.c"
     break;
 
   case 93: /* Term: Term '.' '[' ']' '?'  */
-#line 719 "src/parser.y"
+#line 724 "src/parser.y"
                      {
   (yyval.blk) = block_join((yyvsp[-4].blk), gen_op_simple(EACH_OPT));
 }
-#line 3490 "src/parser.c"
+#line 3495 "src/parser.c"
     break;
 
   case 94: /* Term: Term '.' '[' ']'  */
-#line 722 "src/parser.y"
+#line 727 "src/parser.y"
                               {
   (yyval.blk) = block_join((yyvsp[-3].blk), gen_op_simple(EACH));
 }
-#line 3498 "src/parser.c"
+#line 3503 "src/parser.c"
     break;
 
   case 95: /* Term: Term '[' Exp ':' Exp ']' '?'  */
-#line 725 "src/parser.y"
+#line 730 "src/parser.y"
                              {
   (yyval.blk) = gen_slice_index((yyvsp[-6].blk), (yyvsp[-4].blk), (yyvsp[-2].blk), INDEX_OPT);
 }
-#line 3506 "src/parser.c"
+#line 3511 "src/parser.c"
     break;
 
   case 96: /* Term: Term '[' Exp ':' ']' '?'  */
-#line 728 "src/parser.y"
+#line 733 "src/parser.y"
                          {
   (yyval.blk) = gen_slice_index((yyvsp[-5].blk), (yyvsp[-3].blk), gen_const(jv_null()), INDEX_OPT);
 }
-#line 3514 "src/parser.c"
+#line 3519 "src/parser.c"
     break;
 
   case 97: /* Term: Term '[' ':' Exp ']' '?'  */
-#line 731 "src/parser.y"
+#line 736 "src/parser.y"
                          {
   (yyval.blk) = gen_slice_index((yyvsp[-5].blk), gen_const(jv_null()), (yyvsp[-2].blk), INDEX_OPT);
 }
-#line 3522 "src/parser.c"
+#line 3527 "src/parser.c"
     break;
 
   case 98: /* Term: Term '[' Exp ':' Exp ']'  */
-#line 734 "src/parser.y"
+#line 739 "src/parser.y"
                                       {
   (yyval.blk) = gen_slice_index((yyvsp[-5].blk), (yyvsp[-3].blk), (yyvsp[-1].blk), INDEX);
 }
-#line 3530 "src/parser.c"
+#line 3535 "src/parser.c"
     break;
 
   case 99: /* Term: Term '[' Exp ':' ']'  */
-#line 737 "src/parser.y"
+#line 742 "src/parser.y"
                                   {
   (yyval.blk) = gen_slice_index((yyvsp[-4].blk), (yyvsp[-2].blk), gen_const(jv_null()), INDEX);
 }
-#line 3538 "src/parser.c"
+#line 3543 "src/parser.c"
     break;
 
   case 100: /* Term: Term '[' ':' Exp ']'  */
-#line 740 "src/parser.y"
+#line 745 "src/parser.y"
                                   {
   (yyval.blk) = gen_slice_index((yyvsp[-4].blk), gen_const(jv_null()), (yyvsp[-1].blk), INDEX);
 }
-#line 3546 "src/parser.c"
+#line 3551 "src/parser.c"
     break;
 
   case 101: /* Term: LITERAL  */
-#line 743 "src/parser.y"
+#line 748 "src/parser.y"
         {
   (yyval.blk) = gen_const((yyvsp[0].literal));
 }
-#line 3554 "src/parser.c"
+#line 3559 "src/parser.c"
     break;
 
   case 102: /* Term: String  */
-#line 746 "src/parser.y"
+#line 751 "src/parser.y"
        {
   (yyval.blk) = (yyvsp[0].blk);
 }
-#line 3562 "src/parser.c"
+#line 3567 "src/parser.c"
     break;
 
   case 103: /* Term: FORMAT  */
-#line 749 "src/parser.y"
+#line 754 "src/parser.y"
        {
   (yyval.blk) = gen_format(gen_noop(), (yyvsp[0].literal));
 }
-#line 3570 "src/parser.c"
+#line 3575 "src/parser.c"
     break;
 
   case 104: /* Term: '(' Exp ')'  */
-#line 752 "src/parser.y"
+#line 757 "src/parser.y"
             {
   (yyval.blk) = (yyvsp[-1].blk);
 }
-#line 3578 "src/parser.c"
+#line 3583 "src/parser.c"
     break;
 
   case 105: /* Term: '[' Exp ']'  */
-#line 755 "src/parser.y"
+#line 760 "src/parser.y"
             {
   (yyval.blk) = gen_collect((yyvsp[-1].blk));
 }
-#line 3586 "src/parser.c"
+#line 3591 "src/parser.c"
     break;
 
   case 106: /* Term: '[' ']'  */
-#line 758 "src/parser.y"
+#line 763 "src/parser.y"
         {
   (yyval.blk) = gen_const(jv_array());
 }
-#line 3594 "src/parser.c"
+#line 3599 "src/parser.c"
     break;
 
   case 107: /* Term: '{' MkDict '}'  */
-#line 761 "src/parser.y"
+#line 766 "src/parser.y"
                {
   block o = gen_const_object((yyvsp[-1].blk));
   if (o.first != NULL)
@@ -3602,37 +3607,37 @@ YYLTYPE yylloc = yyloc_default;
   else
     (yyval.blk) = BLOCK(gen_subexp(gen_const(jv_object())), (yyvsp[-1].blk), gen_op_simple(POP));
 }
-#line 3606 "src/parser.c"
+#line 3611 "src/parser.c"
     break;
 
   case 108: /* Term: '$' '$' '$' BINDING  */
-#line 783 "src/parser.y"
+#line 788 "src/parser.y"
                     {
   (yyval.blk) = gen_location((yyloc), locations, gen_op_unbound(LOADVN, jv_string_value((yyvsp[0].literal))));
   jv_free((yyvsp[0].literal));
 }
-#line 3615 "src/parser.c"
+#line 3620 "src/parser.c"
     break;
 
   case 109: /* Term: BINDING  */
-#line 787 "src/parser.y"
+#line 792 "src/parser.y"
         {
   (yyval.blk) = gen_location((yyloc), locations, gen_op_unbound(LOADV, jv_string_value((yyvsp[0].literal))));
   jv_free((yyvsp[0].literal));
 }
-#line 3624 "src/parser.c"
+#line 3629 "src/parser.c"
     break;
 
   case 110: /* Term: "$__loc__"  */
-#line 791 "src/parser.y"
+#line 796 "src/parser.y"
            {
   (yyval.blk) = gen_loc_object(&(yyloc), locations);
 }
-#line 3632 "src/parser.c"
+#line 3637 "src/parser.c"
     break;
 
   case 111: /* Term: IDENT  */
-#line 794 "src/parser.y"
+#line 799 "src/parser.y"
       {
   const char *s = jv_string_value((yyvsp[0].literal));
   if (strcmp(s, "false") == 0)
@@ -3645,198 +3650,198 @@ YYLTYPE yylloc = yyloc_default;
     (yyval.blk) = gen_location((yyloc), locations, gen_call(s, gen_noop()));
   jv_free((yyvsp[0].literal));
 }
-#line 3649 "src/parser.c"
+#line 3654 "src/parser.c"
     break;
 
   case 112: /* Term: IDENT '(' Args ')'  */
-#line 806 "src/parser.y"
+#line 811 "src/parser.y"
                    {
   (yyval.blk) = gen_call(jv_string_value((yyvsp[-3].literal)), (yyvsp[-1].blk));
   (yyval.blk) = gen_location((yylsp[-3]), locations, (yyval.blk));
   jv_free((yyvsp[-3].literal));
 }
-#line 3659 "src/parser.c"
+#line 3664 "src/parser.c"
     break;
 
   case 113: /* Term: '(' error ')'  */
-#line 811 "src/parser.y"
+#line 816 "src/parser.y"
               { (yyval.blk) = gen_noop(); }
-#line 3665 "src/parser.c"
+#line 3670 "src/parser.c"
     break;
 
   case 114: /* Term: '[' error ']'  */
-#line 812 "src/parser.y"
+#line 817 "src/parser.y"
               { (yyval.blk) = gen_noop(); }
-#line 3671 "src/parser.c"
+#line 3676 "src/parser.c"
     break;
 
   case 115: /* Term: Term '[' error ']'  */
-#line 813 "src/parser.y"
+#line 818 "src/parser.y"
                    { (yyval.blk) = (yyvsp[-3].blk); }
-#line 3677 "src/parser.c"
+#line 3682 "src/parser.c"
     break;
 
   case 116: /* Term: '{' error '}'  */
-#line 814 "src/parser.y"
+#line 819 "src/parser.y"
               { (yyval.blk) = gen_noop(); }
-#line 3683 "src/parser.c"
+#line 3688 "src/parser.c"
     break;
 
   case 117: /* Args: Arg  */
-#line 817 "src/parser.y"
+#line 822 "src/parser.y"
     {
   (yyval.blk) = (yyvsp[0].blk);
 }
-#line 3691 "src/parser.c"
+#line 3696 "src/parser.c"
     break;
 
   case 118: /* Args: Args ';' Arg  */
-#line 820 "src/parser.y"
+#line 825 "src/parser.y"
              {
   (yyval.blk) = BLOCK((yyvsp[-2].blk), (yyvsp[0].blk));
 }
-#line 3699 "src/parser.c"
+#line 3704 "src/parser.c"
     break;
 
   case 119: /* Arg: Exp  */
-#line 825 "src/parser.y"
+#line 830 "src/parser.y"
     {
   (yyval.blk) = gen_lambda((yyvsp[0].blk));
 }
-#line 3707 "src/parser.c"
+#line 3712 "src/parser.c"
     break;
 
   case 120: /* RepPatterns: RepPatterns "?//" Pattern  */
-#line 830 "src/parser.y"
+#line 835 "src/parser.y"
                           {
   (yyval.blk) = BLOCK((yyvsp[-2].blk), gen_destructure_alt((yyvsp[0].blk)));
 }
-#line 3715 "src/parser.c"
+#line 3720 "src/parser.c"
     break;
 
   case 121: /* RepPatterns: Pattern  */
-#line 833 "src/parser.y"
+#line 838 "src/parser.y"
         {
   (yyval.blk) = gen_destructure_alt((yyvsp[0].blk));
 }
-#line 3723 "src/parser.c"
+#line 3728 "src/parser.c"
     break;
 
   case 122: /* Patterns: RepPatterns "?//" Pattern  */
-#line 838 "src/parser.y"
+#line 843 "src/parser.y"
                           {
   (yyval.blk) = BLOCK((yyvsp[-2].blk), (yyvsp[0].blk));
 }
-#line 3731 "src/parser.c"
+#line 3736 "src/parser.c"
     break;
 
   case 123: /* Patterns: Pattern  */
-#line 841 "src/parser.y"
+#line 846 "src/parser.y"
         {
   (yyval.blk) = (yyvsp[0].blk);
 }
-#line 3739 "src/parser.c"
+#line 3744 "src/parser.c"
     break;
 
   case 124: /* Pattern: BINDING  */
-#line 846 "src/parser.y"
+#line 851 "src/parser.y"
         {
   (yyval.blk) = gen_op_unbound(STOREV, jv_string_value((yyvsp[0].literal)));
   jv_free((yyvsp[0].literal));
 }
-#line 3748 "src/parser.c"
+#line 3753 "src/parser.c"
     break;
 
   case 125: /* Pattern: '[' ArrayPats ']'  */
-#line 850 "src/parser.y"
+#line 855 "src/parser.y"
                   {
   (yyval.blk) = BLOCK((yyvsp[-1].blk), gen_op_simple(POP));
 }
-#line 3756 "src/parser.c"
+#line 3761 "src/parser.c"
     break;
 
   case 126: /* Pattern: '{' ObjPats '}'  */
-#line 853 "src/parser.y"
+#line 858 "src/parser.y"
                 {
   (yyval.blk) = BLOCK((yyvsp[-1].blk), gen_op_simple(POP));
 }
-#line 3764 "src/parser.c"
+#line 3769 "src/parser.c"
     break;
 
   case 127: /* ArrayPats: Pattern  */
-#line 858 "src/parser.y"
+#line 863 "src/parser.y"
         {
   (yyval.blk) = gen_array_matcher(gen_noop(), (yyvsp[0].blk));
 }
-#line 3772 "src/parser.c"
+#line 3777 "src/parser.c"
     break;
 
   case 128: /* ArrayPats: ArrayPats ',' Pattern  */
-#line 861 "src/parser.y"
+#line 866 "src/parser.y"
                       {
   (yyval.blk) = gen_array_matcher((yyvsp[-2].blk), (yyvsp[0].blk));
 }
-#line 3780 "src/parser.c"
+#line 3785 "src/parser.c"
     break;
 
   case 129: /* ObjPats: ObjPat  */
-#line 866 "src/parser.y"
+#line 871 "src/parser.y"
        {
   (yyval.blk) = (yyvsp[0].blk);
 }
-#line 3788 "src/parser.c"
+#line 3793 "src/parser.c"
     break;
 
   case 130: /* ObjPats: ObjPats ',' ObjPat  */
-#line 869 "src/parser.y"
+#line 874 "src/parser.y"
                    {
   (yyval.blk) = BLOCK((yyvsp[-2].blk), (yyvsp[0].blk));
 }
-#line 3796 "src/parser.c"
+#line 3801 "src/parser.c"
     break;
 
   case 131: /* ObjPat: BINDING  */
-#line 874 "src/parser.y"
+#line 879 "src/parser.y"
         {
   (yyval.blk) = gen_object_matcher(gen_const((yyvsp[0].literal)), gen_op_unbound(STOREV, jv_string_value((yyvsp[0].literal))));
 }
-#line 3804 "src/parser.c"
+#line 3809 "src/parser.c"
     break;
 
   case 132: /* ObjPat: BINDING ':' Pattern  */
-#line 877 "src/parser.y"
+#line 882 "src/parser.y"
                     {
   (yyval.blk) = gen_object_matcher(gen_const((yyvsp[-2].literal)), BLOCK(gen_op_simple(DUP), gen_op_unbound(STOREV, jv_string_value((yyvsp[-2].literal))), (yyvsp[0].blk)));
 }
-#line 3812 "src/parser.c"
+#line 3817 "src/parser.c"
     break;
 
   case 133: /* ObjPat: IDENT ':' Pattern  */
-#line 880 "src/parser.y"
+#line 885 "src/parser.y"
                   {
   (yyval.blk) = gen_object_matcher(gen_const((yyvsp[-2].literal)), (yyvsp[0].blk));
 }
-#line 3820 "src/parser.c"
+#line 3825 "src/parser.c"
     break;
 
   case 134: /* ObjPat: Keyword ':' Pattern  */
-#line 883 "src/parser.y"
+#line 888 "src/parser.y"
                     {
   (yyval.blk) = gen_object_matcher(gen_const((yyvsp[-2].literal)), (yyvsp[0].blk));
 }
-#line 3828 "src/parser.c"
+#line 3833 "src/parser.c"
     break;
 
   case 135: /* ObjPat: String ':' Pattern  */
-#line 886 "src/parser.y"
+#line 891 "src/parser.y"
                    {
   (yyval.blk) = gen_object_matcher((yyvsp[-2].blk), (yyvsp[0].blk));
 }
-#line 3836 "src/parser.c"
+#line 3841 "src/parser.c"
     break;
 
   case 136: /* ObjPat: '(' Exp ')' ':' Pattern  */
-#line 889 "src/parser.y"
+#line 894 "src/parser.y"
                         {
   jv msg = check_object_key((yyvsp[-3].blk));
   if (jv_is_valid(msg)) {
@@ -3845,269 +3850,269 @@ YYLTYPE yylloc = yyloc_default;
   jv_free(msg);
   (yyval.blk) = gen_object_matcher((yyvsp[-3].blk), (yyvsp[0].blk));
 }
-#line 3849 "src/parser.c"
+#line 3854 "src/parser.c"
     break;
 
   case 137: /* ObjPat: error ':' Pattern  */
-#line 897 "src/parser.y"
+#line 902 "src/parser.y"
                   {
   FAIL((yyloc), "May need parentheses around object key expression");
   (yyval.blk) = (yyvsp[0].blk);
 }
-#line 3858 "src/parser.c"
+#line 3863 "src/parser.c"
     break;
 
   case 138: /* Keyword: "as"  */
-#line 903 "src/parser.y"
+#line 908 "src/parser.y"
      {
   (yyval.literal) = jv_string("as");
 }
-#line 3866 "src/parser.c"
+#line 3871 "src/parser.c"
     break;
 
   case 139: /* Keyword: "def"  */
-#line 906 "src/parser.y"
+#line 911 "src/parser.y"
       {
   (yyval.literal) = jv_string("def");
 }
-#line 3874 "src/parser.c"
+#line 3879 "src/parser.c"
     break;
 
   case 140: /* Keyword: "module"  */
-#line 909 "src/parser.y"
+#line 914 "src/parser.y"
          {
   (yyval.literal) = jv_string("module");
 }
-#line 3882 "src/parser.c"
+#line 3887 "src/parser.c"
     break;
 
   case 141: /* Keyword: "import"  */
-#line 912 "src/parser.y"
+#line 917 "src/parser.y"
          {
   (yyval.literal) = jv_string("import");
 }
-#line 3890 "src/parser.c"
+#line 3895 "src/parser.c"
     break;
 
   case 142: /* Keyword: "include"  */
-#line 915 "src/parser.y"
+#line 920 "src/parser.y"
           {
   (yyval.literal) = jv_string("include");
 }
-#line 3898 "src/parser.c"
+#line 3903 "src/parser.c"
     break;
 
   case 143: /* Keyword: "if"  */
-#line 918 "src/parser.y"
+#line 923 "src/parser.y"
      {
   (yyval.literal) = jv_string("if");
 }
-#line 3906 "src/parser.c"
+#line 3911 "src/parser.c"
     break;
 
   case 144: /* Keyword: "then"  */
-#line 921 "src/parser.y"
+#line 926 "src/parser.y"
        {
   (yyval.literal) = jv_string("then");
 }
-#line 3914 "src/parser.c"
+#line 3919 "src/parser.c"
     break;
 
   case 145: /* Keyword: "else"  */
-#line 924 "src/parser.y"
+#line 929 "src/parser.y"
        {
   (yyval.literal) = jv_string("else");
 }
-#line 3922 "src/parser.c"
+#line 3927 "src/parser.c"
     break;
 
   case 146: /* Keyword: "elif"  */
-#line 927 "src/parser.y"
+#line 932 "src/parser.y"
        {
   (yyval.literal) = jv_string("elif");
 }
-#line 3930 "src/parser.c"
+#line 3935 "src/parser.c"
     break;
 
   case 147: /* Keyword: "reduce"  */
-#line 930 "src/parser.y"
+#line 935 "src/parser.y"
          {
   (yyval.literal) = jv_string("reduce");
 }
-#line 3938 "src/parser.c"
+#line 3943 "src/parser.c"
     break;
 
   case 148: /* Keyword: "foreach"  */
-#line 933 "src/parser.y"
+#line 938 "src/parser.y"
           {
   (yyval.literal) = jv_string("foreach");
 }
-#line 3946 "src/parser.c"
+#line 3951 "src/parser.c"
     break;
 
   case 149: /* Keyword: "end"  */
-#line 936 "src/parser.y"
+#line 941 "src/parser.y"
       {
   (yyval.literal) = jv_string("end");
 }
-#line 3954 "src/parser.c"
+#line 3959 "src/parser.c"
     break;
 
   case 150: /* Keyword: "and"  */
-#line 939 "src/parser.y"
+#line 944 "src/parser.y"
       {
   (yyval.literal) = jv_string("and");
 }
-#line 3962 "src/parser.c"
+#line 3967 "src/parser.c"
     break;
 
   case 151: /* Keyword: "or"  */
-#line 942 "src/parser.y"
+#line 947 "src/parser.y"
      {
   (yyval.literal) = jv_string("or");
 }
-#line 3970 "src/parser.c"
+#line 3975 "src/parser.c"
     break;
 
   case 152: /* Keyword: "try"  */
-#line 945 "src/parser.y"
+#line 950 "src/parser.y"
       {
   (yyval.literal) = jv_string("try");
 }
-#line 3978 "src/parser.c"
+#line 3983 "src/parser.c"
     break;
 
   case 153: /* Keyword: "catch"  */
-#line 948 "src/parser.y"
+#line 953 "src/parser.y"
         {
   (yyval.literal) = jv_string("catch");
 }
-#line 3986 "src/parser.c"
+#line 3991 "src/parser.c"
     break;
 
   case 154: /* Keyword: "label"  */
-#line 951 "src/parser.y"
+#line 956 "src/parser.y"
         {
   (yyval.literal) = jv_string("label");
 }
-#line 3994 "src/parser.c"
+#line 3999 "src/parser.c"
     break;
 
   case 155: /* Keyword: "break"  */
-#line 954 "src/parser.y"
+#line 959 "src/parser.y"
         {
   (yyval.literal) = jv_string("break");
 }
-#line 4002 "src/parser.c"
+#line 4007 "src/parser.c"
     break;
 
   case 156: /* MkDict: %empty  */
-#line 959 "src/parser.y"
+#line 964 "src/parser.y"
        {
   (yyval.blk)=gen_noop();
 }
-#line 4010 "src/parser.c"
+#line 4015 "src/parser.c"
     break;
 
   case 157: /* MkDict: MkDictPair  */
-#line 962 "src/parser.y"
+#line 967 "src/parser.y"
             { (yyval.blk) = (yyvsp[0].blk); }
-#line 4016 "src/parser.c"
+#line 4021 "src/parser.c"
     break;
 
   case 158: /* MkDict: MkDictPair ',' MkDict  */
-#line 963 "src/parser.y"
+#line 968 "src/parser.y"
                         { (yyval.blk)=block_join((yyvsp[-2].blk), (yyvsp[0].blk)); }
-#line 4022 "src/parser.c"
+#line 4027 "src/parser.c"
     break;
 
   case 159: /* MkDict: error ',' MkDict  */
-#line 964 "src/parser.y"
+#line 969 "src/parser.y"
                    { (yyval.blk) = (yyvsp[0].blk); }
-#line 4028 "src/parser.c"
+#line 4033 "src/parser.c"
     break;
 
   case 160: /* MkDictPair: IDENT ':' ExpD  */
-#line 967 "src/parser.y"
+#line 972 "src/parser.y"
                {
   (yyval.blk) = gen_dictpair(gen_const((yyvsp[-2].literal)), (yyvsp[0].blk));
  }
-#line 4036 "src/parser.c"
+#line 4041 "src/parser.c"
     break;
 
   case 161: /* MkDictPair: Keyword ':' ExpD  */
-#line 970 "src/parser.y"
+#line 975 "src/parser.y"
                    {
   (yyval.blk) = gen_dictpair(gen_const((yyvsp[-2].literal)), (yyvsp[0].blk));
   }
-#line 4044 "src/parser.c"
+#line 4049 "src/parser.c"
     break;
 
   case 162: /* MkDictPair: String ':' ExpD  */
-#line 973 "src/parser.y"
+#line 978 "src/parser.y"
                   {
   (yyval.blk) = gen_dictpair((yyvsp[-2].blk), (yyvsp[0].blk));
   }
-#line 4052 "src/parser.c"
+#line 4057 "src/parser.c"
     break;
 
   case 163: /* MkDictPair: String  */
-#line 976 "src/parser.y"
+#line 981 "src/parser.y"
          {
   (yyval.blk) = gen_dictpair((yyvsp[0].blk), BLOCK(gen_op_simple(POP), gen_op_simple(DUP2),
                               gen_op_simple(DUP2), gen_op_simple(INDEX)));
   }
-#line 4061 "src/parser.c"
+#line 4066 "src/parser.c"
     break;
 
   case 164: /* MkDictPair: BINDING ':' ExpD  */
-#line 980 "src/parser.y"
+#line 985 "src/parser.y"
                    {
   (yyval.blk) = gen_dictpair(gen_location((yyloc), locations, gen_op_unbound(LOADV, jv_string_value((yyvsp[-2].literal)))),
                     (yyvsp[0].blk));
   jv_free((yyvsp[-2].literal));
   }
-#line 4071 "src/parser.c"
+#line 4076 "src/parser.c"
     break;
 
   case 165: /* MkDictPair: BINDING  */
-#line 985 "src/parser.y"
+#line 990 "src/parser.y"
           {
   (yyval.blk) = gen_dictpair(gen_const((yyvsp[0].literal)),
                     gen_location((yyloc), locations, gen_op_unbound(LOADV, jv_string_value((yyvsp[0].literal)))));
   }
-#line 4080 "src/parser.c"
+#line 4085 "src/parser.c"
     break;
 
   case 166: /* MkDictPair: IDENT  */
-#line 989 "src/parser.y"
+#line 994 "src/parser.y"
         {
   (yyval.blk) = gen_dictpair(gen_const(jv_copy((yyvsp[0].literal))),
                     gen_index(gen_noop(), gen_const((yyvsp[0].literal))));
   }
-#line 4089 "src/parser.c"
+#line 4094 "src/parser.c"
     break;
 
   case 167: /* MkDictPair: "$__loc__"  */
-#line 993 "src/parser.y"
+#line 998 "src/parser.y"
              {
   (yyval.blk) = gen_dictpair(gen_const(jv_string("__loc__")),
                     gen_loc_object(&(yyloc), locations));
   }
-#line 4098 "src/parser.c"
+#line 4103 "src/parser.c"
     break;
 
   case 168: /* MkDictPair: Keyword  */
-#line 997 "src/parser.y"
+#line 1002 "src/parser.y"
           {
   (yyval.blk) = gen_dictpair(gen_const(jv_copy((yyvsp[0].literal))),
                     gen_index(gen_noop(), gen_const((yyvsp[0].literal))));
   }
-#line 4107 "src/parser.c"
+#line 4112 "src/parser.c"
     break;
 
   case 169: /* MkDictPair: '(' Exp ')' ':' ExpD  */
-#line 1001 "src/parser.y"
+#line 1006 "src/parser.y"
                        {
   jv msg = check_object_key((yyvsp[-3].blk));
   if (jv_is_valid(msg)) {
@@ -4116,20 +4121,20 @@ YYLTYPE yylloc = yyloc_default;
   jv_free(msg);
   (yyval.blk) = gen_dictpair((yyvsp[-3].blk), (yyvsp[0].blk));
   }
-#line 4120 "src/parser.c"
+#line 4125 "src/parser.c"
     break;
 
   case 170: /* MkDictPair: error ':' ExpD  */
-#line 1009 "src/parser.y"
+#line 1014 "src/parser.y"
                  {
   FAIL((yyloc), "May need parentheses around object key expression");
   (yyval.blk) = (yyvsp[0].blk);
   }
-#line 4129 "src/parser.c"
+#line 4134 "src/parser.c"
     break;
 
 
-#line 4133 "src/parser.c"
+#line 4138 "src/parser.c"
 
       default: break;
     }
@@ -4358,7 +4363,7 @@ YYLTYPE yylloc = yyloc_default;
   return yyresult;
 }
 
-#line 1013 "src/parser.y"
+#line 1018 "src/parser.y"
 
 
 int jq_parse(struct locfile* locations, block* answer) {
diff --git a/src/parser.y b/src/parser.y
index f23f5953de..94ede7c4ab 100644
--- a/src/parser.y
+++ b/src/parser.y
@@ -230,7 +230,16 @@ static block constant_fold(block a, block b, int op) {
     case '+': res = jv_number(na + nb); break;
     case '-': res = jv_number(na - nb); break;
     case '*': res = jv_number(na * nb); break;
-    case '/': res = jv_number(na / nb); break;
+    case '/':
+      if (nb == 0.0) return gen_noop();
+      res = jv_number(na / nb);
+      break;
+    case '%':
+#define is_unsafe_to_int_cast(n) (isnan(n) || (n) < INTMAX_MIN || -(n) < INTMAX_MIN)
+      if (is_unsafe_to_int_cast(na) || is_unsafe_to_int_cast(nb) || (intmax_t)nb == 0) return gen_noop();
+#undef is_unsafe_to_int_cast
+      res = jv_number((intmax_t)na % (intmax_t)nb);
+      break;
     case EQ:  res = (cmp == 0 ? jv_true() : jv_false()); break;
     case NEQ: res = (cmp != 0 ? jv_true() : jv_false()); break;
     case '<': res = (cmp < 0 ? jv_true() : jv_false()); break;
@@ -463,14 +472,10 @@ Exp "*=" Exp {
 
 Exp '/' Exp {
   $$ = gen_binop($1, $3, '/');
-  if (block_is_const_inf($$))
-    FAIL(@$, "Division by zero?");
 } |
 
 Exp '%' Exp {
   $$ = gen_binop($1, $3, '%');
-  if (block_is_const_inf($$))
-    FAIL(@$, "Remainder by zero?");
 } |
 
 Exp "/=" Exp {


Test patch:
diff --git a/tests/jq.test b/tests/jq.test
index 55630a5a43..73cf93d02c 100644
--- a/tests/jq.test
+++ b/tests/jq.test
@@ -556,9 +556,13 @@ null
 null
 172
 
-[(infinite, -infinite) % (1, -1)]
+[(infinite, -infinite) % (1, -1, infinite)]
 null
-[0,0,0,0]
+[0,0,0,0,0,-1]
+
+[nan % 1, 1 % nan | isnan]
+null
+[true,true]
 
 1 + tonumber + ("10" | tonumber)
 4
@@ -1664,18 +1668,21 @@ try (1/.) catch .
 0
 "number (1) and number (0) cannot be divided because the divisor is zero"
 
-0/0, (0 as $x | $x/0) | isnan
+try (1/0) catch .
 0
-true
-true
+"number (1) and number (0) cannot be divided because the divisor is zero"
+
+try (0/0) catch .
+0
+"number (0) and number (0) cannot be divided because the divisor is zero"
 
 try (1%.) catch .
 0
 "number (1) and number (0) cannot be divided (remainder) because the divisor is zero"
 
-%%FAIL
-1/0
-jq: error: Division by zero? at <top-level>, line 1:
+try (1%0) catch .
+0
+"number (1) and number (0) cannot be divided (remainder) because the divisor is zero"
 
 # Basic numbers tests: integers, powers of two
 [range(-52;52;1)] as $powers | [$powers[]|pow(2;.)|log2|round] == $powers
diff --git a/tests/shtest b/tests/shtest
index a644b5742d..3ed22c3001 100755
--- a/tests/shtest
+++ b/tests/shtest
@@ -30,47 +30,67 @@ $VALGRIND $Q $JQ -Rne '[inputs] == ["a\u0000b", "c\u0000d", "e"]' $d/input
 
 # String constant folding (addition only)
 nref=$($VALGRIND $Q $JQ -n --debug-dump-disasm '"foo"' | wc -l)
+n=$($VALGRIND $Q $JQ -n --debug-dump-disasm '"foo" + "bar"' | wc -l)
+if [ $n -ne $nref ]; then
+  echo "Constant expression folding for strings didn't work"
+  exit 1
+fi
 
-# Numeric constant folding (not all ops yet)
+# Numeric constant folding (binary operators)
 n=$($VALGRIND $Q $JQ -n --debug-dump-disasm '1+1' | wc -l)
 if [ $n -ne $nref ]; then
-    echo "Constant expression folding for strings didn't work"
-    exit 1
+  echo "Constant expression folding for numbers didn't work"
+  exit 1
 fi
 n=$($VALGRIND $Q $JQ -n --debug-dump-disasm '1-1' | wc -l)
 if [ $n -ne $nref ]; then
-    echo "Constant expression folding for strings didn't work"
-    exit 1
+  echo "Constant expression folding for numbers didn't work"
+  exit 1
 fi
 n=$($VALGRIND $Q $JQ -n --debug-dump-disasm '2*3' | wc -l)
 if [ $n -ne $nref ]; then
-    echo "Constant expression folding for strings didn't work"
-    exit 1
+  echo "Constant expression folding for numbers didn't work"
+  exit 1
 fi
 n=$($VALGRIND $Q $JQ -n --debug-dump-disasm '9/3' | wc -l)
 if [ $n -ne $nref ]; then
-    echo "Constant expression folding for strings didn't work"
-    exit 1
+  echo "Constant expression folding for numbers didn't work"
+  exit 1
+fi
+n=$($VALGRIND $Q $JQ -n --debug-dump-disasm '9%3' | wc -l)
+if [ $n -ne $nref ]; then
+  echo "Constant expression folding for numbers didn't work"
+  exit 1
 fi
 n=$($VALGRIND $Q $JQ -n --debug-dump-disasm '9==3' | wc -l)
 if [ $n -ne $nref ]; then
-    echo "Constant expression folding for strings didn't work"
-    exit 1
+  echo "Constant expression folding for numbers didn't work"
+  exit 1
 fi
 n=$($VALGRIND $Q $JQ -n --debug-dump-disasm '9!=3' | wc -l)
 if [ $n -ne $nref ]; then
-    echo "Constant expression folding for strings didn't work"
-    exit 1
+  echo "Constant expression folding for numbers didn't work"
+  exit 1
+fi
+n=$($VALGRIND $Q $JQ -n --debug-dump-disasm '9<3' | wc -l)
+if [ $n -ne $nref ]; then
+  echo "Constant expression folding for numbers didn't work"
+  exit 1
+fi
+n=$($VALGRIND $Q $JQ -n --debug-dump-disasm '9>3' | wc -l)
+if [ $n -ne $nref ]; then
+  echo "Constant expression folding for numbers didn't work"
+  exit 1
 fi
 n=$($VALGRIND $Q $JQ -n --debug-dump-disasm '9<=3' | wc -l)
 if [ $n -ne $nref ]; then
-    echo "Constant expression folding for strings didn't work"
-    exit 1
+  echo "Constant expression folding for numbers didn't work"
+  exit 1
 fi
 n=$($VALGRIND $Q $JQ -n --debug-dump-disasm '9>=3' | wc -l)
 if [ $n -ne $nref ]; then
-    echo "Constant expression folding for strings didn't work"
-    exit 1
+  echo "Constant expression folding for numbers didn't work"
+  exit 1
 fi
 
 ## Test JSON sequence support