From 47489ab9c4c4ab67edd4886aceccd2cc34fa5071 Mon Sep 17 00:00:00 2001 From: Mattias Wadman Date: Mon, 28 Oct 2024 09:30:27 +0100 Subject: [PATCH 01/18] Add jaq support Useful snippets and references: jaq -L . -n 'include "jqjq"; _builtins_src | lex' jaq -n -L . -rRs 'include "jqjq"; . | jqjq(["", "--run-tests"]; {})' < jqjq.test jaq -n -L . 'include "jqjq"; eval("1+2")' Disable TCO https://github.com/01mf02/jaq/pull/223#discussion_r1829094617 --- README.md | 3 +- jqjq.jq | 95 +++++++++++++++++++++++++++++++++++-------------------- 2 files changed, 62 insertions(+), 36 deletions(-) diff --git a/README.md b/README.md index e90cd59..cf59c98 100644 --- a/README.md +++ b/README.md @@ -242,8 +242,7 @@ Note that the tests are meant to be used with jq 1.7.1. - [x] gojq - [x] jqjq - Used to work but runs out of memory on my laptop - - [ ] jaq - - Fails on missing destructing support + - [x] jaq - [ ] xq - Fails on missing `debug`, maths, jqjq `lex` returns empty (regexp issues?) - [x] Bugs diff --git a/jqjq.jq b/jqjq.jq index e0e2cc8..3281583 100644 --- a/jqjq.jq +++ b/jqjq.jq @@ -153,8 +153,8 @@ def lex: // _re("^\\)"; {rparen: ., string_stack: ($string_stack[0:-1])}) // _re("^\\["; {lsquare: .}) // _re("^\\]"; {rsquare: .}) - // _re("^{"; {lcurly: .}) - // _re("^}"; {rcurly: .}) + // _re("^\\{"; {lcurly: .}) + // _re("^\\}"; {rcurly: .}) // _re("^\\.\\."; {dotdot: .}) // _re("^\\."; {dot: .}) // _re("^\\?"; {qmark: .}) @@ -173,7 +173,9 @@ def lex: [_lex]; def parse: - def _consume(f): select(.[0] | f) | .[1:]; + # TODO: jaq null index + # TODO: maybe a consume helper that returns first? ala [$rest, $v]? + def _consume(f): select(length > 0 and (.[0] | f)) | .[1:]; def _optional(f): ( f // [., null] @@ -200,7 +202,9 @@ def parse: # filter is used to disable operators, ex in keyval query def _op_prec_climb($p; filter): def _ops: - if filter then false + # TODO: jaq: nice way? don't even call _ops for null case? + if . == null then false + elif filter then false elif .pipe then {prec: 0, name: "|", assoc: "right"} # TODO: understand why jq has left associativity for "," but right seems to give correct parse tree elif .comma then {prec: 1, name: ",", assoc: "right"} @@ -956,7 +960,9 @@ def parse: # "abc \(123)" def _string_query: - ( .[0] as {$string_start} + # TODO: jaq null index (consume checks length and outoput [value, rest]?) + ( select(length > 0) + | .[0] as {$string_start} | _consume(.string_start) | _repeat( ( select(length > 0 ) # make sure there is something @@ -1060,7 +1066,7 @@ def parse: ] ); - ( .# debug({_p: $type}) + ( . #debug({_p: $type, dot: .}) | if $type == "query" then _op_prec_climb(0; false) elif $type == "keyval_query" then @@ -1267,8 +1273,12 @@ def func_defs_to_env($env): def eval_ast($query; $path; $env; undefined_func): def _e($query; $path; $env): - ( .#| debug({c: ., $query, $path, $env}) - | $query as + ( . # debug({c: ., $query, $path, $env}) + | ( $query + # TODO: jaq: destruct null index + | if .term == null then .term = {} end + | if .term.suffix_list == null then .term.suffix_list = [{}] end + ) as { term: { type: $type , suffix_list: [{$optional}] @@ -1417,7 +1427,17 @@ def eval_ast($query; $path; $env; undefined_func): # .name def _index: - _e_index($query.term.index; $path; .; $query.term.suffix_list[0].optional); + _e_index( + $query.term.index; + $path; + .; + # TODO: jaq null index + ( $query.term.suffix_list + | if . then .[0].optional + else false + end + ) + ); def _func: def _fromjson: @@ -1454,8 +1474,9 @@ def eval_ast($query; $path; $env; undefined_func): | $query.term.func as {$name, $args} | func_name($name; $args) as $name | $query_env[$name] as $e - | if $e | has("value") then [[null], $e.value] - elif $e.body then + # TODO: jaq null index and null has() + | if $e != null and ($e | has("value")) then [[null], $e.value] + elif $e != null and $e.body then ( ($e.args // []) as $func_args | ($args // []) as $call_args | ( $func_args @@ -1519,10 +1540,10 @@ def eval_ast($query; $path; $env; undefined_func): ( a0 as $a0 | [[null], has($a0)] ) - elif $name == "delpaths/1" then - ( a0 as $a0 - | [[null], delpaths($a0)] - ) + # elif $name == "delpaths/1" then + # ( a0 as $a0 + # | [[null], delpaths($a0)] + # ) elif $name == "explode/0" then [[null], explode] elif $name == "implode/0" then [[null], implode] elif $name == "tonumber/0" then [[null], tonumber] @@ -1539,19 +1560,20 @@ def eval_ast($query; $path; $env; undefined_func): | error($a0) ) elif $name == "halt_error/1" then [[null], halt_error(a0)] - elif $name == "getpath/1" then - ( a0 as $a0 - | [ $path+$a0 - , getpath($a0) - ] - ) - elif $name == "setpath/2" then - ( a0 as $a0 - | a1 as $a1 - | [ [] - , setpath($a0; $a1) - ] - ) + # TODO: jaq: setpath/getpath missing + # elif $name == "getpath/1" then + # ( a0 as $a0 + # | [ $path+$a0 + # , getpath($a0) + # ] + # ) + # elif $name == "setpath/2" then + # ( a0 as $a0 + # | a1 as $a1 + # | [ [] + # , setpath($a0; $a1) + # ] + # ) elif $name == "path/1" then ( _e($args[0]; []; $query_env) as [$p, $_v] # TODO: try/catch error @@ -1903,7 +1925,7 @@ def eval_ast($query; $path; $env; undefined_func): # .a.b?? case, just skip extra optional "?" elif $suffix_list[0].optional then _f($suffix_list[1:]) else - ( $suffix_list[1].optional as $opt + ( ($suffix_list[1] // {}).optional as $opt # TOOO: jaq null index | $suffix_list[if $opt then 2 else 1 end:] as $n | _e_suffix($suffix_list[0]; $path; $input; $opt) | _f($n) @@ -2541,10 +2563,12 @@ def eval($expr; $globals; $builtins_env): # TODO: does not work with jq yet because issue with bind patterns # $ gojq -cn -L . 'include "jqjq"; {} | {a:1} | eval(".a") += 1' # {"a":2} - | if $path | . == [] or . == [null] then $value - else getpath($path) - end - ); + | $value + ); + # | if $path | . == [] or . == [null] then $value + # else getpath($path) + # end + # ); def eval($expr): eval($expr; {}; _builtins_env); @@ -2924,7 +2948,10 @@ def jqjq($args; $env): ) ]; def _f: - def _to_unslurped: map(tojson) | join(","); + # TODO: jaq: [] | join("") -> null + # TODO: jaq: join works with more than strings + def _join($s): if . == [] then "" else join($s) end; + def _to_unslurped: map(tojson) | _join(","); ( _builtins_env as $builtins_env | _from_jqtest[] | . as $c From a0d4d4648f87dab3612d4ddf40a5d6757461aba7 Mon Sep 17 00:00:00 2001 From: Mattias Wadman Date: Sat, 9 Nov 2024 17:27:11 +0100 Subject: [PATCH 02/18] Add -r when using -j (is implied for jq) --- jqjq.jq | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/jqjq.jq b/jqjq.jq index 3281583..5116dbb 100644 --- a/jqjq.jq +++ b/jqjq.jq @@ -2768,8 +2768,9 @@ def invoke_client_jqjq: | parse_options | [ (.jq // "jq" | sh_escape) , if .action == "run-tests" then "-nsRr" - elif .mode == "repl" then "-njR" - else "-nj" + # TODO: jaq: -j does not imply -r + elif .mode == "repl" then "-njrR" + else "-njr" end , if .unbuffered_output and (.jq == null or (.jq | test("(^|[/\\\\])(gojq|jaq)[^/\\\\]*$") | not)) then From 1f9987edacd4a74d4bacb8bdd35a7cc283542f57 Mon Sep 17 00:00:00 2001 From: Mattias Wadman Date: Sat, 9 Nov 2024 17:51:45 +0100 Subject: [PATCH 03/18] _alt foreach workaround --- jqjq.jq | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/jqjq.jq b/jqjq.jq index 5116dbb..c74c85c 100644 --- a/jqjq.jq +++ b/jqjq.jq @@ -2087,15 +2087,23 @@ def _alt(lhs; $op; rhs): 0; if $v[0] == \"lhs\" then if $v[1] then . + 1 - else empty + else . end elif $v[0] == \"end\" then if . > 0 then error($b) - else empty + else . end else . end; - $v + # TODO: jaq: foreach empty update backtracks + if $v[0] == \"lhs\" then + if $v[1] then $v + else empty + end + elif $v[0] == \"end\" then + empty + else $v + end ) | .[1] ) From ef8092819c757648ee724fe4a7eaeda4b1a1182e Mon Sep 17 00:00:00 2001 From: Mattias Wadman Date: Sat, 9 Nov 2024 18:01:37 +0100 Subject: [PATCH 04/18] More null index --- jqjq.jq | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/jqjq.jq b/jqjq.jq index c74c85c..fb18515 100644 --- a/jqjq.jq +++ b/jqjq.jq @@ -1670,8 +1670,7 @@ def eval_ast($query; $path; $env; undefined_func): # based on an earlier object, for that output combination def _object: ( . as $input - | $query.term.object as {$key_vals} - | $key_vals + | $query.term.object.key_vals | map( ( def _term_str: { term: @@ -1685,7 +1684,11 @@ def eval_ast($query; $path; $env; undefined_func): | if startswith("$") then .[1:] else . end | _term_str ) - , ( $kv.val.queries[0] + , ( # TOOD: jaq: null index + ( $kv.val + | values + | .queries[0] + ) // ( $kv.key | if startswith("$") then @@ -1708,7 +1711,11 @@ def eval_ast($query; $path; $env; undefined_func): ( [ ( $kv.key_string.str | _term_str ) - , ( $kv.val.queries[0] + , ( # TODO: jaq: null index + ( $kv.val + | values + | .queries[0] + ) // { term: { type: "TermTypeIndex" From e61b372e81d67bea67db809156b99fe574162cfe Mon Sep 17 00:00:00 2001 From: Mattias Wadman Date: Sat, 9 Nov 2024 18:09:14 +0100 Subject: [PATCH 05/18] .123 is not valid json --- jqjq.jq | 2 +- jqjq.test | 4 +--- 2 files changed, 2 insertions(+), 4 deletions(-) diff --git a/jqjq.jq b/jqjq.jq index fb18515..f68ac71 100644 --- a/jqjq.jq +++ b/jqjq.jq @@ -101,7 +101,7 @@ def lex: // _re("^@[_a-zA-Z][_a-zA-Z0-9]*"; {at_ident: .}) // _re("^\\$[_a-zA-Z][_a-zA-Z0-9]*"; {binding: .}) # 1.23, .123, 123e2, 1.23e2, 123E2, 1.23e+2, 1.23E-2 or 123 - // _re("^(?:[0-9]*\\.[0-9]+|[0-9]+)(?:[eE][-\\+]?[0-9]+)?"; {number: .}) + // _re("^(?:[0-9]+\\.[0-9]+|[0-9]+)(?:[eE][-\\+]?[0-9]+)?"; {number: .}) // _re("^\"(?:[^\"\\\\]|\\\\.)*?\\\\\\("; ( .[1:-2] | _unescape diff --git a/jqjq.test b/jqjq.test index be88f04..8189402 100644 --- a/jqjq.test +++ b/jqjq.test @@ -11,13 +11,11 @@ null null # number -123, 1.23, 0.123, .123, .123e1, 1.23e1, 2E3, 1.23e+1, 1.23E-2 +123, 1.23, 0.123, 1.23e1, 2E3, 1.23e+1, 1.23E-2 null 123 1.23 0.123 -0.123 -1.23 12.3 2000 12.3 From b941f9f36a6f80222b53722f0a7b12592e71bd50 Mon Sep 17 00:00:00 2001 From: Mattias Wadman Date: Sun, 10 Nov 2024 16:59:39 +0100 Subject: [PATCH 06/18] More null index --- jqjq.jq | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/jqjq.jq b/jqjq.jq index f68ac71..80ba285 100644 --- a/jqjq.jq +++ b/jqjq.jq @@ -979,7 +979,8 @@ def parse: , queries: [ {term: {str: $string_start, type: "TermTypeString"}} , ( $queries[] - | if .term.type == "TermTypeString" then . + # TODO: jaq: null index + | if .term and .term.type == "TermTypeString" then . else {term: {query: ., type: "TermTypeQuery"}} end ) From 4d56fa2b13ffbab12cea9dea5e59c7dc9a430f1f Mon Sep 17 00:00:00 2001 From: Mattias Wadman Date: Wed, 13 Nov 2024 11:30:30 +0100 Subject: [PATCH 07/18] Use floor instead of trunc to get an integer --- jqjq.jq | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/jqjq.jq b/jqjq.jq index 80ba285..b8947eb 100644 --- a/jqjq.jq +++ b/jqjq.jq @@ -2471,15 +2471,15 @@ def _utf8_bytes: . elif . < 2048 then # 2-byte = 110xxxxx 10xxxxxx - ( ((. / 64) | trunc) as $x1 + ( ((. / 64) | floor) as $x1 | (. - ($x1 * 64)) as $x0 | 192 + $x1 , 128 + $x0 ) elif . < 65536 then # 3-byte = 1110xxxx 10xxxxxx 10xxxxxx - ( ((. / 4096) | trunc) as $x2 - | ((. - ($x2 * 4096)) / 64 | trunc) as $x1 + ( ((. / 4096) | floor) as $x2 + | ((. - ($x2 * 4096)) / 64 | floor) as $x1 | (. - ($x2 * 4096) - ($x1 * 64)) as $x0 | 224 + $x2 , 128 + $x1 @@ -2487,9 +2487,9 @@ def _utf8_bytes: ) else # 4-byte = 11110xxx 10xxxxxx 10xxxxxx 10xxxxxx - ( ((. / 262144) | trunc) as $x3 - | ((. - ($x3 * 262144)) / 4096 | trunc) as $x2 - | ((. - ($x3 * 262144) - ($x2 * 4096)) / 64 | trunc) as $x1 + ( ((. / 262144) | floor) as $x3 + | ((. - ($x3 * 262144)) / 4096 | floor) as $x2 + | ((. - ($x3 * 262144) - ($x2 * 4096)) / 64 | floor) as $x1 | (. - ($x3 * 262144) - ($x2 * 4096) - ($x1 * 64)) as $x0 | 240 + $x3 , 128 + $x2 @@ -2506,7 +2506,7 @@ def _format_uri: .c | [ _utf8_bytes[] | 37 # % - , (((. / 16) | trunc) | _hex) + , (((. / 16) | floor) | _hex) , ((. % 16) | _hex) ] | implode From 87a30725a670d8257783ea51e66326f2b66511de Mon Sep 17 00:00:00 2001 From: Mattias Wadman Date: Thu, 14 Nov 2024 00:06:08 +0100 Subject: [PATCH 08/18] Add own setpath/getpath --- jqjq.jq | 75 ++++++++++++++++++++++++++++++++++++++++++------------- jqjq.test | 2 +- 2 files changed, 59 insertions(+), 18 deletions(-) diff --git a/jqjq.jq b/jqjq.jq index b8947eb..95f24fb 100644 --- a/jqjq.jq +++ b/jqjq.jq @@ -1273,6 +1273,39 @@ def func_defs_to_env($env): ); def eval_ast($query; $path; $env; undefined_func): + def _setpath($path; $v): + def _f($p): + if $p | length == 0 then + $v + else + ( $p[0] as $p0 + | ($p0 | type) as $t + #| debug({type: type, $t, $p0, $t, dot: .}) + | if . == null then + if $t == "number" then + [range($p0+1) | null] + else {} + end + end + | if (type == "object" and $t == "string") or + (type == "array" and $t == "number") then + .[$p0] |= _f($p[1:]) + else + error("cannot index \(type) with \($t) \($p0)") + end + ) + end; + _f($path); + + def _getpath($path): + reduce $path[] as $p ( + .; + #debug({dot: ., $p}) | + if . == null then null + else .[$p] + end + ); + def _e($query; $path; $env): ( . # debug({c: ., $query, $path, $env}) | ( $query @@ -1295,14 +1328,20 @@ def eval_ast($query; $path; $env; undefined_func): # eval a index, is also used by _e_suffix def _e_index($index; $query_path; $query_input; $opt): try - ( . as $input - | $index as + ( $index as { $name , $str , $is_slice , $start , end: $end_ } + | ( # TODO: jaq: allow null index + if . == null then + if $name then if $name | type == "string" then {} else [] end + elif $str then {} + end + end + ) as $input | if $name then [($query_path + [$name]), $input[$name]] elif $str then [($query_path + [$str.str]), $input[$str.str]] elif $is_slice then @@ -1322,7 +1361,10 @@ def eval_ast($query; $path; $env; undefined_func): ( $query_input | _e($start; []; $query_env) as [$_, $v] | [ ($query_path + [$v]) - , $input[$v] + , # TODO: jaq: allow null index + ( $input + | if . != null then .[$v] end + ) ] ) else . # TODO: error? @@ -1561,20 +1603,19 @@ def eval_ast($query; $path; $env; undefined_func): | error($a0) ) elif $name == "halt_error/1" then [[null], halt_error(a0)] - # TODO: jaq: setpath/getpath missing - # elif $name == "getpath/1" then - # ( a0 as $a0 - # | [ $path+$a0 - # , getpath($a0) - # ] - # ) - # elif $name == "setpath/2" then - # ( a0 as $a0 - # | a1 as $a1 - # | [ [] - # , setpath($a0; $a1) - # ] - # ) + elif $name == "getpath/1" then + ( a0 as $a0 + | [ $path+$a0 + , _getpath($a0) + ] + ) + elif $name == "setpath/2" then + ( a0 as $a0 + | a1 as $a1 + | [ [] + , _setpath($a0; $a1) + ] + ) elif $name == "path/1" then ( _e($args[0]; []; $query_env) as [$p, $_v] # TODO: try/catch error diff --git a/jqjq.test b/jqjq.test index 8189402..5a58957 100644 --- a/jqjq.test +++ b/jqjq.test @@ -692,7 +692,7 @@ try error(null) catch . 123 null -try (1+"a") catch if . == "number (1) and string (\"a\") cannot be added" or . == "cannot add: number (1) and string (\"a\")" then "expected add error" else . end +try (1+"a") catch if . == "number (1) and string (\"a\") cannot be added" or . == "cannot add: number (1) and string (\"a\")" or . == "cannot calculate 1 + \"a\"" then "expected add error" else . end null "expected add error" From 45016e6463207fff35cabf94a8dcd3eb935ee87a Mon Sep 17 00:00:00 2001 From: Mattias Wadman Date: Fri, 15 Nov 2024 14:19:39 +0100 Subject: [PATCH 09/18] add 01mf02's delpaths --- jqjq.jq | 17 +++++++++++++---- 1 file changed, 13 insertions(+), 4 deletions(-) diff --git a/jqjq.jq b/jqjq.jq index 95f24fb..6f57f56 100644 --- a/jqjq.jq +++ b/jqjq.jq @@ -1306,6 +1306,15 @@ def eval_ast($query; $path; $env; undefined_func): end ); + def _delpaths($paths): + def _delpath($p): + if $p == [] then empty + elif has($p[0]) then .[$p[0]] |= _delpath($p[1:]) + else . + end; + reduce ($paths | unique | reverse)[] as $p + (.; _delpath($p)); + def _e($query; $path; $env): ( . # debug({c: ., $query, $path, $env}) | ( $query @@ -1583,10 +1592,10 @@ def eval_ast($query; $path; $env; undefined_func): ( a0 as $a0 | [[null], has($a0)] ) - # elif $name == "delpaths/1" then - # ( a0 as $a0 - # | [[null], delpaths($a0)] - # ) + elif $name == "delpaths/1" then + ( a0 as $a0 + | [[null], _delpaths($a0)] + ) elif $name == "explode/0" then [[null], explode] elif $name == "implode/0" then [[null], implode] elif $name == "tonumber/0" then [[null], tonumber] From 6f126792b5bda54eefef7e7fe5989bcc7e68f3a5 Mon Sep 17 00:00:00 2001 From: Mattias Wadman Date: Tue, 19 Nov 2024 10:37:45 +0100 Subject: [PATCH 10/18] Don't rely on null | halt_error for no output --- jqjq.jq | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/jqjq.jq b/jqjq.jq index 6f57f56..73f5f5e 100644 --- a/jqjq.jq +++ b/jqjq.jq @@ -3100,7 +3100,7 @@ def jqjq($args; $env): ) | if .end then ( "\(.oks) of \(.oks + .errors) tests passed" - , if .errors > 0 then null | halt_error(1) else empty end + , if .errors > 0 then "" | halt_error(1) else empty end ) elif .line then .line else empty From 9d00fb2d9361b05f6036440d7eed2536b6289f9b Mon Sep 17 00:00:00 2001 From: Mattias Wadman Date: Tue, 19 Nov 2024 11:37:38 +0100 Subject: [PATCH 11/18] jaq -j now imply -r --- jqjq.jq | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/jqjq.jq b/jqjq.jq index 73f5f5e..a4883c3 100644 --- a/jqjq.jq +++ b/jqjq.jq @@ -2834,9 +2834,8 @@ def invoke_client_jqjq: | parse_options | [ (.jq // "jq" | sh_escape) , if .action == "run-tests" then "-nsRr" - # TODO: jaq: -j does not imply -r - elif .mode == "repl" then "-njrR" - else "-njr" + elif .mode == "repl" then "-njR" + else "-nj" end , if .unbuffered_output and (.jq == null or (.jq | test("(^|[/\\\\])(gojq|jaq)[^/\\\\]*$") | not)) then From 27b2328ae83836c3c9bedb60b2dae17557687821 Mon Sep 17 00:00:00 2001 From: Mattias Wadman Date: Tue, 19 Nov 2024 14:41:28 +0100 Subject: [PATCH 12/18] Add jaq to ci --- .github/workflows/ci.yml | 14 ++++++++++++++ 1 file changed, 14 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index a02c75b..b61f340 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,3 +41,17 @@ jobs: curl -Ls 'https://github.com/itchyny/gojq/releases/download/v0.12.16/gojq_v0.12.16_linux_amd64.tar.gz' | tar xz mv gojq*/gojq gojq PATH="$PWD:$PATH" make test-jqjq JQ=gojq + + test-jqjq-jaq: + name: Run tests with jqjq using jaq + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v3 + - run: | + curl -sOLJ 'https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-linux-amd64' + mv jq-linux-amd64 jq + chmod a+x jq + curl -sOLJ 'https://github.com/01mf02/jaq/releases/download/v2.0.0-epsilon/jaq-x86_64-unknown-linux-gnu' + mv jaq-x86_64-unknown-linux-gnu jaq + chmod a+x jaq + PATH="$PWD:$PATH" make test-jqjq JQ=jaq From 9c118fe523ab975d954e2088da186c2237ceed97 Mon Sep 17 00:00:00 2001 From: Mattias Wadman Date: Tue, 19 Nov 2024 17:51:19 +0100 Subject: [PATCH 13/18] Make it possible to set host jq via JQ env Also run invoke_client_jqjq with host jq --- .github/workflows/ci.yml | 13 +++---------- Makefile | 4 ++-- jqjq | 2 +- jqjq.jq | 2 +- 4 files changed, 7 insertions(+), 14 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index b61f340..526d303 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -22,12 +22,11 @@ jobs: runs-on: ubuntu-latest steps: - uses: actions/checkout@v3 - # does PATH tricks so that jq that runs invoke_client_jqjq is same jq - run: | curl -sOLJ 'https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-linux-amd64' mv jq-linux-amd64 jq chmod a+x jq - PATH="$PWD:$PATH" make test-jqjq JQ=jq + PATH="$PWD:$PATH" JQ=jq make test-jqjq test-jqjq-gojq: name: Run tests with jqjq using gojq @@ -35,12 +34,9 @@ jobs: steps: - uses: actions/checkout@v3 - run: | - curl -sOLJ 'https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-linux-amd64' - mv jq-linux-amd64 jq - chmod a+x jq curl -Ls 'https://github.com/itchyny/gojq/releases/download/v0.12.16/gojq_v0.12.16_linux_amd64.tar.gz' | tar xz mv gojq*/gojq gojq - PATH="$PWD:$PATH" make test-jqjq JQ=gojq + PATH="$PWD:$PATH" JQ=gojq make test-jqjq test-jqjq-jaq: name: Run tests with jqjq using jaq @@ -48,10 +44,7 @@ jobs: steps: - uses: actions/checkout@v3 - run: | - curl -sOLJ 'https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-linux-amd64' - mv jq-linux-amd64 jq - chmod a+x jq curl -sOLJ 'https://github.com/01mf02/jaq/releases/download/v2.0.0-epsilon/jaq-x86_64-unknown-linux-gnu' mv jaq-x86_64-unknown-linux-gnu jaq chmod a+x jaq - PATH="$PWD:$PATH" make test-jqjq JQ=jaq + PATH="$PWD:$PATH" JQ=jaq make test-jqjq diff --git a/Makefile b/Makefile index 4bc32ab..bb09b0b 100644 --- a/Makefile +++ b/Makefile @@ -1,4 +1,4 @@ -JQ=jq +JQ ?= jq test: test-jq test-jqjq @@ -8,4 +8,4 @@ test-jq: .PHONY: test-jqjq test-jqjq: - ./jqjq --jq "${JQ}" --run-tests < jqjq.test + ./jqjq --run-tests < jqjq.test diff --git a/jqjq b/jqjq index 7569df6..b760082 100755 --- a/jqjq +++ b/jqjq @@ -1,6 +1,6 @@ #!/usr/bin/env bash eval "$( - jq \ + "${JQ:=jq}" \ -nr \ -L "$(dirname "$(realpath "${BASH_SOURCE[0]}")")" \ 'include "jqjq"; $ARGS.positional | invoke_client_jqjq' \ diff --git a/jqjq.jq b/jqjq.jq index a4883c3..86f5148 100644 --- a/jqjq.jq +++ b/jqjq.jq @@ -2832,7 +2832,7 @@ def invoke_client_jqjq: end; ( . as $args | parse_options - | [ (.jq // "jq" | sh_escape) + | [ (.jq // env.JQ // "jq" | sh_escape) , if .action == "run-tests" then "-nsRr" elif .mode == "repl" then "-njR" else "-nj" From d617ea0c9d853f4f60b6dd1c0049bdc01a413fd4 Mon Sep 17 00:00:00 2001 From: Mattias Wadman Date: Thu, 21 Nov 2024 23:30:32 +0100 Subject: [PATCH 14/18] Simplify CI runs a bit --- .github/workflows/ci.yml | 14 ++++++-------- 1 file changed, 6 insertions(+), 8 deletions(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 526d303..86cad7c 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -15,7 +15,7 @@ jobs: - run: | curl -sOLJ 'https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-linux-amd64' chmod a+x jq-linux-amd64 - make test-jq JQ=./jq-linux-amd64 + JQ=./jq-linux-amd64 make test-jq test-jqjq-jq: name: Run tests with jqjq using jq @@ -24,9 +24,8 @@ jobs: - uses: actions/checkout@v3 - run: | curl -sOLJ 'https://github.com/jqlang/jq/releases/download/jq-1.7.1/jq-linux-amd64' - mv jq-linux-amd64 jq - chmod a+x jq - PATH="$PWD:$PATH" JQ=jq make test-jqjq + chmod a+x jq-linux-amd64 + JQ=./jq-linux-amd64 make test-jqjq test-jqjq-gojq: name: Run tests with jqjq using gojq @@ -36,7 +35,7 @@ jobs: - run: | curl -Ls 'https://github.com/itchyny/gojq/releases/download/v0.12.16/gojq_v0.12.16_linux_amd64.tar.gz' | tar xz mv gojq*/gojq gojq - PATH="$PWD:$PATH" JQ=gojq make test-jqjq + JQ=./gojq make test-jqjq test-jqjq-jaq: name: Run tests with jqjq using jaq @@ -45,6 +44,5 @@ jobs: - uses: actions/checkout@v3 - run: | curl -sOLJ 'https://github.com/01mf02/jaq/releases/download/v2.0.0-epsilon/jaq-x86_64-unknown-linux-gnu' - mv jaq-x86_64-unknown-linux-gnu jaq - chmod a+x jaq - PATH="$PWD:$PATH" JQ=jaq make test-jqjq + chmod a+x jaq-x86_64-unknown-linux-gnu + JQ=./jaq-x86_64-unknown-linux-gnu make test-jqjq From 2f9a522241e8b616e9acc53b96efaaf19da27a2e Mon Sep 17 00:00:00 2001 From: Mattias Wadman Date: Tue, 26 Nov 2024 17:01:48 +0100 Subject: [PATCH 15/18] Clarift some more jaq comments --- jqjq.jq | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/jqjq.jq b/jqjq.jq index 86f5148..3e8cc27 100644 --- a/jqjq.jq +++ b/jqjq.jq @@ -1344,7 +1344,7 @@ def eval_ast($query; $path; $env; undefined_func): , $start , end: $end_ } - | ( # TODO: jaq: allow null index + | ( # jaq: for null provide object or array if . == null then if $name then if $name | type == "string" then {} else [] end elif $str then {} @@ -1370,7 +1370,7 @@ def eval_ast($query; $path; $env; undefined_func): ( $query_input | _e($start; []; $query_env) as [$_, $v] | [ ($query_path + [$v]) - , # TODO: jaq: allow null index + , # jaq: only index if non-null ( $input | if . != null then .[$v] end ) @@ -1483,9 +1483,9 @@ def eval_ast($query; $path; $env; undefined_func): $query.term.index; $path; .; - # TODO: jaq null index + # TODO: jaq: make ast more explicit? ( $query.term.suffix_list - | if . then .[0].optional + | if . != null then .[0].optional else false end ) From dcce4729f43a33e1310b8f2c7161166d9a6ef1fc Mon Sep 17 00:00:00 2001 From: Mattias Wadman Date: Tue, 26 Nov 2024 19:58:00 +0100 Subject: [PATCH 16/18] Move $next token null check out of _ops --- jqjq.jq | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/jqjq.jq b/jqjq.jq index 3e8cc27..fa5b000 100644 --- a/jqjq.jq +++ b/jqjq.jq @@ -202,9 +202,7 @@ def parse: # filter is used to disable operators, ex in keyval query def _op_prec_climb($p; filter): def _ops: - # TODO: jaq: nice way? don't even call _ops for null case? - if . == null then false - elif filter then false + if filter then null elif .pipe then {prec: 0, name: "|", assoc: "right"} # TODO: understand why jq has left associativity for "," but right seems to give correct parse tree elif .comma then {prec: 1, name: ",", assoc: "right"} @@ -229,14 +227,14 @@ def parse: elif .star then {prec: 8, name: "*", assoc: "left"} elif .slash then {prec: 8, name: "/", assoc: "left"} elif .percent then {prec: 8, name: "%", assoc: "left"} - else false + else null end; ( _p("query1") as [$rest, $t] | $rest | def _f($t): ( .[0] as $next # peek next - | ($next | _ops) as $next_op + | ($next | if . != null then _ops end) as $next_op | if $next_op and $next_op.prec >= $p then ( .[1:] # consume | ( if $next_op.assoc == "right" then From f54625e4ed286b0da743714795ed54410e97eea3 Mon Sep 17 00:00:00 2001 From: Mattias Wadman Date: Tue, 26 Nov 2024 20:53:36 +0100 Subject: [PATCH 17/18] Make _consume check length and return [$rest, $first] --- jqjq.jq | 263 +++++++++++++++++++++++++++----------------------------- 1 file changed, 128 insertions(+), 135 deletions(-) diff --git a/jqjq.jq b/jqjq.jq index fa5b000..12a0321 100644 --- a/jqjq.jq +++ b/jqjq.jq @@ -173,9 +173,10 @@ def lex: [_lex]; def parse: - # TODO: jaq null index - # TODO: maybe a consume helper that returns first? ala [$rest, $v]? - def _consume(f): select(length > 0 and (.[0] | f)) | .[1:]; + def _consume(f): + ( select(length > 0 and (.[0] | f)) + | [.[1:], .[0]] + ); def _optional(f): ( f // [., null] @@ -194,7 +195,7 @@ def parse: else [$c, []] end ); - def _keyword($name): _consume(.ident == $name); + def _keyword($name): _consume(.ident == $name)[0]; def _p($type): # based on: @@ -272,11 +273,10 @@ def parse: ); def _scalar($type; c; f): - ( . as [$first] - | _consume(c) - | [ . + ( _consume(c) as [$rest, $v] + | [ $rest , { term: - ( $first + ( $v | f | .type = $type ) @@ -292,7 +292,7 @@ def parse: # "name": # : def _object: - ( _consume(.lcurly) + ( _consume(.lcurly)[0] | _repeat( # TODO: # string interpolated key @@ -301,7 +301,7 @@ def parse: # multi query val: # term | ... ( ( def _colon_val: - ( _consume(.colon) + ( _consume(.colon)[0] # keyval_query only allows | operator (, is separator) | _p("keyval_query") as [$rest, $val] | $rest @@ -313,12 +313,12 @@ def parse: ( # {a} -> {a: .a} # {a: ...} -> {a: ...} - ( .[0] as $ident - | _consume(.ident) + ( _consume(.ident) as [$rest, {$ident}] + | $rest | _optional(_colon_val) as [$rest, $val] | $rest | [ . - , { key: $ident.ident + , { key: $ident , val: $val } ] @@ -339,10 +339,9 @@ def parse: ) // # {$a} -> {a: $a} - ( .[0] as $binding - | _consume(.binding) - | [ . - , {key: $binding.binding} + ( _consume(.binding) as [$rest, {$binding}] + | [ $rest + , {key: $binding} ] ) // @@ -365,8 +364,8 @@ def parse: [$rest, null] else # or there must be a comma - ( _consume(.comma) - | [., null] + ( _consume(.comma) as [$rest, $_] + | [$rest, null] ) end ) as [$rest, $_] @@ -374,8 +373,8 @@ def parse: ) ) as [$rest, $key_vals] | $rest - | _consume(.rcurly) - | [ . + | _consume(.rcurly) as [$rest, $_] + | [ $rest , { term: { type: "TermTypeObject" , object: @@ -394,10 +393,10 @@ def parse: # (query): pattern # [pattern, ...] def _pattern: - ( ( _consume(.lcurly) + ( ( _consume(.lcurly)[0] | _repeat( ( ( def _colon_pattern: - ( _consume(.colon) + ( _consume(.colon)[0] | _p("pattern") as [$rest, $pattern] | $rest | [ . @@ -407,12 +406,12 @@ def parse: ( # {a} -> {a: .a} # {a: ...} -> {a: ...} - ( .[0] as $ident - | _consume(.ident) + ( _consume(.ident) as [$rest, {$ident}] + | $rest | _optional(_colon_pattern) as [$rest, $val] | $rest | [ . - , { key: $ident.ident + , { key: $ident , val: $val } ] @@ -433,10 +432,9 @@ def parse: ) // # {$a} -> {a: $a} - ( .[0] as $binding - | _consume(.binding) - | [ . - , {key: $binding.binding} + ( _consume(.binding) as [$rest, {$binding}] + | [ $rest + , {key: $binding} ] ) // @@ -459,8 +457,8 @@ def parse: [$rest, null] else # or there must be a comma - ( _consume(.comma) - | [., null] + ( _consume(.comma) as [$rest, $_] + | [$rest, null] ) end ) as [$rest, $_] @@ -468,47 +466,47 @@ def parse: ) ) as [$rest, $key_patterns] | $rest - | _consume(.rcurly) - | [ . + | _consume(.rcurly) as [$rest, $_] + | [ $rest , {object: $key_patterns} ] ) // - ( _consume(.lsquare) + ( _consume(.lsquare) as [$rest, $_] + | $rest | _repeat( ( _p("pattern") as [$rest, $pattern] | $rest | _optional( # TODO: _one() etc? - ( _consume(.comma) - | [., null] + ( _consume(.comma) as [$rest, $_] + | [$rest, null] ) ) as [$rest, $_] | [$rest, $pattern] ) ) as [$rest, $pattern] | $rest - | _consume(.rsquare) - | [ . + | _consume(.rsquare) as [$rest, $_] + | [ $rest , {array: $pattern} ] ) // - ( .[0] as $binding - | _consume(.binding) - | [ . - , {name: $binding.binding} + ( _consume(.binding) as [$rest, {$binding}] + | [ $rest + , {name: $binding} ] ) ); # () def _subquery: - ( _consume(.lparen) + ( _consume(.lparen)[0] | _p("query") as [$rest, $query] | $rest - | _consume(.rparen) - | [ . + | _consume(.rparen) as [$rest, $_] + | [ $rest , { term: { type: "TermTypeQuery" , query: $query @@ -520,28 +518,28 @@ def parse: # ident # ident([;...]) def _func: - ( . as [$first] - | _consume(.ident) - | ( _consume(.lparen) + ( _consume(.ident) as [$rest, {$ident}] + | $rest + | ( _consume(.lparen)[0] | _repeat( ( _p("query") as [$rest, $arg] | $rest | _optional( # TODO: _one() etc? - ( _consume(.semicolon) - | [., null] + ( _consume(.semicolon) as [$rest, $_] + | [$rest, null] ) ) as [$rest, $_] | [$rest, $arg] ) ) as [$rest, $args] | $rest - | _consume(.rparen) + | _consume(.rparen)[0] | [ . , { term: { type: "TermTypeFunc" , func: - { name: $first.ident + { name: $ident , args: $args } } @@ -553,7 +551,7 @@ def parse: , { term: { type: "TermTypeFunc" , func: - {name: $first.ident} + {name: $ident} } } ] @@ -561,13 +559,12 @@ def parse: # $name def _binding: - ( . as [$first] - | _consume(.binding) - | [ . + ( _consume(.binding) as [$rest, {$binding}] + | [ $rest , { term: { type: "TermTypeFunc" , func: - {name: $first.binding} + {name: $binding} } } ] @@ -575,11 +572,11 @@ def parse: # [] def _array: - ( _consume(.lsquare) + ( _consume(.lsquare)[0] | _optional(_p("query")) as [$rest, $query] | $rest - | _consume(.rsquare) - | [ . + | _consume(.rsquare) as [$rest, $_] + | [ $rest , { term: { type: "TermTypeArray" , array: @@ -597,13 +594,13 @@ def parse: | _keyword("as") | _p("pattern") as [$rest, $pattern] | $rest - | _consume(.lparen) + | _consume(.lparen)[0] | _p("query") as [$rest, $start] | $rest - | _consume(.semicolon) + | _consume(.semicolon)[0] | _p("query") as [$rest, $update] | $rest - | _consume(.rparen) + | _consume(.rparen)[0] | [ . , { term: { type: "TermTypeReduce" @@ -626,19 +623,21 @@ def parse: | _keyword("as") | _p("pattern") as [$rest, $pattern] | $rest - | _consume(.lparen) + | _consume(.lparen)[0] | _p("query") as [$rest, $start] | $rest - | _consume(.semicolon) + | _consume(.semicolon)[0] | _p("query") as [$rest, $update] | $rest | _optional( - ( _consume(.semicolon) + ( _consume(.semicolon) as [$rest, $_] + | $rest | _p("query") ) ) as [$rest, $extract] | $rest - | _consume(.rparen) + | _consume(.rparen) as [$rest, $_] + | $rest | [ . , { term: { type: "TermTypeForeach" @@ -711,46 +710,45 @@ def parse: def _func_defs: _repeat( ( _keyword("def") - | . as [{ident: $name}] - | _consume(.ident) - | ( ( _consume(.lparen) + | _consume(.ident) as [$rest, $ident] + | $rest + | ( ( _consume(.lparen)[0] | _repeat( - ( .[0] as $arg - | ( ( _consume(.ident) - | [., $arg.ident] + ( ( ( _consume(.ident) as [$rest, {$ident}] + | [$rest, $ident] ) // - ( _consume(.binding) - | [., $arg.binding] + ( _consume(.binding) as [$rest, {$binding}] + | [$rest, $binding] ) ) as [$rest, $arg] | $rest - | ( _consume(.semicolon) + | ( _consume(.semicolon)[0] // . ) | [., $arg] ) ) as [$rest, $args] | $rest - | _consume(.rparen) - | _consume(.colon) + | _consume(.rparen)[0] + | _consume(.colon)[0] | _p("query") as [$rest, $body] | $rest - | _consume(.semicolon) - | [ . - , { name: $name + | _consume(.semicolon) as [$rest, $_] + | [ $rest + , { name: $ident.ident , args: $args , body: $body } ] ) // - ( _consume(.colon) + ( _consume(.colon)[0] | _p("query") as [$rest, $body] | $rest - | _consume(.semicolon) - | [ . - , { name: $name + | _consume(.semicolon) as [$rest, $_] + | [ $rest + , { name: $ident.ident , body: $body } ] @@ -772,29 +770,29 @@ def parse: # [] iter ( _optional( # TODO: _one() etc? - ( _consume(.dot) - | [., null] + ( _consume(.dot) as [$rest, $_] + | [$rest, null] ) ) as [$rest, $_] | $rest - | _consume(.lsquare) - | _consume(.rsquare) - | [., {iter: true}] + | _consume(.lsquare)[0] + | _consume(.rsquare) as [$rest, $_] + | [$rest, {iter: true}] ) // # [...] query ( _optional( # TODO: _one() etc? - ( _consume(.dot) - | [., null] + ( _consume(.dot) as [$rest, $_] + | [$rest, null] ) ) as [$rest, $_] | $rest - | _consume(.lsquare) + | _consume(.lsquare)[0] | _p("query") as [$rest, $start] | $rest - | _consume(.rsquare) - | [ . + | _consume(.rsquare) as [$rest, $_] + | [ $rest , { index: {start: $start} } @@ -804,15 +802,15 @@ def parse: # [...:...] # [:...] # [...:] - ( _consume(.lsquare) + ( _consume(.lsquare)[0] | _optional(_p("query")) as [$rest, $start] | $rest - | _consume(.colon) + | _consume(.colon)[0] | _optional(_p("query")) as [$rest, $end_] | $rest - | _consume(.rsquare) + | _consume(.rsquare)[0] # fail if both missing - | if $start == null and $end_ == null then empty else . end + | if $start == null and $end_ == null then empty end | [ . , { index: ( {is_slice: true} @@ -824,17 +822,16 @@ def parse: ) // # .name index - ( .[0] as $index - | _consume(.index) - | [ . + ( _consume(.index) as [$rest, {$index}] + | [ $rest , { index: - {name: $index.index} + {name: $index} } ] ) // # ."name" index - ( _consume(.dot) + ( _consume(.dot)[0] | _p("string") as [$rest, $string] | $rest | [ . @@ -847,8 +844,8 @@ def parse: ) // # ? optional (try) - ( _consume(.qmark) - | [ . + ( _consume(.qmark) as [$rest, $_] + | [ $rest , {optional: true} ] ) @@ -856,7 +853,7 @@ def parse: ( _keyword("as") | _p("pattern") as [$rest, $pattern] | $rest - | _consume(.pipe) + | _consume(.pipe)[0] | _p("query") as [$rest, $body] | $rest | [ . @@ -871,8 +868,8 @@ def parse: # . def _identity: - ( _consume(.dot) - | [ . + ( _consume(.dot) as [$rest, $_] + | [ $rest , { term: {type: "TermTypeIdentity"} } @@ -885,12 +882,12 @@ def parse: # ."name" # TODO: share with _suffix? tricky because of leading dot def _index: - ( ( _consume(.dot) - | _consume(.lsquare) + ( ( _consume(.dot)[0] + | _consume(.lsquare)[0] | _p("query") as [$rest, $query] | $rest - | _consume(.rsquare) - | [ . + | _consume(.rsquare) as [$rest, $_] + | [ $rest , { term: { type: "TermTypeIndex" , index: @@ -900,14 +897,14 @@ def parse: ] ) // - ( _consume(.dot) - | _consume(.lsquare) + ( _consume(.dot)[0] + | _consume(.lsquare)[0] | _optional(_p("query")) as [$rest, $start] | $rest - | _consume(.colon) + | _consume(.colon)[0] | _optional(_p("query")) as [$rest, $end_] | $rest - | _consume(.rsquare) + | _consume(.rsquare)[0] # fail is both missing | if $start == null and $end_ == null then empty else . end | [ . @@ -923,20 +920,19 @@ def parse: ] ) // - ( .[0] as $index - | _consume(.index) - | [ . + ( _consume(.index) as [$rest, {$index}] + | [ $rest , { term: { type: "TermTypeIndex" , index: - {name: $index.index} + {name: $index} } } ] ) // # ."name" index - ( _consume(.dot) + ( _consume(.dot)[0] | _p("string") as [$rest, $string] | $rest | [ . @@ -958,10 +954,8 @@ def parse: # "abc \(123)" def _string_query: - # TODO: jaq null index (consume checks length and outoput [value, rest]?) - ( select(length > 0) - | .[0] as {$string_start} - | _consume(.string_start) + ( _consume(.string_start) as [$rest, {$string_start}] + | $rest | _repeat( ( select(length > 0 ) # make sure there is something | _p("query") @@ -969,9 +963,8 @@ def parse: ) ) as [$rest, $queries] | $rest - | .[0] as {$string_end} - | _consume(.string_end) - | [ . + | _consume(.string_end) as [$rest, {$string_end}] + | [ $rest , { term: { type: "TermTypeString" , queries: @@ -996,8 +989,8 @@ def parse: # @format "abc" def _format_string: - ( .[0] as {$at_ident} - | _consume(.at_ident) + ( _consume(.at_ident) as [$rest, {$at_ident}] + | $rest | _string as [$rest, $string] | [ $rest , { term: @@ -1036,7 +1029,7 @@ def parse: # + etc def _unary_op(f; $op): - ( _consume(f) + ( _consume(f)[0] | _p("term") as [$rest, $term] | $rest | [ . @@ -1054,8 +1047,8 @@ def parse: # .. # transform .. into recurse call def _recurse: - ( _consume(.dotdot) - | [ . + ( _consume(.dotdot) as [$rest, $_] + | [ $rest , { term: { type: "TermTypeFunc" , func: From b3c6edc287d6b7b7a02e3a513e5389594223f26b Mon Sep 17 00:00:00 2001 From: Mattias Wadman Date: Tue, 26 Nov 2024 21:23:37 +0100 Subject: [PATCH 18/18] More jaq null index comments cleanup --- jqjq.jq | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/jqjq.jq b/jqjq.jq index 12a0321..10d2eef 100644 --- a/jqjq.jq +++ b/jqjq.jq @@ -970,7 +970,6 @@ def parse: , queries: [ {term: {str: $string_start, type: "TermTypeString"}} , ( $queries[] - # TODO: jaq: null index | if .term and .term.type == "TermTypeString" then . else {term: {query: ., type: "TermTypeQuery"}} end @@ -1474,7 +1473,6 @@ def eval_ast($query; $path; $env; undefined_func): $query.term.index; $path; .; - # TODO: jaq: make ast more explicit? ( $query.term.suffix_list | if . != null then .[0].optional else false @@ -1517,7 +1515,7 @@ def eval_ast($query; $path; $env; undefined_func): | $query.term.func as {$name, $args} | func_name($name; $args) as $name | $query_env[$name] as $e - # TODO: jaq null index and null has() + # jaq: null | has() is an error | if $e != null and ($e | has("value")) then [[null], $e.value] elif $e != null and $e.body then ( ($e.args // []) as $func_args @@ -1726,7 +1724,7 @@ def eval_ast($query; $path; $env; undefined_func): | if startswith("$") then .[1:] else . end | _term_str ) - , ( # TOOD: jaq: null index + , ( # TODO: jaq: null index ( $kv.val | values | .queries[0]