Files
hakorune/src/mir/builder/stmts.rs
tomoaki 4ef2261e97 fix(joinir): Phase 269 P1.2 - static call normalization for this.method()
Problem:
- `this.method()` calls in static box loops were using string constant "StringUtils" as receiver
- This violated Box Theory (static box this should not be runtime object)
- Pattern8 was trying to pass receiver to JoinIR, causing SSA/PHI issues

Solution (3-part fix):
1. **MeResolverBox Fail-Fast** (stmts.rs): Remove string fallback, error message cleanup
2. **ReceiverNormalizeBox** (calls/build.rs): Normalize `this/me.method()` → static call at compile-time
3. **Pattern8 Skip Static Box** (pattern8_scan_bool_predicate.rs): Reject Pattern8 for static box contexts

Key changes:
- **stmts.rs**: Update error message - remove MeBindingInitializerBox mentions
- **calls/build.rs**: Add This/Me receiver check, normalize to static call if current_static_box exists
- **calls/lowering.rs**: Remove NewBox-based "me" initialization (2 locations - fixes Stage1Cli regression)
- **pattern8_scan_bool_predicate.rs**: Skip Pattern8 for static boxes (use Pattern1 fallback instead)

Result:
-  phase269_p1_2_this_method_in_loop_vm.sh PASS (exit=7)
-  No regression: phase259_p0_is_integer_vm PASS
-  No regression: phase269_p0_pattern8_frag_vm PASS
-  Stage1Cli unit tests PASS
-  MIR uses CallTarget::Global, no const "StringUtils" as receiver

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
2025-12-22 03:33:30 +09:00

610 lines
26 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

use super::{Effect, EffectMask, MirInstruction, MirType, ValueId};
use crate::ast::{ASTNode, CallExpr};
use crate::mir::utils::is_current_block_terminated;
use crate::mir::TypeOpKind;
impl super::MirBuilder {
// Print statement: env.console.log(value) with early TypeOp handling
pub(super) fn build_print_statement(&mut self, expression: ASTNode) -> Result<ValueId, String> {
super::utils::builder_debug_log("enter build_print_statement");
// Prefer wrapper for simple function-call pattern (non-breaking refactor)
if let Ok(call) = CallExpr::try_from(expression.clone()) {
if (call.name == "isType" || call.name == "asType") && call.arguments.len() == 2 {
super::utils::builder_debug_log(
"pattern: print(FunctionCall isType|asType) [via wrapper]",
);
if let Some(type_name) =
super::MirBuilder::extract_string_literal(&call.arguments[1])
{
super::utils::builder_debug_log(&format!(
"extract_string_literal OK: {}",
type_name
));
let val = self.build_expression(call.arguments[0].clone())?;
let ty = super::MirBuilder::parse_type_name_to_mir(&type_name);
let dst = self.next_value_id();
let op = if call.name == "isType" {
TypeOpKind::Check
} else {
TypeOpKind::Cast
};
super::utils::builder_debug_log(&format!(
"emit TypeOp {:?} value={} dst= {}",
op, val, dst
));
self.emit_instruction(MirInstruction::TypeOp {
dst,
op,
value: val,
ty,
})?;
self.emit_instruction(MirInstruction::ExternCall {
dst: None,
iface_name: "env.console".to_string(),
method_name: "log".to_string(),
args: vec![dst],
effects: EffectMask::PURE.add(Effect::Io),
})?;
return Ok(dst);
} else {
super::utils::builder_debug_log("extract_string_literal FAIL [via wrapper]");
}
}
}
match &expression {
// print(isType(val, "Type")) / print(asType(...))
ASTNode::FunctionCall {
name, arguments, ..
} if (name == "isType" || name == "asType") && arguments.len() == 2 => {
super::utils::builder_debug_log("pattern: print(FunctionCall isType|asType)");
if let Some(type_name) = super::MirBuilder::extract_string_literal(&arguments[1]) {
super::utils::builder_debug_log(&format!(
"extract_string_literal OK: {}",
type_name
));
let val = self.build_expression(arguments[0].clone())?;
let ty = super::MirBuilder::parse_type_name_to_mir(&type_name);
let dst = self.next_value_id();
let op = if name == "isType" {
TypeOpKind::Check
} else {
TypeOpKind::Cast
};
super::utils::builder_debug_log(&format!(
"emit TypeOp {:?} value={} dst= {}",
op, val, dst
));
self.emit_instruction(MirInstruction::TypeOp {
dst,
op,
value: val,
ty,
})?;
self.emit_instruction(MirInstruction::ExternCall {
dst: None,
iface_name: "env.console".to_string(),
method_name: "log".to_string(),
args: vec![dst],
effects: EffectMask::PURE.add(Effect::Io),
})?;
return Ok(dst);
} else {
super::utils::builder_debug_log("extract_string_literal FAIL");
}
}
// print(obj.is("Type")) / print(obj.as("Type"))
ASTNode::MethodCall {
object,
method,
arguments,
..
} if (method == "is" || method == "as") && arguments.len() == 1 => {
super::utils::builder_debug_log("pattern: print(MethodCall is|as)");
if let Some(type_name) = super::MirBuilder::extract_string_literal(&arguments[0]) {
super::utils::builder_debug_log(&format!(
"extract_string_literal OK: {}",
type_name
));
let obj_val = self.build_expression(*object.clone())?;
let ty = super::MirBuilder::parse_type_name_to_mir(&type_name);
let dst = self.next_value_id();
let op = if method == "is" {
TypeOpKind::Check
} else {
TypeOpKind::Cast
};
super::utils::builder_debug_log(&format!(
"emit TypeOp {:?} obj={} dst= {}",
op, obj_val, dst
));
self.emit_instruction(MirInstruction::TypeOp {
dst,
op,
value: obj_val,
ty,
})?;
self.emit_instruction(MirInstruction::ExternCall {
dst: None,
iface_name: "env.console".to_string(),
method_name: "log".to_string(),
args: vec![dst],
effects: EffectMask::PURE.add(Effect::Io),
})?;
return Ok(dst);
} else {
super::utils::builder_debug_log("extract_string_literal FAIL");
}
}
_ => {}
}
let value = self.build_expression(expression)?;
super::utils::builder_debug_log(&format!("fallback print value={}", value));
// Phase 3.2: Use unified call for print statements
let use_unified = super::calls::call_unified::is_unified_call_enabled();
if use_unified {
// New unified path - treat print as global function
self.emit_unified_call(
None, // print returns nothing
super::builder_calls::CallTarget::Global("print".to_string()),
vec![value],
)?;
} else {
// Legacy path - use ExternCall
self.emit_instruction(MirInstruction::ExternCall {
dst: None,
iface_name: "env.console".to_string(),
method_name: "log".to_string(),
args: vec![value],
effects: EffectMask::PURE.add(Effect::Io),
})?;
}
Ok(value)
}
// Block: sequentially build statements and return last value or Void
pub(super) fn build_block(&mut self, statements: Vec<ASTNode>) -> Result<ValueId, String> {
let trace = crate::mir::builder::control_flow::joinir::trace::trace();
// Scope hint for bare block (Program)
let scope_id = self.current_block.map(|bb| bb.as_u32()).unwrap_or(0);
self.hint_scope_enter(scope_id);
let _lex_scope = super::vars::lexical_scope::LexicalScopeGuard::new(self);
let mut last_value = None;
let total = statements.len();
trace.emit_if(
"debug",
"build_block",
&format!("Processing {} statements", total),
trace.is_enabled(),
);
// Phase 132 P0.5: Use while loop instead of for loop to support suffix skipping
let mut idx = 0;
while idx < statements.len() {
// Phase 132 P0.5: Try suffix router (dev-only)
if crate::config::env::joinir_dev_enabled() {
let remaining = &statements[idx..];
// Clone func_name to avoid borrow conflict
let func_name = self
.scope_ctx
.current_function
.as_ref()
.map(|f| f.signature.name.clone())
.unwrap_or_else(|| "unknown".to_string());
let debug = trace.is_enabled();
use crate::mir::builder::control_flow::joinir::patterns::policies::normalized_shadow_suffix_router_box::NormalizedShadowSuffixRouterBox;
// Phase 141 P1.5: Pass prefix variables for external env inputs
// Clone to avoid borrow checker conflict (self is borrowed mutably in try_lower_loop_suffix)
let prefix_var_map = self.variable_ctx.variable_map.clone();
match NormalizedShadowSuffixRouterBox::try_lower_loop_suffix(
self, remaining, &func_name, debug, Some(&prefix_var_map)
)? {
Some(consumed) => {
trace.emit_if(
"debug",
"build_block/suffix_router",
&format!("Phase 142 P0: Suffix router consumed {} statement(s), continuing to process subsequent statements", consumed),
debug,
);
// Phase 142 P0: Normalization unit is now "statement (loop 1個)"
// Loop normalization returns consumed=1, and subsequent statements
// (return, assignments, etc.) are handled by normal MIR lowering
idx += consumed;
// No break - continue processing subsequent statements
}
None => {
// No match, proceed with normal statement build
}
}
}
trace.emit_if(
"debug",
"build_block",
&format!(
"Statement {}/{} current_block={:?} current_function={}",
idx + 1,
total,
self.current_block,
self.scope_ctx
.current_function
.as_ref()
.map(|f| f.signature.name.as_str())
.unwrap_or("none")
),
trace.is_enabled(),
);
last_value = Some(self.build_statement(statements[idx].clone())?);
idx += 1;
// If the current block was terminated by this statement (e.g., return/throw),
// do not emit any further instructions for this block.
if is_current_block_terminated(self)? {
trace.emit_if(
"debug",
"build_block",
&format!("Block terminated after statement {}", idx),
trace.is_enabled(),
);
break;
}
}
let out = last_value.unwrap_or_else(|| {
// Use ConstantEmissionBox for Void
crate::mir::builder::emission::constant::emit_void(self)
});
// Scope leave only if block not already terminated
if !self.is_current_block_terminated() {
self.hint_scope_leave(scope_id);
}
trace.emit_if(
"debug",
"build_block",
&format!("Completed, returning value {:?}", out),
trace.is_enabled(),
);
Ok(out)
}
/// Build a single statement node.
///
/// Note:
/// - While/ForRange は将来 Loop lowering へ委譲する拡張ポイントとして扱い、
/// 現状は他の専用ビルダ/既存パスと同様に build_expression に委譲する。
///
/// Phase 212.5: If statement のサポート追加
/// - Statement としての If副作用のみが欲しいを明示的に処理
/// - Expression としての If値を使うは build_expression 経由のまま
pub(super) fn build_statement(&mut self, node: ASTNode) -> Result<ValueId, String> {
// Align current_span to this statement node before lowering expressions under it.
self.metadata_ctx.set_current_span(node.span());
match node {
// Phase 212.5: Statement としての If 処理
ASTNode::If {
condition,
then_body,
else_body,
..
} => {
// Statement としての If - 既存 If lowering を呼ぶ
self.build_if_statement(*condition, then_body, else_body)?;
// Statement なので値は使わないVoid を返す)
Ok(crate::mir::builder::emission::constant::emit_void(self))
}
// 将来ここに While / ForRange / Match / Using など statement 専用分岐を追加する。
other => self.build_expression(other),
}
}
/// Phase 212.5: Statement としての If 処理(副作用のみ)
///
/// ループ内 if や top-level statement if はここを通る。
/// Expression としての if値を使う場合は build_expression 経由。
///
/// # Arguments
/// * `condition` - If の条件式
/// * `then_body` - then ブロックの statements
/// * `else_body` - else ブロックの statements (optional)
///
/// # Example
/// ```hako
/// if i > 0 {
/// sum = sum + 1 // ← Statement としての If
/// }
/// ```
pub(super) fn build_if_statement(
&mut self,
condition: ASTNode,
then_body: Vec<ASTNode>,
else_body: Option<Vec<ASTNode>>,
) -> Result<(), String> {
use crate::ast::Span;
// then_body と else_body を ASTNode::Program に変換
let then_node = ASTNode::Program {
statements: then_body,
span: Span::unknown(),
};
let else_node = else_body.map(|b| ASTNode::Program {
statements: b,
span: Span::unknown(),
});
// 既存の If lowering を呼ぶcf_if は lower_if_form を呼ぶ)
// 戻り値は無視Statement なので値は使わない)
let _result = self.cf_if(condition, then_node, else_node)?;
Ok(())
}
// Local declarations with optional initializers
pub(super) fn build_local_statement(
&mut self,
variables: Vec<String>,
initial_values: Vec<Option<Box<ASTNode>>>,
) -> Result<ValueId, String> {
if std::env::var("NYASH_LOOPFORM_DEBUG").is_ok() {
crate::mir::builder::control_flow::joinir::trace::trace().stderr_if(
&format!(
"[build_local_statement] ENTRY: variables={:?}, initial_values.len()={}",
variables,
initial_values.len()
),
true,
);
}
let mut last_value = None;
for (i, var_name) in variables.iter().enumerate() {
let var_id = if i < initial_values.len() && initial_values[i].is_some() {
// Evaluate the initializer expression
let init_expr = initial_values[i].as_ref().unwrap();
let init_val = self.build_expression(*init_expr.clone())?;
// FIX: Allocate a new ValueId for this local variable
// Use next_value_id() which respects function context
let var_id = self.next_value_id();
if std::env::var("NYASH_LOOPFORM_DEBUG").is_ok() {
crate::mir::builder::control_flow::joinir::trace::trace().stderr_if(
&format!(
"[build_local_statement] '{}': init_val={:?}, allocated var_id={:?}",
var_name, init_val, var_id
),
true,
);
}
self.emit_instruction(crate::mir::MirInstruction::Copy {
dst: var_id,
src: init_val,
})?;
// Propagate metadata (type/origin) from initializer to variable
crate::mir::builder::metadata::propagate::propagate(self, init_val, var_id);
var_id
} else {
// Create a concrete register for uninitialized locals (Void)
let void_id = crate::mir::builder::emission::constant::emit_void(self);
if std::env::var("NYASH_LOOPFORM_DEBUG").is_ok() {
crate::mir::builder::control_flow::joinir::trace::trace().stderr_if(
&format!(
"[build_local_statement] '{}': uninitialized, void_id={:?}",
var_name, void_id
),
true,
);
}
void_id
};
if std::env::var("NYASH_LOOPFORM_DEBUG").is_ok() {
crate::mir::builder::control_flow::joinir::trace::trace().stderr_if(
&format!(
"[build_local_statement] Inserting '{}' -> {:?} into variable_map",
var_name, var_id
),
true,
);
}
self.declare_local_in_current_scope(var_name, var_id)?;
// SlotRegistry にもローカル変数スロットを登録しておくよ(観測専用)
if let Some(reg) = self.comp_ctx.current_slot_registry.as_mut() {
let ty = self.type_ctx.value_types.get(&var_id).cloned();
reg.ensure_slot(&var_name, ty);
}
last_value = Some(var_id);
}
// Phase 135 P0: Use function-level ValueId (SSOT) - build_local_statement is always in function context
Ok(last_value.unwrap_or_else(|| self.next_value_id()))
}
// Return statement
pub(super) fn build_return_statement(
&mut self,
value: Option<Box<ASTNode>>,
) -> Result<ValueId, String> {
// Enforce cleanup policy
if self.in_cleanup_block && !self.cleanup_allow_return {
return Err("return is not allowed inside cleanup block (enable NYASH_CLEANUP_ALLOW_RETURN=1 to permit)".to_string());
}
let return_value = if let Some(expr) = value {
self.build_expression(*expr)?
} else {
crate::mir::builder::emission::constant::emit_void(self)
};
if self.return_defer_active {
// Defer: copy into slot and jump to target
if let (Some(slot), Some(target)) = (self.return_defer_slot, self.return_defer_target) {
self.return_deferred_emitted = true;
self.emit_instruction(MirInstruction::Copy {
dst: slot,
src: return_value,
})?;
crate::mir::builder::metadata::propagate::propagate(self, return_value, slot);
if !self.is_current_block_terminated() {
crate::mir::builder::emission::branch::emit_jump(self, target)?;
}
Ok(return_value)
} else {
// Fallback: no configured slot/target; emit a real return
self.emit_instruction(MirInstruction::Return {
value: Some(return_value),
})?;
Ok(return_value)
}
} else {
// Normal return
self.emit_instruction(MirInstruction::Return {
value: Some(return_value),
})?;
Ok(return_value)
}
}
// Nowait: prefer env.future.spawn_instance if method call; else FutureNew
pub(super) fn build_nowait_statement(
&mut self,
variable: String,
expression: ASTNode,
) -> Result<ValueId, String> {
if let ASTNode::MethodCall {
object,
method,
arguments,
..
} = expression.clone()
{
let recv_val = self.build_expression(*object)?;
let mname_id =
crate::mir::builder::emission::constant::emit_string(self, method.clone());
let mut arg_vals: Vec<ValueId> = Vec::with_capacity(2 + arguments.len());
arg_vals.push(recv_val);
arg_vals.push(mname_id);
for a in arguments.into_iter() {
arg_vals.push(self.build_expression(a)?);
}
let future_id = self.next_value_id();
self.emit_instruction(MirInstruction::ExternCall {
dst: Some(future_id),
iface_name: "env.future".to_string(),
method_name: "spawn_instance".to_string(),
args: arg_vals,
effects: crate::mir::effect::EffectMask::PURE.add(crate::mir::effect::Effect::Io),
})?;
// Future spawn returns a Future<T>; the inner type is not statically known here.
// Register at least Future<Unknown> to avoid later fail-fast type inference panics.
self.type_ctx
.value_types
.insert(future_id, MirType::Future(Box::new(MirType::Unknown)));
self.variable_ctx
.variable_map
.insert(variable.clone(), future_id);
if let Some(reg) = self.comp_ctx.current_slot_registry.as_mut() {
reg.ensure_slot(&variable, None);
}
return Ok(future_id);
}
let expression_value = self.build_expression(expression)?;
let future_id = self.next_value_id();
self.emit_instruction(MirInstruction::FutureNew {
dst: future_id,
value: expression_value,
})?;
let inner = self
.type_ctx
.value_types
.get(&expression_value)
.cloned()
.unwrap_or(MirType::Unknown);
self.type_ctx
.value_types
.insert(future_id, MirType::Future(Box::new(inner)));
self.variable_ctx
.variable_map
.insert(variable.clone(), future_id);
if let Some(reg) = self.comp_ctx.current_slot_registry.as_mut() {
reg.ensure_slot(&variable, None);
}
Ok(future_id)
}
// Await: insert Safepoint before/after and emit Await
pub(super) fn build_await_expression(
&mut self,
expression: ASTNode,
) -> Result<ValueId, String> {
let future_value = self.build_expression(expression)?;
self.emit_instruction(MirInstruction::Safepoint)?;
let result_id = self.next_value_id();
self.emit_instruction(MirInstruction::Await {
dst: result_id,
future: future_value,
})?;
let result_type = match self.type_ctx.value_types.get(&future_value) {
Some(MirType::Future(inner)) => (**inner).clone(),
_ => MirType::Unknown,
};
self.type_ctx.value_types.insert(result_id, result_type);
self.emit_instruction(MirInstruction::Safepoint)?;
Ok(result_id)
}
/// MeResolverBox - SSOT for "me" resolution
/// 箱理論: variable_map["me"] のみを参照、文字列フォールバック禁止
/// Phase 269 P1.2: Removed string constant fallback (Fail-Fast principle)
pub(super) fn build_me_expression(&mut self) -> Result<ValueId, String> {
// Phase 269 P1.2: SSOT - variable_map["me"] only (no string fallback)
const ME_VAR: &str = "me"; // Small constant SSOT
// Fast path: return if "me" is in variable_map
if let Some(id) = self.variable_ctx.variable_map.get(ME_VAR).cloned() {
return Ok(id);
}
// ✅ Fail-Fast: "me" must be in variable_map (no string fallback)
// This is a contract violation - caller must initialize "me" before use
let function_context = self.scope_ctx.current_function
.as_ref()
.map(|f| f.signature.name.clone())
.unwrap_or_else(|| "unknown".to_string());
let static_box_context = self.comp_ctx.current_static_box
.as_ref()
.map(|s| s.as_str())
.unwrap_or("none");
Err(format!(
"[Phase269/P1.2/MeResolverBox] 'me'/'this' not found in variable_map\n\
\n\
Function: {}\n\
Static box context: {}\n\
\n\
This is an **instance method** context error.\n\
The legacy string constant fallback has been removed (Fail-Fast principle).\n\
\n\
Expected: variable_map contains 'me' → Box receiver ValueId (instance method)\n\
Got: variable_map missing 'me' entry\n\
\n\
Possible causes:\n\
1. Instance method called without proper 'me' initialization\n\
2. Method called from incorrect context (instance method in static context)\n\
\n\
Note: For **static box this.method()** calls, use ReceiverNormalizeBox\n\
(MethodCall common entry point handles static call normalization).\n\
\n\
Hint: Enable NYASH_TRACE_VARMAP=1 to trace variable_map changes.",
function_context,
static_box_context
))
}
}