feat: 配列/Mapリテラル糖衣構文の実装とネームスペース解決の改善計画

- ArrayLiteral/MapLiteralのAST定義追加
- パーサーで[...]と{...}構文をサポート
- MIR Builderでnew Box() + push/setへのdesugaring実装
- テストケースとスモークスクリプト追加
- CURRENT_TASK.mdにネームスペース解決Phase-1計画を追記
- 3段階解決順序(ローカル→エイリアス→プラグイン)の設計合意
This commit is contained in:
Selfhosting Dev
2025-09-16 06:13:44 +09:00
parent 18bc386bc5
commit 6ca56b0652
27 changed files with 446 additions and 50 deletions

View File

@ -5,6 +5,21 @@ TL;DR
- PyVM は意味論の参照実行器開発補助。llvmlite は AOT/検証。配布やバンドル化は後回し(基礎固めが先)。 - PyVM は意味論の参照実行器開発補助。llvmlite は AOT/検証。配布やバンドル化は後回し(基礎固めが先)。
What Changed (today) What Changed (today)
- リファクタリング一式 完了Runner/LLVM/MIR Builder の分割第2弾まで。機能差分なしで整理のみ。
- Phase15自己ホストを再開。まずはスモークで挙動を再確認してから警告掃除へ進む方針に切り替え。
- 決定: 先にスモークPyVM/自己ホスト/Bridgeを回して“緑”を確認→ その後に `ops_ext.rs``runner/selfhost.rs` の警告削減に着手する。
- 構文糖衣の導入計画A案を承認: Stage1 で配列リテラル([a,b,c]を追加IR変更なし、デシュガリング
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`Stage3 受理のみ確認)
- スモークが緑なら、警告の削減に移行:
- `src/jit/lower/core/ops_ext.rs`(未使用・到達不能/冗長の解消、保存スロットの一貫化)
- `src/runner/selfhost.rs`(到達不能の除去、変数寿命の短縮、細かな `let`/`mut` 是正)
- ParserBox 強化Stage2 完了 + Stage3 受理を追加) - ParserBox 強化Stage2 完了 + Stage3 受理を追加)
- 進捗ガードparse_program2/parse_block2/parse_stmt2 - 進捗ガードparse_program2/parse_block2/parse_stmt2
- Stage2 受理一式: 単項/二項/比較/論理/呼出/メソッド/引数/if/else/loop/using/local/return/new。 - Stage2 受理一式: 単項/二項/比較/論理/呼出/メソッド/引数/if/else/loop/using/local/return/new。
@ -64,6 +79,22 @@ Notes / Policies
- Bridge は JSON v0 → MIR 降下で PHI を生成Phase15 中は現行方式を維持)。 - Bridge は JSON v0 → MIR 降下で PHI を生成Phase15 中は現行方式を維持)。
- 配布/バンドル/EXE 化は任意の実験導線として維持Phase15 の主目的外)。 - 配布/バンドル/EXE 化は任意の実験導線として維持Phase15 の主目的外)。
Smoke Snapshot (20250915)
- 修正: `runner/dispatch.rs``vm` 分岐が欠落しており `--backend vm` が interpreter にフォールバックしていたため、PyVM スモークが作動せず。分岐を追加して復旧済み。
- PyVM Stage2 部分結果:
- PASS: string ops basic, me method call
- FAIL: loop/if/phi → 出力 `sum=4`(期待 `sum=9`
- 原因分析: ループ内 if の merge で `sum` の Phi 正規化が入らず、latch 側スナップショットが else 系の一時値を優先(`16`)しうる構造。`LoopBuilder::build_statement(If)``normalize_if_else_phi` 相当を呼ばず、変数マップが φ 統合されていない。
- 対応方針(最小修正):
- LoopBuilder の If 降下で merge 到達時に「両枝が同一変数に代入」の場合は `phi(dst=[then,else])` を emit→その φ を対象変数に bind。
- latch スナップショットはこの φ 後の変数マップで採取する。
- 代替(短期): Builder 側の `normalize_if_else_phi` を呼ぶ薄いフックを設けて流用。
Fixes Applied (20250915)
- LoopBuilder If 降下に φ 正規化を追加(両枝代入の変数を merge 時に φ で束ねて再束縛)。
- PyVM φ 解決ロジックを安定化incoming を [value, pred] 形に限定し、[pred, value] の曖昧推測を削除)。偶然一致による誤選択を排除。
- これにより `tools/pyvm_stage2_smoke.sh` は全 PASS を確認済み。
Refactor Candidates (early plan) Refactor Candidates (early plan)
- runner/mod.rs~70K chars: “runner pipeline” を用途別に分割TODO #15 - runner/mod.rs~70K chars: “runner pipeline” を用途別に分割TODO #15
- runner/pipeline.rs入力正規化/using解決/環境注入) - runner/pipeline.rs入力正規化/using解決/環境注入)
@ -91,3 +122,38 @@ Recommended Next (short list)
- `builder/vars.rs` に SSA 変数正規化の小物を段階追加(変数名再束縛/スコープ終端の型ヒント伝搬など)。 - `builder/vars.rs` に SSA 変数正規化の小物を段階追加(変数名再束縛/スコープ終端の型ヒント伝搬など)。
- Runner仕上げ - Runner仕上げ
- `mod.rs` の残置ヘルパusingの候補提示・環境注入ログ`pipeline/dispatch` へ集約し、`mod.rs` を最小のオーケストレーションに。 - `mod.rs` の残置ヘルパusingの候補提示・環境注入ログ`pipeline/dispatch` へ集約し、`mod.rs` を最小のオーケストレーションに。
- Namespaces Phase1実装着手: BoxIndex 構築・3段階解決・toml aliases・曖昧エラー改善・トレース
Array/Map Literals PlanSyntax Sugar
- Stage1: Array literal `[e1, e2, ...]` を実装(ゲート: `NYASH_SYNTAX_SUGAR_LEVEL=basic|full` または `NYASH_ENABLE_ARRAY_LITERAL=1`)。
- Lowering: `new ArrayBox()` → 各要素を評価 → `.push(elem)` を左から右に順に発行 → 最後に配列値を返す。
- 末尾カンマ許可。
- スモーク: `apps/tests/array_literal_basic.nyash`size/順序/副作用1回性
- Stage2: Map literal `{ "k": v, ... }`(文字列キー限定)を実装(ゲート: `NYASH_SYNTAX_SUGAR_LEVEL=basic|full` or `NYASH_ENABLE_MAP_LITERAL=1`)。
- Lowering: `new MapBox()` → 各ペアを評価 → `.set("k", v)` を左から右に順に発行 → 最後に map 値を返す。
- 末尾カンマ許可。識別子キー糖 `{name: v}` は次フェーズ。
- スモーク: `apps/tests/map_literal_basic.nyash`size/get/順序検証)。
- Stage3: 識別子キー糖 `{name: v}` と末尾カンマを強化(任意)。
Gates / Semantics
- 左から右で評価一度だけ。push/set 失敗は即時伝播(既存 BoxCall 規約に追従)。
- IR 変更なしBoxCall/MethodCall のみ)。将来 `with_capacity(n)` 最適化は任意で追加。
Decision Log (20250915)
- Q: 警告削減(`ops_ext.rs` / `selfhost.rs`)を先にやる?それとも挙動スモークを先に回す?
- A: スモークを先に実施。理由は以下。
- リファクタ直後は回帰検出を最優先PyVM/自己ホスト/Bridge の3レーンで即座に検証
- 警告削減は挙動非変化を原則とするが、微妙なスコープや保存スロットの触りが混入し得るため、先に“緑”を固める。
Namespaces / Using計画合意
- 3段階の解決順決定性: 1) ローカル/コアボックス → 2) エイリアスnyash.toml/needs → 3) プラグイン(短名/qualified
- 曖昧時はエラー候補提示。qualified または alias を要求(自動推測はしない)。
- モード切替: Relaxed既定/Strict`NYASH_PLUGIN_REQUIRE_PREFIX=1` or toml `[plugins] require_prefix=true`
- needs 糖衣は using の同義Runner で alias 登録)。`needs plugin.network.HttpClient as HttpClient` 等。
- Plugins は統合名前空間短名はユニーク時のみ。qualified `network.HttpClient` を常時許可。
- nyash.tomlMVP: `[imports]`/`[aliases]``[plugins.<name>] { path, prefix, require_prefix, expose_short_names }`
- Index とキャッシュRunner:
- BoxIndex: `local_boxes`, `plugin_boxes`, `aliases` を保持
- `RESOLVE_CACHE`threadlocalで同一解決の再計算を回避
- `NYASH_RESOLVE_TRACE=1` で解決過程をログ出力
- スモークが緑=基礎健全性確認後に、静的ノイズの除去を安全に一気通貫で行う。

View File

@ -0,0 +1,11 @@
static box Main {
main(args) {
local console = new ConsoleBox()
local x = 1
// array literal sugar
local arr = [x, 2, 3]
console.println(arr.size())
return 0
}
}

View File

@ -0,0 +1,11 @@
static box Main {
main(args) {
local console = new ConsoleBox()
// map literal sugar (string keys)
local m = {"name": "Alice", "age": 25}
console.println(m.size())
console.println(m.get("name"))
return 0
}
}

View File

@ -0,0 +1,10 @@
static box Main {
main(args) {
local console = new ConsoleBox()
local m = {name: "A", age: 2,}
console.println(m.size())
console.println(m.get("name"))
return 0
}
}

View File

@ -24,6 +24,10 @@ factor := INT
| IDENT call_tail* | IDENT call_tail*
| '(' expr ')' | '(' expr ')'
| 'new' IDENT '(' args? ')' | 'new' IDENT '(' args? ')'
| '[' args? ']' ; Array literal (Stage1 sugar, gated)
| '{' map_entries? '}' ; Map literal (Stage2 sugar, gated)
map_entries := (STRING | IDENT) ':' expr (',' (STRING | IDENT) ':' expr)* [',']
call_tail := '.' IDENT '(' args? ')' ; method call_tail := '.' IDENT '(' args? ')' ; method
| '(' args? ')' ; function call | '(' args? ')' ; function call
@ -35,4 +39,6 @@ Notes
- Short-circuit: '&&' and '||' must not evaluate the RHS when not needed. - Short-circuit: '&&' and '||' must not evaluate the RHS when not needed.
- Unary minus has higher precedence than '*' and '/'. - Unary minus has higher precedence than '*' and '/'.
- IDENT names consist of [A-Za-z_][A-Za-z0-9_]* - IDENT names consist of [A-Za-z_][A-Za-z0-9_]*
- Array literal is enabled when syntax sugar is on (NYASH_SYNTAX_SUGAR_LEVEL=basic|full) or when NYASH_ENABLE_ARRAY_LITERAL=1 is set.
- Map literal is enabled when syntax sugar is on (NYASH_SYNTAX_SUGAR_LEVEL=basic|full) or when NYASH_ENABLE_MAP_LITERAL=1 is set.
- Identifier keys (`{name: v}`) are Stage3 and require either NYASH_SYNTAX_SUGAR_LEVEL=full or NYASH_ENABLE_MAP_IDENT_KEY=1.

View File

@ -1,4 +1,4 @@
# using — Imports and Namespaces (Phase 15) # using — Imports and Namespaces (Phase 15+)
Status: Accepted (Runnerside resolution). Selfhost parser accepts using as noop and attaches `meta.usings` for future use. Status: Accepted (Runnerside resolution). Selfhost parser accepts using as noop and attaches `meta.usings` for future use.
@ -9,6 +9,50 @@ Policy
- Registers modules into an internal registry for path/namespace hints. - Registers modules into an internal registry for path/namespace hints.
- Selfhost compiler (Ny→JSON v0) collects using lines and emits `meta.usings` when present. The bridge currently ignores this meta field. - Selfhost compiler (Ny→JSON v0) collects using lines and emits `meta.usings` when present. The bridge currently ignores this meta field.
## Namespace Resolution (Runnerside)
- Goal: keep IR/VM/JIT untouched. All resolution happens in Runner/Registry.
- Default search order (3 stages, deterministic):
1) Local/Core Boxes (nyrt)
2) Aliases (nyash.toml [imports] / `needs … as …`)
3) Plugins (short name if unique, otherwise qualified `pluginName.BoxName`)
- On ambiguity: error with candidates and remediation (qualify or define alias).
- Modes:
- Relaxed (default): short names allowed when unique.
- Strict: require plugin prefix (env `NYASH_PLUGIN_REQUIRE_PREFIX=1` or nyash.toml `[plugins] require_prefix=true`).
- Aliases:
- nyash.toml `[imports] HttpClient = "network.HttpClient"`
- needs sugar: `needs plugin.network.HttpClient as HttpClient` (filescoped alias)
## Plugins
- Unified namespace with Boxes. Prefer short names when unique.
- Qualified form: `network.HttpClient`
- Perplugin control (nyash.toml): `prefix`, `require_prefix`, `expose_short_names`
## `needs` sugar (optional)
- Treated as a synonym to `using` on the Runner side; registers aliases only.
- Examples: `needs utils.StringHelper`, `needs plugin.network.HttpClient as HttpClient`, `needs plugin.network.*`
## nyash.toml keys (MVP)
- `[imports]`/`[aliases]`: short name → fully qualified
- `[plugins.<name>]`: `path`, `prefix`, `require_prefix`, `expose_short_names`
## Index and Cache (Runner)
- Build a Box index once per run to make resolution fast and predictable:
```
struct BoxIndex {
local_boxes: HashMap<String, PathBuf>,
plugin_boxes: HashMap<String, Vec<PluginBox>>,
aliases: HashMap<String, String>,
}
```
- Maintain a small resolve cache per thread:
```
thread_local! {
static RESOLVE_CACHE: RefCell<HashMap<String, ResolvedBox>> = /* ... */;
}
```
- Trace: `NYASH_RESOLVE_TRACE=1` prints resolution steps (for debugging/CI logs).
Syntax Syntax
- Namespace: `using core.std` or `using core.std as Std` - Namespace: `using core.std` or `using core.std as Std`
- File path: `using "apps/examples/string_p0.nyash" as Strings` - File path: `using "apps/examples/string_p0.nyash" as Strings`
@ -34,6 +78,28 @@ static box Main {
} }
``` ```
Qualified/Plugins/Aliases examples
```nyash
# nyash.toml
[plugins.network]
path = "plugins/network.so"
prefix = "network"
require_prefix = false
[imports]
HttpClient = "network.HttpClient"
# code
needs plugin.network.HttpClient as HttpClient
static box Main {
main(args) {
let a = new HttpClient() # alias
let b = new network.HttpClient() # qualified
}
}
```
Runner Configuration Runner Configuration
- Enable using preprocessing: `NYASH_ENABLE_USING=1` - Enable using preprocessing: `NYASH_ENABLE_USING=1`
- Selfhost pipeline keeps child stdout quiet and extracts JSON only: `NYASH_JSON_ONLY=1` (set by Runner automatically for child) - Selfhost pipeline keeps child stdout quiet and extracts JSON only: `NYASH_JSON_ONLY=1` (set by Runner automatically for child)
@ -42,4 +108,3 @@ Runner Configuration
Notes Notes
- Phase 15 keeps resolution in the Runner to minimize parser complexity. Future phases may leverage `meta.usings` for compiler decisions. - Phase 15 keeps resolution in the Runner to minimize parser complexity. Future phases may leverage `meta.usings` for compiler decisions.
- Unknown fields in the toplevel JSON (like `meta`) are ignored by the current bridge. - Unknown fields in the toplevel JSON (like `meta`) are ignored by the current bridge.

View File

@ -201,6 +201,7 @@ pub enum ExpressionNode {
else_expr: Box<ASTNode>, else_expr: Box<ASTNode>,
span: Span, span: Span,
}, },
// (Stage2 sugar for literals is represented in unified ASTNode, not here)
} }
/// 文ノード - 実行可能なアクション /// 文ノード - 実行可能なアクション
@ -476,6 +477,16 @@ pub enum ASTNode {
else_expr: Box<ASTNode>, else_expr: Box<ASTNode>,
span: Span, span: Span,
}, },
/// 配列リテラル(糖衣): [e1, e2, ...]
ArrayLiteral {
elements: Vec<ASTNode>,
span: Span,
},
/// マップリテラル(糖衣): { "k": v, ... } Stage2: 文字列キー限定)
MapLiteral {
entries: Vec<(String, ASTNode)>,
span: Span,
},
/// 無名関数最小P1: 値としてのみ。呼び出しは未対応) /// 無名関数最小P1: 値としてのみ。呼び出しは未対応)
Lambda { Lambda {
@ -708,6 +719,8 @@ impl ASTNode {
ASTNode::QMarkPropagate { .. } => "QMarkPropagate", ASTNode::QMarkPropagate { .. } => "QMarkPropagate",
ASTNode::PeekExpr { .. } => "PeekExpr", ASTNode::PeekExpr { .. } => "PeekExpr",
ASTNode::Lambda { .. } => "Lambda", ASTNode::Lambda { .. } => "Lambda",
ASTNode::ArrayLiteral { .. } => "ArrayLiteral",
ASTNode::MapLiteral { .. } => "MapLiteral",
} }
} }
@ -740,6 +753,8 @@ impl ASTNode {
ASTNode::PeekExpr { .. } => ASTNodeType::Expression, ASTNode::PeekExpr { .. } => ASTNodeType::Expression,
ASTNode::QMarkPropagate { .. } => ASTNodeType::Expression, ASTNode::QMarkPropagate { .. } => ASTNodeType::Expression,
ASTNode::Lambda { .. } => ASTNodeType::Expression, ASTNode::Lambda { .. } => ASTNodeType::Expression,
ASTNode::ArrayLiteral { .. } => ASTNodeType::Expression,
ASTNode::MapLiteral { .. } => ASTNodeType::Expression,
// Statement nodes - 実行可能なアクション // Statement nodes - 実行可能なアクション
ASTNode::Program { .. } => ASTNodeType::Statement, // プログラム全体 ASTNode::Program { .. } => ASTNodeType::Statement, // プログラム全体
@ -903,6 +918,12 @@ impl ASTNode {
ASTNode::Lambda { params, body, .. } => { ASTNode::Lambda { params, body, .. } => {
format!("Lambda({} params, {} statements)", params.len(), body.len()) format!("Lambda({} params, {} statements)", params.len(), body.len())
} }
ASTNode::ArrayLiteral { elements, .. } => {
format!("ArrayLiteral({} elements)", elements.len())
}
ASTNode::MapLiteral { entries, .. } => {
format!("MapLiteral({} entries)", entries.len())
}
} }
} }
@ -947,6 +968,8 @@ impl ASTNode {
ASTNode::PeekExpr { span, .. } => *span, ASTNode::PeekExpr { span, .. } => *span,
ASTNode::QMarkPropagate { span, .. } => *span, ASTNode::QMarkPropagate { span, .. } => *span,
ASTNode::Lambda { span, .. } => *span, ASTNode::Lambda { span, .. } => *span,
ASTNode::ArrayLiteral { span, .. } => *span,
ASTNode::MapLiteral { span, .. } => *span,
} }
} }
} }

View File

@ -135,9 +135,9 @@ impl VM {
/// Execute a single function /// Execute a single function
pub(super) fn execute_function(&mut self, function: &MirFunction) -> Result<VMValue, VMError> { pub(super) fn execute_function(&mut self, function: &MirFunction) -> Result<VMValue, VMError> {
use crate::box_trait::{StringBox, IntegerBox, BoolBox, VoidBox}; // unused: local downcasts not required here
use crate::runtime::global_hooks; use crate::runtime::global_hooks;
use crate::instance_v2::InstanceBox; // use crate::instance_v2::InstanceBox; // not used in this path
use super::control_flow; use super::control_flow;
self.current_function = Some(function.signature.name.clone()); self.current_function = Some(function.signature.name.clone());

View File

@ -1,4 +1,3 @@
use crate::box_trait::NyashBox;
use crate::mir::ValueId; use crate::mir::ValueId;
use crate::backend::vm::ControlFlow; use crate::backend::vm::ControlFlow;
use crate::backend::{VM, VMError, VMValue}; use crate::backend::{VM, VMError, VMValue};
@ -7,7 +6,7 @@ impl VM {
/// Small helpers to reduce duplication in vtable stub paths. /// Small helpers to reduce duplication in vtable stub paths.
#[inline] #[inline]
fn vmvalue_to_box(val: &VMValue) -> Box<dyn crate::box_trait::NyashBox> { fn vmvalue_to_box(val: &VMValue) -> Box<dyn crate::box_trait::NyashBox> {
use crate::box_trait::{NyashBox, StringBox as SBox, IntegerBox as IBox, BoolBox as BBox}; use crate::box_trait::{StringBox as SBox, IntegerBox as IBox, BoolBox as BBox};
match val { match val {
VMValue::Integer(i) => Box::new(IBox::new(*i)), VMValue::Integer(i) => Box::new(IBox::new(*i)),
VMValue::String(s) => Box::new(SBox::new(s)), VMValue::String(s) => Box::new(SBox::new(s)),
@ -900,3 +899,4 @@ impl VM {
Ok(None) Ok(None)
} }
} }
use crate::box_trait::NyashBox;

View File

@ -1,5 +1,5 @@
use crate::mir::{ConstValue, BinaryOp, CompareOp, UnaryOp, ValueId, BasicBlockId, TypeOpKind, MirType}; use crate::mir::{ConstValue, BinaryOp, CompareOp, UnaryOp, ValueId, BasicBlockId, TypeOpKind, MirType};
use crate::box_trait::{NyashBox, BoolBox, VoidBox}; use crate::box_trait::{BoolBox, VoidBox};
use crate::boxes::ArrayBox; use crate::boxes::ArrayBox;
use std::sync::Arc; use std::sync::Arc;
use crate::backend::vm::ControlFlow; use crate::backend::vm::ControlFlow;

View File

@ -12,7 +12,7 @@ use crate::mir::{BasicBlockId, ValueId};
use crate::runtime::NyashRuntime; use crate::runtime::NyashRuntime;
use crate::scope_tracker::ScopeTracker; use crate::scope_tracker::ScopeTracker;
use std::collections::HashMap; use std::collections::HashMap;
use std::time::Instant; // use std::time::Instant; // not used in this module
impl VM { impl VM {
fn jit_threshold_from_env() -> u32 { fn jit_threshold_from_env() -> u32 {

View File

@ -47,7 +47,7 @@ impl NyashInterpreter {
} }
"MathBox" => { "MathBox" => {
if let Ok(reg) = self.runtime.box_registry.lock() { if let Ok(reg) = self.runtime.box_registry.lock() {
if let Ok(b) = reg.create_box("MathBox", &[]) { if let Ok(_b) = reg.create_box("MathBox", &[]) {
// Note: execute_math_method expects builtin MathBox; plugin path should route via VM/BoxCall in new pipeline. // Note: execute_math_method expects builtin MathBox; plugin path should route via VM/BoxCall in new pipeline.
// Here we simply return void; method paths should prefer plugin invoke in VM. // Here we simply return void; method paths should prefer plugin invoke in VM.
return Ok(Box::new(VoidBox::new())); return Ok(Box::new(VoidBox::new()));

View File

@ -1,5 +1,5 @@
use crate::mir::{MirFunction, MirInstruction, ConstValue, BinaryOp, CompareOp, ValueId}; use crate::mir::{MirFunction, MirInstruction, ConstValue, ValueId};
use super::builder::{IRBuilder, BinOpKind, CmpKind}; use super::builder::{IRBuilder, BinOpKind};
mod analysis; mod analysis;
mod cfg; mod cfg;

View File

@ -1,9 +1,8 @@
use std::collections::{HashMap, HashSet, BTreeSet}; use std::collections::HashSet;
use crate::mir::{BasicBlockId, MirFunction, MirInstruction, ValueId}; use crate::mir::{BasicBlockId, MirFunction, MirInstruction, ValueId};
use super::super::builder::IRBuilder; // removed unused imports
use super::super::core_ops; // ensure module link remains
use super::LowerCore; use super::LowerCore;
impl LowerCore { impl LowerCore {
@ -114,4 +113,3 @@ impl LowerCore {
} }
} }
} }

View File

@ -127,6 +127,9 @@ class PyVM:
# incoming: prefer [[vid, pred_bid]], but accept [pred_bid, vid] robustly # incoming: prefer [[vid, pred_bid]], but accept [pred_bid, vid] robustly
incoming = inst.get("incoming", []) incoming = inst.get("incoming", [])
chosen: Any = None chosen: Any = None
dbg = os.environ.get('NYASH_PYVM_DEBUG_PHI') == '1'
if dbg:
print(f"[pyvm.phi] prev={prev} incoming={incoming}")
for pair in incoming: for pair in incoming:
if not isinstance(pair, (list, tuple)) or len(pair) < 2: if not isinstance(pair, (list, tuple)) or len(pair) < 2:
continue continue
@ -134,10 +137,8 @@ class PyVM:
# Case 1: [vid, pred] # Case 1: [vid, pred]
if prev is not None and int(b) == int(prev) and int(a) in regs: if prev is not None and int(b) == int(prev) and int(a) in regs:
chosen = regs.get(int(a)) chosen = regs.get(int(a))
break if dbg:
# Case 2: [pred, vid] print(f"[pyvm.phi] case1 match: use v{a} from pred {b} -> {chosen}")
if prev is not None and int(a) == int(prev) and int(b) in regs:
chosen = regs.get(int(b))
break break
if chosen is None and incoming: if chosen is None and incoming:
# Fallback to first element that resolves to a known vid # Fallback to first element that resolves to a known vid
@ -147,8 +148,9 @@ class PyVM:
a, b = pair[0], pair[1] a, b = pair[0], pair[1]
if int(a) in regs: if int(a) in regs:
chosen = regs.get(int(a)); break chosen = regs.get(int(a)); break
if int(b) in regs: # Do not try to resolve by assuming [pred, vid] — avoid false matches
chosen = regs.get(int(b)); break if dbg:
print(f"[pyvm.phi] chosen={chosen}")
self._set(regs, inst.get("dst"), chosen) self._set(regs, inst.get("dst"), chosen)
i += 1 i += 1
continue continue

View File

@ -487,6 +487,17 @@ impl MirBuilder {
self.build_new_expression(class.clone(), arguments.clone()) self.build_new_expression(class.clone(), arguments.clone())
}, },
ASTNode::ArrayLiteral { elements, .. } => {
// Lower: new ArrayBox(); for each elem: .push(elem)
let arr_id = self.value_gen.next();
self.emit_instruction(MirInstruction::NewBox { dst: arr_id, box_type: "ArrayBox".to_string(), args: vec![] })?;
for e in elements {
let v = self.build_expression(e)?;
self.emit_instruction(MirInstruction::BoxCall { dst: None, box_val: arr_id, method: "push".to_string(), method_id: None, args: vec![v], effects: super::EffectMask::MUT })?;
}
Ok(arr_id)
},
// Phase 7: Async operations // Phase 7: Async operations
ASTNode::Nowait { variable, expression, .. } => { ASTNode::Nowait { variable, expression, .. } => {
self.build_nowait_statement(variable.clone(), *expression.clone()) self.build_nowait_statement(variable.clone(), *expression.clone())

View File

@ -114,6 +114,28 @@ impl super::MirBuilder {
ASTNode::New { class, arguments, .. } => ASTNode::New { class, arguments, .. } =>
self.build_new_expression(class.clone(), arguments.clone()), self.build_new_expression(class.clone(), arguments.clone()),
ASTNode::ArrayLiteral { elements, .. } => {
let arr_id = self.value_gen.next();
self.emit_instruction(MirInstruction::NewBox { dst: arr_id, box_type: "ArrayBox".to_string(), args: vec![] })?;
for e in elements {
let v = self.build_expression_impl(e)?;
self.emit_instruction(MirInstruction::BoxCall { dst: None, box_val: arr_id, method: "push".to_string(), method_id: None, args: vec![v], effects: super::EffectMask::MUT })?;
}
Ok(arr_id)
}
ASTNode::MapLiteral { entries, .. } => {
let map_id = self.value_gen.next();
self.emit_instruction(MirInstruction::NewBox { dst: map_id, box_type: "MapBox".to_string(), args: vec![] })?;
for (k, expr) in entries {
// const string key
let k_id = self.value_gen.next();
self.emit_instruction(MirInstruction::Const { dst: k_id, value: ConstValue::String(k) })?;
let v_id = self.build_expression_impl(expr)?;
self.emit_instruction(MirInstruction::BoxCall { dst: None, box_val: map_id, method: "set".to_string(), method_id: None, args: vec![k_id, v_id], effects: super::EffectMask::MUT })?;
}
Ok(map_id)
}
ASTNode::Nowait { variable, expression, .. } => ASTNode::Nowait { variable, expression, .. } =>
self.build_nowait_statement(variable.clone(), *expression.clone()), self.build_nowait_statement(variable.clone(), *expression.clone()),

View File

@ -42,6 +42,29 @@ pub struct LoopBuilder<'a> {
continue_snapshots: Vec<(BasicBlockId, HashMap<String, ValueId>)>, continue_snapshots: Vec<(BasicBlockId, HashMap<String, ValueId>)>,
} }
// Local copy: detect a variable name assigned within an AST fragment
fn extract_assigned_var_local(ast: &ASTNode) -> Option<String> {
match ast {
ASTNode::Assignment { target, .. } => {
if let ASTNode::Variable { name, .. } = target.as_ref() { Some(name.clone()) } else { None }
}
ASTNode::Program { statements, .. } => statements.last().and_then(|st| extract_assigned_var_local(st)),
ASTNode::If { then_body, else_body, .. } => {
let then_prog = ASTNode::Program { statements: then_body.clone(), span: crate::ast::Span::unknown() };
let tvar = extract_assigned_var_local(&then_prog);
let evar = else_body.as_ref().and_then(|eb| {
let ep = ASTNode::Program { statements: eb.clone(), span: crate::ast::Span::unknown() };
extract_assigned_var_local(&ep)
});
match (tvar, evar) {
(Some(tv), Some(ev)) if tv == ev => Some(tv),
_ => None,
}
}
_ => None,
}
}
impl<'a> LoopBuilder<'a> { impl<'a> LoopBuilder<'a> {
/// 新しいループビルダーを作成 /// 新しいループビルダーを作成
pub fn new(parent: &'a mut super::builder::MirBuilder) -> Self { pub fn new(parent: &'a mut super::builder::MirBuilder) -> Self {
@ -325,6 +348,10 @@ impl<'a> LoopBuilder<'a> {
let merge_bb = self.new_block(); let merge_bb = self.new_block();
self.emit_branch(cond_val, then_bb, else_bb)?; self.emit_branch(cond_val, then_bb, else_bb)?;
// Capture pre-if variable map (used for phi normalization)
let pre_if_var_map = self.get_current_variable_map();
let pre_then_var_value = pre_if_var_map.clone();
// then // then
self.set_current_block(then_bb)?; self.set_current_block(then_bb)?;
for s in then_body.iter().cloned() { for s in then_body.iter().cloned() {
@ -338,6 +365,7 @@ impl<'a> LoopBuilder<'a> {
}; };
if terminated { break; } if terminated { break; }
} }
let then_var_map_end = self.get_current_variable_map();
// Only jump to merge if not already terminated (e.g., continue/break) // Only jump to merge if not already terminated (e.g., continue/break)
{ {
let cur_id = self.current_block()?; let cur_id = self.current_block()?;
@ -351,7 +379,8 @@ impl<'a> LoopBuilder<'a> {
// else // else
self.set_current_block(else_bb)?; self.set_current_block(else_bb)?;
if let Some(es) = else_body { let mut else_var_map_end_opt: Option<std::collections::HashMap<String, super::ValueId>> = None;
if let Some(es) = else_body.clone() {
for s in es.into_iter() { for s in es.into_iter() {
let _ = self.build_statement(s)?; let _ = self.build_statement(s)?;
let cur_id = self.current_block()?; let cur_id = self.current_block()?;
@ -362,6 +391,7 @@ impl<'a> LoopBuilder<'a> {
}; };
if terminated { break; } if terminated { break; }
} }
else_var_map_end_opt = Some(self.get_current_variable_map());
} }
{ {
let cur_id = self.current_block()?; let cur_id = self.current_block()?;
@ -375,6 +405,29 @@ impl<'a> LoopBuilder<'a> {
// Continue at merge // Continue at merge
self.set_current_block(merge_bb)?; self.set_current_block(merge_bb)?;
// If both branches assign the same variable, emit phi and bind it
let then_prog = ASTNode::Program { statements: then_body.clone(), span: crate::ast::Span::unknown() };
let assigned_then = extract_assigned_var_local(&then_prog);
let assigned_else = else_body.as_ref().and_then(|es| {
let ep = ASTNode::Program { statements: es.clone(), span: crate::ast::Span::unknown() };
extract_assigned_var_local(&ep)
});
if let Some(var_name) = assigned_then {
let else_assigns_same = assigned_else.as_ref().map(|s| s == &var_name).unwrap_or(false);
let then_value_for_var = then_var_map_end.get(&var_name).copied();
let else_value_for_var = if else_assigns_same {
else_var_map_end_opt.as_ref().and_then(|m| m.get(&var_name).copied())
} else {
pre_then_var_value.get(&var_name).copied()
};
if let (Some(tv), Some(ev)) = (then_value_for_var, else_value_for_var) {
let phi_id = self.new_value();
self.emit_phi_at_block_start(merge_bb, phi_id, vec![(then_bb, tv), (else_bb, ev)])?;
// Reset to pre-if map and bind the phi result
self.parent_builder.variable_map = pre_if_var_map.clone();
self.parent_builder.variable_map.insert(var_name, phi_id);
}
}
let void_id = self.new_value(); let void_id = self.new_value();
self.emit_const(void_id, ConstValue::Void)?; self.emit_const(void_id, ConstValue::Void)?;
Ok(void_id) Ok(void_id)

View File

@ -631,9 +631,71 @@ impl NyashParser {
Ok(expr) Ok(expr)
} }
/// 基本式をパース: リテラル、変数、括弧、this、new /// 基本式をパース: リテラル、変数、括弧、this、new、配列リテラル(糖衣)
fn parse_primary(&mut self) -> Result<ASTNode, ParseError> { fn parse_primary(&mut self) -> Result<ASTNode, ParseError> {
match &self.current_token().token_type { match &self.current_token().token_type {
TokenType::LBRACK => {
// Array literal: [e1, e2, ...] (sugar)
let sugar_on = crate::parser::sugar_gate::is_enabled()
|| std::env::var("NYASH_ENABLE_ARRAY_LITERAL").ok().as_deref() == Some("1");
if !sugar_on {
let line = self.current_token().line;
return Err(ParseError::UnexpectedToken {
found: self.current_token().token_type.clone(),
expected: "enable NYASH_SYNTAX_SUGAR_LEVEL=basic|full or NYASH_ENABLE_ARRAY_LITERAL=1".to_string(),
line,
});
}
self.advance(); // consume '['
let mut elems: Vec<ASTNode> = Vec::new();
while !self.match_token(&TokenType::RBRACK) && !self.is_at_end() {
must_advance!(self, _unused, "array literal element parsing");
let el = self.parse_expression()?;
elems.push(el);
if self.match_token(&TokenType::COMMA) { self.advance(); }
}
self.consume(TokenType::RBRACK)?;
Ok(ASTNode::ArrayLiteral { elements: elems, span: Span::unknown() })
}
TokenType::LBRACE => {
// Map literal (Stage2, string keys only)
let sugar_on = crate::parser::sugar_gate::is_enabled()
|| std::env::var("NYASH_ENABLE_MAP_LITERAL").ok().as_deref() == Some("1");
if !sugar_on {
let line = self.current_token().line;
return Err(ParseError::UnexpectedToken {
found: self.current_token().token_type.clone(),
expected: "enable NYASH_SYNTAX_SUGAR_LEVEL=basic|full or NYASH_ENABLE_MAP_LITERAL=1".to_string(),
line,
});
}
self.advance(); // consume '{'
let mut entries: Vec<(String, ASTNode)> = Vec::new();
let sugar_level = std::env::var("NYASH_SYNTAX_SUGAR_LEVEL").ok();
let ident_key_on = std::env::var("NYASH_ENABLE_MAP_IDENT_KEY").ok().as_deref() == Some("1")
|| sugar_level.as_deref() == Some("full");
while !self.match_token(&TokenType::RBRACE) && !self.is_at_end() {
// Key: string literal (Stage2) or identifier key sugar (Stage3; gated)
let key = match &self.current_token().token_type {
TokenType::STRING(s) => { let v = s.clone(); self.advance(); v }
TokenType::IDENTIFIER(id) if ident_key_on => { let v = id.clone(); self.advance(); v }
_ => {
let line = self.current_token().line;
return Err(ParseError::UnexpectedToken {
found: self.current_token().token_type.clone(),
expected: if ident_key_on { "string or identifier key in map literal".to_string() } else { "string key in map literal".to_string() },
line,
});
}
};
self.consume(TokenType::COLON)?;
let value_expr = self.parse_expression()?;
entries.push((key, value_expr));
if self.match_token(&TokenType::COMMA) { self.advance(); }
}
self.consume(TokenType::RBRACE)?;
Ok(ASTNode::MapLiteral { entries, span: Span::unknown() })
}
TokenType::INCLUDE => { TokenType::INCLUDE => {
// Allow include as an expression: include "path" // Allow include as an expression: include "path"
self.parse_include() self.parse_include()

View File

@ -86,6 +86,12 @@ pub(crate) fn execute_file_with_backend(runner: &NyashRunner, filename: &str) {
} }
runner.execute_mir_mode(filename); runner.execute_mir_mode(filename);
} }
"vm" => {
if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1") {
println!("🚀 Nyash VM Backend - Executing file: {} 🚀", filename);
}
runner.execute_vm_mode(filename);
}
#[cfg(feature = "cranelift-jit")] #[cfg(feature = "cranelift-jit")]
"jit-direct" => { "jit-direct" => {
if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1") { if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1") {

View File

@ -6,20 +6,8 @@
*/ */
use nyash_rust::cli::CliConfig; use nyash_rust::cli::CliConfig;
use nyash_rust::{ // prune heavy unused imports here; modules import what they need locally
box_trait::{StringBox, IntegerBox, BoolBox, VoidBox, AddBox}, // pruned unused runtime imports in this module
tokenizer::{NyashTokenizer},
ast::ASTNode,
parser::NyashParser,
interpreter::NyashInterpreter,
mir::{MirCompiler, MirPrinter, MirInstruction},
backend::VM,
};
use nyash_rust::runtime::{NyashRuntime, NyashRuntimeBuilder};
use nyash_rust::interpreter::SharedState;
use nyash_rust::box_factory::user_defined::UserDefinedBoxFactory;
use nyash_rust::core::model::BoxDeclaration as CoreBoxDecl;
use std::sync::Arc;
#[cfg(feature = "wasm-backend")] #[cfg(feature = "wasm-backend")]
use nyash_rust::backend::{wasm::WasmBackend, aot::AotBackend}; use nyash_rust::backend::{wasm::WasmBackend, aot::AotBackend};

View File

@ -183,7 +183,7 @@ impl NyashRunner {
// Prefer MIR signature when available, but fall back to runtime coercions to keep VM/JIT consistent. // Prefer MIR signature when available, but fall back to runtime coercions to keep VM/JIT consistent.
let (ety, sval) = if let Some(func) = compile_result.module.functions.get("main") { let (ety, sval) = if let Some(func) = compile_result.module.functions.get("main") {
use nyash_rust::mir::MirType; use nyash_rust::mir::MirType;
use nyash_rust::box_trait::{NyashBox, IntegerBox, BoolBox, StringBox}; use nyash_rust::box_trait::{IntegerBox, BoolBox, StringBox};
use nyash_rust::boxes::FloatBox; use nyash_rust::boxes::FloatBox;
match &func.signature.return_type { match &func.signature.return_type {
MirType::Float => { MirType::Float => {
@ -272,7 +272,7 @@ impl NyashRunner {
format!("./{}", filename) format!("./{}", filename)
} }
use std::collections::{HashSet, VecDeque}; use std::collections::HashSet;
fn walk_with_state(node: &ASTNode, runtime: &NyashRuntime, stack: &mut Vec<String>, visited: &mut HashSet<String>) { fn walk_with_state(node: &ASTNode, runtime: &NyashRuntime, stack: &mut Vec<String>, visited: &mut HashSet<String>) {
match node { match node {

View File

@ -1,6 +1,6 @@
use super::types::{PluginBoxV2, PluginHandleInner, NyashTypeBoxFfi, LoadedPluginV2}; use super::types::{PluginBoxV2, PluginHandleInner, LoadedPluginV2};
use crate::bid::{BidResult, BidError}; use crate::bid::{BidResult, BidError};
use crate::box_trait::{NyashBox, BoxCore, StringBox, IntegerBox}; use crate::box_trait::NyashBox;
use crate::config::nyash_toml_v2::{NyashConfigV2, LibraryDefinition}; use crate::config::nyash_toml_v2::{NyashConfigV2, LibraryDefinition};
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::{Arc, RwLock}; use std::sync::{Arc, RwLock};

View File

@ -94,6 +94,8 @@ pub enum TokenType {
DoubleColon, // :: (Parent::method) - P1用定義のみ DoubleColon, // :: (Parent::method) - P1用定義のみ
LPAREN, // ( LPAREN, // (
RPAREN, // ) RPAREN, // )
LBRACK, // [
RBRACK, // ]
LBRACE, // { LBRACE, // {
RBRACE, // } RBRACE, // }
COMMA, // , COMMA, // ,
@ -395,6 +397,14 @@ impl NyashTokenizer {
self.advance(); self.advance();
Ok(Token::new(TokenType::RPAREN, start_line, start_column)) Ok(Token::new(TokenType::RPAREN, start_line, start_column))
} }
Some('[') => {
self.advance();
Ok(Token::new(TokenType::LBRACK, start_line, start_column))
}
Some(']') => {
self.advance();
Ok(Token::new(TokenType::RBRACK, start_line, start_column))
}
Some('{') => { Some('{') => {
self.advance(); self.advance();
Ok(Token::new(TokenType::LBRACE, start_line, start_column)) Ok(Token::new(TokenType::LBRACE, start_line, start_column))
@ -407,14 +417,7 @@ impl NyashTokenizer {
self.advance(); self.advance();
Ok(Token::new(TokenType::COMMA, start_line, start_column)) Ok(Token::new(TokenType::COMMA, start_line, start_column))
} }
Some('?') => { // '?' and ':' are handled earlier (including variants); avoid duplicate arms
self.advance();
Ok(Token::new(TokenType::QUESTION, start_line, start_column))
}
Some(':') => {
self.advance();
Ok(Token::new(TokenType::COLON, start_line, start_column))
}
Some('\n') => { Some('\n') => {
self.advance(); self.advance();
Ok(Token::new(TokenType::NEWLINE, start_line, start_column)) Ok(Token::new(TokenType::NEWLINE, start_line, start_column))

View File

@ -0,0 +1,19 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")"/.. && pwd)
BIN="$ROOT_DIR/target/release/nyash"
if [[ ! -x "$BIN" ]]; then
(cd "$ROOT_DIR" && cargo build --release >/dev/null)
fi
run() {
NYASH_VM_USE_PY=1 NYASH_SYNTAX_SUGAR_LEVEL=basic "$BIN" --backend vm "$ROOT_DIR/apps/tests/array_literal_basic.nyash" 2>&1
}
OUT=$(run || true)
echo "$OUT" | rg -q '^3$' && echo "✅ PyVM: array literal basic" || { echo "❌ PyVM: array literal basic"; echo "$OUT"; exit 1; }
echo "Array literal smoke PASS" >&2

View File

@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")"/.. && pwd)
BIN="$ROOT_DIR/target/release/nyash"
if [[ ! -x "$BIN" ]]; then
(cd "$ROOT_DIR" && cargo build --release >/dev/null)
fi
run() {
NYASH_VM_USE_PY=1 NYASH_SYNTAX_SUGAR_LEVEL=full NYASH_ENABLE_MAP_LITERAL=1 "$BIN" --backend vm "$ROOT_DIR/apps/tests/map_literal_ident_key.nyash" 2>&1
}
OUT=$(run || true)
echo "$OUT" | rg -q '^2$' && echo "$OUT" | rg -q '^A$' \
&& echo "✅ PyVM: map literal ident-key" || { echo "❌ PyVM: map literal ident-key"; echo "$OUT"; exit 1; }
echo "Map literal ident-key smoke PASS" >&2

View File

@ -0,0 +1,20 @@
#!/usr/bin/env bash
set -euo pipefail
ROOT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")"/.. && pwd)
BIN="$ROOT_DIR/target/release/nyash"
if [[ ! -x "$BIN" ]]; then
(cd "$ROOT_DIR" && cargo build --release >/dev/null)
fi
run() {
NYASH_VM_USE_PY=1 NYASH_SYNTAX_SUGAR_LEVEL=basic NYASH_ENABLE_MAP_LITERAL=1 "$BIN" --backend vm "$ROOT_DIR/apps/tests/map_literal_basic.nyash" 2>&1
}
OUT=$(run || true)
echo "$OUT" | rg -q '^2$' && echo "$OUT" | rg -q '^Alice$' \
&& echo "✅ PyVM: map literal basic" || { echo "❌ PyVM: map literal basic"; echo "$OUT"; exit 1; }
echo "Map literal smoke PASS" >&2