diff --git a/CURRENT_TASK.md b/CURRENT_TASK.md index f3a3ab4a..e9eee63f 100644 --- a/CURRENT_TASK.md +++ b/CURRENT_TASK.md @@ -5,6 +5,21 @@ TL;DR - 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変更なし、デシュガリング)。 + +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。 @@ -64,6 +79,22 @@ Notes / Policies - Bridge は JSON v0 → MIR 降下で PHI を生成(Phase‑15 中は現行方式を維持)。 - 配布/バンドル/EXE 化は任意の実験導線として維持(Phase‑15 の主目的外)。 +Smoke Snapshot (2025‑09‑15) +- 修正: `runner/dispatch.rs` に `vm` 分岐が欠落しており `--backend vm` が interpreter にフォールバックしていたため、PyVM スモークが作動せず。分岐を追加して復旧済み。 +- PyVM Stage‑2 部分結果: + - 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 (2025‑09‑15) +- LoopBuilder If 降下に φ 正規化を追加(両枝代入の変数を merge 時に φ で束ねて再束縛)。 +- PyVM φ 解決ロジックを安定化(incoming を [value, pred] 形に限定し、[pred, value] の曖昧推測を削除)。偶然一致による誤選択を排除。 +- これにより `tools/pyvm_stage2_smoke.sh` は全 PASS を確認済み。 + Refactor Candidates (early plan) - runner/mod.rs(~70K chars): “runner pipeline” を用途別に分割(TODO #15) - runner/pipeline.rs(入力正規化/using解決/環境注入) @@ -91,3 +122,38 @@ Recommended Next (short list) - `builder/vars.rs` に SSA 変数正規化の小物を段階追加(変数名再束縛/スコープ終端の型ヒント伝搬など)。 - Runner(仕上げ) - `mod.rs` の残置ヘルパ(usingの候補提示・環境注入ログ)を `pipeline/dispatch` へ集約し、`mod.rs` を最小のオーケストレーションに。 + - Namespaces Phase‑1(実装着手): BoxIndex 構築・3段階解決・toml aliases・曖昧エラー改善・トレース + +Array/Map Literals Plan(Syntax Sugar) +- Stage‑1: 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回性)。 +- Stage‑2: 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/順序検証)。 +- Stage‑3: 識別子キー糖 `{name: v}` と末尾カンマを強化(任意)。 + +Gates / Semantics +- 左から右で評価(一度だけ)。push/set 失敗は即時伝播(既存 BoxCall 規約に追従)。 +- IR 変更なし(BoxCall/MethodCall のみ)。将来 `with_capacity(n)` 最適化は任意で追加。 + +Decision Log (2025‑09‑15) +- 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.toml(MVP): `[imports]`/`[aliases]`,`[plugins.] { path, prefix, require_prefix, expose_short_names }` +- Index とキャッシュ(Runner): + - BoxIndex: `local_boxes`, `plugin_boxes`, `aliases` を保持 + - `RESOLVE_CACHE`(thread‑local)で同一解決の再計算を回避 + - `NYASH_RESOLVE_TRACE=1` で解決過程をログ出力 + + - スモークが緑=基礎健全性確認後に、静的ノイズの除去を安全に一気通貫で行う。 diff --git a/apps/tests/array_literal_basic.nyash b/apps/tests/array_literal_basic.nyash new file mode 100644 index 00000000..a083548c --- /dev/null +++ b/apps/tests/array_literal_basic.nyash @@ -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 + } +} + diff --git a/apps/tests/map_literal_basic.nyash b/apps/tests/map_literal_basic.nyash new file mode 100644 index 00000000..00e128e7 --- /dev/null +++ b/apps/tests/map_literal_basic.nyash @@ -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 + } +} + diff --git a/apps/tests/map_literal_ident_key.nyash b/apps/tests/map_literal_ident_key.nyash new file mode 100644 index 00000000..e901b25c --- /dev/null +++ b/apps/tests/map_literal_ident_key.nyash @@ -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 + } +} + diff --git a/docs/reference/language/EBNF.md b/docs/reference/language/EBNF.md index 4aa82423..cbe145bb 100644 --- a/docs/reference/language/EBNF.md +++ b/docs/reference/language/EBNF.md @@ -24,6 +24,10 @@ factor := INT | IDENT call_tail* | '(' expr ')' | 'new' IDENT '(' args? ')' + | '[' args? ']' ; Array literal (Stage‑1 sugar, gated) + | '{' map_entries? '}' ; Map literal (Stage‑2 sugar, gated) + +map_entries := (STRING | IDENT) ':' expr (',' (STRING | IDENT) ':' expr)* [','] call_tail := '.' IDENT '(' args? ')' ; method | '(' args? ')' ; function call @@ -35,4 +39,6 @@ Notes - Short-circuit: '&&' and '||' must not evaluate the RHS when not needed. - Unary minus has higher precedence than '*' and '/'. - 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 Stage‑3 and require either NYASH_SYNTAX_SUGAR_LEVEL=full or NYASH_ENABLE_MAP_IDENT_KEY=1. diff --git a/docs/reference/language/using.md b/docs/reference/language/using.md index cb993178..53377d41 100644 --- a/docs/reference/language/using.md +++ b/docs/reference/language/using.md @@ -1,4 +1,4 @@ -# using — Imports and Namespaces (Phase 15) +# using — Imports and Namespaces (Phase 15+) Status: Accepted (Runner‑side resolution). Selfhost parser accepts using as no‑op and attaches `meta.usings` for future use. @@ -9,6 +9,50 @@ Policy - 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. +## Namespace Resolution (Runner‑side) +- 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` (file‑scoped alias) + +## Plugins +- Unified namespace with Boxes. Prefer short names when unique. +- Qualified form: `network.HttpClient` +- Per‑plugin 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.]`: `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, + plugin_boxes: HashMap>, + aliases: HashMap, +} +``` +- Maintain a small resolve cache per thread: +``` +thread_local! { + static RESOLVE_CACHE: RefCell> = /* ... */; +} +``` +- Trace: `NYASH_RESOLVE_TRACE=1` prints resolution steps (for debugging/CI logs). + Syntax - Namespace: `using core.std` or `using core.std as Std` - 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 - Enable using pre‑processing: `NYASH_ENABLE_USING=1` - 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 - Phase 15 keeps resolution in the Runner to minimize parser complexity. Future phases may leverage `meta.usings` for compiler decisions. - Unknown fields in the top‑level JSON (like `meta`) are ignored by the current bridge. - diff --git a/src/ast.rs b/src/ast.rs index 0ad6a6b8..5213210c 100644 --- a/src/ast.rs +++ b/src/ast.rs @@ -201,6 +201,7 @@ pub enum ExpressionNode { else_expr: Box, span: Span, }, + // (Stage‑2 sugar for literals is represented in unified ASTNode, not here) } /// 文ノード - 実行可能なアクション @@ -476,6 +477,16 @@ pub enum ASTNode { else_expr: Box, span: Span, }, + /// 配列リテラル(糖衣): [e1, e2, ...] + ArrayLiteral { + elements: Vec, + span: Span, + }, + /// マップリテラル(糖衣): { "k": v, ... } (Stage‑2: 文字列キー限定) + MapLiteral { + entries: Vec<(String, ASTNode)>, + span: Span, + }, /// 無名関数(最小P1: 値としてのみ。呼び出しは未対応) Lambda { @@ -708,6 +719,8 @@ impl ASTNode { ASTNode::QMarkPropagate { .. } => "QMarkPropagate", ASTNode::PeekExpr { .. } => "PeekExpr", ASTNode::Lambda { .. } => "Lambda", + ASTNode::ArrayLiteral { .. } => "ArrayLiteral", + ASTNode::MapLiteral { .. } => "MapLiteral", } } @@ -740,6 +753,8 @@ impl ASTNode { ASTNode::PeekExpr { .. } => ASTNodeType::Expression, ASTNode::QMarkPropagate { .. } => ASTNodeType::Expression, ASTNode::Lambda { .. } => ASTNodeType::Expression, + ASTNode::ArrayLiteral { .. } => ASTNodeType::Expression, + ASTNode::MapLiteral { .. } => ASTNodeType::Expression, // Statement nodes - 実行可能なアクション ASTNode::Program { .. } => ASTNodeType::Statement, // プログラム全体 @@ -903,6 +918,12 @@ impl ASTNode { ASTNode::Lambda { params, body, .. } => { 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::QMarkPropagate { span, .. } => *span, ASTNode::Lambda { span, .. } => *span, + ASTNode::ArrayLiteral { span, .. } => *span, + ASTNode::MapLiteral { span, .. } => *span, } } } diff --git a/src/backend/vm_exec.rs b/src/backend/vm_exec.rs index 0147e6dd..02b7c464 100644 --- a/src/backend/vm_exec.rs +++ b/src/backend/vm_exec.rs @@ -135,9 +135,9 @@ impl VM { /// Execute a single function pub(super) fn execute_function(&mut self, function: &MirFunction) -> Result { - use crate::box_trait::{StringBox, IntegerBox, BoolBox, VoidBox}; + // unused: local downcasts not required here use crate::runtime::global_hooks; - use crate::instance_v2::InstanceBox; + // use crate::instance_v2::InstanceBox; // not used in this path use super::control_flow; self.current_function = Some(function.signature.name.clone()); diff --git a/src/backend/vm_instructions/boxcall.rs b/src/backend/vm_instructions/boxcall.rs index 8df56ca9..7cc0f011 100644 --- a/src/backend/vm_instructions/boxcall.rs +++ b/src/backend/vm_instructions/boxcall.rs @@ -1,4 +1,3 @@ -use crate::box_trait::NyashBox; use crate::mir::ValueId; use crate::backend::vm::ControlFlow; use crate::backend::{VM, VMError, VMValue}; @@ -7,7 +6,7 @@ impl VM { /// Small helpers to reduce duplication in vtable stub paths. #[inline] fn vmvalue_to_box(val: &VMValue) -> Box { - 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 { VMValue::Integer(i) => Box::new(IBox::new(*i)), VMValue::String(s) => Box::new(SBox::new(s)), @@ -900,3 +899,4 @@ impl VM { Ok(None) } } +use crate::box_trait::NyashBox; diff --git a/src/backend/vm_instructions/core.rs b/src/backend/vm_instructions/core.rs index e36d528b..e812af63 100644 --- a/src/backend/vm_instructions/core.rs +++ b/src/backend/vm_instructions/core.rs @@ -1,5 +1,5 @@ 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 std::sync::Arc; use crate::backend::vm::ControlFlow; diff --git a/src/backend/vm_state.rs b/src/backend/vm_state.rs index 7e153869..bf4a12f4 100644 --- a/src/backend/vm_state.rs +++ b/src/backend/vm_state.rs @@ -12,7 +12,7 @@ use crate::mir::{BasicBlockId, ValueId}; use crate::runtime::NyashRuntime; use crate::scope_tracker::ScopeTracker; use std::collections::HashMap; -use std::time::Instant; +// use std::time::Instant; // not used in this module impl VM { fn jit_threshold_from_env() -> u32 { diff --git a/src/interpreter/expressions/builtins.rs b/src/interpreter/expressions/builtins.rs index ebfa5b9a..c80d3ed5 100644 --- a/src/interpreter/expressions/builtins.rs +++ b/src/interpreter/expressions/builtins.rs @@ -47,7 +47,7 @@ impl NyashInterpreter { } "MathBox" => { 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. // Here we simply return void; method paths should prefer plugin invoke in VM. return Ok(Box::new(VoidBox::new())); diff --git a/src/jit/lower/core.rs b/src/jit/lower/core.rs index 3f6469f9..b2e0a099 100644 --- a/src/jit/lower/core.rs +++ b/src/jit/lower/core.rs @@ -1,5 +1,5 @@ -use crate::mir::{MirFunction, MirInstruction, ConstValue, BinaryOp, CompareOp, ValueId}; -use super::builder::{IRBuilder, BinOpKind, CmpKind}; +use crate::mir::{MirFunction, MirInstruction, ConstValue, ValueId}; +use super::builder::{IRBuilder, BinOpKind}; mod analysis; mod cfg; diff --git a/src/jit/lower/core/analysis.rs b/src/jit/lower/core/analysis.rs index 7229b8b0..3998b569 100644 --- a/src/jit/lower/core/analysis.rs +++ b/src/jit/lower/core/analysis.rs @@ -1,9 +1,8 @@ -use std::collections::{HashMap, HashSet, BTreeSet}; +use std::collections::HashSet; use crate::mir::{BasicBlockId, MirFunction, MirInstruction, ValueId}; -use super::super::builder::IRBuilder; -use super::super::core_ops; // ensure module link remains +// removed unused imports use super::LowerCore; impl LowerCore { @@ -114,4 +113,3 @@ impl LowerCore { } } } - diff --git a/src/llvm_py/pyvm/vm.py b/src/llvm_py/pyvm/vm.py index 7182c25f..1b5c0270 100644 --- a/src/llvm_py/pyvm/vm.py +++ b/src/llvm_py/pyvm/vm.py @@ -127,6 +127,9 @@ class PyVM: # incoming: prefer [[vid, pred_bid]], but accept [pred_bid, vid] robustly incoming = inst.get("incoming", []) 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: if not isinstance(pair, (list, tuple)) or len(pair) < 2: continue @@ -134,10 +137,8 @@ class PyVM: # Case 1: [vid, pred] if prev is not None and int(b) == int(prev) and int(a) in regs: chosen = regs.get(int(a)) - break - # Case 2: [pred, vid] - if prev is not None and int(a) == int(prev) and int(b) in regs: - chosen = regs.get(int(b)) + if dbg: + print(f"[pyvm.phi] case1 match: use v{a} from pred {b} -> {chosen}") break if chosen is None and incoming: # Fallback to first element that resolves to a known vid @@ -147,8 +148,9 @@ class PyVM: a, b = pair[0], pair[1] if int(a) in regs: chosen = regs.get(int(a)); break - if int(b) in regs: - chosen = regs.get(int(b)); break + # Do not try to resolve by assuming [pred, vid] — avoid false matches + if dbg: + print(f"[pyvm.phi] chosen={chosen}") self._set(regs, inst.get("dst"), chosen) i += 1 continue diff --git a/src/mir/builder.rs b/src/mir/builder.rs index 4e3eb771..7da10858 100644 --- a/src/mir/builder.rs +++ b/src/mir/builder.rs @@ -487,6 +487,17 @@ impl MirBuilder { 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 ASTNode::Nowait { variable, expression, .. } => { self.build_nowait_statement(variable.clone(), *expression.clone()) diff --git a/src/mir/builder/exprs.rs b/src/mir/builder/exprs.rs index a5fe4ab1..c03fc134 100644 --- a/src/mir/builder/exprs.rs +++ b/src/mir/builder/exprs.rs @@ -114,6 +114,28 @@ impl super::MirBuilder { ASTNode::New { class, arguments, .. } => 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, .. } => self.build_nowait_statement(variable.clone(), *expression.clone()), diff --git a/src/mir/loop_builder.rs b/src/mir/loop_builder.rs index c63b73ec..3db9d68c 100644 --- a/src/mir/loop_builder.rs +++ b/src/mir/loop_builder.rs @@ -42,6 +42,29 @@ pub struct LoopBuilder<'a> { continue_snapshots: Vec<(BasicBlockId, HashMap)>, } +// Local copy: detect a variable name assigned within an AST fragment +fn extract_assigned_var_local(ast: &ASTNode) -> Option { + match ast { + ASTNode::Assignment { target, .. } => { + if let ASTNode::Variable { name, .. } = target.as_ref() { Some(name.clone()) } else { None } + } + ASTNode::Program { statements, .. } => statements.last().and_then(|st| extract_assigned_var_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> { /// 新しいループビルダーを作成 pub fn new(parent: &'a mut super::builder::MirBuilder) -> Self { @@ -325,6 +348,10 @@ impl<'a> LoopBuilder<'a> { let merge_bb = self.new_block(); 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 self.set_current_block(then_bb)?; for s in then_body.iter().cloned() { @@ -338,6 +365,7 @@ impl<'a> LoopBuilder<'a> { }; 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) { let cur_id = self.current_block()?; @@ -351,7 +379,8 @@ impl<'a> LoopBuilder<'a> { // else self.set_current_block(else_bb)?; - if let Some(es) = else_body { + let mut else_var_map_end_opt: Option> = None; + if let Some(es) = else_body.clone() { for s in es.into_iter() { let _ = self.build_statement(s)?; let cur_id = self.current_block()?; @@ -362,6 +391,7 @@ impl<'a> LoopBuilder<'a> { }; if terminated { break; } } + else_var_map_end_opt = Some(self.get_current_variable_map()); } { let cur_id = self.current_block()?; @@ -375,6 +405,29 @@ impl<'a> LoopBuilder<'a> { // Continue at merge 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(); self.emit_const(void_id, ConstValue::Void)?; Ok(void_id) diff --git a/src/parser/expressions.rs b/src/parser/expressions.rs index b0f45f32..0362ddbb 100644 --- a/src/parser/expressions.rs +++ b/src/parser/expressions.rs @@ -631,9 +631,71 @@ impl NyashParser { Ok(expr) } - /// 基本式をパース: リテラル、変数、括弧、this、new + /// 基本式をパース: リテラル、変数、括弧、this、new、配列リテラル(糖衣) fn parse_primary(&mut self) -> Result { 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 = 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 (Stage‑2, 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 (Stage‑2) or identifier key sugar (Stage‑3; 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 => { // Allow include as an expression: include "path" self.parse_include() diff --git a/src/runner/dispatch.rs b/src/runner/dispatch.rs index 68ea012b..4711c722 100644 --- a/src/runner/dispatch.rs +++ b/src/runner/dispatch.rs @@ -86,6 +86,12 @@ pub(crate) fn execute_file_with_backend(runner: &NyashRunner, filename: &str) { } 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")] "jit-direct" => { if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1") { diff --git a/src/runner/mod.rs b/src/runner/mod.rs index 78cf016a..1ca8bce3 100644 --- a/src/runner/mod.rs +++ b/src/runner/mod.rs @@ -6,20 +6,8 @@ */ use nyash_rust::cli::CliConfig; -use nyash_rust::{ - box_trait::{StringBox, IntegerBox, BoolBox, VoidBox, AddBox}, - 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; +// prune heavy unused imports here; modules import what they need locally +// pruned unused runtime imports in this module #[cfg(feature = "wasm-backend")] use nyash_rust::backend::{wasm::WasmBackend, aot::AotBackend}; diff --git a/src/runner/modes/vm.rs b/src/runner/modes/vm.rs index e90c90f8..67fcd3c8 100644 --- a/src/runner/modes/vm.rs +++ b/src/runner/modes/vm.rs @@ -183,7 +183,7 @@ impl NyashRunner { // 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") { 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; match &func.signature.return_type { MirType::Float => { @@ -272,7 +272,7 @@ impl NyashRunner { format!("./{}", filename) } - use std::collections::{HashSet, VecDeque}; + use std::collections::HashSet; fn walk_with_state(node: &ASTNode, runtime: &NyashRuntime, stack: &mut Vec, visited: &mut HashSet) { match node { diff --git a/src/runtime/plugin_loader_v2/enabled/loader.rs b/src/runtime/plugin_loader_v2/enabled/loader.rs index 72866f98..9cc0de0d 100644 --- a/src/runtime/plugin_loader_v2/enabled/loader.rs +++ b/src/runtime/plugin_loader_v2/enabled/loader.rs @@ -1,6 +1,6 @@ -use super::types::{PluginBoxV2, PluginHandleInner, NyashTypeBoxFfi, LoadedPluginV2}; +use super::types::{PluginBoxV2, PluginHandleInner, LoadedPluginV2}; 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 std::collections::HashMap; use std::sync::{Arc, RwLock}; diff --git a/src/tokenizer.rs b/src/tokenizer.rs index 602a51d7..09ba1c8d 100644 --- a/src/tokenizer.rs +++ b/src/tokenizer.rs @@ -94,6 +94,8 @@ pub enum TokenType { DoubleColon, // :: (Parent::method) - P1用(定義のみ) LPAREN, // ( RPAREN, // ) + LBRACK, // [ + RBRACK, // ] LBRACE, // { RBRACE, // } COMMA, // , @@ -395,6 +397,14 @@ impl NyashTokenizer { self.advance(); 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('{') => { self.advance(); Ok(Token::new(TokenType::LBRACE, start_line, start_column)) @@ -407,14 +417,7 @@ impl NyashTokenizer { self.advance(); Ok(Token::new(TokenType::COMMA, start_line, start_column)) } - Some('?') => { - self.advance(); - Ok(Token::new(TokenType::QUESTION, start_line, start_column)) - } - Some(':') => { - self.advance(); - Ok(Token::new(TokenType::COLON, start_line, start_column)) - } + // '?' and ':' are handled earlier (including variants); avoid duplicate arms Some('\n') => { self.advance(); Ok(Token::new(TokenType::NEWLINE, start_line, start_column)) diff --git a/tools/pyvm_array_literal_smoke.sh b/tools/pyvm_array_literal_smoke.sh new file mode 100644 index 00000000..8a62d131 --- /dev/null +++ b/tools/pyvm_array_literal_smoke.sh @@ -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 + diff --git a/tools/pyvm_map_literal_ident_smoke.sh b/tools/pyvm_map_literal_ident_smoke.sh new file mode 100644 index 00000000..81d1e922 --- /dev/null +++ b/tools/pyvm_map_literal_ident_smoke.sh @@ -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 + diff --git a/tools/pyvm_map_literal_smoke.sh b/tools/pyvm_map_literal_smoke.sh new file mode 100644 index 00000000..62342d4d --- /dev/null +++ b/tools/pyvm_map_literal_smoke.sh @@ -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 +