feat(joinir): Phase 188 JoinInlineBoundary + Pattern 1 working! 🎉
Major milestone: loop_min_while.hako outputs "0 1 2" correctly! ## JoinInlineBoundary (Option D from ChatGPT Pro design review) - New struct for clean SSA boundary between JoinIR and host function - JoinIR uses local ValueIds (0,1,2...) - no host ValueId dependency - Copy injection at entry block connects host → JoinIR values ## Pattern 1 Simple While Loop - Refactored to use pure local ValueIds - Removed Pattern1Context dependency on host ValueIds - Clean separation: lowerer generates, merger connects ## Key Design Principles (Box Theory) - Box A: JoinIR Frontend (host-agnostic) - Box B: Join→MIR Bridge (independent functions) - Box C: JoinInlineBoundary (boundary info only) - Box D: JoinMirInlineMerger (Copy injection) ## Files Changed - NEW: inline_boundary.rs - JoinInlineBoundary struct - control_flow.rs - merge with boundary, void return fix - simple_while_minimal.rs - pure local ValueIds - mod.rs - module export Test: NYASH_DISABLE_PLUGINS=1 ./target/release/hakorune apps/tests/loop_min_while.hako Output: 0\n1\n2 ✅ 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@ -358,7 +358,8 @@ impl super::MirBuilder {
|
||||
// - Add generated blocks to current_function
|
||||
// - Jump from current_block to the entry of generated loop
|
||||
// - The loop exit becomes the new current_block
|
||||
self.merge_joinir_mir_blocks(&mir_module, debug)?;
|
||||
// Phase 188-Impl-3: Pass None for boundary (legacy path without boundary)
|
||||
self.merge_joinir_mir_blocks(&mir_module, None, debug)?;
|
||||
|
||||
// Return void for now (loop doesn't have a meaningful return value in this context)
|
||||
let void_val = self.next_value_id();
|
||||
@ -484,26 +485,38 @@ impl super::MirBuilder {
|
||||
}
|
||||
|
||||
// Merge JoinIR blocks into current function
|
||||
self.merge_joinir_mir_blocks(&mir_module, debug)?;
|
||||
// Phase 188-Impl-3: Create and pass JoinInlineBoundary for Pattern 1
|
||||
let boundary = crate::mir::join_ir::lowering::inline_boundary::JoinInlineBoundary::new_inputs_only(
|
||||
vec![ValueId(0)], // JoinIR's main() parameter (loop variable)
|
||||
vec![loop_var_id], // Host's loop variable
|
||||
);
|
||||
self.merge_joinir_mir_blocks(&mir_module, Some(&boundary), debug)?;
|
||||
|
||||
// Return void/0 as loop result (Pattern 1 loops return 0)
|
||||
// Use the current block to emit the constant
|
||||
let zero_val = self.value_gen.next();
|
||||
use crate::mir::types::ConstValue;
|
||||
let current_block = self.current_block.ok_or_else(|| {
|
||||
"[cf_loop/joinir/pattern1] No current block available".to_string()
|
||||
})?;
|
||||
// Phase 188-Impl-4-FIX: Return Void instead of trying to emit Const
|
||||
//
|
||||
// PROBLEM: Emitting instructions after merge_joinir_mir_blocks is fragile because:
|
||||
// 1. merge creates exit block and switches to it
|
||||
// 2. We try to add Const to exit block
|
||||
// 3. But subsequent code (return statement) might overwrite the block
|
||||
//
|
||||
// SOLUTION: Loops don't produce values - they return Void.
|
||||
// The subsequent "return 0" statement will emit its own Const + Return.
|
||||
//
|
||||
// This is cleaner because:
|
||||
// - Loop lowering doesn't need to know about the return value
|
||||
// - The return statement handles its own code generation
|
||||
// - No risk of instructions being lost due to block management issues
|
||||
|
||||
if let Some(ref mut func) = self.current_function {
|
||||
if let Some(block) = func.get_block_mut(current_block) {
|
||||
block.instructions.push(crate::mir::MirInstruction::Const {
|
||||
dst: zero_val,
|
||||
value: ConstValue::Integer(0),
|
||||
});
|
||||
}
|
||||
let void_val = crate::mir::builder::emission::constant::emit_void(self);
|
||||
|
||||
if debug {
|
||||
eprintln!(
|
||||
"[cf_loop/joinir/pattern1] Loop complete, returning Void {:?}",
|
||||
void_val
|
||||
);
|
||||
}
|
||||
|
||||
Ok(Some(zero_val))
|
||||
Ok(Some(void_val))
|
||||
}
|
||||
|
||||
/// Phase 49-3.2: Merge JoinIR-generated MIR blocks into current_function
|
||||
@ -521,9 +534,25 @@ impl super::MirBuilder {
|
||||
/// - Pattern 1 (Simple While) generates 3 functions: entry + loop_step + k_exit
|
||||
/// - All functions are flattened into current_function with global ID remapping
|
||||
/// - Single exit block receives all Return instructions from all functions
|
||||
///
|
||||
/// # Phase 188-Impl-3: JoinInlineBoundary Support
|
||||
///
|
||||
/// When `boundary` is provided, injects Copy instructions at the entry block
|
||||
/// to connect host ValueIds to JoinIR local ValueIds:
|
||||
///
|
||||
/// ```text
|
||||
/// entry_block:
|
||||
/// // Injected by boundary
|
||||
/// ValueId(100) = Copy ValueId(4) // join_input → host_input
|
||||
/// // Original JoinIR instructions follow...
|
||||
/// ```
|
||||
///
|
||||
/// This enables clean separation: JoinIR uses local IDs (0,1,2...),
|
||||
/// host uses its own IDs, and Copy instructions bridge the gap.
|
||||
fn merge_joinir_mir_blocks(
|
||||
&mut self,
|
||||
mir_module: &crate::mir::MirModule,
|
||||
boundary: Option<&crate::mir::join_ir::lowering::inline_boundary::JoinInlineBoundary>,
|
||||
debug: bool,
|
||||
) -> Result<(), String> {
|
||||
use crate::mir::{BasicBlock, BasicBlockId, MirInstruction, ValueId};
|
||||
@ -588,14 +617,19 @@ impl super::MirBuilder {
|
||||
|
||||
// 3. Collect all ValueIds used across ALL functions (Phase 189)
|
||||
// Also build a map of ValueId → function name for Call→Jump conversion
|
||||
// Phase 188-Impl-3: Also collect function parameters for tail call conversion
|
||||
if debug {
|
||||
eprintln!("[cf_loop/joinir] Phase 189: Collecting value IDs from all functions");
|
||||
}
|
||||
let mut used_values: std::collections::BTreeSet<ValueId> =
|
||||
std::collections::BTreeSet::new();
|
||||
let mut value_to_func_name: HashMap<ValueId, String> = HashMap::new();
|
||||
let mut function_params: HashMap<String, Vec<ValueId>> = HashMap::new();
|
||||
|
||||
for (func_name, func) in &mir_module.functions {
|
||||
// Phase 188-Impl-3: Collect function parameters for tail call conversion
|
||||
function_params.insert(func_name.clone(), func.params.clone());
|
||||
|
||||
for func in mir_module.functions.values() {
|
||||
for block in func.blocks.values() {
|
||||
Self::collect_values_in_block(block, &mut used_values);
|
||||
// Phase 189: Track Const String instructions that define function names
|
||||
@ -731,6 +765,7 @@ impl super::MirBuilder {
|
||||
}
|
||||
|
||||
// Second pass: Insert parameter bindings for tail calls
|
||||
// Phase 188-Impl-3: Use actual parameter ValueIds from target function
|
||||
if let Some((target_block, args)) = tail_call_target {
|
||||
if debug {
|
||||
eprintln!(
|
||||
@ -739,20 +774,36 @@ impl super::MirBuilder {
|
||||
);
|
||||
}
|
||||
|
||||
// Insert Copy instructions for parameter binding
|
||||
for (i, arg_val_remapped) in args.iter().enumerate() {
|
||||
let param_val_original = ValueId(i as u32);
|
||||
if let Some(¶m_val_remapped) = value_map.get(¶m_val_original) {
|
||||
new_block.instructions.push(MirInstruction::Copy {
|
||||
dst: param_val_remapped,
|
||||
src: *arg_val_remapped,
|
||||
});
|
||||
// Find the target function name from the target_block
|
||||
// We need to reverse-lookup the function name from the entry block
|
||||
let mut target_func_name: Option<String> = None;
|
||||
for (fname, &entry_block) in &function_entry_map {
|
||||
if entry_block == target_block {
|
||||
target_func_name = Some(fname.clone());
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
if debug {
|
||||
eprintln!(
|
||||
"[cf_loop/joinir] Param binding: arg {:?} → param {:?}",
|
||||
arg_val_remapped, param_val_remapped
|
||||
);
|
||||
if let Some(target_func_name) = target_func_name {
|
||||
if let Some(target_params) = function_params.get(&target_func_name) {
|
||||
// Insert Copy instructions for parameter binding
|
||||
for (i, arg_val_remapped) in args.iter().enumerate() {
|
||||
if i < target_params.len() {
|
||||
let param_val_original = target_params[i];
|
||||
if let Some(¶m_val_remapped) = value_map.get(¶m_val_original) {
|
||||
new_block.instructions.push(MirInstruction::Copy {
|
||||
dst: param_val_remapped,
|
||||
src: *arg_val_remapped,
|
||||
});
|
||||
|
||||
if debug {
|
||||
eprintln!(
|
||||
"[cf_loop/joinir] Param binding: arg {:?} → param {:?}",
|
||||
arg_val_remapped, param_val_remapped
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -804,6 +855,62 @@ impl super::MirBuilder {
|
||||
}
|
||||
}
|
||||
|
||||
// Phase 188-Impl-3: Inject Copy instructions for boundary inputs
|
||||
if let Some(boundary) = boundary {
|
||||
// Get entry function's entry block (first function by convention)
|
||||
let (entry_func_name, entry_func) = mir_module
|
||||
.functions
|
||||
.iter()
|
||||
.next()
|
||||
.ok_or("JoinIR module has no functions")?;
|
||||
let entry_block_remapped = block_map[&(entry_func_name.clone(), entry_func.entry_block)];
|
||||
|
||||
if debug {
|
||||
eprintln!(
|
||||
"[cf_loop/joinir] Phase 188-Impl-3: Injecting {} Copy instructions at entry block {:?}",
|
||||
boundary.join_inputs.len(),
|
||||
entry_block_remapped
|
||||
);
|
||||
}
|
||||
|
||||
// Inject Copy instructions: join_input_remapped = Copy host_input
|
||||
if let Some(ref mut current_func) = self.current_function {
|
||||
if let Some(entry_block) = current_func.get_block_mut(entry_block_remapped) {
|
||||
// Insert Copy instructions at the BEGINNING of the block
|
||||
let mut copy_instructions = Vec::new();
|
||||
for (join_in, host_in) in boundary.join_inputs.iter().zip(&boundary.host_inputs) {
|
||||
// join_in is JoinIR's local ValueId (e.g., ValueId(0))
|
||||
// host_in is host function's ValueId (e.g., ValueId(4))
|
||||
// We need to remap join_in to the merged space
|
||||
if let Some(&join_in_remapped) = value_map.get(join_in) {
|
||||
copy_instructions.push(MirInstruction::Copy {
|
||||
dst: join_in_remapped,
|
||||
src: *host_in,
|
||||
});
|
||||
if debug {
|
||||
eprintln!(
|
||||
"[cf_loop/joinir] Copy boundary: {:?} (host) → {:?} (join_remapped)",
|
||||
host_in, join_in_remapped
|
||||
);
|
||||
}
|
||||
} else {
|
||||
if debug {
|
||||
eprintln!(
|
||||
"[cf_loop/joinir] WARNING: join_input {:?} not found in value_map",
|
||||
join_in
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Insert at beginning (reverse order so they appear in correct order)
|
||||
for inst in copy_instructions.into_iter().rev() {
|
||||
entry_block.instructions.insert(0, inst);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 6. Create exit block (empty for now, will be populated after loop)
|
||||
if let Some(ref mut func) = self.current_function {
|
||||
let exit_block = BasicBlock::new(exit_block_id);
|
||||
|
||||
Reference in New Issue
Block a user