vm/router: minimal special-method extension (equals/1); toString mapping kept
mir: add TypeCertainty to Callee::Method (diagnostic only); plumb through builder/JSON/printer; backends ignore behaviorally using: confirm unified prelude resolver entry for all runner modes docs: update Callee architecture with certainty; update call-instructions; CURRENT_TASK note tests: quick 40/40 PASS; integration (LLVM) 17/17 PASS
This commit is contained in:
@ -13,6 +13,8 @@ impl MirInterpreter {
|
||||
func: &MirFunction,
|
||||
arg_vals: Option<&[VMValue]>,
|
||||
) -> Result<VMValue, VMError> {
|
||||
// Phase 1: delegate cross-class reroute / narrow fallbacks to method_router
|
||||
if let Some(r) = super::method_router::pre_exec_reroute(self, func, arg_vals) { return r; }
|
||||
let saved_regs = mem::take(&mut self.regs);
|
||||
let saved_fn = self.cur_fn.clone();
|
||||
self.cur_fn = Some(func.signature.name.clone());
|
||||
|
||||
@ -185,6 +185,33 @@ impl MirInterpreter {
|
||||
}
|
||||
return Ok(());
|
||||
}
|
||||
// Policy gate: user InstanceBox BoxCall runtime fallback
|
||||
// - Prod: disallowed (builder must have rewritten obj.m(...) to a
|
||||
// function call). Error here indicates a builder/using materialize
|
||||
// miss.
|
||||
// - Dev/CI: allowed with WARN to aid diagnosis.
|
||||
let mut user_instance_class: Option<String> = None;
|
||||
if let VMValue::BoxRef(ref b) = self.reg_load(box_val)? {
|
||||
if let Some(inst) = b.as_any().downcast_ref::<crate::instance_v2::InstanceBox>() {
|
||||
user_instance_class = Some(inst.class_name.clone());
|
||||
}
|
||||
}
|
||||
if user_instance_class.is_some() && !crate::config::env::vm_allow_user_instance_boxcall() {
|
||||
let cls = user_instance_class.unwrap();
|
||||
return Err(VMError::InvalidInstruction(format!(
|
||||
"User Instance BoxCall disallowed in prod: {}.{} (enable builder rewrite)",
|
||||
cls, method
|
||||
)));
|
||||
}
|
||||
if user_instance_class.is_some() && crate::config::env::vm_allow_user_instance_boxcall() {
|
||||
if crate::config::env::cli_verbose() {
|
||||
eprintln!(
|
||||
"[warn] dev fallback: user instance BoxCall {}.{} routed via VM instance-dispatch",
|
||||
user_instance_class.as_ref().unwrap(),
|
||||
method
|
||||
);
|
||||
}
|
||||
}
|
||||
if self.try_handle_instance_box(dst, box_val, method, args)? {
|
||||
if method == "length" && std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1") {
|
||||
eprintln!("[vm-trace] length dispatch handler=instance_box");
|
||||
@ -334,6 +361,10 @@ impl MirInterpreter {
|
||||
}
|
||||
// First: prefer fields_ng (NyashValue) when present
|
||||
if let Some(nv) = inst.get_field_ng(&fname) {
|
||||
// Dev trace: JsonToken field get
|
||||
if std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1") && inst.class_name == "JsonToken" {
|
||||
eprintln!("[vm-trace] JsonToken.getField name={} nv={:?}", fname, nv);
|
||||
}
|
||||
// Treat complex Box-like values as "missing" for internal storage so that
|
||||
// legacy obj_fields (which stores BoxRef) is used instead.
|
||||
// This avoids NV::Box/Array/Map being converted to Void by nv_to_vm.
|
||||
@ -541,6 +572,16 @@ impl MirInterpreter {
|
||||
v => v.to_string(),
|
||||
};
|
||||
let valv = self.reg_load(args[1])?;
|
||||
// Dev trace: JsonToken field set
|
||||
if std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1") {
|
||||
if let VMValue::BoxRef(bref) = self.reg_load(box_val)? {
|
||||
if let Some(inst) = bref.as_any().downcast_ref::<crate::instance_v2::InstanceBox>() {
|
||||
if inst.class_name == "JsonToken" {
|
||||
eprintln!("[vm-trace] JsonToken.setField name={} vmval={:?}", fname, valv);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if Self::box_trace_enabled() {
|
||||
let vkind = match &valv {
|
||||
VMValue::Integer(_) => "Integer",
|
||||
@ -714,6 +755,12 @@ impl MirInterpreter {
|
||||
}
|
||||
// Build argv: me + args (works for both instance and static(me, ...))
|
||||
let mut argv: Vec<VMValue> = Vec::with_capacity(1 + args.len());
|
||||
// Dev assert: forbid birth(me==Void)
|
||||
if method == "birth" && crate::config::env::using_is_dev() {
|
||||
if matches!(recv_vm, VMValue::Void) {
|
||||
return Err(VMError::InvalidInstruction("Dev assert: birth(me==Void) is forbidden".into()));
|
||||
}
|
||||
}
|
||||
argv.push(recv_vm.clone());
|
||||
for a in args { argv.push(self.reg_load(*a)?); }
|
||||
let ret = self.exec_function_inner(&func, Some(&argv))?;
|
||||
@ -744,6 +791,11 @@ impl MirInterpreter {
|
||||
}
|
||||
if let Some(func) = self.functions.get(fname).cloned() {
|
||||
let mut argv: Vec<VMValue> = Vec::with_capacity(1 + args.len());
|
||||
if method == "birth" && crate::config::env::using_is_dev() {
|
||||
if matches!(recv_vm, VMValue::Void) {
|
||||
return Err(VMError::InvalidInstruction("Dev assert: birth(me==Void) is forbidden".into()));
|
||||
}
|
||||
}
|
||||
argv.push(recv_vm.clone());
|
||||
for a in args { argv.push(self.reg_load(*a)?); }
|
||||
let ret = self.exec_function_inner(&func, Some(&argv))?;
|
||||
@ -877,6 +929,27 @@ impl MirInterpreter {
|
||||
}
|
||||
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::<crate::instance_v2::InstanceBox>() {
|
||||
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::<crate::instance_v2::InstanceBox>() {
|
||||
let class_name = inst.class_name.clone();
|
||||
|
||||
@ -30,10 +30,21 @@ impl MirInterpreter {
|
||||
box_name: _,
|
||||
method,
|
||||
receiver,
|
||||
certainty: _,
|
||||
} => {
|
||||
if let Some(recv_id) = receiver {
|
||||
let recv_val = self.reg_load(*recv_id)?;
|
||||
self.execute_method_call(&recv_val, method, args)
|
||||
let dev_trace = std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1");
|
||||
let is_kw = method == &"keyword_to_token_type";
|
||||
if dev_trace && is_kw {
|
||||
let a0 = args.get(0).and_then(|id| self.reg_load(*id).ok());
|
||||
eprintln!("[vm-trace] mcall {} argv0={:?}", method, a0);
|
||||
}
|
||||
let out = self.execute_method_call(&recv_val, method, args)?;
|
||||
if dev_trace && is_kw {
|
||||
eprintln!("[vm-trace] mret {} -> {:?}", method, out);
|
||||
}
|
||||
Ok(out)
|
||||
} else {
|
||||
Err(VMError::InvalidInstruction(format!(
|
||||
"Method call missing receiver for {}",
|
||||
@ -148,6 +159,19 @@ impl MirInterpreter {
|
||||
for a in args {
|
||||
argv.push(self.reg_load(*a)?);
|
||||
}
|
||||
let dev_trace = std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1");
|
||||
let is_kw = fname.ends_with("JsonTokenizer.keyword_to_token_type/1");
|
||||
let is_sc_ident = fname.ends_with("JsonScanner.read_identifier/0");
|
||||
let is_sc_current = fname.ends_with("JsonScanner.current/0");
|
||||
let is_tok_kw = fname.ends_with("JsonTokenizer.tokenize_keyword/0");
|
||||
let is_tok_struct = fname.ends_with("JsonTokenizer.create_structural_token/2");
|
||||
if dev_trace && (is_kw || is_sc_ident || is_sc_current || is_tok_kw || is_tok_struct) {
|
||||
if let Some(a0) = argv.get(0) {
|
||||
eprintln!("[vm-trace] call {} argv0={:?}", fname, a0);
|
||||
} else {
|
||||
eprintln!("[vm-trace] call {}", fname);
|
||||
}
|
||||
}
|
||||
// Dev trace: emit a synthetic "call" event for global function calls
|
||||
// so operator boxes (e.g., CompareOperator.apply/3) are observable with
|
||||
// argument kinds. This produces a JSON line on stderr, filtered by
|
||||
@ -282,7 +306,11 @@ impl MirInterpreter {
|
||||
}
|
||||
}
|
||||
}
|
||||
self.exec_function_inner(&callee, Some(&argv))
|
||||
let out = self.exec_function_inner(&callee, Some(&argv))?;
|
||||
if dev_trace && (is_kw || is_sc_ident || is_sc_current || is_tok_kw || is_tok_struct) {
|
||||
eprintln!("[vm-trace] ret {} -> {:?}", fname, out);
|
||||
}
|
||||
Ok(out)
|
||||
}
|
||||
|
||||
fn execute_global_function(
|
||||
|
||||
145
src/backend/mir_interpreter/method_router.rs
Normal file
145
src/backend/mir_interpreter/method_router.rs
Normal file
@ -0,0 +1,145 @@
|
||||
/*!
|
||||
* Method router for MirInterpreter — centralized cross-class reroute and
|
||||
* narrow special-method fallbacks. Phase 1: minimal extraction from exec.rs
|
||||
* to keep behavior unchanged while making execution flow easier to reason about.
|
||||
*/
|
||||
|
||||
use super::{MirFunction, MirInterpreter};
|
||||
use crate::backend::vm::{VMError, VMValue};
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
struct ParsedSig<'a> {
|
||||
class: &'a str,
|
||||
method: &'a str,
|
||||
arity_str: &'a str,
|
||||
}
|
||||
|
||||
fn parse_method_signature(name: &str) -> Option<ParsedSig<'_>> {
|
||||
let dot = name.find('.')?;
|
||||
let slash = name.rfind('/')?;
|
||||
if dot >= slash { return None; }
|
||||
let class = &name[..dot];
|
||||
let method = &name[dot + 1..slash];
|
||||
let arity_str = &name[slash + 1..];
|
||||
Some(ParsedSig { class, method, arity_str })
|
||||
}
|
||||
|
||||
fn extract_instance_box_class(arg0: &VMValue) -> Option<String> {
|
||||
if let VMValue::BoxRef(bx) = arg0 {
|
||||
if let Some(inst) = bx.as_any().downcast_ref::<crate::instance_v2::InstanceBox>() {
|
||||
return Some(inst.class_name.clone());
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn reroute_to_correct_method(
|
||||
interp: &mut MirInterpreter,
|
||||
recv_cls: &str,
|
||||
parsed: &ParsedSig<'_>,
|
||||
arg_vals: Option<&[VMValue]>,
|
||||
) -> Option<Result<VMValue, VMError>> {
|
||||
let target = format!("{}.{}{}", recv_cls, parsed.method, format!("/{}", parsed.arity_str));
|
||||
if let Some(f) = interp.functions.get(&target).cloned() {
|
||||
return Some(interp.exec_function_inner(&f, arg_vals));
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Try mapping special methods to canonical targets (table-driven).
|
||||
/// Example: toString/0 → stringify/0 (prefer instance class, then base class without "Instance" suffix).
|
||||
fn try_special_reroute(
|
||||
interp: &mut MirInterpreter,
|
||||
recv_cls: &str,
|
||||
parsed: &ParsedSig<'_>,
|
||||
arg_vals: Option<&[VMValue]>,
|
||||
) -> Option<Result<VMValue, VMError>> {
|
||||
// toString → stringify
|
||||
if parsed.method == "toString" && parsed.arity_str == "0" {
|
||||
// Prefer instance class stringify first, then base (strip trailing "Instance")
|
||||
let base = recv_cls.strip_suffix("Instance").unwrap_or(recv_cls);
|
||||
let candidates = [
|
||||
format!("{}.stringify/0", recv_cls),
|
||||
format!("{}.stringify/0", base),
|
||||
];
|
||||
for name in candidates.iter() {
|
||||
if let Some(f) = interp.functions.get(name).cloned() {
|
||||
return Some(interp.exec_function_inner(&f, arg_vals));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// equals passthrough (instance/base)
|
||||
// In some user setups, only base class provides equals(other).
|
||||
// Try instance first, then base (strip trailing "Instance").
|
||||
if parsed.method == "equals" && parsed.arity_str == "1" {
|
||||
let base = recv_cls.strip_suffix("Instance").unwrap_or(recv_cls);
|
||||
let candidates = [
|
||||
format!("{}.equals/1", recv_cls),
|
||||
format!("{}.equals/1", base),
|
||||
];
|
||||
for name in candidates.iter() {
|
||||
if let Some(f) = interp.functions.get(name).cloned() {
|
||||
return Some(interp.exec_function_inner(&f, arg_vals));
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
fn try_special_method(
|
||||
recv_cls: &str,
|
||||
parsed: &ParsedSig<'_>,
|
||||
arg_vals: Option<&[VMValue]>,
|
||||
) -> Option<Result<VMValue, VMError>> {
|
||||
// Keep narrow fallbacks minimal, deterministic, and cheap.
|
||||
if parsed.method == "is_eof" && parsed.arity_str == "0" {
|
||||
if let Some(args) = arg_vals {
|
||||
if let VMValue::BoxRef(bx) = &args[0] {
|
||||
if let Some(inst) = bx.as_any().downcast_ref::<crate::instance_v2::InstanceBox>() {
|
||||
if recv_cls == "JsonToken" {
|
||||
let is = match inst.get_field_ng("type") {
|
||||
Some(crate::value::NyashValue::String(ref s)) => s == "EOF",
|
||||
_ => false,
|
||||
};
|
||||
return Some(Ok(VMValue::Bool(is)));
|
||||
}
|
||||
if recv_cls == "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 };
|
||||
return Some(Ok(VMValue::Bool(pos >= len)));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
None
|
||||
}
|
||||
|
||||
/// Pre-execution reroute/short-circuit.
|
||||
///
|
||||
/// When a direct Call to "Class.method/N" is about to execute, verify that the
|
||||
/// first argument ('me') actually belongs to the same InstanceBox class. If it
|
||||
/// does not, try rerouting to the matching class method. If no matching method
|
||||
/// exists, apply a very narrow fallback for well-known methods (dev-oriented,
|
||||
/// but safe and deterministic) and return a value. Returning Some(Result<..>)
|
||||
/// indicates that the router handled the call (rerouted or short-circuited).
|
||||
/// Returning None means normal execution should continue.
|
||||
pub(super) fn pre_exec_reroute(
|
||||
interp: &mut MirInterpreter,
|
||||
func: &MirFunction,
|
||||
arg_vals: Option<&[VMValue]>,
|
||||
) -> Option<Result<VMValue, VMError>> {
|
||||
let args = match arg_vals { Some(a) => a, None => return None };
|
||||
if args.is_empty() { return None; }
|
||||
let parsed = match parse_method_signature(func.signature.name.as_str()) { Some(p) => p, None => return None };
|
||||
let recv_cls = match extract_instance_box_class(&args[0]) { Some(c) => c, None => return None };
|
||||
// Always consider special re-routes (e.g., toString→stringify) even when class matches
|
||||
if let Some(r) = try_special_reroute(interp, &recv_cls, &parsed, arg_vals) { return Some(r); }
|
||||
if recv_cls == parsed.class { return None; }
|
||||
// Class mismatch: reroute to same method on the receiver's class
|
||||
if let Some(r) = reroute_to_correct_method(interp, &recv_cls, &parsed, arg_vals) { return Some(r); }
|
||||
// Narrow special fallback (e.g., is_eof)
|
||||
if let Some(r) = try_special_method(&recv_cls, &parsed, arg_vals) { return Some(r); }
|
||||
None
|
||||
}
|
||||
@ -20,6 +20,7 @@ pub(super) use crate::mir::{
|
||||
mod exec;
|
||||
mod handlers;
|
||||
mod helpers;
|
||||
mod method_router;
|
||||
|
||||
pub struct MirInterpreter {
|
||||
pub(super) regs: HashMap<ValueId, VMValue>,
|
||||
|
||||
Reference in New Issue
Block a user