diff --git a/CURRENT_TASK.md b/CURRENT_TASK.md index 7b7905cb..f6b9935c 100644 --- a/CURRENT_TASK.md +++ b/CURRENT_TASK.md @@ -1,5 +1,23 @@ # Current Task (2025-09-14 改定) — Phase 15 llvmlite(既定)+ PyVM(新規) +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 実装スケルトン配置済み。 + +Next (short) +1) 三項(?:)の PyVM/llvmlite パリティE2E(`tools/parity.sh`) +2) peek サンプル/テスト拡充(式文・ブロック・戻り値) +3) Docs 追補(最小例、must_use 注意) +4) Parser MVP Stage‑2 設計(local/if/loop/call/method/new/me/substring/length/lastIndexOf) + +Quick Verify +- Ternary (VM): create file with `return (1 < 2) ? 10 : 20` and run + `./target/release/nyash --backend vm /tmp/tern4.nyash` +- Peek block (VM): `./target/release/nyash --backend vm apps/tests/peek_expr_block.nyash` +- Python parser RT: `NYASH_CLI_VERBOSE=1 tools/ny_parser_mvp_roundtrip.sh` + Summary - JIT/Cranelift は一時停止。Rust/inkwell LLVM は参照のみ。 - 既定の実行/ビルド経路は Python/llvmlite ハーネス(MIR JSON→.o→NyRT link)。 diff --git a/app_parity_esc_dirname_smoke b/app_parity_esc_dirname_smoke index 5703ec19..4838e9ad 100644 Binary files a/app_parity_esc_dirname_smoke and b/app_parity_esc_dirname_smoke differ diff --git a/app_parity_main b/app_parity_main index 86865326..881121b3 100644 Binary files a/app_parity_main and b/app_parity_main differ diff --git a/app_parity_peek_expr_block b/app_parity_peek_expr_block new file mode 100644 index 00000000..b3f80ec0 Binary files /dev/null and b/app_parity_peek_expr_block differ diff --git a/app_parity_peek_return_value b/app_parity_peek_return_value new file mode 100644 index 00000000..9cff0ae6 Binary files /dev/null and b/app_parity_peek_return_value differ diff --git a/app_parity_ternary_basic b/app_parity_ternary_basic new file mode 100644 index 00000000..a8ddf97f Binary files /dev/null and b/app_parity_ternary_basic differ diff --git a/app_parity_ternary_nested b/app_parity_ternary_nested new file mode 100644 index 00000000..ee7d8dca Binary files /dev/null and b/app_parity_ternary_nested differ diff --git a/app_peek_expr_block b/app_peek_expr_block new file mode 100644 index 00000000..c45b4566 Binary files /dev/null and b/app_peek_expr_block differ diff --git a/apps/tests/peek_return_value.nyash b/apps/tests/peek_return_value.nyash new file mode 100644 index 00000000..a885e494 --- /dev/null +++ b/apps/tests/peek_return_value.nyash @@ -0,0 +1,15 @@ +static box Main { + main(args) { + // Use peek as an expression to produce a value + local d = "1" + local v = peek d { + "0" => { 0 } + "1" => { 1 } + else => { 0 } + } + // Show the result and finish + local console = new ConsoleBox() + console.println(v) + return 0 + } +} diff --git a/apps/tests/ternary_nested.nyash b/apps/tests/ternary_nested.nyash new file mode 100644 index 00000000..9d930288 --- /dev/null +++ b/apps/tests/ternary_nested.nyash @@ -0,0 +1,10 @@ +static box Main { + main(args) { + local a = 3 + local b = 5 + // Nested ternary: should evaluate to 50 + local v = (a < b) ? ((b < 0) ? 40 : 50) : 60 + return v + } +} + diff --git a/docs/guides/language-guide.md b/docs/guides/language-guide.md index 9a9553b5..2f525d5c 100644 --- a/docs/guides/language-guide.md +++ b/docs/guides/language-guide.md @@ -15,6 +15,39 @@ Common Constructs - Null-coalesce: `x ?? y` → `peek x { null => y, else => x }` - Safe access: `a?.b` → `peek a { null => null, else => a.b }` +Minimal Examples +- Ternary + ```nyash + static box Main { + main(args) { + local a = 3 + local b = 5 + // Nested ternary is supported + local v = (a < b) ? ((b < 0) ? 40 : 50) : 60 + return v + } + } + ``` +- Peek as expression block (last expression is the value) + ```nyash + static box Main { + main(args) { + local d = "1" + // Each arm can be a block; the last expression becomes the value + local dv = peek d { + "0" => { print("found zero") 0 } + "1" => { print("found one") 1 } + else => { print("other") 0 } + } + return dv + } + } + ``` + +must_use Notes +- Peek arms are expressions. When using a block arm `{ ... }`, the last expression is the resulting value; statements without a final expression yield no usable value. +- Ternary is an expression; ensure both branches are type-compatible at MIR level (e.g., both yield integer or both yield string handle in current phase). + When you need the implementation details - Tokenizer: src/tokenizer.rs - Parser: src/parser/expressions.rs, src/parser/statements.rs diff --git a/docs/reference/architecture/parser_mvp_stage2.md b/docs/reference/architecture/parser_mvp_stage2.md new file mode 100644 index 00000000..0054a3d7 --- /dev/null +++ b/docs/reference/architecture/parser_mvp_stage2.md @@ -0,0 +1,59 @@ +# Parser MVP Stage-2 Design (Phase 15) + +Scope +- Expand Stage-1 (const/binop/return) to cover the minimal everyday surface: + - local bindings: `local x = expr` + - if/else: `if cond { ... } else { ... }` + - loop: `loop(cond) { ... }` and `loop cond { ... }` + - call: `f(a, b)` (free function) + - method: `obj.method(a, b)` + - constructor: `new BoxType(args)` + - implicit receiver `me` in box methods + - String methods: `substring`, `length`, `lastIndexOf` + +Grammar Sketch (incremental) +- Expression + - precedence keeps Phase 12.7: ternary inside pipe; ternary lowers to if/branch + - postfix: method call, field access + - primary: literals, identifiers, parenthesized +- Statement + - `local ident = expr` (no rebind in Stage-2) + - expression statement (for side effects) + - return + +AST Nodes (delta) +- Add nodes for Local, If, Loop, Call, MethodCall, New, Identifier, Return. +- Keep Peek and Ternary in the AST; lower to MIR via the same builder hooks. + +Lowering to MIR JSON v0 +- Local → assign to fresh value id, store into vmap +- If/Ternary → compare + branch blocks; PHI for join values when used as expression +- Loop → cond block → body block → back-edge; loop-carried values explicitly phied +- Call → `call { func: "name", args: [...] }` +- Method → `boxcall { box: vid, method: "name", args: [...] }` +- New → `newbox { type: "BoxType", args: [...] }` +- String ops + - `length()` → i64 length on string handle (VM: Python len; LLVM: handle→ptr at call sites only) + - `substring(a,b)` → `nyash.string.substring_hh` equivalent + - `lastIndexOf(x)` → returns i64 index or -1 + +Type Meta (emitted with JSON where needed) +- Compare: when both sides are string → `cmp_kind: "string"` +- BinOp `+`: if `dst_type = { kind: "handle", box_type: "StringBox" }`, force concat_hh +- Known APIs: annotate `dst_type` for Console.println (i64), dirname/join/read as `StringBox(handle)` + +Policy & Invariants +- Resolver-only: do not wire PHIs in-lowering; snapshots + `finalize_phis` bind incomings +- Strings: inter-block are handle(i64) only; i8* materialized at call sites +- PHI placeholders: created at block heads from JSON-declared phi + +Acceptance +- Parity on: + - `apps/tests/min_str_cat_loop/main.nyash` + - `apps/tests/esc_dirname_smoke.nyash` + - Stage-2 new smokes (ternary nested, peek return) +- `tools/parity.sh --lhs pyvm --rhs llvmlite --show-diff ` → green + +Notes +- Keep Stage-2 additive and test-driven; avoid broad refactors. +- Future: numeric methods, map/array minimal, and using/namespace gate. diff --git a/src/llvm_py/instructions/externcall.py b/src/llvm_py/instructions/externcall.py index 2a610262..c95c8ca4 100644 --- a/src/llvm_py/instructions/externcall.py +++ b/src/llvm_py/instructions/externcall.py @@ -82,6 +82,7 @@ def lower_externcall( # Prepare/coerce arguments call_args: List[ir.Value] = [] for i, arg_id in enumerate(args): + orig_arg_id = arg_id # Prefer resolver if resolver is not None and preds is not None and block_end_values is not None and bb_map is not None: if len(func.args) > i and isinstance(func.args[i].type, ir.PointerType): @@ -99,19 +100,47 @@ def lower_externcall( expected_ty = func.args[i].type if isinstance(expected_ty, ir.PointerType): # Need pointer - if hasattr(aval, 'type'): - if isinstance(aval.type, ir.IntType): - aval = builder.inttoptr(aval, expected_ty, name=f"ext_i2p_arg{i}") - elif not aval.type.is_pointer: - aval = ir.Constant(expected_ty, None) - else: - # Pointer but wrong element type: if pointer-to-array -> GEP to i8* - try: - if isinstance(aval.type.pointee, ir.ArrayType) and isinstance(expected_ty.pointee, ir.IntType) and expected_ty.pointee.width == 8: - c0 = ir.Constant(ir.IntType(32), 0) - aval = builder.gep(aval, [c0, c0], name=f"ext_gep_arg{i}") - except Exception: - pass + # Prefer string literal pointer or handle->i8* bridge when argument is string-ish + used_string_h2p = False + try: + if resolver is not None and hasattr(resolver, 'string_ptrs'): + sp = resolver.string_ptrs.get(orig_arg_id) + if sp is not None: + aval = sp + used_string_h2p = True + if not used_string_h2p and resolver is not None and hasattr(resolver, 'is_stringish') and resolver.is_stringish(orig_arg_id): + # Declare nyash.string.to_i8p_h(i64) and call with handle + i64 = ir.IntType(64) + i8p = ir.IntType(8).as_pointer() + to_i8p = None + for f in module.functions: + if f.name == 'nyash.string.to_i8p_h': + to_i8p = f; break + if to_i8p is None: + to_i8p = ir.Function(module, ir.FunctionType(i8p, [i64]), name='nyash.string.to_i8p_h') + # Ensure we have an i64 handle to pass + if hasattr(aval, 'type') and isinstance(aval.type, ir.PointerType): + aval = builder.ptrtoint(aval, i64, name=f"ext_p2h_{i}") + elif hasattr(aval, 'type') and isinstance(aval.type, ir.IntType) and aval.type.width != 64: + aval = builder.zext(aval, i64, name=f"ext_zext_h_{i}") + aval = builder.call(to_i8p, [aval], name=f"ext_h2p_arg{i}") + used_string_h2p = True + except Exception: + used_string_h2p = used_string_h2p or False + if not used_string_h2p: + if hasattr(aval, 'type'): + if isinstance(aval.type, ir.IntType): + aval = builder.inttoptr(aval, expected_ty, name=f"ext_i2p_arg{i}") + elif not aval.type.is_pointer: + aval = ir.Constant(expected_ty, None) + else: + # Pointer but wrong element type: if pointer-to-array -> GEP to i8* + try: + if isinstance(aval.type.pointee, ir.ArrayType) and isinstance(expected_ty.pointee, ir.IntType) and expected_ty.pointee.width == 8: + c0 = ir.Constant(ir.IntType(32), 0) + aval = builder.gep(aval, [c0, c0], name=f"ext_gep_arg{i}") + except Exception: + pass else: aval = ir.Constant(expected_ty, None) elif isinstance(expected_ty, ir.IntType) and expected_ty.width == 64: diff --git a/src/mir/builder/exprs_peek.rs b/src/mir/builder/exprs_peek.rs index d685016b..eea75d52 100644 --- a/src/mir/builder/exprs_peek.rs +++ b/src/mir/builder/exprs_peek.rs @@ -9,42 +9,81 @@ impl super::MirBuilder { arms: Vec<(LiteralValue, ASTNode)>, else_expr: ASTNode, ) -> Result { + // Evaluate scrutinee in the current block let scr_val = self.build_expression_impl(scrutinee)?; + + // Prepare merge and result let merge_block: BasicBlockId = self.block_gen.next(); let result_val = self.value_gen.next(); let mut phi_inputs: Vec<(BasicBlockId, ValueId)> = Vec::new(); - let mut next_block = self.block_gen.next(); - self.start_new_block(next_block)?; + // Create dispatch block where we start comparing arms + let dispatch_block = self.block_gen.next(); + // Jump from current block to dispatch (ensure terminator exists) + let need_jump = { + let cur = self.current_block; + if let (Some(cb), Some(ref func)) = (cur, &self.current_function) { + if let Some(bb) = func.blocks.get(&cb) { !bb.is_terminated() } else { true } + } else { true } + }; + if need_jump { + self.emit_instruction(super::MirInstruction::Jump { target: dispatch_block })?; + } + self.start_new_block(dispatch_block)?; + + // If there are no arms, fall through to else directly + if arms.is_empty() { + let else_block = self.block_gen.next(); + self.emit_instruction(super::MirInstruction::Jump { target: else_block })?; + self.start_new_block(else_block)?; + let else_val = self.build_expression_impl(else_expr)?; + phi_inputs.push((else_block, else_val)); + self.emit_instruction(super::MirInstruction::Jump { target: merge_block })?; + self.start_new_block(merge_block)?; + self.emit_instruction(super::MirInstruction::Phi { dst: result_val, inputs: phi_inputs })?; + return Ok(result_val); + } + + // Else block to handle default case + let else_block = self.block_gen.next(); + + // Chain dispatch blocks for each arm + let mut cur_dispatch = dispatch_block; for (i, (label, arm_expr)) in arms.iter().cloned().enumerate() { let then_block = self.block_gen.next(); - // Only string labels handled here (behavior unchanged) + // Next dispatch (only for non-last arm) + let next_dispatch = if i + 1 < arms.len() { Some(self.block_gen.next()) } else { None }; + let else_target = next_dispatch.unwrap_or(else_block); + + // In current dispatch block, compare and branch + self.start_new_block(cur_dispatch)?; if let LiteralValue::String(s) = label { let lit_id = self.value_gen.next(); self.emit_instruction(super::MirInstruction::Const { dst: lit_id, value: super::ConstValue::String(s) })?; let cond_id = self.value_gen.next(); self.emit_instruction(super::MirInstruction::Compare { dst: cond_id, op: super::CompareOp::Eq, lhs: scr_val, rhs: lit_id })?; - self.emit_instruction(super::MirInstruction::Branch { condition: cond_id, then_bb: then_block, else_bb: next_block })?; - self.start_new_block(then_block)?; - let then_val = self.build_expression_impl(arm_expr)?; - phi_inputs.push((then_block, then_val)); - self.emit_instruction(super::MirInstruction::Jump { target: merge_block })?; - if i < arms.len() - 1 { - let b = self.block_gen.next(); - self.start_new_block(b)?; - next_block = b; - } + self.emit_instruction(super::MirInstruction::Branch { condition: cond_id, then_bb: then_block, else_bb: else_target })?; } + + // then arm + self.start_new_block(then_block)?; + let then_val = self.build_expression_impl(arm_expr)?; + phi_inputs.push((then_block, then_val)); + self.emit_instruction(super::MirInstruction::Jump { target: merge_block })?; + + // Move to next dispatch or else block + cur_dispatch = else_target; } - let else_block_id = next_block; - self.start_new_block(else_block_id)?; + // Lower else expression in else_block + self.start_new_block(else_block)?; let else_val = self.build_expression_impl(else_expr)?; - phi_inputs.push((else_block_id, else_val)); + phi_inputs.push((else_block, else_val)); self.emit_instruction(super::MirInstruction::Jump { target: merge_block })?; + + // Merge and yield result self.start_new_block(merge_block)?; self.emit_instruction(super::MirInstruction::Phi { dst: result_val, inputs: phi_inputs })?; Ok(result_val) } } - diff --git a/src/parser/expressions.rs b/src/parser/expressions.rs index d3fe7fad..b0f45f32 100644 --- a/src/parser/expressions.rs +++ b/src/parser/expressions.rs @@ -98,7 +98,7 @@ impl NyashParser { condition: Box::new(cond), then_body: vec![then_expr], else_body: Some(vec![else_expr]), - span: Span::Unknown, + span: Span::unknown(), }); } Ok(cond) diff --git a/src/runner/modes/vm.rs b/src/runner/modes/vm.rs index 85014466..e84fd6da 100644 --- a/src/runner/modes/vm.rs +++ b/src/runner/modes/vm.rs @@ -151,15 +151,14 @@ impl NyashRunner { .status() .map_err(|e| format!("spawn pyvm: {}", e)) .unwrap(); + // Always propagate PyVM exit code to match llvmlite semantics + let code = status.code().unwrap_or(1); if !status.success() { - eprintln!("❌ PyVM failed (status={})", status.code().unwrap_or(-1)); - process::exit(1); + if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1") { + eprintln!("❌ PyVM failed (status={})", code); + } } - // Propagate exit code if set - if let Some(code) = status.code() { - process::exit(code); - } - process::exit(0); + process::exit(code); } else { eprintln!("❌ PyVM runner not found: {}", runner.display()); process::exit(1); diff --git a/tools/parity.sh b/tools/parity.sh index 7a6865d0..ea9c473e 100644 --- a/tools/parity.sh +++ b/tools/parity.sh @@ -67,6 +67,7 @@ normalize() { -e '/^🔌/d' \ -e '/^✅/d' \ -e '/^🚀/d' \ + -e '/^❌/d' \ -e '/^⚡/d' \ -e '/^🦀/d' \ -e '/^🧠/d' \