// 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":,"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 "#" 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":""} 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 } }