diff --git a/docs/development/current/main/phase171_jsonparserbox_implementation.md b/docs/development/current/main/phase171_jsonparserbox_implementation.md new file mode 100644 index 00000000..b75322c3 --- /dev/null +++ b/docs/development/current/main/phase171_jsonparserbox_implementation.md @@ -0,0 +1,493 @@ +# Phase 171: JsonParserBox 実装 & hako_check への導入 + +## 0. ゴール + +**Phase 170 で決めた API 草案どおりに .hako 純正 JSON パーサ Box(JsonParserBox)を実装する。** + +目的: +- JSON 文字列をメモリ内オブジェクトに変換する Box を実装 +- hako_check (HC020) で Phase 156 の手書き JSON パーサ(289行)を置き換え +- 将来の selfhost/Stage-B/解析ツールから再利用可能に + +--- + +## 1. 背景と戦略 + +### Phase 156 の課題 + +- hako_check の `analysis_consumer.hako` に 289行の手書き JSON パーサ +- 他の HC ルール(HC021+)でも JSON 解析が必要になる +- 共通ライブラリ化が急務 + +### Phase 170 での決定 + +- **API 草案**: `parse()`, `parse_object()`, `parse_array()` +- **型定義**: JsonValueBox(union 相当), JsonObjectBox, JsonArrayBox +- **MVP スコープ**: MIR/CFG JSON のみ対応(Program JSON v0 は Phase 172+) + +### Phase 171 の戦略 + +1. **最小MVP実装**: MIR/CFG JSON が使う型のみ +2. **段階的導入**: hako_check (HC020) が最初のユーザー +3. **共通ライブラリ化**: `tools/hako_shared/` で shared 設計 + +--- + +## 2. Scope / Non-scope + +### ✅ やること + +1. **JsonParserBox の実装** + - `parse(json_str) -> JsonValue?` + - `parse_object(json_str) -> JsonObjectBox?` + - `parse_array(json_str) -> JsonArrayBox?` + +2. **内部型の定義** + - JsonValueBox(kind で型判定) + - JsonObjectBox(key-value map) + - JsonArrayBox(value list) + +3. **エスケープシーケンス対応** + - MIR JSON が実際に使う範囲:`\"`, `\\`, `\n`, `\t` など + - Unicode (`\uXXXX`) は Phase 172+ + +4. **hako_check への導入** + - `analysis_consumer.hako` の 289行手書きパーサを削除 + - JsonParserBox を使用した 10行規模実装に置き換え + - HC019/HC020 の出力が変わらないことを確認 + +5. **テスト** + - JsonParserBox 単体テスト(正常系・エラー系) + - hako_check スモークテスト(回帰なし) + +### ❌ やらないこと + +- to_json() / serialization(Phase 172+) +- スキーマ検証(Phase 172+) +- Program JSON v0 対応(Phase 172+) +- ストリーミングパーサ(Phase 173+) + +--- + +## 3. Task 1: 設計ドキュメントの確認・MVP の切り出し + +### やること + +1. **Phase 170 設計の再確認** + - `docs/private/roadmap2/phases/phase-170-hako-json-library/README.md` を読む + - API 草案を確認(parse, parse_object, parse_array) + - 型定義案を確認(JsonValueBox, JsonObjectBox, JsonArrayBox) + +2. **MVP スコープの確定** + - MIR/CFG JSON のサブセット: + - 型: null, bool, number, string, object, array + - number: 整数のみ(浮動小数点は未対応) + - string: 基本的なエスケープのみ + - object: キーは文字列固定 + - エラーハンドリング: null を返す + +3. **実装方針の決定** + - Phase 156 の手書きパーサを参照しつつ + - StringBox.substring/indexOf で文字列走査 + - 状態機械を最小化(シンプル > 最適化) + +### 成果物 + +- MVP の詳細仕様書(Task 2 への引き継ぎ) + +--- + +## 4. Task 2: JsonParserBox の実装 (.hako) + +### ファイル位置 + +``` +tools/hako_shared/ +├── json_parser.hako ← 新規作成 +└── tests/ + └── json_parser_test.hako ← 新規作成 +``` + +### やること + +1. **Box 構造の定義** + +```hako +// JsonValueBox: union 相当の型 +static box JsonValueBox { + kind: StringBox # "null", "bool", "number", "string", "array", "object" + boolVal: BoolBox + numVal: IntegerBox + strVal: StringBox + arrVal: JsonArrayBox + objVal: JsonObjectBox + + method is_null() { return me.kind == "null" } + method is_bool() { return me.kind == "bool" } + method is_number() { return me.kind == "number" } + method is_string() { return me.kind == "string" } + method is_array() { return me.kind == "array" } + method is_object() { return me.kind == "object" } +} + +// JsonObjectBox: key-value map +static box JsonObjectBox { + pairs: ArrayBox # [{"key": String, "value": JsonValue}, ...] + + method get(key: String) -> JsonValue? { ... } + method keys() -> ArrayBox { ... } + method values() -> ArrayBox { ... } +} + +// JsonArrayBox: value list +static box JsonArrayBox { + elements: ArrayBox # [JsonValue, ...] + + method get(index: Integer) -> JsonValue? { ... } + method length() -> Integer { ... } +} + +// JsonParserBox: メインパーサ +static box JsonParserBox { + method parse(json_str: String) -> JsonValue? { ... } + method parse_object(json_str: String) -> JsonObjectBox? { ... } + method parse_array(json_str: String) -> JsonArrayBox? { ... } +} +``` + +2. **JSON パーサ実装** + +```hako +static box JsonParserBox { + method parse(json_str: String) -> JsonValue? { + // 前後の空白を削除 + local s = me._trim(json_str) + if s.length() == 0 { return null } + + local result = null + local pos = me._parse_value(s, 0, result) + if pos < 0 { return null } + + return result + } + + method parse_object(json_str: String) -> JsonObjectBox? { + local val = me.parse(json_str) + if val == null or not val.is_object() { return null } + return val.objVal + } + + method parse_array(json_str: String) -> JsonArrayBox? { + local val = me.parse(json_str) + if val == null or not val.is_array() { return null } + return val.arrVal + } + + // 内部ヘルパー + method _parse_value(s: String, pos: Integer, out: JsonValue) -> Integer { ... } + method _parse_null(s: String, pos: Integer, out: JsonValue) -> Integer { ... } + method _parse_bool(s: String, pos: Integer, out: JsonValue) -> Integer { ... } + method _parse_number(s: String, pos: Integer, out: JsonValue) -> Integer { ... } + method _parse_string(s: String, pos: Integer, out: JsonValue) -> Integer { ... } + method _parse_array(s: String, pos: Integer, out: JsonValue) -> Integer { ... } + method _parse_object(s: String, pos: Integer, out: JsonValue) -> Integer { ... } + method _skip_whitespace(s: String, pos: Integer) -> Integer { ... } + method _trim(s: String) -> String { ... } + method _unescape_string(s: String) -> String { ... } +} +``` + +3. **エスケープシーケンス対応** + +MIR JSON で使われるもの: +- `\"` → `"` +- `\\` → `\` +- `\n` → newline +- `\t` → tab +- `\r` → carriage return +- `\b`, `\f` → 対応可(Phase 171) +- `\uXXXX` → 未対応(Phase 172+) + +### 成果物 + +- JsonParserBox 実装(約300-400行推定) +- 単体テストケース + +--- + +## 5. Task 3: hako_check (HC020) から JsonParserBox を使うように置き換え + +### 対象ファイル + +- `tools/hako_check/analysis_consumer.hako` (Lines 206-494: 手書きパーサ) +- `tools/hako_check/rules/rule_dead_blocks.hako` + +### やること + +1. **JSON パーサ部分の削除** + - `analysis_consumer.hako` の手書きパーサ関数を全削除(~289行) + - 代わりに JsonParserBox を import + +2. **CFG 処理の修正** + +```hako +// 修正前(Phase 156) +local mir_json_text = ir.get("_mir_json_text") +local cfg_obj = me._parse_json_object(mir_json_text) // 手写 + +// 修正後(Phase 171) +local mir_json_text = ir.get("_mir_json_text") +local cfg_val = JsonParserBox.parse(mir_json_text) +local cfg_obj = cfg_val.objVal // JsonObjectBox を取得 +``` + +3. **HC020 ルール側の確認** + - `rule_dead_blocks.hako` が CFG を正しく解析できるか確認 + - JsonObjectBox/.get() メソッドとの互換性確認 + +4. **目標** + - `analysis_consumer.hako` の行数を 500+ → 210 に削減(約60%削減) + - Phase 156 の 289行手書きパーサの削除 + +### 成果物 + +- 修正済み `analysis_consumer.hako` +- 修正済み `tools/hako_check/` 関連ファイル + +--- + +## 6. Task 4: 単体テスト & スモークテスト + +### JsonParserBox 単体テスト + +新規作成: `tools/hako_shared/tests/json_parser_test.hako` + +テストケース: + +```hako +static box JsonParserTest { + main() { + me.test_null() + me.test_bool() + me.test_number() + me.test_string() + me.test_array() + me.test_object() + me.test_error_cases() + print("All tests passed!") + } + + test_null() { + local val = JsonParserBox.parse("null") + assert(val != null && val.is_null()) + } + + test_bool() { + local t = JsonParserBox.parse("true") + local f = JsonParserBox.parse("false") + assert(t.is_bool() && t.boolVal == true) + assert(f.is_bool() && f.boolVal == false) + } + + test_number() { + local n = JsonParserBox.parse("123") + assert(n.is_number() && n.numVal == 123) + } + + test_string() { + local s = JsonParserBox.parse('"hello"') + assert(s.is_string() && s.strVal == "hello") + } + + test_array() { + local arr = JsonParserBox.parse("[1, 2, 3]") + assert(arr.is_array() && arr.arrVal.length() == 3) + } + + test_object() { + local obj = JsonParserBox.parse('{"key": "value"}') + assert(obj.is_object()) + local val = obj.objVal.get("key") + assert(val != null && val.is_string()) + } + + test_error_cases() { + local inv1 = JsonParserBox.parse("{") + assert(inv1 == null) + + local inv2 = JsonParserBox.parse("[1,2,]") + assert(inv2 == null) + } +} +``` + +### hako_check スモークテスト + +既存のスモークテストを実行: + +```bash +# HC020 スモーク +./tools/hako_check_deadblocks_smoke.sh + +# HC019 スモーク(回帰なし) +./tools/hako_check_deadcode_smoke.sh +``` + +期待: +- HC020 出力が Phase 156 と変わらない +- HC019 出力が変わらない +- エラーが出ない + +### 成果物 + +- JsonParserBox テストファイル +- スモークテスト成功確認 + +--- + +## 7. Task 5: ドキュメント & CURRENT_TASK 更新 + +### ドキュメント更新 + +1. **phase170_hako_json_library_design.md に追記** + ```markdown + ## Phase 171 実装結果 + + ✅ JsonParserBox 実装完了 + - API: parse(), parse_object(), parse_array() + - サポート型: null, bool, number, string, array, object + - エスケープ: \", \\, \n, \t, \r, \b, \f + + ✅ hako_check への導入完了 + - analysis_consumer.hako: 500+ → 210 行(~60%削減) + - Phase 156 の 289行手書きパーサを削除 + - HC020 が JsonParserBox を使用 + + 📊 削減実績: + - hako_check パーサ: 289行 → JsonParserBox 呼び出し(~10行) + - 共通ライブラリ化: hako_check/selfhost/ツールから再利用可能 + ``` + +2. **hako_check_design.md を更新** + ```markdown + ### JSON 解析 + + - Phase 156 まで: analysis_consumer.hako に手書きパーサ(289行) + - Phase 171 から: JsonParserBox を使用 + - 場所: tools/hako_shared/json_parser.hako + ``` + +3. **CURRENT_TASK.md に Phase 171 セクション追加** + ```markdown + ### Phase 171: JsonParserBox 実装 & hako_check 導入 ✅ + + **完了内容**: + - JsonParserBox 実装(tools/hako_shared/json_parser.hako) + - hako_check (HC020) が JsonParserBox を使用 + - 手書きパーサ 289行 → 共通ライブラリに統合 + + **成果**: + - hako_check 行数削減: 60% (500+ → 210 行) + - 再利用性: HC021+, selfhost, Stage-B で活用可 + - 箱化: .hako 純正 JSON パーサ Box 確立 + + **次フェーズ**: Phase 172 で selfhost/Stage-B への導入予定 + ``` + +### git commit + +``` +feat(hako_check): Phase 171 JsonParserBox implementation + +✨ .hako 純正 JSON パーサ Box 実装完了! + +🎯 実装内容: +- JsonParserBox: parse/parse_object/parse_array メソッド +- JsonValueBox, JsonObjectBox, JsonArrayBox 型定義 +- エスケープシーケンス対応(\", \\, \n, \t など) + +📊 hako_check 統合: +- analysis_consumer.hako: 289行手書きパーサを削除 +- JsonParserBox を使用した軽量実装に置き換え +- HC019/HC020 の出力は変わらず(回帰なし) + +✅ テスト: +- JsonParserBox 単体テスト全 PASS +- hako_check スモークテスト全 PASS +- Phase 156 との後方互換性確認 + +🏗️ 次ステップ: +- Phase 172: selfhost/Stage-B への導入 +- Phase 172+: Program JSON v0 対応 +- Phase 173+: to_json() 逆変換実装 +``` + +--- + +## ✅ 完成チェックリスト(Phase 171) + +- [ ] Task 1: 設計ドキュメント確認・MVP 切り出し + - [ ] Phase 170 設計再確認 + - [ ] MVP スコープ確定 + - [ ] 実装方針決定 +- [ ] Task 2: JsonParserBox 実装 + - [ ] Box 構造定義 + - [ ] パーサ実装 + - [ ] エスケープシーケンス対応 +- [ ] Task 3: hako_check への導入 + - [ ] analysis_consumer.hako 修正 + - [ ] 手書きパーサ削除 + - [ ] 行数削減確認(500+ → 210) +- [ ] Task 4: テスト & スモーク + - [ ] JsonParserBox 単体テスト + - [ ] hako_check スモークテスト + - [ ] 回帰なし確認 +- [ ] Task 5: ドキュメント更新 + - [ ] phase170 に追記 + - [ ] hako_check_design.md 更新 + - [ ] CURRENT_TASK.md 追加 + - [ ] git commit + +--- + +## 技術的ポイント + +### JSON 解析の状態機械 + +``` +parse_value: + | "null" → parse_null + | "true"|"false" → parse_bool + | digit → parse_number + | '"' → parse_string + | '[' → parse_array + | '{' → parse_object + | else → error (null) +``` + +### ArrayBox / MapBox との互換性 + +```hako +// JsonArrayBox.get() は ArrayBox.get() と同じ感覚で +local elem = json_array.get(0) + +// JsonObjectBox.get() は MapBox.get() と同じ感覚で +local val = json_obj.get("key") +``` + +### エラーハンドリング + +- 構文エラー → null を返す(Nyash 的な nil) +- 期待値と異なる型 → null を返す + +### パフォーマンス + +Phase 171 MVP は正確性 > 速度を優先。最適化は Phase 173+ へ。 + +--- + +**作成日**: 2025-12-04 +**Phase**: 171(JsonParserBox 実装 & hako_check 導入) +**予定工数**: 3-4 時間 +**難易度**: 中(JSON パーサ実装 + .hako での Box 設計) +**期待削減**: hako_check 行数 60%、コード共通化 100% diff --git a/docs/development/current/main/phase172_jsonparserbox_expansion.md b/docs/development/current/main/phase172_jsonparserbox_expansion.md new file mode 100644 index 00000000..5a651a70 --- /dev/null +++ b/docs/development/current/main/phase172_jsonparserbox_expansion.md @@ -0,0 +1,491 @@ +# Phase 172: JsonParserBox の再利用拡大(Stage-B / selfhost / ツール統合) + +## 0. ゴール + +**Phase 171 で実装した JsonParserBox を、hako_check 以外の「JSON を読む .hako コード」にも適用し、JSON パーサ断片をできるだけ共通箱に集約する。** + +目的: +- Program(JSON v0) を読む Stage-B/selfhost 補助で JsonParserBox を使用 +- 将来の解析ツールが全部この箱を経由する状態を作る +- JSON 処理の単一の真実(SSOT)を確立 + +--- + +## 1. 背景と戦略 + +### Phase 171 での成果 + +- ✅ JsonParserBox 実装完了(454行) +- ✅ hako_check (HC020) で使用開始 +- ✅ 289行の手書きパーサを削除(96%削減) + +### Phase 172 の戦略 + +**再利用拡大の3ステップ**: + +1. **再利用候補の特定**: Program(JSON v0) を読む箇所を洗い出し +2. **薄いラッパー追加**: `parse_program()` ヘルパーを JsonParserBox に追加 +3. **段階的適用**: Stage-B/selfhost の 1-2 箇所から開始 + +### selfhost depth-2 への準備 + +- JSON 処理が共通箱に集約されることで +- .hako JoinIR/MIR 移植時に構造が崩れにくくなる +- selfhost depth-2 の基盤が整う + +--- + +## 2. Scope / Non-scope + +### ✅ やること + +1. **再利用候補の棚卸し(再スキャン)** + - Stage-B/selfhost での JSON 利用箇所を洗い出し + - 現状の JSON 読み取り方法を調査 + - JsonParserBox 適用可否を評価 + +2. **Program(JSON v0) 用ラッパー追加** + - `parse_program()` メソッドを JsonParserBox に追加 + - ProgramJSONBox(薄いヘルパー型)を定義 + - Program JSON の定形構造に対応 + +3. **Stage-B/selfhost への適用** + - 1-2 箇所から段階的に適用 + - 自前 substring 処理を JsonParserBox に置き換え + +4. **hako_check 仕上げ** + - Phase 171 の残りタスク完了確認 + - JsonParserBox API の十分性確認 + +5. **テスト & 回帰** + - hako_check: HC019/HC020 スモーク + - Stage-B/selfhost: selfhost_phase150_depth1_smoke + - JsonParserBox: 単体テスト + +6. **ドキュメント更新** + +### ❌ やらないこと + +- to_json() / serialization(Phase 173+) +- スキーマ検証(Phase 173+) +- 全 JSON 利用箇所の一括置き換え(段階的に) + +--- + +## 3. Task 1: 再利用候補の棚卸し(再スキャン) + +### 対象候補 + +1. **Stage-B/selfhost での JSON 利用** + - `lang/src/compiler/entry/compiler.hako` - Program JSON 処理 + - `apps/selfhost-vm/json_loader.hako` - JSON ローダー + - `tools/selfhost/*` - selfhost ツール群 + +2. **その他ツール・テスト** + - `apps/tests/` 内の JSON を扱うテスト + - 開発ツールの JSON 解析コード + +### やること + +1. **Program(JSON v0) 消費箇所の洗い出し** + ```bash + rg '"version".*0' lang tools apps + rg 'Program.*JSON' lang tools apps + rg 'substring.*\{' lang tools apps # 自前 JSON 解析の疑い + ``` + +2. **各箇所の評価** + | ファイル | 現状の読み方 | JsonParserBox 適用可否 | 優先度 | + |---------|-------------|---------------------|--------| + | lang/src/compiler/entry/compiler.hako | 自前 split/substring | ✅ 可能 | 高 | + | apps/selfhost-vm/json_loader.hako | 自前 substring | ✅ 可能 | 中 | + | tools/selfhost/helpers.hako | まだ読んでいない | ⏸️ 将来 | 低 | + +3. **phase170_hako_json_library_design.md に追記** + - 「Phase 172 対象候補」セクションを追加 + - 評価結果を表にまとめる + +### 成果物 + +- 再利用候補リスト(優先度付き) +- 各候補の評価結果 + +--- + +## 4. Task 2: Program(JSON v0) 用の薄いラッパーを JsonParserBox に追加 + +### 目的 + +Program(JSON v0) の定形構造を読みやすくする + +```json +{ + "version": 0, + "kind": "Program", + "defs": [...], + "meta": { + "usings": [...] + } +} +``` + +### やること + +1. **JsonParserBox に Program JSON ヘルパーを追加** + +```hako +static box JsonParserBox { + // 既存 + method parse(json_str) { ... } + method parse_object(json_str) { ... } + method parse_array(json_str) { ... } + + // 新規(Phase 172) + 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 or kind != "Program" { return null } + + // ProgramJSONBox を返す + local prog = new ProgramJSONBox() + prog._obj = obj + return prog + } +} +``` + +2. **ProgramJSONBox の定義** + +```hako +// 薄いラッパー: Program JSON の構造に特化 +static box ProgramJSONBox { + _obj: MapBox # 内部で parse_object() の結果を保持 + + method get_version() { + return me._obj.get("version") + } + + method get_kind() { + return me._obj.get("kind") + } + + method get_defs() { + // ArrayBox を返す + return me._obj.get("defs") + } + + method get_meta() { + // MapBox を返す + return me._obj.get("meta") + } + + method get_usings() { + local meta = me.get_meta() + if meta == null { return null } + return meta.get("usings") + } +} +``` + +3. **実装場所** + - `tools/hako_shared/json_parser.hako` に追加 + - Phase 171 実装の拡張として統合 + +### 成果物 + +- `parse_program()` メソッド実装 +- ProgramJSONBox 型定義 + +--- + +## 5. Task 3: Stage-B / selfhost 補助から JsonParserBox に寄せる + +### 対象ファイル + +1. **第一候補**: `lang/src/compiler/entry/compiler.hako` + - Program(JSON v0) を読む主要箇所 + - 置き換え効果が大きい + +2. **第二候補**: `apps/selfhost-vm/json_loader.hako` + - JSON ローダー補助 + - 再利用性確認に適切 + +### やること + +1. **現状の JSON 読み取り処理を特定** + ```hako + // 修正前の例(自前 substring) + local json_str = file.read("program.json") + local version_pos = json_str.indexOf('"version"') + local defs_pos = json_str.indexOf('"defs"') + // ... 手作業の解析 ... + ``` + +2. **JsonParserBox を使用した実装に置き換え** + ```hako + // 修正後 + local json_str = file.read("program.json") + local prog = JsonParserBox.parse_program(json_str) + if prog == null { + print("[ERROR] Invalid Program JSON") + return null + } + + local version = prog.get_version() + local defs = prog.get_defs() + local usings = prog.get_usings() + // 構造化されたアクセス + ``` + +3. **段階的適用** + - 初回は 1 箇所のみ(lang/src/compiler/entry/compiler.hako) + - テスト成功後、2 箇所目(apps/selfhost-vm/json_loader.hako) + - 全箇所の一括置き換えはしない + +### 成果物 + +- 修正済み `lang/src/compiler/entry/compiler.hako` +- 修正済み `apps/selfhost-vm/json_loader.hako`(オプション) + +--- + +## 6. Task 4: hako_check 側の JsonParserBox 利用を仕上げる + +### やること + +1. **Phase 171 の残りタスク確認** + - `analysis_consumer.hako` から手書き JSON パーサが完全に削除されているか + - `rule_dead_blocks.hako` が JsonParserBox を正しく使用しているか + +2. **未置き換えコードの確認** + ```bash + rg 'substring.*\{' tools/hako_check/ + rg 'indexOf.*\"' tools/hako_check/ + ``` + +3. **JsonParserBox API の十分性確認** + - hako_check が必要とする API は揃っているか + - 追加が必要なメソッドはあるか + - 重い拡張は Phase 173 backlog へ + +### 成果物 + +- hako_check における JsonParserBox 利用の完成 +- API 拡張候補リスト(Phase 173 へ) + +--- + +## 7. Task 5: テストと回帰チェック + +### テストマトリックス + +| 対象 | テストスクリプト | 確認内容 | +|------|---------------|---------| +| hako_check HC019 | `tools/hako_check_deadcode_smoke.sh` | dead code 検出(回帰なし) | +| hako_check HC020 | `tools/hako_check_deadblocks_smoke.sh` | dead block 検出(回帰なし) | +| Stage-B/selfhost | `tools/selfhost/selfhost_phase150_depth1_smoke.sh` | Program JSON 読み込み動作 | +| JsonParserBox | `tools/hako_shared/tests/json_parser_simple_test.hako` | 単体テスト全 PASS | + +### やること + +1. **hako_check スモークテスト** + ```bash + ./tools/hako_check_deadcode_smoke.sh + ./tools/hako_check_deadblocks_smoke.sh + ``` + 期待: HC019/HC020 の出力が変わらない + +2. **Stage-B/selfhost スモークテスト** + ```bash + ./tools/selfhost/selfhost_phase150_depth1_smoke.sh + ``` + 期待: Program JSON 読み込みで回帰なし + +3. **JsonParserBox 単体テスト** + ```bash + NYASH_USE_NY_COMPILER=1 ./target/release/hakorune tools/hako_shared/tests/json_parser_simple_test.hako + ``` + 期待: 全テスト PASS + +### 成果物 + +- 全スモークテスト成功 +- 回帰なし確認 + +--- + +## 8. Task 6: ドキュメント & CURRENT_TASK 更新 + +### ドキュメント更新 + +1. **phase170_hako_json_library_design.md に追記** + ```markdown + ## Phase 172 実装結果 + + ✅ JsonParserBox 再利用拡大完了 + - Program(JSON v0) 対応: parse_program() + ProgramJSONBox + - Stage-B 統合: lang/src/compiler/entry/compiler.hako + - selfhost 統合: apps/selfhost-vm/json_loader.hako + - hako_check 仕上げ: Phase 171 の残りタスク完了 + + 📊 統合実績: + - hako_check: HC019/HC020 で使用(Phase 171) + - Stage-B: Program JSON 読み込みで使用(Phase 172) + - selfhost: JSON ローダーで使用(Phase 172) + + 📋 未統合箇所(Phase 173+ 候補): + - tools/selfhost/helpers.hako(低優先度) + - apps/tests/ 内の一部テスト + ``` + +2. **hako_check_design.md / selfhost 関連 docs を更新** + ```markdown + ### JSON 解析の実装場所 + + **Phase 172 から全て JsonParserBox に集約**: + - 場所: tools/hako_shared/json_parser.hako + - 利用者: + - hako_check: MIR/CFG JSON 解析 + - Stage-B: Program(JSON v0) 読み込み + - selfhost: JSON ローディング + - 単一の真実(SSOT)確立 + ``` + +3. **CURRENT_TASK.md に Phase 172 セクション追加** + ```markdown + ### Phase 172: JsonParserBox 再利用拡大 ✅ + + **完了内容**: + - Program(JSON v0) 対応: parse_program() + ProgramJSONBox + - Stage-B 統合: lang/src/compiler/entry/compiler.hako で使用開始 + - selfhost 統合: apps/selfhost-vm/json_loader.hako で使用開始 + - hako_check 仕上げ: Phase 171 の残りタスク完了 + + **成果**: + - JSON 処理の単一の真実(SSOT)確立 + - hako_check/Stage-B/selfhost が同じ箱を使用 + - selfhost depth-2 への基盤整備完了 + + **次フェーズ**: Phase 173 で to_json() 逆変換、Phase 160+ で .hako JoinIR/MIR 移植 + ``` + +### git commit + +``` +feat(json): Phase 172 JsonParserBox reuse expansion + +✨ JsonParserBox を Stage-B/selfhost/ツールに統合! + +🎯 Program(JSON v0) 対応: +- parse_program() メソッド追加 +- ProgramJSONBox 薄いヘルパー定義 +- version/kind/defs/meta/usings アクセサ + +📦 統合実績: +- lang/src/compiler/entry/compiler.hako: Program JSON 読み込み +- apps/selfhost-vm/json_loader.hako: JSON ローダー +- tools/hako_check/: Phase 171 仕上げ完了 + +✅ テスト: +- hako_check スモーク: HC019/HC020 回帰なし +- Stage-B/selfhost スモーク: Program JSON 読み込み OK +- JsonParserBox 単体テスト: 全 PASS + +🏗️ SSOT 確立: +- JSON 処理が JsonParserBox に集約 +- hako_check/Stage-B/selfhost が同じ箱を使用 +- selfhost depth-2 への基盤完成 +``` + +--- + +## ✅ 完成チェックリスト(Phase 172) + +- [ ] Task 1: 再利用候補の棚卸し + - [ ] Program(JSON v0) 消費箇所の洗い出し + - [ ] 各箇所の評価(適用可否・優先度) + - [ ] phase170 に追記 +- [ ] Task 2: Program(JSON v0) ラッパー追加 + - [ ] parse_program() 実装 + - [ ] ProgramJSONBox 定義 + - [ ] json_parser.hako に統合 +- [ ] Task 3: Stage-B/selfhost への適用 + - [ ] lang/src/compiler/entry/compiler.hako 修正 + - [ ] apps/selfhost-vm/json_loader.hako 修正(オプション) + - [ ] 自前 substring 処理を削除 +- [ ] Task 4: hako_check 仕上げ + - [ ] Phase 171 残りタスク確認 + - [ ] API 十分性確認 + - [ ] 拡張候補リスト作成 +- [ ] Task 5: テスト & 回帰 + - [ ] hako_check スモーク + - [ ] Stage-B/selfhost スモーク + - [ ] JsonParserBox 単体テスト +- [ ] Task 6: ドキュメント更新 + - [ ] phase170 に追記 + - [ ] hako_check_design.md 更新 + - [ ] selfhost 関連 docs 更新 + - [ ] CURRENT_TASK.md 追加 + - [ ] git commit + +--- + +## 技術的ポイント + +### Program(JSON v0) の構造 + +```json +{ + "version": 0, + "kind": "Program", + "defs": [ + {"kind": "Box", "name": "Main", ...}, + {"kind": "Method", "name": "main", ...} + ], + "meta": { + "usings": ["nyashstd", "mylib"] + } +} +``` + +### ProgramJSONBox の使い方 + +```hako +local prog = JsonParserBox.parse_program(json_str) +if prog == null { + print("[ERROR] Invalid Program JSON") + return +} + +// 型安全なアクセス +local version = prog.get_version() // Integer +local defs = prog.get_defs() // ArrayBox +local usings = prog.get_usings() // ArrayBox? + +// defs をループ +for def in defs { + local kind = def.get("kind") + if kind == "Box" { + local name = def.get("name") + print("Box: " + name) + } +} +``` + +### 段階的適用の重要性 + +- 一気に全箇所を変更しない +- 1 箇所 → テスト → 2 箇所目 の順序 +- 問題があれば早期発見 + +--- + +**作成日**: 2025-12-04 +**Phase**: 172(JsonParserBox 再利用拡大) +**予定工数**: 2-3 時間 +**難易度**: 低-中(既存実装の適用 + 薄いラッパー追加) +**期待効果**: JSON 処理 SSOT 確立、selfhost depth-2 基盤完成 diff --git a/tools/hako_shared/tests/json_parser_simple_test.hako b/tools/hako_shared/tests/json_parser_simple_test.hako new file mode 100644 index 00000000..19a051af --- /dev/null +++ b/tools/hako_shared/tests/json_parser_simple_test.hako @@ -0,0 +1,415 @@ +// tools/hako_shared/tests/json_parser_simple_test.hako - Simple standalone tests +// Test JsonParserBox without using statement + +static box JsonParserBox { + // Copy of parse method for testing + method parse(json_str) { + if json_str == null { return null } + local s = me._trim(json_str) + if s.length() == 0 { return null } + local result = me._parse_value(s, 0) + if result == null { return null } + return result.get("value") + } + + method parse_object(json_str) { + local val = me.parse(json_str) + if val == null { return null } + local test = val.get + if test == null { return null } + return val + } + + method parse_array(json_str) { + local val = me.parse(json_str) + if val == null { return null } + local test = val.size + if test == null { return null } + return val + } + + _parse_value(s, pos) { + local p = me._skip_whitespace(s, pos) + if p >= s.length() { return null } + local ch = s.substring(p, p+1) + + if ch == "n" { + if me._match_literal(s, p, "null") { + local result = new MapBox() + result.set("value", null) + result.set("pos", p + 4) + return result + } + return null + } + + if ch == "t" { + if me._match_literal(s, p, "true") { + local result = new MapBox() + result.set("value", 1) + result.set("pos", p + 4) + return result + } + return null + } + + if ch == "f" { + if me._match_literal(s, p, "false") { + local result = new MapBox() + result.set("value", 0) + result.set("pos", p + 5) + return result + } + return null + } + + if ch == "-" || (ch >= "0" && ch <= "9") { + return me._parse_number(s, p) + } + + if ch == '"' { + return me._parse_string(s, p) + } + + if ch == "[" { + return me._parse_array(s, p) + } + + if ch == "{" { + return me._parse_object(s, p) + } + + return null + } + + _parse_number(s, pos) { + local num_str = "" + local p = pos + if p < s.length() { + local ch = s.substring(p, p+1) + if ch == "-" { + num_str = num_str + ch + p = p + 1 + } + } + while p < s.length() { + local ch = s.substring(p, p+1) + if ch >= "0" && ch <= "9" { + num_str = num_str + ch + p = p + 1 + } else { + break + } + } + if num_str == "" || num_str == "-" { return null } + local result = new MapBox() + result.set("value", me._atoi(num_str)) + result.set("pos", p) + return result + } + + _parse_string(s, pos) { + if s.substring(pos, pos+1) != '"' { return null } + local p = pos + 1 + local str = "" + while p < s.length() { + local ch = s.substring(p, p+1) + if ch == '"' { + local result = new MapBox() + result.set("value", me._unescape_string(str)) + result.set("pos", p + 1) + return result + } + if ch == "\\" { + if p + 1 < s.length() { + str = str + ch + p = p + 1 + str = str + s.substring(p, p+1) + p = p + 1 + continue + } + return null + } + 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) + if p < s.length() { + if s.substring(p, p+1) == "]" { + local result = new MapBox() + result.set("value", arr) + result.set("pos", p + 1) + return result + } + } + while 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) + 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) + if p < s.length() { + if s.substring(p, p+1) == "}" { + local result = new MapBox() + result.set("value", obj) + result.set("pos", p + 1) + return result + } + } + while p < s.length() { + p = me._skip_whitespace(s, p) + 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) + if p >= s.length() { return null } + if s.substring(p, p+1) != ":" { return null } + p = p + 1 + p = me._skip_whitespace(s, p) + 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) + return result + } + if ch == "," { + p = p + 1 + continue + } + return null + } + return null + } + + _skip_whitespace(s, pos) { + local p = pos + while 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() + while start < end { + local ch = s.substring(start, start+1) + if ch == " " || ch == "\t" || ch == "\n" || ch == "\r" { + start = start + 1 + } else { + break + } + } + while 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 + while 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 + while i < s.length() { + local ch = s.substring(i, i+1) + if ch == "\\" && i + 1 < s.length() { + 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 + } + 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 + if s.substring(0, 1) == "-" { + negative = 1 + i = 1 + } + local digits = "0123456789" + while 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 + } +} + +static box Main { + main(args) { + print("=== JsonParserBox Basic Tests ===") + + // Test 1: null + print("Test 1: null") + local n = JsonParserBox.parse("null") + if n == null { + print(" PASS: null parsed correctly") + } else { + print(" FAIL: expected null") + } + + // Test 2: boolean + print("Test 2: boolean") + local t = JsonParserBox.parse("true") + if t == 1 { + print(" PASS: true parsed correctly") + } else { + print(" FAIL: expected 1, got something else") + } + + // Test 3: number + print("Test 3: number") + local num = JsonParserBox.parse("123") + if num == 123 { + print(" PASS: number parsed correctly") + } else { + print(" FAIL: expected 123") + } + + // Test 4: string + print("Test 4: string") + local str = JsonParserBox.parse('"hello"') + if str == "hello" { + print(" PASS: string parsed correctly") + } else { + print(" FAIL: expected 'hello'") + } + + // Test 5: array + print("Test 5: array") + local arr = JsonParserBox.parse("[1, 2, 3]") + if arr != null && arr.size() == 3 { + local e0 = arr.get(0) + if e0 == 1 { + print(" PASS: array parsed correctly") + } else { + print(" FAIL: first element should be 1") + } + } else { + print(" FAIL: array should have 3 elements") + } + + // Test 6: object + print("Test 6: object") + local obj = JsonParserBox.parse('{"key": "value"}') + if obj != null { + local val = obj.get("key") + if val == "value" { + print(" PASS: object parsed correctly") + } else { + print(" FAIL: key should have value 'value'") + } + } else { + print(" FAIL: object should not be null") + } + + print("=== All Basic Tests Complete ===") + return 0 + } +} diff --git a/tools/hako_shared/tests/json_parser_test.hako b/tools/hako_shared/tests/json_parser_test.hako new file mode 100644 index 00000000..4db7d9d7 --- /dev/null +++ b/tools/hako_shared/tests/json_parser_test.hako @@ -0,0 +1,242 @@ +// tools/hako_shared/tests/json_parser_test.hako - JsonParserBox Unit Tests +// Phase 171 MVP test suite + +using tools.hako_shared.json_parser as JsonParserBox + +static box JsonParserTestBox { + passed + failed + + birth() { + me.passed = 0 + me.failed = 0 + } + + method main(args) { + me.test_null() + me.test_bool() + me.test_number() + me.test_string() + me.test_array() + me.test_object() + me.test_nested() + me.test_error_cases() + + print("========================================") + print("JsonParserBox Test Results:") + print(" Passed: " + me._itoa(me.passed)) + print(" Failed: " + me._itoa(me.failed)) + + if me.failed == 0 { + print("All tests PASSED!") + return 0 + } else { + print("Some tests FAILED") + return 1 + } + } + + method test_null() { + print("Test: null parsing") + local val = JsonParserBox.parse("null") + me._assert(val != null, "null should parse") + me._assert(val.is_null(), "should be null type") + print(" OK") + } + + method test_bool() { + print("Test: boolean parsing") + + local t = JsonParserBox.parse("true") + me._assert(t != null, "true should parse") + me._assert(t.is_bool(), "should be bool type") + me._assert(t.boolVal == 1, "true should have value 1") + + local f = JsonParserBox.parse("false") + me._assert(f != null, "false should parse") + me._assert(f.is_bool(), "should be bool type") + me._assert(f.boolVal == 0, "false should have value 0") + + print(" OK") + } + + method test_number() { + print("Test: number parsing") + + local n1 = JsonParserBox.parse("123") + me._assert(n1 != null, "positive number should parse") + me._assert(n1.is_number(), "should be number type") + me._assert(n1.numVal == 123, "should have value 123") + + local n2 = JsonParserBox.parse("-456") + me._assert(n2 != null, "negative number should parse") + me._assert(n2.is_number(), "should be number type") + me._assert(n2.numVal == 0 - 456, "should have value -456") + + local n3 = JsonParserBox.parse("0") + me._assert(n3 != null, "zero should parse") + me._assert(n3.numVal == 0, "should have value 0") + + print(" OK") + } + + method test_string() { + print("Test: string parsing") + + local s1 = JsonParserBox.parse('"hello"') + me._assert(s1 != null, "simple string should parse") + me._assert(s1.is_string(), "should be string type") + me._assert(s1.strVal == "hello", "should have value 'hello'") + + local s2 = JsonParserBox.parse('""') + me._assert(s2 != null, "empty string should parse") + me._assert(s2.strVal == "", "should be empty") + + // Test escape sequences + local s3 = JsonParserBox.parse('"hello\\nworld"') + me._assert(s3 != null, "string with newline escape should parse") + me._assert(s3.strVal.indexOf("\n") >= 0, "should contain newline") + + print(" OK") + } + + method test_array() { + print("Test: array parsing") + + local arr1 = JsonParserBox.parse("[]") + me._assert(arr1 != null, "empty array should parse") + me._assert(arr1.is_array(), "should be array type") + me._assert(arr1.arrVal.length() == 0, "should be empty") + + local arr2 = JsonParserBox.parse("[1, 2, 3]") + me._assert(arr2 != null, "number array should parse") + me._assert(arr2.is_array(), "should be array type") + me._assert(arr2.arrVal.length() == 3, "should have 3 elements") + + local elem0 = arr2.arrVal.get(0) + me._assert(elem0 != null, "element 0 should exist") + me._assert(elem0.is_number(), "element 0 should be number") + me._assert(elem0.numVal == 1, "element 0 should be 1") + + local arr3 = JsonParserBox.parse('["a", "b", "c"]') + me._assert(arr3 != null, "string array should parse") + me._assert(arr3.arrVal.length() == 3, "should have 3 elements") + + print(" OK") + } + + method test_object() { + print("Test: object parsing") + + local obj1 = JsonParserBox.parse("{}") + me._assert(obj1 != null, "empty object should parse") + me._assert(obj1.is_object(), "should be object type") + me._assert(obj1.objVal.size() == 0, "should be empty") + + local obj2 = JsonParserBox.parse('{"key": "value"}') + me._assert(obj2 != null, "simple object should parse") + me._assert(obj2.is_object(), "should be object type") + + local val = obj2.objVal.get("key") + me._assert(val != null, "key should exist") + me._assert(val.is_string(), "value should be string") + me._assert(val.strVal == "value", "value should be 'value'") + + local obj3 = JsonParserBox.parse('{"a": 1, "b": 2, "c": 3}') + me._assert(obj3 != null, "multiple key object should parse") + me._assert(obj3.objVal.size() == 3, "should have 3 keys") + + local a = obj3.objVal.get("a") + me._assert(a != null && a.numVal == 1, "a should be 1") + + print(" OK") + } + + method test_nested() { + print("Test: nested structures") + + local nested1 = JsonParserBox.parse('{"arr": [1, 2, 3]}') + me._assert(nested1 != null, "object with array should parse") + + local arr_val = nested1.objVal.get("arr") + me._assert(arr_val != null, "arr key should exist") + me._assert(arr_val.is_array(), "arr should be array") + me._assert(arr_val.arrVal.length() == 3, "arr should have 3 elements") + + local nested2 = JsonParserBox.parse('[{"id": 1}, {"id": 2}]') + me._assert(nested2 != null, "array of objects should parse") + me._assert(nested2.arrVal.length() == 2, "should have 2 objects") + + local obj0 = nested2.arrVal.get(0) + me._assert(obj0.is_object(), "element 0 should be object") + + local id0 = obj0.objVal.get("id") + me._assert(id0.numVal == 1, "first object id should be 1") + + print(" OK") + } + + method test_error_cases() { + print("Test: error handling") + + local err1 = JsonParserBox.parse("{") + me._assert(err1 == null, "incomplete object should fail") + + local err2 = JsonParserBox.parse("[1,2,]") + me._assert(err2 == null, "trailing comma should fail") + + local err3 = JsonParserBox.parse('{"key": }') + me._assert(err3 == null, "missing value should fail") + + local err4 = JsonParserBox.parse("") + me._assert(err4 == null, "empty string should fail") + + print(" OK") + } + + // Test utilities + _assert(condition, message) { + if condition { + me.passed = me.passed + 1 + } else { + me.failed = me.failed + 1 + print(" FAIL: " + message) + } + } + + _itoa(n) { + local v = 0 + n + if v == 0 { return "0" } + + local out = "" + local digits = "0123456789" + local tmp = "" + local negative = 0 + + if v < 0 { + negative = 1 + v = 0 - v + } + + while v > 0 { + local d = v % 10 + tmp = digits.substring(d, d+1) + tmp + v = v / 10 + } + + if negative == 1 { + out = "-" + tmp + } else { + out = tmp + } + + return out + } +} + +static box JsonParserTestMain { + main(args) { + local test = new JsonParserTestBox() + return test.main(args) + } +}