From a767f0f3a927bbf575024505b5fa5e7796a0fb19 Mon Sep 17 00:00:00 2001 From: tomoaki Date: Sun, 21 Dec 2025 02:40:07 +0900 Subject: [PATCH] feat(joinir): Phase 259 P0 - Pattern8 BoolPredicateScan + Copy binding fix MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Pattern8 (Boolean Predicate Scan) implementation for is_integer/1: - New pattern detection for `loop + if not predicate() { return false }` - JoinIR lowerer with main/loop_step/k_exit structure - Me receiver passed as param (by-name 禁止) Key fixes: 1. expr_result = Some(join_exit_value) (Pattern7 style) 2. Tail-call: dst: None (no extra Ret instruction) 3. instruction_rewriter: Add `&& is_loop_header_with_phi` check - Pattern8 has no carriers → no PHIs → MUST generate Copy bindings - Without this, ValueId(103/104/105) were undefined Status: Copy instructions now generated correctly, but exit block creation issue remains (next step: Step A-C in指示書). 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 --- apps/tests/phase259_p0_is_integer_min.hako | 35 ++ .../current/main/phases/phase-259/README.md | 42 +- .../joinir/merge/instruction_rewriter.rs | 5 +- .../control_flow/joinir/patterns/mod.rs | 1 + .../patterns/pattern8_scan_bool_predicate.rs | 549 ++++++++++++++++++ .../control_flow/joinir/patterns/router.rs | 5 + src/mir/join_ir/lowering/mod.rs | 1 + .../lowering/scan_bool_predicate_minimal.rs | 252 ++++++++ .../apps/phase259_p0_is_integer_llvm_exe.sh | 14 + .../apps/phase259_p0_is_integer_vm.sh | 14 + 10 files changed, 913 insertions(+), 5 deletions(-) create mode 100644 apps/tests/phase259_p0_is_integer_min.hako create mode 100644 src/mir/builder/control_flow/joinir/patterns/pattern8_scan_bool_predicate.rs create mode 100644 src/mir/join_ir/lowering/scan_bool_predicate_minimal.rs create mode 100644 tools/smokes/v2/profiles/integration/apps/phase259_p0_is_integer_llvm_exe.sh create mode 100644 tools/smokes/v2/profiles/integration/apps/phase259_p0_is_integer_vm.sh diff --git a/apps/tests/phase259_p0_is_integer_min.hako b/apps/tests/phase259_p0_is_integer_min.hako new file mode 100644 index 00000000..5dc14ac9 --- /dev/null +++ b/apps/tests/phase259_p0_is_integer_min.hako @@ -0,0 +1,35 @@ +static box StringUtils { + is_digit(ch) { + return ch == "0" or ch == "1" or ch == "2" or ch == "3" or ch == "4" + or ch == "5" or ch == "6" or ch == "7" or ch == "8" or ch == "9" + } + + is_integer(s) { + if s.length() == 0 { + return false + } + + local start = 0 + if s.substring(0, 1) == "-" { + if s.length() == 1 { + return false + } + start = 1 + } + + local i = start + loop(i < s.length()) { + if not this.is_digit(s.substring(i, i + 1)) { + return false + } + i = i + 1 + } + return true + } +} + +static box Main { + main() { + return StringUtils.is_integer("123") ? 7 : 1 + } +} diff --git a/docs/development/current/main/phases/phase-259/README.md b/docs/development/current/main/phases/phase-259/README.md index 0a8ec299..e26f35e1 100644 --- a/docs/development/current/main/phases/phase-259/README.md +++ b/docs/development/current/main/phases/phase-259/README.md @@ -22,8 +22,42 @@ Related: ## Proposed Approach (P0) -方針: prelude(nested-if)は既存 lowering のまま、loop 部分だけを scan パターンへ寄せる(構造で解決)。 +**P0 Design Decision: Pattern8(新規)採用** -P0 の狙い: -- loop の core は “scan until mismatch” で、Pattern6(scan)と近い -- ただし return 値が `i/-1` ではなく `true/false` なので、scan パターンの “return payload” を一般化する必要がある可能性がある +### Why Pattern8? + +Pattern6(index_of系)は "見つける" scan(返り値: 整数 i or -1)で、is_integer は "全部検証する" predicate scan(返り値: 真偽値 true/false)。役割が異なるため、Pattern8 として分離した。 + +### Pattern8 vs Pattern6 + +| | Pattern6 (index_of系) | Pattern8 (is_integer系) | +|---|---|---| +| 役割 | "見つける" scan | "全部検証する" predicate scan | +| Match形 | `substring(...) == needle` | `not predicate(ch)` → early exit | +| 返り値 | Integer (i or -1) | Boolean (true/false) | +| Exit PHI | `i`(ループ状態変数) | `ret_bool`(検証結果) | +| Carriers | [i] (LoopState) | [] (empty, expr_result のみ) | + +### JoinIR Contract + +- **jump_args_layout**: ExprResultPlusCarriers(carriers=0) +- **expr_result**: Some(join_exit_value) - ret_bool from k_exit (pipeline handling) +- **exit_bindings**: Empty(carriers なし) +- **SSOT**: `join_inputs = entry_func.params.clone()` +- **Me receiver**: Passed as param [i, me, s] (by-name 禁止) + +### 受理形(P0固定) + +```nyash +loop(i < s.length()) { + if not this.is_digit(s.substring(i, i + 1)) { + return false + } + i = i + 1 +} +return true +``` + +- prelude の start 計算は許可(ただし i_init = start で渡す) +- predicate は Me method call(this.is_digit)のみ +- step は 1 固定 diff --git a/src/mir/builder/control_flow/joinir/merge/instruction_rewriter.rs b/src/mir/builder/control_flow/joinir/merge/instruction_rewriter.rs index 392c9841..eeeb1917 100644 --- a/src/mir/builder/control_flow/joinir/merge/instruction_rewriter.rs +++ b/src/mir/builder/control_flow/joinir/merge/instruction_rewriter.rs @@ -645,7 +645,7 @@ pub(super) fn merge_and_rewrite( "[cf_loop/joinir] Phase 33-21: Skip param bindings in header block (PHIs define carriers)" ); } - } else if is_recursive_call || is_target_loop_entry { + } else if (is_recursive_call || is_target_loop_entry) && is_loop_header_with_phi { // Phase 256 P1.10: Skip param bindings for: // - Recursive call (loop_step → loop_step): latch edge // - Entry call (main → loop_step): entry edge @@ -653,6 +653,9 @@ pub(super) fn merge_and_rewrite( // Header PHIs receive values from these edges via separate mechanism. // Generating Copies here would cause multiple definitions. // + // Phase 259 P0 FIX: Only skip if loop header HAS PHIs! + // Pattern8 has no carriers → no PHIs → MUST generate Copy bindings. + // // Update remapper mappings for any further instructions. for (i, arg_val_remapped) in args.iter().enumerate() { if i < target_params.len() { diff --git a/src/mir/builder/control_flow/joinir/patterns/mod.rs b/src/mir/builder/control_flow/joinir/patterns/mod.rs index 1b14d9a7..1ff0aa9f 100644 --- a/src/mir/builder/control_flow/joinir/patterns/mod.rs +++ b/src/mir/builder/control_flow/joinir/patterns/mod.rs @@ -80,6 +80,7 @@ pub(in crate::mir::builder) mod pattern4_with_continue; pub(in crate::mir::builder) mod pattern5_infinite_early_exit; // Phase 131-11 pub(in crate::mir::builder) mod pattern6_scan_with_init; // Phase 254 P0: index_of/find/contains pattern pub(in crate::mir::builder) mod pattern7_split_scan; // Phase 256 P0: split/tokenization with variable step +pub(in crate::mir::builder) mod pattern8_scan_bool_predicate; // Phase 259 P0: boolean predicate scan (is_integer/is_valid) pub(in crate::mir::builder) mod pattern_pipeline; pub(in crate::mir::builder) mod router; pub(in crate::mir::builder) mod trim_loop_lowering; // Phase 180: Dedicated Trim/P5 lowering module diff --git a/src/mir/builder/control_flow/joinir/patterns/pattern8_scan_bool_predicate.rs b/src/mir/builder/control_flow/joinir/patterns/pattern8_scan_bool_predicate.rs new file mode 100644 index 00000000..068672cc --- /dev/null +++ b/src/mir/builder/control_flow/joinir/patterns/pattern8_scan_bool_predicate.rs @@ -0,0 +1,549 @@ +//! Pattern 8: Boolean Predicate Scan (is_integer/is_valid form) +//! +//! Phase 259 P0: Dedicated pattern for boolean predicate validation loops +//! +//! ## Pattern Structure +//! +//! ```nyash +//! is_integer(s) { +//! local i = start // Computed in prelude +//! loop(i < s.length()) { +//! if not this.is_digit(s.substring(i, i + 1)) { +//! return false +//! } +//! i = i + 1 +//! } +//! return true +//! } +//! ``` +//! +//! ## Detection Criteria (P0: Fixed Form Only) +//! +//! 1. Loop condition: `i < s.length()` +//! 2. Loop body has if statement with: +//! - Condition: `not this.method(...)` (UnaryOp::Not + MethodCall) +//! - Then branch: `return false` (early exit) +//! 3. Loop body has step: `i = i + 1` +//! 4. Post-loop: `return true` +//! +//! ## vs Pattern 6 +//! +//! - Pattern 6: Match scan (substring == needle → return i) +//! - Pattern 8: Predicate scan (not is_digit → return false, else true) +//! - Pattern 6: Returns integer (index or -1) +//! - Pattern 8: Returns boolean (true/false) + +use super::super::trace; +use super::common::var; +use crate::ast::{ASTNode, BinaryOperator, LiteralValue, UnaryOperator}; +use crate::mir::builder::MirBuilder; +use crate::mir::ValueId; + +/// Phase 259 P0: Extracted structure for boolean predicate scan +#[derive(Debug, Clone)] +struct BoolPredicateScanParts { + /// Loop variable name (e.g., "i") + loop_var: String, + /// Haystack variable name (e.g., "s") + haystack: String, + /// Predicate method receiver (e.g., "this") + predicate_receiver: String, + /// Predicate method name (e.g., "is_digit") + predicate_method: String, + /// Step literal (P0: must be 1) + step_lit: i64, +} + +/// Phase 259 P0: Detection for Pattern 8 (BoolPredicateScan) +pub(crate) fn can_lower(_builder: &MirBuilder, ctx: &super::router::LoopPatternContext) -> bool { + eprintln!("[pattern8/can_lower] Called for function: {}", ctx.func_name); + match extract_bool_predicate_scan_parts(ctx.condition, ctx.body) { + Ok(Some(_)) => { + if ctx.debug { + trace::trace().debug( + "pattern8/can_lower", + "accept: boolean predicate scan pattern extractable", + ); + } + true + } + Ok(None) => { + if ctx.debug { + trace::trace().debug( + "pattern8/can_lower", + "reject: not a boolean predicate scan pattern", + ); + } + false + } + Err(e) => { + if ctx.debug { + trace::trace().debug( + "pattern8/can_lower", + &format!("reject: extraction error: {}", e), + ); + } + false + } + } +} + +/// Phase 259 P0: Extract boolean predicate scan pattern parts +/// +/// # P0 Restrictions (Fail-Fast) +/// +/// - Loop condition: `i < s.length()` (forward only) +/// - If condition: `not this.method(s.substring(i, i + 1))` (UnaryOp::Not) +/// - Then branch: `return false` (Literal::Bool(false)) +/// - Step: `i = i + 1` (step_lit == 1) +/// - Post-loop: `return true` (enforced by caller) +fn extract_bool_predicate_scan_parts( + condition: &ASTNode, + body: &[ASTNode], +) -> Result, String> { + eprintln!("[pattern8/extract] Starting extraction"); + eprintln!("[pattern8/extract] Body statements: {}", body.len()); + + // 1. Check loop condition: i < s.length() + let (loop_var, haystack) = match condition { + ASTNode::BinaryOp { + operator: BinaryOperator::Less, + left, + right, + .. + } => { + let loop_var = match left.as_ref() { + ASTNode::Variable { name, .. } => name.clone(), + _ => { + eprintln!("[pattern8/extract] REJECT: loop condition left is not Variable"); + return Ok(None); + } + }; + + let haystack = match right.as_ref() { + ASTNode::MethodCall { + object, method, .. + } if method == "length" => match object.as_ref() { + ASTNode::Variable { name, .. } => name.clone(), + _ => { + eprintln!("[pattern8/extract] REJECT: length() object is not Variable"); + return Ok(None); + } + }, + _ => { + eprintln!("[pattern8/extract] REJECT: loop condition right is not .length()"); + return Ok(None); + } + }; + + (loop_var, haystack) + } + _ => { + eprintln!("[pattern8/extract] REJECT: loop condition is not BinaryOp::Less"); + return Ok(None); + } + }; + eprintln!("[pattern8/extract] ✅ Loop condition OK: {} < {}.length()", loop_var, haystack); + + // 2. Find if statement with predicate check and return false + let mut predicate_receiver_opt = None; + let mut predicate_method_opt = None; + + eprintln!("[pattern8/extract] Step 2: Searching for predicate pattern in {} statements", body.len()); + for (i, stmt) in body.iter().enumerate() { + eprintln!("[pattern8/extract] Statement {}: {:?}", i, stmt); + if let ASTNode::If { + condition: if_cond, + then_body, + .. + } = stmt + { + // Check if condition is: not this.method(...) + if let ASTNode::UnaryOp { + operator: UnaryOperator::Not, + operand, + .. + } = if_cond.as_ref() + { + // Operand must be MethodCall + if let ASTNode::MethodCall { + object, + method, + arguments, + .. + } = operand.as_ref() + { + // Extract receiver (e.g., "me") + // Phase 259 P0: Support both Variable and Me node + // IMPORTANT: Me is registered as "me" in variable_map (not "this") + let receiver = match object.as_ref() { + ASTNode::Variable { name, .. } => name.clone(), + ASTNode::Me { .. } => "me".to_string(), // Me is registered as "me" in MirBuilder + _ => { + eprintln!("[pattern8/extract] Receiver is not Variable or Me: {:?}", object); + continue; + } + }; + + // P0: Expect 1 argument: s.substring(i, i + 1) + if arguments.len() != 1 { + continue; + } + + // Validate argument is substring call + if let ASTNode::MethodCall { + object: substr_obj, + method: substr_method, + arguments: substr_args, + .. + } = &arguments[0] + { + if substr_method != "substring" { + continue; + } + + // Object must be haystack + if let ASTNode::Variable { name, .. } = substr_obj.as_ref() { + if name != &haystack { + continue; + } + } else { + continue; + } + + // Args: (i, i + 1) + if substr_args.len() != 2 { + continue; + } + + // Arg 0: loop_var + match &substr_args[0] { + ASTNode::Variable { name, .. } if name == &loop_var => {} + _ => continue, + } + + // Arg 1: loop_var + 1 + match &substr_args[1] { + ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left, + right, + .. + } => { + // Left: loop_var + match left.as_ref() { + ASTNode::Variable { name, .. } if name == &loop_var => {} + _ => continue, + } + + // Right: Literal(1) + match right.as_ref() { + ASTNode::Literal { + value: LiteralValue::Integer(1), + .. + } => {} + _ => continue, + } + } + _ => continue, + } + } else { + continue; + } + + // Check then_body contains: return false + if then_body.len() == 1 { + if let ASTNode::Return { value, .. } = &then_body[0] { + if let Some(ret_val) = value { + if let ASTNode::Literal { + value: LiteralValue::Bool(false), + .. + } = ret_val.as_ref() + { + eprintln!("[pattern8/extract] ✅ Found predicate pattern: {}.{}", receiver, method); + predicate_receiver_opt = Some(receiver); + predicate_method_opt = Some(method.clone()); + } + } + } + } + } + } + } + } + + if predicate_receiver_opt.is_none() { + eprintln!("[pattern8/extract] REJECT: No predicate pattern found"); + } + + let predicate_receiver = predicate_receiver_opt.ok_or_else(|| "No predicate pattern found")?; + let predicate_method = predicate_method_opt.ok_or_else(|| "No predicate method found")?; + + // 3. Check for step: i = i + 1 + let mut step_lit_opt = None; + + eprintln!("[pattern8/extract] Step 3: Searching for step pattern ({} = {} + 1)", loop_var, loop_var); + for stmt in body { + if let ASTNode::Assignment { target, value, .. } = stmt { + if let ASTNode::Variable { name: target_name, .. } = target.as_ref() { + if target_name == &loop_var { + if let ASTNode::BinaryOp { + operator: BinaryOperator::Add, + left, + right, + .. + } = value.as_ref() + { + if let ASTNode::Variable { name: left_name, .. } = left.as_ref() { + if left_name == &loop_var { + if let ASTNode::Literal { + value: LiteralValue::Integer(lit), + .. + } = right.as_ref() + { + eprintln!("[pattern8/extract] ✅ Found step pattern: {} = {} + {}", loop_var, loop_var, lit); + step_lit_opt = Some(*lit); + } + } + } + } + } + } + } + } + + if step_lit_opt.is_none() { + eprintln!("[pattern8/extract] REJECT: No step pattern found"); + } + + let step_lit = step_lit_opt.ok_or_else(|| "No step pattern found")?; + + // P0: Step must be 1 + if step_lit != 1 { + eprintln!("[pattern8/extract] REJECT: Step is {}, expected 1", step_lit); + return Ok(None); + } + + eprintln!("[pattern8/extract] ✅✅✅ ACCEPT: Pattern8 extraction successful!"); + eprintln!("[pattern8/extract] Parts: loop_var={}, haystack={}, predicate={}.{}, step={}", + loop_var, haystack, predicate_receiver, predicate_method, step_lit); + + Ok(Some(BoolPredicateScanParts { + loop_var, + haystack, + predicate_receiver, + predicate_method, + step_lit, + })) +} + +/// Phase 259 P0: Lowering function for Pattern 8 +pub(crate) fn lower( + builder: &mut MirBuilder, + ctx: &super::router::LoopPatternContext, +) -> Result, String> { + builder.cf_loop_pattern8_bool_predicate_impl( + ctx.condition, + ctx.body, + ctx.func_name, + ctx.debug, + ) +} + +impl MirBuilder { + /// Phase 259 P0: Pattern 8 (BoolPredicateScan) implementation + pub(crate) fn cf_loop_pattern8_bool_predicate_impl( + &mut self, + condition: &ASTNode, + body: &[ASTNode], + func_name: &str, + debug: bool, + ) -> Result, String> { + use crate::mir::join_ir::lowering::join_value_space::JoinValueSpace; + use crate::mir::join_ir::lowering::JoinInlineBoundaryBuilder; + + let trace = trace::trace(); + + if debug { + trace.debug( + "pattern8/lower", + &format!("Phase 259 P0: BoolPredicateScan lowering for {}", func_name), + ); + } + + // Step 1: Extract pattern parts + let parts = extract_bool_predicate_scan_parts(condition, body)? + .ok_or_else(|| format!("[pattern8] Not a boolean predicate scan pattern in {}", func_name))?; + + if debug { + trace.debug( + "pattern8/lower", + &format!( + "Extracted: loop_var={}, haystack={}, predicate={}.{}, step={}", + parts.loop_var, parts.haystack, parts.predicate_receiver, parts.predicate_method, parts.step_lit + ), + ); + } + + // Step 2: Get host ValueIds for variables + eprintln!("[pattern8/lower] variable_map contents:"); + for (name, vid) in &self.variable_ctx.variable_map { + eprintln!("[pattern8/lower] {} -> {:?}", name, vid); + } + eprintln!("[pattern8/lower] Looking for receiver: {}", parts.predicate_receiver); + + let s_host = self + .variable_ctx + .variable_map + .get(&parts.haystack) + .copied() + .ok_or_else(|| format!("[pattern8] Variable {} not found", parts.haystack))?; + + let i_host = self + .variable_ctx + .variable_map + .get(&parts.loop_var) + .copied() + .ok_or_else(|| format!("[pattern8] Variable {} not found", parts.loop_var))?; + + if debug { + trace.debug( + "pattern8/lower", + &format!("Host ValueIds: s={:?}, i={:?}", s_host, i_host), + ); + } + + // Step 3: Create JoinModule + let mut join_value_space = JoinValueSpace::new(); + use crate::mir::join_ir::lowering::scan_bool_predicate_minimal::lower_scan_bool_predicate_minimal; + + let join_module = lower_scan_bool_predicate_minimal( + &mut join_value_space, + &parts.predicate_receiver, + &parts.predicate_method, + ); + + // Step 4: Build boundary (SSOT - use entry.params) + use super::common::get_entry_function; + let main_func = get_entry_function(&join_module, "pattern8")?; + + // SSOT: Use actual params from JoinModule entry + let join_inputs = main_func.params.clone(); + + // host_inputs in same order: [i, me, s] (alphabetical) + // Phase 259 P0: me_host = receiver ValueId + // IMPORTANT: Me receiver might not be in variable_map yet, so we use build_me_expression() + let me_host = if parts.predicate_receiver == "me" { + self.build_me_expression()? + } else { + self + .variable_ctx + .variable_map + .get(&parts.predicate_receiver) + .copied() + .ok_or_else(|| format!("[pattern8] Receiver {} not found", parts.predicate_receiver))? + }; + + let host_inputs = vec![i_host, me_host, s_host]; + + if debug { + trace.debug( + "pattern8/lower", + &format!( + "Boundary inputs: join_inputs={:?}, host_inputs={:?}", + join_inputs, host_inputs + ), + ); + } + + // Verify count consistency + if join_inputs.len() != host_inputs.len() { + return Err(format!( + "[pattern8] Params count mismatch: join_inputs={}, host_inputs={}", + join_inputs.len(), host_inputs.len() + )); + } + + // Step 5: Build exit_bindings (no carriers, only expr_result) + // Pattern8 returns ret_bool directly (not loop variable) + use crate::mir::join_ir::lowering::inline_boundary::LoopExitBinding; + + let k_exit_func = join_module.require_function( + crate::mir::join_ir::lowering::canonical_names::K_EXIT, + "Pattern 8", + ); + let join_exit_value = k_exit_func + .params + .first() + .copied() + .expect("k_exit must have parameter for exit value"); + + // Phase 259 P0: Allocate host ValueId for return value + let result_host = self.next_value_id(); + self.type_ctx.value_types.insert(result_host, crate::mir::MirType::Bool); + + // Phase 259 P0: exit_bindings contains ret_bool binding (Pattern7 style) + // This allows remapper to map join_exit_value → result_host + let result_exit_binding = LoopExitBinding { + carrier_name: "ret_bool".to_string(), // Logical name for return value + join_exit_value, + host_slot: result_host, + role: crate::mir::join_ir::lowering::carrier_info::CarrierRole::LoopState, + }; + + let exit_bindings = vec![result_exit_binding]; + + // Step 6: Build boundary with expr_result + use crate::mir::join_ir::lowering::carrier_info::CarrierInfo; + + let carrier_info = CarrierInfo { + loop_var_name: parts.loop_var.clone(), + loop_var_id: i_host, + carriers: vec![], // No carriers - Pattern8 uses expr_result only + trim_helper: None, + promoted_loopbodylocals: Vec::new(), + #[cfg(feature = "normalized_dev")] + promoted_bindings: std::collections::BTreeMap::new(), + }; + + // Phase 259 P0: expr_result = join_exit_value (Pattern7 style) + // Pattern8 returns boolean from k_exit, not loop variable + + let boundary = JoinInlineBoundaryBuilder::new() + .with_inputs(join_inputs, host_inputs) + .with_exit_bindings(exit_bindings) + .with_carrier_info(carrier_info) + .with_expr_result(Some(join_exit_value)) // ✅ CRITICAL: Set expr_result to k_exit param + .build(); + + if debug { + trace.debug( + "pattern8/lower", + "Built boundary with expr_result (ret_bool from k_exit)", + ); + } + + // Step 7: Execute JoinIRConversionPipeline + use super::conversion_pipeline::JoinIRConversionPipeline; + let result = JoinIRConversionPipeline::execute( + self, + join_module, + Some(&boundary), + "pattern8", + debug, + )?; + + eprintln!("[pattern8/lower] Pipeline execution complete, result: {:?}", result); + + if debug { + trace.debug( + "pattern8/lower", + &format!("Pattern 8 complete, result: {:?}", result), + ); + } + + // Pattern8 returns ret_bool (expr_result), not Void + eprintln!("[pattern8/lower] Returning result: {:?}", result); + Ok(result) + } +} diff --git a/src/mir/builder/control_flow/joinir/patterns/router.rs b/src/mir/builder/control_flow/joinir/patterns/router.rs index d31e6601..0a62c39c 100644 --- a/src/mir/builder/control_flow/joinir/patterns/router.rs +++ b/src/mir/builder/control_flow/joinir/patterns/router.rs @@ -203,6 +203,11 @@ pub(crate) static LOOP_PATTERNS: &[LoopPatternEntry] = &[ detect: super::pattern7_split_scan::can_lower, lower: super::pattern7_split_scan::lower, }, + LoopPatternEntry { + name: "Pattern8_BoolPredicateScan", // Phase 259 P0: boolean predicate scan (is_integer/is_valid) + detect: super::pattern8_scan_bool_predicate::can_lower, + lower: super::pattern8_scan_bool_predicate::lower, + }, LoopPatternEntry { name: "Pattern3_WithIfPhi", detect: super::pattern3_with_if_phi::can_lower, diff --git a/src/mir/join_ir/lowering/mod.rs b/src/mir/join_ir/lowering/mod.rs index bf600f81..046240bc 100644 --- a/src/mir/join_ir/lowering/mod.rs +++ b/src/mir/join_ir/lowering/mod.rs @@ -79,6 +79,7 @@ pub mod simple_while_minimal; // Phase 188-Impl-1: Pattern 1 minimal lowerer pub mod scan_with_init_minimal; // Phase 254 P1: Pattern 6 minimal lowerer (index_of/find/contains) pub mod scan_with_init_reverse; // Phase 257 P0: Pattern 6 reverse scan lowerer (last_index_of) pub mod split_scan_minimal; // Phase 256 P0: Pattern 7 minimal lowerer (split/tokenization with variable step) +pub mod scan_bool_predicate_minimal; // Phase 259 P0: Pattern 8 minimal lowerer (is_integer/is_valid boolean predicate scan) pub mod skip_ws; pub mod stage1_using_resolver; pub mod stageb_body; diff --git a/src/mir/join_ir/lowering/scan_bool_predicate_minimal.rs b/src/mir/join_ir/lowering/scan_bool_predicate_minimal.rs new file mode 100644 index 00000000..f91cc78b --- /dev/null +++ b/src/mir/join_ir/lowering/scan_bool_predicate_minimal.rs @@ -0,0 +1,252 @@ +//! Phase 259 P0: Pattern 8 (BoolPredicateScan) JoinIR Lowerer +//! +//! Target: apps/tests/phase259_p0_is_integer_min.hako +//! +//! Expected JoinIR: +//! ```text +//! fn main(i, me, s): +//! result = loop_step(i, me, s) +//! ret result +//! +//! fn loop_step(i, me, s): +//! // 1. Check exit condition: i >= s.length() +//! len = StringBox.length(s) +//! exit_cond = (i >= len) +//! const_true = true +//! Jump(k_exit, [const_true], cond=exit_cond) // All passed +//! +//! // 2. Extract character +//! const_1 = 1 +//! i_plus_1 = i + 1 +//! ch = StringBox.substring(s, i, i_plus_1) +//! +//! // 3. Call predicate (Me method → MethodCall with me receiver) +//! ok = MethodCall(me, predicate_method, [ch]) +//! +//! // 4. Check predicate result (inverted - if !ok, return false) +//! const_false = false +//! not_ok = !ok +//! Jump(k_exit, [const_false], cond=not_ok) // Predicate failed +//! +//! // 5. Tail recursion +//! tail_result = Call(loop_step, [i_plus_1, me, s]) +//! Ret(tail_result) +//! +//! fn k_exit(ret_bool): +//! ret ret_bool +//! ``` + +use crate::mir::join_ir::lowering::canonical_names as cn; +use crate::mir::join_ir::lowering::join_value_space::JoinValueSpace; +use crate::mir::join_ir::{ + CompareOp, ConstValue, JoinFuncId, JoinFunction, JoinInst, JoinModule, MirLikeInst, MirType, + UnaryOp, +}; + +/// Lower Pattern 8 (BoolPredicateScan) to JoinIR +/// +/// # Arguments +/// +/// * `join_value_space` - Unified ValueId allocator +/// * `predicate_receiver` - Receiver name (e.g., "this") +/// * `predicate_method` - Method name (e.g., "is_digit") +/// +/// # Returns +/// +/// * `JoinModule` - Successfully lowered to JoinIR +pub(crate) fn lower_scan_bool_predicate_minimal( + join_value_space: &mut JoinValueSpace, + predicate_receiver: &str, + predicate_method: &str, +) -> JoinModule { + let mut join_module = JoinModule::new(); + + // Function IDs + let main_id = JoinFuncId::new(0); + let loop_step_id = JoinFuncId::new(1); + let k_exit_id = JoinFuncId::new(2); + + // main() params/locals + let i_main_param = join_value_space.alloc_param(); + let me_main_param = join_value_space.alloc_param(); // Phase 259 P0: Me receiver param + let s_main_param = join_value_space.alloc_param(); + let loop_result = join_value_space.alloc_local(); + + // loop_step params/locals + let i_step_param = join_value_space.alloc_param(); + let me_step_param = join_value_space.alloc_param(); // Phase 259 P0: Me receiver param + let s_step_param = join_value_space.alloc_param(); + let len = join_value_space.alloc_local(); + let exit_cond = join_value_space.alloc_local(); + let const_true = join_value_space.alloc_local(); + let const_1 = join_value_space.alloc_local(); + let i_plus_1 = join_value_space.alloc_local(); + let ch = join_value_space.alloc_local(); + let ok = join_value_space.alloc_local(); + let const_false = join_value_space.alloc_local(); + let not_ok = join_value_space.alloc_local(); + + // k_exit params + let ret_bool_param = join_value_space.alloc_param(); + + // main() function + let mut main_func = JoinFunction::new( + main_id, + "main".to_string(), + vec![i_main_param, me_main_param, s_main_param], // Phase 259 P0: +me_param + ); + + main_func.body.push(JoinInst::Call { + func: loop_step_id, + args: vec![i_main_param, me_main_param, s_main_param], // Phase 259 P0: +me_param + k_next: None, + dst: Some(loop_result), + }); + + main_func.body.push(JoinInst::Ret { + value: Some(loop_result), + }); + + join_module.add_function(main_func); + + // loop_step(i, me, s) function + let mut loop_step_func = JoinFunction::new( + loop_step_id, + cn::LOOP_STEP.to_string(), + vec![i_step_param, me_step_param, s_step_param], // Phase 259 P0: +me_param + ); + + // 1. len = s.length() + loop_step_func + .body + .push(JoinInst::Compute(MirLikeInst::BoxCall { + dst: Some(len), + box_name: "StringBox".to_string(), + method: "length".to_string(), + args: vec![s_step_param], + })); + + // 2. exit_cond = (i >= len) + loop_step_func + .body + .push(JoinInst::Compute(MirLikeInst::Compare { + dst: exit_cond, + op: CompareOp::Ge, + lhs: i_step_param, + rhs: len, + })); + + // 3. const_true = true + loop_step_func + .body + .push(JoinInst::Compute(MirLikeInst::Const { + dst: const_true, + value: ConstValue::Bool(true), + })); + + // 4. Jump(k_exit, [true], cond=exit_cond) - all passed + loop_step_func.body.push(JoinInst::Jump { + cont: k_exit_id.as_cont(), + args: vec![const_true], + cond: Some(exit_cond), + }); + + // 5. const_1 = 1 + loop_step_func + .body + .push(JoinInst::Compute(MirLikeInst::Const { + dst: const_1, + value: ConstValue::Integer(1), + })); + + // 6. i_plus_1 = i + 1 + loop_step_func + .body + .push(JoinInst::Compute(MirLikeInst::BinOp { + dst: i_plus_1, + op: crate::mir::join_ir::BinOpKind::Add, + lhs: i_step_param, + rhs: const_1, + })); + + // 7. ch = s.substring(i, i_plus_1) + loop_step_func + .body + .push(JoinInst::Compute(MirLikeInst::BoxCall { + dst: Some(ch), + box_name: "StringBox".to_string(), + method: "substring".to_string(), + args: vec![s_step_param, i_step_param, i_plus_1], + })); + + // 8. ok = me.predicate_method(ch) + // P0: predicate_receiver is "this" → use MethodCall with me_step_param + // Phase 259 P0: Me receiver passed as param (by-name 禁止) + loop_step_func.body.push(JoinInst::MethodCall { + dst: ok, + receiver: me_step_param, // Me receiver as param + method: predicate_method.to_string(), + args: vec![ch], + type_hint: Some(MirType::Bool), + }); + + // 9. const_false = false + loop_step_func + .body + .push(JoinInst::Compute(MirLikeInst::Const { + dst: const_false, + value: ConstValue::Bool(false), + })); + + // 10. not_ok = !ok (invert predicate for jump condition) + loop_step_func + .body + .push(JoinInst::Compute(MirLikeInst::UnaryOp { + dst: not_ok, + op: UnaryOp::Not, + operand: ok, + })); + + // 11. Jump(k_exit, [false], cond=not_ok) - predicate failed + loop_step_func.body.push(JoinInst::Jump { + cont: k_exit_id.as_cont(), + args: vec![const_false], + cond: Some(not_ok), + }); + + // 12. Call(loop_step, [i_plus_1, me, s]) - tail recursion + // Phase 259 P0: dst: None for tail-call (bridge handles optimization) + loop_step_func.body.push(JoinInst::Call { + func: loop_step_id, + args: vec![i_plus_1, me_step_param, s_step_param], // Phase 259 P0: +me_param + k_next: None, + dst: None, // Tail-call: bridge が適切に処理 + }); + + join_module.add_function(loop_step_func); + + // k_exit(ret_bool) function + let mut k_exit_func = JoinFunction::new( + k_exit_id, + cn::K_EXIT.to_string(), + vec![ret_bool_param], + ); + + k_exit_func.body.push(JoinInst::Ret { + value: Some(ret_bool_param), + }); + + join_module.add_function(k_exit_func); + + // Set entry point + join_module.entry = Some(main_id); + + eprintln!("[joinir/pattern8] Generated JoinIR for BoolPredicateScan Pattern"); + eprintln!("[joinir/pattern8] Functions: main, loop_step, k_exit"); + eprintln!( + "[joinir/pattern8] Predicate: {}.{}", + predicate_receiver, predicate_method + ); + + join_module +} diff --git a/tools/smokes/v2/profiles/integration/apps/phase259_p0_is_integer_llvm_exe.sh b/tools/smokes/v2/profiles/integration/apps/phase259_p0_is_integer_llvm_exe.sh new file mode 100644 index 00000000..2170a301 --- /dev/null +++ b/tools/smokes/v2/profiles/integration/apps/phase259_p0_is_integer_llvm_exe.sh @@ -0,0 +1,14 @@ +#!/bin/bash +set -e +cd "$(dirname "$0")/../../.." +HAKORUNE_BIN="${HAKORUNE_BIN:-./target/release/hakorune}" +NYASH_LLVM_USE_HARNESS=1 $HAKORUNE_BIN --backend llvm apps/tests/phase259_p0_is_integer_min.hako > /tmp/phase259_llvm.txt 2>&1 +EXIT_CODE=$? +if [ $EXIT_CODE -eq 7 ]; then + echo "[PASS] phase259_p0_is_integer_llvm_exe" + exit 0 +else + echo "[FAIL] phase259_p0_is_integer_llvm_exe: expected exit 7, got $EXIT_CODE" + cat /tmp/phase259_llvm.txt + exit 1 +fi diff --git a/tools/smokes/v2/profiles/integration/apps/phase259_p0_is_integer_vm.sh b/tools/smokes/v2/profiles/integration/apps/phase259_p0_is_integer_vm.sh new file mode 100644 index 00000000..b3c09466 --- /dev/null +++ b/tools/smokes/v2/profiles/integration/apps/phase259_p0_is_integer_vm.sh @@ -0,0 +1,14 @@ +#!/bin/bash +set -e +cd "$(dirname "$0")/../../.." +HAKORUNE_BIN="${HAKORUNE_BIN:-./target/release/hakorune}" +$HAKORUNE_BIN apps/tests/phase259_p0_is_integer_min.hako > /tmp/phase259_out.txt 2>&1 +EXIT_CODE=$? +if [ $EXIT_CODE -eq 7 ]; then + echo "[PASS] phase259_p0_is_integer_vm" + exit 0 +else + echo "[FAIL] phase259_p0_is_integer_vm: expected exit 7, got $EXIT_CODE" + cat /tmp/phase259_out.txt + exit 1 +fi