🔍 Research: GPT-5-Codex capabilities and GitHub PR integration

## Summary
Investigated OpenAI's new GPT-5-Codex model and Codex GitHub PR review integration capabilities.

## GPT-5-Codex Analysis

### Benchmark Performance (Good)
- SWE-bench Verified: 74.5% (vs GPT-5's 72.8%)
- Refactoring tasks: 51.3% (vs GPT-5's 33.9%)
- Code review: Higher developer ratings

### Real-World Issues (Concerning)
- Users report degraded coding performance
- Scripts that previously worked now fail
- Less consistent than GPT-4.5
- Longer response times (minutes vs instant)
- "Creatively and emotionally flat"
- Basic errors (e.g., counting letters incorrectly)

### Key Finding
Classic case of "optimizing for benchmarks vs real usability" - scores well on tests but performs poorly in practice.

## Codex GitHub PR Integration

### Setup Process
1. Enable MFA and connect GitHub account
2. Authorize Codex GitHub app for repos
3. Enable "Code review" in repository settings

### Usage Methods
- **Manual**: Comment '@codex review' in PR
- **Automatic**: Triggers when PR moves from draft to ready

### Current Limitations
- One-way communication (doesn't respond to review comments)
- Prefers creating new PRs over updating existing ones
- Better for single-pass reviews than iterative feedback

## 'codex resume' Feature
New session management capability:
- Resume previous codex exec sessions
- Useful for continuing long tasks across days
- Maintains context from interrupted work

🐱 The investigation reveals that while GPT-5-Codex shows benchmark improvements, practical developer experience has declined - a reminder that metrics don't always reflect real-world utility\!
This commit is contained in:
Selfhosting Dev
2025-09-16 16:28:25 +09:00
parent 47f4ca0e44
commit 63c8fda808
41 changed files with 854 additions and 146 deletions

View File

@ -1,35 +1,44 @@
# Current Task — Phase 15 SelfHosting (20250915) # Current Task — Phase 15 SelfHosting (20250916)
TL;DR TL;DR
- 目標は「自己ホスティング達成」= Nyash製パーサで Ny → JSON v0 → Bridge → MIR 実行を安定化すること。 - 目標は「自己ホスティング達成」= Nyash製パーサで Ny → JSON v0 → Bridge → MIR 実行を安定化すること。
- PyVM は意味論の参照実行器開発補助。llvmlite は AOT/検証。配布やバンドル化は後回し(基礎固めが先)。 - PyVM は意味論の参照実行器開発補助。llvmlite は AOT/検証。配布やバンドル化は後回し(基礎固めが先)。
What Changed (today) What Changed (today)
- リファクタリング一式 完了Runner/LLVM/MIR Builder の分割第2弾まで。機能差分なしで整理のみ - Selfhost 経路の安定化Python MVP 優先→PyVM 実行。Selfhost Stage2直/Bridgeスモークは緑化
- Phase15自己ホストを再開。まずはスモークで挙動を再確認してから警告掃除へ進む方針に切り替え - Using/Resolver を Runner 前処理に集約し、BoxIndexグローバル解決キャッシュを導入
- 決定: 先にスモークPyVM/自己ホスト/Bridgeを回して“緑”を確認→ その後に `ops_ext.rs``runner/selfhost.rs` の警告削減に着手する - nyash.toml の `[aliases]`/env `NYASH_ALIASES` 対応、候補提示、`NYASH_RESOLVE_TRACE=1` でトレース
- 構文糖衣の導入計画A案を承認: Stage1 で配列リテラル([a,b,c]を追加IR変更なし、デシュガリング - strict プレフィクス: `NYASH_PLUGIN_REQUIRE_PREFIX=1` または `[plugins] require_prefix=true`
- perplugin 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) Quick Next (today)
- 短時間スモーク優先(挙動の健全性を早期確認 - いよいよ「Nyash で書く」段階へSelfHosting 実装の着手
- `source tools/dev_env.sh pyvm` 1) ParserBox 拡張Stage2 の堅牢化・回帰修正)
- `NYASH_VM_USE_PY=1 ./tools/pyvm_stage2_smoke.sh`(参照実行器・意味論確認 - 算術/比較/論理/呼出/メソッド/if/else/loop/local/return/new の受理を再確認
- `NYASH_USE_NY_COMPILER=1 ./tools/selfhost_stage2_smoke.sh`(自己ホスト直) - 代入文の正規化(`identifier = expr` → Local/Store
- `NYASH_USE_NY_COMPILER=1 ./tools/selfhost_stage2_bridge_smoke.sh`自己ホスト→JSON→PyVM 2) EmitterBox 拡張JSON v0 の安定化
- 任意: `./tools/selfhost_stage3_accept_smoke.sh`Stage3 受理のみ確認) - `meta.usings` の付与一貫化、配列/Map リテラルの後方対応(将来拡張の下地)。
- スモークが緑なら、警告の削減に移行: 3) 自己ホスト経路で Ny 実装切替のゲート準備(現状は Python MVP 優先を維持)。
- `src/jit/lower/core/ops_ext.rs`(未使用・到達不能/冗長の解消、保存スロットの一貫化) 4) テスト:
- `src/runner/selfhost.rs`(到達不能の除去、変数寿命の短縮、細かな `let`/`mut` 是正) - `source tools/dev_env.sh pyvm`
- ParserBox 強化Stage2 完了 + Stage3 受理を追加) - `NYASH_VM_USE_PY=1 ./tools/selfhost_stage2_smoke.sh`
- 進捗ガードparse_program2/parse_block2/parse_stmt2 - `NYASH_VM_USE_PY=1 ./tools/selfhost_stage2_bridge_smoke.sh`
- Stage2 受理一式: 単項/二項/比較/論理/呼出/メソッド/引数/if/else/loop/using/local/return/new。 - TortureVM中心: `(cd tests/nyash_syntax_torture_20250916 && BACKENDS="vm" NYASH_BIN=../../target/release/nyash bash run_spec_smoke.sh)`
- 代入文identifier = exprを受理Stage2 は Local に正規化)。
- Stage3 受理のみ(意味論降下は後続): break/continue/throw/try-catch-finally を no-op/Expr に降格して受理。
- Smokes 追加/拡充
- Stage2: `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、メソッドチェーン。
- Stage3 受理確認: `tools/selfhost_stage3_accept_smoke.sh`(受理のみを確認、実行意味論は降格)。
- Runner/Bridge 実行系 - Runner/Bridge 実行系
- `--ny-parser-pipe``NYASH_PIPE_USE_PYVM=1` で PyVM に委譲exit code 判定に統一)。 - `--ny-parser-pipe``NYASH_PIPE_USE_PYVM=1` で PyVM に委譲exit code 判定に統一)。
- 自己ホスト JSON 生成は Python MVP を優先、LLVM EXE/インラインVMを段階フォールバック。 - 自己ホスト JSON 生成は Python MVP を優先、LLVM EXE/インラインVMを段階フォールバック。
@ -44,15 +53,22 @@ Quick Next (today)
Current Status Current Status
- Stage2: 自己ホスト → JSON v0 → PyVM の代表スモークは緑(配列/文字列/論理/if/loop - Stage2: 自己ホスト → JSON v0 → PyVM の代表スモークは緑(配列/文字列/論理/if/loop
- Stage3: 構文受理のみ完了break/continue/throw/try/catch/finally。現時点では JSON 降格no-op/Exprで安全受理。 - Stage3: 構文受理のみ完了break/continue/throw/try/catch/finally。現時点では JSON 降格noop/Exprで安全受理。
- Runner: `--ny-parser-pipe` PyVM 委譲exit code 判定)。自己ホスト JSON は Python MVP/EXE/VM の3段フォールバックで生成可能。 - 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 Open
- Bridge/PHI の正規化: 短絡(入れ子)における merge/PHI incoming を固定化rhs_end/fall_bb の順序)。 - Bridge/PHI の正規化: 短絡(入れ子)における merge/PHI incoming を固定化rhs_end/fall_bb の順序)。
- JSON v0 の拡張方針: break/continue/try/catch/finally の表現(受け皿設計 or 受理時の事前降下)。 - JSON v0 の拡張方針: break/continue/try/catch/finally の表現(受け皿設計 or 受理時の事前降下)。
- perplugin meta の反映: `require_prefix/expose_short_names/prefix` を Resolver 挙動へ段階適用(導線は実装済み)。
- `me` の扱い: MVP は `NYASH_BRIDGE_ME_DUMMY=1` の仮注入を継続(将来撤去)。 - `me` の扱い: MVP は `NYASH_BRIDGE_ME_DUMMY=1` の仮注入を継続(将来撤去)。
- LLVM 直結(任意): JSON v0 → LLVM の導線追加は後回し。 - LLVM 直結(任意): JSON v0 → LLVM の導線追加は後回し。
- NyRT 整頓:
- FFI ヘルパー化handles/boxing 正規化birth_h→new_i64x 統合Core Box のプラグイン事前登録FFI エクスポートのマクロ化。
- llvmlite 整頓:
- boxcall のテーブル駆動化、追加 APIdelete/keys/values など)の段階配線。
Plan (to SelfHosting) Plan (to SelfHosting)
1) Phase1: Stage2 完了+堅牢化(今ここ) 1) Phase1: Stage2 完了+堅牢化(今ここ)
- 正常系スモークを自己ホスト直/BridgePyVMで常緑化追加分を反映済み - 正常系スモークを自己ホスト直/BridgePyVMで常緑化追加分を反映済み
@ -157,3 +173,7 @@ Namespaces / Using現状
- `NYASH_RESOLVE_TRACE=1`: 解決手順/キャッシュヒット/未解決候補をログ出力。 - `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`

BIN
app_alit Normal file

Binary file not shown.

BIN
app_alit_print Normal file

Binary file not shown.

BIN
app_alit_verbose Normal file

Binary file not shown.

BIN
app_mg Normal file

Binary file not shown.

BIN
app_mlit_verbose Normal file

Binary file not shown.

View File

@ -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_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_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 * / // unary minus binds tighter than * /
parse_unary2(src, i) { parse_unary2(src, i) {
local j = me.skip_ws(src, i) local j = me.skip_ws(src, i)
@ -392,6 +545,73 @@ box ParserBox {
me.gpos_set(j) me.gpos_set(j)
return "{\"type\":\"Loop\",\"cond\":" + cond + ",\"body\":" + body_json + "}" 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 // Fallback: expression or unknown token — ensure progress even on malformed input
local expr_start = j local expr_start = j
local e = me.parse_expr2(src, j) local e = me.parse_expr2(src, j)
@ -473,70 +693,3 @@ box ParserBox {
} }
static box ParserStub { main(args) { return 0 } } 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}}"
}

View File

@ -545,6 +545,23 @@ pub extern "C" fn nyash_console_birth_h_export() -> i64 {
0 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<dyn nyash_rust::box_trait::NyashBox> =
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<dyn nyash_rust::box_trait::NyashBox> =
std::sync::Arc::new(nyash_rust::boxes::map_box::MapBox::new());
nyash_rust::jit::rt::handles::to_handle(arc) as i64
}
// ---- Process entry (driver) ---- // ---- Process entry (driver) ----
#[cfg(not(test))] #[cfg(not(test))]
#[no_mangle] #[no_mangle]

View File

@ -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 // get_hh: (map_handle, key_handle) -> value_handle
#[export_name = "nyash.map.get_hh"] #[export_name = "nyash.map.get_hh"]
pub extern "C" fn nyash_map_get_hh(handle: i64, key_h: i64) -> i64 { pub extern "C" fn nyash_map_get_hh(handle: i64, key_any: i64) -> i64 {
use nyash_rust::{box_trait::NyashBox, jit::rt::handles}; use nyash_rust::{box_trait::{NyashBox, IntegerBox}, jit::rt::handles};
if handle <= 0 || key_h <= 0 { if handle <= 0 { return 0; }
return 0; if let Some(obj) = handles::get(handle as u64) {
} if let Some(map) = obj.as_any().downcast_ref::<nyash_rust::boxes::map_box::MapBox>() {
if let (Some(obj), Some(key)) = (handles::get(handle as u64), handles::get(key_h as u64)) { let key_box: Box<dyn NyashBox> = if key_any > 0 {
if let Some(map) = obj if let Some(k) = handles::get(key_any as u64) { k.clone_box() } else { Box::new(IntegerBox::new(key_any)) }
.as_any() } else { Box::new(IntegerBox::new(key_any)) };
.downcast_ref::<nyash_rust::boxes::map_box::MapBox>() let v = map.get(key_box);
{
let v = map.get(key.clone_box());
let arc: std::sync::Arc<dyn NyashBox> = std::sync::Arc::from(v); let arc: std::sync::Arc<dyn NyashBox> = std::sync::Arc::from(v);
let h = handles::to_handle(arc); let h = handles::to_handle(arc);
return h as i64; return h as i64;
@ -82,6 +80,7 @@ pub extern "C" fn nyash_map_get_hh(handle: i64, key_h: i64) -> i64 {
0 0
} }
// set_h: (map_handle, key_i64, val) -> i64 (ignored/0) // set_h: (map_handle, key_i64, val) -> i64 (ignored/0)
#[export_name = "nyash.map.set_h"] #[export_name = "nyash.map.set_h"]
pub extern "C" fn nyash_map_set_h(handle: i64, key: i64, val: i64) -> i64 { 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 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::<nyash_rust::boxes::map_box::MapBox>() {
let kbox: Box<dyn NyashBox> = 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<dyn NyashBox> = 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::<nyash_rust::boxes::map_box::MapBox>() {
let kbox: Box<dyn NyashBox> = 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::<BoolBox>() { return if b.value { 1 } else { 0 }; }
}
}
0
}
// has_h: (map_handle, key_i64) -> i64 (0/1) // has_h: (map_handle, key_i64) -> i64 (0/1)
#[export_name = "nyash.map.has_h"] #[export_name = "nyash.map.has_h"]
pub extern "C" fn nyash_map_has_h(handle: i64, key: i64) -> i64 { 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::<nyash_rust::boxes::map_box::MapBox>() .downcast_ref::<nyash_rust::boxes::map_box::MapBox>()
{ {
let kbox = Box::new(IntegerBox::new(key)); let kbox = Box::new(IntegerBox::new(key));
let v = map.get(kbox); let v = map.has(kbox);
// Consider present if not VoidBox if let Some(b) = v.as_any().downcast_ref::<nyash_rust::box_trait::BoolBox>() { return if b.value { 1 } else { 0 }; }
let present = !v.as_any().is::<nyash_rust::box_trait::VoidBox>();
return if present { 1 } else { 0 };
} }
} }
0 0

View File

@ -84,6 +84,15 @@ def lower_boxcall(
vmap[dst_vid] = result vmap[dst_vid] = result
return 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": if method_name == "substring":
# substring(start, end) # substring(start, end)
# If receiver is a handle (i64), use handle-based helper; else pointer-based API # 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": if method_name == "get":
# ArrayBox.get(index) → nyash.array.get_h(handle, idx) # 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) 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: 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: else:
idx = vmap.get(args[0], ir.Constant(i64, 0)) if args else ir.Constant(i64, 0) k = 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]) callee_map = _declare(module, "nyash.map.get_hh", i64, [i64, i64])
res = builder.call(callee, [recv_h, idx], name="arr_get_h") res = builder.call(callee_map, [recv_h, k], name="map_get_hh")
if dst_vid is not None: if dst_vid is not None:
vmap[dst_vid] = res 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 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"): if method_name in ("print", "println", "log"):
# Console mapping (prefer pointer-API when possible to avoid handle registry mismatch) # Console mapping (prefer pointer-API when possible to avoid handle registry mismatch)
use_ptr = False use_ptr = False

View File

@ -29,9 +29,22 @@ def lower_newbox(
vmap: Value map vmap: Value map
resolver: Optional resolver for type handling 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) i64 = ir.IntType(64)
i8p = ir.IntType(8).as_pointer() 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) # Prefer variadic shim: nyash.env.box.new_i64x(type_name, argc, a1, a2, a3, a4)
new_i64x = None new_i64x = None
for f in module.functions: for f in module.functions:

View File

@ -110,35 +110,64 @@ class NyashLLVMBuilder:
# Create ny_main wrapper if necessary # Create ny_main wrapper if necessary
has_ny_main = any(f.name == 'ny_main' for f in self.module.functions) 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: for f in self.module.functions:
if f.name == 'main': if f.name == 'Main.main/1':
main_fn = f fn_main_box = f
break elif f.name == 'main':
if main_fn is not None: fn_main_plain = f
# Hide the user main to avoid conflict with NyRT's main symbol 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: try:
main_fn.linkage = 'private' target_fn.linkage = 'private'
except Exception: except Exception:
pass pass
if not has_ny_main: # i32 ny_main() { return (i32) Main.main(args) | main(); }
# i32 ny_main() { return (i32) main(); } ny_main_ty = ir.FunctionType(self.i64, [])
ny_main_ty = ir.FunctionType(self.i32, []) ny_main = ir.Function(self.module, ny_main_ty, name='ny_main')
ny_main = ir.Function(self.module, ny_main_ty, name='ny_main') entry = ny_main.append_basic_block('entry')
entry = ny_main.append_basic_block('entry') b = ir.IRBuilder(entry)
b = ir.IRBuilder(entry) if fn_main_box is not None:
if len(main_fn.args) == 0: # Build default args = new ArrayBox() via nyash.env.box.new_i64x
rv = b.call(main_fn, [], name='call_user_main') 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: else:
# If signature mismatches, return 0
rv = ir.Constant(self.i64, 0) rv = ir.Constant(self.i64, 0)
if hasattr(rv, 'type') and isinstance(rv.type, ir.IntType) and rv.type.width != 32: 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) rv64 = b.trunc(rv, self.i64) if rv.type.width > 64 else b.zext(rv, self.i64)
b.ret(rv32) b.ret(rv64)
elif hasattr(rv, 'type') and isinstance(rv.type, ir.IntType) and rv.type.width == 32: elif hasattr(rv, 'type') and isinstance(rv.type, ir.IntType) and rv.type.width == 64:
b.ret(rv) b.ret(rv)
else: else:
b.ret(ir.Constant(self.i32, 0)) b.ret(ir.Constant(self.i64, 0))
ir_text = str(self.module) ir_text = str(self.module)
# Optional IR dump to file for debugging # Optional IR dump to file for debugging
@ -159,7 +188,7 @@ class NyashLLVMBuilder:
def _create_dummy_main(self) -> str: def _create_dummy_main(self) -> str:
"""Create dummy ny_main that returns 0""" """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") ny_main = ir.Function(self.module, ny_main_ty, name="ny_main")
block = ny_main.append_basic_block(name="entry") block = ny_main.append_basic_block(name="entry")
builder = ir.IRBuilder(block) builder = ir.IRBuilder(block)

View File

@ -202,7 +202,45 @@ fn lower_expr(f: &mut MirFunction, cur_bb: BasicBlockId, e: &ExprV0) -> Result<(
Ok((out, merge_bb)) Ok((out, merge_bb))
} }
ExprV0::Call { name, args } => { 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 (arg_ids, cur) = lower_args(f, cur_bb, args)?;
let fun_val = f.next_value_id(); let fun_val = f.next_value_id();
if let Some(bb) = f.get_block_mut(cur) { if let Some(bb) = f.get_block_mut(cur) {
@ -273,6 +311,42 @@ fn lower_expr_with_vars(
Err(format!("undefined variable: {}", name)) Err(format!("undefined variable: {}", name))
} }
ExprV0::Call { name, args } => { 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 // Lower args
let (arg_ids, cur) = lower_args_with_vars(f, cur_bb, args, vars)?; let (arg_ids, cur) = lower_args_with_vars(f, cur_bb, args, vars)?;
// Encode as: const fun_name; call // Encode as: const fun_name; call

Binary file not shown.

View File

@ -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"

View File

@ -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

View File

@ -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)

View File

@ -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"

View File

@ -0,0 +1,5 @@
# Expect: 🐱-9-nyash
local cat = "🐱"
local s = cat + "-" + (3*3) + "-" + "nyash"
print(s)

View File

@ -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")

View File

@ -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)

View File

@ -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())

View File

@ -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())

View File

@ -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")
}

View File

@ -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.

View File

@ -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 "$@"

View File

@ -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"

View File

@ -0,0 +1,4 @@
# Expect: 170
local v = (((((((((5 + 7) * 4) + 2) * 3) + 1) * 2) + 5))))
print(v) # 170

View File

@ -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)

View File

@ -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"

View File

@ -0,0 +1,5 @@
# Expect: 🐱-9-nyash
local cat = "🐱"
local s = cat + "-" + (3*3) + "-" + "nyash"
print(s)

View File

@ -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")

View File

@ -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)

View File

@ -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())

View File

@ -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())

View File

@ -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")
}

View File

@ -18,6 +18,7 @@ Grammar (subset):
term := unary (('*'|'/') unary)* term := unary (('*'|'/') unary)*
unary := '-' unary | factor unary := '-' unary | factor
factor := INT | STRING | IDENT call_tail* | '(' expr ')' | 'new' IDENT '(' args? ')' factor := INT | STRING | IDENT call_tail* | '(' expr ')' | 'new' IDENT '(' args? ')'
| '{' map_entries? '}' # map literal (string keys only)
call_tail:= '.' IDENT '(' args? ')' # method call_tail:= '.' IDENT '(' args? ')' # method
| '(' args? ')' # function call | '(' args? ')' # function call
args := expr (',' expr)* args := expr (',' expr)*
@ -45,7 +46,7 @@ def lex(s: str):
# two-char ops # 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): 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 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 out.append(Tok(c, c, i)); i+=1; continue
if c=='"': if c=='"':
j=i+1; buf=[] j=i+1; buf=[]
@ -144,6 +145,31 @@ class P:
if self.eat('STR'): return {"type":"Str","value":tok.val} if self.eat('STR'): return {"type":"Str","value":tok.val}
if self.eat('('): if self.eat('('):
e=self.expr(); self.expect(')'); return e 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'): if self.eat('NEW'):
t=self.cur(); self.expect('IDENT'); self.expect('(') t=self.cur(); self.expect('IDENT'); self.expect('(')
args=self.args_opt(); self.expect(')') args=self.args_opt(); self.expect(')')

View File

@ -110,5 +110,22 @@ NY
) )
run_case_bridge "assign update (bridge)" "$SRC_ASSIGN" 3 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 echo "All selfhost Stage-2 bridge smokes PASS" >&2
exit 0 exit 0