Files
hakorune/tools/hako_shared/json_parser.hako
nyash-codex 608693af7f fix(json_parser): Fix infinite loop by working around MIR nested-if bug
🐛 バグ修正: JsonParserBox 無限ループ解消

**問題**:
- `vm step budget exceeded` エラーが発生
- `_parse_number()` で無限ループ

**根本原因**:
- MIR lowering の既知のバグ: ループ内のネスト if-else 文が無限ジャンプチェーンを生成
- bb11 → bb6 → bb4 の無条件ジャンプループ
- PHI 更新が実行されず、ループ変数が更新されない

**修正内容**:
1. `while` → `loop()` 構文に統一(10箇所)
2. ネスト if-else をフラット化(workaround):
   - `_parse_number()`: `ch >= "0" && ch <= "9"` → `digits.indexOf(ch) >= 0`
   - `_parse_string()`: ネスト if を分離
   - `_unescape_string()`: `ch == "\\" && i + 1 < s.length()` をフラット化

**テスト**:
- json_parser.hako: RC 0 
- ネスト if-else バグを回避して正常動作

**注記**:
- 根本修正(MIR lowering bug fix)は別タスク
- workaround で Phase 173 ブロック解除

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-12-04 17:42:57 +09:00

534 lines
11 KiB
Plaintext

// tools/hako_shared/json_parser.hako - JsonParserBox (Phase 171 MVP)
// .hako native JSON parser for MIR/CFG JSON parsing
// Replaces 289 lines of hand-written JSON parsing in analysis_consumer.hako
// JsonParserBox: Main parser (static box, all static methods)
static box JsonParserBox {
// Parse JSON string to MapBox (object) or ArrayBox (array) or primitive
// Returns: MapBox (for objects), ArrayBox (for arrays), String, Integer, null
method parse(json_str) {
if json_str == null { return null }
local s = me._trim(json_str)
if s.length() == 0 { return null }
return me._parse_value(s, 0).get("value")
}
// Parse JSON string expecting object, returns MapBox or null
method parse_object(json_str) {
local val = me.parse(json_str)
if val == null { return null }
// Check if it's a MapBox (which indicates object)
local test = val.get
if test == null { return null }
return val
}
// Parse JSON string expecting array, returns ArrayBox or null
method parse_array(json_str) {
local val = me.parse(json_str)
if val == null { return null }
// Check if it's ArrayBox (which has size method)
local test = val.size
if test == null { return null }
return val
}
// Internal: Parse value at position, returns {value: Any, pos: Integer, type: String} or null
_parse_value(s, pos) {
local p = me._skip_whitespace(s, pos)
if p >= s.length() { return null }
local ch = s.substring(p, p+1)
// null
if ch == "n" {
if me._match_literal(s, p, "null") {
local result = new MapBox()
result.set("value", null)
result.set("pos", p + 4)
result.set("type", "null")
return result
}
return null
}
// true
if ch == "t" {
if me._match_literal(s, p, "true") {
local result = new MapBox()
result.set("value", 1)
result.set("pos", p + 4)
result.set("type", "bool")
return result
}
return null
}
// false
if ch == "f" {
if me._match_literal(s, p, "false") {
local result = new MapBox()
result.set("value", 0)
result.set("pos", p + 5)
result.set("type", "bool")
return result
}
return null
}
// number (integer only for MVP)
if ch == "-" || (ch >= "0" && ch <= "9") {
return me._parse_number(s, p)
}
// string
if ch == '"' {
return me._parse_string(s, p)
}
// array
if ch == "[" {
return me._parse_array(s, p)
}
// object
if ch == "{" {
return me._parse_object(s, p)
}
return null
}
_parse_number(s, pos) {
local num_str = ""
local p = pos
// Optional negative sign
if p < s.length() {
local ch = s.substring(p, p+1)
if ch == "-" {
num_str = num_str + ch
p = p + 1
}
}
// Digits (workaround: flatten to avoid MIR nested-if-in-loop bug)
local parsing_done = 0
loop(p < s.length()) {
if parsing_done == 1 { break }
local ch = s.substring(p, p+1)
local digits = "0123456789"
local digit_pos = digits.indexOf(ch)
if digit_pos >= 0 {
num_str = num_str + ch
p = p + 1
} else {
parsing_done = 1
}
}
if num_str == "" || num_str == "-" { return null }
local result = new MapBox()
result.set("value", me._atoi(num_str))
result.set("pos", p)
result.set("type", "number")
return result
}
_parse_string(s, pos) {
if s.substring(pos, pos+1) != '"' { return null }
local p = pos + 1
local str = ""
loop(p < s.length()) {
local ch = s.substring(p, p+1)
if ch == '"' {
// End of string
local result = new MapBox()
result.set("value", me._unescape_string(str))
result.set("pos", p + 1)
result.set("type", "string")
return result
}
if ch == "\\" {
// Escape sequence (workaround: flatten to avoid MIR nested-if bug)
local has_next = 0
if p + 1 < s.length() { has_next = 1 }
if has_next == 0 { return null }
str = str + ch
p = p + 1
str = str + s.substring(p, p+1)
p = p + 1
continue
}
str = str + ch
p = p + 1
}
return null
}
_parse_array(s, pos) {
if s.substring(pos, pos+1) != "[" { return null }
local p = pos + 1
local arr = new ArrayBox()
p = me._skip_whitespace(s, p)
// Empty array
if p < s.length() {
if s.substring(p, p+1) == "]" {
local result = new MapBox()
result.set("value", arr)
result.set("pos", p + 1)
result.set("type", "array")
return result
}
}
// Parse elements
loop(p < s.length()) {
local elem_result = me._parse_value(s, p)
if elem_result == null { return null }
local elem = elem_result.get("value")
arr.push(elem)
p = elem_result.get("pos")
p = me._skip_whitespace(s, p)
if p >= s.length() { return null }
local ch = s.substring(p, p+1)
if ch == "]" {
local result = new MapBox()
result.set("value", arr)
result.set("pos", p + 1)
result.set("type", "array")
return result
}
if ch == "," {
p = p + 1
p = me._skip_whitespace(s, p)
continue
}
return null
}
return null
}
_parse_object(s, pos) {
if s.substring(pos, pos+1) != "{" { return null }
local p = pos + 1
local obj = new MapBox()
p = me._skip_whitespace(s, p)
// Empty object
if p < s.length() {
if s.substring(p, p+1) == "}" {
local result = new MapBox()
result.set("value", obj)
result.set("pos", p + 1)
result.set("type", "object")
return result
}
}
// Parse key-value pairs
loop(p < s.length()) {
p = me._skip_whitespace(s, p)
// Parse key (must be string)
if s.substring(p, p+1) != '"' { return null }
local key_result = me._parse_string(s, p)
if key_result == null { return null }
local key = key_result.get("value")
p = key_result.get("pos")
p = me._skip_whitespace(s, p)
// Expect colon
if p >= s.length() { return null }
if s.substring(p, p+1) != ":" { return null }
p = p + 1
p = me._skip_whitespace(s, p)
// Parse value
local value_result = me._parse_value(s, p)
if value_result == null { return null }
local value = value_result.get("value")
obj.set(key, value)
p = value_result.get("pos")
p = me._skip_whitespace(s, p)
if p >= s.length() { return null }
local ch = s.substring(p, p+1)
if ch == "}" {
local result = new MapBox()
result.set("value", obj)
result.set("pos", p + 1)
result.set("type", "object")
return result
}
if ch == "," {
p = p + 1
continue
}
return null
}
return null
}
// Helper functions
_skip_whitespace(s, pos) {
local p = pos
loop(p < s.length()) {
local ch = s.substring(p, p+1)
if ch == " " || ch == "\t" || ch == "\n" || ch == "\r" {
p = p + 1
} else {
break
}
}
return p
}
_trim(s) {
if s == null { return "" }
local start = 0
local end = s.length()
// Trim leading whitespace
loop(start < end) {
local ch = s.substring(start, start+1)
if ch == " " || ch == "\t" || ch == "\n" || ch == "\r" {
start = start + 1
} else {
break
}
}
// Trim trailing whitespace
loop(end > start) {
local ch = s.substring(end-1, end)
if ch == " " || ch == "\t" || ch == "\n" || ch == "\r" {
end = end - 1
} else {
break
}
}
return s.substring(start, end)
}
_match_literal(s, pos, literal) {
local len = literal.length()
if pos + len > s.length() { return 0 }
local i = 0
loop(i < len) {
if s.substring(pos + i, pos + i + 1) != literal.substring(i, i + 1) {
return 0
}
i = i + 1
}
return 1
}
_unescape_string(s) {
if s == null { return "" }
local result = ""
local i = 0
loop(i < s.length()) {
local ch = s.substring(i, i+1)
// Workaround: flatten to avoid MIR nested-if bug
local is_escape = 0
local has_next = 0
if ch == "\\" { is_escape = 1 }
if i + 1 < s.length() { has_next = 1 }
local process_escape = 0
if is_escape == 1 {
if has_next == 1 {
process_escape = 1
}
}
if process_escape == 1 {
local next = s.substring(i+1, i+2)
if next == "n" {
result = result + "\n"
i = i + 2
continue
}
if next == "t" {
result = result + "\t"
i = i + 2
continue
}
if next == "r" {
result = result + "\r"
i = i + 2
continue
}
if next == '"' {
result = result + '"'
i = i + 2
continue
}
if next == "\\" {
result = result + "\\"
i = i + 2
continue
}
// Other escapes: keep as-is for MVP
result = result + ch
i = i + 1
continue
}
result = result + ch
i = i + 1
}
return result
}
_atoi(s) {
if s == null { return 0 }
local n = s.length()
if n == 0 { return 0 }
local i = 0
local v = 0
local negative = 0
// Check for negative sign
if s.substring(0, 1) == "-" {
negative = 1
i = 1
}
local digits = "0123456789"
loop(i < n) {
local ch = s.substring(i, i+1)
if ch < "0" || ch > "9" { break }
local pos = digits.indexOf(ch)
if pos < 0 { break }
v = v * 10 + pos
i = i + 1
}
if negative == 1 {
v = 0 - v
}
return v
}
// Parse Program JSON v0 (Phase 172)
// Returns ProgramJSONBox or null
method parse_program(json_str) {
local obj = me.parse_object(json_str)
if obj == null { return null }
// Program JSON必須フィールド確認
local version = obj.get("version")
local kind = obj.get("kind")
if version == null { return null }
if kind != "Program" { return null }
// ProgramJSONBox を返す
return new ProgramJSONBox(obj)
}
}
// ProgramJSONBox: Program JSON v0 wrapper (Phase 172)
// Provides type-safe access to Program JSON structure
box ProgramJSONBox {
_obj: MapBox // Internal: parsed JSON object
birth(obj) {
me._obj = obj
}
method get_version() {
return me._obj.get("version")
}
method get_kind() {
return me._obj.get("kind")
}
method get_defs() {
// Returns ArrayBox (definitions: Box/Method/etc)
return me._obj.get("defs")
}
method get_meta() {
// Returns MapBox (metadata)
return me._obj.get("meta")
}
method get_usings() {
// Returns ArrayBox or null (using declarations)
local meta = me.get_meta()
if meta == null { return null }
return meta.get("usings")
}
// Get all objects (for backward compatibility)
method get_object() {
return me._obj
}
}
// Main entry point (required for standalone execution)
static box JsonParserMain {
main(args) {
return 0
}
}