feat(mir): Phase 21.7 Step 1-2 - NamingBox decode & Hotfix 7修正

## 実装内容

### Step 1: NamingBox decode関数追加 (naming.rs)
-  `decode_static_method(func_name) -> Option<(box, method, arity)>`
-  `is_static_method_name(func_name) -> bool`
- 対称性: encode ⇔ decode のペア実装で一貫性確保

### Step 2: unified_emitter Hotfix 7修正 (Lines 267-304)
-  StaticCompiler box kind判定追加
-  static box method は receiver 追加をスキップ
-  instance method(RuntimeData/UserDefined)のみ receiver 追加
-  トレース: NYASH_STATIC_METHOD_TRACE=1 でログ出力

## 判定ロジック
```rust
if box_kind == CalleeBoxKind::StaticCompiler {
    // "BoxName.method/arity" 形式か確認
    let func_name = format!("{}.{}/{}", box_name, method, args.len());
    if is_static_method_name(&func_name) {
        // static box method → receiver 追加しない
    }
}
```

## 検証
 Stage-1 テスト: RC=0 (apps/tests/stage1_skip_ws_repro.hako)
 ビルド成功(0 error)

## 次のステップ
- Step 3: methodization実装 (HAKO_MIR_BUILDER_METHODIZE=1)

Co-Authored-By: ChatGPT5 <chatgpt@openai.com>
This commit is contained in:
nyash-codex
2025-11-21 23:52:10 +09:00
parent 74271f3c5b
commit a13f14cea0
11 changed files with 505 additions and 29 deletions

View File

@ -0,0 +1,7 @@
// Minimal run path fixture for Stage1 CLI.
// Produces a trivial Program(JSON v0) with Main.main returning 0.
static box Main {
static method main() {
return 0
}
}

View File

@ -41,5 +41,19 @@ Rollback
- Disable HAKO_MIR_BUILDER_METHODIZE. Revert to Global("Box.method") resolution path (current 21.6 behavior). - Disable HAKO_MIR_BUILDER_METHODIZE. Revert to Global("Box.method") resolution path (current 21.6 behavior).
Current notes (Phase 25.x bring-up) Current notes (Phase 25.x bring-up)
- static box 内のローカル呼び出し(例: `Main.main``me._nop()`)が Global 呼び出しのまま落ちるケースを確認。NamingBox`src/mir/naming.rs`)で `main``Main` 正規化は済み。 - NamingBox / static 名 SSOT
- 次のステップ: methodization 時に `ensure_static_box_instance` 経由で receiver を付与し、Global 呼び出しを Method 呼び出しに寄せる(本フェーズの実装タスクとして残置) - `src/mir/naming.rs` に NamingBox を実装済み。`Main.main` / `main._nop` などの揺れを `"Main.main/0"` 形式に正規化する経路は Rust VM/LLVM/JSON bridge から既に利用中
- VM 側は `normalize_static_global_name` を通して static box 名を一元化するよう更新済み。
- 既知のギャップ
- static box 内のローカル呼び出し(例: `Main.main``me._nop()`)が Global 呼び出しのまま落ちるケースを確認済み。NamingBox で `main``Main` 正規化は済んでいるが、「Method 化receiver 付与」が未完了。
- MeCallPolicy / method_call_handlers 周りで、static box method に対しても一律で receiver`me`を先頭に追加してしまうパスがあり、arity 不一致や miss-call の温床になり得る。
- このフェーズでやること(具体タスク)
1. `src/mir/builder/method_call_handlers.rs` / MeCallPolicy で「static box method かどうか」を判定し、static のときは receiver を追加しないargs はそのまま Global 互換で流す)ガードを入れる。
2. methodization ロジックHAKO_MIR_BUILDER_METHODIZE=1 有効時で、static box 呼びを `ensure_static_box_instance(Box)` 経由の Method 呼び出しに寄せる:
- Global("Main._nop/0") → Method{ receiver = ensure_static_box_instance("Main"), box="Main", method="_nop", arity=0 }。
3. 最小テストを追加:
- `.hako` 側 minimal: `static box Main { static method _nop() { return 0 } }` を呼ぶケース。
- Rust 側: `mir_stage1_static_main_nop_resolves_and_execs`で、MIR verify + VM 実行が methodization ON/OFF の両方で安定することを確認。
4. docs: 本ファイルに「static box 正規化の完了条件」と「NamingBox / ensure_static_box_instance / MeCallPolicy の責務分担」を 1 ページにまとめ、Phase 21.7 の完了ラインを明文化する。

View File

@ -52,6 +52,7 @@
- Stage1 UsingResolver: - Stage1 UsingResolver:
- Program(JSON) を入力に using/extern を解決(今は apply_usings=0 でバイパス多め。defs/body の構造はそのまま Rust LoopForm v2 に渡る前提。 - Program(JSON) を入力に using/extern を解決(今は apply_usings=0 でバイパス多め。defs/body の構造はそのまま Rust LoopForm v2 に渡る前提。
- region+next_i 形ループで JSON スキャン・modules_map を決定、prefix 結合するだけのテキスト担当箱。 - region+next_i 形ループで JSON スキャン・modules_map を決定、prefix 結合するだけのテキスト担当箱。
- using 解決の意味論そのものは `UsingResolveSSOTBox` に委譲するStage1 は「SSOT に渡す JSON/front を整える箱」)。
- Rust bridge (Stage0): - Rust bridge (Stage0):
- Program(JSON v0) → json_v0_bridge lowering → LoopForm v2 → MIR → VM/LLVM - Program(JSON v0) → json_v0_bridge lowering → LoopForm v2 → MIR → VM/LLVM
- Loop/PHI/SSA の SSOT は Rust 側。Stage1/.hako は「正しい形の Program(JSON) を渡す」責務に徹する。 - Loop/PHI/SSA の SSOT は Rust 側。Stage1/.hako は「正しい形の Program(JSON) を渡す」責務に徹する。
@ -121,6 +122,22 @@ source -> | Stage-B (block) | --> | Stage-1 UsingRes | --> | MirBuilder (.ha
- modules_list 分割ループstart/next_startをそのまま MIR 化できることを `mir_stage1_using_resolver_module_map_regionized_verifies` で固定。 - modules_list 分割ループstart/next_startをそのまま MIR 化できることを `mir_stage1_using_resolver_module_map_regionized_verifies` で固定。
- resolve_for_source 相当で entries ループと modules_map 参照MapBox.has/getを同時に行うケースを `mir_stage1_using_resolver_resolve_with_modules_map_verifies` で固定。 - resolve_for_source 相当で entries ループと modules_map 参照MapBox.has/getを同時に行うケースを `mir_stage1_using_resolver_resolve_with_modules_map_verifies` で固定。
- 残り: なし(現状の 3 ループは all Region 形) - 残り: なし(現状の 3 ループは all Region 形)
### UsingResolveSSOTBox 境界メモ
- ファイル: `lang/src/using/resolve_ssot_box.hako`
- 責務SSOT 箱):
- IO 禁止。modules_json / using_entries_json / ctx(map) だけを入力に取り、解決結果path/prefix/modules_json_resolvedを返す純粋関数群。
- 主な I/F:
- `resolve(name, ctx)`
- ctx.modules / ctx.using_paths / ctx.cwd から純粋に path を合成MVP では modules の厳密一致+相対ヒントのみ)。
- `resolve_modules(modules_json, using_entries_json, ctx)`
- nyash.toml 相当の modules_json と UsingCollector 出力を突き合わせ、競合解決済み modules_json を返す予定MVP では echo-back
- `resolve_prefix(using_entries_json, modules_json, ctx)`
- using_entries_json と modules_json から prefix 文字列を構成MVP では空文字、Stage1 entry 側で prefix 結合だけを担当)。
- Stage1 UsingResolverBox との分担:
- Stage1 UsingResolverBox は「JSON フロントから using_entries / modules_map を Region+next_i で収集・整形」する箱。
- UsingResolveSSOTBox は「その結果をもとに最終的な modules/prefix を決める唯一の箱SSOT」として、IO なしで名前解決の意味論を持つ。
- pipeline_v2 UsingResolverlang/src/compiler/pipeline_v2/using_resolver_box.hako - pipeline_v2 UsingResolverlang/src/compiler/pipeline_v2/using_resolver_box.hako
- 役割: modules_json 上で alias を解決する stateful helperテキスト収集は entry 側)。 - 役割: modules_json 上で alias を解決する stateful helperテキスト収集は entry 側)。
- ループ: `loop(true)` で RegexFlow.find_from を使い key/tail マッチを走査する単一路。continue/backedge の多経路は無く、Region+next_i へのリライトは不要と判断。 - ループ: `loop(true)` で RegexFlow.find_from を使い key/tail マッチを走査する単一路。continue/backedge の多経路は無く、Region+next_i へのリライトは不要と判断。
@ -132,6 +149,13 @@ source -> | Stage-B (block) | --> | Stage-1 UsingRes | --> | MirBuilder (.ha
- 「pos/next_pos が forward するだけの region」で PHI が揺れないこと。 - 「pos/next_pos が forward するだけの region」で PHI が揺れないこと。
- `using` 収集で early-exit しても merge 後の `pos` が決定的になること。 - `using` 収集で early-exit しても merge 後の `pos` が決定的になること。
- いずれも LoopForm v2 経路JSON front→Rustで MirVerifier 緑を確認するスモークとして追加。 - いずれも LoopForm v2 経路JSON front→Rustで MirVerifier 緑を確認するスモークとして追加。
- 追加済みテストRust `src/tests/mir_stage1_using_resolver_verify.rs`)でカバーする LoopForm パターン:
- `mir_stage1_using_resolver_modules_map_early_exit_verifies`: Region+next_i + breakname=="" で早期終了)
- `mir_stage1_using_resolver_modules_map_continue_and_break_verifies`: Region+next_i + continue/break 混在("" を skip, "STOP" で break
- `mir_stage1_using_resolver_resolve_with_modules_map_verifies`: entries ループmodules_map 参照を同時に行う本線形
- `mir_stage1_using_resolver_collect_entries_early_exit_verifies`: JSON スキャンearly-exit sentinel で next_pos を決める Region+next_pos 形
- `mir_stage1_using_resolver_module_map_regionized_verifies`: modules_list 分割start/next_startで MapBox set を行う Region 形
- `mir_stage1_using_resolver_modules_map_continue_break_with_lookup_verifies`: entries ループmodules_map.has/getcontinue/break が同居する本線寄りパターン
- Rust 側テストの取り扱い: - Rust 側テストの取り扱い:
- `src/tests/mir_stage1_using_resolver_verify.rs` に追加した構造テストは cargo test 経路で維持する。v2 quick スモークへの昇格は実行時間とノイズを見つつ後続フェーズで再検討(今回の設計タスクでは据え置き)。 - `src/tests/mir_stage1_using_resolver_verify.rs` に追加した構造テストは cargo test 経路で維持する。v2 quick スモークへの昇格は実行時間とノイズを見つつ後続フェーズで再検討(今回の設計タスクでは据え置き)。
- 観測ログ: MIR dump を残す場合は dev オンリー(`NYASH_LOOPFORM_DEBUG` / `HAKO_LOOP_PHI_TRACE`)に限定し、ログ経路は docs にも記載しておく。 - 観測ログ: MIR dump を残す場合は dev オンリー(`NYASH_LOOPFORM_DEBUG` / `HAKO_LOOP_PHI_TRACE`)に限定し、ログ経路は docs にも記載しておく。

View File

@ -51,13 +51,13 @@ hakorune <command> [<subcommand>] [options] [-- script_args...]
|-----------------------------------|-------------------------------------------|----------------------| |-----------------------------------|-------------------------------------------|----------------------|
| `run` | .hako をコンパイルして実行(既定 VM | プレースホルダ(`[hakorune] run: not implemented yet` | | `run` | .hako をコンパイルして実行(既定 VM | プレースホルダ(`[hakorune] run: not implemented yet` |
| `build exe` | .hako からネイティブ EXE を AOT ビルド | 実装済みenv.codegen経由で `.o` → EXE を生成) | | `build exe` | .hako からネイティブ EXE を AOT ビルド | 実装済みenv.codegen経由で `.o` → EXE を生成) |
| `emit program-json` | StageB で Program(JSON v0) を出力 | 実装済み(`BuildBox.emit_program_json_v0` | | `emit program-json` | StageB で Program(JSON v0) を出力 | 実装済み(Stage0 ブリッジ + `.hako` Stage1Cli 完了 |
| `emit mir-json` | Program(JSON) → MIR(JSON) を出力 | 実装済み(`MirBuilderBox.emit_from_program_json_v0` | | `emit mir-json` | Program(JSON) → MIR(JSON) を出力 | 実装済み(Stage0 ブリッジ + `.hako` Stage1Cli 完了 |
| `check` | 将来の構文/型/using チェック(予約) | プレースホルダ(`[hakorune] check: not implemented yet` | | `check` | 将来の構文/型/using チェック(予約) | プレースホルダ(`[hakorune] check: not implemented yet` |
Phase 25.1a では、**`emit program-json` / `emit mir-json` / `build exe` の 3 系列のみが実働コード** であり、`run` / `check` はメッセージを返して終了するプレースホルダのまま運用する。CLI の出口コード90〜93やログ形式は docs と実装を同期済み。 Phase 25.1a では、**`emit program-json` / `emit mir-json` / `build exe` の 3 系列のみが実働コード** であり、`run` / `check` はメッセージを返して終了するプレースホルダのまま運用する。CLI の出口コード90〜93やログ形式は docs と実装を同期済み。
### 実装ステータスPhase 25.1a ### 実装ステータスPhase 25.1a+
- `.hako → Program(JSON v0)`: - `.hako → Program(JSON v0)`:
- StageB (`lang/src/compiler/entry/compiler_stageb.hako`) で `BuildBox.emit_program_json_v0` を呼び出し、`"version":0,"kind":"Program"` を持つ JSON を生成。 - StageB (`lang/src/compiler/entry/compiler_stageb.hako`) で `BuildBox.emit_program_json_v0` を呼び出し、`"version":0,"kind":"Program"` を持つ JSON を生成。
@ -267,6 +267,18 @@ hakorune emit mir-json [-o <out>] [--quiet] <source.hako>
- `STAGE1_CLI_DEBUG`: - `STAGE1_CLI_DEBUG`:
- `1` のとき Stage1 CLI 側の debug ログ(`[stage1-cli/debug] ...`)と `__mir__.log` を有効化。 - `1` のとき Stage1 CLI 側の debug ログ(`[stage1-cli/debug] ...`)と `__mir__.log` を有効化。
- 新形式のエイリアスPhase 25.1 で導入済み):
- `NYASH_STAGE1_MODE` / `NYASH_STAGE1_INPUT` / `NYASH_STAGE1_BACKEND`
- Stage1 stub が最初に参照する統一 env。未設定なら上記 legacy `STAGE1_*` からブリッジ(`stage1_bridge/env.rs`)が補完する。
- 許容値: `emit-program` / `emit-program-json` / `emit-mir` / `emit-mir-json` / `run`。
| 目的 | 新 env | 既存/フォールバック |
|-----------------|---------------------------|------------------------------|
| モード選択 | `NYASH_STAGE1_MODE` | `STAGE1_EMIT_*` / `NYASH_USE_STAGE1_CLI` |
| 入力パス | `NYASH_STAGE1_INPUT` | `STAGE1_SOURCE` |
| backend 指定 | `NYASH_STAGE1_BACKEND` | `STAGE1_BACKEND` |
| Program(JSON) パス | `NYASH_STAGE1_PROGRAM_JSON` | `STAGE1_PROGRAM_JSON` |
env-only 仕様の原則: env-only 仕様の原則:
- 入口 `Stage1Cli.stage1_main(args)` は `cli_args_raw` を一切参照せず、上記 ENV だけを見てモード/入力ソース/backend を決定する。 - 入口 `Stage1Cli.stage1_main(args)` は `cli_args_raw` を一切参照せず、上記 ENV だけを見てモード/入力ソース/backend を決定する。
- `.hako` 側で Program(JSON v0) / MIR(JSON) を emit したうえで、実行や AOT は常に Stage0/Rust に委譲するStage1 は CLI オーケストレーション専任)。 - `.hako` 側で Program(JSON v0) / MIR(JSON) を emit したうえで、実行や AOT は常に Stage0/Rust に委譲するStage1 は CLI オーケストレーション専任)。
@ -285,7 +297,7 @@ hakorune check [options] <entry.hako>
- System Hakorune subset 制約 - System Hakorune subset 制約
などを検証するためのエントリポイント。 などを検証するためのエントリポイント。
- 実装は `.hako` 側で `tools/hako-check` 相当のロジックを呼び出す想定。 - 実装は `.hako` 側で `tools/hako-check` 相当のロジックを呼び出す想定。
- Phase 25.1 では「名前予約」と「インターフェース定義」のみを行い、実装は Phase 26 以降。 - Phase 25.1 では「名前予約」と「インターフェース定義」のみを行い、実装は Phase 26 以降。
## Stage0 / Stage1 の責務分離CLI 視点) ## Stage0 / Stage1 の責務分離CLI 視点)

View File

@ -157,23 +157,40 @@ static box Stage1Cli {
// env から直接読み取り、Stage1CliConfigBox は将来の構造化用として温存する。 // env から直接読み取り、Stage1CliConfigBox は将来の構造化用として温存する。
// mode 判定env-only // mode 判定env-only
local mode = "disabled" // 新形式NYASH_STAGE1_MODEがあればそちらを優先し、旧 STAGE1_* はフォールバックとして扱う。
if env.get("STAGE1_EMIT_PROGRAM_JSON") == "1" { local mode = env.get("NYASH_STAGE1_MODE")
mode = "emit_program_json" if mode != null { mode = "" + mode }
} else if env.get("STAGE1_EMIT_MIR_JSON") == "1" { if mode == null || mode == "" {
mode = "emit_mir_json" if env.get("STAGE1_EMIT_PROGRAM_JSON") == "1" {
} else if env.get("NYASH_USE_STAGE1_CLI") == "1" { mode = "emit-program"
mode = "run" } else if env.get("STAGE1_EMIT_MIR_JSON") == "1" {
mode = "emit-mir"
} else if env.get("NYASH_USE_STAGE1_CLI") == "1" {
mode = "run"
} else {
mode = "disabled"
}
} }
// backend と入力まわり // backend と入力まわり
local backend = env.get("STAGE1_BACKEND") local backend = env.get("NYASH_STAGE1_BACKEND")
if backend == null || backend == "" {
backend = env.get("STAGE1_BACKEND")
}
if backend == null { backend = "vm" } if backend == null { backend = "vm" }
backend = "" + backend backend = "" + backend
local source = env.get("STAGE1_SOURCE") local source = env.get("NYASH_STAGE1_INPUT")
if source == null || source == "" {
source = env.get("STAGE1_SOURCE")
}
local source_text = env.get("STAGE1_SOURCE_TEXT") local source_text = env.get("STAGE1_SOURCE_TEXT")
local prog_path = env.get("STAGE1_PROGRAM_JSON") local prog_path = env.get("NYASH_STAGE1_PROGRAM_JSON")
if prog_path == null || prog_path == "" {
prog_path = env.get("STAGE1_PROGRAM_JSON")
}
// デバッグは当面 STAGE1_CLI_DEBUG を優先し、将来 NYASH_DEBUG に統合する。
local debug = env.get("STAGE1_CLI_DEBUG") local debug = env.get("STAGE1_CLI_DEBUG")
// Log entry point for debugging // Log entry point for debugging
@ -196,7 +213,7 @@ static box Stage1Cli {
} }
// Dispatch by mode // Dispatch by mode
if mode == "emit_program_json" { if mode == "emit-program" || mode == "emit_program_json" {
if source == null || source == "" { if source == null || source == "" {
if source_text != null && source_text != "" { if source_text != null && source_text != "" {
source = source_text // Fallback to direct text for testing source = source_text // Fallback to direct text for testing
@ -211,7 +228,7 @@ static box Stage1Cli {
return 0 return 0
} }
if mode == "emit_mir_json" { if mode == "emit-mir" || mode == "emit_mir_json" {
local prog_json = null local prog_json = null
if prog_path != null && prog_path != "" { if prog_path != null && prog_path != "" {
prog_json = me._read_file("[stage1-cli] emit mir-json", prog_path) prog_json = me._read_file("[stage1-cli] emit mir-json", prog_path)

View File

@ -264,16 +264,43 @@ impl UnifiedCallEmitterBox {
&mut args_local, &mut args_local,
); );
// 📦 Hotfix 7: Include receiver in args for Callee::Method // 📦 Hotfix 7 (Phase 21.7 fixed): Include receiver in args for instance methods ONLY
// VM's exec_function_inner expects receiver as the first parameter (ValueId(0)) // VM's exec_function_inner expects receiver as the first parameter (ValueId(0))
// but finalize_call_operands keeps receiver in Callee, not in args. // but finalize_call_operands keeps receiver in Callee, not in args.
// We must add it to args_local here so VM can bind it correctly. // We must add it to args_local here so VM can bind it correctly.
//
// 🎯 Phase 21.7: static box method の receiver 追加を防止
// - StaticCompiler box kind: コンパイル時 static boxParserBox, StageBArgsBox等
// - これらは lowered function として定義され、receiver を期待しない
// - instance methodRuntimeData/UserDefinedのみ receiver を追加
if let Callee::Method { if let Callee::Method {
receiver: Some(recv), receiver: Some(recv),
box_kind,
box_name,
method,
.. ..
} = &callee } = &callee
{ {
args_local.insert(0, *recv); use crate::mir::definitions::call_unified::CalleeBoxKind;
// 暫定判定: decode_static_method で static box method か確認
let is_static_box_method = if *box_kind == CalleeBoxKind::StaticCompiler {
// StaticCompiler の場合、関数名が "BoxName.method/arity" 形式か確認
let func_name = format!("{}.{}/{}", box_name, method, args_local.len());
crate::mir::naming::is_static_method_name(&func_name)
} else {
false
};
// instance method のみ receiver を追加static box method は追加しない)
if !is_static_box_method {
args_local.insert(0, *recv);
} else if std::env::var("NYASH_STATIC_METHOD_TRACE").ok().as_deref() == Some("1") {
eprintln!(
"[hotfix7] skipped receiver for static box method: {}.{}",
box_name, method
);
}
} }
// Create MirCall instruction using the new module (pure data composition) // Create MirCall instruction using the new module (pure data composition)

View File

@ -44,3 +44,42 @@ pub fn normalize_static_global_name(func_name: &str) -> String {
func_name.to_string() func_name.to_string()
} }
/// Decode a MIR function name into (box_name, method, arity).
///
/// Format: "BoxName.method/arity"
///
/// Returns:
/// - Some((box_name, method, arity)) if successfully parsed
/// - None if not in expected format
///
/// Examples:
/// - "Main.main/0" → Some(("Main", "main", 0))
/// - "Calculator.add/2" → Some(("Calculator", "add", 2))
/// - "print" → None (no dot)
/// - "Main.main" → None (no arity)
pub fn decode_static_method(func_name: &str) -> Option<(&str, &str, usize)> {
// Split by '.' to extract box_name and "method/arity"
let (box_name, method_arity) = func_name.split_once('.')?;
// Split by '/' to extract method and arity
let (method, arity_str) = method_arity.split_once('/')?;
// Parse arity as usize
let arity = arity_str.parse::<usize>().ok()?;
Some((box_name, method, arity))
}
/// Check if a function name is a static box method.
///
/// Returns true if the name matches "BoxName.method/arity" format.
///
/// Examples:
/// - "Main.main/0" → true
/// - "Calculator.add/2" → true
/// - "print" → false
/// - "instance.method" → false (no arity)
pub fn is_static_method_name(func_name: &str) -> bool {
decode_static_method(func_name).is_some()
}

View File

@ -24,19 +24,31 @@ pub(super) struct Stage1Args {
/// - emit_mir: emit mir-json (<source.hako> or STAGE1_PROGRAM_JSON) /// - emit_mir: emit mir-json (<source.hako> or STAGE1_PROGRAM_JSON)
/// - run: run --backend <backend> <source.hako> /// - run: run --backend <backend> <source.hako>
pub(super) fn build_stage1_args(groups: &CliGroups) -> Stage1Args { pub(super) fn build_stage1_args(groups: &CliGroups) -> Stage1Args {
let source = groups // Prefer new env (NYASH_STAGE1_*) and fall back to legacy names to keep compatibility.
.input let source = std::env::var("NYASH_STAGE1_INPUT")
.file .ok()
.as_ref() .or_else(|| {
.cloned() groups
.input
.file
.as_ref()
.cloned()
})
.or_else(|| std::env::var("STAGE1_SOURCE").ok()) .or_else(|| std::env::var("STAGE1_SOURCE").ok())
.or_else(|| std::env::var("STAGE1_INPUT").ok()); .or_else(|| std::env::var("STAGE1_INPUT").ok());
let emit_program = std::env::var("STAGE1_EMIT_PROGRAM_JSON") let mode_env = std::env::var("NYASH_STAGE1_MODE")
.ok()
.map(|m| m.to_ascii_lowercase().replace('_', "-"));
let emit_program = matches!(
mode_env.as_deref(),
Some("emit-program") | Some("emit-program-json")
) || std::env::var("STAGE1_EMIT_PROGRAM_JSON")
.ok() .ok()
.as_deref() .as_deref()
== Some("1"); == Some("1");
let emit_mir = std::env::var("STAGE1_EMIT_MIR_JSON").ok().as_deref() == Some("1"); let emit_mir = matches!(mode_env.as_deref(), Some("emit-mir") | Some("emit-mir-json"))
|| std::env::var("STAGE1_EMIT_MIR_JSON").ok().as_deref() == Some("1");
let mut args: Vec<String> = Vec::new(); let mut args: Vec<String> = Vec::new();
let mut source_env: Option<String> = None; let mut source_env: Option<String> = None;
@ -52,7 +64,9 @@ pub(super) fn build_stage1_args(groups: &CliGroups) -> Stage1Args {
args.push(src); args.push(src);
source_env = args.last().cloned(); source_env = args.last().cloned();
} else if emit_mir { } else if emit_mir {
if let Ok(pjson) = std::env::var("STAGE1_PROGRAM_JSON") { if let Ok(pjson) = std::env::var("NYASH_STAGE1_PROGRAM_JSON")
.or_else(|_| std::env::var("STAGE1_PROGRAM_JSON"))
{
args.push("emit".into()); args.push("emit".into());
args.push("mir-json".into()); args.push("mir-json".into());
args.push("--from-program-json".into()); args.push("--from-program-json".into());
@ -74,8 +88,9 @@ pub(super) fn build_stage1_args(groups: &CliGroups) -> Stage1Args {
process::exit(97); process::exit(97);
}); });
args.push("run".into()); args.push("run".into());
let backend = std::env::var("STAGE1_BACKEND") let backend = std::env::var("NYASH_STAGE1_BACKEND")
.ok() .ok()
.or_else(|| std::env::var("STAGE1_BACKEND").ok())
.unwrap_or_else(|| groups.backend.backend.clone()); .unwrap_or_else(|| groups.backend.backend.clone());
args.push("--backend".into()); args.push("--backend".into());
args.push(backend); args.push(backend);

View File

@ -21,6 +21,29 @@ pub(super) fn configure_stage1_env(
// Child recursion guard // Child recursion guard
cmd.env("NYASH_STAGE1_CLI_CHILD", "1"); cmd.env("NYASH_STAGE1_CLI_CHILD", "1");
// Unified Stage-1 env (NYASH_STAGE1_*) — derive from legacy if unset to keep compatibility.
if std::env::var("NYASH_STAGE1_MODE").is_err() {
if std::env::var("STAGE1_EMIT_PROGRAM_JSON")
.ok()
.as_deref()
== Some("1")
{
cmd.env("NYASH_STAGE1_MODE", "emit-program");
} else if std::env::var("STAGE1_EMIT_MIR_JSON")
.ok()
.as_deref()
== Some("1")
{
cmd.env("NYASH_STAGE1_MODE", "emit-mir");
} else if std::env::var("NYASH_USE_STAGE1_CLI")
.ok()
.as_deref()
== Some("1")
{
cmd.env("NYASH_STAGE1_MODE", "run");
}
}
// Runtime defaults // Runtime defaults
if std::env::var("NYASH_NYRT_SILENT_RESULT").is_err() { if std::env::var("NYASH_NYRT_SILENT_RESULT").is_err() {
cmd.env("NYASH_NYRT_SILENT_RESULT", "1"); cmd.env("NYASH_NYRT_SILENT_RESULT", "1");
@ -35,6 +58,23 @@ pub(super) fn configure_stage1_env(
cmd.env("NYASH_BOX_FACTORY_POLICY", "builtin_first"); cmd.env("NYASH_BOX_FACTORY_POLICY", "builtin_first");
} }
// Stage-1 unified input/backend (fallback to legacy)
if std::env::var("NYASH_STAGE1_INPUT").is_err() {
if let Ok(src) = std::env::var("STAGE1_SOURCE") {
cmd.env("NYASH_STAGE1_INPUT", src);
}
}
if std::env::var("NYASH_STAGE1_BACKEND").is_err() {
if let Ok(be) = std::env::var("STAGE1_BACKEND") {
cmd.env("NYASH_STAGE1_BACKEND", be);
}
}
if std::env::var("NYASH_STAGE1_PROGRAM_JSON").is_err() {
if let Ok(pjson) = std::env::var("STAGE1_PROGRAM_JSON") {
cmd.env("NYASH_STAGE1_PROGRAM_JSON", pjson);
}
}
// Stage-1 CLI 経路では既定で using 適用を無効化し、 // Stage-1 CLI 経路では既定で using 適用を無効化し、
// prefix は空HAKO_STAGEB_APPLY_USINGS=0とする。 // prefix は空HAKO_STAGEB_APPLY_USINGS=0とする。
// UsingResolver/UsingCollector の検証は専用テストで行い、 // UsingResolver/UsingCollector の検証は専用テストで行い、

View File

@ -233,6 +233,115 @@ static box Stage1UsingResolverRegionLoop {
} }
} }
/// Region+next_i で modules_map を構築し、name=="" で early-exit するパターンを固定する。
#[test]
fn mir_stage1_using_resolver_modules_map_early_exit_verifies() {
ensure_stage3_env();
let src = r#"
static box Stage1UsingResolverModulesMapEarlyExit {
build_modules(entries) {
if entries == null { return new MapBox() }
local modules = new MapBox()
local i = 0
local n = entries.length()
loop(i < n) {
local entry = entries.get(i)
if entry == null { break }
local name = "" + entry.get("name")
if name == "" { break } // early-exit path
local path = "" + entry.get("path")
modules.set(name, path)
i = i + 1
}
return modules
}
main() {
// valid → empty name → break最後の無効要素は無視される
local entries = new ArrayBox()
local e1 = new MapBox()
e1.set("name", "Foo")
e1.set("path", "foo.hako")
entries.push(e1)
local e2 = new MapBox()
e2.set("name", "")
entries.push(e2)
local e3 = new MapBox()
e3.set("name", "Bar")
e3.set("path", "bar.hako")
entries.push(e3)
local modules = me.build_modules(entries)
return modules.size()
}
}
"#;
let ast: ASTNode = NyashParser::parse_from_string(src).expect("parse ok");
let mut mc = MirCompiler::with_options(false);
let cr = mc.compile(ast).expect("compile");
let mut verifier = MirVerifier::new();
if let Err(errors) = verifier.verify_module(&cr.module) {
for e in &errors {
eprintln!("[rust-mir-verify] {}", e);
}
panic!("MIR verification failed for Stage1UsingResolverModulesMapEarlyExit");
}
}
/// Region+next_i で continue/break が混在する modules_map 構築を固定する。
#[test]
fn mir_stage1_using_resolver_modules_map_continue_and_break_verifies() {
ensure_stage3_env();
let src = r#"
static box Stage1UsingResolverModulesMapContinueBreak {
build_modules(entries) {
if entries == null { return new MapBox() }
local modules = new MapBox()
local i = 0
local n = entries.length()
loop(i < n) {
local next_i = i + 1
local entry = entries.get(i)
if entry == null { break }
local name = "" + entry.get("name")
if name == "" { i = next_i; continue } // skip empty → continue
if name == "STOP" { break } // sentinel → break
local path = "" + entry.get("path")
modules.set(name, path)
i = next_i
}
return modules
}
main() {
// A(keep) → ""(continue) → STOP(break) → after break (ignored)
local entries = new ArrayBox()
local e1 = new MapBox(); e1.set("name", "A"); e1.set("path", "a.hako"); entries.push(e1)
local e2 = new MapBox(); e2.set("name", ""); entries.push(e2)
local e3 = new MapBox(); e3.set("name", "STOP"); e3.set("path", "z.hako"); entries.push(e3)
local e4 = new MapBox(); e4.set("name", "B"); e4.set("path", "b.hako"); entries.push(e4)
local modules = me.build_modules(entries)
return modules.size()
}
}
"#;
let ast: ASTNode = NyashParser::parse_from_string(src).expect("parse ok");
let mut mc = MirCompiler::with_options(false);
let cr = mc.compile(ast).expect("compile");
let mut verifier = MirVerifier::new();
if let Err(errors) = verifier.verify_module(&cr.module) {
for e in &errors {
eprintln!("[rust-mir-verify] {}", e);
}
panic!("MIR verification failed for Stage1UsingResolverModulesMapContinueBreak");
}
}
/// JSON スキャン + early-exit パターンでも PHI/SSA が崩れないことを確認する。 /// JSON スキャン + early-exit パターンでも PHI/SSA が崩れないことを確認する。
#[test] #[test]
fn mir_stage1_using_resolver_collect_entries_early_exit_verifies() { fn mir_stage1_using_resolver_collect_entries_early_exit_verifies() {
@ -539,3 +648,95 @@ static box Stage1UsingResolverResolveWithMap {
panic!("MIR verification failed for Stage1UsingResolverResolveWithMap"); panic!("MIR verification failed for Stage1UsingResolverResolveWithMap");
} }
} }
/// entries ループと modules_map 参照に加え、continue/break が混在する本線寄りパターン。
/// Region+next_i ループで MapBox.has/get と sentinel\"STOP\"break/空 name continue が同居しても SSA が崩れないことを確認する。
#[test]
fn mir_stage1_using_resolver_modules_map_continue_break_with_lookup_verifies() {
ensure_stage3_env();
let src = r#"
static box Stage1UsingResolverModulesMapContinueBreakLookup {
_build_module_map(raw) {
// Simple key=val||| 形式を MapBox に積む Region+next_start 形。
local map = new MapBox()
if raw == null { return map }
local delim = "|||"
local start = 0
local cont = 1
loop(cont == 1) {
local next_start = raw.length()
local next = raw.indexOf(delim, start)
local seg = ""
if next >= 0 {
seg = raw.substring(start, next)
next_start = next + delim.length()
} else {
seg = raw.substring(start, raw.length())
cont = 0
}
if seg.length() > 0 {
local eq_idx = seg.indexOf("=", 0)
if eq_idx >= 0 {
local key = seg.substring(0, eq_idx)
local val = seg.substring(eq_idx + 1, seg.length())
if key != "" && val != "" {
map.set(key, val)
}
}
}
start = next_start
}
return map
}
resolve_with_modules(entries, raw) {
if entries == null { return 0 }
local modules_map = me._build_module_map(raw)
local prefix = ""
local i = 0
local n = entries.length()
loop(i < n) {
local next_i = i + 1
local entry = entries.get(i)
if entry == null { break }
local name = "" + entry.get("name")
if name == "" { i = next_i; continue } // 空 name は skip
if name == "STOP" { break } // sentinel で break
prefix = prefix + name
if modules_map.has(name) {
prefix = prefix + modules_map.get(name)
}
i = next_i
}
return prefix.length()
}
main() {
// entries: A(keep) → ""(continue) → STOP(break) → C(無視)
// modules_map: A→/a, C→/c
local entries = new ArrayBox()
local e1 = new MapBox(); e1.set("name", "A"); entries.push(e1)
local e2 = new MapBox(); e2.set("name", ""); entries.push(e2)
local e3 = new MapBox(); e3.set("name", "STOP"); entries.push(e3)
local e4 = new MapBox(); e4.set("name", "C"); entries.push(e4)
local raw = "A=/a|||C=/c|||"
return me.resolve_with_modules(entries, raw)
}
}
"#;
let ast: ASTNode = NyashParser::parse_from_string(src).expect("parse ok");
let mut mc = MirCompiler::with_options(false);
let cr = mc.compile(ast).expect("compile");
let mut verifier = MirVerifier::new();
if let Err(errors) = verifier.verify_module(&cr.module) {
for e in &errors {
eprintln!("[rust-mir-verify] {}", e);
}
panic!(
"MIR verification failed for Stage1UsingResolverModulesMapContinueBreakLookup"
);
}
}

80
tools/stage1_smoke.sh Normal file
View File

@ -0,0 +1,80 @@
#!/usr/bin/env bash
set -euo pipefail
# stage1_smoke.sh — Stage1 CLI quick smoke
#
# 役割:
# - rust Stage0 ブリッジ経由で Stage1Cli.stub を叩き、
# - emit program-json
# - emit mir-json
# の 2 経路が正常に JSON を出力することを確認する軽量スモーク。
ROOT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")/.." && pwd)"
BIN="${ROOT_DIR}/target/release/hakorune"
set +e
if [[ ! -x "${BIN}" ]]; then
echo "[stage1-smoke] missing binary: ${BIN}" >&2
exit 97
fi
set -e
case "${1:-}" in
""|"help"|-h|--help)
cat <<EOF
Usage: tools/stage1_smoke.sh [program-json|mir-json|all]
program-json : apps/tests/stage1_using_minimal.hako で Program(JSON v0) を確認
mir-json : apps/tests/stage1_using_minimal.hako で MIR(JSON) を確認
run : apps/tests/stage1_run_min.hako で run(vm) 経路(現状は MIR 出力のみ)を確認
all : 3 経路すべて実行(既定)
EOF
exit 0
;;
esac
MODE="${1:-all}"
SRC_MIN="apps/tests/stage1_using_minimal.hako"
SRC_RUN="apps/tests/stage1_run_min.hako"
run_prog_json() {
echo "[stage1-smoke] emit program-json: ${SRC_MIN}" >&2
NYASH_USE_STAGE1_CLI=1 \
STAGE1_EMIT_PROGRAM_JSON=1 \
STAGE1_SOURCE="${SRC_MIN}" \
"${BIN}" -- stage1_stub_test > /tmp/stage1_smoke_program.json
echo "[stage1-smoke] program-json length=$(wc -c < /tmp/stage1_smoke_program.json)" >&2
}
run_mir_json() {
echo "[stage1-smoke] emit mir-json: ${SRC_MIN}" >&2
NYASH_USE_STAGE1_CLI=1 \
STAGE1_EMIT_MIR_JSON=1 \
STAGE1_SOURCE="${SRC_MIN}" \
"${BIN}" -- stage1_stub_test > /tmp/stage1_smoke_mir.json
echo "[stage1-smoke] mir-json length=$(wc -c < /tmp/stage1_smoke_mir.json)" >&2
}
run_run_vm() {
echo "[stage1-smoke] run (vm backend): ${SRC_RUN}" >&2
NYASH_STAGE1_MODE="run" \
NYASH_STAGE1_INPUT="${SRC_RUN}" \
NYASH_STAGE1_BACKEND="vm" \
"${BIN}" -- stage1_stub_test > /tmp/stage1_smoke_run_vm.out
echo "[stage1-smoke] run(vm) output length=$(wc -c < /tmp/stage1_smoke_run_vm.out)" >&2
}
if [[ "${MODE}" == "program-json" ]]; then
run_prog_json
elif [[ "${MODE}" == "mir-json" ]]; then
run_mir_json
elif [[ "${MODE}" == "run" ]]; then
run_run_vm
else
run_prog_json
run_mir_json
run_run_vm
fi
exit 0