vm/router: minimal special-method extension (equals/1); toString mapping kept

mir: add TypeCertainty to Callee::Method (diagnostic only); plumb through builder/JSON/printer; backends ignore behaviorally

using: confirm unified prelude resolver entry for all runner modes

docs: update Callee architecture with certainty; update call-instructions; CURRENT_TASK note

tests: quick 40/40 PASS; integration (LLVM) 17/17 PASS
This commit is contained in:
nyash-codex
2025-09-28 01:33:58 +09:00
parent 8ea95c9d76
commit 34be7d2d79
63 changed files with 5008 additions and 356 deletions

View File

@ -13,6 +13,8 @@ impl MirInterpreter {
func: &MirFunction,
arg_vals: Option<&[VMValue]>,
) -> Result<VMValue, VMError> {
// Phase 1: delegate cross-class reroute / narrow fallbacks to method_router
if let Some(r) = super::method_router::pre_exec_reroute(self, func, arg_vals) { return r; }
let saved_regs = mem::take(&mut self.regs);
let saved_fn = self.cur_fn.clone();
self.cur_fn = Some(func.signature.name.clone());

View File

@ -185,6 +185,33 @@ impl MirInterpreter {
}
return Ok(());
}
// Policy gate: user InstanceBox BoxCall runtime fallback
// - Prod: disallowed (builder must have rewritten obj.m(...) to a
// function call). Error here indicates a builder/using materialize
// miss.
// - Dev/CI: allowed with WARN to aid diagnosis.
let mut user_instance_class: Option<String> = None;
if let VMValue::BoxRef(ref b) = self.reg_load(box_val)? {
if let Some(inst) = b.as_any().downcast_ref::<crate::instance_v2::InstanceBox>() {
user_instance_class = Some(inst.class_name.clone());
}
}
if user_instance_class.is_some() && !crate::config::env::vm_allow_user_instance_boxcall() {
let cls = user_instance_class.unwrap();
return Err(VMError::InvalidInstruction(format!(
"User Instance BoxCall disallowed in prod: {}.{} (enable builder rewrite)",
cls, method
)));
}
if user_instance_class.is_some() && crate::config::env::vm_allow_user_instance_boxcall() {
if crate::config::env::cli_verbose() {
eprintln!(
"[warn] dev fallback: user instance BoxCall {}.{} routed via VM instance-dispatch",
user_instance_class.as_ref().unwrap(),
method
);
}
}
if self.try_handle_instance_box(dst, box_val, method, args)? {
if method == "length" && std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1") {
eprintln!("[vm-trace] length dispatch handler=instance_box");
@ -334,6 +361,10 @@ impl MirInterpreter {
}
// First: prefer fields_ng (NyashValue) when present
if let Some(nv) = inst.get_field_ng(&fname) {
// Dev trace: JsonToken field get
if std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1") && inst.class_name == "JsonToken" {
eprintln!("[vm-trace] JsonToken.getField name={} nv={:?}", fname, nv);
}
// Treat complex Box-like values as "missing" for internal storage so that
// legacy obj_fields (which stores BoxRef) is used instead.
// This avoids NV::Box/Array/Map being converted to Void by nv_to_vm.
@ -541,6 +572,16 @@ impl MirInterpreter {
v => v.to_string(),
};
let valv = self.reg_load(args[1])?;
// Dev trace: JsonToken field set
if std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1") {
if let VMValue::BoxRef(bref) = self.reg_load(box_val)? {
if let Some(inst) = bref.as_any().downcast_ref::<crate::instance_v2::InstanceBox>() {
if inst.class_name == "JsonToken" {
eprintln!("[vm-trace] JsonToken.setField name={} vmval={:?}", fname, valv);
}
}
}
}
if Self::box_trace_enabled() {
let vkind = match &valv {
VMValue::Integer(_) => "Integer",
@ -714,6 +755,12 @@ impl MirInterpreter {
}
// Build argv: me + args (works for both instance and static(me, ...))
let mut argv: Vec<VMValue> = Vec::with_capacity(1 + args.len());
// Dev assert: forbid birth(me==Void)
if method == "birth" && crate::config::env::using_is_dev() {
if matches!(recv_vm, VMValue::Void) {
return Err(VMError::InvalidInstruction("Dev assert: birth(me==Void) is forbidden".into()));
}
}
argv.push(recv_vm.clone());
for a in args { argv.push(self.reg_load(*a)?); }
let ret = self.exec_function_inner(&func, Some(&argv))?;
@ -744,6 +791,11 @@ impl MirInterpreter {
}
if let Some(func) = self.functions.get(fname).cloned() {
let mut argv: Vec<VMValue> = Vec::with_capacity(1 + args.len());
if method == "birth" && crate::config::env::using_is_dev() {
if matches!(recv_vm, VMValue::Void) {
return Err(VMError::InvalidInstruction("Dev assert: birth(me==Void) is forbidden".into()));
}
}
argv.push(recv_vm.clone());
for a in args { argv.push(self.reg_load(*a)?); }
let ret = self.exec_function_inner(&func, Some(&argv))?;
@ -877,6 +929,27 @@ impl MirInterpreter {
}
return Ok(());
}
// Minimal runtime fallback for common InstanceBox.is_eof when lowered function is not present.
// This avoids cross-class leaks and hard errors in union-like flows.
if method == "is_eof" && args.is_empty() {
if let Some(inst) = recv_box.as_any().downcast_ref::<crate::instance_v2::InstanceBox>() {
if inst.class_name == "JsonToken" {
let is = match inst.get_field_ng("type") {
Some(crate::value::NyashValue::String(ref s)) => s == "EOF",
_ => false,
};
if let Some(d) = dst { self.regs.insert(d, VMValue::Bool(is)); }
return Ok(());
}
if inst.class_name == "JsonScanner" {
let pos = match inst.get_field_ng("position") { Some(crate::value::NyashValue::Integer(i)) => i, _ => 0 };
let len = match inst.get_field_ng("length") { Some(crate::value::NyashValue::Integer(i)) => i, _ => 0 };
let is = pos >= len;
if let Some(d) = dst { self.regs.insert(d, VMValue::Bool(is)); }
return Ok(());
}
}
}
// Dynamic fallback for user-defined InstanceBox: dispatch to lowered function "Class.method/Arity"
if let Some(inst) = recv_box.as_any().downcast_ref::<crate::instance_v2::InstanceBox>() {
let class_name = inst.class_name.clone();

View File

@ -30,10 +30,21 @@ impl MirInterpreter {
box_name: _,
method,
receiver,
certainty: _,
} => {
if let Some(recv_id) = receiver {
let recv_val = self.reg_load(*recv_id)?;
self.execute_method_call(&recv_val, method, args)
let dev_trace = std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1");
let is_kw = method == &"keyword_to_token_type";
if dev_trace && is_kw {
let a0 = args.get(0).and_then(|id| self.reg_load(*id).ok());
eprintln!("[vm-trace] mcall {} argv0={:?}", method, a0);
}
let out = self.execute_method_call(&recv_val, method, args)?;
if dev_trace && is_kw {
eprintln!("[vm-trace] mret {} -> {:?}", method, out);
}
Ok(out)
} else {
Err(VMError::InvalidInstruction(format!(
"Method call missing receiver for {}",
@ -148,6 +159,19 @@ impl MirInterpreter {
for a in args {
argv.push(self.reg_load(*a)?);
}
let dev_trace = std::env::var("NYASH_VM_TRACE").ok().as_deref() == Some("1");
let is_kw = fname.ends_with("JsonTokenizer.keyword_to_token_type/1");
let is_sc_ident = fname.ends_with("JsonScanner.read_identifier/0");
let is_sc_current = fname.ends_with("JsonScanner.current/0");
let is_tok_kw = fname.ends_with("JsonTokenizer.tokenize_keyword/0");
let is_tok_struct = fname.ends_with("JsonTokenizer.create_structural_token/2");
if dev_trace && (is_kw || is_sc_ident || is_sc_current || is_tok_kw || is_tok_struct) {
if let Some(a0) = argv.get(0) {
eprintln!("[vm-trace] call {} argv0={:?}", fname, a0);
} else {
eprintln!("[vm-trace] call {}", fname);
}
}
// Dev trace: emit a synthetic "call" event for global function calls
// so operator boxes (e.g., CompareOperator.apply/3) are observable with
// argument kinds. This produces a JSON line on stderr, filtered by
@ -282,7 +306,11 @@ impl MirInterpreter {
}
}
}
self.exec_function_inner(&callee, Some(&argv))
let out = self.exec_function_inner(&callee, Some(&argv))?;
if dev_trace && (is_kw || is_sc_ident || is_sc_current || is_tok_kw || is_tok_struct) {
eprintln!("[vm-trace] ret {} -> {:?}", fname, out);
}
Ok(out)
}
fn execute_global_function(

View File

@ -0,0 +1,145 @@
/*!
* Method router for MirInterpreter — centralized cross-class reroute and
* narrow special-method fallbacks. Phase 1: minimal extraction from exec.rs
* to keep behavior unchanged while making execution flow easier to reason about.
*/
use super::{MirFunction, MirInterpreter};
use crate::backend::vm::{VMError, VMValue};
#[derive(Debug, Clone)]
struct ParsedSig<'a> {
class: &'a str,
method: &'a str,
arity_str: &'a str,
}
fn parse_method_signature(name: &str) -> Option<ParsedSig<'_>> {
let dot = name.find('.')?;
let slash = name.rfind('/')?;
if dot >= slash { return None; }
let class = &name[..dot];
let method = &name[dot + 1..slash];
let arity_str = &name[slash + 1..];
Some(ParsedSig { class, method, arity_str })
}
fn extract_instance_box_class(arg0: &VMValue) -> Option<String> {
if let VMValue::BoxRef(bx) = arg0 {
if let Some(inst) = bx.as_any().downcast_ref::<crate::instance_v2::InstanceBox>() {
return Some(inst.class_name.clone());
}
}
None
}
fn reroute_to_correct_method(
interp: &mut MirInterpreter,
recv_cls: &str,
parsed: &ParsedSig<'_>,
arg_vals: Option<&[VMValue]>,
) -> Option<Result<VMValue, VMError>> {
let target = format!("{}.{}{}", recv_cls, parsed.method, format!("/{}", parsed.arity_str));
if let Some(f) = interp.functions.get(&target).cloned() {
return Some(interp.exec_function_inner(&f, arg_vals));
}
None
}
/// Try mapping special methods to canonical targets (table-driven).
/// Example: toString/0 → stringify/0 (prefer instance class, then base class without "Instance" suffix).
fn try_special_reroute(
interp: &mut MirInterpreter,
recv_cls: &str,
parsed: &ParsedSig<'_>,
arg_vals: Option<&[VMValue]>,
) -> Option<Result<VMValue, VMError>> {
// toString → stringify
if parsed.method == "toString" && parsed.arity_str == "0" {
// Prefer instance class stringify first, then base (strip trailing "Instance")
let base = recv_cls.strip_suffix("Instance").unwrap_or(recv_cls);
let candidates = [
format!("{}.stringify/0", recv_cls),
format!("{}.stringify/0", base),
];
for name in candidates.iter() {
if let Some(f) = interp.functions.get(name).cloned() {
return Some(interp.exec_function_inner(&f, arg_vals));
}
}
}
// equals passthrough (instance/base)
// In some user setups, only base class provides equals(other).
// Try instance first, then base (strip trailing "Instance").
if parsed.method == "equals" && parsed.arity_str == "1" {
let base = recv_cls.strip_suffix("Instance").unwrap_or(recv_cls);
let candidates = [
format!("{}.equals/1", recv_cls),
format!("{}.equals/1", base),
];
for name in candidates.iter() {
if let Some(f) = interp.functions.get(name).cloned() {
return Some(interp.exec_function_inner(&f, arg_vals));
}
}
}
None
}
fn try_special_method(
recv_cls: &str,
parsed: &ParsedSig<'_>,
arg_vals: Option<&[VMValue]>,
) -> Option<Result<VMValue, VMError>> {
// Keep narrow fallbacks minimal, deterministic, and cheap.
if parsed.method == "is_eof" && parsed.arity_str == "0" {
if let Some(args) = arg_vals {
if let VMValue::BoxRef(bx) = &args[0] {
if let Some(inst) = bx.as_any().downcast_ref::<crate::instance_v2::InstanceBox>() {
if recv_cls == "JsonToken" {
let is = match inst.get_field_ng("type") {
Some(crate::value::NyashValue::String(ref s)) => s == "EOF",
_ => false,
};
return Some(Ok(VMValue::Bool(is)));
}
if recv_cls == "JsonScanner" {
let pos = match inst.get_field_ng("position") { Some(crate::value::NyashValue::Integer(i)) => i, _ => 0 };
let len = match inst.get_field_ng("length") { Some(crate::value::NyashValue::Integer(i)) => i, _ => 0 };
return Some(Ok(VMValue::Bool(pos >= len)));
}
}
}
}
}
None
}
/// Pre-execution reroute/short-circuit.
///
/// When a direct Call to "Class.method/N" is about to execute, verify that the
/// first argument ('me') actually belongs to the same InstanceBox class. If it
/// does not, try rerouting to the matching class method. If no matching method
/// exists, apply a very narrow fallback for well-known methods (dev-oriented,
/// but safe and deterministic) and return a value. Returning Some(Result<..>)
/// indicates that the router handled the call (rerouted or short-circuited).
/// Returning None means normal execution should continue.
pub(super) fn pre_exec_reroute(
interp: &mut MirInterpreter,
func: &MirFunction,
arg_vals: Option<&[VMValue]>,
) -> Option<Result<VMValue, VMError>> {
let args = match arg_vals { Some(a) => a, None => return None };
if args.is_empty() { return None; }
let parsed = match parse_method_signature(func.signature.name.as_str()) { Some(p) => p, None => return None };
let recv_cls = match extract_instance_box_class(&args[0]) { Some(c) => c, None => return None };
// Always consider special re-routes (e.g., toString→stringify) even when class matches
if let Some(r) = try_special_reroute(interp, &recv_cls, &parsed, arg_vals) { return Some(r); }
if recv_cls == parsed.class { return None; }
// Class mismatch: reroute to same method on the receiver's class
if let Some(r) = reroute_to_correct_method(interp, &recv_cls, &parsed, arg_vals) { return Some(r); }
// Narrow special fallback (e.g., is_eof)
if let Some(r) = try_special_method(&recv_cls, &parsed, arg_vals) { return Some(r); }
None
}

View File

@ -20,6 +20,7 @@ pub(super) use crate::mir::{
mod exec;
mod handlers;
mod helpers;
mod method_router;
pub struct MirInterpreter {
pub(super) regs: HashMap<ValueId, VMValue>,

View File

@ -213,6 +213,10 @@ pub fn from_matches(matches: &ArgMatches) -> CliConfig {
std::env::set_var("NYASH_USING_PROFILE", "dev");
// AST prelude merge
std::env::set_var("NYASH_USING_AST", "1");
// Using grammar is mainline; keep explicit enable for clarity (default is ON; this makes intent obvious in dev)
std::env::set_var("NYASH_ENABLE_USING", "1");
// Allow top-level main resolution in dev for convenience (prod default remains OFF)
std::env::set_var("NYASH_ENTRY_ALLOW_TOPLEVEL_MAIN", "1");
// Ensure project root is available for prelude injection
if std::env::var("NYASH_ROOT").is_err() {
if let Ok(cwd) = std::env::current_dir() {

View File

@ -405,6 +405,17 @@ pub fn using_ast_enabled() -> bool {
_ => !using_is_prod(), // dev/ci → true, prod → false
}
}
/// Policy: allow VM to fallback-dispatch user Instance BoxCall (dev only by default).
/// - prod: default false (disallow)
/// - dev/ci: default true (allow, with WARN)
/// Override with NYASH_VM_USER_INSTANCE_BOXCALL={0|1}
pub fn vm_allow_user_instance_boxcall() -> bool {
match std::env::var("NYASH_VM_USER_INSTANCE_BOXCALL").ok().as_deref().map(|v| v.to_ascii_lowercase()) {
Some(ref s) if s == "0" || s == "false" || s == "off" => false,
Some(ref s) if s == "1" || s == "true" || s == "on" => true,
_ => !using_is_prod(),
}
}
// Legacy resolve_fix_braces() removed (Phase 15 cleanup)
// AST-based integration handles syntax properly without text-level brace fixing
pub fn vm_use_py() -> bool {
@ -476,14 +487,14 @@ pub fn method_catch() -> bool {
}
/// Entry policy: allow top-level `main` resolution in addition to `Main.main`.
/// Default: false (prefer explicit `static box Main { main(...) }`).
/// Default: true (prefer `Main.main` when both exist; otherwise accept `main`).
pub fn entry_allow_toplevel_main() -> bool {
match std::env::var("NYASH_ENTRY_ALLOW_TOPLEVEL_MAIN").ok() {
Some(v) => {
let v = v.to_ascii_lowercase();
v == "1" || v == "true" || v == "on"
}
None => false,
None => true,
}
}

36
src/debug/hub.rs Normal file
View File

@ -0,0 +1,36 @@
use std::io::Write;
/// Minimal debug hub: JSONL event emitter (dev-only; default OFF).
///
/// Env knobs:
/// - NYASH_DEBUG_ENABLE=1 master gate
/// - NYASH_DEBUG_KINDS=resolve,ssa allowed cats (comma-separated)
/// - NYASH_DEBUG_SINK=path file to append JSONL events
pub fn emit(cat: &str, kind: &str, fn_name: Option<&str>, region_id: Option<&str>, meta: serde_json::Value) {
if std::env::var("NYASH_DEBUG_ENABLE").ok().as_deref() != Some("1") {
return;
}
if let Ok(kinds) = std::env::var("NYASH_DEBUG_KINDS") {
if !kinds.split(',').any(|k| k.trim().eq_ignore_ascii_case(cat)) {
return;
}
}
let sink = match std::env::var("NYASH_DEBUG_SINK") {
Ok(s) if !s.is_empty() => s,
_ => return,
};
let ts = chrono::Utc::now().to_rfc3339_opts(chrono::SecondsFormat::Millis, true);
let obj = serde_json::json!({
"ts": ts,
"phase": "builder",
"fn": fn_name.unwrap_or("<unknown>"),
"region_id": region_id.unwrap_or(""),
"cat": cat,
"kind": kind,
"meta": meta,
});
if let Ok(mut f) = std::fs::OpenOptions::new().create(true).append(true).open(&sink) {
let _ = writeln!(f, "{}", obj.to_string());
}
}

View File

@ -1 +1,2 @@
pub mod log;
pub mod hub;

View File

@ -135,6 +135,15 @@ pub struct MirBuilder {
temp_slot_counter: u32,
/// If true, skip entry materialization of pinned slots on the next start_new_block call.
suppress_pin_entry_copy_next: bool,
// ----------------------
// Debug scope context (dev only; zero-cost when unused)
// ----------------------
/// Stack of region identifiers like "loop#1/header" or "join#3/join".
debug_scope_stack: Vec<String>,
/// Monotonic counters for region IDs (deterministic across a run).
debug_loop_counter: u32,
debug_join_counter: u32,
}
impl MirBuilder {
@ -175,6 +184,11 @@ impl MirBuilder {
hint_sink: crate::mir::hints::HintSink::new(),
temp_slot_counter: 0,
suppress_pin_entry_copy_next: false,
// Debug scope context
debug_scope_stack: Vec::new(),
debug_loop_counter: 0,
debug_join_counter: 0,
}
}
@ -201,6 +215,47 @@ impl MirBuilder {
self.hint_sink.loop_carrier(vars.into_iter().map(|s| s.into()).collect::<Vec<_>>());
}
// ----------------------
// Debug scope helpers (region_id for DebugHub events)
// ----------------------
#[inline]
pub(crate) fn debug_next_loop_id(&mut self) -> u32 {
let id = self.debug_loop_counter;
self.debug_loop_counter = self.debug_loop_counter.saturating_add(1);
id
}
#[inline]
pub(crate) fn debug_next_join_id(&mut self) -> u32 {
let id = self.debug_join_counter;
self.debug_join_counter = self.debug_join_counter.saturating_add(1);
id
}
#[inline]
pub(crate) fn debug_push_region<S: Into<String>>(&mut self, region: S) {
self.debug_scope_stack.push(region.into());
}
#[inline]
pub(crate) fn debug_pop_region(&mut self) {
let _ = self.debug_scope_stack.pop();
}
#[inline]
pub(crate) fn debug_replace_region<S: Into<String>>(&mut self, region: S) {
if let Some(top) = self.debug_scope_stack.last_mut() {
*top = region.into();
} else {
self.debug_scope_stack.push(region.into());
}
}
#[inline]
pub(crate) fn debug_current_region_id(&self) -> Option<String> {
self.debug_scope_stack.last().cloned()
}
/// Build a complete MIR module from AST
pub fn build_module(&mut self, ast: ASTNode) -> Result<MirModule, String> {
@ -293,7 +348,90 @@ impl MirBuilder {
pub(super) fn emit_instruction(&mut self, instruction: MirInstruction) -> Result<(), String> {
let block_id = self.current_block.ok_or("No current basic block")?;
// Precompute debug metadata to avoid borrow conflicts later
let dbg_fn_name = self
.current_function
.as_ref()
.map(|f| f.signature.name.clone());
let dbg_region_id = self.debug_current_region_id();
if let Some(ref mut function) = self.current_function {
// Dev-safe meta propagation for PHI: if all incoming values agree on type/origin,
// propagate to the PHI destination. This helps downstream resolution (e.g.,
// instance method rewrite across branches) without changing semantics.
if let MirInstruction::Phi { dst, inputs } = &instruction {
// Propagate value_types when all inputs share the same known type
let mut common_ty: Option<super::MirType> = None;
let mut ty_agree = true;
for (_bb, v) in inputs.iter() {
if let Some(t) = self.value_types.get(v).cloned() {
match &common_ty {
None => common_ty = Some(t),
Some(ct) => {
if ct != &t { ty_agree = false; break; }
}
}
} else {
ty_agree = false;
break;
}
}
if ty_agree {
if let Some(ct) = common_ty.clone() {
self.value_types.insert(*dst, ct);
}
}
// Propagate value_origin_newbox when all inputs share same origin class
let mut common_cls: Option<String> = None;
let mut cls_agree = true;
for (_bb, v) in inputs.iter() {
if let Some(c) = self.value_origin_newbox.get(v).cloned() {
match &common_cls {
None => common_cls = Some(c),
Some(cc) => {
if cc != &c { cls_agree = false; break; }
}
}
} else {
cls_agree = false;
break;
}
}
if cls_agree {
if let Some(cc) = common_cls.clone() {
self.value_origin_newbox.insert(*dst, cc);
}
}
// Emit debug event (dev-only)
{
let preds: Vec<serde_json::Value> = inputs.iter().map(|(bb,v)| {
let t = self.value_types.get(v).cloned();
let o = self.value_origin_newbox.get(v).cloned();
serde_json::json!({
"bb": bb.0,
"v": v.0,
"type": t.as_ref().map(|tt| format!("{:?}", tt)).unwrap_or_default(),
"origin": o.unwrap_or_default(),
})
}).collect();
let decided_t = self.value_types.get(dst).cloned().map(|tt| format!("{:?}", tt)).unwrap_or_default();
let decided_o = self.value_origin_newbox.get(dst).cloned().unwrap_or_default();
let meta = serde_json::json!({
"dst": dst.0,
"preds": preds,
"decided_type": decided_t,
"decided_origin": decided_o,
});
let fn_name = dbg_fn_name.as_deref();
let region = dbg_region_id.as_deref();
crate::debug::hub::emit(
"ssa",
"phi",
fn_name,
region,
meta,
);
}
}
if let Some(block) = function.get_block_mut(block_id) {
if utils::builder_debug_enabled() {
eprintln!(
@ -459,16 +597,22 @@ impl MirBuilder {
argv.extend(arg_values.iter().copied());
self.emit_legacy_call(None, CallTarget::Global(lowered), argv)?;
} else {
// Fallback: instance method BoxCall("birth")
let birt_mid = resolve_slot_by_type_name(&class, "birth");
self.emit_box_or_plugin_call(
None,
dst,
"birth".to_string(),
birt_mid,
arg_values,
EffectMask::READ.add(Effect::ReadHeap),
)?;
// Fallback policy:
// - For user-defined boxes (no explicit constructor), do NOT emit BoxCall("birth").
// VM will treat plain NewBox as constructed; dev verify warns if needed.
// - For builtins/plugins, keep BoxCall("birth") fallback to preserve legacy init.
let is_user_box = self.user_defined_boxes.contains(&class);
if !is_user_box {
let birt_mid = resolve_slot_by_type_name(&class, "birth");
self.emit_box_or_plugin_call(
None,
dst,
"birth".to_string(),
birt_mid,
arg_values,
EffectMask::READ.add(Effect::ReadHeap),
)?;
}
}
}

View File

@ -50,10 +50,18 @@ pub fn convert_target_to_callee(
.unwrap_or_else(|| "UnknownBox".to_string())
});
// Certainty is Known when origin propagation provides a concrete class name
let certainty = if value_origin_newbox.contains_key(&receiver) {
crate::mir::definitions::call_unified::TypeCertainty::Known
} else {
crate::mir::definitions::call_unified::TypeCertainty::Union
};
Ok(Callee::Method {
box_name: inferred_box_type,
method,
receiver: Some(receiver),
certainty,
})
},
@ -195,4 +203,4 @@ pub fn validate_call_args(
}
Ok(())
}
}

View File

@ -32,6 +32,7 @@ pub fn resolve_call_target(
box_name: box_name.clone(),
method: name.to_string(),
receiver: None, // Static method call
certainty: crate::mir::definitions::call_unified::TypeCertainty::Known,
});
}
}

View File

@ -11,6 +11,8 @@ impl MirBuilder {
then_branch: ASTNode,
else_branch: Option<ASTNode>,
) -> Result<ValueId, String> {
// Reserve a deterministic join id for debug region labeling
let join_id = self.debug_next_join_id();
// Heuristic pre-pin: if condition is a comparison, evaluate its operands now and pin them
// so that subsequent branches can safely reuse these values across blocks.
// This leverages existing variable_map merges (PHI) at the merge block.
@ -57,6 +59,8 @@ impl MirBuilder {
// then
self.start_new_block(then_block)?;
// Debug region: join then-branch
self.debug_push_region(format!("join#{}", join_id) + "/then");
// Scope enter for then-branch
self.hint_scope_enter(0);
let then_ast_for_analysis = then_branch.clone();
@ -82,9 +86,13 @@ impl MirBuilder {
self.hint_scope_leave(0);
self.emit_instruction(MirInstruction::Jump { target: merge_block })?;
}
// Pop then-branch debug region
self.debug_pop_region();
// else
self.start_new_block(else_block)?;
// Debug region: join else-branch
self.debug_push_region(format!("join#{}", join_id) + "/else");
// Scope enter for else-branch
self.hint_scope_enter(0);
// Materialize all variables at block entry via single-pred Phi (correctness-first)
@ -115,11 +123,15 @@ impl MirBuilder {
self.hint_scope_leave(0);
self.emit_instruction(MirInstruction::Jump { target: merge_block })?;
}
// Pop else-branch debug region
self.debug_pop_region();
// merge: primary result via helper, then delta-based variable merges
// Ensure PHIs are first in the block by suppressing entry pin copies here
self.suppress_next_entry_pin_copy();
self.start_new_block(merge_block)?;
// Debug region: join merge
self.debug_push_region(format!("join#{}", join_id) + "/join");
self.push_if_merge(merge_block);
// Pre-analysis: identify then/else assigned var for skip and hints
@ -175,6 +187,8 @@ impl MirBuilder {
)?;
self.pop_if_merge();
// Pop merge debug region
self.debug_pop_region();
Ok(result_val)
}
}

View File

@ -215,6 +215,54 @@ impl super::MirBuilder {
function.signature.return_type = mt;
}
}
// Dev-only verify: NewBox → birth() invariant (warn if missing)
if crate::config::env::using_is_dev() {
let mut warn_count = 0usize;
for (_bid, bb) in function.blocks.iter() {
let insns = &bb.instructions;
let mut idx = 0usize;
while idx < insns.len() {
if let MirInstruction::NewBox { dst, box_type, args } = &insns[idx] {
// Skip StringBox (literal optimization path)
if box_type != "StringBox" {
let expect_tail = format!("{}.birth/{}", box_type, args.len());
// Look ahead up to 3 instructions for either BoxCall("birth") on dst or Global(expect_tail)
let mut ok = false;
let mut j = idx + 1;
let mut last_const_name: Option<String> = None;
while j < insns.len() && j <= idx + 3 {
match &insns[j] {
MirInstruction::BoxCall { box_val, method, .. } => {
if method == "birth" && box_val == dst { ok = true; break; }
}
MirInstruction::Const { value, .. } => {
if let super::ConstValue::String(s) = value { last_const_name = Some(s.clone()); }
}
MirInstruction::Call { func, .. } => {
// If immediately preceded by matching Const String, accept
if let Some(prev) = last_const_name.as_ref() {
if prev == &expect_tail { ok = true; break; }
}
// Heuristic: in some forms, builder may reuse a shared const; best-effort only
}
_ => {}
}
j += 1;
}
if !ok {
eprintln!("[warn] dev verify: NewBox {} at v{} not followed by birth() call (expect {})", box_type, dst, expect_tail);
warn_count += 1;
}
}
}
idx += 1;
}
}
if warn_count > 0 {
eprintln!("[warn] dev verify: NewBox→birth invariant warnings: {}", warn_count);
}
}
module.add_function(function);
Ok(module)

View File

@ -125,8 +125,15 @@ impl MirBuilder {
if let MirType::Box(bn) = t { class_name_opt = Some(bn.clone()); }
}
}
// Optional dev/ci gate: enable builder-side instance→function rewrite by default
// in dev/ci profiles, keep OFF in prod. Allow explicit override via env:
// Instance→Function rewrite (obj.m(a) → Box.m/Arity(obj,a))
// Phase 2 policy: Only rewrite when receiver class is Known (from origin propagation).
let class_known = self.value_origin_newbox.get(&object_value).is_some();
// Rationale:
// - Keep language surface idiomatic (obj.method()), while executing
// deterministically as a direct function call.
// - Prod VM forbids user Instance BoxCall fallback by policy; this
// rewrite guarantees prod runs without runtime instance-dispatch.
// Control:
// NYASH_BUILDER_REWRITE_INSTANCE={1|true|on} → force enable
// NYASH_BUILDER_REWRITE_INSTANCE={0|false|off} → force disable
let rewrite_enabled = {
@ -139,27 +146,120 @@ impl MirBuilder {
}
}
};
// Emit resolve.try event (dev-only) before making a decision
if rewrite_enabled {
if let Some(cls) = class_name_opt.clone() {
if self.user_defined_boxes.contains(&cls) {
let arity = arg_values.len(); // function name arity excludes 'me'
let fname = crate::mir::builder::calls::function_lowering::generate_method_function_name(&cls, &method, arity);
// Gate: only rewrite when the lowered function actually exists (prevents false rewrites like JsonScanner.length/0)
let exists = if let Some(ref module) = self.current_module {
module.functions.contains_key(&fname)
} else { false };
if exists {
if let Some(ref module) = self.current_module {
let tail = format!(".{}{}", method, format!("/{}", arguments.len()));
let candidates: Vec<String> = module
.functions
.keys()
.filter(|k| k.ends_with(&tail))
.cloned()
.collect();
let recv_cls = class_name_opt.clone().or_else(|| self.value_origin_newbox.get(&object_value).cloned()).unwrap_or_default();
let meta = serde_json::json!({
"recv_cls": recv_cls,
"method": method,
"arity": arguments.len(),
"candidates": candidates,
});
let fn_name = self.current_function.as_ref().map(|f| f.signature.name.as_str());
let region = self.debug_current_region_id();
crate::debug::hub::emit(
"resolve",
"try",
fn_name,
region.as_deref(),
meta,
);
}
}
// Early special-case: toString → stringify mapping when user function exists
if method == "toString" && arguments.len() == 0 {
if let Some(ref module) = self.current_module {
// Prefer class-qualified stringify if we can infer class
if let Some(cls_ts) = class_name_opt.clone() {
let stringify_name = crate::mir::builder::calls::function_lowering::generate_method_function_name(&cls_ts, "stringify", 0);
if module.functions.contains_key(&stringify_name) {
if super::utils::builder_debug_enabled() || std::env::var("NYASH_BUILDER_DEBUG").ok().as_deref() == Some("1") {
super::utils::builder_debug_log(&format!("(early) toString→stringify cls={} fname={}", cls_ts, stringify_name));
}
// DebugHub emit: resolve.choose (early, class)
{
let meta = serde_json::json!({
"recv_cls": cls_ts,
"method": "toString",
"arity": 0,
"chosen": stringify_name,
"reason": "toString-early-class",
});
let fn_name = self.current_function.as_ref().map(|f| f.signature.name.as_str());
let region = self.debug_current_region_id();
crate::debug::hub::emit(
"resolve",
"choose",
fn_name,
region.as_deref(),
meta,
);
}
let name_const = self.value_gen.next();
self.emit_instruction(MirInstruction::Const {
dst: name_const,
value: crate::mir::builder::ConstValue::String(stringify_name.clone()),
})?;
let mut call_args = Vec::with_capacity(1);
call_args.push(object_value);
let dst = self.value_gen.next();
self.emit_instruction(MirInstruction::Call {
dst: Some(dst),
func: name_const,
callee: None,
args: call_args,
effects: crate::mir::EffectMask::READ.add(crate::mir::Effect::ReadHeap),
})?;
self.annotate_call_result_from_func_name(dst, &stringify_name);
return Ok(dst);
}
}
// Fallback: unique suffix ".stringify/0" in module
let mut cands: Vec<String> = module
.functions
.keys()
.filter(|k| k.ends_with(".stringify/0"))
.cloned()
.collect();
if cands.len() == 1 {
let fname = cands.remove(0);
if super::utils::builder_debug_enabled() || std::env::var("NYASH_BUILDER_DEBUG").ok().as_deref() == Some("1") {
super::utils::builder_debug_log(&format!("userbox method-call cls={} method={} fname={}", cls, method, fname));
super::utils::builder_debug_log(&format!("(early) toString→stringify unique-suffix fname={}", fname));
}
// DebugHub emit: resolve.choose (early, unique)
{
let meta = serde_json::json!({
"recv_cls": class_name_opt.clone().unwrap_or_default(),
"method": "toString",
"arity": 0,
"chosen": fname,
"reason": "toString-early-unique",
});
let fn_name = self.current_function.as_ref().map(|f| f.signature.name.as_str());
let region = self.debug_current_region_id();
crate::debug::hub::emit(
"resolve",
"choose",
fn_name,
region.as_deref(),
meta,
);
}
let name_const = self.value_gen.next();
self.emit_instruction(MirInstruction::Const {
dst: name_const,
value: crate::mir::builder::ConstValue::String(fname.clone()),
})?;
let mut call_args = Vec::with_capacity(arity + 1);
call_args.push(object_value); // 'me'
call_args.extend(arg_values.into_iter());
let mut call_args = Vec::with_capacity(1);
call_args.push(object_value);
let dst = self.value_gen.next();
self.emit_instruction(MirInstruction::Call {
dst: Some(dst),
@ -168,95 +268,41 @@ impl MirBuilder {
args: call_args,
effects: crate::mir::EffectMask::READ.add(crate::mir::Effect::ReadHeap),
})?;
// Annotate return type/origin from lowered function signature
self.annotate_call_result_from_func_name(dst, &format!("{}.{}{}", cls, method, format!("/{}", arity)));
self.annotate_call_result_from_func_name(dst, &fname);
return Ok(dst);
} else {
// Special-case: treat toString as stringify when method not present
if method == "toString" && arity == 0 {
if let Some(ref module) = self.current_module {
let stringify_name = crate::mir::builder::calls::function_lowering::generate_method_function_name(&cls, "stringify", 0);
if module.functions.contains_key(&stringify_name) {
let name_const = self.value_gen.next();
self.emit_instruction(MirInstruction::Const {
dst: name_const,
value: crate::mir::builder::ConstValue::String(stringify_name.clone()),
})?;
let mut call_args = Vec::with_capacity(1);
call_args.push(object_value);
let dst = self.value_gen.next();
self.emit_instruction(MirInstruction::Call {
dst: Some(dst),
func: name_const,
callee: None,
args: call_args,
effects: crate::mir::EffectMask::READ.add(crate::mir::Effect::ReadHeap),
})?;
self.annotate_call_result_from_func_name(dst, &stringify_name);
return Ok(dst);
}
}
}
// Try alternate naming: <Class>Instance.method/Arity
let alt_cls = format!("{}Instance", cls);
let alt_fname = crate::mir::builder::calls::function_lowering::generate_method_function_name(&alt_cls, &method, arity);
let alt_exists = if let Some(ref module) = self.current_module {
module.functions.contains_key(&alt_fname)
} else { false };
if alt_exists {
} else if cands.len() > 1 {
// Deterministic tie-breaker: prefer JsonNode.stringify/0 over JsonNodeInstance.stringify/0
if let Some(pos) = cands.iter().position(|n| n == "JsonNode.stringify/0") {
let fname = cands.remove(pos);
if super::utils::builder_debug_enabled() || std::env::var("NYASH_BUILDER_DEBUG").ok().as_deref() == Some("1") {
super::utils::builder_debug_log(&format!("userbox method-call alt cls={} method={} fname={}", alt_cls, method, alt_fname));
super::utils::builder_debug_log(&format!("(early) toString→stringify prefer JsonNode fname={}", fname));
}
// DebugHub emit: resolve.choose (early, prefer-JsonNode)
{
let meta = serde_json::json!({
"recv_cls": class_name_opt.clone().unwrap_or_default(),
"method": "toString",
"arity": 0,
"chosen": fname,
"reason": "toString-early-prefer-JsonNode",
});
let fn_name = self.current_function.as_ref().map(|f| f.signature.name.as_str());
let region = self.debug_current_region_id();
crate::debug::hub::emit(
"resolve",
"choose",
fn_name,
region.as_deref(),
meta,
);
}
let name_const = self.value_gen.next();
self.emit_instruction(MirInstruction::Const {
dst: name_const,
value: crate::mir::builder::ConstValue::String(alt_fname.clone()),
})?;
let mut call_args = Vec::with_capacity(arity + 1);
call_args.push(object_value); // 'me'
call_args.extend(arg_values.into_iter());
let dst = self.value_gen.next();
self.emit_instruction(MirInstruction::Call {
dst: Some(dst),
func: name_const,
callee: None,
args: call_args,
effects: crate::mir::EffectMask::READ.add(crate::mir::Effect::ReadHeap),
})?;
self.annotate_call_result_from_func_name(dst, &alt_fname);
return Ok(dst);
} else if super::utils::builder_debug_enabled() || std::env::var("NYASH_BUILDER_DEBUG").ok().as_deref() == Some("1") {
super::utils::builder_debug_log(&format!("skip rewrite (no fn): cls={} method={} fname={} alt={} (missing)", cls, method, fname, alt_fname));
}
}
}
}
}
// Fallback (narrowed): only when receiver class is known, and exactly one
// user-defined method matches by name/arity across module, resolve to that.
if rewrite_enabled && class_name_opt.is_some() {
if let Some(ref module) = self.current_module {
let tail = format!(".{}{}", method, format!("/{}", arg_values.len()));
let mut cands: Vec<String> = module
.functions
.keys()
.filter(|k| k.ends_with(&tail))
.cloned()
.collect();
if cands.len() == 1 {
let fname = cands.remove(0);
// sanity: ensure the box prefix looks like a user-defined box
if let Some((bx, _)) = fname.split_once('.') {
if self.user_defined_boxes.contains(bx) {
let name_const = self.value_gen.next();
self.emit_instruction(MirInstruction::Const {
dst: name_const,
value: crate::mir::builder::ConstValue::String(fname.clone()),
})?;
let mut call_args = Vec::with_capacity(arg_values.len() + 1);
call_args.push(object_value); // 'me'
call_args.extend(arg_values.into_iter());
let mut call_args = Vec::with_capacity(1);
call_args.push(object_value);
let dst = self.value_gen.next();
self.emit_instruction(MirInstruction::Call {
dst: Some(dst),
@ -265,13 +311,173 @@ impl MirBuilder {
args: call_args,
effects: crate::mir::EffectMask::READ.add(crate::mir::Effect::ReadHeap),
})?;
// Annotate from signature if present
self.annotate_call_result_from_func_name(dst, &fname);
return Ok(dst);
}
}
}
}
if rewrite_enabled && class_known {
if let Some(cls) = class_name_opt.clone() {
let from_new_origin = self.value_origin_newbox.get(&object_value).is_some();
let allow_new_origin = std::env::var("NYASH_DEV_REWRITE_NEW_ORIGIN").ok().as_deref() == Some("1");
let is_user_box = self.user_defined_boxes.contains(&cls);
let fname = {
let arity = arg_values.len();
crate::mir::builder::calls::function_lowering::generate_method_function_name(&cls, &method, arity)
};
let module_has = if let Some(ref module) = self.current_module { module.functions.contains_key(&fname) } else { false };
let allow_userbox_rewrite = std::env::var("NYASH_DEV_REWRITE_USERBOX").ok().as_deref() == Some("1");
if (is_user_box && (module_has || allow_userbox_rewrite)) || (from_new_origin && allow_new_origin) {
let arity = arg_values.len(); // function name arity excludes 'me'
// Special-case: toString → stringify mapping (only when present)
if method == "toString" && arity == 0 {
if let Some(ref module) = self.current_module {
let stringify_name = crate::mir::builder::calls::function_lowering::generate_method_function_name(&cls, "stringify", 0);
if module.functions.contains_key(&stringify_name) {
if super::utils::builder_debug_enabled() || std::env::var("NYASH_BUILDER_DEBUG").ok().as_deref() == Some("1") {
super::utils::builder_debug_log(&format!("userbox toString→stringify cls={} fname={}", cls, stringify_name));
}
let name_const = self.value_gen.next();
self.emit_instruction(MirInstruction::Const {
dst: name_const,
value: crate::mir::builder::ConstValue::String(stringify_name.clone()),
})?;
let mut call_args = Vec::with_capacity(1);
call_args.push(object_value);
let dst = self.value_gen.next();
self.emit_instruction(MirInstruction::Call {
dst: Some(dst),
func: name_const,
callee: None,
args: call_args,
effects: crate::mir::EffectMask::READ.add(crate::mir::Effect::ReadHeap),
})?;
self.annotate_call_result_from_func_name(dst, &stringify_name);
return Ok(dst);
}
}
}
// Default: unconditionally rewrite to Box.method/Arity. The target
// may be materialized later during lowering of the box; runtime
// resolution by name will succeed once the module is finalized.
let fname = fname.clone();
if super::utils::builder_debug_enabled() || std::env::var("NYASH_BUILDER_DEBUG").ok().as_deref() == Some("1") {
super::utils::builder_debug_log(&format!("userbox method-call cls={} method={} fname={}", cls, method, fname));
}
// Dev WARN when the function is not yet present (materialize pending)
if crate::config::env::cli_verbose() {
if let Some(ref module) = self.current_module {
if !module.functions.contains_key(&fname) {
eprintln!(
"[warn] rewrite (materialize pending): {} (class={}, method={}, arity={})",
fname, cls, method, arity
);
}
}
}
let name_const = self.value_gen.next();
self.emit_instruction(MirInstruction::Const {
dst: name_const,
value: crate::mir::builder::ConstValue::String(fname.clone()),
})?;
let mut call_args = Vec::with_capacity(arity + 1);
call_args.push(object_value); // 'me'
call_args.extend(arg_values.into_iter());
let dst = self.value_gen.next();
self.emit_instruction(MirInstruction::Call {
dst: Some(dst),
func: name_const,
callee: None,
args: call_args,
effects: crate::mir::EffectMask::READ.add(crate::mir::Effect::ReadHeap),
})?;
// Annotate and emit resolve.choose
let chosen = format!("{}.{}{}", cls, method, format!("/{}", arity));
self.annotate_call_result_from_func_name(dst, &chosen);
let meta = serde_json::json!({
"recv_cls": cls,
"method": method,
"arity": arity,
"chosen": chosen,
"reason": "userbox-rewrite",
});
let fn_name = self.current_function.as_ref().map(|f| f.signature.name.as_str());
let region = self.debug_current_region_id();
crate::debug::hub::emit(
"resolve",
"choose",
fn_name,
region.as_deref(),
meta,
);
return Ok(dst);
} else {
// Not a user-defined box; fall through
}
}
}
// Fallback (narrowed): when exactly one user-defined method matches by
// name/arity across the module, resolve to that even if class inference
// failed (defensive for PHI/branch cases). This preserves determinism
// because we require uniqueness and a user-defined box prefix.
if rewrite_enabled && class_known {
if let Some(ref module) = self.current_module {
let tail = format!(".{}{}", method, format!("/{}", arg_values.len()));
let mut cands: Vec<String> = module
.functions
.keys()
.filter(|k| k.ends_with(&tail))
.cloned()
.collect();
if cands.len() == 1 {
let fname = cands.remove(0);
// sanity: ensure the box prefix looks like a user-defined box
if let Some((bx, _)) = fname.split_once('.') {
if self.user_defined_boxes.contains(bx) {
let name_const = self.value_gen.next();
self.emit_instruction(MirInstruction::Const {
dst: name_const,
value: crate::mir::builder::ConstValue::String(fname.clone()),
})?;
let mut call_args = Vec::with_capacity(arg_values.len() + 1);
call_args.push(object_value); // 'me'
let arity_us = arg_values.len();
call_args.extend(arg_values.into_iter());
let dst = self.value_gen.next();
self.emit_instruction(MirInstruction::Call {
dst: Some(dst),
func: name_const,
callee: None,
args: call_args,
effects: crate::mir::EffectMask::READ.add(crate::mir::Effect::ReadHeap),
})?;
// Annotate and emit resolve.choose
self.annotate_call_result_from_func_name(dst, &fname);
let meta = serde_json::json!({
"recv_cls": bx,
"method": method,
"arity": arity_us,
"chosen": fname,
"reason": "unique-suffix",
});
let fn_name = self.current_function.as_ref().map(|f| f.signature.name.as_str());
let region = self.debug_current_region_id();
crate::debug::hub::emit(
"resolve",
"choose",
fn_name,
region.as_deref(),
meta,
);
return Ok(dst);
}
}
}
}
}
// Else fall back to plugin/boxcall path

View File

@ -7,6 +7,15 @@
use crate::mir::{Effect, EffectMask, ValueId};
/// Certainty of callee type information for method calls
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TypeCertainty {
/// Receiver class is known (from origin propagation or static context)
Known,
/// Receiver may be a union/merged flow; class not uniquely known
Union,
}
/// Call target specification for type-safe function resolution
/// Replaces runtime string-based resolution with compile-time typed targets
#[derive(Debug, Clone, PartialEq)]
@ -21,6 +30,7 @@ pub enum Callee {
box_name: String, // "StringBox", "ConsoleStd", etc.
method: String, // "upper", "print", etc.
receiver: Option<ValueId>, // Some(obj) for instance, None for static/constructor
certainty: TypeCertainty, // Phase 3: known vs union
},
/// Constructor call (NewBox equivalent)
@ -181,6 +191,7 @@ impl MirCall {
box_name,
method,
receiver: Some(receiver),
certainty: TypeCertainty::Known,
},
args,
)
@ -288,14 +299,19 @@ pub mod migration {
effects: EffectMask,
) -> MirCall {
// For BoxCall, we need to infer the box type
// This is a temporary solution until we have better type info
MirCall::method(
// Mark certainty as Union (unknown at this stage)
let mut call = MirCall::new(
dst,
"UnknownBox".to_string(), // Will be resolved later
method,
box_val,
Callee::Method {
box_name: "UnknownBox".to_string(),
method,
receiver: Some(box_val),
certainty: TypeCertainty::Union,
},
args,
).with_effects(effects)
);
call.effects = effects;
call
}
/// Convert NewBox to MirCall
@ -318,4 +334,4 @@ pub mod migration {
let full_name = format!("{}.{}", iface_name, method_name);
MirCall::external(dst, full_name, args).with_effects(effects)
}
}
}

View File

@ -118,6 +118,8 @@ impl<'a> LoopBuilder<'a> {
condition: ASTNode,
body: Vec<ASTNode>,
) -> Result<ValueId, String> {
// Reserve a deterministic loop id for debug region labeling
let loop_id = self.parent_builder.debug_next_loop_id();
// Pre-scan body for simple carrier pattern (up to 2 assigned variables, no break/continue)
let mut assigned_vars: Vec<String> = Vec::new();
let mut has_ctrl = false;
@ -147,6 +149,9 @@ impl<'a> LoopBuilder<'a> {
// 3. Headerブロックの準備unsealed状態
self.set_current_block(header_id)?;
// Debug region: loop header
self.parent_builder
.debug_push_region(format!("loop#{}", loop_id) + "/header");
// Hint: loop header (no-op sink)
self.parent_builder.hint_loop_header();
let _ = self.mark_block_unsealed(header_id);
@ -191,6 +196,9 @@ impl<'a> LoopBuilder<'a> {
// 7. ループボディの構築
self.set_current_block(body_id)?;
// Debug region: loop body
self.parent_builder
.debug_replace_region(format!("loop#{}", loop_id) + "/body");
// Materialize pinned slots at entry via single-pred Phi
let names: Vec<String> = self.parent_builder.variable_map.keys().cloned().collect();
for name in names {
@ -221,6 +229,9 @@ impl<'a> LoopBuilder<'a> {
let latch_id = self.current_block()?;
// Hint: loop latch (no-op sink)
self.parent_builder.hint_loop_latch();
// Debug region: loop latch (end of body)
self.parent_builder
.debug_replace_region(format!("loop#{}", loop_id) + "/latch");
// Scope leave for loop body
self.parent_builder.hint_scope_leave(0);
let latch_snapshot = self.get_current_variable_map();
@ -265,12 +276,17 @@ impl<'a> LoopBuilder<'a> {
// 10. ループ後の処理 - Exit PHI生成
self.set_current_block(after_loop_id)?;
// Debug region: loop exit
self.parent_builder
.debug_replace_region(format!("loop#{}", loop_id) + "/exit");
// Exit PHIの生成 - break時点での変数値を統一
self.create_exit_phis(header_id, after_loop_id)?;
// Pop loop context
crate::mir::builder::loops::pop_loop_context(self.parent_builder);
// Pop debug region scope
self.parent_builder.debug_pop_region();
// void値を返す
let void_dst = self.new_value();
@ -500,6 +516,8 @@ impl<'a> LoopBuilder<'a> {
then_body: Vec<ASTNode>,
else_body: Option<Vec<ASTNode>>,
) -> Result<ValueId, String> {
// Reserve a deterministic join id for debug region labeling (nested inside loop)
let join_id = self.parent_builder.debug_next_join_id();
// Pre-pin comparison operands to slots so repeated uses across blocks are safe
if crate::config::env::mir_pre_pin_compare_operands() {
if let ASTNode::BinaryOp { operator, left, right, .. } = &condition {
@ -532,6 +550,9 @@ impl<'a> LoopBuilder<'a> {
// then branch
self.set_current_block(then_bb)?;
// Debug region: join then-branch (inside loop)
self.parent_builder
.debug_push_region(format!("join#{}", join_id) + "/then");
// Materialize all variables at entry via single-pred Phi (correctness-first)
let names_then: Vec<String> = self
.parent_builder
@ -567,9 +588,14 @@ impl<'a> LoopBuilder<'a> {
self.parent_builder,
merge_bb
)?;
// Pop then-branch debug region
self.parent_builder.debug_pop_region();
// else branch
self.set_current_block(else_bb)?;
// Debug region: join else-branch (inside loop)
self.parent_builder
.debug_push_region(format!("join#{}", join_id) + "/else");
// Materialize all variables at entry via single-pred Phi (correctness-first)
let names2: Vec<String> = self
.parent_builder
@ -608,9 +634,14 @@ impl<'a> LoopBuilder<'a> {
self.parent_builder,
merge_bb
)?;
// Pop else-branch debug region
self.parent_builder.debug_pop_region();
// Continue at merge
self.set_current_block(merge_bb)?;
// Debug region: join merge (inside loop)
self.parent_builder
.debug_push_region(format!("join#{}", join_id) + "/join");
let mut vars: std::collections::HashSet<String> = std::collections::HashSet::new();
let then_prog = ASTNode::Program { statements: then_body.clone(), span: crate::ast::Span::unknown() };
@ -656,6 +687,8 @@ impl<'a> LoopBuilder<'a> {
)?;
let void_id = self.new_value();
self.emit_const(void_id, ConstValue::Void)?;
// Pop merge debug region
self.parent_builder.debug_pop_region();
Ok(void_id)
}
}

View File

@ -79,11 +79,24 @@ pub fn format_instruction(
super::Callee::Global(name) => {
format!("call_global {}({})", name, args_str)
}
super::Callee::Method { box_name, method, receiver } => {
super::Callee::Method { box_name, method, receiver, certainty } => {
if let Some(recv) = receiver {
format!("call_method {}.{}({}) [recv: {}]", box_name, method, args_str, recv)
format!(
"call_method {}.{}({}) [recv: {}] [{}]",
box_name,
method,
args_str,
recv,
match certainty { crate::mir::definitions::call_unified::TypeCertainty::Known => "Known", crate::mir::definitions::call_unified::TypeCertainty::Union => "Union" }
)
} else {
format!("call_method {}.{}({})", box_name, method, args_str)
format!(
"call_method {}.{}({}) [{}]",
box_name,
method,
args_str,
match certainty { crate::mir::definitions::call_unified::TypeCertainty::Known => "Known", crate::mir::definitions::call_unified::TypeCertainty::Union => "Union" }
)
}
}
super::Callee::Constructor { box_type } => {

View File

@ -54,12 +54,13 @@ fn emit_unified_mir_call(
"name": name
});
}
Callee::Method { box_name, method, receiver } => {
Callee::Method { box_name, method, receiver, certainty } => {
call_obj["mir_call"]["callee"] = json!({
"type": "Method",
"box_name": box_name,
"method": method,
"receiver": receiver.map(|v| v.as_u32())
"receiver": receiver.map(|v| v.as_u32()),
"certainty": match certainty { crate::mir::definitions::call_unified::TypeCertainty::Known => "Known", crate::mir::definitions::call_unified::TypeCertainty::Union => "Union" }
});
}
Callee::Constructor { box_type } => {

View File

@ -76,32 +76,10 @@ impl NyashRunner {
eprintln!("❌ using: AST prelude merge is disabled in this profile. Enable NYASH_USING_AST=1 or remove 'using' lines.");
std::process::exit(1);
}
if use_ast {
for prelude_path in paths {
match std::fs::read_to_string(&prelude_path) {
Ok(src) => {
match crate::runner::modes::common_util::resolve::collect_using_and_strip(self, &src, &prelude_path) {
Ok((clean_src, nested)) => {
// Nested entries have already been expanded by DFS; ignore `nested` here.
match NyashParser::parse_from_string(&clean_src) {
Ok(ast) => prelude_asts.push(ast),
Err(e) => {
eprintln!("❌ Parse error in using prelude {}: {}", prelude_path, e);
std::process::exit(1);
}
}
}
Err(e) => {
eprintln!("{}", e);
std::process::exit(1);
}
}
}
Err(e) => {
eprintln!("❌ Error reading using prelude {}: {}", prelude_path, e);
std::process::exit(1);
}
}
if use_ast && !paths.is_empty() {
match crate::runner::modes::common_util::resolve::parse_preludes_to_asts(self, &paths) {
Ok(v) => prelude_asts = v,
Err(e) => { eprintln!("{}", e); std::process::exit(1); }
}
}
}
@ -138,23 +116,8 @@ impl NyashRunner {
};
// When using AST prelude mode, combine prelude ASTs + main AST into one Program
let ast = if use_ast && !prelude_asts.is_empty() {
use nyash_rust::ast::ASTNode;
let mut combined: Vec<ASTNode> = Vec::new();
for a in prelude_asts {
if let ASTNode::Program { statements, .. } = a {
combined.extend(statements);
}
}
if let ASTNode::Program { statements, .. } = main_ast.clone() {
combined.extend(statements);
}
ASTNode::Program {
statements: combined,
span: nyash_rust::ast::Span::unknown(),
}
} else {
main_ast
};
crate::runner::modes::common_util::resolve::merge_prelude_asts_with_main(prelude_asts, &main_ast)
} else { main_ast };
// Optional: dump AST statement kinds for quick diagnostics
if std::env::var("NYASH_AST_DUMP").ok().as_deref() == Some("1") {

View File

@ -1,11 +1,29 @@
/*!
* Using resolver utilities (split)
* - strip: remove `using` lines, inline modules, register aliases/modules
* - seam: seam logging and optional brace-fix at join points
* Using resolver utilities — static resolution line (SSOT + AST)
*
* Separation of concerns:
* - Static (using-time): Resolve packages/aliases from nyash.toml (SSOT),
* strip `using` lines, collect prelude file paths, and (when enabled)
* parse/merge them as AST before macro expansion.
* - Dynamic (runtime): Plugin/extern dispatch only. User instance BoxCall
* fallback is disallowed in prod; builder must rewrite obj.method() to
* a function call.
*
* Modules:
* - strip: profile-aware resolution (`collect_using_and_strip`,
* `resolve_prelude_paths_profiled`) — single entrypoints used by all
* runner modes to avoid drift.
* - seam: seam logging and optional boundary markers (for diagnostics).
*/
pub mod strip;
pub mod seam;
// Public re-exports to preserve existing call sites
pub use strip::{preexpand_at_local, collect_using_and_strip, resolve_prelude_paths_profiled};
pub use strip::{
preexpand_at_local,
collect_using_and_strip,
resolve_prelude_paths_profiled,
parse_preludes_to_asts,
merge_prelude_asts_with_main,
};

View File

@ -1,8 +1,14 @@
use crate::runner::NyashRunner;
/// Collect using targets and strip using lines, without inlining.
/// Returns (cleaned_source, prelude_paths) where prelude_paths are resolved file paths
/// to be parsed separately and AST-merged (when NYASH_USING_AST=1).
/// Collect using targets and strip using lines (no inlining).
/// Returns (cleaned_source, prelude_paths) where `prelude_paths` are resolved
/// file paths to be parsed separately and AST-merged (when `NYASH_USING_AST=1`).
///
/// Notes
/// - This function enforces profile policies (prod: disallow file-using; only
/// packages/aliases from nyash.toml are accepted).
/// - SSOT: Resolution sources and aliases come exclusively from nyash.toml.
/// - All runner modes use this static path to avoid logic drift.
pub fn collect_using_and_strip(
runner: &NyashRunner,
code: &str,
@ -322,9 +328,11 @@ pub fn collect_using_and_strip(
Ok((out, prelude_paths))
}
/// Profile-aware prelude resolution wrapper.
/// Currently delegates to `collect_using_and_strip`, but provides a single
/// entry point for callers (common and vm_fallback) to avoid logic drift.
/// Profile-aware prelude resolution wrapper (single entrypoint).
/// - Delegates to `collect_using_and_strip` for the first pass.
/// - When AST using is enabled, resolves nested preludes via DFS and injects
/// OperatorBox preludes when available (stringify/compare/add).
/// - All runners call this helper; do not fork resolution logic elsewhere.
pub fn resolve_prelude_paths_profiled(
runner: &NyashRunner,
code: &str,
@ -427,6 +435,54 @@ pub fn resolve_prelude_paths_profiled(
Ok((cleaned, out))
}
/// Parse prelude source files into ASTs (single helper for all runner modes).
/// - Reads each path, strips nested `using`, and parses to AST.
/// - Returns a Vec of Program ASTs (one per prelude file), preserving DFS order.
pub fn parse_preludes_to_asts(
runner: &NyashRunner,
prelude_paths: &[String],
) -> Result<Vec<nyash_rust::ast::ASTNode>, String> {
let mut out: Vec<nyash_rust::ast::ASTNode> = Vec::with_capacity(prelude_paths.len());
for prelude_path in prelude_paths {
let src = std::fs::read_to_string(prelude_path)
.map_err(|e| format!("using: error reading {}: {}", prelude_path, e))?;
let (clean_src, _nested) = collect_using_and_strip(runner, &src, prelude_path)?;
match crate::parser::NyashParser::parse_from_string(&clean_src) {
Ok(ast) => out.push(ast),
Err(e) => return Err(format!(
"Parse error in using prelude {}: {}",
prelude_path, e
)),
}
}
Ok(out)
}
/// Merge prelude ASTs with the main AST into a single Program node.
/// - Collects statements from each prelude Program in order, then appends
/// statements from the main Program.
/// - If the main AST is not a Program, returns it unchanged (defensive).
pub fn merge_prelude_asts_with_main(
prelude_asts: Vec<nyash_rust::ast::ASTNode>,
main_ast: &nyash_rust::ast::ASTNode,
) -> nyash_rust::ast::ASTNode {
use nyash_rust::ast::{ASTNode, Span};
let mut combined: Vec<ASTNode> = Vec::new();
for a in prelude_asts.into_iter() {
if let ASTNode::Program { statements, .. } = a {
combined.extend(statements);
}
}
if let ASTNode::Program { statements, .. } = main_ast.clone() {
let mut all = combined;
all.extend(statements);
ASTNode::Program { statements: all, span: Span::unknown() }
} else {
// Defensive: unexpected shape; preserve main AST unchanged.
main_ast.clone()
}
}
/// Pre-expand line-head `@name[: Type] = expr` into `local name[: Type] = expr`.
/// Minimal, safe, no semantics change. Applies only at line head (after spaces/tabs).
pub fn preexpand_at_local(src: &str) -> String {

View File

@ -37,31 +37,10 @@ impl NyashRunner {
eprintln!("❌ using: AST prelude merge is disabled in this profile. Enable NYASH_USING_AST=1 or remove 'using' lines.");
std::process::exit(1);
}
if use_ast {
for prelude_path in paths {
match std::fs::read_to_string(&prelude_path) {
Ok(src) => {
match crate::runner::modes::common_util::resolve::collect_using_and_strip(self, &src, &prelude_path) {
Ok((clean_src, _nested)) => {
match NyashParser::parse_from_string(&clean_src) {
Ok(ast) => prelude_asts.push(ast),
Err(e) => {
eprintln!("❌ Parse error in using prelude {}: {}", prelude_path, e);
std::process::exit(1);
}
}
}
Err(e) => {
eprintln!("{}", e);
std::process::exit(1);
}
}
}
Err(e) => {
eprintln!("❌ Error reading using prelude {}: {}", prelude_path, e);
std::process::exit(1);
}
}
if use_ast && !paths.is_empty() {
match crate::runner::modes::common_util::resolve::parse_preludes_to_asts(self, &paths) {
Ok(v) => prelude_asts = v,
Err(e) => { eprintln!("{}", e); std::process::exit(1); }
}
}
}
@ -85,20 +64,8 @@ impl NyashRunner {
};
// Merge preludes + main when enabled
let ast = if use_ast && !prelude_asts.is_empty() {
use nyash_rust::ast::ASTNode;
let mut combined: Vec<ASTNode> = Vec::new();
for a in prelude_asts {
if let ASTNode::Program { statements, .. } = a {
combined.extend(statements);
}
}
if let ASTNode::Program { statements, .. } = main_ast.clone() {
combined.extend(statements);
}
ASTNode::Program { statements: combined, span: nyash_rust::ast::Span::unknown() }
} else {
main_ast
};
crate::runner::modes::common_util::resolve::merge_prelude_asts_with_main(prelude_asts, &main_ast)
} else { main_ast };
// Macro expansion (env-gated) after merge
let ast = crate::r#macro::maybe_expand_and_dump(&ast, false);
let ast = crate::runner::modes::macro_child::normalize_core_pass(&ast);

View File

@ -38,30 +38,10 @@ impl NyashRunner {
eprintln!("❌ using: AST prelude merge is disabled in this profile. Enable NYASH_USING_AST=1 or remove 'using' lines.");
process::exit(1);
}
for prelude_path in paths {
match std::fs::read_to_string(&prelude_path) {
Ok(src) => {
match crate::runner::modes::common_util::resolve::collect_using_and_strip(self, &src, &prelude_path) {
Ok((clean_src, nested)) => {
// Nested entries have already been expanded by DFS; ignore `nested` here.
match NyashParser::parse_from_string(&clean_src) {
Ok(ast) => prelude_asts.push(ast),
Err(e) => {
eprintln!("❌ Parse error in using prelude {}: {}", prelude_path, e);
process::exit(1);
}
}
}
Err(e) => {
eprintln!("{}", e);
process::exit(1);
}
}
}
Err(e) => {
eprintln!("❌ Error reading using prelude {}: {}", prelude_path, e);
process::exit(1);
}
if use_ast_prelude && !paths.is_empty() {
match crate::runner::modes::common_util::resolve::parse_preludes_to_asts(self, &paths) {
Ok(v) => prelude_asts = v,
Err(e) => { eprintln!("{}", e); process::exit(1); }
}
}
}
@ -84,23 +64,8 @@ impl NyashRunner {
};
// When using AST prelude mode, combine prelude ASTs + main AST into one Program before macro expansion
let ast_combined = if use_ast_prelude && !prelude_asts.is_empty() {
use nyash_rust::ast::ASTNode;
let mut combined: Vec<ASTNode> = Vec::new();
for a in prelude_asts {
if let ASTNode::Program { statements, .. } = a {
combined.extend(statements);
}
}
if let ASTNode::Program { statements, .. } = main_ast.clone() {
combined.extend(statements);
}
ASTNode::Program {
statements: combined,
span: nyash_rust::ast::Span::unknown(),
}
} else {
main_ast
};
crate::runner::modes::common_util::resolve::merge_prelude_asts_with_main(prelude_asts, &main_ast)
} else { main_ast };
// Optional: dump AST statement kinds for quick diagnostics
if std::env::var("NYASH_AST_DUMP").ok().as_deref() == Some("1") {
use nyash_rust::ast::ASTNode;