788 lines
16 KiB
Markdown
788 lines
16 KiB
Markdown
# Phase 32: Hakorune Static Analyzer - 実装計画(改訂版)
|
||
|
||
**期間**: 2025-10-20 → 2025-10-27 (7日間)
|
||
**配置**: selfhost/analyzer/(Ring-1: Meta/自己ホスト)
|
||
**構造**: 責務分離(driver/ast_scan/mir_scan/report)
|
||
|
||
---
|
||
|
||
## Day 1: スケルトン作成 + README (2025-10-20)
|
||
|
||
### 🎯 目標
|
||
- ディレクトリ構造作成
|
||
- 4つのBoxスケルトン
|
||
- hako_module.toml設定
|
||
- README.md(責務/入出力/ENV)
|
||
|
||
### タスク
|
||
|
||
#### ディレクトリ作成 (10分)
|
||
```bash
|
||
mkdir -p selfhost/analyzer/{boxes,smokes}
|
||
```
|
||
|
||
#### hako_module.toml作成 (20分)
|
||
```toml
|
||
# selfhost/analyzer/hako_module.toml
|
||
|
||
[package]
|
||
name = "selfhost.analyzer"
|
||
version = "0.1.0"
|
||
|
||
[exports]
|
||
driver = "boxes/driver_box.hako"
|
||
ast_scan = "boxes/ast_scan_box.hako"
|
||
mir_scan = "boxes/mir_scan_box.hako"
|
||
report = "boxes/report_box.hako"
|
||
|
||
[dependencies]
|
||
BoxHelpers = "selfhost.shared.box_helpers"
|
||
MirSchema = "selfhost.shared.mir.schema"
|
||
JsonEmit = "selfhost.shared.json.emit"
|
||
```
|
||
|
||
#### driver_box.hako スケルトン (30分)
|
||
```hako
|
||
# selfhost/analyzer/boxes/driver_box.hako
|
||
|
||
using selfhost.analyzer.mir_scan as MirScanBox
|
||
using selfhost.analyzer.report as ReportBox
|
||
|
||
static box DriverBox {
|
||
birth() {
|
||
# 空のbirth(static boxなので状態なし)
|
||
}
|
||
|
||
# 文字列入力→文字列出力
|
||
run_from_string(input, kind) {
|
||
# TODO: Day 2-3で実装
|
||
return "[DriverBox] Not implemented yet"
|
||
}
|
||
|
||
# MapBox入力→MapBox出力
|
||
run_from_box(input, kind) {
|
||
# TODO: Day 2-3で実装
|
||
local result
|
||
result = new MapBox()
|
||
return result
|
||
}
|
||
}
|
||
```
|
||
|
||
#### mir_scan_box.hako スケルトン (30分)
|
||
```hako
|
||
# selfhost/analyzer/boxes/mir_scan_box.hako
|
||
|
||
using selfhost.shared.box_helpers as BoxHelpers
|
||
using selfhost.shared.mir.schema as MirSchema
|
||
|
||
static box MirScanBox {
|
||
birth() {
|
||
# 空のbirth
|
||
}
|
||
|
||
# MIR JSON → 関数リスト抽出
|
||
extract_functions(mir_json) {
|
||
# TODO: Day 2で実装
|
||
local result
|
||
result = new ArrayBox()
|
||
return result
|
||
}
|
||
|
||
# 呼び出しグラフ構築
|
||
build_call_graph(mir_json) {
|
||
# TODO: Day 3で実装
|
||
local result
|
||
result = new MapBox()
|
||
return result
|
||
}
|
||
|
||
# BFS到達可能性分析
|
||
find_dead_code(call_graph, entry_points) {
|
||
# TODO: Day 4で実装
|
||
local result
|
||
result = new ArrayBox()
|
||
return result
|
||
}
|
||
}
|
||
```
|
||
|
||
#### report_box.hako スケルトン (20分)
|
||
```hako
|
||
# selfhost/analyzer/boxes/report_box.hako
|
||
|
||
static box ReportBox {
|
||
birth() {
|
||
# 空のbirth
|
||
}
|
||
|
||
# 診断サマリ文字列生成
|
||
generate_text_report(dead_functions, all_functions) {
|
||
# TODO: Day 5で実装
|
||
return "[ReportBox] Analysis complete"
|
||
}
|
||
|
||
# MapBox診断生成
|
||
generate_map_report(dead_functions, reachable_functions) {
|
||
# TODO: Day 5で実装
|
||
local result
|
||
result = new MapBox()
|
||
return result
|
||
}
|
||
}
|
||
```
|
||
|
||
#### ast_scan_box.hako スケルトン (10分)
|
||
```hako
|
||
# selfhost/analyzer/boxes/ast_scan_box.hako
|
||
|
||
static box AstScanBox {
|
||
birth() {
|
||
# 空のbirth
|
||
}
|
||
|
||
# AST走査(Phase 32外、将来実装)
|
||
scan(ast_json) {
|
||
return "[AstScanBox] Not implemented in Phase 32"
|
||
}
|
||
}
|
||
```
|
||
|
||
#### README.md作成 (40分)
|
||
```markdown
|
||
# selfhost/analyzer - Hakorune Static Analyzer
|
||
|
||
## 責務
|
||
|
||
HakoruneでHakoruneコードを解析する静的解析ツール(Phase 32: デッドコード検出MVP)
|
||
|
||
## 入出力
|
||
|
||
### 入力
|
||
- MIR JSON文字列(Gate-C Strict前提)
|
||
- またはMapBox(MIR構造)
|
||
|
||
### 出力
|
||
- テキストレポート(診断サマリ)
|
||
- またはMapBox(構造化診断)
|
||
|
||
## ENV変数
|
||
|
||
```bash
|
||
# 追加チェックON
|
||
NYASH_ANALYZER_STRICT=1
|
||
|
||
# 将来の切替(既定=mir)
|
||
HAKO_ANALYZER_ENTRY=ast|mir
|
||
|
||
# Gate-C/AST Strict(既定ON)
|
||
NYASH_GATE_C_STRICT=1
|
||
NYASH_AST_STRICT=1
|
||
```
|
||
|
||
## 非対象
|
||
|
||
- 外部I/O(ファイル読み書き)
|
||
- プラグイン化(当面VM経由のみ)
|
||
- CLI(将来tools/に追加可能)
|
||
|
||
## 使用例
|
||
|
||
```hako
|
||
using selfhost.analyzer.driver as DriverBox
|
||
|
||
local mir_json = "{ ... }" // MIR JSON文字列
|
||
local result = DriverBox.run_from_string(mir_json, "mir")
|
||
print(result)
|
||
```
|
||
```
|
||
|
||
### 成果物
|
||
- ✅ selfhost/analyzer/hako_module.toml: 20行
|
||
- ✅ driver_box.hako: 30行
|
||
- ✅ mir_scan_box.hako: 40行
|
||
- ✅ report_box.hako: 25行
|
||
- ✅ ast_scan_box.hako: 15行
|
||
- ✅ README.md: 50行
|
||
- **合計**: 180行
|
||
|
||
### 完了基準
|
||
- [ ] 全ファイル作成完了
|
||
- [ ] hako_module.toml が正しく読み込まれる
|
||
- [ ] スケルトンBoxがインポート可能
|
||
|
||
---
|
||
|
||
## Day 2: MIR走査基盤(関数抽出) (2025-10-21)
|
||
|
||
### 🎯 目標
|
||
- extract_functions 実装
|
||
- MIR JSON → 関数リスト変換
|
||
- 基本的な走査パターン確立
|
||
|
||
### タスク
|
||
|
||
#### extract_functions 実装 (2時間)
|
||
```hako
|
||
# selfhost/analyzer/boxes/mir_scan_box.hako
|
||
|
||
extract_functions(mir_json) {
|
||
local functions_key
|
||
functions_key = "functions"
|
||
|
||
local has_key
|
||
has_key = mir_json.has(functions_key)
|
||
|
||
if has_key == 0 {
|
||
print("[MirScanBox] ERROR: Missing 'functions' key")
|
||
return null
|
||
}
|
||
|
||
local functions_array
|
||
functions_array = mir_json.get(functions_key)
|
||
|
||
local result
|
||
result = new ArrayBox()
|
||
|
||
local i
|
||
i = 0
|
||
|
||
loop(i < functions_array.size()) {
|
||
local func
|
||
func = functions_array.get(i)
|
||
|
||
local name
|
||
name = me.extract_function_name(func)
|
||
|
||
if name != null {
|
||
result.push(name)
|
||
}
|
||
|
||
i = i + 1
|
||
}
|
||
|
||
return result
|
||
}
|
||
|
||
extract_function_name(func_obj) {
|
||
local name_key
|
||
name_key = "name"
|
||
|
||
local has_name
|
||
has_name = func_obj.has(name_key)
|
||
|
||
if has_name == 0 {
|
||
return null
|
||
}
|
||
|
||
local name
|
||
name = func_obj.get(name_key)
|
||
|
||
local arity_key
|
||
arity_key = "arity"
|
||
|
||
local arity
|
||
arity = func_obj.get(arity_key)
|
||
|
||
# "Main.main/0" 形式
|
||
local slash
|
||
slash = "/"
|
||
|
||
local arity_str
|
||
arity_str = arity.to_string()
|
||
|
||
local qualified
|
||
qualified = name.concat(slash)
|
||
qualified = qualified.concat(arity_str)
|
||
|
||
return qualified
|
||
}
|
||
```
|
||
|
||
#### テストケース作成 (1時間)
|
||
- 簡単なMIR JSON(2関数)
|
||
- 関数リスト抽出確認
|
||
|
||
### 成果物
|
||
- ✅ mir_scan_box.hako拡張: 100行
|
||
- ✅ テストケース: 50行
|
||
- **合計**: 150行
|
||
|
||
### 完了基準
|
||
- [ ] extract_functions が動作
|
||
- [ ] 実際のMIR JSONから関数リスト取得可能
|
||
|
||
---
|
||
|
||
## Day 3: 呼び出しグラフ構築 (2025-10-22)
|
||
|
||
### 🎯 目標
|
||
- build_call_graph 実装
|
||
- mir_call命令から呼び出し先抽出
|
||
- caller → [callees] マップ生成
|
||
|
||
### タスク
|
||
|
||
#### build_call_graph 実装 (3時間)
|
||
- 各関数のinstructionsをスキャン
|
||
- mir_call命令のcalleeを抽出
|
||
- MapBox(caller → ArrayBox<callee>)構築
|
||
|
||
#### Callee型パース実装 (1時間)
|
||
```hako
|
||
parse_callee(callee_obj) {
|
||
# "function:Main.helper/1"
|
||
# "extern:env.console.log"
|
||
# "constructor:ArrayBox"
|
||
local callee_type
|
||
callee_type = callee_obj.get("type")
|
||
|
||
if callee_type == "ModuleFunction" {
|
||
local module
|
||
module = callee_obj.get("module")
|
||
local func
|
||
func = callee_obj.get("function")
|
||
local arity
|
||
arity = callee_obj.get("arity")
|
||
|
||
local dot
|
||
dot = "."
|
||
local slash
|
||
slash = "/"
|
||
|
||
local result
|
||
result = module.concat(dot)
|
||
result = result.concat(func)
|
||
result = result.concat(slash)
|
||
|
||
local arity_str
|
||
arity_str = arity.to_string()
|
||
|
||
result = result.concat(arity_str)
|
||
return result
|
||
}
|
||
|
||
# TODO: Constructor/Extern対応
|
||
return null
|
||
}
|
||
```
|
||
|
||
### 成果物
|
||
- ✅ mir_scan_box.hako拡張: 150行
|
||
- ✅ テストケース: 100行
|
||
- **合計**: 250行
|
||
|
||
### 完了基準
|
||
- [ ] build_call_graph が動作
|
||
- [ ] 実際のMIR JSONから呼び出しグラフ生成可能
|
||
|
||
---
|
||
|
||
## Day 4: BFS到達可能性分析 (2025-10-23)
|
||
|
||
### 🎯 目標
|
||
- find_dead_code 実装
|
||
- BFS(幅優先探索)アルゴリズム
|
||
- デッドコード検出
|
||
|
||
### タスク
|
||
|
||
#### find_dead_code 実装 (2時間)
|
||
```hako
|
||
find_dead_code(call_graph, entry_points) {
|
||
local reachable
|
||
reachable = me.mark_reachable(call_graph, entry_points)
|
||
|
||
local all_funcs
|
||
all_funcs = call_graph.keys()
|
||
|
||
local dead
|
||
dead = new ArrayBox()
|
||
|
||
local i
|
||
i = 0
|
||
|
||
loop(i < all_funcs.size()) {
|
||
local func
|
||
func = all_funcs.get(i)
|
||
|
||
local is_reachable
|
||
is_reachable = reachable.has(func)
|
||
|
||
if is_reachable == 0 {
|
||
dead.push(func)
|
||
}
|
||
|
||
i = i + 1
|
||
}
|
||
|
||
return dead
|
||
}
|
||
|
||
mark_reachable(graph, entry_points) {
|
||
local visited
|
||
visited = new MapBox() # Set代わり
|
||
|
||
local queue
|
||
queue = new ArrayBox() # 簡易キュー
|
||
|
||
# エントリポイント追加
|
||
local i
|
||
i = 0
|
||
|
||
loop(i < entry_points.size()) {
|
||
local entry
|
||
entry = entry_points.get(i)
|
||
queue.push(entry)
|
||
|
||
i = i + 1
|
||
}
|
||
|
||
# BFS
|
||
loop(queue.size() > 0) {
|
||
local current
|
||
current = queue.get(0)
|
||
|
||
# 簡易shift(先頭削除の代わりに新しいArrayBoxを作る)
|
||
local new_queue
|
||
new_queue = new ArrayBox()
|
||
|
||
local j
|
||
j = 1
|
||
|
||
loop(j < queue.size()) {
|
||
local item
|
||
item = queue.get(j)
|
||
new_queue.push(item)
|
||
|
||
j = j + 1
|
||
}
|
||
|
||
queue = new_queue
|
||
|
||
local already_visited
|
||
already_visited = visited.has(current)
|
||
|
||
if already_visited == 1 {
|
||
continue # ✅ Phase 12.7で実装済み
|
||
}
|
||
|
||
visited.set(current, 1)
|
||
|
||
local callees
|
||
callees = graph.get(current)
|
||
|
||
if callees != null {
|
||
local ci
|
||
ci = 0
|
||
|
||
loop(ci < callees.size()) {
|
||
local callee
|
||
callee = callees.get(ci)
|
||
|
||
local callee_visited
|
||
callee_visited = visited.has(callee)
|
||
|
||
if callee_visited == 0 {
|
||
queue.push(callee)
|
||
}
|
||
|
||
ci = ci + 1
|
||
}
|
||
}
|
||
}
|
||
|
||
return visited
|
||
}
|
||
```
|
||
|
||
### 成果物
|
||
- ✅ mir_scan_box.hako完成: 200行
|
||
- ✅ テストケース: 100行
|
||
- **合計**: 300行
|
||
|
||
### 完了基準
|
||
- [ ] find_dead_code が動作
|
||
- [ ] BFS正確性確認(既知デッドコード検出)
|
||
|
||
---
|
||
|
||
## Day 5: レポート生成 (2025-10-24)
|
||
|
||
### 🎯 目標
|
||
- generate_text_report 実装
|
||
- generate_map_report 実装
|
||
- 人間可読・機械可読両対応
|
||
|
||
### タスク
|
||
|
||
#### generate_text_report 実装 (1.5時間)
|
||
```hako
|
||
generate_text_report(dead_functions, all_functions) {
|
||
local header
|
||
header = "Hakorune Dead Code Analyzer\n"
|
||
|
||
local separator
|
||
separator = "=============================\n"
|
||
|
||
local result
|
||
result = header.concat(separator)
|
||
|
||
local all_count
|
||
all_count = all_functions.size()
|
||
|
||
local all_str
|
||
all_str = all_count.to_string()
|
||
|
||
local all_line
|
||
all_line = "Functions found: "
|
||
all_line = all_line.concat(all_str)
|
||
all_line = all_line.concat("\n")
|
||
|
||
result = result.concat(all_line)
|
||
|
||
local dead_count
|
||
dead_count = dead_functions.size()
|
||
|
||
local dead_str
|
||
dead_str = dead_count.to_string()
|
||
|
||
local dead_line
|
||
dead_line = "Dead functions: "
|
||
dead_line = dead_line.concat(dead_str)
|
||
dead_line = dead_line.concat("\n")
|
||
|
||
result = result.concat(dead_line)
|
||
|
||
# 個別リスト表示
|
||
local i
|
||
i = 0
|
||
|
||
loop(i < dead_functions.size()) {
|
||
local func
|
||
func = dead_functions.get(i)
|
||
|
||
local prefix
|
||
prefix = " [DEAD] "
|
||
|
||
local line
|
||
line = prefix.concat(func)
|
||
line = line.concat("\n")
|
||
|
||
result = result.concat(line)
|
||
|
||
i = i + 1
|
||
}
|
||
|
||
return result
|
||
}
|
||
```
|
||
|
||
#### generate_map_report 実装 (1時間)
|
||
```hako
|
||
generate_map_report(dead_functions, reachable_functions) {
|
||
local result
|
||
result = new MapBox()
|
||
|
||
result.set("dead_functions", dead_functions)
|
||
result.set("reachable_functions", reachable_functions)
|
||
|
||
local dead_count
|
||
dead_count = dead_functions.size()
|
||
|
||
result.set("dead_count", dead_count)
|
||
|
||
local reachable_count
|
||
reachable_count = reachable_functions.size()
|
||
|
||
result.set("reachable_count", reachable_count)
|
||
|
||
return result
|
||
}
|
||
```
|
||
|
||
### 成果物
|
||
- ✅ report_box.hako完成: 120行
|
||
- **合計**: 120行
|
||
|
||
### 完了基準
|
||
- [ ] レポートが見やすい
|
||
- [ ] 統計情報が正確
|
||
|
||
---
|
||
|
||
## Day 6: スモークテスト整備 (2025-10-25)
|
||
|
||
### 🎯 目標
|
||
- 3本のスモークテスト作成
|
||
- happy path 2本 + 負例1本
|
||
- quick-lite/quick/integrationプロファイル対応
|
||
|
||
### タスク
|
||
|
||
#### happy path スモーク1 (1時間)
|
||
```bash
|
||
# selfhost/analyzer/smokes/analyzer_mir_gatec_ok_vm.sh
|
||
|
||
#!/bin/bash
|
||
set -euo pipefail
|
||
|
||
# Gate-C MIR JSON入力(正常系)
|
||
|
||
MIR_JSON='{"functions":[{"name":"Main.main","arity":0,"blocks":[]}]}'
|
||
|
||
RESULT=$(NYASH_GATE_C_STRICT=1 ./target/release/hakorune -c "
|
||
using selfhost.analyzer.driver as DriverBox
|
||
|
||
local mir = \"$MIR_JSON\"
|
||
local result = DriverBox.run_from_string(mir, \"mir\")
|
||
print(result)
|
||
")
|
||
|
||
if echo "$RESULT" | grep -q "Functions found: 1"; then
|
||
echo "✅ PASS: analyzer_mir_gatec_ok_vm"
|
||
exit 0
|
||
else
|
||
echo "❌ FAIL: Expected 'Functions found: 1'"
|
||
exit 1
|
||
fi
|
||
```
|
||
|
||
#### 負例スモーク (1時間)
|
||
```bash
|
||
# selfhost/analyzer/smokes/analyzer_bad_mir_fail_vm.sh
|
||
|
||
#!/bin/bash
|
||
set -euo pipefail
|
||
|
||
# 不正MIR: 余剰キー/型不一致
|
||
|
||
BAD_MIR='{"functions":[], "extra_key": "should fail"}'
|
||
|
||
set +e
|
||
RESULT=$(NYASH_GATE_C_STRICT=1 ./target/release/hakorune -c "
|
||
using selfhost.analyzer.driver as DriverBox
|
||
|
||
local mir = \"$BAD_MIR\"
|
||
local result = DriverBox.run_from_string(mir, \"mir\")
|
||
print(result)
|
||
" 2>&1)
|
||
EXIT_CODE=$?
|
||
set -e
|
||
|
||
if [ $EXIT_CODE -ne 0 ]; then
|
||
echo "✅ PASS: analyzer_bad_mir_fail_vm (expected failure)"
|
||
exit 0
|
||
else
|
||
echo "❌ FAIL: Should have failed on bad MIR"
|
||
exit 1
|
||
fi
|
||
```
|
||
|
||
#### プロファイル統合 (1時間)
|
||
- quick-lite: 負例のみ
|
||
- quick: 全3本
|
||
- integration: セルフホスト解析追加
|
||
|
||
### 成果物
|
||
- ✅ スモークテスト3本: 150行
|
||
- ✅ プロファイル設定: 30行
|
||
- **合計**: 180行
|
||
|
||
### 完了基準
|
||
- [ ] 全スモークテストPASS
|
||
- [ ] quick-liteで負例Fail-Fast確認
|
||
|
||
---
|
||
|
||
## Day 7: セルフホスト解析 + ドキュメント (2025-10-26)
|
||
|
||
### 🎯 目標
|
||
- セルフホストコードでの実用検証
|
||
- 精度95%以上確認
|
||
- ドキュメント整備
|
||
|
||
### タスク
|
||
|
||
#### セルフホスト解析 (2時間)
|
||
```bash
|
||
# selfhost/compiler/builder/rewrite/known.hako を解析
|
||
NYASH_GATE_C_STRICT=1 ./target/release/hakorune --emit-mir-json /tmp/known.json \
|
||
apps/selfhost-compiler/builder/rewrite/known.hako
|
||
|
||
# Analyzerで解析
|
||
./target/release/hakorune -c "
|
||
using selfhost.analyzer.driver as DriverBox
|
||
|
||
# /tmp/known.json の内容を読み込み(簡易版)
|
||
local result = DriverBox.run_from_string(mir_json, \"mir\")
|
||
print(result)
|
||
"
|
||
```
|
||
|
||
#### ドキュメント整備 (1時間)
|
||
- Phase 32 README.md 更新(実装完了マーク)
|
||
- PLAN.md 実績LOC記録
|
||
|
||
#### 精度検証 (30分)
|
||
- 手動でデッドコード候補確認
|
||
- 偽陽性/偽陰性チェック
|
||
|
||
### 成果物
|
||
- ✅ 実用検証レポート: 50行
|
||
- ✅ ドキュメント更新
|
||
- **合計**: 50行
|
||
|
||
### 完了基準
|
||
- [ ] セルフホストコードで95%以上の精度
|
||
- [ ] ドキュメント完備
|
||
- [ ] 1,000行ファイルを5秒以内で解析
|
||
|
||
---
|
||
|
||
## 📊 進捗トラッキング
|
||
|
||
| Day | 計画LOC | 実績LOC | 状態 | 備考 |
|
||
|-----|--------|--------|------|------|
|
||
| Day 1 | 180 | - | 🟡 | スケルトン + README |
|
||
| Day 2 | 150 | - | 🟡 | MIR走査基盤 |
|
||
| Day 3 | 250 | - | 🟡 | 呼び出しグラフ |
|
||
| Day 4 | 300 | - | 🟡 | BFS到達可能性 |
|
||
| Day 5 | 120 | - | 🟡 | レポート生成 |
|
||
| Day 6 | 180 | - | 🟡 | スモークテスト |
|
||
| Day 7 | 50 | - | 🟡 | セルフホスト解析 |
|
||
| **Total** | **1,230** | **-** | | |
|
||
|
||
---
|
||
|
||
## 🎯 成功基準(最終確認)
|
||
|
||
- [ ] **精度**: >95%(セルフホストコードで手動検証)
|
||
- [ ] **再現率**: >90%(既知デッドコード検出)
|
||
- [ ] **処理速度**: <5秒(1,000行ファイル)
|
||
- [ ] **使いやすさ**: 1コマンドで実行可能
|
||
- [ ] **テストカバレッジ**: 3件のスモークテスト全PASS
|
||
|
||
---
|
||
|
||
## 🚨 リスク & 対策
|
||
|
||
### リスク1: selfhost/shared依存の複雑さ
|
||
- **対策**: Day 1でimport確認、早期検証
|
||
- **バックアップ**: 簡易JSON解析を自前実装
|
||
|
||
### リスク2: BFS パフォーマンス
|
||
- **対策**: ArrayBox簡易キュー(小規模グラフで十分)
|
||
- **バックアップ**: 将来QueueBox追加可能
|
||
|
||
### リスク3: セルフホスト解析で想定外の問題
|
||
- **対策**: Day 7に余裕を持たせる
|
||
- **バックアップ**: 簡単な例で先に検証
|
||
|
||
---
|
||
|
||
**作成日**: 2025-10-19
|
||
**改訂日**: 2025-10-19(構造改訂版)
|
||
**開始予定**: 2025-10-20
|
||
**完了予定**: 2025-10-27
|