phase(9.78h): stabilize MIR/VM pipeline

- Add MIR26 doc≡code sync test (tests/mir_instruction_set_sync.rs)
- Quiet snapshots; filter plugin/net logs; golden all green
- Delegate VM phi selection to LoopExecutor (borrow-safe)
- ResultBox migration: remove legacy box_trait::ResultBox paths
- VM BoxRef arithmetic fallbacks via toString().parse::<i64>()
- Bridge BoxCall(InstanceBox) to Class.method/arity in VM
- Fix Effects purity (READ -> readonly, not pure)
- Mark Catch as CONTROL to prevent DCE; Try/Catch test green
- Add env-gated debug logs (effects, verifier, mir-printer, trycatch, ref, bin)
- Update CURRENT_TASK with progress and next steps
This commit is contained in:
Moe Charm
2025-08-26 05:49:23 +09:00
parent 248c3ba183
commit bf4b87526e
21 changed files with 310 additions and 95 deletions

View File

@ -484,23 +484,6 @@ ls src/backend/ | grep "^vm_" | xargs -I{} wc -l src/backend/{}
**根本原因:** Claudeのコマンド再構築機能のバグ`pattern`ではなく`op`フィールドを使用) **根本原因:** Claudeのコマンド再構築機能のバグ`pattern`ではなく`op`フィールドを使用)
### 💡 実践的な使い分けガイド
**`bash -c`を使うべき時:**
- glob展開 + パイプを使う時(`ls *.rs | wc -l`
- 複雑なシェル機能を使う時(リダイレクト、サブシェル等)
- 「glob: そのようなファイル...」エラーが出た時
**findを使うべき時**
- ディレクトリを再帰的に検索する時
- より複雑なファイル条件(サイズ、更新日時等)を使う時
- 確実性を最優先する時
**直接ファイル名を書くべき時:**
- ファイル数が少ない時5個以下
- ファイル名が確定している時
- スクリプトで使う時(可読性重視)
## 🔧 開発サポート ## 🔧 開発サポート
### 🤖 AI相談 ### 🤖 AI相談

View File

@ -1,4 +1,4 @@
# 🎯 CURRENT TASK - 2025年8月25日(状況整理) # 🎯 CURRENT TASK - 2025年8月26日(状況整理&再起動ショートカット
## ⏱️ 再開ショートカット(今日のフォーカス) ## ⏱️ 再開ショートカット(今日のフォーカス)
- フォーカス: VM比較経路の安定化後片付け + 1000行分解の下準備 - フォーカス: VM比較経路の安定化後片付け + 1000行分解の下準備
@ -36,6 +36,14 @@ nyash --backend vm local_tests/and_or_truthy_vm.nyash # 期待: false,true,fals
- 9.79P2P本体 前提: 9.78h完了): `docs/development/roadmap/phases/phase-9/phase_9_79_p2pbox_rebuild.md` - 9.79P2P本体 前提: 9.78h完了): `docs/development/roadmap/phases/phase-9/phase_9_79_p2pbox_rebuild.md`
- Phase 10Cranelift JIT主経路: `docs/development/roadmap/phases/phase-10/phase_10_cranelift_jit_backend.md` - Phase 10Cranelift JIT主経路: `docs/development/roadmap/phases/phase-10/phase_10_cranelift_jit_backend.md`
### 🧩 今日の再開ポイント2025-08-26
- 完了(適度な分解): runnerを`modes/`へ分離mir/vm/llvm/bench/wasm/aot/common、objectsを`objects/{ops,methods,fields}.rs`へ、VMに`frame.rs`/`control_flow::record_transition`/`dispatch::execute_instruction`導入。ログ抑制NYASH_VM_DEBUG_* / NYASH_CLI_VERBOSE / NYASH_DEBUG_PLUGIN、Phi正規化Step1previous_block基準
- 次アクション(小さく前進):
1) MIR26命令「総数一致」チェックコード≡ドキュメント
2) Loop SSA Step2seal/pred更新のスケルトンVerifierのphi系文言強化
3) 代表スナップショット再確認TypeOp/extern_call/loop/await/boxcall
- クイックコマンド: `cargo build --release -j32``nyash --backend vm local_tests/compare_box_vm.nyash``./tools/ci_check_golden.sh`
## 🚨 現在の状況2025-08-25 ## 🚨 現在の状況2025-08-25
@ -418,3 +426,41 @@ NYASH_VM_DEBUG_BOXCALL=1 ./target/release/nyash --backend vm local_tests/test_vm
- Optimizer: 未lowering検知is/as/isType/asTypeをBoxCall/Call両経路で検出、`NYASH_OPT_DIAG_FAIL=1` と連携。 - Optimizer: 未lowering検知is/as/isType/asTypeをBoxCall/Call両経路で検出、`NYASH_OPT_DIAG_FAIL=1` と連携。
- 代表スナップショット: extern_call/loop/boxcall/typeop_mixed をCIに追加、全件緑。 - 代表スナップショット: extern_call/loop/boxcall/typeop_mixed をCIに追加、全件緑。
- 注: WeakRef/Barrier の“統合”はPoCフラグで切替可能レガシー命令も支援—MIR26はドキュメントの正典、実装は互換を維持。 - 注: WeakRef/Barrier の“統合”はPoCフラグで切替可能レガシー命令も支援—MIR26はドキュメントの正典、実装は互換を維持。
## ✅ 本日の成果9.78h
- MIR26命令のコード≡ドキュメント同期テストを追加tests/mir_instruction_set_sync.rs→ 緑
- スナップショット安定化tools/snapshot_mir.sh に静音フィルタ、golden全緑
- Phi選択の委譲スケルトンをVMへ実装LoopExecutor::execute_phi
- ResultBox移行スイープVMのレガシーbox_trait::ResultBox経路を削除、boxes::ResultBoxに統一
- VM BoxRef演算フォールバック拡張toString→parse<i64>、比較/四則/混在を広くカバー)
- ロガー導入環境変数ONでのみ出力:
- NYASH_DEBUG_EFFECTS / NYASH_DEBUG_VERIFIER / NYASH_DEBUG_MIR_PRINTER / NYASH_DEBUG_TRYCATCH
- NYASH_VM_DEBUG_REF / NYASH_VM_DEBUG_BIN / 既存の NYASH_VM_DEBUG_* 系
- VMユーザBoxのBoxCallをInstance関数へ橋渡しInstanceBox → Class.method/arity へ委譲)
## 🧪 テスト概況(要点)
- 緑: 175件 / 赤: 1件`mir::verification::tests::test_if_merge_uses_phi_not_predecessor`
- 失敗要旨: Mergeブロックで前任値使用と判定DominatorViolation/MergeUsesPredecessorValue
- 生成MIRCLI/Builderはmergeにphiを含むため、Verifier側の検査条件かvariable_map束縛の拾い漏れの可能性。
### デバッグ用コマンドログON例
```bash
# Effects純度
NYASH_DEBUG_EFFECTS=1 cargo test --lib mir::effect::tests::test_effect_mask_creation -- --nocapture
# Verifierphi/merge
NYASH_DEBUG_VERIFIER=1 cargo test --lib mir::verification::tests::test_if_merge_uses_phi_not_predecessor -- --nocapture
# Try/CatchのLowering/Printer
NYASH_DEBUG_TRYCATCH=1 NYASH_DEBUG_MIR_PRINTER=1 cargo test --lib mir::tests::test_try_catch_compilation -- --nocapture
# VM 参照/演算
NYASH_VM_DEBUG_REF=1 cargo test --lib backend::vm::tests::test_vm_user_box_birth_and_method -- --nocapture
NYASH_VM_DEBUG_BIN=1 cargo test --lib tests::mir_vm_poc::test_boxref_arith -- --nocapture
```
## 🎯 次の着手残1の精密駆逐
1) Verifierのmerge-phi検査を補強phi dst/variable束縛の対応づけ強化 or 条件緩和の適用)
2) Builderのvariable_map束縛拾い漏れがあれば補修Program直下パターンの明示
3) 代表MIRダンプをfixture化して回帰チェックphi存在の有無を機械判定

View File

@ -222,25 +222,35 @@ pub struct VM {
} }
impl VM { impl VM {
/// Helper: execute phi selection based on previous_block (borrow-safe minimal) /// Helper: execute phi via LoopExecutor with previous_block-based selection (Step2 skeleton)
pub(super) fn loop_execute_phi(&mut self, _dst: ValueId, inputs: &[(BasicBlockId, ValueId)]) -> Result<VMValue, VMError> { pub(super) fn loop_execute_phi(&mut self, dst: ValueId, inputs: &[(BasicBlockId, ValueId)]) -> Result<VMValue, VMError> {
if inputs.is_empty() { if inputs.is_empty() {
return Err(VMError::InvalidInstruction("Phi node has no inputs".to_string())); return Err(VMError::InvalidInstruction("Phi node has no inputs".to_string()));
} }
let debug_phi = std::env::var("NYASH_VM_DEBUG").ok().as_deref() == Some("1") let debug_phi = std::env::var("NYASH_VM_DEBUG").ok().as_deref() == Some("1")
|| std::env::var("NYASH_VM_DEBUG_PHI").ok().as_deref() == Some("1"); || std::env::var("NYASH_VM_DEBUG_PHI").ok().as_deref() == Some("1");
let prev = self.previous_block; if debug_phi { eprintln!("[VM] phi-select (delegated) prev={:?} inputs={:?}", self.previous_block, inputs); }
if debug_phi { eprintln!("[VM] phi-select prev={:?} inputs={:?}", prev, inputs); } // Borrow just the values storage immutably to avoid borrowing entire &self in closure
if let Some(prev_bb) = prev { let values_ref = &self.values;
if let Some((_, val_id)) = inputs.iter().find(|(bb, _)| *bb == prev_bb) { let res = self.loop_executor.execute_phi(dst, inputs, |val_id| {
if debug_phi { eprintln!("[VM] phi-select hit prev={:?} -> {:?}", prev_bb, val_id); } let index = val_id.to_usize();
return self.get_value(*val_id); if index < values_ref.len() {
if let Some(ref value) = values_ref[index] {
Ok(value.clone())
} else {
Err(VMError::InvalidValue(format!("Value {} not set", val_id)))
}
} else {
Err(VMError::InvalidValue(format!("Value {} out of bounds", val_id)))
}
});
if debug_phi {
match &res {
Ok(v) => eprintln!("[VM] phi-result -> {:?}", v),
Err(e) => eprintln!("[VM] phi-error -> {:?}", e),
} }
} }
// Fallback: first input res
let (_, val_id) = inputs[0];
if debug_phi { eprintln!("[VM] phi-select fallback first -> {:?}", val_id); }
self.get_value(val_id)
} }
/// Create a new VM instance /// Create a new VM instance
pub fn new() -> Self { pub fn new() -> Self {
@ -395,7 +405,7 @@ impl VM {
.ok_or_else(|| VMError::InvalidBasicBlock(format!("Block {} not found", current_block)))?; .ok_or_else(|| VMError::InvalidBasicBlock(format!("Block {} not found", current_block)))?;
self.frame.current_block = Some(current_block); self.frame.current_block = Some(current_block);
self.pc = 0; self.frame.pc = 0;
let mut next_block = None; let mut next_block = None;
let mut should_return = None; let mut should_return = None;
@ -403,7 +413,7 @@ impl VM {
// Execute instructions in this block (including terminator) // Execute instructions in this block (including terminator)
let all_instructions: Vec<_> = block.all_instructions().collect(); let all_instructions: Vec<_> = block.all_instructions().collect();
for (index, instruction) in all_instructions.iter().enumerate() { for (index, instruction) in all_instructions.iter().enumerate() {
self.pc = index; self.frame.pc = index;
match self.execute_instruction(instruction)? { match self.execute_instruction(instruction)? {
ControlFlow::Continue => continue, ControlFlow::Continue => continue,
@ -550,24 +560,7 @@ impl VM {
} }
} }
// ResultBox (box_trait::ResultBox - legacy) // Legacy box_trait::ResultBox is no longer handled here (migration complete)
{
#[allow(deprecated)]
if let Some(result_box_legacy) = box_value.as_any().downcast_ref::<crate::box_trait::ResultBox>() {
match method {
"is_ok" | "isOk" => {
return Ok(result_box_legacy.is_ok());
}
"get_value" | "getValue" => {
return Ok(result_box_legacy.get_value());
}
"get_error" | "getError" => {
return Ok(result_box_legacy.get_error());
}
_ => return Ok(Box::new(VoidBox::new())),
}
}
}
// Generic fallback: toString for any Box type // Generic fallback: toString for any Box type
if method == "toString" { if method == "toString" {

View File

@ -23,18 +23,7 @@ impl VM {
} }
} }
// ResultBox (box_trait::ResultBox - legacy) // Legacy box_trait::ResultBox path removed (migration complete)
{
#[allow(deprecated)]
if let Some(result_box_legacy) = box_value.as_any().downcast_ref::<crate::box_trait::ResultBox>() {
match method {
"is_ok" | "isOk" => { return Ok(result_box_legacy.is_ok()); }
"get_value" | "getValue" => { return Ok(result_box_legacy.get_value()); }
"get_error" | "getError" => { return Ok(result_box_legacy.get_error()); }
_ => return Ok(Box::new(VoidBox::new())),
}
}
}
// Generic fallback: toString for any Box type // Generic fallback: toString for any Box type
if method == "toString" { if method == "toString" {

View File

@ -335,6 +335,8 @@ impl VM {
/// Execute RefGet instruction /// Execute RefGet instruction
pub(super) fn execute_ref_get(&mut self, dst: ValueId, reference: ValueId, field: &str) -> Result<ControlFlow, VMError> { pub(super) fn execute_ref_get(&mut self, dst: ValueId, reference: ValueId, field: &str) -> Result<ControlFlow, VMError> {
let debug_ref = std::env::var("NYASH_VM_DEBUG_REF").ok().as_deref() == Some("1");
if debug_ref { eprintln!("[VM] RefGet ref={:?} field={}", reference, field); }
// Visibility check (if class known and visibility declared). Skip for internal refs. // Visibility check (if class known and visibility declared). Skip for internal refs.
let is_internal = self.object_internal.contains(&reference); let is_internal = self.object_internal.contains(&reference);
if !is_internal { if !is_internal {
@ -352,13 +354,16 @@ impl VM {
// Get field value from object // Get field value from object
let field_value = if let Some(fields) = self.object_fields.get(&reference) { let field_value = if let Some(fields) = self.object_fields.get(&reference) {
if let Some(value) = fields.get(field) { if let Some(value) = fields.get(field) {
if debug_ref { eprintln!("[VM] RefGet hit: {} -> {:?}", field, value); }
value.clone() value.clone()
} else { } else {
// Field not set yet, return default // Field not set yet, return default
if debug_ref { eprintln!("[VM] RefGet miss: {} -> default 0", field); }
VMValue::Integer(0) VMValue::Integer(0)
} }
} else { } else {
// Object has no fields yet, return default // Object has no fields yet, return default
if debug_ref { eprintln!("[VM] RefGet no fields: -> default 0"); }
VMValue::Integer(0) VMValue::Integer(0)
}; };
@ -368,8 +373,10 @@ impl VM {
/// Execute RefSet instruction /// Execute RefSet instruction
pub(super) fn execute_ref_set(&mut self, reference: ValueId, field: &str, value: ValueId) -> Result<ControlFlow, VMError> { pub(super) fn execute_ref_set(&mut self, reference: ValueId, field: &str, value: ValueId) -> Result<ControlFlow, VMError> {
let debug_ref = std::env::var("NYASH_VM_DEBUG_REF").ok().as_deref() == Some("1");
// Get the value to set // Get the value to set
let new_value = self.get_value(value)?; let new_value = self.get_value(value)?;
if debug_ref { eprintln!("[VM] RefSet ref={:?} field={} value={:?}", reference, field, new_value); }
// Visibility check (Skip for internal refs; otherwise enforce public) // Visibility check (Skip for internal refs; otherwise enforce public)
let is_internal = self.object_internal.contains(&reference); let is_internal = self.object_internal.contains(&reference);
if !is_internal { if !is_internal {
@ -393,6 +400,7 @@ impl VM {
// Set the field // Set the field
if let Some(fields) = self.object_fields.get_mut(&reference) { if let Some(fields) = self.object_fields.get_mut(&reference) {
fields.insert(field.to_string(), new_value); fields.insert(field.to_string(), new_value);
if debug_ref { eprintln!("[VM] RefSet stored: {}", field); }
} }
Ok(ControlFlow::Continue) Ok(ControlFlow::Continue)
@ -515,19 +523,28 @@ impl VM {
// Call the method based on receiver type // Call the method based on receiver type
let result = match &recv { let result = match &recv {
VMValue::BoxRef(arc_box) => { VMValue::BoxRef(arc_box) => {
// Direct box method call // If this is a user InstanceBox, redirect to lowered function: Class.method/arity
if debug_boxcall { if let Some(inst) = arc_box.as_any().downcast_ref::<crate::instance_v2::InstanceBox>() {
eprintln!("[BoxCall] Taking BoxRef path for method '{}'", method); let func_name = format!("{}.{}{}", inst.class_name, method, format!("/{}", args.len()));
if debug_boxcall { eprintln!("[BoxCall] InstanceBox -> call {}", func_name); }
// Build VMValue args: receiver first, then original VMValue args
let mut vm_args = Vec::with_capacity(1 + args.len());
vm_args.push(recv.clone());
for a in args { vm_args.push(self.get_value(*a)?); }
let res = self.call_function_by_name(&func_name, vm_args)?;
return {
if let Some(dst_id) = dst { self.set_value(dst_id, res); }
Ok(ControlFlow::Continue)
};
} }
// Otherwise, direct box method call
if debug_boxcall { eprintln!("[BoxCall] Taking BoxRef path for method '{}'", method); }
let cloned_box = arc_box.share_box(); let cloned_box = arc_box.share_box();
self.call_box_method(cloned_box, method, nyash_args)? self.call_box_method(cloned_box, method, nyash_args)?
} }
_ => { _ => {
// Convert primitive to box and call // Convert primitive to box and call
if debug_boxcall { if debug_boxcall { eprintln!("[BoxCall] Converting primitive to box for method '{}'", method); }
eprintln!("[BoxCall] Converting primitive to box for method '{}'", method);
}
let box_value = recv.to_nyash_box(); let box_value = recv.to_nyash_box();
self.call_box_method(box_value, method, nyash_args)? self.call_box_method(box_value, method, nyash_args)?

View File

@ -13,6 +13,8 @@ use super::vm::{VM, VMError, VMValue};
impl VM { impl VM {
/// Execute binary operation /// Execute binary operation
pub(super) fn execute_binary_op(&self, op: &BinaryOp, left: &VMValue, right: &VMValue) -> Result<VMValue, VMError> { pub(super) fn execute_binary_op(&self, op: &BinaryOp, left: &VMValue, right: &VMValue) -> Result<VMValue, VMError> {
let debug_bin = std::env::var("NYASH_VM_DEBUG_BIN").ok().as_deref() == Some("1");
if debug_bin { eprintln!("[VM] binop {:?} {:?} {:?}", op, left, right); }
// Fast path: logical AND/OR accept any truthy via as_bool // Fast path: logical AND/OR accept any truthy via as_bool
if matches!(*op, BinaryOp::And | BinaryOp::Or) { if matches!(*op, BinaryOp::And | BinaryOp::Or) {
let l = left.as_bool()?; let l = left.as_bool()?;
@ -121,6 +123,43 @@ impl VM {
Ok(VMValue::Integer(res)) Ok(VMValue::Integer(res))
} }
// 80/20 fallback: BoxRef(any) numeric via toString().parse::<i64>()
(VMValue::BoxRef(lb), VMValue::BoxRef(rb)) => {
let li = lb.to_string_box().value.trim().parse::<i64>().ok();
let ri = rb.to_string_box().value.trim().parse::<i64>().ok();
if let (Some(l), Some(r)) = (li, ri) {
let res = match op {
BinaryOp::Add => l + r,
BinaryOp::Sub => l - r,
BinaryOp::Mul => l * r,
BinaryOp::Div => { if r == 0 { return Err(VMError::DivisionByZero); } l / r },
_ => return Err(VMError::InvalidInstruction(format!("Unsupported integer operation: {:?}", op))),
};
if debug_bin { eprintln!("[VM] binop fallback BoxRef-BoxRef -> {}", res); }
Ok(VMValue::Integer(res))
} else {
Err(VMError::TypeError(format!("Unsupported binary operation: {:?} on {:?} and {:?}", op, left, right)))
}
}
(VMValue::BoxRef(lb), VMValue::Integer(r)) => {
if let Ok(l) = lb.to_string_box().value.trim().parse::<i64>() {
let res = match op { BinaryOp::Add => l + *r, BinaryOp::Sub => l - *r, BinaryOp::Mul => l * *r, BinaryOp::Div => { if *r == 0 { return Err(VMError::DivisionByZero); } l / *r }, _ => return Err(VMError::InvalidInstruction(format!("Unsupported integer operation: {:?}", op))), };
if debug_bin { eprintln!("[VM] binop fallback BoxRef-Int -> {}", res); }
Ok(VMValue::Integer(res))
} else {
Err(VMError::TypeError(format!("Unsupported binary operation: {:?} on {:?} and {:?}", op, left, right)))
}
}
(VMValue::Integer(l), VMValue::BoxRef(rb)) => {
if let Ok(r) = rb.to_string_box().value.trim().parse::<i64>() {
let res = match op { BinaryOp::Add => *l + r, BinaryOp::Sub => *l - r, BinaryOp::Mul => *l * r, BinaryOp::Div => { if r == 0 { return Err(VMError::DivisionByZero); } *l / r }, _ => return Err(VMError::InvalidInstruction(format!("Unsupported integer operation: {:?}", op))), };
if debug_bin { eprintln!("[VM] binop fallback Int-BoxRef -> {}", res); }
Ok(VMValue::Integer(res))
} else {
Err(VMError::TypeError(format!("Unsupported binary operation: {:?} on {:?} and {:?}", op, left, right)))
}
}
_ => Err(VMError::TypeError(format!("Unsupported binary operation: {:?} on {:?} and {:?}", op, left, right))), _ => Err(VMError::TypeError(format!("Unsupported binary operation: {:?} on {:?} and {:?}", op, left, right))),
} }
} }

View File

@ -199,9 +199,12 @@ mod tests {
let config = CliConfig { let config = CliConfig {
file: None, file: None,
debug_fuel: Some(100000), debug_fuel: Some(100000),
dump_ast: false,
dump_mir: false, dump_mir: false,
verify_mir: false, verify_mir: false,
mir_verbose: false, mir_verbose: false,
mir_verbose_effects: false,
no_optimize: false,
backend: "interpreter".to_string(), backend: "interpreter".to_string(),
compile_wasm: false, compile_wasm: false,
compile_native: false, compile_native: false,

22
src/debug/log.rs Normal file
View File

@ -0,0 +1,22 @@
//! Tiny env-gated logging helpers (quiet by default)
/// Returns true if the given env var is set to "1".
pub fn on(var: &str) -> bool {
std::env::var(var).ok().as_deref() == Some("1")
}
/// Log a message to stderr if the env var is enabled.
pub fn log(var: &str, msg: &str) {
if on(var) { eprintln!("{}", msg); }
}
/// Log with formatting if the env var is enabled.
#[macro_export]
macro_rules! debug_logf {
($var:expr, $($arg:tt)*) => {{
if std::env::var($var).ok().as_deref() == Some("1") {
eprintln!($($arg)*);
}
}};
}

2
src/debug/mod.rs Normal file
View File

@ -0,0 +1,2 @@
pub mod log;

View File

@ -59,6 +59,7 @@ pub mod cli;
// Runtime system (plugins, registry, etc.) // Runtime system (plugins, registry, etc.)
pub mod runtime; pub mod runtime;
pub mod runner_plugin_init; pub mod runner_plugin_init;
pub mod debug;
#[cfg(target_arch = "wasm32")] #[cfg(target_arch = "wasm32")]
pub mod wasm_test; pub mod wasm_test;

View File

@ -53,6 +53,7 @@ pub mod config;
// Runtime system (plugins, registry, etc.) // Runtime system (plugins, registry, etc.)
pub mod runtime; pub mod runtime;
pub mod debug;
use nyash_rust::cli::CliConfig; use nyash_rust::cli::CliConfig;
use runner::NyashRunner; use runner::NyashRunner;

View File

@ -844,6 +844,9 @@ impl MirBuilder {
// Set up exception handler for the try block (before we enter it) // Set up exception handler for the try block (before we enter it)
if let Some(catch_clause) = catch_clauses.first() { if let Some(catch_clause) = catch_clauses.first() {
if std::env::var("NYASH_DEBUG_TRYCATCH").ok().as_deref() == Some("1") {
eprintln!("[BUILDER] Emitting catch handler for {:?}", catch_clause.exception_type);
}
let exception_value = self.value_gen.next(); let exception_value = self.value_gen.next();
// Register catch handler for exceptions that may occur in try block // Register catch handler for exceptions that may occur in try block
@ -874,9 +877,15 @@ impl MirBuilder {
// Build catch block (reachable via exception handling) // Build catch block (reachable via exception handling)
self.start_new_block(catch_block)?; self.start_new_block(catch_block)?;
if std::env::var("NYASH_DEBUG_TRYCATCH").ok().as_deref() == Some("1") {
eprintln!("[BUILDER] Enter catch block {:?}", catch_block);
}
// Handle catch clause // Handle catch clause
if let Some(catch_clause) = catch_clauses.first() { if let Some(catch_clause) = catch_clauses.first() {
if std::env::var("NYASH_DEBUG_TRYCATCH").ok().as_deref() == Some("1") {
eprintln!("[BUILDER] Emitting catch handler for {:?}", catch_clause.exception_type);
}
// Build catch body // Build catch body
let catch_ast = ASTNode::Program { let catch_ast = ASTNode::Program {
statements: catch_clause.body.clone(), statements: catch_clause.body.clone(),

View File

@ -5,6 +5,7 @@
*/ */
use std::fmt; use std::fmt;
use crate::debug::log as dlog;
/// Effect flags for tracking side effects and enabling optimizations /// Effect flags for tracking side effects and enabling optimizations
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
@ -129,8 +130,16 @@ impl EffectMask {
/// Check if the computation is pure (no side effects) /// Check if the computation is pure (no side effects)
pub fn is_pure(self) -> bool { pub fn is_pure(self) -> bool {
// 純粋性は一次カテゴリで判定Pureビットが立っていてもIO/WRITE/CONTROLがあれば純粋ではない) // READ/WRITE/IO/CONTROLがあれば純粋ではないREADはreadonly扱い
self.primary_category() == Effect::Pure let pure = !self.contains(Effect::ReadHeap)
&& !self.is_mut()
&& !self.is_io()
&& !self.is_control();
if dlog::on("NYASH_DEBUG_EFFECTS") {
eprintln!("[EFFECT] bits={:#06x} primary={:?} is_pure={} read_only={} mut={} io={}",
self.bits(), self.primary_category(), pure, self.is_read_only(), self.is_mut(), self.is_io());
}
pure
} }
/// Check if the computation is mutable (modifies state) /// Check if the computation is mutable (modifies state)

View File

@ -412,7 +412,7 @@ impl MirInstruction {
// Phase 5: Control flow & exception handling // Phase 5: Control flow & exception handling
MirInstruction::Throw { effects, .. } => *effects, MirInstruction::Throw { effects, .. } => *effects,
MirInstruction::Catch { .. } => EffectMask::PURE, // Setting up handler is pure MirInstruction::Catch { .. } => EffectMask::CONTROL, // Handler setup affects control handling
MirInstruction::Safepoint => EffectMask::PURE, // No-op for now MirInstruction::Safepoint => EffectMask::PURE, // No-op for now
// Phase 6: Box reference operations // Phase 6: Box reference operations

View File

@ -0,0 +1,35 @@
//! Introspection helpers for MIR instruction set
//! This module enumerates the canonical 26 core instruction names to sync with docs.
/// Returns the canonical list of core MIR instruction names (26 items).
/// This list must match docs/reference/mir/INSTRUCTION_SET.md under "Core Instructions".
pub fn core_instruction_names() -> &'static [&'static str] {
&[
"Const",
"Copy",
"Load",
"Store",
"UnaryOp",
"BinOp",
"Compare",
"Jump",
"Branch",
"Phi",
"Return",
"Call",
"ExternCall",
"BoxCall",
"NewBox",
"ArrayGet",
"ArraySet",
"RefNew",
"RefGet",
"RefSet",
"Await",
"Print",
"TypeOp",
"WeakRef",
"Barrier",
]
}

View File

@ -7,6 +7,7 @@
pub mod instruction; pub mod instruction;
pub mod instruction_v2; // New 25-instruction specification pub mod instruction_v2; // New 25-instruction specification
pub mod instruction_introspection; // Introspection helpers for tests (core instruction names)
pub mod basic_block; pub mod basic_block;
pub mod function; pub mod function;
pub mod builder; pub mod builder;
@ -190,13 +191,10 @@ mod tests {
#[test] #[test]
fn test_lowering_extern_console_log() { fn test_lowering_extern_console_log() {
// Build AST: console.log("hi") → ExternCall env.console.log // Build AST: console.log("hi") → ExternCall env.console.log
let ast = ASTNode::Expression { let ast = ASTNode::MethodCall {
expr: Box::new(ASTNode::MethodCall { object: Box::new(ASTNode::Variable { name: "console".to_string(), span: crate::ast::Span::unknown() }),
object: Box::new(ASTNode::Variable { name: "console".to_string(), span: crate::ast::Span::unknown() }), method: "log".to_string(),
method: "log".to_string(), arguments: vec![ ASTNode::Literal { value: LiteralValue::String("hi".to_string()), span: crate::ast::Span::unknown() } ],
arguments: vec![ ASTNode::Literal { value: LiteralValue::String("hi".to_string()), span: crate::ast::Span::unknown() } ],
span: crate::ast::Span::unknown(),
}),
span: crate::ast::Span::unknown(), span: crate::ast::Span::unknown(),
}; };
@ -210,13 +208,10 @@ mod tests {
#[test] #[test]
fn test_lowering_boxcall_array_push() { fn test_lowering_boxcall_array_push() {
// Build AST: (new ArrayBox()).push(1) // Build AST: (new ArrayBox()).push(1)
let ast = ASTNode::Expression { let ast = ASTNode::MethodCall {
expr: Box::new(ASTNode::MethodCall { object: Box::new(ASTNode::New { class: "ArrayBox".to_string(), arguments: vec![], type_arguments: vec![], span: crate::ast::Span::unknown() }),
object: Box::new(ASTNode::New { class: "ArrayBox".to_string(), arguments: vec![], type_arguments: vec![], span: crate::ast::Span::unknown() }), method: "push".to_string(),
method: "push".to_string(), arguments: vec![ ASTNode::Literal { value: LiteralValue::Integer(1), span: crate::ast::Span::unknown() } ],
arguments: vec![ ASTNode::Literal { value: LiteralValue::Integer(1), span: crate::ast::Span::unknown() } ],
span: crate::ast::Span::unknown(),
}),
span: crate::ast::Span::unknown(), span: crate::ast::Span::unknown(),
}; };

View File

@ -6,6 +6,7 @@
use super::{MirModule, MirFunction, BasicBlock, MirInstruction}; use super::{MirModule, MirFunction, BasicBlock, MirInstruction};
use std::fmt::Write; use std::fmt::Write;
use crate::debug::log as dlog;
/// MIR printer for debug output and visualization /// MIR printer for debug output and visualization
pub struct MirPrinter { pub struct MirPrinter {
@ -148,6 +149,8 @@ impl MirPrinter {
for block in function.blocks.values() { for block in function.blocks.values() {
for inst in &block.instructions { for inst in &block.instructions {
match inst { match inst {
MirInstruction::Throw { .. } => { if dlog::on("NYASH_DEBUG_MIR_PRINTER") { eprintln!("[PRINTER] found throw in {}", function.signature.name); } }
MirInstruction::Catch { .. } => { if dlog::on("NYASH_DEBUG_MIR_PRINTER") { eprintln!("[PRINTER] found catch in {}", function.signature.name); } }
MirInstruction::TypeCheck { .. } => type_check += 1, MirInstruction::TypeCheck { .. } => type_check += 1,
MirInstruction::Cast { .. } => type_cast += 1, MirInstruction::Cast { .. } => type_cast += 1,
MirInstruction::TypeOp { op, .. } => match op { MirInstruction::TypeOp { op, .. } => match op {
@ -171,6 +174,8 @@ impl MirPrinter {
} }
if let Some(term) = &block.terminator { if let Some(term) = &block.terminator {
match term { match term {
MirInstruction::Throw { .. } => { if dlog::on("NYASH_DEBUG_MIR_PRINTER") { eprintln!("[PRINTER] found throw(term) in {}", function.signature.name); } }
MirInstruction::Catch { .. } => { if dlog::on("NYASH_DEBUG_MIR_PRINTER") { eprintln!("[PRINTER] found catch(term) in {}", function.signature.name); } }
MirInstruction::TypeCheck { .. } => type_check += 1, MirInstruction::TypeCheck { .. } => type_check += 1,
MirInstruction::Cast { .. } => type_cast += 1, MirInstruction::Cast { .. } => type_cast += 1,
MirInstruction::TypeOp { op, .. } => match op { MirInstruction::TypeOp { op, .. } => match op {

View File

@ -5,6 +5,7 @@
*/ */
use super::{MirModule, MirFunction, BasicBlockId, ValueId}; use super::{MirModule, MirFunction, BasicBlockId, ValueId};
use crate::debug::log as dlog;
use std::collections::{HashSet, HashMap}; use std::collections::{HashSet, HashMap};
/// Verification error types /// Verification error types
@ -145,6 +146,10 @@ impl MirVerifier {
if local_errors.is_empty() { if local_errors.is_empty() {
Ok(()) Ok(())
} else { } else {
if dlog::on("NYASH_DEBUG_VERIFIER") {
eprintln!("[VERIFY] {} errors in function {}", local_errors.len(), function.signature.name);
for e in &local_errors { eprintln!("{:?}", e); }
}
Err(local_errors) Err(local_errors)
} }
} }

View File

@ -51,7 +51,10 @@ impl NyashRunner {
runtime::init_global_unified_registry(); runtime::init_global_unified_registry();
// Try to initialize BID plugins from nyash.toml (best-effort) // Try to initialize BID plugins from nyash.toml (best-effort)
runner_plugin_init::init_bid_plugins(); // Allow disabling during snapshot/CI via NYASH_DISABLE_PLUGINS=1
if std::env::var("NYASH_DISABLE_PLUGINS").ok().as_deref() != Some("1") {
runner_plugin_init::init_bid_plugins();
}
// Optional: enable VM stats via CLI flags // Optional: enable VM stats via CLI flags
if self.config.vm_stats { if self.config.vm_stats {
@ -114,7 +117,8 @@ impl NyashRunner {
println!("Memory safety guaranteed by Rust's borrow checker! 🛡️"); println!("Memory safety guaranteed by Rust's borrow checker! 🛡️");
} }
/// Execute Nyash file with interpreter /// Execute Nyash file with interpreter (moved to modes/common.rs)
#[cfg(any())]
fn execute_nyash_file(&self, filename: &str) { fn execute_nyash_file(&self, filename: &str) {
// Read the file // Read the file
let code = match fs::read_to_string(filename) { let code = match fs::read_to_string(filename) {
@ -178,7 +182,8 @@ impl NyashRunner {
} }
} }
/// Execute MIR compilation and processing mode /// Execute MIR compilation and processing mode (moved to modes/mir.rs)
#[cfg(any())]
fn execute_mir_mode(&self, filename: &str) { fn execute_mir_mode(&self, filename: &str) {
// Read the file // Read the file
let code = match fs::read_to_string(filename) { let code = match fs::read_to_string(filename) {
@ -233,7 +238,8 @@ impl NyashRunner {
} }
} }
/// Execute VM mode /// Execute VM mode (moved to modes/vm.rs)
#[cfg(any())]
fn execute_vm_mode(&self, filename: &str) { fn execute_vm_mode(&self, filename: &str) {
// Read the file // Read the file
let code = match fs::read_to_string(filename) { let code = match fs::read_to_string(filename) {
@ -295,7 +301,8 @@ impl NyashRunner {
/// Collect Box declarations from AST and register into runtime /// Collect Box declarations (moved to modes/vm.rs)
#[cfg(any())]
fn collect_box_declarations(&self, ast: &ASTNode, runtime: &NyashRuntime) { fn collect_box_declarations(&self, ast: &ASTNode, runtime: &NyashRuntime) {
fn walk(node: &ASTNode, runtime: &NyashRuntime) { fn walk(node: &ASTNode, runtime: &NyashRuntime) {
match node { match node {
@ -342,7 +349,8 @@ impl NyashRunner {
// execute_aot_mode moved to runner::modes::aot // execute_aot_mode moved to runner::modes::aot
/// Execute LLVM mode /// Execute LLVM mode (moved to modes/llvm.rs)
#[cfg(any())]
fn execute_llvm_mode(&self, filename: &str) { fn execute_llvm_mode(&self, filename: &str) {
// Read the file // Read the file
let code = match fs::read_to_string(filename) { let code = match fs::read_to_string(filename) {
@ -436,7 +444,8 @@ impl NyashRunner {
} }
} }
/// Execute benchmark mode /// Execute benchmark mode (moved to modes/bench.rs)
#[cfg(any())]
fn execute_benchmark_mode(&self) { fn execute_benchmark_mode(&self) {
println!("🏁 Running benchmark mode with {} iterations", self.config.iterations); println!("🏁 Running benchmark mode with {} iterations", self.config.iterations);
@ -728,9 +737,12 @@ mod tests {
let config = CliConfig { let config = CliConfig {
file: None, file: None,
debug_fuel: Some(100000), debug_fuel: Some(100000),
dump_ast: false,
dump_mir: false, dump_mir: false,
verify_mir: false, verify_mir: false,
mir_verbose: false, mir_verbose: false,
mir_verbose_effects: false,
no_optimize: false,
backend: "interpreter".to_string(), backend: "interpreter".to_string(),
compile_wasm: false, compile_wasm: false,
compile_native: false, compile_native: false,

View File

@ -0,0 +1,46 @@
use std::fs;
use std::path::Path;
use nyash_rust::mir::instruction_introspection;
// Compare the source-of-truth doc and implementation core instruction names.
#[test]
fn mir_instruction_set_doc_sync() {
// 1) Read the canonical list from docs
let doc_path = Path::new("docs/reference/mir/INSTRUCTION_SET.md");
let content = fs::read_to_string(doc_path)
.expect("Failed to read docs/reference/mir/INSTRUCTION_SET.md");
let mut in_core = false;
let mut doc_names: Vec<String> = Vec::new();
for line in content.lines() {
let line = line.trim();
if line.starts_with("## Core Instructions") {
in_core = true;
continue;
}
if in_core && line.starts_with("## ") { // stop at next section (e.g., Meta)
break;
}
if in_core {
if let Some(rest) = line.strip_prefix("- ") {
// Each bullet is a name, possibly with annotations like ... → strip anything after a space/paren
let name = rest.split(|c: char| c.is_whitespace() || c == '' || c == '(')
.next().unwrap_or("").trim();
if !name.is_empty() {
doc_names.push(name.to_string());
}
}
}
}
assert_eq!(doc_names.len(), 26, "Doc must list exactly 26 core instructions");
// 2) Implementation list
let impl_names = instruction_introspection::core_instruction_names();
// Compare as sets (order-agnostic)
let doc_set: std::collections::BTreeSet<_> = doc_names.iter().map(|s| s.as_str()).collect();
let impl_set: std::collections::BTreeSet<_> = impl_names.iter().copied().collect();
assert_eq!(doc_set, impl_set, "MIR core instruction names must match docs exactly");
}

View File

@ -29,8 +29,11 @@ fi
if [ -n "$OUTFILE" ]; then if [ -n "$OUTFILE" ]; then
mkdir -p "$(dirname "$OUTFILE")" mkdir -p "$(dirname "$OUTFILE")"
"${CMD[@]}" | sed -e :a -e '/^\n*$/{$d;N;ba' -e '}' > "$OUTFILE" # Filter noisy plugin/runtime banners to stabilize snapshots
NYASH_CLI_VERBOSE=1 "${CMD[@]}" \
| grep -Ev '^(\[PluginLoaderV2\]|\[FileBox\]|Net plugin:)' \
| sed -e :a -e '/^\n*$/{$d;N;ba' -e '}' > "$OUTFILE"
echo "Wrote MIR snapshot: $OUTFILE" echo "Wrote MIR snapshot: $OUTFILE"
else else
"${CMD[@]}" NYASH_CLI_VERBOSE=1 "${CMD[@]}"
fi fi