From 972a442e17c300e869fae94853de16471f58f633 Mon Sep 17 00:00:00 2001 From: Miguel Ceriani Date: Tue, 15 Apr 2025 12:44:27 +0200 Subject: [PATCH 1/3] Add function to generate back textual syntax from AST --- jqjq.jq | 117 ++++++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 117 insertions(+) diff --git a/jqjq.jq b/jqjq.jq index bd92502..caf6a14 100644 --- a/jqjq.jq +++ b/jqjq.jq @@ -2090,6 +2090,123 @@ def eval_ast($query; $path; $env; undefined_func): def eval_ast($ast): eval_ast($ast; []; {}; undefined_func_error); +def ast_tostring: + def _pattern: + if .name then .name + elif .array then "[\([.array[] | _pattern] | join(", "))]" + elif .object then "{\([.object[] | "\(.key)\(if .val then ": \(.val | _pattern)" else "" end)"] | join(", "))}" + else error("unsupported type of pattern: \(.)") + end; + def _f: + def _index: + ".\( + if .start then + "[\(.start | _f)\(if .end then ":\(.end | _f)" else "" end)]" + elif .name then .name + elif .str then .str.str | tojson + else error("unsupported type of index: \(.)") + end + )"; + . as {term: {type: $type}, $op, $func_defs} + | if $type then .term | + "\( + if $func_defs then [ + $func_defs.[] + | "def \(.name)\( + if .args then "(\(.args | join(", ")))" + else "" + end + ): \(.body | _f); " + ] | join("") + else "" + end + )\( + if $type == "TermTypeNull" then "null" + elif $type == "TermTypeNumber" then .number | tostring + elif $type == "TermTypeString" then + if .str then + .str | tojson + else + [ + .queries.[] | _f | + if .[0:1] == "\"" + then .[1:-1] + else "\\\(.)" + end + ] | join("") | "\"\(.)\"" + end + elif $type == "TermTypeTrue" then "true" + elif $type == "TermTypeFalse" then "false" + elif $type == "TermTypeIdentity" then "." + elif $type == "TermTypeUnary" then .unary | "\(.op)\({term: .term} | _f)" + elif $type == "TermTypeIndex" then .index | _index + elif $type == "TermTypeFunc" then "\(.func.name)\( + if .func.args then + [.func.args.[] | _f] | join(", ") | "(\(.))" + else + "" + end + )" + elif $type == "TermTypeObject" then + ( .object.key_vals + | "{\( + [ .[] | + "\( + if .key then .key + elif .key_string then .key_string.str | tojson + else .key_query | _f + end + )\( + if .val then + ": \([.val.queries.[] | _f] | join(", "))" + else "" + end + )" + ] | join(", ") + )}" ) + elif $type == "TermTypeArray" then "[\(.array.query | _f)]" + elif $type == "TermTypeIf" then .if | + "if \(.cond | _f) then \(.then | _f) \( + if .elif then [ + .elif.[] | + "elif \(.cond | _f) then \(.then | _f) " + ] | join("") + else "" + end + )else \(.else | _f) end" + elif $type == "TermTypeReduce" then .reduce | + "reduce \({term: .term} | _f) as \(.pattern | _pattern) (\(.start | _f);\(.update | _f))" + elif $type == "TermTypeForeach" then .foreach | + "foreach \({term: .term} | _f) as \(.pattern | _pattern) (\(.start | _f);\(.update | _f)\( + if .extract then ";\(.extract | _f)" else "" end + ))" + elif $type == "TermTypeQuery" then "(\(.query | _f))" + elif $type == "TermTypeFormat" then "\(.format) \(.str)" + elif $type == "TermTypeTry" then .try | + "try \(.body | _f)\(if .catch then " catch \(.catch | _f)" else "" end)" + else error("unsupported term: \(.)") + end + )\( + if .suffix_list then + [ + .suffix_list[] | + if .index then .index | _index + elif .bind then .bind | "as \([.patterns[] | _pattern] | join(", ")) | \(.body | _f)" + elif .iter then "[]" + elif .optional then "?" + else error("unsupported suffix type: \(.)") + end + ] | join("") + else "" + end + )" + elif $op then + (if $op | . == "," then "" else " " end) as $pad + | "\(.left | _f)\($pad)\($op) \(.right | _f)" + else error("unsupported query: \(.)") + end; + [_f] | join(""); + def _builtins_src: " def debug(msgs): (msgs | debug | empty), .; def halt_error: halt_error(5); From c2951babb896f8cb041b654ddecf04855aa84aec Mon Sep 17 00:00:00 2001 From: Miguel Ceriani Date: Tue, 15 Apr 2025 17:17:19 +0200 Subject: [PATCH 2/3] Add test for serialization and fix mapping of function args --- jqjq.jq | 17 ++++++++++++----- 1 file changed, 12 insertions(+), 5 deletions(-) diff --git a/jqjq.jq b/jqjq.jq index caf6a14..a8b45d3 100644 --- a/jqjq.jq +++ b/jqjq.jq @@ -2113,7 +2113,7 @@ def ast_tostring: if $func_defs then [ $func_defs.[] | "def \(.name)\( - if .args then "(\(.args | join(", ")))" + if .args then "(\(.args | join("; ")))" else "" end ): \(.body | _f); " @@ -2142,7 +2142,7 @@ def ast_tostring: elif $type == "TermTypeIndex" then .index | _index elif $type == "TermTypeFunc" then "\(.func.name)\( if .func.args then - [.func.args.[] | _f] | join(", ") | "(\(.))" + [.func.args.[] | _f] | join("; ") | "(\(.))" else "" end @@ -2762,6 +2762,7 @@ def usage: + " --jq PATH jq implementation to run with\n" + " --lex Lex EXPR\n" + " --parse Lex then parse EXPR\n" + + " --serialize Expect an AST in EXPR and serialize it as jq\n" + " --repl REPL\n" + " --no-builtins Don't include builtins\n" + "\n" @@ -2888,6 +2889,7 @@ def parse_options: // option(null; "repl"; .mode = "repl") // option(null; "lex"; .mode = "lex") // option(null; "parse"; .mode = "parse") + // option(null; "serialize"; .mode = "serialize") // option(null; "no-builtins"; .no_builtins = true) // ( if .args.is_short then "-\(.args.curr[:1])" else "--\(.args.curr)" end @@ -3066,7 +3068,7 @@ def jqjq($args; $env): , "\n" # input interrupted so no line entered ); - def _run_tests: + def _run_tests($testSerialize): # read jq test format: # # comment # expr @@ -3151,7 +3153,10 @@ def jqjq($args; $env): | "line \(.line): \(.input | tojson) | \(.expr) -> \(.output | _to_unslurped)" as $test_name | . as $test | try - ( ($test.expr | lex | parse) as $ast + ( ( + ($test.expr | lex | parse) + | if $testSerialize then ast_tostring | lex| parse else . end + ) as $ast | $test.input | [ eval_ast( $ast; @@ -3246,8 +3251,10 @@ def jqjq($args; $env): | if $opts.action == "help" then usage elif $opts.mode == "lex" then $opts.program | lex, "\n" elif $opts.mode == "parse" then $opts.program | lex | parse, "\n" + elif $opts.mode == "serialize" then $opts.program | ast_tostring, "\n" elif $opts.mode == "repl" then _repl($opts) - elif $opts.action == "run-tests" then input | _run_tests + elif $opts.action == "run-tests-without-serialize" then input | _run_tests(false) + elif $opts.action == "run-tests" then input | (_run_tests(false), _run_tests(true)) else _filter($opts) end ); From 2411113cd8a2c641df54c6981e729f782754248e Mon Sep 17 00:00:00 2001 From: Miguel Ceriani Date: Tue, 15 Apr 2025 22:44:33 +0200 Subject: [PATCH 3/3] Fix all errors found with tests --- jqjq.jq | 38 +++++++++++++++++++++++++++----------- 1 file changed, 27 insertions(+), 11 deletions(-) diff --git a/jqjq.jq b/jqjq.jq index a8b45d3..6ba0ac6 100644 --- a/jqjq.jq +++ b/jqjq.jq @@ -2094,22 +2094,35 @@ def ast_tostring: def _pattern: if .name then .name elif .array then "[\([.array[] | _pattern] | join(", "))]" - elif .object then "{\([.object[] | "\(.key)\(if .val then ": \(.val | _pattern)" else "" end)"] | join(", "))}" + elif .object then + "{\( + [.object[] | "\( + if .key then .key else .key_string.str | tojson end + )\( + if .val then ": \(.val | _pattern)" else "" end + )"] | + join(", ") + )}" else error("unsupported type of pattern: \(.)") end; def _f: def _index: ".\( - if .start then - "[\(.start | _f)\(if .end then ":\(.end | _f)" else "" end)]" + if .start or .is_slice then + "[\( + if .start then .start | _f else "" end + )\( + if .is_slice then ":" else "" end + )\( + if .end then .end | _f else "" end + )]" elif .name then .name elif .str then .str.str | tojson else error("unsupported type of index: \(.)") end )"; . as {term: {type: $type}, $op, $func_defs} - | if $type then .term | - "\( + | "\( if $func_defs then [ $func_defs.[] | "def \(.name)\( @@ -2120,7 +2133,9 @@ def ast_tostring: ] | join("") else "" end - )\( + )\( + if $type then .term | + "\( if $type == "TermTypeNull" then "null" elif $type == "TermTypeNumber" then .number | tostring elif $type == "TermTypeString" then @@ -2164,7 +2179,7 @@ def ast_tostring: )" ] | join(", ") )}" ) - elif $type == "TermTypeArray" then "[\(.array.query | _f)]" + elif $type == "TermTypeArray" then .array | "[\(if .query then .query | _f else "" end)]" elif $type == "TermTypeIf" then .if | "if \(.cond | _f) then \(.then | _f) \( if .elif then [ @@ -2173,7 +2188,7 @@ def ast_tostring: ] | join("") else "" end - )else \(.else | _f) end" + )\(if .else then " else \(.else | _f)" else "" end) end" elif $type == "TermTypeReduce" then .reduce | "reduce \({term: .term} | _f) as \(.pattern | _pattern) (\(.start | _f);\(.update | _f))" elif $type == "TermTypeForeach" then .foreach | @@ -2181,7 +2196,7 @@ def ast_tostring: if .extract then ";\(.extract | _f)" else "" end ))" elif $type == "TermTypeQuery" then "(\(.query | _f))" - elif $type == "TermTypeFormat" then "\(.format) \(.str)" + elif $type == "TermTypeFormat" then "\(.format) \(.str | _f)" elif $type == "TermTypeTry" then .try | "try \(.body | _f)\(if .catch then " catch \(.catch | _f)" else "" end)" else error("unsupported term: \(.)") @@ -2191,7 +2206,7 @@ def ast_tostring: [ .suffix_list[] | if .index then .index | _index - elif .bind then .bind | "as \([.patterns[] | _pattern] | join(", ")) | \(.body | _f)" + elif .bind then .bind | " as \([.patterns[] | _pattern] | join(", ")) | \(.body | _f)" elif .iter then "[]" elif .optional then "?" else error("unsupported suffix type: \(.)") @@ -2204,7 +2219,8 @@ def ast_tostring: (if $op | . == "," then "" else " " end) as $pad | "\(.left | _f)\($pad)\($op) \(.right | _f)" else error("unsupported query: \(.)") - end; + end + )"; [_f] | join(""); def _builtins_src: "