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

41 KiB
Raw Blame History

@enum Macro Implementation Specification (Choice A'': Macro-Only)

Version: 1.0 Date: 2025-10-08 Status: 完了2025-10-08 - Phase 19 実装済み Approach: Macro-only desugaring (no VariantBox Core implementation)


🎉 実装完了状況2025-10-08

  • @enum Parser: src/parser/declarations/enum_parser.rs (147行)
  • Macro Expansion: src/macro/engine.rs (expand_enum_to_boxes関数)
  • match式: src/parser/expr/match_expr.rs (392行、literal/type patterns + guards)
  • ResultBox Phase 1: apps/lib/boxes/result.hako (103行)
  • OptionBox: apps/lib/boxes/option.hako (94行、manual implementation)
  • ⚠️ API競合未解決: 小文字版 vs @enum版詳細は Section 1.3 参照)

Table of Contents

  1. Overview
  2. Parser Changes Specification
  3. Macro Desugaring Rules
  4. Edge Cases
  5. Test Suite Design
  6. Implementation Task Breakdown
  7. Integration with Existing Code
  8. Risk Analysis

1. Overview

1.1 Goal

Implement @enum macro that desugars into:

  • 1 box (data container with _tag field)
  • 1 static box (constructors + helper methods)
  • All Box-typed fields (Everything is Box principle)
  • MIR16 Frozen: No IR changes, pure desugaring

1.2 Design Philosophy

  • Box-First: All fields are Box-typed (no primitives)
  • Tag-Based: Use _tag: StringBox for variant discrimination
  • Fail-Fast: Type mismatches panic with clear error messages
  • Zero IR Impact: Everything compiles to existing MIR16 instructions

1.3 Existing Foundation

  • Option/Result already implemented (apps/lib/boxes/option.hako, result.hako)
  • Current pattern: Uses IntegerBox for flags (_is_some, _ok)
  • New pattern: Switch to StringBox _tag field for consistency

2. Parser Changes Specification

2.1 Token Recognition

File: src/parser/mod.rs

New Token Pattern:

// In tokenizer or parser
TokenType::At  // '@' character
TokenType::Identifier("enum")

Parsing Trigger:

// In parse_statement() or top-level parser
if current_token == TokenType::At && peek() == TokenType::Identifier("enum") {
    return parse_enum_declaration();
}

2.2 AST Node Structure

Option A: Reuse BoxDeclaration with flag

// In src/ast.rs ASTNode enum
BoxDeclaration {
    name: String,
    // ... existing fields ...
    is_enum: bool,              // NEW: Flag for enum boxes
    enum_variants: Vec<EnumVariant>, // NEW: Variant definitions
    span: Span,
}

#[derive(Debug, Clone)]
pub struct EnumVariant {
    pub name: String,           // "Ok", "Err", "None", etc.
    pub fields: Vec<EnumField>, // Field list for this variant
    pub span: Span,
}

#[derive(Debug, Clone)]
pub struct EnumField {
    pub name: String,           // Field name (or auto-generated)
    pub span: Span,
}

Option B: New EnumDeclaration variant (recommended for clarity)

// In src/ast.rs ASTNode enum
EnumDeclaration {
    name: String,               // "Result", "Option", etc.
    variants: Vec<EnumVariant>, // List of variants
    span: Span,
}

2.3 Parser Function

Location: src/parser/declarations/mod.rs or new src/parser/declarations/enum_parser.rs

/// Parse @enum declaration
///
/// Grammar:
///   @enum EnumName {
///     Variant1
///     Variant2(field)
///     Variant3(field1, field2)
///   }
fn parse_enum_declaration(&mut self) -> Result<ASTNode, ParseError> {
    // 1. Consume '@enum' tokens
    self.expect(TokenType::At)?;
    self.expect_keyword("enum")?;

    // 2. Parse enum name
    let name = self.expect_identifier()?;
    let enum_name = format!("{}Box", name); // Result -> ResultBox

    // 3. Parse variants block
    self.expect(TokenType::LeftBrace)?;

    let mut variants = Vec::new();
    while !self.check(TokenType::RightBrace) {
        let variant = self.parse_enum_variant()?;
        variants.push(variant);

        // Optional newline/semicolon between variants
        self.consume_if(TokenType::Newline);
        self.consume_if(TokenType::Semicolon);
    }

    self.expect(TokenType::RightBrace)?;

    // 4. Build AST node
    Ok(ASTNode::EnumDeclaration {
        name,
        variants,
        span: self.span_from(start),
    })
}

/// Parse single enum variant
///
/// Grammar:
///   Variant
///   Variant(field)
///   Variant(field1, field2)
fn parse_enum_variant(&mut self) -> Result<EnumVariant, ParseError> {
    let name = self.expect_identifier()?;
    let mut fields = Vec::new();

    // Check for field list
    if self.consume_if(TokenType::LeftParen) {
        while !self.check(TokenType::RightParen) {
            let field_name = self.expect_identifier()?;
            fields.push(EnumField {
                name: field_name,
                span: self.current_span(),
            });

            if !self.check(TokenType::RightParen) {
                self.expect(TokenType::Comma)?;
            }
        }
        self.expect(TokenType::RightParen)?;
    }

    Ok(EnumVariant {
        name,
        fields,
        span: self.span_from(start),
    })
}

2.4 Error Handling

Parse Errors:

#[derive(Error, Debug)]
pub enum ParseError {
    // ... existing errors ...

    #[error("Invalid enum syntax at line {line}: {message}")]
    InvalidEnumSyntax { message: String, line: usize },

    #[error("Duplicate variant name '{name}' in enum at line {line}")]
    DuplicateVariant { name: String, line: usize },

    #[error("Empty enum declaration at line {line}")]
    EmptyEnum { line: usize },
}

Validation:

  1. Enum must have at least 1 variant
  2. Variant names must be unique
  3. Variant names must be valid identifiers
  4. Field names within a variant must be unique

3. Macro Desugaring Rules

3.1 Overall Transformation

Input:

@enum Result {
  Ok(value)
  Err(error)
}

Output:

box ResultBox {
  _tag: StringBox
  _value: Box
  _error: Box

  birth() {
    me._tag = ""
    me._value = null
    me._error = null
  }
}

static box Result {
  Ok(v) {
    local r = new ResultBox()
    r._tag = "Ok"
    r._value = v
    return r
  }

  Err(e) {
    local r = new ResultBox()
    r._tag = "Err"
    r._error = e
    return r
  }

  is_Ok(variant) { return variant._tag == "Ok" }
  is_Err(variant) { return variant._tag == "Err" }

  as_Ok(variant) {
    if variant._tag != "Ok" {
      print("[PANIC] Result.as_Ok: called on " + variant._tag)
      return null
    }
    return variant._value
  }

  as_Err(variant) {
    if variant._tag != "Err" {
      print("[PANIC] Result.as_Err: called on " + variant._tag)
      return null
    }
    return variant._error
  }
}

3.2 Naming Conventions

Element Convention Example
Enum Name Input name Result, Option
Box Name {Name}Box ResultBox, OptionBox
Tag Field _tag (always) _tag: StringBox
Variant Field (single) _value For Ok(value)_value
Variant Field (named) _{field} For Err(error)_error
Variant Field (multi) _{field1}, _{field2} For Pair(first, second)_first, _second
Constructor Variant name Ok(), Err()
Is-checker is_{Variant}(v) is_Ok(v), is_Err(v)
Getter as_{Variant}(v) as_Ok(v), as_Err(v)

3.3 Field Generation Rules

Rule 1: Collect all fields from all variants

@enum Result {
  Ok(value)      → _value: Box
  Err(error)     → _error: Box
}
→ Fields: [_tag, _value, _error]

Rule 2: All fields are Box-typed

_tag: StringBox    // Always StringBox
_value: Box        // Generic Box (runtime polymorphism)
_error: Box

Rule 3: Single unnamed field → _value

Some(value) → _value: Box

Rule 4: Multiple/named fields → prefix with _

Pair(first, second) → _first: Box, _second: Box

3.4 Birth Method Generation

Template:

birth() {
  me._tag = ""
  {for each field}
    me.{field_name} = null
  {end for}
}

Example (Result):

birth() {
  me._tag = ""
  me._value = null
  me._error = null
}

3.5 Constructor Generation

Template for each variant:

{VariantName}({param1}, {param2}, ...) {
  local inst = new {EnumName}Box()
  inst._tag = "{VariantName}"
  {for each field in variant}
    inst._{field_name} = {param_name}
  {end for}
  return inst
}

Example (Ok variant):

Ok(v) {
  local r = new ResultBox()
  r._tag = "Ok"
  r._value = v
  return r
}

Example (0-field variant):

None() {
  local opt = new OptionBox()
  opt._tag = "None"
  return opt
}

3.6 Helper Method Generation

3.6.1 is_{Variant} Methods

Template:

is_{VariantName}(variant) {
  return variant._tag == "{VariantName}"
}

Example:

is_Ok(variant) { return variant._tag == "Ok" }
is_Err(variant) { return variant._tag == "Err" }

3.6.2 as_{Variant} Methods

Template (single field):

as_{VariantName}(variant) {
  if variant._tag != "{VariantName}" {
    print("[PANIC] {EnumName}.as_{VariantName}: called on " + variant._tag)
    return null
  }
  return variant._{field_name}
}

Template (multi-field) - returns ArrayBox:

as_{VariantName}(variant) {
  if variant._tag != "{VariantName}" {
    print("[PANIC] {EnumName}.as_{VariantName}: called on " + variant._tag)
    return null
  }
  local result = new ArrayBox()
  result.push(variant._{field1})
  result.push(variant._{field2})
  return result
}

Example (single field):

as_Ok(variant) {
  if variant._tag != "Ok" {
    print("[PANIC] Result.as_Ok: called on " + variant._tag)
    return null
  }
  return variant._value
}

Example (multi-field):

as_Pair(variant) {
  if variant._tag != "Pair" {
    print("[PANIC] Triple.as_Pair: called on " + variant._tag)
    return null
  }
  local result = new ArrayBox()
  result.push(variant._first)
  result.push(variant._second)
  return result
}

3.6.3 0-field variant getter

For variants with no fields, as_* still exists but returns null:

as_None(variant) {
  if variant._tag != "None" {
    print("[PANIC] Option.as_None: called on " + variant._tag)
    return null
  }
  return null  // No fields to return
}

3.7 Macro Implementation Location

File: src/macro/enum_macro.rs (new file)

use crate::ast::{ASTNode, EnumVariant, EnumField};

/// Expand @enum declaration into box + static box
pub fn expand_enum(name: &str, variants: &[EnumVariant]) -> Vec<ASTNode> {
    let box_name = format!("{}Box", name);

    // 1. Generate data box
    let data_box = generate_data_box(&box_name, variants);

    // 2. Generate static box
    let static_box = generate_static_box(name, &box_name, variants);

    vec![data_box, static_box]
}

fn generate_data_box(box_name: &str, variants: &[EnumVariant]) -> ASTNode {
    // Collect all unique fields from all variants
    let mut all_fields = vec!["_tag".to_string()];

    for variant in variants {
        for field in &variant.fields {
            let field_name = if variant.fields.len() == 1 && field.name == "value" {
                "_value".to_string()
            } else {
                format!("_{}", field.name)
            };

            if !all_fields.contains(&field_name) {
                all_fields.push(field_name);
            }
        }
    }

    // Generate birth method
    let birth_body = generate_birth_method(&all_fields);

    // Build BoxDeclaration AST node
    ASTNode::BoxDeclaration {
        name: box_name.to_string(),
        fields: all_fields,
        methods: hashmap! {
            "birth".to_string() => birth_body,
        },
        // ... other fields with defaults ...
    }
}

fn generate_static_box(enum_name: &str, box_name: &str, variants: &[EnumVariant]) -> ASTNode {
    let mut methods = HashMap::new();

    for variant in variants {
        // Generate constructor
        let constructor = generate_constructor(box_name, &variant.name, &variant.fields);
        methods.insert(variant.name.clone(), constructor);

        // Generate is_* helper
        let is_helper = generate_is_helper(&variant.name);
        methods.insert(format!("is_{}", variant.name), is_helper);

        // Generate as_* helper
        let as_helper = generate_as_helper(enum_name, &variant.name, &variant.fields);
        methods.insert(format!("as_{}", variant.name), as_helper);
    }

    ASTNode::BoxDeclaration {
        name: enum_name.to_string(),
        is_static: true,
        methods,
        // ... other fields with defaults ...
    }
}

3.8 Integration Point

File: src/macro/mod.rs

mod enum_macro;

pub fn expand_macros(ast: ASTNode) -> ASTNode {
    // ... existing macro expansion ...

    // Apply enum expansion
    ast = enum_macro::expand_all_enums(ast);

    // ... continue with other macros ...
}

4. Edge Cases

4.1 Single Variant Enum

Input:

@enum Always {
  Value(data)
}

Output:

box AlwaysBox {
  _tag: StringBox
  _data: Box

  birth() {
    me._tag = ""
    me._data = null
  }
}

static box Always {
  Value(d) {
    local inst = new AlwaysBox()
    inst._tag = "Value"
    inst._data = d
    return inst
  }

  is_Value(variant) { return variant._tag == "Value" }

  as_Value(variant) {
    if variant._tag != "Value" {
      print("[PANIC] Always.as_Value: called on " + variant._tag)
      return null
    }
    return variant._data
  }
}

Note: Still generates all helpers for consistency.

4.2 Zero-Field Variant

Input:

@enum Option {
  Some(value)
  None
}

Output:

box OptionBox {
  _tag: StringBox
  _value: Box

  birth() {
    me._tag = ""
    me._value = null
  }
}

static box Option {
  Some(v) {
    local opt = new OptionBox()
    opt._tag = "Some"
    opt._value = v
    return opt
  }

  None() {
    local opt = new OptionBox()
    opt._tag = "None"
    return opt
  }

  is_Some(variant) { return variant._tag == "Some" }
  is_None(variant) { return variant._tag == "None" }

  as_Some(variant) {
    if variant._tag != "Some" {
      print("[PANIC] Option.as_Some: called on " + variant._tag)
      return null
    }
    return variant._value
  }

  as_None(variant) {
    if variant._tag != "None" {
      print("[PANIC] Option.as_None: called on " + variant._tag)
      return null
    }
    return null
  }
}

4.3 Multi-Field Variant

Input:

@enum Triple {
  One(a)
  Two(a, b)
  Three(a, b, c)
}

Output:

box TripleBox {
  _tag: StringBox
  _a: Box
  _b: Box
  _c: Box

  birth() {
    me._tag = ""
    me._a = null
    me._b = null
    me._c = null
  }
}

static box Triple {
  One(a) {
    local inst = new TripleBox()
    inst._tag = "One"
    inst._a = a
    return inst
  }

  Two(a, b) {
    local inst = new TripleBox()
    inst._tag = "Two"
    inst._a = a
    inst._b = b
    return inst
  }

  Three(a, b, c) {
    local inst = new TripleBox()
    inst._tag = "Three"
    inst._a = a
    inst._b = b
    inst._c = c
    return inst
  }

  is_One(variant) { return variant._tag == "One" }
  is_Two(variant) { return variant._tag == "Two" }
  is_Three(variant) { return variant._tag == "Three" }

  as_One(variant) {
    if variant._tag != "One" {
      print("[PANIC] Triple.as_One: called on " + variant._tag)
      return null
    }
    return variant._a
  }

  as_Two(variant) {
    if variant._tag != "Two" {
      print("[PANIC] Triple.as_Two: called on " + variant._tag)
      return null
    }
    local result = new ArrayBox()
    result.push(variant._a)
    result.push(variant._b)
    return result
  }

  as_Three(variant) {
    if variant._tag != "Three" {
      print("[PANIC] Triple.as_Three: called on " + variant._tag)
      return null
    }
    local result = new ArrayBox()
    result.push(variant._a)
    result.push(variant._b)
    result.push(variant._c)
    return result
  }
}

4.4 Field Name Conflicts

Problem: Different variants use same field name with different semantics.

Input:

@enum Conflict {
  TypeA(value)
  TypeB(value)
}

Resolution: Both variants use _value field (shared storage).

Output:

box ConflictBox {
  _tag: StringBox
  _value: Box

  birth() {
    me._tag = ""
    me._value = null
  }
}

static box Conflict {
  TypeA(v) {
    local inst = new ConflictBox()
    inst._tag = "TypeA"
    inst._value = v
    return inst
  }

  TypeB(v) {
    local inst = new ConflictBox()
    inst._tag = "TypeB"
    inst._value = v
    return inst
  }

  is_TypeA(variant) { return variant._tag == "TypeA" }
  is_TypeB(variant) { return variant._tag == "TypeB" }

  as_TypeA(variant) {
    if variant._tag != "TypeA" {
      print("[PANIC] Conflict.as_TypeA: called on " + variant._tag)
      return null
    }
    return variant._value
  }

  as_TypeB(variant) {
    if variant._tag != "TypeB" {
      print("[PANIC] Conflict.as_TypeB: called on " + variant._tag)
      return null
    }
    return variant._value
  }
}

Note: This is intentional - field reuse is OK because tag discriminates.

4.5 Nested Enum (Not Supported in Phase 1)

Input:

@enum Outer {
  @enum Inner {  // ERROR
    A
    B
  }
  C
}

Error: "Nested @enum declarations are not supported"

Workaround: Declare enums separately.

4.6 Reserved Names

Problem: Variant name conflicts with built-in methods.

Prohibited variant names:

  • birth
  • fini
  • new (reserved keyword)
  • me (reserved keyword)
  • this (reserved keyword)

Error: "Variant name '{name}' is reserved"


5. Test Suite Design

5.1 Test File Structure

apps/lib/boxes/tests/
├── enum_basic_test.hako              # Test 1-3
├── enum_multi_variant_test.hako      # Test 4
├── enum_zero_field_test.hako         # Test 5
├── enum_multi_field_test.hako        # Test 6
├── enum_helpers_test.hako            # Test 7-8
├── enum_pattern_match_test.hako      # Test 9
├── enum_integration_test.hako        # Test 10
└── enum_real_world_ast_test.hako     # Test 11

5.2 Test Cases

Test 1: Basic 2-Variant Enum (Result-like)

File: enum_basic_test.hako

@enum Result {
  Ok(value)
  Err(error)
}

static box Main {
  main() {
    local r1 = Result.Ok(42)
    print(r1._tag)  // "Ok"

    local r2 = Result.Err("failed")
    print(r2._tag)  // "Err"

    return 0
  }
}

Expected Output:

Ok
Err

Test 2: Basic 2-Variant Enum (Option-like)

File: enum_basic_test.hako (additional test)

@enum Option {
  Some(value)
  None
}

static box Main {
  main() {
    local opt1 = Option.Some(100)
    print(opt1._tag)  // "Some"
    print(opt1._value)  // "100"

    local opt2 = Option.None()
    print(opt2._tag)  // "None"

    return 0
  }
}

Expected Output:

Some
100
None

Test 3: Constructor Field Assignment

File: enum_basic_test.hako (additional test)

@enum Result {
  Ok(value)
  Err(error)
}

static box Main {
  main() {
    local r = Result.Ok("success")

    if r._tag == "Ok" {
      print(r._value)  // "success"
    }

    if r._tag != "Err" {
      print("not error")  // "not error"
    }

    return 0
  }
}

Expected Output:

success
not error

Test 4: 3+ Variant Enum

File: enum_multi_variant_test.hako

@enum Status {
  Pending
  Running(task_id)
  Success(result)
  Failed(error)
}

static box Main {
  main() {
    local s1 = Status.Pending()
    local s2 = Status.Running(123)
    local s3 = Status.Success("done")
    local s4 = Status.Failed("timeout")

    print(s1._tag)  // "Pending"
    print(s2._tag)  // "Running"
    print(s2._task_id)  // "123"
    print(s3._tag)  // "Success"
    print(s3._result)  // "done"
    print(s4._tag)  // "Failed"
    print(s4._error)  // "timeout"

    return 0
  }
}

Expected Output:

Pending
Running
123
Success
done
Failed
timeout

Test 5: Variant with 0 Fields

File: enum_zero_field_test.hako

@enum Flag {
  On
  Off
}

static box Main {
  main() {
    local f1 = Flag.On()
    local f2 = Flag.Off()

    print(f1._tag)  // "On"
    print(f2._tag)  // "Off"

    return 0
  }
}

Expected Output:

On
Off

Test 6: Variant with Multiple Fields

File: enum_multi_field_test.hako

@enum Point {
  Point2D(x, y)
  Point3D(x, y, z)
}

static box Main {
  main() {
    local p2 = Point.Point2D(10, 20)
    local p3 = Point.Point3D(1, 2, 3)

    print(p2._tag)  // "Point2D"
    print(p2._x)    // "10"
    print(p2._y)    // "20"

    print(p3._tag)  // "Point3D"
    print(p3._x)    // "1"
    print(p3._y)    // "2"
    print(p3._z)    // "3"

    return 0
  }
}

Expected Output:

Point2D
10
20
Point3D
1
2
3

Test 7: is_* Helper Usage

File: enum_helpers_test.hako

@enum Result {
  Ok(value)
  Err(error)
}

static box Main {
  main() {
    local r1 = Result.Ok(42)
    local r2 = Result.Err("fail")

    if Result.is_Ok(r1) {
      print("r1 is Ok")  // "r1 is Ok"
    }

    if Result.is_Err(r2) {
      print("r2 is Err")  // "r2 is Err"
    }

    if Result.is_Ok(r2) {
      print("r2 is Ok")  // (not printed)
    }

    return 0
  }
}

Expected Output:

r1 is Ok
r2 is Err

Test 8: as_* Helper Success

File: enum_helpers_test.hako (additional test)

@enum Result {
  Ok(value)
  Err(error)
}

static box Main {
  main() {
    local r = Result.Ok(100)

    local val = Result.as_Ok(r)
    print(val)  // "100"

    return 0
  }
}

Expected Output:

100

Test 9: as_* Helper Panic (Wrong Variant)

File: enum_helpers_test.hako (additional test)

@enum Result {
  Ok(value)
  Err(error)
}

static box Main {
  main() {
    local r = Result.Err("failed")

    local val = Result.as_Ok(r)
    // Expected: "[PANIC] Result.as_Ok: called on Err"
    // Returns: null

    if val == null {
      print("got null after panic")
    }

    return 0
  }
}

Expected Output:

[PANIC] Result.as_Ok: called on Err
got null after panic

Test 10: Pattern Matching Preparation

File: enum_pattern_match_test.hako

@enum Option {
  Some(value)
  None
}

static box Main {
  process_option(opt) {
    if opt._tag == "Some" {
      print("Has value: " + opt._value)
    }

    if opt._tag == "None" {
      print("No value")
    }
  }

  main() {
    local opt1 = Option.Some(42)
    local opt2 = Option.None()

    me.process_option(opt1)  // "Has value: 42"
    me.process_option(opt2)  // "No value"

    return 0
  }
}

Expected Output:

Has value: 42
No value

Test 11: Integration with Existing Option/Result

File: enum_integration_test.hako

Goal: Test @enum-generated Result alongside old ResultBox.

using std.result as OldResult

@enum Result {
  Ok(value)
  Err(error)
}

static box Main {
  main() {
    local old_r = OldResult.ok(100)
    local new_r = Result.Ok(200)

    print(old_r.value())  // "100"
    print(Result.as_Ok(new_r))  // "200"

    return 0
  }
}

Expected Output:

100
200

Test 12: Complex Real-World Case (AST Node Example)

File: enum_real_world_ast_test.hako

@enum ASTNode {
  Literal(value)
  Variable(name)
  BinaryOp(op, left, right)
  UnaryOp(op, operand)
}

static box Main {
  main() {
    local lit = ASTNode.Literal(42)
    local var = ASTNode.Variable("x")
    local binop = ASTNode.BinaryOp("+", lit, var)

    print(ASTNode.is_Literal(lit))  // "1" (true)
    print(ASTNode.is_BinaryOp(binop))  // "1" (true)

    if ASTNode.is_BinaryOp(binop) {
      local parts = ASTNode.as_BinaryOp(binop)
      // parts is ArrayBox: ["+", lit, var]
      print(parts.get(0))  // "+"
    }

    return 0
  }
}

Expected Output:

1
1
+

5.3 Negative Tests

Test N1: Duplicate Variant Names

File: enum_error_duplicate_variant.hako

@enum Bad {
  Ok(value)
  Ok(other)  // ERROR: Duplicate variant
}

Expected Error: "Duplicate variant name 'Ok' in enum at line X"

Test N2: Reserved Variant Name

File: enum_error_reserved_name.hako

@enum Bad {
  birth(value)  // ERROR: Reserved name
}

Expected Error: "Variant name 'birth' is reserved"

Test N3: Empty Enum

File: enum_error_empty.hako

@enum Empty {
  // ERROR: No variants
}

Expected Error: "Empty enum declaration at line X"

5.4 Test Runner Script

File: tools/run_enum_tests.sh

#!/bin/bash
set -e

HAKO="./target/release/hako"

echo "=== @enum Macro Test Suite ==="

# Basic tests
echo "[1/12] Basic 2-variant enum (Result)..."
$HAKO apps/lib/boxes/tests/enum_basic_test.hako

echo "[2/12] Basic 2-variant enum (Option)..."
$HAKO apps/lib/boxes/tests/enum_basic_test.hako

echo "[3/12] Constructor field assignment..."
$HAKO apps/lib/boxes/tests/enum_basic_test.hako

echo "[4/12] Multi-variant enum (4 variants)..."
$HAKO apps/lib/boxes/tests/enum_multi_variant_test.hako

echo "[5/12] Zero-field variant..."
$HAKO apps/lib/boxes/tests/enum_zero_field_test.hako

echo "[6/12] Multi-field variant..."
$HAKO apps/lib/boxes/tests/enum_multi_field_test.hako

echo "[7/12] is_* helper usage..."
$HAKO apps/lib/boxes/tests/enum_helpers_test.hako

echo "[8/12] as_* helper success..."
$HAKO apps/lib/boxes/tests/enum_helpers_test.hako

echo "[9/12] as_* helper panic (wrong variant)..."
$HAKO apps/lib/boxes/tests/enum_helpers_test.hako

echo "[10/12] Pattern matching preparation..."
$HAKO apps/lib/boxes/tests/enum_pattern_match_test.hako

echo "[11/12] Integration with existing Option/Result..."
$HAKO apps/lib/boxes/tests/enum_integration_test.hako

echo "[12/12] Real-world AST node example..."
$HAKO apps/lib/boxes/tests/enum_real_world_ast_test.hako

# Negative tests
echo "[N1/3] Duplicate variant names..."
$HAKO apps/lib/boxes/tests/enum_error_duplicate_variant.hako 2>&1 | grep -q "Duplicate variant" && echo "  ✓ Error caught" || (echo "  ✗ Error not caught"; exit 1)

echo "[N2/3] Reserved variant name..."
$HAKO apps/lib/boxes/tests/enum_error_reserved_name.hako 2>&1 | grep -q "reserved" && echo "  ✓ Error caught" || (echo "  ✗ Error not caught"; exit 1)

echo "[N3/3] Empty enum..."
$HAKO apps/lib/boxes/tests/enum_error_empty.hako 2>&1 | grep -q "Empty enum" && echo "  ✓ Error caught" || (echo "  ✗ Error not caught"; exit 1)

echo "=== All tests passed ==="

6. Implementation Task Breakdown

Day 1: Parser Changes + AST Nodes (6-8 hours)

Morning (3-4h):

  • Add TokenType::At if not exists
  • Implement parse_enum_declaration() in src/parser/declarations/enum_parser.rs
  • Implement parse_enum_variant() helper
  • Add EnumDeclaration, EnumVariant, EnumField to src/ast.rs

Afternoon (3-4h):

  • Add validation (duplicate variants, empty enum, reserved names)
  • Add error types to ParseError enum
  • Write parser unit tests
  • Test parser with minimal inputs (no macro expansion yet)

Success Criteria:

  • Parser can parse @enum syntax without panicking
  • AST nodes are created correctly
  • Validation errors are caught

Day 2: Macro Engine Integration + Code Generation (8-10 hours)

Morning (4-5h):

  • Create src/macro/enum_macro.rs
  • Implement expand_enum() function
  • Implement generate_data_box() - create box with fields
  • Implement generate_birth_method() - initialize all fields to null

Afternoon (4-5h):

  • Implement generate_static_box() - create static box
  • Implement generate_constructor() - one per variant
  • Add macro invocation to src/macro/mod.rs
  • Test expansion with debug prints (dump generated AST)

Success Criteria:

  • @enum Result { Ok(value) Err(error) } expands to correct AST
  • Generated AST can be printed back as code
  • No panics during expansion

Day 3: Helper Method Generation + Edge Cases (8-10 hours)

Morning (4-5h):

  • Implement generate_is_helper() - is_* methods
  • Implement generate_as_helper() - as_* methods (single field)
  • Implement multi-field as_* (returns ArrayBox)
  • Handle 0-field variant as_* (returns null)

Afternoon (4-5h):

  • Test edge cases (single variant, zero fields, multi-field)
  • Test field name conflicts (same name across variants)
  • Add diagnostics/trace output (NYASH_MACRO_TRACE=1)
  • Verify generated code compiles to MIR

Success Criteria:

  • All helper methods generate correctly
  • Edge cases handled without errors
  • Generated code compiles and runs

Day 4: Test Suite (6-8 hours)

Morning (3-4h):

  • Write tests 1-6 (basic cases)
  • Write tests 7-9 (helper methods)
  • Verify all tests run and produce expected output

Afternoon (3-4h):

  • Write tests 10-12 (pattern matching, integration, real-world)
  • Write negative tests (N1-N3)
  • Create tools/run_enum_tests.sh runner script
  • Run full test suite

Success Criteria:

  • All 12 positive tests pass
  • All 3 negative tests catch errors correctly
  • Test runner script exits with 0

Day 5: Smoke Tests + Integration (6-8 hours)

Morning (3-4h):

  • Add @enum tests to smoke test suite
  • Run tools/smokes/v2/run.sh --profile quick
  • Fix any integration issues
  • Document known limitations

Afternoon (3-4h):

  • Update CURRENT_TASK.md with @enum status
  • Update CLAUDE.md development log
  • Create migration guide for Option/Result
  • Review and commit

Success Criteria:

  • Smoke tests pass
  • Documentation updated
  • Ready for production use

7. Integration with Existing Code

7.1 Migration Strategy for Option/Result

Current State:

  • apps/lib/boxes/option.hako - uses _is_some: IntegerBox
  • apps/lib/boxes/result.hako - uses _ok: IntegerBox
  • 5+ files use these boxes

Migration Plan:

Phase 1: Parallel Existence (Week 1)

  1. Keep existing option.hako and result.hako
  2. Create new @enum Option and @enum Result in separate files:
    • apps/lib/boxes/option_v2.hako
    • apps/lib/boxes/result_v2.hako
  3. Update hako.toml:
[modules.overrides]
std.option = "apps/lib/boxes/option.hako"      # Old version
std.option_v2 = "apps/lib/boxes/option_v2.hako" # New @enum version
std.result = "apps/lib/boxes/result.hako"
std.result_v2 = "apps/lib/boxes/result_v2.hako"

Phase 2: Gradual Migration (Week 2-3)

  1. Migrate test files first
  2. Migrate non-critical tools
  3. Verify behavior matches old version

Phase 3: Deprecation (Week 4)

  1. Rename old files:
    • option.hakooption_deprecated.hako
    • result.hakoresult_deprecated.hako
  2. Rename new files:
    • option_v2.hakooption.hako
    • result_v2.hakoresult.hako
  3. Update all import sites

Phase 4: Cleanup (Week 5)

  1. Delete deprecated files
  2. Remove old module aliases
  3. Update documentation

7.2 API Compatibility Matrix

Method Old Option @enum Option Compatible?
Option.some(v) Option.Some(v) Different name
Option.none() Option.None() Different name
opt.is_some() Option.is_Some(opt) Different signature
opt.unwrap() Option.as_Some(opt) Different name
opt._value ✓ Compatible
opt._is_some ✗ (uses _tag) Breaking

Conclusion: NOT backward compatible. Requires explicit migration.

7.3 Wrapper Strategy for Compatibility

Option: Create compatibility wrapper

File: apps/lib/boxes/option_compat.hako

@enum OptionInternal {
  Some(value)
  None
}

static box Option {
  // Old API
  some(v) { return OptionInternal.Some(v) }
  none() { return OptionInternal.None() }

  // Adapter for instance methods
  is_some(opt) { return opt._tag == "Some" }
  is_none(opt) { return opt._tag == "None" }
  unwrap(opt) { return OptionInternal.as_Some(opt) }
  unwrap_or(opt, def) {
    if opt._tag == "Some" {
      return opt._value
    }
    return def
  }
}

// Create facade OptionBox (for `new OptionBox()` compatibility)
box OptionBox {
  _internal: Box

  birth() {
    me._internal = OptionInternal.None()
  }

  is_some() { return me._internal._tag == "Some" }
  is_none() { return me._internal._tag == "None" }
  unwrap() { return OptionInternal.as_Some(me._internal) }
}

Trade-off: Adds complexity but maintains backward compatibility.

7.4 Migration Guide Document

File: docs/guides/enum-migration-guide.md

Contents:

  1. Why migrate to @enum?
  2. API differences table
  3. Step-by-step migration process
  4. Common pitfalls
  5. Examples (before/after)

8. Risk Analysis

8.1 Parser Conflicts

Risk: @ token conflicts with other syntax (e.g., @local sugar)

Mitigation:

  • Check existing @ usage with grep
  • Use lookahead: @enum specifically (not just @)
  • Add parser tests for ambiguous cases

Action Items:

  • Grep codebase for existing @ usage
  • Test parser with @local and @enum in same file
  • Document precedence rules

8.2 Macro Expansion Order

Risk: @enum expansion happens before/after other macros, causing issues

Current Macro Order (from CLAUDE.md):

// src/macro/mod.rs
1. @local expansion
2. Map literal sugar
3. (add @enum here?)

Mitigation:

  • Run @enum expansion before other macros (early in pipeline)
  • @enum generates pure box definitions (no further macro dependencies)

Action Items:

  • Verify macro execution order in src/macro/mod.rs
  • Test interaction with map literal sugar
  • Test interaction with @local sugar

8.3 Field Naming Conflicts

Risk: Generated _tag, _value conflict with user-defined fields

Current Design: Fields always prefixed with _ (e.g., _tag, _value)

Mitigation:

  • Document that _-prefixed fields are reserved for enum internals
  • Add validation: reject variant field names starting with _
  • Generate error: "Variant field names cannot start with '_' (reserved)"

Action Items:

  • Add validation in parse_enum_variant()
  • Test case: Variant(_tag) → error
  • Document in language guide

8.4 Performance Considerations

Risk: String tag comparison slower than integer flag comparison

Analysis:

  • Old: if opt._is_some == 1 (integer compare, ~1 cycle)
  • New: if opt._tag == "Some" (string compare, ~N cycles where N = length)

Mitigation:

  • VM: String comparison already optimized in StringBox
  • LLVM: Compiler can optimize string literals to pointer comparison
  • Measurement: Add benchmark comparing old vs new

Action Items:

  • Benchmark: Old Result vs @enum Result (1M operations)
  • Profile: Measure hotspot in string comparison
  • Document performance characteristics

Expected Result: <10% slowdown (acceptable for MVP)

8.5 Debugging Challenges

Risk: Expanded code hard to debug (macro generates verbose output)

Mitigation:

  • Add NYASH_MACRO_TRACE=1 to dump expansion
  • Add --dump-mir flag support (show post-expansion code)
  • Preserve source spans in generated AST
  • Add comment markers in generated code:
    // BEGIN @enum Result expansion
    box ResultBox { ... }
    // END @enum Result expansion
    

Action Items:

  • Implement trace output in enum_macro.rs
  • Test --dump-mir with @enum code
  • Add source span preservation
  • Add debug comments to generated AST

8.6 Error Message Quality

Risk: Users get confusing errors from generated code (not original @enum)

Example Bad Error:

Error at line 523: Field '_tag' not found in ResultBox
  (User wrote @enum at line 10, error points to generated code at line 523)

Mitigation:

  • Preserve original span in AST nodes
  • Error formatter shows original @enum location
  • Add note: "in expansion of @enum Result at line 10"

Action Items:

  • Test error reporting with intentionally broken @enum
  • Verify span preservation in macro expansion
  • Update error formatter to show macro context

8.7 Macro-Generated Code Stability

Risk: Generated code doesn't compile to MIR (syntax errors, etc.)

Mitigation:

  • Generate only well-tested AST patterns (box, static box, simple statements)
  • Add roundtrip test: expand → parse → expand (should be stable)
  • Add integration test: @enum → MIR → VM execution

Action Items:

  • Test: Expand @enum Result → dump AST → parse again
  • Test: @enum → MIR JSON → verify structure
  • Test: @enum → VM execution → verify behavior

9. Success Criteria

Phase 1 Complete When:

  1. All 12 positive tests pass ✓
  2. All 3 negative tests catch errors ✓
  3. Smoke tests pass ✓
  4. Documentation complete ✓
  5. Performance benchmark shows <10% regression ✓
  6. Integration guide written ✓

Phase 2 (Future):

  1. Pattern matching syntax (match expressions)
  2. Exhaustiveness checking
  3. Performance optimization (intern tag strings)

10. Future Enhancements (Post-MVP)

10.1 Pattern Matching Integration

Syntax (future):

match result {
  Ok(value) => print("Success: " + value)
  Err(error) => print("Error: " + error)
}

Desugaring (future):

if result._tag == "Ok" {
  local value = result._value
  print("Success: " + value)
}
if result._tag == "Err" {
  local error = result._error
  print("Error: " + error)
}

10.2 Exhaustiveness Checking

Goal: Compiler ensures all variants handled.

Example:

match option {
  Some(v) => print(v)
  // WARNING: Missing case for 'None'
}

10.3 String Interning for Tags

Optimization: Intern tag strings to enable pointer comparison.

Implementation:

  • Tag strings stored in intern table
  • Comparison becomes pointer equality (1 cycle)
  • Backward compatible (still strings at API level)

11. References

11.1 Existing Code

  • apps/lib/boxes/option.hako - Current Option implementation
  • apps/lib/boxes/result.hako - Current Result implementation
  • src/parser/mod.rs - Parser entry point
  • src/macro/macro_box.rs - Macro system infrastructure

11.2 Documentation

  • CLAUDE.md - Development log and context
  • docs/reference/language/quick-reference.md - Language syntax
  • docs/private/roadmap/phases/phase-20-variant-box/ - Variant box proposals

11.3 Test Examples

  • apps/selfhost/test_*.hako - Existing test patterns
  • tools/smokes/v2/ - Smoke test infrastructure

Appendix A: Complete Example Expansion

Input:

@enum Option {
  Some(value)
  None
}

Complete Expansion (with all helpers):

// Data box
box OptionBox {
  _tag: StringBox
  _value: Box

  birth() {
    me._tag = ""
    me._value = null
  }
}

// Static box with constructors and helpers
static box Option {
  // Constructors
  Some(v) {
    local opt = new OptionBox()
    opt._tag = "Some"
    opt._value = v
    return opt
  }

  None() {
    local opt = new OptionBox()
    opt._tag = "None"
    return opt
  }

  // is_* helpers
  is_Some(variant) {
    return variant._tag == "Some"
  }

  is_None(variant) {
    return variant._tag == "None"
  }

  // as_* helpers
  as_Some(variant) {
    if variant._tag != "Some" {
      print("[PANIC] Option.as_Some: called on " + variant._tag)
      return null
    }
    return variant._value
  }

  as_None(variant) {
    if variant._tag != "None" {
      print("[PANIC] Option.as_None: called on " + variant._tag)
      return null
    }
    return null
  }
}

Total Lines: 47 (for 2-variant enum)


Appendix B: Implementation Checklist

Parser (src/parser/)

  • TokenType::At exists or added
  • parse_enum_declaration() implemented
  • parse_enum_variant() implemented
  • Validation: duplicate variants
  • Validation: empty enum
  • Validation: reserved names
  • Validation: field names (no _ prefix)
  • Error types added to ParseError
  • Parser unit tests

AST (src/ast.rs)

  • EnumDeclaration variant added
  • EnumVariant struct defined
  • EnumField struct defined
  • Span preservation

Macro (src/macro/)

  • enum_macro.rs created
  • expand_enum() function
  • generate_data_box() function
  • generate_birth_method() function
  • generate_static_box() function
  • generate_constructor() function
  • generate_is_helper() function
  • generate_as_helper() (single field)
  • generate_as_helper() (multi-field)
  • Integration with src/macro/mod.rs
  • Trace output (NYASH_MACRO_TRACE=1)

Tests (apps/lib/boxes/tests/)

  • Test 1: Basic Result
  • Test 2: Basic Option
  • Test 3: Constructor field assignment
  • Test 4: 3+ variants
  • Test 5: Zero-field variant
  • Test 6: Multi-field variant
  • Test 7: is_* helpers
  • Test 8: as_* success
  • Test 9: as_* panic
  • Test 10: Pattern matching prep
  • Test 11: Integration
  • Test 12: Real-world AST
  • Test N1: Duplicate variants
  • Test N2: Reserved names
  • Test N3: Empty enum
  • Test runner script

Documentation

  • Migration guide
  • Language reference update
  • CURRENT_TASK.md update
  • CLAUDE.md update
  • Performance benchmark results

Integration

  • Smoke tests pass
  • hako.toml updated (if needed)
  • No regressions in existing tests
  • Performance <10% regression

End of Specification