stage3: unify to cleanup; MIR return-defer; docs+smokes updated; LLVM(harness): finalize_phis ownership, ret.py simplified, uses-predeclare; cleanup return override green; method-postfix cleanup return WIP (PHI head)

This commit is contained in:
Selfhosting Dev
2025-09-19 02:07:38 +09:00
parent 951a050592
commit 5e818eeb7e
205 changed files with 9671 additions and 1849 deletions

102
src/mir/builder/if_form.rs Normal file
View File

@ -0,0 +1,102 @@
use super::{ConstValue, MirBuilder, MirInstruction, ValueId};
use crate::mir::loop_api::LoopBuilderApi; // for current_block()
use crate::ast::ASTNode;
impl MirBuilder {
/// Lower an if/else using a structured IfForm (header→then/else→merge).
/// PHI-off: edge-copy only on predecessors; PHI-on: Phi at merge.
pub(super) fn lower_if_form(
&mut self,
condition: ASTNode,
then_branch: ASTNode,
else_branch: Option<ASTNode>,
) -> Result<ValueId, String> {
let condition_val = self.build_expression(condition)?;
// Create blocks
let then_block = self.block_gen.next();
let else_block = self.block_gen.next();
let merge_block = self.block_gen.next();
// Branch
self.emit_instruction(MirInstruction::Branch {
condition: condition_val,
then_bb: then_block,
else_bb: else_block,
})?;
// Snapshot variables before entering branches
let pre_if_var_map = self.variable_map.clone();
// then
self.current_block = Some(then_block);
self.ensure_block_exists(then_block)?;
let then_ast_for_analysis = then_branch.clone();
self.variable_map = pre_if_var_map.clone();
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();
if !self.is_current_block_terminated() {
self.emit_instruction(MirInstruction::Jump { target: merge_block })?;
}
// else
self.current_block = Some(else_block);
self.ensure_block_exists(else_block)?;
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())?;
(val, Some(else_ast), Some(self.variable_map.clone()))
} else {
let void_val = self.value_gen.next();
self.emit_instruction(MirInstruction::Const { dst: void_val, value: ConstValue::Void })?;
(void_val, None, None)
};
let else_exit_block = self.current_block()?;
if !self.is_current_block_terminated() {
self.emit_instruction(MirInstruction::Jump { target: merge_block })?;
}
// merge: primary result via helper, then delta-based variable merges
self.current_block = Some(merge_block);
self.ensure_block_exists(merge_block)?;
self.push_if_merge(merge_block);
// Pre-analysis: identify then-assigned var for skip
let assigned_then_pre = super::phi::extract_assigned_var(&then_ast_for_analysis);
let pre_then_var_value = assigned_then_pre
.as_ref()
.and_then(|name| pre_if_var_map.get(name).copied());
let result_val = self.normalize_if_else_phi(
then_block,
else_block,
Some(then_exit_block),
Some(else_exit_block),
then_value_raw,
else_value_raw,
&pre_if_var_map,
&then_ast_for_analysis,
&else_ast_for_analysis,
&then_var_map_end,
&else_var_map_end_opt,
pre_then_var_value,
)?;
// Merge other modified variables (skip the primary assignment if any)
let skip_name = assigned_then_pre.as_deref();
self.merge_modified_vars(
then_block,
else_block,
then_exit_block,
Some(else_exit_block),
&pre_if_var_map,
&then_var_map_end,
&else_var_map_end_opt,
skip_name,
)?;
self.pop_if_merge();
Ok(result_val)
}
}

View File

@ -67,6 +67,76 @@ pub(super) fn extract_assigned_var(ast: &ASTNode) -> Option<String> {
}
impl MirBuilder {
/// Merge all variables modified in then/else relative to pre_if_snapshot.
/// In PHI-off mode inserts edge copies from branch exits to merge. In PHI-on mode emits Phi.
/// `skip_var` allows skipping a variable already merged elsewhere (e.g., bound to an expression result).
pub(super) fn merge_modified_vars(
&mut self,
then_block: super::BasicBlockId,
else_block: super::BasicBlockId,
then_exit_block: super::BasicBlockId,
else_exit_block_opt: Option<super::BasicBlockId>,
pre_if_snapshot: &std::collections::HashMap<String, super::ValueId>,
then_map_end: &std::collections::HashMap<String, super::ValueId>,
else_map_end_opt: &Option<std::collections::HashMap<String, super::ValueId>>,
skip_var: Option<&str>,
) -> Result<(), String> {
use std::collections::HashSet;
let mut names: HashSet<&str> = HashSet::new();
for k in then_map_end.keys() { names.insert(k.as_str()); }
if let Some(emap) = else_map_end_opt.as_ref() {
for k in emap.keys() { names.insert(k.as_str()); }
}
// Only variables that changed against pre_if_snapshot
let mut changed: Vec<&str> = Vec::new();
for &name in &names {
let pre = pre_if_snapshot.get(name);
let t = then_map_end.get(name);
let e = else_map_end_opt.as_ref().and_then(|m| m.get(name));
// changed when either branch value differs from pre
if (t.is_some() && Some(t.copied().unwrap()) != pre.copied())
|| (e.is_some() && Some(e.copied().unwrap()) != pre.copied())
{
changed.push(name);
}
}
for name in changed {
if skip_var.map(|s| s == name).unwrap_or(false) {
continue;
}
let pre = match pre_if_snapshot.get(name) {
Some(v) => *v,
None => continue, // unknown before-if; skip
};
let then_v = then_map_end.get(name).copied().unwrap_or(pre);
let else_v = else_map_end_opt
.as_ref()
.and_then(|m| m.get(name).copied())
.unwrap_or(pre);
if self.is_no_phi_mode() {
let merged = self.value_gen.next();
// Insert edge copies from then/else exits into merge
self.insert_edge_copy(then_exit_block, merged, then_v)?;
if let Some(else_exit_block) = else_exit_block_opt {
self.insert_edge_copy(else_exit_block, merged, else_v)?;
} else {
// Fallback: if else missing, copy pre value from then as both inputs already cover
self.insert_edge_copy(then_exit_block, merged, then_v)?;
}
self.variable_map.insert(name.to_string(), merged);
} else {
let merged = self.value_gen.next();
self.emit_instruction(
MirInstruction::Phi {
dst: merged,
inputs: vec![(then_block, then_v), (else_block, else_v)],
}
)?;
self.variable_map.insert(name.to_string(), merged);
}
}
Ok(())
}
/// Normalize Phi creation for if/else constructs.
/// This handles variable reassignment patterns and ensures a single exit value.
pub(super) fn normalize_if_else_phi(

View File

@ -1,4 +1,3 @@
use super::phi::extract_assigned_var;
use super::{ConstValue, Effect, EffectMask, MirInstruction, ValueId};
use crate::ast::{ASTNode, CallExpr};
use crate::mir::loop_builder::LoopBuilder;
@ -162,82 +161,7 @@ impl super::MirBuilder {
then_branch: ASTNode,
else_branch: Option<ASTNode>,
) -> Result<ValueId, String> {
let condition_val = self.build_expression(condition)?;
let then_block = self.block_gen.next();
let else_block = self.block_gen.next();
let merge_block = self.block_gen.next();
self.emit_instruction(MirInstruction::Branch {
condition: condition_val,
then_bb: then_block,
else_bb: else_block,
})?;
// Snapshot variable map before entering branches to avoid cross-branch pollution
let pre_if_var_map = self.variable_map.clone();
// Pre-analysis: detect then-branch assigned var and capture its pre-if value
let assigned_then_pre = extract_assigned_var(&then_branch);
let pre_then_var_value: Option<ValueId> = assigned_then_pre
.as_ref()
.and_then(|name| pre_if_var_map.get(name).copied());
// then
self.current_block = Some(then_block);
self.ensure_block_exists(then_block)?;
let then_ast_for_analysis = then_branch.clone();
// Build then with a clean snapshot of pre-if variables
self.variable_map = pre_if_var_map.clone();
let then_value_raw = self.build_expression(then_branch)?;
let then_exit_block = Self::current_block(self)?;
let then_var_map_end = self.variable_map.clone();
if !self.is_current_block_terminated() {
self.emit_instruction(MirInstruction::Jump {
target: merge_block,
})?;
}
// else
self.current_block = Some(else_block);
self.ensure_block_exists(else_block)?;
// Build else with a clean snapshot of pre-if variables
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())?;
(val, Some(else_ast), Some(self.variable_map.clone()))
} else {
let void_val = self.value_gen.next();
self.emit_instruction(MirInstruction::Const {
dst: void_val,
value: ConstValue::Void,
})?;
(void_val, None, None)
};
let else_exit_block = Self::current_block(self)?;
if !self.is_current_block_terminated() {
self.emit_instruction(MirInstruction::Jump {
target: merge_block,
})?;
}
// merge + phi
self.current_block = Some(merge_block);
self.ensure_block_exists(merge_block)?;
let result_val = self.normalize_if_else_phi(
then_block,
else_block,
Some(then_exit_block),
Some(else_exit_block),
then_value_raw,
else_value_raw,
&pre_if_var_map,
&then_ast_for_analysis,
&else_ast_for_analysis,
&then_var_map_end,
&else_var_map_end_opt,
pre_then_var_value,
)?;
Ok(result_val)
self.lower_if_form(condition, then_branch, else_branch)
}
// Loop: delegate to LoopBuilder
@ -278,6 +202,21 @@ impl super::MirBuilder {
};
let exit_block = self.block_gen.next();
// Snapshot and enable deferred-return mode for try/catch
let saved_defer_active = self.return_defer_active;
let saved_defer_slot = self.return_defer_slot;
let saved_defer_target = self.return_defer_target;
let saved_deferred_flag = self.return_deferred_emitted;
let saved_in_cleanup = self.in_cleanup_block;
let saved_allow_ret = self.cleanup_allow_return;
let saved_allow_throw = self.cleanup_allow_throw;
let ret_slot = self.value_gen.next();
self.return_defer_active = true;
self.return_defer_slot = Some(ret_slot);
self.return_deferred_emitted = false;
self.return_defer_target = Some(finally_block.unwrap_or(exit_block));
if let Some(catch_clause) = catch_clauses.first() {
if std::env::var("NYASH_DEBUG_TRYCATCH").ok().as_deref() == Some("1") {
eprintln!(
@ -331,27 +270,61 @@ impl super::MirBuilder {
})?;
}
let mut cleanup_terminated = false;
if let (Some(finally_block_id), Some(finally_statements)) = (finally_block, finally_body) {
self.start_new_block(finally_block_id)?;
// Enter cleanup mode; returns may or may not be allowed by policy
self.in_cleanup_block = true;
self.cleanup_allow_return = crate::config::env::cleanup_allow_return();
self.cleanup_allow_throw = crate::config::env::cleanup_allow_throw();
// Inside cleanup, do not defer returns; either forbid or emit real Return
self.return_defer_active = false;
let finally_ast = ASTNode::Program {
statements: finally_statements,
span: crate::ast::Span::unknown(),
};
self.build_expression(finally_ast)?;
self.emit_instruction(MirInstruction::Jump { target: exit_block })?;
// Do not emit a Jump if the finally block already terminated (e.g., via return/throw)
cleanup_terminated = self.is_current_block_terminated();
if !cleanup_terminated {
self.emit_instruction(MirInstruction::Jump { target: exit_block })?;
}
self.in_cleanup_block = false;
}
self.start_new_block(exit_block)?;
let result = self.value_gen.next();
self.emit_instruction(MirInstruction::Const {
dst: result,
value: ConstValue::Void,
})?;
// If any deferred return occurred in try/catch and cleanup did not already return,
// finalize with a Return of the slot; otherwise emit a dummy const/ret to satisfy structure.
let result = if self.return_deferred_emitted && !cleanup_terminated {
self.emit_instruction(MirInstruction::Return { value: Some(ret_slot) })?;
// Emit a symbolic const to satisfy return type inference paths when inspecting non-terminated blocks (not used here)
let r = self.value_gen.next();
self.emit_instruction(MirInstruction::Const { dst: r, value: ConstValue::Void })?;
r
} else {
let r = self.value_gen.next();
self.emit_instruction(MirInstruction::Const { dst: r, value: ConstValue::Void })?;
r
};
// Restore deferred-return context
self.return_defer_active = saved_defer_active;
self.return_defer_slot = saved_defer_slot;
self.return_defer_target = saved_defer_target;
self.return_deferred_emitted = saved_deferred_flag;
self.in_cleanup_block = saved_in_cleanup;
self.cleanup_allow_return = saved_allow_ret;
self.cleanup_allow_throw = saved_allow_throw;
Ok(result)
}
// Throw: emit Throw or fallback to env.debug.trace when disabled
pub(super) fn build_throw_statement(&mut self, expression: ASTNode) -> Result<ValueId, String> {
// Enforce cleanup policy
if self.in_cleanup_block && !self.cleanup_allow_throw {
return Err("throw is not allowed inside cleanup block (enable NYASH_CLEANUP_ALLOW_THROW=1 to permit)".to_string());
}
if std::env::var("NYASH_BUILDER_DISABLE_THROW").ok().as_deref() == Some("1") {
let v = self.build_expression(expression)?;
self.emit_instruction(MirInstruction::ExternCall {
@ -396,20 +369,38 @@ impl super::MirBuilder {
&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 {
let void_dst = self.value_gen.next();
self.emit_instruction(MirInstruction::Const {
dst: void_dst,
value: ConstValue::Void,
})?;
self.emit_instruction(MirInstruction::Const { dst: void_dst, value: ConstValue::Void })?;
void_dst
};
self.emit_instruction(MirInstruction::Return {
value: Some(return_value),
})?;
Ok(return_value)
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 })?;
if !self.is_current_block_terminated() {
self.emit_instruction(MirInstruction::Jump { 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
@ -493,4 +484,4 @@ impl super::MirBuilder {
Ok(me_value)
}
}
use crate::mir::loop_api::LoopBuilderApi; // for current_block()
// use crate::mir::loop_api::LoopBuilderApi; // no longer needed here