diff --git a/apps/tests/phase29aq_string_index_of_string_min.hako b/apps/tests/phase29aq_string_index_of_string_min.hako
new file mode 100644
index 00000000..8b899715
--- /dev/null
+++ b/apps/tests/phase29aq_string_index_of_string_min.hako
@@ -0,0 +1,3 @@
+using "apps/lib/json_native/utils/string.hako" as StringUtils
+
+print(StringUtils.index_of_string("hello", "ll"))
diff --git a/apps/tests/phase29aq_string_to_upper_min.hako b/apps/tests/phase29aq_string_to_upper_min.hako
new file mode 100644
index 00000000..154239aa
--- /dev/null
+++ b/apps/tests/phase29aq_string_to_upper_min.hako
@@ -0,0 +1,3 @@
+using "apps/lib/json_native/utils/string.hako" as StringUtils
+
+print(StringUtils.to_upper("abc"))
diff --git a/docs/development/current/main/10-Now.md b/docs/development/current/main/10-Now.md
index 0f2bbf1e..c1f015ed 100644
--- a/docs/development/current/main/10-Now.md
+++ b/docs/development/current/main/10-Now.md
@@ -3,7 +3,7 @@
## Current Focus
- Phase: `docs/development/current/main/phases/phase-29aq/README.md`
-- Next: Phase 29aq P2 (stdlib scan subset extensions)
+- Next: Phase 29aq P3 (stdlib split/scan derivatives)
## Gate (SSOT)
diff --git a/docs/development/current/main/30-Backlog.md b/docs/development/current/main/30-Backlog.md
index 5ffa5bd3..36858fe0 100644
--- a/docs/development/current/main/30-Backlog.md
+++ b/docs/development/current/main/30-Backlog.md
@@ -5,7 +5,7 @@ Scope: 「次にやる候補」を短く列挙するメモ。入口は `docs/dev
## Active
-- Phase 29aq: `docs/development/current/main/phases/phase-29aq/README.md` (Next: P2 scan subsets)
+- Phase 29aq: `docs/development/current/main/phases/phase-29aq/README.md` (Next: P3 split/scan derivatives)
- JoinIR regression gate SSOT: `docs/development/current/main/phases/phase-29ae/README.md`
- CorePlan hardening (docs-first): `docs/development/current/main/phases/phase-29al/README.md`
diff --git a/docs/development/current/main/design/coreplan-migration-roadmap-ssot.md b/docs/development/current/main/design/coreplan-migration-roadmap-ssot.md
index a92d8ea7..1f21189e 100644
--- a/docs/development/current/main/design/coreplan-migration-roadmap-ssot.md
+++ b/docs/development/current/main/design/coreplan-migration-roadmap-ssot.md
@@ -34,7 +34,7 @@ Related:
## 1.1 Current (active)
- Active phase: `docs/development/current/main/phases/phase-29aq/README.md`
-- Next step: Phase 29aq P2 (stdlib scan subset extensions)
+- Next step: Phase 29aq P3 (stdlib split/scan derivatives)
## 2. すでに固めた SSOT(再発防止の土台)
diff --git a/docs/development/current/main/phases/phase-29ae/README.md b/docs/development/current/main/phases/phase-29ae/README.md
index dd5ec09b..149d7bc0 100644
--- a/docs/development/current/main/phases/phase-29ae/README.md
+++ b/docs/development/current/main/phases/phase-29ae/README.md
@@ -17,8 +17,10 @@ Goal: JoinIR の最小回帰セットを SSOT として固定する。
- Pattern1 (stdlib join, VM): `phase29ap_stringutils_join_vm`
- ScanWithInit (stdlib index_of, VM): `phase29aq_string_index_of_min_vm`
- ScanWithInit (stdlib last_index_of, VM): `phase29aq_string_last_index_of_min_vm`
+- ScanWithInit (stdlib index_of_string, VM): `phase29aq_string_index_of_string_min_vm`
- Pattern2 (stdlib parse_integer, VM): `phase29aq_string_parse_integer_min_vm`
- SplitScan (stdlib split, VM): `phase29aq_string_split_min_vm`
+- Pattern1 (stdlib to_upper, VM): `phase29aq_string_to_upper_min_vm`
- Pattern5 (Break, VM): `phase286_pattern5_break_vm`
- Pattern5 (strict shadow, VM): `phase29ao_pattern5_strict_shadow_vm`
- Pattern5 (release adopt, VM): `phase29ao_pattern5_release_adopt_vm`
diff --git a/docs/development/current/main/phases/phase-29aq/P2-INDEXOFSTRING-TOUPPER-SUBSET-INSTRUCTIONS.md b/docs/development/current/main/phases/phase-29aq/P2-INDEXOFSTRING-TOUPPER-SUBSET-INSTRUCTIONS.md
new file mode 100644
index 00000000..6724b9d6
--- /dev/null
+++ b/docs/development/current/main/phases/phase-29aq/P2-INDEXOFSTRING-TOUPPER-SUBSET-INSTRUCTIONS.md
@@ -0,0 +1,58 @@
+---
+Status: Done
+Scope: stdlib subsets (index_of_string, to_upper)
+Related:
+- docs/development/current/main/phases/phase-29aq/README.md
+- docs/development/current/main/phases/phase-29ae/README.md
+---
+
+# Phase 29aq P2: stdlib subsets (index_of_string, to_upper)
+
+Goal: add two stdlib subsets with fixtures and integration smokes, wired to the
+JoinIR regression gate (phase29ae pack).
+
+## P2-1: to_upper (Pattern1CharMap)
+
+Target: `apps/lib/json_native/utils/string.hako`
+
+- loop(i < s.length())
+- local ch = s.substring(i, i + 1)
+- result = result + this.char_to_upper(ch)
+- i = i + 1
+
+Notes:
+- Use existing Pattern1CharMap facts/planner/normalizer path.
+- No new CorePlan vocabulary or logs.
+
+Fixtures/smokes:
+- `apps/tests/phase29aq_string_to_upper_min.hako`
+- `tools/smokes/v2/profiles/integration/joinir/phase29aq_string_to_upper_min_vm.sh`
+
+## P2-2: index_of_string (ScanWithInit dynamic needle)
+
+Target: `apps/lib/json_native/utils/string.hako`
+
+- loop(i <= s.length() - substr.length())
+- if s.substring(i, i + substr.length()) == substr { return i }
+- i = i + 1
+- return -1
+
+Notes:
+- Treat as ScanWithInit with dynamic needle length.
+- Facts must detect the dynamic needle length and forward scan shape.
+
+Fixtures/smokes:
+- `apps/tests/phase29aq_string_index_of_string_min.hako`
+- `tools/smokes/v2/profiles/integration/joinir/phase29aq_string_index_of_string_min_vm.sh`
+
+## Gate wiring (SSOT)
+
+- Add both smokes to `tools/smokes/v2/profiles/integration/joinir/phase29aq_stdlib_pack_vm.sh`.
+- Ensure `phase29ae_regression_pack_vm.sh` runs the stdlib pack.
+- Update `docs/development/current/main/phases/phase-29ae/README.md`.
+
+## Verification
+
+- `cargo build --release`
+- `./tools/smokes/v2/run.sh --profile quick`
+- `./tools/smokes/v2/profiles/integration/joinir/phase29ae_regression_pack_vm.sh`
diff --git a/docs/development/current/main/phases/phase-29aq/README.md b/docs/development/current/main/phases/phase-29aq/README.md
index 36b20730..61bd5a09 100644
--- a/docs/development/current/main/phases/phase-29aq/README.md
+++ b/docs/development/current/main/phases/phase-29aq/README.md
@@ -57,7 +57,8 @@ Plan/Composer subsets (or mark unsupported) before adding new subsets.
## Progress
- P1: Add stdlib subsets in priority order (index_of/last_index_of → parse_integer → split).
+- P2: Add stdlib subsets (index_of_string → to_upper).
## Next (planned)
-- P2: Extend stdlib scan subsets (candidate: index_of_string, to_upper).
+- P3: Expand split/scan derivatives (candidate: starts_with/ends_with).
diff --git a/src/mir/builder/control_flow/plan/composer/coreloop_v0.rs b/src/mir/builder/control_flow/plan/composer/coreloop_v0.rs
index 4a3494cf..3fb76cd8 100644
--- a/src/mir/builder/control_flow/plan/composer/coreloop_v0.rs
+++ b/src/mir/builder/control_flow/plan/composer/coreloop_v0.rs
@@ -51,9 +51,15 @@ pub(super) fn try_compose_core_loop_v0_scan_with_init(
Some(haystack) => haystack == &scan.haystack,
None => true,
};
+ let needle_matches = match shape.needle_var.as_ref() {
+ Some(needle) => needle == &scan.needle,
+ None => true,
+ };
shape.idx_var == scan.loop_var
&& shape.step_lit == scan.step_lit
+ && shape.dynamic_needle == scan.dynamic_needle
&& haystack_matches
+ && needle_matches
});
if !shapes_match {
return Ok(None);
@@ -70,7 +76,7 @@ pub(super) fn try_compose_core_loop_v0_scan_with_init(
},
not_found_return_lit: -1,
scan_direction,
- dynamic_needle: false,
+ dynamic_needle: scan.dynamic_needle,
};
let core = PlanNormalizer::normalize_scan_with_init(builder, plan, ctx)?;
Ok(Some(core))
@@ -409,6 +415,7 @@ mod tests {
haystack: "s".to_string(),
needle: "ch".to_string(),
step_lit: 1,
+ dynamic_needle: false,
}),
split_scan: None,
pattern1_simplewhile: None,
@@ -477,6 +484,7 @@ mod tests {
haystack: "s".to_string(),
needle: "ch".to_string(),
step_lit: 1,
+ dynamic_needle: false,
}),
split_scan: None,
pattern1_simplewhile: None,
@@ -544,6 +552,7 @@ mod tests {
haystack: "s".to_string(),
needle: "ch".to_string(),
step_lit: 1,
+ dynamic_needle: false,
}),
split_scan: None,
pattern1_simplewhile: None,
diff --git a/src/mir/builder/control_flow/plan/composer/coreloop_v1.rs b/src/mir/builder/control_flow/plan/composer/coreloop_v1.rs
index 05ff6ce9..e016ba6e 100644
--- a/src/mir/builder/control_flow/plan/composer/coreloop_v1.rs
+++ b/src/mir/builder/control_flow/plan/composer/coreloop_v1.rs
@@ -384,6 +384,7 @@ mod tests {
haystack: "s".to_string(),
needle: "ch".to_string(),
step_lit: 1,
+ dynamic_needle: false,
}),
split_scan: None,
pattern1_simplewhile: None,
diff --git a/src/mir/builder/control_flow/plan/facts/loop_facts.rs b/src/mir/builder/control_flow/plan/facts/loop_facts.rs
index c3c4fe63..9f666e56 100644
--- a/src/mir/builder/control_flow/plan/facts/loop_facts.rs
+++ b/src/mir/builder/control_flow/plan/facts/loop_facts.rs
@@ -77,6 +77,7 @@ pub(in crate::mir::builder) struct ScanWithInitFacts {
pub haystack: String,
pub needle: String,
pub step_lit: i64,
+ pub dynamic_needle: bool,
}
#[derive(Debug, Clone)]
@@ -224,37 +225,46 @@ fn try_extract_condition_shape(condition: &ASTNode) -> Result