diff --git a/apps/tests/stage1_skip_ws_repro.hako b/apps/tests/stage1_skip_ws_repro.hako new file mode 100644 index 00000000..2ed43bbd --- /dev/null +++ b/apps/tests/stage1_skip_ws_repro.hako @@ -0,0 +1,5 @@ +static box Main { + main() { + return StringHelpers.skip_ws(" a", 0) + } +} diff --git a/docs/development/runtime/cli-hakorune-stage1.md b/docs/development/runtime/cli-hakorune-stage1.md index bb65467e..7ebaf28d 100644 --- a/docs/development/runtime/cli-hakorune-stage1.md +++ b/docs/development/runtime/cli-hakorune-stage1.md @@ -248,19 +248,28 @@ hakorune emit mir-json [-o ] [--quiet] - `mir_stage1_cli_emit_program_min_compiles_and_verifies`: - Stage1Cli + UsingResolver + BuildBox を含んだモジュールが MIR 生成・verify まで通ることを確認(SSA/PHI 崩壊なし)。 - `mir_stage1_cli_emit_program_min_exec_hits_type_error`: - - VM 実行まで進め、Stage‑1 CLI 経路がまだ決定的に失敗すること(型エラー or 未解決呼び出し)が Rust テスト内で再現できることを確認するための箱。 -- 現時点で観測されている実行時エラー(2025‑11‑21 時点): - - `apps/tests/stage1_cli_emit_program_min.hako` を直接 `hakorune` で実行すると、 - - 以前の「String + Void」「String + MapBox」ではなく、 - - `Type error: unsupported compare Ge on String("using \"foo/bar.hako\" as Foo\n") and Integer(19)` - - というエラーが Stage‑1 経路で発生している。 - - 経路としては `Stage1Cli.emit_program_json` → `BuildBox.emit_program_json_v0` → `ParserBox.parse_program2` まで進んだうえで、 - どこかで Compare(Ge) の両辺が String vs Int に崩れていることが確認できている。 -- デバッグ方針: - - Stage‑1 CLI 本線のバグ追跡は、必ず「小さな箱」から行う: - - `.hako` 側: `apps/tests/stage1_cli_emit_program_min.hako` で Stage1Cli+UsingResolver+BuildBox をまとめて叩く中間スモーク。 - - Rust 側: `src/tests/mir_stage1_cli_emit_program_min.rs` で同じソースを `NyashParser → MirCompiler → VM` まで持ち込むテスト。 - - 次フェーズでは、このハーネス上で Compare(Ge) 命令をスキャンし、「左オペランドがソース行 String、右が Int になっている Compare」を 1 関数単位まで特定し、その関数を別の minimal .hako + Rust テストに切り出して修正する。 + - VM 実行まで進め、Stage‑1 CLI 経路の型エラーや未解決呼び出しが発生しないことを確認するための箱(現在は安定化済み)。 + +### Stage‑1 CLI 環境変数(env-only 仕様) + +- Stage0 の `stage1_bridge.rs` から `.hako` 側 `stage1_cli.hako` を呼び出す際の最低限の ENV: + - `STAGE1_EMIT_PROGRAM_JSON` / `STAGE1_EMIT_MIR_JSON` / `NYASH_USE_STAGE1_CLI`: + - モード選択(emit_program_json / emit_mir_json / run)。 + - `STAGE1_SOURCE`: + - .hako ソースパス(FileBox 経由で読み込むときに使用)。 + - `STAGE1_SOURCE_TEXT`: + - ソース文字列を直接渡す開発用ショートカット(FileBox 不要、Stage‑B/Stage‑1 の最小スモーク用)。 + - `STAGE1_PROGRAM_JSON`: + - 事前に生成した Program(JSON v0) のパス。 + - `emit_mir_json` / `run` モードでは、これが設定されていれば file→JSON を優先し、無ければ `.hako → Program(JSON)` を呼び出す。 + - `STAGE1_BACKEND`: + - `run` 時の backend 選択(`vm` / `llvm` / `pyvm`。既定は `vm`)。 + - `STAGE1_CLI_DEBUG`: + - `1` のとき Stage‑1 CLI 側の debug ログ(`[stage1-cli/debug] ...`)と `__mir__.log` を有効化。 + +env-only 仕様の原則: +- 入口 `Stage1Cli.stage1_main(args)` は `cli_args_raw` を一切参照せず、上記 ENV だけを見てモード/入力ソース/backend を決定する。 +- `.hako` 側で Program(JSON v0) / MIR(JSON) を emit したうえで、実行や AOT は常に Stage0/Rust に委譲する(Stage1 は CLI オーケストレーション専任)。 ## `check` コマンド(予約) diff --git a/lang/src/compiler/parser/scan/parser_string_utils_box.hako b/lang/src/compiler/parser/scan/parser_string_utils_box.hako index 74bc5b8c..e46250de 100644 --- a/lang/src/compiler/parser/scan/parser_string_utils_box.hako +++ b/lang/src/compiler/parser/scan/parser_string_utils_box.hako @@ -2,6 +2,7 @@ // ParserStringUtilsBox — Minimal self-contained string helpers // Responsibility: Backward compatibility wrapper for parser code // Notes: sh_core への依存で VM 差分が出たため、ここに最小実装を内包する。 +using sh_core as StringHelpers // reuse shared helpers to align semantics static box ParserStringUtilsBox { // Numeric to string (minimal) @@ -104,13 +105,7 @@ static box ParserStringUtilsBox { } skip_ws(src, i) { - if src == null { return i } - local n = src.length() - local j = i - loop(j < n) { - local ch = src.substring(j, j + 1) - if ParserStringUtilsBox.is_space(ch) == 1 { j = j + 1 } else { break } - } - return j + // Delegate to shared helper to stay consistent with Stage‑B parser semantics + return StringHelpers.skip_ws(src, i) } } diff --git a/lang/src/runner/stage1_cli.hako b/lang/src/runner/stage1_cli.hako index c175af5a..aad06c82 100644 --- a/lang/src/runner/stage1_cli.hako +++ b/lang/src/runner/stage1_cli.hako @@ -6,6 +6,12 @@ // - NYASH_USE_STAGE1_CLI=1 : Rust 側がこの CLI を呼ぶときに使用 // - STAGE1_EMIT_PROGRAM_JSON=1 : source → Program(JSON v0) を emit // - STAGE1_EMIT_MIR_JSON=1 : Program(JSON v0) → MIR(JSON) を emit +// 環境変数(env-only 仕様): +// - STAGE1_SOURCE : .hako ソースパス(FileBox 経由で読み込む) +// - STAGE1_SOURCE_TEXT : ソース文字列(FileBox なしで渡すテスト用) +// - STAGE1_PROGRAM_JSON : 事前生成した Program(JSON v0) のパス(emit_mir_json/run で利用) +// - STAGE1_BACKEND : backend 選択(vm/llvm/pyvm、既定 vm) +// - STAGE1_CLI_DEBUG : debug ログを有効化 using lang.compiler.build.build_box as BuildBox using lang.compiler.entry.using_resolver_box as Stage1UsingResolverBox @@ -224,11 +230,20 @@ static box Stage1Cli { } // Default: run path (mode == "run") - if source == null || source == "" { - print("[stage1-cli] run: source path is required (set STAGE1_SOURCE)") - return 96 + local prog_json = null + if prog_path != null && prog_path != "" { + prog_json = me._read_file("[stage1-cli] run", prog_path) + } else { + if source == null || source == "" { + if source_text != null && source_text != "" { + source = source_text + } else { + print("[stage1-cli] run: source path is required (set STAGE1_SOURCE)") + return 96 + } + } + prog_json = me.emit_program_json(source) } - local prog_json = me.emit_program_json(source) if prog_json == null { return 96 } return me.run_program_json(prog_json, backend) } diff --git a/lang/src/shared/common/string_helpers.hako b/lang/src/shared/common/string_helpers.hako index ab5d2267..29b3ce9a 100644 --- a/lang/src/shared/common/string_helpers.hako +++ b/lang/src/shared/common/string_helpers.hako @@ -168,17 +168,20 @@ static box StringHelpers { // Skip whitespace from position i skip_ws(src, i) { if src == null { return i } - local n = src.length() + // Canonicalize to string to avoid mis-typed receivers in static boxes + local s = "" + src + local n = s.length() + local j = i local cont = 1 local guard = 0 local max = 100000 loop(cont == 1) { - if guard > max { return i } else { guard = guard + 1 } - if i < n { - if me.is_space(src.substring(i, i+1)) { i = i + 1 } else { cont = 0 } + if guard > max { return j } else { guard = guard + 1 } + if j < n { + if me.is_space(s.substring(j, j+1)) { j = j + 1 } else { cont = 0 } } else { cont = 0 } } - return i + return j } // Find last occurrence of pattern in string (backward search)