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:
nyash-codex
2025-11-13 16:40:58 +09:00
parent 9e2fa1e36e
commit dda65b94b7
160 changed files with 6773 additions and 1692 deletions

View File

@ -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> {

View File

@ -1,5 +1,4 @@
use super::*;
use super::super::utils::*;
use crate::box_trait::NyashBox;
impl MirInterpreter {

View File

@ -1,5 +1,4 @@
use super::*;
use super::super::utils::*;
use crate::box_trait::NyashBox;
pub(super) fn try_handle_array_box(

View File

@ -1,5 +1,4 @@
use super::*;
use super::super::utils::*;
use crate::box_trait::NyashBox;
pub(super) fn try_handle_instance_box(

View File

@ -1,5 +1,4 @@
use super::*;
use super::super::utils::*;
use crate::box_trait::NyashBox;
pub(super) fn try_handle_map_box(

View File

@ -1,5 +1,4 @@
use super::*;
use super::super::utils::*;
use crate::box_trait::NyashBox;
pub(super) fn try_handle_object_fields(

View File

@ -1,5 +1,4 @@
use super::*;
use super::super::utils::*;
use crate::box_trait::NyashBox;
pub(super) fn invoke_plugin_box(

View File

@ -1,5 +1,4 @@
use super::*;
use super::super::utils::*;
use crate::box_trait::NyashBox;
pub(super) fn try_handle_string_box(

View File

@ -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())
}
}
}

View File

@ -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 UTF8, 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))),
}
}
}

View 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.

View 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`, percallee 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。byname 旧経路は削除済み(`legacy.rs`/`resolution.rs`)。
- `callee=None` の呼び出しは「厳密一致のモジュール関数名」以外は FailFastビルダーで 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`).

View 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))),
}
}
}

View 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))),
}
}
}

View 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 UTF8, 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))),
}
}
}

View 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),
}
}
}

View File

@ -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)

View File

@ -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();

View File

@ -1,5 +1,4 @@
use super::*;
use super::super::utils::*;
impl MirInterpreter {
pub(super) fn handle_ref_set(

View File

@ -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> {

View File

@ -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;

View File

@ -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::*;

View 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,
}
}