feat: Add VM instruction statistics and fix plugin TLV debugging

Major changes:
- Add --vm-stats and --vm-stats-json CLI flags for VM instruction profiling
- Implement instruction counting by opcode type with JSON output support
- Add enhanced TLV debug logging with NYASH_DEBUG_PLUGIN=1 environment variable
- Fix missing fields in CliConfig and ASTNode::BoxDeclaration for test compatibility
- Improve plugin method call error messages with argument count/type details

This enables MIR→VM conversion health checks and supports the Phase 8.6 VM optimization goals.

🤖 Generated with [Claude Code](https://claude.ai/code)

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Moe Charm
2025-08-23 03:40:17 +09:00
parent 4760921e91
commit dd09e81018
7 changed files with 200 additions and 50 deletions

View File

@ -13,6 +13,7 @@ use crate::scope_tracker::ScopeTracker;
// MirModule is already imported via crate::mir at top
use crate::instance_v2::InstanceBox;
use super::vm_phi::LoopExecutor;
use std::time::Instant;
// Phase 9.78a: Import necessary components for unified Box handling
// TODO: Re-enable when interpreter refactoring is complete
@ -185,6 +186,10 @@ pub struct VM {
scope_tracker: ScopeTracker,
/// Active MIR module during execution (for function calls)
module: Option<MirModule>,
/// Instruction execution counters (by MIR opcode)
instr_counter: std::collections::HashMap<&'static str, usize>,
/// Execution start time for optional stats
exec_start: Option<Instant>,
// Phase 9.78a: Add unified Box handling components
// TODO: Re-enable when interpreter refactoring is complete
// /// Box registry for creating all Box types
@ -215,6 +220,8 @@ impl VM {
runtime: NyashRuntime::new(),
scope_tracker: ScopeTracker::new(),
module: None,
instr_counter: std::collections::HashMap::new(),
exec_start: None,
// TODO: Re-enable when interpreter refactoring is complete
// box_registry: Arc::new(UnifiedBoxRegistry::new()),
// #[cfg(all(feature = "plugins", not(target_arch = "wasm32")))]
@ -240,6 +247,8 @@ impl VM {
runtime,
scope_tracker: ScopeTracker::new(),
module: None,
instr_counter: std::collections::HashMap::new(),
exec_start: None,
}
}
@ -270,6 +279,9 @@ impl VM {
pub fn execute_module(&mut self, module: &MirModule) -> Result<Box<dyn NyashBox>, VMError> {
// Store module for nested calls
self.module = Some(module.clone());
// Reset stats
self.instr_counter.clear();
self.exec_start = Some(Instant::now());
// Find main function
let main_function = module.get_function("main")
.ok_or_else(|| VMError::InvalidInstruction("No main function found".to_string()))?;
@ -277,6 +289,9 @@ impl VM {
// Execute main function
let result = self.execute_function(main_function)?;
// Optional: print VM stats
self.maybe_print_stats();
// Convert result to NyashBox
Ok(result.to_nyash_box())
}
@ -392,6 +407,8 @@ impl VM {
/// Execute a single instruction
fn execute_instruction(&mut self, instruction: &MirInstruction) -> Result<ControlFlow, VMError> {
// Record instruction for stats
self.record_instruction(instruction);
match instruction {
MirInstruction::Const { dst, value } => {
let vm_value = VMValue::from(value);
@ -985,6 +1002,90 @@ impl VM {
_ => Err(VMError::TypeError(format!("Unsupported comparison: {:?} on {:?} and {:?}", op, left, right))),
}
}
/// Record an instruction execution for statistics
fn record_instruction(&mut self, instruction: &MirInstruction) {
let key: &'static str = match instruction {
MirInstruction::Const { .. } => "Const",
MirInstruction::BinOp { .. } => "BinOp",
MirInstruction::UnaryOp { .. } => "UnaryOp",
MirInstruction::Compare { .. } => "Compare",
MirInstruction::Load { .. } => "Load",
MirInstruction::Store { .. } => "Store",
MirInstruction::Call { .. } => "Call",
MirInstruction::BoxCall { .. } => "BoxCall",
MirInstruction::Branch { .. } => "Branch",
MirInstruction::Jump { .. } => "Jump",
MirInstruction::Return { .. } => "Return",
MirInstruction::Phi { .. } => "Phi",
MirInstruction::NewBox { .. } => "NewBox",
MirInstruction::TypeCheck { .. } => "TypeCheck",
MirInstruction::Cast { .. } => "Cast",
MirInstruction::ArrayGet { .. } => "ArrayGet",
MirInstruction::ArraySet { .. } => "ArraySet",
MirInstruction::Copy { .. } => "Copy",
MirInstruction::Debug { .. } => "Debug",
MirInstruction::Print { .. } => "Print",
MirInstruction::Nop => "Nop",
MirInstruction::Throw { .. } => "Throw",
MirInstruction::Catch { .. } => "Catch",
MirInstruction::Safepoint => "Safepoint",
MirInstruction::RefNew { .. } => "RefNew",
MirInstruction::RefGet { .. } => "RefGet",
MirInstruction::RefSet { .. } => "RefSet",
MirInstruction::WeakNew { .. } => "WeakNew",
MirInstruction::WeakLoad { .. } => "WeakLoad",
MirInstruction::BarrierRead { .. } => "BarrierRead",
MirInstruction::BarrierWrite { .. } => "BarrierWrite",
MirInstruction::FutureNew { .. } => "FutureNew",
MirInstruction::FutureSet { .. } => "FutureSet",
MirInstruction::Await { .. } => "Await",
MirInstruction::ExternCall { .. } => "ExternCall",
};
*self.instr_counter.entry(key).or_insert(0) += 1;
}
/// Print simple VM execution statistics when enabled via env var
fn maybe_print_stats(&mut self) {
let enabled = std::env::var("NYASH_VM_STATS").ok().map(|v| v != "0").unwrap_or(false);
if !enabled { return; }
let elapsed_ms = self.exec_start.map(|t| t.elapsed().as_secs_f64() * 1000.0).unwrap_or(0.0);
let mut items: Vec<(&str, usize)> = self.instr_counter.iter().map(|(k,v)| (*k, *v)).collect();
items.sort_by(|a,b| b.1.cmp(&a.1).then_with(|| a.0.cmp(&b.0)));
let total: usize = items.iter().map(|(_,v)| *v).sum();
let json_enabled = std::env::var("NYASH_VM_STATS_JSON").ok().map(|v| v != "0").unwrap_or(false)
|| std::env::var("NYASH_VM_STATS_FORMAT").map(|v| v == "json").unwrap_or(false);
if json_enabled {
// Build JSON structure: { total, elapsed_ms, counts: {op: n, ...}, top20: [{op, count}], timestamp_ms }
let counts_obj: std::collections::BTreeMap<&str, usize> = self.instr_counter.iter().map(|(k,v)| (*k, *v)).collect();
let top20: Vec<_> = items.iter().take(20).map(|(op,cnt)| {
serde_json::json!({ "op": op, "count": cnt })
}).collect();
let now_ms = {
use std::time::{SystemTime, UNIX_EPOCH};
SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_millis() as u64).unwrap_or(0)
};
let payload = serde_json::json!({
"total": total,
"elapsed_ms": elapsed_ms,
"counts": counts_obj,
"top20": top20,
"timestamp_ms": now_ms
});
match serde_json::to_string_pretty(&payload) {
Ok(s) => println!("{}", s),
Err(_) => println!("{{\"total\":{},\"elapsed_ms\":{:.3}}}", total, elapsed_ms),
}
} else {
println!("\n🧮 VM Stats: {} instructions in {:.3} ms", total, elapsed_ms);
for (k, v) in items.into_iter().take(20) {
println!(" {:>10}: {:>8}", k, v);
}
}
}
/// Phase 9.78a: Unified method dispatch for all Box types
fn call_unified_method(&self, box_value: Box<dyn NyashBox>, method: &str, args: Vec<Box<dyn NyashBox>>) -> Result<Box<dyn NyashBox>, VMError> {

View File

@ -21,6 +21,8 @@ pub struct CliConfig {
pub output_file: Option<String>,
pub benchmark: bool,
pub iterations: u32,
pub vm_stats: bool,
pub vm_stats_json: bool,
}
impl CliConfig {
@ -112,6 +114,18 @@ impl CliConfig {
.help("Number of iterations for benchmarks (default: 10)")
.default_value("10")
)
.arg(
Arg::new("vm-stats")
.long("vm-stats")
.help("Enable VM instruction statistics (equivalent to NYASH_VM_STATS=1)")
.action(clap::ArgAction::SetTrue)
)
.arg(
Arg::new("vm-stats-json")
.long("vm-stats-json")
.help("Output VM statistics in JSON format")
.action(clap::ArgAction::SetTrue)
)
}
/// Convert ArgMatches to CliConfig
@ -128,6 +142,8 @@ impl CliConfig {
output_file: matches.get_one::<String>("output").cloned(),
benchmark: matches.get_flag("benchmark"),
iterations: matches.get_one::<String>("iterations").unwrap().parse().unwrap_or(10),
vm_stats: matches.get_flag("vm-stats"),
vm_stats_json: matches.get_flag("vm-stats-json"),
}
}
}
@ -168,9 +184,11 @@ mod tests {
output_file: None,
benchmark: false,
iterations: 10,
vm_stats: false,
vm_stats_json: false,
};
assert_eq!(config.backend, "interpreter");
assert_eq!(config.iterations, 10);
}
}
}

View File

@ -51,6 +51,15 @@ impl NyashRunner {
// Try to initialize BID plugins from nyash.toml (best-effort)
self.init_bid_plugins();
// Optional: enable VM stats via CLI flags
if self.config.vm_stats {
std::env::set_var("NYASH_VM_STATS", "1");
}
if self.config.vm_stats_json {
// Prefer explicit JSON flag over any default
std::env::set_var("NYASH_VM_STATS_JSON", "1");
}
// Benchmark mode - can run without a file
if self.config.benchmark {
println!("📊 Nyash Performance Benchmark Suite");
@ -926,6 +935,8 @@ mod tests {
output_file: None,
benchmark: false,
iterations: 10,
vm_stats: false,
vm_stats_json: false,
};
let runner = NyashRunner::new(config);

View File

@ -386,6 +386,14 @@ impl PluginBoxV2 {
let expected_args = box_conf.methods.get(method_name).and_then(|m| m.args.clone());
if let Some(exp) = expected_args.as_ref() {
if exp.len() != args.len() {
eprintln!(
"[PluginLoaderV2] InvalidArgs: {}.{} expects {} args, got {} (schema={:?})",
box_type,
method_name,
exp.len(),
args.len(),
exp.iter().map(|a| a.kind_str()).collect::<Vec<_>>()
);
return Err(BidError::InvalidArgs);
}
}
@ -417,7 +425,8 @@ impl PluginBoxV2 {
}
}
_ => {
// Unsupported kind in this minimal implementation
eprintln!("[PluginLoaderV2] InvalidArgs: unsupported kind '{}' for {}.{} arg[{}]",
kind, box_type, method_name, idx);
return Err(BidError::InvalidArgs);
}
}
@ -427,6 +436,8 @@ impl PluginBoxV2 {
let is_string = a.as_any().downcast_ref::<StringBox>().is_some();
let is_int = a.as_any().downcast_ref::<IntegerBox>().is_some();
if !(is_string || is_int) {
eprintln!("[PluginLoaderV2] InvalidArgs: expected string/int for {}.{} arg[{}]",
box_type, method_name, idx);
return Err(BidError::InvalidArgs);
}
}
@ -481,7 +492,15 @@ impl PluginBoxV2 {
buf
};
eprintln!("[VM→Plugin] call {}.{} recv_id={} returns_result={}", box_type, method_name, instance_id, returns_result);
if dbg_on() { eprintln!("[VM→Plugin] call {}.{} recv_id={} returns_result={}", box_type, method_name, instance_id, returns_result); }
if dbg_on() {
// Dump compact TLV header and first few bytes for diagnostics
let hdr_ver = u16::from_le_bytes([tlv_args[0], tlv_args[1]]);
let hdr_argc = u16::from_le_bytes([tlv_args[2], tlv_args[3]]);
let preview_len = std::cmp::min(tlv_args.len(), 64);
let preview: Vec<String> = tlv_args[..preview_len].iter().map(|b| format!("{:02X}", b)).collect();
eprintln!("[VM→Plugin] TLV ver={} argc={} bytes={} preview={}...",
hdr_ver, hdr_argc, tlv_args.len(), preview.join(" "));
}
let mut out = vec![0u8; 1024];
let mut out_len: usize = out.len();
let rc = unsafe {