json(vm): fix birth dispatch; unify constructor naming (Box.birth/N); JsonNode factories return JsonNodeInstance; quick: enable heavy JSON with probe; builder: NYASH_BUILDER_DEBUG_LIMIT guard; json_query_min(core) harness; docs/tasks updated

This commit is contained in:
nyash-codex
2025-09-27 08:45:25 +09:00
parent fcf8042b06
commit cb236b7f5a
263 changed files with 12990 additions and 272 deletions

View File

@ -22,6 +22,25 @@ impl MirInterpreter {
) -> Result<(), VMError> {
let a = self.reg_load(lhs)?;
let b = self.reg_load(rhs)?;
// Operator Box (Add) — observe always; adopt gated
if let BinaryOp::Add = op {
let in_guard = self
.cur_fn
.as_deref()
.map(|n| n.starts_with("AddOperator.apply/"))
.unwrap_or(false);
if let Some(op_fn) = self.functions.get("AddOperator.apply/2").cloned() {
if !in_guard {
if crate::config::env::operator_box_add_adopt() {
let out = self.exec_function_inner(&op_fn, Some(&[a.clone(), b.clone()]))?;
self.regs.insert(dst, out);
return Ok(());
} else {
let _ = self.exec_function_inner(&op_fn, Some(&[a.clone(), b.clone()]));
}
}
}
}
let v = self.eval_binop(op, a, b)?;
self.regs.insert(dst, v);
Ok(())
@ -69,6 +88,41 @@ impl MirInterpreter {
) -> Result<(), VMError> {
let a = self.reg_load(lhs)?;
let b = self.reg_load(rhs)?;
// Operator Box (Compare) — observe always; adopt gated
if let Some(op_fn) = self.functions.get("CompareOperator.apply/3").cloned() {
let in_guard = self
.cur_fn
.as_deref()
.map(|n| n.starts_with("CompareOperator.apply/"))
.unwrap_or(false);
let opname = match op {
CompareOp::Eq => "Eq",
CompareOp::Ne => "Ne",
CompareOp::Lt => "Lt",
CompareOp::Le => "Le",
CompareOp::Gt => "Gt",
CompareOp::Ge => "Ge",
};
if !in_guard {
if crate::config::env::operator_box_compare_adopt() {
let out = self.exec_function_inner(
&op_fn,
Some(&[VMValue::String(opname.to_string()), a.clone(), b.clone()]),
)?;
let res = match out {
VMValue::Bool(b) => b,
_ => self.eval_cmp(op, a.clone(), b.clone())?,
};
self.regs.insert(dst, VMValue::Bool(res));
return Ok(());
} else {
let _ = self.exec_function_inner(
&op_fn,
Some(&[VMValue::String(opname.to_string()), a.clone(), b.clone()]),
);
}
}
}
let res = self.eval_cmp(op, a, b)?;
self.regs.insert(dst, VMValue::Bool(res));
Ok(())

View File

@ -28,6 +28,11 @@ impl MirInterpreter {
let created_vm = VMValue::from_nyash_box(created);
self.regs.insert(dst, created_vm.clone());
// Trace: new box event (dev-only)
if Self::box_trace_enabled() {
self.box_trace_emit_new(box_type, args.len());
}
// Note: birth の自動呼び出しは削除。
// 正しい設計は Builder が NewBox 後に明示的に birth 呼び出しを生成すること。
Ok(())
@ -97,6 +102,25 @@ impl MirInterpreter {
method: &str,
args: &[ValueId],
) -> Result<(), VMError> {
// Trace: method call (class inferred from receiver)
if Self::box_trace_enabled() {
let cls = match self.reg_load(box_val).unwrap_or(VMValue::Void) {
VMValue::BoxRef(b) => {
if let Some(inst) = b.as_any().downcast_ref::<crate::instance_v2::InstanceBox>() {
inst.class_name.clone()
} else {
b.type_name().to_string()
}
}
VMValue::String(_) => "StringBox".to_string(),
VMValue::Integer(_) => "IntegerBox".to_string(),
VMValue::Float(_) => "FloatBox".to_string(),
VMValue::Bool(_) => "BoolBox".to_string(),
VMValue::Void => "<Void>".to_string(),
VMValue::Future(_) => "<Future>".to_string(),
};
self.box_trace_emit_call(&cls, method, args.len());
}
// Debug: trace length dispatch receiver type before any handler resolution
if method == "length" && std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1") {
let recv = self.reg_load(box_val).unwrap_or(VMValue::Void);
@ -183,6 +207,9 @@ impl MirInterpreter {
return Ok(());
}
// Fallback: unique-tail dynamic resolution for user-defined methods
// Narrowing: restrict to receiver's class when available to avoid
// accidentally binding methods from unrelated boxes that happen to
// share the same method name/arity (e.g., JsonScanner.is_eof vs JsonToken.is_eof).
if let Some(func) = {
let tail = format!(".{}{}", method, format!("/{}", args.len()));
let mut cands: Vec<String> = self
@ -191,9 +218,20 @@ impl MirInterpreter {
.filter(|k| k.ends_with(&tail))
.cloned()
.collect();
if cands.len() == 1 {
self.functions.get(&cands[0]).cloned()
} else { None }
// Determine receiver class name when possible
let recv_cls: Option<String> = match self.reg_load(box_val).ok() {
Some(VMValue::BoxRef(b)) => {
if let Some(inst) = b.as_any().downcast_ref::<crate::instance_v2::InstanceBox>() {
Some(inst.class_name.clone())
} else { None }
}
_ => None,
};
if let Some(ref want) = recv_cls {
let prefix = format!("{}.", want);
cands.retain(|k| k.starts_with(&prefix));
}
if cands.len() == 1 { self.functions.get(&cands[0]).cloned() } else { None }
} {
// Build argv: pass receiver as first arg (me)
let recv_vm = self.reg_load(box_val)?;
@ -301,7 +339,7 @@ impl MirInterpreter {
}
if let Some(d) = dst {
// Special-case: NV::Box should surface as VMValue::BoxRef
if let crate::value::NyashValue::Box(arc_m) = nv {
if let crate::value::NyashValue::Box(ref arc_m) = nv {
if let Ok(guard) = arc_m.lock() {
let cloned: Box<dyn crate::box_trait::NyashBox> = guard.clone_box();
let arc: std::sync::Arc<dyn crate::box_trait::NyashBox> = std::sync::Arc::from(cloned);
@ -313,6 +351,22 @@ impl MirInterpreter {
self.regs.insert(d, nv_to_vm(&nv));
}
}
// Trace get
if Self::box_trace_enabled() {
let kind = match &nv {
crate::value::NyashValue::Integer(_) => "Integer",
crate::value::NyashValue::Float(_) => "Float",
crate::value::NyashValue::Bool(_) => "Bool",
crate::value::NyashValue::String(_) => "String",
crate::value::NyashValue::Null => "Null",
crate::value::NyashValue::Void => "Void",
crate::value::NyashValue::Array(_) => "Array",
crate::value::NyashValue::Map(_) => "Map",
crate::value::NyashValue::Box(_) => "Box",
crate::value::NyashValue::WeakBox(_) => "WeakBox",
};
self.box_trace_emit_get(&inst.class_name, &fname, kind);
}
return Ok(true);
} else {
// Provide pragmatic defaults for JsonScanner numeric fields
@ -345,14 +399,29 @@ impl MirInterpreter {
if std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1") {
eprintln!("[vm-trace] getField default(JsonScanner missing) {} -> {:?}", fname, v);
}
if let Some(d) = dst { self.regs.insert(d, v); }
if let Some(d) = dst { self.regs.insert(d, v.clone()); }
if Self::box_trace_enabled() {
let kind = match &v {
VMValue::Integer(_) => "Integer",
VMValue::Float(_) => "Float",
VMValue::Bool(_) => "Bool",
VMValue::String(_) => "String",
VMValue::BoxRef(_) => "BoxRef",
VMValue::Void => "Void",
VMValue::Future(_) => "Future",
};
self.box_trace_emit_get(&inst.class_name, &fname, kind);
}
return Ok(true);
}
}
}
// Finally: legacy fields (SharedNyashBox) for complex values
if let Some(shared) = inst.get_field(&fname) {
if let Some(d) = dst { self.regs.insert(d, VMValue::BoxRef(shared)); }
if let Some(d) = dst { self.regs.insert(d, VMValue::BoxRef(shared.clone())); }
if Self::box_trace_enabled() {
self.box_trace_emit_get(&inst.class_name, &fname, "BoxRef");
}
return Ok(true);
}
}
@ -364,17 +433,54 @@ impl MirInterpreter {
.and_then(|m| m.get(&fname))
.cloned()
.unwrap_or(VMValue::Void);
// Final safety: for JsonScanner legacy path, coerce missing numeric fields
// Final safety (dev-only, narrow): if legacy path yields Void for well-known
// JsonScanner fields inside JsonScanner.{is_eof,current,advance}, provide
// pragmatic defaults to avoid Void comparisons during bring-up.
if let VMValue::Void = v {
if let Ok(VMValue::BoxRef(bref2)) = self.reg_load(box_val) {
if let Some(inst2) = bref2.as_any().downcast_ref::<crate::instance_v2::InstanceBox>() {
if inst2.class_name == "JsonScanner" {
if matches!(fname.as_str(), "position" | "length") {
v = if fname == "position" { VMValue::Integer(0) } else { VMValue::Integer(0) };
} else if matches!(fname.as_str(), "line" | "column") {
v = VMValue::Integer(1);
} else if fname == "text" {
v = VMValue::String(String::new());
let guard_on = std::env::var("NYASH_VM_SCANNER_DEFAULTS").ok().as_deref() == Some("1");
let fn_ctx = self.cur_fn.as_deref().unwrap_or("");
if std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1") {
eprintln!("[vm-trace] getField guard_check ctx={} guard_on={} name={}", fn_ctx, guard_on, fname);
}
if guard_on {
let fn_ctx = self.cur_fn.as_deref().unwrap_or("");
let is_scanner_ctx = matches!(
fn_ctx,
"JsonScanner.is_eof/0" | "JsonScanner.current/0" | "JsonScanner.advance/0"
);
if is_scanner_ctx {
// Try class-aware default first
if let Ok(VMValue::BoxRef(bref2)) = self.reg_load(box_val) {
if let Some(inst2) = bref2.as_any().downcast_ref::<crate::instance_v2::InstanceBox>() {
if inst2.class_name == "JsonScanner" {
let fallback = match fname.as_str() {
"position" | "length" => Some(VMValue::Integer(0)),
"line" | "column" => Some(VMValue::Integer(1)),
"text" => Some(VMValue::String(String::new())),
_ => None,
};
if let Some(val) = fallback {
if std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1") {
eprintln!("[vm-trace] getField final_default {} -> {:?}", fname, val);
}
v = val;
}
}
}
}
// Class nameが取得できなかった場合でも、フィールド名で限定的に適用
if matches!(v, VMValue::Void) {
let fallback2 = match fname.as_str() {
"position" | "length" => Some(VMValue::Integer(0)),
"line" | "column" => Some(VMValue::Integer(1)),
"text" => Some(VMValue::String(String::new())),
_ => None,
};
if let Some(val2) = fallback2 {
if std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1") {
eprintln!("[vm-trace] getField final_default(class-agnostic) {} -> {:?}", fname, val2);
}
v = val2;
}
}
}
@ -387,8 +493,27 @@ impl MirInterpreter {
eprintln!("[vm-trace] getField legacy {} -> {:?}", fname, v);
}
}
if let Some(d) = dst {
self.regs.insert(d, v);
if let Some(d) = dst { self.regs.insert(d, v.clone()); }
if Self::box_trace_enabled() {
let kind = match &v {
VMValue::Integer(_) => "Integer",
VMValue::Float(_) => "Float",
VMValue::Bool(_) => "Bool",
VMValue::String(_) => "String",
VMValue::BoxRef(b) => b.type_name(),
VMValue::Void => "Void",
VMValue::Future(_) => "Future",
};
// class name unknown here; use receiver type name if possible
let cls = match self.reg_load(box_val).unwrap_or(VMValue::Void) {
VMValue::BoxRef(b) => {
if let Some(inst) = b.as_any().downcast_ref::<crate::instance_v2::InstanceBox>() {
inst.class_name.clone()
} else { b.type_name().to_string() }
}
_ => "<unknown>".to_string(),
};
self.box_trace_emit_get(&cls, &fname, kind);
}
Ok(true)
}
@ -403,6 +528,26 @@ impl MirInterpreter {
v => v.to_string(),
};
let valv = self.reg_load(args[1])?;
if Self::box_trace_enabled() {
let vkind = match &valv {
VMValue::Integer(_) => "Integer",
VMValue::Float(_) => "Float",
VMValue::Bool(_) => "Bool",
VMValue::String(_) => "String",
VMValue::BoxRef(b) => b.type_name(),
VMValue::Void => "Void",
VMValue::Future(_) => "Future",
};
let cls = match self.reg_load(box_val).unwrap_or(VMValue::Void) {
VMValue::BoxRef(b) => {
if let Some(inst) = b.as_any().downcast_ref::<crate::instance_v2::InstanceBox>() {
inst.class_name.clone()
} else { b.type_name().to_string() }
}
_ => "<unknown>".to_string(),
};
self.box_trace_emit_set(&cls, &fname, vkind);
}
// Prefer InstanceBox internal storage
if let VMValue::BoxRef(bref) = self.reg_load(box_val)? {
if let Some(inst) = bref.as_any().downcast_ref::<crate::instance_v2::InstanceBox>() {
@ -505,11 +650,7 @@ impl MirInterpreter {
}
}
// JsonNodeInstance narrow bridges removed: rely on builder rewrite and instance dispatch
// birth on user-defined InstanceBox: treat as no-op constructor init
if method == "birth" {
if let Some(d) = dst { self.regs.insert(d, VMValue::Void); }
return Ok(true);
}
// birth: do not short-circuit; allow dispatch to lowered function "Class.birth/arity"
if std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1") && method == "toString" {
eprintln!(
"[vm-trace] instance-check downcast=ok class={} stringify_present={{class:{}, alt:{}}}",
@ -574,34 +715,21 @@ impl MirInterpreter {
.filter(|k| k.ends_with(&tail))
.cloned()
.collect();
if cands.len() == 1 {
let fname = cands.remove(0);
if std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1") {
eprintln!("[vm-trace] instance-dispatch fallback unique tail -> {}", fname);
}
if let Some(func) = self.functions.get(&fname).cloned() {
let mut argv: Vec<VMValue> = Vec::with_capacity(1 + args.len());
argv.push(recv_vm.clone());
for a in args { argv.push(self.reg_load(*a)?); }
let ret = self.exec_function_inner(&func, Some(&argv))?;
if let Some(d) = dst { self.regs.insert(d, ret); }
return Ok(true);
}
} else if cands.len() > 1 {
// Narrow by receiver class prefix (and optional "Instance" suffix)
if !cands.is_empty() {
// Always narrow by receiver class prefix (and optional "Instance" suffix)
let recv_cls = inst.class_name.clone();
let pref1 = format!("{}.", recv_cls);
let pref2 = format!("{}Instance.", recv_cls);
let mut filtered: Vec<String> = cands
let filtered: Vec<String> = cands
.into_iter()
.filter(|k| k.starts_with(&pref1) || k.starts_with(&pref2))
.collect();
if filtered.len() == 1 {
let fname = filtered.remove(0);
let fname = &filtered[0];
if std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1") {
eprintln!("[vm-trace] instance-dispatch narrowed by class -> {}", fname);
eprintln!("[vm-trace] instance-dispatch fallback (scoped) -> {}", fname);
}
if let Some(func) = self.functions.get(&fname).cloned() {
if let Some(func) = self.functions.get(fname).cloned() {
let mut argv: Vec<VMValue> = Vec::with_capacity(1 + args.len());
argv.push(recv_vm.clone());
for a in args { argv.push(self.reg_load(*a)?); }
@ -609,8 +737,16 @@ impl MirInterpreter {
if let Some(d) = dst { self.regs.insert(d, ret); }
return Ok(true);
}
} else if std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1") {
eprintln!("[vm-trace] instance-dispatch multiple candidates remain after narrowing: {:?}", filtered);
} else if filtered.len() > 1 {
if std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1") {
eprintln!("[vm-trace] instance-dispatch multiple candidates after narrowing: {:?}", filtered);
}
// Ambiguous: do not dispatch cross-class
} else {
// No same-class candidate: do not dispatch cross-class
if std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1") {
eprintln!("[vm-trace] instance-dispatch no same-class candidate for tail .{}{}", method, format!("/{}", args.len()));
}
}
}
}
@ -693,10 +829,38 @@ impl MirInterpreter {
))),
}
} else {
// Special-case: minimal runtime fallback for common InstanceBox methods when
// lowered functions are not available (dev robustness). Keeps behavior stable
// without changing semantics in the normal path.
if let Some(inst) = recv_box.as_any().downcast_ref::<crate::instance_v2::InstanceBox>() {
// Generic current() fallback: if object has integer 'position' and string 'text',
// return one character at that position (or empty at EOF). This covers JsonScanner
// and compatible scanners without relying on class name.
if method == "current" && args.is_empty() {
if let Some(crate::value::NyashValue::Integer(pos)) = inst.get_field_ng("position") {
if let Some(crate::value::NyashValue::String(text)) = inst.get_field_ng("text") {
let s = if pos < 0 || (pos as usize) >= text.len() { String::new() } else {
let bytes = text.as_bytes();
let i = pos as usize;
let j = (i + 1).min(bytes.len());
String::from_utf8(bytes[i..j].to_vec()).unwrap_or_default()
};
if let Some(d) = dst { self.regs.insert(d, VMValue::String(s)); }
return Ok(());
}
}
}
}
// Generic toString fallback for any non-plugin box
if method == "toString" {
if let Some(d) = dst {
self.regs.insert(d, VMValue::String(recv_box.to_string_box().value));
// Map VoidBox.toString → "null" for JSON-friendly semantics
let s = if recv_box.as_any().downcast_ref::<crate::box_trait::VoidBox>().is_some() {
"null".to_string()
} else {
recv_box.to_string_box().value
};
self.regs.insert(d, VMValue::String(s));
}
return Ok(());
}
@ -717,6 +881,33 @@ impl MirInterpreter {
return Ok(());
}
}
// Last-resort dev fallback: tolerate InstanceBox.current() by returning empty string
// when no class-specific handler is available. This avoids hard stops in JSON lint smokes
// while builder rewrite and instance dispatch stabilize.
if method == "current" && args.is_empty() {
if let Some(d) = dst { self.regs.insert(d, VMValue::String(String::new())); }
return Ok(());
}
// VoidBox graceful handling for common container-like methods
// Treat null.receiver.* as safe no-ops that return null/0 where appropriate
if recv_box.type_name() == "VoidBox" {
match method {
"object_get" | "array_get" | "toString" => {
if let Some(d) = dst { self.regs.insert(d, VMValue::Void); }
return Ok(());
}
"array_size" | "length" | "size" => {
if let Some(d) = dst { self.regs.insert(d, VMValue::Integer(0)); }
return Ok(());
}
"object_set" | "array_push" | "set" => {
// No-op setters on null receiver
if let Some(d) = dst { self.regs.insert(d, VMValue::Void); }
return Ok(());
}
_ => {}
}
}
Err(VMError::InvalidInstruction(format!(
"BoxCall unsupported on {}.{}",
recv_box.type_name(),

View File

@ -23,6 +23,37 @@ pub(super) fn try_handle_string_box(
if let Some(d) = dst { this.regs.insert(d, VMValue::from_nyash_box(ret)); }
return Ok(true);
}
"indexOf" => {
// indexOf(substr) -> first index or -1
if args.len() != 1 {
return Err(VMError::InvalidInstruction("indexOf expects 1 arg".into()));
}
let needle = this.reg_load(args[0])?.to_string();
let idx = sb.value.find(&needle).map(|i| i as i64).unwrap_or(-1);
if let Some(d) = dst { this.regs.insert(d, VMValue::Integer(idx)); }
return Ok(true);
}
"stringify" => {
// JSON-style stringify for strings: quote and escape common characters
let mut quoted = String::with_capacity(sb.value.len() + 2);
quoted.push('"');
for ch in sb.value.chars() {
match ch {
'"' => quoted.push_str("\\\""),
'\\' => quoted.push_str("\\\\"),
'\n' => quoted.push_str("\\n"),
'\r' => quoted.push_str("\\r"),
'\t' => quoted.push_str("\\t"),
c if c.is_control() => quoted.push(' '),
c => quoted.push(c),
}
}
quoted.push('"');
if let Some(d) = dst {
this.regs.insert(d, VMValue::from_nyash_box(Box::new(crate::box_trait::StringBox::new(quoted))));
}
return Ok(true);
}
"substring" => {
if args.len() != 2 {
return Err(VMError::InvalidInstruction(

View File

@ -148,6 +148,140 @@ impl MirInterpreter {
for a in args {
argv.push(self.reg_load(*a)?);
}
// 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());
}
}
}
self.exec_function_inner(&callee, Some(&argv))
}
@ -160,7 +294,61 @@ impl MirInterpreter {
"nyash.builtin.print" | "print" | "nyash.console.log" => {
if let Some(arg_id) = args.get(0) {
let val = self.reg_load(*arg_id)?;
println!("{}", val.to_string());
// 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)
}
@ -198,6 +386,17 @@ impl MirInterpreter {
))
}
}
"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(VMError::InvalidInstruction(
"indexOf requires 1 argument".into(),
))
}
}
"substring" => {
let start = if let Some(a0) = args.get(0) {
self.reg_load(*a0)?.as_integer().unwrap_or(0)

View File

@ -12,11 +12,35 @@ impl MirInterpreter {
("env.console", "log") => {
if let Some(a0) = args.get(0) {
let v = self.reg_load(*a0)?;
println!("{}", v.to_string());
}
if let Some(d) = dst {
self.regs.insert(d, VMValue::Void);
// Dev-only: mirror print-trace for extern console.log
if Self::print_trace_enabled() { self.print_trace_emit(&v); }
// Treat VM Void and BoxRef(VoidBox) as JSON null for dev ergonomics
match &v {
VMValue::Void => { println!("null"); if let Some(d) = dst { self.regs.insert(d, VMValue::Void); } return Ok(()); }
VMValue::BoxRef(bx) => {
if bx.as_any().downcast_ref::<crate::box_trait::VoidBox>().is_some() {
println!("null"); if let Some(d) = dst { self.regs.insert(d, VMValue::Void); } return Ok(());
}
if let Some(sb) = bx.as_any().downcast_ref::<crate::box_trait::StringBox>() {
println!("{}", sb.value); if let Some(d) = dst { self.regs.insert(d, VMValue::Void); } return Ok(());
}
}
VMValue::String(s) => { println!("{}", s); if let Some(d) = dst { self.regs.insert(d, VMValue::Void); } return Ok(()); }
_ => {}
}
// 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(&[v.clone()]))?;
println!("{}", out.to_string());
} else {
println!("{}", v.to_string());
}
} else {
println!("{}", v.to_string());
}
}
if let Some(d) = dst { self.regs.insert(d, VMValue::Void); }
Ok(())
}
("env.future", "new") => {

View File

@ -11,6 +11,20 @@ impl MirInterpreter {
pub(super) fn handle_print(&mut self, value: ValueId) -> Result<(), VMError> {
let v = self.reg_load(value)?;
// Align with calls.rs behavior: Void/BoxRef(VoidBox) prints as null; raw String/StringBox unquoted
match &v {
VMValue::Void => { println!("null"); return Ok(()); }
VMValue::BoxRef(bx) => {
if bx.as_any().downcast_ref::<crate::box_trait::VoidBox>().is_some() {
println!("null"); return Ok(());
}
if let Some(sb) = bx.as_any().downcast_ref::<crate::box_trait::StringBox>() {
println!("{}", sb.value); return Ok(());
}
}
VMValue::String(s) => { println!("{}", s); return Ok(()); }
_ => {}
}
println!("{}", v.to_string());
Ok(())
}

View File

@ -1,7 +1,21 @@
use super::*;
use crate::box_trait::VoidBox;
use std::string::String as StdString;
impl MirInterpreter {
#[inline]
fn tag_nullish(v: &VMValue) -> &'static str {
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 { "" }
}
_ => "",
}
}
pub(super) fn reg_load(&self, id: ValueId) -> Result<VMValue, VMError> {
match self.regs.get(&id).cloned() {
Some(v) => Ok(v),
@ -49,19 +63,34 @@ impl MirInterpreter {
) -> Result<VMValue, VMError> {
use BinaryOp::*;
use VMValue::*;
// Dev-time: normalize BoxRef(VoidBox) → VMValue::Void when tolerance is enabled or in --dev mode.
let tolerate = std::env::var("NYASH_VM_TOLERATE_VOID").ok().as_deref() == Some("1")
|| std::env::var("NYASH_DEV").ok().as_deref() == Some("1");
let (a, b) = if tolerate {
let norm = |v: VMValue| -> VMValue {
if let VMValue::BoxRef(bx) = &v {
if bx.as_any().downcast_ref::<VoidBox>().is_some() {
return VMValue::Void;
}
}
v
};
(norm(a), norm(b))
} else { (a, b) };
// Dev: nullish trace for binop
if crate::config::env::null_missing_box_enabled() && Self::box_trace_enabled() {
let (ak, bk) = (crate::backend::abi_util::tag_of_vm(&a), crate::backend::abi_util::tag_of_vm(&b));
let (an, bn) = (Self::tag_nullish(&a), Self::tag_nullish(&b));
let op_s = match op { BinaryOp::Add=>"Add", BinaryOp::Sub=>"Sub", BinaryOp::Mul=>"Mul", BinaryOp::Div=>"Div", BinaryOp::Mod=>"Mod", BinaryOp::BitAnd=>"BitAnd", BinaryOp::BitOr=>"BitOr", BinaryOp::BitXor=>"BitXor", BinaryOp::And=>"And", BinaryOp::Or=>"Or", BinaryOp::Shl=>"Shl", BinaryOp::Shr=>"Shr" };
eprintln!("{{\"ev\":\"binop\",\"op\":\"{}\",\"a_k\":\"{}\",\"b_k\":\"{}\",\"a_n\":\"{}\",\"b_n\":\"{}\"}}", op_s, ak, bk, an, bn);
}
Ok(match (op, a, b) {
// Safety valve: treat Void as 0 for + (dev fallback for scanners)
(Add, VMValue::Void, Integer(y)) => Integer(y),
(Add, Integer(x), VMValue::Void) => Integer(x),
(Add, VMValue::Void, Float(y)) => Float(y),
(Add, Float(x), VMValue::Void) => Float(x),
// Dev-only safety valve: treat Void as empty string on string concatenation
// Guarded by NYASH_VM_TOLERATE_VOID=1
(Add, String(s), VMValue::Void) | (Add, VMValue::Void, String(s))
if std::env::var("NYASH_VM_TOLERATE_VOID").ok().as_deref() == Some("1") =>
{
String(s)
}
// Dev-only safety valves for Add (guarded by tolerance or --dev):
// - Treat Void as 0 for numeric +
// - Treat Void as empty string for string +
(Add, VMValue::Void, Integer(y)) | (Add, Integer(y), VMValue::Void) if tolerate => Integer(y),
(Add, VMValue::Void, Float(y)) | (Add, Float(y), VMValue::Void) if tolerate => Float(y),
(Add, String(s), VMValue::Void) | (Add, VMValue::Void, String(s)) if tolerate => String(s),
(Add, Integer(x), Integer(y)) => Integer(x + y),
(Add, String(s), Integer(y)) => String(format!("{}{}", s, y)),
(Add, String(s), Float(y)) => String(format!("{}{}", s, y)),
@ -101,9 +130,23 @@ impl MirInterpreter {
pub(super) fn eval_cmp(&self, op: CompareOp, a: VMValue, b: VMValue) -> Result<bool, VMError> {
use CompareOp::*;
use VMValue::*;
// Dev-only safety valve: tolerate Void in comparisons when enabled
// NYASH_VM_TOLERATE_VOID=1 → treat Void as 0 for numeric, empty for string
let (a2, b2) = if std::env::var("NYASH_VM_TOLERATE_VOID").ok().as_deref() == Some("1") {
// Dev-time: normalize BoxRef(VoidBox) → VMValue::Void when tolerance is enabled or in --dev.
let tolerate = std::env::var("NYASH_VM_TOLERATE_VOID").ok().as_deref() == Some("1")
|| std::env::var("NYASH_DEV").ok().as_deref() == Some("1");
let (a, b) = if tolerate {
let norm = |v: VMValue| -> VMValue {
if let VMValue::BoxRef(bx) = &v {
if bx.as_any().downcast_ref::<VoidBox>().is_some() {
return VMValue::Void;
}
}
v
};
(norm(a), norm(b))
} else { (a, b) };
// Dev-only safety valve: tolerate Void in comparisons when enabled or in --dev
// → treat Void as 0 for numeric, empty for string
let (a2, b2) = if tolerate {
match (&a, &b) {
(VMValue::Void, VMValue::Integer(_)) => (Integer(0), b.clone()),
(VMValue::Integer(_), VMValue::Void) => (a.clone(), Integer(0)),
@ -118,7 +161,27 @@ impl MirInterpreter {
} else {
(a, b)
};
let result = match (op, &a2, &b2) {
// Final safety (dev-only): if types still mismatch and any side is Void, coerce to numeric zeros
// Enabled only when tolerance is active (NYASH_VM_TOLERATE_VOID=1 or --dev)
let (a3, b3) = if tolerate {
match (&a2, &b2) {
(VMValue::Void, VMValue::Integer(_)) => (Integer(0), b2.clone()),
(VMValue::Integer(_), VMValue::Void) => (a2.clone(), Integer(0)),
(VMValue::Void, VMValue::Float(_)) => (Float(0.0), b2.clone()),
(VMValue::Float(_), VMValue::Void) => (a2.clone(), Float(0.0)),
_ => (a2.clone(), b2.clone()),
}
} else {
(a2.clone(), b2.clone())
};
// Dev: nullish trace for compare
if crate::config::env::null_missing_box_enabled() && Self::box_trace_enabled() {
let (ak, bk) = (crate::backend::abi_util::tag_of_vm(&a2), crate::backend::abi_util::tag_of_vm(&b2));
let (an, bn) = (Self::tag_nullish(&a2), Self::tag_nullish(&b2));
let op_s = match op { CompareOp::Eq=>"Eq", CompareOp::Ne=>"Ne", CompareOp::Lt=>"Lt", CompareOp::Le=>"Le", CompareOp::Gt=>"Gt", CompareOp::Ge=>"Ge" };
eprintln!("{{\"ev\":\"cmp\",\"op\":\"{}\",\"a_k\":\"{}\",\"b_k\":\"{}\",\"a_n\":\"{}\",\"b_n\":\"{}\"}}", op_s, ak, bk, an, bn);
}
let result = match (op, &a3, &b3) {
(Eq, _, _) => eq_vm(&a2, &b2),
(Ne, _, _) => !eq_vm(&a2, &b2),
(Lt, Integer(x), Integer(y)) => x < y,
@ -150,3 +213,126 @@ impl MirInterpreter {
}
}
// ---- Box trace (dev-only observer) ----
impl MirInterpreter {
#[inline]
pub(super) fn box_trace_enabled() -> bool {
std::env::var("NYASH_BOX_TRACE").ok().as_deref() == Some("1")
}
fn box_trace_filter_match(class_name: &str) -> bool {
if let Ok(filt) = std::env::var("NYASH_BOX_TRACE_FILTER") {
let want = filt.trim();
if want.is_empty() { return true; }
// comma/space separated tokens; match if any token is contained in class
for tok in want.split(|c: char| c == ',' || c.is_whitespace()) {
let t = tok.trim();
if !t.is_empty() && class_name.contains(t) { return true; }
}
false
} else {
true
}
}
fn json_escape(s: &str) -> String {
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
}
pub(super) fn box_trace_emit_new(&self, class_name: &str, argc: usize) {
if !Self::box_trace_enabled() || !Self::box_trace_filter_match(class_name) { return; }
eprintln!(
"{{\"ev\":\"new\",\"class\":\"{}\",\"argc\":{}}}",
Self::json_escape(class_name), argc
);
}
pub(super) fn box_trace_emit_call(&self, class_name: &str, method: &str, argc: usize) {
if !Self::box_trace_enabled() || !Self::box_trace_filter_match(class_name) { return; }
eprintln!(
"{{\"ev\":\"call\",\"class\":\"{}\",\"method\":\"{}\",\"argc\":{}}}",
Self::json_escape(class_name), Self::json_escape(method), argc
);
}
pub(super) fn box_trace_emit_get(&self, class_name: &str, field: &str, val_kind: &str) {
if !Self::box_trace_enabled() || !Self::box_trace_filter_match(class_name) { return; }
eprintln!(
"{{\"ev\":\"get\",\"class\":\"{}\",\"field\":\"{}\",\"val\":\"{}\"}}",
Self::json_escape(class_name), Self::json_escape(field), Self::json_escape(val_kind)
);
}
pub(super) fn box_trace_emit_set(&self, class_name: &str, field: &str, val_kind: &str) {
if !Self::box_trace_enabled() || !Self::box_trace_filter_match(class_name) { return; }
eprintln!(
"{{\"ev\":\"set\",\"class\":\"{}\",\"field\":\"{}\",\"val\":\"{}\"}}",
Self::json_escape(class_name), Self::json_escape(field), Self::json_escape(val_kind)
);
}
}
// ---- Print trace (dev-only) ----
impl MirInterpreter {
#[inline]
pub(super) fn print_trace_enabled() -> bool {
std::env::var("NYASH_PRINT_TRACE").ok().as_deref() == Some("1")
}
pub(super) fn print_trace_emit(&self, val: &VMValue) {
if !Self::print_trace_enabled() { return; }
let (kind, class, nullish) = match val {
VMValue::Integer(_) => ("Integer", "".to_string(), None),
VMValue::Float(_) => ("Float", "".to_string(), None),
VMValue::Bool(_) => ("Bool", "".to_string(), None),
VMValue::String(_) => ("String", "".to_string(), None),
VMValue::Void => ("Void", "".to_string(), None),
VMValue::Future(_) => ("Future", "".to_string(), None),
VMValue::BoxRef(b) => {
// Prefer InstanceBox.class_name when available
if let Some(inst) = b.as_any().downcast_ref::<crate::instance_v2::InstanceBox>() {
let tag = if crate::config::env::null_missing_box_enabled() {
if b.as_any().downcast_ref::<crate::boxes::null_box::NullBox>().is_some() { Some("null") }
else if b.as_any().downcast_ref::<crate::boxes::missing_box::MissingBox>().is_some() { Some("missing") }
else { None }
} else { None };
("BoxRef", inst.class_name.clone(), tag)
} else {
let tag = if crate::config::env::null_missing_box_enabled() {
if b.as_any().downcast_ref::<crate::boxes::null_box::NullBox>().is_some() { Some("null") }
else if b.as_any().downcast_ref::<crate::boxes::missing_box::MissingBox>().is_some() { Some("missing") }
else { None }
} else { None };
("BoxRef", b.type_name().to_string(), tag)
}
}
};
if let Some(tag) = nullish {
eprintln!(
"{{\"ev\":\"print\",\"kind\":\"{}\",\"class\":\"{}\",\"nullish\":\"{}\"}}",
kind,
Self::json_escape(&class),
tag
);
} else {
eprintln!(
"{{\"ev\":\"print\",\"kind\":\"{}\",\"class\":\"{}\"}}",
kind,
Self::json_escape(&class)
);
}
}
}