From 294b45b9f4b61dcaf57ed21f9da0fbdcd94e6338 Mon Sep 17 00:00:00 2001 From: Moe Charm Date: Wed, 3 Sep 2025 05:56:57 +0900 Subject: [PATCH] Phase 12: extern registry + diagnostics, JIT host-bridge PoC by-slot, vtable Map.set + VT/PIC tracing, and tests --- src/backend/vm_instructions.rs | 63 ++++++++++++++++++++++++-- src/config/env.rs | 2 + src/jit/engine.rs | 11 +++++ src/jit/extern/host_bridge.rs | 83 ++++++++++++++++++++++++++++++++++ src/jit/extern/mod.rs | 1 + src/runtime/extern_registry.rs | 37 +++++++++++++++ src/runtime/mod.rs | 1 + src/tests/vtable_strict.rs | 40 ++++++++++++++++ 8 files changed, 234 insertions(+), 4 deletions(-) create mode 100644 src/jit/extern/host_bridge.rs create mode 100644 src/runtime/extern_registry.rs create mode 100644 src/tests/vtable_strict.rs diff --git a/src/backend/vm_instructions.rs b/src/backend/vm_instructions.rs index 310ad040..f39f216c 100644 --- a/src/backend/vm_instructions.rs +++ b/src/backend/vm_instructions.rs @@ -646,11 +646,22 @@ impl VM { } Err(_) => { 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))); + // Build suggestion list + let mut msg = String::new(); + if strict { msg.push_str("ExternCall STRICT: unregistered or unsupported call "); } else { msg.push_str("ExternCall failed: "); } + msg.push_str(&format!("{}.{}", iface_name, method_name)); + if let Some(spec) = crate::runtime::extern_registry::resolve(iface_name, method_name) { + msg.push_str(&format!(" (expected arity {}..{})", spec.min_arity, spec.max_arity)); } else { - return Err(VMError::InvalidInstruction(format!("ExternCall failed: {}.{}", iface_name, method_name))); + let known = crate::runtime::extern_registry::known_for_iface(iface_name); + if !known.is_empty() { + msg.push_str(&format!("; known methods: {}", known.join(", "))); + } else { + let ifaces = crate::runtime::extern_registry::all_ifaces(); + msg.push_str(&format!("; known interfaces: {}", ifaces.join(", "))); + } } + return Err(VMError::InvalidInstruction(msg)); } } Ok(ControlFlow::Continue) @@ -918,6 +929,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) { + if crate::config::env::vm_pic_trace() { eprintln!("[PIC] poly hit {}", pic_key); } 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()); @@ -928,6 +940,7 @@ impl VM { } // Fallback to Mono-PIC (legacy) if present if let Some(func_name) = self.boxcall_pic_funcname.get(&pic_key).cloned() { + if crate::config::env::vm_pic_trace() { eprintln!("[PIC] mono hit {}", pic_key); } 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()); @@ -1131,10 +1144,11 @@ impl VM { if let Some(_tb) = crate::runtime::type_registry::resolve_typebox_by_name(ty_name) { // name+arity→slot 解決 let slot = crate::runtime::type_registry::resolve_slot_by_name(ty_name, _method, _args.len()); - // MapBox: size/len/has/get + // MapBox: size/len/has/get/set 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); + if crate::config::env::vm_vt_trace() { eprintln!("[VT] MapBox.size/len slot={:?}", slot); } 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)); @@ -1151,6 +1165,8 @@ impl VM { VMValue::Void => Box::new(crate::box_trait::VoidBox::new()), }; self.boxcall_hits_vtable = self.boxcall_hits_vtable.saturating_add(1); + self.boxcall_hits_vtable = self.boxcall_hits_vtable.saturating_add(1); + if crate::config::env::vm_vt_trace() { eprintln!("[VT] MapBox.has"); } 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)); @@ -1168,16 +1184,48 @@ impl VM { VMValue::Void => Box::new(crate::box_trait::VoidBox::new()), }; self.boxcall_hits_vtable = self.boxcall_hits_vtable.saturating_add(1); + self.boxcall_hits_vtable = self.boxcall_hits_vtable.saturating_add(1); + if crate::config::env::vm_vt_trace() { eprintln!("[VT] MapBox.get"); } 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)); } } + if matches!(slot, Some(204)) { + if let (Ok(a0), Ok(a1)) = (self.get_value(_args[0]), self.get_value(_args[1])) { + let key_box: Box = match a0 { + VMValue::Integer(i) => Box::new(crate::box_trait::IntegerBox::new(i)), + VMValue::String(ref s) => Box::new(crate::box_trait::StringBox::new(s)), + VMValue::Bool(b) => Box::new(crate::box_trait::BoolBox::new(b)), + VMValue::Float(f) => Box::new(crate::boxes::math_box::FloatBox::new(f)), + VMValue::BoxRef(ref bx) => bx.share_box(), + VMValue::Future(ref fut) => Box::new(fut.clone()), + VMValue::Void => Box::new(crate::box_trait::VoidBox::new()), + }; + let val_box: Box = match a1 { + VMValue::Integer(i) => Box::new(crate::box_trait::IntegerBox::new(i)), + VMValue::String(ref s) => Box::new(crate::box_trait::StringBox::new(s)), + VMValue::Bool(b) => Box::new(crate::box_trait::BoolBox::new(b)), + VMValue::Float(f) => Box::new(crate::boxes::math_box::FloatBox::new(f)), + VMValue::BoxRef(ref bx) => bx.share_box(), + VMValue::Future(ref fut) => Box::new(fut.clone()), + VMValue::Void => Box::new(crate::box_trait::VoidBox::new()), + }; + // Barrier: mutation + crate::backend::gc_helpers::gc_write_barrier_site(&self.runtime, "VTable.Map.set"); + self.boxcall_hits_vtable = self.boxcall_hits_vtable.saturating_add(1); + if crate::config::env::vm_vt_trace() { eprintln!("[VT] MapBox.set"); } + let out = map.set(key_box, val_box); + if let Some(dst_id) = _dst { self.set_value(dst_id, VMValue::from_nyash_box(out)); } + return Some(Ok(ControlFlow::Continue)); + } + } } // 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); + if crate::config::env::vm_vt_trace() { eprintln!("[VT] ArrayBox.len"); } 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)); @@ -1192,6 +1240,8 @@ impl VM { 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); + self.boxcall_hits_vtable = self.boxcall_hits_vtable.saturating_add(1); + if crate::config::env::vm_vt_trace() { eprintln!("[VT] ArrayBox.get"); } 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)); @@ -1216,6 +1266,10 @@ impl VM { VMValue::Void => Box::new(crate::box_trait::VoidBox::new()), }; self.boxcall_hits_vtable = self.boxcall_hits_vtable.saturating_add(1); + // Barrier: mutation + crate::backend::gc_helpers::gc_write_barrier_site(&self.runtime, "VTable.Array.set"); + self.boxcall_hits_vtable = self.boxcall_hits_vtable.saturating_add(1); + if crate::config::env::vm_vt_trace() { eprintln!("[VT] ArrayBox.set"); } 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)); @@ -1226,6 +1280,7 @@ impl VM { 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); + if crate::config::env::vm_vt_trace() { eprintln!("[VT] StringBox.len"); } 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/config/env.rs b/src/config/env.rs index ccce6edb..3a021541 100644 --- a/src/config/env.rs +++ b/src/config/env.rs @@ -99,6 +99,8 @@ pub fn gc_trace() -> bool { std::env::var("NYASH_GC_TRACE").ok().as_deref() == S pub fn gc_barrier_trace() -> bool { std::env::var("NYASH_GC_BARRIER_TRACE").ok().as_deref() == Some("1") } pub fn runtime_checkpoint_trace() -> bool { std::env::var("NYASH_RUNTIME_CHECKPOINT_TRACE").ok().as_deref() == Some("1") } pub fn vm_pic_stats() -> bool { std::env::var("NYASH_VM_PIC_STATS").ok().as_deref() == Some("1") } +pub fn vm_vt_trace() -> bool { std::env::var("NYASH_VM_VT_TRACE").ok().as_deref() == Some("1") } +pub fn vm_pic_trace() -> bool { std::env::var("NYASH_VM_PIC_TRACE").ok().as_deref() == Some("1") } pub fn gc_barrier_strict() -> bool { std::env::var("NYASH_GC_BARRIER_STRICT").ok().as_deref() == Some("1") } /// Return 0 (off) to 3 (max) for `NYASH_GC_TRACE`. diff --git a/src/jit/engine.rs b/src/jit/engine.rs index fdab67ec..4684a0a8 100644 --- a/src/jit/engine.rs +++ b/src/jit/engine.rs @@ -206,6 +206,7 @@ impl JitEngine { /// Register built-in externs (collections) fn register_default_externs(&mut self) { use crate::jit::r#extern::collections as c; + use crate::jit::r#extern::host_bridge as hb; self.register_extern(c::SYM_ARRAY_LEN, Arc::new(|args| c::array_len(args))); self.register_extern(c::SYM_ARRAY_GET, Arc::new(|args| c::array_get(args))); self.register_extern(c::SYM_ARRAY_SET, Arc::new(|args| c::array_set(args))); @@ -213,6 +214,16 @@ impl JitEngine { self.register_extern(c::SYM_MAP_GET, Arc::new(|args| c::map_get(args))); self.register_extern(c::SYM_MAP_SET, Arc::new(|args| c::map_set(args))); self.register_extern(c::SYM_MAP_SIZE, Arc::new(|args| c::map_size(args))); + // Host-bridge variants (by-slot via C symbol). Guarded by env opt-in for now. + if std::env::var("NYASH_JIT_HOST_BRIDGE").ok().as_deref() == Some("1") { + self.register_extern(hb::SYM_HOST_ARRAY_LEN, Arc::new(|args| hb::array_len(args))); + self.register_extern(hb::SYM_HOST_ARRAY_GET, Arc::new(|args| hb::array_get(args))); + self.register_extern(hb::SYM_HOST_ARRAY_SET, Arc::new(|args| hb::array_set(args))); + self.register_extern(hb::SYM_HOST_MAP_SIZE, Arc::new(|args| hb::map_size(args))); + self.register_extern(hb::SYM_HOST_MAP_GET, Arc::new(|args| hb::map_get(args))); + self.register_extern(hb::SYM_HOST_MAP_SET, Arc::new(|args| hb::map_set(args))); + self.register_extern(hb::SYM_HOST_MAP_HAS, Arc::new(|args| hb::map_has(args))); + } } pub fn register_extern(&mut self, name: &str, f: Arc crate::backend::vm::VMValue + Send + Sync>) { diff --git a/src/jit/extern/host_bridge.rs b/src/jit/extern/host_bridge.rs new file mode 100644 index 00000000..bc14c867 --- /dev/null +++ b/src/jit/extern/host_bridge.rs @@ -0,0 +1,83 @@ +//! JIT externs bridging to NyRT host API (C symbols) via by-slot encoding. +//! +//! 目的: VM/JIT一致のため、JITからも host_api::nyrt_host_call_slot を使うPoC。 + +use crate::backend::vm::VMValue; + +fn tlv_encode_values(args: &[VMValue]) -> Vec { + use crate::runtime::plugin_ffi_common::encode as enc; + let mut buf = crate::runtime::plugin_ffi_common::encode_tlv_header(args.len() as u16); + for a in args { + match a { + VMValue::Integer(i) => enc::i64(&mut buf, *i), + VMValue::Float(f) => enc::f64(&mut buf, *f), + VMValue::Bool(b) => enc::bool(&mut buf, *b), + VMValue::String(s) => enc::string(&mut buf, s), + VMValue::BoxRef(_) | VMValue::Future(_) | VMValue::Void => enc::string(&mut buf, ""), + } + } + buf +} + +fn call_slot(handle: u64, slot: u64, argv: &[VMValue]) -> VMValue { + let tlv = tlv_encode_values(argv); + let mut out = vec![0u8; 256]; + let mut out_len: usize = out.len(); + let code = unsafe { crate::runtime::host_api::nyrt_host_call_slot(handle, slot, tlv.as_ptr(), tlv.len(), out.as_mut_ptr(), &mut out_len) }; + if code != 0 { return VMValue::Void; } + if let Some((tag, _sz, payload)) = crate::runtime::plugin_ffi_common::decode::tlv_first(&out[..out_len]) { + match tag { + 6|7 => VMValue::String(crate::runtime::plugin_ffi_common::decode::string(payload)), + 1 => crate::runtime::plugin_ffi_common::decode::bool(payload).map(VMValue::Bool).unwrap_or(VMValue::Void), + 2 => crate::runtime::plugin_ffi_common::decode::i32(payload).map(|v| VMValue::Integer(v as i64)).unwrap_or(VMValue::Void), + 3 => crate::runtime::plugin_ffi_common::decode::u64(payload).map(|v| VMValue::Integer(v as i64)).unwrap_or(VMValue::Void), + 5 => crate::runtime::plugin_ffi_common::decode::f64(payload).map(VMValue::Float).unwrap_or(VMValue::Void), + 9 => { + if let Some(h) = crate::runtime::plugin_ffi_common::decode::u64(payload) { + if let Some(arc) = crate::runtime::host_handles::get(h) { return VMValue::BoxRef(arc); } + } + VMValue::Void + } + _ => VMValue::Void, + } + } else { VMValue::Void } +} + +fn to_handle(recv: &VMValue) -> Option { + match recv { + VMValue::BoxRef(arc) => Some(crate::runtime::host_handles::to_handle_arc(arc.clone())), + _ => None, + } +} + +// Public bridge helpers (symbol strings align with collections for PoC) +pub const SYM_HOST_ARRAY_GET: &str = "nyash.host.array.get"; // (ArrayBox, i64) +pub const SYM_HOST_ARRAY_SET: &str = "nyash.host.array.set"; // (ArrayBox, i64, val) +pub const SYM_HOST_ARRAY_LEN: &str = "nyash.host.array.len"; // (ArrayBox) +pub const SYM_HOST_MAP_GET: &str = "nyash.host.map.get"; // (MapBox, key) +pub const SYM_HOST_MAP_SET: &str = "nyash.host.map.set"; // (MapBox, key, val) +pub const SYM_HOST_MAP_SIZE: &str = "nyash.host.map.size"; // (MapBox) +pub const SYM_HOST_MAP_HAS: &str = "nyash.host.map.has"; // (MapBox, key) + +pub fn array_get(args: &[VMValue]) -> VMValue { + if let Some(h) = to_handle(args.get(0).unwrap_or(&VMValue::Void)) { call_slot(h, 100, &args[1..]) } else { VMValue::Void } +} +pub fn array_set(args: &[VMValue]) -> VMValue { + if let Some(h) = to_handle(args.get(0).unwrap_or(&VMValue::Void)) { call_slot(h, 101, &args[1..]) } else { VMValue::Void } +} +pub fn array_len(args: &[VMValue]) -> VMValue { + if let Some(h) = to_handle(args.get(0).unwrap_or(&VMValue::Void)) { call_slot(h, 102, &[]) } else { VMValue::Integer(0) } +} +pub fn map_get(args: &[VMValue]) -> VMValue { + if let Some(h) = to_handle(args.get(0).unwrap_or(&VMValue::Void)) { call_slot(h, 203, &args[1..]) } else { VMValue::Void } +} +pub fn map_set(args: &[VMValue]) -> VMValue { + if let Some(h) = to_handle(args.get(0).unwrap_or(&VMValue::Void)) { call_slot(h, 204, &args[1..]) } else { VMValue::Void } +} +pub fn map_size(args: &[VMValue]) -> VMValue { + if let Some(h) = to_handle(args.get(0).unwrap_or(&VMValue::Void)) { call_slot(h, 200, &[]) } else { VMValue::Integer(0) } +} +pub fn map_has(args: &[VMValue]) -> VMValue { + if let Some(h) = to_handle(args.get(0).unwrap_or(&VMValue::Void)) { call_slot(h, 202, &args[1..]) } else { VMValue::Bool(false) } +} + diff --git a/src/jit/extern/mod.rs b/src/jit/extern/mod.rs index 4e1dbe55..044413e8 100644 --- a/src/jit/extern/mod.rs +++ b/src/jit/extern/mod.rs @@ -5,6 +5,7 @@ //! these externs once call emission is added. pub mod collections; +pub mod host_bridge; pub mod handles; pub mod birth; pub mod runtime; diff --git a/src/runtime/extern_registry.rs b/src/runtime/extern_registry.rs new file mode 100644 index 00000000..f75c8cc8 --- /dev/null +++ b/src/runtime/extern_registry.rs @@ -0,0 +1,37 @@ +//! Extern interface registry (env.*) for diagnostics and optional slotting +//! +//! 目的: ExternCallの未登録/未対応時に候補提示やSTRICT診断を改善する。 + +use once_cell::sync::Lazy; + +#[derive(Clone, Copy, Debug)] +pub struct ExternSpec { pub iface: &'static str, pub method: &'static str, pub min_arity: u8, pub max_arity: u8 } + +static EXTERNS: Lazy> = Lazy::new(|| vec![ + // console + ExternSpec { iface: "env.console", method: "log", min_arity: 1, max_arity: 1 }, + // debug + ExternSpec { iface: "env.debug", method: "trace", min_arity: 1, max_arity: 255 }, + // runtime + ExternSpec { iface: "env.runtime", method: "checkpoint", min_arity: 0, max_arity: 0 }, + // future (scaffold) + ExternSpec { iface: "env.future", method: "new", min_arity: 1, max_arity: 1 }, + ExternSpec { iface: "env.future", method: "birth", min_arity: 1, max_arity: 1 }, + ExternSpec { iface: "env.future", method: "set", min_arity: 2, max_arity: 2 }, + ExternSpec { iface: "env.future", method: "await", min_arity: 1, max_arity: 1 }, +]); + +pub fn resolve(iface: &str, method: &str) -> Option { + EXTERNS.iter().copied().find(|e| e.iface == iface && e.method == method) +} + +pub fn known_for_iface(iface: &str) -> Vec<&'static str> { + let mut v: Vec<&'static str> = EXTERNS.iter().filter(|e| e.iface == iface).map(|e| e.method).collect(); + v.sort(); v.dedup(); v +} + +pub fn all_ifaces() -> Vec<&'static str> { + let mut v: Vec<&'static str> = EXTERNS.iter().map(|e| e.iface).collect(); + v.sort(); v.dedup(); v +} + diff --git a/src/runtime/mod.rs b/src/runtime/mod.rs index 083d1352..60570e53 100644 --- a/src/runtime/mod.rs +++ b/src/runtime/mod.rs @@ -21,6 +21,7 @@ pub mod type_box_abi; // Phase 12: Nyash ABI (vtable) 雛形 pub mod type_registry; // Phase 12: TypeId→TypeBox 解決(雛形) pub mod host_handles; // C ABI(TLV) 向け HostHandle レジストリ(ユーザー/内蔵Box受け渡し) pub mod host_api; // C ABI: plugins -> host 逆呼び出しAPI(TLSでVMに橋渡し) +pub mod extern_registry; // ExternCall (env.*) 登録・診断用レジストリ #[cfg(test)] mod tests; diff --git a/src/tests/vtable_strict.rs b/src/tests/vtable_strict.rs new file mode 100644 index 00000000..bff443a3 --- /dev/null +++ b/src/tests/vtable_strict.rs @@ -0,0 +1,40 @@ +#[test] +fn vtable_map_set_and_strict_unknown() { + use crate::backend::vm::VM; + use crate::mir::{MirModule, MirFunction, FunctionSignature, MirInstruction, EffectMask, BasicBlockId, ConstValue, MirType}; + std::env::set_var("NYASH_ABI_VTABLE", "1"); + + // Build: new MapBox; call set("k","v"); size(); return size + let sig = FunctionSignature { name: "main".into(), params: vec![], return_type: MirType::Integer, effects: EffectMask::PURE }; + let mut f = MirFunction::new(sig, BasicBlockId::new(0)); + let bb = f.entry_block; + let mapv = f.next_value_id(); + f.get_block_mut(bb).unwrap().add_instruction(MirInstruction::NewBox { dst: mapv, box_type: "MapBox".into(), args: vec![] }); + let k = f.next_value_id(); let v = f.next_value_id(); + f.get_block_mut(bb).unwrap().add_instruction(MirInstruction::Const { dst: k, value: ConstValue::String("k".into()) }); + f.get_block_mut(bb).unwrap().add_instruction(MirInstruction::Const { dst: v, value: ConstValue::String("v".into()) }); + f.get_block_mut(bb).unwrap().add_instruction(MirInstruction::BoxCall { dst: None, box_val: mapv, method: "set".into(), args: vec![k, v], method_id: None, effects: EffectMask::PURE }); + let sz = f.next_value_id(); + f.get_block_mut(bb).unwrap().add_instruction(MirInstruction::BoxCall { dst: Some(sz), box_val: mapv, method: "size".into(), args: vec![], method_id: None, effects: EffectMask::PURE }); + f.get_block_mut(bb).unwrap().add_instruction(MirInstruction::Return { value: Some(sz) }); + let mut m = MirModule::new("t".into()); m.add_function(f); + let mut vm = VM::new(); + let out = vm.execute_module(&m).expect("vm exec"); + let s = out.to_string_box().value; assert_eq!(s, "1"); + + // STRICT unknown method on MapBox should error + std::env::set_var("NYASH_ABI_STRICT", "1"); + let sig2 = FunctionSignature { name: "main".into(), params: vec![], return_type: MirType::Void, effects: EffectMask::PURE }; + let mut f2 = MirFunction::new(sig2, BasicBlockId::new(0)); + let bb2 = f2.entry_block; + let m2 = f2.next_value_id(); + f2.get_block_mut(bb2).unwrap().add_instruction(MirInstruction::NewBox { dst: m2, box_type: "MapBox".into(), args: vec![] }); + // Call unknown method + f2.get_block_mut(bb2).unwrap().add_instruction(MirInstruction::BoxCall { dst: None, box_val: m2, method: "unknown".into(), args: vec![], method_id: None, effects: EffectMask::PURE }); + f2.get_block_mut(bb2).unwrap().add_instruction(MirInstruction::Return { value: None }); + let mut mm = MirModule::new("t2".into()); mm.add_function(f2); + let mut vm2 = VM::new(); + let res = vm2.execute_module(&mm); + assert!(res.is_err(), "STRICT should error on unknown vtable method"); +} +