Files
hakorune/docs/private/roadmap/phases/phase-20-variant-box/@match-macro-implementation-spec.md

1686 lines
38 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# @match Macro Implementation Specification
**✅ Status**: **Phase 19 完了2025-10-08** - 正規match構文として実装済み
**Created**: 2025-10-08
**Target Phase**: ~~Phase 20 (VariantBox)~~ **✅ Phase 19完了**
**Strategy**: ~~Choice A'' (macro-only)~~ **正規match式として実装**
**MIR Compatibility**: MIR16 Frozen (no new instructions) ✅
---
## 🎉 実装完了状況2025-10-08
-**match式 Parser**: `src/parser/expr/match_expr.rs` (392行)
-**Literal patterns**: 整数・文字列・bool リテラルマッチング対応
-**Type patterns**: Box型パターンマッチング対応
-**Guards**: `if` ガード条件対応
- ⚠️ **実装方式**: `@match`マクロではなく、正規`match`**式**として実装
- ⚠️ **脱糖化**: マクロではなくParser/MIR段階で直接処理
**実装場所**:
- Parser: `src/parser/expr/match_expr.rs` (392行)
- AST: `ASTNode::Match` variant
- MIR: if-elseチェインに直接変換
**使用例** (実装済み):
```hakorune
local result = match value {
0 => "zero"
1 => "one"
n if n > 10 => "large"
_ => "other"
}
```
---
## 📋 Table of Contents
1. [Design Goals](#design-goals)
2. [Parser Changes](#parser-changes)
3. [Macro Desugaring Rules](#macro-desugaring-rules)
4. [Pattern Types Support](#pattern-types-support)
5. [Edge Cases Handling](#edge-cases-handling)
6. [Test Suite Design](#test-suite-design)
7. [Implementation Task Breakdown](#implementation-task-breakdown)
8. [Integration with @enum](#integration-with-enum)
9. [Desugaring Algorithm](#desugaring-algorithm)
10. [Error Handling Design](#error-handling-design)
11. [Risk Analysis](#risk-analysis)
---
## 🎯 Design Goals
### Philosophy Alignment
-**Everything-is-Box**: Result/Option are existing Box types with `_tag`/`_value` fields
-**MIR16 Frozen**: Desugar to existing if/else/compare MIR instructions
-**Macro-Only**: No runtime support, pure AST transformation
-**Fail-Fast**: Runtime panic for non-exhaustive matches (compile-time check is future work)
### Technical Goals
- Pattern matching for Result/Option (existing boxes)
- Variable binding extraction (`Ok(v)``local v = result._value`)
- Exhaustiveness check via runtime panic
- Expression-level match (return value support)
---
## 🔧 Parser Changes
### File: `src/parser/mod.rs`
### 1. New AST Node Variant
Add to `ASTNode` enum in `src/ast.rs`:
```rust
/// Pattern match expression: @match expr { Pattern(vars) => body ... }
MatchPattern {
scrutinee: Box<ASTNode>,
arms: Vec<MatchArm>,
span: Span,
}
```
### 2. New Struct Definitions
```rust
/// Match arm: Pattern(x, y) => expression/block
#[derive(Debug, Clone, PartialEq)]
pub struct MatchArm {
pub pattern: Pattern,
pub body: Vec<ASTNode>, // Can be single expression or multi-statement block
pub span: Span,
}
/// Pattern types
#[derive(Debug, Clone, PartialEq)]
pub enum Pattern {
/// Variant pattern: Ok(v), Err(e), Some(x), None
Variant {
name: String, // "Ok", "Err", "Some", "None"
bindings: Vec<String>, // ["v"], ["e"], ["x"], []
span: Span,
},
/// Wildcard pattern: _
Wildcard {
span: Span,
},
}
```
### 3. Parser Method: `parse_match_pattern()`
Add to `src/parser/statements/mod.rs`:
```rust
fn parse_match_pattern(&mut self) -> Result<ASTNode, ParseError> {
let start = self.current_token().line;
// Consume @match
self.advance();
// Parse scrutinee expression
let scrutinee = Box::new(self.parse_expression()?);
// Expect {
self.expect(TokenType::LeftBrace)?;
// Parse match arms
let mut arms = Vec::new();
while !self.check(&TokenType::RightBrace) {
let arm = self.parse_match_arm()?;
arms.push(arm);
}
// Expect }
self.expect(TokenType::RightBrace)?;
// Validation: at least one arm required
if arms.is_empty() {
return Err(ParseError::InvalidExpression {
line: start,
message: "Match expression must have at least one arm".to_string()
});
}
Ok(ASTNode::MatchPattern {
scrutinee,
arms,
span: Span::unknown(),
})
}
fn parse_match_arm(&mut self) -> Result<MatchArm, ParseError> {
let start_line = self.current_token().line;
// Parse pattern
let pattern = self.parse_pattern()?;
// Expect =>
self.expect(TokenType::FatArrow)?; // => operator
// Parse body (can be single expression or block with {})
let body = if self.check(&TokenType::LeftBrace) {
// Multi-statement block
self.advance(); // consume {
let mut statements = Vec::new();
while !self.check(&TokenType::RightBrace) {
statements.push(self.parse_statement()?);
}
self.expect(TokenType::RightBrace)?;
statements
} else {
// Single expression (implicit return)
vec![self.parse_expression()?]
};
Ok(MatchArm {
pattern,
body,
span: Span::unknown(),
})
}
fn parse_pattern(&mut self) -> Result<Pattern, ParseError> {
let start_line = self.current_token().line;
match &self.current_token().token_type {
// Wildcard: _
TokenType::Underscore => {
self.advance();
Ok(Pattern::Wildcard {
span: Span::unknown(),
})
}
// Variant pattern: Ok(v), Err(e), Some(x), None
TokenType::Identifier(name) => {
let variant_name = name.clone();
self.advance();
// Check for bindings: (x, y, z)
let bindings = if self.check(&TokenType::LeftParen) {
self.advance(); // consume (
let mut vars = Vec::new();
while !self.check(&TokenType::RightParen) {
if let TokenType::Identifier(var) = &self.current_token().token_type {
vars.push(var.clone());
self.advance();
// Optional comma
if self.check(&TokenType::Comma) {
self.advance();
}
} else {
return Err(ParseError::ExpectedIdentifier { line: start_line });
}
}
self.expect(TokenType::RightParen)?;
vars
} else {
Vec::new() // 0-field variant (e.g., None)
};
Ok(Pattern::Variant {
name: variant_name,
bindings,
span: Span::unknown(),
})
}
_ => Err(ParseError::InvalidExpression {
line: start_line,
message: "Expected pattern (variant or wildcard)".to_string(),
})
}
}
```
### 4. Token Type Addition
Add to `src/tokenizer.rs`:
```rust
pub enum TokenType {
// ... existing tokens ...
/// => (fat arrow for match arms)
FatArrow,
/// _ (wildcard pattern)
Underscore,
/// @match, @enum, etc.
MacroKeyword(String),
}
```
### 5. Tokenizer Update
Modify `src/tokenizer.rs` to recognize:
- `@match` as `MacroKeyword("match")`
- `=>` as `FatArrow`
- `_` as `Underscore` (if not already present)
---
## 📝 Macro Desugaring Rules
### Core Principle
**Every @match transforms to a chain of if/else with field access and runtime exhaustiveness check.**
### Example 1: Basic Result Matching
**Input**:
```hakorune
@match result {
Ok(v) => print("Success: " + v)
Err(e) => print("Error: " + e)
}
```
**Desugared Output**:
```hakorune
if result._ok == 1 {
local v
v = result._val
print("Success: " + v)
} else if result._ok == 0 {
local e
e = result._err
print("Error: " + e)
} else {
print("[PANIC] non-exhaustive match on Result: unknown state")
}
```
**Implementation Notes**:
- Result uses `_ok` (1=Ok, 0=Err) instead of `_tag` string
- Field mapping: `Ok``_val`, `Err``_err`
- Exhaustiveness: catch-all else for safety
### Example 2: Option Matching with Wildcard
**Input**:
```hakorune
@match option {
Some(x) => return x * 2
None => return 0
}
```
**Desugared Output**:
```hakorune
if option._is_some == 1 {
local x
x = option._value
return x * 2
} else if option._is_some == 0 {
return 0
} else {
print("[PANIC] non-exhaustive match on Option: unknown state")
}
```
**Implementation Notes**:
- Option uses `_is_some` (1=Some, 0=None) instead of `_tag`
- Field mapping: `Some``_value`, `None` → (no field)
- Exhaustiveness: still required for robustness
### Example 3: Generic VariantBox (Future @enum)
**Input**:
```hakorune
@enum Point {
Cartesian(x, y)
Polar(r, theta)
}
@match point {
Cartesian(x, y) => print("x=" + x + " y=" + y)
Polar(r, theta) => print("r=" + r + " theta=" + theta)
}
```
**Desugared Output**:
```hakorune
if point._tag == "Cartesian" {
local x
local y
x = point._x
y = point._y
print("x=" + x + " y=" + y)
} else if point._tag == "Polar" {
local r
local theta
r = point._r
theta = point._theta
print("r=" + r + " theta=" + theta)
} else {
print("[PANIC] non-exhaustive match on Point: " + point._tag)
}
```
**Implementation Notes**:
- Generic VariantBox uses string `_tag` field
- Field mapping: `Variant(a, b)``_a`, `_b` (lowercase field names)
- Exhaustiveness: show actual unknown tag in panic message
### Example 4: Match as Expression (Return Value)
**Input**:
```hakorune
local result
result = @match status {
Success(val) => val * 2
Failure(msg) => 0
}
print(result)
```
**Desugared Output**:
```hakorune
local result
local __match_tmp
if status._tag == "Success" {
local val
val = status._val
__match_tmp = val * 2
} else if status._tag == "Failure" {
local msg
msg = status._msg
__match_tmp = 0
} else {
print("[PANIC] non-exhaustive match on Status: " + status._tag)
__match_tmp = null
}
result = __match_tmp
print(result)
```
**Implementation Notes**:
- Introduce temporary variable `__match_tmp` to hold result
- Each arm assigns to `__match_tmp` instead of returning
- Final assignment outside if/else chain
- Exhaustiveness fallback assigns `null`
### Example 5: Nested Blocks with Multiple Statements
**Input**:
```hakorune
@match result {
Ok(v) => {
local doubled
doubled = v * 2
print("Result: " + doubled)
return doubled
}
Err(e) => {
print("Error: " + e)
return -1
}
}
```
**Desugared Output**:
```hakorune
if result._ok == 1 {
local v
v = result._val
local doubled
doubled = v * 2
print("Result: " + doubled)
return doubled
} else if result._ok == 0 {
local e
e = result._err
print("Error: " + e)
return -1
} else {
print("[PANIC] non-exhaustive match on Result: unknown state")
}
```
**Implementation Notes**:
- Multi-statement blocks preserved as-is
- Variable bindings inserted at top of block
- No wrapping needed (already in block context)
---
## 🔍 Pattern Types Support
### Phase 1 (MVP Implementation)
| Pattern Type | Syntax | Example | Status |
|--------------|--------|---------|--------|
| **Variant 0-field** | `Name` | `None` | ✅ MVP |
| **Variant 1-field** | `Name(x)` | `Ok(v)`, `Some(x)` | ✅ MVP |
| **Variant N-field** | `Name(x, y, ...)` | `Cartesian(x, y)` | ✅ MVP |
| **Wildcard** | `_` | `_ => default` | ✅ MVP |
### Phase 2 (Future Enhancements)
| Pattern Type | Syntax | Example | Status |
|--------------|--------|---------|--------|
| **Literal** | `42`, `"str"` | `42 => ...` | ❌ Future |
| **Guard Clause** | `pattern if cond` | `Some(x) if x > 10` | ❌ Future |
| **Nested Pattern** | `Outer(Inner(x))` | `Some(Ok(v))` | ❌ Future |
| **Multiple Patterns** | `A \| B` | `Ok(v) \| Some(v)` | ❌ Future |
**MVP Focus**: Variant patterns only (sufficient for Result/Option/VariantBox)
---
## ⚠️ Edge Cases Handling
### 1. Match as Expression
**Issue**: Match must produce a value for assignment
**Solution**: Introduce temporary variable pattern (see Example 4 above)
**Test Case**:
```hakorune
local result = @match opt {
Some(x) => x * 2
None => 0
}
assert(result == (opt._is_some == 1 ? opt._value * 2 : 0))
```
### 2. Single Pattern Match
**Issue**: Single arm still needs exhaustiveness check
**Solution**: Always add catch-all else clause
**Input**:
```hakorune
@match result {
Ok(v) => print(v)
}
```
**Desugared**:
```hakorune
if result._ok == 1 {
local v
v = result._val
print(v)
} else {
print("[PANIC] non-exhaustive match: missing Err case")
}
```
### 3. Empty Match
**Issue**: Match with zero arms is invalid
**Solution**: Parser error at parse time
**Error Message**:
```
[ERROR] Match expression must have at least one arm at line 42
```
### 4. Duplicate Patterns
**Issue**: Two arms with same pattern
**Input**:
```hakorune
@match result {
Ok(v) => print("first")
Ok(x) => print("second") // ❌ Duplicate
}
```
**Solution**: Warning (first arm wins, second unreachable)
**Warning Message**:
```
[WARN] Unreachable pattern 'Ok' at line 43 (already matched at line 42)
```
### 5. Variable Shadowing
**Issue**: Pattern variable shadows local variable
**Input**:
```hakorune
local v = 10
@match result {
Ok(v) => print(v) // Different 'v'
}
print(v) // Original 'v' = 10
```
**Solution**: Each arm introduces new scope (via if block)
**Desugared**:
```hakorune
local v = 10
if result._ok == 1 {
local v // New scope, shadows outer v
v = result._val
print(v)
}
print(v) // Original v still 10
```
### 6. Match on Non-Enum Value
**Issue**: Match on primitive type (IntegerBox, StringBox)
**Input**:
```hakorune
@match 42 {
Ok(v) => print(v) // ❌ 42 has no _ok field
}
```
**Solution**: Runtime error (field access fails)
**Alternative**: Static check (Phase 2, requires type system)
### 7. Early Return in Match Arm
**Issue**: Return statement inside match arm
**Input**:
```hakorune
local compute() {
@match result {
Ok(v) => return v
Err(e) => return -1
}
}
```
**Desugared**:
```hakorune
local compute() {
if result._ok == 1 {
local v
v = result._val
return v // ✅ Works (early exit from function)
} else if result._ok == 0 {
local e
e = result._err
return -1
} else {
print("[PANIC] non-exhaustive match on Result")
}
}
```
**Result**: Works correctly (return exits function)
### 8. Match Inside Loop with Break/Continue
**Input**:
```hakorune
loop(i < 10) {
@match opt {
Some(x) => {
if x > 5 {
break
}
continue
}
None => i = i + 1
}
}
```
**Desugared**:
```hakorune
loop(i < 10) {
if opt._is_some == 1 {
local x
x = opt._value
if x > 5 {
break // ✅ Breaks from loop
}
continue // ✅ Continues loop
} else if opt._is_some == 0 {
i = i + 1
} else {
print("[PANIC] non-exhaustive match on Option")
}
}
```
**Result**: Works correctly (break/continue affect loop)
---
## 🧪 Test Suite Design
### Minimum 15 Test Patterns
### Basic Matching (5 tests)
#### Test 1: Result 2-Variant Match
```hakorune
// apps/tests/match/test_result_basic.hako
using "../../lib/boxes/result.hako"
static box Main {
main() {
local r = Result.ok(42)
local result = @match r {
Ok(v) => v * 2
Err(e) => -1
}
assert(result == 84)
print("PASS: Result basic match")
}
}
```
#### Test 2: Option 2-Variant Match
```hakorune
// apps/tests/match/test_option_basic.hako
using "../../lib/boxes/option.hako"
static box Main {
main() {
local opt = Option.some(10)
@match opt {
Some(x) => print("Value: " + x)
None => print("No value")
}
print("PASS: Option basic match")
}
}
```
#### Test 3: 3+ Variant Enum Match (Future VariantBox)
```hakorune
// apps/tests/match/test_variant_3way.hako
@enum Status {
Pending
Success(result)
Failure(error)
}
static box Main {
main() {
local s = Status.Success("Done")
@match s {
Pending => print("Waiting...")
Success(r) => print("OK: " + r)
Failure(e) => print("Error: " + e)
}
print("PASS: 3-way variant match")
}
}
```
#### Test 4: Match with Return Values
```hakorune
// apps/tests/match/test_match_expression.hako
using "../../lib/boxes/result.hako"
static box Main {
compute(r) {
return @match r {
Ok(v) => v * 2
Err(e) => 0
}
}
main() {
local r1 = Result.ok(21)
local r2 = Result.err("fail")
assert(me.compute(r1) == 42)
assert(me.compute(r2) == 0)
print("PASS: Match as expression")
}
}
```
#### Test 5: Match with Multi-Statement Blocks
```hakorune
// apps/tests/match/test_match_blocks.hako
using "../../lib/boxes/option.hako"
static box Main {
main() {
local opt = Option.some(5)
local result = 0
@match opt {
Some(x) => {
local doubled = x * 2
local tripled = doubled + x
result = tripled
print("Computed: " + result)
}
None => {
result = -1
print("No value")
}
}
assert(result == 15)
print("PASS: Multi-statement blocks")
}
}
```
### Variable Binding (3 tests)
#### Test 6: Single Variable Binding
```hakorune
// apps/tests/match/test_binding_single.hako
using "../../lib/boxes/result.hako"
static box Main {
main() {
local r = Result.ok("hello")
@match r {
Ok(msg) => {
assert(msg == "hello")
print("PASS: Single variable binding")
}
Err(e) => print("Error: " + e)
}
}
}
```
#### Test 7: Multi-Variable Binding
```hakorune
// apps/tests/match/test_binding_multi.hako
@enum Point {
Cartesian(x, y)
Polar(r, theta)
}
static box Main {
main() {
local p = Point.Cartesian(3, 4)
@match p {
Cartesian(x, y) => {
assert(x == 3)
assert(y == 4)
print("PASS: Multi-variable binding")
}
Polar(r, theta) => print("Polar")
}
}
}
```
#### Test 8: Variable Shadowing Test
```hakorune
// apps/tests/match/test_shadowing.hako
using "../../lib/boxes/option.hako"
static box Main {
main() {
local x = 100
local opt = Option.some(42)
@match opt {
Some(x) => {
// Inner x = 42
assert(x == 42)
}
None => print("None")
}
// Outer x still 100
assert(x == 100)
print("PASS: Variable shadowing")
}
}
```
### Exhaustiveness (3 tests)
#### Test 9: Exhaustive Match (All Variants)
```hakorune
// apps/tests/match/test_exhaustive_ok.hako
using "../../lib/boxes/result.hako"
static box Main {
main() {
local r = Result.ok(42)
local handled = 0
@match r {
Ok(v) => handled = 1
Err(e) => handled = 2
}
assert(handled == 1)
print("PASS: Exhaustive match")
}
}
```
#### Test 10: Non-Exhaustive Match (Runtime Panic)
```hakorune
// apps/tests/match/test_exhaustive_fail.hako
using "../../lib/boxes/result.hako"
static box Main {
main() {
// Manually create invalid Result state
local r = new ResultBox()
r._ok = 99 // Invalid state
// This should trigger panic
@match r {
Ok(v) => print("OK")
Err(e) => print("Err")
}
// Expected output: [PANIC] non-exhaustive match on Result: unknown state
}
}
```
#### Test 11: Wildcard Pattern Exhaustiveness
```hakorune
// apps/tests/match/test_wildcard.hako
using "../../lib/boxes/option.hako"
static box Main {
main() {
local opt = Option.none()
local result = @match opt {
Some(x) => x
_ => -1 // Wildcard catches None and any invalid states
}
assert(result == -1)
print("PASS: Wildcard pattern")
}
}
```
### Edge Cases (4 tests)
#### Test 12: Match on 0-Field Variant
```hakorune
// apps/tests/match/test_0field_variant.hako
using "../../lib/boxes/option.hako"
static box Main {
main() {
local opt = Option.none()
@match opt {
Some(x) => print("Value: " + x)
None => print("PASS: 0-field variant")
}
}
}
```
#### Test 13: Nested If-Else Inside Match Arm
```hakorune
// apps/tests/match/test_nested_if.hako
using "../../lib/boxes/result.hako"
static box Main {
main() {
local r = Result.ok(10)
@match r {
Ok(v) => {
if v > 5 {
print("Large: " + v)
} else {
print("Small: " + v)
}
}
Err(e) => print("Error: " + e)
}
print("PASS: Nested if-else")
}
}
```
#### Test 14: Match Inside Loop with Break/Continue
```hakorune
// apps/tests/match/test_loop_control.hako
using "../../lib/boxes/option.hako"
static box Main {
main() {
local i = 0
local found = 0
loop(i < 10) {
local opt = (i == 5) ? Option.some(i) : Option.none()
@match opt {
Some(x) => {
found = x
break
}
None => i = i + 1
}
i = i + 1
}
assert(found == 5)
print("PASS: Loop control with match")
}
}
```
#### Test 15: Match with Early Return in Each Arm
```hakorune
// apps/tests/match/test_early_return.hako
using "../../lib/boxes/result.hako"
static box Main {
compute(r) {
@match r {
Ok(v) => return v * 2
Err(e) => return -1
}
// This line is unreachable
}
main() {
local r1 = Result.ok(21)
local r2 = Result.err("fail")
assert(me.compute(r1) == 42)
assert(me.compute(r2) == -1)
print("PASS: Early return in arms")
}
}
```
---
## 📅 Implementation Task Breakdown
### 6-Day Implementation Plan
#### Day 1: Parser Infrastructure (8 hours)
- [ ] Add `MatchPattern` AST node variant
- [ ] Add `Pattern` and `MatchArm` struct definitions
- [ ] Implement `parse_match_pattern()` method
- [ ] Implement `parse_match_arm()` method
- [ ] Implement `parse_pattern()` method
- [ ] Add `FatArrow` and `Underscore` token types
- [ ] Update tokenizer to recognize `@match`, `=>`, `_`
- [ ] **Milestone**: Parser can parse @match syntax without errors
#### Day 2: Pattern Analysis & Tag Comparison (8 hours)
- [ ] Create field mapping registry (Pattern → field names)
- Result: `Ok``_ok==1, _val`, `Err``_ok==0, _err`
- Option: `Some``_is_some==1, _value`, `None``_is_some==0`
- VariantBox: `Tag``_tag=="Tag", _field0, _field1, ...`
- [ ] Implement tag comparison code generation
- [ ] Generate if-condition AST nodes for each pattern
- [ ] **Milestone**: Can generate if/else skeleton for patterns
#### Day 3: Variable Binding Extraction (8 hours)
- [ ] Implement binding extraction (`Variant(x, y)` → field names)
- [ ] Generate `local` declaration AST nodes
- [ ] Generate field access AST nodes (`result._val`, etc.)
- [ ] Insert binding code at top of each arm
- [ ] Handle 0-field variants (no bindings)
- [ ] **Milestone**: Variable bindings work correctly
#### Day 4: Exhaustiveness Check & Panic Generation (8 hours)
- [ ] Generate catch-all else clause AST
- [ ] Generate panic print statement
- [ ] Implement exhaustiveness check logic
- [ ] Add tag name to panic message (for VariantBox)
- [ ] Handle match-as-expression case (assign null in panic)
- [ ] **Milestone**: Exhaustiveness panic works
#### Day 5: Test Suite - Basic & Variable Binding (8 hours)
- [ ] Write Test 1-5 (Basic Matching)
- [ ] Write Test 6-8 (Variable Binding)
- [ ] Run tests, fix issues
- [ ] Verify desugared code with `--dump-mir`
- [ ] **Milestone**: 8/15 tests passing
#### Day 6: Test Suite - Exhaustiveness & Edge Cases (8 hours)
- [ ] Write Test 9-11 (Exhaustiveness)
- [ ] Write Test 12-15 (Edge Cases)
- [ ] Run full test suite
- [ ] Document known issues/limitations
- [ ] Add smoke tests to CI
- [ ] **Milestone**: 15/15 tests passing, MVP complete
---
## 🔗 Integration with @enum
### Phase 1 (MVP): Hardcoded Field Mappings
**Strategy**: Use fixed field mapping for existing boxes (Result, Option)
```rust
// In desugaring logic
fn get_field_mapping(variant: &str, index: usize) -> String {
match variant {
// Result
"Ok" => if index == 0 { "_val" } else { panic!("Ok has 1 field") },
"Err" => if index == 0 { "_err" } else { panic!("Err has 1 field") },
// Option
"Some" => if index == 0 { "_value" } else { panic!("Some has 1 field") },
"None" => panic!("None has 0 fields"),
// Unknown: assume generic VariantBox convention
_ => format!("_{}", index), // _0, _1, _2, ...
}
}
fn get_tag_check(variant: &str) -> (String, String) {
match variant {
// Result: check _ok field
"Ok" => ("_ok".to_string(), "1".to_string()),
"Err" => ("_ok".to_string(), "0".to_string()),
// Option: check _is_some field
"Some" => ("_is_some".to_string(), "1".to_string()),
"None" => ("_is_some".to_string(), "0".to_string()),
// Unknown: assume generic _tag field
_ => ("_tag".to_string(), format!("\"{}\"", variant)),
}
}
```
### Phase 2 (Future): @enum Integration
**When @enum is implemented**:
1. @enum generates EnumSchemaBox metadata
2. @match queries EnumSchemaBox for field names
3. Compile-time exhaustiveness check possible
**Example**:
```hakorune
@enum Status {
Pending
Success(result)
Failure(error, code)
}
// EnumSchemaBox generated:
// Status._schema.variants = {
// "Pending": { arity: 0, fields: [] },
// "Success": { arity: 1, fields: ["result"] },
// "Failure": { arity: 2, fields: ["error", "code"] }
// }
@match status {
Pending => ...
Success(r) => ...
// ❌ Missing Failure → Compile-time error (Phase 2)
}
```
**Exhaustiveness Check Algorithm (Phase 2)**:
```rust
fn check_exhaustiveness(scrutinee_type: &str, arms: &[MatchArm]) -> Result<(), String> {
// 1. Get enum schema
let schema = get_enum_schema(scrutinee_type)?;
// 2. Collect covered variants
let mut covered = HashSet::new();
let mut has_wildcard = false;
for arm in arms {
match &arm.pattern {
Pattern::Variant { name, .. } => { covered.insert(name); }
Pattern::Wildcard { .. } => { has_wildcard = true; }
}
}
// 3. Check if all variants covered
if !has_wildcard {
for variant in schema.variants.keys() {
if !covered.contains(variant) {
return Err(format!("Missing pattern: {}", variant));
}
}
}
Ok(())
}
```
---
## 🛠️ Desugaring Algorithm
### High-Level Algorithm
```
INPUT: MatchPattern { scrutinee, arms }
OUTPUT: If { condition, then_body, else_body }
ALGORITHM:
1. Generate temporary variable (if match is expression)
2. For each arm in arms:
a. Extract pattern and body
b. Generate tag comparison condition
c. Generate variable bindings (local declarations)
d. Prepend bindings to body
e. If expression: wrap body with assignment to temp var
3. Chain arms into if/else-if/else structure
4. Add exhaustiveness panic as final else clause
5. If expression: append temp var access after if/else
```
### Pseudo-Code Implementation
```rust
fn desugar_match(
scrutinee: &ASTNode,
arms: &[MatchArm],
is_expression: bool
) -> ASTNode {
// Step 1: Temp var for expression case
let temp_var = if is_expression {
Some(format!("__match_tmp_{}", unique_id()))
} else {
None
};
// Step 2: Build if/else chain
let mut current_else: Option<Vec<ASTNode>> = Some(vec![
// Final else: exhaustiveness panic
ASTNode::Print {
expression: Box::new(ASTNode::Literal {
value: LiteralValue::String(
"[PANIC] non-exhaustive match".to_string()
),
span: Span::unknown(),
}),
span: Span::unknown(),
}
]);
// Build chain in reverse order (last arm first)
for arm in arms.iter().rev() {
// Step 3a: Extract pattern
let (tag_field, tag_value) = get_tag_check(&arm.pattern);
// Step 3b: Generate condition
let condition = ASTNode::BinaryOp {
operator: BinaryOperator::Equals,
left: Box::new(ASTNode::FieldAccess {
object: Box::new(scrutinee.clone()),
field: tag_field,
span: Span::unknown(),
}),
right: Box::new(ASTNode::Literal {
value: tag_value,
span: Span::unknown(),
}),
span: Span::unknown(),
};
// Step 3c: Generate bindings
let bindings = generate_bindings(
scrutinee,
&arm.pattern
);
// Step 3d: Build then_body
let mut then_body = bindings;
if let Some(ref temp) = temp_var {
// Expression case: wrap body with assignment
let value = if arm.body.len() == 1 {
arm.body[0].clone()
} else {
// Multi-statement: last statement is value
arm.body.last().unwrap().clone()
};
then_body.extend(arm.body[..arm.body.len()-1].to_vec());
then_body.push(ASTNode::Assignment {
target: Box::new(ASTNode::Variable {
name: temp.clone(),
span: Span::unknown(),
}),
value: Box::new(value),
span: Span::unknown(),
});
} else {
// Statement case: just append body
then_body.extend(arm.body.clone());
}
// Step 3e: Create if node
current_else = Some(vec![ASTNode::If {
condition: Box::new(condition),
then_body,
else_body: current_else,
span: Span::unknown(),
}]);
}
// Step 4: Return final structure
if let Some(temp) = temp_var {
// Expression: declare temp, if/else, return temp
let mut result = vec![
ASTNode::Local {
variables: vec![temp.clone()],
span: Span::unknown(),
},
];
result.extend(current_else.unwrap());
result.push(ASTNode::Variable {
name: temp,
span: Span::unknown(),
});
ASTNode::Program {
statements: result,
span: Span::unknown(),
}
} else {
// Statement: just if/else
current_else.unwrap()[0].clone()
}
}
fn generate_bindings(
scrutinee: &ASTNode,
pattern: &Pattern
) -> Vec<ASTNode> {
match pattern {
Pattern::Variant { name, bindings, .. } => {
let mut result = Vec::new();
for (i, var) in bindings.iter().enumerate() {
let field_name = get_field_mapping(name, i);
// local var
result.push(ASTNode::Local {
variables: vec![var.clone()],
span: Span::unknown(),
});
// var = scrutinee._field
result.push(ASTNode::Assignment {
target: Box::new(ASTNode::Variable {
name: var.clone(),
span: Span::unknown(),
}),
value: Box::new(ASTNode::FieldAccess {
object: Box::new(scrutinee.clone()),
field: field_name,
span: Span::unknown(),
}),
span: Span::unknown(),
});
}
result
}
Pattern::Wildcard { .. } => {
Vec::new() // No bindings
}
}
}
```
---
## ⚠️ Error Handling Design
### Parser Errors
#### 1. Pattern Syntax Error
**Input**:
```hakorune
@match result {
Ok(v => // ❌ Missing )
}
```
**Error Message**:
```
[ERROR] Expected ')' at line 2, found '=>'
Context: Parsing variant pattern 'Ok'
```
#### 2. Unknown Variant (Future, with @enum)
**Input**:
```hakorune
@match result {
Success(v) => ... // ❌ Result has Ok/Err, not Success
}
```
**Error Message**:
```
[ERROR] Unknown variant 'Success' for type 'Result' at line 2
Available variants: Ok(value), Err(error)
Help: Did you mean 'Ok'?
```
#### 3. Duplicate Pattern
**Input**:
```hakorune
@match result {
Ok(v) => print("first")
Ok(x) => print("second") // ❌ Duplicate
}
```
**Warning Message**:
```
[WARN] Unreachable pattern 'Ok' at line 3
Note: This pattern is already matched at line 2
```
#### 4. Type Mismatch (Future, with type system)
**Input**:
```hakorune
@match 42 {
Ok(v) => ... // ❌ 42 is IntegerBox, not Result
}
```
**Error Message**:
```
[ERROR] Type mismatch in match expression at line 1
Expected: Result, Option, or VariantBox
Found: IntegerBox
```
#### 5. Missing Fat Arrow
**Input**:
```hakorune
@match result {
Ok(v) print(v) // ❌ Missing =>
}
```
**Error Message**:
```
[ERROR] Expected '=>' at line 2, found 'print'
Context: After pattern 'Ok(v)'
```
#### 6. Empty Match Arms
**Input**:
```hakorune
@match result {
}
```
**Error Message**:
```
[ERROR] Match expression must have at least one arm at line 1
```
### Runtime Errors
#### 1. Non-Exhaustive Match Panic
**Trigger**: Unknown variant tag at runtime
**Output**:
```
[PANIC] non-exhaustive match on Result: unknown state
```
**For VariantBox with _tag**:
```
[PANIC] non-exhaustive match on Point: InvalidTag
```
#### 2. Field Access Error (Arity Mismatch)
**Trigger**: Pattern has wrong number of bindings
**Input** (if validation skipped):
```hakorune
@match result {
Ok(v, x) => ... // ❌ Ok has 1 field, not 2
}
```
**Runtime Error**:
```
[ERROR] Field access failed: result._field1 does not exist
```
**Future Prevention**: Static arity check (Phase 2)
---
## 🚨 Risk Analysis
### 1. Parser Conflicts with Existing Syntax
**Risk**: `=>` arrow operator may conflict with other syntax
**Mitigation**:
- Check existing uses of `>` and `=` tokens
- Ensure tokenizer prioritizes `=>` as single token
- Run full parser test suite
**Impact**: Medium (parser failure)
### 2. Pattern Variable Naming Conflicts
**Risk**: Pattern variable shadows important local/field
**Mitigation**:
- Each if arm creates new scope (natural shadowing)
- Warning for shadowing (optional)
- Document scoping rules
**Impact**: Low (expected behavior)
### 3. Exhaustiveness Check False Positives/Negatives
**Risk**: Runtime panic when all cases actually covered
**False Negative Example**:
```hakorune
// All cases covered, but panic still present
@match result {
Ok(v) => ...
Err(e) => ...
}
// Else clause still added: panic on invalid state
```
**Mitigation**:
- Phase 1: Always add panic (safe but verbose)
- Phase 2: Static analysis to remove panic if proven exhaustive
**Impact**: Low (extra safety, no correctness issue)
**False Positive Risk**: None (panic only triggers on actual invalid state)
### 4. Performance of If/Else Chain vs Jump Table
**Risk**: Long match arms → slow O(n) check instead of O(1) jump
**Benchmark**:
```hakorune
@match status {
Case1 => ...
Case2 => ...
// ... 100 cases
Case100 => ...
}
// Worst case: 100 comparisons
```
**Mitigation**:
- Phase 1: Accept O(n) for simplicity
- Phase 2: Optimize to jump table (requires MIR switch instruction)
**Impact**: Low (typical match has 2-5 arms)
### 5. Debugging Challenges (Pattern → Field Mapping)
**Risk**: Hard to trace which field maps to which variable
**Example Debug Scenario**:
```hakorune
@match point {
Cartesian(x, y) => print(x) // Which field is 'x'?
}
```
**Mitigation**:
- Clear documentation of field naming convention
- `--dump-mir` flag shows desugared code
- Error messages show field names
**Example Desugared (for debugging)**:
```hakorune
if point._tag == "Cartesian" {
local x // Maps to point._x
local y // Maps to point._y
x = point._x
y = point._y
print(x)
}
```
**Impact**: Medium (developer experience)
### 6. Integration Risks with @enum
**Risk**: @match implemented before @enum, field mappings hardcoded
**Mitigation**:
- Phase 1: Hardcode Result/Option mappings
- Phase 1.5: Assume generic VariantBox convention (`_tag`, `_0`, `_1`, ...)
- Phase 2: Replace with EnumSchemaBox query
**Impact**: Low (phased approach, backwards compatible)
### 7. Macro Expansion Order Issues
**Risk**: @match expands before @enum, can't find schema
**Mitigation**:
- Phase 1: @match doesn't depend on @enum (hardcoded mappings)
- Phase 2: Macro dependency ordering system
**Impact**: None (Phase 1 independent)
### 8. AST Size Explosion
**Risk**: Large match → large if/else AST → slow compilation
**Example**:
```hakorune
@match value {
Case1 => { /* 100 lines */ }
Case2 => { /* 100 lines */ }
// ... 50 cases
}
// Desugared AST: 5000+ nodes
```
**Mitigation**:
- Accept for Phase 1 (typical case: 2-5 arms)
- Optimize AST representation (Phase 2)
**Impact**: Low (rare in practice)
---
## 📊 Success Metrics
### MVP Completion Criteria
- ✅ All 15 test cases pass
- ✅ Result/Option matching works correctly
- ✅ Variable bindings extract fields properly
- ✅ Exhaustiveness panic triggers on invalid states
- ✅ Match-as-expression works (returns value)
- ✅ No regressions in existing tests
- ✅ Documentation complete (this spec + user guide)
### Performance Benchmarks (Optional)
- Match with 2 arms: <1ms overhead vs hand-written if/else
- Match with 10 arms: <5ms overhead
- Compilation time: <10% increase for typical codebase
### Quality Metrics
- Code coverage: >90% for match desugaring logic
- Zero parser crashes on valid input
- Clear error messages for all invalid inputs
---
## 📚 Related Documentation
- **VariantBox Design**: [DESIGN.md](./DESIGN.md)
- **@enum Macro Spec**: (To be created)
- **Result/Option API**: [apps/lib/boxes/result.hako](../../../apps/lib/boxes/result.hako), [option.hako](../../../apps/lib/boxes/option.hako)
- **Macro System**: [docs/private/roadmap/phases/phase-16-macro-revolution/](../phase-16-macro-revolution/)
---
## 🎯 Next Steps After MVP
### Phase 20.1: @enum Macro
- Implement @enum desugaring to static box + VariantBox
- Generate EnumSchemaBox metadata
- Connect @match to schema for field name resolution
### Phase 20.2: Compile-Time Exhaustiveness Check
- Implement exhaustiveness checker
- Remove runtime panic for proven-exhaustive matches
- Add warnings for unreachable patterns
### Phase 20.3: Advanced Patterns
- Literal patterns (`42 => ...`)
- Guard clauses (`Some(x) if x > 10`)
- Nested patterns (`Some(Ok(v))`)
- Multiple patterns (`Ok(v) | Some(v)`)
---
**Document Status**: ✅ Complete, ready for implementation
**Estimated Effort**: 6 person-days (48 hours)
**Dependencies**: None (self-contained)
**Blocking**: None (can start immediately)