macro(if/match normalize): implement If-expression normalization for common statement contexts (Assignment/Return/Print); add golden (if-assign) and JSON builder helpers (print, return)
This commit is contained in:
@ -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, ",") + "]}"
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -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
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
4
apps/tests/macro_golden_if_assign.nyash
Normal file
4
apps/tests/macro_golden_if_assign.nyash
Normal file
@ -0,0 +1,4 @@
|
||||
local x = 0
|
||||
x = if (1 < 2) { 10 } else { 20 }
|
||||
print(x)
|
||||
|
||||
10
tools/test/golden/macro/if_assign.expanded.json
Normal file
10
tools/test/golden/macro/if_assign.expanded.json
Normal file
@ -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"}}
|
||||
]}
|
||||
|
||||
32
tools/test/golden/macro/if_assign_user_macro_golden.sh
Normal file
32
tools/test/golden/macro/if_assign_user_macro_golden.sh
Normal file
@ -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"
|
||||
|
||||
Reference in New Issue
Block a user