diff --git a/CURRENT_TASK.md b/CURRENT_TASK.md index f6b9935c..679cacf0 100644 --- a/CURRENT_TASK.md +++ b/CURRENT_TASK.md @@ -1,10 +1,55 @@ # Current Task (2025-09-14 改定) — Phase 15 llvmlite(既定)+ PyVM(新規) +Quick Summary(反映内容) +- 実装済み: peek CFG修正、llvmlite文字列ブリッジ、混在‘+’結合、追加テスト緑。 +- JSON v0 Bridge拡張: Stmt(Expr/Local/If/Loop)、Expr(Call/Method/New/Var) 降下を実装。 +- PHI合流: If/Loop の PHI 合流を Bridge 側で実装(If: then/else→merge、Loop: preheader/loop‑latch→header)。 +- PythonパーサMVP: Stage‑2 サブセット(local/if/loop/call/method/new/var ほか)を出力。 +- Outstanding: then/else 片側のみで新規生成された変数のスコープ(現在は外へ未伝播)。 +- Next(handoff): Stage‑2 E2E緑化→me の扱い検討(Method糖衣)。 +- How to run: 下の手順に確認コマンドを追記。 + +Hot Update — 2025‑09‑14(JSON v0 Bridge/Parser Stage‑2 + Parity hardening) +- llvmlite/PyVM parity 強化(緑維持): + - peek の MIR 降下を修正(entry→dispatch 明示 jump、then/else/merge 正規 CFG、全ブロック終端保障)。 + - console.* の文字列引数を to_i8p_h ブリッジで正規化(from_i8_string 直呼び回避)。 + - “文字列+数値” 混在 ‘+’ を concat_si/is+from_i8_string による橋渡しで安定化、両辺文字列は concat_hh。 + - 代表追加テスト(緑): string_ops_basic / me_method_call / loop_if_phi。 + +- JSON v0 Bridge(Option A)の受け口を Stage‑2 方向に拡張(src/runner/json_v0_bridge.rs) + - StmtV0: Expr / Local / If / Loop を追加。If/Loop は実ブロック生成(then/else/merge、cond/body/exit)まで実装、未終端 Jump 補完、最後に未終端ブロックへ ret 0 補完。 + - ExprV0: Call / Method / New / Var を追加。Lowering: Call→Const+Call、Method→BoxCall、New→NewBox、Var→簡易 var_map 解決。 + - 現状の制限: 変数の合流(PHI)は未実装(If/Loop 内で同名 Local を更新→外側参照する場合の値統合)。 + +- Python Parser MVP(tools/ny_parser_mvp.py)を Stage‑2 サブセットへ拡張 + - 構文: local / if / loop / call / method / new / var / 比較(==,!=,<,>,<=,>=)/ 論理(&&,||)/ 算術。 + - 出力: 上記に対応する JSON v0(Bridge と互換)。 + - 確認: 小さな local/return ケースは JSON→Bridge→MIR で実行可。If/Loop の合流は Bridge 側 PHI 実装後に E2E 緑化予定。 + +Outstanding(要対応) +- PHI 合流(JSON v0 ブリッジ側) + - If: then/else で同名 Local を更新した変数を merge で Phi 統合(なければ片側/既存値を採用)。 + - Loop: header で初期値と body 末端の更新を Phi 化(latch→header)。 + - 変数スコープ: 現状は簡易 var_map。PHI 決定時に then_vars / else_vars / 事前値の差分から対象を検出。 + +Next(handoff short plan) +1) JSON Bridge: PHI 合流実装(If/Loop)と最小テスト追加(ループ後/if後の変数参照)。 +2) Parser MVP: Stage‑2 生成 JSON の if/loop ケースで Bridge→MIR→PyVM/llvmlite の parity 緑化。 +3) me の扱い検討(当面は Method 降下で十分。必要なら me→Main.method/N 直呼シンタックスを Bridge 側で糖衣対応)。 + +How to run(現状確認) +- Build(release): `cargo build --release`(必要に応じて `NYASH_CLI_VERBOSE=1`)。 +- Parser MVP RT(算術/return): `./tools/ny_parser_mvp_roundtrip.sh`(緑)。 +- Bridge(JSON v0 パイプ): `echo '{...}' | target/release/nyash --ny-parser-pipe`。 +- Parser Stage‑2 → Bridge: `python3 tools/ny_parser_mvp.py tmp/sample.ny | target/release/nyash --ny-parser-pipe`。 +- Bridge smoke(一式): `./tools/ny_parser_bridge_smoke.sh`(pipe/--json-file の両経路)。 +- Stage‑2 PHI smoke: `./tools/ny_parser_stage2_phi_smoke.sh`(If/Loop の PHI 合流検証)。 + Context Snapshot — Open After Reset - Status: A6 受入(PyVM↔llvmlite parity + LLVM verify→.o→EXE)完了。 - Lang: peek ブロック式(最後の式が値)OK、式文フォールバックOK、三項(?:)パーサ導入済み(VM E2E 緑)。 - Docs: 言語/アーキの入口を整備(guides/language-guide.md, reference/language/**, reference/architecture/**)。 -- Parser MVP: Python Stage‑1 実装 + roundtrip スモーク緑。Nyash 実装スケルトン配置済み。 +- Parser MVP: Python Stage‑2 サブセットまで拡張(Stage‑1 roundtrip 緑維持)。Nyash 実装スケルトン配置済み。 Next (short) 1) 三項(?:)の PyVM/llvmlite パリティE2E(`tools/parity.sh`) diff --git a/app_parity_loop_if_phi b/app_parity_loop_if_phi new file mode 100644 index 00000000..ae3a77cf Binary files /dev/null and b/app_parity_loop_if_phi differ diff --git a/app_parity_me_method_call b/app_parity_me_method_call new file mode 100644 index 00000000..99280033 Binary files /dev/null and b/app_parity_me_method_call differ diff --git a/app_parity_string_ops_basic b/app_parity_string_ops_basic new file mode 100644 index 00000000..a26f03db Binary files /dev/null and b/app_parity_string_ops_basic differ diff --git a/apps/tests/loop_if_phi.nyash b/apps/tests/loop_if_phi.nyash new file mode 100644 index 00000000..e5d92d54 --- /dev/null +++ b/apps/tests/loop_if_phi.nyash @@ -0,0 +1,14 @@ +static box Main { + main(args) { + local console = new ConsoleBox() + local i = 1 + local sum = 0 + loop(i <= 5) { + if (i % 2 == 1) { sum = sum + i } else { sum = sum + 0 } + i = i + 1 + } + console.println("sum=" + sum) + return 0 + } +} + diff --git a/apps/tests/me_method_call.nyash b/apps/tests/me_method_call.nyash new file mode 100644 index 00000000..1642a502 --- /dev/null +++ b/apps/tests/me_method_call.nyash @@ -0,0 +1,12 @@ +static box Main { + helper(s) { + return s.length() + } + main(args) { + local console = new ConsoleBox() + local n = me.helper("abc") + console.println("n=" + n) + return 0 + } +} + diff --git a/apps/tests/string_ops_basic.nyash b/apps/tests/string_ops_basic.nyash new file mode 100644 index 00000000..ad6085b8 --- /dev/null +++ b/apps/tests/string_ops_basic.nyash @@ -0,0 +1,15 @@ +static box Main { + main(args) { + local console = new ConsoleBox() + local s = "abcde" + // length + console.println("len=" + s.length()) + // substring [1,4) -> bcd + local t = s.substring(1, 4) + console.println("sub=" + t) + // lastIndexOf("b") -> 1 + console.println("idx=" + s.lastIndexOf("b")) + return 0 + } +} + diff --git a/src/llvm_py/instructions/binop.py b/src/llvm_py/instructions/binop.py index 34171d62..0cc8c865 100644 --- a/src/llvm_py/instructions/binop.py +++ b/src/llvm_py/instructions/binop.py @@ -121,18 +121,101 @@ def lower_binop( return val return ir.Constant(i64, 0) - hl = to_handle(lhs_raw, lhs_val, 'l', lhs) - hr = to_handle(rhs_raw, rhs_val, 'r', rhs) - # concat_hh(handle, handle) -> handle - hh_fnty = ir.FunctionType(i64, [i64, i64]) - callee = None - for f in builder.module.functions: - if f.name == 'nyash.string.concat_hh': - callee = f; break - if callee is None: - callee = ir.Function(builder.module, hh_fnty, name='nyash.string.concat_hh') - res = builder.call(callee, [hl, hr], name=f"concat_hh_{dst}") - vmap[dst] = res + # Decide route: handle+handle when both sides are string-ish; otherwise pointer+int route. + lhs_tag = False; rhs_tag = False + try: + if resolver is not None and hasattr(resolver, 'is_stringish'): + lhs_tag = resolver.is_stringish(lhs) + rhs_tag = resolver.is_stringish(rhs) + except Exception: + pass + if lhs_tag and rhs_tag: + # Both sides string-ish: concat_hh(handle, handle) + hl = to_handle(lhs_raw, lhs_val, 'l', lhs) + hr = to_handle(rhs_raw, rhs_val, 'r', rhs) + hh_fnty = ir.FunctionType(i64, [i64, i64]) + callee = None + for f in builder.module.functions: + if f.name == 'nyash.string.concat_hh': + callee = f; break + if callee is None: + callee = ir.Function(builder.module, hh_fnty, name='nyash.string.concat_hh') + res = builder.call(callee, [hl, hr], name=f"concat_hh_{dst}") + vmap[dst] = res + else: + # Mixed string + non-string (e.g., "len=" + 5). Use pointer concat helpers then box. + i32 = ir.IntType(32); i8p = ir.IntType(8).as_pointer(); i64 = ir.IntType(64) + # Helper: to i8* pointer for stringish side + def to_i8p_from_vid(vid: int, raw, val, tag: str): + # If raw is pointer-to-array: GEP + if raw is not None and hasattr(raw, 'type') and isinstance(raw.type, ir.PointerType): + try: + if isinstance(raw.type.pointee, ir.ArrayType): + c0 = ir.Constant(i32, 0) + return builder.gep(raw, [c0, c0], name=f"bin_gep_{tag}_{dst}") + except Exception: + pass + # If we have a string handle: call to_i8p_h + to_i8p = None + for f in builder.module.functions: + if f.name == 'nyash.string.to_i8p_h': + to_i8p = f; break + if to_i8p is None: + to_i8p = ir.Function(builder.module, ir.FunctionType(i8p, [i64]), name='nyash.string.to_i8p_h') + # Ensure we pass an i64 handle + hv = val + if hv is None: + hv = ir.Constant(i64, 0) + if hasattr(hv, 'type') and isinstance(hv.type, ir.PointerType): + hv = builder.ptrtoint(hv, i64, name=f"bin_p2h_{tag}_{dst}") + elif hasattr(hv, 'type') and isinstance(hv.type, ir.IntType) and hv.type.width != 64: + hv = builder.zext(hv, i64, name=f"bin_zext_h_{tag}_{dst}") + return builder.call(to_i8p, [hv], name=f"bin_h2p_{tag}_{dst}") + + # Resolve numeric side as i64 value + def as_i64(val): + if val is None: + return ir.Constant(i64, 0) + if hasattr(val, 'type') and isinstance(val.type, ir.PointerType): + return builder.ptrtoint(val, i64, name=f"bin_p2i_{dst}") + if hasattr(val, 'type') and isinstance(val.type, ir.IntType) and val.type.width != 64: + return builder.zext(val, i64, name=f"bin_zext_i_{dst}") + return val + + if lhs_tag: + lp = to_i8p_from_vid(lhs, lhs_raw, lhs_val, 'l') + ri = as_i64(rhs_val) + cf = None + for f in builder.module.functions: + if f.name == 'nyash.string.concat_si': + cf = f; break + if cf is None: + cf = ir.Function(builder.module, ir.FunctionType(i8p, [i8p, i64]), name='nyash.string.concat_si') + p = builder.call(cf, [lp, ri], name=f"concat_si_{dst}") + boxer = None + for f in builder.module.functions: + if f.name == 'nyash.box.from_i8_string': + boxer = f; break + if boxer is None: + boxer = ir.Function(builder.module, ir.FunctionType(i64, [i8p]), name='nyash.box.from_i8_string') + vmap[dst] = builder.call(boxer, [p], name=f"concat_box_{dst}") + else: + li = as_i64(lhs_val) + rp = to_i8p_from_vid(rhs, rhs_raw, rhs_val, 'r') + cf = None + for f in builder.module.functions: + if f.name == 'nyash.string.concat_is': + cf = f; break + if cf is None: + cf = ir.Function(builder.module, ir.FunctionType(i8p, [i64, i8p]), name='nyash.string.concat_is') + p = builder.call(cf, [li, rp], name=f"concat_is_{dst}") + boxer = None + for f in builder.module.functions: + if f.name == 'nyash.box.from_i8_string': + boxer = f; break + if boxer is None: + boxer = ir.Function(builder.module, ir.FunctionType(i64, [i8p]), name='nyash.box.from_i8_string') + vmap[dst] = builder.call(boxer, [p], name=f"concat_box_{dst}") # Tag result as string handle so subsequent '+' stays in string domain try: if resolver is not None and hasattr(resolver, 'mark_string'): diff --git a/src/runner/json_v0_bridge.rs b/src/runner/json_v0_bridge.rs index 8093b23b..0fea754e 100644 --- a/src/runner/json_v0_bridge.rs +++ b/src/runner/json_v0_bridge.rs @@ -16,6 +16,14 @@ struct ProgramV0 { enum StmtV0 { Return { expr: ExprV0 }, Extern { iface: String, method: String, args: Vec }, + // Optional: expression statement (side effects only) + Expr { expr: ExprV0 }, + // Optional: local binding (Stage-2) + Local { name: String, expr: ExprV0 }, + // Optional: if/else (Stage-2) + If { cond: ExprV0, then: Vec, #[serde(rename="else", default)] r#else: Option> }, + // Optional: loop (Stage-2) + Loop { cond: ExprV0, body: Vec }, } #[derive(Debug, Deserialize, Serialize, Clone)] @@ -28,6 +36,11 @@ enum ExprV0 { Extern { iface: String, method: String, args: Vec }, Compare { op: String, lhs: Box, rhs: Box }, Logical { op: String, lhs: Box, rhs: Box }, // short-circuit: &&, || (or: "and"/"or") + // Stage-2 additions (optional): + Call { name: String, args: Vec }, + Method { recv: Box, method: String, args: Vec }, + New { class: String, args: Vec }, + Var { name: String }, } pub fn parse_json_v0_to_module(json: &str) -> Result { @@ -43,37 +56,20 @@ pub fn parse_json_v0_to_module(json: &str) -> Result { if prog.body.is_empty() { return Err("empty body".into()); } - // Lower all statements; capture last expression for return when the last is Return - let mut last_ret: Option<(crate::mir::ValueId, BasicBlockId)> = None; - for (i, stmt) in prog.body.iter().enumerate() { - match stmt { - StmtV0::Extern { iface, method, args } => { - // void extern call - let entry_bb = f.entry_block; - let (arg_ids, _cur) = lower_args(&mut f, entry_bb, args)?; - if let Some(bb) = f.get_block_mut(entry) { - bb.add_instruction(MirInstruction::ExternCall { dst: None, iface_name: iface.clone(), method_name: method.clone(), args: arg_ids, effects: EffectMask::IO }); - } - if i == prog.body.len()-1 { last_ret = None; } - } - StmtV0::Return { expr } => { - let entry_bb = f.entry_block; - last_ret = Some(lower_expr(&mut f, entry_bb, expr)?); - } - } - } - // Return last value (or 0) - if let Some((rv, cur)) = last_ret { - if let Some(bb) = f.get_block_mut(cur) { - bb.set_terminator(MirInstruction::Return { value: Some(rv) }); - } else { - return Err("invalid block when setting return".into()); - } - } else { + // Variable map for simple locals (Stage-2; currently minimal) + let mut var_map: std::collections::HashMap = std::collections::HashMap::new(); + let start_bb = f.entry_block; + let end_bb = lower_stmt_list_with_vars(&mut f, start_bb, &prog.body, &mut var_map)?; + // Ensure function terminates: add `ret 0` to last un-terminated block (prefer end_bb else entry) + let need_default_ret = f.blocks.iter().any(|(_k,b)| !b.is_terminated()); + if need_default_ret { + let target_bb = end_bb; let dst_id = f.next_value_id(); - if let Some(bb) = f.get_block_mut(entry) { - bb.add_instruction(MirInstruction::Const { dst: dst_id, value: ConstValue::Integer(0) }); - bb.set_terminator(MirInstruction::Return { value: Some(dst_id) }); + if let Some(bb) = f.get_block_mut(target_bb) { + if !bb.is_terminated() { + bb.add_instruction(MirInstruction::Const { dst: dst_id, value: ConstValue::Integer(0) }); + bb.set_terminator(MirInstruction::Return { value: Some(dst_id) }); + } } } // Keep return type unknown to allow dynamic display (VM/Interpreter) @@ -203,9 +199,359 @@ fn lower_expr(f: &mut MirFunction, cur_bb: BasicBlockId, e: &ExprV0) -> Result<( } Ok((out, merge_bb)) } + ExprV0::Call { name, args } => { + // Fallback: no vars context; treat as normal call + let (arg_ids, cur) = lower_args(f, cur_bb, args)?; + let fun_val = f.next_value_id(); + if let Some(bb) = f.get_block_mut(cur) { + bb.add_instruction(MirInstruction::Const { dst: fun_val, value: ConstValue::String(name.clone()) }); + } + let dst = f.next_value_id(); + if let Some(bb) = f.get_block_mut(cur) { + bb.add_instruction(MirInstruction::Call { dst: Some(dst), func: fun_val, args: arg_ids, effects: EffectMask::READ }); + } + Ok((dst, cur)) + } + ExprV0::Method { recv, method, args } => { + let (recv_v, cur) = lower_expr(f, cur_bb, recv)?; + let (arg_ids, cur2) = lower_args(f, cur, args)?; + let dst = f.next_value_id(); + if let Some(bb) = f.get_block_mut(cur2) { + bb.add_instruction(MirInstruction::BoxCall { dst: Some(dst), box_val: recv_v, method: method.clone(), method_id: None, args: arg_ids, effects: EffectMask::READ }); + } + Ok((dst, cur2)) + } + ExprV0::New { class, args } => { + let (arg_ids, cur) = lower_args(f, cur_bb, args)?; + let dst = f.next_value_id(); + if let Some(bb) = f.get_block_mut(cur) { + bb.add_instruction(MirInstruction::NewBox { dst, box_type: class.clone(), args: arg_ids }); + } + Ok((dst, cur)) + } + ExprV0::Var { name } => Err(format!("undefined variable in this context: {}", name)), } } +fn lower_expr_with_vars( + f: &mut MirFunction, + cur_bb: BasicBlockId, + e: &ExprV0, + vars: &mut std::collections::HashMap, +) -> Result<(crate::mir::ValueId, BasicBlockId), String> { + match e { + ExprV0::Var { name } => { + if let Some(&vid) = vars.get(name) { + Ok((vid, cur_bb)) + } else { + Err(format!("undefined variable: {}", name)) + } + } + ExprV0::Call { name, args } => { + // Lower args + let (arg_ids, cur) = lower_args_with_vars(f, cur_bb, args, vars)?; + // Encode as: const fun_name; call + let fun_val = f.next_value_id(); + if let Some(bb) = f.get_block_mut(cur) { + bb.add_instruction(MirInstruction::Const { dst: fun_val, value: ConstValue::String(name.clone()) }); + } + let dst = f.next_value_id(); + if let Some(bb) = f.get_block_mut(cur) { + bb.add_instruction(MirInstruction::Call { dst: Some(dst), func: fun_val, args: arg_ids, effects: EffectMask::READ }); + } + Ok((dst, cur)) + } + ExprV0::Method { recv, method, args } => { + let (recv_v, cur) = lower_expr_with_vars(f, cur_bb, recv, vars)?; + let (arg_ids, cur2) = lower_args_with_vars(f, cur, args, vars)?; + let dst = f.next_value_id(); + if let Some(bb) = f.get_block_mut(cur2) { + bb.add_instruction(MirInstruction::BoxCall { dst: Some(dst), box_val: recv_v, method: method.clone(), method_id: None, args: arg_ids, effects: EffectMask::READ }); + } + Ok((dst, cur2)) + } + ExprV0::New { class, args } => { + let (arg_ids, cur) = lower_args_with_vars(f, cur_bb, args, vars)?; + let dst = f.next_value_id(); + if let Some(bb) = f.get_block_mut(cur) { + bb.add_instruction(MirInstruction::NewBox { dst, box_type: class.clone(), args: arg_ids }); + } + Ok((dst, cur)) + } + ExprV0::Binary { op, lhs, rhs } => { + let (l, cur_after_l) = lower_expr_with_vars(f, cur_bb, lhs, vars)?; + let (r, cur_after_r) = lower_expr_with_vars(f, cur_after_l, rhs, vars)?; + let bop = match op.as_str() { "+" => BinaryOp::Add, "-" => BinaryOp::Sub, "*" => BinaryOp::Mul, "/" => BinaryOp::Div, _ => return Err("unsupported op".into()) }; + let dst = f.next_value_id(); + if let Some(bb) = f.get_block_mut(cur_after_r) { + bb.add_instruction(MirInstruction::BinOp { dst, op: bop, lhs: l, rhs: r }); + } + Ok((dst, cur_after_r)) + } + ExprV0::Compare { op, lhs, rhs } => { + let (l, cur_after_l) = lower_expr_with_vars(f, cur_bb, lhs, vars)?; + let (r, cur_after_r) = lower_expr_with_vars(f, cur_after_l, rhs, vars)?; + let cop = match op.as_str() { + "==" => crate::mir::CompareOp::Eq, + "!=" => crate::mir::CompareOp::Ne, + "<" => crate::mir::CompareOp::Lt, + "<=" => crate::mir::CompareOp::Le, + ">" => crate::mir::CompareOp::Gt, + ">=" => crate::mir::CompareOp::Ge, + _ => return Err("unsupported compare op".into()), + }; + let dst = f.next_value_id(); + if let Some(bb) = f.get_block_mut(cur_after_r) { + bb.add_instruction(MirInstruction::Compare { dst, op: cop, lhs: l, rhs: r }); + } + Ok((dst, cur_after_r)) + } + ExprV0::Logical { op, lhs, rhs } => { + let (l, cur_after_l) = lower_expr_with_vars(f, cur_bb, lhs, vars)?; + let rhs_bb = next_block_id(f); + let fall_bb = BasicBlockId::new(rhs_bb.0 + 1); + let merge_bb = BasicBlockId::new(rhs_bb.0 + 2); + f.add_block(crate::mir::BasicBlock::new(rhs_bb)); + f.add_block(crate::mir::BasicBlock::new(fall_bb)); + f.add_block(crate::mir::BasicBlock::new(merge_bb)); + let is_and = matches!(op.as_str(), "&&" | "and"); + if let Some(bb) = f.get_block_mut(cur_after_l) { + if is_and { + bb.set_terminator(MirInstruction::Branch { condition: l, then_bb: rhs_bb, else_bb: fall_bb }); + } else { + bb.set_terminator(MirInstruction::Branch { condition: l, then_bb: fall_bb, else_bb: rhs_bb }); + } + } + let cdst = f.next_value_id(); + if let Some(bb) = f.get_block_mut(fall_bb) { + let cval = if is_and { ConstValue::Bool(false) } else { ConstValue::Bool(true) }; + bb.add_instruction(MirInstruction::Const { dst: cdst, value: cval }); + bb.set_terminator(MirInstruction::Jump { target: merge_bb }); + } + let (rval, _rhs_end) = lower_expr_with_vars(f, rhs_bb, rhs, vars)?; + if let Some(bb) = f.get_block_mut(rhs_bb) { if !bb.is_terminated() { bb.set_terminator(MirInstruction::Jump { target: merge_bb }); } } + let out = f.next_value_id(); + if let Some(bb) = f.get_block_mut(merge_bb) { bb.insert_instruction_after_phis(MirInstruction::Phi { dst: out, inputs: vec![(rhs_bb, rval), (fall_bb, cdst)] }); } + Ok((out, merge_bb)) + } + _ => lower_expr(f, cur_bb, e), + } +} + +fn lower_stmt_with_vars( + f: &mut MirFunction, + cur_bb: BasicBlockId, + s: &StmtV0, + vars: &mut std::collections::HashMap, +) -> Result { + match s { + StmtV0::Return { expr } => { + let (v, cur) = lower_expr_with_vars(f, cur_bb, expr, vars)?; + if let Some(bb) = f.get_block_mut(cur) { bb.set_terminator(MirInstruction::Return { value: Some(v) }); } + Ok(cur) + } + StmtV0::Extern { iface, method, args } => { + let (arg_ids, cur) = lower_args_with_vars(f, cur_bb, args, vars)?; + if let Some(bb) = f.get_block_mut(cur) { bb.add_instruction(MirInstruction::ExternCall { dst: None, iface_name: iface.clone(), method_name: method.clone(), args: arg_ids, effects: EffectMask::IO }); } + Ok(cur) + } + StmtV0::Expr { expr } => { + let (_v, cur) = lower_expr_with_vars(f, cur_bb, expr, vars)?; Ok(cur) + } + StmtV0::Local { name, expr } => { + let (v, cur) = lower_expr_with_vars(f, cur_bb, expr, vars)?; vars.insert(name.clone(), v); Ok(cur) + } + StmtV0::If { cond, then, r#else } => { + // Lower condition first + let (cval, cur) = lower_expr_with_vars(f, cur_bb, cond, vars)?; + // Create then/else/merge blocks + let then_bb = next_block_id(f); + let else_bb = BasicBlockId::new(then_bb.0 + 1); + let merge_bb = BasicBlockId::new(then_bb.0 + 2); + f.add_block(crate::mir::BasicBlock::new(then_bb)); + f.add_block(crate::mir::BasicBlock::new(else_bb)); + f.add_block(crate::mir::BasicBlock::new(merge_bb)); + // Branch to then/else + if let Some(bb) = f.get_block_mut(cur) { + bb.set_terminator(MirInstruction::Branch { condition: cval, then_bb, else_bb }); + } + + // Clone current vars as branch-local maps + let base_vars = vars.clone(); + let mut then_vars = base_vars.clone(); + let tend = lower_stmt_list_with_vars(f, then_bb, then, &mut then_vars)?; + if let Some(bb) = f.get_block_mut(tend) { + if !bb.is_terminated() { bb.set_terminator(MirInstruction::Jump { target: merge_bb }); } + } + + let (else_end_pred, else_vars) = if let Some(elses) = r#else { + let mut ev = base_vars.clone(); + let eend = lower_stmt_list_with_vars(f, else_bb, elses, &mut ev)?; + if let Some(bb) = f.get_block_mut(eend) { + if !bb.is_terminated() { bb.set_terminator(MirInstruction::Jump { target: merge_bb }); } + } + (eend, ev) + } else { + // No else: empty path falls through with base vars + if let Some(bb) = f.get_block_mut(else_bb) { + bb.set_terminator(MirInstruction::Jump { target: merge_bb }); + } + (else_bb, base_vars.clone()) + }; + + // PHI merge at merge_bb + use std::collections::HashSet; + let mut names: HashSet = base_vars.keys().cloned().collect(); + // Also merge variables newly defined on both sides + for k in then_vars.keys() { names.insert(k.clone()); } + for k in else_vars.keys() { names.insert(k.clone()); } + + for name in names { + let tv = then_vars.get(&name).copied(); + let ev = else_vars.get(&name).copied(); + // Only propagate if variable exists on both paths or existed before + let exists_base = base_vars.contains_key(&name); + match (tv, ev, exists_base) { + (Some(tval), Some( eval), _) => { + let merged = if tval == eval { + tval + } else { + let dst = f.next_value_id(); + if let Some(bb) = f.get_block_mut(merge_bb) { + bb.insert_instruction_after_phis(MirInstruction::Phi { dst, inputs: vec![(tend, tval), (else_end_pred, eval)] }); + } + dst + }; + vars.insert(name, merged); + } + (Some(tval), None, true) => { + // Else path inherits base; merge then vs base + if let Some(&bval) = base_vars.get(&name) { + let merged = if tval == bval { tval } else { + let dst = f.next_value_id(); + if let Some(bb) = f.get_block_mut(merge_bb) { + bb.insert_instruction_after_phis(MirInstruction::Phi { dst, inputs: vec![(tend, tval), (else_end_pred, bval)] }); + } + dst + }; + vars.insert(name, merged); + } + } + (None, Some(eval), true) => { + // Then path inherits base; merge else vs base + if let Some(&bval) = base_vars.get(&name) { + let merged = if eval == bval { eval } else { + let dst = f.next_value_id(); + if let Some(bb) = f.get_block_mut(merge_bb) { + bb.insert_instruction_after_phis(MirInstruction::Phi { dst, inputs: vec![(tend, bval), (else_end_pred, eval)] }); + } + dst + }; + vars.insert(name, merged); + } + } + // If neither side has it, or only one side has it without base, skip (out-of-scope new var) + _ => {} + } + } + + Ok(merge_bb) + } + StmtV0::Loop { cond, body } => { + // Create loop blocks + let cond_bb = next_block_id(f); + let body_bb = BasicBlockId::new(cond_bb.0 + 1); + let exit_bb = BasicBlockId::new(cond_bb.0 + 2); + f.add_block(crate::mir::BasicBlock::new(cond_bb)); + f.add_block(crate::mir::BasicBlock::new(body_bb)); + f.add_block(crate::mir::BasicBlock::new(exit_bb)); + + // Preheader jump into cond + if let Some(bb) = f.get_block_mut(cur_bb) { + if !bb.is_terminated() { bb.add_instruction(MirInstruction::Jump { target: cond_bb }); } + } + + // Snapshot base vars and set up PHI placeholders at cond for loop-carried vars + let base_vars = vars.clone(); + let orig_names: Vec = base_vars.keys().cloned().collect(); + let mut phi_map: std::collections::HashMap = std::collections::HashMap::new(); + for name in &orig_names { + if let Some(&bval) = base_vars.get(name) { + let dst = f.next_value_id(); + if let Some(bb) = f.get_block_mut(cond_bb) { + // Initial incoming from preheader + bb.insert_instruction_after_phis(MirInstruction::Phi { dst, inputs: vec![(cur_bb, bval)] }); + } + phi_map.insert(name.clone(), dst); + } + } + // Redirect current vars to PHIs for use in cond/body + for (name, &phi) in &phi_map { vars.insert(name.clone(), phi); } + + // Lower condition using phi-backed vars + let (cval, _cend) = lower_expr_with_vars(f, cond_bb, cond, vars)?; + if let Some(bb) = f.get_block_mut(cond_bb) { + bb.set_terminator(MirInstruction::Branch { condition: cval, then_bb: body_bb, else_bb: exit_bb }); + } + + // Lower body; record end block and body-out vars + let mut body_vars = vars.clone(); + let bend = lower_stmt_list_with_vars(f, body_bb, body, &mut body_vars)?; + if let Some(bb) = f.get_block_mut(bend) { + if !bb.is_terminated() { bb.set_terminator(MirInstruction::Jump { target: cond_bb }); } + } + + // Wire PHI second incoming from latch (body end) + if let Some(bb) = f.get_block_mut(cond_bb) { + for (name, &phi_dst) in &phi_map { + if let Some(&latch_val) = body_vars.get(name) { + for inst in &mut bb.instructions { + if let MirInstruction::Phi { dst, inputs } = inst { + if *dst == phi_dst { + inputs.push((bend, latch_val)); + break; + } + } + } + } + } + } + + // After the loop, keep vars mapped to the PHI values (current loop state) + for (name, &phi) in &phi_map { vars.insert(name.clone(), phi); } + + Ok(exit_bb) + } + } +} + +fn lower_stmt_list_with_vars( + f: &mut MirFunction, + start_bb: BasicBlockId, + stmts: &[StmtV0], + vars: &mut std::collections::HashMap, +) -> Result { + let mut cur = start_bb; + for s in stmts { + cur = lower_stmt_with_vars(f, cur, s, vars)?; + if let Some(bb) = f.blocks.get(&cur) { if bb.is_terminated() { break; } } + } + Ok(cur) +} +fn lower_args_with_vars( + f: &mut MirFunction, + cur_bb: BasicBlockId, + args: &[ExprV0], + vars: &mut std::collections::HashMap, +) -> Result<(Vec, BasicBlockId), String> { + let mut out = Vec::with_capacity(args.len()); + let mut cur = cur_bb; + for a in args { + let (v, c) = lower_expr_with_vars(f, cur, a, vars)?; out.push(v); cur = c; + } + Ok((out, cur)) +} + fn lower_args(f: &mut MirFunction, cur_bb: BasicBlockId, args: &[ExprV0]) -> Result<(Vec, BasicBlockId), String> { let mut out = Vec::with_capacity(args.len()); let mut cur = cur_bb; diff --git a/tools/ny_parser_mvp.py b/tools/ny_parser_mvp.py index fcdb9ba8..a14fc79f 100644 --- a/tools/ny_parser_mvp.py +++ b/tools/ny_parser_mvp.py @@ -1,12 +1,25 @@ #!/usr/bin/env python3 """ -Ny parser MVP (Stage 1): Ny -> JSON v0 +Ny parser MVP (Stage 2): Ny -> JSON v0 Grammar (subset): - program := [return] expr EOF - expr := term (('+'|'-') term)* + program := stmt* EOF + stmt := 'return' expr + | 'local' IDENT '=' expr + | 'if' expr block ('else' block)? + | 'loop' '(' expr ')' block + | expr # expression statement + block := '{' stmt* '}' + + expr := logic + logic := compare (('&&'|'||') compare)* + compare := sum (('=='|'!='|'<'|'>'|'<='|'>=') sum)? + sum := term (('+'|'-') term)* term := factor (('*'|'/') factor)* - factor := INT | STRING | '(' expr ')' + factor := INT | STRING | IDENT call_tail* | '(' expr ')' | 'new' IDENT '(' args? ')' + call_tail:= '.' IDENT '(' args? ')' # method + | '(' args? ')' # function call + args := expr (',' expr)* Outputs JSON v0 compatible with --ny-parser-pipe. """ @@ -16,30 +29,44 @@ class Tok: def __init__(self, kind, val, pos): self.kind, self.val, self.pos = kind, val, pos +KEYWORDS = { + 'return':'RETURN', 'local':'LOCAL', 'if':'IF', 'else':'ELSE', 'loop':'LOOP', 'new':'NEW' +} + def lex(s: str): - i=n=0; n=len(s); out=[] + i=0; n=len(s); out=[] + def peek(): + return s[i] if i=', i) or s.startswith('&&', i) or s.startswith('||', i): + out.append(Tok('OP2', s[i:i+2], i)); i+=2; continue + if c in '+-*/(){}.,<>=': + out.append(Tok(c, c, i)); i+=1; continue if c=='"': j=i+1; buf=[] while j=')) or k in ('<','>'): + op = v if k=='OP2' else self.cur().kind + self.i+=1 + rhs=self.sum(); return {"type":"Compare","op":op,"lhs":lhs,"rhs":rhs} + return lhs + def sum(self): + lhs=self.term() + while self.cur().kind in ('+','-'): + op=self.cur().kind; self.i+=1 + rhs=self.term(); lhs={"type":"Binary","op":op,"lhs":lhs,"rhs":rhs} + return lhs + def term(self): + lhs=self.factor() + while self.cur().kind in ('*','/'): + op=self.cur().kind; self.i+=1 + rhs=self.factor(); lhs={"type":"Binary","op":op,"lhs":lhs,"rhs":rhs} + return lhs def factor(self): tok=self.cur() if self.eat('INT'): return {"type":"Int","value":tok.val} if self.eat('STR'): return {"type":"Str","value":tok.val} if self.eat('('): e=self.expr(); self.expect(')'); return e + if self.eat('NEW'): + t=self.cur(); self.expect('IDENT'); self.expect('(') + args=self.args_opt(); self.expect(')') + return {"type":"New","class":t.val,"args":args} + if self.eat('IDENT'): + node={"type":"Var","name":tok.val} + # call/methtail + while True: + if self.eat('('): + args=self.args_opt(); self.expect(')') + node={"type":"Call","name":tok.val,"args":args} + elif self.eat('.'): + m=self.cur(); self.expect('IDENT'); self.expect('(') + args=self.args_opt(); self.expect(')') + node={"type":"Method","recv":node,"method":m.val,"args":args} + else: + break + return node raise SyntaxError(f"factor at {tok.pos}") - def term(self): - lhs=self.factor() - while self.cur().kind in ('*','/'): - op=self.cur().kind; self.i+=1 - rhs=self.factor() - lhs={"type":"Binary","op":op,"lhs":lhs,"rhs":rhs} - return lhs - def expr(self): - lhs=self.term() - while self.cur().kind in ('+','-'): - op=self.cur().kind; self.i+=1 - rhs=self.term() - lhs={"type":"Binary","op":op,"lhs":lhs,"rhs":rhs} - return lhs - def program(self): - if self.eat('RETURN'): - e=self.expr() - else: - e=self.expr() - self.expect('EOF') - return {"version":0, "kind":"Program", "body":[{"type":"Return","expr":e}]} + def args_opt(self): + args=[] + if self.cur().kind in (')',): + return args + args.append(self.expr()) + while self.eat(','): + args.append(self.expr()) + return args def main(): if len(sys.argv)<2: diff --git a/tools/ny_parser_stage2_phi_smoke.sh b/tools/ny_parser_stage2_phi_smoke.sh new file mode 100644 index 00000000..38c93ad2 --- /dev/null +++ b/tools/ny_parser_stage2_phi_smoke.sh @@ -0,0 +1,45 @@ +#!/usr/bin/env bash +set -euo pipefail + +SCRIPT_DIR=$(CDPATH= cd -- "$(dirname -- "$0")" && pwd) +ROOT_DIR=$(CDPATH= cd -- "$SCRIPT_DIR/.." && pwd) +BIN="$ROOT_DIR/target/release/nyash" + +if [[ ! -x "$BIN" ]]; then + echo "[build] nyash (release) ..." >&2 + cargo build --release >/dev/null +fi + +TMP_DIR="$ROOT_DIR/tmp" +mkdir -p "$TMP_DIR" + +# If/Else PHI merge +cat >"$TMP_DIR/phi_if_sample.ny" <<'NY' +local x = 1 +if 1 < 2 { + local x = 10 +} else { + local x = 20 +} +return x +NY + +OUT1=$(python3 "$ROOT_DIR/tools/ny_parser_mvp.py" "$TMP_DIR/phi_if_sample.ny" | "$BIN" --ny-parser-pipe || true) +echo "$OUT1" | rg -q '^Result:\s*10\b' && echo "✅ If/Else PHI merge OK" || { echo "❌ If/Else PHI merge FAILED"; echo "$OUT1"; exit 1; } + +# Loop PHI merge +cat >"$TMP_DIR/phi_loop_sample.ny" <<'NY' +local i = 0 +local s = 0 +loop(i < 3) { + local s = s + 1 + local i = i + 1 +} +return s +NY + +OUT2=$(python3 "$ROOT_DIR/tools/ny_parser_mvp.py" "$TMP_DIR/phi_loop_sample.ny" | "$BIN" --ny-parser-pipe || true) +echo "$OUT2" | rg -q '^Result:\s*3\b' && echo "✅ Loop PHI merge OK" || { echo "❌ Loop PHI merge FAILED"; echo "$OUT2"; exit 1; } + +echo "All Stage-2 PHI smokes PASS" >&2 +