Phase 21.7 normalization: optimization pre-work + bench harness expansion
- Add opt-in optimizations (defaults OFF) - Ret purity verifier: NYASH_VERIFY_RET_PURITY=1 - strlen FAST enhancement for const handles - FAST_INT gate for same-BB SSA optimization - length cache for string literals in llvmlite - Expand bench harness (tools/perf/microbench.sh) - Add branch/call/stringchain/arraymap/chip8/kilo cases - Auto-calculate ratio vs C reference - Document in benchmarks/README.md - Compiler health improvements - Unify PHI insertion to insert_phi_at_head() - Add NYASH_LLVM_SKIP_BUILD=1 for build reuse - Runtime & safety enhancements - Clarify Rust/Hako ownership boundaries - Strengthen receiver localization (LocalSSA/pin/after-PHIs) - Stop excessive PluginInvoke→BoxCall rewrites - Update CURRENT_TASK.md, docs, and canaries 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@ -1,5 +1,4 @@
|
||||
use super::*;
|
||||
use super::super::utils::*;
|
||||
|
||||
impl MirInterpreter {
|
||||
pub(super) fn handle_const(&mut self, dst: ValueId, value: &ConstValue) -> Result<(), VMError> {
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
use super::*;
|
||||
use super::super::utils::*;
|
||||
use crate::box_trait::NyashBox;
|
||||
|
||||
impl MirInterpreter {
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
use super::*;
|
||||
use super::super::utils::*;
|
||||
use crate::box_trait::NyashBox;
|
||||
|
||||
pub(super) fn try_handle_array_box(
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
use super::*;
|
||||
use super::super::utils::*;
|
||||
use crate::box_trait::NyashBox;
|
||||
|
||||
pub(super) fn try_handle_instance_box(
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
use super::*;
|
||||
use super::super::utils::*;
|
||||
use crate::box_trait::NyashBox;
|
||||
|
||||
pub(super) fn try_handle_map_box(
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
use super::*;
|
||||
use super::super::utils::*;
|
||||
use crate::box_trait::NyashBox;
|
||||
|
||||
pub(super) fn try_handle_object_fields(
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
use super::*;
|
||||
use super::super::utils::*;
|
||||
use crate::box_trait::NyashBox;
|
||||
|
||||
pub(super) fn invoke_plugin_box(
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
use super::*;
|
||||
use super::super::utils::*;
|
||||
use crate::box_trait::NyashBox;
|
||||
|
||||
pub(super) fn try_handle_string_box(
|
||||
|
||||
@ -1,89 +0,0 @@
|
||||
// Unique-tail function name resolution
|
||||
// Extracted from execute_legacy_call to keep handlers lean
|
||||
|
||||
use std::collections::HashMap;
|
||||
|
||||
use super::MirFunction;
|
||||
|
||||
/// Resolve function name using unique-tail matching algorithm.
|
||||
///
|
||||
/// Resolution strategy:
|
||||
/// 1. Fast path: exact match on raw name
|
||||
/// 2. Normalize with arity: "base/N"
|
||||
/// 3. Unique-tail matching: find candidates ending with ".method/N"
|
||||
/// 4. Same-box preference: prioritize functions in the same box as current_function
|
||||
///
|
||||
/// Returns resolved function name, or None if resolution fails.
|
||||
pub(super) fn resolve_function_name(
|
||||
raw: &str,
|
||||
arity: usize,
|
||||
functions: &HashMap<String, MirFunction>,
|
||||
current_function: Option<&str>,
|
||||
) -> Option<String> {
|
||||
// Fast path: exact match
|
||||
if functions.contains_key(raw) {
|
||||
return Some(raw.to_string());
|
||||
}
|
||||
|
||||
// Robust normalization for names like "Box.method/Arity" or just "method"
|
||||
let (base, ar_from_raw) = if let Some((b, a)) = raw.rsplit_once('/') {
|
||||
(b.to_string(), a.parse::<usize>().ok())
|
||||
} else {
|
||||
(raw.to_string(), None)
|
||||
};
|
||||
let want_arity = ar_from_raw.unwrap_or(arity);
|
||||
|
||||
// Try exact canonical form: "base/arity"
|
||||
let exact = format!("{}/{}", base, want_arity);
|
||||
if functions.contains_key(&exact) {
|
||||
return Some(exact);
|
||||
}
|
||||
|
||||
// Split base into optional box and method name
|
||||
let (maybe_box, method) = if let Some((bx, m)) = base.split_once('.') {
|
||||
(Some(bx.to_string()), m.to_string())
|
||||
} else {
|
||||
(None, base.clone())
|
||||
};
|
||||
|
||||
// Collect candidates that end with ".method/arity"
|
||||
let mut cands: Vec<String> = Vec::new();
|
||||
let tail = format!(".{}{}", method, format!("/{}", want_arity));
|
||||
for k in functions.keys() {
|
||||
if k.ends_with(&tail) {
|
||||
if let Some(ref bx) = maybe_box {
|
||||
if k.starts_with(&format!("{}.", bx)) {
|
||||
cands.push(k.clone());
|
||||
}
|
||||
} else {
|
||||
cands.push(k.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if cands.len() > 1 {
|
||||
// Prefer same-box candidate based on current function's box
|
||||
if let Some(cur) = current_function {
|
||||
let cur_box = cur.split('.').next().unwrap_or("");
|
||||
let scoped: Vec<String> = cands
|
||||
.iter()
|
||||
.filter(|k| k.starts_with(&format!("{}.", cur_box)))
|
||||
.cloned()
|
||||
.collect();
|
||||
if scoped.len() == 1 {
|
||||
cands = scoped;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
match cands.len() {
|
||||
0 => None,
|
||||
1 => Some(cands.into_iter().next().unwrap()),
|
||||
_ => {
|
||||
// Multiple candidates: sort and pick first (deterministic fallback)
|
||||
let mut c = cands;
|
||||
c.sort();
|
||||
Some(c.into_iter().next().unwrap())
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,873 +0,0 @@
|
||||
use super::*;
|
||||
use super::super::utils::*;
|
||||
|
||||
impl MirInterpreter {
|
||||
pub(super) fn handle_call(
|
||||
&mut self,
|
||||
dst: Option<ValueId>,
|
||||
func: ValueId,
|
||||
callee: Option<&Callee>,
|
||||
args: &[ValueId],
|
||||
) -> Result<(), VMError> {
|
||||
if std::env::var("HAKO_CABI_TRACE").ok().as_deref() == Some("1") {
|
||||
match callee {
|
||||
Some(Callee::Global(n)) => eprintln!("[hb:path] call Callee::Global {} argc={}", n, args.len()),
|
||||
Some(Callee::Method{ box_name, method, ..}) => eprintln!("[hb:path] call Callee::Method {}.{} argc={}", box_name, method, args.len()),
|
||||
Some(Callee::Constructor{ box_type }) => eprintln!("[hb:path] call Callee::Constructor {} argc={}", box_type, args.len()),
|
||||
Some(Callee::Closure{ .. }) => eprintln!("[hb:path] call Callee::Closure argc={}", args.len()),
|
||||
Some(Callee::Value(_)) => eprintln!("[hb:path] call Callee::Value argc={}", args.len()),
|
||||
Some(Callee::Extern(n)) => eprintln!("[hb:path] call Callee::Extern {} argc={}", n, args.len()),
|
||||
None => eprintln!("[hb:path] call Legacy func_id={:?} argc={}", func, args.len()),
|
||||
}
|
||||
}
|
||||
// SSOT fast-path: route hostbridge.extern_invoke to extern dispatcher regardless of resolution form
|
||||
if let Some(Callee::Global(func_name)) = callee {
|
||||
if func_name == "hostbridge.extern_invoke" || func_name.starts_with("hostbridge.extern_invoke/") {
|
||||
let v = self.execute_extern_function("hostbridge.extern_invoke", args)?;
|
||||
self.write_result(dst, v);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
let call_result = if let Some(callee_type) = callee {
|
||||
self.execute_callee_call(callee_type, args)?
|
||||
} else {
|
||||
self.execute_legacy_call(func, args)?
|
||||
};
|
||||
self.write_result(dst, call_result);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn execute_callee_call(
|
||||
&mut self,
|
||||
callee: &Callee,
|
||||
args: &[ValueId],
|
||||
) -> Result<VMValue, VMError> {
|
||||
match callee {
|
||||
Callee::Global(func_name) => {
|
||||
// Phase 21.2: Dev by-name bridge removed - all adapter functions now in .hako
|
||||
self.execute_global_function(func_name, args)
|
||||
}
|
||||
Callee::Method { box_name: _, method, receiver, certainty: _, } => {
|
||||
if let Some(recv_id) = receiver {
|
||||
// Primary: load receiver by id. Dev fallback: if undefined and env allows,
|
||||
// use args[0] as a surrogate receiver (builder localization gap workaround).
|
||||
let recv_val = match self.reg_load(*recv_id) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
let tolerate = std::env::var("NYASH_VM_RECV_ARG_FALLBACK").ok().as_deref() == Some("1")
|
||||
|| std::env::var("NYASH_VM_TOLERATE_VOID").ok().as_deref() == Some("1");
|
||||
if tolerate {
|
||||
if let Some(a0) = args.get(0) {
|
||||
self.reg_load(*a0)?
|
||||
} else {
|
||||
return Err(e);
|
||||
}
|
||||
} else {
|
||||
return Err(e);
|
||||
}
|
||||
}
|
||||
};
|
||||
let dev_trace = std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1");
|
||||
// Fast bridge for builtin boxes (Array) and common methods.
|
||||
// Preserve legacy semantics when plugins are absent.
|
||||
if let VMValue::BoxRef(bx) = &recv_val {
|
||||
// ArrayBox bridge
|
||||
if let Some(arr) = bx.as_any().downcast_ref::<crate::boxes::array::ArrayBox>() {
|
||||
match method.as_str() {
|
||||
"birth" => { return Ok(VMValue::Void); }
|
||||
"push" => {
|
||||
if let Some(a0) = args.get(0) {
|
||||
let v = self.load_as_box(*a0)?;
|
||||
let _ = arr.push(v);
|
||||
return Ok(VMValue::Void);
|
||||
}
|
||||
}
|
||||
"len" | "length" | "size" => {
|
||||
let ret = arr.length();
|
||||
return Ok(VMValue::from_nyash_box(ret));
|
||||
}
|
||||
"get" => {
|
||||
if let Some(a0) = args.get(0) {
|
||||
let idx = self.load_as_box(*a0)?;
|
||||
let ret = arr.get(idx);
|
||||
return Ok(VMValue::from_nyash_box(ret));
|
||||
}
|
||||
}
|
||||
"set" => {
|
||||
if args.len() >= 2 {
|
||||
let idx = self.load_as_box(args[0])?;
|
||||
let val = self.load_as_box(args[1])?;
|
||||
let _ = arr.set(idx, val);
|
||||
return Ok(VMValue::Void);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Minimal bridge for birth(): delegate to BoxCall handler and return Void
|
||||
if method == &"birth" {
|
||||
let _ = self.handle_box_call(None, *recv_id, method, args)?;
|
||||
return Ok(VMValue::Void);
|
||||
}
|
||||
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(self.err_with_context("Method call", &format!("missing receiver for {}", method)))
|
||||
}
|
||||
}
|
||||
Callee::Constructor { box_type } => Err(self.err_unsupported(&format!("Constructor calls for {}", box_type))),
|
||||
Callee::Closure { .. } => Err(self.err_unsupported("Closure creation in VM")),
|
||||
Callee::Value(func_val_id) => {
|
||||
let _func_val = self.reg_load(*func_val_id)?;
|
||||
Err(self.err_unsupported("First-class function calls in VM"))
|
||||
}
|
||||
Callee::Extern(extern_name) => self.execute_extern_function(extern_name, args),
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) fn execute_legacy_call(
|
||||
&mut self,
|
||||
func_id: ValueId,
|
||||
args: &[ValueId],
|
||||
) -> Result<VMValue, VMError> {
|
||||
let name_val = self.reg_load(func_id)?;
|
||||
let raw = match name_val {
|
||||
VMValue::String(ref s) => s.clone(),
|
||||
other => other.to_string(),
|
||||
};
|
||||
if std::env::var("HAKO_DEBUG_LEGACY_CALL").ok().as_deref() == Some("1") {
|
||||
eprintln!("[vm-debug] legacy-call raw='{}' argc={}", raw, args.len());
|
||||
}
|
||||
|
||||
// Minimal builtin bridge: support print-like globals in legacy form
|
||||
// Accept: "print", "nyash.console.log", "env.console.log", "nyash.builtin.print"
|
||||
match raw.as_str() {
|
||||
"print" | "nyash.console.log" | "env.console.log" | "nyash.builtin.print" => {
|
||||
if let Some(a0) = args.get(0) {
|
||||
let v = self.reg_load(*a0)?;
|
||||
println!("{}", v.to_string());
|
||||
} else {
|
||||
println!("");
|
||||
}
|
||||
return Ok(VMValue::Void);
|
||||
}
|
||||
name if name == "hostbridge.extern_invoke" || name.starts_with("hostbridge.extern_invoke/") =>
|
||||
// SSOT: always delegate to extern dispatcher
|
||||
{ return self.execute_extern_function("hostbridge.extern_invoke", args); }
|
||||
name if name == "env.get" || name.starts_with("env.get/") || name.contains("env.get") => {
|
||||
return self.execute_extern_function("env.get", args);
|
||||
}
|
||||
// Minimal bridge for Hako static provider: HakoruneExternProviderBox.get(name, arg)
|
||||
// Purpose: allow -c/inline alias path to tolerate static box calls without unresolved errors.
|
||||
// Behavior: when HAKO_V1_EXTERN_PROVIDER_C_ABI=1, emit stable tag; always return empty string.
|
||||
name if name == "HakoruneExternProviderBox.get"
|
||||
|| name.starts_with("HakoruneExternProviderBox.get/")
|
||||
|| name.contains("HakoruneExternProviderBox.get") =>
|
||||
{
|
||||
// Read provider name from first arg if available
|
||||
let mut prov: Option<String> = None;
|
||||
if let Some(a0) = args.get(0) {
|
||||
if let Ok(v) = self.reg_load(*a0) { prov = Some(v.to_string()); }
|
||||
}
|
||||
if std::env::var("HAKO_V1_EXTERN_PROVIDER_C_ABI").ok().as_deref() == Some("1") {
|
||||
if let Some(p) = prov.as_deref() {
|
||||
match p {
|
||||
"env.mirbuilder.emit" => eprintln!("[extern/c-abi:mirbuilder.emit]"),
|
||||
"env.codegen.emit_object" => eprintln!("[extern/c-abi:codegen.emit_object]"),
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
return Ok(VMValue::String(String::new()));
|
||||
}
|
||||
// Phase 21.2: Dev bridge removed - all adapter functions now resolved via .hako implementation
|
||||
// MirCallV1HandlerBox.handle, JsonFragBox._str_to_int, AbiAdapterRegistryBox.*
|
||||
// are now implemented in lang/src/vm/ and compiled via text-merge
|
||||
_ => {}
|
||||
}
|
||||
|
||||
// Resolve function name using unique-tail matching
|
||||
let fname = call_resolution::resolve_function_name(
|
||||
&raw,
|
||||
args.len(),
|
||||
&self.functions,
|
||||
self.cur_fn.as_deref(),
|
||||
)
|
||||
.ok_or_else(|| {
|
||||
ErrorBuilder::with_context("call", &format!("unresolved: '{}' (arity={})", raw, args.len()))
|
||||
})?;
|
||||
|
||||
if std::env::var("NYASH_VM_CALL_TRACE").ok().as_deref() == Some("1") {
|
||||
eprintln!("[vm] legacy-call resolved '{}' -> '{}'", raw, fname);
|
||||
}
|
||||
if std::env::var("HAKO_CABI_TRACE").ok().as_deref() == Some("1") {
|
||||
eprintln!("[hb:resolved:calls] fname='{}'", fname);
|
||||
}
|
||||
|
||||
let callee =
|
||||
self.functions.get(&fname).cloned().ok_or_else(|| {
|
||||
self.err_with_context("function call", &format!("function not found: {}", fname))
|
||||
})?;
|
||||
|
||||
// SSOT: delegate hostbridge.extern_invoke to the extern dispatcher early,
|
||||
// avoiding duplicated legacy handling below. This ensures C-API link path
|
||||
// is consistently covered by extern_provider_dispatch.
|
||||
if fname == "hostbridge.extern_invoke" || fname.starts_with("hostbridge.extern_invoke/") {
|
||||
if std::env::var("HAKO_CABI_TRACE").ok().as_deref() == Some("1") {
|
||||
eprintln!("[hb:delegate:calls] hostbridge.extern_invoke -> execute_extern_function (early)");
|
||||
}
|
||||
return self.execute_extern_function("hostbridge.extern_invoke", args);
|
||||
}
|
||||
|
||||
let mut argv: Vec<VMValue> = Vec::new();
|
||||
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
|
||||
// NYASH_BOX_TRACE_FILTER like other box traces.
|
||||
if Self::box_trace_enabled() {
|
||||
// Render class/method from canonical fname like "Class.method/Arity"
|
||||
let (class_name, method_name) = if let Some((cls, rest)) = fname.split_once('.') {
|
||||
let method = rest.split('/').next().unwrap_or(rest);
|
||||
(cls.to_string(), method.to_string())
|
||||
} else {
|
||||
("<global>".to_string(), fname.split('/').next().unwrap_or(&fname).to_string())
|
||||
};
|
||||
// Simple filter match (local copy to avoid private helper)
|
||||
let filt_ok = match std::env::var("NYASH_BOX_TRACE_FILTER").ok() {
|
||||
Some(filt) => {
|
||||
let want = filt.trim();
|
||||
if want.is_empty() { true } else {
|
||||
want.split(|c: char| c == ',' || c.is_whitespace())
|
||||
.map(|t| t.trim())
|
||||
.filter(|t| !t.is_empty())
|
||||
.any(|t| class_name.contains(t))
|
||||
}
|
||||
}
|
||||
None => true,
|
||||
};
|
||||
if filt_ok {
|
||||
// Optionally include argument kinds for targeted debugging.
|
||||
let with_args = std::env::var("NYASH_OP_TRACE_ARGS").ok().as_deref() == Some("1")
|
||||
|| class_name == "CompareOperator";
|
||||
if with_args {
|
||||
// local JSON string escaper (subset)
|
||||
let mut esc = |s: &str| {
|
||||
let mut out = String::with_capacity(s.len() + 8);
|
||||
for ch in s.chars() {
|
||||
match ch {
|
||||
'"' => out.push_str("\\\""),
|
||||
'\\' => out.push_str("\\\\"),
|
||||
'\n' => out.push_str("\\n"),
|
||||
'\r' => out.push_str("\\r"),
|
||||
'\t' => out.push_str("\\t"),
|
||||
c if c.is_control() => out.push(' '),
|
||||
c => out.push(c),
|
||||
}
|
||||
}
|
||||
out
|
||||
};
|
||||
let mut kinds: Vec<String> = Vec::with_capacity(argv.len());
|
||||
let mut nullish: Vec<String> = Vec::with_capacity(argv.len());
|
||||
for v in &argv {
|
||||
let k = match v {
|
||||
VMValue::Integer(_) => "Integer".to_string(),
|
||||
VMValue::Float(_) => "Float".to_string(),
|
||||
VMValue::Bool(_) => "Bool".to_string(),
|
||||
VMValue::String(_) => "String".to_string(),
|
||||
VMValue::Void => "Void".to_string(),
|
||||
VMValue::Future(_) => "Future".to_string(),
|
||||
VMValue::BoxRef(b) => {
|
||||
// Prefer InstanceBox.class_name when available
|
||||
if let Some(inst) = b.as_any().downcast_ref::<crate::instance_v2::InstanceBox>() {
|
||||
format!("BoxRef:{}", inst.class_name)
|
||||
} else {
|
||||
format!("BoxRef:{}", b.type_name())
|
||||
}
|
||||
}
|
||||
};
|
||||
kinds.push(k);
|
||||
// nullish tag (env-gated): "null" | "missing" | "void" | ""
|
||||
if crate::config::env::null_missing_box_enabled() {
|
||||
let tag = match v {
|
||||
VMValue::Void => "void",
|
||||
VMValue::BoxRef(b) => {
|
||||
if b.as_any().downcast_ref::<crate::boxes::null_box::NullBox>().is_some() { "null" }
|
||||
else if b.as_any().downcast_ref::<crate::boxes::missing_box::MissingBox>().is_some() { "missing" }
|
||||
else if b.as_any().downcast_ref::<crate::box_trait::VoidBox>().is_some() { "void" }
|
||||
else { "" }
|
||||
}
|
||||
_ => "",
|
||||
};
|
||||
nullish.push(tag.to_string());
|
||||
}
|
||||
}
|
||||
let args_json = kinds
|
||||
.into_iter()
|
||||
.map(|s| format!("\"{}\"", esc(&s)))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
let nullish_json = if crate::config::env::null_missing_box_enabled() {
|
||||
let arr = nullish
|
||||
.into_iter()
|
||||
.map(|s| format!("\"{}\"", esc(&s)))
|
||||
.collect::<Vec<_>>()
|
||||
.join(",");
|
||||
Some(arr)
|
||||
} else { None };
|
||||
// For CompareOperator, include op string value if present in argv[0]
|
||||
let cur_fn = self
|
||||
.cur_fn
|
||||
.as_deref()
|
||||
.map(|s| esc(s))
|
||||
.unwrap_or_else(|| String::from("") );
|
||||
if class_name == "CompareOperator" && !argv.is_empty() {
|
||||
let op_str = match &argv[0] {
|
||||
VMValue::String(s) => esc(s),
|
||||
_ => String::from("")
|
||||
};
|
||||
if let Some(nj) = nullish_json {
|
||||
eprintln!(
|
||||
"{{\"ev\":\"call\",\"class\":\"{}\",\"method\":\"{}\",\"argc\":{},\"fn\":\"{}\",\"op\":\"{}\",\"argk\":[{}],\"nullish\":[{}]}}",
|
||||
esc(&class_name), esc(&method_name), argv.len(), cur_fn, op_str, args_json, nj
|
||||
);
|
||||
} else {
|
||||
eprintln!(
|
||||
"{{\"ev\":\"call\",\"class\":\"{}\",\"method\":\"{}\",\"argc\":{},\"fn\":\"{}\",\"op\":\"{}\",\"argk\":[{}]}}",
|
||||
esc(&class_name), esc(&method_name), argv.len(), cur_fn, op_str, args_json
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if let Some(nj) = nullish_json {
|
||||
eprintln!(
|
||||
"{{\"ev\":\"call\",\"class\":\"{}\",\"method\":\"{}\",\"argc\":{},\"fn\":\"{}\",\"argk\":[{}],\"nullish\":[{}]}}",
|
||||
esc(&class_name), esc(&method_name), argv.len(), cur_fn, args_json, nj
|
||||
);
|
||||
} else {
|
||||
eprintln!(
|
||||
"{{\"ev\":\"call\",\"class\":\"{}\",\"method\":\"{}\",\"argc\":{},\"fn\":\"{}\",\"argk\":[{}]}}",
|
||||
esc(&class_name), esc(&method_name), argv.len(), cur_fn, args_json
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.box_trace_emit_call(&class_name, &method_name, argv.len());
|
||||
}
|
||||
}
|
||||
}
|
||||
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(
|
||||
&mut self,
|
||||
func_name: &str,
|
||||
args: &[ValueId],
|
||||
) -> Result<VMValue, VMError> {
|
||||
match func_name {
|
||||
name if name == "env.get" || name.starts_with("env.get/") => {
|
||||
// Route env.get global to extern handler
|
||||
return self.execute_extern_function("env.get", args);
|
||||
}
|
||||
name if name == "hostbridge.extern_invoke" || name.starts_with("hostbridge.extern_invoke/") => {
|
||||
// SSOT: delegate to extern dispatcher (provider). Keep legacy block unreachable.
|
||||
return self.execute_extern_function("hostbridge.extern_invoke", args);
|
||||
// Treat as extern_invoke in legacy/global-resolved form
|
||||
if args.len() < 3 {
|
||||
return Err(self.err_arg_count("hostbridge.extern_invoke", 3, args.len()));
|
||||
}
|
||||
let name = self.reg_load(args[0])?.to_string();
|
||||
let method = self.reg_load(args[1])?.to_string();
|
||||
let v = self.reg_load(args[2])?;
|
||||
let mut first_arg_str: Option<String> = None;
|
||||
match v {
|
||||
VMValue::BoxRef(b) => {
|
||||
if let Some(ab) = b.as_any().downcast_ref::<crate::boxes::array::ArrayBox>() {
|
||||
let idx: Box<dyn crate::box_trait::NyashBox> =
|
||||
Box::new(crate::box_trait::IntegerBox::new(0));
|
||||
let elem = ab.get(idx);
|
||||
first_arg_str = Some(elem.to_string_box().value);
|
||||
} else {
|
||||
first_arg_str = Some(b.to_string_box().value);
|
||||
}
|
||||
}
|
||||
_ => first_arg_str = Some(v.to_string()),
|
||||
}
|
||||
if std::env::var("HAKO_CABI_TRACE").ok().as_deref() == Some("1") {
|
||||
eprintln!("[hb:dispatch:calls] {} {}", name, method);
|
||||
}
|
||||
match (name.as_str(), method.as_str()) {
|
||||
("env.mirbuilder", "emit") => {
|
||||
if let Some(s) = first_arg_str {
|
||||
match crate::host_providers::mir_builder::program_json_to_mir_json(&s) {
|
||||
Ok(out) => Ok(VMValue::String(out)),
|
||||
Err(e) => Err(self.err_with_context("env.mirbuilder.emit", &e.to_string())),
|
||||
}
|
||||
} else {
|
||||
Err(self.err_invalid("extern_invoke env.mirbuilder.emit expects 1 arg"))
|
||||
}
|
||||
}
|
||||
("env.codegen", "emit_object") => {
|
||||
if let Some(s) = first_arg_str {
|
||||
let opts = crate::host_providers::llvm_codegen::Opts {
|
||||
out: None,
|
||||
nyrt: std::env::var("NYASH_EMIT_EXE_NYRT").ok().map(std::path::PathBuf::from),
|
||||
opt_level: std::env::var("HAKO_LLVM_OPT_LEVEL").ok().or(Some("0".to_string())),
|
||||
timeout_ms: None,
|
||||
};
|
||||
match crate::host_providers::llvm_codegen::mir_json_to_object(&s, opts) {
|
||||
Ok(p) => Ok(VMValue::String(p.to_string_lossy().into_owned())),
|
||||
Err(e) => Err(self.err_with_context("env.codegen.emit_object", &e.to_string())),
|
||||
}
|
||||
} else {
|
||||
Err(self.err_invalid("extern_invoke env.codegen.emit_object expects 1 arg"))
|
||||
}
|
||||
}
|
||||
("env.codegen", "link_object") => {
|
||||
// C-API route only; here args[2] is already loaded into `v`
|
||||
if std::env::var("NYASH_LLVM_USE_CAPI").ok().as_deref() != Some("1") ||
|
||||
std::env::var("HAKO_V1_EXTERN_PROVIDER_C_ABI").ok().as_deref() != Some("1") {
|
||||
return Err(self.err_invalid("env.codegen.link_object: C-API route disabled"));
|
||||
}
|
||||
let (obj_path, exe_out) = match v {
|
||||
VMValue::BoxRef(b) => {
|
||||
if let Some(ab) = b.as_any().downcast_ref::<crate::boxes::array::ArrayBox>() {
|
||||
let idx0: Box<dyn crate::box_trait::NyashBox> = Box::new(crate::box_trait::IntegerBox::new(0));
|
||||
let elem0 = ab.get(idx0).to_string_box().value;
|
||||
let mut exe: Option<String> = None;
|
||||
let idx1: Box<dyn crate::box_trait::NyashBox> = Box::new(crate::box_trait::IntegerBox::new(1));
|
||||
let e1 = ab.get(idx1).to_string_box().value;
|
||||
if !e1.is_empty() { exe = Some(e1); }
|
||||
(elem0, exe)
|
||||
} else { (b.to_string_box().value, None) }
|
||||
}
|
||||
_ => (v.to_string(), None),
|
||||
};
|
||||
let extra = std::env::var("HAKO_AOT_LDFLAGS").ok();
|
||||
let obj = std::path::PathBuf::from(obj_path);
|
||||
let exe = exe_out.map(std::path::PathBuf::from).unwrap_or_else(|| std::env::temp_dir().join("hako_link_out.exe"));
|
||||
match crate::host_providers::llvm_codegen::link_object_capi(&obj, &exe, extra.as_deref()) {
|
||||
Ok(()) => Ok(VMValue::String(exe.to_string_lossy().into_owned())),
|
||||
Err(e) => Err(self.err_with_context("env.codegen.link_object", &e.to_string()))
|
||||
}
|
||||
}
|
||||
("env.codegen", "link_object") => {
|
||||
// C-API route only; args[2] is expected to be an ArrayBox [obj_path, exe_out?]
|
||||
if std::env::var("NYASH_LLVM_USE_CAPI").ok().as_deref() != Some("1") ||
|
||||
std::env::var("HAKO_V1_EXTERN_PROVIDER_C_ABI").ok().as_deref() != Some("1") {
|
||||
return Err(self.err_invalid("env.codegen.link_object: C-API route disabled"));
|
||||
}
|
||||
if args.len() < 3 { return Err(self.err_arg_count("env.codegen.link_object", 3, args.len())); }
|
||||
let v = self.reg_load(args[2])?;
|
||||
let (obj_path, exe_out) = match v {
|
||||
VMValue::BoxRef(b) => {
|
||||
if let Some(ab) = b.as_any().downcast_ref::<crate::boxes::array::ArrayBox>() {
|
||||
let idx0: Box<dyn crate::box_trait::NyashBox> = Box::new(crate::box_trait::IntegerBox::new(0));
|
||||
let elem0 = ab.get(idx0).to_string_box().value;
|
||||
let mut exe: Option<String> = None;
|
||||
let idx1: Box<dyn crate::box_trait::NyashBox> = Box::new(crate::box_trait::IntegerBox::new(1));
|
||||
let e1 = ab.get(idx1).to_string_box().value;
|
||||
if !e1.is_empty() { exe = Some(e1); }
|
||||
(elem0, exe)
|
||||
} else { (b.to_string_box().value, None) }
|
||||
}
|
||||
_ => (v.to_string(), None),
|
||||
};
|
||||
let extra = std::env::var("HAKO_AOT_LDFLAGS").ok();
|
||||
let obj = std::path::PathBuf::from(obj_path);
|
||||
let exe = exe_out.map(std::path::PathBuf::from).unwrap_or_else(|| std::env::temp_dir().join("hako_link_out.exe"));
|
||||
match crate::host_providers::llvm_codegen::link_object_capi(&obj, &exe, extra.as_deref()) {
|
||||
Ok(()) => Ok(VMValue::String(exe.to_string_lossy().into_owned())),
|
||||
Err(e) => Err(self.err_with_context("env.codegen.link_object", &e.to_string()))
|
||||
}
|
||||
}
|
||||
("env.codegen", "link_object") => {
|
||||
if std::env::var("NYASH_LLVM_USE_CAPI").ok().as_deref() != Some("1") ||
|
||||
std::env::var("HAKO_V1_EXTERN_PROVIDER_C_ABI").ok().as_deref() != Some("1") {
|
||||
return Err(self.err_invalid("env.codegen.link_object: C-API route disabled"));
|
||||
}
|
||||
// Extract from args[2] (ArrayBox): [obj_path, exe_out?]
|
||||
let v = self.reg_load(args[2])?;
|
||||
let (obj_path, exe_out) = match v {
|
||||
VMValue::BoxRef(b) => {
|
||||
if let Some(ab) = b.as_any().downcast_ref::<crate::boxes::array::ArrayBox>() {
|
||||
let idx0: Box<dyn crate::box_trait::NyashBox> = Box::new(crate::box_trait::IntegerBox::new(0));
|
||||
let elem0 = ab.get(idx0).to_string_box().value;
|
||||
let mut exe: Option<String> = None;
|
||||
let idx1: Box<dyn crate::box_trait::NyashBox> = Box::new(crate::box_trait::IntegerBox::new(1));
|
||||
let e1 = ab.get(idx1).to_string_box().value;
|
||||
if !e1.is_empty() { exe = Some(e1); }
|
||||
(elem0, exe)
|
||||
} else { (b.to_string_box().value, None) }
|
||||
}
|
||||
_ => (v.to_string(), None),
|
||||
};
|
||||
let extra = std::env::var("HAKO_AOT_LDFLAGS").ok();
|
||||
let obj = std::path::PathBuf::from(obj_path);
|
||||
let exe = exe_out.map(std::path::PathBuf::from).unwrap_or_else(|| std::env::temp_dir().join("hako_link_out.exe"));
|
||||
match crate::host_providers::llvm_codegen::link_object_capi(&obj, &exe, extra.as_deref()) {
|
||||
Ok(()) => Ok(VMValue::String(exe.to_string_lossy().into_owned())),
|
||||
Err(e) => Err(self.err_with_context("env.codegen.link_object", &e.to_string()))
|
||||
}
|
||||
}
|
||||
("env.codegen", "link_object") => {
|
||||
if std::env::var("NYASH_LLVM_USE_CAPI").ok().as_deref() != Some("1") ||
|
||||
std::env::var("HAKO_V1_EXTERN_PROVIDER_C_ABI").ok().as_deref() != Some("1") {
|
||||
return Err(self.err_invalid("env.codegen.link_object: C-API route disabled"));
|
||||
}
|
||||
// Here args[2] is already loaded into `v`; parse ArrayBox [obj, exe?]
|
||||
let (obj_path, exe_out) = match v {
|
||||
VMValue::BoxRef(b) => {
|
||||
if let Some(ab) = b.as_any().downcast_ref::<crate::boxes::array::ArrayBox>() {
|
||||
let idx0: Box<dyn crate::box_trait::NyashBox> = Box::new(crate::box_trait::IntegerBox::new(0));
|
||||
let elem0 = ab.get(idx0).to_string_box().value;
|
||||
let mut exe: Option<String> = None;
|
||||
let idx1: Box<dyn crate::box_trait::NyashBox> = Box::new(crate::box_trait::IntegerBox::new(1));
|
||||
let e1 = ab.get(idx1).to_string_box().value;
|
||||
if !e1.is_empty() { exe = Some(e1); }
|
||||
(elem0, exe)
|
||||
} else { (b.to_string_box().value, None) }
|
||||
}
|
||||
_ => (v.to_string(), None),
|
||||
};
|
||||
let extra = std::env::var("HAKO_AOT_LDFLAGS").ok();
|
||||
let obj = std::path::PathBuf::from(obj_path);
|
||||
let exe = exe_out.map(std::path::PathBuf::from).unwrap_or_else(|| std::env::temp_dir().join("hako_link_out.exe"));
|
||||
match crate::host_providers::llvm_codegen::link_object_capi(&obj, &exe, extra.as_deref()) {
|
||||
Ok(()) => Ok(VMValue::String(exe.to_string_lossy().into_owned())),
|
||||
Err(e) => Err(self.err_with_context("env.codegen.link_object", &e.to_string()))
|
||||
}
|
||||
}
|
||||
_ => {
|
||||
// Last-chance bridge: some older dispatch paths may fall through here
|
||||
if name == "env.codegen" && method == "link_object" {
|
||||
if std::env::var("NYASH_LLVM_USE_CAPI").ok().as_deref() != Some("1") ||
|
||||
std::env::var("HAKO_V1_EXTERN_PROVIDER_C_ABI").ok().as_deref() != Some("1") {
|
||||
return Err(self.err_invalid("env.codegen.link_object: C-API route disabled"));
|
||||
}
|
||||
// Expect args[2] as ArrayBox [obj, exe?]
|
||||
if args.len() >= 3 {
|
||||
let v = self.reg_load(args[2])?;
|
||||
let (obj_path, exe_out) = match v {
|
||||
VMValue::BoxRef(b) => {
|
||||
if let Some(ab) = b.as_any().downcast_ref::<crate::boxes::array::ArrayBox>() {
|
||||
let i0: Box<dyn crate::box_trait::NyashBox> = Box::new(crate::box_trait::IntegerBox::new(0));
|
||||
let s0 = ab.get(i0).to_string_box().value;
|
||||
let mut e: Option<String> = None;
|
||||
let i1: Box<dyn crate::box_trait::NyashBox> = Box::new(crate::box_trait::IntegerBox::new(1));
|
||||
let s1 = ab.get(i1).to_string_box().value;
|
||||
if !s1.is_empty() { e = Some(s1); }
|
||||
(s0, e)
|
||||
} else { (b.to_string_box().value, None) }
|
||||
}
|
||||
_ => (v.to_string(), None),
|
||||
};
|
||||
let extra = std::env::var("HAKO_AOT_LDFLAGS").ok();
|
||||
let obj = std::path::PathBuf::from(obj_path);
|
||||
let exe = exe_out.map(std::path::PathBuf::from).unwrap_or_else(|| std::env::temp_dir().join("hako_link_out.exe"));
|
||||
match crate::host_providers::llvm_codegen::link_object_capi(&obj, &exe, extra.as_deref()) {
|
||||
Ok(()) => return Ok(VMValue::String(exe.to_string_lossy().into_owned())),
|
||||
Err(e) => return Err(self.err_with_context("env.codegen.link_object", &e.to_string())),
|
||||
}
|
||||
}
|
||||
}
|
||||
// As a final safety, try provider dispatcher again
|
||||
if let Some(res) = self.extern_provider_dispatch("hostbridge.extern_invoke", args) {
|
||||
return res;
|
||||
}
|
||||
if std::env::var("HAKO_CABI_TRACE").ok().as_deref() == Some("1") {
|
||||
eprintln!("[hb:unsupported:calls] {}.{}", name, method);
|
||||
}
|
||||
Err(self.err_invalid(format!(
|
||||
"hostbridge.extern_invoke unsupported for {}.{} [calls]",
|
||||
name, method
|
||||
)))
|
||||
},
|
||||
}
|
||||
}
|
||||
"nyash.builtin.print" | "print" | "nyash.console.log" => {
|
||||
if let Some(arg_id) = args.get(0) {
|
||||
let val = self.reg_load(*arg_id)?;
|
||||
// Dev-only: print trace (kind/class) before actual print
|
||||
if Self::print_trace_enabled() { self.print_trace_emit(&val); }
|
||||
// Dev observe: Null/Missing boxes quick normalization (no behavior change to prod)
|
||||
if let VMValue::BoxRef(bx) = &val {
|
||||
// NullBox → always print as null (stable)
|
||||
if bx.as_any().downcast_ref::<crate::boxes::null_box::NullBox>().is_some() {
|
||||
println!("null");
|
||||
return Ok(VMValue::Void);
|
||||
}
|
||||
// MissingBox → default prints as null; when flag ON, show (missing)
|
||||
if bx.as_any().downcast_ref::<crate::boxes::missing_box::MissingBox>().is_some() {
|
||||
if crate::config::env::null_missing_box_enabled() {
|
||||
println!("(missing)");
|
||||
} else {
|
||||
println!("null");
|
||||
}
|
||||
return Ok(VMValue::Void);
|
||||
}
|
||||
}
|
||||
// Dev: treat VM Void and BoxRef(VoidBox) as JSON null for print
|
||||
match &val {
|
||||
VMValue::Void => {
|
||||
println!("null");
|
||||
return Ok(VMValue::Void);
|
||||
}
|
||||
VMValue::BoxRef(bx) => {
|
||||
if bx.as_any().downcast_ref::<crate::box_trait::VoidBox>().is_some() {
|
||||
println!("null");
|
||||
return Ok(VMValue::Void);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
// Print raw strings directly (avoid double quoting via StringifyOperator)
|
||||
match &val {
|
||||
VMValue::String(s) => { println!("{}", s); return Ok(VMValue::Void); }
|
||||
VMValue::BoxRef(bx) => {
|
||||
if let Some(sb) = bx.as_any().downcast_ref::<crate::box_trait::StringBox>() {
|
||||
println!("{}", sb.value);
|
||||
return Ok(VMValue::Void);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
// Operator Box (Stringify) – dev flag gated
|
||||
if std::env::var("NYASH_OPERATOR_BOX_STRINGIFY").ok().as_deref() == Some("1") {
|
||||
if let Some(op) = self.functions.get("StringifyOperator.apply/1").cloned() {
|
||||
let out = self.exec_function_inner(&op, Some(&[val.clone()]))?;
|
||||
println!("{}", out.to_string());
|
||||
} else {
|
||||
println!("{}", val.to_string());
|
||||
}
|
||||
} else {
|
||||
println!("{}", val.to_string());
|
||||
}
|
||||
}
|
||||
Ok(VMValue::Void)
|
||||
}
|
||||
"nyash.builtin.error" => {
|
||||
if let Some(arg_id) = args.get(0) {
|
||||
let val = self.reg_load(*arg_id)?;
|
||||
eprintln!("Error: {}", val.to_string());
|
||||
}
|
||||
Ok(VMValue::Void)
|
||||
}
|
||||
_ => Err(self.err_with_context("global function", &format!("Unknown: {}", func_name))),
|
||||
}
|
||||
}
|
||||
|
||||
fn execute_method_call(
|
||||
&mut self,
|
||||
receiver: &VMValue,
|
||||
method: &str,
|
||||
args: &[ValueId],
|
||||
) -> Result<VMValue, VMError> {
|
||||
match receiver {
|
||||
VMValue::String(s) => match method {
|
||||
"length" => Ok(VMValue::Integer(s.len() as i64)),
|
||||
"concat" => {
|
||||
if let Some(arg_id) = args.get(0) {
|
||||
let arg_val = self.reg_load(*arg_id)?;
|
||||
let new_str = format!("{}{}", s, arg_val.to_string());
|
||||
Ok(VMValue::String(new_str))
|
||||
} else {
|
||||
Err(self.err_invalid("concat requires 1 argument"))
|
||||
}
|
||||
}
|
||||
"replace" => {
|
||||
if args.len() == 2 {
|
||||
let old = self.reg_load(args[0])?.to_string();
|
||||
let new = self.reg_load(args[1])?.to_string();
|
||||
Ok(VMValue::String(s.replace(&old, &new)))
|
||||
} else {
|
||||
Err(self.err_invalid("replace requires 2 arguments"))
|
||||
}
|
||||
}
|
||||
"indexOf" => {
|
||||
if let Some(arg_id) = args.get(0) {
|
||||
let needle = self.reg_load(*arg_id)?.to_string();
|
||||
let idx = s.find(&needle).map(|i| i as i64).unwrap_or(-1);
|
||||
Ok(VMValue::Integer(idx))
|
||||
} else {
|
||||
Err(self.err_invalid("indexOf requires 1 argument"))
|
||||
}
|
||||
}
|
||||
"lastIndexOf" => {
|
||||
if let Some(arg_id) = args.get(0) {
|
||||
let needle = self.reg_load(*arg_id)?.to_string();
|
||||
let idx = s.rfind(&needle).map(|i| i as i64).unwrap_or(-1);
|
||||
Ok(VMValue::Integer(idx))
|
||||
} else {
|
||||
Err(self.err_invalid("lastIndexOf requires 1 argument"))
|
||||
}
|
||||
}
|
||||
"substring" => {
|
||||
let start = if let Some(a0) = args.get(0) {
|
||||
self.reg_load(*a0)?.as_integer().unwrap_or(0)
|
||||
} else { 0 };
|
||||
let end = if let Some(a1) = args.get(1) {
|
||||
self.reg_load(*a1)?.as_integer().unwrap_or(s.len() as i64)
|
||||
} else { s.len() as i64 };
|
||||
let len = s.len() as i64;
|
||||
let i0 = start.max(0).min(len) as usize;
|
||||
let i1 = end.max(0).min(len) as usize;
|
||||
if i0 > i1 { return Ok(VMValue::String(String::new())); }
|
||||
// Note: operating on bytes; Nyash strings are UTF‑8, but tests are ASCII only here
|
||||
let bytes = s.as_bytes();
|
||||
let sub = String::from_utf8(bytes[i0..i1].to_vec()).unwrap_or_default();
|
||||
Ok(VMValue::String(sub))
|
||||
}
|
||||
_ => Err(self.err_method_not_found("String", method)),
|
||||
},
|
||||
VMValue::BoxRef(box_ref) => {
|
||||
// Try builtin StringBox first
|
||||
if let Some(string_box) = box_ref
|
||||
.as_any()
|
||||
.downcast_ref::<crate::boxes::string_box::StringBox>()
|
||||
{
|
||||
match method {
|
||||
"lastIndexOf" => {
|
||||
if let Some(arg_id) = args.get(0) {
|
||||
let needle = self.reg_load(*arg_id)?.to_string();
|
||||
let result_box = string_box.lastIndexOf(&needle);
|
||||
Ok(VMValue::from_nyash_box(result_box))
|
||||
} else {
|
||||
Err(self.err_invalid("lastIndexOf requires 1 argument"))
|
||||
}
|
||||
}
|
||||
"indexOf" | "find" => {
|
||||
if let Some(arg_id) = args.get(0) {
|
||||
let needle = self.reg_load(*arg_id)?.to_string();
|
||||
let result_box = string_box.find(&needle);
|
||||
Ok(VMValue::from_nyash_box(result_box))
|
||||
} else {
|
||||
Err(self.err_invalid("indexOf/find requires 1 argument"))
|
||||
}
|
||||
}
|
||||
_ => Err(self.err_method_not_found("StringBox", method)),
|
||||
}
|
||||
} else if let Some(p) = box_ref
|
||||
.as_any()
|
||||
.downcast_ref::<crate::runtime::plugin_loader_v2::PluginBoxV2>()
|
||||
{
|
||||
let host = crate::runtime::plugin_loader_unified::get_global_plugin_host();
|
||||
let host = host.read().unwrap();
|
||||
let argv = self.load_args_as_boxes(args)?;
|
||||
match host.invoke_instance_method(
|
||||
&p.box_type,
|
||||
method,
|
||||
p.inner.instance_id,
|
||||
&argv,
|
||||
) {
|
||||
Ok(Some(ret)) => Ok(VMValue::from_nyash_box(ret)),
|
||||
Ok(None) => Ok(VMValue::Void),
|
||||
Err(e) => Err(self.err_with_context(
|
||||
&format!("Plugin method {}.{}", p.box_type, method),
|
||||
&format!("{:?}", e)
|
||||
)),
|
||||
}
|
||||
} else {
|
||||
Err(self.err_method_not_found(&box_ref.type_name(), method))
|
||||
}
|
||||
}
|
||||
_ => Err(self.err_with_context("method call", &format!("{} not supported on {:?}", method, receiver))),
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: user-defined function dispatch for Global calls
|
||||
// If none of the above extern/provider/global bridges matched,
|
||||
// try to resolve and execute a user function present in this module.
|
||||
{
|
||||
// Use unique-tail resolver against snapshot of self.functions
|
||||
let fname = call_resolution::resolve_function_name(
|
||||
func_name,
|
||||
args.len(),
|
||||
&self.functions,
|
||||
self.cur_fn.as_deref(),
|
||||
);
|
||||
if let Some(fname) = fname {
|
||||
if let Some(func) = self.functions.get(&fname).cloned() {
|
||||
// Load arguments and execute
|
||||
let mut argv: Vec<VMValue> = Vec::new();
|
||||
for a in args { argv.push(self.reg_load(*a)?); }
|
||||
if std::env::var("NYASH_VM_CALL_TRACE").ok().as_deref() == Some("1") {
|
||||
eprintln!("[vm] global-call resolved '{}' -> '{}'", func_name, fname);
|
||||
}
|
||||
return self.exec_function_inner(&func, Some(&argv));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn execute_extern_function(
|
||||
&mut self,
|
||||
extern_name: &str,
|
||||
args: &[ValueId],
|
||||
) -> Result<VMValue, VMError> {
|
||||
if let Some(res) = self.extern_provider_dispatch(extern_name, args) {
|
||||
if std::env::var("HAKO_CABI_TRACE").ok().as_deref() == Some("1") {
|
||||
eprintln!("[hb:dispatch:calls] provider {}", extern_name);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
match extern_name {
|
||||
// Minimal console externs
|
||||
"nyash.console.log" | "env.console.log" | "print" | "nyash.builtin.print" => {
|
||||
if let Some(arg_id) = args.get(0) {
|
||||
let v = self.reg_load(*arg_id)?;
|
||||
println!("{}", v.to_string());
|
||||
} else {
|
||||
println!("");
|
||||
}
|
||||
Ok(VMValue::Void)
|
||||
}
|
||||
// Direct provider calls (bypass hostbridge.extern_invoke)
|
||||
// Above provider covers env.* family; keep legacy fallbacks below
|
||||
"exit" => {
|
||||
let code = if let Some(arg_id) = args.get(0) {
|
||||
self.reg_load(*arg_id)?.as_integer().unwrap_or(0)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
std::process::exit(code as i32);
|
||||
}
|
||||
"panic" => {
|
||||
let msg = if let Some(arg_id) = args.get(0) {
|
||||
self.reg_load(*arg_id)?.to_string()
|
||||
} else {
|
||||
"VM panic".to_string()
|
||||
};
|
||||
panic!("{}", msg);
|
||||
}
|
||||
"hostbridge.extern_invoke" => Err(self.err_invalid("hostbridge.extern_invoke should be routed via extern_provider_dispatch")),
|
||||
_ => Err(self.err_with_context("extern function", &format!("Unknown: {}", extern_name))),
|
||||
}
|
||||
}
|
||||
}
|
||||
13
src/backend/mir_interpreter/handlers/calls/LAYER_GUARD.md
Normal file
13
src/backend/mir_interpreter/handlers/calls/LAYER_GUARD.md
Normal file
@ -0,0 +1,13 @@
|
||||
Layer Guard — handlers/calls
|
||||
|
||||
Scope
|
||||
- Route MIR Call by callee kind (Global/Method/Extern/Legacy only).
|
||||
- Keep legacy resolver isolated for phased removal.
|
||||
|
||||
Allowed
|
||||
- Use `super::*` and `super::super::utils::*` helpers (e.g., `normalize_arity_suffix`).
|
||||
|
||||
Forbidden
|
||||
- Direct provider/registry imports from runtime or plugins.
|
||||
- Code generation or MIR building is out of scope.
|
||||
|
||||
27
src/backend/mir_interpreter/handlers/calls/README.md
Normal file
27
src/backend/mir_interpreter/handlers/calls/README.md
Normal file
@ -0,0 +1,27 @@
|
||||
# handlers/calls — Call Handling Layer
|
||||
|
||||
Purpose: isolate call handling by callee kind for the MIR interpreter.
|
||||
|
||||
- In scope: routing `handle_call`, per‑callee executors, legacy name resolution.
|
||||
- Out of scope: arithmetic/box handlers, memory ops, extern provider registry.
|
||||
|
||||
Do not import sibling handler modules directly from here (keep boundaries tight).
|
||||
Use `super::*` only and call through the interpreter methods.
|
||||
|
||||
Files
|
||||
- `mod.rs`: entry point and callee routing
|
||||
- `global.rs`: global function calls (Callee::Global)
|
||||
- `method.rs`: instance/static method calls (Callee::Method)
|
||||
- `externs.rs`: extern calls (Callee::Extern)
|
||||
|
||||
Removal status (Phase 2 complete)
|
||||
- Unified callee path is default。by‑name 旧経路は削除済み(`legacy.rs`/`resolution.rs`)。
|
||||
- `callee=None` の呼び出しは「厳密一致のモジュール関数名」以外は Fail‑Fast(ビルダーで Callee を付与してね)。
|
||||
|
||||
Extern SSOT
|
||||
- `externs.rs` is the runtime SSOT for provider dispatch. Global calls that are extern-like should delegate here (e.g., `env.get`).
|
||||
- Arity suffix normalization: names like `env.get/1` are accepted and normalized to `env.get` before dispatch (both in Global and ExternCall paths).
|
||||
|
||||
Layer Guard
|
||||
- Scope: call routing only (Global/Method/Extern/Legacy isolation). Do not import provider registries or runtime plugins directly from here.
|
||||
- Use helpers under `super::super::utils::*` for shared concerns (e.g., `normalize_arity_suffix`).
|
||||
63
src/backend/mir_interpreter/handlers/calls/externs.rs
Normal file
63
src/backend/mir_interpreter/handlers/calls/externs.rs
Normal file
@ -0,0 +1,63 @@
|
||||
use super::*;
|
||||
|
||||
impl MirInterpreter {
|
||||
pub(super) fn execute_extern_function(
|
||||
&mut self,
|
||||
extern_name: &str,
|
||||
args: &[ValueId],
|
||||
) -> Result<VMValue, VMError> {
|
||||
// Normalize arity suffix (e.g., "env.get/1" -> "env.get")
|
||||
let base = super::super::utils::normalize_arity_suffix(extern_name);
|
||||
if let Some(res) = self.extern_provider_dispatch(base, args) {
|
||||
if std::env::var("HAKO_CABI_TRACE").ok().as_deref() == Some("1") {
|
||||
eprintln!("[hb:dispatch:calls] provider {}", base);
|
||||
}
|
||||
return res;
|
||||
}
|
||||
match base {
|
||||
// Minimal console externs
|
||||
"nyash.console.log" | "env.console.log" | "print" | "nyash.builtin.print" => {
|
||||
if let Some(arg_id) = args.get(0) {
|
||||
let v = self.reg_load(*arg_id)?;
|
||||
match &v {
|
||||
VMValue::Void => println!("null"),
|
||||
VMValue::BoxRef(bx) => {
|
||||
if bx.as_any().downcast_ref::<crate::box_trait::VoidBox>().is_some() {
|
||||
println!("null");
|
||||
} else if let Some(sb) = bx.as_any().downcast_ref::<crate::box_trait::StringBox>() {
|
||||
println!("{}", sb.value);
|
||||
} else {
|
||||
println!("{}", v.to_string());
|
||||
}
|
||||
}
|
||||
VMValue::String(s) => println!("{}", s),
|
||||
_ => println!("{}", v.to_string()),
|
||||
}
|
||||
} else {
|
||||
println!("");
|
||||
}
|
||||
Ok(VMValue::Void)
|
||||
}
|
||||
// Direct provider calls (bypass hostbridge.extern_invoke)
|
||||
// Above provider covers env.* family; keep legacy fallbacks below
|
||||
"exit" => {
|
||||
let code = if let Some(arg_id) = args.get(0) {
|
||||
self.reg_load(*arg_id)?.as_integer().unwrap_or(0)
|
||||
} else {
|
||||
0
|
||||
};
|
||||
std::process::exit(code as i32);
|
||||
}
|
||||
"panic" => {
|
||||
let msg = if let Some(arg_id) = args.get(0) {
|
||||
self.reg_load(*arg_id)?.to_string()
|
||||
} else {
|
||||
"VM panic".to_string()
|
||||
};
|
||||
panic!("{}", msg);
|
||||
}
|
||||
"hostbridge.extern_invoke" => Err(self.err_invalid("hostbridge.extern_invoke should be routed via extern_provider_dispatch")),
|
||||
_ => Err(self.err_with_context("extern function", &format!("Unknown: {}", extern_name))),
|
||||
}
|
||||
}
|
||||
}
|
||||
115
src/backend/mir_interpreter/handlers/calls/global.rs
Normal file
115
src/backend/mir_interpreter/handlers/calls/global.rs
Normal file
@ -0,0 +1,115 @@
|
||||
use super::*;
|
||||
|
||||
impl MirInterpreter {
|
||||
pub(super) fn execute_global_function(
|
||||
&mut self,
|
||||
func_name: &str,
|
||||
args: &[ValueId],
|
||||
) -> Result<VMValue, VMError> {
|
||||
// Normalize arity suffix for extern-like dispatch, but keep original name
|
||||
// for module-local function table lookup (functions may carry arity suffix).
|
||||
let base = super::super::utils::normalize_arity_suffix(func_name);
|
||||
// Module-local/global function: execute by function table if present (use original name)
|
||||
if let Some(func) = self.functions.get(func_name).cloned() {
|
||||
let mut argv: Vec<VMValue> = Vec::with_capacity(args.len());
|
||||
for a in args { argv.push(self.reg_load(*a)?); }
|
||||
return self.exec_function_inner(&func, Some(&argv));
|
||||
}
|
||||
|
||||
match base {
|
||||
// Console-like globals
|
||||
"print" | "nyash.builtin.print" => {
|
||||
// Reuse extern handler for consistency with other console names
|
||||
return self.execute_extern_function("print", args);
|
||||
}
|
||||
"error" => {
|
||||
if let Some(arg_id) = args.get(0) {
|
||||
let val = self.reg_load(*arg_id)?;
|
||||
eprintln!("Error: {}", val.to_string());
|
||||
}
|
||||
return Ok(VMValue::Void);
|
||||
}
|
||||
"panic" => {
|
||||
return self.execute_extern_function("panic", args);
|
||||
}
|
||||
"exit" => {
|
||||
return self.execute_extern_function("exit", args);
|
||||
}
|
||||
"env.get" => {
|
||||
// Route env.get global to extern handler
|
||||
return self.execute_extern_function("env.get", args);
|
||||
}
|
||||
"hostbridge.extern_invoke" => {
|
||||
// SSOT: delegate to extern dispatcher (provider)
|
||||
return self.execute_extern_function("hostbridge.extern_invoke", args);
|
||||
}
|
||||
// LLVM harness providers (direct)
|
||||
"env.mirbuilder.emit" | "env.mirbuilder.emit/1" => {
|
||||
if let Some(a0) = args.get(0) {
|
||||
let s = self.reg_load(*a0)?.to_string();
|
||||
match crate::host_providers::mir_builder::program_json_to_mir_json(&s) {
|
||||
Ok(out) => Ok(VMValue::String(out)),
|
||||
Err(e) => Err(self.err_with_context("env.mirbuilder.emit", &e.to_string())),
|
||||
}
|
||||
} else {
|
||||
Err(self.err_invalid("env.mirbuilder.emit expects 1 arg"))
|
||||
}
|
||||
}
|
||||
"env.codegen.emit_object" | "env.codegen.emit_object/1" => {
|
||||
if let Some(a0) = args.get(0) {
|
||||
let s = self.reg_load(*a0)?.to_string();
|
||||
let opts = crate::host_providers::llvm_codegen::Opts {
|
||||
out: None,
|
||||
nyrt: std::env::var("NYASH_EMIT_EXE_NYRT").ok().map(std::path::PathBuf::from),
|
||||
opt_level: std::env::var("HAKO_LLVM_OPT_LEVEL").ok().or(Some("0".to_string())),
|
||||
timeout_ms: None,
|
||||
};
|
||||
match crate::host_providers::llvm_codegen::mir_json_to_object(&s, opts) {
|
||||
Ok(p) => Ok(VMValue::String(p.to_string_lossy().into_owned())),
|
||||
Err(e) => Err(self.err_with_context("env.codegen.emit_object", &e.to_string())),
|
||||
}
|
||||
} else {
|
||||
Err(self.err_invalid("env.codegen.emit_object expects 1 arg"))
|
||||
}
|
||||
}
|
||||
"env.codegen.link_object" | "env.codegen.link_object/3" => {
|
||||
// C-API route only; args[2] is expected to be an ArrayBox [obj_path, exe_out?]
|
||||
if std::env::var("NYASH_LLVM_USE_CAPI").ok().as_deref() != Some("1") ||
|
||||
std::env::var("HAKO_V1_EXTERN_PROVIDER_C_ABI").ok().as_deref() != Some("1") {
|
||||
return Err(self.err_invalid("env.codegen.link_object: C-API route disabled"));
|
||||
}
|
||||
if args.len() < 3 { return Err(self.err_arg_count("env.codegen.link_object", 3, args.len())); }
|
||||
let v = self.reg_load(args[2])?;
|
||||
let (obj_path, exe_out) = match v {
|
||||
VMValue::BoxRef(b) => {
|
||||
if let Some(ab) = b.as_any().downcast_ref::<crate::boxes::array::ArrayBox>() {
|
||||
let idx0: Box<dyn crate::box_trait::NyashBox> = Box::new(crate::box_trait::IntegerBox::new(0));
|
||||
let elem0 = ab.get(idx0).to_string_box().value;
|
||||
let mut exe: Option<String> = None;
|
||||
let idx1: Box<dyn crate::box_trait::NyashBox> = Box::new(crate::box_trait::IntegerBox::new(1));
|
||||
let e1 = ab.get(idx1).to_string_box().value;
|
||||
if !e1.is_empty() { exe = Some(e1); }
|
||||
(elem0, exe)
|
||||
} else { (b.to_string_box().value, None) }
|
||||
}
|
||||
_ => (v.to_string(), None),
|
||||
};
|
||||
let extra = std::env::var("HAKO_AOT_LDFLAGS").ok();
|
||||
let obj = std::path::PathBuf::from(obj_path);
|
||||
let exe = exe_out.map(std::path::PathBuf::from).unwrap_or_else(|| std::env::temp_dir().join("hako_link_out.exe"));
|
||||
match crate::host_providers::llvm_codegen::link_object_capi(&obj, &exe, extra.as_deref()) {
|
||||
Ok(()) => Ok(VMValue::String(exe.to_string_lossy().into_owned())),
|
||||
Err(e) => Err(self.err_with_context("env.codegen.link_object", &e.to_string()))
|
||||
}
|
||||
}
|
||||
"nyash.builtin.error" => {
|
||||
if let Some(arg_id) = args.get(0) {
|
||||
let val = self.reg_load(*arg_id)?;
|
||||
eprintln!("Error: {}", val.to_string());
|
||||
}
|
||||
Ok(VMValue::Void)
|
||||
}
|
||||
_ => Err(self.err_with_context("global function", &format!("Unknown: {}", func_name))),
|
||||
}
|
||||
}
|
||||
}
|
||||
234
src/backend/mir_interpreter/handlers/calls/method.rs
Normal file
234
src/backend/mir_interpreter/handlers/calls/method.rs
Normal file
@ -0,0 +1,234 @@
|
||||
use super::*;
|
||||
|
||||
impl MirInterpreter {
|
||||
pub(super) fn execute_method_callee(
|
||||
&mut self,
|
||||
box_name: &str,
|
||||
method: &str,
|
||||
receiver: &Option<ValueId>,
|
||||
args: &[ValueId],
|
||||
) -> Result<VMValue, VMError> {
|
||||
if let Some(recv_id) = receiver {
|
||||
// Primary: load receiver by id. If undefined due to builder localization gap,
|
||||
// try to auto-locate the most recent `NewBox <box_name>` in the current block
|
||||
// (same fn/last_block) and use its dst as the receiver. This is a structural
|
||||
// recovery, not a by-name fallback, and keeps semantics stable for plugin boxes.
|
||||
let recv_val = match self.reg_load(*recv_id) {
|
||||
Ok(v) => v,
|
||||
Err(e) => {
|
||||
// Attempt structured autoscan for receiver in current block
|
||||
if let (Some(cur_fn), Some(bb)) = (self.cur_fn.clone(), self.last_block) {
|
||||
if let Some(func) = self.functions.get(&cur_fn) {
|
||||
if let Some(block) = func.blocks.get(&bb) {
|
||||
let mut last_recv: Option<ValueId> = None;
|
||||
for inst in &block.instructions {
|
||||
if let crate::mir::MirInstruction::NewBox { dst, box_type, .. } = inst {
|
||||
if box_type == box_name { last_recv = Some(*dst); }
|
||||
}
|
||||
}
|
||||
if let Some(rid) = last_recv {
|
||||
if let Ok(v) = self.reg_load(rid) { v } else { return Err(e); }
|
||||
} else {
|
||||
// Dev fallback (guarded): use args[0] as surrogate receiver if explicitly allowed
|
||||
let tolerate = std::env::var("NYASH_VM_RECV_ARG_FALLBACK").ok().as_deref() == Some("1")
|
||||
|| std::env::var("NYASH_VM_TOLERATE_VOID").ok().as_deref() == Some("1");
|
||||
if tolerate { if let Some(a0) = args.get(0) { self.reg_load(*a0)? } else { return Err(e); } }
|
||||
else { return Err(e); }
|
||||
}
|
||||
} else {
|
||||
return Err(e);
|
||||
}
|
||||
} else {
|
||||
return Err(e);
|
||||
}
|
||||
} else {
|
||||
// Dev fallback (guarded): use args[0] as surrogate receiver if explicitly allowed
|
||||
let tolerate = std::env::var("NYASH_VM_RECV_ARG_FALLBACK").ok().as_deref() == Some("1")
|
||||
|| std::env::var("NYASH_VM_TOLERATE_VOID").ok().as_deref() == Some("1");
|
||||
if tolerate { if let Some(a0) = args.get(0) { self.reg_load(*a0)? } else { return Err(e); } }
|
||||
else { return Err(e); }
|
||||
}
|
||||
}
|
||||
};
|
||||
let dev_trace = std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1");
|
||||
// Fast bridge for builtin boxes (Array) and common methods.
|
||||
// Preserve legacy semantics when plugins are absent.
|
||||
if let VMValue::BoxRef(bx) = &recv_val {
|
||||
// ArrayBox bridge
|
||||
if let Some(arr) = bx.as_any().downcast_ref::<crate::boxes::array::ArrayBox>() {
|
||||
match method {
|
||||
"birth" => { return Ok(VMValue::Void); }
|
||||
"push" => {
|
||||
if let Some(a0) = args.get(0) {
|
||||
let v = self.load_as_box(*a0)?;
|
||||
let _ = arr.push(v);
|
||||
return Ok(VMValue::Void);
|
||||
}
|
||||
}
|
||||
"len" | "length" | "size" => {
|
||||
let ret = arr.length();
|
||||
return Ok(VMValue::from_nyash_box(ret));
|
||||
}
|
||||
"get" => {
|
||||
if let Some(a0) = args.get(0) {
|
||||
let idx = self.load_as_box(*a0)?;
|
||||
let ret = arr.get(idx);
|
||||
return Ok(VMValue::from_nyash_box(ret));
|
||||
}
|
||||
}
|
||||
"set" => {
|
||||
if args.len() >= 2 {
|
||||
let idx = self.load_as_box(args[0])?;
|
||||
let val = self.load_as_box(args[1])?;
|
||||
let _ = arr.set(idx, val);
|
||||
return Ok(VMValue::Void);
|
||||
}
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
// Minimal bridge for birth(): delegate to BoxCall handler and return Void
|
||||
if method == "birth" {
|
||||
let _ = self.handle_box_call(None, *recv_id, &method.to_string(), args)?;
|
||||
return Ok(VMValue::Void);
|
||||
}
|
||||
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 {
|
||||
// Receiver not provided: try static singleton instance for the box (methodize PoC fallback)
|
||||
if self.static_box_decls.contains_key(box_name) {
|
||||
let instance = self.ensure_static_box_instance(box_name)?;
|
||||
let recv_val = VMValue::from_nyash_box(Box::new(instance.clone()));
|
||||
return self.execute_method_call(&recv_val, method, args);
|
||||
}
|
||||
Err(self.err_with_context("Method call", &format!("missing receiver for {}", method)))
|
||||
}
|
||||
}
|
||||
|
||||
fn execute_method_call(
|
||||
&mut self,
|
||||
receiver: &VMValue,
|
||||
method: &str,
|
||||
args: &[ValueId],
|
||||
) -> Result<VMValue, VMError> {
|
||||
match receiver {
|
||||
VMValue::String(s) => match method {
|
||||
"length" => Ok(VMValue::Integer(s.len() as i64)),
|
||||
"concat" => {
|
||||
if let Some(arg_id) = args.get(0) {
|
||||
let arg_val = self.reg_load(*arg_id)?;
|
||||
let new_str = format!("{}{}", s, arg_val.to_string());
|
||||
Ok(VMValue::String(new_str))
|
||||
} else {
|
||||
Err(self.err_invalid("concat requires 1 argument"))
|
||||
}
|
||||
}
|
||||
"replace" => {
|
||||
if args.len() == 2 {
|
||||
let old = self.reg_load(args[0])?.to_string();
|
||||
let new = self.reg_load(args[1])?.to_string();
|
||||
Ok(VMValue::String(s.replace(&old, &new)))
|
||||
} else {
|
||||
Err(self.err_invalid("replace requires 2 arguments"))
|
||||
}
|
||||
}
|
||||
"indexOf" => {
|
||||
if let Some(arg_id) = args.get(0) {
|
||||
let needle = self.reg_load(*arg_id)?.to_string();
|
||||
let idx = s.find(&needle).map(|i| i as i64).unwrap_or(-1);
|
||||
Ok(VMValue::Integer(idx))
|
||||
} else {
|
||||
Err(self.err_invalid("indexOf requires 1 argument"))
|
||||
}
|
||||
}
|
||||
"lastIndexOf" => {
|
||||
if let Some(arg_id) = args.get(0) {
|
||||
let needle = self.reg_load(*arg_id)?.to_string();
|
||||
let idx = s.rfind(&needle).map(|i| i as i64).unwrap_or(-1);
|
||||
Ok(VMValue::Integer(idx))
|
||||
} else {
|
||||
Err(self.err_invalid("lastIndexOf requires 1 argument"))
|
||||
}
|
||||
}
|
||||
"substring" => {
|
||||
let start = if let Some(a0) = args.get(0) {
|
||||
self.reg_load(*a0)?.as_integer().unwrap_or(0)
|
||||
} else { 0 };
|
||||
let end = if let Some(a1) = args.get(1) {
|
||||
self.reg_load(*a1)?.as_integer().unwrap_or(s.len() as i64)
|
||||
} else { s.len() as i64 };
|
||||
let len = s.len() as i64;
|
||||
let i0 = start.max(0).min(len) as usize;
|
||||
let i1 = end.max(0).min(len) as usize;
|
||||
if i0 > i1 { return Ok(VMValue::String(String::new())); }
|
||||
// Note: operating on bytes; Nyash strings are UTF‑8, but tests are ASCII only here
|
||||
let bytes = s.as_bytes();
|
||||
let sub = String::from_utf8(bytes[i0..i1].to_vec()).unwrap_or_default();
|
||||
Ok(VMValue::String(sub))
|
||||
}
|
||||
_ => Err(self.err_method_not_found("String", method)),
|
||||
},
|
||||
VMValue::BoxRef(box_ref) => {
|
||||
// Try builtin StringBox first
|
||||
if let Some(string_box) = box_ref
|
||||
.as_any()
|
||||
.downcast_ref::<crate::boxes::string_box::StringBox>()
|
||||
{
|
||||
match method {
|
||||
"lastIndexOf" => {
|
||||
if let Some(arg_id) = args.get(0) {
|
||||
let needle = self.reg_load(*arg_id)?.to_string();
|
||||
let result_box = string_box.lastIndexOf(&needle);
|
||||
Ok(VMValue::from_nyash_box(result_box))
|
||||
} else {
|
||||
Err(self.err_invalid("lastIndexOf requires 1 argument"))
|
||||
}
|
||||
}
|
||||
"indexOf" | "find" => {
|
||||
if let Some(arg_id) = args.get(0) {
|
||||
let needle = self.reg_load(*arg_id)?.to_string();
|
||||
let result_box = string_box.find(&needle);
|
||||
Ok(VMValue::from_nyash_box(result_box))
|
||||
} else {
|
||||
Err(self.err_invalid("indexOf/find requires 1 argument"))
|
||||
}
|
||||
}
|
||||
_ => Err(self.err_method_not_found("StringBox", method)),
|
||||
}
|
||||
} else if let Some(p) = box_ref
|
||||
.as_any()
|
||||
.downcast_ref::<crate::runtime::plugin_loader_v2::PluginBoxV2>()
|
||||
{
|
||||
let host = crate::runtime::plugin_loader_unified::get_global_plugin_host();
|
||||
let host = host.read().unwrap();
|
||||
let argv = self.load_args_as_boxes(args)?;
|
||||
match host.invoke_instance_method(
|
||||
&p.box_type,
|
||||
method,
|
||||
p.inner.instance_id,
|
||||
&argv,
|
||||
) {
|
||||
Ok(Some(ret)) => Ok(VMValue::from_nyash_box(ret)),
|
||||
Ok(None) => Ok(VMValue::Void),
|
||||
Err(e) => Err(self.err_with_context(
|
||||
&format!("Plugin method {}.{}", p.box_type, method),
|
||||
&format!("{:?}", e)
|
||||
)),
|
||||
}
|
||||
} else {
|
||||
Err(self.err_method_not_found(&box_ref.type_name(), method))
|
||||
}
|
||||
}
|
||||
_ => Err(self.err_with_context("method call", &format!("{} not supported on {:?}", method, receiver))),
|
||||
}
|
||||
}
|
||||
}
|
||||
83
src/backend/mir_interpreter/handlers/calls/mod.rs
Normal file
83
src/backend/mir_interpreter/handlers/calls/mod.rs
Normal file
@ -0,0 +1,83 @@
|
||||
//! Call handling (split from handlers/calls.rs)
|
||||
//! - Route by Callee kind
|
||||
//! - Keep legacy path isolated for phased removal
|
||||
|
||||
use super::*;
|
||||
|
||||
mod global;
|
||||
mod method;
|
||||
mod externs;
|
||||
// legacy by-name resolver has been removed (Phase 2 complete)
|
||||
|
||||
impl MirInterpreter {
|
||||
pub(super) fn handle_call(
|
||||
&mut self,
|
||||
dst: Option<ValueId>,
|
||||
func: ValueId,
|
||||
callee: Option<&Callee>,
|
||||
args: &[ValueId],
|
||||
) -> Result<(), VMError> {
|
||||
if std::env::var("HAKO_CABI_TRACE").ok().as_deref() == Some("1") {
|
||||
match callee {
|
||||
Some(Callee::Global(n)) => eprintln!("[hb:path] call Callee::Global {} argc={}", n, args.len()),
|
||||
Some(Callee::Method{ box_name, method, ..}) => eprintln!("[hb:path] call Callee::Method {}.{} argc={}", box_name, method, args.len()),
|
||||
Some(Callee::Constructor{ box_type }) => eprintln!("[hb:path] call Callee::Constructor {} argc={}", box_type, args.len()),
|
||||
Some(Callee::Closure{ .. }) => eprintln!("[hb:path] call Callee::Closure argc={}", args.len()),
|
||||
Some(Callee::Value(_)) => eprintln!("[hb:path] call Callee::Value argc={}", args.len()),
|
||||
Some(Callee::Extern(n)) => eprintln!("[hb:path] call Callee::Extern {} argc={}", n, args.len()),
|
||||
None => eprintln!("[hb:path] call Legacy func_id={:?} argc={}", func, args.len()),
|
||||
}
|
||||
}
|
||||
// SSOT fast-path: route hostbridge.extern_invoke to extern dispatcher regardless of resolution form
|
||||
if let Some(Callee::Global(func_name)) = callee {
|
||||
if func_name == "hostbridge.extern_invoke" || func_name.starts_with("hostbridge.extern_invoke/") {
|
||||
let v = self.execute_extern_function("hostbridge.extern_invoke", args)?;
|
||||
self.write_result(dst, v);
|
||||
return Ok(());
|
||||
}
|
||||
}
|
||||
let call_result = if let Some(callee_type) = callee {
|
||||
self.execute_callee_call(callee_type, args)?
|
||||
} else {
|
||||
// Fast path: allow exact module function calls without legacy resolver.
|
||||
let name_val = self.reg_load(func)?;
|
||||
if let VMValue::String(ref s) = name_val {
|
||||
if let Some(f) = self.functions.get(s).cloned() {
|
||||
let mut argv: Vec<VMValue> = Vec::with_capacity(args.len());
|
||||
for a in args { argv.push(self.reg_load(*a)?); }
|
||||
self.exec_function_inner(&f, Some(&argv))?
|
||||
} else {
|
||||
return Err(self.err_with_context("call", &format!(
|
||||
"unknown function '{}' (by-name calls unsupported). attach Callee in builder or define the function",
|
||||
s
|
||||
)));
|
||||
}
|
||||
} else {
|
||||
return Err(self.err_with_context("call", "by-name calls unsupported without Callee attachment"));
|
||||
}
|
||||
};
|
||||
self.write_result(dst, call_result);
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(super) fn execute_callee_call(
|
||||
&mut self,
|
||||
callee: &Callee,
|
||||
args: &[ValueId],
|
||||
) -> Result<VMValue, VMError> {
|
||||
match callee {
|
||||
Callee::Global(func_name) => self.execute_global_function(func_name, args),
|
||||
Callee::Method { box_name, method, receiver, certainty: _ } => {
|
||||
self.execute_method_callee(box_name, method, receiver, args)
|
||||
}
|
||||
Callee::Constructor { box_type } => Err(self.err_unsupported(&format!("Constructor calls for {}", box_type))),
|
||||
Callee::Closure { .. } => Err(self.err_unsupported("Closure creation in VM")),
|
||||
Callee::Value(func_val_id) => {
|
||||
let _ = self.reg_load(*func_val_id)?;
|
||||
Err(self.err_unsupported("First-class function calls in VM"))
|
||||
}
|
||||
Callee::Extern(extern_name) => self.execute_extern_function(extern_name, args),
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@ -1,5 +1,5 @@
|
||||
use super::*;
|
||||
use super::super::utils::*;
|
||||
use crate::backend::mir_interpreter::utils::error_helpers::ErrorBuilder;
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
impl MirInterpreter {
|
||||
@ -56,18 +56,81 @@ impl MirInterpreter {
|
||||
match extern_name {
|
||||
// Console family (minimal)
|
||||
"nyash.console.log" | "env.console.log" | "print" | "nyash.builtin.print" => {
|
||||
let s = if let Some(a0) = args.get(0) { self.reg_load(*a0).ok() } else { None };
|
||||
if let Some(v) = s { println!("{}", v.to_string()); } else { println!(""); }
|
||||
if let Some(a0) = args.get(0) {
|
||||
let v = self.reg_load(*a0).ok();
|
||||
if let Some(v) = v {
|
||||
match &v {
|
||||
VMValue::Void => println!("null"),
|
||||
VMValue::String(s) => println!("{}", s),
|
||||
VMValue::BoxRef(bx) => {
|
||||
if bx.as_any().downcast_ref::<crate::box_trait::VoidBox>().is_some() {
|
||||
println!("null");
|
||||
} else if let Some(sb) = bx.as_any().downcast_ref::<crate::box_trait::StringBox>() {
|
||||
println!("{}", sb.value);
|
||||
} else {
|
||||
println!("{}", v.to_string());
|
||||
}
|
||||
}
|
||||
_ => println!("{}", v.to_string()),
|
||||
}
|
||||
} else {
|
||||
println!("");
|
||||
}
|
||||
} else {
|
||||
println!("");
|
||||
}
|
||||
Some(Ok(VMValue::Void))
|
||||
}
|
||||
"env.console.warn" | "nyash.console.warn" => {
|
||||
let s = if let Some(a0) = args.get(0) { self.reg_load(*a0).ok() } else { None };
|
||||
if let Some(v) = s { eprintln!("[warn] {}", v.to_string()); } else { eprintln!("[warn]"); }
|
||||
if let Some(a0) = args.get(0) {
|
||||
let v = self.reg_load(*a0).ok();
|
||||
if let Some(v) = v {
|
||||
match &v {
|
||||
VMValue::Void => eprintln!("[warn] null"),
|
||||
VMValue::String(s) => eprintln!("[warn] {}", s),
|
||||
VMValue::BoxRef(bx) => {
|
||||
if bx.as_any().downcast_ref::<crate::box_trait::VoidBox>().is_some() {
|
||||
eprintln!("[warn] null");
|
||||
} else if let Some(sb) = bx.as_any().downcast_ref::<crate::box_trait::StringBox>() {
|
||||
eprintln!("[warn] {}", sb.value);
|
||||
} else {
|
||||
eprintln!("[warn] {}", v.to_string());
|
||||
}
|
||||
}
|
||||
_ => eprintln!("[warn] {}", v.to_string()),
|
||||
}
|
||||
} else {
|
||||
eprintln!("[warn]");
|
||||
}
|
||||
} else {
|
||||
eprintln!("[warn]");
|
||||
}
|
||||
Some(Ok(VMValue::Void))
|
||||
}
|
||||
"env.console.error" | "nyash.console.error" => {
|
||||
let s = if let Some(a0) = args.get(0) { self.reg_load(*a0).ok() } else { None };
|
||||
if let Some(v) = s { eprintln!("[error] {}", v.to_string()); } else { eprintln!("[error]"); }
|
||||
if let Some(a0) = args.get(0) {
|
||||
let v = self.reg_load(*a0).ok();
|
||||
if let Some(v) = v {
|
||||
match &v {
|
||||
VMValue::Void => eprintln!("[error] null"),
|
||||
VMValue::String(s) => eprintln!("[error] {}", s),
|
||||
VMValue::BoxRef(bx) => {
|
||||
if bx.as_any().downcast_ref::<crate::box_trait::VoidBox>().is_some() {
|
||||
eprintln!("[error] null");
|
||||
} else if let Some(sb) = bx.as_any().downcast_ref::<crate::box_trait::StringBox>() {
|
||||
eprintln!("[error] {}", sb.value);
|
||||
} else {
|
||||
eprintln!("[error] {}", v.to_string());
|
||||
}
|
||||
}
|
||||
_ => eprintln!("[error] {}", v.to_string()),
|
||||
}
|
||||
} else {
|
||||
eprintln!("[error]");
|
||||
}
|
||||
} else {
|
||||
eprintln!("[error]");
|
||||
}
|
||||
Some(Ok(VMValue::Void))
|
||||
}
|
||||
// Extern providers (env.mirbuilder / env.codegen)
|
||||
|
||||
@ -1,6 +1,5 @@
|
||||
use super::*;
|
||||
use super::super::utils::*;
|
||||
use serde_json::{Value as JsonValue, Map as JsonMap};
|
||||
use serde_json::Value as JsonValue;
|
||||
|
||||
impl MirInterpreter {
|
||||
#[inline]
|
||||
@ -25,7 +24,9 @@ impl MirInterpreter {
|
||||
method: &str,
|
||||
args: &[ValueId],
|
||||
) -> Result<(), VMError> {
|
||||
match (iface, method) {
|
||||
// Normalize method arity suffix (e.g., get/1 -> get)
|
||||
let mbase = super::super::utils::normalize_arity_suffix(method);
|
||||
match (iface, mbase) {
|
||||
("env", "get") => {
|
||||
if let Some(a0) = args.get(0) {
|
||||
let key = self.reg_load(*a0)?.to_string();
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
use super::*;
|
||||
use super::super::utils::*;
|
||||
|
||||
impl MirInterpreter {
|
||||
pub(super) fn handle_ref_set(
|
||||
|
||||
@ -1,5 +1,4 @@
|
||||
use super::*;
|
||||
use super::super::utils::*;
|
||||
|
||||
impl MirInterpreter {
|
||||
pub(super) fn handle_debug(&mut self, message: &str, value: ValueId) -> Result<(), VMError> {
|
||||
|
||||
@ -20,7 +20,6 @@ mod boxes_object_fields;
|
||||
mod boxes_instance;
|
||||
mod boxes_plugin;
|
||||
mod boxes_void_guards;
|
||||
mod call_resolution;
|
||||
mod calls;
|
||||
mod externals;
|
||||
mod extern_provider;
|
||||
|
||||
@ -5,11 +5,8 @@ pub mod arg_validation;
|
||||
pub mod receiver_helpers;
|
||||
pub mod error_helpers;
|
||||
pub mod conversion_helpers;
|
||||
pub mod naming;
|
||||
// Phase 21.2: adapter_dev removed - all adapter functions now in .hako implementation
|
||||
|
||||
// Re-export for convenience
|
||||
pub use destination_helpers::*;
|
||||
pub use arg_validation::*;
|
||||
pub use receiver_helpers::*;
|
||||
pub use error_helpers::*;
|
||||
pub use conversion_helpers::*;
|
||||
// Selective re-export (only naming is widely used via utils::normalize_arity_suffix)
|
||||
pub use naming::*;
|
||||
|
||||
15
src/backend/mir_interpreter/utils/naming.rs
Normal file
15
src/backend/mir_interpreter/utils/naming.rs
Normal file
@ -0,0 +1,15 @@
|
||||
//! Naming helpers shared in MIR Interpreter handlers.
|
||||
|
||||
/// Normalize an optional arity suffix from a function/method name.
|
||||
/// Examples:
|
||||
/// - "env.get/1" -> "env.get"
|
||||
/// - "hostbridge.extern_invoke/3" -> "hostbridge.extern_invoke"
|
||||
/// - "print" -> "print"
|
||||
#[inline]
|
||||
pub fn normalize_arity_suffix(name: &str) -> &str {
|
||||
match name.split_once('/') {
|
||||
Some((base, _)) => base,
|
||||
None => name,
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user