From a13f14cea04d13750fae5f9436837b9c57c4a58e Mon Sep 17 00:00:00 2001 From: nyash-codex Date: Fri, 21 Nov 2025 23:52:10 +0900 Subject: [PATCH] =?UTF-8?q?feat(mir):=20Phase=2021.7=20Step=201-2=20-=20Na?= =?UTF-8?q?mingBox=20decode=20&=20Hotfix=207=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit ## 実装内容 ### 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 --- apps/tests/stage1_run_min.hako | 7 + .../phases/phase-21.7-normalization/README.md | 18 +- .../stage1-usingresolver-loopform.md | 24 +++ .../runtime/cli-hakorune-stage1.md | 20 +- lang/src/runner/stage1_cli.hako | 41 ++-- src/mir/builder/calls/unified_emitter.rs | 31 ++- src/mir/naming.rs | 39 ++++ src/runner/stage1_bridge/args.rs | 33 ++- src/runner/stage1_bridge/env.rs | 40 ++++ src/tests/mir_stage1_using_resolver_verify.rs | 201 ++++++++++++++++++ tools/stage1_smoke.sh | 80 +++++++ 11 files changed, 505 insertions(+), 29 deletions(-) create mode 100644 apps/tests/stage1_run_min.hako create mode 100644 tools/stage1_smoke.sh diff --git a/apps/tests/stage1_run_min.hako b/apps/tests/stage1_run_min.hako new file mode 100644 index 00000000..e41d38e6 --- /dev/null +++ b/apps/tests/stage1_run_min.hako @@ -0,0 +1,7 @@ +// Minimal run path fixture for Stage‑1 CLI. +// Produces a trivial Program(JSON v0) with Main.main returning 0. +static box Main { + static method main() { + return 0 + } +} diff --git a/docs/development/roadmap/phases/phase-21.7-normalization/README.md b/docs/development/roadmap/phases/phase-21.7-normalization/README.md index 9d0077dd..f1a369a9 100644 --- a/docs/development/roadmap/phases/phase-21.7-normalization/README.md +++ b/docs/development/roadmap/phases/phase-21.7-normalization/README.md @@ -41,5 +41,19 @@ Rollback - Disable HAKO_MIR_BUILDER_METHODIZE. Revert to Global("Box.method") resolution path (current 21.6 behavior). Current notes (Phase 25.x bring-up) -- static box 内のローカル呼び出し(例: `Main.main` → `me._nop()`)が Global 呼び出しのまま落ちるケースを確認。NamingBox(`src/mir/naming.rs`)で `main`→`Main` 正規化は済み。 -- 次のステップ: methodization 時に `ensure_static_box_instance` 経由で receiver を付与し、Global 呼び出しを Method 呼び出しに寄せる(本フェーズの実装タスクとして残置)。 +- NamingBox / static 名 SSOT + - `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 の完了ラインを明文化する。 diff --git a/docs/development/roadmap/phases/phase-25.1/stage1-usingresolver-loopform.md b/docs/development/roadmap/phases/phase-25.1/stage1-usingresolver-loopform.md index a798f266..cd8ed6a4 100644 --- a/docs/development/roadmap/phases/phase-25.1/stage1-usingresolver-loopform.md +++ b/docs/development/roadmap/phases/phase-25.1/stage1-usingresolver-loopform.md @@ -52,6 +52,7 @@ - Stage‑1 UsingResolver: - Program(JSON) を入力に using/extern を解決(今は apply_usings=0 でバイパス多め)。defs/body の構造はそのまま Rust LoopForm v2 に渡る前提。 - region+next_i 形ループで JSON スキャン・modules_map を決定、prefix 結合するだけのテキスト担当箱。 + - using 解決の意味論そのものは `UsingResolveSSOTBox` に委譲する(Stage‑1 は「SSOT に渡す JSON/front を整える箱」)。 - Rust bridge (Stage0): - Program(JSON v0) → json_v0_bridge lowering → LoopForm v2 → MIR → VM/LLVM - Loop/PHI/SSA の SSOT は Rust 側。Stage‑1/.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` で固定。 - resolve_for_source 相当で entries ループと modules_map 参照(MapBox.has/get)を同時に行うケースを `mir_stage1_using_resolver_resolve_with_modules_map_verifies` で固定。 - 残り: なし(現状の 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 では空文字、Stage‑1 entry 側で prefix 結合だけを担当)。 +- Stage‑1 UsingResolverBox との分担: + - Stage‑1 UsingResolverBox は「JSON フロントから using_entries / modules_map を Region+next_i で収集・整形」する箱。 + - UsingResolveSSOTBox は「その結果をもとに最終的な modules/prefix を決める唯一の箱(SSOT)」として、IO なしで名前解決の意味論を持つ。 - pipeline_v2 UsingResolver(lang/src/compiler/pipeline_v2/using_resolver_box.hako) - 役割: modules_json 上で alias を解決する stateful helper(テキスト収集は entry 側)。 - ループ: `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 が揺れないこと。 - `using` 収集で early-exit しても merge 後の `pos` が決定的になること。 - いずれも 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 + break(name=="" で早期終了) + - `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/get+continue/break が同居する本線寄りパターン - Rust 側テストの取り扱い: - `src/tests/mir_stage1_using_resolver_verify.rs` に追加した構造テストは cargo test 経路で維持する。v2 quick スモークへの昇格は実行時間とノイズを見つつ後続フェーズで再検討(今回の設計タスクでは据え置き)。 - 観測ログ: MIR dump を残す場合は dev オンリー(`NYASH_LOOPFORM_DEBUG` / `HAKO_LOOP_PHI_TRACE`)に限定し、ログ経路は docs にも記載しておく。 diff --git a/docs/development/runtime/cli-hakorune-stage1.md b/docs/development/runtime/cli-hakorune-stage1.md index 7ebaf28d..65d8d7f1 100644 --- a/docs/development/runtime/cli-hakorune-stage1.md +++ b/docs/development/runtime/cli-hakorune-stage1.md @@ -51,13 +51,13 @@ hakorune [] [options] [-- script_args...] |-----------------------------------|-------------------------------------------|----------------------| | `run` | .hako をコンパイルして実行(既定 VM) | プレースホルダ(`[hakorune] run: not implemented yet`) | | `build exe` | .hako からネイティブ EXE を AOT ビルド | 実装済み(env.codegen経由で `.o` → EXE を生成) | -| `emit program-json` | Stage‑B で Program(JSON v0) を出力 | 実装済み(`BuildBox.emit_program_json_v0`) | -| `emit mir-json` | Program(JSON) → MIR(JSON) を出力 | 実装済み(`MirBuilderBox.emit_from_program_json_v0`) | +| `emit program-json` | Stage‑B で Program(JSON v0) を出力 | 実装済み(Stage0 ブリッジ + `.hako` Stage1Cli 完了) | +| `emit mir-json` | Program(JSON) → MIR(JSON) を出力 | 実装済み(Stage0 ブリッジ + `.hako` Stage1Cli 完了) | | `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) +### 実装ステータス(Phase 25.1a+) - `.hako → Program(JSON v0)`: - Stage‑B (`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 ] [--quiet] - `STAGE1_CLI_DEBUG`: - `1` のとき Stage‑1 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 仕様の原則: - 入口 `Stage1Cli.stage1_main(args)` は `cli_args_raw` を一切参照せず、上記 ENV だけを見てモード/入力ソース/backend を決定する。 - `.hako` 側で Program(JSON v0) / MIR(JSON) を emit したうえで、実行や AOT は常に Stage0/Rust に委譲する(Stage1 は CLI オーケストレーション専任)。 @@ -285,7 +297,7 @@ hakorune check [options] - System Hakorune subset 制約 などを検証するためのエントリポイント。 -- 実装は `.hako` 側で `tools/hako-check` 相当のロジックを呼び出す想定。 + - 実装は `.hako` 側で `tools/hako-check` 相当のロジックを呼び出す想定。 - Phase 25.1 では「名前予約」と「インターフェース定義」のみを行い、実装は Phase 26 以降。 ## Stage0 / Stage1 の責務分離(CLI 視点) diff --git a/lang/src/runner/stage1_cli.hako b/lang/src/runner/stage1_cli.hako index aad06c82..dd405615 100644 --- a/lang/src/runner/stage1_cli.hako +++ b/lang/src/runner/stage1_cli.hako @@ -157,23 +157,40 @@ static box Stage1Cli { // env から直接読み取り、Stage1CliConfigBox は将来の構造化用として温存する。 // mode 判定(env-only) - local mode = "disabled" - if env.get("STAGE1_EMIT_PROGRAM_JSON") == "1" { - mode = "emit_program_json" - } else if env.get("STAGE1_EMIT_MIR_JSON") == "1" { - mode = "emit_mir_json" - } else if env.get("NYASH_USE_STAGE1_CLI") == "1" { - mode = "run" + // 新形式(NYASH_STAGE1_MODE)があればそちらを優先し、旧 STAGE1_* はフォールバックとして扱う。 + local mode = env.get("NYASH_STAGE1_MODE") + if mode != null { mode = "" + mode } + if mode == null || mode == "" { + if env.get("STAGE1_EMIT_PROGRAM_JSON") == "1" { + mode = "emit-program" + } 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 と入力まわり - 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" } 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 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") // Log entry point for debugging @@ -196,7 +213,7 @@ static box Stage1Cli { } // Dispatch by mode - if mode == "emit_program_json" { + if mode == "emit-program" || mode == "emit_program_json" { if source == null || source == "" { if source_text != null && source_text != "" { source = source_text // Fallback to direct text for testing @@ -211,7 +228,7 @@ static box Stage1Cli { return 0 } - if mode == "emit_mir_json" { + if mode == "emit-mir" || mode == "emit_mir_json" { local prog_json = null if prog_path != null && prog_path != "" { prog_json = me._read_file("[stage1-cli] emit mir-json", prog_path) diff --git a/src/mir/builder/calls/unified_emitter.rs b/src/mir/builder/calls/unified_emitter.rs index 93f89cb6..579c52d3 100644 --- a/src/mir/builder/calls/unified_emitter.rs +++ b/src/mir/builder/calls/unified_emitter.rs @@ -264,16 +264,43 @@ impl UnifiedCallEmitterBox { &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)) // 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. + // + // 🎯 Phase 21.7: static box method の receiver 追加を防止 + // - StaticCompiler box kind: コンパイル時 static box(ParserBox, StageBArgsBox等) + // - これらは lowered function として定義され、receiver を期待しない + // - instance method(RuntimeData/UserDefined)のみ receiver を追加 if let Callee::Method { receiver: Some(recv), + box_kind, + box_name, + method, .. } = &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) diff --git a/src/mir/naming.rs b/src/mir/naming.rs index 29dad92e..c408d526 100644 --- a/src/mir/naming.rs +++ b/src/mir/naming.rs @@ -44,3 +44,42 @@ pub fn normalize_static_global_name(func_name: &str) -> 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::().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() +} + diff --git a/src/runner/stage1_bridge/args.rs b/src/runner/stage1_bridge/args.rs index cff020b1..e9cc761f 100644 --- a/src/runner/stage1_bridge/args.rs +++ b/src/runner/stage1_bridge/args.rs @@ -24,19 +24,31 @@ pub(super) struct Stage1Args { /// - emit_mir: emit mir-json ( or STAGE1_PROGRAM_JSON) /// - run: run --backend pub(super) fn build_stage1_args(groups: &CliGroups) -> Stage1Args { - let source = groups - .input - .file - .as_ref() - .cloned() + // Prefer new env (NYASH_STAGE1_*) and fall back to legacy names to keep compatibility. + let source = std::env::var("NYASH_STAGE1_INPUT") + .ok() + .or_else(|| { + groups + .input + .file + .as_ref() + .cloned() + }) .or_else(|| std::env::var("STAGE1_SOURCE").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() .as_deref() == 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 = Vec::new(); let mut source_env: Option = None; @@ -52,7 +64,9 @@ pub(super) fn build_stage1_args(groups: &CliGroups) -> Stage1Args { args.push(src); source_env = args.last().cloned(); } 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("mir-json".into()); args.push("--from-program-json".into()); @@ -74,8 +88,9 @@ pub(super) fn build_stage1_args(groups: &CliGroups) -> Stage1Args { process::exit(97); }); args.push("run".into()); - let backend = std::env::var("STAGE1_BACKEND") + let backend = std::env::var("NYASH_STAGE1_BACKEND") .ok() + .or_else(|| std::env::var("STAGE1_BACKEND").ok()) .unwrap_or_else(|| groups.backend.backend.clone()); args.push("--backend".into()); args.push(backend); diff --git a/src/runner/stage1_bridge/env.rs b/src/runner/stage1_bridge/env.rs index 45e2a3be..08bdf183 100644 --- a/src/runner/stage1_bridge/env.rs +++ b/src/runner/stage1_bridge/env.rs @@ -21,6 +21,29 @@ pub(super) fn configure_stage1_env( // Child recursion guard 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 if std::env::var("NYASH_NYRT_SILENT_RESULT").is_err() { 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"); } + // 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 適用を無効化し、 // prefix は空(HAKO_STAGEB_APPLY_USINGS=0)とする。 // UsingResolver/UsingCollector の検証は専用テストで行い、 diff --git a/src/tests/mir_stage1_using_resolver_verify.rs b/src/tests/mir_stage1_using_resolver_verify.rs index 05ed0302..aec084c2 100644 --- a/src/tests/mir_stage1_using_resolver_verify.rs +++ b/src/tests/mir_stage1_using_resolver_verify.rs @@ -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 が崩れないことを確認する。 #[test] fn mir_stage1_using_resolver_collect_entries_early_exit_verifies() { @@ -539,3 +648,95 @@ static box 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" + ); + } +} diff --git a/tools/stage1_smoke.sh b/tools/stage1_smoke.sh new file mode 100644 index 00000000..050b79f4 --- /dev/null +++ b/tools/stage1_smoke.sh @@ -0,0 +1,80 @@ +#!/usr/bin/env bash +set -euo pipefail + +# stage1_smoke.sh — Stage‑1 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 <&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