5.4.13. Macro Tutorial 13: Enumeration Macros
Previous tutorials transformed calls, functions, structures, blocks, variants, for-loops, lambda captures, reader macros, and typeinfo expressions. Enumeration macros let you intercept enum declarations at compile time and either modify the enum or generate new code.
[enumeration_macro(name="X")] registers a class that extends
AstEnumerationAnnotation. This is the simplest macro type — it has
only one method:
apply(var enu, var group, args, var errors) → boolCalled before the infer pass. The macro receives the full
EnumerationPtrand can add/remove entries, generate functions, or create global variables. Returntrueon success,falsewith an error message to abort compilation.
5.4.13.1. Motivation
Enumerations in daslang are simple value lists. But real-world code often needs derived information:
A “total” sentinel — the number of values, for array sizing or range checking.
String constructors — converting user input strings to enum values at runtime.
Both can be achieved with enumeration macros:
Section 1 shows
[enum_total]— a custom macro that modifies an enum by appending atotalentry.Section 2 shows
[string_to_enum]fromdaslib/enum_trait— a standard library macro that generates code (a lookup table and two constructor functions).
5.4.13.2. The module file — enum_total
The macro module defines a single AstEnumerationAnnotation subclass
that adds a total entry to any enum.
Full source: enum_macro_mod.das
[enumeration_macro(name="enum_total")]
class EnumTotalAnnotation : AstEnumerationAnnotation {
def override apply(var enu : EnumerationPtr;
var group : ModuleGroup;
args : AnnotationArgumentList;
var errors : das_string) : bool {
// Check that the enum doesn't already have a "total" entry.
for (ee in enu.list) {
if (ee.name == "total") {
errors := "enumeration already has a 'total' field"
return false
}
}
// Add a new "total" entry at the end.
let idx = add_enumeration_entry(enu, "total")
if (idx < 0) {
errors := "failed to add 'total' field"
return false
}
// Set total = number of original entries.
// length(enu.list) is now N+1, so total = N+1-1 = N.
enu.list[idx].value |> move_new() <| new ExprConstInt(
at = enu.at, value = length(enu.list) - 1)
return true
}
}
Key points:
enu.listis the array ofEnumEntrynodes — iterate it to validate existing entries.add_enumeration_entry(enu, "total")appends a new entry and returns its index (or-1on failure).enu.list[idx].valueis anExpressionPtr— usemove_newto assign a newExprConstIntwith the desired integer value.enu.atprovides the source location for the generated expression.Return
falsewith an error message to abort compilation — the error will point to the enum declaration.
5.4.13.3. The usage file
Full source: 13_enumeration_macro.das
5.4.13.3.1. Section 1 — enum_total (modifying the enum)
require enum_macro_mod
[enum_total]
enum Direction {
North
South
East
West
}
def section1() {
print("--- Section 1: enum_total ---\n")
print("Direction.total = {int(Direction.total)}\n")
for (d in each(Direction.North)) {
if (d == Direction.total) {
break
}
print(" {d}\n")
}
}
The [enum_total] annotation runs before type inference and appends
total = 4 to the enum. At compile time the enum becomes:
enum Direction {
North = 0
South = 1
East = 2
West = 3
total = 4
}
The each() function from daslib/enum_trait iterates all values
(including total), so the loop breaks when it reaches the sentinel.
5.4.13.3.2. Section 2 — string_to_enum (generating code)
require daslib/enum_trait
[string_to_enum]
enum Color {
Red
Green
Blue
}
def section2() {
print("--- Section 2: string_to_enum ---\n")
let c1 = Color("Red")
print("Color(\"Red\") = {c1}\n")
let c2 = Color("invalid", Color.Blue)
print("Color(\"invalid\", Color.Blue) = {c2}\n")
}
The [string_to_enum] annotation from daslib/enum_trait generates
three things at compile time:
A private global table
_`enum`table`Colormapping strings to enum values — created withadd_global_private_letandenum_to_table.A single-argument constructor
Color(src : string) : Color— panics if the string is not a valid enum name.A two-argument constructor
Color(src : string; defaultValue : Color) : Color— returnsdefaultValueif the string is not found.
Both constructors are generated with qmacro_function, registered
with add_function(compiling_module(), fn), and marked as
non-private with enumFn.flags &= ~FunctionFlags.privateFunction.
5.4.13.4. How string_to_enum works internally
The EnumFromStringConstruction class in daslib/enum_trait.das
demonstrates the code generation pattern for enumeration macros:
[enumeration_macro(name="string_to_enum")]
class EnumFromStringConstruction : AstEnumerationAnnotation {
def override apply(var enu : EnumerationPtr;
var group : ModuleGroup;
args : AnnotationArgumentList;
var errors : das_string) : bool {
var inscope enumT <- new TypeDecl(
baseType = Type.tEnumeration,
enumType = enu.get_ptr())
// 1. Create a private global lookup table.
let varName = "_`enum`table`{enu.name}"
add_global_private_let(
compiling_module(), varName, enu.at,
qmacro(enum_to_table(type<$t(enumT)>)))
// 2. Generate the panic-on-miss constructor.
var inscope enumFn <- qmacro_function("{enu.name}")
$(src : string) : $t(enumT) {
if (!key_exists($i(varName), src)) {
panic("enum value '{src}' not found")
}
return $i(varName)?[src] ?? default<$t(enumT)>
}
enumFn.flags &= ~FunctionFlags.privateFunction
force_at(enumFn, enu.at)
force_generated(enumFn, true)
compiling_module() |> add_function(enumFn)
// 3. Generate the default-on-miss constructor.
var inscope enumFnDefault <- qmacro_function("{enu.name}")
$(src : string; defaultValue : $t(enumT)) : $t(enumT) {
return $i(varName)?[src] ?? defaultValue
}
enumFnDefault.flags &= ~FunctionFlags.privateFunction
force_at(enumFnDefault, enu.at)
force_generated(enumFnDefault, true)
compiling_module() |> add_function(enumFnDefault)
return true
}
}
Key code-generation techniques:
``qmacro_function(“name”) $(args) : ReturnType { body }`` — creates a new function AST node. Reification splices (
$t(),$i()) inject types and identifiers from variables.``add_global_private_let(module, name, at, expr)`` — adds a private global
letvariable initialized byexpr.``compiling_module()`` — returns the module being compiled (where the annotated enum lives), so generated functions appear in the user’s module.
``force_at(fn, at)`` — sets the source location of all nodes in the generated function to
at, so error messages point to the enum declaration.``force_generated(fn, true)`` — marks the function as compiler-generated (suppresses “unused function” warnings).
5.4.13.5. Output
--- Section 1: enum_total ---
Direction.total = 4
North
South
East
West
--- Section 2: string_to_enum ---
Color("Red") = Red
Color("invalid", Color.Blue) = Blue
5.4.13.6. Real-world usage
daslib/enum_trait provides a rich set of enumeration utilities:
[string_to_enum]— generates string constructors (shown above)each(enumValue)— iterates over all values of an enum typestring(enumValue)— converts an enum value to its nameto_enum(type<E>, "name")— runtime string-to-enum conversionenum_to_table(type<E>)— creates atable<string; E>lookuptypeinfo enum_length(type<E>)— compile-time count of enum valuestypeinfo enum_names(type<E>)— compile-time array of value names
The two patterns shown in this tutorial cover the majority of enumeration macro use cases:
Modify the enum — add sentinel values, computed entries, or validation (like
[enum_total]).Generate code — create functions, tables, or variables derived from the enum’s structure (like
[string_to_enum]).
See also
Full source:
13_enumeration_macro.das,
enum_macro_mod.das
Previous tutorial: tutorial_macro_typeinfo_macro
Next tutorial: tutorial_macro_pass_macro
Standard library: daslib/enum_trait.das —
enum_trait module reference
Language reference: Macros — full macro system documentation