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

38 KiB
Raw Blame History

@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チェインに直接変換

使用例 (実装済み):

local result = match value {
  0 => "zero"
  1 => "one"
  n if n > 10 => "large"
  _ => "other"
}

📋 Table of Contents

  1. Design Goals
  2. Parser Changes
  3. Macro Desugaring Rules
  4. Pattern Types Support
  5. Edge Cases Handling
  6. Test Suite Design
  7. Implementation Task Breakdown
  8. Integration with @enum
  9. Desugaring Algorithm
  10. Error Handling Design
  11. 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:

/// 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:

  • @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:

@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 _tag string
  • 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 _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:

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_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:

@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 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)

// 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:

@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-mir flag 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


🎯 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)