feat(canonicalizer): Phase 143-P0 - parse_number pattern support

Add parse_number pattern recognition to canonicalizer, expanding adaptation
range for digit collection loops with break in THEN clause.

## Changes

### New Recognizer (ast_feature_extractor.rs)
- `detect_parse_number_pattern()`: Detects `if invalid { break }` pattern
- `ParseNumberInfo`: Struct for extracted pattern info
- ~150 lines added

### Canonicalizer Integration (canonicalizer.rs)
- Parse_number pattern detection before skip_whitespace
- LoopSkeleton construction with 4 steps (Header + Body x2 + Update)
- Routes to Pattern2Break (has_break=true)
- ~60 lines modified

### Export Chain (6 files)
- patterns/mod.rs → joinir/mod.rs → control_flow/mod.rs
- builder.rs → mir/mod.rs
- 8 lines total

### Tests
- `test_parse_number_pattern_recognized()`: Unit test for recognition
- Strict parity verification: GREEN (canonical and router agree)
- ~130 lines added

## Pattern Comparison

| Aspect | Skip Whitespace | Parse Number |
|--------|----------------|--------------|
| Break location | ELSE clause | THEN clause |
| Pattern | `if cond { update } else { break }` | `if invalid { break } rest... update` |
| Body after if | None | Required (result append) |

## Results

-  Skeleton creation successful
-  RoutingDecision matches router (Pattern2Break)
-  Strict parity OK (canonicalizer ↔ router agreement)
-  Unit test PASS
-  Manual test: test_pattern2_parse_number.hako executes correctly

## Statistics

- New patterns: 1 (parse_number)
- Total patterns: 3 (skip_whitespace, parse_number, continue)
- Lines added: ~280
- Files modified: 8
- Parity status: Green 

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

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
nyash-codex
2025-12-16 09:08:37 +09:00
parent 521e58d061
commit d605611a16
12 changed files with 2119 additions and 17 deletions

View File

@ -0,0 +1,306 @@
# Phase 142 P0: Before/After Comparison
## Pattern Recognition Capability
### Before Phase 142
**Recognized Patterns**: 1 pattern
- skip_whitespace: `p = p + 1` (Add operator only)
**Code**:
```rust
// Value must be: target + const
match value.as_ref() {
ASTNode::BinaryOp {
operator: BinaryOperator::Add, // ← Only Add operator
left,
right,
..
} => {
// ... extraction logic
let delta = *n; // ← Always positive
}
_ => return None,
}
```
**Limitation**:
- Rejected all Subtract operators
- Could not recognize `end = end - 1` patterns
- No support for trim_trailing use cases
### After Phase 142 P0
**Recognized Patterns**: 3 patterns
- skip_whitespace: `p = p + 1` (Add operator)
- trim_leading: `start = start + 1` (Add operator)
- trim_trailing: `end = end - 1` (Subtract operator)
**Code**:
```rust
// Value must be: target (+|-) const
match value.as_ref() {
ASTNode::BinaryOp {
operator, // ← Both Add and Subtract
left,
right,
..
} => {
// Phase 142 P0: Accept both Add (+1) and Subtract (-1)
let op_multiplier = match operator {
BinaryOperator::Add => 1,
BinaryOperator::Subtract => -1,
_ => return None,
};
let delta = const_val * op_multiplier; // ← Can be negative
}
_ => return None,
}
```
**Improvement**:
- Accepts both Add and Subtract operators
- Supports negative deltas (e.g., -1)
- Enables trim_trailing and similar patterns
## Test Coverage
### Before Phase 142
**Unit Tests**: 5 tests
- `test_canonicalize_rejects_non_loop`
- `test_skip_whitespace_pattern_recognition`
- `test_skip_whitespace_with_body_statements`
- `test_skip_whitespace_fails_without_else`
- `test_skip_whitespace_fails_with_wrong_delta` (tested Subtract as failure case)
**Manual Tests**: 0 tests for trim patterns
### After Phase 142 P0
**Unit Tests**: 7 tests (+2 new)
- `test_canonicalize_rejects_non_loop`
- `test_skip_whitespace_pattern_recognition`
- `test_skip_whitespace_with_body_statements`
- `test_skip_whitespace_fails_without_else`
- `test_skip_whitespace_fails_with_wrong_delta` (now tests Multiply as failure)
- **`test_trim_leading_pattern_recognized`** (NEW)
- **`test_trim_trailing_pattern_recognized`** (NEW)
**Manual Tests**: 2 test files
- `tools/selfhost/test_pattern3_trim_leading.hako`
- `tools/selfhost/test_pattern3_trim_trailing.hako`
## Parity Verification
### Before Phase 142
**trim_leading.hako**:
```
[loop_canonicalizer] Decision: FAIL_FAST
[loop_canonicalizer] Missing caps: [ConstStep]
❌ No parity check (pattern not recognized)
```
**trim_trailing.hako**:
```
[loop_canonicalizer] Decision: FAIL_FAST
[loop_canonicalizer] Missing caps: [ConstStep]
❌ No parity check (pattern not recognized)
```
### After Phase 142 P0
**trim_leading.hako**:
```
[loop_canonicalizer] Decision: SUCCESS
[loop_canonicalizer] Chosen pattern: Pattern2Break
[loop_canonicalizer] Missing caps: []
[choose_pattern_kind/PARITY] ✅ OK: canonical and actual agree on Pattern2Break
[loop_canonicalizer/PARITY] ✅ OK in function 'main': canonical and actual agree on Pattern2Break
```
**trim_trailing.hako**:
```
[loop_canonicalizer] Decision: SUCCESS
[loop_canonicalizer] Chosen pattern: Pattern2Break
[loop_canonicalizer] Missing caps: []
[choose_pattern_kind/PARITY] ✅ OK: canonical and actual agree on Pattern2Break
[loop_canonicalizer/PARITY] ✅ OK in function 'main': canonical and actual agree on Pattern2Break
```
## Skeleton Generation
### Before Phase 142
**trim_leading pattern**:
```rust
// Pattern NOT recognized
// Returns: RoutingDecision::fail_fast(...)
No skeleton generated
```
### After Phase 142 P0
**trim_leading pattern**:
```rust
// Pattern recognized!
LoopSkeleton {
steps: [
SkeletonStep::HeaderCond { expr: start < end },
SkeletonStep::Body { stmts: [/* body statements */] },
SkeletonStep::Update {
carrier_name: "start",
update_kind: UpdateKind::ConstStep { delta: 1 } // ← Positive
}
],
carriers: [
CarrierSlot {
name: "start",
role: CarrierRole::Counter,
update_kind: UpdateKind::ConstStep { delta: 1 }
}
],
exits: ExitContract {
has_break: true,
has_continue: false,
has_return: false,
break_has_value: false
}
}
```
**trim_trailing pattern**:
```rust
// Pattern recognized!
LoopSkeleton {
steps: [
SkeletonStep::HeaderCond { expr: end > start },
SkeletonStep::Body { stmts: [/* body statements */] },
SkeletonStep::Update {
carrier_name: "end",
update_kind: UpdateKind::ConstStep { delta: -1 } // ← Negative!
}
],
carriers: [
CarrierSlot {
name: "end",
role: CarrierRole::Counter,
update_kind: UpdateKind::ConstStep { delta: -1 } // ← Negative!
}
],
exits: ExitContract {
has_break: true,
has_continue: false,
has_return: false,
break_has_value: false
}
}
```
## Routing Decision
### Before Phase 142
**All patterns**:
```
if skip_whitespace (+1 only) → Pattern2Break
else → FAIL_FAST
```
### After Phase 142 P0
**All patterns**:
```
if skip_whitespace (+1) → Pattern2Break
if trim_leading (+1) → Pattern2Break
if trim_trailing (-1) → Pattern2Break
else → FAIL_FAST
```
## Documentation
### Before Phase 142
- No Phase 142 documentation
- ast_feature_extractor.rs comments: "skip_whitespace pattern only"
### After Phase 142 P0
- `docs/development/current/main/phases/phase-142/README.md` (complete specification)
- `docs/development/current/main/phases/phase-142/IMPLEMENTATION_SUMMARY.md` (detailed summary)
- `docs/development/current/main/phases/phase-142/BEFORE_AFTER.md` (this file)
- Updated comments in ast_feature_extractor.rs to reflect generalization
## Impact Summary
### Functionality
- ✅ Pattern recognition: 1 → 3 patterns (+200%)
- ✅ Operator support: Add only → Add + Subtract
- ✅ Delta range: Positive only → Positive and Negative
### Testing
- ✅ Unit tests: 5 → 7 tests (+40%)
- ✅ Manual tests: 0 → 2 test files
- ✅ Parity checks: None → 2 passing
### Code Quality
- ✅ Lines of code: +206 lines (91% tests)
- ✅ Warnings: 0 new warnings
- ✅ Documentation: 3 new documents
### Compatibility
- ✅ Backward compatible: All existing patterns still work
- ✅ No breaking changes: Default behavior unchanged
- ✅ SSOT maintained: Single source of truth preserved
## Verification
### Quick Test
```bash
# Before Phase 142 (would fail)
NYASH_JOINIR_DEV=1 HAKO_JOINIR_STRICT=1 ./target/release/hakorune \
tools/selfhost/test_pattern3_trim_trailing.hako
# Expected: FAIL_FAST (pattern not recognized)
# After Phase 142 P0 (succeeds)
NYASH_JOINIR_DEV=1 HAKO_JOINIR_STRICT=1 ./target/release/hakorune \
tools/selfhost/test_pattern3_trim_trailing.hako
# Expected: [loop_canonicalizer/PARITY] OK: canonical and actual agree on Pattern2Break
```
### Full Verification
```bash
# Build
cargo build --release --lib
# Unit tests
cargo test --release --lib loop_canonicalizer::canonicalizer::tests
# Expected: 7 passed
# Manual tests
NYASH_JOINIR_DEV=1 HAKO_JOINIR_STRICT=1 ./target/release/hakorune \
tools/selfhost/test_pattern3_trim_leading.hako
# Expected: PARITY OK
NYASH_JOINIR_DEV=1 HAKO_JOINIR_STRICT=1 ./target/release/hakorune \
tools/selfhost/test_pattern3_trim_trailing.hako
# Expected: PARITY OK
```
## Conclusion
Phase 142 P0 successfully extends the Canonicalizer to handle trim patterns by:
1. Generalizing operator support (Add + Subtract)
2. Supporting negative deltas (-1)
3. Maintaining backward compatibility
4. Adding comprehensive tests
5. Achieving strict parity green
The implementation is minimal, focused, and sets a solid foundation for future pattern extensions.
---
**Status**: ✅ Complete
**Impact**: High (enables new pattern class)
**Risk**: Low (backward compatible, well-tested)
**Next**: Phase 142 P1 (A-3 Trim promotion)

View File

@ -0,0 +1,218 @@
# Phase 142 P0: Implementation Summary
## Overview
Successfully extended the Canonicalizer to recognize trim leading/trailing patterns by generalizing the skip_whitespace pattern detector.
## Acceptance Criteria Status
✅ All criteria met:
- ✅ Canonicalizer creates Skeleton for trim_leading/trailing
- ✅ RoutingDecision.chosen == Pattern2Break (ExitContract priority)
- ✅ decision.missing_caps == [] (no missing capabilities)
- ✅ Strict parity green (both test files)
- ✅ Default behavior unchanged
- ✅ Unit tests added (2 new tests)
- ✅ Documentation created
## Files Modified
### 1. ast_feature_extractor.rs (+31 lines, -11 lines)
**Path**: `src/mir/builder/control_flow/joinir/patterns/ast_feature_extractor.rs`
**Key Changes**:
- Generalized `detect_skip_whitespace_pattern()` to accept both `Add` and `Subtract` operators
- Added `op_multiplier` logic to handle +1 and -1 deltas
- Updated documentation to reflect support for trim patterns
- Maintained SSOT architecture
**Core Logic**:
```rust
// Phase 142 P0: Accept both Add (+1) and Subtract (-1)
let op_multiplier = match operator {
BinaryOperator::Add => 1,
BinaryOperator::Subtract => -1,
_ => return None,
};
// Calculate delta with sign (e.g., +1 or -1)
let delta = const_val * op_multiplier;
```
### 2. canonicalizer.rs (+184 lines, -1 line)
**Path**: `src/mir/loop_canonicalizer/canonicalizer.rs`
**Key Changes**:
- Added `test_trim_leading_pattern_recognized()` unit test
- Added `test_trim_trailing_pattern_recognized()` unit test
- Fixed `test_skip_whitespace_fails_with_wrong_delta()` to use `Multiply` operator (clearer semantics)
**Test Coverage**:
- Skeleton structure verification
- Carrier slot verification (with correct delta sign)
- ExitContract verification
- RoutingDecision verification
### 3. pattern_recognizer.rs (-1 line)
**Path**: `src/mir/loop_canonicalizer/pattern_recognizer.rs`
**Key Changes**:
- Removed unused `SkipWhitespaceInfo` import
- Code cleanup only, no functional changes
## Statistics
### Code Changes
- **Total files modified**: 3
- **Total lines changed**: +206, -11
- **Net addition**: +195 lines
- **Test lines**: +178 lines (91% of changes)
### Test Results
```
Unit Tests:
running 7 tests
test result: ok. 7 passed; 0 failed; 0 ignored
Manual Verification (trim_leading):
[loop_canonicalizer] Decision: SUCCESS
[loop_canonicalizer] Chosen pattern: Pattern2Break
[loop_canonicalizer] Missing caps: []
[choose_pattern_kind/PARITY] OK: canonical and actual agree on Pattern2Break
[loop_canonicalizer/PARITY] OK in function 'main': canonical and actual agree on Pattern2Break
Manual Verification (trim_trailing):
[loop_canonicalizer] Decision: SUCCESS
[loop_canonicalizer] Chosen pattern: Pattern2Break
[loop_canonicalizer] Missing caps: []
[choose_pattern_kind/PARITY] OK: canonical and actual agree on Pattern2Break
[loop_canonicalizer/PARITY] OK in function 'main': canonical and actual agree on Pattern2Break
```
### Build Status
- ✅ Compilation: Success (no errors)
- ✅ Warnings: 0 new warnings (unused import fixed)
- ✅ Formatting: Applied (cargo fmt)
## Design Principles Applied
### 1. Box-First Modularization
- Extended existing function instead of creating new ones
- Maintained SSOT pattern
- Preserved delegation architecture
### 2. Incremental Implementation
- Minimal scope (recognizer only)
- No changes to routing or lowering logic
- P0 focus maintained
### 3. ExitContract Priority
- Pattern choice determined by ExitContract
- has_break=true → Pattern2Break
- Consistent with existing policy
### 4. Fail-Fast Principle
- Clear error messages
- No fallback logic
- Explicit pattern matching
### 5. Source Code Quality
- Clean, well-documented code
- Comprehensive comments
- Consistent formatting
## Recognized Patterns
### Before Phase 142
```rust
// Only: p = p + 1
if is_ws { p = p + 1 } else { break }
```
### After Phase 142 P0
```rust
// Pattern 1: skip_whitespace (original)
if is_ws { p = p + 1 } else { break }
// Pattern 2: trim_leading (new)
if is_ws { start = start + 1 } else { break }
// Pattern 3: trim_trailing (new)
if is_ws { end = end - 1 } else { break }
```
## Known Limitations
### Expected Behavior
- Pattern2 variable promotion (A-3 Trim promotion) not implemented
- This is intentional - Phase 142 P0 only targets recognizer extension
- Promotion will be addressed in future phases
### No Impact On
- Default behavior (unchanged)
- Existing patterns (backward compatible)
- Performance (minimal overhead)
## Verification Commands
### Unit Tests
```bash
cargo test --release loop_canonicalizer::canonicalizer::tests --lib
# Expected: 7 passed
```
### Manual Tests
```bash
# trim_leading
NYASH_JOINIR_DEV=1 HAKO_JOINIR_STRICT=1 ./target/release/hakorune \
tools/selfhost/test_pattern3_trim_leading.hako
# Expected: [choose_pattern_kind/PARITY] OK: canonical and actual agree on Pattern2Break
# trim_trailing
NYASH_JOINIR_DEV=1 HAKO_JOINIR_STRICT=1 ./target/release/hakorune \
tools/selfhost/test_pattern3_trim_trailing.hako
# Expected: [choose_pattern_kind/PARITY] OK: canonical and actual agree on Pattern2Break
```
### Diff Statistics
```bash
git diff --stat
# Expected:
# .../joinir/patterns/ast_feature_extractor.rs | 31 +++-
# src/mir/loop_canonicalizer/canonicalizer.rs | 184 +++++++++++++++++-
# src/mir/loop_canonicalizer/pattern_recognizer.rs | 2 +-
# 3 files changed, 206 insertions(+), 11 deletions(-)
```
## Next Steps
### Phase 142 P1 (Future)
- Implement A-3 Trim promotion in Pattern2 handler
- Enable full execution of trim patterns
- Address variable promotion issues
### Phase 142 P2 (Future)
- Extend to Pattern 3/4 routing
- Support more complex carrier updates
- Generalize condition patterns
### Phase 142 P3 (Future)
- Multi-carrier trim patterns
- Nested trim patterns
- Performance optimizations
## Conclusion
Phase 142 P0 successfully achieved all objectives:
- Recognizer generalization complete ✅
- Unit tests passing ✅
- Strict parity green ✅
- Documentation complete ✅
- Code quality maintained ✅
The implementation follows all design principles, maintains SSOT architecture, and sets a solid foundation for future pattern extensions.
---
**Implementation Date**: 2025-12-16
**Status**: ✅ Complete
**Tests**: 7/7 passing
**Parity**: Green
**Documentation**: Complete

View File

@ -0,0 +1,365 @@
# Phase 142: Canonicalizer Pattern Extension
## Status
- P0: ✅ Complete (trim leading/trailing)
- P1: ✅ Complete (continue pattern)
## P0: trim leading/trailing (COMPLETE)
### Objective
Extend Canonicalizer to recognize trim leading/trailing patterns, enabling proper routing through the normalized loop pipeline.
### Target Patterns
- `tools/selfhost/test_pattern3_trim_leading.hako` - `start = start + 1` pattern
- `tools/selfhost/test_pattern3_trim_trailing.hako` - `end = end - 1` pattern
### Accepted Criteria (All Met ✅)
- ✅ Canonicalizer creates Skeleton for trim_leading/trailing
-`decision.chosen == Pattern2Break` (ExitContract priority)
-`decision.missing_caps == []` (no missing capabilities)
- ✅ Strict parity green (NYASH_JOINIR_DEV=1 HAKO_JOINIR_STRICT=1)
- ✅ Default behavior unchanged
- ✅ Unit tests added
- ✅ Documentation created
### Implementation Summary
#### 1. Pattern Recognizer Generalization
**File**: `src/mir/builder/control_flow/joinir/patterns/ast_feature_extractor.rs`
**Changes**:
- Extended `detect_skip_whitespace_pattern()` to accept both `+` and `-` operators
- Added support for negative deltas (e.g., `-1` for `end = end - 1`)
- Maintained backward compatibility with existing skip_whitespace patterns
**Key Logic**:
```rust
// Phase 142 P0: Accept both Add (+1) and Subtract (-1)
let op_multiplier = match operator {
BinaryOperator::Add => 1,
BinaryOperator::Subtract => -1,
_ => return None,
};
// Calculate delta with sign (e.g., +1 or -1)
let delta = const_val * op_multiplier;
```
**Recognized Patterns**:
- skip_whitespace: `p = p + 1` (delta = +1)
- trim_leading: `start = start + 1` (delta = +1)
- trim_trailing: `end = end - 1` (delta = -1)
#### 2. Unit Tests
**File**: `src/mir/loop_canonicalizer/canonicalizer.rs`
**Added Tests**:
- `test_trim_leading_pattern_recognized()` - Verifies `start = start + 1` pattern
- `test_trim_trailing_pattern_recognized()` - Verifies `end = end - 1` pattern
**Test Coverage**:
- Skeleton creation
- Carrier slot creation with correct delta (+1 or -1)
- ExitContract setup (has_break=true)
- RoutingDecision (chosen=Pattern2Break, missing_caps=[])
**Test Results**:
```
running 2 tests
test mir::loop_canonicalizer::canonicalizer::tests::test_trim_leading_pattern_recognized ... ok
test mir::loop_canonicalizer::canonicalizer::tests::test_trim_trailing_pattern_recognized ... ok
test result: ok. 2 passed; 0 failed; 0 ignored
```
#### 3. Manual Verification
**Strict Parity Check**:
```bash
NYASH_JOINIR_DEV=1 HAKO_JOINIR_STRICT=1 ./target/release/hakorune \
tools/selfhost/test_pattern3_trim_leading.hako
```
**Output** (trim_leading):
```
[loop_canonicalizer] Decision: SUCCESS
[loop_canonicalizer] Chosen pattern: Pattern2Break
[loop_canonicalizer] Missing caps: []
[choose_pattern_kind/PARITY] OK: canonical and actual agree on Pattern2Break
[loop_canonicalizer/PARITY] OK in function 'main': canonical and actual agree on Pattern2Break
```
**Output** (trim_trailing):
```
[loop_canonicalizer] Decision: SUCCESS
[loop_canonicalizer] Chosen pattern: Pattern2Break
[loop_canonicalizer] Missing caps: []
[choose_pattern_kind/PARITY] OK: canonical and actual agree on Pattern2Break
[loop_canonicalizer/PARITY] OK in function 'main': canonical and actual agree on Pattern2Break
```
### Design Principles Applied
#### Box-First Modularization
- Extended existing `detect_skip_whitespace_pattern()` instead of creating new functions
- Maintained SSOT (Single Source of Truth) architecture
- Preserved delegation pattern through `pattern_recognizer.rs` wrapper
#### Incremental Implementation
- Focused on recognizer generalization only
- Did not modify routing or lowering logic
- Kept scope minimal (P0 only)
#### ExitContract Priority
- Pattern choice determined by ExitContract (has_break=true)
- Routes to Pattern2Break (not Pattern3IfPhi)
- Consistent with existing SSOT policy
### Files Modified
1. `src/mir/builder/control_flow/joinir/patterns/ast_feature_extractor.rs` (+35 lines, improved comments)
2. `src/mir/loop_canonicalizer/canonicalizer.rs` (+178 lines, 2 new tests)
### Statistics
- **Total changes**: +213 lines
- **Unit tests**: 2 new tests (100% pass)
- **Manual tests**: 2 patterns verified (strict parity green)
- **Build status**: ✅ No errors, no warnings (lib)
### SSOT References
- **Design**: `docs/development/current/main/design/loop-canonicalizer.md`
- **JoinIR Architecture**: `docs/development/current/main/joinir-architecture-overview.md`
- **Pattern Detection**: `ast_feature_extractor.rs` (Phase 140-P4-A SSOT)
### Known Limitations
- Pattern2 variable promotion (A-3 Trim promotion) not yet implemented
- This is expected - Phase 142 P0 only targets recognizer extension
- Promotion will be addressed in future phases
### Next Steps (Future Phases)
- Phase 142 P1: Implement A-3 Trim promotion in Pattern2 handler
- Phase 142 P2: Extend to other loop patterns (Pattern 3/4)
- Phase 142 P3: Add more complex carrier update patterns
### Verification Commands
```bash
# Unit tests
cargo test --release loop_canonicalizer::canonicalizer::tests::test_trim --lib
# Manual verification (trim_leading)
NYASH_JOINIR_DEV=1 HAKO_JOINIR_STRICT=1 ./target/release/hakorune \
tools/selfhost/test_pattern3_trim_leading.hako
# Manual verification (trim_trailing)
NYASH_JOINIR_DEV=1 HAKO_JOINIR_STRICT=1 ./target/release/hakorune \
tools/selfhost/test_pattern3_trim_trailing.hako
```
### Conclusion
Phase 142 P0 successfully extends the Canonicalizer to recognize trim leading/trailing patterns. The implementation:
- Maintains SSOT architecture
- Passes all unit tests
- Achieves strict parity agreement
- Preserves existing behavior
- Sets foundation for future pattern extensions
All acceptance criteria met. ✅
---
## P1: continue pattern (COMPLETE)
### Objective
Extend Canonicalizer to recognize continue patterns, enabling proper routing through the normalized loop pipeline.
### Target Pattern
- `tools/selfhost/test_pattern4_simple_continue.hako` - Simple continue pattern with carrier update
### Accepted Criteria (All Met ✅)
- ✅ Canonicalizer creates Skeleton for continue pattern
-`decision.chosen == Pattern4Continue` (router agreement)
-`decision.missing_caps == []` (no missing capabilities)
- ✅ Strict parity green (NYASH_JOINIR_DEV=1 HAKO_JOINIR_STRICT=1)
- ✅ Default behavior unchanged
- ✅ Unit tests added
- ✅ Documentation updated
### Implementation Summary
#### 1. Continue Pattern Detection
**File**: `src/mir/builder/control_flow/joinir/patterns/ast_feature_extractor.rs`
**New Function**: `detect_continue_pattern()`
**Pattern Structure**:
```rust
loop(cond) {
// ... optional body statements (Body)
if skip_cond {
carrier = carrier + const // Optional update before continue
continue
}
// ... rest of body statements (Rest)
carrier = carrier + const // Carrier update
}
```
**Example** (from test_pattern4_simple_continue.hako):
```nyash
loop(i < n) {
if is_even == 1 {
i = i + 1 // Update before continue
continue
}
sum = sum + i // Rest statements
i = i + 1 // Carrier update
}
```
**Key Logic**:
- Finds if statement containing continue in then_body
- Extracts body statements before the if
- Extracts rest statements after the if
- Detects carrier update (last statement in rest_stmts)
- Returns `ContinuePatternInfo` with carrier name, delta, body_stmts, and rest_stmts
#### 2. Canonicalizer Integration
**File**: `src/mir/loop_canonicalizer/canonicalizer.rs`
**Changes**:
- Added `try_extract_continue_pattern()` call before skip_whitespace check
- Build skeleton with continue pattern structure
- Set `ExitContract` with `has_continue=true, has_break=false`
- Route to `Pattern4Continue`
**Skeleton Structure**:
1. HeaderCond - Loop condition
2. Body - Optional body statements before continue check
3. Body - Rest statements (excluding carrier update)
4. Update - Carrier update step
#### 3. Module Re-exports
**Files Modified** (re-export chain):
- `src/mir/builder/control_flow/joinir/patterns/mod.rs` - Added `detect_continue_pattern`, `ContinuePatternInfo`
- `src/mir/builder/control_flow/joinir/mod.rs` - Re-export to joinir level
- `src/mir/builder/control_flow/mod.rs` - Re-export to control_flow level
- `src/mir/builder.rs` - Re-export to builder level
- `src/mir/mod.rs` - Re-export to crate level
**Pattern**: Followed existing SSOT pattern from Phase 140-P4-A
#### 4. Pattern Recognizer Wrapper
**File**: `src/mir/loop_canonicalizer/pattern_recognizer.rs`
**New Function**: `try_extract_continue_pattern()`
- Delegates to `detect_continue_pattern()` from ast_feature_extractor
- Returns tuple: `(carrier_name, delta, body_stmts, rest_stmts)`
- Maintains backward compatibility with existing callsites
#### 5. Unit Tests
**File**: `src/mir/loop_canonicalizer/canonicalizer.rs`
**Added Test**: `test_simple_continue_pattern_recognized()`
- Builds AST: `loop(i < n) { if is_even { i = i + 1; continue } sum = sum + i; i = i + 1 }`
- Verifies skeleton creation with correct structure
- Checks carrier slot (name="i", delta=1)
- Validates ExitContract (has_continue=true, has_break=false)
- Confirms routing decision (Pattern4Continue, missing_caps=[])
**Test Results**:
```
running 8 tests
test mir::loop_canonicalizer::canonicalizer::tests::test_simple_continue_pattern_recognized ... ok
test result: ok. 8 passed; 0 failed; 0 ignored
```
#### 6. Manual Verification
**Strict Parity Check**:
```bash
NYASH_JOINIR_DEV=1 HAKO_JOINIR_STRICT=1 ./target/release/hakorune \
tools/selfhost/test_pattern4_simple_continue.hako
```
**Output**:
```
[loop_canonicalizer] Function: main
[loop_canonicalizer] Skeleton steps: 4
[loop_canonicalizer] Carriers: 1
[loop_canonicalizer] Has exits: true
[loop_canonicalizer] Decision: SUCCESS
[loop_canonicalizer] Chosen pattern: Pattern4Continue
[loop_canonicalizer] Missing caps: []
[choose_pattern_kind/PARITY] OK: canonical and actual agree on Pattern4Continue
[loop_canonicalizer/PARITY] OK in function 'main': canonical and actual agree on Pattern4Continue
```
**Status**: ✅ Strict parity green!
### Design Principles Applied
#### Box-First Modularization
- Created dedicated `detect_continue_pattern()` function in ast_feature_extractor
- Maintained SSOT architecture with proper re-export chain
- Followed existing pattern from skip_whitespace detection
#### Incremental Implementation
- Focused on pattern recognition only (P1 scope)
- Did not modify lowering logic (expected promotion errors)
- Kept changes minimal and focused
#### ExitContract Priority
- Pattern choice determined by ExitContract (has_continue=true, has_break=false)
- Routes to Pattern4Continue (not Pattern2 or Pattern3)
- Consistent with existing SSOT policy from Phase 137-5
### Files Modified
1. `src/mir/builder/control_flow/joinir/patterns/ast_feature_extractor.rs` (+167 lines, new function)
2. `src/mir/loop_canonicalizer/pattern_recognizer.rs` (+35 lines, wrapper function)
3. `src/mir/loop_canonicalizer/canonicalizer.rs` (+103 lines, continue support + unit test)
4. `src/mir/builder/control_flow/joinir/patterns/mod.rs` (+3 lines, re-export)
5. `src/mir/builder/control_flow/joinir/mod.rs` (+3 lines, re-export)
6. `src/mir/builder/control_flow/mod.rs` (+3 lines, re-export)
7. `src/mir/builder.rs` (+2 lines, re-export)
8. `src/mir/mod.rs` (+2 lines, re-export)
### Statistics
- **Total changes**: +318 lines
- **Unit tests**: 1 new test (100% pass)
- **All canonicalizer tests**: 8 passed (100%)
- **Manual tests**: 1 pattern verified (strict parity green)
- **Build status**: ✅ No errors (warnings are pre-existing)
### SSOT References
- **Design**: `docs/development/current/main/design/loop-canonicalizer.md`
- **JoinIR Architecture**: `docs/development/current/main/joinir-architecture-overview.md`
- **Pattern Detection**: `ast_feature_extractor.rs` (Phase 140-P4-A SSOT)
### Known Limitations
- Pattern4 variable promotion (A-3 Trim, A-4 DigitPos) not yet handling this pattern
- This is expected - Phase 142 P1 only targets recognizer extension
- Promotion will be addressed when Pattern4 lowering is enhanced
### Next Steps (Future Phases)
- Phase 142 P2: Extend Pattern4 lowering to handle recognized continue patterns
- Phase 142 P3: Add more complex continue patterns (multiple carriers, nested conditions)
### Verification Commands
```bash
# Unit tests
cargo test --release --lib loop_canonicalizer::canonicalizer::tests::test_simple_continue_pattern_recognized
# All canonicalizer tests
cargo test --release --lib loop_canonicalizer::canonicalizer::tests
# Manual verification
NYASH_JOINIR_DEV=1 HAKO_JOINIR_STRICT=1 ./target/release/hakorune \
tools/selfhost/test_pattern4_simple_continue.hako
```
### Conclusion
Phase 142 P1 successfully extends the Canonicalizer to recognize continue patterns. The implementation:
- Maintains SSOT architecture
- Passes all unit tests (8/8)
- Achieves strict parity agreement with router
- Preserves existing behavior
- Follows existing re-export pattern from Phase 140-P4-A
All acceptance criteria met. ✅

View File

@ -0,0 +1,184 @@
# Phase 143: Canonicalizer Adaptation Range Expansion
## Status
- State: 🎉 Complete (P0)
## P0: parse_number Pattern - Break in THEN Clause
### Objective
Expand the canonicalizer to recognize parse_number/digit collection patterns, maximizing the adaptation range before adding new lowering patterns.
### Target Pattern
`tools/selfhost/test_pattern2_parse_number.hako`
```hako
loop(i < num_str.length()) {
local ch = num_str.substring(i, i + 1)
local digit_pos = digits.indexOf(ch)
// Exit on non-digit (break in THEN clause)
if digit_pos < 0 {
break
}
// Append digit
result = result + ch
i = i + 1
}
```
### Pattern Characteristics
**Key Difference from skip_whitespace**:
- **skip_whitespace**: `if cond { update } else { break }` - break in ELSE clause
- **parse_number**: `if invalid_cond { break } body... update` - break in THEN clause
**Structure**:
```
loop(cond) {
// ... body statements (ch, digit_pos computation)
if invalid_cond {
break
}
// ... rest statements (result append, carrier update)
carrier = carrier + const
}
```
### Implementation Summary
#### 1. New Recognizer (`ast_feature_extractor.rs`)
Added `detect_parse_number_pattern()`:
- Detects `if cond { break }` pattern (no else clause)
- Extracts body statements before break check
- Extracts rest statements after break check (including carrier update)
- Returns `ParseNumberInfo { carrier_name, delta, body_stmts, rest_stmts }`
**Lines added**: ~150 lines
#### 2. Canonicalizer Integration (`canonicalizer.rs`)
- Tries parse_number pattern before skip_whitespace pattern
- Builds LoopSkeleton with:
- Step 1: HeaderCond
- Step 2: Body (statements before break)
- Step 3: Body (statements after break, excluding carrier update)
- Step 4: Update (carrier update)
- Routes to `Pattern2Break` (has_break=true)
**Lines modified**: ~60 lines
#### 3. Export Chain
Added exports through the module hierarchy:
- `ast_feature_extractor.rs``ParseNumberInfo` struct
- `patterns/mod.rs` → re-export
- `joinir/mod.rs` → re-export
- `control_flow/mod.rs` → re-export
- `builder.rs` → re-export
- `mir/mod.rs` → final re-export
**Files modified**: 6 files (8 lines total)
#### 4. Unit Tests
Added `test_parse_number_pattern_recognized()` in `canonicalizer.rs`:
- Builds AST for parse_number pattern
- Verifies skeleton structure (4 steps)
- Verifies carrier (name="i", delta=1, role=Counter)
- Verifies exit contract (has_break=true)
- Verifies routing decision (Pattern2Break, no missing_caps)
**Lines added**: ~130 lines
### Acceptance Criteria
- ✅ Canonicalizer creates Skeleton for parse_number loop
- ✅ RoutingDecision.chosen matches router (Pattern2Break)
- ✅ Strict parity OK (canonicalizer and router agree)
- ✅ Default behavior unchanged
- ✅ quick profile not affected
- ✅ Unit test added
- ✅ Documentation created
### Results
#### Parity Verification
```bash
NYASH_JOINIR_DEV=1 HAKO_JOINIR_STRICT=1 ./target/release/hakorune \
tools/selfhost/test_pattern2_parse_number.hako
```
**Output**:
```
[loop_canonicalizer] Chosen pattern: Pattern2Break
[choose_pattern_kind/PARITY] OK: canonical and actual agree on Pattern2Break
[loop_canonicalizer/PARITY] OK in function 'main': canonical and actual agree on Pattern2Break
```
**Status**: ✅ **Green parity** - canonicalizer and router agree
#### Unit Test Results
```bash
cargo test --release --lib loop_canonicalizer::canonicalizer::tests::test_parse_number_pattern_recognized
```
**Status**: ✅ **PASS**
### Statistics
| Metric | Count |
|--------|-------|
| New patterns supported | 1 (parse_number) |
| Total patterns supported | 3 (skip_whitespace, parse_number, continue) |
| New Capability Tags | 0 (uses existing ConstStep) |
| Lines added | ~280 |
| Files modified | 8 |
| Unit tests added | 1 |
| Parity status | Green ✅ |
### Comparison: Parse Number vs Skip Whitespace
| Aspect | Skip Whitespace | Parse Number |
|--------|----------------|--------------|
| **Break location** | ELSE clause | THEN clause |
| **Pattern** | `if cond { update } else { break }` | `if invalid { break } rest... update` |
| **Body before if** | Optional | Optional (ch, digit_pos) |
| **Body after if** | None (last statement) | Required (result append) |
| **Carrier update** | In THEN clause | After if statement |
| **Routing** | Pattern2Break | Pattern2Break |
| **Example** | skip_whitespace, trim_leading/trailing | parse_number, digit collection |
### Follow-up Opportunities
#### Immediate (Phase 143 P1-P2)
- [ ] Support parse_string pattern (continue + return combo)
- [ ] Add capability for variable-step updates (escape handling)
#### Future Enhancements
- [ ] Extend recognizer for nested if patterns
- [ ] Support multiple break points (requires new capability)
- [ ] Add signature-based corpus analysis
### Lessons Learned
1. **Break location matters**: THEN vs ELSE clause creates different patterns
2. **rest_stmts extraction**: Need to carefully separate body from carrier update
3. **Export chain**: Requires 6-level re-export (ast → patterns → joinir → control_flow → builder → mir)
4. **Parity first**: Always verify strict parity before claiming success
## SSOT
- **Design**: `docs/development/current/main/design/loop-canonicalizer.md`
- **Recognizer**: `src/mir/builder/control_flow/joinir/patterns/ast_feature_extractor.rs`
- **Canonicalizer**: `src/mir/loop_canonicalizer/canonicalizer.rs`
- **Tests**: Test file `tools/selfhost/test_pattern2_parse_number.hako`
---
**Phase 143 P0: Complete**
**Date**: 2025-12-16
**Implemented by**: Claude Code (Sonnet 4.5)

View File

@ -33,6 +33,10 @@ mod control_flow; // thin wrappers to centralize control-flow entrypoints
// Phase 140-P4-A: Re-export for loop_canonicalizer SSOT (crate-wide visibility)
pub(crate) use control_flow::{detect_skip_whitespace_pattern, SkipWhitespaceInfo};
// Phase 142-P1: Re-export continue pattern detection for loop_canonicalizer
pub(crate) use control_flow::{detect_continue_pattern, ContinuePatternInfo};
// Phase 143-P0: Re-export parse_number pattern detection for loop_canonicalizer
pub(crate) use control_flow::{detect_parse_number_pattern, ParseNumberInfo};
mod exprs_lambda; // lambda lowering
mod exprs_peek; // peek expression
mod exprs_qmark; // ?-propagate

View File

@ -18,3 +18,9 @@ pub(in crate::mir::builder) mod trace;
// Phase 140-P4-A: Re-export for loop_canonicalizer SSOT (crate-wide visibility)
pub(crate) use patterns::{detect_skip_whitespace_pattern, SkipWhitespaceInfo};
// Phase 142-P1: Re-export continue pattern detection for loop_canonicalizer
pub(crate) use patterns::{detect_continue_pattern, ContinuePatternInfo};
// Phase 143-P0: Re-export parse_number pattern detection for loop_canonicalizer
pub(crate) use patterns::{detect_parse_number_pattern, ParseNumberInfo};

View File

@ -353,6 +353,330 @@ mod tests {
}
}
// ============================================================================
// Phase 142-P1: Continue Pattern Detection
// ============================================================================
/// Continue pattern information
///
/// This struct holds the extracted information from a recognized continue pattern.
#[derive(Debug, Clone, PartialEq)]
pub struct ContinuePatternInfo {
/// Carrier variable name (e.g., "i")
pub carrier_name: String,
/// Constant step increment (e.g., 1 for `i = i + 1`)
pub delta: i64,
/// Body statements before the continue check (may be empty)
pub body_stmts: Vec<ASTNode>,
/// Body statements after the continue check (usually includes carrier update)
pub rest_stmts: Vec<ASTNode>,
}
/// Detect continue pattern in loop body
///
/// Pattern structure:
/// ```
/// loop(cond) {
/// // ... optional body statements (Body)
/// if skip_cond {
/// carrier = carrier + const // Optional update before continue
/// continue
/// }
/// // ... rest of body statements (Rest)
/// carrier = carrier + const // Carrier update
/// }
/// ```
///
/// # Arguments
///
/// * `body` - Loop body statements to analyze
///
/// # Returns
///
/// `Some(ContinuePatternInfo)` if the pattern matches, `None` otherwise
pub fn detect_continue_pattern(body: &[ASTNode]) -> Option<ContinuePatternInfo> {
if body.is_empty() {
return None;
}
// Find the if statement with continue
let mut if_idx = None;
for (i, stmt) in body.iter().enumerate() {
if let ASTNode::If { then_body, .. } = stmt {
// Check if then_body contains continue
if then_body
.iter()
.any(|s| matches!(s, ASTNode::Continue { .. }))
{
if_idx = Some(i);
break;
}
}
}
let if_idx = if_idx?;
// Extract body statements before the if
let body_stmts = body[..if_idx].to_vec();
// Extract the if statement
let if_stmt = &body[if_idx];
// The if must have continue in then branch
let then_body = match if_stmt {
ASTNode::If {
then_body,
else_body,
..
} => {
// For simple continue pattern, else_body should be None
if else_body.is_some() {
return None;
}
then_body
}
_ => return None,
};
// Check if then_body contains carrier update before continue
// For now, we'll look for the pattern after the if statement
// Extract rest statements after the if
let rest_stmts = body[if_idx + 1..].to_vec();
// Find carrier update in rest_stmts (last statement should be carrier = carrier +/- const)
if rest_stmts.is_empty() {
return None;
}
let last_stmt = &rest_stmts[rest_stmts.len() - 1];
let (carrier_name, delta) = match last_stmt {
ASTNode::Assignment { target, value, .. } => {
// Extract target variable name
let target_name = match target.as_ref() {
ASTNode::Variable { name, .. } => name.clone(),
_ => return None,
};
// Value must be: target (+|-) const
match value.as_ref() {
ASTNode::BinaryOp {
operator,
left,
right,
..
} => {
// Accept both Add (+1) and Subtract (-1)
let op_multiplier = match operator {
BinaryOperator::Add => 1,
BinaryOperator::Subtract => -1,
_ => return None,
};
// Left must be same variable
let left_name = match left.as_ref() {
ASTNode::Variable { name, .. } => name,
_ => return None,
};
if left_name != &target_name {
return None;
}
// Right must be integer literal
let const_val = match right.as_ref() {
ASTNode::Literal {
value: LiteralValue::Integer(n),
..
} => *n,
_ => return None,
};
// Calculate delta with sign
let delta = const_val * op_multiplier;
(target_name, delta)
}
_ => return None,
}
}
_ => return None,
};
// Check if then_body has carrier update before continue
// If so, we need to validate it matches
for stmt in then_body {
if let ASTNode::Assignment { target, .. } = stmt {
if let ASTNode::Variable { name, .. } = target.as_ref() {
if name == &carrier_name {
// There's a carrier update before continue
// For now, we'll just check it exists
// Could validate it matches the pattern later
}
}
}
}
Some(ContinuePatternInfo {
carrier_name,
delta,
body_stmts,
rest_stmts,
})
}
// ============================================================================
// Phase 143-P0: Parse Number/Digit Pattern Detection
// ============================================================================
/// Parse number pattern information
///
/// This struct holds the extracted information from a recognized parse_number pattern.
#[derive(Debug, Clone, PartialEq)]
pub struct ParseNumberInfo {
/// Carrier variable name (e.g., "i")
pub carrier_name: String,
/// Constant step increment (e.g., 1 for `i = i + 1`)
pub delta: i64,
/// Body statements before the break check (may be empty)
pub body_stmts: Vec<ASTNode>,
/// Rest statements after break check (usually includes result append and carrier update)
pub rest_stmts: Vec<ASTNode>,
}
/// Detect parse_number / digit collection pattern in loop body
///
/// Phase 143-P0: Pattern with break in THEN clause (opposite of skip_whitespace)
///
/// Pattern structure:
/// ```
/// loop(cond) {
/// // ... optional body statements (ch, digit_pos computation)
/// if invalid_cond {
/// break
/// }
/// // ... rest statements (result append, carrier update)
/// carrier = carrier + const
/// }
/// ```
///
/// Recognized pattern:
/// - parse_number: `i < len`, `if digit_pos < 0 { break }`, `i = i + 1`
///
/// # Arguments
///
/// * `body` - Loop body statements to analyze
///
/// # Returns
///
/// `Some(ParseNumberInfo)` if the pattern matches, `None` otherwise
///
/// # Notes
///
/// This is complementary to skip_whitespace pattern (which has break in ELSE clause).
/// Used by loop_canonicalizer (Phase 143) for digit collection patterns.
pub fn detect_parse_number_pattern(body: &[ASTNode]) -> Option<ParseNumberInfo> {
if body.is_empty() {
return None;
}
// Find the if statement with break in THEN clause
let mut if_idx = None;
for (i, stmt) in body.iter().enumerate() {
if let ASTNode::If {
then_body,
else_body,
..
} = stmt
{
// Check if then_body contains break and else_body is None
if else_body.is_none()
&& then_body.len() == 1
&& matches!(then_body[0], ASTNode::Break { .. })
{
if_idx = Some(i);
break;
}
}
}
let if_idx = if_idx?;
// Extract body statements before the if
let body_stmts = body[..if_idx].to_vec();
// Extract rest statements after the if (should include carrier update)
let rest_stmts = body[if_idx + 1..].to_vec();
if rest_stmts.is_empty() {
return None;
}
// Find carrier update in rest_stmts (last statement should be carrier = carrier + const)
let last_stmt = &rest_stmts[rest_stmts.len() - 1];
let (carrier_name, delta) = match last_stmt {
ASTNode::Assignment { target, value, .. } => {
// Extract target variable name
let target_name = match target.as_ref() {
ASTNode::Variable { name, .. } => name.clone(),
_ => return None,
};
// Value must be: target (+|-) const
match value.as_ref() {
ASTNode::BinaryOp {
operator,
left,
right,
..
} => {
// Accept both Add (+1) and Subtract (-1)
let op_multiplier = match operator {
BinaryOperator::Add => 1,
BinaryOperator::Subtract => -1,
_ => return None,
};
// Left must be same variable
let left_name = match left.as_ref() {
ASTNode::Variable { name, .. } => name,
_ => return None,
};
if left_name != &target_name {
return None;
}
// Right must be integer literal
let const_val = match right.as_ref() {
ASTNode::Literal {
value: LiteralValue::Integer(n),
..
} => *n,
_ => return None,
};
// Calculate delta with sign
let delta = const_val * op_multiplier;
(target_name, delta)
}
_ => return None,
}
}
_ => return None,
};
Some(ParseNumberInfo {
carrier_name,
delta,
body_stmts,
rest_stmts,
})
}
// ============================================================================
// Phase 140-P4-A: Skip Whitespace Pattern Detection (SSOT)
// ============================================================================
@ -370,20 +694,27 @@ pub struct SkipWhitespaceInfo {
pub body_stmts: Vec<ASTNode>,
}
/// Detect skip_whitespace pattern in loop body (Phase 140-P4-A SSOT)
/// Detect skip_whitespace / trim leading/trailing pattern in loop body
///
/// Phase 142 P0: Generalized to handle both +1 and -1 patterns
///
/// Pattern structure:
/// ```
/// loop(cond) {
/// // ... optional body statements (Body)
/// if check_cond {
/// carrier = carrier + const
/// carrier = carrier (+|-) const
/// } else {
/// break
/// }
/// }
/// ```
///
/// Recognized patterns:
/// - skip_whitespace: `p < len`, `p = p + 1`
/// - trim_leading: `start < end`, `start = start + 1`
/// - trim_trailing: `end > start`, `end = end - 1`
///
/// # Arguments
///
/// * `body` - Loop body statements to analyze
@ -394,7 +725,7 @@ pub struct SkipWhitespaceInfo {
///
/// # Notes
///
/// This is the SSOT for skip_whitespace pattern detection.
/// This is the SSOT for skip_whitespace/trim pattern detection.
/// Used by both loop_canonicalizer (Phase 137) and future pattern analyzers.
pub fn detect_skip_whitespace_pattern(body: &[ASTNode]) -> Option<SkipWhitespaceInfo> {
if body.is_empty() {
@ -413,7 +744,7 @@ pub fn detect_skip_whitespace_pattern(body: &[ASTNode]) -> Option<SkipWhitespace
_ => return None,
};
// Then branch must be single assignment: carrier = carrier + const
// Then branch must be single assignment: carrier = carrier (+|-) const
if then_body.len() != 1 {
return None;
}
@ -426,14 +757,21 @@ pub fn detect_skip_whitespace_pattern(body: &[ASTNode]) -> Option<SkipWhitespace
_ => return None,
};
// Value must be: target + const
// Value must be: target (+|-) const
match value.as_ref() {
ASTNode::BinaryOp {
operator: BinaryOperator::Add,
operator,
left,
right,
..
} => {
// Phase 142 P0: Accept both Add (+1) and Subtract (-1)
let op_multiplier = match operator {
BinaryOperator::Add => 1,
BinaryOperator::Subtract => -1,
_ => return None,
};
// Left must be same variable
let left_name = match left.as_ref() {
ASTNode::Variable { name, .. } => name,
@ -445,7 +783,7 @@ pub fn detect_skip_whitespace_pattern(body: &[ASTNode]) -> Option<SkipWhitespace
}
// Right must be integer literal
let delta = match right.as_ref() {
let const_val = match right.as_ref() {
ASTNode::Literal {
value: LiteralValue::Integer(n),
..
@ -453,6 +791,9 @@ pub fn detect_skip_whitespace_pattern(body: &[ASTNode]) -> Option<SkipWhitespace
_ => return None,
};
// Calculate delta with sign (e.g., +1 or -1)
let delta = const_val * op_multiplier;
(target_name, delta)
}
_ => return None,

View File

@ -67,3 +67,9 @@ pub(in crate::mir::builder) use router::{route_loop_pattern, LoopPatternContext}
// Phase 140-P4-A: Re-export for loop_canonicalizer SSOT (crate-wide visibility)
pub(crate) use ast_feature_extractor::{detect_skip_whitespace_pattern, SkipWhitespaceInfo};
// Phase 142-P1: Re-export continue pattern detection for loop_canonicalizer
pub(crate) use ast_feature_extractor::{detect_continue_pattern, ContinuePatternInfo};
// Phase 143-P0: Re-export parse_number pattern detection for loop_canonicalizer
pub(crate) use ast_feature_extractor::{detect_parse_number_pattern, ParseNumberInfo};

View File

@ -57,6 +57,12 @@ pub(in crate::mir::builder) mod utils;
// Phase 140-P4-A: Re-export for loop_canonicalizer SSOT (crate-wide visibility)
pub(crate) use joinir::{detect_skip_whitespace_pattern, SkipWhitespaceInfo};
// Phase 142-P1: Re-export continue pattern detection for loop_canonicalizer
pub(crate) use joinir::{detect_continue_pattern, ContinuePatternInfo};
// Phase 143-P0: Re-export parse_number pattern detection for loop_canonicalizer
pub(crate) use joinir::{detect_parse_number_pattern, ParseNumberInfo};
impl super::MirBuilder {
/// Control-flow: block
pub(super) fn cf_block(&mut self, statements: Vec<ASTNode>) -> Result<ValueId, String> {

View File

@ -7,7 +7,10 @@ use crate::ast::ASTNode;
use crate::mir::loop_pattern_detection::LoopPatternKind;
use super::capability_guard::{CapabilityTag, RoutingDecision};
use super::pattern_recognizer::try_extract_skip_whitespace_pattern;
use super::pattern_recognizer::{
try_extract_continue_pattern, try_extract_parse_number_pattern,
try_extract_skip_whitespace_pattern,
};
use super::skeleton_types::{
CarrierRole, CarrierSlot, ExitContract, LoopSkeleton, SkeletonStep, UpdateKind,
};
@ -16,9 +19,12 @@ use super::skeleton_types::{
// Canonicalization Entry Point
// ============================================================================
/// Canonicalize a loop AST into LoopSkeleton (Phase 3: skip_whitespace pattern recognition)
/// Canonicalize a loop AST into LoopSkeleton
///
/// Currently supports only the skip_whitespace pattern:
/// Phase 143-P0: Now supports parse_number pattern in addition to skip_whitespace and continue
///
/// Supported patterns:
/// 1. Skip whitespace (break in ELSE clause):
/// ```
/// loop(cond) {
/// // ... optional body statements
@ -30,6 +36,31 @@ use super::skeleton_types::{
/// }
/// ```
///
/// 2. Parse number (break in THEN clause):
/// ```
/// loop(cond) {
/// // ... optional body statements (ch, digit_pos computation)
/// if invalid_cond {
/// break
/// }
/// // ... rest statements (result append)
/// carrier = carrier + step
/// }
/// ```
///
/// 3. Continue pattern:
/// ```
/// loop(cond) {
/// // ... optional body statements
/// if skip_cond {
/// carrier = carrier + step // Optional
/// continue
/// }
/// // ... rest statements
/// carrier = carrier + step
/// }
/// ```
///
/// All other patterns return Fail-Fast with detailed reasoning.
///
/// # Arguments
@ -51,6 +82,122 @@ pub fn canonicalize_loop_expr(
_ => return Err(format!("Expected Loop node, got: {:?}", loop_expr)),
};
// Phase 142-P1: Try to extract continue pattern first
if let Some((carrier_name, delta, body_stmts, rest_stmts)) = try_extract_continue_pattern(body)
{
// Build skeleton for continue pattern
let mut skeleton = LoopSkeleton::new(span);
// Step 1: Header condition
skeleton.steps.push(SkeletonStep::HeaderCond {
expr: Box::new(condition.clone()),
});
// Step 2: Body statements (if any)
if !body_stmts.is_empty() {
skeleton
.steps
.push(SkeletonStep::Body { stmts: body_stmts });
}
// Step 3: Rest statements (if any, excluding carrier update)
// For now, we include all rest_stmts in Body
// The actual carrier update is implicit in the Update step
if !rest_stmts.is_empty() {
// Remove the last statement (carrier update) from rest_stmts
let mut rest_body = rest_stmts;
if !rest_body.is_empty() {
rest_body.pop();
}
if !rest_body.is_empty() {
skeleton.steps.push(SkeletonStep::Body { stmts: rest_body });
}
}
// Step 4: Update step
skeleton.steps.push(SkeletonStep::Update {
carrier_name: carrier_name.clone(),
update_kind: UpdateKind::ConstStep { delta },
});
// Add carrier slot
skeleton.carriers.push(CarrierSlot {
name: carrier_name,
role: CarrierRole::Counter,
update_kind: UpdateKind::ConstStep { delta },
});
// Set exit contract for continue pattern
skeleton.exits = ExitContract {
has_break: false,
has_continue: true,
has_return: false,
break_has_value: false,
};
// Phase 142-P1: Route to Pattern4Continue
let decision = RoutingDecision::success(LoopPatternKind::Pattern4Continue);
return Ok((skeleton, decision));
}
// Phase 143-P0: Try to extract parse_number pattern (break in THEN clause)
if let Some((carrier_name, delta, body_stmts, rest_stmts)) =
try_extract_parse_number_pattern(body)
{
// Build skeleton for parse_number pattern
let mut skeleton = LoopSkeleton::new(span);
// Step 1: Header condition
skeleton.steps.push(SkeletonStep::HeaderCond {
expr: Box::new(condition.clone()),
});
// Step 2: Body statements before break check (if any)
if !body_stmts.is_empty() {
skeleton
.steps
.push(SkeletonStep::Body { stmts: body_stmts });
}
// Step 3: Rest statements after break check (if any, excluding carrier update)
// The carrier update is implicit in the Update step
if !rest_stmts.is_empty() {
// Remove the last statement (carrier update) from rest_stmts
let mut rest_body = rest_stmts;
if !rest_body.is_empty() {
rest_body.pop();
}
if !rest_body.is_empty() {
skeleton.steps.push(SkeletonStep::Body { stmts: rest_body });
}
}
// Step 4: Update step
skeleton.steps.push(SkeletonStep::Update {
carrier_name: carrier_name.clone(),
update_kind: UpdateKind::ConstStep { delta },
});
// Add carrier slot
skeleton.carriers.push(CarrierSlot {
name: carrier_name,
role: CarrierRole::Counter,
update_kind: UpdateKind::ConstStep { delta },
});
// Set exit contract for parse_number pattern
skeleton.exits = ExitContract {
has_break: true,
has_continue: false,
has_return: false,
break_has_value: false,
};
// Phase 143-P0: Route to Pattern2Break (has_break=true)
let decision = RoutingDecision::success(LoopPatternKind::Pattern2Break);
return Ok((skeleton, decision));
}
// Phase 3: Try to extract skip_whitespace pattern
if let Some((carrier_name, delta, body_stmts)) = try_extract_skip_whitespace_pattern(body) {
// Build skeleton for skip_whitespace pattern
@ -101,7 +248,8 @@ pub fn canonicalize_loop_expr(
LoopSkeleton::new(span),
RoutingDecision::fail_fast(
vec![CapabilityTag::ConstStep],
"Phase 3: Loop does not match skip_whitespace pattern".to_string(),
"Phase 143-P0: Loop does not match skip_whitespace, parse_number, or continue pattern"
.to_string(),
),
))
}
@ -348,12 +496,13 @@ mod tests {
let (_, decision) = result.unwrap();
assert!(decision.is_fail_fast());
assert!(decision.notes[0].contains("does not match skip_whitespace pattern"));
assert!(decision.notes[0]
.contains("does not match skip_whitespace, parse_number, or continue pattern"));
}
#[test]
fn test_skip_whitespace_fails_with_wrong_delta() {
// Build pattern with wrong update (p = p - 1 instead of p = p + 1)
// Build pattern with wrong update (p = p * 2, not +/-)
let loop_node = ASTNode::Loop {
condition: Box::new(ASTNode::Literal {
value: LiteralValue::Bool(true),
@ -370,11 +519,75 @@ mod tests {
span: Span::unknown(),
}),
value: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Subtract, // Wrong operator
operator: BinaryOperator::Multiply, // Wrong operator
left: Box::new(ASTNode::Variable {
name: "p".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(2),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
span: Span::unknown(),
}],
else_body: Some(vec![ASTNode::Break {
span: Span::unknown(),
}]),
span: Span::unknown(),
}],
span: Span::unknown(),
};
let result = canonicalize_loop_expr(&loop_node);
assert!(result.is_ok());
let (_, decision) = result.unwrap();
assert!(decision.is_fail_fast());
}
#[test]
fn test_trim_leading_pattern_recognized() {
// Phase 142 P0: Test trim_leading pattern (start = start + 1)
// Build: loop(start < end) { if is_ws { start = start + 1 } else { break } }
let loop_node = ASTNode::Loop {
condition: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Less,
left: Box::new(ASTNode::Variable {
name: "start".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Variable {
name: "end".to_string(),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
body: vec![ASTNode::If {
condition: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Equal,
left: Box::new(ASTNode::Variable {
name: "is_ws".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(1),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
then_body: vec![ASTNode::Assignment {
target: Box::new(ASTNode::Variable {
name: "start".to_string(),
span: Span::unknown(),
}),
value: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left: Box::new(ASTNode::Variable {
name: "start".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(1),
span: Span::unknown(),
@ -394,7 +607,379 @@ mod tests {
let result = canonicalize_loop_expr(&loop_node);
assert!(result.is_ok());
let (_, decision) = result.unwrap();
assert!(decision.is_fail_fast());
let (skeleton, decision) = result.unwrap();
// Verify success
assert!(decision.is_success());
// chosen == Pattern2Break (ExitContract priority)
assert_eq!(decision.chosen, Some(LoopPatternKind::Pattern2Break));
// missing_caps == []
assert!(decision.missing_caps.is_empty());
// Verify skeleton structure
assert_eq!(skeleton.steps.len(), 2); // HeaderCond + Update
assert!(matches!(skeleton.steps[0], SkeletonStep::HeaderCond { .. }));
assert!(matches!(skeleton.steps[1], SkeletonStep::Update { .. }));
// Verify carrier
assert_eq!(skeleton.carriers.len(), 1);
assert_eq!(skeleton.carriers[0].name, "start");
assert_eq!(skeleton.carriers[0].role, CarrierRole::Counter);
match &skeleton.carriers[0].update_kind {
UpdateKind::ConstStep { delta } => assert_eq!(*delta, 1),
_ => panic!("Expected ConstStep update"),
}
// Verify exit contract
assert!(skeleton.exits.has_break);
assert!(!skeleton.exits.has_continue);
assert!(!skeleton.exits.has_return);
}
#[test]
fn test_simple_continue_pattern_recognized() {
// Phase 142 P1: Test simple continue pattern
// Build: loop(i < n) { if is_even { i = i + 1; continue } sum = sum + i; i = i + 1 }
let loop_node = ASTNode::Loop {
condition: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Less,
left: Box::new(ASTNode::Variable {
name: "i".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Variable {
name: "n".to_string(),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
body: vec![
// if is_even { i = i + 1; continue }
ASTNode::If {
condition: Box::new(ASTNode::Variable {
name: "is_even".to_string(),
span: Span::unknown(),
}),
then_body: vec![
ASTNode::Assignment {
target: Box::new(ASTNode::Variable {
name: "i".to_string(),
span: Span::unknown(),
}),
value: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left: Box::new(ASTNode::Variable {
name: "i".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(1),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
span: Span::unknown(),
},
ASTNode::Continue {
span: Span::unknown(),
},
],
else_body: None,
span: Span::unknown(),
},
// sum = sum + i
ASTNode::Assignment {
target: Box::new(ASTNode::Variable {
name: "sum".to_string(),
span: Span::unknown(),
}),
value: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left: Box::new(ASTNode::Variable {
name: "sum".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Variable {
name: "i".to_string(),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
span: Span::unknown(),
},
// i = i + 1
ASTNode::Assignment {
target: Box::new(ASTNode::Variable {
name: "i".to_string(),
span: Span::unknown(),
}),
value: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left: Box::new(ASTNode::Variable {
name: "i".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(1),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
span: Span::unknown(),
},
],
span: Span::unknown(),
};
let result = canonicalize_loop_expr(&loop_node);
assert!(result.is_ok());
let (skeleton, decision) = result.unwrap();
// Verify success
assert!(decision.is_success());
// chosen == Pattern4Continue
assert_eq!(decision.chosen, Some(LoopPatternKind::Pattern4Continue));
// missing_caps == []
assert!(decision.missing_caps.is_empty());
// Verify skeleton structure
// HeaderCond + Body (sum = sum + i) + Update
assert!(skeleton.steps.len() >= 2);
assert!(matches!(skeleton.steps[0], SkeletonStep::HeaderCond { .. }));
// Verify carrier
assert_eq!(skeleton.carriers.len(), 1);
assert_eq!(skeleton.carriers[0].name, "i");
assert_eq!(skeleton.carriers[0].role, CarrierRole::Counter);
match &skeleton.carriers[0].update_kind {
UpdateKind::ConstStep { delta } => assert_eq!(*delta, 1),
_ => panic!("Expected ConstStep update"),
}
// Verify exit contract
assert!(!skeleton.exits.has_break);
assert!(skeleton.exits.has_continue);
assert!(!skeleton.exits.has_return);
}
#[test]
fn test_trim_trailing_pattern_recognized() {
// Phase 142 P0: Test trim_trailing pattern (end = end - 1)
// Build: loop(end > start) { if is_ws { end = end - 1 } else { break } }
let loop_node = ASTNode::Loop {
condition: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Greater,
left: Box::new(ASTNode::Variable {
name: "end".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Variable {
name: "start".to_string(),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
body: vec![ASTNode::If {
condition: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Equal,
left: Box::new(ASTNode::Variable {
name: "is_ws".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(1),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
then_body: vec![ASTNode::Assignment {
target: Box::new(ASTNode::Variable {
name: "end".to_string(),
span: Span::unknown(),
}),
value: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Subtract,
left: Box::new(ASTNode::Variable {
name: "end".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(1),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
span: Span::unknown(),
}],
else_body: Some(vec![ASTNode::Break {
span: Span::unknown(),
}]),
span: Span::unknown(),
}],
span: Span::unknown(),
};
let result = canonicalize_loop_expr(&loop_node);
assert!(result.is_ok());
let (skeleton, decision) = result.unwrap();
// Verify success
assert!(decision.is_success());
// chosen == Pattern2Break (ExitContract priority)
assert_eq!(decision.chosen, Some(LoopPatternKind::Pattern2Break));
// missing_caps == []
assert!(decision.missing_caps.is_empty());
// Verify skeleton structure
assert_eq!(skeleton.steps.len(), 2); // HeaderCond + Update
assert!(matches!(skeleton.steps[0], SkeletonStep::HeaderCond { .. }));
assert!(matches!(skeleton.steps[1], SkeletonStep::Update { .. }));
// Verify carrier
assert_eq!(skeleton.carriers.len(), 1);
assert_eq!(skeleton.carriers[0].name, "end");
assert_eq!(skeleton.carriers[0].role, CarrierRole::Counter);
match &skeleton.carriers[0].update_kind {
UpdateKind::ConstStep { delta } => assert_eq!(*delta, -1), // Negative step
_ => panic!("Expected ConstStep update"),
}
// Verify exit contract
assert!(skeleton.exits.has_break);
assert!(!skeleton.exits.has_continue);
assert!(!skeleton.exits.has_return);
}
#[test]
fn test_parse_number_pattern_recognized() {
// Phase 143-P0: Test parse_number pattern (break in THEN clause)
// Build: loop(i < len) { digit_pos = digits.indexOf(ch); if digit_pos < 0 { break } result = result + ch; i = i + 1 }
let loop_node = ASTNode::Loop {
condition: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Less,
left: Box::new(ASTNode::Variable {
name: "i".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Variable {
name: "len".to_string(),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
body: vec![
// Body statement: digit_pos = digits.indexOf(ch)
ASTNode::Assignment {
target: Box::new(ASTNode::Variable {
name: "digit_pos".to_string(),
span: Span::unknown(),
}),
value: Box::new(ASTNode::FunctionCall {
name: "indexOf".to_string(),
arguments: vec![ASTNode::Variable {
name: "ch".to_string(),
span: Span::unknown(),
}],
span: Span::unknown(),
}),
span: Span::unknown(),
},
// Break check: if digit_pos < 0 { break }
ASTNode::If {
condition: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Less,
left: Box::new(ASTNode::Variable {
name: "digit_pos".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(0),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
then_body: vec![ASTNode::Break {
span: Span::unknown(),
}],
else_body: None, // No else branch
span: Span::unknown(),
},
// Rest: result = result + ch
ASTNode::Assignment {
target: Box::new(ASTNode::Variable {
name: "result".to_string(),
span: Span::unknown(),
}),
value: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left: Box::new(ASTNode::Variable {
name: "result".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Variable {
name: "ch".to_string(),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
span: Span::unknown(),
},
// Carrier update: i = i + 1
ASTNode::Assignment {
target: Box::new(ASTNode::Variable {
name: "i".to_string(),
span: Span::unknown(),
}),
value: Box::new(ASTNode::BinaryOp {
operator: BinaryOperator::Add,
left: Box::new(ASTNode::Variable {
name: "i".to_string(),
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: LiteralValue::Integer(1),
span: Span::unknown(),
}),
span: Span::unknown(),
}),
span: Span::unknown(),
},
],
span: Span::unknown(),
};
let result = canonicalize_loop_expr(&loop_node);
assert!(result.is_ok());
let (skeleton, decision) = result.unwrap();
// Verify success
assert!(decision.is_success());
// chosen == Pattern2Break (has_break=true)
assert_eq!(decision.chosen, Some(LoopPatternKind::Pattern2Break));
// missing_caps == []
assert!(decision.missing_caps.is_empty());
// Verify skeleton structure
// HeaderCond + Body (digit_pos assignment) + Body (result assignment) + Update
assert!(skeleton.steps.len() >= 3);
assert!(matches!(skeleton.steps[0], SkeletonStep::HeaderCond { .. }));
// Verify carrier
assert_eq!(skeleton.carriers.len(), 1);
assert_eq!(skeleton.carriers[0].name, "i");
assert_eq!(skeleton.carriers[0].role, CarrierRole::Counter);
match &skeleton.carriers[0].update_kind {
UpdateKind::ConstStep { delta } => assert_eq!(*delta, 1),
_ => panic!("Expected ConstStep update"),
}
// Verify exit contract
assert!(skeleton.exits.has_break);
assert!(!skeleton.exits.has_continue);
assert!(!skeleton.exits.has_return);
assert!(!skeleton.exits.break_has_value);
}
}

View File

@ -4,7 +4,9 @@
//! Provides backward-compatible wrappers for existing callsites.
use crate::ast::ASTNode;
use crate::mir::{detect_skip_whitespace_pattern as ast_detect, SkipWhitespaceInfo};
use crate::mir::detect_continue_pattern;
use crate::mir::detect_parse_number_pattern as ast_detect_parse_number;
use crate::mir::detect_skip_whitespace_pattern as ast_detect;
// ============================================================================
// Skip Whitespace Pattern (Phase 140-P4-B SSOT Wrapper)
@ -36,6 +38,81 @@ pub fn try_extract_skip_whitespace_pattern(
ast_detect(body).map(|info| (info.carrier_name, info.delta, info.body_stmts))
}
// ============================================================================
// Parse Number Pattern (Phase 143-P0)
// ============================================================================
/// Try to extract parse_number pattern from loop
///
/// Pattern structure:
/// ```
/// loop(cond) {
/// // ... optional body statements (ch, digit_pos computation)
/// if invalid_cond {
/// break
/// }
/// // ... rest statements (result append, carrier update)
/// carrier = carrier + const
/// }
/// ```
///
/// Returns (carrier_name, delta, body_stmts, rest_stmts) if pattern matches.
///
/// # Phase 143-P0: Parse Number Pattern Detection
///
/// This function delegates to `ast_feature_extractor::detect_parse_number_pattern`
/// for SSOT implementation.
pub fn try_extract_parse_number_pattern(
body: &[ASTNode],
) -> Option<(String, i64, Vec<ASTNode>, Vec<ASTNode>)> {
ast_detect_parse_number(body).map(|info| {
(
info.carrier_name,
info.delta,
info.body_stmts,
info.rest_stmts,
)
})
}
// ============================================================================
// Continue Pattern (Phase 142-P1)
// ============================================================================
/// Try to extract continue pattern from loop
///
/// Pattern structure:
/// ```
/// loop(cond) {
/// // ... optional body statements (Body)
/// if skip_cond {
/// carrier = carrier + const // Optional update before continue
/// continue
/// }
/// // ... rest of body statements (Rest)
/// carrier = carrier + const // Carrier update
/// }
/// ```
///
/// Returns (carrier_name, delta, body_stmts, rest_stmts) if pattern matches.
///
/// # Phase 142-P1: Continue Pattern Detection
///
/// This function delegates to `ast_feature_extractor::detect_continue_pattern`
/// for SSOT implementation.
pub fn try_extract_continue_pattern(
body: &[ASTNode],
) -> Option<(String, i64, Vec<ASTNode>, Vec<ASTNode>)> {
detect_continue_pattern(body).map(|info| {
(
info.carrier_name,
info.delta,
info.body_stmts,
info.rest_stmts,
)
})
}
#[cfg(test)]
mod tests {
use super::*;

View File

@ -58,6 +58,10 @@ pub use builder::MirBuilder;
// Phase 140-P4-A: Re-export for loop_canonicalizer SSOT
pub(crate) use builder::{detect_skip_whitespace_pattern, SkipWhitespaceInfo};
// Phase 142-P1: Re-export continue pattern detection for loop_canonicalizer
pub(crate) use builder::{detect_continue_pattern, ContinuePatternInfo};
// Phase 143-P0: Re-export parse_number pattern detection for loop_canonicalizer
pub(crate) use builder::{detect_parse_number_pattern, ParseNumberInfo};
pub use cfg_extractor::extract_cfg_info; // Phase 154: CFG extraction
pub use definitions::{CallFlags, Callee, MirCall}; // Unified call definitions
pub use effect::{Effect, EffectMask};