fix(jit): NewBoxのJIT安全化とDebugBox Phase 1実装

- NewBoxのJIT扱いを安全化(src/jit/lower/core.rs)
  - NYASH_USE_PLUGIN_BUILTINS=1 && args.is_empty() かつ StringBox/IntegerBox のみJIT許可
  - ArrayBox/MapBox等のプラグインBoxまたは引数ありはunsupportedとしてカウント
  - unsupported>0の関数はJIT対象外となりVM実行にフォールバック(Segfault回避)

- DebugBox Phase 1実装(JITトレース機能)
  - tracePluginCalls(bool)でJITシムトレースON/OFF
  - getJitEvents()で直近のJITイベント取得
  - src/jit/shim_trace.rs追加でトレース基盤実装

- Printのサポート
  - PrintはJIT非対応に戻しVM経路で確実に出力(出力消失解消)

- テストとサンプル追加
  - examples/jit_plugin_invoke_param_array.nyash: 最小JITスモークテスト
  - examples/py_result_*.nyash: Python plugin結果チェーン処理デモ

- PyRuntimeBox拡張
  - str()メソッドでPyObjectのstring表現を取得可能に
  - エラーハンドリング改善とResultチェーンサポート

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
Moe Charm
2025-08-29 13:46:18 +09:00
parent a9e82933cc
commit 3d8ba3f3ec
26 changed files with 1384 additions and 100 deletions

View File

@ -308,6 +308,18 @@ impl DebugBox {
let tracked = self.tracked_boxes.read().unwrap();
Ok(Box::new(crate::box_trait::IntegerBox::new(tracked.len() as i64)))
}
// --- Phase 1: JIT/Plugin shim tracing ---
pub fn trace_plugin_calls(&self, on: bool) -> Result<Box<dyn NyashBox>, RuntimeError> {
crate::jit::shim_trace::set_enabled(on);
println!("[DEBUG] JIT shim trace: {}", if on {"ENABLED"} else {"DISABLED"});
Ok(Box::new(VoidBox::new()))
}
pub fn get_jit_events(&self) -> Result<Box<dyn NyashBox>, RuntimeError> {
let s = crate::jit::shim_trace::snapshot_joined();
Ok(Box::new(StringBox::new(s)))
}
}
// Manual Clone implementation for DebugBox (RwLock doesn't auto-derive Clone)
@ -388,4 +400,4 @@ impl NyashBox for DebugBox {
}
}
}

View File

@ -357,6 +357,23 @@ impl NyashInterpreter {
}
debug_box.show_call_stack()
}
"tracePluginCalls" => {
if arg_values.len() != 1 {
return Err(RuntimeError::InvalidOperation {
message: format!("tracePluginCalls(on:bool) expects 1 argument, got {}", arg_values.len()),
});
}
let on = if let Some(b) = arg_values[0].as_any().downcast_ref::<crate::box_trait::BoolBox>() { b.value } else { false };
debug_box.trace_plugin_calls(on)
}
"getJitEvents" => {
if !arg_values.is_empty() {
return Err(RuntimeError::InvalidOperation {
message: format!("getJitEvents() expects 0 arguments, got {}", arg_values.len()),
});
}
debug_box.get_jit_events()
}
"clear" => {
if !arg_values.is_empty() {
return Err(RuntimeError::InvalidOperation {
@ -388,4 +405,4 @@ impl NyashInterpreter {
}
}
}
}
}

View File

@ -143,6 +143,7 @@ extern "C" fn nyash_host_stub0() -> i64 { 0 }
#[cfg(feature = "cranelift-jit")]
extern "C" fn nyash_plugin_invoke3_i64(type_id: i64, method_id: i64, argc: i64, a0: i64, a1: i64, a2: i64) -> i64 {
use crate::runtime::plugin_loader_v2::PluginBoxV2;
let trace = crate::jit::shim_trace::is_enabled();
// Resolve receiver instance from legacy VM args (param index)
let mut instance_id: u32 = 0;
let mut invoke: Option<unsafe extern "C" fn(u32,u32,u32,*const u8,usize,*mut u8,*mut usize)->i32> = None;
@ -157,19 +158,54 @@ extern "C" fn nyash_plugin_invoke3_i64(type_id: i64, method_id: i64, argc: i64,
}
});
}
// If not resolved, scan all VM args for a matching PluginBoxV2 by type_id
if invoke.is_none() {
crate::jit::rt::with_legacy_vm_args(|args| {
for v in args.iter() {
if let crate::backend::vm::VMValue::BoxRef(b) = v {
if let Some(p) = b.as_any().downcast_ref::<PluginBoxV2>() {
// type_id compatibility is best-effort; fall back to first PluginBoxV2
if p.inner.type_id == (type_id as u32) || invoke.is_none() {
instance_id = p.instance_id();
invoke = Some(p.inner.invoke_fn);
if p.inner.type_id == (type_id as u32) { break; }
}
}
}
}
});
}
if invoke.is_none() { return 0; }
// Build TLV args from a1/a2 if present
let mut buf = crate::runtime::plugin_ffi_common::encode_tlv_header((argc.saturating_sub(1).max(0) as u16));
let mut add_i64 = |v: i64| { crate::runtime::plugin_ffi_common::encode::i64(&mut buf, v); };
if argc >= 2 { add_i64(a1); }
if argc >= 3 { add_i64(a2); }
// Prepare output buffer
let mut out: [u8; 32] = [0; 32];
let mut out_len: usize = out.len();
let rc = unsafe { invoke.unwrap()(type_id as u32, method_id as u32, instance_id, buf.as_ptr(), buf.len(), out.as_mut_ptr(), &mut out_len) };
// Prepare output buffer with canaries for overrun detection
let mut out = vec![0xCDu8; 4096 + 32];
let canary_val = 0xABu8;
let canary_len = 16usize;
for i in 0..canary_len { out[i] = canary_val; }
for i in 0..canary_len { out[4096 + canary_len + i] = canary_val; }
let mut out_len: usize = 4096;
let out_ptr = unsafe { out.as_mut_ptr().add(canary_len) };
if trace { eprintln!("[JIT-SHIM i64] invoke type={} method={} argc={} inst_id={} a1={} a2={} buf_len={}", type_id, method_id, argc, instance_id, a1, a2, buf.len()); }
crate::jit::shim_trace::push(format!("i64.start type={} method={} argc={} inst={} a1={} a2={}", type_id, method_id, argc, instance_id, a1, a2));
let rc = unsafe { invoke.unwrap()(type_id as u32, method_id as u32, instance_id, buf.as_ptr(), buf.len(), out_ptr, &mut out_len) };
// Canary check
let pre_ok = out[..canary_len].iter().all(|&b| b==canary_val);
let post_ok = out[canary_len + out_len .. canary_len + out_len + canary_len].iter().all(|&b| b==canary_val);
if trace { eprintln!("[JIT-SHIM i64] rc={} out_len={} canary_pre={} canary_post={}", rc, out_len, pre_ok, post_ok); }
crate::jit::shim_trace::push(format!("i64.end rc={} out_len={} pre_ok={} post_ok={}", rc, out_len, pre_ok, post_ok));
if rc != 0 { return 0; }
if let Some((tag, _sz, payload)) = crate::runtime::plugin_ffi_common::decode::tlv_first(&out[..out_len]) {
let out_slice = unsafe { std::slice::from_raw_parts(out_ptr, out_len) };
if let Some((tag, sz, payload)) = crate::runtime::plugin_ffi_common::decode::tlv_first(out_slice) {
if trace { eprintln!("[JIT-SHIM i64] TLV tag={} sz={}", tag, sz); }
crate::jit::shim_trace::push(format!("i64.tlv tag={} sz={}", tag, sz));
match tag {
2 => { // I32
if let Some(v) = crate::runtime::plugin_ffi_common::decode::i32(payload) { return v as i64; }
}
3 => { // I64
if let Some(v) = crate::runtime::plugin_ffi_common::decode::i32(payload) { return v as i64; }
if payload.len() == 8 { let mut b=[0u8;8]; b.copy_from_slice(payload); return i64::from_le_bytes(b); }
@ -177,11 +213,99 @@ extern "C" fn nyash_plugin_invoke3_i64(type_id: i64, method_id: i64, argc: i64,
1 => { // Bool
return if crate::runtime::plugin_ffi_common::decode::bool(payload).unwrap_or(false) { 1 } else { 0 };
}
5 => { // F64 → optional conversion to i64 when enabled
if std::env::var("NYASH_JIT_NATIVE_F64").ok().as_deref() == Some("1") {
if sz == 8 {
let mut b=[0u8;8]; b.copy_from_slice(payload);
let f = f64::from_le_bytes(b);
return f as i64;
}
}
}
_ => {}
}
}
0
}
// F64-typed shim: decodes TLV first entry and returns f64 when possible
extern "C" fn nyash_plugin_invoke3_f64(type_id: i64, method_id: i64, argc: i64, a0: i64, a1: i64, a2: i64) -> f64 {
use crate::runtime::plugin_loader_v2::PluginBoxV2;
let trace = crate::jit::shim_trace::is_enabled();
// Resolve receiver + invoke_fn from legacy VM args
let mut instance_id: u32 = 0;
let mut invoke: Option<unsafe extern "C" fn(u32,u32,u32,*const u8,usize,*mut u8,*mut usize)->i32> = None;
if a0 >= 0 {
crate::jit::rt::with_legacy_vm_args(|args| {
let idx = a0 as usize;
if let Some(crate::backend::vm::VMValue::BoxRef(b)) = args.get(idx) {
if let Some(p) = b.as_any().downcast_ref::<PluginBoxV2>() {
instance_id = p.instance_id();
invoke = Some(p.inner.invoke_fn);
}
}
});
}
if invoke.is_none() {
crate::jit::rt::with_legacy_vm_args(|args| {
for v in args.iter() {
if let crate::backend::vm::VMValue::BoxRef(b) = v {
if let Some(p) = b.as_any().downcast_ref::<PluginBoxV2>() {
if p.inner.type_id == (type_id as u32) || invoke.is_none() {
instance_id = p.instance_id();
invoke = Some(p.inner.invoke_fn);
if p.inner.type_id == (type_id as u32) { break; }
}
}
}
}
});
}
if invoke.is_none() { return 0.0; }
// Build TLV args from a1/a2 if present (i64 only for now)
let mut buf = crate::runtime::plugin_ffi_common::encode_tlv_header((argc.saturating_sub(1).max(0) as u16));
let mut add_i64 = |v: i64| { crate::runtime::plugin_ffi_common::encode::i64(&mut buf, v); };
if argc >= 2 { add_i64(a1); }
if argc >= 3 { add_i64(a2); }
// Prepare output buffer with canaries
let mut out = vec![0xCDu8; 4096 + 32];
let canary_val = 0xABu8;
let canary_len = 16usize;
for i in 0..canary_len { out[i] = canary_val; }
for i in 0..canary_len { out[4096 + canary_len + i] = canary_val; }
let mut out_len: usize = 4096;
let out_ptr = unsafe { out.as_mut_ptr().add(canary_len) };
if trace { eprintln!("[JIT-SHIM f64] invoke type={} method={} argc={} inst_id={} a1={} a2={} buf_len={}", type_id, method_id, argc, instance_id, a1, a2, buf.len()); }
crate::jit::shim_trace::push(format!("f64.start type={} method={} argc={} inst={} a1={} a2={}", type_id, method_id, argc, instance_id, a1, a2));
let rc = unsafe { invoke.unwrap()(type_id as u32, method_id as u32, instance_id, buf.as_ptr(), buf.len(), out_ptr, &mut out_len) };
let pre_ok = out[..canary_len].iter().all(|&b| b==canary_val);
let post_ok = out[canary_len + out_len .. canary_len + out_len + canary_len].iter().all(|&b| b==canary_val);
if trace { eprintln!("[JIT-SHIM f64] rc={} out_len={} canary_pre={} canary_post={}", rc, out_len, pre_ok, post_ok); }
crate::jit::shim_trace::push(format!("f64.end rc={} out_len={} pre_ok={} post_ok={}", rc, out_len, pre_ok, post_ok));
if rc != 0 { return 0.0; }
let out_slice = unsafe { std::slice::from_raw_parts(out_ptr, out_len) };
if let Some((tag, sz, payload)) = crate::runtime::plugin_ffi_common::decode::tlv_first(out_slice) {
if trace { eprintln!("[JIT-SHIM f64] TLV tag={} sz={}", tag, sz); }
crate::jit::shim_trace::push(format!("f64.tlv tag={} sz={}", tag, sz));
match tag {
5 => { // F64
if sz == 8 { let mut b=[0u8;8]; b.copy_from_slice(payload); return f64::from_le_bytes(b); }
}
3 => { // I64 → f64
if payload.len() == 8 { let mut b=[0u8;8]; b.copy_from_slice(payload); return (i64::from_le_bytes(b)) as f64; }
if let Some(v) = crate::runtime::plugin_ffi_common::decode::i32(payload) { return (v as i64) as f64; }
}
2 => { // I32 → f64
if let Some(v) = crate::runtime::plugin_ffi_common::decode::i32(payload) { return (v as i64) as f64; }
}
1 => { // Bool → 0.0/1.0
return if crate::runtime::plugin_ffi_common::decode::bool(payload).unwrap_or(false) { 1.0 } else { 0.0 };
}
_ => {}
}
}
0.0
}
#[cfg(feature = "cranelift-jit")]
use super::extern_thunks::{
nyash_math_sin_f64, nyash_math_cos_f64, nyash_math_abs_f64, nyash_math_min_f64, nyash_math_max_f64,
@ -752,13 +876,22 @@ impl IRBuilder for CraneliftBuilder {
z
}); }
// Build signature: (i64 type_id, i64 method_id, i64 argc, i64 a0, i64 a1, i64 a2) -> i64
// Choose f64 shim if allowlisted
let use_f64 = if has_ret {
if let Ok(list) = std::env::var("NYASH_JIT_PLUGIN_F64") {
list.split(',').any(|e| {
let mut it = e.split(':');
match (it.next(), it.next()) { (Some(t), Some(m)) => t.parse::<u32>().ok()==Some(type_id) && m.parse::<u32>().ok()==Some(method_id), _ => false }
})
} else { false }
} else { false };
// Build signature: (i64 type_id, i64 method_id, i64 argc, i64 a0, i64 a1, i64 a2) -> i64/f64
let call_conv = self.module.isa().default_call_conv();
let mut sig = Signature::new(call_conv);
for _ in 0..6 { sig.params.push(AbiParam::new(types::I64)); }
if has_ret { sig.returns.push(AbiParam::new(types::I64)); }
if has_ret { sig.returns.push(AbiParam::new(if use_f64 { types::F64 } else { types::I64 })); }
let symbol = "nyash_plugin_invoke3_i64";
let symbol = if use_f64 { "nyash_plugin_invoke3_f64" } else { "nyash_plugin_invoke3_i64" };
let func_id = self.module
.declare_function(symbol, Linkage::Import, &sig)
.expect("declare plugin shim failed");
@ -770,6 +903,7 @@ impl IRBuilder for CraneliftBuilder {
let c_type = fb.ins().iconst(types::I64, type_id as i64);
let c_meth = fb.ins().iconst(types::I64, method_id as i64);
let c_argc = fb.ins().iconst(types::I64, argc as i64);
// Pass receiver param index (a0) when known; shim will fallback-scan if invalid (<0)
let call_inst = fb.ins().call(fref, &[c_type, c_meth, c_argc, arg_vals[0], arg_vals[1], arg_vals[2]]);
if has_ret {
let results = fb.inst_results(call_inst).to_vec();
@ -1235,8 +1369,18 @@ impl IRBuilder for ObjectBuilder {
use cranelift_codegen::ir::{AbiParam, Signature, types}; use cranelift_frontend::FunctionBuilder; use cranelift_module::{Linkage, Module};
let mut arg_vals: Vec<cranelift_codegen::ir::Value> = Vec::new(); let take_n = argc.min(self.value_stack.len()); for _ in 0..take_n { if let Some(v) = self.value_stack.pop() { arg_vals.push(v); } } arg_vals.reverse();
while arg_vals.len() < 3 { let mut fb = FunctionBuilder::new(&mut self.ctx.func, &mut self.fbc); if let Some(idx) = self.current_block_index { fb.switch_to_block(self.blocks[idx]); } else if let Some(b) = self.entry_block { fb.switch_to_block(b); } let z = fb.ins().iconst(types::I64, 0); fb.finalize(); arg_vals.push(z); }
let call_conv = self.module.isa().default_call_conv(); let mut sig = Signature::new(call_conv); for _ in 0..6 { sig.params.push(AbiParam::new(types::I64)); } if has_ret { sig.returns.push(AbiParam::new(types::I64)); }
let symbol = "nyash_plugin_invoke3_i64"; let func_id = self.module.declare_function(symbol, Linkage::Import, &sig).expect("declare plugin shim");
// Choose f64 or i64 shim based on env allowlist: NYASH_JIT_PLUGIN_F64="type:method,type:method"
let use_f64 = if has_ret {
if let Ok(list) = std::env::var("NYASH_JIT_PLUGIN_F64") {
list.split(',').any(|e| {
let mut it = e.split(':');
match (it.next(), it.next()) { (Some(t), Some(m)) => t.parse::<u32>().ok()==Some(type_id) && m.parse::<u32>().ok()==Some(method_id), _ => false }
})
} else { false }
} else { false };
let call_conv = self.module.isa().default_call_conv(); let mut sig = Signature::new(call_conv); for _ in 0..6 { sig.params.push(AbiParam::new(types::I64)); } if has_ret { if use_f64 { sig.returns.push(AbiParam::new(types::F64)); } else { sig.returns.push(AbiParam::new(types::I64)); } }
let symbol = if use_f64 { "nyash_plugin_invoke3_f64" } else { "nyash_plugin_invoke3_i64" };
let func_id = self.module.declare_function(symbol, Linkage::Import, &sig).expect("declare plugin shim");
let mut fb = FunctionBuilder::new(&mut self.ctx.func, &mut self.fbc); if let Some(idx) = self.current_block_index { fb.switch_to_block(self.blocks[idx]); } else if let Some(b) = self.entry_block { fb.switch_to_block(b); }
let fref = self.module.declare_func_in_func(func_id, fb.func);
let c_type = fb.ins().iconst(types::I64, type_id as i64); let c_meth = fb.ins().iconst(types::I64, method_id as i64); let c_argc = fb.ins().iconst(types::I64, argc as i64);

View File

@ -472,7 +472,8 @@ impl LowerCore {
| I::Store { .. }
| I::Load { .. }
| I::Phi { .. }
| I::Print { .. }
// PrintはJIT経路では未対応VMにフォールバックしてコンソール出力を保持
// | I::Print { .. }
| I::Debug { .. }
| I::ExternCall { .. }
| I::Safepoint
@ -492,13 +493,18 @@ impl LowerCore {
"StringBox" => {
// Emit host-call to create a new StringBox handle; push as i64
b.emit_host_call(crate::jit::r#extern::collections::SYM_STRING_BIRTH_H, 0, true);
// Do not attempt to classify; downstream ops will treat as handle
}
"IntegerBox" => {
b.emit_host_call(crate::jit::r#extern::collections::SYM_INTEGER_BIRTH_H, 0, true);
}
_ => { /* Other boxes: no-op for now */ }
_ => {
// Any other NewBox (e.g., ArrayBox/MapBox/etc.) is UNSUPPORTED in JIT for now
self.unsupported += 1;
}
}
} else {
// NewBox with args or NYASH_USE_PLUGIN_BUILTINS!=1 → unsupported in JIT
self.unsupported += 1;
}
// Track boxed numeric literals to aid signature checks (FloatBox/IntegerBox)
if box_type == "FloatBox" {

View File

@ -11,3 +11,4 @@ pub mod policy;
pub mod events;
pub mod hostcall_registry;
pub mod boundary;
pub mod shim_trace;

39
src/jit/shim_trace.rs Normal file
View File

@ -0,0 +1,39 @@
use once_cell::sync::Lazy;
use std::collections::VecDeque;
use std::sync::Mutex;
use std::sync::atomic::{AtomicBool, Ordering};
static TRACE_ENABLED: AtomicBool = AtomicBool::new(false);
static EVENTS: Lazy<Mutex<VecDeque<String>>> = Lazy::new(|| Mutex::new(VecDeque::with_capacity(256)));
const MAX_EVENTS: usize = 256;
pub fn set_enabled(on: bool) { TRACE_ENABLED.store(on, Ordering::Relaxed); }
pub fn is_enabled() -> bool {
if TRACE_ENABLED.load(Ordering::Relaxed) { return true; }
std::env::var("NYASH_JIT_SHIM_TRACE").ok().as_deref() == Some("1")
}
pub fn push(event: String) {
if !is_enabled() { return; }
if let Ok(mut q) = EVENTS.lock() {
if q.len() >= MAX_EVENTS { q.pop_front(); }
q.push_back(event);
}
}
pub fn snapshot_joined() -> String {
if let Ok(q) = EVENTS.lock() {
let mut out = String::new();
for (i, e) in q.iter().enumerate() {
if i > 0 { out.push('\n'); }
out.push_str(e);
}
out
} else { String::new() }
}
pub fn clear() {
if let Ok(mut q) = EVENTS.lock() { q.clear(); }
}