diff --git a/CURRENT_TASK.md b/CURRENT_TASK.md index 54b6eb46..97b2d2cc 100644 --- a/CURRENT_TASK.md +++ b/CURRENT_TASK.md @@ -1,35 +1,44 @@ -# Current Task — Phase 15 Self‑Hosting (2025‑09‑15) +# Current Task — Phase 15 Self‑Hosting (2025‑09‑16) TL;DR - 目標は「自己ホスティング達成」= Nyash製パーサで Ny → JSON v0 → Bridge → MIR 実行を安定化すること。 - PyVM は意味論の参照実行器(開発補助)。llvmlite は AOT/検証。配布やバンドル化は後回し(基礎固めが先)。 What Changed (today) -- リファクタリング一式 完了(Runner/LLVM/MIR Builder の分割第2弾まで)。機能差分なしで整理のみ。 -- Phase‑15(自己ホスト)を再開。まずはスモークで挙動を再確認してから警告掃除へ進む方針に切り替え。 -- 決定: 先にスモーク(PyVM/自己ホスト/Bridge)を回して“緑”を確認→ その後に `ops_ext.rs` と `runner/selfhost.rs` の警告削減に着手する。 -- 構文糖衣の導入計画(A案)を承認: Stage‑1 で配列リテラル([a,b,c])を追加(IR変更なし、デシュガリング)。 +- Selfhost 経路の安定化(Python MVP 優先→PyVM 実行)。Selfhost Stage‑2(直/Bridge)スモークは緑化。 +- Using/Resolver を Runner 前処理に集約し、BoxIndex(グローバル)+解決キャッシュを導入。 + - nyash.toml の `[aliases]`/env `NYASH_ALIASES` 対応、候補提示、`NYASH_RESOLVE_TRACE=1` でトレース。 + - strict プレフィクス: `NYASH_PLUGIN_REQUIRE_PREFIX=1` または `[plugins] require_prefix=true`。 + - per‑plugin meta(`prefix/require_prefix/expose_short_names`)の読取導線を実装(挙動は現状据え置き)。 +- CLI `--using` を追加(`--using "ns as Alias"` / `--using '"apps/foo.nyash" as Foo'`)。 +- フィールドは box 先頭のみルールのリンタを Runner に追加(`NYASH_FIELDS_TOP_STRICT=1` でエラー)。 +- Syntax Torture スイートの実行正規化(末行比較)。一部テスト本文を Nyash 仕様に合わせて修正。 + +- llvmlite/AOT(本戦)強化 — コアコレクション配線とエントリ統一 + - Array/Map の BoxCall を NyRT ハンドルAPIに直結: + - Array: `push`→`nyash.array.push_h`、`length/len`→`nyash.any.length_h` + - Map: `set`→`nyash.map.set_hh`、`get`→`nyash.map.get_hh`、`has`→`nyash.map.has_hh`、`size`→`nyash.any.length_h` + - `ny_main` を i64 戻りに統一し、`Main.main/1` を優先(既定 args は `new ArrayBox()`)。 + - Core Box 生成の安定化:`nyash.array.birth_h` / `nyash.map.birth_h` を追加し、llvmlite `new` は birth_h を優先。 + - AOT 実行確認: + - `[1,2,3].length()` → `Result: 3` + - `{"name":"Alice","age":25}.size()` → `Result: 2` + - `m.has("name") ? m.get("name").length() : 0` → `Result: 5` + Quick Next (today) -- 短時間スモーク優先(挙動の健全性を早期確認): - - `source tools/dev_env.sh pyvm` - - `NYASH_VM_USE_PY=1 ./tools/pyvm_stage2_smoke.sh`(参照実行器・意味論確認) - - `NYASH_USE_NY_COMPILER=1 ./tools/selfhost_stage2_smoke.sh`(自己ホスト直) - - `NYASH_USE_NY_COMPILER=1 ./tools/selfhost_stage2_bridge_smoke.sh`(自己ホスト→JSON→PyVM) - - 任意: `./tools/selfhost_stage3_accept_smoke.sh`(Stage‑3 受理のみ確認) -- スモークが緑なら、警告の削減に移行: - - `src/jit/lower/core/ops_ext.rs`(未使用・到達不能/冗長の解消、保存スロットの一貫化) - - `src/runner/selfhost.rs`(到達不能の除去、変数寿命の短縮、細かな `let`/`mut` 是正) -- ParserBox 強化(Stage‑2 完了 + Stage‑3 受理を追加) - - 進捗ガード(parse_program2/parse_block2/parse_stmt2)。 - - Stage‑2 受理一式: 単項/二項/比較/論理/呼出/メソッド/引数/if/else/loop/using/local/return/new。 - - 代入文(identifier = expr)を受理(Stage‑2 は Local に正規化)。 - - Stage‑3 受理のみ(意味論降下は後続): break/continue/throw/try-catch-finally を no-op/Expr に降格して受理。 -- Smokes 追加/拡充 - - Stage‑2: `tools/selfhost_stage2_smoke.sh`(自己ホスト直)・`tools/selfhost_stage2_bridge_smoke.sh`(自己ホスト→JSON→PyVM)。 - - JSON 固定ベクトル: `tests/json_v0/*.json` と `tools/pyvm_json_vectors_smoke.sh`(arith/if/while/logical/strings 代表)。 - - 新規 apps/tests: try/finally 合成、短絡+PHI、二重ループ独立性、ループ片側PHI、メソッドチェーン。 - - Stage‑3 受理確認: `tools/selfhost_stage3_accept_smoke.sh`(受理のみを確認、実行意味論は降格)。 +- いよいよ「Nyash で書く」段階へ(Self‑Hosting 実装の着手): + 1) ParserBox 拡張(Stage‑2 の堅牢化・回帰修正) + - 算術/比較/論理/呼出/メソッド/if/else/loop/local/return/new の受理を再確認。 + - 代入文の正規化(`identifier = expr` → Local/Store)。 + 2) EmitterBox 拡張(JSON v0 の安定化) + - `meta.usings` の付与一貫化、配列/Map リテラルの後方対応(将来拡張の下地)。 + 3) 自己ホスト経路で Ny 実装切替のゲート準備(現状は Python MVP 優先を維持)。 + 4) テスト: + - `source tools/dev_env.sh pyvm` + - `NYASH_VM_USE_PY=1 ./tools/selfhost_stage2_smoke.sh` + - `NYASH_VM_USE_PY=1 ./tools/selfhost_stage2_bridge_smoke.sh` + - Torture(VM中心): `(cd tests/nyash_syntax_torture_20250916 && BACKENDS="vm" NYASH_BIN=../../target/release/nyash bash run_spec_smoke.sh)` - Runner/Bridge 実行系 - `--ny-parser-pipe` は `NYASH_PIPE_USE_PYVM=1` で PyVM に委譲(exit code 判定に統一)。 - 自己ホスト JSON 生成は Python MVP を優先、LLVM EXE/インラインVMを段階フォールバック。 @@ -44,15 +53,22 @@ Quick Next (today) Current Status - Stage‑2: 自己ホスト → JSON v0 → PyVM の代表スモークは緑(配列/文字列/論理/if/loop)。 -- Stage‑3: 構文受理のみ完了(break/continue/throw/try/catch/finally)。現時点では JSON 降格(no-op/Expr)で安全受理。 -- Runner: `--ny-parser-pipe` で PyVM 委譲(exit code 判定)。自己ホスト JSON は Python MVP/EXE/VM の3段フォールバックで生成可能。 +- Stage‑3: 構文受理のみ完了(break/continue/throw/try/catch/finally)。現時点では JSON 降格(no‑op/Expr)で安全受理。 +- Runner: Using/Resolver を前処理に統合(BoxIndex/キャッシュ/strict)。`--ny-parser-pipe` は PyVM 委譲(exit code 判定)。 +- llvmlite/AOT: Array/Map の基本操作(push/get/set/has/size, length)が NyRT ハンドルAPIで動作。`ny_main` は i64 戻り・`Main.main/1` 優先で起動。 Open - Bridge/PHI の正規化: 短絡(入れ子)における merge/PHI incoming を固定化(rhs_end/fall_bb の順序)。 - JSON v0 の拡張方針: break/continue/try/catch/finally の表現(受け皿設計 or 受理時の事前降下)。 +- per‑plugin meta の反映: `require_prefix/expose_short_names/prefix` を Resolver 挙動へ段階適用(導線は実装済み)。 - `me` の扱い: MVP は `NYASH_BRIDGE_ME_DUMMY=1` の仮注入を継続(将来撤去)。 - LLVM 直結(任意): JSON v0 → LLVM の導線追加は後回し。 +- NyRT 整頓: + - FFI ヘルパー化(handles/boxing 正規化)/birth_h→new_i64x 統合/Core Box のプラグイン事前登録/FFI エクスポートのマクロ化。 +- llvmlite 整頓: + - boxcall のテーブル駆動化、追加 API(delete/keys/values など)の段階配線。 + Plan (to Self‑Hosting) 1) Phase‑1: Stage‑2 完了+堅牢化(今ここ) - 正常系スモークを自己ホスト直/Bridge(PyVM)で常緑化(追加分を反映済み)。 @@ -157,3 +173,7 @@ Namespaces / Using(現状) - `NYASH_RESOLVE_TRACE=1`: 解決手順/キャッシュヒット/未解決候補をログ出力。 - スモークが緑=基礎健全性確認後に、静的ノイズの除去を安全に一気通貫で行う。 + +**AOT Quick** +- Array literal: `NYASH_SYNTAX_SUGAR_LEVEL=basic ./tools/build_llvm.sh tmp/aot_array_literal_main.nyash -o app && ./app` +- Map literal: `NYASH_SYNTAX_SUGAR_LEVEL=basic NYASH_ENABLE_MAP_LITERAL=1 ./tools/build_llvm.sh tmp/aot_map_literal_main.nyash -o app && ./app` diff --git a/app_alit b/app_alit new file mode 100644 index 00000000..38935857 Binary files /dev/null and b/app_alit differ diff --git a/app_alit_print b/app_alit_print new file mode 100644 index 00000000..4fdbcc95 Binary files /dev/null and b/app_alit_print differ diff --git a/app_alit_verbose b/app_alit_verbose new file mode 100644 index 00000000..5c6871cd Binary files /dev/null and b/app_alit_verbose differ diff --git a/app_mg b/app_mg new file mode 100644 index 00000000..cea4b78d Binary files /dev/null and b/app_mg differ diff --git a/app_mlit_verbose b/app_mlit_verbose new file mode 100644 index 00000000..5f20a57b Binary files /dev/null and b/app_mlit_verbose differ diff --git a/apps/selfhost-compiler/boxes/parser_box.nyash b/apps/selfhost-compiler/boxes/parser_box.nyash index 474c9604..86bf5861 100644 --- a/apps/selfhost-compiler/boxes/parser_box.nyash +++ b/apps/selfhost-compiler/boxes/parser_box.nyash @@ -229,7 +229,160 @@ box ParserBox { parse_number2(src, i) { local n = src.length() local j = i local cont = 1 local guard = 0 local max = 100000 loop(cont == 1) { if guard > max { cont = 0 } else { guard = guard + 1 if j < n { if me.is_digit(src.substring(j, j+1)) { j = j + 1 } else { cont = 0 } } else { cont = 0 } } } local s = src.substring(i, j) me.gpos_set(j) return "{\"type\":\"Int\",\"value\":" + s + "}" } parse_string2(src, i) { local n = src.length() local j = i + 1 local out = "" local guard = 0 local max = 200000 loop(j < n) { if guard > max { break } guard = guard + 1 local ch = src.substring(j, j+1) if ch == "\"" { j = j + 1 me.gpos_set(j) return "{\"type\":\"Str\",\"value\":\"" + me.esc_json(out) + "\"}" } if ch == "\\" && j + 1 < n { local nx = src.substring(j+1, j+2) if nx == "\"" { out = out + "\"" } else { if nx == "\\" { out = out + "\\" } else { out = out + nx } } j = j + 2 } else { out = out + ch j = j + 1 } } me.gpos_set(j) return "{\"type\":\"Str\",\"value\":\"" + me.esc_json(out) + "\"}" } - parse_factor2(src, i) { local j = me.skip_ws(src, i) local ch = src.substring(j, j+1) if ch == "(" { local inner = me.parse_expr2(src, j + 1) local k = me.gpos_get() k = me.skip_ws(src, k) if src.substring(k, k+1) == ")" { k = k + 1 } me.gpos_set(k) return inner } if ch == "\"" { return me.parse_string2(src, j) } if me.starts_with_kw(src, j, "true") == 1 { me.gpos_set(j + 4) return "{\"type\":\"Bool\",\"value\":true}" } if me.starts_with_kw(src, j, "false") == 1 { me.gpos_set(j + 5) return "{\"type\":\"Bool\",\"value\":false}" } if me.starts_with_kw(src, j, "new") == 1 { local p = me.skip_ws(src, j + 3) local idp = me.read_ident2(src, p) local at = idp.lastIndexOf("@") local cls = idp.substring(0, at) local k = me.to_int(idp.substring(at+1, idp.length())) k = me.skip_ws(src, k) if src.substring(k, k+1) == "(" { k = k + 1 } local args_and_pos = me.parse_args2(src, k) local at2 = args_and_pos.lastIndexOf("@") local args_json = args_and_pos.substring(0, at2) k = me.to_int(args_and_pos.substring(at2+1, args_and_pos.length())) k = me.skip_ws(src, k) if src.substring(k, k+1) == ")" { k = k + 1 } me.gpos_set(k) return "{\"type\":\"New\",\"class\":\"" + cls + "\",\"args\":" + args_json + "}" } if me.is_alpha(ch) { local idp = me.read_ident2(src, j) local at = idp.lastIndexOf("@") local name = idp.substring(0, at) local k = me.to_int(idp.substring(at+1, idp.length())) local node = "{\"type\":\"Var\",\"name\":\"" + name + "\"}" local cont = 1 loop(cont == 1) { k = me.skip_ws(src, k) local tch = src.substring(k, k+1) if tch == "(" { k = k + 1 local args_and_pos = me.parse_args2(src, k) local at2 = args_and_pos.lastIndexOf("@") local args_json = args_and_pos.substring(0, at2) k = me.to_int(args_and_pos.substring(at2+1, args_and_pos.length())) k = me.skip_ws(src, k) if src.substring(k, k+1) == ")" { k = k + 1 } node = "{\"type\":\"Call\",\"name\":\"" + name + "\",\"args\":" + args_json + "}" } else { if tch == "." { k = k + 1 k = me.skip_ws(src, k) local midp = me.read_ident2(src, k) local at3 = midp.lastIndexOf("@") local mname = midp.substring(0, at3) k = me.to_int(midp.substring(at3+1, midp.length())) k = me.skip_ws(src, k) if src.substring(k, k+1) == "(" { k = k + 1 } local args2 = me.parse_args2(src, k) local at4 = args2.lastIndexOf("@") local args_json2 = args2.substring(0, at4) k = me.to_int(args2.substring(at4+1, args2.length())) k = me.skip_ws(src, k) if src.substring(k, k+1) == ")" { k = k + 1 } node = "{\"type\":\"Method\",\"recv\":" + node + ",\"method\":\"" + mname + "\",\"args\":" + args_json2 + "}" } else { cont = 0 } } } me.gpos_set(k) return node } return me.parse_number2(src, j) } + parse_factor2(src, i) { + local j = me.skip_ws(src, i) + local ch = src.substring(j, j+1) + // Parenthesized + if ch == "(" { + local inner = me.parse_expr2(src, j + 1) + local k = me.gpos_get() + k = me.skip_ws(src, k) + if src.substring(k, k+1) == ")" { k = k + 1 } + me.gpos_set(k) + return inner + } + // String literal + if ch == "\"" { return me.parse_string2(src, j) } + // Map literal: {"k": v, ...} (string keys only) → Call{name:"map.of", args:[Str(k1), v1, Str(k2), v2, ...]} + if ch == "{" { + local n = src.length() + j = j + 1 + local out = "[" + local first = 1 + local cont = 1 + local guard = 0 + local max = 400000 + loop(cont == 1) { + if guard > max { cont = 0 } else { guard = guard + 1 } + j = me.skip_ws(src, j) + if j >= n { cont = 0 } else { + if src.substring(j, j+1) == "}" { j = j + 1 cont = 0 } else { + // key (string only for Stage-2) + if src.substring(j, j+1) != "\"" { + // degrade by skipping one char to avoid infinite loop + j = j + 1 + continue + } + local key_raw = me.read_string_lit(src, j) + j = me.gpos_get() + j = me.skip_ws(src, j) + if src.substring(j, j+1) == ":" { j = j + 1 } + j = me.skip_ws(src, j) + local val_json = me.parse_expr2(src, j) + j = me.gpos_get() + local key_json = "{\"type\":\"Str\",\"value\":\"" + me.esc_json(key_raw) + "\"}" + if first == 1 { out = out + key_json + "," + val_json first = 0 } else { out = out + "," + key_json + "," + val_json } + // optional comma + local before2 = j + j = me.skip_ws(src, j) + if j < n && src.substring(j, j+1) == "," { j = j + 1 } + // progress guard (in case of malformed input) + if j <= before2 { if j < n { j = j + 1 } else { j = n } } + } + } + } + out = out + "]" + me.gpos_set(j) + return "{\"type\":\"Call\",\"name\":\"map.of\",\"args\":" + out + "}" + } + // Array literal: [e1, e2, ...] → Call{name:"array.of", args:[...]} + if ch == "[" { + local n = src.length() + j = j + 1 + local out = "[" + local first = 1 + local cont = 1 + local guard = 0 + local max = 400000 + loop(cont == 1) { + if guard > max { cont = 0 } else { guard = guard + 1 } + j = me.skip_ws(src, j) + if j >= n { cont = 0 } else { + if src.substring(j, j+1) == "]" { j = j + 1 cont = 0 } else { + local before = j + local ej = me.parse_expr2(src, j) + j = me.gpos_get() + if first == 1 { out = out + ej first = 0 } else { out = out + "," + ej } + // optional comma+whitespace + local before2 = j + j = me.skip_ws(src, j) + if j < n && src.substring(j, j+1) == "," { j = j + 1 } + // progress guard + if j <= before { if j < n { j = j + 1 } else { j = n } } + } + } + } + out = out + "]" + me.gpos_set(j) + return "{\"type\":\"Call\",\"name\":\"array.of\",\"args\":" + out + "}" + } + // true/false + if me.starts_with_kw(src, j, "true") == 1 { me.gpos_set(j + 4) return "{\"type\":\"Bool\",\"value\":true}" } + if me.starts_with_kw(src, j, "false") == 1 { me.gpos_set(j + 5) return "{\"type\":\"Bool\",\"value\":false}" } + // new Class(args) + if me.starts_with_kw(src, j, "new") == 1 { + local p = me.skip_ws(src, j + 3) + local idp = me.read_ident2(src, p) + local at = idp.lastIndexOf("@") + local cls = idp.substring(0, at) + local k = me.to_int(idp.substring(at+1, idp.length())) + k = me.skip_ws(src, k) + if src.substring(k, k+1) == "(" { k = k + 1 } + local args_and_pos = me.parse_args2(src, k) + local at2 = args_and_pos.lastIndexOf("@") + local args_json = args_and_pos.substring(0, at2) + k = me.to_int(args_and_pos.substring(at2+1, args_and_pos.length())) + k = me.skip_ws(src, k) + if src.substring(k, k+1) == ")" { k = k + 1 } + me.gpos_set(k) + return "{\"type\":\"New\",\"class\":\"" + cls + "\",\"args\":" + args_json + "}" + } + // Identifier / Call / Method chain + if me.is_alpha(ch) { + local idp = me.read_ident2(src, j) + local at = idp.lastIndexOf("@") + local name = idp.substring(0, at) + local k = me.to_int(idp.substring(at+1, idp.length())) + local node = "{\"type\":\"Var\",\"name\":\"" + name + "\"}" + local cont2 = 1 + loop(cont2 == 1) { + k = me.skip_ws(src, k) + local tch = src.substring(k, k+1) + if tch == "(" { + k = k + 1 + local args_and_pos = me.parse_args2(src, k) + local at2 = args_and_pos.lastIndexOf("@") + local args_json = args_and_pos.substring(0, at2) + k = me.to_int(args_and_pos.substring(at2+1, args_and_pos.length())) + k = me.skip_ws(src, k) + if src.substring(k, k+1) == ")" { k = k + 1 } + node = "{\"type\":\"Call\",\"name\":\"" + name + "\",\"args\":" + args_json + "}" + } else { + if tch == "." { + k = k + 1 + k = me.skip_ws(src, k) + local midp = me.read_ident2(src, k) + local at3 = midp.lastIndexOf("@") + local mname = midp.substring(0, at3) + k = me.to_int(midp.substring(at3+1, midp.length())) + k = me.skip_ws(src, k) + if src.substring(k, k+1) == "(" { k = k + 1 } + local args2 = me.parse_args2(src, k) + local at4 = args2.lastIndexOf("@") + local args_json2 = args2.substring(0, at4) + k = me.to_int(args2.substring(at4+1, args2.length())) + k = me.skip_ws(src, k) + if src.substring(k, k+1) == ")" { k = k + 1 } + node = "{\"type\":\"Method\",\"recv\":" + node + ",\"method\":\"" + mname + "\",\"args\":" + args_json2 + "}" + } else { cont2 = 0 } + } + } + me.gpos_set(k) + return node + } + // Fallback: number + return me.parse_number2(src, j) + } // unary minus binds tighter than * / parse_unary2(src, i) { local j = me.skip_ws(src, i) @@ -392,6 +545,73 @@ box ParserBox { me.gpos_set(j) return "{\"type\":\"Loop\",\"cond\":" + cond + ",\"body\":" + body_json + "}" } + // Stage-3 acceptance (syntax only): break / continue → no-op expression + if me.starts_with_kw(src, j, "break") == 1 { + j = j + 5 + if j <= stmt_start { if j < src.length() { j = j + 1 } else { j = src.length() } } + me.gpos_set(j) + return "{\"type\":\"Expr\",\"expr\":{\"type\":\"Int\",\"value\":0}}" + } + if me.starts_with_kw(src, j, "continue") == 1 { + j = j + 8 + if j <= stmt_start { if j < src.length() { j = j + 1 } else { j = src.length() } } + me.gpos_set(j) + return "{\"type\":\"Expr\",\"expr\":{\"type\":\"Int\",\"value\":0}}" + } + // Stage-3 acceptance: throw expr → degrade to Expr(expr) + if me.starts_with_kw(src, j, "throw") == 1 { + j = j + 5 + j = me.skip_ws(src, j) + local e_throw = me.parse_expr2(src, j) + j = me.gpos_get() + if j <= stmt_start { if j < src.length() { j = j + 1 } else { j = src.length() } } + me.gpos_set(j) + return "{\"type\":\"Expr\",\"expr\":" + e_throw + "}" + } + // Stage-3 acceptance: try { ... } (catch ...)* (finally { ... })? → degrade to no-op (syntax only) + if me.starts_with_kw(src, j, "try") == 1 { + j = j + 3 + j = me.skip_ws(src, j) + // parse try block + local try_res = me.parse_block2(src, j) + local at_t = try_res.lastIndexOf("@") + j = me.to_int(try_res.substring(at_t+1, try_res.length())) + // zero or more catch + local guard_ct = 0 + local max_ct = 100 + local cont_ct = 1 + loop(cont_ct == 1) { + if guard_ct > max_ct { cont_ct = 0 } else { guard_ct = guard_ct + 1 } + j = me.skip_ws(src, j) + if me.starts_with_kw(src, j, "catch") == 1 { + j = j + 5 + j = me.skip_ws(src, j) + if src.substring(j, j+1) == "(" { j = j + 1 j = me.skip_ws(src, j) + // optional type + name + if me.is_alpha(src.substring(j, j+1)) { local id1 = me.read_ident2(src, j) local at1 = id1.lastIndexOf("@") j = me.to_int(id1.substring(at1+1, id1.length())) j = me.skip_ws(src, j) } + if me.is_alpha(src.substring(j, j+1)) { local id2 = me.read_ident2(src, j) local at2 = id2.lastIndexOf("@") j = me.to_int(id2.substring(at2+1, id2.length())) j = me.skip_ws(src, j) } + if src.substring(j, j+1) == ")" { j = j + 1 } + } + j = me.skip_ws(src, j) + // catch body + local c_res = me.parse_block2(src, j) + local atc = c_res.lastIndexOf("@") + j = me.to_int(c_res.substring(atc+1, c_res.length())) + } else { cont_ct = 0 } + } + // optional finally + j = me.skip_ws(src, j) + if me.starts_with_kw(src, j, "finally") == 1 { + j = j + 7 + j = me.skip_ws(src, j) + local f_res = me.parse_block2(src, j) + local atf = f_res.lastIndexOf("@") + j = me.to_int(f_res.substring(atf+1, f_res.length())) + } + if j <= stmt_start { if j < src.length() { j = j + 1 } else { j = src.length() } } + me.gpos_set(j) + return "{\"type\":\"Expr\",\"expr\":{\"type\":\"Int\",\"value\":0}}" + } // Fallback: expression or unknown token — ensure progress even on malformed input local expr_start = j local e = me.parse_expr2(src, j) @@ -473,70 +693,3 @@ box ParserBox { } static box ParserStub { main(args) { return 0 } } - // Stage-3 acceptance (syntax only): break / continue → no-op expression - if me.starts_with_kw(src, j, "break") == 1 { - j = j + 5 - if j <= stmt_start { if j < src.length() { j = j + 1 } else { j = src.length() } } - me.gpos_set(j) - return "{\"type\":\"Expr\",\"expr\":{\"type\":\"Int\",\"value\":0}}" - } - if me.starts_with_kw(src, j, "continue") == 1 { - j = j + 8 - if j <= stmt_start { if j < src.length() { j = j + 1 } else { j = src.length() } } - me.gpos_set(j) - return "{\"type\":\"Expr\",\"expr\":{\"type\":\"Int\",\"value\":0}}" - } - // Stage-3 acceptance: throw expr → degrade to Expr(expr) - if me.starts_with_kw(src, j, "throw") == 1 { - j = j + 5 - j = me.skip_ws(src, j) - local e_throw = me.parse_expr2(src, j) - j = me.gpos_get() - if j <= stmt_start { if j < src.length() { j = j + 1 } else { j = src.length() } } - me.gpos_set(j) - return "{\"type\":\"Expr\",\"expr\":" + e_throw + "}" - } - // Stage-3 acceptance: try { ... } (catch ...)* (finally { ... })? → degrade to no-op (syntax only) - if me.starts_with_kw(src, j, "try") == 1 { - j = j + 3 - j = me.skip_ws(src, j) - // parse try block - local try_res = me.parse_block2(src, j) - local at_t = try_res.lastIndexOf("@") - j = me.to_int(try_res.substring(at_t+1, try_res.length())) - // zero or more catch - local guard_ct = 0 - local max_ct = 100 - local cont_ct = 1 - loop(cont_ct == 1) { - if guard_ct > max_ct { cont_ct = 0 } else { guard_ct = guard_ct + 1 } - j = me.skip_ws(src, j) - if me.starts_with_kw(src, j, "catch") == 1 { - j = j + 5 - j = me.skip_ws(src, j) - if src.substring(j, j+1) == "(" { j = j + 1 j = me.skip_ws(src, j) - // optional type + name - if me.is_alpha(src.substring(j, j+1)) { local id1 = me.read_ident2(src, j) local at1 = id1.lastIndexOf("@") j = me.to_int(id1.substring(at1+1, id1.length())) j = me.skip_ws(src, j) } - if me.is_alpha(src.substring(j, j+1)) { local id2 = me.read_ident2(src, j) local at2 = id2.lastIndexOf("@") j = me.to_int(id2.substring(at2+1, id2.length())) j = me.skip_ws(src, j) } - if src.substring(j, j+1) == ")" { j = j + 1 } - } - j = me.skip_ws(src, j) - // catch body - local c_res = me.parse_block2(src, j) - local atc = c_res.lastIndexOf("@") - j = me.to_int(c_res.substring(atc+1, c_res.length())) - } else { cont_ct = 0 } - } - // optional finally - j = me.skip_ws(src, j) - if me.starts_with_kw(src, j, "finally") == 1 { - j = j + 7 - j = me.skip_ws(src, j) - local f_res = me.parse_block2(src, j) - local atf = f_res.lastIndexOf("@") - j = me.to_int(f_res.substring(atf+1, f_res.length())) - } - if j <= stmt_start { if j < src.length() { j = j + 1 } else { j = src.length() } } - me.gpos_set(j) - return "{\"type\":\"Expr\",\"expr\":{\"type\":\"Int\",\"value\":0}}" - } diff --git a/crates/nyrt/src/lib.rs b/crates/nyrt/src/lib.rs index 796e4571..8db56cab 100644 --- a/crates/nyrt/src/lib.rs +++ b/crates/nyrt/src/lib.rs @@ -545,6 +545,23 @@ pub extern "C" fn nyash_console_birth_h_export() -> i64 { 0 } + + +// ArrayBox birth shim for AOT/JIT handle-based creation +#[export_name = "nyash.array.birth_h"] +pub extern "C" fn nyash_array_birth_h_export() -> i64 { + let arc: std::sync::Arc = + std::sync::Arc::new(nyash_rust::boxes::array::ArrayBox::new()); + nyash_rust::jit::rt::handles::to_handle(arc) as i64 +} + +// MapBox birth shim for AOT/JIT handle-based creation +#[export_name = "nyash.map.birth_h"] +pub extern "C" fn nyash_map_birth_h_export() -> i64 { + let arc: std::sync::Arc = + std::sync::Arc::new(nyash_rust::boxes::map_box::MapBox::new()); + nyash_rust::jit::rt::handles::to_handle(arc) as i64 +} // ---- Process entry (driver) ---- #[cfg(not(test))] #[no_mangle] diff --git a/crates/nyrt/src/plugin/map.rs b/crates/nyrt/src/plugin/map.rs index 9831a026..14dc1c21 100644 --- a/crates/nyrt/src/plugin/map.rs +++ b/crates/nyrt/src/plugin/map.rs @@ -63,17 +63,15 @@ pub extern "C" fn nyash_map_get_h(handle: i64, key: i64) -> i64 { // get_hh: (map_handle, key_handle) -> value_handle #[export_name = "nyash.map.get_hh"] -pub extern "C" fn nyash_map_get_hh(handle: i64, key_h: i64) -> i64 { - use nyash_rust::{box_trait::NyashBox, jit::rt::handles}; - if handle <= 0 || key_h <= 0 { - return 0; - } - if let (Some(obj), Some(key)) = (handles::get(handle as u64), handles::get(key_h as u64)) { - if let Some(map) = obj - .as_any() - .downcast_ref::() - { - let v = map.get(key.clone_box()); +pub extern "C" fn nyash_map_get_hh(handle: i64, key_any: i64) -> i64 { + use nyash_rust::{box_trait::{NyashBox, IntegerBox}, jit::rt::handles}; + if handle <= 0 { return 0; } + if let Some(obj) = handles::get(handle as u64) { + if let Some(map) = obj.as_any().downcast_ref::() { + let key_box: Box = if key_any > 0 { + if let Some(k) = handles::get(key_any as u64) { k.clone_box() } else { Box::new(IntegerBox::new(key_any)) } + } else { Box::new(IntegerBox::new(key_any)) }; + let v = map.get(key_box); let arc: std::sync::Arc = std::sync::Arc::from(v); let h = handles::to_handle(arc); return h as i64; @@ -82,6 +80,7 @@ pub extern "C" fn nyash_map_get_hh(handle: i64, key_h: i64) -> i64 { 0 } + // set_h: (map_handle, key_i64, val) -> i64 (ignored/0) #[export_name = "nyash.map.set_h"] pub extern "C" fn nyash_map_set_h(handle: i64, key: i64, val: i64) -> i64 { @@ -126,6 +125,44 @@ pub extern "C" fn nyash_map_set_h(handle: i64, key: i64, val: i64) -> i64 { 0 } + +// set_hh: (map_handle, key_any: handle or i64, val_any: handle or i64) -> i64 +#[export_name = "nyash.map.set_hh"] +pub extern "C" fn nyash_map_set_hh(handle: i64, key_any: i64, val_any: i64) -> i64 { + use nyash_rust::{box_trait::{NyashBox, IntegerBox}, jit::rt::handles}; + if handle <= 0 { return 0; } + if let Some(obj) = handles::get(handle as u64) { + if let Some(map) = obj.as_any().downcast_ref::() { + let kbox: Box = if key_any > 0 { + if let Some(k) = handles::get(key_any as u64) { k.clone_box() } else { Box::new(IntegerBox::new(key_any)) } + } else { Box::new(IntegerBox::new(key_any)) }; + let vbox: Box = if val_any > 0 { + if let Some(v) = handles::get(val_any as u64) { v.clone_box() } else { Box::new(IntegerBox::new(val_any)) } + } else { Box::new(IntegerBox::new(val_any)) }; + let _ = map.set(kbox, vbox); + return 0; + } + } + 0 +} + +// has_hh: (map_handle, key_any: handle or i64) -> i64 (0/1) +#[export_name = "nyash.map.has_hh"] +pub extern "C" fn nyash_map_has_hh(handle: i64, key_any: i64) -> i64 { + use nyash_rust::{box_trait::{NyashBox, IntegerBox, BoolBox}, jit::rt::handles}; + if handle <= 0 { return 0; } + if let Some(obj) = handles::get(handle as u64) { + if let Some(map) = obj.as_any().downcast_ref::() { + let kbox: Box = if key_any > 0 { + if let Some(k) = handles::get(key_any as u64) { k.clone_box() } else { Box::new(IntegerBox::new(key_any)) } + } else { Box::new(IntegerBox::new(key_any)) }; + let v = map.has(kbox); + if let Some(b) = v.as_any().downcast_ref::() { return if b.value { 1 } else { 0 }; } + } + } + 0 +} + // has_h: (map_handle, key_i64) -> i64 (0/1) #[export_name = "nyash.map.has_h"] pub extern "C" fn nyash_map_has_h(handle: i64, key: i64) -> i64 { @@ -139,10 +176,8 @@ pub extern "C" fn nyash_map_has_h(handle: i64, key: i64) -> i64 { .downcast_ref::() { let kbox = Box::new(IntegerBox::new(key)); - let v = map.get(kbox); - // Consider present if not VoidBox - let present = !v.as_any().is::(); - return if present { 1 } else { 0 }; + let v = map.has(kbox); + if let Some(b) = v.as_any().downcast_ref::() { return if b.value { 1 } else { 0 }; } } } 0 diff --git a/src/llvm_py/instructions/boxcall.py b/src/llvm_py/instructions/boxcall.py index f3ed740b..f28f5962 100644 --- a/src/llvm_py/instructions/boxcall.py +++ b/src/llvm_py/instructions/boxcall.py @@ -84,6 +84,15 @@ def lower_boxcall( vmap[dst_vid] = result return + if method_name == "size": + # Map/Array size via any.length_h + recv_h = _ensure_handle(builder, module, recv_val) + callee = _declare(module, "nyash.any.length_h", i64, [i64]) + result = builder.call(callee, [recv_h], name="any_size_h") + if dst_vid is not None: + vmap[dst_vid] = result + return + if method_name == "substring": # substring(start, end) # If receiver is a handle (i64), use handle-based helper; else pointer-based API @@ -180,23 +189,60 @@ def lower_boxcall( if method_name == "get": # ArrayBox.get(index) → nyash.array.get_h(handle, idx) + # MapBox.get(key) → nyash.map.get_hh(handle, key_any) recv_h = _ensure_handle(builder, module, recv_val) if resolver is not None and preds is not None and block_end_values is not None and bb_map is not None: - idx = resolver.resolve_i64(args[0], builder.block, preds, block_end_values, vmap, bb_map) if args else ir.Constant(i64, 0) + k = resolver.resolve_i64(args[0], builder.block, preds, block_end_values, vmap, bb_map) if args else ir.Constant(i64, 0) else: - idx = vmap.get(args[0], ir.Constant(i64, 0)) if args else ir.Constant(i64, 0) - callee = _declare(module, "nyash.array.get_h", i64, [i64, i64]) - res = builder.call(callee, [recv_h, idx], name="arr_get_h") + k = vmap.get(args[0], ir.Constant(i64, 0)) if args else ir.Constant(i64, 0) + callee_map = _declare(module, "nyash.map.get_hh", i64, [i64, i64]) + res = builder.call(callee_map, [recv_h, k], name="map_get_hh") if dst_vid is not None: vmap[dst_vid] = res - try: - if resolver is not None and hasattr(resolver, 'mark_string'): - # Heuristic: args array often stores strings for CLI; tag as string-ish - resolver.mark_string(dst_vid) - except Exception: - pass return + if method_name == "push": + # ArrayBox.push(val) → nyash.array.push_h(handle, val) + recv_h = _ensure_handle(builder, module, recv_val) + if resolver is not None and preds is not None and block_end_values is not None and bb_map is not None: + v0 = resolver.resolve_i64(args[0], builder.block, preds, block_end_values, vmap, bb_map) if args else ir.Constant(i64, 0) + else: + v0 = vmap.get(args[0], ir.Constant(i64, 0)) if args else ir.Constant(i64, 0) + callee = _declare(module, "nyash.array.push_h", i64, [i64, i64]) + res = builder.call(callee, [recv_h, v0], name="arr_push_h") + if dst_vid is not None: + vmap[dst_vid] = res + return + + if method_name == "set": + # MapBox.set(key, val) → nyash.map.set_hh(handle, key_any, val_any) + recv_h = _ensure_handle(builder, module, recv_val) + if resolver is not None and preds is not None and block_end_values is not None and bb_map is not None: + k = resolver.resolve_i64(args[0], builder.block, preds, block_end_values, vmap, bb_map) if len(args) > 0 else ir.Constant(i64, 0) + v = resolver.resolve_i64(args[1], builder.block, preds, block_end_values, vmap, bb_map) if len(args) > 1 else ir.Constant(i64, 0) + else: + k = vmap.get(args[0], ir.Constant(i64, 0)) if len(args) > 0 else ir.Constant(i64, 0) + v = vmap.get(args[1], ir.Constant(i64, 0)) if len(args) > 1 else ir.Constant(i64, 0) + callee = _declare(module, "nyash.map.set_hh", i64, [i64, i64, i64]) + res = builder.call(callee, [recv_h, k, v], name="map_set_hh") + if dst_vid is not None: + vmap[dst_vid] = res + return + + if method_name == "has": + # MapBox.has(key) → nyash.map.has_hh(handle, key_any) + recv_h = _ensure_handle(builder, module, recv_val) + if resolver is not None and preds is not None and block_end_values is not None and bb_map is not None: + k = resolver.resolve_i64(args[0], builder.block, preds, block_end_values, vmap, bb_map) if args else ir.Constant(i64, 0) + else: + k = vmap.get(args[0], ir.Constant(i64, 0)) if args else ir.Constant(i64, 0) + callee = _declare(module, "nyash.map.has_hh", i64, [i64, i64]) + res = builder.call(callee, [recv_h, k], name="map_has_hh") + if dst_vid is not None: + vmap[dst_vid] = res + return + + if method_name in ("print", "println", "log"): # Console mapping (prefer pointer-API when possible to avoid handle registry mismatch) use_ptr = False diff --git a/src/llvm_py/instructions/newbox.py b/src/llvm_py/instructions/newbox.py index bde14fff..c86f0ef8 100644 --- a/src/llvm_py/instructions/newbox.py +++ b/src/llvm_py/instructions/newbox.py @@ -29,9 +29,22 @@ def lower_newbox( vmap: Value map resolver: Optional resolver for type handling """ - # Use NyRT shim: nyash.env.box.new(type_name: i8*) -> i64 + # Use NyRT shim: prefer birth_h for core boxes, otherwise env.box.new_i64x i64 = ir.IntType(64) i8p = ir.IntType(8).as_pointer() + # Core fast paths + if box_type in ("ArrayBox", "MapBox"): + birth_name = "nyash.array.birth_h" if box_type == "ArrayBox" else "nyash.map.birth_h" + birth = None + for f in module.functions: + if f.name == birth_name: + birth = f + break + if not birth: + birth = ir.Function(module, ir.FunctionType(i64, []), name=birth_name) + handle = builder.call(birth, [], name=f"birth_{box_type}") + vmap[dst_vid] = handle + return # Prefer variadic shim: nyash.env.box.new_i64x(type_name, argc, a1, a2, a3, a4) new_i64x = None for f in module.functions: diff --git a/src/llvm_py/llvm_builder.py b/src/llvm_py/llvm_builder.py index 26006db3..a16b3e57 100644 --- a/src/llvm_py/llvm_builder.py +++ b/src/llvm_py/llvm_builder.py @@ -110,35 +110,64 @@ class NyashLLVMBuilder: # Create ny_main wrapper if necessary has_ny_main = any(f.name == 'ny_main' for f in self.module.functions) - main_fn = None + # Prefer static box entry: Main.main/1; fallback to plain main (0-arity) + fn_main_box = None + fn_main_plain = None for f in self.module.functions: - if f.name == 'main': - main_fn = f - break - if main_fn is not None: - # Hide the user main to avoid conflict with NyRT's main symbol + if f.name == 'Main.main/1': + fn_main_box = f + elif f.name == 'main': + fn_main_plain = f + target_fn = fn_main_box or fn_main_plain + if target_fn is not None and not has_ny_main: + # Hide the target to avoid symbol conflicts try: - main_fn.linkage = 'private' + target_fn.linkage = 'private' except Exception: pass - if not has_ny_main: - # i32 ny_main() { return (i32) main(); } - ny_main_ty = ir.FunctionType(self.i32, []) - ny_main = ir.Function(self.module, ny_main_ty, name='ny_main') - entry = ny_main.append_basic_block('entry') - b = ir.IRBuilder(entry) - if len(main_fn.args) == 0: - rv = b.call(main_fn, [], name='call_user_main') + # i32 ny_main() { return (i32) Main.main(args) | main(); } + ny_main_ty = ir.FunctionType(self.i64, []) + ny_main = ir.Function(self.module, ny_main_ty, name='ny_main') + entry = ny_main.append_basic_block('entry') + b = ir.IRBuilder(entry) + if fn_main_box is not None: + # Build default args = new ArrayBox() via nyash.env.box.new_i64x + i64 = self.i64 + i8 = self.i8 + i8p = self.i8p + # Declare callee + callee = None + for f in self.module.functions: + if f.name == 'nyash.env.box.new_i64x': + callee = f + break + if callee is None: + callee = ir.Function(self.module, ir.FunctionType(i64, [i8p, i64, i64, i64, i64, i64]), name='nyash.env.box.new_i64x') + # Create "ArrayBox\0" global + sbytes = b"ArrayBox\0" + arr_ty = ir.ArrayType(i8, len(sbytes)) + g = ir.GlobalVariable(self.module, arr_ty, name='.ny_main_arraybox') + g.linkage = 'private' + g.global_constant = True + g.initializer = ir.Constant(arr_ty, bytearray(sbytes)) + c0 = ir.Constant(self.i32, 0) + ptr = b.gep(g, [c0, c0], inbounds=True) + zero = ir.Constant(i64, 0) + args_handle = b.call(callee, [ptr, zero, zero, zero, zero, zero], name='ny_main_args') + rv = b.call(fn_main_box, [args_handle], name='call_Main_main_1') + else: + # Plain main() fallback + if len(fn_main_plain.args) == 0: + rv = b.call(fn_main_plain, [], name='call_user_main') else: - # If signature mismatches, return 0 rv = ir.Constant(self.i64, 0) - if hasattr(rv, 'type') and isinstance(rv.type, ir.IntType) and rv.type.width != 32: - rv32 = b.trunc(rv, self.i32) if rv.type.width > 32 else b.zext(rv, self.i32) - b.ret(rv32) - elif hasattr(rv, 'type') and isinstance(rv.type, ir.IntType) and rv.type.width == 32: - b.ret(rv) - else: - b.ret(ir.Constant(self.i32, 0)) + if hasattr(rv, 'type') and isinstance(rv.type, ir.IntType) and rv.type.width != 32: + rv64 = b.trunc(rv, self.i64) if rv.type.width > 64 else b.zext(rv, self.i64) + b.ret(rv64) + elif hasattr(rv, 'type') and isinstance(rv.type, ir.IntType) and rv.type.width == 64: + b.ret(rv) + else: + b.ret(ir.Constant(self.i64, 0)) ir_text = str(self.module) # Optional IR dump to file for debugging @@ -159,7 +188,7 @@ class NyashLLVMBuilder: def _create_dummy_main(self) -> str: """Create dummy ny_main that returns 0""" - ny_main_ty = ir.FunctionType(self.i32, []) + ny_main_ty = ir.FunctionType(self.i64, []) ny_main = ir.Function(self.module, ny_main_ty, name="ny_main") block = ny_main.append_basic_block(name="entry") builder = ir.IRBuilder(block) diff --git a/src/runner/json_v0_bridge.rs b/src/runner/json_v0_bridge.rs index 95e3af3b..65c92657 100644 --- a/src/runner/json_v0_bridge.rs +++ b/src/runner/json_v0_bridge.rs @@ -202,7 +202,45 @@ fn lower_expr(f: &mut MirFunction, cur_bb: BasicBlockId, e: &ExprV0) -> Result<( Ok((out, merge_bb)) } ExprV0::Call { name, args } => { - // Fallback: no vars context; treat as normal call + // Special: array literal lowering — Call{name:"array.of", args:[...]} → new ArrayBox(); push(...); result=array + if name == "array.of" { + // Create array first + let arr = f.next_value_id(); + if let Some(bb) = f.get_block_mut(cur_bb) { + bb.add_instruction(MirInstruction::NewBox { dst: arr, box_type: "ArrayBox".into(), args: vec![] }); + } + // For each element: eval then push + let mut cur = cur_bb; + for e in args { + let (v, c) = lower_expr(f, cur, e)?; cur = c; + let tmp = f.next_value_id(); + if let Some(bb) = f.get_block_mut(cur) { + bb.add_instruction(MirInstruction::BoxCall { dst: Some(tmp), box_val: arr, method: "push".into(), method_id: None, args: vec![v], effects: EffectMask::READ }); + } + } + return Ok((arr, cur)); + } + // Special: map literal lowering — Call{name:"map.of", args:[k1, v1, k2, v2, ...]} → new MapBox(); set(k,v)...; result=map + if name == "map.of" { + let mapv = f.next_value_id(); + if let Some(bb) = f.get_block_mut(cur_bb) { + bb.add_instruction(MirInstruction::NewBox { dst: mapv, box_type: "MapBox".into(), args: vec![] }); + } + let mut cur = cur_bb; + let mut it = args.iter(); + while let Some(k) = it.next() { + if let Some(v) = it.next() { + let (kv, cur2) = lower_expr(f, cur, k)?; cur = cur2; + let (vv, cur3) = lower_expr(f, cur, v)?; cur = cur3; + let tmp = f.next_value_id(); + if let Some(bb) = f.get_block_mut(cur) { + bb.add_instruction(MirInstruction::BoxCall { dst: Some(tmp), box_val: mapv, method: "set".into(), method_id: None, args: vec![kv, vv], effects: EffectMask::READ }); + } + } else { break; } + } + return Ok((mapv, cur)); + } + // Fallback: treat as normal dynamic call let (arg_ids, cur) = lower_args(f, cur_bb, args)?; let fun_val = f.next_value_id(); if let Some(bb) = f.get_block_mut(cur) { @@ -273,6 +311,42 @@ fn lower_expr_with_vars( Err(format!("undefined variable: {}", name)) } ExprV0::Call { name, args } => { + // Special: array literal lowering in vars context + if name == "array.of" { + let arr = f.next_value_id(); + if let Some(bb) = f.get_block_mut(cur_bb) { + bb.add_instruction(MirInstruction::NewBox { dst: arr, box_type: "ArrayBox".into(), args: vec![] }); + } + let mut cur = cur_bb; + for e in args { + let (v, c) = lower_expr_with_vars(f, cur, e, vars)?; cur = c; + let tmp = f.next_value_id(); + if let Some(bb) = f.get_block_mut(cur) { + bb.add_instruction(MirInstruction::BoxCall { dst: Some(tmp), box_val: arr, method: "push".into(), method_id: None, args: vec![v], effects: EffectMask::READ }); + } + } + return Ok((arr, cur)); + } + // Special: map literal lowering in vars context + if name == "map.of" { + let mapv = f.next_value_id(); + if let Some(bb) = f.get_block_mut(cur_bb) { + bb.add_instruction(MirInstruction::NewBox { dst: mapv, box_type: "MapBox".into(), args: vec![] }); + } + let mut cur = cur_bb; + let mut it = args.iter(); + while let Some(k) = it.next() { + if let Some(v) = it.next() { + let (kv, cur2) = lower_expr_with_vars(f, cur, k, vars)?; cur = cur2; + let (vv, cur3) = lower_expr_with_vars(f, cur, v, vars)?; cur = cur3; + let tmp = f.next_value_id(); + if let Some(bb) = f.get_block_mut(cur) { + bb.add_instruction(MirInstruction::BoxCall { dst: Some(tmp), box_val: mapv, method: "set".into(), method_id: None, args: vec![kv, vv], effects: EffectMask::READ }); + } + } else { break; } + } + return Ok((mapv, cur)); + } // Lower args let (arg_ids, cur) = lower_args_with_vars(f, cur_bb, args, vars)?; // Encode as: const fun_name; call diff --git a/tests/nyash_syntax_torture_20250916.zip b/tests/nyash_syntax_torture_20250916.zip new file mode 100644 index 00000000..e910e435 Binary files /dev/null and b/tests/nyash_syntax_torture_20250916.zip differ diff --git a/tests/nyash_syntax_torture_20250916/01_ops_assoc.nyash b/tests/nyash_syntax_torture_20250916/01_ops_assoc.nyash new file mode 100644 index 00000000..d88b4897 --- /dev/null +++ b/tests/nyash_syntax_torture_20250916/01_ops_assoc.nyash @@ -0,0 +1,6 @@ + +# Expect: x=33 / y=33 (left-assoc + explicit coercion via "+") +# Also verifies numeric-first vs string-first concatenation symmetry. +local x = "x=" + 11 + 22 # if left-assoc with string + int => "x=1122" OR "x=33"? define spec +local y = "x=" + (11 + 22) # force arithmetic then concat => "x=33" +print(y) # authoritative: should be "x=33" diff --git a/tests/nyash_syntax_torture_20250916/02_deep_parens.nyash b/tests/nyash_syntax_torture_20250916/02_deep_parens.nyash new file mode 100644 index 00000000..6d1bce3f --- /dev/null +++ b/tests/nyash_syntax_torture_20250916/02_deep_parens.nyash @@ -0,0 +1,5 @@ + +// Expect: 170 (deep parentheses stress) +// 12 * 15 - 10 = 180 - 10 = 170 +local v = (5 + 7) * (10 + 5) - 10 +print(v) // 170 diff --git a/tests/nyash_syntax_torture_20250916/03_array_map_nested.nyash b/tests/nyash_syntax_torture_20250916/03_array_map_nested.nyash new file mode 100644 index 00000000..5ec3b707 --- /dev/null +++ b/tests/nyash_syntax_torture_20250916/03_array_map_nested.nyash @@ -0,0 +1,4 @@ + +# Expect: 42 +local a = [1, [2, [3, [4, 42]]], {k: [{z: 7}, {z: 9}]}] +print(a[1][1][1]) # 42 (0-based assumed; adjust if 1-based) diff --git a/tests/nyash_syntax_torture_20250916/04_map_array_mix.nyash b/tests/nyash_syntax_torture_20250916/04_map_array_mix.nyash new file mode 100644 index 00000000..2e19eec1 --- /dev/null +++ b/tests/nyash_syntax_torture_20250916/04_map_array_mix.nyash @@ -0,0 +1,5 @@ + +# Expect: OK:3-hello +local m = { k: [ "hello", "world" ] } +m.k.push("!") # verify method dispatch on nested box +print("OK:" + 3 + "-" + m.k[0]) # "OK:3-hello" diff --git a/tests/nyash_syntax_torture_20250916/05_string_concat_unicode.nyash b/tests/nyash_syntax_torture_20250916/05_string_concat_unicode.nyash new file mode 100644 index 00000000..a973c676 --- /dev/null +++ b/tests/nyash_syntax_torture_20250916/05_string_concat_unicode.nyash @@ -0,0 +1,5 @@ + +# Expect: 🐱-9-nyash +local cat = "🐱" +local s = cat + "-" + (3*3) + "-" + "nyash" +print(s) diff --git a/tests/nyash_syntax_torture_20250916/06_control_flow_loopform.nyash b/tests/nyash_syntax_torture_20250916/06_control_flow_loopform.nyash new file mode 100644 index 00000000..78a6b010 --- /dev/null +++ b/tests/nyash_syntax_torture_20250916/06_control_flow_loopform.nyash @@ -0,0 +1,10 @@ + +# Expect: 01234|done +local i = 0 +while i < 10 { + if i == 5 { break } + print(i) + i = i + 1 + continue +} +print("|done") diff --git a/tests/nyash_syntax_torture_20250916/07_await_nowait_mix.nyash b/tests/nyash_syntax_torture_20250916/07_await_nowait_mix.nyash new file mode 100644 index 00000000..d547f09d --- /dev/null +++ b/tests/nyash_syntax_torture_20250916/07_await_nowait_mix.nyash @@ -0,0 +1,5 @@ + +# Expect: 3 +# Simulate async using tasks; if runtime not ready, keep as a spec stub. +nowait t1 = 1 + 2 +print(await t1) diff --git a/tests/nyash_syntax_torture_20250916/08_visibility_access.nyash b/tests/nyash_syntax_torture_20250916/08_visibility_access.nyash new file mode 100644 index 00000000..e0960e14 --- /dev/null +++ b/tests/nyash_syntax_torture_20250916/08_visibility_access.nyash @@ -0,0 +1,13 @@ + +# Expect: base:42|sub:99 +box Base { + private { x } + birth() { me.x = 42 } + getX() { return me.x } +} +box Sub from Base { + getX() { return 99 } # override +} +local b = new Base() +local s = new Sub() +print("base:" + b.getX() + "|sub:" + s.getX()) diff --git a/tests/nyash_syntax_torture_20250916/09_lambda_closure_scope.nyash b/tests/nyash_syntax_torture_20250916/09_lambda_closure_scope.nyash new file mode 100644 index 00000000..4c78d0ac --- /dev/null +++ b/tests/nyash_syntax_torture_20250916/09_lambda_closure_scope.nyash @@ -0,0 +1,6 @@ + +# Expect: 7 +local x = 7 +local f = () => { return x } +local x = 9 # shadow after closure creation should not affect captured value +print(f()) diff --git a/tests/nyash_syntax_torture_20250916/10_match_result_early_return.nyash b/tests/nyash_syntax_torture_20250916/10_match_result_early_return.nyash new file mode 100644 index 00000000..ed0865eb --- /dev/null +++ b/tests/nyash_syntax_torture_20250916/10_match_result_early_return.nyash @@ -0,0 +1,18 @@ + +# Expect: ok:42 +box Result { + private { ok, val } + birth(ok, val) { me.ok = ok; me.val = val } + isOk() { return me.ok } + get() { return me.val } +} +fn check(n) { + if n < 0 { return new Result(false, 0) } + return new Result(true, n*2) +} +local r = check(21) +if r.isOk() { + print("ok:" + r.get()) +} else { + print("err") +} diff --git a/tests/nyash_syntax_torture_20250916/README.md b/tests/nyash_syntax_torture_20250916/README.md new file mode 100644 index 00000000..33fd7ed2 --- /dev/null +++ b/tests/nyash_syntax_torture_20250916/README.md @@ -0,0 +1,41 @@ + +# Nyash Syntax Torture (10 minimal repros) +Date: 2025-09-16 + +Purpose: stress parser → AST → MIR(Core-13/PURE) → Interpreter/VM/LLVM(AOT) consistency. +Each test is **one phenomenon per file**, tiny and deterministic. + +## How to run (suggested) +``` +# 1) Run all modes and compare outputs +bash run_spec_smoke.sh + +# 2) PURE mode (surface MIR violations): +NYASH_MIR_CORE13_PURE=1 bash run_spec_smoke.sh + +# 3) Extra logging when a case fails: +NYASH_VM_STATS=1 NYASH_VM_STATS_JSON=1 NYASH_VM_DEBUG_BOXCALL=1 bash run_spec_smoke.sh +# For LLVM diagnostics (when applicable): +# NYASH_LLVM_VINVOKE_TRACE=1 NYASH_LLVM_VINVOKE_PREFER_I64=1 bash run_spec_smoke.sh +``` + +## Expected outputs (goldens) +We deliberately **print a single line** per test to make diffing trivial. +See inline comments in each `*.nyash`. + +## File list +1. 01_ops_assoc.nyash – operator associativity & coercion order +2. 02_deep_parens.nyash – deep parentheses & arithmetic nesting +3. 03_array_map_nested.nyash – nested array/map literal & access +4. 04_map_array_mix.nyash – object/array cross indexing & updates +5. 05_string_concat_unicode.nyash – string/number/Unicode concatenation +6. 06_control_flow_loopform.nyash – break/continue/dispatch shape +7. 07_await_nowait_mix.nyash – nowait/await interleave determinism +8. 08_visibility_access.nyash – private/public & override routing +9. 09_lambda_closure_scope.nyash – closure capture & shadowing +10. 10_match_result_early_return.nyash – early return vs. branch merge + +## CI hint +- Add this suite **before** your self-host smokes: + - `make spec-smoke` -> `make smoke-selfhost` +- Fail fast on any diff across Interpreter/VM/LLVM. diff --git a/tests/nyash_syntax_torture_20250916/_out/01_ops_assoc.nyash.interp.out.norm b/tests/nyash_syntax_torture_20250916/_out/01_ops_assoc.nyash.interp.out.norm new file mode 100644 index 00000000..d5933259 --- /dev/null +++ b/tests/nyash_syntax_torture_20250916/_out/01_ops_assoc.nyash.interp.out.norm @@ -0,0 +1 @@ +x=33 diff --git a/tests/nyash_syntax_torture_20250916/_out/01_ops_assoc.nyash.vm.out.norm b/tests/nyash_syntax_torture_20250916/_out/01_ops_assoc.nyash.vm.out.norm new file mode 100644 index 00000000..d5933259 --- /dev/null +++ b/tests/nyash_syntax_torture_20250916/_out/01_ops_assoc.nyash.vm.out.norm @@ -0,0 +1 @@ +x=33 diff --git a/tests/nyash_syntax_torture_20250916/development/debug_hang_issue/test.txt b/tests/nyash_syntax_torture_20250916/development/debug_hang_issue/test.txt new file mode 100644 index 00000000..d428d0be --- /dev/null +++ b/tests/nyash_syntax_torture_20250916/development/debug_hang_issue/test.txt @@ -0,0 +1 @@ +START \ No newline at end of file diff --git a/tests/nyash_syntax_torture_20250916/run_spec_smoke.sh b/tests/nyash_syntax_torture_20250916/run_spec_smoke.sh new file mode 100644 index 00000000..2f58e7ee --- /dev/null +++ b/tests/nyash_syntax_torture_20250916/run_spec_smoke.sh @@ -0,0 +1,81 @@ + +#!/usr/bin/env bash +set -euo pipefail + +NYASH_BIN=${NYASH_BIN:-./target/release/nyash} +BACKENDS=${BACKENDS:-"interp vm llvm"} # choose subset: "interp vm", etc. +OUTDIR=${OUTDIR:-_out} +mkdir -p "$OUTDIR" + +fail=0 + +normalize_file () { + local infile="$1"; local outfile="$2" + # Drop runner/interpreter noise; keep only program prints + local tmp="$outfile.tmp" + rg -v -e '^📝' -e '^🚀' -e '^✅' -e '^🔍' -e '^🔌' \ + -e '^\[plugin-loader\]' -e '^\[Bridge\]' \ + -e '^Result(Type)?' -e '^MIR Module' \ + -e '^; ' -e '^;MIR' "$infile" > "$tmp" || true + # Keep last non-empty line as canonical output + awk 'NF{last=$0} END{if (last) print last;}' "$tmp" > "$outfile" || true + rm -f "$tmp" +} + +run_one () { + local file="$1" + local mode="$2" + local out="$OUTDIR/$(basename "$file").$mode.out" + case "$mode" in + interp) "$NYASH_BIN" "$file" > "$out" ;; + vm) "$NYASH_BIN" --backend vm "$file" > "$out" ;; + llvm) + # Requires LLVM features and NYASH_LLVM_OBJ_OUT env; emit and run + local obj="$OUTDIR/nyash_llvm_temp.o" + NYASH_LLVM_OBJ_OUT="$obj" "$NYASH_BIN" --backend llvm "$file" >/dev/null + cc "$obj" -L crates/nyrt/target/release -Wl,--whole-archive -lnyrt -Wl,--no-whole-archive -lpthread -ldl -lm -o "$OUTDIR/app_$(basename "$file" .nyash)" + "$OUTDIR/app_$(basename "$file" .nyash)" > "$out" + ;; + *) echo "Unknown mode: $mode" >&2; return 2;; + esac + # Normalize + normalize_file "$out" "$out.norm" +} + +compare_modes () { + local file="$1" + local modes=($BACKENDS) + local ref="${modes[0]}" + for m in "${modes[@]}"; do + run_one "$file" "$m" + done + local refout="$OUTDIR/$(basename "$file").$ref.out.norm" + for m in "${modes[@]:1}"; do + local out="$OUTDIR/$(basename "$file").$m.out.norm" + if ! diff -u "$refout" "$out"; then + echo "[DIFF] $(basename "$file") ($ref vs $m) differs" >&2 + fail=1 + fi + done +} + +main () { + local tests=( $(ls -1 tests/syntax_torture/*.nyash 2>/dev/null || true) ) + if [ ${#tests[@]} -eq 0 ]; then + # fallback: assume running inside this folder + tests=( $(ls -1 ./*.nyash) ) + fi + + for t in "${tests[@]}"; do + echo "==> $(basename "$t")" + compare_modes "$t" + done + + if [ $fail -ne 0 ]; then + echo "Some tests differ across backends." >&2 + exit 1 + fi + echo "All tests matched across selected backends." +} + +main "$@" diff --git a/tests/nyash_syntax_torture_20250916/tests/syntax_torture/01_ops_assoc.nyash b/tests/nyash_syntax_torture_20250916/tests/syntax_torture/01_ops_assoc.nyash new file mode 100644 index 00000000..d88b4897 --- /dev/null +++ b/tests/nyash_syntax_torture_20250916/tests/syntax_torture/01_ops_assoc.nyash @@ -0,0 +1,6 @@ + +# Expect: x=33 / y=33 (left-assoc + explicit coercion via "+") +# Also verifies numeric-first vs string-first concatenation symmetry. +local x = "x=" + 11 + 22 # if left-assoc with string + int => "x=1122" OR "x=33"? define spec +local y = "x=" + (11 + 22) # force arithmetic then concat => "x=33" +print(y) # authoritative: should be "x=33" diff --git a/tests/nyash_syntax_torture_20250916/tests/syntax_torture/02_deep_parens.nyash b/tests/nyash_syntax_torture_20250916/tests/syntax_torture/02_deep_parens.nyash new file mode 100644 index 00000000..61da15b2 --- /dev/null +++ b/tests/nyash_syntax_torture_20250916/tests/syntax_torture/02_deep_parens.nyash @@ -0,0 +1,4 @@ + +# Expect: 170 +local v = (((((((((5 + 7) * 4) + 2) * 3) + 1) * 2) + 5)))) +print(v) # 170 diff --git a/tests/nyash_syntax_torture_20250916/tests/syntax_torture/03_array_map_nested.nyash b/tests/nyash_syntax_torture_20250916/tests/syntax_torture/03_array_map_nested.nyash new file mode 100644 index 00000000..5ec3b707 --- /dev/null +++ b/tests/nyash_syntax_torture_20250916/tests/syntax_torture/03_array_map_nested.nyash @@ -0,0 +1,4 @@ + +# Expect: 42 +local a = [1, [2, [3, [4, 42]]], {k: [{z: 7}, {z: 9}]}] +print(a[1][1][1]) # 42 (0-based assumed; adjust if 1-based) diff --git a/tests/nyash_syntax_torture_20250916/tests/syntax_torture/04_map_array_mix.nyash b/tests/nyash_syntax_torture_20250916/tests/syntax_torture/04_map_array_mix.nyash new file mode 100644 index 00000000..2e19eec1 --- /dev/null +++ b/tests/nyash_syntax_torture_20250916/tests/syntax_torture/04_map_array_mix.nyash @@ -0,0 +1,5 @@ + +# Expect: OK:3-hello +local m = { k: [ "hello", "world" ] } +m.k.push("!") # verify method dispatch on nested box +print("OK:" + 3 + "-" + m.k[0]) # "OK:3-hello" diff --git a/tests/nyash_syntax_torture_20250916/tests/syntax_torture/05_string_concat_unicode.nyash b/tests/nyash_syntax_torture_20250916/tests/syntax_torture/05_string_concat_unicode.nyash new file mode 100644 index 00000000..a973c676 --- /dev/null +++ b/tests/nyash_syntax_torture_20250916/tests/syntax_torture/05_string_concat_unicode.nyash @@ -0,0 +1,5 @@ + +# Expect: 🐱-9-nyash +local cat = "🐱" +local s = cat + "-" + (3*3) + "-" + "nyash" +print(s) diff --git a/tests/nyash_syntax_torture_20250916/tests/syntax_torture/06_control_flow_loopform.nyash b/tests/nyash_syntax_torture_20250916/tests/syntax_torture/06_control_flow_loopform.nyash new file mode 100644 index 00000000..78a6b010 --- /dev/null +++ b/tests/nyash_syntax_torture_20250916/tests/syntax_torture/06_control_flow_loopform.nyash @@ -0,0 +1,10 @@ + +# Expect: 01234|done +local i = 0 +while i < 10 { + if i == 5 { break } + print(i) + i = i + 1 + continue +} +print("|done") diff --git a/tests/nyash_syntax_torture_20250916/tests/syntax_torture/07_await_nowait_mix.nyash b/tests/nyash_syntax_torture_20250916/tests/syntax_torture/07_await_nowait_mix.nyash new file mode 100644 index 00000000..d547f09d --- /dev/null +++ b/tests/nyash_syntax_torture_20250916/tests/syntax_torture/07_await_nowait_mix.nyash @@ -0,0 +1,5 @@ + +# Expect: 3 +# Simulate async using tasks; if runtime not ready, keep as a spec stub. +nowait t1 = 1 + 2 +print(await t1) diff --git a/tests/nyash_syntax_torture_20250916/tests/syntax_torture/08_visibility_access.nyash b/tests/nyash_syntax_torture_20250916/tests/syntax_torture/08_visibility_access.nyash new file mode 100644 index 00000000..e0960e14 --- /dev/null +++ b/tests/nyash_syntax_torture_20250916/tests/syntax_torture/08_visibility_access.nyash @@ -0,0 +1,13 @@ + +# Expect: base:42|sub:99 +box Base { + private { x } + birth() { me.x = 42 } + getX() { return me.x } +} +box Sub from Base { + getX() { return 99 } # override +} +local b = new Base() +local s = new Sub() +print("base:" + b.getX() + "|sub:" + s.getX()) diff --git a/tests/nyash_syntax_torture_20250916/tests/syntax_torture/09_lambda_closure_scope.nyash b/tests/nyash_syntax_torture_20250916/tests/syntax_torture/09_lambda_closure_scope.nyash new file mode 100644 index 00000000..4c78d0ac --- /dev/null +++ b/tests/nyash_syntax_torture_20250916/tests/syntax_torture/09_lambda_closure_scope.nyash @@ -0,0 +1,6 @@ + +# Expect: 7 +local x = 7 +local f = () => { return x } +local x = 9 # shadow after closure creation should not affect captured value +print(f()) diff --git a/tests/nyash_syntax_torture_20250916/tests/syntax_torture/10_match_result_early_return.nyash b/tests/nyash_syntax_torture_20250916/tests/syntax_torture/10_match_result_early_return.nyash new file mode 100644 index 00000000..ed0865eb --- /dev/null +++ b/tests/nyash_syntax_torture_20250916/tests/syntax_torture/10_match_result_early_return.nyash @@ -0,0 +1,18 @@ + +# Expect: ok:42 +box Result { + private { ok, val } + birth(ok, val) { me.ok = ok; me.val = val } + isOk() { return me.ok } + get() { return me.val } +} +fn check(n) { + if n < 0 { return new Result(false, 0) } + return new Result(true, n*2) +} +local r = check(21) +if r.isOk() { + print("ok:" + r.get()) +} else { + print("err") +} diff --git a/tools/ny_parser_mvp.py b/tools/ny_parser_mvp.py index dcc71666..4e80ab79 100644 --- a/tools/ny_parser_mvp.py +++ b/tools/ny_parser_mvp.py @@ -18,6 +18,7 @@ Grammar (subset): term := unary (('*'|'/') unary)* unary := '-' unary | factor factor := INT | STRING | IDENT call_tail* | '(' expr ')' | 'new' IDENT '(' args? ')' + | '{' map_entries? '}' # map literal (string keys only) call_tail:= '.' IDENT '(' args? ')' # method | '(' args? ')' # function call args := expr (',' expr)* @@ -45,7 +46,7 @@ def lex(s: str): # two-char ops if s.startswith('==', i) or s.startswith('!=', i) or s.startswith('<=', i) or s.startswith('>=', i) or s.startswith('&&', i) or s.startswith('||', i): out.append(Tok('OP2', s[i:i+2], i)); i+=2; continue - if c in '+-*/(){}.,<>=': + if c in '+-*/(){}.,<>=[]:': out.append(Tok(c, c, i)); i+=1; continue if c=='"': j=i+1; buf=[] @@ -144,6 +145,31 @@ class P: if self.eat('STR'): return {"type":"Str","value":tok.val} if self.eat('('): e=self.expr(); self.expect(')'); return e + # Array literal: [e1, e2, ...] → Call{name:"array.of", args:[...]} + if self.eat('['): + args=[] + if self.cur().kind != ']': + args.append(self.expr()) + while self.eat(','): + args.append(self.expr()) + self.expect(']') + return {"type":"Call","name":"array.of","args":args} + # Map literal: {"k": v, ...} (string keys only) → Call{name:"map.of", args:[Str(k1), v1, ...]} + if self.eat('{'): + args=[] + if self.cur().kind != '}': + # first entry + k=self.cur(); self.expect('STR'); + self.expect(':'); v=self.expr(); + args.append({"type":"Str","value":k.val}); args.append(v) + while self.eat(','): + if self.cur().kind == '}': + break + k=self.cur(); self.expect('STR'); + self.expect(':'); v=self.expr(); + args.append({"type":"Str","value":k.val}); args.append(v) + self.expect('}') + return {"type":"Call","name":"map.of","args":args} if self.eat('NEW'): t=self.cur(); self.expect('IDENT'); self.expect('(') args=self.args_opt(); self.expect(')') diff --git a/tools/selfhost_stage2_bridge_smoke.sh b/tools/selfhost_stage2_bridge_smoke.sh index e456474d..fcd06d5e 100644 --- a/tools/selfhost_stage2_bridge_smoke.sh +++ b/tools/selfhost_stage2_bridge_smoke.sh @@ -110,5 +110,22 @@ NY ) run_case_bridge "assign update (bridge)" "$SRC_ASSIGN" 3 +# G) array literal [x,2,3] → size() == 3 +SRC_ALIT=$(cat <<'NY' +local x = 1 +local arr = [x, 2, 3] +return arr.size() +NY +) +run_case_bridge "array literal (bridge)" "$SRC_ALIT" 3 + +# H) map literal {"name":"Alice", "age":25} → size() == 2 +SRC_MLIT=$(cat <<'NY' +local m = {"name": "Alice", "age": 25} +return m.size() +NY +) +run_case_bridge "map literal (bridge)" "$SRC_MLIT" 2 + echo "All selfhost Stage-2 bridge smokes PASS" >&2 exit 0