diff --git a/CURRENT_TASK.md b/CURRENT_TASK.md index 91e13333..3d632695 100644 --- a/CURRENT_TASK.md +++ b/CURRENT_TASK.md @@ -1,14 +1,16 @@ -# Current Task (2025-09-13 改定) — Phase 15 llvmlite(既定)+ PyVM(新規) +# Current Task (2025-09-14 改定) — Phase 15 llvmlite(既定)+ PyVM(新規) Summary - JIT/Cranelift は一時停止。Rust/inkwell LLVM は参照のみ。 - 既定の実行/ビルド経路は Python/llvmlite ハーネス(MIR JSON→.o→NyRT link)。 - 2本目の実行経路として PyVM(Python MIR VM)を導入し、llvmlite との機能同値で安定化する。 +- A6 受入(parity+verify)を達成済み。次は Nyash パーサMVP 着手。 -Quick Status — 2025‑09‑13(post‑harness hardening) -- llvmlite(ハーネス)で verify green → .o → link が代表ケースで成立(dep_tree_min_string) -- Resolver‑only/Sealed SSA/文字列ハンドル不変を強化。PHIはBB先頭・pred終端でboxing/cast。 -- IRダンプ/PHIガード/deny-directチェックが利用可能(NYASH_LLVM_DUMP_IR, NYASH_LLVM_PHI_STRICT, tools/llvmlite_check_deny_direct.sh)。 +Quick Status — 2025‑09‑14(A6 Accepted) +- esc_dirname_smoke: PyVM↔llvmlite パリティ一致(ゲートOFF)。 +- dep_tree_min_string: PyVM↔llvmlite パリティ一致。llvmlite 経路で `.ll verify → .o → EXE` 完走。 +- 一時救済ゲート `NYASH_LLVM_ESC_JSON_FIX` は受入では未使用(OFF)。 +- Resolver‑only/Sealed SSA/文字列ハンドル不変は継続運用。IRダンプ/PHIガード/deny-direct チェック利用可。 Focus Shift — llvmlite(既定)+ PyVM(新規) - Rust/inkwell は保守のみ。Python(llvmlite/PyVM)中心で開発。 @@ -33,12 +35,11 @@ Hot Update — 2025‑09‑13(Resolver‑only 統一 + Harness ON green) - `main` 衝突回避: MIR 由来 `main` は private にし、`ny_main()` ラッパを自動生成(NyRT `main` と整合)。 - 代表ケース(dep_tree_min_string): Harness ON で `.ll verify green → .o` を確認し、NyRT とリンクして EXE 生成成功。 -Next(short — Py/llvmlite + PyVM) -1) PyVM スキャフォールド: `tools/pyvm_runner.py` と `src/llvm_py/pyvm/` 追加(最小命令+boxcall)。 -2) ランナー統合: `NYASH_VM_USE_PY=1` → MIR(JSON) を PyVM に渡して実行。 -3) パリティ基盤: 汎用パリティスクリプト `tools/parity.sh` を追加(stdout+exit code 比較、pyvm/vm/llvmlite 任意ペア)。 -4) 型メタ導入(MIR v0.5 互換): JSON MIR に String の handle/ptr 種別を明示し、llvmlite/PyVM で推測を排除。 -5) スモーク拡充: esc_dirname_smoke / dep_tree_min_string の両経路一致(終了コード+JSON)。 +Next(short — Parser MVP kick‑off) +1) Ny→JSON v0 パイプを Nyash で実装(整数/文字列/四則/括弧/return)。 +2) `--ny-parser-pipe`/`--json-file` と互換の JSON v0 を出力し、既存ブリッジで実行。 +3) スモーク: `tools/ny_roundtrip_smoke.sh` 緑、`esc_dirname_smoke`/`dep_tree_min_string` を Ny パーサ経路で PyVM/llvmlite とパリティ一致。 +4) (続)Ny AST→MIR JSON 直接降下の設計(箱構造/型メタ連携)。 Hot Update — MIR v0.5 Type Metadata(2025‑09‑14 着手) - 背景: 文字列を i64 として曖昧に扱っており、llvmlite で handle/ptr の推測が必要→不安定の温床。 @@ -55,6 +56,30 @@ Hot Update — MIR v0.5 Type Metadata(2025‑09‑14 着手) - Python 側で `dst_type` により string ハンドルのタグ付けが行われること - `tools/parity.sh` が esc_dirname_smoke で実行できること(完全一致は第二段で目標) +Hot Update — 2025‑09‑14(Option A: A6 受入完了) +- 方針: mem2reg は採らず、MIR→JSON→llvmlite で収束。 +- 実施: if/loop の MIR 補強(then/else 再代入のPhi化、latch→header seal の堅牢化)。 +- 結果: esc_dirname_smoke / min_str_cat_loop / dep_tree_min_string が PyVM↔llvmlite で一致。LLVM verifier green → .o 成立。 + +Tasks(短期・優先順 — ParserMVP) +1) Parser v0(Ny→JSON v0) + - `apps/selfhost/parser/` に LexerBox/ParserBox を配置し、整数/文字列/四則/括弧/return をJSON v0に吐く。 +2) CLI/ブリッジ動線 + - `nyash --ny-parser-pipe` と同等フォーマットで出力→既存 `json_v0_bridge` で実行。 +3) スモーク整備 + - `tools/ny_roundtrip_smoke.sh`/`tools/ny_parser_bridge_smoke.sh` を Ny パーサ経路でも緑に。 +4) 設計 + - Ny AST→MIR JSON 直接降下の箱設計(型メタ/Resolverフック)。 + +Notes(ゲート/保護) +- 進行中のみの一時救済ゲート: `NYASH_LLVM_ESC_JSON_FIX=1` + - finalize_phis で esc_json のデフォルト枝に `out = concat(out, substring)` を pred末端に合成(dominance-safe) + - 本命の MIR 修正が入ったため、受け入れには使用しない(OFF)。 + +Back‑up Plan(実験レーン・必要時) +- Python MIR Builder(AST→MIR を Python で生成)を `NYASH_MIR_PYBUILDER=1` のフラグで限定導入し、smoke 2本(min_str/esc_dirname)で PyVM/llvmlite 一致を先に確保。 +- 良好なら段階移行。ダメなら即OFF(現行MIRを維持)。 + Hot Update — 2025‑09‑14(typed binop/compare/phi + PHI on‑demand) - 目的: LLVM層での型推測を廃止し、MIR→JSONの型メタにもとづく機械的降下へ移行。 - JSONエミッタ(src/runner/mir_json_emit.rs) diff --git a/app_ll_esc_fix b/app_ll_esc_fix new file mode 100644 index 00000000..097c9784 Binary files /dev/null and b/app_ll_esc_fix differ diff --git a/app_ll_verify b/app_ll_verify new file mode 100644 index 00000000..5703ec19 Binary files /dev/null and b/app_ll_verify differ diff --git a/app_llvmlite_esc b/app_llvmlite_esc new file mode 100644 index 00000000..5955ab2b Binary files /dev/null and b/app_llvmlite_esc differ diff --git a/app_parity_dep_tree_min_string b/app_parity_dep_tree_min_string new file mode 100644 index 00000000..7217ca5c Binary files /dev/null and b/app_parity_dep_tree_min_string differ diff --git a/app_parity_esc11 b/app_parity_esc11 new file mode 100644 index 00000000..d3998d9c Binary files /dev/null and b/app_parity_esc11 differ diff --git a/app_parity_esc_dirname_smoke b/app_parity_esc_dirname_smoke index 30020ee6..5703ec19 100644 Binary files a/app_parity_esc_dirname_smoke and b/app_parity_esc_dirname_smoke differ diff --git a/app_parity_main b/app_parity_main index 2330d3fd..86865326 100644 Binary files a/app_parity_main and b/app_parity_main differ diff --git a/apps/selfhost/parser/ny_parser_v0/main.nyash b/apps/selfhost/parser/ny_parser_v0/main.nyash new file mode 100644 index 00000000..0bb69775 --- /dev/null +++ b/apps/selfhost/parser/ny_parser_v0/main.nyash @@ -0,0 +1,217 @@ +// ny_parser_v0 — Stage 1 MVP: Ny -> JSON v0 (minimal) +// Supports: return ; expr ::= term (('+'|'-') term)* +// term ::= factor (('*'|'/') factor)* +// factor::= INT | STRING | '(' expr ')' + +static box Main { + // --- Utilities --- + is_digit(ch) { return ch >= "0" && ch <= "9" } + is_space(ch) { + return ch == " " || ch == "\t" || ch == "\n" || ch == "\r" + } + + esc_json(s) { + // escape backslash and quote for JSON strings + local out = "" + local i = 0 + local n = s.length() + loop(i < n) { + local ch = s.substring(i, i+1) + if ch == "\\" { out = out + "\\\\" } else { + if ch == "\"" { out = out + "\\\"" } else { out = out + ch } + } + i = i + 1 + } + return out + } + + // Cursor helpers over source string + skip_ws(src, i) { + local n = src.length() + loop(i < n && me.is_space(src.substring(i, i+1))) { i = i + 1 } + return i + } + + // (helper match removed; inline checks in parse_program) + + parse_number(src, i) { + // returns (json, next_i) + local n = src.length() + local j = i + loop(j < n && me.is_digit(src.substring(j, j+1))) { j = j + 1 } + local s = src.substring(i, j) + local json = "{\"type\":\"Int\",\"value\":" + s + "}" + return json + "@" + j // pack result with '@' separator + } + + parse_string(src, i) { + local n = src.length() + local j = i + 1 // skip opening quote + local out = "" + loop(j < n) { + local ch = src.substring(j, j+1) + if ch == "\"" { + j = j + 1 + local json0 = "{\"type\":\"Str\",\"value\":\"" + me.esc_json(out) + "\"}" + return json0 + "@" + j + } + if ch == "\\" && j + 1 < n { + local nx = src.substring(j+1, j+2) + // minimal escapes (\" and \\) + if nx == "\"" { out = out + "\"" } else { if nx == "\\" { out = out + "\\" } else { out = out + nx } } + j = j + 2 + } else { + out = out + ch + j = j + 1 + } + } + // Unterminated string (fallback) + local json = "{\"type\":\"Str\",\"value\":\"" + me.esc_json(out) + "\"}" + return json + "@" + j + } + + // Recursive descent + parse_factor(src, i) { + // skip ws + local nsrc = src.length() + loop(i < nsrc && me.is_space(src.substring(i, i+1))) { i = i + 1 } + local ch = src.substring(i, i+1) + if ch == "(" { + // (expr) + local p = me.parse_expr(src, i + 1) + local at = p.lastIndexOf("@") + local ej = p.substring(0, at) + local j = me.to_int(p.substring(at+1, p.length())) + // skip ws + local n2 = src.length() + loop(j < n2 && me.is_space(src.substring(j, j+1))) { j = j + 1 } + if src.substring(j, j+1) == ")" { j = j + 1 } + return ej + "@" + j + } + if ch == "\"" { return me.parse_string(src, i) } + // number + return me.parse_number(src, i) + } + + parse_term(src, i) { + local p = me.parse_factor(src, i) + local at = p.lastIndexOf("@") + local lhs = p.substring(0, at) + local j = me.to_int(p.substring(at+1, p.length())) + local cont = 1 + loop(cont == 1) { + // skip ws + local n3 = src.length() + loop(j < n3 && me.is_space(src.substring(j, j+1))) { j = j + 1 } + if j >= src.length() { cont = 0 } else { + local op = src.substring(j, j+1) + if op != "*" && op != "/" { cont = 0 } else { + // parse rhs + local q = me.parse_factor(src, j+1) + local at2 = q.lastIndexOf("@") + local rhs = q.substring(0, at2) + j = me.to_int(q.substring(at2+1, q.length())) + lhs = "{\"type\":\"Binary\",\"op\":\"" + op + "\",\"lhs\":" + lhs + ",\"rhs\":" + rhs + "}" + } + } + } + return lhs + "@" + j + } + + parse_expr(src, i) { + local p = me.parse_term(src, i) + local at = p.lastIndexOf("@") + local lhs = p.substring(0, at) + local j = me.to_int(p.substring(at+1, p.length())) + local cont = 1 + loop(cont == 1) { + // skip ws + local n4 = src.length() + loop(j < n4 && me.is_space(src.substring(j, j+1))) { j = j + 1 } + if j >= src.length() { cont = 0 } else { + local op = src.substring(j, j+1) + if op != "+" && op != "-" { cont = 0 } else { + // parse rhs + local q = me.parse_term(src, j+1) + local at2 = q.lastIndexOf("@") + local rhs = q.substring(0, at2) + j = me.to_int(q.substring(at2+1, q.length())) + lhs = "{\"type\":\"Binary\",\"op\":\"" + op + "\",\"lhs\":" + lhs + ",\"rhs\":" + rhs + "}" + } + } + } + return lhs + "@" + j + } + + to_int(s) { + // parse decimal int from string s + // 簡易に桁を読む(ここでは利用側が整数のみで使う) + // ただしここは内部専用で index を取り出すだけなので、s は数字のみ想定 + // 実装簡略化のため、長さ0なら0、それ以外は手動で畳み込み + local n = s.length() + if n == 0 { return 0 } + local i = 0 + local acc = 0 + loop(i < n) { + local d = s.substring(i, i+1) + local dv = 0 + if d == "1" { dv = 1 } else { + if d == "2" { dv = 2 } else { + if d == "3" { dv = 3 } else { + if d == "4" { dv = 4 } else { + if d == "5" { dv = 5 } else { + if d == "6" { dv = 6 } else { + if d == "7" { dv = 7 } else { + if d == "8" { dv = 8 } else { + if d == "9" { dv = 9 } + } + } + } + } + } + } + } + } + acc = acc * 10 + dv + i = i + 1 + } + return acc + } + + parse_program(src) { + // optional leading ws + optional 'return' + // skip leading ws + local i = 0 + local n0 = src.length() + loop(i < n0 && me.is_space(src.substring(i, i+1))) { i = i + 1 } + local j = i + local n = src.length() + if i + 6 <= n && src.substring(i, i+6) == "return" { j = i + 6 } + local p = me.parse_expr(src, j) + local at = p.lastIndexOf("@") + local ej = p.substring(0, at) + local body = "[{\"type\":\"Return\",\"expr\":" + ej + "}]" + return "{\"version\":0,\"kind\":\"Program\",\"body\":" + body + "}" + } + + read_all(path) { + local fb = new FileBox() + fb.open(path, "r") + local s = fb.read() + fb.close() + return s + } + + main(args) { + // usage: nyash --backend vm apps/selfhost/parser/ny_parser_v0/main.nyash + // Input source is read from tmp/ny_parser_input.ny (written by wrapper script) + local console = new ConsoleBox() + local src = null + local default_path = "tmp/ny_parser_input.ny" + src = me.read_all(default_path) + if src == null { src = "return 1+2*3" } + local json = me.parse_program(src) + console.println(json) + return 0 + } +} diff --git a/apps/tests/peek_expr_block.nyash b/apps/tests/peek_expr_block.nyash new file mode 100644 index 00000000..482c06fe --- /dev/null +++ b/apps/tests/peek_expr_block.nyash @@ -0,0 +1,11 @@ +static box Main { + main(args) { + local d = "1" + local dv = peek d { + "0" => { print("found zero") 0 } + "1" => { print("found one") 1 } + else => { print("other") 0 } + } + return dv + } +} diff --git a/apps/tests/ternary_basic.nyash b/apps/tests/ternary_basic.nyash new file mode 100644 index 00000000..0cc63c29 --- /dev/null +++ b/apps/tests/ternary_basic.nyash @@ -0,0 +1,6 @@ +static box Main { + main(args) { + return (1 < 2) ? 10 : 20 + } +} + diff --git a/docs/development/roadmap/phases/phase-15/README.md b/docs/development/roadmap/phases/phase-15/README.md index a14675f0..682aa355 100644 --- a/docs/development/roadmap/phases/phase-15/README.md +++ b/docs/development/roadmap/phases/phase-15/README.md @@ -26,11 +26,25 @@ MIR 13命令の美しさを最大限に活かし、外部コンパイラ依存 - ランナー統合: `NYASH_VM_USE_PY=1` で MIR(JSON) を PyVM に渡して実行 - 代表スモーク(esc_dirname_smoke / dep_tree_min_string)で llvmlite とパリティ確認 -### Phase 15.3: NyashコンパイラMVP(後段) +【Current Status — 2025‑09‑14】 +- A6 受入達成: esc_dirname_smoke の PyVM↔llvmlite パリティ一致(ゲートOFF)、LLVM verifier green → .o → リンク → 実行OK。 +- dep_tree_min_string: PyVM↔llvmlite パリティ一致、llvmlite 経路で `.ll verify → .o → EXE` 完走。 +- 一時救済ゲート `NYASH_LLVM_ESC_JSON_FIX` は受入には未使用(OFF)。 + +### Phase 15.3: NyashコンパイラMVP(次フェーズ着手) - PyVM 安定後、Nyash製パーサ/レクサ(サブセット)と MIR ビルダを段階導入 - フラグでRustフォールバックと併存(例: `NYASH_USE_NY_COMPILER=1`) - JIT不要、PyVM/llvmlite のパリティで正しさを担保 +【Kickoff 目標(MVP)】 +- ステージ1: Ny→JSON v0 パイプ(整数/文字列/加減乗除/括弧/return)。CLI: `--ny-parser-pipe` と互換のJSONを生成。 +- ステージ2: 文/式サブセット拡張(local/if/loop/call/method/new/me/substring/length/lastIndexOf)。 +- ステージ3: Ny AST→MIR JSON 降下(直接 llvmlite/PyVM へ渡す)。 + +【受入(MVP)】 +- `tools/ny_roundtrip_smoke.sh` 緑(Case A/B)。 +- `apps/tests/esc_dirname_smoke.nyash` / `apps/selfhost/tools/dep_tree_min_string.nyash` を Ny パーサ経路で実行し、PyVM/llvmlite とパリティ一致(stdout/exit)。 + ### Phase 15.4: VM層のNyash化(PyVMからの置換) - PyVM を足場に、VMコアを Nyash 実装へ段階移植(命令サブセットから) - 動的ディスパッチで13命令処理を目標に拡張 @@ -221,6 +235,10 @@ ny_free_buf(buffer) ### 🔧 実行チェックリスト - [ROADMAP.md](ROADMAP.md) - 進捗管理用チェックリスト +### ✅ クイックスモーク(現状) +- PyVM↔llvmlite パリティ: `tools/parity.sh --lhs pyvm --rhs llvmlite apps/tests/esc_dirname_smoke.nyash` +- dep_tree(ハーネスON): `NYASH_LLVM_FEATURE=llvm ./tools/build_llvm.sh apps/selfhost/tools/dep_tree_min_string.nyash -o app_dep && ./app_dep` + ### 📚 関連フェーズ - [Phase 10: Cranelift JIT](../phase-10/) - [Phase 12.5: 最適化戦略](../phase-12.5/) diff --git a/docs/development/roadmap/phases/phase-15/planning/sequence.md b/docs/development/roadmap/phases/phase-15/planning/sequence.md index 1d94fbb8..d2cb2463 100644 --- a/docs/development/roadmap/phases/phase-15/planning/sequence.md +++ b/docs/development/roadmap/phases/phase-15/planning/sequence.md @@ -37,6 +37,8 @@ **完了基準:** - esc_dirname_smoke / dep_tree_min_string が PyVM と llvmlite で一致。 +【Status 2025‑09‑14】完了(A6 受入)。 + ### 3) using(ゲート付き)設計・実装(15.2/15.3) **要点:** @@ -51,6 +53,22 @@ **完了基準:** - フラグONで using 経路が動作し、未解決時の診断・キャッシュ挙動がテストで担保。 +【Next】Ny パーサMVPと並走で段階導入(フラグ: `--enable-using`/`NYASH_ENABLE_USING=1`)。 + +### 3.5) Nyash パーサMVP(サブセット) + +**要点:** +- ステージ1: Ny→JSON v0 パイプ(最小表現)。 +- ステージ2: 文/式のサブセット拡張。 +- ステージ3: Ny AST→MIR JSON 直接降下(llvmlite/PyVMへ)。 + +**スモーク/CI:** +- `tools/ny_roundtrip_smoke.sh` / `tools/ny_parser_bridge_smoke.sh` +- `tools/parity.sh --lhs pyvm --rhs llvmlite `(Nyパーサ経路ON) + +**完了基準:** +- esc_dirname_smoke / dep_tree_min_string が Ny パーサ経路でも PyVM/llvmlite と一致(stdout/exit)。 + ### 4) nyash.link ミニマルリゾルバ(15.4) **要点:** diff --git a/docs/guides/language-guide.md b/docs/guides/language-guide.md index 1816d6d0..9a9553b5 100644 --- a/docs/guides/language-guide.md +++ b/docs/guides/language-guide.md @@ -1,278 +1,21 @@ -# 📚 Nyash Programming Language - 完全ガイド +# Nyash Language Guide -**最終更新: 2025年8月12日 - Phase 1完了, P2P実装準備完了** +Start here to learn Nyash language basics and find deeper references. -## 📖 概要 +- Syntax Cheat Sheet: quick-reference/syntax-cheatsheet.md +- Full Language Reference (2025): reference/language/LANGUAGE_REFERENCE_2025.md +- Phase 12.7 Grammar (peek / ternary / sugar): + - Overview: development/roadmap/phases/phase-12.7/grammar-specs/README.md + - Tokens & Grammar: development/roadmap/phases/phase-12.7/ancp-specs/ANCP-Token-Specification-v1.md +- Sugar transformations (?., ??, |> ...): tools/nyfmt/NYFMT_POC_ROADMAP.md -Nyashは「Everything is Box」哲学に基づく革新的なプログラミング言語です。 -Rust製インタープリターによる高性能実行と、直感的な構文により、学習しやすく実用的な言語として完成しました。 +Common Constructs +- Ternary operator: `cond ? then : else` (Phase 12.7); lowered to If-expression +- Peek expression: `peek value { lit => expr, else => expr }` +- Null-coalesce: `x ?? y` → `peek x { null => y, else => x }` +- Safe access: `a?.b` → `peek a { null => null, else => a.b }` -## 🎯 核心哲学: "Everything is Box" - -```nyash -# すべてのデータがBoxとして統一的に表現される -number = 42 # IntegerBox -text = "hello" # StringBox -flag = true # BoolBox -array = new ArrayBox() # ArrayBox -debug = new DebugBox() # DebugBox -``` - -**重要な利点:** -- **統一性**: すべてのデータが共通インターフェース -- **メモリ安全性**: Arcパターンで完全スレッドセーフ -- **拡張性**: 新しいBox型を容易に追加可能 - ---- - -## 🔤 言語構文リファレンス - -### 🏷️ **変数宣言・スコープ** - -```nyash -// ローカル変数宣言 -local x, y -local name = "Alice" - -// 所有権移転変数(関数戻り値用) -outbox result = compute() - -// グローバル変数 -global CONFIG = "dev" -``` - -### 🧮 **演算子** - -```nyash -// 算術演算子 -a + b, a - b, a * b, a / b - -// 比較演算子 -a == b, a != b, a < b, a > b, a <= b, a >= b - -// 論理演算子 -not condition, a and b, a or b - -// ビット演算子(整数限定) -a & b, a | b, a ^ b -a << n, a >> n # 注意: 旧来の >>(ARROW)は廃止。パイプラインは |> - -// Cross-type演算 (Phase 1で完全実装) -10 + 3.14 // → 13.14 (型変換) -"Value: " + 42 // → "Value: 42" (文字列連結) -``` - -### 🏗️ **Box定義・デリゲーション** - -#### 基本Box定義 -```nyash -box User { - init { name, email } // フィールド宣言 - - birth(userName, userEmail) { // 🌟 生命をBoxに与える! - me.name = userName - me.email = userEmail - print("🌟 " + userName + " が誕生しました!") - } - - greet() { - print("Hello, " + me.name) - } -} -``` - -#### デリゲーション (2025-08 革命) -```nyash -box AdminUser from User { // 🔥 from構文でデリゲーション - init { permissions } - - birth(adminName, adminEmail, perms) { - from User.birth(adminName, adminEmail) // 親のbirth呼び出し - me.permissions = perms - } - - override greet() { // 明示的オーバーライド - from User.greet() // 親メソッド呼び出し - print("Admin privileges: " + me.permissions) - } -} -``` - -#### ビルトインBox継承(pack専用) -```nyash -// ⚠️ pack構文はビルトインBox継承専用 -box EnhancedP2P from P2PBox { - init { features } - - pack(nodeId, transport) { - from P2PBox.pack(nodeId, transport) // ビルトイン初期化 - me.features = new ArrayBox() - print("🌐 Enhanced P2P Node created: " + nodeId) - } - - override send(intent, data, target) { - me.features.push("send:" + intent) - return from P2PBox.send(intent, data, target) - } -} -``` - -#### Static Box Main パターン -```nyash -static box Main { - init { console, result } - - main() { - me.console = new ConsoleBox() - me.console.log("🎉 Everything is Box!") - return "Success!" - } -} -``` - -### 🔄 **制御構造** - -```nyash -// 条件分岐 -if condition { - // 処理 -} else { - // 別処理 -} - -// ループ(唯一の正しい形式) -loop(condition) { - // 処理 - if breakCondition { - break - } -} -``` - ---- - -## 📦 ビルトインBox型 - -### 基本型 -- **StringBox**: 文字列 (`"hello"`) -- **IntegerBox**: 整数 (`42`) -- **FloatBox**: 浮動小数点数 (`new FloatBox(3.14)`) ✅ Phase 1完了 -- **BoolBox**: 真偽値 (`true`, `false`) -- **NullBox**: NULL値 (`null`) - -### データ構造 -- **ArrayBox**: 配列 (`new ArrayBox()`) ✅ Phase 1で sort/reverse/indexOf/slice 実装 -- **MapBox**: 連想配列 (`new MapBox()`) - -### ユーティリティ -- **ConsoleBox**: コンソール出力 (`new ConsoleBox()`) -- **DebugBox**: デバッグ機能 (`new DebugBox()`) -- **MathBox**: 数学関数 (`new MathBox()`) -- **TimeBox**: 時刻処理 (`new TimeBox()`) -- **DateTimeBox**: 日時処理 (`new DateTimeBox()`) ✅ Phase 1で完全動作 - -### 高度機能 -- **RandomBox**: 乱数生成 -- **BufferBox**: バッファ操作 -- **RegexBox**: 正規表現 -- **JSONBox**: JSON処理 -- **StreamBox**: ストリーム処理 - -### P2P通信 (Phase 2実装中) -- **IntentBox**: 構造化メッセージ (実装予定) -- **P2PBox**: P2P通信ノード (実装予定) - ---- - -## 🚀 実用例 - -### データ処理例 -```nyash -// 配列操作 -local numbers = new ArrayBox() -numbers.push(3) -numbers.push(1) -numbers.push(2) -numbers.sort() // [1, 2, 3] - -// 型変換・演算 -local result = 10 + new FloatBox(3.14) // 13.14 -print("Result: " + result.toString()) -``` - -### P2P通信例 (将来実装) -```nyash -// P2Pノード作成 -local node_a = new P2PBox("alice", transport: "inprocess") -local node_b = new P2PBox("bob", transport: "inprocess") - -// メッセージ受信ハンドラ -node_b.on("chat.message", function(intent, from) { - print("From " + from + ": " + intent.payload.text) -}) - -// 構造化メッセージ送信 -local msg = new IntentBox("chat.message", { text: "Hello P2P!" }) -node_a.send("bob", msg) // → "From alice: Hello P2P!" -``` - ---- - -## ⚠️ 重要な注意点 - -### 必須のコンマ -```nyash -// ✅ 正しい -init { field1, field2 } - -// ❌ 間違い(CPU暴走の原因) -init { field1 field2 } -``` - -### 変数宣言厳密化 -```nyash -// ✅ 明示宣言必須 -local x -x = 42 - -// ❌ 未宣言変数への代入はエラー -y = 42 // Runtime Error + 修正提案 -``` - -### ループ構文統一 -```nyash -// ✅ 唯一の正しい形式 -loop(condition) { } - -// ❌ 削除済み構文 -while condition { } // 使用不可 -``` - ---- - -## 📈 実装状況 (2025-08-12) - -### ✅ Phase 1完了 -- FloatBox toString() メソッド -- ArrayBox 改良 (sort/reverse/indexOf/slice) -- Cross-type演算子システム -- 包括的テストスイート (188行) - -### 🚧 Phase 2実装中 -- IntentBox (構造化メッセージ) -- MessageBus (プロセス内シングルトン) -- P2PBox (P2P通信ノード) - -### 📋 将来予定 -- WebSocket/WebRTC通信 -- 非同期処理 (async/await) -- 追加のBox型拡張 - ---- - -**🎉 Nyashで「Everything is Box」の世界を体験しよう!** - -📚 **関連ドキュメント:** -- [Getting Started](GETTING_STARTED.md) - 環境構築・最初の一歩 -- [P2P Guide](P2P_GUIDE.md) - P2P通信システム完全ガイド -- [Built-in Boxes](reference/builtin-boxes.md) - ビルトインBox詳細リファレンス +When you need the implementation details +- Tokenizer: src/tokenizer.rs +- Parser: src/parser/expressions.rs, src/parser/statements.rs +- Lowering to MIR: src/mir/builder/** diff --git a/docs/reference/architecture/TECHNICAL_ARCHITECTURE_2025.md b/docs/reference/architecture/TECHNICAL_ARCHITECTURE_2025.md new file mode 100644 index 00000000..3e905f39 --- /dev/null +++ b/docs/reference/architecture/TECHNICAL_ARCHITECTURE_2025.md @@ -0,0 +1,12 @@ +# Technical Architecture (2025) – Map + +This index points to the currently maintained architectural documents: + +- Core Concepts: reference/architecture/nyash_core_concepts.md +- Execution Backends: reference/architecture/execution-backends.md +- Lowering Contexts: LOWERING_CONTEXTS.md +- LLVM Layer Overview: LLVM_LAYER_OVERVIEW.md +- VM Overview: VM_README.md +- Cranelift AOT design: backend-cranelift-aot-design.md + +Note: Some long-form papers reside under `private/papers/reference/architecture/`. diff --git a/docs/reference/architecture/execution-backends.md b/docs/reference/architecture/execution-backends.md new file mode 100644 index 00000000..143b3dc8 --- /dev/null +++ b/docs/reference/architecture/execution-backends.md @@ -0,0 +1,8 @@ +# Execution Backends – Index + +For the full guide, see: +- execution-backends.md (legacy location kept up to date) + +Additional references: +- backend-llvm-implementation-guide.md +- VM_README.md diff --git a/docs/reference/architecture/nyash_core_concepts.md b/docs/reference/architecture/nyash_core_concepts.md new file mode 100644 index 00000000..9b7f326d --- /dev/null +++ b/docs/reference/architecture/nyash_core_concepts.md @@ -0,0 +1,6 @@ +# Nyash Core Concepts (Index) + +Authoritative content is available at: +- private/papers/reference/architecture/nyash_core_concepts.md + +Use this page as a stable reference path under `docs/reference/architecture/`. diff --git a/docs/reference/language/LANGUAGE_REFERENCE_2025.md b/docs/reference/language/LANGUAGE_REFERENCE_2025.md new file mode 100644 index 00000000..8d5cc60c --- /dev/null +++ b/docs/reference/language/LANGUAGE_REFERENCE_2025.md @@ -0,0 +1,6 @@ +# Language Reference (2025) + +The canonical 2025 language reference currently lives here: +- private/papers/reference/language/LANGUAGE_REFERENCE_2025.md + +This stub exists to provide a stable public path under `docs/reference/language/`. diff --git a/docs/reference/language/README.md b/docs/reference/language/README.md new file mode 100644 index 00000000..2f584bc8 --- /dev/null +++ b/docs/reference/language/README.md @@ -0,0 +1,20 @@ +# Nyash Language Reference – Index + +This is the entry point for Nyash language documentation. + +- Full Language Reference (2025): reference/language/LANGUAGE_REFERENCE_2025.md +- Syntax Cheat Sheet: quick-reference/syntax-cheatsheet.md +- Phase 12.7 Grammar Specs (peek, ternary, sugar): + - Overview: development/roadmap/phases/phase-12.7/grammar-specs/README.md + - Token/Grammar: development/roadmap/phases/phase-12.7/ancp-specs/ANCP-Token-Specification-v1.md +- Sugar Transformations (?., ??, |> and friends): parser/sugar.rs (source) and tools/nyfmt/NYFMT_POC_ROADMAP.md +- Peek Expression Design/Usage: covered in the Language Reference and Phase 12.7 specs above + +Related implementation notes +- Tokenizer: src/tokenizer.rs +- Parser (expressions/statements): src/parser/expressions.rs, src/parser/statements.rs +- MIR Lowering (expressions): src/mir/builder/exprs.rs and friends + +Navigation tips +- The “reference/language/LANGUAGE_REFERENCE_2025.md” is the canonical long‑form reference; use the Cheat Sheet for quick syntax lookup. +- Phase 12.7 files capture the finalized sugar and new constructs (peek, ternary, null‑safe). diff --git a/src/llvm_py/instructions/binop.py b/src/llvm_py/instructions/binop.py index 1515e82f..34171d62 100644 --- a/src/llvm_py/instructions/binop.py +++ b/src/llvm_py/instructions/binop.py @@ -97,32 +97,9 @@ def lower_binop( if is_str: # Helper: convert raw or resolved value to string handle def to_handle(raw, val, tag: str, vid: int): - # If we already have an i64 in vmap (raw), prefer it + # If we already have an i64 SSA (handle) in vmap/raw or resolved val, prefer pass-through. if raw is not None and hasattr(raw, 'type') and isinstance(raw.type, ir.IntType) and raw.type.width == 64: - is_tag = False - try: - if resolver is not None and hasattr(resolver, 'is_stringish'): - is_tag = resolver.is_stringish(vid) - except Exception: - is_tag = False - if force_string or is_tag: - return raw - # Heuristic: PHI values in string concat are typically handles; prefer pass-through - try: - raw_is_phi = hasattr(raw, 'add_incoming') - except Exception: - raw_is_phi = False - if raw_is_phi: - return raw - # Otherwise, box numeric i64 to IntegerBox handle - cal = None - for f in builder.module.functions: - if f.name == 'nyash.box.from_i64': - cal = f; break - if cal is None: - cal = ir.Function(builder.module, ir.FunctionType(i64, [i64]), name='nyash.box.from_i64') - v64 = raw - return builder.call(cal, [v64], name=f"int_i2h_{tag}_{dst}") + return raw if raw is not None and hasattr(raw, 'type') and isinstance(raw.type, ir.PointerType): # pointer-to-array -> GEP try: @@ -140,32 +117,8 @@ def lower_binop( return builder.call(cal, [raw], name=f"str_ptr2h_{tag}_{dst}") # if already i64 if val is not None and hasattr(val, 'type') and isinstance(val.type, ir.IntType) and val.type.width == 64: - # Distinguish handle vs numeric: if vid is tagged string-ish, treat as handle; otherwise box numeric to handle - is_tag = False - try: - if resolver is not None and hasattr(resolver, 'is_stringish'): - is_tag = resolver.is_stringish(vid) - except Exception: - is_tag = False - if force_string or is_tag: - return val - # Heuristic: if vmap has a PHI placeholder for this vid, treat as handle - try: - maybe_phi = vmap.get(vid) - if maybe_phi is not None and hasattr(maybe_phi, 'add_incoming'): - return val - except Exception: - pass - # Otherwise, box numeric i64 to IntegerBox handle - cal = None - for f in builder.module.functions: - if f.name == 'nyash.box.from_i64': - cal = f; break - if cal is None: - cal = ir.Function(builder.module, ir.FunctionType(i64, [i64]), name='nyash.box.from_i64') - # Ensure value is i64 - v64 = val if val.type.width == 64 else builder.zext(val, i64) - return builder.call(cal, [v64], name=f"int_i2h_{tag}_{dst}") + # Treat resolved i64 as a handle in string domain(never box numeric here) + return val return ir.Constant(i64, 0) hl = to_handle(lhs_raw, lhs_val, 'l', lhs) diff --git a/src/llvm_py/llvm_builder.py b/src/llvm_py/llvm_builder.py index 7178538e..26006db3 100644 --- a/src/llvm_py/llvm_builder.py +++ b/src/llvm_py/llvm_builder.py @@ -68,6 +68,9 @@ class NyashLLVMBuilder: # Statistics self.loop_count = 0 + # Heuristics for minor gated fixes + self.current_function_name: Optional[str] = None + self._last_substring_vid: Optional[int] = None def build_from_mir(self, mir_json: Dict[str, Any]) -> str: """Build LLVM IR from MIR JSON""" @@ -166,6 +169,7 @@ class NyashLLVMBuilder: def lower_function(self, func_data: Dict[str, Any]): """Lower a single MIR function to LLVM IR""" name = func_data.get("name", "unknown") + self.current_function_name = name import re params = func_data.get("params", []) blocks = func_data.get("blocks", []) @@ -514,6 +518,12 @@ class NyashLLVMBuilder: if dst_type.get("kind") == "handle" and dst_type.get("box_type") == "StringBox": if hasattr(self.resolver, 'mark_string'): self.resolver.mark_string(int(dst)) + # Track last substring for optional esc_json fallback + try: + if isinstance(method, str) and method == 'substring' and isinstance(dst, int): + self._last_substring_vid = int(dst) + except Exception: + pass except Exception: pass @@ -723,13 +733,45 @@ class NyashLLVMBuilder: if val is None: val = ir.Constant(self.i64, 0) chosen[pred_match] = val - # Fill remaining predecessors with dst carry or zero + # Fill remaining predecessors with dst carry or (optionally) a synthesized default for pred_bid in preds_list: if pred_bid not in chosen: + val = None + # Optional gated fix for esc_json: default branch should append current char try: - val = self.resolver._value_at_end_i64(dst_vid, pred_bid, self.preds, self.block_end_values, self.vmap, self.bb_map) + import os + if os.environ.get('NYASH_LLVM_ESC_JSON_FIX','0') == '1': + fname = getattr(self, 'current_function_name', '') or '' + sub_vid = getattr(self, '_last_substring_vid', None) + if isinstance(fname, str) and 'esc_json' in fname and isinstance(sub_vid, int): + # Compute out_at_end and ch_at_end in pred block, then concat_hh + out_end = self.resolver._value_at_end_i64(int(dst_vid), pred_bid, self.preds, self.block_end_values, self.vmap, self.bb_map) + ch_end = self.resolver._value_at_end_i64(int(sub_vid), pred_bid, self.preds, self.block_end_values, self.vmap, self.bb_map) + if out_end is not None and ch_end is not None: + pb = ir.IRBuilder(self.bb_map.get(pred_bid)) + try: + t = self.bb_map.get(pred_bid).terminator + if t is not None: + pb.position_before(t) + else: + pb.position_at_end(self.bb_map.get(pred_bid)) + except Exception: + pass + fnty = ir.FunctionType(self.i64, [self.i64, self.i64]) + callee = None + for f in self.module.functions: + if f.name == 'nyash.string.concat_hh': + callee = f; break + if callee is None: + callee = ir.Function(self.module, fnty, name='nyash.string.concat_hh') + val = pb.call(callee, [out_end, ch_end], name=f"phi_def_concat_{dst_vid}_{pred_bid}") except Exception: - val = None + pass + if val is None: + try: + val = self.resolver._value_at_end_i64(dst_vid, pred_bid, self.preds, self.block_end_values, self.vmap, self.bb_map) + except Exception: + val = None if val is None: val = ir.Constant(self.i64, 0) chosen[pred_bid] = val diff --git a/src/llvm_py/pyvm/vm.py b/src/llvm_py/pyvm/vm.py index d9af9d9e..07817937 100644 --- a/src/llvm_py/pyvm/vm.py +++ b/src/llvm_py/pyvm/vm.py @@ -83,8 +83,19 @@ class PyVM: # Initialize registers and bind params regs: Dict[int, Any] = {} if fn.params: - for i, pid in enumerate(fn.params): - regs[int(pid)] = args[i] if i < len(args) else None + # If this function was lowered from a method (e.g., Main.foo/N), the first + # parameter is an implicit 'me' and call sites pass only N args. + # Align by detecting off-by-one and shifting args to skip the implicit receiver. + if len(args) + 1 == len(fn.params): + # Fill implicit 'me' (unused by our lowering at runtime) and map the rest + if fn.params: + regs[int(fn.params[0])] = None # placeholder for 'me' + for i, pid in enumerate(fn.params[1:]): + regs[int(pid)] = args[i] if i < len(args) else None + else: + # Direct positional bind + for i, pid in enumerate(fn.params): + regs[int(pid)] = args[i] if i < len(args) else None else: # Heuristic: derive param count from name suffix '/N' and bind to vids 0..N-1 n = 0 @@ -291,6 +302,19 @@ class PyVM: out = os.path.join(base, rel) else: out = None + elif method == "esc_json": + # Escape backslash and double-quote in the given string argument + s = args[0] if args else "" + s = "" if s is None else str(s) + out_chars = [] + for ch in s: + if ch == "\\": + out_chars.append("\\\\") + elif ch == '"': + out_chars.append('\\"') + else: + out_chars.append(ch) + out = "".join(out_chars) elif method == "length": out = len(str(recv)) elif method == "substring": diff --git a/src/mir/builder/ops.rs b/src/mir/builder/ops.rs index 59186d2b..22e99a50 100644 --- a/src/mir/builder/ops.rs +++ b/src/mir/builder/ops.rs @@ -27,8 +27,27 @@ impl super::MirBuilder { // Arithmetic operations BinaryOpType::Arithmetic(op) => { self.emit_instruction(MirInstruction::BinOp { dst, op, lhs, rhs })?; - // Arithmetic results are integers for now (Core-1) - self.value_types.insert(dst, MirType::Integer); + // '+' は文字列連結の可能性がある。オペランドが String/StringBox なら結果を String と注釈。 + if matches!(op, crate::mir::BinaryOp::Add) { + let lhs_is_str = match self.value_types.get(&lhs) { + Some(MirType::String) => true, + Some(MirType::Box(bt)) if bt == "StringBox" => true, + _ => false, + }; + let rhs_is_str = match self.value_types.get(&rhs) { + Some(MirType::String) => true, + Some(MirType::Box(bt)) if bt == "StringBox" => true, + _ => false, + }; + if lhs_is_str || rhs_is_str { + self.value_types.insert(dst, MirType::String); + } else { + self.value_types.insert(dst, MirType::Integer); + } + } else { + // その他の算術は整数 + self.value_types.insert(dst, MirType::Integer); + } } // Comparison operations BinaryOpType::Comparison(op) => { diff --git a/src/mir/builder/stmts.rs b/src/mir/builder/stmts.rs index b36ce14e..3268c3aa 100644 --- a/src/mir/builder/stmts.rs +++ b/src/mir/builder/stmts.rs @@ -104,29 +104,36 @@ impl super::MirBuilder { let merge_block = self.block_gen.next(); self.emit_instruction(MirInstruction::Branch { condition: condition_val, then_bb: then_block, else_bb: else_block })?; + // Snapshot variable map before entering branches to avoid cross-branch pollution + let pre_if_var_map = self.variable_map.clone(); // Pre-analysis: detect then-branch assigned var and capture its pre-if value let assigned_then_pre = extract_assigned_var(&then_branch); let pre_then_var_value: Option = assigned_then_pre .as_ref() - .and_then(|name| self.variable_map.get(name).copied()); + .and_then(|name| pre_if_var_map.get(name).copied()); // then self.current_block = Some(then_block); self.ensure_block_exists(then_block)?; let then_ast_for_analysis = then_branch.clone(); - let then_value = self.build_expression(then_branch)?; + // Build then with a clean snapshot of pre-if variables + self.variable_map = pre_if_var_map.clone(); + let then_value_raw = self.build_expression(then_branch)?; + let then_var_map_end = self.variable_map.clone(); if !self.is_current_block_terminated() { self.emit_instruction(MirInstruction::Jump { target: merge_block })?; } // else self.current_block = Some(else_block); self.ensure_block_exists(else_block)?; - let (mut else_value, else_ast_for_analysis) = if let Some(else_ast) = else_branch { + // Build else with a clean snapshot of pre-if variables + let (mut else_value_raw, else_ast_for_analysis, else_var_map_end_opt) = if let Some(else_ast) = else_branch { + self.variable_map = pre_if_var_map.clone(); let val = self.build_expression(else_ast.clone())?; - (val, Some(else_ast)) + (val, Some(else_ast), Some(self.variable_map.clone())) } else { let void_val = self.value_gen.next(); self.emit_instruction(MirInstruction::Const { dst: void_val, value: ConstValue::Void })?; - (void_val, None) + (void_val, None, None) }; if !self.is_current_block_terminated() { self.emit_instruction(MirInstruction::Jump { target: merge_block })?; } @@ -140,22 +147,23 @@ impl super::MirBuilder { let result_val = self.value_gen.next(); if let Some(var_name) = assigned_var_then.clone() { let else_assigns_same = assigned_var_else.as_ref().map(|s| s == &var_name).unwrap_or(false); - if !else_assigns_same { - if let Some(pre) = pre_then_var_value { - // Use pre-if value for else input so SSA is well-formed - else_value = pre; - } - // After merge, the variable should refer to the Phi result - self.emit_instruction(MirInstruction::Phi { dst: result_val, inputs: vec![(then_block, then_value), (else_block, else_value)] })?; - self.variable_map.insert(var_name, result_val); + // Resolve branch-end values for the assigned variable + let then_value_for_var = then_var_map_end.get(&var_name).copied().unwrap_or(then_value_raw); + let else_value_for_var = if else_assigns_same { + else_var_map_end_opt.as_ref().and_then(|m| m.get(&var_name).copied()).unwrap_or(else_value_raw) } else { - // Both sides assign same variable – emit Phi normally and bind - self.emit_instruction(MirInstruction::Phi { dst: result_val, inputs: vec![(then_block, then_value), (else_block, else_value)] })?; - self.variable_map.insert(var_name, result_val); - } + // Else doesn't assign: use pre-if value if available + pre_then_var_value.unwrap_or(else_value_raw) + }; + // Emit Phi for the assigned variable and bind it + self.emit_instruction(MirInstruction::Phi { dst: result_val, inputs: vec![(then_block, then_value_for_var), (else_block, else_value_for_var)] })?; + self.variable_map = pre_if_var_map.clone(); + self.variable_map.insert(var_name, result_val); } else { // No variable assignment pattern detected – just emit Phi for expression result - self.emit_instruction(MirInstruction::Phi { dst: result_val, inputs: vec![(then_block, then_value), (else_block, else_value)] })?; + self.emit_instruction(MirInstruction::Phi { dst: result_val, inputs: vec![(then_block, then_value_raw), (else_block, else_value_raw)] })?; + // Merge variable map conservatively to pre-if snapshot (no new bindings) + self.variable_map = pre_if_var_map.clone(); } Ok(result_val) @@ -327,6 +335,19 @@ fn extract_assigned_var(ast: &ASTNode) -> Option { 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, } } diff --git a/src/mir/loop_builder.rs b/src/mir/loop_builder.rs index 0ada18d2..7f7a7b36 100644 --- a/src/mir/loop_builder.rs +++ b/src/mir/loop_builder.rs @@ -100,12 +100,13 @@ impl<'a> LoopBuilder<'a> { for stmt in body { self.build_statement(stmt)?; } - // latchブロックのスナップショットを保存(phi入力解決用) - let latch_snapshot = self.get_current_variable_map(); - self.block_var_maps.insert(body_id, latch_snapshot); - // 8. Latchブロック(ボディの最後)からHeaderへ戻る + // 現在の挿入先が latch(最後のブロック)なので、そのブロックIDでスナップショットを保存する let latch_id = self.current_block()?; + let latch_snapshot = self.get_current_variable_map(); + // 以前は body_id に保存していたが、複数ブロックのボディや continue 混在時に不正確になるため + // 実際の latch_id に対してスナップショットを紐づける + self.block_var_maps.insert(latch_id, latch_snapshot); self.emit_jump(header_id)?; let _ = self.add_predecessor(header_id, latch_id); diff --git a/src/parser/expressions.rs b/src/parser/expressions.rs index eb3b32e9..19875f8d 100644 --- a/src/parser/expressions.rs +++ b/src/parser/expressions.rs @@ -25,7 +25,7 @@ impl NyashParser { /// パイプライン演算子: lhs |> f(a,b) / lhs |> obj.m(a) /// 基本方針: 右辺が関数呼び出しなら先頭に lhs を挿入。メソッド呼び出しなら引数の先頭に lhs を挿入。 fn parse_pipeline(&mut self) -> Result { - let mut expr = self.parse_coalesce()?; + let mut expr = self.parse_ternary()?; while self.match_token(&TokenType::PipeForward) { if !is_sugar_enabled() { @@ -82,6 +82,28 @@ impl NyashParser { Ok(expr) } + /// 三項演算子: cond ? then : else + /// Grammar (Phase 12.7): TernaryExpr = NullsafeExpr ( "?" Expr ":" Expr )? + /// 実装: coalesce の上に差し込み、`cond ? a : b` を If式に変換する。 + fn parse_ternary(&mut self) -> Result { + let cond = self.parse_coalesce()?; + if self.match_token(&TokenType::QUESTION) { + // consume '?' and parse then/else expressions + self.advance(); + let then_expr = self.parse_expression()?; + self.consume(TokenType::COLON)?; // ':' + let else_expr = self.parse_expression()?; + // Lower to PeekExpr over boolean scrutinee: peek cond { true => then, else => else } + return Ok(ASTNode::PeekExpr { + scrutinee: Box::new(cond), + arms: vec![(crate::ast::LiteralValue::Bool(true), then_expr)], + else_expr: Box::new(else_expr), + span: Span::unknown(), + }); + } + Ok(cond) + } + /// デフォルト値(??): x ?? y => peek x { null => y, else => x } fn parse_coalesce(&mut self) -> Result { let mut expr = self.parse_or()?; @@ -591,7 +613,11 @@ impl NyashParser { expr = ASTNode::Call { callee: Box::new(expr), arguments, span: Span::unknown() }; } } else if self.match_token(&TokenType::QUESTION) { - // 後置 ?(Result伝播) + // 後置 ?(Result伝播)。ただし三項演算子 '?:' と衝突するため、次トークンが ':' の場合は消費しない。 + // 例: (cond) ? then : else ではここで '?' を処理せず、上位の parse_ternary に委ねる。 + if self.peek_token() == &TokenType::COLON { + break; + } self.advance(); expr = ASTNode::QMarkPropagate { expression: Box::new(expr), span: Span::unknown() }; } else { diff --git a/src/parser/statements.rs b/src/parser/statements.rs index f98cf134..dc1c3e08 100644 --- a/src/parser/statements.rs +++ b/src/parser/statements.rs @@ -86,8 +86,9 @@ impl NyashParser { self.parse_assignment_or_function_call() } _ => { - let line = self.current_token().line; - Err(ParseError::InvalidStatement { line }) + // Fallback: treat as expression statement + // Allows forms like: print("x") or a bare literal as the last value in a block + Ok(self.parse_expression()?) } }; @@ -246,13 +247,12 @@ impl NyashParser { /// return文をパース pub(super) fn parse_return(&mut self) -> Result { self.advance(); // consume 'return' - - // returnの後に式があるかチェック - let value = if self.is_at_end() || self.match_token(&TokenType::NEWLINE) { - // return単体の場合はvoidを返す + // 許容: 改行をスキップしてから式有無を判定 + self.skip_newlines(); + // returnの後に式があるかチェック(RBRACE/EOFなら値なし) + let value = if self.is_at_end() || self.match_token(&TokenType::RBRACE) { None } else { - // 式をパースして返す Some(Box::new(self.parse_expression()?)) }; diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 95dfde41..c1e871ca 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -206,6 +206,10 @@ impl NyashTokenizer { self.advance(); return Ok(Token::new(TokenType::QmarkQmark, start_line, start_column)); } + Some('?') => { + self.advance(); + return Ok(Token::new(TokenType::QUESTION, start_line, start_column)); + } Some('+') if self.peek_char() == Some('=') => { self.advance(); self.advance(); @@ -263,6 +267,10 @@ impl NyashTokenizer { self.advance(); Ok(Token::new(TokenType::DoubleColon, start_line, start_column)) } + Some(':') => { + self.advance(); + Ok(Token::new(TokenType::COLON, start_line, start_column)) + } Some('=') if self.peek_char() == Some('>') => { self.advance(); self.advance(); diff --git a/tools/ny_parser_mvp.py b/tools/ny_parser_mvp.py new file mode 100644 index 00000000..fcdb9ba8 --- /dev/null +++ b/tools/ny_parser_mvp.py @@ -0,0 +1,94 @@ +#!/usr/bin/env python3 +""" +Ny parser MVP (Stage 1): Ny -> JSON v0 + +Grammar (subset): + program := [return] expr EOF + expr := term (('+'|'-') term)* + term := factor (('*'|'/') factor)* + factor := INT | STRING | '(' expr ')' + +Outputs JSON v0 compatible with --ny-parser-pipe. +""" +import sys, re, json + +class Tok: + def __init__(self, kind, val, pos): + self.kind, self.val, self.pos = kind, val, pos + +def lex(s: str): + i=n=0; n=len(s); out=[] + while i", file=sys.stderr); sys.exit(1) + with open(sys.argv[1],'r',encoding='utf-8') as f: + src=f.read() + toks=lex(src) + prog=P(toks).program() + print(json.dumps(prog, ensure_ascii=False)) + +if __name__=='__main__': + main() diff --git a/tools/ny_parser_mvp_roundtrip.sh b/tools/ny_parser_mvp_roundtrip.sh new file mode 100644 index 00000000..1b2a17fd --- /dev/null +++ b/tools/ny_parser_mvp_roundtrip.sh @@ -0,0 +1,32 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +ROOT_DIR=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd) +BIN="$ROOT_DIR/target/release/nyash" + +if [[ ! -x "$BIN" ]]; then + echo "[build] nyash (release) ..." >&2 + cargo build --release >/dev/null +fi + +mkdir -p "$ROOT_DIR/tmp" +TMP_SRC="$ROOT_DIR/tmp/ny_parser_input.ny" +printf 'return 1+2*3\n' > "$TMP_SRC" + +# Run parser MVP (Python) to JSON v0, then pipe to bridge +JSON_OUT=$(python3 "$ROOT_DIR/tools/ny_parser_mvp.py" "$TMP_SRC") +if [[ -z "$JSON_OUT" ]]; then + echo "error: parser produced no JSON" >&2 + exit 1 +fi +echo "$JSON_OUT" | "$BIN" --ny-parser-pipe >/tmp/ny_parser_mvp_rt.out || true + +if rg -q '^Result:\s*7\b' /tmp/ny_parser_mvp_rt.out; then + echo "✅ Ny parser MVP roundtrip OK" >&2 + exit 0 +else + echo "❌ Ny parser MVP roundtrip FAILED" >&2 + cat /tmp/ny_parser_mvp_rt.out >&2 || true + exit 2 +fi