mir/vm: SSA pin+PHI + short-circuit; user-defined method calls → functions; entry single-pred PHIs; compare-operand pin; VM BoxCall fallback to InstanceBox methods; docs: update CURRENT_TASK (plan + acceptance)
- Lower And/Or to branch+PHI (RHS not evaluated) - Always slotify compare operands (dominance safety) - Insert single-predecessor PHIs at then/else/short-circuit entries - pin_to_slot now logs (NYASH_PIN_TRACE) and participates in PHI - Rewrite user-defined instance method calls to Box.method/Arity (builder) - VM fallback: BoxCall on InstanceBox dispatches to lowered functions with 'me'+args - Keep plugin/BoxCall path for core boxes (String/Array/Map) - Add env-gated pre-pin for if/loop (NYASH_MIR_PREPIN) - CURRENT_TASK: add SSA/userbox plan, debug steps, acceptance criteria
This commit is contained in:
@ -57,8 +57,44 @@ impl super::MirBuilder {
|
||||
args: Vec<ValueId>,
|
||||
) -> Result<(), String> {
|
||||
match target {
|
||||
CallTarget::Method { receiver, method, .. } => {
|
||||
// Use existing emit_box_or_plugin_call
|
||||
CallTarget::Method { receiver, method, box_type } => {
|
||||
// If receiver is a user-defined box, lower to function call: "Box.method/(1+arity)" with receiver as first arg
|
||||
let mut is_user_box = false;
|
||||
let mut class_name_opt: Option<String> = None;
|
||||
if let Some(bt) = box_type.clone() { class_name_opt = Some(bt); }
|
||||
if class_name_opt.is_none() {
|
||||
if let Some(cn) = self.value_origin_newbox.get(&receiver) { class_name_opt = Some(cn.clone()); }
|
||||
}
|
||||
if class_name_opt.is_none() {
|
||||
if let Some(t) = self.value_types.get(&receiver) {
|
||||
if let super::MirType::Box(bn) = t { class_name_opt = Some(bn.clone()); }
|
||||
}
|
||||
}
|
||||
if let Some(cls) = class_name_opt.clone() {
|
||||
// Prefer explicit registry of user-defined boxes when available
|
||||
if self.user_defined_boxes.contains(&cls) { is_user_box = true; }
|
||||
}
|
||||
if is_user_box {
|
||||
let cls = class_name_opt.unwrap();
|
||||
let arity = args.len(); // function name arity excludes 'me'
|
||||
let fname = super::calls::function_lowering::generate_method_function_name(&cls, &method, arity);
|
||||
let name_const = self.value_gen.next();
|
||||
self.emit_instruction(MirInstruction::Const {
|
||||
dst: name_const,
|
||||
value: super::ConstValue::String(fname),
|
||||
})?;
|
||||
let mut call_args = Vec::with_capacity(arity);
|
||||
call_args.push(receiver); // pass 'me' first
|
||||
call_args.extend(args.into_iter());
|
||||
return self.emit_instruction(MirInstruction::Call {
|
||||
dst,
|
||||
func: name_const,
|
||||
callee: None,
|
||||
args: call_args,
|
||||
effects: EffectMask::READ.add(Effect::ReadHeap),
|
||||
});
|
||||
}
|
||||
// Else fall back to plugin/boxcall path (StringBox/ArrayBox/MapBox etc.)
|
||||
self.emit_box_or_plugin_call(dst, receiver, method, None, args, EffectMask::IO)
|
||||
},
|
||||
CallTarget::Constructor(box_type) => {
|
||||
|
||||
@ -14,6 +14,7 @@ impl MirBuilder {
|
||||
// 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.
|
||||
if crate::config::env::mir_pre_pin_compare_operands() {
|
||||
if let ASTNode::BinaryOp { operator, left, right, .. } = &condition {
|
||||
match operator {
|
||||
BinaryOperator::Equal
|
||||
@ -32,6 +33,7 @@ impl MirBuilder {
|
||||
_ => {}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let condition_val = self.build_expression(condition)?;
|
||||
|
||||
@ -41,6 +43,7 @@ impl MirBuilder {
|
||||
let merge_block = self.block_gen.next();
|
||||
|
||||
// Branch
|
||||
let pre_branch_bb = self.current_block()?;
|
||||
self.emit_instruction(MirInstruction::Branch {
|
||||
condition: condition_val,
|
||||
then_bb: then_block,
|
||||
@ -51,12 +54,18 @@ impl MirBuilder {
|
||||
let pre_if_var_map = self.variable_map.clone();
|
||||
|
||||
// then
|
||||
self.current_block = Some(then_block);
|
||||
self.ensure_block_exists(then_block)?;
|
||||
self.start_new_block(then_block)?;
|
||||
// Scope enter for then-branch
|
||||
self.hint_scope_enter(0);
|
||||
let then_ast_for_analysis = then_branch.clone();
|
||||
self.variable_map = pre_if_var_map.clone();
|
||||
// Materialize all variables at block entry via single-pred Phi (correctness-first)
|
||||
for (name, &pre_v) in pre_if_var_map.iter() {
|
||||
let phi_val = self.value_gen.next();
|
||||
let inputs = vec![(pre_branch_bb, pre_v)];
|
||||
self.emit_instruction(MirInstruction::Phi { dst: phi_val, inputs })?;
|
||||
self.variable_map.insert(name.clone(), phi_val);
|
||||
}
|
||||
let then_value_raw = self.build_expression(then_branch)?;
|
||||
let then_exit_block = self.current_block()?;
|
||||
let then_var_map_end = self.variable_map.clone();
|
||||
@ -67,10 +76,16 @@ impl MirBuilder {
|
||||
}
|
||||
|
||||
// else
|
||||
self.current_block = Some(else_block);
|
||||
self.ensure_block_exists(else_block)?;
|
||||
self.start_new_block(else_block)?;
|
||||
// Scope enter for else-branch
|
||||
self.hint_scope_enter(0);
|
||||
// Materialize all variables at block entry via single-pred Phi (correctness-first)
|
||||
for (name, &pre_v) in pre_if_var_map.iter() {
|
||||
let phi_val = self.value_gen.next();
|
||||
let inputs = vec![(pre_branch_bb, pre_v)];
|
||||
self.emit_instruction(MirInstruction::Phi { dst: phi_val, inputs })?;
|
||||
self.variable_map.insert(name.clone(), phi_val);
|
||||
}
|
||||
let (else_value_raw, else_ast_for_analysis, else_var_map_end_opt) = if let Some(else_ast) = else_branch {
|
||||
self.variable_map = pre_if_var_map.clone();
|
||||
let val = self.build_expression(else_ast.clone())?;
|
||||
@ -88,8 +103,9 @@ impl MirBuilder {
|
||||
}
|
||||
|
||||
// merge: primary result via helper, then delta-based variable merges
|
||||
self.current_block = Some(merge_block);
|
||||
self.ensure_block_exists(merge_block)?;
|
||||
// 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)?;
|
||||
self.push_if_merge(merge_block);
|
||||
|
||||
// Pre-analysis: identify then/else assigned var for skip and hints
|
||||
|
||||
@ -97,6 +97,84 @@ impl MirBuilder {
|
||||
arg_values.push(self.build_expression(arg.clone())?);
|
||||
}
|
||||
|
||||
// If receiver is a user-defined box, lower to function call: "Box.method/(1+arity)"
|
||||
let mut class_name_opt: Option<String> = None;
|
||||
if let Some(cn) = self.value_origin_newbox.get(&object_value) { class_name_opt = Some(cn.clone()); }
|
||||
if class_name_opt.is_none() {
|
||||
if let Some(t) = self.value_types.get(&object_value) {
|
||||
if let MirType::Box(bn) = t { class_name_opt = Some(bn.clone()); }
|
||||
}
|
||||
}
|
||||
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);
|
||||
// Only use userbox path if such a function actually exists in the module
|
||||
let has_fn = if let Some(ref module) = self.current_module {
|
||||
module.functions.contains_key(&fname)
|
||||
} else { false };
|
||||
if has_fn {
|
||||
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));
|
||||
}
|
||||
let name_const = self.value_gen.next();
|
||||
self.emit_instruction(MirInstruction::Const {
|
||||
dst: name_const,
|
||||
value: crate::mir::builder::ConstValue::String(fname),
|
||||
})?;
|
||||
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),
|
||||
})?;
|
||||
return Ok(dst);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback: if exactly one user-defined method matches by name/arity across module, resolve to that
|
||||
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),
|
||||
})?;
|
||||
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 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),
|
||||
})?;
|
||||
return Ok(dst);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Else fall back to plugin/boxcall path
|
||||
let result_id = self.value_gen.next();
|
||||
self.emit_box_or_plugin_call(
|
||||
Some(result_id),
|
||||
@ -109,4 +187,4 @@ impl MirBuilder {
|
||||
|
||||
Ok(result_id)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@ -87,9 +87,10 @@ impl super::MirBuilder {
|
||||
} else {
|
||||
(lhs, rhs)
|
||||
};
|
||||
// Materialize operands in the current block to avoid dominance/undef issues
|
||||
let lhs2 = lhs2_raw;
|
||||
let rhs2 = rhs2_raw;
|
||||
// Ensure operands are safe across blocks: pin ephemeral values into slots
|
||||
// This guarantees they participate in PHI merges and have block-local defs.
|
||||
let lhs2 = self.ensure_slotified_for_use(lhs2_raw, "@cmp_lhs")?;
|
||||
let rhs2 = self.ensure_slotified_for_use(rhs2_raw, "@cmp_rhs")?;
|
||||
self.emit_instruction(MirInstruction::Compare {
|
||||
dst,
|
||||
op,
|
||||
@ -128,16 +129,24 @@ impl super::MirBuilder {
|
||||
then_bb: then_block,
|
||||
else_bb: else_block,
|
||||
})?;
|
||||
// Record predecessor block for branch (for single-pred PHI materialization)
|
||||
let pre_branch_bb = self.current_block()?;
|
||||
|
||||
// Snapshot variables before entering branches
|
||||
let pre_if_var_map = self.variable_map.clone();
|
||||
|
||||
// ---- THEN branch ----
|
||||
self.current_block = Some(then_block);
|
||||
self.ensure_block_exists(then_block)?;
|
||||
self.start_new_block(then_block)?;
|
||||
self.hint_scope_enter(0);
|
||||
// Reset scope to pre-if snapshot for clean deltas
|
||||
self.variable_map = pre_if_var_map.clone();
|
||||
// Materialize all variables at entry via single-pred PHI (correctness-first)
|
||||
for (name, &pre_v) in pre_if_var_map.iter() {
|
||||
let phi_val = self.value_gen.next();
|
||||
let inputs = vec![(pre_branch_bb, pre_v)];
|
||||
self.emit_instruction(MirInstruction::Phi { dst: phi_val, inputs })?;
|
||||
self.variable_map.insert(name.clone(), phi_val);
|
||||
}
|
||||
|
||||
// AND: then → evaluate RHS and reduce to bool
|
||||
// OR: then → constant true
|
||||
@ -187,10 +196,16 @@ impl super::MirBuilder {
|
||||
}
|
||||
|
||||
// ---- ELSE branch ----
|
||||
self.current_block = Some(else_block);
|
||||
self.ensure_block_exists(else_block)?;
|
||||
self.start_new_block(else_block)?;
|
||||
self.hint_scope_enter(0);
|
||||
self.variable_map = pre_if_var_map.clone();
|
||||
// Materialize all variables at entry via single-pred PHI (correctness-first)
|
||||
for (name, &pre_v) in pre_if_var_map.iter() {
|
||||
let phi_val = self.value_gen.next();
|
||||
let inputs = vec![(pre_branch_bb, pre_v)];
|
||||
self.emit_instruction(MirInstruction::Phi { dst: phi_val, inputs })?;
|
||||
self.variable_map.insert(name.clone(), phi_val);
|
||||
}
|
||||
// AND: else → false
|
||||
// OR: else → evaluate RHS and reduce to bool
|
||||
let else_value_raw = if is_and {
|
||||
@ -238,8 +253,9 @@ impl super::MirBuilder {
|
||||
}
|
||||
|
||||
// ---- MERGE ----
|
||||
self.current_block = Some(merge_block);
|
||||
self.ensure_block_exists(merge_block)?;
|
||||
// Merge block: suppress entry pin copy so PHIs remain first and materialize pins explicitly
|
||||
self.suppress_next_entry_pin_copy();
|
||||
self.start_new_block(merge_block)?;
|
||||
self.push_if_merge(merge_block);
|
||||
|
||||
// Result PHI (bool)
|
||||
|
||||
@ -25,6 +25,8 @@ impl MirBuilder {
|
||||
then_map_end,
|
||||
else_map_end_opt,
|
||||
);
|
||||
use std::collections::HashSet;
|
||||
let changed_set: HashSet<String> = changed.iter().cloned().collect();
|
||||
for name in changed {
|
||||
if skip_var.map(|s| s == name).unwrap_or(false) {
|
||||
continue;
|
||||
@ -50,6 +52,29 @@ impl MirBuilder {
|
||||
self.emit_instruction(MirInstruction::Phi { dst: merged, inputs })?;
|
||||
self.variable_map.insert(name, merged);
|
||||
}
|
||||
|
||||
// Ensure pinned synthetic slots ("__pin$...") have a block-local definition at the merge,
|
||||
// even if their values did not change across branches. This avoids undefined uses when
|
||||
// subsequent blocks re-use pinned values without modifications.
|
||||
for (pin_name, pre_val) in pre_if_snapshot.iter() {
|
||||
if !pin_name.starts_with("__pin$") { continue; }
|
||||
if skip_var.map(|s| s == pin_name.as_str()).unwrap_or(false) { continue; }
|
||||
if changed_set.contains(pin_name) { continue; }
|
||||
let then_v = then_map_end.get(pin_name.as_str()).copied().unwrap_or(*pre_val);
|
||||
let else_v = else_map_end_opt
|
||||
.as_ref()
|
||||
.and_then(|m| m.get(pin_name.as_str()).copied())
|
||||
.unwrap_or(*pre_val);
|
||||
let then_pred = then_exit_block;
|
||||
let else_pred = else_exit_block_opt.unwrap_or(else_block);
|
||||
let merged = self.value_gen.next();
|
||||
let inputs = vec![(then_pred, then_v), (else_pred, else_v)];
|
||||
if let (Some(func), Some(cur_bb)) = (&self.current_function, self.current_block) {
|
||||
crate::mir::phi_core::common::debug_verify_phi_inputs(func, cur_bb, &inputs);
|
||||
}
|
||||
self.emit_instruction(MirInstruction::Phi { dst: merged, inputs })?;
|
||||
self.variable_map.insert(pin_name.clone(), merged);
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
/// Normalize Phi creation for if/else constructs.
|
||||
|
||||
@ -32,17 +32,21 @@ impl super::MirBuilder {
|
||||
if let Some(ref mut function) = self.current_function {
|
||||
function.add_block(BasicBlock::new(block_id));
|
||||
self.current_block = Some(block_id);
|
||||
// Entry materialization for pinned slots only: ensure a local def exists in this block
|
||||
// This avoids dominance/undef issues when pinned values are referenced across blocks.
|
||||
let names: Vec<String> = self.variable_map.keys().cloned().collect();
|
||||
for name in names {
|
||||
if !name.starts_with("__pin$") { continue; }
|
||||
if let Some(&src) = self.variable_map.get(&name) {
|
||||
let dst = self.value_gen.next();
|
||||
self.emit_instruction(super::MirInstruction::Copy { dst, src })?;
|
||||
self.variable_map.insert(name.clone(), dst);
|
||||
// Entry materialization for pinned slots only when not suppressed.
|
||||
// This provides block-local defs in single-predecessor flows without touching user vars.
|
||||
if !self.suppress_pin_entry_copy_next {
|
||||
let names: Vec<String> = self.variable_map.keys().cloned().collect();
|
||||
for name in names {
|
||||
if !name.starts_with("__pin$") { continue; }
|
||||
if let Some(&src) = self.variable_map.get(&name) {
|
||||
let dst = self.value_gen.next();
|
||||
self.emit_instruction(super::MirInstruction::Copy { dst, src })?;
|
||||
self.variable_map.insert(name.clone(), dst);
|
||||
}
|
||||
}
|
||||
}
|
||||
// Reset suppression flag after use (one-shot)
|
||||
self.suppress_pin_entry_copy_next = false;
|
||||
Ok(())
|
||||
} else {
|
||||
Err("No current function".to_string())
|
||||
@ -204,12 +208,16 @@ impl super::MirBuilder {
|
||||
})
|
||||
}
|
||||
|
||||
/// Pin a block-crossing ephemeral value into a pseudo local slot so it participates in PHI merges.
|
||||
/// Pin a block-crossing ephemeral value into a pseudo local slot and register it in variable_map
|
||||
/// so it participates in PHI merges across branches/blocks. Safe default for correctness-first.
|
||||
pub(crate) fn pin_to_slot(&mut self, v: super::ValueId, hint: &str) -> Result<super::ValueId, String> {
|
||||
self.temp_slot_counter = self.temp_slot_counter.wrapping_add(1);
|
||||
let slot_name = format!("__pin${}${}", self.temp_slot_counter, hint);
|
||||
let dst = self.value_gen.next();
|
||||
self.emit_instruction(super::MirInstruction::Copy { dst, src: v })?;
|
||||
if super::utils::builder_debug_enabled() || std::env::var("NYASH_PIN_TRACE").ok().as_deref() == Some("1") {
|
||||
super::utils::builder_debug_log(&format!("pin slot={} src={} dst={}", slot_name, v.0, dst.0));
|
||||
}
|
||||
self.variable_map.insert(slot_name, dst);
|
||||
Ok(dst)
|
||||
}
|
||||
@ -220,4 +228,10 @@ impl super::MirBuilder {
|
||||
self.emit_instruction(super::MirInstruction::Copy { dst, src: v })?;
|
||||
Ok(dst)
|
||||
}
|
||||
|
||||
/// Ensure a value is safe to use in the current block by slotifying (pinning) it.
|
||||
/// Currently correctness-first: always pin to get a block-local def and PHI participation.
|
||||
pub(crate) fn ensure_slotified_for_use(&mut self, v: super::ValueId, hint: &str) -> Result<super::ValueId, String> {
|
||||
self.pin_to_slot(v, hint)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user