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