feat: nested multi-field enum patterns (bootstrap + selfhost)

Multi-field variants use Enum_Variant_Payload nested types (avoids tag name clash).
Sema resolves data.Variant.Variant_i; enum field types fully resolve tuples.
Selfhost parses full type exprs in enum payloads; emit structs/tuples before enums.
Example: examples/nested_patterns.bux (Pair::Two, Box::Val((a,c)), Shape::Dot).
This commit is contained in:
2026-07-18 01:33:36 +03:00
parent f9185c96b2
commit 66f11d1869
12 changed files with 383 additions and 209 deletions
+20 -19
View File
@@ -600,23 +600,12 @@ proc emitEnum*(be: var CBackend, name: string, variants: seq[HirEnumVariant]) =
let typ = typeToC(be, v.fields[0])
be.emitLine(&"{typ} {v.name}_0;")
elif v.fields.len > 1:
# Multi positional fields — nested struct so fields don't overlay
be.emitLine(&"struct {{")
inc be.indent
for i, f in v.fields:
let typ = typeToC(be, f)
be.emitLine(&"{typ} {v.name}_{i};")
dec be.indent
be.emitLine(&"}} {v.name};")
# Multi positional — named nested typedef Enum_Variant_Payload
let nestedName = name & "_" & v.name & "_Payload"
be.emitLine(&"{nestedName} {v.name};")
elif v.namedFields.len > 0:
# Named fields - generate as struct
be.emitLine(&"struct {{")
inc be.indent
for nf in v.namedFields:
let typ = typeToC(be, nf.typ)
be.emitLine(&"{typ} {nf.name};")
dec be.indent
be.emitLine(&"}} {v.name};")
let nestedName = name & "_" & v.name & "_Payload"
be.emitLine(&"{nestedName} {v.name};")
dec be.indent
be.emitLine(&"}} {name}_Data;")
be.emitLine("")
@@ -716,15 +705,27 @@ proc emitModule*(be: var CBackend, module: HirModule): string =
if module.structs.len > 0:
be.emitLine("")
# Enum definitions (must come before structs that reference them)
# Nested multi-field enum payloads (Enum_Variant_Payload) must be fully
# defined before the algebraic enum union that embeds them by value.
var payloadStructNames: seq[string] = @[]
for e in module.enums:
for v in e.variants:
if v.fields.len > 1 or v.namedFields.len > 0:
payloadStructNames.add(e.name & "_" & v.name & "_Payload")
for s in module.structs:
if s.name in payloadStructNames:
be.emitStruct(s.name, s.fields)
# Enum definitions (after payload structs; before user structs that may use them)
for e in module.enums:
be.emitEnum(e.name, e.variants)
if module.enums.len > 0:
be.emitLine("")
# Struct definitions
# Remaining struct definitions (skip payloads already emitted)
for s in module.structs:
be.emitStruct(s.name, s.fields)
if s.name notin payloadStructNames:
be.emitStruct(s.name, s.fields)
# Slice fat-pointer typedefs
if sliceTypes.len > 0:
+25 -25
View File
@@ -180,7 +180,9 @@ proc matchPatternBindings(ctx: var LowerCtx, subject: HirNode, pattern: Pattern,
let multiField = fieldTypes.len > 1
var payloadBase = dataLoad
if multiField:
let variantStructTy = makeNamed(variantName)
# Nested struct type Enum_Variant_Payload (avoids clash with tag Enum_Variant)
let nestedName = enumName & "_" & variantName & "_Payload"
let variantStructTy = makeNamed(nestedName)
let variantPtr = HirNode(kind: hFieldPtr, fieldPtrBase: dataLoad, fieldName: variantName,
typ: makePointer(variantStructTy), loc: loc)
payloadBase = HirNode(kind: hLoad, loadPtr: variantPtr, typ: variantStructTy, loc: loc)
@@ -208,10 +210,12 @@ proc matchPatternBindings(ctx: var LowerCtx, subject: HirNode, pattern: Pattern,
if entry.name == nf.name:
fieldTy = entry.typ
break
# Named payload fields live under data.VariantName.name
# Named payload fields live under data.Variant.name on Enum_Variant_Payload
let nestedName = enumName & "_" & variantName & "_Payload"
let variantStructTy = makeNamed(nestedName)
let variantPtr = HirNode(kind: hFieldPtr, fieldPtrBase: dataLoad, fieldName: variantName,
typ: makePointer(makeNamed(variantName)), loc: loc)
let variantLoad = HirNode(kind: hLoad, loadPtr: variantPtr, typ: makeNamed(variantName), loc: loc)
typ: makePointer(variantStructTy), loc: loc)
let variantLoad = HirNode(kind: hLoad, loadPtr: variantPtr, typ: variantStructTy, loc: loc)
let fieldPtr = HirNode(kind: hFieldPtr, fieldPtrBase: variantLoad, fieldName: nf.name,
typ: makePointer(fieldTy), loc: loc)
let fieldLoad = HirNode(kind: hLoad, loadPtr: fieldPtr, typ: fieldTy, loc: loc)
@@ -2217,33 +2221,29 @@ proc lowerModule*(module: Module, sema: Sema): HirModule =
for v in decl.declEnumVariants:
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)
# Full resolve — supports tuples, pointers, named types, etc.
fields.add(if f != nil: ctx.resolveTypeExpr(f) else: makeUnknown())
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)
let fType = if nf.ftype != nil: ctx.resolveTypeExpr(nf.ftype) else: makeUnknown()
namedFields.add((nf.name, fType))
variants.add(HirEnumVariant(name: v.name, fields: fields, namedFields: namedFields))
# Multi-field / named-field variants get a named nested struct type
# Enum_Variant_Payload (suffix avoids clash with tag constant Enum_Variant).
if fields.len > 1:
var nestedFields: seq[tuple[name: string, typ: Type]] = @[]
for i, ft in fields:
nestedFields.add((v.name & "_" & $i, ft))
let nestedName = decl.declEnumName & "_" & v.name & "_Payload"
structs.add((nestedName, nestedFields))
elif namedFields.len > 0:
var nestedFields: seq[tuple[name: string, typ: Type]] = @[]
for nf in namedFields:
nestedFields.add((nf.name, nf.typ))
let nestedName = decl.declEnumName & "_" & v.name & "_Payload"
structs.add((nestedName, nestedFields))
enums.add((decl.declEnumName, variants))
of dkConst:
let value = ctx.lowerExpr(decl.declConstValue)
+107 -111
View File
@@ -500,20 +500,12 @@ proc emitEnumDef(be: var LirCBackend, name: string, variants: seq[HirEnumVariant
# Single positional field — flat (compat: data.Variant_0)
be.emitLine(&"{typeToCStr(v.fields[0])} {v.name}_0;")
elif v.fields.len > 1:
# Multi positional — nested struct so fields don't share union storage
be.emitLine(&"struct {{")
be.indent += 1
for i, f in v.fields:
be.emitLine(&"{typeToCStr(f)} {v.name}_{i};")
be.indent -= 1
be.emitLine(&"}} {v.name};")
# Multi positional — named nested struct Enum_Variant_Payload
let nestedName = name & "_" & v.name & "_Payload"
be.emitLine(&"{nestedName} {v.name};")
elif v.namedFields.len > 0:
be.emitLine(&"struct {{")
be.indent += 1
for nf in v.namedFields:
be.emitLine(&"{typeToCStr(nf.typ)} {nf.name};")
be.indent -= 1
be.emitLine(&"}} {v.name};")
let nestedName = name & "_" & v.name & "_Payload"
be.emitLine(&"{nestedName} {v.name};")
be.indent -= 1
be.emitLine(&"}} {name}_Data;")
be.emitLine("")
@@ -648,104 +640,8 @@ proc emitModule*(be: var LirCBackend, builder: LirBuilder, module: HirModule): s
for e in module.enums:
localTypeNames.incl(e.name)
# Collect slice types used in struct fields and enum payloads.
var sliceTypes: seq[tuple[name: string, elem: string]] = @[]
var sliceNames: HashSet[string]
proc registerSlice(t: Type) =
if t == nil or t.kind != tkSlice: return
let name = typeToCStr(t)
if sliceNames.contains(name): return
sliceNames.incl(name)
let elem = if t.inner.len > 0: typeToCStr(t.inner[0]) else: "void"
sliceTypes.add((name, elem))
for s in module.structs:
for f in s.fields:
registerSlice(f.typ)
for e in module.enums:
for v in e.variants:
for ft in v.fields:
registerSlice(ft)
for nf in v.namedFields:
registerSlice(nf.typ)
# Build dependency graph among structs, enums, and slice types.
# Edge A -> B means "A depends on B, so B must be emitted before A".
var deps: Table[string, seq[string]]
for s in module.structs:
deps[s.name] = @[]
for e in module.enums:
deps[e.name] = @[]
for st in sliceTypes:
deps[st.name] = @[]
proc addDeps(node: string, t: Type) =
for dep in collectValueDeps(t):
if dep == node: continue
if localTypeNames.contains(dep) or sliceNames.contains(dep):
if dep notin deps[node]:
deps[node].add(dep)
for s in module.structs:
for f in s.fields:
addDeps(s.name, f.typ)
for e in module.enums:
for v in e.variants:
for ft in v.fields:
addDeps(e.name, ft)
for nf in v.namedFields:
addDeps(e.name, nf.typ)
# Topological sort (Kahn's algorithm).
var inDegree: Table[string, int]
var dependents: Table[string, seq[string]]
for node in deps.keys:
inDegree[node] = 0
for node, nodeDeps in deps:
for d in nodeDeps:
if not inDegree.hasKey(d): inDegree[d] = 0
inDegree[node] += 1
dependents.mgetOrPut(d, @[]).add(node)
var queue: seq[string] = @[]
for node, deg in inDegree:
if deg == 0:
queue.add(node)
var sorted: seq[string] = @[]
while queue.len > 0:
let node = queue.pop()
sorted.add(node)
for depNode in dependents.getOrDefault(node):
inDegree[depNode] -= 1
if inDegree[depNode] == 0:
queue.add(depNode)
if sorted.len < deps.len:
# Cycle detected; fall back to a safe deterministic order.
sorted = @[]
for s in module.structs: sorted.add(s.name)
for e in module.enums: sorted.add(e.name)
for st in sliceTypes: sorted.add(st.name)
# Map type names back to their definitions.
var structMap: Table[string, seq[tuple[name: string, typ: Type]]]
for s in module.structs: structMap[s.name] = s.fields
var enumMap: Table[string, seq[HirEnumVariant]]
for e in module.enums: enumMap[e.name] = e.variants
var sliceMap: Table[string, string]
for st in sliceTypes: sliceMap[st.name] = st.elem
# Emit type definitions in dependency order.
for name in sorted:
if structMap.hasKey(name):
be.emitStructDef(name, structMap[name])
elif enumMap.hasKey(name):
be.emitEnumDef(name, enumMap[name])
elif sliceMap.hasKey(name):
be.emitSliceTypeDef(name, sliceMap[name])
# Collect and emit tuple typedefs used in the module (and nested tuples first).
# Emit tuple typedefs early — enums/structs may embed them by value
# (e.g. Box::Val((int,int)) → Tuple_int_int Val_0 in the union).
var tupleTypes: seq[Type] = @[]
var tupleNames: HashSet[string]
proc registerTuple(t: Type) =
@@ -840,6 +736,106 @@ proc emitModule*(be: var LirCBackend, builder: LirBuilder, module: HirModule): s
for tt in tupleTypes:
be.emitTupleDef(tt)
# Collect slice types used in struct fields and enum payloads.
var sliceTypes: seq[tuple[name: string, elem: string]] = @[]
var sliceNames: HashSet[string]
proc registerSlice(t: Type) =
if t == nil or t.kind != tkSlice: return
let name = typeToCStr(t)
if sliceNames.contains(name): return
sliceNames.incl(name)
let elem = if t.inner.len > 0: typeToCStr(t.inner[0]) else: "void"
sliceTypes.add((name, elem))
for s in module.structs:
for f in s.fields:
registerSlice(f.typ)
for e in module.enums:
for v in e.variants:
for ft in v.fields:
registerSlice(ft)
for nf in v.namedFields:
registerSlice(nf.typ)
# Build dependency graph among structs, enums, and slice types.
# Edge A -> B means "A depends on B, so B must be emitted before A".
var deps: Table[string, seq[string]]
for s in module.structs:
deps[s.name] = @[]
for e in module.enums:
deps[e.name] = @[]
for st in sliceTypes:
deps[st.name] = @[]
proc addDeps(node: string, t: Type) =
for dep in collectValueDeps(t):
if dep == node: continue
if localTypeNames.contains(dep) or sliceNames.contains(dep):
if dep notin deps[node]:
deps[node].add(dep)
for s in module.structs:
for f in s.fields:
addDeps(s.name, f.typ)
for e in module.enums:
for v in e.variants:
for ft in v.fields:
addDeps(e.name, ft)
for nf in v.namedFields:
addDeps(e.name, nf.typ)
# Multi-field / named-field nested struct must be defined before the enum
if v.fields.len > 1 or v.namedFields.len > 0:
addDeps(e.name, makeNamed(e.name & "_" & v.name & "_Payload"))
# Topological sort (Kahn's algorithm).
var inDegree: Table[string, int]
var dependents: Table[string, seq[string]]
for node in deps.keys:
inDegree[node] = 0
for node, nodeDeps in deps:
for d in nodeDeps:
if not inDegree.hasKey(d): inDegree[d] = 0
inDegree[node] += 1
dependents.mgetOrPut(d, @[]).add(node)
var queue: seq[string] = @[]
for node, deg in inDegree:
if deg == 0:
queue.add(node)
var sorted: seq[string] = @[]
while queue.len > 0:
let node = queue.pop()
sorted.add(node)
for depNode in dependents.getOrDefault(node):
inDegree[depNode] -= 1
if inDegree[depNode] == 0:
queue.add(depNode)
if sorted.len < deps.len:
# Cycle detected; fall back to a safe deterministic order.
sorted = @[]
for s in module.structs: sorted.add(s.name)
for e in module.enums: sorted.add(e.name)
for st in sliceTypes: sorted.add(st.name)
# Map type names back to their definitions.
var structMap: Table[string, seq[tuple[name: string, typ: Type]]]
for s in module.structs: structMap[s.name] = s.fields
var enumMap: Table[string, seq[HirEnumVariant]]
for e in module.enums: enumMap[e.name] = e.variants
var sliceMap: Table[string, string]
for st in sliceTypes: sliceMap[st.name] = st.elem
# Emit type definitions in dependency order.
for name in sorted:
if structMap.hasKey(name):
be.emitStructDef(name, structMap[name])
elif enumMap.hasKey(name):
be.emitEnumDef(name, enumMap[name])
elif sliceMap.hasKey(name):
be.emitSliceTypeDef(name, sliceMap[name])
# Fat function-pointer typedefs (BuxFn_*) — before forward decls that use them
var fatTypes: seq[Type] = @[]
var fatNames: HashSet[string]
+30 -3
View File
@@ -1330,12 +1330,18 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type =
if enumSym != nil and enumSym.decl != nil and enumSym.decl.kind == dkEnum:
# Look for the field in enum variants
for variant in enumSym.decl.declEnumVariants:
# Check positional fields: Ok_0, Ok_1, etc.
# Multi-field / named-field variant: data.Variant → Enum_Variant_Payload
# (suffix avoids clashing with tag constant Enum_Variant)
if variant.fields.len > 1 and variant.name == expr.exprFieldName:
return makeNamed(enumName & "_" & variant.name & "_Payload")
if variant.namedFields.len > 0 and variant.name == expr.exprFieldName:
return makeNamed(enumName & "_" & variant.name & "_Payload")
# Single positional fields: Ok_0, Ok_1, etc. (flat on the union)
for i, f in variant.fields:
let fieldName = variant.name & "_" & $i
if fieldName == expr.exprFieldName:
return sema.resolveType(f)
# Check named fields
# Named fields nested under data.Variant.name
for nf in variant.namedFields:
if nf.name == expr.exprFieldName:
return sema.resolveType(nf.ftype)
@@ -1378,7 +1384,28 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type =
else:
sema.emitError(expr.loc, &"cannot access field on type {obj.toString}")
else:
sema.emitError(expr.loc, &"cannot access field on type {obj.toString}")
# Synthetic nested multi-field type Enum_Variant_Payload — generated for
# multi-field / named-field algebraic variants (not a user-declared type).
var foundNested = false
for (_, gsym) in sema.globalScope.table.pairs:
if gsym.decl == nil or gsym.decl.kind != dkEnum: continue
let ename = gsym.decl.declEnumName
for variant in gsym.decl.declEnumVariants:
let nestedName = ename & "_" & variant.name & "_Payload"
if nestedName != objType.name: continue
foundNested = true
for i, f in variant.fields:
let fieldName = variant.name & "_" & $i
if fieldName == expr.exprFieldName:
return sema.resolveType(f)
for nf in variant.namedFields:
if nf.name == expr.exprFieldName:
return sema.resolveType(nf.ftype)
sema.emitError(expr.loc, &"nested variant type '{objType.name}' has no field '{expr.exprFieldName}'")
return makeUnknown()
if not foundNested:
sema.emitError(expr.loc, &"undeclared type '{objType.name}'")
return makeUnknown()
elif objType.kind == tkDynRef:
# Trait object: methods come from the interface
let ifaceName = objType.name