38 KiB
@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::Matchvariant - MIR: if-elseチェインに直接変換
使用例 (実装済み):
local result = match value {
0 => "zero"
1 => "one"
n if n > 10 => "large"
_ => "other"
}
📋 Table of Contents
- Design Goals
- Parser Changes
- Macro Desugaring Rules
- Pattern Types Support
- Edge Cases Handling
- Test Suite Design
- Implementation Task Breakdown
- Integration with @enum
- Desugaring Algorithm
- Error Handling Design
- Risk Analysis
🎯 Design Goals
Philosophy Alignment
- ✅ Everything-is-Box: Result/Option are existing Box types with
_tag/_valuefields - ✅ 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:
/// Pattern match expression: @match expr { Pattern(vars) => body ... }
MatchPattern {
scrutinee: Box<ASTNode>,
arms: Vec<MatchArm>,
span: Span,
}
2. New Struct Definitions
/// 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:
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:
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:
@matchasMacroKeyword("match")=>asFatArrow_asUnderscore(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:
@match result {
Ok(v) => print("Success: " + v)
Err(e) => print("Error: " + e)
}
Desugared Output:
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_tagstring - Field mapping:
Ok→_val,Err→_err - Exhaustiveness: catch-all else for safety
Example 2: Option Matching with Wildcard
Input:
@match option {
Some(x) => return x * 2
None => return 0
}
Desugared Output:
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:
@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:
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
_tagfield - 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:
local result
result = @match status {
Success(val) => val * 2
Failure(msg) => 0
}
print(result)
Desugared Output:
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_tmpto hold result - Each arm assigns to
__match_tmpinstead of returning - Final assignment outside if/else chain
- Exhaustiveness fallback assigns
null
Example 5: Nested Blocks with Multiple Statements
Input:
@match result {
Ok(v) => {
local doubled
doubled = v * 2
print("Result: " + doubled)
return doubled
}
Err(e) => {
print("Error: " + e)
return -1
}
}
Desugared Output:
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:
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:
@match result {
Ok(v) => print(v)
}
Desugared:
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:
@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:
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:
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:
@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:
local compute() {
@match result {
Ok(v) => return v
Err(e) => return -1
}
}
Desugared:
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:
loop(i < 10) {
@match opt {
Some(x) => {
if x > 5 {
break
}
continue
}
None => i = i + 1
}
}
Desugared:
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
// 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
// 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)
// 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
// 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
// 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
// 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
// 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
// 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)
// 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)
// 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
// 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
// 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
// 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
// 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
// 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
MatchPatternAST node variant - Add
PatternandMatchArmstruct definitions - Implement
parse_match_pattern()method - Implement
parse_match_arm()method - Implement
parse_pattern()method - Add
FatArrowandUnderscoretoken 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, ...
- Result:
- 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
localdeclaration 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)
// 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:
- @enum generates EnumSchemaBox metadata
- @match queries EnumSchemaBox for field names
- Compile-time exhaustiveness check possible
Example:
@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):
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
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:
@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:
@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:
@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:
@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:
@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:
@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):
@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:
// 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:
@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:
@match point {
Cartesian(x, y) => print(x) // Which field is 'x'?
}
Mitigation:
- Clear documentation of field naming convention
--dump-mirflag shows desugared code- Error messages show field names
Example Desugared (for debugging):
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:
@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
- @enum Macro Spec: (To be created)
- Result/Option API: apps/lib/boxes/result.hako, option.hako
- Macro System: docs/private/roadmap/phases/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)