feat(joinir): Phase 171-C-2 Trim pattern detection in LoopBodyCarrierPromoter

Implements the Trim pattern detection logic for carrier promotion:

- find_definition_in_body(): Iterative AST traversal to locate variable definitions
- is_substring_method_call(): Detects substring() method calls
- extract_equality_literals(): Extracts string literals from OR chains (ch == " " || ch == "\t")
- TrimPatternInfo: Captures detected pattern details for carrier promotion

This enables Pattern 5 to detect trim-style loops:
```hako
loop(start < end) {
    local ch = s.substring(start, start+1)
    if ch == " " || ch == "\t" || ch == "\n" || ch == "\r" {
        start = start + 1
    } else {
        break
    }
}
```

Unit tests cover:
- Simple and nested definition detection
- substring method call detection
- Single and chained equality literal extraction
- Full Trim pattern detection with 2-4 whitespace characters

Next: Phase 171-C-3 integration with Pattern 2/4 routing

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
nyash-codex
2025-12-07 23:09:25 +09:00
parent 907a54b55c
commit 88400e7e22
11 changed files with 2334 additions and 4668 deletions

View File

@ -130,8 +130,17 @@ pub fn is_outer_scope_variable(
scope: Option<&LoopScopeShape>,
) -> bool {
match scope {
None => false, // No scope info → assume body-local
// No scope information: be conservative but *not* overstrict.
// We treat unknown as body-local only when we have a LoopScopeShape
// that explicitly marks it so (via body_locals / definitions).
// Here we simply say "unknown" and let the caller decide.
None => false,
Some(scope) => {
// If the variable is explicitly marked as body-local, it is NOT outer.
if scope.body_locals.contains(var_name) {
return false;
}
// Check 1: Is it a pinned variable (loop parameter or passed-in)?
if scope.pinned.contains(var_name) {
return true;
@ -151,15 +160,27 @@ pub fn is_outer_scope_variable(
// This supports loop patterns like:
// local i = 0 (header)
// loop(i < 10) {
// ...
// i = i + 1 (latch)
// ...
// i = i + 1 (latch)
// }
if def_blocks.iter().all(|b| *b == scope.header || *b == scope.latch) {
return true;
}
// Any other definition pattern (e.g. body-only or body+header)
// is treated as body-local / internal.
return false;
}
false
// At this point:
// - The variable is NOT in body_locals
// - There is no explicit definition info for it
//
// This typically means "function parameter" or "outer local"
// (e.g. JsonParserBox.s, .pos, etc.). Those should be treated
// as OuterLocal for condition analysis, otherwise we wrongly
// block valid loops as using loop-body-local variables.
true
}
}
}
@ -338,6 +359,34 @@ mod tests {
assert!(!is_outer_scope_variable("ch", Some(&scope)));
}
#[test]
fn test_is_outer_scope_variable_function_param_like() {
// Variables that are *not* marked as body_locals and have no explicit
// variable_definitions entry represent things like function parameters
// or outer locals. These must be treated as OuterLocal so that valid
// conditions such as `p < s.length()` (with `s` a parameter) are
// accepted by Pattern 2/4.
use std::collections::{BTreeMap, BTreeSet};
let scope = LoopScopeShape {
header: BasicBlockId(0),
body: BasicBlockId(1),
latch: BasicBlockId(2),
exit: BasicBlockId(3),
pinned: BTreeSet::new(),
carriers: BTreeSet::new(),
body_locals: BTreeSet::new(),
exit_live: BTreeSet::new(),
progress_carrier: None,
variable_definitions: BTreeMap::new(),
};
assert!(
is_outer_scope_variable("s", Some(&scope)),
"Function parameterlike variable should be classified as OuterLocal"
);
}
// ========================================================================
// Phase 170-ultrathink: Additional Edge Case Tests (Issue #3)
// ========================================================================