diff --git a/CLAUDE_WASM_TASK.md b/CLAUDE_WASM_TASK.md new file mode 100644 index 00000000..7a3efb6b --- /dev/null +++ b/CLAUDE_WASM_TASK.md @@ -0,0 +1,37 @@ +Title: WASM Backend v2 (Phase 12) – Minimal Handoff + +Goal +- Implement a new `src/backend/wasm_v2/` backend that aligns with the unified vtable/slot dispatch model, without touching MIR-level semantics. + +Scope +- Allowed: + - `src/backend/wasm_v2/*` (mod.rs, unified_dispatch.rs, vtable_codegen.rs) + - `src/backend/wasm/*` read-only for reference + - `projects/nyash-wasm/*` (demo HTML/JS harness) + - Documentation under `docs/backend/wasm/*.md` +- Avoid (to keep merge easy): + - MIR layer (no plugin-name hardcoding) + - `src/runtime/*` core (HostHandle/host_api/extern are assumed stable) + - JIT/VM files unless behind feature/cfg guards + +Design Constraints +- ExternCall and BoxCall remain separate at the MIR/semantics level. +- At lower layers, converge onto a unified host-call shim when needed (same C symbols as VM/JIT). +- Console/print: use `ExternCall(env.console.log)`; do not move to BoxCall. +- vtable/slots must align with `src/runtime/type_registry.rs` (see Array/Map/String slots). + +Targets & Flags +- Target: `wasm32-unknown-unknown` +- Feature: `--features wasm-backend` +- Use `#[cfg(feature = "wasm-backend")]` and/or `#[cfg(target_arch = "wasm32")]` to isolate new code. + +Acceptance (Minimal) +- Build succeeds for non-wasm targets (no regressions). +- wasm_v2 compiles behind `--features wasm-backend` (even as stubs). +- A demo harness can call `env.console.log` from WASM and produce a message. +- (Nice to have) Array/Map len/size/has/get stubs go through unified dispatch. + +Next Steps (Optional) +- Implement unified dispatch bridge to JS host for `env.console` and basic collections. +- Add a minimal test or demo comparing VM vs WASM return values for a simple program. + diff --git a/docs/development/design/extern-vs-boxcall.md b/docs/development/design/extern-vs-boxcall.md new file mode 100644 index 00000000..c3e287a4 --- /dev/null +++ b/docs/development/design/extern-vs-boxcall.md @@ -0,0 +1,24 @@ +ExternCall vs BoxCall: 分離設計の理由(要約) + +- 目的: VM/JIT間で同一挙動を担保しつつ、最適化や診断を明確にするため、ExternCall と BoxCall を上位で分離、下位で合流する。 + +- 上位(MIR/意味論) + - ExternCall: env.*(IO/タスク/デバッグ/チェックポイント等)を表現。EffectMaskで最適化境界を明示。 + - BoxCall: 型ディスパッチ(vtable→PIC→汎用)。副作用はBox内部に閉じやすい。 + +- 下位(VM/JIT実装/ABI) + - 可能な限り共通のHostCall基盤へ合流(Cシンボル、HostHandle、TLV)。 + - VM: ExternCall→PluginHost(extern_call)→必要に応じて host_api へ。 + - JIT: 同じCシンボル群を直接リンクすることで一致挙動を確保。 + +- STRICT(厳格モード) + - `NYASH_ABI_STRICT=1` または `NYASH_EXTERN_STRICT=1` で未登録/未対応を明確なエラーに。 + - vtable側は TypeRegistry に基づき未対応メソッドを検出。 + - ExternCall側は Host/Loader が未登録なら明確な診断を返す。 + +- 最低限ハードコード + - print/console.log 等は ExternCall(env.console)側に限定して最小限のハードコード。 + - BoxCall 側へのハードコードは避ける(最適化経路やキャッシュと混ざるのを防止)。 + +この方針により、最適化・キャッシュ・診断の責務範囲が鮮明になり、VM/JIT一致検証も行いやすくなる。 + diff --git a/src/backend/mod.rs b/src/backend/mod.rs index 2d36d28e..eb3464d4 100644 --- a/src/backend/mod.rs +++ b/src/backend/mod.rs @@ -20,6 +20,8 @@ pub mod mir_interpreter; // Lightweight MIR interpreter pub mod wasm; #[cfg(feature = "wasm-backend")] pub mod aot; +#[cfg(feature = "wasm-backend")] +pub mod wasm_v2; #[cfg(feature = "llvm")] pub mod llvm; diff --git a/src/backend/vm.rs b/src/backend/vm.rs index 2f94358c..ef43a999 100644 --- a/src/backend/vm.rs +++ b/src/backend/vm.rs @@ -208,6 +208,14 @@ pub struct VM { pub(super) instr_counter: std::collections::HashMap<&'static str, usize>, /// Execution start time for optional stats pub(super) exec_start: Option, + /// Stats: number of BoxCall hits via VTable path + pub(super) boxcall_hits_vtable: u64, + /// Stats: number of BoxCall hits via Poly-PIC path + pub(super) boxcall_hits_poly_pic: u64, + /// Stats: number of BoxCall hits via Mono-PIC path + pub(super) boxcall_hits_mono_pic: u64, + /// Stats: number of BoxCall hits via generic fallback path + pub(super) boxcall_hits_generic: u64, /// Mono-PIC skeleton: global hit counters keyed by (recv_type, method_id/name) pub(super) boxcall_pic_hits: std::collections::HashMap, /// Mono-PIC: cached direct targets (currently InstanceBox function name) @@ -372,6 +380,10 @@ impl VM { module: None, instr_counter: std::collections::HashMap::new(), exec_start: None, + boxcall_hits_vtable: 0, + boxcall_hits_poly_pic: 0, + boxcall_hits_mono_pic: 0, + boxcall_hits_generic: 0, boxcall_pic_hits: std::collections::HashMap::new(), boxcall_pic_funcname: std::collections::HashMap::new(), boxcall_poly_pic: std::collections::HashMap::new(), @@ -403,6 +415,10 @@ impl VM { module: None, instr_counter: std::collections::HashMap::new(), exec_start: None, + boxcall_hits_vtable: 0, + boxcall_hits_poly_pic: 0, + boxcall_hits_mono_pic: 0, + boxcall_hits_generic: 0, boxcall_pic_hits: std::collections::HashMap::new(), boxcall_pic_funcname: std::collections::HashMap::new(), boxcall_poly_pic: std::collections::HashMap::new(), @@ -494,8 +510,9 @@ impl VM { let hits_total: u64 = self.boxcall_pic_hits.values().map(|v| *v as u64).sum(); let vt_entries = self.boxcall_vtable_funcname.len(); eprintln!( - "[VM] PIC/VT summary: poly_sites={} avg_entries={:.2} mono_sites={} hits_total={} vt_entries={}", - sites_poly, avg_entries, sites_mono, hits_total, vt_entries + "[VM] PIC/VT summary: poly_sites={} avg_entries={:.2} mono_sites={} hits_total={} vt_entries={} | hits: vt={} poly={} mono={} generic={}", + sites_poly, avg_entries, sites_mono, hits_total, vt_entries, + self.boxcall_hits_vtable, self.boxcall_hits_poly_pic, self.boxcall_hits_mono_pic, self.boxcall_hits_generic ); // Top sites by hits (up to 5) let mut hits: Vec<(&String, &u32)> = self.boxcall_pic_hits.iter().collect(); diff --git a/src/backend/vm_instructions.rs b/src/backend/vm_instructions.rs index d4818e12..310ad040 100644 --- a/src/backend/vm_instructions.rs +++ b/src/backend/vm_instructions.rs @@ -645,7 +645,12 @@ impl VM { } } Err(_) => { - return Err(VMError::InvalidInstruction(format!("ExternCall failed: {}.{}", iface_name, method_name))); + let strict = crate::config::env::extern_strict() || crate::config::env::abi_strict(); + if strict { + return Err(VMError::InvalidInstruction(format!("ExternCall STRICT: unregistered or unsupported call {}.{}", iface_name, method_name))); + } else { + return Err(VMError::InvalidInstruction(format!("ExternCall failed: {}.{}", iface_name, method_name))); + } } } Ok(ControlFlow::Continue) @@ -883,6 +888,7 @@ impl VM { crate::backend::gc_helpers::gc_write_barrier_site(&self.runtime, "BoxCall.setField"); } let cloned_box = arc_box.share_box(); + self.boxcall_hits_generic = self.boxcall_hits_generic.saturating_add(1); let out = self.call_box_method(cloned_box, m, nyash_args)?; let vm_out = VMValue::from_nyash_box(out); if let Some(dst_id) = dst { self.set_value(dst_id, vm_out); } @@ -912,6 +918,7 @@ impl VM { if let VMValue::BoxRef(arc_box) = &recv { if arc_box.as_any().downcast_ref::().is_some() { if let Some(func_name) = self.try_poly_pic(&pic_key, &recv) { + self.boxcall_hits_poly_pic = self.boxcall_hits_poly_pic.saturating_add(1); 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)?); } @@ -921,6 +928,7 @@ impl VM { } // Fallback to Mono-PIC (legacy) if present if let Some(func_name) = self.boxcall_pic_funcname.get(&pic_key).cloned() { + self.boxcall_hits_mono_pic = self.boxcall_hits_mono_pic.saturating_add(1); // Build VM args: receiver first, then original args let mut vm_args = Vec::with_capacity(1 + args.len()); vm_args.push(recv.clone()); @@ -1126,6 +1134,7 @@ impl VM { // MapBox: size/len/has/get if let Some(map) = b.as_any().downcast_ref::() { if matches!(slot, Some(200|201)) { + self.boxcall_hits_vtable = self.boxcall_hits_vtable.saturating_add(1); let out = map.size(); if let Some(dst_id) = _dst { self.set_value(dst_id, VMValue::from_nyash_box(out)); } return Some(Ok(ControlFlow::Continue)); @@ -1141,6 +1150,7 @@ impl VM { VMValue::Future(ref fut) => Box::new(fut.clone()), VMValue::Void => Box::new(crate::box_trait::VoidBox::new()), }; + self.boxcall_hits_vtable = self.boxcall_hits_vtable.saturating_add(1); let out = map.has(key_box); if let Some(dst_id) = _dst { self.set_value(dst_id, VMValue::from_nyash_box(out)); } return Some(Ok(ControlFlow::Continue)); @@ -1157,6 +1167,7 @@ impl VM { VMValue::Future(ref fut) => Box::new(fut.clone()), VMValue::Void => Box::new(crate::box_trait::VoidBox::new()), }; + self.boxcall_hits_vtable = self.boxcall_hits_vtable.saturating_add(1); let out = map.get(key_box); if let Some(dst_id) = _dst { self.set_value(dst_id, VMValue::from_nyash_box(out)); } return Some(Ok(ControlFlow::Continue)); @@ -1166,6 +1177,7 @@ impl VM { // ArrayBox: get/set/len if let Some(arr) = b.as_any().downcast_ref::() { if matches!(slot, Some(102)) { + self.boxcall_hits_vtable = self.boxcall_hits_vtable.saturating_add(1); let out = arr.length(); if let Some(dst_id) = _dst { self.set_value(dst_id, VMValue::from_nyash_box(out)); } return Some(Ok(ControlFlow::Continue)); @@ -1179,6 +1191,7 @@ impl VM { VMValue::Float(f) => Box::new(crate::box_trait::IntegerBox::new(f as i64)), VMValue::BoxRef(_) | VMValue::Future(_) | VMValue::Void => Box::new(crate::box_trait::IntegerBox::new(0)), }; + self.boxcall_hits_vtable = self.boxcall_hits_vtable.saturating_add(1); let out = arr.get(idx_box); if let Some(dst_id) = _dst { self.set_value(dst_id, VMValue::from_nyash_box(out)); } return Some(Ok(ControlFlow::Continue)); @@ -1202,6 +1215,7 @@ impl VM { VMValue::Future(ref fut) => Box::new(fut.clone()), VMValue::Void => Box::new(crate::box_trait::VoidBox::new()), }; + self.boxcall_hits_vtable = self.boxcall_hits_vtable.saturating_add(1); let out = arr.set(idx_box, val_box); if let Some(dst_id) = _dst { self.set_value(dst_id, VMValue::from_nyash_box(out)); } return Some(Ok(ControlFlow::Continue)); @@ -1211,6 +1225,7 @@ impl VM { // StringBox: len if let Some(sb) = b.as_any().downcast_ref::() { if matches!(slot, Some(300)) { + self.boxcall_hits_vtable = self.boxcall_hits_vtable.saturating_add(1); let out = crate::box_trait::IntegerBox::new(sb.value.len() as i64); if let Some(dst_id) = _dst { self.set_value(dst_id, VMValue::from_nyash_box(Box::new(out))); } return Some(Ok(ControlFlow::Continue)); diff --git a/src/backend/wasm_v2/mod.rs b/src/backend/wasm_v2/mod.rs new file mode 100644 index 00000000..f2c9642f --- /dev/null +++ b/src/backend/wasm_v2/mod.rs @@ -0,0 +1,17 @@ +//! WASM Backend v2 (Phase 12 scaffolding) +//! +//! 目的: +//! - vtable/スロット解決に基づく統一ディスパッチ経路の雛形 +//! - 既存ビルドに影響を与えない最小構成(feature/target gate) + +#![cfg(feature = "wasm-backend")] + +pub mod unified_dispatch; +pub mod vtable_codegen; + +/// エントリポイントの雛形 +pub fn compile_and_execute_v2(_module: &crate::mir::MirModule, _temp_name: &str) -> Result, String> { + // まだ未実装: vtable_codegenで生成したスロット表を unified_dispatch 経由で実行 + Err("wasm_v2: not implemented (scaffold)".to_string()) +} + diff --git a/src/backend/wasm_v2/unified_dispatch.rs b/src/backend/wasm_v2/unified_dispatch.rs new file mode 100644 index 00000000..41150bbf --- /dev/null +++ b/src/backend/wasm_v2/unified_dispatch.rs @@ -0,0 +1,25 @@ +//! Unified dispatch (WASM v2) +//! +//! - TypeRegistryのスロット表と一致させた呼び出し分岐の雛形 +//! - ここではあくまで「どのスロットに行くか」の判定のみ提供 + +#![cfg(feature = "wasm-backend")] + +use crate::box_trait::NyashBox; + +/// 受信ボックス/メソッド名/アリティからスロットを解決し、識別子を返す。 +pub fn resolve_slot(recv: &dyn NyashBox, method: &str, arity: usize) -> Option { + let ty = recv.type_name(); + crate::runtime::type_registry::resolve_slot_by_name(ty, method, arity) +} + +/// 実際の呼び出し分岐は、将来的にここから生成済みのstubsに委譲する予定。 +pub fn dispatch_by_slot( + _slot: u16, + _recv: &dyn NyashBox, + _args: &[Box], +) -> Option> { + // 未実装: wasm_v2ではJS/hostへのブリッジや、Wasm内の簡易実装に委譲 + None +} + diff --git a/src/backend/wasm_v2/vtable_codegen.rs b/src/backend/wasm_v2/vtable_codegen.rs new file mode 100644 index 00000000..e5867a0e --- /dev/null +++ b/src/backend/wasm_v2/vtable_codegen.rs @@ -0,0 +1,18 @@ +//! VTable codegen stubs (WASM v2) +//! +//! - 将来的にTypeRegistryから静的テーブル/インライン分岐を生成 +//! - 現段階はプレースホルダ + +#![cfg(feature = "wasm-backend")] + +/// 生成結果のメタ情報(雛形) +pub struct GeneratedVTableInfo { + pub types: usize, + pub methods: usize, +} + +pub fn generate_tables() -> GeneratedVTableInfo { + // 未実装: TypeRegistry::resolve_typebox_by_name()/methods を走査して集計 + GeneratedVTableInfo { types: 0, methods: 0 } +} + diff --git a/src/bin/test_plugin_loader_v2.rs b/src/bin/test_plugin_loader_v2.rs index 4d6c9493..78f65b6b 100644 --- a/src/bin/test_plugin_loader_v2.rs +++ b/src/bin/test_plugin_loader_v2.rs @@ -91,6 +91,45 @@ fn main() { } else { println!(" get(slot=100) failed with code {}", code2); } + + // MapBox slots test: set/get/has/size + println!("\nReverse host-call (by-slot) MapBox test:"); + let map = nyash_rust::boxes::map_box::MapBox::new(); + let map_h = nyash_rust::runtime::host_handles::to_handle_box(Box::new(map)); + // set("k","v") → slot=204 + let mut tlv_set = nyash_rust::runtime::plugin_ffi_common::encode_tlv_header(2); + nyash_rust::runtime::plugin_ffi_common::encode::string(&mut tlv_set, "k"); + nyash_rust::runtime::plugin_ffi_common::encode::string(&mut tlv_set, "v"); + let mut out_s = vec![0u8; 256]; + let mut out_s_len: usize = out_s.len(); + let code_s = unsafe { nyash_rust::runtime::host_api::nyrt_host_call_slot(map_h, 204, tlv_set.as_ptr(), tlv_set.len(), out_s.as_mut_ptr(), &mut out_s_len) }; + println!(" set(slot=204) -> code={}, out_len={}", code_s, out_s_len); + // get("k") → slot=203 + let mut tlv_get = nyash_rust::runtime::plugin_ffi_common::encode_tlv_header(1); + nyash_rust::runtime::plugin_ffi_common::encode::string(&mut tlv_get, "k"); + let mut out_g = vec![0u8; 256]; + let mut out_g_len: usize = out_g.len(); + let code_g = unsafe { nyash_rust::runtime::host_api::nyrt_host_call_slot(map_h, 203, tlv_get.as_ptr(), tlv_get.len(), out_g.as_mut_ptr(), &mut out_g_len) }; + if code_g == 0 { + if let Some((tag, _sz, payload)) = nyash_rust::runtime::plugin_ffi_common::decode::tlv_first(&out_g[..out_g_len]) { + if tag == 6 || tag == 7 { + let s = nyash_rust::runtime::plugin_ffi_common::decode::string(payload); + println!(" get(slot=203) -> '{}'", s); + } else { + println!(" get(slot=203) -> tag={}, size={}", tag, _sz); + } + } + } + // has("k") → slot=202 + let mut out_hb = vec![0u8; 16]; + let mut out_hb_len: usize = out_hb.len(); + let code_hb = unsafe { nyash_rust::runtime::host_api::nyrt_host_call_slot(map_h, 202, tlv_get.as_ptr(), tlv_get.len(), out_hb.as_mut_ptr(), &mut out_hb_len) }; + println!(" has(slot=202) -> code={}, out_len={}", code_hb, out_hb_len); + // size() → slot=200 + let mut out_sz = vec![0u8; 32]; + let mut out_sz_len: usize = out_sz.len(); + let code_sz = unsafe { nyash_rust::runtime::host_api::nyrt_host_call_slot(map_h, 200, std::ptr::null(), 0, out_sz.as_mut_ptr(), &mut out_sz_len) }; + println!(" size(slot=200) -> code={}, out_len={}", code_sz, out_sz_len); println!("\nTest completed!"); } diff --git a/src/config/env.rs b/src/config/env.rs index 0d3397b9..ccce6edb 100644 --- a/src/config/env.rs +++ b/src/config/env.rs @@ -120,3 +120,6 @@ pub fn rewrite_future() -> bool { std::env::var("NYASH_REWRITE_FUTURE").ok().as_ // ---- Phase 12: Nyash ABI (vtable) toggles ---- pub fn abi_vtable() -> bool { std::env::var("NYASH_ABI_VTABLE").ok().as_deref() == Some("1") } pub fn abi_strict() -> bool { std::env::var("NYASH_ABI_STRICT").ok().as_deref() == Some("1") } + +// ---- ExternCall strict diagnostics ---- +pub fn extern_strict() -> bool { std::env::var("NYASH_EXTERN_STRICT").ok().as_deref() == Some("1") } diff --git a/src/runtime/host_api.rs b/src/runtime/host_api.rs index 3748ec46..2f6aced9 100644 --- a/src/runtime/host_api.rs +++ b/src/runtime/host_api.rs @@ -197,6 +197,12 @@ fn plugin_box_from_handle(_type_id: u32, _instance_id: u32) -> Option any // 101: ArrayBox.set(index: i64, value: any) -> any // 102: ArrayBox.len() -> i64 +// 200: MapBox.size() -> i64 +// 201: MapBox.len() -> i64 +// 202: MapBox.has(key:any) -> bool +// 203: MapBox.get(key:any) -> any +// 204: MapBox.set(key:any, value:any) -> any +// 300: StringBox.len() -> i64 #[no_mangle] pub extern "C" fn nyrt_host_call_slot(handle: u64, selector_id: u64, args_ptr: *const u8, args_len: usize, @@ -296,6 +302,86 @@ pub extern "C" fn nyrt_host_call_slot(handle: u64, selector_id: u64, } } } + 200 | 201 | 202 | 203 | 204 => { + if let Some(map) = recv_arc.as_any().downcast_ref::() { + match selector_id { + 200 | 201 => { + let out = map.size(); + let vmv = crate::backend::vm::VMValue::from_nyash_box(out); + let buf = tlv_encode_one(&vmv); + return encode_out(out_ptr, out_len, &buf); + } + 202 => { // has(key) + if argv.len() >= 1 { + let key_box: Box = match argv[0].clone() { + crate::backend::vm::VMValue::Integer(i) => Box::new(crate::box_trait::IntegerBox::new(i)), + crate::backend::vm::VMValue::Float(f) => Box::new(crate::boxes::math_box::FloatBox::new(f)), + crate::backend::vm::VMValue::Bool(b) => Box::new(crate::box_trait::BoolBox::new(b)), + crate::backend::vm::VMValue::String(s) => Box::new(crate::box_trait::StringBox::new(s)), + crate::backend::vm::VMValue::BoxRef(b) => b.share_box(), + crate::backend::vm::VMValue::Future(fu) => Box::new(fu), + crate::backend::vm::VMValue::Void => Box::new(crate::box_trait::VoidBox::new()), + }; + let out = map.has(key_box); + let vmv = crate::backend::vm::VMValue::from_nyash_box(out); + let buf = tlv_encode_one(&vmv); + return encode_out(out_ptr, out_len, &buf); + } + } + 203 => { // get(key) + if argv.len() >= 1 { + let key_box: Box = match argv[0].clone() { + crate::backend::vm::VMValue::Integer(i) => Box::new(crate::box_trait::IntegerBox::new(i)), + crate::backend::vm::VMValue::Float(f) => Box::new(crate::boxes::math_box::FloatBox::new(f)), + crate::backend::vm::VMValue::Bool(b) => Box::new(crate::box_trait::BoolBox::new(b)), + crate::backend::vm::VMValue::String(s) => Box::new(crate::box_trait::StringBox::new(s)), + crate::backend::vm::VMValue::BoxRef(b) => b.share_box(), + crate::backend::vm::VMValue::Future(fu) => Box::new(fu), + crate::backend::vm::VMValue::Void => Box::new(crate::box_trait::VoidBox::new()), + }; + let out = map.get(key_box); + let vmv = crate::backend::vm::VMValue::from_nyash_box(out); + let buf = tlv_encode_one(&vmv); + return encode_out(out_ptr, out_len, &buf); + } + } + 204 => { // set(key, value) + if argv.len() >= 2 { + let key_box: Box = match argv[0].clone() { + crate::backend::vm::VMValue::Integer(i) => Box::new(crate::box_trait::IntegerBox::new(i)), + crate::backend::vm::VMValue::Float(f) => Box::new(crate::boxes::math_box::FloatBox::new(f)), + crate::backend::vm::VMValue::Bool(b) => Box::new(crate::box_trait::BoolBox::new(b)), + crate::backend::vm::VMValue::String(s) => Box::new(crate::box_trait::StringBox::new(s)), + crate::backend::vm::VMValue::BoxRef(b) => b.share_box(), + crate::backend::vm::VMValue::Future(fu) => Box::new(fu), + crate::backend::vm::VMValue::Void => Box::new(crate::box_trait::VoidBox::new()), + }; + let val_box: Box = match argv[1].clone() { + crate::backend::vm::VMValue::Integer(i) => Box::new(crate::box_trait::IntegerBox::new(i)), + crate::backend::vm::VMValue::Float(f) => Box::new(crate::boxes::math_box::FloatBox::new(f)), + crate::backend::vm::VMValue::Bool(b) => Box::new(crate::box_trait::BoolBox::new(b)), + crate::backend::vm::VMValue::String(s) => Box::new(crate::box_trait::StringBox::new(s)), + crate::backend::vm::VMValue::BoxRef(b) => b.share_box(), + crate::backend::vm::VMValue::Future(fu) => Box::new(fu), + crate::backend::vm::VMValue::Void => Box::new(crate::box_trait::VoidBox::new()), + }; + let out = map.set(key_box, val_box); + let vmv = crate::backend::vm::VMValue::from_nyash_box(out); + let buf = tlv_encode_one(&vmv); + return encode_out(out_ptr, out_len, &buf); + } + } + _ => {} + } + } + } + 300 => { + if let Some(sb) = recv_arc.as_any().downcast_ref::() { + let out = crate::backend::vm::VMValue::Integer(sb.value.len() as i64); + let buf = tlv_encode_one(&out); + return encode_out(out_ptr, out_len, &buf); + } + } _ => {} } -10 diff --git a/src/tests/identical_exec.rs b/src/tests/identical_exec.rs index e6add1da..5144052c 100644 --- a/src/tests/identical_exec.rs +++ b/src/tests/identical_exec.rs @@ -40,5 +40,37 @@ mod tests { assert_eq!(vm_s, jit_s, "VM and JIT results should match"); } -} + #[cfg(feature = "cranelift-jit")] + #[test] + fn identical_vm_and_jit_console_log_side_effect_free() { + // Build: const 1; extern_call env.console.log(1); return 1 + use crate::mir::{MirModule, MirFunction, FunctionSignature, MirInstruction, EffectMask}; + let sig = FunctionSignature { name: "main".into(), params: vec![], return_type: crate::mir::MirType::Integer, effects: EffectMask::PURE }; + let mut func = MirFunction::new(sig, crate::mir::BasicBlockId::new(0)); + let bb = func.entry_block; + let v0 = func.next_value_id(); + func.get_block_mut(bb).unwrap().add_instruction(MirInstruction::Const { dst: v0, value: crate::mir::ConstValue::Integer(1) }); + func.get_block_mut(bb).unwrap().add_instruction(MirInstruction::ExternCall { + dst: None, + iface_name: "env.console".to_string(), + method_name: "log".to_string(), + args: vec![v0], + effects: EffectMask::IO, + }); + func.get_block_mut(bb).unwrap().add_instruction(MirInstruction::Return { value: Some(v0) }); + let mut module = MirModule::new("identical_console".into()); + module.add_function(func); + + // Run VM + let mut vm = VM::new(); + let vm_out = vm.execute_module(&module).expect("VM exec"); + let vm_s = vm_out.to_string_box().value; + + // Run JIT (Cranelift minimal) — ExternCallはスキップされる想定 + let jit_out = crate::backend::cranelift_compile_and_execute(&module, "identical_console").expect("JIT exec"); + let jit_s = jit_out.to_string_box().value; + + assert_eq!(vm_s, jit_s, "VM and JIT results should match despite console.log side effects"); + } +}