diff --git a/apps/lib/json_builder.nyash b/apps/lib/json_builder.nyash index e28dc9fa..5c144e08 100644 --- a/apps/lib/json_builder.nyash +++ b/apps/lib/json_builder.nyash @@ -60,6 +60,16 @@ static box JsonBuilder { return "{\"kind\":\"Assignment\",\"target\":" + target_json + ",\"value\":" + value_json + "}" } + print_(expr_json) { + return "{\"kind\":\"Print\",\"expression\":" + expr_json + "}" + } + + return_(value_json) { + local v = "null" + if value_json != null { v = value_json } + return "{\"kind\":\"Return\",\"value\":" + v + "}" + } + local(vars, inits) { // vars: array[string], inits: array[string|null] local vs = [] @@ -110,4 +120,3 @@ static box JsonBuilder { return "{\"kind\":\"Program\",\"statements\":[" + this.join(stmts_json, ",") + "]}" } } - diff --git a/apps/macros/examples/if_match_normalize_macro.nyash b/apps/macros/examples/if_match_normalize_macro.nyash index e244b424..20d90786 100644 --- a/apps/macros/examples/if_match_normalize_macro.nyash +++ b/apps/macros/examples/if_match_normalize_macro.nyash @@ -4,10 +4,309 @@ // in docs/guides/if-match-normalize.md. static box MacroBoxSpec { - name() { return "IfMatchNormalizeScaffold" } + name() { return "IfMatchNormalize" } expand(json, ctx) { + local JB = include "apps/lib/json_builder.nyash" + + // --- helpers copied/adapted from loop_normalize --- + function parse_value(s, i) { + local n = s.length() + if i >= n { return ("" + i) + "#" + "" } + local ch = s.substring(i, i+1) + if ch == "\"" { + local j = i + 1 + loop(j < n) { + local c = s.substring(j, j+1) + if c == "\\" { j = j + 2; continue } + if c == "\"" { j = j + 1; break } + j = j + 1 + } + return ("" + j) + "#" + s.substring(i, j) + } + if ch == "{" { + local depth = 1 + local j = i + 1 + local in_str = false + loop(j < n && depth > 0) { + local c = s.substring(j, j+1) + if c == "\"" { + local k = j - 1 + local esc = false + if k >= 0 && s.substring(k, k+1) == "\\" { esc = true } + if not esc { in_str = not in_str } + j = j + 1 + continue + } + if not in_str { + if c == "{" { depth = depth + 1 } + if c == "}" { depth = depth - 1 } + } + j = j + 1 + } + return ("" + j) + "#" + s.substring(i, j) + } + if ch == "[" { + local depth = 1 + local j = i + 1 + local in_str = false + loop(j < n && depth > 0) { + local c = s.substring(j, j+1) + if c == "\"" { + local k = j - 1 + local esc = false + if k >= 0 && s.substring(k, k+1) == "\\" { esc = true } + if not esc { in_str = not in_str } + j = j + 1 + continue + } + if not in_str { + if c == "[" { depth = depth + 1 } + if c == "]" { depth = depth - 1 } + } + j = j + 1 + } + return ("" + j) + "#" + s.substring(i, j) + } + local j = i + loop(j < n) { + local c = s.substring(j, j+1) + if c == "," || c == "]" || c == "}" || c == "\n" || c == "\r" || c == "\t" || c == " " { break } + j = j + 1 + } + return ("" + j) + "#" + s.substring(i, j) + } + + function pair_idx(pair) { + local i = 0 + local n = pair.length() + local val = 0 + loop(i < n) { + local ch = pair.substring(i, i+1) + if ch == "#" { break } + local d = 0 + if ch == "0" { d = 0 } + if ch == "1" { d = 1 } + if ch == "2" { d = 2 } + if ch == "3" { d = 3 } + if ch == "4" { d = 4 } + if ch == "5" { d = 5 } + if ch == "6" { d = 6 } + if ch == "7" { d = 7 } + if ch == "8" { d = 8 } + if ch == "9" { d = 9 } + val = val * 10 + d + i = i + 1 + } + return val + } + + function pair_json(pair) { + local i = 0 + local n = pair.length() + loop(i < n) { + if pair.substring(i, i+1) == "#" { return pair.substring(i+1, n) } + i = i + 1 + } + return "" + } + + function find_field_pos(obj, key) { + local tok = "\"" + key + "\":" + local i = 0 + loop(i + tok.length() <= obj.length()) { + if obj.substring(i, i + tok.length()) == tok { return i + tok.length() } + i = i + 1 + } + return -1 + } + + function has_kind(obj, k) { + local tok = "\"kind\":\"" + k + "\"" + local i = 0 + loop(i + tok.length() <= obj.length()) { + if obj.substring(i, i + tok.length()) == tok { return true } + i = i + 1 + } + return false + } + + function split_array(arr_json) { + // arr_json like "[ ... ]" + if arr_json.length() < 2 { return [] } + local inner = arr_json.substring(1, arr_json.length()-1) + local out = [] + local i = 0 + local n = inner.length() + local in_str = false + local d_obj = 0 + local d_arr = 0 + local start = 0 + loop(i < n) { + local c = inner.substring(i, i+1) + if c == "\"" { + local k = i - 1 + local esc = false + if k >= 0 && inner.substring(k, k+1) == "\\" { esc = true } + if not esc { in_str = not in_str } + } else if not in_str { + if c == "{" { d_obj = d_obj + 1 } + if c == "}" { d_obj = d_obj - 1 } + if c == "[" { d_arr = d_arr + 1 } + if c == "]" { d_arr = d_arr - 1 } + if c == "," && d_obj == 0 && d_arr == 0 { + out.push(inner.substring(start, i)) + start = i + 1 + } + } + i = i + 1 + } + if start < n { out.push(inner.substring(start, n)) } + // trim whitespace + local j = 0 + loop(j < out.length()) { + local e = out.get(j) + local a = 0 + local b = e.length() + loop(a < b && (e.substring(a,a+1)==" " || e.substring(a,a+1)=="\n" || e.substring(a,a+1)=="\t" || e.substring(a,a+1)=="\r")) { a = a + 1 } + loop(b > a && (e.substring(b-1,b)==" " || e.substring(b-1,b)=="\n" || e.substring(b-1,b)=="\t" || e.substring(b-1,b)=="\r")) { b = b - 1 } + out.set(j, e.substring(a,b)) + j = j + 1 + } + return out + } + + function get_field(obj, key) { + local p = find_field_pos(obj, key) + if p < 0 { return null } + local pr = parse_value(obj, p) + return pair_json(pr) + } + + function replace_field(obj, key, new_val) { + local tok = "\"" + key + "\":" + local start = -1 + local i2 = 0 + loop(i2 + tok.length() <= obj.length()) { + if obj.substring(i2, i2 + tok.length()) == tok { start = i2; break } + i2 = i2 + 1 + } + if start < 0 { return obj } + local p = start + tok.length() + local pr = parse_value(obj, p) + local endi = pair_idx(pr) + local prefix = obj.substring(0, start + tok.length()) + local suffix = obj.substring(endi, obj.length()) + return prefix + new_val + suffix + } + + function rewrite_stmt_node(njson) { + // Assignment with If RHS → If(assign ...) + if has_kind(njson, "Assignment") { + local val_json = get_field(njson, "value") + if val_json != null && has_kind(val_json, "If") { + local tgt = get_field(njson, "target") + local cond = get_field(val_json, "condition") + local th = get_field(val_json, "then") + local el = get_field(val_json, "else") + // then/else are arrays; take first element as expr + local th_e = null + if th != null && th.substring(0,1) == "[" { + local parts = split_array(th) + th_e = parts.length() > 0 ? parts.get(0) : JB.literal_null() + } + local el_e = null + if el == null || el == "null" { + el_e = null + } else if el.substring(0,1) == "[" { + local parts2 = split_array(el) + el_e = parts2.length() > 0 ? parts2.get(0) : JB.literal_null() + } + local then_s = [ JB.assignment(tgt, th_e) ] + local else_s = null + if el_e != null { else_s = [ JB.assignment(tgt, el_e) ] } + return JB.if_(cond, then_s, else_s) + } + } + // Return with If value → If(Return ...) + if has_kind(njson, "Return") { + local val_json = get_field(njson, "value") + if val_json != null && val_json != "null" && has_kind(val_json, "If") { + local cond = get_field(val_json, "condition") + local th = get_field(val_json, "then") + local el = get_field(val_json, "else") + local th_e = null + if th != null && th.substring(0,1) == "[" { local p = split_array(th); th_e = p.length()>0 ? p.get(0) : JB.literal_null() } + local el_e = null + if el == null || el == "null" { el_e = null } else if el.substring(0,1) == "[" { local p2 = split_array(el); el_e = p2.length()>0 ? p2.get(0) : JB.literal_null() } + local then_s = [ JB.return_(th_e) ] + local else_s = null + if el_e != null { else_s = [ JB.return_(el_e) ] } + return JB.if_(cond, then_s, else_s) + } + } + // Print with If expression → If(Print ...) + if has_kind(njson, "Print") { + local ex_json = get_field(njson, "expression") + if ex_json != null && has_kind(ex_json, "If") { + local cond = get_field(ex_json, "condition") + local th = get_field(ex_json, "then") + local el = get_field(ex_json, "else") + local th_e = null + if th != null && th.substring(0,1) == "[" { local p = split_array(th); th_e = p.length()>0 ? p.get(0) : JB.literal_null() } + local el_e = null + if el == null || el == "null" { el_e = null } else if el.substring(0,1) == "[" { local p2 = split_array(el); el_e = p2.length()>0 ? p2.get(0) : JB.literal_null() } + local then_s = [ JB.print_(th_e) ] + local else_s = null + if el_e != null { else_s = [ JB.print_(el_e) ] } + return JB.if_(cond, then_s, else_s) + } + } + // Recurse for If/Loop nodes + if has_kind(njson, "If") { + local th = get_field(njson, "then") + local el = get_field(njson, "else") + if th != null && th.substring(0,1) == "[" { + local parts = split_array(th) + local rebuilt = [] + local i3 = 0 + loop(i3 < parts.length()) { rebuilt.push(rewrite_stmt_node(parts.get(i3))); i3 = i3 + 1 } + njson = replace_field(njson, "then", "[" + JB.join(rebuilt, ",") + "]") + } + if el != null && el != "null" && el.substring(0,1) == "[" { + local parts2 = split_array(el) + local rebuilt2 = [] + local j3 = 0 + loop(j3 < parts2.length()) { rebuilt2.push(rewrite_stmt_node(parts2.get(j3))); j3 = j3 + 1 } + njson = replace_field(njson, "else", "[" + JB.join(rebuilt2, ",") + "]") + } + return njson + } + if has_kind(njson, "Loop") { + local body = get_field(njson, "body") + if body != null && body.substring(0,1) == "[" { + local parts = split_array(body) + local rebuilt = [] + local i4 = 0 + loop(i4 < parts.length()) { rebuilt.push(rewrite_stmt_node(parts.get(i4))); i4 = i4 + 1 } + njson = replace_field(njson, "body", "[" + JB.join(rebuilt, ",") + "]") + } + return njson + } + return njson + } + + // entry: expect Program at top + if has_kind(json, "Program") { + local stm = get_field(json, "statements") + if stm != null && stm.substring(0,1) == "[" { + local parts = split_array(stm) + local rebuilt = [] + local i0 = 0 + loop(i0 < parts.length()) { rebuilt.push(rewrite_stmt_node(parts.get(i0))); i0 = i0 + 1 } + return replace_field(json, "statements", "[" + JB.join(rebuilt, ",") + "]") + } + } return json } } - diff --git a/apps/tests/macro_golden_if_assign.nyash b/apps/tests/macro_golden_if_assign.nyash new file mode 100644 index 00000000..4d5224b2 --- /dev/null +++ b/apps/tests/macro_golden_if_assign.nyash @@ -0,0 +1,4 @@ +local x = 0 +x = if (1 < 2) { 10 } else { 20 } +print(x) + diff --git a/tools/test/golden/macro/if_assign.expanded.json b/tools/test/golden/macro/if_assign.expanded.json new file mode 100644 index 00000000..9682a0b7 --- /dev/null +++ b/tools/test/golden/macro/if_assign.expanded.json @@ -0,0 +1,10 @@ +{"kind":"Program","statements":[ + {"kind":"Local","variables":["x"],"inits":[{"kind":"Literal","value":{"type":"int","value":0}}]}, + {"kind":"If","condition":{"kind":"BinaryOp","op":"<","left":{"kind":"Literal","value":{"type":"int","value":1}},"right":{"kind":"Literal","value":{"type":"int","value":2}}},"then":[ + {"kind":"Assignment","target":{"kind":"Variable","name":"x"},"value":{"kind":"Literal","value":{"type":"int","value":10}}} + ],"else":[ + {"kind":"Assignment","target":{"kind":"Variable","name":"x"},"value":{"kind":"Literal","value":{"type":"int","value":20}}} + ]}, + {"kind":"Print","expression":{"kind":"Variable","name":"x"}} +]} + diff --git a/tools/test/golden/macro/if_assign_user_macro_golden.sh b/tools/test/golden/macro/if_assign_user_macro_golden.sh new file mode 100644 index 00000000..ffd9e92d --- /dev/null +++ b/tools/test/golden/macro/if_assign_user_macro_golden.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -euo pipefail + +root=$(cd "$(dirname "$0")"/../../../.. && pwd) +bin="$root/target/release/nyash" +src="apps/tests/macro_golden_if_assign.nyash" +golden="$root/tools/test/golden/macro/if_assign.expanded.json" + +if [ ! -x "$bin" ]; then + echo "nyash binary not found at $bin; build first (cargo build --release)" >&2 + exit 1 +fi + +export NYASH_MACRO_ENABLE=1 +export NYASH_MACRO_PATHS="apps/macros/examples/if_match_normalize_macro.nyash" + +normalize_json() { + python3 -c 'import sys,json; print(json.dumps(json.loads(sys.stdin.read()), sort_keys=True, separators=(",",":")))' +} + +out_raw=$("$bin" --dump-expanded-ast-json "$src") +out_norm=$(printf '%s' "$out_raw" | normalize_json) +gold_norm=$(normalize_json < "$golden") + +if [ "$out_norm" != "$gold_norm" ]; then + echo "Golden mismatch (if-assign normalization)" >&2 + diff -u <(echo "$out_norm") <(echo "$gold_norm") || true + exit 2 +fi + +echo "[OK] golden if-assign normalization matched" +