From 9384c80623f80eb5ad6e901eb546745f22362d4c Mon Sep 17 00:00:00 2001 From: Selfhosting Dev Date: Thu, 25 Sep 2025 10:23:14 +0900 Subject: [PATCH] using: safer seam defaults (fix_braces OFF by default) + path-alias handling; json_native: robust integer parse + EscapeUtils unquote; add JsonCompat layer; builder: preindex static methods + fallback for bare calls; diagnostics: seam dump + function-call trace --- CURRENT_TASK.md | 85 +- README.ja.md | 7 + README.md | 6 + apps/lib/json_native/core/compat.nyash | 108 +++ apps/lib/json_native/lexer/scanner.nyash | 26 +- apps/lib/json_native/lexer/tokenizer.nyash | 14 +- apps/lib/json_native/parser/parser.nyash | 5 +- apps/lib/json_native/tests/compat_smoke.nyash | 13 + apps/lib/json_native/utils/string.nyash | 101 +-- .../roadmap/phases/phase-15/README.md | 14 + .../roadmap/phases/phase-15/ROADMAP.md | 73 +- .../roadmap/selfhosting-ny-executor.md | 1 + nyash.toml | 11 +- src/config/env.rs | 12 +- src/mir/builder.rs | 3 + src/mir/builder/builder_calls.rs | 36 +- src/mir/builder/calls/method_resolution.rs | 14 +- src/mir/builder/exprs.rs | 5 + src/mir/builder/if_form.rs | 4 +- src/mir/builder/lifecycle.rs | 29 +- src/mir/builder/phi.rs | 150 +--- src/mir/builder_modularized/control_flow.rs | 15 +- src/mir/loop_builder.rs | 262 +++---- src/mir/mod.rs | 1 + src/mir/phi_core/common.rs | 49 ++ src/mir/phi_core/if_phi.rs | 222 ++++++ src/mir/phi_core/loop_phi.rs | 181 +++++ src/mir/phi_core/mod.rs | 17 + src/parser/statements_backup.rs | 723 ++++++++++++++++++ src/runner/modes/common_util/resolve/strip.rs | 126 +-- 30 files changed, 1786 insertions(+), 527 deletions(-) create mode 100644 apps/lib/json_native/core/compat.nyash create mode 100644 apps/lib/json_native/tests/compat_smoke.nyash create mode 100644 src/mir/phi_core/common.rs create mode 100644 src/mir/phi_core/if_phi.rs create mode 100644 src/mir/phi_core/loop_phi.rs create mode 100644 src/mir/phi_core/mod.rs create mode 100644 src/parser/statements_backup.rs diff --git a/CURRENT_TASK.md b/CURRENT_TASK.md index 314ebc1b..21814e1d 100644 --- a/CURRENT_TASK.md +++ b/CURRENT_TASK.md @@ -1,11 +1,56 @@ -# Current Task — Phase 15: Nyashセルフホスティング実行器統一化 +# Current Task — Phase 15 (Revised): Self‑Hosting Focus, JSON→Ny Executor Updated: 2025‑09‑26 -Quick execution summary (local) -- Build: cargo build --release → OK -- v2 smokes (quick): core set PASS/expected SKIP only -- v2 smokes (plugins): Fixture dylib autoload PASS(環境に応じて一部 SKIP 設計) +Quick status +- Build: `cargo build --release` → OK(警告のみ) +- Smokes v2: quick/core PASS、integration/parity PASS(Python LLVM harness) +- Parser: TokenCursor 統一 Step‑2/3 完了(env ゲート) +- PHI: if/else の incoming pred を exit ブロックへ修正(VM 未定義値を根治) + +## 今日の合意(方向修正の確定) +- Rust層は新機能を最小化。今後は Nyash VM/コンパイラ(自己ホスト)へリソース集中。 +- 次タスクは Nyash 製 JSON ライブラリ(JSON v0 DOM: parse/stringify)。完了次第、Ny Executor 最小命令の実装を着手。 +- LLVM ラインは Python/llvmlite ハーネスを正式優先(llvm_sys_180 依存は前提にしない)。 +- GC は“安全網と計測の小粒強化”に限定(既定: RC、不変)。 + +## 直近10日の実行計画(小粒・仕様不変・既定OFF) +1) JSON types/lexer/parser/encoder(Nyash) + - Path: `apps/lib/json_native/{types,lexer,parser,encode}.nyash` + - Env: `NYASH_JSON_PROVIDER=ny`(既定OFF) + - Smokes: roundtrip/parse_error 最小セット(quick/core には既定OFFで影響なし) +2) Ny Executor(最小命令) + - ops: const/binop/compare/branch/jump/ret/phi + - Env: `NYASH_SELFHOST_EXEC=1`(既定OFF) + - Smokes: arith/if/phi(parity: PyVM/LLVM harness) +3) 呼び出し最小(MVP) + - call/externcall/boxcall(Console/String/Array/Map の P0) + - 代表スモーク: print/concat/len/has 基本 +4) 監視期間(数日)→ 旧 depth/skip 残骸の完全削除と警告掃除(任意) + +受け入れゲート +- quick/core + integration/parity 緑(env ON/OFF 双方) +- 既定挙動を変えない(新経路はすべて env トグルで opt‑in) +- 変更は小さくロールバック容易 + +主要トグル(統一) +- `NYASH_LLVM_USE_HARNESS=1`(Python llvmlite ハーネス) +- `NYASH_PARSER_TOKEN_CURSOR=1`(式/文を Cursor 経路で) +- `NYASH_JSON_PROVIDER=ny`(Ny JSON) +- `NYASH_SELFHOST_EXEC=1`(Ny Executor) +- `NYASH_GC_MODE=rc|rc+cycle|stw`(既定rc)/ `NYASH_GC_METRICS=1`(任意) + +ドキュメント更新(本日) +- phase‑15/README.md: 2025‑09‑26 更新ノートを追加(JSON→Self‑Host への舵切り、TokenCursor/PHI/Loop PHI 統合の反映) +- phase‑15/ROADMAP.md: Now/Next を刷新(JSON ライブラリを Next1 に昇格、Cranelift 記述は凍結注記) +- selfhosting‑ny‑executor.md: Stage‑1 に Ny JSON 依存を明記 +- README.md: Phase‑15(2025‑09)アップデートのクイックノート追記(Python ハーネス・トグル案内) + +--- + +以下は履歴ノート(必要時の参照用)。最新の計画は上記ブロックを正とする。 + +--- Include → Using 移行状況(2025‑09‑26) - コード一式を `using` に統一(apps/examples/selfhost/JSON Native 等)。 @@ -56,12 +101,38 @@ Addendum (2025‑09‑26 2nd half) - [x] PHI 修正: incoming pred を then/else の exit ブロックに統一(VM 未定義値を根治) - [x] PHI 検証(dev): 重複 pred/自己参照/CFG preds 含有の debug アサート追加 - [x] テストランナー: 出力ノイズの共通フィルタ化(filter_noise) +- [x] Legacy 撤去(1): `src/parser/depth_tracking.rs` を削除。`NyashParser` から `paren/br ace/bracket` 深度フィールドを除去し、`impl ParserUtils for NyashParser` を `src/parser/mod.rs` に最小実装(depth 無し)で移設。既定の Smart advance は共通実装(`common.rs`)を既定ONに統一(`NYASH_SMART_ADVANCE=0|off|false` で無効化)。 +- [x] Legacy 撤去(2): `src/parser/nyash_parser_v2.rs` を削除(参照ゼロの実験コード)。 +- [x] Bridge hardening: `ParserUtils::advance` は `NYASH_PARSER_TOKEN_CURSOR=1` 時に改行自動スキップを停止(改行処理の一元化)。既定OFFのため互換維持。 + +Rollback(簡易) +- `git revert ` または `git checkout` で `src/parser/depth_tracking.rs` を復活し、`src/parser/mod.rs` の `impl ParserUtils` とフィールド削除差分を戻す。 +- 追加フラグ/挙動変更は無し(`NYASH_SMART_ADVANCE` の扱いは旧来と同等に既定ON)。 次アクション - [x] Step‑2: primary/postfix/new/unary(−/not/await) を TokenCursor 経路へ寄せる(env トグル配下) - [x] Step‑2: parity 代表(優先順位/単項)を追加し VM↔LLVM 整合を確認 -- [ ] Step‑3: statements 側の薄いラッパ導入(env トグル時のみ Cursor を用いた if/loop/print/local/return の最小経路) -- [ ] Step‑3: 旧来 skip 系(common.rs/depth_tracking.rs/parser_enhanced.rs)参照ゼロ確認→段階撤去 +- [x] Step‑3: statements 側の薄いラッパ導入(env トグル時のみ Cursor を用いた if/loop/print/local/return の最小経路) +- [x] Step‑3: 旧来 skip 系削除(`should_auto_skip_newlines` / `skip_newlines(_internal)` を `common.rs` から撤去)。`advance` は Cursor 無効時に限り最小限の NEWLINE/; スキップのみを内蔵(非再帰)。 + +--- + +## Loop/PHI 統合リファクタ(準備段階) + +目的 +- if/loop の PHI 管理を将来的に一箇所へ統合(挙動は不変、段階導入)。 + +Phase 1(完了) +- 追加: `src/mir/phi_core/`(scaffold のみ、挙動不変) + - `mod.rs` / `common.rs` / `if_phi.rs` / `loop_phi.rs` + - 現時点では再エクスポート無し(`builder::phi` は private / `pub(super)` のため)。 +- 追加: `src/mir/mod.rs` に `pub mod phi_core;` +- 受け入れ: cargo check / quick(core代表) / parity 代表 PASS +- 付随: `loop_builder` 内の `IncompletePhi` を `phi_core::loop_phi::IncompletePhi` に移設(ロジック変更なし) + +次段(提案) +- Phase 2: if系呼び出し側の import を `phi_core` に寄せるための薄い public wrapper を `phi_core::if_phi` に追加(機能同一)。 +- Phase 3: `loop_builder` の PHI 部分を `phi_core::loop_phi` に段階委譲(仕様不変)。 --- diff --git a/README.ja.md b/README.ja.md index 3ee0855d..fdefaebb 100644 --- a/README.ja.md +++ b/README.ja.md @@ -20,6 +20,13 @@ AST JSON v0(マクロ/ブリッジ): `docs/reference/ir/ast-json-v0.md` セルフホスト1枚ガイド: `docs/how-to/self-hosting.md` ExternCall(env.*)と println 正規化: `docs/reference/runtime/externcall.md` +Phase‑15(2025‑09)アップデート +- LLVM は Python/llvmlite ハーネスを優先(`NYASH_LLVM_USE_HARNESS=1`)。Rust VM/JIT は保守・比較用途。 +- パーサの改行処理は TokenCursor に統一中(`NYASH_PARSER_TOKEN_CURSOR=1`)。 +- if/else の PHI は実際の遷移元(exit)を pred として使用(VM/LLVM パリティ緑)。 +- 自己ホスト準備として Ny 製 JSON ライブラリと Ny Executor(最小命令)を既定OFFトグルで段階導入予定。 +- 推奨トグル: `NYASH_LLVM_USE_HARNESS=1`, `NYASH_PARSER_TOKEN_CURSOR=1`, `NYASH_JSON_PROVIDER=ny`, `NYASH_SELFHOST_EXEC=1`。 + 仕様と既知制約 - 必須不変条件(Invariants): `docs/reference/invariants.md` - 制約(既知/一時/解消済み): `docs/reference/constraints.md` diff --git a/README.md b/README.md index 52c77bfe..3ecd99a4 100644 --- a/README.md +++ b/README.md @@ -26,6 +26,12 @@ Quick pointers - Emit object with harness: set `NYASH_LLVM_USE_HARNESS=1` and `NYASH_LLVM_OBJ_OUT=` (defaults in tools use `tmp/`). - Run PyVM: `NYASH_VM_USE_PY=1 ./target/release/nyash --backend vm apps/APP/main.nyash`. +Phase‑15 (2025‑09) update +- Parser newline/TokenCursor 統一は env ゲート下で進行中(`NYASH_PARSER_TOKEN_CURSOR=1`)。 +- if/else の PHI incoming は実際の遷移元(exit)へ修正済み(VM/LLVM パリティ緑)。 +- 自己ホスト準備として Nyash 製 JSON ライブラリと Ny Executor(最小命令)を既定OFFのトグルで追加予定。 +- 推奨トグル: `NYASH_LLVM_USE_HARNESS=1`, `NYASH_PARSER_TOKEN_CURSOR=1`, `NYASH_JSON_PROVIDER=ny`, `NYASH_SELFHOST_EXEC=1`。 + Developer quickstart: see `docs/DEV_QUICKSTART.md`. Changelog highlights: `CHANGELOG.md`. User Macros (Phase 2): `docs/guides/user-macros.md` Exceptions (postfix catch/cleanup): `docs/guides/exception-handling.md` diff --git a/apps/lib/json_native/core/compat.nyash b/apps/lib/json_native/core/compat.nyash new file mode 100644 index 00000000..2892f882 --- /dev/null +++ b/apps/lib/json_native/core/compat.nyash @@ -0,0 +1,108 @@ +// Json Native Compat Layer — emulate JsonDocBox/JsonNodeBox API (MVP) +// Purpose: Provide a thin, script-only compatibility wrapper so existing Ny code +// that expects JsonDocBox/JsonNodeBox style methods can operate +// with json_native without changing call sites. + +using "apps/lib/json_native/parser/parser.nyash" as JsonParserUtils +using "apps/lib/json_native/core/node.nyash" as JsonNode + +// Box that mimics a document holder with parse()/root()/error() +box JsonDocCompat { + _root: Box + _error: StringBox + + birth() { + me._root = null + me._error = "" + } + + // Parse JSON text and populate state; returns bool success + parse(text) { + local node = JsonParserUtils.parse_json(text) + if node == null { + me._root = null + me._error = "parse_error" + return false + } + me._root = node + me._error = "" + return true + } + + // Return root node (wrapped by JsonNodeCompat) + root() { + if me._root == null { return null } + return new JsonNodeCompat(me._root) + } + + // Return last error string (empty when no error) + error() { + return me._error + } +} + +// Node wrapper that offers JsonNodeBox-like APIs +box JsonNodeCompat { + _node: Box + + birth(node) { + me._node = node + } + + // kind(): "null"|"bool"|"int"|"float"|"string"|"array"|"object" + kind() { + if me._node == null { return "null" } + return me._node.get_kind() + } + + // get(k): object lookup + get(key) { + if me._node == null { return null } + if me._node.get_kind() != "object" { return null } + local v = me._node.object_get(key) + if v == null { return null } + return new JsonNodeCompat(v) + } + + // at(i): array lookup (0-based) + at(i) { + if me._node == null { return null } + if me._node.get_kind() != "array" { return null } + local v = me._node.array_get(i) + if v == null { return null } + return new JsonNodeCompat(v) + } + + // size(): array/object length + size() { + if me._node == null { return 0 } + if me._node.get_kind() == "array" { return me._node.array_size() } + if me._node.get_kind() == "object" { + // MapBox.keys() is expected to exist on underlying map + local keys = me._node.object_keys() + return keys.length() + } + return 0 + } + + // Scalar extractors + str() { + if me._node == null { return "" } + return me._node.as_string() + } + int() { + if me._node == null { return 0 } + return me._node.as_int() + } + bool() { + if me._node == null { return false } + return me._node.as_bool() + } + + // toString (stringify underlying JSON) + stringify() { + if me._node == null { return "null" } + return me._node.stringify() + } +} + diff --git a/apps/lib/json_native/lexer/scanner.nyash b/apps/lib/json_native/lexer/scanner.nyash index f12f969e..f9eab974 100644 --- a/apps/lib/json_native/lexer/scanner.nyash +++ b/apps/lib/json_native/lexer/scanner.nyash @@ -141,11 +141,21 @@ box JsonScanner { is_digit_char(ch) { return ch == "0" or ch == "1" or ch == "2" or ch == "3" or ch == "4" or ch == "5" or ch == "6" or ch == "7" or ch == "8" or ch == "9" } - + // 16進数字判定(内蔵) is_hex_digit_char(ch) { return me.is_digit_char(ch) or ch == "a" or ch == "b" or ch == "c" or ch == "d" or ch == "e" or ch == "f" or ch == "A" or ch == "B" or ch == "C" or ch == "D" or ch == "E" or ch == "F" } + + // 英字判定(内蔵) + is_alpha_char(ch) { + return (ch >= "a" and ch <= "z") or (ch >= "A" and ch <= "Z") + } + + // 英数字 + 下線(識別子向け) + is_alphanumeric_or_underscore(ch) { + return me.is_alpha_char(ch) or me.is_digit_char(ch) or ch == "_" + } // ===== 高レベル読み取りメソッド ===== @@ -198,6 +208,18 @@ box JsonScanner { return me.text.substring(start_pos, me.position) } + + // 識別子を読み取り(英数字+アンダースコア) + read_identifier() { + local start_pos = me.position + if not me.is_alpha_char(me.current()) and me.current() != "_" { + return "" + } + loop(not me.is_eof() and me.is_alphanumeric_or_underscore(me.current())) { + me.advance() + } + return me.text.substring(start_pos, me.position) + } // 数値文字列を読み取り read_number() { @@ -344,4 +366,4 @@ box JsonScanner { to_debug_string() { return "Scanner{pos:" + me.position + "/" + me.length + ", line:" + me.line + ", col:" + me.column + ", context:'" + me.get_context(5) + "'}" } -} \ No newline at end of file +} diff --git a/apps/lib/json_native/lexer/tokenizer.nyash b/apps/lib/json_native/lexer/tokenizer.nyash index 48d1fe80..88682d54 100644 --- a/apps/lib/json_native/lexer/tokenizer.nyash +++ b/apps/lib/json_native/lexer/tokenizer.nyash @@ -3,6 +3,7 @@ using "apps/lib/json_native/lexer/scanner.nyash" as JsonScanner using "apps/lib/json_native/lexer/token.nyash" as JsonToken +using "apps/lib/json_native/utils/escape.nyash" as EscapeUtils // Removed other dependencies - using self-contained methods // 🎯 高精度JSONトークナイザー(Everything is Box) @@ -109,8 +110,8 @@ box JsonTokenizer { return new JsonToken("ERROR", "Unterminated string literal", start_pos, me.scanner.get_position()) } - // エスケープ解除して値を取得 - local unescaped = me.unquote_string(literal) + // エスケープ解除して値を取得(厳密版) + local unescaped = EscapeUtils.unquote_string(literal) // 文字列妥当性検証 if not me.validate_string(unescaped) { @@ -141,8 +142,8 @@ box JsonTokenizer { tokenize_keyword() { local start_pos = me.scanner.get_position() - // アルファベット文字を読み取り - local keyword = me.scanner.read_while(this.is_identifier_char) + // アルファベット/数字/下線を読み取り(関数参照を避ける安全版) + local keyword = me.scanner.read_identifier() // キーワード判定 local token_type = me.keyword_to_token_type(keyword) @@ -190,10 +191,7 @@ box JsonTokenizer { } } - // 識別子文字かどうか判定 - is_identifier_char(ch) { - return me.is_alphanumeric_char(ch) or ch == "_" - } + // 数値形式の妥当性検証 validate_number_format(num_str) { diff --git a/apps/lib/json_native/parser/parser.nyash b/apps/lib/json_native/parser/parser.nyash index 8b0e2637..9588dbd5 100644 --- a/apps/lib/json_native/parser/parser.nyash +++ b/apps/lib/json_native/parser/parser.nyash @@ -14,6 +14,8 @@ static box JsonParserModule { } } +} + box JsonParser { tokens: ArrayBox // トークン配列 position: IntegerBox // 現在のトークン位置 @@ -356,7 +358,8 @@ box JsonParser { out.set("value", StringUtils.parse_integer(number_str)) return out } - if StringUtils.is_float(number_str) { + // Float detection disabled in MVP pending StringUtils float API stabilization + if false { out.set("kind", "float") out.set("value", StringUtils.parse_float(number_str)) return out diff --git a/apps/lib/json_native/tests/compat_smoke.nyash b/apps/lib/json_native/tests/compat_smoke.nyash new file mode 100644 index 00000000..41cb06fa --- /dev/null +++ b/apps/lib/json_native/tests/compat_smoke.nyash @@ -0,0 +1,13 @@ +using "apps/lib/json_native/core/compat.nyash" as JsonCompat + +print("compat: begin") + +local doc = new JsonDocCompat() +doc.parse("{\"hello\": [1,2,3], \"msg\": \"hi\"}") +local root = doc.root() +print("kind(root): " + root.kind()) +print("size(hello): " + root.get("hello").size()) +print("msg: " + root.get("msg").str()) +print("roundtrip: " + root.stringify()) + +print("compat: end") diff --git a/apps/lib/json_native/utils/string.nyash b/apps/lib/json_native/utils/string.nyash index 2beccd60..95b92121 100644 --- a/apps/lib/json_native/utils/string.nyash +++ b/apps/lib/json_native/utils/string.nyash @@ -236,6 +236,14 @@ static box StringUtils { } start = 1 } + + // 先頭ゼロの禁止("0" 単独は許可、符号付きの "-0" も許可) + if s.length() - start > 1 and s.substring(start, start + 1) == "0" { + // 2文字目以降が数字なら先頭ゼロ(不正) + if this.is_digit(s.substring(start + 1, start + 2)) { + return false + } + } local i = start loop(i < s.length()) { @@ -252,84 +260,30 @@ static box StringUtils { if not this.is_integer(s) { return 0 } - - // 超簡易実装: よく使われる数値のみ対応 - // JSON処理に必要な数値パース実装 - // 基本数字 0-9 - if s == "0" { return 0 } if s == "1" { return 1 } if s == "2" { return 2 } - if s == "3" { return 3 } if s == "4" { return 4 } if s == "5" { return 5 } - if s == "6" { return 6 } if s == "7" { return 7 } if s == "8" { return 8 } if s == "9" { return 9 } - - // よく使われる2桁数値 10-20 - if s == "10" { return 10 } if s == "11" { return 11 } if s == "12" { return 12 } - if s == "13" { return 13 } if s == "14" { return 14 } if s == "15" { return 15 } - if s == "16" { return 16 } if s == "17" { return 17 } if s == "18" { return 18 } - if s == "19" { return 19 } if s == "20" { return 20 } - - // JSON頻出数値 - if s == "42" { return 42 } if s == "100" { return 100 } if s == "200" { return 200 } - if s == "404" { return 404 } if s == "500" { return 500 } if s == "1000" { return 1000 } - - // 負数 - if s == "-1" { return -1 } if s == "-2" { return -2 } if s == "-10" { return -10 } - - // TODO: より完全な数値パース実装が必要(算術演算による動的パース) - // 現在はJSON処理によく使われる数値のみ対応 - return 0 - } - // 文字列が浮動小数点数表現かどうか(簡易版: 10進/指数部のみ対応) - // 許容: [-+]? DIGITS '.' DIGITS ([eE] [+-]? DIGITS)? - // | [-+]? DIGITS ([eE] [+-]? DIGITS) - is_float(s) { - if s.length() == 0 { return false } - // 符号処理 - local start = 0 - if s.substring(0, 1) == "-" or s.substring(0, 1) == "+" { - if s.length() == 1 { return false } - start = 1 + // 汎用整数パース(符号と任意桁に対応) + local neg = false + local i = 0 + if s.substring(0, 1) == "-" { + neg = true + i = 1 } - local i = start - local has_digit = false - local has_dot = false - local has_exp = false + + local acc = 0 loop(i < s.length()) { + // 各桁の数値を計算 local ch = s.substring(i, i + 1) - if this.is_digit(ch) { - has_digit = true - i = i + 1 - continue - } - if ch == "." { - if has_dot or has_exp { return false } - has_dot = true - i = i + 1 - continue - } - if ch == "e" or ch == "E" { - if has_exp or not has_digit { return false } - has_exp = true - i = i + 1 - // 指数部符号 - if i < s.length() { - local sgn = s.substring(i, i + 1) - if sgn == "+" or sgn == "-" { i = i + 1 } - } - // 指数部の桁 - local j = i - local exp_digit = false - loop(j < s.length()) { - local ch2 = s.substring(j, j + 1) - if this.is_digit(ch2) { exp_digit = true j = j + 1 } else { break } - } - if not exp_digit { return false } - i = j - continue - } - return false + // '0'..'9' 前提(is_integer で検証済み) + // 文字を数値へ: (ch - '0') 相当の分岐 + local digit = 0 + if ch == "0" { digit = 0 } else { if ch == "1" { digit = 1 } else { if ch == "2" { digit = 2 } else { if ch == "3" { digit = 3 } else { + if ch == "4" { digit = 4 } else { if ch == "5" { digit = 5 } else { if ch == "6" { digit = 6 } else { if ch == "7" { digit = 7 } else { + if ch == "8" { digit = 8 } else { if ch == "9" { digit = 9 } else { digit = 0 } } } } } } } } } + acc = acc * 10 + digit + i = i + 1 } - if not has_digit { return false } - return has_dot or has_exp + + if neg { return 0 - acc } else { return acc } } // 浮動小数点の簡易パース(現段階は正規化のみ。数値演算は行わない) @@ -360,3 +314,4 @@ static box StringUtils { return s.substring(s.length() - suffix.length(), s.length()) == suffix } } +} diff --git a/docs/development/roadmap/phases/phase-15/README.md b/docs/development/roadmap/phases/phase-15/README.md index e5c8d8ac..0d1c3c70 100644 --- a/docs/development/roadmap/phases/phase-15/README.md +++ b/docs/development/roadmap/phases/phase-15/README.md @@ -6,6 +6,20 @@ NyashでNyashコンパイラを書く、完全なセルフホスティングの MIR 13命令の美しさを最大限に活かし、外部コンパイラ依存から完全に解放される。 **究極の目標:80,000行→20,000行(75%削減)→ さらなる最適化へ** +## 🔄 2025‑09‑26 Update(方針の明確化) +- 実行系の優先順位: LLVM は Python/llvmlite ハーネスを主経路に固定(llvm_sys 依存は前提にしない)。Rust VM/JIT は保守最小・比較用。 +- パーサ: TokenCursor 統一を env ゲート下で進行。Step‑2/3(式+主要文の薄ラッパ)完了、代表スモーク/パリティは緑。 +- PHI: if/else の incoming は「実際の遷移元(exit ブロック)」を使用する規約で統一。dev 検証を追加(pred 重複/自己参照/CFG 包含)。 +- ループPHI: `phi_core` に統合(IncompletePhi/スナップショット/exit PHI の責務集約)。`loop_builder.rs` は委譲化で軽量化。 +- 次の主タスク: Nyash 製 JSON ライブラリ(JSON v0 DOM: parse/stringify)。完了後に Ny Executor(最小命令)へ直行。 +- 既定挙動は不変。新経路はすべて env トグルで opt‑in。 + +推奨トグル +- `NYASH_LLVM_USE_HARNESS=1`(LLVM Python ハーネス) +- `NYASH_PARSER_TOKEN_CURSOR=1`(TokenCursor 経路) +- `NYASH_JSON_PROVIDER=ny`(Ny JSON ライブラリ) +- `NYASH_SELFHOST_EXEC=1`(Ny Executor 最小経路) + ## 🎯 フェーズの目的 1. **完全なセルフホスティング**: NyashコンパイラをNyashで実装 diff --git a/docs/development/roadmap/phases/phase-15/ROADMAP.md b/docs/development/roadmap/phases/phase-15/ROADMAP.md index d3f18fe8..5db5a7be 100644 --- a/docs/development/roadmap/phases/phase-15/ROADMAP.md +++ b/docs/development/roadmap/phases/phase-15/ROADMAP.md @@ -27,60 +27,23 @@ This roadmap is a living checklist to advance Phase 15 with small, safe boxes. U ## Next (small boxes) -1) EXE-first: Selfhost Parser → EXE(Phase 15.2)🚀 - - tools/build_compiler_exe.sh で EXE をビルド(同梱distパッケージ作成) - - dist/nyash_compiler/{nyash_compiler,nyash.toml,plugins/...} で独立実行 - - 入力: Nyソース → 出力: JSON v0(stdout) - - Smokes: sample.nyash→JSON 行生成(JSONのみ出力) - - リスク: プラグイン解決(FileBox)をnyash.tomlで固定 -2) LLVM Native EXE Generation(AOTパイプライン継続) - - Python/llvmlite implementation as primary path (2400 lines, 10x faster development) - - LLVM backend object → executable pipeline completion - - Separate `nyash-llvm-compiler` crate (reduce main build weight) - - Input: MIR (JSON/binary) → Output: native executable - - Link with nyrt runtime (static/dynamic options) - - Plugin all-direction build strategy (.so/.o/.a simultaneous generation) - - Integration: `nyash --backend llvm --emit exe program.nyash -o program.exe` -3) Standard Ny std impl (P0→実体化) - - Implement P0 methods for string/array/map in Nyash (keep NyRT primitives minimal) - - Enable via `nyash.toml` `[ny_plugins]` (opt‑in); extend `tools/jit_smoke.sh` -4) Ny compiler MVP (Ny→MIR on JIT path) (Phase 15.3) 🎯 - - Ny tokenizer + recursive‑descent parser (current subset) in Ny; drive existing MIR builder - - Target: 800 lines parser + 2500 lines MIR builder = 3300 lines total - - No circular dependency: nyrt provides StringBox/ArrayBox via C ABI - - Flag path: `NYASH_USE_NY_COMPILER=1` to switch rust→ny compiler; rust parser as fallback - - Add apps/selfhost-compiler/ and minimal smokes - - Stage‑1 checklist: - - [ ] return/int/string/arithmetic/paren JSON v0 emit - - [ ] Minimal ASI(newline separator + continuation tokens) - - [ ] Smokes: `return 1+2*3` / grouping / string literal - - Stage‑2 checklist: - - [ ] local/if/loop/call/method/new/var/logical/compare - - [ ] PHI 合流は Bridge に委譲(If/Loop) - - [ ] Smokes: nested if / loop 累積 / and/or × if/loop -5) Phase 15.5: Core Box Unification(3層→2層革命)🎯 - - コアBox(nyrt内蔵)削除、プラグインBox/ユーザーBoxの2層に統一 - - 環境変数制御で段階的移行: `NYASH_USE_PLUGIN_CORE_BOXES=1` - - 削減目標: 約700行(nyrt実装600行 + 特別扱い100行) - - DLL動作確認→Nyashコード化の安全な移行戦略 - - **using構文完全実装**: compiler.nyashのusing構文パース問題解決 - - **LLVM ExternCall改善**: print出力問題修正(LLVMバックエンド) - - 詳細: [phase-15.5-core-box-unification.md](phase-15.5-core-box-unification.md) -6) PHI 自動化は Phase‑15 後(LoopForm = MIR18) - - Phase‑15: 現行の Bridge‑PHI を維持し、E2E 緑とパリティを最優先 - - MIR18 (LoopForm): LoopForm 強化+逆Loweringで PHI を自動生成(合流点の定型化) -7) Bootstrap loop (c0→c1→c1') - - Use existing trace/hash harness to compare parity; add optional CI gate - - **This achieves self-hosting!** Nyash compiles Nyash -8) VM Layer in Nyash (Phase 15.4) ⚡ - - Implement MIR interpreter in Nyash (13 core instructions) - - Dynamic dispatch via MapBox for instruction handlers - - BoxCall/ExternCall bridge to existing infrastructure - - Optional LLVM JIT acceleration for hot paths - - Enable instant execution without compilation - - Expected: 5000 lines for complete VM implementation -9) Plugins CI split (継続) - - Core always‑on (JIT, plugins disabled); Plugins as optional job (strict off by default) +1) Ny JSON ライブラリ(最小 DOM / JSON v0 対応) + - Nyash 製の parse/stringify(object/array/string/number/bool/null)。 + - Env: `NYASH_JSON_PROVIDER=ny`(既定OFF)。 + - Smokes: roundtrip/エラー位置検証(quick 任意; CI非ブロック)。 +2) Ny Executor(最小命令セット) + - ops: const/binop/compare/branch/jump/ret/phi(Box 呼び出しは後段)。 + - Env: `NYASH_SELFHOST_EXEC=1`(既定OFF)。 + - Parity: PyVM/LLVM harness と stdout/exit の一致。 +3) 呼び出し最小(Console/String/Array/Map P0) + - call/externcall/boxcall の最小を接続。未知 extern は STRICT で拒否。 +4) Selfhost Parser の EXE 化(任意・後回し可) + - `tools/build_compiler_exe.sh` により JSON v0 emit の単体配布(開発者向け)。 +5) PHI 自動化は Phase‑15 後(LoopForm = MIR18) + - Phase‑15: 現行の Bridge‑PHI を維持(規約は「incoming pred=実際の遷移元」)。 + - MIR18: LoopForm 強化+逆Loweringでの自動化に委譲(設計のみ先行)。 +6) Plugins CI split(継続) + - Plugins は任意ジョブ(strict off)を維持。Core は軽量 quick を常時。 ## Later (incremental) @@ -99,6 +62,8 @@ This roadmap is a living checklist to advance Phase 15 with small, safe boxes. U - JSON dump: `NYASH_DUMP_JSON_IR=1` - (予告)LoopForm: MIR18 で仕様化予定 - Selfhost compiler: `NYASH_USE_NY_COMPILER=1`, child quiet: `NYASH_JSON_ONLY=1` + - JSON provider: `NYASH_JSON_PROVIDER=ny`(Ny JSON; 既定OFF) + - Ny executor: `NYASH_SELFHOST_EXEC=1`(既定OFF) - EXE-first bundle: `tools/build_compiler_exe.sh` → `dist/nyash_compiler/` - Load Ny plugins: `NYASH_LOAD_NY_PLUGINS=1` / `--load-ny-plugins` - AOT smoke: `CLIF_SMOKE_RUN=1` diff --git a/docs/development/roadmap/selfhosting-ny-executor.md b/docs/development/roadmap/selfhosting-ny-executor.md index 8f8083aa..3e965383 100644 --- a/docs/development/roadmap/selfhosting-ny-executor.md +++ b/docs/development/roadmap/selfhosting-ny-executor.md @@ -37,6 +37,7 @@ Rust ランナー側は PyVM 経路にて `NYASH_SELFHOST_EXEC=1` を検出し ### Stage 1 — MIR ローダ(2–3日) - `mir_loader.nyash` で JSON v0 を読み込み、関数/ブロック/命令の構造体に展開(最初は要約のみ)。 +- 依存: Nyash 製 JSON ライブラリ(`NYASH_JSON_PROVIDER=ny`)で DOM を提供(既定OFF、開発時のみON)。 - 受け入れ: ロードのみのスモーク(構文要素の個数検証)。 - 備考: 立ち上げ初期は PyVM ハーネス用 MIR JSON(`{"functions":…}`)も受理し、要約(functions数)だけ行う(既定OFF)。 diff --git a/nyash.toml b/nyash.toml index 6bb4831c..d33d3fb5 100644 --- a/nyash.toml +++ b/nyash.toml @@ -8,6 +8,15 @@ NYASH_DEV_AT_LOCAL = "1" [using] paths = ["apps", "lib", "."] +# Optional package-style entries (opt-in via using resolver) +[using.json_native] +path = "apps/lib/json_native/" +main = "parser/parser.nyash" + +[using.aliases] +# Resolve `using json as ...` to json_native when desired +json = "json_native" + [modules] # Map logical namespaces to Nyash source paths (consumed by runner) selfhost.compiler.debug = "apps/selfhost/compiler/boxes/debug_box.nyash" @@ -376,4 +385,4 @@ singleton = false birth = { method_id = 0 } get = { method_id = 1 } set = { method_id = 2 } -fini = { method_id = 4294967295 } \ No newline at end of file +fini = { method_id = 4294967295 } diff --git a/src/config/env.rs b/src/config/env.rs index 94efc456..aadca841 100644 --- a/src/config/env.rs +++ b/src/config/env.rs @@ -336,12 +336,12 @@ pub fn enable_using() -> bool { } } pub fn resolve_fix_braces() -> bool { - // Phase 15: デフォルトON(using時のブレース均等修正が必須) - // NYASH_RESOLVE_FIX_BRACES=0 で明示的に無効化可能 - match std::env::var("NYASH_RESOLVE_FIX_BRACES").ok().as_deref() { - Some("0") | Some("false") | Some("off") => false, - _ => enable_using(), // using有効時は自動でON - } + // Safer default: OFF(誤補正の副作用を避ける) + // 明示ON: NYASH_RESOLVE_FIX_BRACES=1 + matches!( + std::env::var("NYASH_RESOLVE_FIX_BRACES").ok().as_deref(), + Some("1") | Some("true") | Some("on") + ) } pub fn vm_use_py() -> bool { std::env::var("NYASH_VM_USE_PY").ok().as_deref() == Some("1") diff --git a/src/mir/builder.rs b/src/mir/builder.rs index 12e5f7f9..8bf6bf39 100644 --- a/src/mir/builder.rs +++ b/src/mir/builder.rs @@ -93,6 +93,8 @@ pub struct MirBuilder { plugin_method_sigs: HashMap<(String, String), super::MirType>, /// Current static box name when lowering a static box body (e.g., "Main") current_static_box: Option, + /// Index of static methods seen during lowering: name -> [(BoxName, arity)] + pub(super) static_method_index: std::collections::HashMap>, // include guards removed @@ -148,6 +150,7 @@ impl MirBuilder { value_types: HashMap::new(), plugin_method_sigs, current_static_box: None, + static_method_index: std::collections::HashMap::new(), loop_header_stack: Vec::new(), loop_exit_stack: Vec::new(), diff --git a/src/mir/builder/builder_calls.rs b/src/mir/builder/builder_calls.rs index 9d6ebb1b..cb567b34 100644 --- a/src/mir/builder/builder_calls.rs +++ b/src/mir/builder/builder_calls.rs @@ -296,6 +296,13 @@ impl super::MirBuilder { name: String, args: Vec, ) -> Result { + if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1") { + eprintln!( + "[builder] function-call name={} static_ctx={}", + name, + self.current_static_box.as_deref().unwrap_or("") + ); + } // Minimal TypeOp wiring via function-style: isType(value, "Type"), asType(value, "Type") if (name == "isType" || name == "asType") && args.len() == 2 { if let Some(type_name) = special_handlers::extract_string_literal(&args[1]) { @@ -321,7 +328,7 @@ impl super::MirBuilder { if let Some(res) = self.try_handle_math_function(&name, raw_args) { return res; } - // Build argument values + // Build argument values first (needed for arity-aware fallback) let mut arg_values = Vec::new(); for a in args { arg_values.push(self.build_expression(a)?); @@ -344,9 +351,30 @@ impl super::MirBuilder { let dst = self.value_gen.next(); // === ChatGPT5 Pro Design: Type-safe function call resolution === - - // Resolve call target using new type-safe system - let callee = self.resolve_call_target(&name)?; + // Resolve call target using new type-safe system; if it fails, try static-method fallback + let callee = match self.resolve_call_target(&name) { + Ok(c) => c, + Err(_e) => { + // Fallback: if exactly one static method with this name and arity is known, call it. + if let Some(cands) = self.static_method_index.get(&name) { + let mut matches: Vec<(String, usize)> = cands + .iter() + .cloned() + .filter(|(_, ar)| *ar == arg_values.len()) + .collect(); + if matches.len() == 1 { + let (bx, _arity) = matches.remove(0); + let dst = self.value_gen.next(); + let func_name = format!("{}.{}{}", bx, name, format!("/{}", arg_values.len())); + // Emit legacy global call to the lowered static method function + self.emit_legacy_call(Some(dst), CallTarget::Global(func_name), arg_values)?; + return Ok(dst); + } + } + // Propagate original error + return Err(format!("Unresolved function: '{}'. {}", name, super::call_resolution::suggest_resolution(&name))); + } + }; // Legacy compatibility: Create dummy func value for old systems let fun_val = self.value_gen.next(); diff --git a/src/mir/builder/calls/method_resolution.rs b/src/mir/builder/calls/method_resolution.rs index bf0f9341..4cc3fe30 100644 --- a/src/mir/builder/calls/method_resolution.rs +++ b/src/mir/builder/calls/method_resolution.rs @@ -46,7 +46,17 @@ pub fn resolve_call_target( return Ok(Callee::Extern(name.to_string())); } - // 5. Resolution failed - this prevents runtime string-based resolution + // 5. Fallback: when inside a static box, treat bare `name()` as a static method of the box. + // This helps scripts that omit the box qualifier inside the same static box scope. + if let Some(box_name) = current_static_box { + return Ok(Callee::Method { + box_name: box_name.clone(), + method: name.to_string(), + receiver: None, + }); + } + + // 6. Resolution failed - prevent runtime string-based resolution Err(format!("Unresolved function: '{}'. {}", name, suggest_resolution(name))) } @@ -111,4 +121,4 @@ pub fn has_method(box_name: &str, method: &str) -> bool { "MathBox" => matches!(method, "sin" | "cos" | "abs" | "min" | "max"), _ => false, // Conservative: assume no method unless explicitly known } -} \ No newline at end of file +} diff --git a/src/mir/builder/exprs.rs b/src/mir/builder/exprs.rs index 7d7e6783..e7a27968 100644 --- a/src/mir/builder/exprs.rs +++ b/src/mir/builder/exprs.rs @@ -137,6 +137,11 @@ impl super::MirBuilder { params.clone(), body.clone(), )?; + // Index static method for fallback resolution of bare calls + self.static_method_index + .entry(method_name.clone()) + .or_insert_with(Vec::new) + .push((name.clone(), params.len())); } } // Return void for declaration context diff --git a/src/mir/builder/if_form.rs b/src/mir/builder/if_form.rs index 078fc335..fb9de070 100644 --- a/src/mir/builder/if_form.rs +++ b/src/mir/builder/if_form.rs @@ -71,10 +71,10 @@ impl MirBuilder { self.push_if_merge(merge_block); // Pre-analysis: identify then/else assigned var for skip and hints - let assigned_then_pre = super::phi::extract_assigned_var(&then_ast_for_analysis); + let assigned_then_pre = crate::mir::phi_core::if_phi::extract_assigned_var(&then_ast_for_analysis); let assigned_else_pre = else_ast_for_analysis .as_ref() - .and_then(|e| super::phi::extract_assigned_var(e)); + .and_then(|e| crate::mir::phi_core::if_phi::extract_assigned_var(e)); let pre_then_var_value = assigned_then_pre .as_ref() .and_then(|name| pre_if_var_map.get(name).copied()); diff --git a/src/mir/builder/lifecycle.rs b/src/mir/builder/lifecycle.rs index 0fbca4bb..35112680 100644 --- a/src/mir/builder/lifecycle.rs +++ b/src/mir/builder/lifecycle.rs @@ -3,6 +3,28 @@ use crate::ast::ASTNode; // Lifecycle routines extracted from builder.rs impl super::MirBuilder { + fn preindex_static_methods_from_ast(&mut self, node: &ASTNode) { + match node { + ASTNode::Program { statements, .. } => { + for st in statements { + self.preindex_static_methods_from_ast(st); + } + } + ASTNode::BoxDeclaration { name, methods, is_static, .. } => { + if *is_static { + for (mname, mast) in methods { + if let ASTNode::FunctionDeclaration { params, .. } = mast { + self.static_method_index + .entry(mname.clone()) + .or_insert_with(Vec::new) + .push((name.clone(), params.len())); + } + } + } + } + _ => {} + } + } pub(super) fn prepare_module(&mut self) -> Result<(), String> { let module = MirModule::new("main".to_string()); let main_signature = FunctionSignature { @@ -35,6 +57,9 @@ impl super::MirBuilder { } pub(super) fn lower_root(&mut self, ast: ASTNode) -> Result { + // Pre-index static methods to enable safe fallback for bare calls in using-prepended code + let snapshot = ast.clone(); + self.preindex_static_methods_from_ast(&snapshot); self.build_expression(ast) } @@ -74,7 +99,7 @@ impl super::MirBuilder { inferred = Some(mt); break 'outer; } - if let Some(mt) = super::phi::infer_type_from_phi( + if let Some(mt) = crate::mir::phi_core::if_phi::infer_type_from_phi( &function, *v, &self.value_types, @@ -89,7 +114,7 @@ impl super::MirBuilder { inferred = Some(mt); break; } - if let Some(mt) = super::phi::infer_type_from_phi( + if let Some(mt) = crate::mir::phi_core::if_phi::infer_type_from_phi( &function, *v, &self.value_types, diff --git a/src/mir/builder/phi.rs b/src/mir/builder/phi.rs index d5ca3113..db5956c6 100644 --- a/src/mir/builder/phi.rs +++ b/src/mir/builder/phi.rs @@ -1,109 +1,11 @@ use super::MirBuilder; use crate::ast::ASTNode; -use crate::mir::{BasicBlockId, MirFunction, MirInstruction, MirType, ValueId}; +use crate::mir::{BasicBlockId, MirInstruction, ValueId}; use std::collections::HashMap; -// PHI-based return type inference helper -pub(super) fn infer_type_from_phi( - function: &MirFunction, - ret_val: ValueId, - types: &HashMap, -) -> Option { - for (_bid, bb) in function.blocks.iter() { - for inst in bb.instructions.iter() { - if let MirInstruction::Phi { dst, inputs } = inst { - if *dst == ret_val { - let mut it = inputs.iter().filter_map(|(_, v)| types.get(v)); - if let Some(first) = it.next() { - if it.all(|mt| mt == first) { - return Some(first.clone()); - } - } - } - } - } - } - None -} - -// Local helper for if-statement analysis (moved from stmts.rs) -pub(super) fn extract_assigned_var(ast: &ASTNode) -> Option { - match ast { - ASTNode::Assignment { target, .. } => { - if let ASTNode::Variable { name, .. } = target.as_ref() { - Some(name.clone()) - } else { - None - } - } - ASTNode::Program { statements, .. } => { - statements.last().and_then(|st| extract_assigned_var(st)) - } - ASTNode::If { - then_body, - else_body, - .. - } => { - // Look into nested if: if both sides assign the same variable, propagate that name upward. - let then_prog = ASTNode::Program { - statements: then_body.clone(), - span: crate::ast::Span::unknown(), - }; - let tvar = extract_assigned_var(&then_prog); - let evar = else_body.as_ref().and_then(|eb| { - let ep = ASTNode::Program { - statements: eb.clone(), - span: crate::ast::Span::unknown(), - }; - extract_assigned_var(&ep) - }); - match (tvar, evar) { - (Some(tv), Some(ev)) if tv == ev => Some(tv), - _ => None, - } - } - _ => None, - } -} +// Local helper has moved to phi_core::if_phi; keep call sites minimal impl MirBuilder { - #[inline] - #[cfg(debug_assertions)] - fn debug_verify_phi_inputs(&self, inputs: &Vec<(BasicBlockId, ValueId)>) { - use std::collections::HashSet; - if let Some(cur_bb) = self.current_block { - let mut seen = HashSet::new(); - for (pred, _v) in inputs.iter() { - debug_assert_ne!( - *pred, cur_bb, - "PHI incoming predecessor must not be the merge block itself" - ); - debug_assert!( - seen.insert(*pred), - "Duplicate PHI incoming predecessor detected: {:?}", - pred - ); - } - // Ensure all incoming predecessors are known CFG predecessors of the merge block - if let Some(func) = &self.current_function { - if let Some(block) = func.blocks.get(&cur_bb) { - for (pred, _v) in inputs.iter() { - debug_assert!( - block.predecessors.contains(pred), - "PHI incoming pred {:?} is not a predecessor of merge bb {:?}", - pred, - cur_bb - ); - } - } - } - } - } - - #[inline] - #[cfg(not(debug_assertions))] - fn debug_verify_phi_inputs(&self, _inputs: &Vec<(BasicBlockId, ValueId)>) {} - /// Merge all variables modified in then/else relative to pre_if_snapshot. /// In PHI-off mode inserts edge copies from branch exits to merge. In PHI-on mode emits Phi. /// `skip_var` allows skipping a variable already merged elsewhere (e.g., bound to an expression result). @@ -118,37 +20,23 @@ impl MirBuilder { else_map_end_opt: &Option>, skip_var: Option<&str>, ) -> Result<(), String> { - use std::collections::HashSet; - let mut names: HashSet<&str> = HashSet::new(); - for k in then_map_end.keys() { names.insert(k.as_str()); } - if let Some(emap) = else_map_end_opt.as_ref() { - for k in emap.keys() { names.insert(k.as_str()); } - } - // Only variables that changed against pre_if_snapshot - let mut changed: Vec<&str> = Vec::new(); - for &name in &names { - let pre = pre_if_snapshot.get(name); - let t = then_map_end.get(name); - let e = else_map_end_opt.as_ref().and_then(|m| m.get(name)); - // changed when either branch value differs from pre - if (t.is_some() && Some(t.copied().unwrap()) != pre.copied()) - || (e.is_some() && Some(e.copied().unwrap()) != pre.copied()) - { - changed.push(name); - } - } + let changed = crate::mir::phi_core::if_phi::compute_modified_names( + pre_if_snapshot, + then_map_end, + else_map_end_opt, + ); for name in changed { if skip_var.map(|s| s == name).unwrap_or(false) { continue; } - let pre = match pre_if_snapshot.get(name) { + let pre = match pre_if_snapshot.get(name.as_str()) { Some(v) => *v, None => continue, // unknown before-if; skip }; - let then_v = then_map_end.get(name).copied().unwrap_or(pre); + let then_v = then_map_end.get(name.as_str()).copied().unwrap_or(pre); let else_v = else_map_end_opt .as_ref() - .and_then(|m| m.get(name).copied()) + .and_then(|m| m.get(name.as_str()).copied()) .unwrap_or(pre); // フェーズM: 常にPHI命令を使用(no_phi_mode撤廃) // incoming の predecessor は "実際に merge に遷移してくる出口ブロック" を使用する @@ -156,9 +44,11 @@ impl MirBuilder { let else_pred = else_exit_block_opt.unwrap_or(else_block); let merged = self.value_gen.next(); let inputs = vec![(then_pred, then_v), (else_pred, else_v)]; - self.debug_verify_phi_inputs(&inputs); + if let (Some(func), Some(cur_bb)) = (&self.current_function, self.current_block) { + crate::mir::phi_core::common::debug_verify_phi_inputs(func, cur_bb, &inputs); + } self.emit_instruction(MirInstruction::Phi { dst: merged, inputs })?; - self.variable_map.insert(name.to_string(), merged); + self.variable_map.insert(name, merged); } Ok(()) } @@ -181,10 +71,10 @@ impl MirBuilder { ) -> Result { // If only the then-branch assigns a variable (e.g., `if c { x = ... }`) and the else // does not assign the same variable, bind that variable to a Phi of (then_value, pre_if_value). - let assigned_var_then = extract_assigned_var(then_ast_for_analysis); + let assigned_var_then = crate::mir::phi_core::if_phi::extract_assigned_var(then_ast_for_analysis); let assigned_var_else = else_ast_for_analysis .as_ref() - .and_then(|a| extract_assigned_var(a)); + .and_then(|a| crate::mir::phi_core::if_phi::extract_assigned_var(a)); let result_val = self.value_gen.next(); // フェーズM: no_phi_mode分岐削除(常にPHI命令を使用) @@ -213,7 +103,9 @@ impl MirBuilder { let else_pred = else_exit_block_opt.unwrap_or(else_block); // Emit Phi for the assigned variable and bind it let inputs = vec![(then_pred, then_value_for_var), (else_pred, else_value_for_var)]; - self.debug_verify_phi_inputs(&inputs); + if let (Some(func), Some(cur_bb)) = (&self.current_function, self.current_block) { + crate::mir::phi_core::common::debug_verify_phi_inputs(func, cur_bb, &inputs); + } self.emit_instruction(MirInstruction::Phi { dst: result_val, inputs })?; self.variable_map = pre_if_var_map.clone(); self.variable_map.insert(var_name, result_val); @@ -222,7 +114,9 @@ impl MirBuilder { let then_pred = then_exit_block_opt.unwrap_or(then_block); let else_pred = else_exit_block_opt.unwrap_or(else_block); let inputs = vec![(then_pred, then_value_raw), (else_pred, else_value_raw)]; - self.debug_verify_phi_inputs(&inputs); + if let (Some(func), Some(cur_bb)) = (&self.current_function, self.current_block) { + crate::mir::phi_core::common::debug_verify_phi_inputs(func, cur_bb, &inputs); + } self.emit_instruction(MirInstruction::Phi { dst: result_val, inputs })?; // Merge variable map conservatively to pre-if snapshot (no new bindings) self.variable_map = pre_if_var_map.clone(); diff --git a/src/mir/builder_modularized/control_flow.rs b/src/mir/builder_modularized/control_flow.rs index 7d0f4648..83ca491d 100644 --- a/src/mir/builder_modularized/control_flow.rs +++ b/src/mir/builder_modularized/control_flow.rs @@ -13,20 +13,7 @@ impl MirBuilder { self.lower_if_form(condition, then_branch, else_branch) } - /// Extract assigned variable name from an AST node if it represents an assignment to a variable. - /// Handles direct Assignment and Program with trailing single-statement Assignment. - fn extract_assigned_var(ast: &ASTNode) -> Option { - match ast { - ASTNode::Assignment { target, .. } => { - if let ASTNode::Variable { name, .. } = target.as_ref() { Some(name.clone()) } else { None } - } - ASTNode::Program { statements, .. } => { - // Inspect the last statement as the resulting value of the block - statements.last().and_then(|st| Self::extract_assigned_var(st)) - } - _ => None, - } - } + // Assigned variable extraction is centralized in phi_core::if_phi now. /// Build a loop statement: loop(condition) { body } /// diff --git a/src/mir/loop_builder.rs b/src/mir/loop_builder.rs index 3f34c9d2..8775ea2e 100644 --- a/src/mir/loop_builder.rs +++ b/src/mir/loop_builder.rs @@ -6,6 +6,7 @@ */ use super::{BasicBlockId, ConstValue, MirInstruction, ValueId}; +use crate::mir::phi_core::loop_phi::IncompletePhi; use crate::ast::ASTNode; use std::collections::HashMap; @@ -15,16 +16,7 @@ use super::utils::{ capture_actual_predecessor_and_jump, }; -/// 不完全なPhi nodeの情報 -#[derive(Debug, Clone)] -struct IncompletePhi { - /// Phi nodeの結果ValueId - phi_id: ValueId, - /// 変数名 - var_name: String, - /// 既知の入力値 (predecessor block id, value) - known_inputs: Vec<(BasicBlockId, ValueId)>, -} +// IncompletePhi has moved to phi_core::loop_phi /// ループビルダー - SSA形式でのループ構築を管理 pub struct LoopBuilder<'a> { @@ -52,6 +44,8 @@ pub struct LoopBuilder<'a> { impl<'a> LoopBuilder<'a> { + // Implement phi_core LoopPhiOps on LoopBuilder for in-place delegation + // ============================================================= // Control Helpers — break/continue/jumps/unreachable handling // ============================================================= @@ -125,33 +119,9 @@ impl<'a> LoopBuilder<'a> { body: Vec, ) -> Result { // Pre-scan body for simple carrier pattern (up to 2 assigned variables, no break/continue) - fn collect_assigns(n: &ASTNode, vars: &mut Vec, has_ctrl: &mut bool) { - match n { - ASTNode::Assignment { target, .. } => { - if let ASTNode::Variable { name, .. } = target.as_ref() { - if !vars.iter().any(|v| v == name) { - vars.push(name.clone()); - } - } - } - ASTNode::Break { .. } | ASTNode::Continue { .. } => { *has_ctrl = true; } - ASTNode::If { then_body, else_body, .. } => { - let tp = ASTNode::Program { statements: then_body.clone(), span: crate::ast::Span::unknown() }; - collect_assigns(&tp, vars, has_ctrl); - if let Some(eb) = else_body { - let ep = ASTNode::Program { statements: eb.clone(), span: crate::ast::Span::unknown() }; - collect_assigns(&ep, vars, has_ctrl); - } - } - ASTNode::Program { statements, .. } => { - for s in statements { collect_assigns(s, vars, has_ctrl); } - } - _ => {} - } - } let mut assigned_vars: Vec = Vec::new(); let mut has_ctrl = false; - for st in &body { collect_assigns(st, &mut assigned_vars, &mut has_ctrl); } + for st in &body { crate::mir::phi_core::loop_phi::collect_carrier_assigns(st, &mut assigned_vars, &mut has_ctrl); } if !has_ctrl && !assigned_vars.is_empty() && assigned_vars.len() <= 2 { // Emit a carrier hint (no-op sink by default; visible with NYASH_MIR_TRACE_HINTS=1) self.parent_builder.hint_loop_carrier(assigned_vars.clone()); @@ -214,7 +184,11 @@ impl<'a> LoopBuilder<'a> { let latch_snapshot = self.get_current_variable_map(); // 以前は body_id に保存していたが、複数ブロックのボディや continue 混在時に不正確になるため // 実際の latch_id に対してスナップショットを紐づける - self.block_var_maps.insert(latch_id, latch_snapshot); + crate::mir::phi_core::loop_phi::save_block_snapshot( + &mut self.block_var_maps, + latch_id, + &latch_snapshot, + ); // Only jump back to header if the latch block is not already terminated { let need_jump = { @@ -266,113 +240,49 @@ impl<'a> LoopBuilder<'a> { header_id: BasicBlockId, preheader_id: BasicBlockId, ) -> Result<(), String> { - // 現在の変数マップから、ループで使用される可能性のある変数を取得 let current_vars = self.get_current_variable_map(); - // preheader時点のスナップショット(後でphi入力の解析に使う) - self.block_var_maps - .insert(preheader_id, current_vars.clone()); - - // 各変数に対して不完全なPhi nodeを作成 - let mut incomplete_phis = Vec::new(); - for (var_name, &value_before) in ¤t_vars { - let phi_id = self.new_value(); - - // 不完全なPhi nodeを作成(preheaderからの値のみ設定) - let incomplete_phi = IncompletePhi { - phi_id, - var_name: var_name.clone(), - known_inputs: vec![(preheader_id, value_before)], - }; - - incomplete_phis.push(incomplete_phi); - - // フェーズM: no_phi_mode分岐削除(常にPHI使用) - - // 変数マップを更新(Phi nodeの結果を使用) - self.update_variable(var_name.clone(), phi_id); - } - - // 不完全なPhi nodeを記録 - self.incomplete_phis.insert(header_id, incomplete_phis); - + crate::mir::phi_core::loop_phi::save_block_snapshot( + &mut self.block_var_maps, + preheader_id, + ¤t_vars, + ); + let incs = crate::mir::phi_core::loop_phi::prepare_loop_variables_with( + self, + header_id, + preheader_id, + ¤t_vars, + )?; + self.incomplete_phis.insert(header_id, incs); Ok(()) } /// ブロックをシールし、不完全なPhi nodeを完成させる fn seal_block(&mut self, block_id: BasicBlockId, latch_id: BasicBlockId) -> Result<(), String> { - // 不完全なPhi nodeを取得 if let Some(incomplete_phis) = self.incomplete_phis.remove(&block_id) { - for mut phi in incomplete_phis { - for (cid, snapshot) in &self.continue_snapshots { - if let Some(v) = snapshot.get(&phi.var_name) { - phi.known_inputs.push((*cid, *v)); - } - } - - let value_after = self - .get_variable_at_block(&phi.var_name, latch_id) - .ok_or_else(|| format!("Variable {} not found at latch block", phi.var_name))?; - - phi.known_inputs.push((latch_id, value_after)); - - // フェーズM: 常にPHI命令を使用(no_phi_mode分岐削除) - self.emit_phi_at_block_start(block_id, phi.phi_id, phi.known_inputs)?; - self.update_variable(phi.var_name.clone(), phi.phi_id); - } + let cont_snaps = self.continue_snapshots.clone(); + crate::mir::phi_core::loop_phi::seal_incomplete_phis_with( + self, + block_id, + latch_id, + incomplete_phis, + &cont_snaps, + )?; } - - // ブロックをシール済みとしてマーク self.mark_block_sealed(block_id)?; - Ok(()) } /// Exitブロックで変数のPHIを生成(breakポイントでの値を統一) fn create_exit_phis(&mut self, header_id: BasicBlockId, exit_id: BasicBlockId) -> Result<(), String> { - // 全変数名を収集(exit_snapshots内のすべての変数) - let mut all_vars = std::collections::HashSet::new(); - - // Header直行ケース(0回実行)の変数を収集 let header_vars = self.get_current_variable_map(); - for var_name in header_vars.keys() { - all_vars.insert(var_name.clone()); - } - - // break時点の変数を収集 - for (_, snapshot) in &self.exit_snapshots { - for var_name in snapshot.keys() { - all_vars.insert(var_name.clone()); - } - } - - // 各変数に対してExit PHIを生成 - for var_name in all_vars { - let mut phi_inputs = Vec::new(); - - // Header直行ケース(0回実行)の入力 - if let Some(header_value) = header_vars.get(&var_name) { - phi_inputs.push((header_id, *header_value)); - } - - // 各breakポイントからの入力 - for (block_id, snapshot) in &self.exit_snapshots { - if let Some(value) = snapshot.get(&var_name) { - phi_inputs.push((*block_id, *value)); - } - } - - // PHI入力が2つ以上なら、PHIノードを生成 - if phi_inputs.len() > 1 { - let phi_dst = self.new_value(); - self.emit_phi_at_block_start(exit_id, phi_dst, phi_inputs)?; - self.update_variable(var_name, phi_dst); - } else if phi_inputs.len() == 1 { - // 単一入力なら直接使用(最適化) - self.update_variable(var_name, phi_inputs[0].1); - } - } - - Ok(()) + let exit_snaps = self.exit_snapshots.clone(); + crate::mir::phi_core::loop_phi::build_exit_phis_with( + self, + header_id, + exit_id, + &header_vars, + &exit_snaps, + ) } // --- ヘルパーメソッド(親ビルダーへの委譲) --- @@ -549,7 +459,7 @@ impl<'a> LoopBuilder<'a> { // Capture pre-if variable map (used for phi normalization) let pre_if_var_map = self.get_current_variable_map(); - let pre_then_var_value = pre_if_var_map.clone(); + // (legacy) kept for earlier merge style; now unified helpers compute deltas directly. // then branch self.set_current_block(then_bb)?; @@ -588,60 +498,74 @@ impl<'a> LoopBuilder<'a> { // Continue at merge self.set_current_block(merge_bb)?; - // collect assigned variables in both branches - fn collect_assigned_vars(ast: &ASTNode, out: &mut std::collections::HashSet) { - match ast { - ASTNode::Assignment { target, .. } => { - if let ASTNode::Variable { name, .. } = target.as_ref() { out.insert(name.clone()); } - } - ASTNode::Program { statements, .. } => { for s in statements { collect_assigned_vars(s, out); } } - ASTNode::If { then_body, else_body, .. } => { - let tp = ASTNode::Program { statements: then_body.clone(), span: crate::ast::Span::unknown() }; - collect_assigned_vars(&tp, out); - if let Some(eb) = else_body { - let ep = ASTNode::Program { statements: eb.clone(), span: crate::ast::Span::unknown() }; - collect_assigned_vars(&ep, out); - } - } - _ => {} - } - } let mut vars: std::collections::HashSet = std::collections::HashSet::new(); let then_prog = ASTNode::Program { statements: then_body.clone(), span: crate::ast::Span::unknown() }; - collect_assigned_vars(&then_prog, &mut vars); + crate::mir::phi_core::if_phi::collect_assigned_vars(&then_prog, &mut vars); if let Some(es) = &else_body { let else_prog = ASTNode::Program { statements: es.clone(), span: crate::ast::Span::unknown() }; - collect_assigned_vars(&else_prog, &mut vars); + crate::mir::phi_core::if_phi::collect_assigned_vars(&else_prog, &mut vars); } // Reset to pre-if map before rebinding to ensure a clean environment self.parent_builder.variable_map = pre_if_var_map.clone(); - for var_name in vars.into_iter() { - let then_val = then_var_map_end.get(&var_name).copied().or_else(|| pre_then_var_value.get(&var_name).copied()); - let else_val = else_var_map_end_opt.as_ref().and_then(|m| m.get(&var_name).copied()).or_else(|| pre_then_var_value.get(&var_name).copied()); - - if let (Some(tv), Some(ev)) = (then_val, else_val) { - let mut incomings: Vec<(BasicBlockId, ValueId)> = Vec::new(); - if let Some(pred) = then_pred_to_merge { incomings.push((pred, tv)); } - if let Some(pred) = else_pred_to_merge { incomings.push((pred, ev)); } - match incomings.len() { - 0 => {} - 1 => { - let (_pred, v) = incomings[0]; - self.parent_builder.variable_map.insert(var_name, v); - } - _ => { - let phi_id = self.new_value(); - // フェーズM: 常にPHI命令を使用(no_phi_mode分岐削除) - self.emit_phi_at_block_start(merge_bb, phi_id, incomings)?; - self.parent_builder.variable_map.insert(var_name, phi_id); - } + // Use shared helper to merge modified variables at merge block + struct Ops<'b, 'a>(&'b mut LoopBuilder<'a>); + impl<'b, 'a> crate::mir::phi_core::if_phi::PhiMergeOps for Ops<'b, 'a> { + fn new_value(&mut self) -> ValueId { self.0.new_value() } + fn emit_phi_at_block_start( + &mut self, + block: BasicBlockId, + dst: ValueId, + inputs: Vec<(BasicBlockId, ValueId)>, + ) -> Result<(), String> { self.0.emit_phi_at_block_start(block, dst, inputs) } + fn update_var(&mut self, name: String, value: ValueId) { self.0.parent_builder.variable_map.insert(name, value); } + fn debug_verify_phi_inputs(&mut self, merge_bb: BasicBlockId, inputs: &[(BasicBlockId, ValueId)]) { + if let Some(ref func) = self.0.parent_builder.current_function { + crate::mir::phi_core::common::debug_verify_phi_inputs(func, merge_bb, inputs); } } } + // Reset to pre-if snapshot, then delegate to shared helper + self.parent_builder.variable_map = pre_if_var_map.clone(); + let mut ops = Ops(self); + crate::mir::phi_core::if_phi::merge_modified_at_merge_with( + &mut ops, + merge_bb, + then_bb, + else_bb, + then_pred_to_merge, + else_pred_to_merge, + &pre_if_var_map, + &then_var_map_end, + &else_var_map_end_opt, + None, + )?; let void_id = self.new_value(); self.emit_const(void_id, ConstValue::Void)?; Ok(void_id) } } + +// Implement phi_core LoopPhiOps on LoopBuilder for in-place delegation +impl crate::mir::phi_core::loop_phi::LoopPhiOps for LoopBuilder<'_> { + fn new_value(&mut self) -> ValueId { self.new_value() } + fn emit_phi_at_block_start( + &mut self, + block: BasicBlockId, + dst: ValueId, + inputs: Vec<(BasicBlockId, ValueId)>, + ) -> Result<(), String> { + self.emit_phi_at_block_start(block, dst, inputs) + } + fn update_var(&mut self, name: String, value: ValueId) { self.update_variable(name, value) } + fn get_variable_at_block(&mut self, name: &str, block: BasicBlockId) -> Option { + // Call the inherent method (immutable borrow) to avoid recursion + LoopBuilder::get_variable_at_block(self, name, block) + } + fn debug_verify_phi_inputs(&mut self, merge_bb: BasicBlockId, inputs: &[(BasicBlockId, ValueId)]) { + if let Some(ref func) = self.parent_builder.current_function { + crate::mir::phi_core::common::debug_verify_phi_inputs(func, merge_bb, inputs); + } + } +} diff --git a/src/mir/mod.rs b/src/mir/mod.rs index 8150b9d0..67d3eba8 100644 --- a/src/mir/mod.rs +++ b/src/mir/mod.rs @@ -20,6 +20,7 @@ pub mod loop_api; // Minimal LoopBuilder facade (adapter-ready) pub mod loop_builder; // SSA loop construction with phi nodes pub mod optimizer; pub mod utils; // Phase 15 control flow utilities for root treatment +pub mod phi_core; // Phase 1 scaffold: unified PHI entry (re-exports only) pub mod optimizer_passes; // optimizer passes (normalize/diagnostics) pub mod optimizer_stats; // extracted stats struct pub mod passes; diff --git a/src/mir/phi_core/common.rs b/src/mir/phi_core/common.rs new file mode 100644 index 00000000..13f391f5 --- /dev/null +++ b/src/mir/phi_core/common.rs @@ -0,0 +1,49 @@ +/*! + * phi_core::common – shared types and invariants (scaffold) + * + * Phase 1 keeps this minimal; future phases may move debug asserts and + * predicate set checks here for both if/loop PHI normalization. + */ + +/// Placeholder for future shared PHI input type alias. +/// Using the same tuple form as MIR Phi instruction inputs. +pub type PhiInput = (crate::mir::BasicBlockId, crate::mir::ValueId); + +#[cfg(debug_assertions)] +pub fn debug_verify_phi_inputs( + function: &crate::mir::MirFunction, + merge_bb: crate::mir::BasicBlockId, + inputs: &[(crate::mir::BasicBlockId, crate::mir::ValueId)], +) { + use std::collections::HashSet; + let mut seen = HashSet::new(); + for (pred, _v) in inputs.iter() { + debug_assert_ne!( + *pred, merge_bb, + "PHI incoming predecessor must not be the merge block itself" + ); + debug_assert!( + seen.insert(*pred), + "Duplicate PHI incoming predecessor detected: {:?}", + pred + ); + } + if let Some(block) = function.blocks.get(&merge_bb) { + for (pred, _v) in inputs.iter() { + debug_assert!( + block.predecessors.contains(pred), + "PHI incoming pred {:?} is not a predecessor of merge bb {:?}", + pred, + merge_bb + ); + } + } +} + +#[cfg(not(debug_assertions))] +pub fn debug_verify_phi_inputs( + _function: &crate::mir::MirFunction, + _merge_bb: crate::mir::BasicBlockId, + _inputs: &[(crate::mir::BasicBlockId, crate::mir::ValueId)], +) { +} diff --git a/src/mir/phi_core/if_phi.rs b/src/mir/phi_core/if_phi.rs new file mode 100644 index 00000000..3be83540 --- /dev/null +++ b/src/mir/phi_core/if_phi.rs @@ -0,0 +1,222 @@ +/*! + * phi_core::if_phi – if/else PHI helpers (Phase 2) + * + * Public thin wrappers that mirror the semantics of existing builder::phi + * helpers. Implemented locally to avoid depending on private submodules. + * Behavior is identical to the current in-tree logic. + */ + +use crate::ast::ASTNode; +use crate::mir::{MirFunction, MirInstruction, MirType, ValueId}; +use std::collections::HashMap; + +/// Infer return type by scanning for a Phi that defines `ret_val` and +/// verifying that all incoming values have the same type in `types`. +pub fn infer_type_from_phi( + function: &MirFunction, + ret_val: ValueId, + types: &HashMap, +) -> Option { + for (_bid, bb) in function.blocks.iter() { + for inst in bb.instructions.iter() { + if let MirInstruction::Phi { dst, inputs } = inst { + if *dst == ret_val { + let mut it = inputs.iter().filter_map(|(_, v)| types.get(v)); + if let Some(first) = it.next() { + if it.all(|mt| mt == first) { + return Some(first.clone()); + } + } + } + } + } + } + None +} + +/// Extract the assigned variable name from an AST fragment commonly used in +/// if/else analysis. Same logic as builder::phi::extract_assigned_var. +pub fn extract_assigned_var(ast: &ASTNode) -> Option { + match ast { + ASTNode::Assignment { target, .. } => { + if let ASTNode::Variable { name, .. } = target.as_ref() { + Some(name.clone()) + } else { + None + } + } + ASTNode::Program { statements, .. } => { + statements.last().and_then(|st| extract_assigned_var(st)) + } + ASTNode::If { then_body, else_body, .. } => { + let then_prog = ASTNode::Program { + statements: then_body.clone(), + span: crate::ast::Span::unknown(), + }; + let tvar = extract_assigned_var(&then_prog); + let evar = else_body.as_ref().and_then(|eb| { + let ep = ASTNode::Program { + statements: eb.clone(), + span: crate::ast::Span::unknown(), + }; + extract_assigned_var(&ep) + }); + match (tvar, evar) { + (Some(tv), Some(ev)) if tv == ev => Some(tv), + _ => None, + } + } + _ => None, + } +} + +/// Collect all variable names that are assigned within the given AST subtree. +/// Useful for computing PHI merge candidates across branches/blocks. +pub fn collect_assigned_vars(ast: &ASTNode, out: &mut std::collections::HashSet) { + match ast { + ASTNode::Assignment { target, .. } => { + if let ASTNode::Variable { name, .. } = target.as_ref() { + out.insert(name.clone()); + } + } + ASTNode::Program { statements, .. } => { + for s in statements { collect_assigned_vars(s, out); } + } + ASTNode::If { then_body, else_body, .. } => { + let tp = ASTNode::Program { statements: then_body.clone(), span: crate::ast::Span::unknown() }; + collect_assigned_vars(&tp, out); + if let Some(eb) = else_body { + let ep = ASTNode::Program { statements: eb.clone(), span: crate::ast::Span::unknown() }; + collect_assigned_vars(&ep, out); + } + } + _ => {} + } +} + +/// Compute the set of variable names whose values changed in either branch +/// relative to the pre-if snapshot. +pub fn compute_modified_names( + pre_if_snapshot: &HashMap, + then_map_end: &HashMap, + else_map_end_opt: &Option>, +) -> Vec { + use std::collections::HashSet; + let mut names: HashSet<&str> = HashSet::new(); + for k in then_map_end.keys() { names.insert(k.as_str()); } + if let Some(emap) = else_map_end_opt.as_ref() { + for k in emap.keys() { names.insert(k.as_str()); } + } + let mut changed: Vec = Vec::new(); + for &name in &names { + let pre = pre_if_snapshot.get(name); + let t = then_map_end.get(name); + let e = else_map_end_opt.as_ref().and_then(|m| m.get(name)); + if (t.is_some() && Some(*t.unwrap()) != pre.copied()) + || (e.is_some() && Some(*e.unwrap()) != pre.copied()) + { + changed.push(name.to_string()); + } + } + changed +} + +/// Operations required for emitting a PHI or direct binding at a merge point. +pub trait PhiMergeOps { + fn new_value(&mut self) -> ValueId; + fn emit_phi_at_block_start( + &mut self, + block: crate::mir::BasicBlockId, + dst: ValueId, + inputs: Vec<(crate::mir::BasicBlockId, ValueId)>, + ) -> Result<(), String>; + fn update_var(&mut self, name: String, value: ValueId); + fn debug_verify_phi_inputs( + &mut self, + _merge_bb: crate::mir::BasicBlockId, + _inputs: &[(crate::mir::BasicBlockId, ValueId)], + ) { + } +} + +/// Merge variables modified in branches at the merge block using provided ops. +/// Handles both two-pred and single-pred (reachable) cases gracefully. +pub fn merge_modified_at_merge_with( + ops: &mut O, + merge_bb: crate::mir::BasicBlockId, + _then_block: crate::mir::BasicBlockId, + else_block: crate::mir::BasicBlockId, + then_pred_opt: Option, + else_pred_opt: Option, + pre_if_snapshot: &HashMap, + then_map_end: &HashMap, + else_map_end_opt: &Option>, + skip_var: Option<&str>, +) -> Result<(), String> { + let changed = compute_modified_names(pre_if_snapshot, then_map_end, else_map_end_opt); + for name in changed { + if skip_var.map(|s| s == name).unwrap_or(false) { + continue; + } + let pre = match pre_if_snapshot.get(name.as_str()) { + Some(v) => *v, + None => continue, + }; + let then_v = then_map_end.get(name.as_str()).copied().unwrap_or(pre); + let else_v = else_map_end_opt + .as_ref() + .and_then(|m| m.get(name.as_str()).copied()) + .unwrap_or(pre); + + // Build incoming pairs from reachable predecessors only + let mut inputs: Vec<(crate::mir::BasicBlockId, ValueId)> = Vec::new(); + if let Some(tp) = then_pred_opt { inputs.push((tp, then_v)); } + if let Some(ep) = else_pred_opt.or(Some(else_block)) { inputs.push((ep, else_v)); } + + match inputs.len() { + 0 => {} + 1 => { + let (_pred, v) = inputs[0]; + ops.update_var(name, v); + } + _ => { + ops.debug_verify_phi_inputs(merge_bb, &inputs); + let dst = ops.new_value(); + ops.emit_phi_at_block_start(merge_bb, dst, inputs)?; + ops.update_var(name, dst); + } + } + } + Ok(()) +} + +/// Convenience wrapper: reset variable map (via a caller-provided closure) +/// then perform merge at the merge block. Keeps caller simple while +/// avoiding tying phi_core to concrete builder internals. +pub fn merge_with_reset_at_merge_with( + ops: &mut O, + merge_bb: crate::mir::BasicBlockId, + then_block: crate::mir::BasicBlockId, + else_block: crate::mir::BasicBlockId, + then_pred_opt: Option, + else_pred_opt: Option, + pre_if_snapshot: &HashMap, + then_map_end: &HashMap, + else_map_end_opt: &Option>, + reset_vars: impl FnOnce(), + skip_var: Option<&str>, +) -> Result<(), String> { + reset_vars(); + merge_modified_at_merge_with( + ops, + merge_bb, + then_block, + else_block, + then_pred_opt, + else_pred_opt, + pre_if_snapshot, + then_map_end, + else_map_end_opt, + skip_var, + ) +} diff --git a/src/mir/phi_core/loop_phi.rs b/src/mir/phi_core/loop_phi.rs new file mode 100644 index 00000000..dbc5f6e0 --- /dev/null +++ b/src/mir/phi_core/loop_phi.rs @@ -0,0 +1,181 @@ +/*! + * phi_core::loop_phi – loop-specific PHI management (scaffold) + * + * Phase 1 defines minimal types only. The concrete logic remains in + * `mir::loop_builder` and will be delegated/moved here in later phases. + */ + +use crate::mir::{BasicBlockId, ValueId}; +use crate::ast::ASTNode; + +/// Loop-local placeholder of an incomplete PHI (header-time declaration). +/// Moved from loop_builder to centralize PHI-related types. +#[derive(Debug, Clone)] +pub struct IncompletePhi { + pub phi_id: ValueId, + pub var_name: String, + pub known_inputs: Vec<(BasicBlockId, ValueId)>, +} + +/// Common snapshot type used for continue/exit points +pub type VarSnapshot = std::collections::HashMap; +pub type SnapshotAt = (BasicBlockId, VarSnapshot); + +#[derive(Default)] +pub struct LoopPhiManager; + +impl LoopPhiManager { + pub fn new() -> Self { Self::default() } +} + +/// Operations required from a loop builder to finalize PHIs. +pub trait LoopPhiOps { + fn new_value(&mut self) -> ValueId; + fn emit_phi_at_block_start( + &mut self, + block: BasicBlockId, + dst: ValueId, + inputs: Vec<(BasicBlockId, ValueId)>, + ) -> Result<(), String>; + fn update_var(&mut self, name: String, value: ValueId); + fn get_variable_at_block(&mut self, name: &str, block: BasicBlockId) -> Option; + fn debug_verify_phi_inputs(&mut self, _merge_bb: BasicBlockId, _inputs: &[(BasicBlockId, ValueId)]) {} +} + +/// Finalize PHIs at loop exit (merge of break points and header fall-through). +/// Behavior mirrors loop_builder's create_exit_phis using the provided ops. +pub fn build_exit_phis_with( + ops: &mut O, + header_id: BasicBlockId, + exit_id: BasicBlockId, + header_vars: &std::collections::HashMap, + exit_snapshots: &[(BasicBlockId, VarSnapshot)], +) -> Result<(), String> { + // 1) Collect all variable names possibly participating in exit PHIs + let mut all_vars = std::collections::HashSet::new(); + for var_name in header_vars.keys() { + all_vars.insert(var_name.clone()); + } + for (_bid, snapshot) in exit_snapshots.iter() { + for var_name in snapshot.keys() { + all_vars.insert(var_name.clone()); + } + } + + // 2) For each variable, gather incoming values + for var_name in all_vars { + let mut phi_inputs: Vec<(BasicBlockId, ValueId)> = Vec::new(); + + if let Some(&hv) = header_vars.get(&var_name) { + phi_inputs.push((header_id, hv)); + } + for (block_id, snapshot) in exit_snapshots.iter() { + if let Some(&v) = snapshot.get(&var_name) { + phi_inputs.push((*block_id, v)); + } + } + + match phi_inputs.len() { + 0 => {} // nothing to do + 1 => { + // single predecessor: direct binding + ops.update_var(var_name, phi_inputs[0].1); + } + _ => { + let dst = ops.new_value(); + ops.debug_verify_phi_inputs(exit_id, &phi_inputs); + ops.emit_phi_at_block_start(exit_id, dst, phi_inputs)?; + ops.update_var(var_name, dst); + } + } + } + Ok(()) +} + +/// Seal a header block by completing its incomplete PHIs with values from +/// continue snapshots and the latch block. +pub fn seal_incomplete_phis_with( + ops: &mut O, + block_id: BasicBlockId, + latch_id: BasicBlockId, + mut incomplete_phis: Vec, + continue_snapshots: &[(BasicBlockId, VarSnapshot)], +) -> Result<(), String> { + for mut phi in incomplete_phis.drain(..) { + // from continue points + for (cid, snapshot) in continue_snapshots.iter() { + if let Some(&v) = snapshot.get(&phi.var_name) { + phi.known_inputs.push((*cid, v)); + } + } + // from latch + let value_after = ops + .get_variable_at_block(&phi.var_name, latch_id) + .ok_or_else(|| format!("Variable {} not found at latch block", phi.var_name))?; + phi.known_inputs.push((latch_id, value_after)); + + ops.debug_verify_phi_inputs(block_id, &phi.known_inputs); + ops.emit_phi_at_block_start(block_id, phi.phi_id, phi.known_inputs)?; + ops.update_var(phi.var_name.clone(), phi.phi_id); + } + Ok(()) +} + +/// Prepare loop header PHIs by declaring one IncompletePhi per variable found +/// in `current_vars` (preheader snapshot), seeding each with (preheader_id, val) +/// and rebinding the variable to the newly allocated Phi result in the builder. +pub fn prepare_loop_variables_with( + ops: &mut O, + _header_id: BasicBlockId, + preheader_id: BasicBlockId, + current_vars: &std::collections::HashMap, +) -> Result, String> { + let mut incomplete_phis: Vec = Vec::new(); + for (var_name, &value_before) in current_vars.iter() { + let phi_id = ops.new_value(); + let inc = IncompletePhi { + phi_id, + var_name: var_name.clone(), + known_inputs: vec![(preheader_id, value_before)], + }; + incomplete_phis.push(inc); + ops.update_var(var_name.clone(), phi_id); + } + Ok(incomplete_phis) +} + +/// Collect variables assigned within a loop body and detect control-flow +/// statements (break/continue). Used for lightweight carrier hinting. +pub fn collect_carrier_assigns(node: &ASTNode, vars: &mut Vec, has_ctrl: &mut bool) { + match node { + ASTNode::Assignment { target, .. } => { + if let ASTNode::Variable { name, .. } = target.as_ref() { + if !vars.iter().any(|v| v == name) { + vars.push(name.clone()); + } + } + } + ASTNode::Break { .. } | ASTNode::Continue { .. } => { *has_ctrl = true; } + ASTNode::If { then_body, else_body, .. } => { + let tp = ASTNode::Program { statements: then_body.clone(), span: crate::ast::Span::unknown() }; + collect_carrier_assigns(&tp, vars, has_ctrl); + if let Some(eb) = else_body { + let ep = ASTNode::Program { statements: eb.clone(), span: crate::ast::Span::unknown() }; + collect_carrier_assigns(&ep, vars, has_ctrl); + } + } + ASTNode::Program { statements, .. } => { + for s in statements { collect_carrier_assigns(s, vars, has_ctrl); } + } + _ => {} + } +} + +/// Save a block-local variable snapshot into the provided store. +pub fn save_block_snapshot( + store: &mut std::collections::HashMap, + block: BasicBlockId, + snapshot: &VarSnapshot, +) { + store.insert(block, snapshot.clone()); +} diff --git a/src/mir/phi_core/mod.rs b/src/mir/phi_core/mod.rs new file mode 100644 index 00000000..7c7ebab1 --- /dev/null +++ b/src/mir/phi_core/mod.rs @@ -0,0 +1,17 @@ +/*! + * phi_core – Unified PHI management scaffold (Phase 1) + * + * Purpose: + * - Provide a single, stable entry point for PHI-related helpers. + * - Start with re-exports of existing, verified logic (zero behavior change). + * - Prepare ground for gradual consolidation of loop PHI handling. + */ + +pub mod common; +pub mod if_phi; +pub mod loop_phi; + +// Public surface for callers that want a stable path: +// Phase 1: No re-exports to avoid touching private builder internals. +// Callers should continue using existing paths. Future phases may expose +// stable wrappers here once migrated. diff --git a/src/parser/statements_backup.rs b/src/parser/statements_backup.rs new file mode 100644 index 00000000..7d205494 --- /dev/null +++ b/src/parser/statements_backup.rs @@ -0,0 +1,723 @@ +/*! + * Nyash Parser - Statement Parsing Module + * + * 文(Statement)の解析を担当するモジュール + * if, loop, break, return, print等の制御構文を処理 + */ + +use super::common::ParserUtils; +use super::cursor::TokenCursor; +use super::{NyashParser, ParseError}; +use crate::ast::{ASTNode, CatchClause, Span}; +use crate::tokenizer::TokenType; + +impl NyashParser { + #[inline] + fn cursor_enabled() -> bool { + std::env::var("NYASH_PARSER_TOKEN_CURSOR").ok().as_deref() == Some("1") + } + + /// Thin adapter: when Cursor route is enabled, align statement start position + /// by letting TokenCursor apply its statement-mode newline policy, then + /// continue with the legacy statement parser. No behavior change otherwise. + #[inline] + fn with_stmt_cursor(&mut self, f: F) -> Result + where + F: FnOnce(&mut Self) -> Result, + { + if Self::cursor_enabled() { + let mut cursor = TokenCursor::new(&self.tokens); + cursor.set_position(self.current); + cursor.with_stmt_mode(|c| { + // Allow cursor to collapse any leading NEWLINEs in stmt mode + c.skip_newlines(); + }); + self.current = cursor.position(); + } + f(self) + } + /// Map a starting token into a grammar keyword string used by GRAMMAR_DIFF tracing. + #[inline] + fn grammar_keyword_for(start: &TokenType) -> Option<&'static str> { + match start { + TokenType::BOX => Some("box"), + TokenType::GLOBAL => Some("global"), + TokenType::FUNCTION => Some("function"), + TokenType::STATIC => Some("static"), + TokenType::IF => Some("if"), + TokenType::LOOP => Some("loop"), + TokenType::BREAK => Some("break"), + TokenType::RETURN => Some("return"), + TokenType::PRINT => Some("print"), + TokenType::NOWAIT => Some("nowait"), + TokenType::LOCAL => Some("local"), + TokenType::OUTBOX => Some("outbox"), + TokenType::TRY => Some("try"), + TokenType::THROW => Some("throw"), + TokenType::USING => Some("using"), + TokenType::FROM => Some("from"), + _ => None, + } + } + /// Small helper: build UnexpectedToken with current token and line. + #[inline] + fn err_unexpected>(&self, expected: S) -> ParseError { + ParseError::UnexpectedToken { + found: self.current_token().token_type.clone(), + expected: expected.into(), + line: self.current_token().line, + } + } + + /// Expect an identifier and advance. Returns its string or an UnexpectedToken error. + #[inline] + fn expect_identifier(&mut self, what: &str) -> Result { + if let TokenType::IDENTIFIER(name) = &self.current_token().token_type { + let out = name.clone(); + self.advance(); + Ok(out) + } else { + Err(self.err_unexpected(what)) + } + } + + /// Parse a standalone block `{ ... }` and optional postfix `catch/cleanup` sequence. + /// Returns Program(body) when no postfix keywords follow. + fn parse_standalone_block_statement(&mut self) -> Result { + // Parse the block body first + let try_body = self.parse_block_statements()?; + + if crate::config::env::block_postfix_catch() + && (self.match_token(&TokenType::CATCH) || self.match_token(&TokenType::CLEANUP)) + { + // Parse at most one catch, then optional cleanup + let mut catch_clauses: Vec = Vec::new(); + if self.match_token(&TokenType::CATCH) { + self.advance(); // consume 'catch' + self.consume(TokenType::LPAREN)?; + let (exception_type, exception_var) = self.parse_catch_param()?; + self.consume(TokenType::RPAREN)?; + let catch_body = self.parse_block_statements()?; + catch_clauses.push(CatchClause { + exception_type, + variable_name: exception_var, + body: catch_body, + span: Span::unknown(), + }); + + // Single‑catch policy (MVP): disallow multiple catch in postfix form + if self.match_token(&TokenType::CATCH) { + let line = self.current_token().line; + return Err(ParseError::UnexpectedToken { + found: self.current_token().token_type.clone(), + expected: "single catch only after standalone block".to_string(), + line, + }); + } + } + + // Optional cleanup + let finally_body = if self.match_token(&TokenType::CLEANUP) { + self.advance(); // consume 'cleanup' + Some(self.parse_block_statements()?) + } else { + None + }; + + Ok(ASTNode::TryCatch { + try_body, + catch_clauses, + finally_body, + span: Span::unknown(), + }) + } else { + // No postfix keywords. If gate is on, enforce MVP static check: + // direct top-level `throw` inside the standalone block must be followed by catch + if crate::config::env::block_postfix_catch() + && try_body.iter().any(|n| matches!(n, ASTNode::Throw { .. })) + { + let line = self.current_token().line; + return Err(ParseError::UnexpectedToken { + found: self.current_token().token_type.clone(), + expected: "block with direct 'throw' must be followed by 'catch'".to_string(), + line, + }); + } + Ok(ASTNode::Program { + statements: try_body, + span: Span::unknown(), + }) + } + } + /// Helper: parse a block `{ stmt* }` and return its statements + pub(super) fn parse_block_statements(&mut self) -> Result, ParseError> { + self.consume(TokenType::LBRACE)?; + let mut body = Vec::new(); + while !self.match_token(&TokenType::RBRACE) && !self.is_at_end() { + if !self.match_token(&TokenType::RBRACE) { + body.push(self.parse_statement()?); + } + } + self.consume(TokenType::RBRACE)?; + Ok(body) + } + + /// Grouped: declarations (box/interface/global/function/static/import) + fn parse_declaration_statement(&mut self) -> Result { + match &self.current_token().token_type { + TokenType::BOX => self.parse_box_declaration(), + TokenType::IMPORT => self.parse_import(), + TokenType::INTERFACE => self.parse_interface_box_declaration(), + TokenType::GLOBAL => self.parse_global_var(), + TokenType::FUNCTION => self.parse_function_declaration(), + TokenType::STATIC => self.parse_static_declaration(), + _ => Err(self.err_unexpected("declaration statement")), + } + } + + /// Grouped: control flow (if/loop/break/continue/return) + fn parse_control_flow_statement(&mut self) -> Result { + match &self.current_token().token_type { + TokenType::IF => self.parse_if(), + TokenType::LOOP => self.parse_loop(), + TokenType::BREAK => self.parse_break(), + TokenType::CONTINUE => self.parse_continue(), + TokenType::RETURN => self.parse_return(), + _ => Err(self.err_unexpected("control-flow statement")), + } + } + + /// Grouped: IO/module-ish (print/nowait) + fn parse_io_module_statement(&mut self) -> Result { + match &self.current_token().token_type { + TokenType::PRINT => self.parse_print(), + TokenType::NOWAIT => self.parse_nowait(), + _ => Err(self.err_unexpected("io/module statement")), + } + } + + /// Grouped: variable-related (local/outbox) + fn parse_variable_declaration_statement(&mut self) -> Result { + match &self.current_token().token_type { + TokenType::LOCAL => self.parse_local(), + TokenType::OUTBOX => self.parse_outbox(), + _ => Err(self.err_unexpected("variable declaration")), + } + } + + /// Grouped: exception (try/throw) with gate checks preserved + fn parse_exception_statement(&mut self) -> Result { + match &self.current_token().token_type { + TokenType::TRY => { + if crate::config::env::parser_stage3() { + self.parse_try_catch() + } else { + Err(ParseError::UnexpectedToken { + found: self.current_token().token_type.clone(), + expected: "enable NYASH_PARSER_STAGE3=1 to use 'try'".to_string(), + line: self.current_token().line, + }) + } + } + TokenType::THROW => { + if crate::config::env::parser_stage3() { + self.parse_throw() + } else { + Err(ParseError::UnexpectedToken { + found: self.current_token().token_type.clone(), + expected: "enable NYASH_PARSER_STAGE3=1 to use 'throw'".to_string(), + line: self.current_token().line, + }) + } + } + _ => Err(self.err_unexpected("try/throw")), + } + } + + /// Error helpers for standalone postfix keywords (catch/cleanup) + fn parse_postfix_catch_cleanup_error(&mut self) -> Result { + match &self.current_token().token_type { + TokenType::CATCH => { + if crate::config::env::block_postfix_catch() { + Err(ParseError::UnexpectedToken { + found: self.current_token().token_type.clone(), + expected: "postfix 'catch' is only allowed immediately after a standalone block: { ... } catch (...) { ... } (wrap if/else/loop in a standalone block)".to_string(), + line: self.current_token().line, + }) + } else { + Err(ParseError::UnexpectedToken { + found: self.current_token().token_type.clone(), + expected: "enable NYASH_BLOCK_CATCH=1 (or NYASH_PARSER_STAGE3=1) to use postfix 'catch' after a standalone block".to_string(), + line: self.current_token().line, + }) + } + } + TokenType::CLEANUP => { + if crate::config::env::block_postfix_catch() { + Err(ParseError::UnexpectedToken { + found: self.current_token().token_type.clone(), + expected: "postfix 'cleanup' is only allowed immediately after a standalone block: { ... } cleanup { ... }".to_string(), + line: self.current_token().line, + }) + } else { + Err(ParseError::UnexpectedToken { + found: self.current_token().token_type.clone(), + expected: "enable NYASH_BLOCK_CATCH=1 (or NYASH_PARSER_STAGE3=1) to use postfix 'cleanup' after a standalone block".to_string(), + line: self.current_token().line, + }) + } + } + _ => unreachable!(), + } + } + + /// Helper: parse catch parameter inside parentheses (after '(' consumed) + /// Forms: (Type ident) | (ident) | () + pub(super) fn parse_catch_param(&mut self) -> Result<(Option, Option), ParseError> { + if self.match_token(&TokenType::RPAREN) { + return Ok((None, None)); + } + match &self.current_token().token_type { + TokenType::IDENTIFIER(first) => { + let first_str = first.clone(); + let two_idents = matches!(self.peek_token(), TokenType::IDENTIFIER(_)); + if two_idents { + self.advance(); // consume type ident + if let TokenType::IDENTIFIER(var_name) = &self.current_token().token_type { + let var = var_name.clone(); + self.advance(); + Ok((Some(first_str), Some(var))) + } else { + Err(self.err_unexpected("exception variable name")) + } + } else { + self.advance(); + Ok((None, Some(first_str))) + } + } + _ => { + if self.match_token(&TokenType::RPAREN) { + Ok((None, None)) + } else { + Err(self.err_unexpected(") or identifier")) + } + } + } + } + + /// 文をパース + pub(super) fn parse_statement(&mut self) -> Result { + // For grammar diff: capture starting token to classify statement keyword + let start_tok = self.current_token().token_type.clone(); + let result = match &start_tok { + TokenType::LBRACE => self.parse_standalone_block_statement(), + TokenType::BOX + | TokenType::IMPORT + | TokenType::INTERFACE + | TokenType::GLOBAL + | TokenType::FUNCTION + | TokenType::STATIC => self.parse_declaration_statement(), + TokenType::IF + | TokenType::LOOP + | TokenType::BREAK + | TokenType::CONTINUE + | TokenType::RETURN => self.parse_control_flow_statement(), + TokenType::PRINT | TokenType::NOWAIT => self.parse_io_module_statement(), + TokenType::LOCAL | TokenType::OUTBOX => self.parse_variable_declaration_statement(), + TokenType::TRY | TokenType::THROW => self.parse_exception_statement(), + TokenType::CATCH | TokenType::CLEANUP => self.parse_postfix_catch_cleanup_error(), + TokenType::USING => self.parse_using(), + TokenType::FROM => { + // 🔥 from構文: from Parent.method(args) または from Parent.constructor(args) + self.parse_from_call_statement() + } + TokenType::IDENTIFIER(_name) => { + // function宣言 または 代入文 または 関数呼び出し + self.parse_assignment_or_function_call() + } + TokenType::THIS | TokenType::ME => { + // this/me で始まる文も通常の代入文または関数呼び出しとして処理 + self.parse_assignment_or_function_call() + } + _ => { + // Fallback: treat as expression statement + // Allows forms like: print("x") or a bare literal as the last value in a block + // Thin-adapt with Cursor in stmt mode (env on) to normalize leading newlines. + self.with_stmt_cursor(|p| Ok(p.parse_expression()?)) + } + }; + + // Non-invasive syntax rule check + if std::env::var("NYASH_GRAMMAR_DIFF").ok().as_deref() == Some("1") { + if let Some(k) = Self::grammar_keyword_for(&start_tok) { + let ok = crate::grammar::engine::get().syntax_is_allowed_statement(k); + if !ok { + eprintln!( + "[GRAMMAR-DIFF][Parser] statement '{}' not allowed by syntax rules", + k + ); + } + } + } + result + } + + /// import文をパース: import "path" (as Alias)? + pub(super) fn parse_import(&mut self) -> Result { + self.advance(); // consume 'import' + let path = if let TokenType::STRING(s) = &self.current_token().token_type { + let v = s.clone(); + self.advance(); + v + } else { + return Err(self.err_unexpected("string literal")); + }; + // Optional: 'as' Alias (treat 'as' as identifier literal) + let mut alias: Option = None; + if let TokenType::IDENTIFIER(w) = &self.current_token().token_type { + if w == "as" { + self.advance(); + if let TokenType::IDENTIFIER(name) = &self.current_token().token_type { + alias = Some(name.clone()); + self.advance(); + } else { + return Err(self.err_unexpected("alias name")); + } + } + } + Ok(ASTNode::ImportStatement { + path, + alias, + span: Span::unknown(), + }) + } + + /// if文をパース: if (condition) { body } else if ... else { body } + pub(super) fn parse_if(&mut self) -> Result { + // Thin-adapt statement start when Cursor route is enabled + if Self::cursor_enabled() { + let mut cursor = TokenCursor::new(&self.tokens); + cursor.set_position(self.current); + cursor.with_stmt_mode(|c| c.skip_newlines()); + self.current = cursor.position(); + } + self.advance(); // consume 'if' + + // 条件部分を取得 + let condition = Box::new(self.parse_expression()?); + + // then部分を取得(共通ブロックヘルパー) + let then_body = self.parse_block_statements()?; + + // else if/else部分を処理 + let else_body = if self.match_token(&TokenType::ELSE) { + self.advance(); // consume 'else' + + if self.match_token(&TokenType::IF) { + // else if を ネストしたifとして処理 + let nested_if = self.parse_if()?; + Some(vec![nested_if]) + } else { + // plain else(共通ブロックヘルパー) + Some(self.parse_block_statements()?) + } + } else { + None + }; + + Ok(ASTNode::If { + condition, + then_body, + else_body, + span: Span::unknown(), + }) + } + + /// loop文をパース + pub(super) fn parse_loop(&mut self) -> Result { + if Self::cursor_enabled() { + let mut cursor = TokenCursor::new(&self.tokens); + cursor.set_position(self.current); + cursor.with_stmt_mode(|c| c.skip_newlines()); + self.current = cursor.position(); + } + self.advance(); // consume 'loop' + + // 条件部分を取得(省略可: `loop { ... }` は無条件ループとして扱う) + let condition = if self.match_token(&TokenType::LPAREN) { + self.advance(); // consume '(' + let cond = Box::new(self.parse_expression()?); + self.consume(TokenType::RPAREN)?; + cond + } else { + // default: true + Box::new(ASTNode::Literal { + value: crate::ast::LiteralValue::Bool(true), + span: Span::unknown(), + }) + }; + + // body部分を取得(共通ブロックヘルパー) + let body = self.parse_block_statements()?; + + Ok(ASTNode::Loop { + condition, + body, + span: Span::unknown(), + }) + } + + /// break文をパース + pub(super) fn parse_break(&mut self) -> Result { + self.advance(); // consume 'break' + Ok(ASTNode::Break { + span: Span::unknown(), + }) + } + + /// continue文をパース + pub(super) fn parse_continue(&mut self) -> Result { + self.advance(); // consume 'continue' + Ok(ASTNode::Continue { + span: Span::unknown(), + }) + } + + /// return文をパース + pub(super) fn parse_return(&mut self) -> Result { + if Self::cursor_enabled() { + let mut cursor = TokenCursor::new(&self.tokens); + cursor.set_position(self.current); + cursor.with_stmt_mode(|c| c.skip_newlines()); + self.current = cursor.position(); + } + self.advance(); // consume 'return' + // returnの後に式があるかチェック(RBRACE/EOFなら値なし) + let value = if self.is_at_end() || self.match_token(&TokenType::RBRACE) { + None + } else { + Some(Box::new(self.parse_expression()?)) + }; + + Ok(ASTNode::Return { + value, + span: Span::unknown(), + }) + } + + /// print文をパース + pub(super) fn parse_print(&mut self) -> Result { + if Self::cursor_enabled() { + let mut cursor = TokenCursor::new(&self.tokens); + cursor.set_position(self.current); + cursor.with_stmt_mode(|c| c.skip_newlines()); + self.current = cursor.position(); + } + self.advance(); // consume 'print' + self.consume(TokenType::LPAREN)?; + let value = Box::new(self.parse_expression()?); + self.consume(TokenType::RPAREN)?; + + Ok(ASTNode::Print { + expression: value, + span: Span::unknown(), + }) + } + + /// nowait文をパース: nowait variable = expression + pub(super) fn parse_nowait(&mut self) -> Result { + self.advance(); // consume 'nowait' + + // 変数名を取得 + let variable = self.expect_identifier("variable name")?; + + self.consume(TokenType::ASSIGN)?; + let expression = Box::new(self.parse_expression()?); + + Ok(ASTNode::Nowait { + variable, + expression, + span: Span::unknown(), + }) + } + + // include文は廃止(usingを使用) + + /// local変数宣言をパース: local var1, var2, var3 または local x = 10 + pub(super) fn parse_local(&mut self) -> Result { + if Self::cursor_enabled() { + let mut cursor = TokenCursor::new(&self.tokens); + cursor.set_position(self.current); + cursor.with_stmt_mode(|c| c.skip_newlines()); + self.current = cursor.position(); + } + self.advance(); // consume 'local' + + let mut names = Vec::new(); + let mut initial_values = Vec::new(); + + // 最初の変数名を取得 + if let TokenType::IDENTIFIER(name) = &self.current_token().token_type { + names.push(name.clone()); + self.advance(); + + // = があれば初期値を設定 + if self.match_token(&TokenType::ASSIGN) { + self.advance(); // consume '=' + initial_values.push(Some(Box::new(self.parse_expression()?))); + + // 初期化付きlocalは単一変数のみ(カンマ区切り不可) + Ok(ASTNode::Local { + variables: names, + initial_values, + span: Span::unknown(), + }) + } else { + // 初期化なしの場合はカンマ区切りで複数変数可能 + initial_values.push(None); + + // カンマ区切りで追加の変数名を取得 + while self.match_token(&TokenType::COMMA) { + self.advance(); // consume ',' + + if let TokenType::IDENTIFIER(name) = &self.current_token().token_type { + names.push(name.clone()); + initial_values.push(None); + self.advance(); + } else { + return Err(self.err_unexpected("identifier")); + } + } + + Ok(ASTNode::Local { + variables: names, + initial_values, + span: Span::unknown(), + }) + } + } else { + Err(self.err_unexpected("identifier")) + } + } + + /// outbox変数宣言をパース: outbox var1, var2, var3 + pub(super) fn parse_outbox(&mut self) -> Result { + self.advance(); // consume 'outbox' + + let mut names = Vec::new(); + + // 最初の変数名を取得 + if let TokenType::IDENTIFIER(name) = &self.current_token().token_type { + names.push(name.clone()); + self.advance(); + + // カンマ区切りで追加の変数名を取得 + while self.match_token(&TokenType::COMMA) { + self.advance(); // consume ',' + + if let TokenType::IDENTIFIER(name) = &self.current_token().token_type { + names.push(name.clone()); + self.advance(); + } else { + return Err(self.err_unexpected("identifier")); + } + } + + let num_vars = names.len(); + Ok(ASTNode::Outbox { + variables: names, + initial_values: vec![None; num_vars], + span: Span::unknown(), + }) + } else { + Err(self.err_unexpected("identifier")) + } + } + + /// try-catch文をパース + pub(super) fn parse_try_catch(&mut self) -> Result { + self.advance(); // consume 'try' + let try_body = self.parse_block_statements()?; + + let mut catch_clauses = Vec::new(); + + // catch節をパース + while self.match_token(&TokenType::CATCH) { + self.advance(); // consume 'catch' + self.consume(TokenType::LPAREN)?; + let (exception_type, exception_var) = self.parse_catch_param()?; + self.consume(TokenType::RPAREN)?; + let catch_body = self.parse_block_statements()?; + + catch_clauses.push(CatchClause { + exception_type, + variable_name: exception_var, + body: catch_body, + span: Span::unknown(), + }); + } + + // cleanup節をパース (オプション) + let finally_body = if self.match_token(&TokenType::CLEANUP) { + self.advance(); // consume 'cleanup' + Some(self.parse_block_statements()?) + } else { + None + }; + + Ok(ASTNode::TryCatch { + try_body, + catch_clauses, + finally_body, + span: Span::unknown(), + }) + } + + /// throw文をパース + pub(super) fn parse_throw(&mut self) -> Result { + self.advance(); // consume 'throw' + let value = Box::new(self.parse_expression()?); + Ok(ASTNode::Throw { + expression: value, + span: Span::unknown(), + }) + } + + /// 🔥 from構文を文としてパース: from Parent.method(args) + pub(super) fn parse_from_call_statement(&mut self) -> Result { + // 既存のparse_from_call()を使用してFromCall ASTノードを作成 + let from_call_expr = self.parse_from_call()?; + + // FromCallは式でもあるが、文としても使用可能 + // 例: from Animal.constructor() (戻り値を使わない) + Ok(from_call_expr) + } + + /// using文をパース: using namespace_name + pub(super) fn parse_using(&mut self) -> Result { + self.advance(); // consume 'using' + + // 名前空間名を取得 + if let TokenType::IDENTIFIER(namespace_name) = &self.current_token().token_type { + let name = namespace_name.clone(); + self.advance(); + + // Phase 0では "nyashstd" のみ許可 + if name != "nyashstd" { + return Err(ParseError::UnsupportedNamespace { + name, + line: self.current_token().line, + }); + } + + Ok(ASTNode::UsingStatement { + namespace_name: name, + span: Span::unknown(), + }) + } else { + Err(ParseError::ExpectedIdentifier { + line: self.current_token().line, + }) + } + } +} diff --git a/src/runner/modes/common_util/resolve/strip.rs b/src/runner/modes/common_util/resolve/strip.rs index 09d451b1..1ce2de21 100644 --- a/src/runner/modes/common_util/resolve/strip.rs +++ b/src/runner/modes/common_util/resolve/strip.rs @@ -141,64 +141,79 @@ pub fn strip_using_and_register( // Two forms: // - using path "..." [as Alias] // - using namespace.with.dots [as Alias] - let resolved_path = if let Some(alias) = alias_opt { - // alias case: resolve namespace to a concrete path - let mut found: Option = using_ctx - .pending_modules - .iter() - .find(|(n, _)| n == &ns) - .map(|(_, p)| p.clone()); - if trace { - if let Some(f) = &found { - eprintln!("[using/resolve] alias '{}' -> '{}'", ns, f); - } - } - if found.is_none() { - match crate::runner::pipeline::resolve_using_target( - &ns, - false, - &using_ctx.pending_modules, - &using_ctx.using_paths, - &using_ctx.aliases, - &using_ctx.packages, - ctx_dir, - strict, - verbose, - ) { - Ok(v) => { - // Treat unchanged token (namespace) as unresolved - if v == ns { found = None; } else { found = Some(v) } - } - Err(e) => return Err(format!("using: {}", e)), - } - } - if let Some(value) = found.clone() { + let resolved_path = if let Some(alias_or_path) = alias_opt { + // Disambiguate: when alias_opt looks like a file path, treat it as direct path. + let is_path_hint = alias_or_path.ends_with(".nyash") + || alias_or_path.contains('/') + || alias_or_path.contains('\\'); + if is_path_hint { + // Direct path provided (e.g., using "path/file.nyash" as Name) + let value = alias_or_path.clone(); + // Register: Name -> path let sb = crate::box_trait::StringBox::new(value.clone()); - crate::runtime::modules_registry::set(alias.clone(), Box::new(sb)); - let sb2 = crate::box_trait::StringBox::new(value.clone()); - crate::runtime::modules_registry::set(ns.clone(), Box::new(sb2)); - // Optional: autoload dylib when using kind="dylib" and NYASH_USING_DYLIB_AUTOLOAD=1 - if value.starts_with("dylib:") && std::env::var("NYASH_USING_DYLIB_AUTOLOAD").ok().as_deref() == Some("1") { - let lib_path = value.trim_start_matches("dylib:"); - // Derive lib name from file stem (strip leading 'lib') - let p = std::path::Path::new(lib_path); - if let Some(stem) = p.file_stem().and_then(|s| s.to_str()) { - let mut lib_name = stem.to_string(); - if lib_name.starts_with("lib") { lib_name = lib_name.trim_start_matches("lib").to_string(); } - // Determine box list from using packages (prefer [using.].bid) - let mut boxes: Vec = Vec::new(); - if let Some(pkg) = using_ctx.packages.get(&ns) { - if let Some(b) = &pkg.bid { boxes.push(b.clone()); } - } - if verbose { eprintln!("[using] autoload dylib: {} as {} boxes=[{}]", lib_path, lib_name, boxes.join(",")); } - let host = crate::runtime::plugin_loader_unified::get_global_plugin_host(); - let _ = host.read().unwrap().load_library_direct(&lib_name, lib_path, &boxes); + crate::runtime::modules_registry::set(ns.clone(), Box::new(sb)); + Some(value) + } else { + // alias string for a namespace (e.g., using ns.token as Alias) + let alias = alias_or_path; + // alias case: resolve namespace to a concrete path + let mut found: Option = using_ctx + .pending_modules + .iter() + .find(|(n, _)| n == &ns) + .map(|(_, p)| p.clone()); + if trace { + if let Some(f) = &found { + eprintln!("[using/resolve] alias '{}' -> '{}'", ns, f); } } - } else if trace { - eprintln!("[using] still unresolved: {} as {}", ns, alias); + if found.is_none() { + match crate::runner::pipeline::resolve_using_target( + &ns, + false, + &using_ctx.pending_modules, + &using_ctx.using_paths, + &using_ctx.aliases, + &using_ctx.packages, + ctx_dir, + strict, + verbose, + ) { + Ok(v) => { + // Treat unchanged token (namespace) as unresolved + if v == ns { found = None; } else { found = Some(v) } + } + Err(e) => return Err(format!("using: {}", e)), + } + } + if let Some(value) = found.clone() { + let sb = crate::box_trait::StringBox::new(value.clone()); + crate::runtime::modules_registry::set(alias.clone(), Box::new(sb)); + let sb2 = crate::box_trait::StringBox::new(value.clone()); + crate::runtime::modules_registry::set(ns.clone(), Box::new(sb2)); + // Optional: autoload dylib when using kind="dylib" and NYASH_USING_DYLIB_AUTOLOAD=1 + if value.starts_with("dylib:") && std::env::var("NYASH_USING_DYLIB_AUTOLOAD").ok().as_deref() == Some("1") { + let lib_path = value.trim_start_matches("dylib:"); + // Derive lib name from file stem (strip leading 'lib') + let p = std::path::Path::new(lib_path); + if let Some(stem) = p.file_stem().and_then(|s| s.to_str()) { + let mut lib_name = stem.to_string(); + if lib_name.starts_with("lib") { lib_name = lib_name.trim_start_matches("lib").to_string(); } + // Determine box list from using packages (prefer [using.].bid) + let mut boxes: Vec = Vec::new(); + if let Some(pkg) = using_ctx.packages.get(&ns) { + if let Some(b) = &pkg.bid { boxes.push(b.clone()); } + } + if verbose { eprintln!("[using] autoload dylib: {} as {} boxes=[{}]", lib_path, lib_name, boxes.join(",")); } + let host = crate::runtime::plugin_loader_unified::get_global_plugin_host(); + let _ = host.read().unwrap().load_library_direct(&lib_name, lib_path, &boxes); + } + } + } else if trace { + eprintln!("[using] still unresolved: {} as {}", ns, alias); + } + found } - found } else { // direct namespace without alias match crate::runner::pipeline::resolve_using_target( @@ -397,6 +412,9 @@ pub fn strip_using_and_register( combined.push('\n'); crate::runner::modes::common_util::resolve::seam::fix_prelude_braces_if_enabled(prelude_clean, &mut combined, trace); combined.push_str(&out); + if std::env::var("NYASH_RESOLVE_SEAM_DEBUG").ok().as_deref() == Some("1") { + let _ = std::fs::write("/tmp/nyash_using_combined.nyash", &combined); + } Ok(combined) }