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

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

using: confirm unified prelude resolver entry for all runner modes

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

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

View File

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

View File

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

View File

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

View File

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

View File

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