stage3: unify to cleanup; MIR return-defer; docs+smokes updated; LLVM(harness): finalize_phis ownership, ret.py simplified, uses-predeclare; cleanup return override green; method-postfix cleanup return WIP (PHI head)

This commit is contained in:
Selfhosting Dev
2025-09-19 02:07:38 +09:00
parent 951a050592
commit 5e818eeb7e
205 changed files with 9671 additions and 1849 deletions

View File

@ -0,0 +1,108 @@
/*!
* Nyash Interpreter - Modular Rust Implementation
*
* Refactored from massive 2,633-line interpreter.rs into logical modules
* Everything is Box philosophy with clean separation of concerns
*/
// Import all necessary dependencies
use crate::ast::{ASTNode, CatchClause};
use crate::box_trait::{BoolBox, BoxCore, ErrorBox, NyashBox, StringBox, VoidBox};
use crate::boxes::debug_box::DebugBox;
use crate::boxes::math_box::MathBox;
use crate::boxes::random_box::RandomBox;
use crate::boxes::time_box::TimerBox;
use crate::boxes::FutureBox;
use crate::channel_box::ChannelBox;
use crate::instance_v2::InstanceBox;
// WASM-specific Box types (conditionally included)
#[cfg(target_arch = "wasm32")]
use crate::boxes::web::{WebCanvasBox, WebConsoleBox, WebDisplayBox};
use crate::exception_box;
use std::collections::HashMap;
// Module declarations
mod async_methods;
mod box_methods;
mod calls;
mod core;
pub mod errors;
mod eval;
mod expressions;
mod functions;
mod io;
mod math_methods;
mod methods;
mod methods_dispatch;
pub mod objects;
mod objects_basic_constructors;
mod special_methods;
pub mod state;
mod statements;
mod system_methods;
pub mod utils;
mod web_methods;
// Main interpreter implementation - will be moved from interpreter.rs
pub use core::NyashInterpreter;
pub use errors::RuntimeError;
pub use state::SharedState;
/// 実行制御フロー
#[derive(Debug)]
pub enum ControlFlow {
None,
Break,
Continue,
Return(Box<dyn NyashBox>),
Throw(Box<dyn NyashBox>),
}
/// コンストラクタ実行コンテキスト
#[derive(Debug, Clone)]
pub struct ConstructorContext {
pub class_name: String,
pub parent_class: Option<String>,
}
// Re-export core model so existing interpreter modules keep working
pub use crate::core::model::BoxDeclaration;
/// 🔥 Static Box定義を保持する構造体
#[derive(Debug, Clone)]
pub struct StaticBoxDefinition {
pub name: String,
pub fields: Vec<String>,
pub methods: HashMap<String, ASTNode>,
pub init_fields: Vec<String>,
pub weak_fields: Vec<String>, // 🔗 weak修飾子が付いたフィールドのリスト
pub static_init: Option<Vec<ASTNode>>, // static { } ブロック
pub extends: Vec<String>, // 🚀 Multi-delegation: Changed from Option<String> to Vec<String>
pub implements: Vec<String>,
pub type_parameters: Vec<String>,
/// 初期化状態
pub initialization_state: StaticBoxState,
}
/// 🔥 Static Box初期化状態
#[derive(Debug, Clone, PartialEq)]
pub enum StaticBoxState {
NotInitialized, // 未初期化
Initializing, // 初期化中(循環参照検出用)
Initialized, // 初期化完了
}
/// 関数宣言を保持する構造体
#[derive(Debug, Clone)]
pub struct FunctionDeclaration {
pub name: String,
pub params: Vec<String>,
pub body: Vec<ASTNode>,
}
// Re-export core interpreter types
pub use core::*;
// Import and re-export stdlib for interpreter modules
pub use crate::stdlib::BuiltinStdlib;

View File

@ -5,18 +5,24 @@
// VM core types are always available
pub mod vm_types;
// Legacy VM execution pipeline (feature-gated)
// Legacy VM execution pipeline (feature-gated) — loaded from archive path
#[cfg(feature = "vm-legacy")]
#[path = "../archive/vm_legacy/vm.rs"]
pub mod vm;
#[cfg(feature = "vm-legacy")]
#[path = "../archive/vm_legacy/vm_boxcall.rs"]
pub mod vm_boxcall;
#[cfg(feature = "vm-legacy")]
#[path = "../archive/vm_legacy/vm_instructions/mod.rs"]
pub mod vm_instructions;
#[cfg(feature = "vm-legacy")]
#[path = "../archive/vm_legacy/vm_phi.rs"]
pub mod vm_phi;
#[cfg(feature = "vm-legacy")]
#[path = "../archive/vm_legacy/vm_stats.rs"]
pub mod vm_stats;
#[cfg(feature = "vm-legacy")]
#[path = "../archive/vm_legacy/vm_values.rs"]
pub mod vm_values;
// When vm-legacy is disabled, provide a compatibility shim module so
@ -28,22 +34,30 @@ pub mod vm {
// Phase 9.78h: VM split scaffolding (control_flow/dispatch/frame)
pub mod abi_util; // Shared ABI/utility helpers
#[cfg(feature = "vm-legacy")]
#[path = "../archive/vm_legacy/control_flow.rs"]
pub mod control_flow;
#[cfg(feature = "vm-legacy")]
#[path = "../archive/vm_legacy/dispatch.rs"]
pub mod dispatch;
#[cfg(feature = "vm-legacy")]
#[path = "../archive/vm_legacy/frame.rs"]
pub mod frame;
pub mod gc_helpers;
pub mod mir_interpreter;
#[cfg(feature = "vm-legacy")]
#[path = "../archive/vm_legacy/vm_control_flow.rs"]
pub mod vm_control_flow;
#[cfg(feature = "vm-legacy")]
#[path = "../archive/vm_legacy/vm_exec.rs"]
mod vm_exec; // A3: execution loop extracted
#[cfg(feature = "vm-legacy")]
#[path = "../archive/vm_legacy/vm_gc.rs"]
mod vm_gc; // A3: GC roots & diagnostics extracted
#[cfg(feature = "vm-legacy")]
#[path = "../archive/vm_legacy/vm_methods.rs"]
mod vm_methods; // A3-S1: method dispatch wrappers extracted
#[cfg(feature = "vm-legacy")]
#[path = "../archive/vm_legacy/vm_state.rs"]
mod vm_state; // A3: state & basic helpers extracted // Lightweight MIR interpreter
#[cfg(feature = "wasm-backend")]

View File

@ -34,6 +34,7 @@ use once_cell::sync::OnceCell;
use std::sync::RwLock;
static GLOBAL_ENV: OnceCell<RwLock<NyashEnv>> = OnceCell::new();
static PHI_ON_GATED_WARNED: OnceCell<()> = OnceCell::new();
pub fn current() -> NyashEnv {
if let Some(lock) = GLOBAL_ENV.get() {
@ -111,7 +112,24 @@ pub fn mir_no_phi() -> bool {
match std::env::var("NYASH_MIR_NO_PHI").ok() {
Some(v) => {
let lv = v.to_ascii_lowercase();
!(lv == "0" || lv == "false" || lv == "off")
let requested_no_phi = !(lv == "0" || lv == "false" || lv == "off");
if requested_no_phi {
return true;
}
// PHI-on requested
#[cfg(feature = "phi-legacy")]
{
return false;
}
#[cfg(not(feature = "phi-legacy"))]
{
if PHI_ON_GATED_WARNED.set(()).is_ok() {
eprintln!(
"[nyash] PHI-on requested but disabled in this build (missing 'phi-legacy' feature). Falling back to PHI-off."
);
}
return true;
}
}
// Default: ON for MIR13 stability (PHI generation off by default)
None => true,
@ -123,6 +141,13 @@ pub fn verify_allow_no_phi() -> bool {
std::env::var("NYASH_VERIFY_ALLOW_NO_PHI").ok().as_deref() == Some("1") || mir_no_phi()
}
/// Enable strict edge-copy policy verification in PHI-off mode.
/// When enabled, merge blocks must receive merged values via predecessor copies only,
/// and the merge block itself must not introduce a self-copy to the merged destination.
pub fn verify_edge_copy_strict() -> bool {
std::env::var("NYASH_VERIFY_EDGE_COPY_STRICT").ok().as_deref() == Some("1")
}
// ---- LLVM harness toggle (llvmlite) ----
pub fn llvm_use_harness() -> bool {
std::env::var("NYASH_LLVM_USE_HARNESS").ok().as_deref() == Some("1")
@ -237,6 +262,29 @@ pub fn gc_alloc_threshold() -> Option<u64> {
std::env::var("NYASH_GC_ALLOC_THRESHOLD").ok()?.parse().ok()
}
// ---- Cleanup (method-level postfix) policy toggles ----
/// Allow `return` inside a cleanup block. Default: false (0)
pub fn cleanup_allow_return() -> bool {
match std::env::var("NYASH_CLEANUP_ALLOW_RETURN").ok() {
Some(v) => {
let lv = v.to_ascii_lowercase();
!(lv == "0" || lv == "false" || lv == "off")
}
None => false,
}
}
/// Allow `throw` inside a cleanup block. Default: false (0)
pub fn cleanup_allow_throw() -> bool {
match std::env::var("NYASH_CLEANUP_ALLOW_THROW").ok() {
Some(v) => {
let lv = v.to_ascii_lowercase();
!(lv == "0" || lv == "false" || lv == "off")
}
None => false,
}
}
/// Run a collection every N safepoints (if Some)
pub fn gc_collect_sp_interval() -> Option<u64> {
std::env::var("NYASH_GC_COLLECT_SP").ok()?.parse().ok()
@ -332,6 +380,35 @@ pub fn selfhost_read_tmp() -> bool {
pub fn ny_compiler_stage3() -> bool {
std::env::var("NYASH_NY_COMPILER_STAGE3").ok().as_deref() == Some("1")
}
/// Core (Rust) parser Stage-3 gate
/// When enabled, the Rust parser accepts Stage-3 surface (try/catch/finally, throw).
/// Default is OFF to keep Stage-2 stable.
pub fn parser_stage3() -> bool {
std::env::var("NYASH_PARSER_STAGE3").ok().as_deref() == Some("1")
}
/// Parser gate for BlockPostfix Catch acceptance
/// Enabled when either NYASH_BLOCK_CATCH=1 or Stage3 gate is on.
/// Phase 15.5 allows parsing a standalone `{ ... }` block optionally followed by
/// a single `catch (...) { ... }` and/or `finally { ... }`, which is folded into
/// ASTNode::TryCatch with the preceding block as the try body.
pub fn block_postfix_catch() -> bool {
std::env::var("NYASH_BLOCK_CATCH").ok().as_deref() == Some("1") || parser_stage3()
}
/// Bridge lowering: use Result-style try/throw lowering instead of MIR Catch/Throw
/// When on, try/catch is lowered using structured blocks and direct jumps,
/// without emitting MIR Throw/Catch. The thrown value is routed to catch via
/// block parameters (PHI-off uses edge-copy).
pub fn try_result_mode() -> bool {
std::env::var("NYASH_TRY_RESULT_MODE").ok().as_deref() == Some("1")
}
/// Parser gate for method-level postfix catch/finally acceptance on method definitions.
/// Enabled when either NYASH_METHOD_CATCH=1 or Stage3 gate is on.
pub fn method_catch() -> bool {
std::env::var("NYASH_METHOD_CATCH").ok().as_deref() == Some("1") || parser_stage3()
}
pub fn ny_compiler_child_args() -> Option<String> {
std::env::var("NYASH_NY_COMPILER_CHILD_ARGS").ok()
}

View File

@ -1,108 +1,6 @@
/*!
* Nyash Interpreter - Modular Rust Implementation
*
* Refactored from massive 2,633-line interpreter.rs into logical modules
* Everything is Box philosophy with clean separation of concerns
*/
#![cfg(feature = "interpreter-legacy")]
// Shim module to re-export legacy interpreter from archive path
#[path = "../archive/interpreter_legacy/mod.rs"]
mod legacy_interpreter_mod;
pub use legacy_interpreter_mod::*;
// Import all necessary dependencies
use crate::ast::{ASTNode, CatchClause};
use crate::box_trait::{BoolBox, BoxCore, ErrorBox, NyashBox, StringBox, VoidBox};
use crate::boxes::debug_box::DebugBox;
use crate::boxes::math_box::MathBox;
use crate::boxes::random_box::RandomBox;
use crate::boxes::time_box::TimerBox;
use crate::boxes::FutureBox;
use crate::channel_box::ChannelBox;
use crate::instance_v2::InstanceBox;
// WASM-specific Box types (conditionally included)
#[cfg(target_arch = "wasm32")]
use crate::boxes::web::{WebCanvasBox, WebConsoleBox, WebDisplayBox};
use crate::exception_box;
use std::collections::HashMap;
// Module declarations
mod async_methods;
mod box_methods;
mod calls;
mod core;
pub mod errors;
mod eval;
mod expressions;
mod functions;
mod io;
mod math_methods;
mod methods;
mod methods_dispatch;
pub mod objects;
mod objects_basic_constructors;
mod special_methods;
pub mod state;
mod statements;
mod system_methods;
pub mod utils;
mod web_methods;
// Main interpreter implementation - will be moved from interpreter.rs
pub use core::NyashInterpreter;
pub use errors::RuntimeError;
pub use state::SharedState;
/// 実行制御フロー
#[derive(Debug)]
pub enum ControlFlow {
None,
Break,
Continue,
Return(Box<dyn NyashBox>),
Throw(Box<dyn NyashBox>),
}
/// コンストラクタ実行コンテキスト
#[derive(Debug, Clone)]
pub struct ConstructorContext {
pub class_name: String,
pub parent_class: Option<String>,
}
// Re-export core model so existing interpreter modules keep working
pub use crate::core::model::BoxDeclaration;
/// 🔥 Static Box定義を保持する構造体
#[derive(Debug, Clone)]
pub struct StaticBoxDefinition {
pub name: String,
pub fields: Vec<String>,
pub methods: HashMap<String, ASTNode>,
pub init_fields: Vec<String>,
pub weak_fields: Vec<String>, // 🔗 weak修飾子が付いたフィールドのリスト
pub static_init: Option<Vec<ASTNode>>, // static { } ブロック
pub extends: Vec<String>, // 🚀 Multi-delegation: Changed from Option<String> to Vec<String>
pub implements: Vec<String>,
pub type_parameters: Vec<String>,
/// 初期化状態
pub initialization_state: StaticBoxState,
}
/// 🔥 Static Box初期化状態
#[derive(Debug, Clone, PartialEq)]
pub enum StaticBoxState {
NotInitialized, // 未初期化
Initializing, // 初期化中(循環参照検出用)
Initialized, // 初期化完了
}
/// 関数宣言を保持する構造体
#[derive(Debug, Clone)]
pub struct FunctionDeclaration {
pub name: String,
pub params: Vec<String>,
pub body: Vec<ASTNode>,
}
// Re-export core interpreter types
pub use core::*;
// Import and re-export stdlib for interpreter modules
pub use crate::stdlib::BuiltinStdlib;

View File

@ -9,7 +9,8 @@ from typing import Dict, Optional
def lower_barrier(
builder: ir.IRBuilder,
barrier_type: str,
ordering: Optional[str] = None
ordering: Optional[str] = None,
ctx=None,
) -> None:
"""
Lower MIR Barrier instruction

View File

@ -5,6 +5,7 @@ Experimental loop normalization following paper-e-loop-signal-ir
import os
import llvmlite.ir as ir
from phi_wiring import phi_at_block_head
from dataclasses import dataclass
from typing import Dict, Tuple, List, Optional, Any
from instructions.safepoint import insert_automatic_safepoint
@ -109,9 +110,9 @@ def lower_while_loopform(
i8 = ir.IntType(8)
i64 = ir.IntType(64)
# Create PHI nodes
tag_phi = builder.phi(i8, name=f"lf{loop_id}_tag")
payload_phi = builder.phi(i64, name=f"lf{loop_id}_payload")
# Create PHI nodes at the block head (LLVM requires PHIs grouped at top)
tag_phi = phi_at_block_head(lf.dispatch, i8, name=f"lf{loop_id}_tag")
payload_phi = phi_at_block_head(lf.dispatch, i64, name=f"lf{loop_id}_payload")
# Add incoming values
# From header (condition false): Break signal

View File

@ -4,6 +4,7 @@ Critical for SSA form - handles value merging from different control flow paths
"""
import llvmlite.ir as ir
from phi_wiring import phi_at_block_head
from typing import Dict, List, Tuple, Optional
def lower_phi(
@ -123,8 +124,8 @@ def lower_phi(
vmap[dst_vid] = ir.Constant(phi_type, 0)
return
# Create PHI instruction now and add incoming
phi = builder.phi(phi_type, name=f"phi_{dst_vid}")
# Create PHI instruction at the block head and add incoming
phi = phi_at_block_head(current_block, phi_type, name=f"phi_{dst_vid}")
for block, val in incoming_pairs:
phi.add_incoming(val, block)

View File

@ -45,83 +45,73 @@ def lower_return(
else:
# Get return value (prefer resolver)
ret_val = None
if resolver is not None and preds is not None and block_end_values is not None and bb_map is not None:
# Fast path: if vmap has a concrete non-PHI value defined in this block, use it directly
if isinstance(value_id, int):
tmp0 = vmap.get(value_id)
try:
# If this block has a declared PHI for the return value, force using the
# local PHI placeholder to ensure dominance and let finalize_phis wire it.
try:
block_name = builder.block.name
cur_bid = int(str(block_name).replace('bb',''))
except Exception:
cur_bid = -1
try:
bm = getattr(resolver, 'block_phi_incomings', {}) or {}
except Exception:
bm = {}
if isinstance(value_id, int) and isinstance(bm.get(cur_bid), dict) and value_id in bm.get(cur_bid):
# Reuse predeclared ret-phi when available
cur = None
is_phi0 = hasattr(tmp0, 'add_incoming')
except Exception:
is_phi0 = False
if tmp0 is not None and not is_phi0:
ret_val = tmp0
if ret_val is None:
if resolver is not None and preds is not None and block_end_values is not None and bb_map is not None:
# Multi-pred special case: construct a PHI at block head and wire per-pred values
# to keep PHIs grouped at top and avoid late synthesis that violates ordering.
cur_bid = int(str(builder.block.name).replace('bb','')) if hasattr(builder.block, 'name') else -1
pred_ids = [p for p in preds.get(cur_bid, []) if p != cur_bid] if isinstance(preds, dict) else []
if isinstance(value_id, int) and len(pred_ids) > 1 and isinstance(return_type, ir.IntType):
# Create PHI at block head
btop = ir.IRBuilder(builder.block)
try:
rp = getattr(resolver, 'ret_phi_map', {}) or {}
key = (int(cur_bid), int(value_id))
if key in rp:
cur = rp[key]
except Exception:
cur = None
if cur is None:
btop = ir.IRBuilder(builder.block)
try:
btop.position_at_start(builder.block)
except Exception:
pass
# Reuse existing local phi if present; otherwise create
cur = vmap.get(value_id)
need_new = True
try:
need_new = not (cur is not None and hasattr(cur, 'add_incoming') and getattr(getattr(cur, 'basic_block', None), 'name', None) == builder.block.name)
except Exception:
need_new = True
if need_new:
cur = btop.phi(ir.IntType(64), name=f"phi_ret_{value_id}")
# Bind to maps
vmap[value_id] = cur
try:
if hasattr(resolver, 'global_vmap') and isinstance(resolver.global_vmap, dict):
resolver.global_vmap[value_id] = cur
btop.position_at_start(builder.block)
except Exception:
pass
ret_val = cur
if ret_val is not None:
builder.ret(ret_val)
return
if isinstance(return_type, ir.PointerType):
ret_val = resolver.resolve_ptr(value_id, builder.block, preds, block_end_values, vmap)
ph = btop.phi(return_type, name=f"res_phi_{value_id}_{cur_bid}")
# Wire per-pred end values
for pred_bid in pred_ids:
pred_bb = bb_map.get(pred_bid) if isinstance(bb_map, dict) else None
if pred_bb is None:
continue
val = resolver._value_at_end_i64(value_id, pred_bid, preds, block_end_values, vmap, bb_map)
if not hasattr(val, 'type'):
val = ir.Constant(return_type, 0)
ph.add_incoming(val, pred_bb)
ret_val = ph
else:
# Prefer pointer→handle reboxing for string-ish returns even if function return type is i64
is_stringish = False
if hasattr(resolver, 'is_stringish'):
try:
is_stringish = resolver.is_stringish(int(value_id))
except Exception:
is_stringish = False
if is_stringish and hasattr(resolver, 'string_ptrs') and int(value_id) in getattr(resolver, 'string_ptrs'):
# Re-box known string pointer to handle
p = resolver.string_ptrs[int(value_id)]
i8p = ir.IntType(8).as_pointer()
i64 = ir.IntType(64)
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')
ret_val = builder.call(boxer, [p], name='ret_ptr2h')
# Resolve direct value
if isinstance(return_type, ir.PointerType):
ret_val = resolver.resolve_ptr(value_id, builder.block, preds, block_end_values, vmap)
else:
ret_val = resolver.resolve_i64(value_id, builder.block, preds, block_end_values, vmap, bb_map)
except Exception:
ret_val = None
is_stringish = False
if hasattr(resolver, 'is_stringish'):
try:
is_stringish = resolver.is_stringish(int(value_id))
except Exception:
is_stringish = False
if is_stringish and hasattr(resolver, 'string_ptrs') and int(value_id) in getattr(resolver, 'string_ptrs'):
p = resolver.string_ptrs[int(value_id)]
i8p = ir.IntType(8).as_pointer()
i64 = ir.IntType(64)
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')
ret_val = builder.call(boxer, [p], name='ret_ptr2h')
else:
ret_val = resolver.resolve_i64(value_id, builder.block, preds, block_end_values, vmap, bb_map)
if ret_val is None:
ret_val = vmap.get(value_id)
# Default to vmap (non-PHI) if available
tmp = vmap.get(value_id)
try:
is_phi = hasattr(tmp, 'add_incoming')
except Exception:
is_phi = False
if tmp is not None and not is_phi:
ret_val = tmp
if not ret_val:
# Default based on return type
if isinstance(return_type, ir.IntType):

View File

@ -32,6 +32,12 @@ from instructions.controlflow.while_ import lower_while_regular
from phi_wiring import setup_phi_placeholders as _setup_phi_placeholders, finalize_phis as _finalize_phis
from trace import debug as trace_debug
from trace import phi as trace_phi
try:
# Structured JSON trace for PHI wiring (shared with phi_wiring)
from phi_wiring.common import trace as trace_phi_json
except Exception:
def trace_phi_json(_msg):
pass
from prepass.loops import detect_simple_while
from prepass.if_merge import plan_ret_phi_predeclare
from build_ctx import BuildCtx
@ -397,6 +403,90 @@ class NyashLLVMBuilder:
except Exception:
pass
# Predeclare PHIs for values used in a block but defined in predecessors (multi-pred only).
# This keeps PHI nodes grouped at the top and avoids late synthesis during operand resolution.
try:
from cfg.utils import build_preds_succs
local_preds, _ = build_preds_succs(block_by_id)
def _collect_defs(block):
defs = set()
for ins in block.get('instructions') or []:
try:
dstv = ins.get('dst')
if isinstance(dstv, int):
defs.add(int(dstv))
except Exception:
pass
return defs
def _collect_uses(block):
uses = set()
for ins in block.get('instructions') or []:
# Minimal keys: lhs/rhs (binop), value (ret/copy), cond (branch), box_val (boxcall)
for k in ('lhs','rhs','value','cond','box_val'):
try:
v = ins.get(k)
if isinstance(v, int):
uses.add(int(v))
except Exception:
pass
return uses
# Ensure map for declared incomings exists
if not hasattr(self, 'block_phi_incomings') or self.block_phi_incomings is None:
self.block_phi_incomings = {}
for bid, blk in block_by_id.items():
# Only multi-pred blocks need PHIs
try:
preds_raw = [p for p in local_preds.get(int(bid), []) if p != int(bid)]
except Exception:
preds_raw = []
# Dedup preds preserve order
seen = set(); preds_list = []
for p in preds_raw:
if p not in seen: preds_list.append(p); seen.add(p)
if len(preds_list) <= 1:
continue
defs = _collect_defs(blk)
uses = _collect_uses(blk)
need = [u for u in uses if u not in defs]
if not need:
continue
bb0 = self.bb_map.get(int(bid))
if bb0 is None:
continue
b0 = ir.IRBuilder(bb0)
try:
b0.position_at_start(bb0)
except Exception:
pass
for vid in need:
# Skip if we already have a PHI mapped for (bid, vid)
cur = self.vmap.get(int(vid))
has_phi_here = False
try:
has_phi_here = (
cur is not None and hasattr(cur, 'add_incoming') and
getattr(getattr(cur, 'basic_block', None), 'name', None) == bb0.name
)
except Exception:
has_phi_here = False
if not has_phi_here:
ph = b0.phi(self.i64, name=f"phi_{vid}")
self.vmap[int(vid)] = ph
# Record incoming metadata for finalize_phis (pred -> same vid)
try:
self.block_phi_incomings.setdefault(int(bid), {}).setdefault(int(vid), [])
# Overwrite with dedup list of (pred, vid)
self.block_phi_incomings[int(bid)][int(vid)] = [(int(p), int(vid)) for p in preds_list]
except Exception:
pass
# Expose to resolver
try:
self.resolver.block_phi_incomings = self.block_phi_incomings
except Exception:
pass
except Exception:
pass
# Optional: simple loop prepass → synthesize a structured while body
loop_plan = None
try:
@ -444,9 +534,19 @@ class NyashLLVMBuilder:
try:
# Use a clean per-while vmap context seeded from global placeholders
self._current_vmap = dict(self.vmap)
ok = lower_while_loopform(builder, func, cond_vid, body_insts,
self.loop_count, self.vmap, self.bb_map,
self.resolver, self.preds, self.block_end_values)
ok = lower_while_loopform(
builder,
func,
cond_vid,
body_insts,
self.loop_count,
self.vmap,
self.bb_map,
self.resolver,
self.preds,
self.block_end_values,
getattr(self, 'ctx', None),
)
except Exception:
ok = False
if not ok:
@ -810,7 +910,11 @@ class NyashLLVMBuilder:
try:
import os
keys = sorted(list(snap.keys()))
trace_phi(f"[builder] snapshot bb{bid} keys={keys[:20]}...")
# Emit structured snapshot event for up to first 20 keys
try:
trace_phi_json({"phi": "snapshot", "block": int(bid), "keys": [int(k) for k in keys[:20]]})
except Exception:
pass
except Exception:
pass
# Record block-local definitions for lifetime hinting
@ -941,7 +1045,7 @@ class NyashLLVMBuilder:
elif op == "barrier":
barrier_type = inst.get("type", "memory")
lower_barrier(builder, barrier_type)
lower_barrier(builder, barrier_type, ctx=getattr(self, 'ctx', None))
elif op == "while":
# Experimental LoopForm lowering
@ -1001,7 +1105,10 @@ class NyashLLVMBuilder:
for fr in from_list:
succs.setdefault(fr, []).append(to_bid)
for block_id, dst_map in (getattr(self, 'block_phi_incomings', {}) or {}).items():
trace_phi(f"[finalize] bb{block_id} dsts={list(dst_map.keys())}")
try:
trace_phi_json({"phi": "finalize_begin", "block": int(block_id), "dsts": [int(k) for k in (dst_map or {}).keys()]})
except Exception:
pass
bb = self.bb_map.get(block_id)
if bb is None:
continue
@ -1011,7 +1118,10 @@ class NyashLLVMBuilder:
except Exception:
pass
for dst_vid, incoming in (dst_map or {}).items():
trace_phi(f"[finalize] dst v{dst_vid} incoming={incoming}")
try:
trace_phi_json({"phi": "finalize_dst", "block": int(block_id), "dst": int(dst_vid), "incoming": [(int(v), int(b)) for (b, v) in [(b, v) for (v, b) in (incoming or [])]]})
except Exception:
pass
# Ensure placeholder exists at block head
# Prefer predeclared ret-phi when available and force using it.
predecl = getattr(self, 'predeclared_ret_phis', {}) if hasattr(self, 'predeclared_ret_phis') else {}
@ -1038,7 +1148,10 @@ class NyashLLVMBuilder:
phi = b.phi(self.i64, name=f"phi_{dst_vid}")
self.vmap[dst_vid] = phi
n = getattr(phi, 'name', b'').decode() if hasattr(getattr(phi, 'name', None), 'decode') else str(getattr(phi, 'name', ''))
trace_phi(f"[finalize] target phi={n}")
try:
trace_phi_json({"phi": "finalize_target", "block": int(block_id), "dst": int(dst_vid), "ir": str(n)})
except Exception:
pass
# Wire incoming per CFG predecessor; map src_vid when provided
preds_raw = [p for p in self.preds.get(block_id, []) if p != block_id]
# Deduplicate while preserving order
@ -1214,8 +1327,7 @@ def main():
if dummy:
# Emit dummy ny_main
ir_text = builder._create_dummy_main()
if os.environ.get('NYASH_CLI_VERBOSE') == '1':
print(f"[Python LLVM] Generated dummy IR:\n{ir_text}")
trace_debug(f"[Python LLVM] Generated dummy IR:\n{ir_text}")
builder.compile_to_object(output_file)
print(f"Compiled to {output_file}")
return
@ -1229,8 +1341,7 @@ def main():
mir_json = json.load(f)
llvm_ir = builder.build_from_mir(mir_json)
if os.environ.get('NYASH_CLI_VERBOSE') == '1':
print(f"[Python LLVM] Generated LLVM IR (see NYASH_LLVM_DUMP_IR or tmp/nyash_harness.ll)")
trace_debug("[Python LLVM] Generated LLVM IR (see NYASH_LLVM_DUMP_IR or tmp/nyash_harness.ll)")
builder.compile_to_object(output_file)
print(f"Compiled to {output_file}")

View File

@ -1,270 +0,0 @@
"""
PHI wiring helpers
- setup_phi_placeholders: Predeclare PHIs and collect incoming metadata
- finalize_phis: Wire PHI incomings using end-of-block snapshots and resolver
These operate on the NyashLLVMBuilder instance to keep changes minimal.
Refactor note: core responsibilities are split into smaller helpers so they
can be unit-tested in isolation.
"""
from typing import Dict, List, Any, Optional, Tuple
import os
import json
import llvmlite.ir as ir
# ---- Small helpers (analyzable/testable) ----
def _collect_produced_stringish(blocks: List[Dict[str, Any]]) -> Dict[int, bool]:
produced_str: Dict[int, bool] = {}
for block_data in blocks:
for inst in block_data.get("instructions", []) or []:
try:
opx = inst.get("op")
dstx = inst.get("dst")
if dstx is None:
continue
is_str = False
if opx == "const":
v = inst.get("value", {}) or {}
t = v.get("type")
if t == "string" or (isinstance(t, dict) and t.get("kind") in ("handle", "ptr") and t.get("box_type") == "StringBox"):
is_str = True
elif opx in ("binop", "boxcall", "externcall"):
t = inst.get("dst_type")
if isinstance(t, dict) and t.get("kind") == "handle" and t.get("box_type") == "StringBox":
is_str = True
if is_str:
produced_str[int(dstx)] = True
except Exception:
pass
return produced_str
def _trace(msg: Any):
if os.environ.get("NYASH_LLVM_TRACE_PHI", "0") == "1":
out = os.environ.get("NYASH_LLVM_TRACE_OUT")
# Format as single-line JSON for machine parsing
if not isinstance(msg, (str, bytes)):
try:
msg = json.dumps(msg, ensure_ascii=False, separators=(",", ":"))
except Exception:
msg = str(msg)
if out:
try:
with open(out, "a", encoding="utf-8") as f:
f.write(msg.rstrip() + "\n")
except Exception:
pass
else:
try:
print(msg)
except Exception:
pass
def analyze_incomings(blocks: List[Dict[str, Any]]) -> Dict[int, Dict[int, List[Tuple[int, int]]]]:
"""Return block_phi_incomings map: block_id -> { dst_vid -> [(decl_b, v_src), ...] }"""
result: Dict[int, Dict[int, List[Tuple[int, int]]]] = {}
for block_data in blocks:
bid0 = block_data.get("id", 0)
for inst in block_data.get("instructions", []) or []:
if inst.get("op") == "phi":
try:
dst0 = int(inst.get("dst"))
incoming0 = inst.get("incoming", []) or []
except Exception:
dst0 = None; incoming0 = []
if dst0 is None:
continue
try:
pairs = [(int(b), int(v)) for (v, b) in incoming0]
result.setdefault(int(bid0), {})[dst0] = pairs
_trace({
"phi": "analyze",
"block": int(bid0),
"dst": dst0,
"incoming": pairs,
})
except Exception:
pass
return result
def ensure_phi(builder, block_id: int, dst_vid: int, bb: ir.Block) -> ir.Instruction:
"""Ensure a PHI placeholder exists at the block head for dst_vid and return it."""
b = ir.IRBuilder(bb)
try:
b.position_at_start(bb)
except Exception:
pass
# Prefer predeclared PHIs (e.g., from if-merge prepass)
predecl = getattr(builder, 'predeclared_ret_phis', {}) if hasattr(builder, 'predeclared_ret_phis') else {}
phi = predecl.get((int(block_id), int(dst_vid))) if predecl else None
if phi is not None:
builder.vmap[dst_vid] = phi
_trace({"phi": "ensure_predecl", "block": int(block_id), "dst": int(dst_vid)})
return phi
# Reuse current if it is a PHI in the correct block
cur = builder.vmap.get(dst_vid)
try:
if cur is not None and hasattr(cur, 'add_incoming') and getattr(getattr(cur, 'basic_block', None), 'name', None) == bb.name:
return cur
except Exception:
pass
# Create a new placeholder
ph = b.phi(builder.i64, name=f"phi_{dst_vid}")
builder.vmap[dst_vid] = ph
_trace({"phi": "ensure_create", "block": int(block_id), "dst": int(dst_vid)})
return ph
def _build_succs(preds: Dict[int, List[int]]) -> Dict[int, List[int]]:
succs: Dict[int, List[int]] = {}
for to_bid, from_list in (preds or {}).items():
for fr in from_list:
succs.setdefault(fr, []).append(to_bid)
return succs
def _nearest_pred_on_path(succs: Dict[int, List[int]], preds_list: List[int], decl_b: int, target_bid: int) -> Optional[int]:
from collections import deque
q = deque([decl_b])
visited = set([decl_b])
parent: Dict[int, Any] = {decl_b: None}
while q:
cur = q.popleft()
if cur == target_bid:
par = parent.get(target_bid)
return par if par in preds_list else None
for nx in succs.get(cur, []):
if nx not in visited:
visited.add(nx)
parent[nx] = cur
q.append(nx)
return None
def wire_incomings(builder, block_id: int, dst_vid: int, incoming: List[Tuple[int, int]]):
"""Wire PHI incoming edges for (block_id, dst_vid) using declared (decl_b, v_src) pairs."""
bb = builder.bb_map.get(block_id)
if bb is None:
return
phi = ensure_phi(builder, block_id, dst_vid, bb)
# Normalize predecessor list
preds_raw = [p for p in builder.preds.get(block_id, []) if p != block_id]
seen = set()
preds_list: List[int] = []
for p in preds_raw:
if p not in seen:
preds_list.append(p)
seen.add(p)
succs = _build_succs(builder.preds)
# Precompute a non-self initial source for self-carry
init_src_vid = None
for (_bd0, vs0) in incoming:
try:
vi = int(vs0)
except Exception:
continue
if vi != int(dst_vid):
init_src_vid = vi
break
chosen: Dict[int, ir.Value] = {}
for (b_decl, v_src) in incoming:
try:
bd = int(b_decl); vs = int(v_src)
except Exception:
continue
pred_match = _nearest_pred_on_path(succs, preds_list, bd, block_id)
if pred_match is None:
_trace({
"phi": "wire_skip_no_path",
"decl_b": bd,
"target": int(block_id),
"src": vs,
})
continue
if vs == int(dst_vid) and init_src_vid is not None:
vs = int(init_src_vid)
try:
val = builder.resolver._value_at_end_i64(vs, pred_match, builder.preds, builder.block_end_values, builder.vmap, builder.bb_map)
except Exception:
val = None
if val is None:
val = ir.Constant(builder.i64, 0)
chosen[pred_match] = val
_trace({
"phi": "wire_choose",
"pred": int(pred_match),
"dst": int(dst_vid),
"src": int(vs),
})
for pred_bid, val in chosen.items():
pred_bb = builder.bb_map.get(pred_bid)
if pred_bb is None:
continue
phi.add_incoming(val, pred_bb)
_trace({"phi": "add_incoming", "dst": int(dst_vid), "pred": int(pred_bid)})
# ---- Public API (used by llvm_builder) ----
def setup_phi_placeholders(builder, blocks: List[Dict[str, Any]]):
"""Predeclare PHIs and collect incoming metadata for finalize_phis.
This pass is function-local and must be invoked after basic blocks are
created and before lowering individual blocks. It also tags string-ish
values eagerly to help downstream resolvers choose correct intrinsics.
"""
try:
produced_str = _collect_produced_stringish(blocks)
builder.block_phi_incomings = analyze_incomings(blocks)
_trace({"phi": "setup", "produced_str_keys": list(produced_str.keys())})
# Materialize placeholders and propagate stringish tags
for block_data in blocks:
bid0 = block_data.get("id", 0)
bb0 = builder.bb_map.get(bid0)
for inst in block_data.get("instructions", []) or []:
if inst.get("op") != "phi":
continue
try:
dst0 = int(inst.get("dst"))
incoming0 = inst.get("incoming", []) or []
except Exception:
dst0 = None; incoming0 = []
if dst0 is None or bb0 is None:
continue
_ = ensure_phi(builder, bid0, dst0, bb0)
# Tag propagation
try:
dst_type0 = inst.get("dst_type")
mark_str = isinstance(dst_type0, dict) and dst_type0.get("kind") == "handle" and dst_type0.get("box_type") == "StringBox"
if not mark_str:
for (_b_decl_i, v_src_i) in incoming0:
try:
if produced_str.get(int(v_src_i)):
mark_str = True; break
except Exception:
pass
if mark_str and hasattr(builder.resolver, 'mark_string'):
builder.resolver.mark_string(int(dst0))
except Exception:
pass
# Definition hint: PHI defines dst in this block
try:
builder.def_blocks.setdefault(int(dst0), set()).add(int(bid0))
except Exception:
pass
# Sync to resolver
try:
builder.resolver.block_phi_incomings = builder.block_phi_incomings
except Exception:
pass
except Exception:
pass
def finalize_phis(builder):
"""Finalize PHIs declared in JSON by wiring incoming edges at block heads.
Uses resolver._value_at_end_i64 to materialize values at predecessor ends.
"""
for block_id, dst_map in (getattr(builder, 'block_phi_incomings', {}) or {}).items():
for dst_vid, incoming in (dst_map or {}).items():
wire_incomings(builder, int(block_id), int(dst_vid), incoming)
_trace({"phi": "finalize", "block": int(block_id), "dst": int(dst_vid)})

View File

@ -0,0 +1,33 @@
"""
PHI wiring package
Submodules
- analysis: analyze_incomings and stringish production scan
- wiring: ensure_phi, wire_incomings, finalize_phis
- tagging: setup_phi_placeholders (predeclare + tagging + sync)
This package re-exports the primary helpers for backward compatibility with
`from phi_wiring import ...` and `from src.llvm_py import phi_wiring` usage.
"""
from .analysis import analyze_incomings, collect_produced_stringish
from .wiring import ensure_phi, wire_incomings, finalize_phis, build_succs, nearest_pred_on_path, phi_at_block_head
from .tagging import setup_phi_placeholders
# Backward-compatible aliases for tests that used private helpers
_build_succs = build_succs
_nearest_pred_on_path = nearest_pred_on_path
__all__ = [
"analyze_incomings",
"collect_produced_stringish",
"ensure_phi",
"wire_incomings",
"finalize_phis",
"phi_at_block_head",
"build_succs",
"nearest_pred_on_path",
"setup_phi_placeholders",
"_build_succs",
"_nearest_pred_on_path",
]

View File

@ -0,0 +1,68 @@
from __future__ import annotations
from typing import Dict, List, Any, Tuple
from .common import trace
def collect_produced_stringish(blocks: List[Dict[str, Any]]) -> Dict[int, bool]:
produced_str: Dict[int, bool] = {}
for block_data in blocks:
for inst in block_data.get("instructions", []) or []:
try:
opx = inst.get("op")
dstx = inst.get("dst")
if dstx is None:
continue
is_str = False
if opx == "const":
v = inst.get("value", {}) or {}
t = v.get("type")
if t == "string" or (
isinstance(t, dict)
and t.get("kind") in ("handle", "ptr")
and t.get("box_type") == "StringBox"
):
is_str = True
elif opx in ("binop", "boxcall", "externcall"):
t = inst.get("dst_type")
if (
isinstance(t, dict)
and t.get("kind") == "handle"
and t.get("box_type") == "StringBox"
):
is_str = True
if is_str:
produced_str[int(dstx)] = True
except Exception:
pass
return produced_str
def analyze_incomings(blocks: List[Dict[str, Any]]) -> Dict[int, Dict[int, List[Tuple[int, int]]]]:
"""Return block_phi_incomings: block_id -> { dst_vid -> [(decl_b, v_src), ...] }"""
result: Dict[int, Dict[int, List[Tuple[int, int]]]] = {}
for block_data in blocks:
bid0 = block_data.get("id", 0)
for inst in block_data.get("instructions", []) or []:
if inst.get("op") == "phi":
try:
dst0 = int(inst.get("dst"))
incoming0 = inst.get("incoming", []) or []
except Exception:
dst0 = None
incoming0 = []
if dst0 is None:
continue
try:
pairs = [(int(b), int(v)) for (v, b) in incoming0]
result.setdefault(int(bid0), {})[dst0] = pairs
trace({
"phi": "analyze",
"block": int(bid0),
"dst": dst0,
"incoming": pairs,
})
except Exception:
pass
return result

View File

@ -0,0 +1,26 @@
from __future__ import annotations
from typing import Any
import os
import json
def trace(msg: Any):
if os.environ.get("NYASH_LLVM_TRACE_PHI", "0") != "1":
return
out = os.environ.get("NYASH_LLVM_TRACE_OUT")
if not isinstance(msg, (str, bytes)):
try:
msg = json.dumps(msg, ensure_ascii=False, separators=(",", ":"))
except Exception:
msg = str(msg)
if out:
try:
with open(out, "a", encoding="utf-8") as f:
f.write(msg.rstrip() + "\n")
except Exception:
pass
else:
try:
print(msg)
except Exception:
pass

View File

@ -0,0 +1,66 @@
from __future__ import annotations
from typing import Dict, List, Any
from .common import trace
from .analysis import analyze_incomings, collect_produced_stringish
from .wiring import ensure_phi
def setup_phi_placeholders(builder, blocks: List[Dict[str, Any]]):
"""Predeclare PHIs and collect incoming metadata for finalize_phis.
Function-local: must be invoked after basic blocks are created and before
lowering individual blocks. Also tags string-ish values to help downstream
resolvers.
"""
try:
produced_str = collect_produced_stringish(blocks)
builder.block_phi_incomings = analyze_incomings(blocks)
trace({"phi": "setup", "produced_str_keys": list(produced_str.keys())})
for block_data in blocks:
bid0 = block_data.get("id", 0)
bb0 = builder.bb_map.get(bid0)
for inst in block_data.get("instructions", []) or []:
if inst.get("op") != "phi":
continue
try:
dst0 = int(inst.get("dst"))
incoming0 = inst.get("incoming", []) or []
except Exception:
dst0 = None
incoming0 = []
if dst0 is None or bb0 is None:
continue
_ = ensure_phi(builder, bid0, dst0, bb0)
# Tag propagation
try:
dst_type0 = inst.get("dst_type")
mark_str = (
isinstance(dst_type0, dict)
and dst_type0.get("kind") == "handle"
and dst_type0.get("box_type") == "StringBox"
)
if not mark_str:
# JSON v0 incoming pairs are (value, block)
for (v_src_i, _b_decl_i) in incoming0:
try:
if produced_str.get(int(v_src_i)):
mark_str = True
break
except Exception:
pass
if mark_str and hasattr(builder.resolver, "mark_string"):
builder.resolver.mark_string(int(dst0))
except Exception:
pass
# Definition hint: PHI defines dst in this block
try:
builder.def_blocks.setdefault(int(dst0), set()).add(int(bid0))
except Exception:
pass
try:
builder.resolver.block_phi_incomings = builder.block_phi_incomings
except Exception:
pass
except Exception:
pass

View File

@ -0,0 +1,143 @@
from __future__ import annotations
from typing import Dict, List, Any, Optional, Tuple
import llvmlite.ir as ir
from .common import trace
def ensure_phi(builder, block_id: int, dst_vid: int, bb: ir.Block) -> ir.Instruction:
"""Ensure a PHI placeholder exists at the block head for dst_vid and return it."""
b = ir.IRBuilder(bb)
try:
b.position_at_start(bb)
except Exception:
pass
predecl = getattr(builder, "predeclared_ret_phis", {}) if hasattr(builder, "predeclared_ret_phis") else {}
phi = predecl.get((int(block_id), int(dst_vid))) if predecl else None
if phi is not None:
builder.vmap[dst_vid] = phi
trace({"phi": "ensure_predecl", "block": int(block_id), "dst": int(dst_vid)})
return phi
cur = builder.vmap.get(dst_vid)
try:
if cur is not None and hasattr(cur, "add_incoming") and getattr(getattr(cur, "basic_block", None), "name", None) == bb.name:
return cur
except Exception:
pass
ph = b.phi(builder.i64, name=f"phi_{dst_vid}")
builder.vmap[dst_vid] = ph
trace({"phi": "ensure_create", "block": int(block_id), "dst": int(dst_vid)})
return ph
def phi_at_block_head(block: ir.Block, ty: ir.Type, name: str | None = None) -> ir.Instruction:
"""Create a PHI at the very start of `block` and return it.
Keeps LLVM's requirement that PHI nodes are grouped at the top of a block.
"""
b = ir.IRBuilder(block)
try:
b.position_at_start(block)
except Exception:
pass
return b.phi(ty, name=name) if name is not None else b.phi(ty)
def build_succs(preds: Dict[int, List[int]]) -> Dict[int, List[int]]:
succs: Dict[int, List[int]] = {}
for to_bid, from_list in (preds or {}).items():
for fr in from_list:
succs.setdefault(fr, []).append(to_bid)
return succs
def nearest_pred_on_path(
succs: Dict[int, List[int]], preds_list: List[int], decl_b: int, target_bid: int
) -> Optional[int]:
from collections import deque
q = deque([decl_b])
visited = set([decl_b])
parent: Dict[int, Any] = {decl_b: None}
while q:
cur = q.popleft()
if cur == target_bid:
par = parent.get(target_bid)
return par if par in preds_list else None
for nx in succs.get(cur, []):
if nx not in visited:
visited.add(nx)
parent[nx] = cur
q.append(nx)
return None
def wire_incomings(builder, block_id: int, dst_vid: int, incoming: List[Tuple[int, int]]):
"""Wire PHI incoming edges for (block_id, dst_vid) using declared (decl_b, v_src) pairs."""
bb = builder.bb_map.get(block_id)
if bb is None:
return
phi = ensure_phi(builder, block_id, dst_vid, bb)
preds_raw = [p for p in builder.preds.get(block_id, []) if p != block_id]
seen = set()
preds_list: List[int] = []
for p in preds_raw:
if p not in seen:
preds_list.append(p)
seen.add(p)
succs = build_succs(builder.preds)
init_src_vid = None
for (_bd0, vs0) in incoming:
try:
vi = int(vs0)
except Exception:
continue
if vi != int(dst_vid):
init_src_vid = vi
break
chosen: Dict[int, ir.Value] = {}
for (b_decl, v_src) in incoming:
try:
bd = int(b_decl)
vs = int(v_src)
except Exception:
continue
pred_match = nearest_pred_on_path(succs, preds_list, bd, block_id)
if pred_match is None:
trace({"phi": "wire_skip_no_path", "decl_b": bd, "target": int(block_id), "src": vs})
continue
if vs == int(dst_vid) and init_src_vid is not None:
vs = int(init_src_vid)
try:
val = builder.resolver._value_at_end_i64(
vs, pred_match, builder.preds, builder.block_end_values, builder.vmap, builder.bb_map
)
except Exception:
val = None
if val is None:
val = ir.Constant(builder.i64, 0)
chosen[pred_match] = val
trace({"phi": "wire_choose", "pred": int(pred_match), "dst": int(dst_vid), "src": int(vs)})
wired = 0
for pred_bid, val in chosen.items():
pred_bb = builder.bb_map.get(pred_bid)
if pred_bb is None:
continue
phi.add_incoming(val, pred_bb)
trace({"phi": "add_incoming", "dst": int(dst_vid), "pred": int(pred_bid)})
wired += 1
return wired
def finalize_phis(builder):
total_blocks = 0
total_dsts = 0
total_wired = 0
for block_id, dst_map in (getattr(builder, "block_phi_incomings", {}) or {}).items():
total_blocks += 1
for dst_vid, incoming in (dst_map or {}).items():
total_dsts += 1
wired = wire_incomings(builder, int(block_id), int(dst_vid), incoming)
total_wired += int(wired or 0)
trace({"phi": "finalize", "block": int(block_id), "dst": int(dst_vid), "wired": int(wired or 0)})
trace({"phi": "finalize_summary", "blocks": int(total_blocks), "dsts": int(total_dsts), "incoming_wired": int(total_wired)})

View File

@ -28,8 +28,20 @@ def plan_ret_phi_predeclare(block_by_id: Dict[int, Dict[str, Any]]) -> Optional[
val = term.get('value')
if not isinstance(val, int):
continue
# Heuristic: skip when the return value is freshly defined in this block
# (e.g., returning a const computed in cleanup). Predeclaring a PHI for such
# values is unnecessary and may violate PHI grouping/order.
try:
defined_here = False
for ins in blk.get('instructions') or []:
if isinstance(ins, dict) and ins.get('dst') == int(val):
defined_here = True
break
if defined_here:
continue
except Exception:
pass
pred_list = [p for p in preds.get(int(bid), []) if p != int(bid)]
if len(pred_list) > 1:
plan[int(bid)] = int(val)
return plan or None

View File

@ -222,37 +222,8 @@ class Resolver:
placeholder = vmap.get(value_id)
result = placeholder if (placeholder is not None and hasattr(placeholder, 'add_incoming')) else ir.Constant(self.i64, 0)
else:
# Synthesize a PHI to localize the value and dominate its uses.
try:
bb = bb_map.get(cur_bid) if isinstance(bb_map, dict) else current_block
except Exception:
bb = current_block
b = ir.IRBuilder(bb)
try:
b.position_at_start(bb)
except Exception:
pass
existing = vmap.get(value_id)
if existing is not None and hasattr(existing, 'add_incoming'):
phi = existing
else:
phi = b.phi(self.i64, name=f"res_phi_{value_id}_{cur_bid}")
vmap[value_id] = phi
# Wire end-of-block values from each predecessor
for pred_bid in pred_ids:
try:
pred_bb = bb_map.get(pred_bid) if isinstance(bb_map, dict) else None
except Exception:
pred_bb = None
val = self._value_at_end_i64(value_id, pred_bid, preds, block_end_values, vmap, bb_map)
if pred_bb is None:
# If we cannot map to a real basic block (shouldn't happen),
# fallback to a zero to keep IR consistent.
val = val if hasattr(val, 'type') else ir.Constant(self.i64, 0)
# llvmlite requires a real BasicBlock; skip if missing.
continue
phi.add_incoming(val, pred_bb)
result = phi
# No declared PHI and multi-pred: do not synthesize; fallback to zero
result = ir.Constant(self.i64, 0)
# Cache and return
self.i64_cache[cache_key] = result

View File

@ -0,0 +1,80 @@
import unittest
import llvmlite.ir as ir
from src.llvm_py.phi_wiring import setup_phi_placeholders
class DummyResolver:
def __init__(self):
self.marked = set()
def mark_string(self, vid: int):
self.marked.add(int(vid))
class DummyBuilder:
def __init__(self, bb_map):
self.i64 = ir.IntType(64)
self.vmap = {}
self.def_blocks = {}
self.resolver = DummyResolver()
self.bb_map = bb_map
class TestPhiTagging(unittest.TestCase):
def _mk_blocks_and_bbs(self):
mod = ir.Module(name="m")
fnty = ir.FunctionType(ir.VoidType(), [])
fn = ir.Function(mod, fnty, name="f")
b0 = fn.append_basic_block(name="b0")
b1 = fn.append_basic_block(name="b1")
return mod, {0: b0, 1: b1}
def test_mark_by_dst_type(self):
_mod, bb_map = self._mk_blocks_and_bbs()
builder = DummyBuilder(bb_map)
blocks = [
{
"id": 1,
"instructions": [
{
"op": "phi",
"dst": 42,
"dst_type": {"kind": "handle", "box_type": "StringBox"},
"incoming": [[7, 0]],
}
],
}
]
setup_phi_placeholders(builder, blocks)
self.assertIn(42, builder.resolver.marked)
def test_mark_by_incoming_stringish(self):
_mod, bb_map = self._mk_blocks_and_bbs()
builder = DummyBuilder(bb_map)
blocks = [
{
"id": 0,
"instructions": [
{"op": "const", "dst": 7, "value": {"type": "string", "value": "hi"}}
],
},
{
"id": 1,
"instructions": [
{
"op": "phi",
"dst": 43,
# no dst_type string; inference should happen via incoming
"incoming": [[7, 0]],
}
],
},
]
setup_phi_placeholders(builder, blocks)
self.assertIn(43, builder.resolver.marked)
if __name__ == "__main__":
unittest.main()

View File

@ -14,28 +14,40 @@ Import and use:
"""
import os
import json
_TRACE_OUT = os.environ.get('NYASH_LLVM_TRACE_OUT')
def _write(msg: str) -> None:
if _TRACE_OUT:
try:
with open(_TRACE_OUT, 'a', encoding='utf-8') as f:
f.write(msg.rstrip() + "\n")
return
except Exception:
pass
try:
print(msg, flush=True)
except Exception:
pass
def _enabled(env_key: str) -> bool:
return os.environ.get(env_key) == '1'
def debug(msg: str) -> None:
if _enabled('NYASH_CLI_VERBOSE'):
try:
print(msg, flush=True)
except Exception:
pass
_write(msg)
def phi(msg: str) -> None:
def phi(msg) -> None:
if _enabled('NYASH_LLVM_TRACE_PHI'):
try:
print(msg, flush=True)
except Exception:
pass
# Accept raw strings or arbitrary objects; non-strings are JSON-encoded
if not isinstance(msg, (str, bytes)):
try:
msg = json.dumps(msg, ensure_ascii=False, separators=(",", ":"))
except Exception:
msg = str(msg)
_write(msg)
def values(msg: str) -> None:
if _enabled('NYASH_LLVM_TRACE_VALUES'):
try:
print(msg, flush=True)
except Exception:
pass
_write(msg)

View File

@ -25,6 +25,7 @@ mod fields; // field access/assignment lowering split
pub(crate) mod loops;
mod ops;
mod phi;
mod if_form;
mod lifecycle; // prepare/lower_root/finalize split
// legacy large-match remains inline for now (planned extraction)
mod plugin_sigs; // plugin signature loader
@ -89,8 +90,28 @@ pub struct MirBuilder {
pub(super) loop_header_stack: Vec<BasicBlockId>,
pub(super) loop_exit_stack: Vec<BasicBlockId>,
/// If/merge context stack (innermost first). Used to make merge targets explicit
/// when lowering nested conditionals and to simplify jump generation.
pub(super) if_merge_stack: Vec<BasicBlockId>,
/// Whether PHI emission is disabled (edge-copy mode)
pub(super) no_phi_mode: bool,
// ---- Try/Catch/Cleanup lowering context ----
/// When true, `return` statements are deferred: they assign to `return_defer_slot`
/// and jump to `return_defer_target` (typically the cleanup/exit block).
pub(super) return_defer_active: bool,
/// Slot value to receive deferred return values (edge-copy mode friendly).
pub(super) return_defer_slot: Option<ValueId>,
/// Target block to jump to on deferred return.
pub(super) return_defer_target: Option<BasicBlockId>,
/// Set to true when a deferred return has been emitted in the current context.
pub(super) return_deferred_emitted: bool,
/// True while lowering the cleanup block.
pub(super) in_cleanup_block: bool,
/// Policy flags (snapshotted at entry of try/catch lowering)
pub(super) cleanup_allow_return: bool,
pub(super) cleanup_allow_throw: bool,
}
impl MirBuilder {
@ -117,10 +138,22 @@ impl MirBuilder {
include_box_map: HashMap::new(),
loop_header_stack: Vec::new(),
loop_exit_stack: Vec::new(),
if_merge_stack: Vec::new(),
no_phi_mode,
return_defer_active: false,
return_defer_slot: None,
return_defer_target: None,
return_deferred_emitted: false,
in_cleanup_block: false,
cleanup_allow_return: false,
cleanup_allow_throw: false,
}
}
/// Push/pop helpers for If merge context (best-effort; optional usage)
pub(super) fn push_if_merge(&mut self, bb: BasicBlockId) { self.if_merge_stack.push(bb); }
pub(super) fn pop_if_merge(&mut self) { let _ = self.if_merge_stack.pop(); }
// moved to builder_calls.rs: lower_method_as_function
/// Build a complete MIR module from AST
@ -795,6 +828,10 @@ impl MirBuilder {
dst: ValueId,
src: ValueId,
) -> Result<(), String> {
// No-op self copy guard to avoid useless instructions and accidental reordering issues
if dst == src {
return Ok(());
}
if let Some(ref mut function) = self.current_function {
let block = function
.get_block_mut(block_id)

102
src/mir/builder/if_form.rs Normal file
View File

@ -0,0 +1,102 @@
use super::{ConstValue, MirBuilder, MirInstruction, ValueId};
use crate::mir::loop_api::LoopBuilderApi; // for current_block()
use crate::ast::ASTNode;
impl MirBuilder {
/// Lower an if/else using a structured IfForm (header→then/else→merge).
/// PHI-off: edge-copy only on predecessors; PHI-on: Phi at merge.
pub(super) fn lower_if_form(
&mut self,
condition: ASTNode,
then_branch: ASTNode,
else_branch: Option<ASTNode>,
) -> Result<ValueId, String> {
let condition_val = self.build_expression(condition)?;
// Create blocks
let then_block = self.block_gen.next();
let else_block = self.block_gen.next();
let merge_block = self.block_gen.next();
// Branch
self.emit_instruction(MirInstruction::Branch {
condition: condition_val,
then_bb: then_block,
else_bb: else_block,
})?;
// Snapshot variables before entering branches
let pre_if_var_map = self.variable_map.clone();
// then
self.current_block = Some(then_block);
self.ensure_block_exists(then_block)?;
let then_ast_for_analysis = then_branch.clone();
self.variable_map = pre_if_var_map.clone();
let then_value_raw = self.build_expression(then_branch)?;
let then_exit_block = self.current_block()?;
let then_var_map_end = self.variable_map.clone();
if !self.is_current_block_terminated() {
self.emit_instruction(MirInstruction::Jump { target: merge_block })?;
}
// else
self.current_block = Some(else_block);
self.ensure_block_exists(else_block)?;
let (else_value_raw, else_ast_for_analysis, else_var_map_end_opt) = if let Some(else_ast) = else_branch {
self.variable_map = pre_if_var_map.clone();
let val = self.build_expression(else_ast.clone())?;
(val, Some(else_ast), Some(self.variable_map.clone()))
} else {
let void_val = self.value_gen.next();
self.emit_instruction(MirInstruction::Const { dst: void_val, value: ConstValue::Void })?;
(void_val, None, None)
};
let else_exit_block = self.current_block()?;
if !self.is_current_block_terminated() {
self.emit_instruction(MirInstruction::Jump { target: merge_block })?;
}
// merge: primary result via helper, then delta-based variable merges
self.current_block = Some(merge_block);
self.ensure_block_exists(merge_block)?;
self.push_if_merge(merge_block);
// Pre-analysis: identify then-assigned var for skip
let assigned_then_pre = super::phi::extract_assigned_var(&then_ast_for_analysis);
let pre_then_var_value = assigned_then_pre
.as_ref()
.and_then(|name| pre_if_var_map.get(name).copied());
let result_val = self.normalize_if_else_phi(
then_block,
else_block,
Some(then_exit_block),
Some(else_exit_block),
then_value_raw,
else_value_raw,
&pre_if_var_map,
&then_ast_for_analysis,
&else_ast_for_analysis,
&then_var_map_end,
&else_var_map_end_opt,
pre_then_var_value,
)?;
// Merge other modified variables (skip the primary assignment if any)
let skip_name = assigned_then_pre.as_deref();
self.merge_modified_vars(
then_block,
else_block,
then_exit_block,
Some(else_exit_block),
&pre_if_var_map,
&then_var_map_end,
&else_var_map_end_opt,
skip_name,
)?;
self.pop_if_merge();
Ok(result_val)
}
}

View File

@ -67,6 +67,76 @@ pub(super) fn extract_assigned_var(ast: &ASTNode) -> Option<String> {
}
impl MirBuilder {
/// Merge all variables modified in then/else relative to pre_if_snapshot.
/// In PHI-off mode inserts edge copies from branch exits to merge. In PHI-on mode emits Phi.
/// `skip_var` allows skipping a variable already merged elsewhere (e.g., bound to an expression result).
pub(super) fn merge_modified_vars(
&mut self,
then_block: super::BasicBlockId,
else_block: super::BasicBlockId,
then_exit_block: super::BasicBlockId,
else_exit_block_opt: Option<super::BasicBlockId>,
pre_if_snapshot: &std::collections::HashMap<String, super::ValueId>,
then_map_end: &std::collections::HashMap<String, super::ValueId>,
else_map_end_opt: &Option<std::collections::HashMap<String, super::ValueId>>,
skip_var: Option<&str>,
) -> Result<(), String> {
use std::collections::HashSet;
let mut names: HashSet<&str> = HashSet::new();
for k in then_map_end.keys() { names.insert(k.as_str()); }
if let Some(emap) = else_map_end_opt.as_ref() {
for k in emap.keys() { names.insert(k.as_str()); }
}
// Only variables that changed against pre_if_snapshot
let mut changed: Vec<&str> = Vec::new();
for &name in &names {
let pre = pre_if_snapshot.get(name);
let t = then_map_end.get(name);
let e = else_map_end_opt.as_ref().and_then(|m| m.get(name));
// changed when either branch value differs from pre
if (t.is_some() && Some(t.copied().unwrap()) != pre.copied())
|| (e.is_some() && Some(e.copied().unwrap()) != pre.copied())
{
changed.push(name);
}
}
for name in changed {
if skip_var.map(|s| s == name).unwrap_or(false) {
continue;
}
let pre = match pre_if_snapshot.get(name) {
Some(v) => *v,
None => continue, // unknown before-if; skip
};
let then_v = then_map_end.get(name).copied().unwrap_or(pre);
let else_v = else_map_end_opt
.as_ref()
.and_then(|m| m.get(name).copied())
.unwrap_or(pre);
if self.is_no_phi_mode() {
let merged = self.value_gen.next();
// Insert edge copies from then/else exits into merge
self.insert_edge_copy(then_exit_block, merged, then_v)?;
if let Some(else_exit_block) = else_exit_block_opt {
self.insert_edge_copy(else_exit_block, merged, else_v)?;
} else {
// Fallback: if else missing, copy pre value from then as both inputs already cover
self.insert_edge_copy(then_exit_block, merged, then_v)?;
}
self.variable_map.insert(name.to_string(), merged);
} else {
let merged = self.value_gen.next();
self.emit_instruction(
MirInstruction::Phi {
dst: merged,
inputs: vec![(then_block, then_v), (else_block, else_v)],
}
)?;
self.variable_map.insert(name.to_string(), merged);
}
}
Ok(())
}
/// Normalize Phi creation for if/else constructs.
/// This handles variable reassignment patterns and ensures a single exit value.
pub(super) fn normalize_if_else_phi(

View File

@ -1,4 +1,3 @@
use super::phi::extract_assigned_var;
use super::{ConstValue, Effect, EffectMask, MirInstruction, ValueId};
use crate::ast::{ASTNode, CallExpr};
use crate::mir::loop_builder::LoopBuilder;
@ -162,82 +161,7 @@ impl super::MirBuilder {
then_branch: ASTNode,
else_branch: Option<ASTNode>,
) -> Result<ValueId, String> {
let condition_val = self.build_expression(condition)?;
let then_block = self.block_gen.next();
let else_block = self.block_gen.next();
let merge_block = self.block_gen.next();
self.emit_instruction(MirInstruction::Branch {
condition: condition_val,
then_bb: then_block,
else_bb: else_block,
})?;
// Snapshot variable map before entering branches to avoid cross-branch pollution
let pre_if_var_map = self.variable_map.clone();
// Pre-analysis: detect then-branch assigned var and capture its pre-if value
let assigned_then_pre = extract_assigned_var(&then_branch);
let pre_then_var_value: Option<ValueId> = assigned_then_pre
.as_ref()
.and_then(|name| pre_if_var_map.get(name).copied());
// then
self.current_block = Some(then_block);
self.ensure_block_exists(then_block)?;
let then_ast_for_analysis = then_branch.clone();
// Build then with a clean snapshot of pre-if variables
self.variable_map = pre_if_var_map.clone();
let then_value_raw = self.build_expression(then_branch)?;
let then_exit_block = Self::current_block(self)?;
let then_var_map_end = self.variable_map.clone();
if !self.is_current_block_terminated() {
self.emit_instruction(MirInstruction::Jump {
target: merge_block,
})?;
}
// else
self.current_block = Some(else_block);
self.ensure_block_exists(else_block)?;
// Build else with a clean snapshot of pre-if variables
let (else_value_raw, else_ast_for_analysis, else_var_map_end_opt) =
if let Some(else_ast) = else_branch {
self.variable_map = pre_if_var_map.clone();
let val = self.build_expression(else_ast.clone())?;
(val, Some(else_ast), Some(self.variable_map.clone()))
} else {
let void_val = self.value_gen.next();
self.emit_instruction(MirInstruction::Const {
dst: void_val,
value: ConstValue::Void,
})?;
(void_val, None, None)
};
let else_exit_block = Self::current_block(self)?;
if !self.is_current_block_terminated() {
self.emit_instruction(MirInstruction::Jump {
target: merge_block,
})?;
}
// merge + phi
self.current_block = Some(merge_block);
self.ensure_block_exists(merge_block)?;
let result_val = self.normalize_if_else_phi(
then_block,
else_block,
Some(then_exit_block),
Some(else_exit_block),
then_value_raw,
else_value_raw,
&pre_if_var_map,
&then_ast_for_analysis,
&else_ast_for_analysis,
&then_var_map_end,
&else_var_map_end_opt,
pre_then_var_value,
)?;
Ok(result_val)
self.lower_if_form(condition, then_branch, else_branch)
}
// Loop: delegate to LoopBuilder
@ -278,6 +202,21 @@ impl super::MirBuilder {
};
let exit_block = self.block_gen.next();
// Snapshot and enable deferred-return mode for try/catch
let saved_defer_active = self.return_defer_active;
let saved_defer_slot = self.return_defer_slot;
let saved_defer_target = self.return_defer_target;
let saved_deferred_flag = self.return_deferred_emitted;
let saved_in_cleanup = self.in_cleanup_block;
let saved_allow_ret = self.cleanup_allow_return;
let saved_allow_throw = self.cleanup_allow_throw;
let ret_slot = self.value_gen.next();
self.return_defer_active = true;
self.return_defer_slot = Some(ret_slot);
self.return_deferred_emitted = false;
self.return_defer_target = Some(finally_block.unwrap_or(exit_block));
if let Some(catch_clause) = catch_clauses.first() {
if std::env::var("NYASH_DEBUG_TRYCATCH").ok().as_deref() == Some("1") {
eprintln!(
@ -331,27 +270,61 @@ impl super::MirBuilder {
})?;
}
let mut cleanup_terminated = false;
if let (Some(finally_block_id), Some(finally_statements)) = (finally_block, finally_body) {
self.start_new_block(finally_block_id)?;
// Enter cleanup mode; returns may or may not be allowed by policy
self.in_cleanup_block = true;
self.cleanup_allow_return = crate::config::env::cleanup_allow_return();
self.cleanup_allow_throw = crate::config::env::cleanup_allow_throw();
// Inside cleanup, do not defer returns; either forbid or emit real Return
self.return_defer_active = false;
let finally_ast = ASTNode::Program {
statements: finally_statements,
span: crate::ast::Span::unknown(),
};
self.build_expression(finally_ast)?;
self.emit_instruction(MirInstruction::Jump { target: exit_block })?;
// Do not emit a Jump if the finally block already terminated (e.g., via return/throw)
cleanup_terminated = self.is_current_block_terminated();
if !cleanup_terminated {
self.emit_instruction(MirInstruction::Jump { target: exit_block })?;
}
self.in_cleanup_block = false;
}
self.start_new_block(exit_block)?;
let result = self.value_gen.next();
self.emit_instruction(MirInstruction::Const {
dst: result,
value: ConstValue::Void,
})?;
// If any deferred return occurred in try/catch and cleanup did not already return,
// finalize with a Return of the slot; otherwise emit a dummy const/ret to satisfy structure.
let result = if self.return_deferred_emitted && !cleanup_terminated {
self.emit_instruction(MirInstruction::Return { value: Some(ret_slot) })?;
// Emit a symbolic const to satisfy return type inference paths when inspecting non-terminated blocks (not used here)
let r = self.value_gen.next();
self.emit_instruction(MirInstruction::Const { dst: r, value: ConstValue::Void })?;
r
} else {
let r = self.value_gen.next();
self.emit_instruction(MirInstruction::Const { dst: r, value: ConstValue::Void })?;
r
};
// Restore deferred-return context
self.return_defer_active = saved_defer_active;
self.return_defer_slot = saved_defer_slot;
self.return_defer_target = saved_defer_target;
self.return_deferred_emitted = saved_deferred_flag;
self.in_cleanup_block = saved_in_cleanup;
self.cleanup_allow_return = saved_allow_ret;
self.cleanup_allow_throw = saved_allow_throw;
Ok(result)
}
// Throw: emit Throw or fallback to env.debug.trace when disabled
pub(super) fn build_throw_statement(&mut self, expression: ASTNode) -> Result<ValueId, String> {
// Enforce cleanup policy
if self.in_cleanup_block && !self.cleanup_allow_throw {
return Err("throw is not allowed inside cleanup block (enable NYASH_CLEANUP_ALLOW_THROW=1 to permit)".to_string());
}
if std::env::var("NYASH_BUILDER_DISABLE_THROW").ok().as_deref() == Some("1") {
let v = self.build_expression(expression)?;
self.emit_instruction(MirInstruction::ExternCall {
@ -396,20 +369,38 @@ impl super::MirBuilder {
&mut self,
value: Option<Box<ASTNode>>,
) -> Result<ValueId, String> {
// Enforce cleanup policy
if self.in_cleanup_block && !self.cleanup_allow_return {
return Err("return is not allowed inside cleanup block (enable NYASH_CLEANUP_ALLOW_RETURN=1 to permit)".to_string());
}
let return_value = if let Some(expr) = value {
self.build_expression(*expr)?
} else {
let void_dst = self.value_gen.next();
self.emit_instruction(MirInstruction::Const {
dst: void_dst,
value: ConstValue::Void,
})?;
self.emit_instruction(MirInstruction::Const { dst: void_dst, value: ConstValue::Void })?;
void_dst
};
self.emit_instruction(MirInstruction::Return {
value: Some(return_value),
})?;
Ok(return_value)
if self.return_defer_active {
// Defer: copy into slot and jump to target
if let (Some(slot), Some(target)) = (self.return_defer_slot, self.return_defer_target) {
self.return_deferred_emitted = true;
self.emit_instruction(MirInstruction::Copy { dst: slot, src: return_value })?;
if !self.is_current_block_terminated() {
self.emit_instruction(MirInstruction::Jump { target })?;
}
Ok(return_value)
} else {
// Fallback: no configured slot/target; emit a real return
self.emit_instruction(MirInstruction::Return { value: Some(return_value) })?;
Ok(return_value)
}
} else {
// Normal return
self.emit_instruction(MirInstruction::Return { value: Some(return_value) })?;
Ok(return_value)
}
}
// Nowait: prefer env.future.spawn_instance if method call; else FutureNew
@ -493,4 +484,4 @@ impl super::MirBuilder {
Ok(me_value)
}
}
use crate::mir::loop_api::LoopBuilderApi; // for current_block()
// use crate::mir::loop_api::LoopBuilderApi; // no longer needed here

View File

@ -10,72 +10,7 @@ use crate::ast::ASTNode;
impl MirBuilder {
/// Build if statement with conditional branches
pub(super) fn build_if_statement(&mut self, condition: ASTNode, then_branch: ASTNode, else_branch: Option<ASTNode>) -> Result<ValueId, String> {
let condition_val = self.build_expression(condition)?;
// Create basic blocks for then/else/merge
let then_block = self.block_gen.next();
let else_block = self.block_gen.next();
let merge_block = self.block_gen.next();
// Emit branch instruction in current block
self.emit_instruction(MirInstruction::Branch {
condition: condition_val,
then_bb: then_block,
else_bb: else_block,
})?;
// Build then branch
self.current_block = Some(then_block);
self.ensure_block_exists(then_block)?;
// Keep a copy of AST for analysis (phi for variable reassignment)
let then_ast_for_analysis = then_branch.clone();
let then_value = self.build_expression(then_branch)?;
if !self.is_current_block_terminated() {
self.emit_instruction(MirInstruction::Jump { target: merge_block })?;
}
// Build else branch
self.current_block = Some(else_block);
self.ensure_block_exists(else_block)?;
let (else_value, else_ast_for_analysis) = if let Some(else_ast) = else_branch {
let val = self.build_expression(else_ast.clone())?;
(val, Some(else_ast))
} else {
// No else branch, use void
let void_val = self.value_gen.next();
self.emit_instruction(MirInstruction::Const {
dst: void_val,
value: ConstValue::Void,
})?;
(void_val, None)
};
if !self.is_current_block_terminated() {
self.emit_instruction(MirInstruction::Jump { target: merge_block })?;
}
// Create merge block with phi function
self.current_block = Some(merge_block);
self.ensure_block_exists(merge_block)?;
let result_val = self.value_gen.next();
self.emit_instruction(MirInstruction::Phi {
dst: result_val,
inputs: vec![
(then_block, then_value),
(else_block, else_value),
],
})?;
// Heuristic: If both branches assign the same variable name, bind that variable to the phi result
let assigned_var_then = Self::extract_assigned_var(&then_ast_for_analysis);
let assigned_var_else = else_ast_for_analysis.as_ref().and_then(|a| Self::extract_assigned_var(a));
if let (Some(a), Some(b)) = (assigned_var_then, assigned_var_else) {
if a == b {
self.variable_map.insert(a, result_val);
}
}
Ok(result_val)
self.lower_if_form(condition, then_branch, else_branch)
}
/// Extract assigned variable name from an AST node if it represents an assignment to a variable.

View File

@ -82,6 +82,48 @@ fn extract_assigned_var_local(ast: &ASTNode) -> Option<String> {
}
impl<'a> LoopBuilder<'a> {
// --- Small helpers for continue/break commonization ---
/// Emit a jump to `target` from the current block and record predecessor metadata.
fn jump_with_pred(&mut self, target: BasicBlockId) -> Result<(), String> {
let cur_block = self.current_block()?;
self.emit_jump(target)?;
let _ = crate::mir::builder::loops::add_predecessor(self.parent_builder, target, cur_block);
Ok(())
}
/// Switch insertion to a fresh (unreachable) block and place a Void const to keep callers satisfied.
fn switch_to_unreachable_block_with_void(&mut self) -> Result<ValueId, String> {
let next_block = self.new_block();
self.set_current_block(next_block)?;
let void_id = self.new_value();
self.emit_const(void_id, ConstValue::Void)?;
Ok(void_id)
}
/// Handle a `break` statement: jump to loop exit and continue in a fresh unreachable block.
fn do_break(&mut self) -> Result<ValueId, String> {
if let Some(exit_bb) = crate::mir::builder::loops::current_exit(self.parent_builder) {
self.jump_with_pred(exit_bb)?;
}
self.switch_to_unreachable_block_with_void()
}
/// Handle a `continue` statement: snapshot vars, jump to loop header, then continue in a fresh unreachable block.
fn do_continue(&mut self) -> Result<ValueId, String> {
// Snapshot variables at current block to be considered as a predecessor input
let snapshot = self.get_current_variable_map();
let cur_block = self.current_block()?;
self.block_var_maps.insert(cur_block, snapshot.clone());
self.continue_snapshots.push((cur_block, snapshot));
if let Some(header) = self.loop_header {
self.jump_with_pred(header)?;
}
self.switch_to_unreachable_block_with_void()
}
/// 新しいループビルダーを作成
pub fn new(parent: &'a mut super::builder::MirBuilder) -> Self {
let no_phi_mode = parent.is_no_phi_mode();
@ -528,38 +570,8 @@ impl<'a> LoopBuilder<'a> {
self.emit_const(void_id, ConstValue::Void)?;
Ok(void_id)
}
ASTNode::Break { .. } => {
// Jump to loop exit (after_loop_id) if available
let cur_block = self.current_block()?;
if let Some(exit_bb) = crate::mir::builder::loops::current_exit(self.parent_builder) {
self.emit_jump(exit_bb)?;
let _ = crate::mir::builder::loops::add_predecessor(self.parent_builder, exit_bb, cur_block);
}
// Continue building in a fresh (unreachable) block to satisfy callers
let next_block = self.new_block();
self.set_current_block(next_block)?;
let void_id = self.new_value();
self.emit_const(void_id, ConstValue::Void)?;
Ok(void_id)
}
ASTNode::Continue { .. } => {
let snapshot = self.get_current_variable_map();
let cur_block = self.current_block()?;
self.block_var_maps.insert(cur_block, snapshot.clone());
self.continue_snapshots.push((cur_block, snapshot));
if let Some(header) = self.loop_header {
self.emit_jump(header)?;
let _ = crate::mir::builder::loops::add_predecessor(self.parent_builder, header, cur_block);
}
let next_block = self.new_block();
self.set_current_block(next_block)?;
let void_id = self.new_value();
self.emit_const(void_id, ConstValue::Void)?;
Ok(void_id)
}
ASTNode::Break { .. } => self.do_break(),
ASTNode::Continue { .. } => self.do_continue(),
other => self.parent_builder.build_expression(other),
}
}

View File

@ -90,6 +90,13 @@ impl MirVerifier {
local_errors.append(&mut await_cp);
}
// 9. PHI-off strict edge-copy policy (optional)
if crate::config::env::mir_no_phi() && crate::config::env::verify_edge_copy_strict() {
if let Err(mut ecs) = self.verify_edge_copy_strict(function) {
local_errors.append(&mut ecs);
}
}
if local_errors.is_empty() {
Ok(())
} else {
@ -173,6 +180,80 @@ impl MirVerifier {
}
}
/// When PHI-off strict mode is enabled, enforce that merge blocks do not
/// introduce self-copies and that each predecessor provides a Copy into the
/// merged destination for values used in the merge block that do not dominate it.
fn verify_edge_copy_strict(
&self,
function: &MirFunction,
) -> Result<(), Vec<VerificationError>> {
let mut errors = Vec::new();
let preds = utils::compute_predecessors(function);
let def_block = utils::compute_def_blocks(function);
let dominators = utils::compute_dominators(function);
for (merge_bid, merge_bb) in &function.blocks {
let p = preds.get(merge_bid).cloned().unwrap_or_default();
if p.len() < 2 {
continue; // Only enforce on real merges (>=2 predecessors)
}
// Collect values used in merge block
let mut used_values = std::collections::HashSet::new();
for inst in merge_bb.all_instructions() {
for v in inst.used_values() {
used_values.insert(v);
}
}
// For each used value that doesn't dominate the merge block, enforce edge-copy policy
for &u in &used_values {
if let Some(&def_bb) = def_block.get(&u) {
// If the def dominates the merge block, edge copies are not required
let dominated = dominators
.get(merge_bid)
.map(|set| set.contains(&def_bb))
.unwrap_or(false);
if dominated && def_bb != *merge_bid {
continue;
}
}
// Merge block itself must not contain a Copy to the merged value
let has_self_copy_in_merge = merge_bb
.instructions
.iter()
.any(|inst| matches!(inst, super::MirInstruction::Copy { dst, .. } if *dst == u));
if has_self_copy_in_merge {
errors.push(VerificationError::EdgeCopyStrictViolation {
block: *merge_bid,
value: u,
pred_block: None,
reason: "merge block contains Copy to merged value; use predecessor copies only".to_string(),
});
}
// Each predecessor must provide a Copy into the merged destination
for pred in &p {
let Some(pbb) = function.blocks.get(pred) else { continue; };
let has_copy = pbb.instructions.iter().any(|inst| matches!(
inst,
super::MirInstruction::Copy { dst, .. } if *dst == u
));
if !has_copy {
errors.push(VerificationError::MergeUsesPredecessorValue {
value: u,
merge_block: *merge_bid,
pred_block: *pred,
});
}
}
}
}
if errors.is_empty() { Ok(()) } else { Err(errors) }
}
/// Reject legacy instructions that should be rewritten to Core-15 equivalents
/// Skips check when NYASH_VERIFY_ALLOW_LEGACY=1
fn verify_no_legacy_ops(&self, function: &MirFunction) -> Result<(), Vec<VerificationError>> {
@ -527,6 +608,21 @@ impl std::fmt::Display for VerificationError {
position, block, instruction_index
)
}
VerificationError::EdgeCopyStrictViolation { block, value, pred_block, reason } => {
if let Some(pb) = pred_block {
write!(
f,
"EdgeCopyStrictViolation for value {} at merge block {} from pred {}: {}",
value, block, pb, reason
)
} else {
write!(
f,
"EdgeCopyStrictViolation for value {} at merge block {}: {}",
value, block, reason
)
}
}
}
}
}

View File

@ -66,4 +66,11 @@ pub enum VerificationError {
instruction_index: usize,
position: &'static str,
},
/// PHI-off strict policy violation (edge-copy rules)
EdgeCopyStrictViolation {
block: BasicBlockId,
value: ValueId,
pred_block: Option<BasicBlockId>,
reason: String,
},
}

View File

@ -144,9 +144,42 @@ impl NyashParser {
let mut init_fields = Vec::new();
let mut weak_fields = Vec::new(); // 🔗 Track weak fields
let mut last_method_name: Option<String> = None;
while !self.match_token(&TokenType::RBRACE) && !self.is_at_end() {
self.skip_newlines(); // ループ開始時に改行をスキップ
// Fallback: method-level postfix catch/cleanup after a method (non-static box)
if (self.match_token(&TokenType::CATCH) || self.match_token(&TokenType::CLEANUP)) && last_method_name.is_some() {
let mname = last_method_name.clone().unwrap();
let mut catch_clauses: Vec<crate::ast::CatchClause> = Vec::new();
if self.match_token(&TokenType::CATCH) {
self.advance();
self.consume(TokenType::LPAREN)?;
let (exc_ty, exc_var) = self.parse_catch_param()?;
self.consume(TokenType::RPAREN)?;
let catch_body = self.parse_block_statements()?;
catch_clauses.push(crate::ast::CatchClause { exception_type: exc_ty, variable_name: exc_var, body: catch_body, span: crate::ast::Span::unknown() });
self.skip_newlines();
if self.match_token(&TokenType::CATCH) {
let line = self.current_token().line;
return Err(ParseError::UnexpectedToken { found: self.current_token().token_type.clone(), expected: "single catch only after method body".to_string(), line });
}
}
let finally_body = if self.match_token(&TokenType::CLEANUP) { self.advance(); Some(self.parse_block_statements()?) } else { None };
if let Some(mnode) = methods.get_mut(&mname) {
if let crate::ast::ASTNode::FunctionDeclaration { body, .. } = mnode {
let already = body.iter().any(|n| matches!(n, crate::ast::ASTNode::TryCatch{..}));
if already {
let line = self.current_token().line;
return Err(ParseError::UnexpectedToken { found: self.current_token().token_type.clone(), expected: "duplicate postfix catch/cleanup after method".to_string(), line });
}
let old = std::mem::take(body);
*body = vec![crate::ast::ASTNode::TryCatch { try_body: old, catch_clauses, finally_body, span: crate::ast::Span::unknown() }];
continue;
}
}
}
// RBRACEに到達していればループを抜ける
if self.match_token(&TokenType::RBRACE) {
break;
@ -244,21 +277,50 @@ impl NyashParser {
}
self.consume(TokenType::RPAREN)?;
self.consume(TokenType::LBRACE)?;
let mut body = self.parse_block_statements()?;
self.skip_newlines();
let mut body = Vec::new();
while !self.match_token(&TokenType::RBRACE) && !self.is_at_end() {
must_advance!(self, _unused, "constructor body parsing");
self.skip_newlines();
if self.match_token(&TokenType::RBRACE) {
break;
// Method-level postfix catch/cleanup (gate)
if self.match_token(&TokenType::CATCH) || self.match_token(&TokenType::CLEANUP)
{
let mut catch_clauses: Vec<crate::ast::CatchClause> = Vec::new();
if self.match_token(&TokenType::CATCH) {
self.advance(); // consume 'catch'
self.consume(TokenType::LPAREN)?;
let (exc_ty, exc_var) = self.parse_catch_param()?;
self.consume(TokenType::RPAREN)?;
let catch_body = self.parse_block_statements()?;
catch_clauses.push(crate::ast::CatchClause {
exception_type: exc_ty,
variable_name: exc_var,
body: catch_body,
span: crate::ast::Span::unknown(),
});
self.skip_newlines();
if self.match_token(&TokenType::CATCH) {
let line = self.current_token().line;
return Err(ParseError::UnexpectedToken {
found: self.current_token().token_type.clone(),
expected: "single catch only after method body".to_string(),
line,
});
}
}
body.push(self.parse_statement()?);
let finally_body = if self.match_token(&TokenType::CLEANUP) {
self.advance();
Some(self.parse_block_statements()?)
} else {
None
};
// Wrap original body with TryCatch
body = vec![ASTNode::TryCatch {
try_body: body,
catch_clauses,
finally_body,
span: crate::ast::Span::unknown(),
}];
}
self.consume(TokenType::RBRACE)?;
let constructor = ASTNode::FunctionDeclaration {
name: field_or_method.clone(),
params: params.clone(),
@ -307,20 +369,7 @@ impl NyashParser {
}
self.consume(TokenType::RPAREN)?;
self.consume(TokenType::LBRACE)?;
let mut body = Vec::new();
while !self.match_token(&TokenType::RBRACE) && !self.is_at_end() {
must_advance!(self, _unused, "pack body parsing");
self.skip_newlines();
if self.match_token(&TokenType::RBRACE) {
break;
}
body.push(self.parse_statement()?);
}
self.consume(TokenType::RBRACE)?;
let body = self.parse_block_statements()?;
let constructor = ASTNode::FunctionDeclaration {
name: field_or_method.clone(),
@ -369,20 +418,7 @@ impl NyashParser {
}
self.consume(TokenType::RPAREN)?;
self.consume(TokenType::LBRACE)?;
let mut body = Vec::new();
while !self.match_token(&TokenType::RBRACE) && !self.is_at_end() {
must_advance!(self, _unused, "birth body parsing");
self.skip_newlines();
if self.match_token(&TokenType::RBRACE) {
break;
}
body.push(self.parse_statement()?);
}
self.consume(TokenType::RBRACE)?;
let body = self.parse_block_statements()?;
let constructor = ASTNode::FunctionDeclaration {
name: field_or_method.clone(),
@ -513,20 +549,7 @@ impl NyashParser {
}
self.consume(TokenType::RPAREN)?;
self.consume(TokenType::LBRACE)?;
let mut body = Vec::new();
while !self.match_token(&TokenType::RBRACE) && !self.is_at_end() {
must_advance!(self, _unused, "method body parsing");
self.skip_newlines();
if self.match_token(&TokenType::RBRACE) {
break;
}
body.push(self.parse_statement()?);
}
self.consume(TokenType::RBRACE)?;
let body = self.parse_block_statements()?;
let method = ASTNode::FunctionDeclaration {
name: field_or_method.clone(),
@ -537,6 +560,7 @@ impl NyashParser {
span: Span::unknown(),
};
last_method_name = Some(field_or_method.clone());
methods.insert(field_or_method, method);
} else {
// フィールド定義P0: 型注釈 name: Type を受理して破棄)

View File

@ -131,9 +131,46 @@ impl NyashParser {
let mut weak_fields = Vec::new(); // 🔗 Track weak fields for static box
let mut static_init = None;
// Track last inserted method name to allow postfix catch/cleanup fallback parsing
let mut last_method_name: Option<String> = None;
while !self.match_token(&TokenType::RBRACE) && !self.is_at_end() {
self.skip_newlines(); // ループ開始時に改行をスキップ
// Fallback: method-level postfix catch/cleanup immediately following a method
if (self.match_token(&TokenType::CATCH) || self.match_token(&TokenType::CLEANUP)) && last_method_name.is_some() {
let mname = last_method_name.clone().unwrap();
// Parse optional catch then optional cleanup
let mut catch_clauses: Vec<crate::ast::CatchClause> = Vec::new();
if self.match_token(&TokenType::CATCH) {
self.advance();
self.consume(TokenType::LPAREN)?;
let (exc_ty, exc_var) = self.parse_catch_param()?;
self.consume(TokenType::RPAREN)?;
let catch_body = self.parse_block_statements()?;
catch_clauses.push(crate::ast::CatchClause { exception_type: exc_ty, variable_name: exc_var, body: catch_body, span: crate::ast::Span::unknown() });
self.skip_newlines();
if self.match_token(&TokenType::CATCH) {
let line = self.current_token().line;
return Err(ParseError::UnexpectedToken { found: self.current_token().token_type.clone(), expected: "single catch only after method body".to_string(), line });
}
}
let finally_body = if self.match_token(&TokenType::CLEANUP) { self.advance(); Some(self.parse_block_statements()?) } else { None };
// Wrap existing method body
if let Some(mnode) = methods.get_mut(&mname) {
if let crate::ast::ASTNode::FunctionDeclaration { body, .. } = mnode {
// If already TryCatch present, disallow duplicate postfix
let already = body.iter().any(|n| matches!(n, crate::ast::ASTNode::TryCatch{..}));
if already {
let line = self.current_token().line;
return Err(ParseError::UnexpectedToken { found: self.current_token().token_type.clone(), expected: "duplicate postfix catch/cleanup after method".to_string(), line });
}
let old = std::mem::take(body);
*body = vec![crate::ast::ASTNode::TryCatch { try_body: old, catch_clauses, finally_body, span: crate::ast::Span::unknown() }];
continue;
}
}
}
// RBRACEに到達していればループを抜ける
if self.match_token(&TokenType::RBRACE) {
break;
@ -142,17 +179,7 @@ impl NyashParser {
// 🔥 static { } ブロックの処理
if self.match_token(&TokenType::STATIC) {
self.advance(); // consume 'static'
self.consume(TokenType::LBRACE)?;
let mut static_body = Vec::new();
while !self.match_token(&TokenType::RBRACE) && !self.is_at_end() {
self.skip_newlines();
if !self.match_token(&TokenType::RBRACE) {
static_body.push(self.parse_statement()?);
}
}
self.consume(TokenType::RBRACE)?;
let static_body = self.parse_block_statements()?;
static_init = Some(static_body);
continue;
}
@ -230,18 +257,50 @@ impl NyashParser {
}
self.consume(TokenType::RPAREN)?;
self.consume(TokenType::LBRACE)?;
let mut body = self.parse_block_statements()?;
self.skip_newlines();
let mut body = Vec::new();
while !self.match_token(&TokenType::RBRACE) && !self.is_at_end() {
self.skip_newlines();
if !self.match_token(&TokenType::RBRACE) {
body.push(self.parse_statement()?);
// Method-level postfix catch/cleanup (gate)
if self.match_token(&TokenType::CATCH) || self.match_token(&TokenType::CLEANUP)
{
let mut catch_clauses: Vec<crate::ast::CatchClause> = Vec::new();
if self.match_token(&TokenType::CATCH) {
self.advance(); // consume 'catch'
self.consume(TokenType::LPAREN)?;
let (exc_ty, exc_var) = self.parse_catch_param()?;
self.consume(TokenType::RPAREN)?;
let catch_body = self.parse_block_statements()?;
catch_clauses.push(crate::ast::CatchClause {
exception_type: exc_ty,
variable_name: exc_var,
body: catch_body,
span: crate::ast::Span::unknown(),
});
self.skip_newlines();
if self.match_token(&TokenType::CATCH) {
let line = self.current_token().line;
return Err(ParseError::UnexpectedToken {
found: self.current_token().token_type.clone(),
expected: "single catch only after method body".to_string(),
line,
});
}
}
let finally_body = if self.match_token(&TokenType::CLEANUP) {
self.advance();
Some(self.parse_block_statements()?)
} else {
None
};
// Wrap original body with TryCatch
body = vec![ASTNode::TryCatch {
try_body: body,
catch_clauses,
finally_body,
span: crate::ast::Span::unknown(),
}];
}
self.consume(TokenType::RBRACE)?;
let method = ASTNode::FunctionDeclaration {
name: field_or_method.clone(),
params,
@ -251,6 +310,7 @@ impl NyashParser {
span: Span::unknown(),
};
last_method_name = Some(field_or_method.clone());
methods.insert(field_or_method, method);
} else {
// フィールド定義

View File

@ -53,19 +53,8 @@ impl NyashParser {
self.consume(TokenType::RPAREN)?;
// 関数本体をパース
self.consume(TokenType::LBRACE)?;
self.skip_newlines();
let mut body = Vec::new();
while !self.match_token(&TokenType::RBRACE) && !self.is_at_end() {
self.skip_newlines();
if !self.match_token(&TokenType::RBRACE) {
body.push(self.parse_statement()?);
}
}
self.consume(TokenType::RBRACE)?;
// 関数本体をパース(共通ブロックヘルパー)
let body = self.parse_block_statements()?;
Ok(ASTNode::FunctionDeclaration {
name,

View File

@ -91,19 +91,8 @@ impl NyashParser {
self.consume(TokenType::RPAREN)?;
// 関数本体をパース
self.consume(TokenType::LBRACE)?;
self.skip_newlines();
let mut body = Vec::new();
while !self.match_token(&TokenType::RBRACE) && !self.is_at_end() {
self.skip_newlines();
if !self.match_token(&TokenType::RBRACE) {
body.push(self.parse_statement()?);
}
}
self.consume(TokenType::RBRACE)?;
// 関数本体をパース(共通ブロックヘルパー)
let body = self.parse_block_statements()?;
Ok(ASTNode::FunctionDeclaration {
name,

View File

@ -11,11 +11,133 @@ use crate::ast::{ASTNode, CatchClause, Span};
use crate::tokenizer::TokenType;
impl NyashParser {
/// Helper: parse a block `{ stmt* }` and return its statements
pub(super) fn parse_block_statements(&mut self) -> Result<Vec<ASTNode>, ParseError> {
self.consume(TokenType::LBRACE)?;
let mut body = Vec::new();
while !self.match_token(&TokenType::RBRACE) && !self.is_at_end() {
self.skip_newlines();
if !self.match_token(&TokenType::RBRACE) {
body.push(self.parse_statement()?);
}
}
self.consume(TokenType::RBRACE)?;
Ok(body)
}
/// Helper: parse catch parameter inside parentheses (after '(' consumed)
/// Forms: (Type ident) | (ident) | ()
pub(super) fn parse_catch_param(&mut self) -> Result<(Option<String>, Option<String>), ParseError> {
if self.match_token(&TokenType::RPAREN) {
return Ok((None, None));
}
match &self.current_token().token_type {
TokenType::IDENTIFIER(first) => {
let first_str = first.clone();
let two_idents = matches!(self.peek_token(), TokenType::IDENTIFIER(_));
if two_idents {
self.advance(); // consume type ident
if let TokenType::IDENTIFIER(var_name) = &self.current_token().token_type {
let var = var_name.clone();
self.advance();
Ok((Some(first_str), Some(var)))
} else {
let line = self.current_token().line;
Err(ParseError::UnexpectedToken { found: self.current_token().token_type.clone(), expected: "exception variable name".to_string(), line })
}
} else {
self.advance();
Ok((None, Some(first_str)))
}
}
_ => {
if self.match_token(&TokenType::RPAREN) {
Ok((None, None))
} else {
let line = self.current_token().line;
Err(ParseError::UnexpectedToken { found: self.current_token().token_type.clone(), expected: ") or identifier".to_string(), line })
}
}
}
}
/// 文をパース
pub(super) fn parse_statement(&mut self) -> Result<ASTNode, ParseError> {
// For grammar diff: capture starting token to classify statement keyword
let start_tok = self.current_token().token_type.clone();
let result = match &start_tok {
TokenType::LBRACE => {
// Standalone block (Phase 15.5): may be followed by blockpostfix catch/finally
// Only enabled under gate; otherwise treat as error via expression fallback
// Parse the block body first
let try_body = self.parse_block_statements()?;
// Allow whitespace/newlines between block and postfix keywords
self.skip_newlines();
if crate::config::env::block_postfix_catch()
&& (self.match_token(&TokenType::CATCH) || self.match_token(&TokenType::CLEANUP))
{
// Parse at most one catch, then optional cleanup
let mut catch_clauses: Vec<CatchClause> = Vec::new();
if self.match_token(&TokenType::CATCH) {
self.advance(); // consume 'catch'
self.consume(TokenType::LPAREN)?;
let (exception_type, exception_var) = self.parse_catch_param()?;
self.consume(TokenType::RPAREN)?;
let catch_body = self.parse_block_statements()?;
catch_clauses.push(CatchClause {
exception_type,
variable_name: exception_var,
body: catch_body,
span: Span::unknown(),
});
// Singlecatch policy (MVP): disallow multiple catch in postfix form
self.skip_newlines();
if self.match_token(&TokenType::CATCH) {
let line = self.current_token().line;
return Err(ParseError::UnexpectedToken {
found: self.current_token().token_type.clone(),
expected: "single catch only after standalone block".to_string(),
line,
});
}
}
// Optional cleanup
let finally_body = if self.match_token(&TokenType::CLEANUP) {
self.advance(); // consume 'cleanup'
Some(self.parse_block_statements()?)
} else {
None
};
Ok(ASTNode::TryCatch {
try_body,
catch_clauses,
finally_body,
span: Span::unknown(),
})
} else {
// No postfix keywords. If gate is on, enforce MVP static check:
// direct top-level `throw` inside the standalone block must be followed by catch
if crate::config::env::block_postfix_catch()
&& try_body.iter().any(|n| matches!(n, ASTNode::Throw { .. }))
{
let line = self.current_token().line;
return Err(ParseError::UnexpectedToken {
found: self.current_token().token_type.clone(),
expected: "block with direct 'throw' must be followed by 'catch'".to_string(),
line,
});
}
Ok(ASTNode::Program {
statements: try_body,
span: Span::unknown(),
})
}
}
TokenType::BOX => self.parse_box_declaration(),
TokenType::IMPORT => self.parse_import(),
TokenType::INTERFACE => self.parse_interface_box_declaration(),
@ -34,8 +156,59 @@ impl NyashParser {
TokenType::INCLUDE => self.parse_include(),
TokenType::LOCAL => self.parse_local(),
TokenType::OUTBOX => self.parse_outbox(),
TokenType::TRY => self.parse_try_catch(),
TokenType::THROW => self.parse_throw(),
TokenType::TRY => {
if crate::config::env::parser_stage3() {
self.parse_try_catch()
} else {
Err(ParseError::UnexpectedToken {
found: self.current_token().token_type.clone(),
expected: "enable NYASH_PARSER_STAGE3=1 to use 'try'".to_string(),
line: self.current_token().line,
})
}
}
TokenType::THROW => {
if crate::config::env::parser_stage3() {
self.parse_throw()
} else {
Err(ParseError::UnexpectedToken {
found: self.current_token().token_type.clone(),
expected: "enable NYASH_PARSER_STAGE3=1 to use 'throw'".to_string(),
line: self.current_token().line,
})
}
}
TokenType::CATCH => {
// Provide a friendlier error when someone writes: if { .. } catch { .. }
if crate::config::env::block_postfix_catch() {
Err(ParseError::UnexpectedToken {
found: self.current_token().token_type.clone(),
expected: "postfix 'catch' is only allowed immediately after a standalone block: { ... } catch (...) { ... } (wrap if/else/loop in a standalone block)".to_string(),
line: self.current_token().line,
})
} else {
Err(ParseError::UnexpectedToken {
found: self.current_token().token_type.clone(),
expected: "enable NYASH_BLOCK_CATCH=1 (or NYASH_PARSER_STAGE3=1) to use postfix 'catch' after a standalone block".to_string(),
line: self.current_token().line,
})
}
}
TokenType::CLEANUP => {
if crate::config::env::block_postfix_catch() {
Err(ParseError::UnexpectedToken {
found: self.current_token().token_type.clone(),
expected: "postfix 'cleanup' is only allowed immediately after a standalone block: { ... } cleanup { ... }".to_string(),
line: self.current_token().line,
})
} else {
Err(ParseError::UnexpectedToken {
found: self.current_token().token_type.clone(),
expected: "enable NYASH_BLOCK_CATCH=1 (or NYASH_PARSER_STAGE3=1) to use postfix 'cleanup' after a standalone block".to_string(),
line: self.current_token().line,
})
}
}
TokenType::USING => self.parse_using(),
TokenType::FROM => {
// 🔥 from構文: from Parent.method(args) または from Parent.constructor(args)
@ -136,16 +309,8 @@ impl NyashParser {
// 条件部分を取得
let condition = Box::new(self.parse_expression()?);
// then部分を取得
self.consume(TokenType::LBRACE)?;
let mut then_body = Vec::new();
while !self.match_token(&TokenType::RBRACE) && !self.is_at_end() {
self.skip_newlines();
if !self.match_token(&TokenType::RBRACE) {
then_body.push(self.parse_statement()?);
}
}
self.consume(TokenType::RBRACE)?;
// then部分を取得(共通ブロックヘルパー)
let then_body = self.parse_block_statements()?;
// else if/else部分を処理
let else_body = if self.match_token(&TokenType::ELSE) {
@ -156,17 +321,8 @@ impl NyashParser {
let nested_if = self.parse_if()?;
Some(vec![nested_if])
} else {
// plain else
self.consume(TokenType::LBRACE)?;
let mut else_stmts = Vec::new();
while !self.match_token(&TokenType::RBRACE) && !self.is_at_end() {
self.skip_newlines();
if !self.match_token(&TokenType::RBRACE) {
else_stmts.push(self.parse_statement()?);
}
}
self.consume(TokenType::RBRACE)?;
Some(else_stmts)
// plain else(共通ブロックヘルパー)
Some(self.parse_block_statements()?)
}
} else {
None
@ -198,16 +354,8 @@ impl NyashParser {
})
};
// body部分を取得
self.consume(TokenType::LBRACE)?;
let mut body = Vec::new();
while !self.match_token(&TokenType::RBRACE) && !self.is_at_end() {
self.skip_newlines();
if !self.match_token(&TokenType::RBRACE) {
body.push(self.parse_statement()?);
}
}
self.consume(TokenType::RBRACE)?;
// body部分を取得(共通ブロックヘルパー)
let body = self.parse_block_statements()?;
Ok(ASTNode::Loop {
condition,
@ -422,17 +570,7 @@ impl NyashParser {
/// try-catch文をパース
pub(super) fn parse_try_catch(&mut self) -> Result<ASTNode, ParseError> {
self.advance(); // consume 'try'
self.consume(TokenType::LBRACE)?;
let mut try_body = Vec::new();
while !self.match_token(&TokenType::RBRACE) && !self.is_at_end() {
self.skip_newlines();
if !self.match_token(&TokenType::RBRACE) {
try_body.push(self.parse_statement()?);
}
}
self.consume(TokenType::RBRACE)?;
let try_body = self.parse_block_statements()?;
let mut catch_clauses = Vec::new();
@ -440,68 +578,22 @@ impl NyashParser {
while self.match_token(&TokenType::CATCH) {
self.advance(); // consume 'catch'
self.consume(TokenType::LPAREN)?;
// 例外型 (オプション)
let exception_type =
if let TokenType::IDENTIFIER(type_name) = &self.current_token().token_type {
let type_name = type_name.clone();
self.advance();
Some(type_name)
} else {
None
};
// 例外変数名
let exception_var =
if let TokenType::IDENTIFIER(var_name) = &self.current_token().token_type {
let var_name = var_name.clone();
self.advance();
var_name
} else {
let line = self.current_token().line;
return Err(ParseError::UnexpectedToken {
found: self.current_token().token_type.clone(),
expected: "exception variable name".to_string(),
line,
});
};
let (exception_type, exception_var) = self.parse_catch_param()?;
self.consume(TokenType::RPAREN)?;
self.consume(TokenType::LBRACE)?;
let mut catch_body = Vec::new();
while !self.match_token(&TokenType::RBRACE) && !self.is_at_end() {
self.skip_newlines();
if !self.match_token(&TokenType::RBRACE) {
catch_body.push(self.parse_statement()?);
}
}
self.consume(TokenType::RBRACE)?;
let catch_body = self.parse_block_statements()?;
catch_clauses.push(CatchClause {
exception_type,
variable_name: Some(exception_var),
variable_name: exception_var,
body: catch_body,
span: Span::unknown(),
});
}
// finally節をパース (オプション)
let finally_body = if self.match_token(&TokenType::FINALLY) {
self.advance(); // consume 'finally'
self.consume(TokenType::LBRACE)?;
let mut body = Vec::new();
while !self.match_token(&TokenType::RBRACE) && !self.is_at_end() {
self.skip_newlines();
if !self.match_token(&TokenType::RBRACE) {
body.push(self.parse_statement()?);
}
}
self.consume(TokenType::RBRACE)?;
Some(body)
// cleanup節をパース (オプション)
let finally_body = if self.match_token(&TokenType::CLEANUP) {
self.advance(); // consume 'cleanup'
Some(self.parse_block_statements()?)
} else {
None
};

View File

@ -13,8 +13,8 @@ pub(crate) fn execute_file_with_backend(runner: &NyashRunner, filename: &str) {
if std::env::var("NYASH_USE_NY_COMPILER").ok().as_deref() == Some("1") {
if runner.try_run_selfhost_pipeline(filename) {
return;
} else if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1") {
eprintln!("[ny-compiler] fallback to default path (MVP unavailable for this input)");
} else {
crate::cli_v!("[ny-compiler] fallback to default path (MVP unavailable for this input)");
}
}
@ -31,12 +31,7 @@ pub(crate) fn execute_file_with_backend(runner: &NyashRunner, filename: &str) {
};
match json_v0_bridge::parse_source_v0_to_module(&code) {
Ok(module) => {
if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1") {
println!(
"🚀 Nyash MIR Interpreter - (parser=ny) Executing file: {} 🚀",
filename
);
}
crate::cli_v!("🚀 Nyash MIR Interpreter - (parser=ny) Executing file: {} 🚀", filename);
runner.execute_mir_module(&module);
return;
}
@ -70,9 +65,7 @@ pub(crate) fn execute_file_with_backend(runner: &NyashRunner, filename: &str) {
// MIR dump/verify
if runner.config.dump_mir || runner.config.verify_mir {
if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1") {
println!("🚀 Nyash MIR Compiler - Processing file: {} 🚀", filename);
}
crate::cli_v!("🚀 Nyash MIR Compiler - Processing file: {} 🚀", filename);
runner.execute_mir_mode(filename);
return;
}
@ -106,15 +99,11 @@ pub(crate) fn execute_file_with_backend(runner: &NyashRunner, filename: &str) {
// Backend selection
match runner.config.backend.as_str() {
"mir" => {
if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1") {
println!("🚀 Nyash MIR Interpreter - Executing file: {} 🚀", filename);
}
crate::cli_v!("🚀 Nyash MIR Interpreter - Executing file: {} 🚀", 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);
}
crate::cli_v!("🚀 Nyash VM Backend - Executing file: {} 🚀", filename);
#[cfg(feature = "vm-legacy")]
{
runner.execute_vm_mode(filename);
@ -139,12 +128,7 @@ pub(crate) fn execute_file_with_backend(runner: &NyashRunner, filename: &str) {
}
#[cfg(feature = "cranelift-jit")]
"jit-direct" => {
if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1") {
println!(
"⚡ Nyash JIT-Direct Backend - Executing file: {}",
filename
);
}
crate::cli_v!("⚡ Nyash JIT-Direct Backend - Executing file: {} ⚡", filename);
#[cfg(feature = "cranelift-jit")]
{
// Use independent JIT-direct runner method (no VM execute loop)
@ -157,9 +141,7 @@ pub(crate) fn execute_file_with_backend(runner: &NyashRunner, filename: &str) {
}
}
"llvm" => {
if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1") {
println!("⚡ Nyash LLVM Backend - Executing file: {}", filename);
}
crate::cli_v!("⚡ Nyash LLVM Backend - Executing file: {} ⚡", filename);
runner.execute_llvm_mode(filename);
}
other => {
@ -183,41 +165,13 @@ impl NyashRunner {
}
// If CLI requested EXE emit, generate JSON then invoke ny-llvmc to link NyRT and exit.
if let Some(exe_out) = self.config.emit_exe.as_ref() {
let tmp_dir = std::path::Path::new("tmp");
let _ = std::fs::create_dir_all(tmp_dir);
let json_path = tmp_dir.join("nyash_cli_emit.json");
if let Err(e) = crate::runner::mir_json_emit::emit_mir_json_for_harness_bin(module, &json_path) {
eprintln!("❌ MIR JSON emit error: {}", e);
std::process::exit(1);
}
// Resolve ny-llvmc
let ny_llvmc = std::env::var("NYASH_NY_LLVM_COMPILER")
.ok()
.and_then(|s| if !s.is_empty() { Some(std::path::PathBuf::from(s)) } else { None })
.or_else(|| which::which("ny-llvmc").ok())
.unwrap_or_else(|| std::path::PathBuf::from("target/release/ny-llvmc"));
// Build command
let mut cmd = std::process::Command::new(ny_llvmc);
cmd.arg("--in").arg(&json_path)
.arg("--emit").arg("exe")
.arg("--out").arg(exe_out);
if let Some(dir) = self.config.emit_exe_nyrt.as_ref() {
cmd.arg("--nyrt").arg(dir);
} else {
// default hint
cmd.arg("--nyrt").arg("target/release");
}
if let Some(flags) = self.config.emit_exe_libs.as_ref() {
if !flags.trim().is_empty() {
cmd.arg("--libs").arg(flags);
}
}
let status = cmd.status().unwrap_or_else(|e| {
eprintln!("❌ failed to spawn ny-llvmc: {}", e);
std::process::exit(1);
});
if !status.success() {
eprintln!("❌ ny-llvmc failed with status: {:?}", status.code());
if let Err(e) = crate::runner::modes::common_util::exec::ny_llvmc_emit_exe_bin(
module,
exe_out,
self.config.emit_exe_nyrt.as_deref(),
self.config.emit_exe_libs.as_deref(),
) {
eprintln!("{}", e);
std::process::exit(1);
}
println!("EXE written: {}", exe_out);

View File

@ -15,6 +15,7 @@ pub(super) mod try_catch;
pub(super) mod expr;
pub(super) mod ternary; // placeholder (not wired)
pub(super) mod peek; // placeholder (not wired)
pub(super) mod throw_ctx; // thread-local ctx for Result-mode throw routing
#[derive(Clone, Copy)]
pub(super) struct LoopContext {
@ -28,19 +29,96 @@ pub(super) struct BridgeEnv {
pub(super) mir_no_phi: bool,
pub(super) allow_me_dummy: bool,
pub(super) me_class: String,
pub(super) try_result_mode: bool,
}
impl BridgeEnv {
pub(super) fn load() -> Self {
let trm = crate::config::env::try_result_mode();
let no_phi = crate::config::env::mir_no_phi();
if crate::config::env::cli_verbose() {
eprintln!("[Bridge] load: try_result_mode={} mir_no_phi={}", trm, no_phi);
}
Self {
throw_enabled: std::env::var("NYASH_BRIDGE_THROW_ENABLE").ok().as_deref() == Some("1"),
mir_no_phi: crate::config::env::mir_no_phi(),
mir_no_phi: no_phi,
allow_me_dummy: std::env::var("NYASH_BRIDGE_ME_DUMMY").ok().as_deref() == Some("1"),
me_class: std::env::var("NYASH_BRIDGE_ME_CLASS").unwrap_or_else(|_| "Main".to_string()),
try_result_mode: trm,
}
}
}
/// Small helper: set Jump terminator and record predecessor on the target.
fn jump_with_pred(f: &mut MirFunction, cur_bb: BasicBlockId, target: BasicBlockId) {
if let Some(bb) = f.get_block_mut(cur_bb) {
bb.set_terminator(MirInstruction::Jump { target });
}
if let Some(succ) = f.get_block_mut(target) {
succ.add_predecessor(cur_bb);
}
}
/// Strip Phi instructions by inserting edge copies on each predecessor.
/// This normalizes MIR to PHI-off form for downstream harnesses that synthesize PHIs.
fn strip_phi_functions(f: &mut MirFunction) {
// Collect block ids to avoid borrow issues while mutating
let block_ids: Vec<BasicBlockId> = f.blocks.keys().copied().collect();
for bbid in block_ids {
// Snapshot phi instructions at the head
let mut phi_entries: Vec<(ValueId, Vec<(BasicBlockId, ValueId)>)> = Vec::new();
if let Some(bb) = f.blocks.get(&bbid) {
for inst in &bb.instructions {
if let MirInstruction::Phi { dst, inputs } = inst {
phi_entries.push((*dst, inputs.clone()));
} else {
// PHIs must be at the beginning; once we see non-Phi, stop
break;
}
}
}
if phi_entries.is_empty() {
continue;
}
// Insert copies on predecessors
for (dst, inputs) in &phi_entries {
for (pred, val) in inputs {
if let Some(pbb) = f.blocks.get_mut(pred) {
pbb.add_instruction(MirInstruction::Copy { dst: *dst, src: *val });
}
}
}
// Remove Phi instructions from the merge block
if let Some(bb) = f.blocks.get_mut(&bbid) {
let non_phi: Vec<MirInstruction> = bb
.instructions
.iter()
.cloned()
.skip_while(|inst| matches!(inst, MirInstruction::Phi { .. }))
.collect();
bb.instructions = non_phi;
}
}
}
fn lower_break_stmt(f: &mut MirFunction, cur_bb: BasicBlockId, exit_bb: BasicBlockId) {
jump_with_pred(f, cur_bb, exit_bb);
crate::jit::events::emit_lower(
serde_json::json!({ "id": "loop_break","exit_bb": exit_bb.0,"decision": "lower" }),
"loop",
"<json_v0>",
);
}
fn lower_continue_stmt(f: &mut MirFunction, cur_bb: BasicBlockId, cond_bb: BasicBlockId) {
jump_with_pred(f, cur_bb, cond_bb);
crate::jit::events::emit_lower(
serde_json::json!({ "id": "loop_continue","cond_bb": cond_bb.0,"decision": "lower" }),
"loop",
"<json_v0>",
);
}
pub(super) fn lower_stmt_with_vars(
f: &mut MirFunction,
@ -86,31 +164,13 @@ pub(super) fn lower_stmt_with_vars(
}
StmtV0::Break => {
if let Some(ctx) = loop_stack.last().copied() {
if let Some(bb) = f.get_block_mut(cur_bb) {
bb.set_terminator(MirInstruction::Jump {
target: ctx.exit_bb,
});
}
crate::jit::events::emit_lower(
serde_json::json!({ "id": "loop_break","exit_bb": ctx.exit_bb.0,"decision": "lower" }),
"loop",
"<json_v0>",
);
lower_break_stmt(f, cur_bb, ctx.exit_bb);
}
Ok(cur_bb)
}
StmtV0::Continue => {
if let Some(ctx) = loop_stack.last().copied() {
if let Some(bb) = f.get_block_mut(cur_bb) {
bb.set_terminator(MirInstruction::Jump {
target: ctx.cond_bb,
});
}
crate::jit::events::emit_lower(
serde_json::json!({ "id": "loop_continue","cond_bb": ctx.cond_bb.0,"decision": "lower" }),
"loop",
"<json_v0>",
);
lower_continue_stmt(f, cur_bb, ctx.cond_bb);
}
Ok(cur_bb)
}
@ -194,12 +254,16 @@ pub(super) fn lower_program(prog: ProgramV0) -> Result<MirModule, String> {
}
}
f.signature.return_type = MirType::Unknown;
// PHI-off normalization for Bridge output
if env.mir_no_phi {
strip_phi_functions(&mut f);
}
module.add_function(f);
Ok(module)
}
pub(super) fn maybe_dump_mir(module: &MirModule) {
if std::env::var("NYASH_CLI_VERBOSE").ok().as_deref() == Some("1") {
if crate::config::env::cli_verbose() {
let p = MirPrinter::new();
println!("{}", p.print_module(module));
}

Some files were not shown because too many files have changed in this diff Show More