41 KiB
@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
- Overview
- Parser Changes Specification
- Macro Desugaring Rules
- Edge Cases
- Test Suite Design
- Implementation Task Breakdown
- Integration with Existing Code
- Risk Analysis
1. Overview
1.1 Goal
Implement @enum macro that desugars into:
- 1 box (data container with
_tagfield) - 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: StringBoxfor 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
IntegerBoxfor flags (_is_some,_ok) - New pattern: Switch to
StringBox_tagfield 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:
- Enum must have at least 1 variant
- Variant names must be unique
- Variant names must be valid identifiers
- 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:
birthfininew(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::Atif not exists - Implement
parse_enum_declaration()insrc/parser/declarations/enum_parser.rs - Implement
parse_enum_variant()helper - Add
EnumDeclaration,EnumVariant,EnumFieldtosrc/ast.rs
Afternoon (3-4h):
- Add validation (duplicate variants, empty enum, reserved names)
- Add error types to
ParseErrorenum - Write parser unit tests
- Test parser with minimal inputs (no macro expansion yet)
Success Criteria:
- Parser can parse
@enumsyntax 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.shrunner 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.mdwith @enum status - Update
CLAUDE.mddevelopment 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: IntegerBoxapps/lib/boxes/result.hako- uses_ok: IntegerBox- 5+ files use these boxes
Migration Plan:
Phase 1: Parallel Existence (Week 1)
- Keep existing
option.hakoandresult.hako - Create new
@enum Optionand@enum Resultin separate files:apps/lib/boxes/option_v2.hakoapps/lib/boxes/result_v2.hako
- 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)
- Migrate test files first
- Migrate non-critical tools
- Verify behavior matches old version
Phase 3: Deprecation (Week 4)
- Rename old files:
option.hako→option_deprecated.hakoresult.hako→result_deprecated.hako
- Rename new files:
option_v2.hako→option.hakoresult_v2.hako→result.hako
- Update all import sites
Phase 4: Cleanup (Week 5)
- Delete deprecated files
- Remove old module aliases
- 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:
- Why migrate to @enum?
- API differences table
- Step-by-step migration process
- Common pitfalls
- 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:
@enumspecifically (not just@) - Add parser tests for ambiguous cases
Action Items:
- Grep codebase for existing
@usage - Test parser with
@localand@enumin 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=1to dump expansion - Add
--dump-mirflag 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-mirwith @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:
- All 12 positive tests pass ✓
- All 3 negative tests catch errors ✓
- Smoke tests pass ✓
- Documentation complete ✓
- Performance benchmark shows <10% regression ✓
- Integration guide written ✓
Phase 2 (Future):
- Pattern matching syntax (
matchexpressions) - Exhaustiveness checking
- 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 implementationapps/lib/boxes/result.hako- Current Result implementationsrc/parser/mod.rs- Parser entry pointsrc/macro/macro_box.rs- Macro system infrastructure
11.2 Documentation
CLAUDE.md- Development log and contextdocs/reference/language/quick-reference.md- Language syntaxdocs/private/roadmap/phases/phase-20-variant-box/- Variant box proposals
11.3 Test Examples
apps/selfhost/test_*.hako- Existing test patternstools/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::Atexists or addedparse_enum_declaration()implementedparse_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)
EnumDeclarationvariant addedEnumVariantstruct definedEnumFieldstruct defined- Span preservation
Macro (src/macro/)
enum_macro.rscreatedexpand_enum()functiongenerate_data_box()functiongenerate_birth_method()functiongenerate_static_box()functiongenerate_constructor()functiongenerate_is_helper()functiongenerate_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