feat: add algebraic enums (tagged unions) support

- Generate tagged unions in C backend for enums with data
- Add HirEnumVariant type with fields and namedFields
- Support _Tag and _Data field access in sema
- Support enum variant constants (Result_Ok, Result_Err)
- Support _Data union field access (Ok_0, Err_0, etc.)
- Add algebraic_enums.bux example

Example:
  enum Result {
      Ok(int),
      Err(String)
  }

  let r: Result = Result { tag: Result_Ok };
  r.data.Ok_0 = 42;
This commit is contained in:
2026-05-30 23:50:33 +03:00
parent b59e852330
commit cf074bec89
5 changed files with 181 additions and 22 deletions
+29
View File
@@ -0,0 +1,29 @@
// Algebraic Enums - Enums with data (tagged unions)
extern func Std_Io_PrintLine(s: String);
extern func Std_Io_PrintInt(n: int);
enum Result {
Ok(int),
Err(String)
}
func Main() -> int {
let r1: Result = Result { tag: Result_Ok };
r1.data.Ok_0 = 42;
let r2: Result = Result { tag: Result_Err };
r2.data.Err_0 = "error message";
if r1.tag == Result_Ok {
Std_Io_PrintLine("r1 is Ok:");
Std_Io_PrintInt(r1.data.Ok_0);
Std_Io_PrintLine("");
}
if r2.tag == Result_Err {
Std_Io_PrintLine("r2 is Err:");
Std_Io_PrintLine(r2.data.Err_0);
}
return 0;
}
+64 -11
View File
@@ -355,17 +355,70 @@ proc emitStruct*(be: var CBackend, name: string, fields: seq[tuple[name: string,
be.emitLine(&"}} {name};") be.emitLine(&"}} {name};")
be.emitLine("") be.emitLine("")
proc emitEnum*(be: var CBackend, name: string, variants: seq[string]) = proc emitEnum*(be: var CBackend, name: string, variants: seq[HirEnumVariant]) =
be.emitLine(&"typedef enum {{") # Check if this is a simple enum (no data) or algebraic enum (with data)
inc be.indent var hasData = false
for i, v in variants: for v in variants:
if i < variants.len - 1: if v.fields.len > 0 or v.namedFields.len > 0:
be.emitLine(&"{name}_{v},") hasData = true
else: break
be.emitLine(&"{name}_{v}")
dec be.indent if not hasData:
be.emitLine(&"}} {name};") # Simple enum - generate as before
be.emitLine("") be.emitLine(&"typedef enum {{")
inc be.indent
for i, v in variants:
if i < variants.len - 1:
be.emitLine(&"{name}_{v.name},")
else:
be.emitLine(&"{name}_{v.name}")
dec be.indent
be.emitLine(&"}} {name};")
be.emitLine("")
else:
# Algebraic enum - generate tagged union
# 1. Generate tag enum
be.emitLine(&"typedef enum {{")
inc be.indent
for i, v in variants:
if i < variants.len - 1:
be.emitLine(&"{name}_{v.name},")
else:
be.emitLine(&"{name}_{v.name}")
dec be.indent
be.emitLine(&"}} {name}_Tag;")
be.emitLine("")
# 2. Generate union for data
be.emitLine(&"typedef union {{")
inc be.indent
for v in variants:
if v.fields.len > 0:
# Positional fields
for i, f in v.fields:
let typ = typeToC(f)
be.emitLine(&"{typ} {v.name}_{i};")
elif v.namedFields.len > 0:
# Named fields - generate as struct
be.emitLine(&"struct {{")
inc be.indent
for nf in v.namedFields:
let typ = typeToC(nf.typ)
be.emitLine(&"{typ} {nf.name};")
dec be.indent
be.emitLine(&"}} {v.name};")
dec be.indent
be.emitLine(&"}} {name}_Data;")
be.emitLine("")
# 3. Generate main struct with tag + union
be.emitLine(&"typedef struct {{")
inc be.indent
be.emitLine(&"{name}_Tag tag;")
be.emitLine(&"{name}_Data data;")
dec be.indent
be.emitLine(&"}} {name};")
be.emitLine("")
proc emitExternDecl*(be: var CBackend, efunc: HirFunc) = proc emitExternDecl*(be: var CBackend, efunc: HirFunc) =
let retType = typeToC(efunc.retType) let retType = typeToC(efunc.retType)
+6 -1
View File
@@ -124,11 +124,16 @@ type
body*: HirNode body*: HirNode
isPublic*: bool isPublic*: bool
HirEnumVariant* = object
name*: string
fields*: seq[Type] # Positional fields for algebraic enums
namedFields*: seq[tuple[name: string, typ: Type]] # Named fields
HirModule* = object HirModule* = object
funcs*: seq[HirFunc] funcs*: seq[HirFunc]
externFuncs*: seq[HirFunc] # Functions declared with extern (no body) externFuncs*: seq[HirFunc] # Functions declared with extern (no body)
structs*: seq[tuple[name: string, fields: seq[tuple[name: string, typ: Type]]]] structs*: seq[tuple[name: string, fields: seq[tuple[name: string, typ: Type]]]]
enums*: seq[tuple[name: string, variants: seq[string]]] enums*: seq[tuple[name: string, variants: seq[HirEnumVariant]]]
consts*: seq[tuple[name: string, typ: Type, value: HirNode]] consts*: seq[tuple[name: string, typ: Type, value: HirNode]]
# Constructor helpers # Constructor helpers
+31 -3
View File
@@ -433,7 +433,7 @@ proc lowerModule*(module: Module, sema: Sema): HirModule =
var funcs: seq[HirFunc] = @[] var funcs: seq[HirFunc] = @[]
var externFuncs: seq[HirFunc] = @[] var externFuncs: seq[HirFunc] = @[]
var structs: seq[tuple[name: string, fields: seq[tuple[name: string, typ: Type]]]] = @[] var structs: seq[tuple[name: string, fields: seq[tuple[name: string, typ: Type]]]] = @[]
var enums: seq[tuple[name: string, variants: seq[string]]] = @[] var enums: seq[tuple[name: string, variants: seq[HirEnumVariant]]] = @[]
var consts: seq[tuple[name: string, typ: Type, value: HirNode]] = @[] var consts: seq[tuple[name: string, typ: Type, value: HirNode]] = @[]
for decl in module.items: for decl in module.items:
@@ -463,9 +463,37 @@ proc lowerModule*(module: Module, sema: Sema): HirModule =
fields.add((f.name, fType)) fields.add((f.name, fType))
structs.add((decl.declStructName, fields)) structs.add((decl.declStructName, fields))
of dkEnum: of dkEnum:
var variants: seq[string] = @[] var variants: seq[HirEnumVariant] = @[]
for v in decl.declEnumVariants: for v in decl.declEnumVariants:
variants.add(v.name) var fields: seq[Type] = @[]
for f in v.fields:
var fType = makeUnknown()
if f != nil and f.kind == tekNamed:
case f.typeName
of "int", "int32": fType = makeInt()
of "int64": fType = makeInt64()
of "float64": fType = makeFloat64()
of "float32": fType = makeFloat32()
of "bool": fType = makeBool()
of "String", "str": fType = makeStr()
else: fType = makeNamed(f.typeName)
fields.add(fType)
var namedFields: seq[tuple[name: string, typ: Type]] = @[]
for nf in v.namedFields:
var fType = makeUnknown()
if nf.ftype != nil and nf.ftype.kind == tekNamed:
case nf.ftype.typeName
of "int", "int32": fType = makeInt()
of "int64": fType = makeInt64()
of "float64": fType = makeFloat64()
of "float32": fType = makeFloat32()
of "bool": fType = makeBool()
of "String", "str": fType = makeStr()
else: fType = makeNamed(nf.ftype.typeName)
namedFields.add((nf.name, fType))
variants.add(HirEnumVariant(name: v.name, fields: fields, namedFields: namedFields))
enums.add((decl.declEnumName, variants)) enums.add((decl.declEnumName, variants))
of dkConst: of dkConst:
let value = ctx.lowerExpr(decl.declConstValue) let value = ctx.lowerExpr(decl.declConstValue)
+51 -7
View File
@@ -132,6 +132,13 @@ proc collectGlobals*(sema: var Sema) =
if not sema.globalScope.define(sym): if not sema.globalScope.define(sym):
sema.emitError(decl.loc, &"duplicate symbol '{decl.declEnumName}'") sema.emitError(decl.loc, &"duplicate symbol '{decl.declEnumName}'")
sema.typeTable[decl.declEnumName] = t sema.typeTable[decl.declEnumName] = t
# For algebraic enums, add variant constants with _Tag type
for variant in decl.declEnumVariants:
let variantName = decl.declEnumName & "_" & variant.name
let variantType = makeNamed(decl.declEnumName & "_Tag")
let variantSym = Symbol(kind: skConst, name: variantName, typ: variantType,
decl: decl, isPublic: decl.isPublic)
discard sema.globalScope.define(variantSym)
of dkUnion: of dkUnion:
let t = makeNamed(decl.declUnionName) let t = makeNamed(decl.declUnionName)
let sym = Symbol(kind: skType, name: decl.declUnionName, typ: t, let sym = Symbol(kind: skType, name: decl.declUnionName, typ: t,
@@ -398,14 +405,51 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type =
of ekField: of ekField:
let obj = sema.checkExpr(expr.exprFieldObj, scope) let obj = sema.checkExpr(expr.exprFieldObj, scope)
if obj.kind == tkNamed: if obj.kind == tkNamed:
let sym = sema.globalScope.lookup(obj.name) # Check if this is a _Data union field access
if sym != nil and sym.decl != nil and sym.decl.kind == dkStruct: if obj.name.endsWith("_Data"):
for f in sym.decl.declStructFields: let enumName = obj.name[0..^6] # Remove "_Data" suffix
if f.name == expr.exprFieldName: let enumSym = sema.globalScope.lookup(enumName)
return sema.resolveType(f.ftype) if enumSym != nil and enumSym.decl != nil and enumSym.decl.kind == dkEnum:
sema.emitError(expr.loc, &"struct '{obj.name}' has no field '{expr.exprFieldName}'") # Look for the field in enum variants
for variant in enumSym.decl.declEnumVariants:
# Check positional fields: Ok_0, Ok_1, etc.
for i, f in variant.fields:
let fieldName = variant.name & "_" & $i
if fieldName == expr.exprFieldName:
return sema.resolveType(f)
# Check named fields
for nf in variant.namedFields:
if nf.name == expr.exprFieldName:
return sema.resolveType(nf.ftype)
sema.emitError(expr.loc, &"union '{obj.name}' has no field '{expr.exprFieldName}'")
else:
sema.emitError(expr.loc, &"cannot access field on type {obj.toString}")
else: else:
sema.emitError(expr.loc, &"cannot access field on type {obj.toString}") let sym = sema.globalScope.lookup(obj.name)
if sym != nil and sym.decl != nil:
if sym.decl.kind == dkStruct:
for f in sym.decl.declStructFields:
if f.name == expr.exprFieldName:
return sema.resolveType(f.ftype)
sema.emitError(expr.loc, &"struct '{obj.name}' has no field '{expr.exprFieldName}'")
elif sym.decl.kind == dkEnum:
# Algebraic enum fields
if expr.exprFieldName == "tag":
return makeNamed(obj.name & "_Tag")
elif expr.exprFieldName == "data":
return makeNamed(obj.name & "_Data")
else:
sema.emitError(expr.loc, &"enum '{obj.name}' has no field '{expr.exprFieldName}'")
elif sym.decl.kind == dkUnion:
# Union fields
for f in sym.decl.declUnionFields:
if f.name == expr.exprFieldName:
return sema.resolveType(f.ftype)
sema.emitError(expr.loc, &"union '{obj.name}' has no field '{expr.exprFieldName}'")
else:
sema.emitError(expr.loc, &"cannot access field on type {obj.toString}")
else:
sema.emitError(expr.loc, &"cannot access field on type {obj.toString}")
else: else:
sema.emitError(expr.loc, &"cannot access field on type {obj.toString}") sema.emitError(expr.loc, &"cannot access field on type {obj.toString}")
return makeUnknown() return makeUnknown()