diff --git a/src/mir/builder/control_flow/joinir/mod.rs b/src/mir/builder/control_flow/joinir/mod.rs
index 7f64b44c..bb56a6ac 100644
--- a/src/mir/builder/control_flow/joinir/mod.rs
+++ b/src/mir/builder/control_flow/joinir/mod.rs
@@ -2,6 +2,7 @@
//!
//! This module contains JoinIR-related control flow logic:
//! - Pattern lowerers (patterns/)
-//! - Routing logic (future Phase 3)
+//! - Routing logic (routing.rs) ✅
pub(in crate::mir::builder) mod patterns;
+pub(in crate::mir::builder) mod routing;
diff --git a/src/mir/builder/control_flow/joinir/routing.rs b/src/mir/builder/control_flow/joinir/routing.rs
new file mode 100644
index 00000000..cb6f673d
--- /dev/null
+++ b/src/mir/builder/control_flow/joinir/routing.rs
@@ -0,0 +1,343 @@
+//! JoinIR routing logic for loop lowering
+
+use crate::ast::ASTNode;
+use crate::mir::builder::MirBuilder;
+use crate::mir::ValueId;
+
+impl MirBuilder {
+ /// Phase 49: Try JoinIR Frontend for mainline integration
+ ///
+ /// Returns `Ok(Some(value))` if the current function should use JoinIR Frontend,
+ /// `Ok(None)` to fall through to the legacy LoopBuilder path.
+ ///
+ /// # Phase 49-4: Multi-target support
+ ///
+ /// Targets are enabled via separate dev flags:
+ /// - `HAKO_JOINIR_PRINT_TOKENS_MAIN=1`: JsonTokenizer.print_tokens/0
+ /// - `HAKO_JOINIR_ARRAY_FILTER_MAIN=1`: ArrayExtBox.filter/2
+ ///
+ /// Note: Arity in function names does NOT include implicit `me` receiver.
+ /// - Instance method `print_tokens()` → `/0` (no explicit params)
+ /// - Static method `filter(arr, pred)` → `/2` (two params)
+ pub(in crate::mir::builder) fn try_cf_loop_joinir(
+ &mut self,
+ condition: &ASTNode,
+ body: &[ASTNode],
+ ) -> Result