// string_helpers.hako - StringHelpers (common, pure helpers) // Responsibility: numeric/string conversions and JSON quoting for selfhost tools. // Non-responsibility: JSON scanning beyond local character processing; use CfgNavigatorBox/StringScanBox for navigation. static box StringHelpers { // Convert a numeric or numeric-like string to its decimal representation. int_to_str(n) { local v = me.to_i64(n) if v == 0 { return "0" } if v < 0 { return "-" + me.int_to_str(0 - v) } local out = "" local digits = "0123456789" loop (v > 0) { local d = v % 10 local ch = digits.substring(d, d+1) out = ch + out v = v / 10 } return out } // Parse integer from number or numeric-like string (leading '-' allowed; stops at first non-digit). to_i64(x) { local s = "" + x local i = 0 local neg = 0 if s.substring(0,1) == "-" { neg = 1 i = 1 } local n = s.length() if i >= n { return 0 } local acc = 0 loop (i < n) { local ch = s.substring(i, i+1) if ch < "0" || ch > "9" { break } local ds = "0123456789" local dpos = ds.indexOf(ch) if dpos < 0 { break } acc = acc * 10 + dpos i = i + 1 } if neg == 1 { return 0 - acc } return acc } // Quote a string for JSON (escape backslash, quote, and control chars) and wrap with quotes json_quote(s) { if s == null { return "\"\"" } local out = "" local i = 0 local n = s.length() loop (i < n) { local ch = s.substring(i, i+1) if ch == "\\" { out = out + "\\\\" } else { if ch == "\"" { out = out + "\\\"" } else { if ch == "\n" { out = out + "\\n" } else { if ch == "\r" { out = out + "\\r" } else { if ch == "\t" { out = out + "\\t" } else { out = out + ch } } } }} i = i + 1 } return "\"" + out + "\"" } // Check if string is numeric-like (optional leading '-', then digits). is_numeric_str(s) { if s == null { return 0 } local n = s.length() if n == 0 { return 0 } local i = 0 if s.substring(0,1) == "-" { if n == 1 { return 0 } i = 1 } loop(i < n) { local ch = s.substring(i, i+1) if ch < "0" || ch > "9" { return 0 } i = i + 1 } return 1 } // Read consecutive digits starting at pos (no sign handling here; keep semantics simple) read_digits(text, pos) { local out = "" loop (true) { local ch = text.substring(pos, pos+1) if ch == "" { break } if ch >= "0" && ch <= "9" { out = out + ch pos = pos + 1 } else { break } } return out } // ------------------------------------ // NEW: Extended String Utilities (added for parser/compiler unification) // ------------------------------------ // Character predicates is_digit(ch) { return ch >= "0" && ch <= "9" } is_alpha(ch) { return (ch >= "A" && ch <= "Z") || (ch >= "a" && ch <= "z") || ch == "_" } is_space(ch) { return ch == " " || ch == "\t" || ch == "\n" || ch == "\r" } // Pattern matching starts_with(src, i, pat) { local n = src.length() local m = pat.length() { // Dev-only trace for Stage‑B / parser調査用 local dbg = env.get("HAKO_STAGEB_DEBUG") if dbg != null && ("" + dbg) == "1" { print("[string_helpers/starts_with] src=\"" + src + "\"") print("[string_helpers/starts_with] i=" + ("" + i) + " m=" + ("" + m) + " n=" + ("" + n)) } } if i + m > n { return 0 } local k = 0 loop(k < m) { if src.substring(i + k, i + k + 1) != pat.substring(k, k + 1) { return 0 } k = k + 1 } return 1 } // Keyword match with word boundary (next char not [A-Za-z0-9_]) starts_with_kw(src, i, kw) { { local dbg = env.get("HAKO_STAGEB_DEBUG") if dbg != null && ("" + dbg) == "1" { print("[string_helpers/starts_with_kw] src=\"" + src + "\" i=" + ("" + i) + " kw=\"" + kw + "\"") } } // 同一箱内ヘルパーとして、receiver 経由ではなく // 明示的な箱名付きの静的呼び出しにする(引数順のずれ防止)。 if StringHelpers.starts_with(src, i, kw) == 0 { return 0 } local n = src.length() local j = i + kw.length() if j >= n { return 1 } local ch = src.substring(j, j+1) if me.is_alpha(ch) || me.is_digit(ch) { return 0 } return 1 } // String search index_of(src, i, pat) { local n = src.length() local m = pat.length() if m == 0 { return i } local j = i loop(j + m <= n) { if me.starts_with(src, j, pat) { return j } j = j + 1 } return -1 } // Trim spaces and tabs (with optional semicolon at end) trim(s) { local i = 0 local n = s.length() loop(i < n && (s.substring(i,i+1) == " " || s.substring(i,i+1) == "\t")) { i = i + 1 } local j = n loop(j > i && (s.substring(j-1,j) == " " || s.substring(j-1,j) == "\t" || s.substring(j-1,j) == ";")) { j = j - 1 } return s.substring(i, j) } // Skip whitespace from position i skip_ws(src, i) { if src == null { return i } local n = src.length() local cont = 1 local guard = 0 local max = 100000 loop(cont == 1) { if guard > max { return i } else { guard = guard + 1 } if i < n { if me.is_space(src.substring(i, i+1)) { i = i + 1 } else { cont = 0 } } else { cont = 0 } } return i } // Find last occurrence of pattern in string (backward search) last_index_of(src, pat) { if src == null { return -1 } if pat == null { return -1 } local n = src.length() local m = pat.length() if m == 0 { return n } if m > n { return -1 } local i = n - m loop(i >= 0) { if me.starts_with(src, i, pat) { return i } i = i - 1 } return -1 } // Split string by newline into ArrayBox (without relying on StringBox.split) split_lines(s) { local arr = new ArrayBox() if s == null { return arr } local n = s.length() local last = 0 local i = 0 loop (i < n) { local ch = s.substring(i, i+1) if ch == "\n" { arr.push(s.substring(last, i)) last = i + 1 } i = i + 1 } // push tail if last <= n { arr.push(s.substring(last)) } return arr } }