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