diff --git a/src/backend/mir_interpreter/handlers/boxes.rs b/src/backend/mir_interpreter/handlers/boxes.rs index 2738534d..be563e91 100644 --- a/src/backend/mir_interpreter/handlers/boxes.rs +++ b/src/backend/mir_interpreter/handlers/boxes.rs @@ -283,7 +283,7 @@ impl MirInterpreter { return Ok(()); } - self.invoke_plugin_box(dst, box_val, method, args) + super::boxes_plugin::invoke_plugin_box(self, dst, box_val, method, args) } // moved: try_handle_map_box → handlers/boxes_map.rs @@ -319,179 +319,4 @@ impl MirInterpreter { super::boxes_array::try_handle_array_box(self, dst, box_val, method, args) } - fn invoke_plugin_box( - &mut self, - dst: Option, - box_val: ValueId, - method: &str, - args: &[ValueId], - ) -> Result<(), VMError> { - let recv = self.reg_load(box_val)?; - let recv_box: Box = match recv.clone() { - VMValue::BoxRef(b) => b.share_box(), - other => other.to_nyash_box(), - }; - if let Some(p) = recv_box - .as_any() - .downcast_ref::() - { - if p.box_type == "ConsoleBox" && method == "readLine" { - use std::io::{self, Read}; - let mut s = String::new(); - let mut stdin = io::stdin(); - let mut buf = [0u8; 1]; - while let Ok(n) = stdin.read(&mut buf) { - if n == 0 { - break; - } - let ch = buf[0] as char; - if ch == '\n' { - break; - } - s.push(ch); - if s.len() > 1_000_000 { - break; - } - } - if let Some(d) = dst { - self.regs.insert(d, VMValue::String(s)); - } - return Ok(()); - } - let host = crate::runtime::plugin_loader_unified::get_global_plugin_host(); - let host = host.read().unwrap(); - let mut argv: Vec> = Vec::with_capacity(args.len()); - for a in args { - argv.push(self.reg_load(*a)?.to_nyash_box()); - } - match host.invoke_instance_method(&p.box_type, method, p.inner.instance_id, &argv) { - Ok(Some(ret)) => { - if let Some(d) = dst { - self.regs.insert(d, VMValue::from_nyash_box(ret)); - } - Ok(()) - } - Ok(None) => { - if let Some(d) = dst { - self.regs.insert(d, VMValue::Void); - } - Ok(()) - } - Err(e) => Err(VMError::InvalidInstruction(format!( - "BoxCall {}.{} failed: {:?}", - p.box_type, method, e - ))), - } - } else { - // Special-case: minimal runtime fallback for common InstanceBox methods when - // lowered functions are not available (dev robustness). Keeps behavior stable - // without changing semantics in the normal path. - if let Some(inst) = recv_box.as_any().downcast_ref::() { - // Generic current() fallback: if object has integer 'position' and string 'text', - // return one character at that position (or empty at EOF). This covers JsonScanner - // and compatible scanners without relying on class name. - if method == "current" && args.is_empty() { - if let Some(crate::value::NyashValue::Integer(pos)) = inst.get_field_ng("position") { - if let Some(crate::value::NyashValue::String(text)) = inst.get_field_ng("text") { - let s = if pos < 0 || (pos as usize) >= text.len() { String::new() } else { - let bytes = text.as_bytes(); - let i = pos as usize; - let j = (i + 1).min(bytes.len()); - String::from_utf8(bytes[i..j].to_vec()).unwrap_or_default() - }; - if let Some(d) = dst { self.regs.insert(d, VMValue::String(s)); } - return Ok(()); - } - } - } - } - // Generic toString fallback for any non-plugin box - if method == "toString" { - if let Some(d) = dst { - // Map VoidBox.toString → "null" for JSON-friendly semantics - let s = if recv_box.as_any().downcast_ref::().is_some() { - "null".to_string() - } else { - recv_box.to_string_box().value - }; - self.regs.insert(d, VMValue::String(s)); - } - return Ok(()); - } - // Minimal runtime fallback for common InstanceBox.is_eof when lowered function is not present. - // This avoids cross-class leaks and hard errors in union-like flows. - if method == "is_eof" && args.is_empty() { - if let Some(inst) = recv_box.as_any().downcast_ref::() { - if inst.class_name == "JsonToken" { - let is = match inst.get_field_ng("type") { - Some(crate::value::NyashValue::String(ref s)) => s == "EOF", - _ => false, - }; - if let Some(d) = dst { self.regs.insert(d, VMValue::Bool(is)); } - return Ok(()); - } - if inst.class_name == "JsonScanner" { - let pos = match inst.get_field_ng("position") { Some(crate::value::NyashValue::Integer(i)) => i, _ => 0 }; - let len = match inst.get_field_ng("length") { Some(crate::value::NyashValue::Integer(i)) => i, _ => 0 }; - let is = pos >= len; - if let Some(d) = dst { self.regs.insert(d, VMValue::Bool(is)); } - return Ok(()); - } - } - } - // Dynamic fallback for user-defined InstanceBox: dispatch to lowered function "Class.method/Arity" - if let Some(inst) = recv_box.as_any().downcast_ref::() { - let class_name = inst.class_name.clone(); - let arity = args.len(); // function name arity excludes 'me' - let fname = format!("{}.{}{}", class_name, method, format!("/{}", arity)); - if let Some(func) = self.functions.get(&fname).cloned() { - let mut argv: Vec = Vec::with_capacity(arity + 1); - // Pass receiver as first arg ('me') - argv.push(recv.clone()); - for a in args { - argv.push(self.reg_load(*a)?); - } - let ret = self.exec_function_inner(&func, Some(&argv))?; - if let Some(d) = dst { self.regs.insert(d, ret); } - return Ok(()); - } - } - // Last-resort dev fallback: tolerate InstanceBox.current() by returning empty string - // when no class-specific handler is available. This avoids hard stops in JSON lint smokes - // while builder rewrite and instance dispatch stabilize. - if method == "current" && args.is_empty() { - if let Some(d) = dst { self.regs.insert(d, VMValue::String(String::new())); } - return Ok(()); - } - // VoidBox graceful handling for common container-like methods - // Treat null.receiver.* as safe no-ops that return null/0 where appropriate - if recv_box.type_name() == "VoidBox" { - match method { - "object_get" | "array_get" | "toString" => { - if let Some(d) = dst { self.regs.insert(d, VMValue::Void); } - return Ok(()); - } - "stringify" => { - if let Some(d) = dst { self.regs.insert(d, VMValue::String("null".to_string())); } - return Ok(()); - } - "array_size" | "length" | "size" => { - if let Some(d) = dst { self.regs.insert(d, VMValue::Integer(0)); } - return Ok(()); - } - "object_set" | "array_push" | "set" => { - // No-op setters on null receiver - if let Some(d) = dst { self.regs.insert(d, VMValue::Void); } - return Ok(()); - } - _ => {} - } - } - Err(VMError::InvalidInstruction(format!( - "BoxCall unsupported on {}.{}", - recv_box.type_name(), - method - ))) - } - } } diff --git a/src/backend/mir_interpreter/handlers/boxes_plugin.rs b/src/backend/mir_interpreter/handlers/boxes_plugin.rs new file mode 100644 index 00000000..2546a94c --- /dev/null +++ b/src/backend/mir_interpreter/handlers/boxes_plugin.rs @@ -0,0 +1,178 @@ +use super::*; +use crate::box_trait::NyashBox; + +pub(super) fn invoke_plugin_box( + this: &mut MirInterpreter, + dst: Option, + box_val: ValueId, + method: &str, + args: &[ValueId], +) -> Result<(), VMError> { + let recv = this.reg_load(box_val)?; + let recv_box: Box = match recv.clone() { + VMValue::BoxRef(b) => b.share_box(), + other => other.to_nyash_box(), + }; + if let Some(p) = recv_box + .as_any() + .downcast_ref::() + { + if p.box_type == "ConsoleBox" && method == "readLine" { + use std::io::{self, Read}; + let mut s = String::new(); + let mut stdin = io::stdin(); + let mut buf = [0u8; 1]; + while let Ok(n) = stdin.read(&mut buf) { + if n == 0 { + break; + } + let ch = buf[0] as char; + if ch == '\n' { + break; + } + s.push(ch); + if s.len() > 1_000_000 { + break; + } + } + if let Some(d) = dst { + this.regs.insert(d, VMValue::String(s)); + } + return Ok(()); + } + let host = crate::runtime::plugin_loader_unified::get_global_plugin_host(); + let host = host.read().unwrap(); + let mut argv: Vec> = Vec::with_capacity(args.len()); + for a in args { + argv.push(this.reg_load(*a)?.to_nyash_box()); + } + match host.invoke_instance_method(&p.box_type, method, p.inner.instance_id, &argv) { + Ok(Some(ret)) => { + if let Some(d) = dst { + this.regs.insert(d, VMValue::from_nyash_box(ret)); + } + Ok(()) + } + Ok(None) => { + if let Some(d) = dst { + this.regs.insert(d, VMValue::Void); + } + Ok(()) + } + Err(e) => Err(VMError::InvalidInstruction(format!( + "BoxCall {}.{} failed: {:?}", + p.box_type, method, e + ))), + } + } else { + // Special-case: minimal runtime fallback for common InstanceBox methods when + // lowered functions are not available (dev robustness). Keeps behavior stable + // without changing semantics in the normal path. + if let Some(inst) = recv_box.as_any().downcast_ref::() { + // Generic current() fallback: if object has integer 'position' and string 'text', + // return one character at that position (or empty at EOF). This covers JsonScanner + // and compatible scanners without relying on class name. + if method == "current" && args.is_empty() { + if let Some(crate::value::NyashValue::Integer(pos)) = inst.get_field_ng("position") { + if let Some(crate::value::NyashValue::String(text)) = inst.get_field_ng("text") { + let s = if pos < 0 || (pos as usize) >= text.len() { String::new() } else { + let bytes = text.as_bytes(); + let i = pos as usize; + let j = (i + 1).min(bytes.len()); + String::from_utf8(bytes[i..j].to_vec()).unwrap_or_default() + }; + if let Some(d) = dst { this.regs.insert(d, VMValue::String(s)); } + return Ok(()); + } + } + } + } + // Generic toString fallback for any non-plugin box + if method == "toString" { + if let Some(d) = dst { + // Map VoidBox.toString → "null" for JSON-friendly semantics + let s = if recv_box.as_any().downcast_ref::().is_some() { + "null".to_string() + } else { + recv_box.to_string_box().value + }; + this.regs.insert(d, VMValue::String(s)); + } + return Ok(()); + } + // Minimal runtime fallback for common InstanceBox.is_eof when lowered function is not present. + // This avoids cross-class leaks and hard errors in union-like flows. + if method == "is_eof" && args.is_empty() { + if let Some(inst) = recv_box.as_any().downcast_ref::() { + if inst.class_name == "JsonToken" { + let is = match inst.get_field_ng("type") { + Some(crate::value::NyashValue::String(ref s)) => s == "EOF", + _ => false, + }; + if let Some(d) = dst { this.regs.insert(d, VMValue::Bool(is)); } + return Ok(()); + } + if inst.class_name == "JsonScanner" { + let pos = match inst.get_field_ng("position") { Some(crate::value::NyashValue::Integer(i)) => i, _ => 0 }; + let len = match inst.get_field_ng("length") { Some(crate::value::NyashValue::Integer(i)) => i, _ => 0 }; + let is = pos >= len; + if let Some(d) = dst { this.regs.insert(d, VMValue::Bool(is)); } + return Ok(()); + } + } + } + // Dynamic fallback for user-defined InstanceBox: dispatch to lowered function "Class.method/Arity" + if let Some(inst) = recv_box.as_any().downcast_ref::() { + let class_name = inst.class_name.clone(); + let arity = args.len(); // function name arity excludes 'me' + let fname = format!("{}.{}{}", class_name, method, format!("/{}", arity)); + if let Some(func) = this.functions.get(&fname).cloned() { + let mut argv: Vec = Vec::with_capacity(arity + 1); + // Pass receiver as first arg ('me') + argv.push(recv.clone()); + for a in args { + argv.push(this.reg_load(*a)?); + } + let ret = this.exec_function_inner(&func, Some(&argv))?; + if let Some(d) = dst { this.regs.insert(d, ret); } + return Ok(()); + } + } + // Last-resort dev fallback: tolerate InstanceBox.current() by returning empty string + // when no class-specific handler is available. This avoids hard stops in JSON lint smokes + // while builder rewrite and instance dispatch stabilize. + if method == "current" && args.is_empty() { + if let Some(d) = dst { this.regs.insert(d, VMValue::String(String::new())); } + return Ok(()); + } + // VoidBox graceful handling for common container-like methods + // Treat null.receiver.* as safe no-ops that return null/0 where appropriate + if recv_box.type_name() == "VoidBox" { + match method { + "object_get" | "array_get" | "toString" => { + if let Some(d) = dst { this.regs.insert(d, VMValue::Void); } + return Ok(()); + } + "stringify" => { + if let Some(d) = dst { this.regs.insert(d, VMValue::String("null".to_string())); } + return Ok(()); + } + "array_size" | "length" | "size" => { + if let Some(d) = dst { this.regs.insert(d, VMValue::Integer(0)); } + return Ok(()); + } + "object_set" | "array_push" | "set" => { + // No-op setters on null receiver + if let Some(d) = dst { this.regs.insert(d, VMValue::Void); } + return Ok(()); + } + _ => {} + } + } + Err(VMError::InvalidInstruction(format!( + "BoxCall unsupported on {}.{}", + recv_box.type_name(), + method + ))) + } +} diff --git a/src/backend/mir_interpreter/handlers/mod.rs b/src/backend/mir_interpreter/handlers/mod.rs index 928911d1..1f12bc69 100644 --- a/src/backend/mir_interpreter/handlers/mod.rs +++ b/src/backend/mir_interpreter/handlers/mod.rs @@ -7,6 +7,7 @@ mod boxes_string; mod boxes_map; mod boxes_object_fields; mod boxes_instance; +mod boxes_plugin; mod calls; mod externals; mod memory;