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:
@ -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 } => {
|
||||
|
||||
Reference in New Issue
Block a user