Files
hakorune/apps/macros/examples/loop_normalize_macro.nyash

394 lines
13 KiB
Plaintext
Raw Normal View History

// loop_normalize_macro.nyash
// MVP: identity expansion with (json, ctx) signature.
// Next steps: normalize `loop(cond){ body }` into carrier-based LoopForm.
static box MacroBoxSpec {
name() { return "LoopNormalize" }
expand(json, ctx) {
// MVP normalizer: detect Loop nodes with canonical key order
// "kind":"Loop","condition":<json>,"body":[ ... ] and rewrite them
// into a normalized form using JsonBuilder (keys ordered as condition/body).
local JB = include "apps/lib/json_builder.nyash"
// helpers
local s = json
local out = ""
local i = 0
// parse a JSON value starting at i and return "<end_index>#<json>"
function parse_value(s, i) {
local n = s.length()
if i >= n { return ("" + i) + "#" + "" }
local ch = s.substring(i, i+1)
// string
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)
}
// object
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 == "\"" {
// toggle string (respect escape)
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)
}
// array
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)
}
// number/true/false/null: read until delimiter
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) {
// parse decimal at start until '#'
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) {
// substring after first '#'
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 ""
}
// Extract assignment target variable name from an Assignment JSON object string.
// Returns "" if not found.
function extract_assign_target_var(e2) {
// pattern: "target":{"kind":"Variable","name":"<NAME>"}
local pat = "\"target\":{\"kind\":\"Variable\",\"name\":\""
local pos = -1
local j = 0
loop(j + pat.length() <= e2.length()) {
if e2.substring(j, j + pat.length()) == pat { pos = j + pat.length(); break }
j = j + 1
}
if pos < 0 { return "" }
// read until next unescaped quote
local name = ""
local k = pos
loop(k < e2.length()) {
local c = e2.substring(k, k+1)
if c == "\\" {
// skip escape and next char
if k + 1 < e2.length() { name = name + e2.substring(k, k+2); k = k + 2; continue }
}
if c == "\"" { break }
name = name + c
k = k + 1
}
return name
}
// pattern tokens
local t_kind_loop = "\"kind\":\"Loop\""
local t_cond = "\"condition\":"
local t_body = "\"body\":"
loop(i < s.length()) {
// try to detect a Loop object start
if i + 6 < s.length() && s.substring(i, i+1) == "{" {
// look ahead inside this object to see if it begins with kind:Loop
local val = parse_value(s, i)
local endi = pair_idx(val)
local obj = pair_json(val)
// quick check: contains kind:"Loop" (manual scan)
{
local found = 0
local k = 0
loop(k + t_kind_loop.length() <= obj.length()) {
if obj.substring(k, k + t_kind_loop.length()) == t_kind_loop {
found = 1
break
}
k = k + 1
}
if found == 0 {
out = out + obj
i = endi
continue
}
}
// Now attempt to parse condition and body assuming canonical order
// Find condition token within object string
local oj = 0
local pos_c = -1
loop(oj + t_cond.length() <= obj.length()) {
if obj.substring(oj, oj + t_cond.length()) == t_cond {
pos_c = oj + t_cond.length()
break
}
oj = oj + 1
}
local pos_b = -1
local kk = 0
loop(kk + t_body.length() <= obj.length()) {
if obj.substring(kk, kk + t_body.length()) == t_body {
pos_b = kk + t_body.length()
break
}
kk = kk + 1
}
if pos_c >= 0 && pos_b >= 0 {
// extract values
local cond_pair = parse_value(obj, pos_c)
local cond_json = pair_json(cond_pair)
// move after condition to find body array
// ensure we re-scan from pos_b to robustly pick body
local body_pair = parse_value(obj, pos_b)
local body_json = pair_json(body_pair)
// if body_json is not array, keep identity
if body_json.substring(0,1) == "[" {
// Reorder body: move Assignment nodes to the tail (carrier-like normalization)
local inner = body_json.substring(1, body_json.length()-1)
local elems = []
local p2 = 0
local n2 = inner.length()
local in_str2 = false
local depth_obj2 = 0
local depth_arr2 = 0
local start2 = 0
loop(p2 < n2) {
local c2 = inner.substring(p2, p2+1)
if c2 == "\"" {
local k2 = p2 - 1
local esc2 = false
if k2 >= 0 && inner.substring(k2, k2+1) == "\\" { esc2 = true }
if not esc2 { in_str2 = not in_str2 }
} else if not in_str2 {
if c2 == "{" { depth_obj2 = depth_obj2 + 1 }
if c2 == "}" { depth_obj2 = depth_obj2 - 1 }
if c2 == "[" { depth_arr2 = depth_arr2 + 1 }
if c2 == "]" { depth_arr2 = depth_arr2 - 1 }
if c2 == "," && depth_obj2 == 0 && depth_arr2 == 0 {
elems.push(inner.substring(start2, p2))
start2 = p2 + 1
}
}
p2 = p2 + 1
}
if start2 < n2 { elems.push(inner.substring(start2, n2)) }
// Classify with original indices
local assigns = [] // list of [idx,json]
local others = [] // list of [idx,json]
local tagA = "\"kind\":\"Assignment\""
local t = 0
loop(t < elems.length()) {
local e2 = elems.get(t)
// trim
local a = 0
local b = e2.length()
loop(a < b && (e2.substring(a,a+1)==" " || e2.substring(a,a+1)=="\n" || e2.substring(a,a+1)=="\t" || e2.substring(a,a+1)=="\r")) { a = a + 1 }
loop(b > a && (e2.substring(b-1,b)==" " || e2.substring(b-1,b)=="\n" || e2.substring(b-1,b)=="\t" || e2.substring(b-1,b)=="\r")) { b = b - 1 }
e2 = e2.substring(a,b)
// contains tagA?
local found = 0
local q = 0
loop(q + tagA.length() <= e2.length()) {
if e2.substring(q, q + tagA.length()) == tagA {
found = 1
break
}
q = q + 1
}
if found == 1 { assigns.push([t, e2]) } else { others.push([t, e2]) }
t = t + 1
}
// Only reorder when all others appear before all assigns in the original order
local ok = 1
if assigns.length() > 0 && others.length() > 0 {
// max index of others, min index of assigns
local max_o = others.get(0).get(0)
local i2 = 1
loop(i2 < others.length()) { if others.get(i2).get(0) > max_o { max_o = others.get(i2).get(0) } i2 = i2 + 1 }
local min_a = assigns.get(0).get(0)
i2 = 1
loop(i2 < assigns.length()) { if assigns.get(i2).get(0) < min_a { min_a = assigns.get(i2).get(0) } i2 = i2 + 1 }
if not (max_o <= min_a) { ok = 0 }
}
// MVP-2 gate: skip when Break/Continue exists (conservative)
if ok == 1 {
local has_ctrl = 0
local tagBr = "\"kind\":\"Break\""
local tagCt = "\"kind\":\"Continue\""
t = 0
loop(t < elems.length()) {
local e3 = elems.get(t)
// cheap contains
local p = 0
loop(p + tagBr.length() <= e3.length()) {
if e3.substring(p, p + tagBr.length()) == tagBr { has_ctrl = 1; break }
p = p + 1
}
if has_ctrl == 0 {
p = 0
loop(p + tagCt.length() <= e3.length()) {
if e3.substring(p, p + tagCt.length()) == tagCt { has_ctrl = 1; break }
p = p + 1
}
}
if has_ctrl == 1 { break }
t = t + 1
}
if has_ctrl == 1 { ok = 0 }
}
// MVP-2 gate: allow up to 2 unique assignment targets; else keep original
if ok == 1 {
local uniq = []
t = 0
local too_many = 0
loop(t < assigns.length()) {
local aj = assigns.get(t).get(1)
local nm = extract_assign_target_var(aj)
if nm == "" {
// unknown structure → conservative: abort reorder
too_many = 1
break
}
// check if nm already recorded
local seen = 0
local u = 0
loop(u < uniq.length()) {
if uniq.get(u) == nm { seen = 1; break }
u = u + 1
}
if seen == 0 { uniq.push(nm) }
if uniq.length() > 2 { too_many = 1; break }
t = t + 1
}
if too_many == 1 { ok = 0 }
}
// Rebuild body (others then assigns) only when ok; otherwise keep original
local body_new = "["
local first = 1
t = 0
if ok == 1 {
loop(t < others.length()) {
if first == 1 { first = 0 } else { body_new = body_new + "," }
body_new = body_new + others.get(t).get(1)
t = t + 1
}
t = 0
loop(t < assigns.length()) {
if first == 1 { first = 0 } else { body_new = body_new + "," }
body_new = body_new + assigns.get(t).get(1)
t = t + 1
}
} else {
// keep original order
local back = 0
loop(back < elems.length()) {
if first == 1 { first = 0 } else { body_new = body_new + "," }
body_new = body_new + elems.get(back)
back = back + 1
}
}
body_new = body_new + "]"
// rebuild Loop string directly (canonical key order: condition, body)
local loop_norm = "{\"kind\":\"Loop\",\"condition\":" + cond_json + ",\"body\":" + body_new + "}"
out = out + loop_norm
i = endi
continue
}
}
// fallback: copy as-is if parsing failed
out = out + obj
i = endi
continue
}
// default: copy through one char
out = out + s.substring(i, i+1)
i = i + 1
}
return out
}
}