phase: 20.49 COMPLETE; 20.50 Flow+String minimal reps; 20.51 selfhost v0/v1 minimal (Option A/B); hv1-inline binop/unop/copy; docs + run_all + CURRENT_TASK -> 21.0
This commit is contained in:
393
apps/macros/examples/loop_normalize_macro.hako
Normal file
393
apps/macros/examples/loop_normalize_macro.hako
Normal file
@ -0,0 +1,393 @@
|
||||
// loop_normalize_macro.hako
|
||||
// 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).
|
||||
|
||||
using "apps/lib/json_builder.hako" as JB
|
||||
|
||||
// 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
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user