🎯 目的: Stage-1 CLI の env/トグル処理を整理・改善 ✅ 改善内容: - stage1_cli.hako: 関数名修正・簡略化 - パラメータ名を cli_args_raw に統一 - __mir__.log マーカー整備(デバッグ用) - env処理のコメント改善 - string_helpers.hako: to_i64 改善 - null/Void ガード追加 - NYASH_TO_I64_DEBUG 対応 - NYASH_TO_I64_FORCE_ZERO トグル準備 - tools/stage1_debug.sh: デバッグ改善 - NYASH_TO_I64_DEBUG フラグ追加 - NYASH_TO_I64_FORCE_ZERO フラグ追加 - ログ観測の改善 - apps/tests/minimal_to_i64_void.hako: テストケース追加 - Void値の to_i64 処理確認用 📋 Phase 25.4-B への準備: - 次フェーズで Stage1CliConfigBox を導入予定 - env.get() を Config 箱に集約する基盤完成 - 既存動作は維持(Fail-Fast + テスト緑キープ) 🎯 効果: - デバッグ観測性向上 - Void/null 処理の安全性向上 - 将来の Config 箱化への準備完了 Co-Authored-By: Claude <noreply@anthropic.com>
220 lines
6.6 KiB
Plaintext
220 lines
6.6 KiB
Plaintext
// 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) {
|
||
// Optional debug hook: observe incoming values/boxes.
|
||
// Enable with NYASH_TO_I64_DEBUG=1 when diagnosing numeric coercions.
|
||
if env.get("NYASH_TO_I64_DEBUG") == "1" {
|
||
__mir__.log("[string_helpers/to_i64] x", x)
|
||
}
|
||
// Fail-close for unexpected boxes (e.g., Void) when explicitly requested.
|
||
// This avoids String + Void type errors during Stage-1 CLI bring-up.
|
||
if env.get("NYASH_TO_I64_FORCE_ZERO") == "1" { return 0 }
|
||
if x == null { return 0 }
|
||
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
|
||
}
|
||
}
|