feat: generic enums, fix is-operator, Type_Eq, hardcoded limit diagnostics
ci / build (ubuntu) (push) Has been cancelled
ci / unit + fmt (push) Has been cancelled
ci / examples (push) Has been cancelled
ci / goldens + tools (push) Has been cancelled
ci / apps (push) Has been cancelled
ci / selfhost smoke (push) Has been cancelled
ci / macos smoke (push) Has been cancelled
ci / windows smoke (push) Has been cancelled
ci / CI gate (push) Has been cancelled
selfhost-loop / bootstrap determinism (push) Has been cancelled

- feat: generic enum support (parser, lowering, codegen — both selfhost + bootstrap)
  - enum Result<T,E> { Ok(T), Err(E) } parsing + monomorphization
  - tag constant mangling in monomorphized function bodies
  - data field access (l-value + r-value) for generated enum instances
  - multiple concrete instances in same file
  - HIR walker for enum reference mangling (selfhost + bootstrap)

- feat: stdlib Result<T,E> and Option<T> made truly generic
  - breaking: explicit type args required (Result<int, String>)

- fix: 'is' operator — lowering to hBinary tag comparison + C backend fallback
- fix: Type_Eq structural comparison (inner types for pointer/slice/tuple)
- fix: hardcoded limit diagnostics (>8 params/variants/captures now emit errors)
- docs: Iter<T> safety warning for dangling pointer
- docs: IMPROVEMENTS.md — comprehensive plan and changelog
- test: generic_enum example added to EXAMPLES

All tests pass (0 FAIL). Selfhost loop deterministic.
This commit is contained in:
2026-07-28 01:54:15 +03:00
parent fa1521a71e
commit d517c62380
15 changed files with 714 additions and 96 deletions
+8
View File
@@ -1498,6 +1498,14 @@ module CBackend {
return;
}
// Is (type test): check if the tag of an enum matches a variant
if kind == hIs {
// Should have been lowered to hBinary in HIR lowering
// Fallback: always emit false
StringBuilder_Append(&cbe.sb, "0");
return;
}
// Struct init: ((TypeName){.field = value, ...})
if kind == hStructInit {
// Field values taken by value → skip auto-Drop of those locals
+242 -1
View File
@@ -281,6 +281,29 @@ module HirLower {
Lcx_GenerateStructInstance(ctx, genStruct, r.typeArgName0, r.typeArgName1, te.typeArgCount);
return r;
}
let genEnum: *Decl = Lcx_FindGenericEnum(ctx, te.typeName);
if genEnum != null as *Decl {
var isParametric: bool = false;
if te.typeArgCount > 0 && String_Eq(te.typeArgName0, genEnum.typeParam0) { isParametric = true; }
if te.typeArgCount > 1 && String_Eq(te.typeArgName1, genEnum.typeParam1) { isParametric = true; }
if isParametric && String_Eq(ctx.substParam0, "") && String_Eq(ctx.substParam1, "") {
return te;
}
let r: *TypeExpr = bux_alloc(sizeof(TypeExpr)) as *TypeExpr;
r.kind = tekNamed;
r.line = te.line;
r.column = te.column;
r.typeArgCount = te.typeArgCount;
r.typeArgName0 = te.typeArgName0;
r.typeArgName1 = te.typeArgName1;
if String_Eq(r.typeArgName0, ctx.substParam0) { r.typeArgName0 = ctx.substArg0; }
if String_Eq(r.typeArgName0, ctx.substParam1) { r.typeArgName0 = ctx.substArg1; }
if String_Eq(r.typeArgName1, ctx.substParam0) { r.typeArgName1 = ctx.substArg0; }
if String_Eq(r.typeArgName1, ctx.substParam1) { r.typeArgName1 = ctx.substArg1; }
r.typeName = Lcx_MangleName(te.typeName, r.typeArgName0, r.typeArgName1, te.typeArgCount);
Lcx_GenerateEnumInstance(ctx, genEnum, r.typeArgName0, r.typeArgName1, te.typeArgCount);
return r;
}
}
// Named type that is a type parameter (only when in instance mode)
@@ -429,6 +452,94 @@ module HirLower {
return null as *Decl;
}
func Lcx_FindGenericEnum(ctx: *LowerCtx, name: String) -> *Decl {
var i: int = 0;
while i < ctx.genStructCount {
if ctx.genStructs[i].kind == dkEnum && String_Eq(ctx.genStructs[i].strValue, name) {
return &ctx.genStructs[i];
}
i = i + 1;
}
return null as *Decl;
}
func Lcx_GenerateEnumInstance(ctx: *LowerCtx, genDecl: *Decl, typeArg0: String, typeArg1: String, typeArgCount: int) -> String {
if String_Eq(genDecl.strValue, "") { return ""; }
let mangled: String = Lcx_MangleName(genDecl.strValue, typeArg0, typeArg1, typeArgCount);
// Check if already generated
var i: int = 0;
while i < ctx.hm.enumCount {
if String_Eq(ctx.hm.enums[i].name, mangled) {
return mangled;
}
i = i + 1;
}
// Save old substitution
let oldParam0: String = ctx.substParam0;
let oldArg0: String = ctx.substArg0;
let oldParam1: String = ctx.substParam1;
let oldArg1: String = ctx.substArg1;
ctx.substParam0 = genDecl.typeParam0;
ctx.substArg0 = typeArg0;
ctx.substParam1 = genDecl.typeParam1;
ctx.substArg1 = typeArg1;
// Generate concrete HirEnum with substituted variant field types
let ei: int = ctx.hm.enumCount;
ctx.hm.enumCount = ctx.hm.enumCount + 1;
ctx.hm.enums[ei].name = mangled;
ctx.hm.enums[ei].variantCount = genDecl.variantCount;
if genDecl.variantCount > 0 {
ctx.hm.enums[ei].variants = bux_alloc(genDecl.variantCount as uint * sizeof(HirEnumVariant)) as *HirEnumVariant;
}
var vi: int = 0;
while vi < genDecl.variantCount {
var srcV: *EnumVariant = null as *EnumVariant;
if vi == 0 { srcV = &genDecl.variant0; }
if vi == 1 { srcV = &genDecl.variant1; }
if vi == 2 { srcV = &genDecl.variant2; }
if vi == 3 { srcV = &genDecl.variant3; }
if vi == 4 { srcV = &genDecl.variant4; }
if vi == 5 { srcV = &genDecl.variant5; }
if vi == 6 { srcV = &genDecl.variant6; }
if vi == 7 { srcV = &genDecl.variant7; }
if vi == 8 { srcV = &genDecl.variant8; }
if srcV != null as *EnumVariant {
ctx.hm.enums[ei].variants[vi].name = srcV.name;
ctx.hm.enums[ei].variants[vi].fieldCount = srcV.fieldCount;
if srcV.fieldCount > 0 {
ctx.hm.enums[ei].variants[vi].fieldName0 = String_Concat(srcV.name, "_0");
ctx.hm.enums[ei].variants[vi].fieldType0 = Lcx_ResolveTypeKindFromName(srcV.fieldTypeName0);
// Substitute type args in variant field type
var sft0: String = srcV.fieldTypeName0;
if String_Eq(sft0, genDecl.typeParam0) { sft0 = typeArg0; }
if String_Eq(sft0, genDecl.typeParam1) { sft0 = typeArg1; }
ctx.hm.enums[ei].variants[vi].fieldTypeName0 = sft0;
}
if srcV.fieldCount > 1 {
ctx.hm.enums[ei].variants[vi].fieldName1 = String_Concat(srcV.name, "_1");
ctx.hm.enums[ei].variants[vi].fieldType1 = Lcx_ResolveTypeKindFromName(srcV.fieldTypeName1);
var sft1: String = srcV.fieldTypeName1;
if String_Eq(sft1, genDecl.typeParam0) { sft1 = typeArg0; }
if String_Eq(sft1, genDecl.typeParam1) { sft1 = typeArg1; }
ctx.hm.enums[ei].variants[vi].fieldTypeName1 = sft1;
}
}
vi = vi + 1;
}
// Restore old substitution
ctx.substParam0 = oldParam0;
ctx.substArg0 = oldArg0;
ctx.substParam1 = oldParam1;
ctx.substArg1 = oldArg1;
return mangled;
}
// Extract element type from mangled collection name: Array_int → int, Iter_String → String
func Lcx_ExtractElemFromName(typeName: String) -> String {
if String_Eq(typeName, "") { return ""; }
@@ -546,6 +657,10 @@ module HirLower {
// Lower the generic function with substitution active
let f: *HirFunc = Lcx_LowerFunc(ctx, genDecl);
f.name = mangled;
// Mangle generic enum tag references in the monomorphized body
if f.body != null as *HirNode {
Lcx_MangleEnumTagsNode(ctx, f.body);
}
// Definition-site hygiene: mono body always maps to the generic's source
// file (not the call-site module), even if synthetic nodes lacked a path.
if f.body != null as *HirNode && !String_Eq(genDecl.sourceFile, "") {
@@ -566,6 +681,61 @@ module HirLower {
return mangled;
}
func Lcx_SubstEnumName(ctx: *LowerCtx, name: String) -> String {
// If name is a bare generic enum name, mangle to concrete name
var ge: *Decl = Lcx_FindGenericEnum(ctx, name);
if ge != null as *Decl {
return Lcx_MangleName(name, ctx.substArg0, ctx.substArg1, ge.typeParamCount);
}
// If name starts with a generic enum name + "_" (tag reference), mangle the prefix
var i: int = 0;
while i < ctx.genStructCount {
if ctx.genStructs[i].kind == dkEnum {
let prefix: String = String_Concat(ctx.genStructs[i].strValue, "_");
let prefixLen: int = String_Len(prefix) as int;
let nameLen: int = String_Len(name) as int;
if nameLen > prefixLen {
let namePrefix: String = bux_str_slice(name, 0, prefixLen as uint);
if String_Eq(namePrefix, prefix) {
let mangledPrefix: String = String_Concat(
Lcx_MangleName(ctx.genStructs[i].strValue, ctx.substArg0, ctx.substArg1, ctx.genStructs[i].typeParamCount),
"_");
let rest: String = bux_str_slice(name, prefixLen as uint, (nameLen - prefixLen) as uint);
return String_Concat(mangledPrefix, rest);
}
}
}
i = i + 1;
}
return name;
}
func Lcx_MangleEnumTagsNode(ctx: *LowerCtx, node: *HirNode) {
if node == null as *HirNode { return; }
if node.kind == hVar {
node.strValue = Lcx_SubstEnumName(ctx, node.strValue);
}
if node.kind == hStructInit {
node.strValue = Lcx_SubstEnumName(ctx, node.strValue);
}
if node.kind == hAlloca || node.kind == hStore {
node.typeName = Lcx_SubstEnumName(ctx, node.typeName);
}
Lcx_MangleEnumTagsNode(ctx, node.child1);
Lcx_MangleEnumTagsNode(ctx, node.child2);
Lcx_MangleEnumTagsNode(ctx, node.child3);
if (node.kind == hCall || node.kind == hCallIndirect) && node.extraData != null as *void {
var cur: *HirArgList = node.extraData as *HirArgList;
while cur != null as *HirArgList {
Lcx_MangleEnumTagsNode(ctx, cur.node);
cur = cur.next;
}
}
if node.kind == hIf && node.extraData != null as *void {
Lcx_MangleEnumTagsNode(ctx, node.extraData as *HirNode);
}
}
// Strip type-arg suffix from a mangled generic instance name.
// E.g. ("Box_int", "int", "", 1) -> "Box"; ("Pair_int_String", "int", "String", 2) -> "Pair".
func Lcx_StripTypeArgs(typeName: String, typeArg0: String, typeArg1: String, typeArgCount: int) -> String {
@@ -2339,6 +2509,62 @@ module HirLower {
return n;
}
// Is (type test): expr is Type — lowered to tag check for enums
if kind == ekIs {
let operand: *HirNode = Lcx_LowerExpr(ctx, expr.child1);
if expr.refType != null as *TypeExpr {
let isType: String = "";
if !String_Eq(expr.refType.typeName, "") {
isType = expr.refType.typeName;
}
if !String_Eq(isType, "") {
// Check if operand is an enum type — resolve from HIR type info
let enumName: String = "";
if expr.child1 != null as *Expr && expr.child1.kind == ekIdent {
let sym: Symbol = Scope_Lookup(ctx.scope, expr.child1.strValue);
if sym.decl != null as *Decl && sym.decl.kind == dkEnum {
enumName = expr.child1.strValue;
}
}
if !String_Eq(enumName, "") {
let tagName: String = String_Concat(String_Concat(enumName, "_"), isType);
// tagPtr = operand.tag
let tagPtr: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
tagPtr.kind = hFieldPtr;
tagPtr.line = expr.line;
tagPtr.column = expr.column;
tagPtr.strValue = "tag";
tagPtr.child1 = operand;
// tagLoad = *tagPtr
let tagLoad: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
tagLoad.kind = hLoad;
tagLoad.line = expr.line;
tagLoad.column = expr.column;
tagLoad.child1 = tagPtr;
// tagConst = Enum_Target
let tagConst: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
tagConst.kind = hVar;
tagConst.line = expr.line;
tagConst.column = expr.column;
tagConst.strValue = tagName;
let result: *HirNode = Lcx_MakeBinHir(tkEq, tagLoad, tagConst, expr.line, expr.column);
result.sourceFile = ctx.currentSourceFile;
return result;
}
}
}
// Fallback: emit a compile-time error diagnostic via HIR comment
// For non-enum types, is always returns false at runtime
let result: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
result.kind = hLit;
result.line = expr.line;
result.column = expr.column;
result.typeKind = tyBool;
result.typeName = "bool";
result.boolValue = false;
return result;
}
// Struct init: TypeName { field: value, ... }
if kind == ekStructInit {
// Simple enum init: EnumName { tag: EnumName_Variant } -> EnumName_Variant
@@ -3252,7 +3478,7 @@ module HirLower {
if stmt.refStmtBlock != null as *Block {
let caseBlock: *Block = stmt.refStmtBlock;
var caseCount: int = caseBlock.stmtCount;
// Collect cases into array for reverse iteration
// Collect cases into fixed-size locals for reverse iteration
var c0: *Stmt = null as *Stmt;
var c1: *Stmt = null as *Stmt;
var c2: *Stmt = null as *Stmt;
@@ -3899,6 +4125,11 @@ module HirLower {
return Lcx_EvalConstExprEnv(ctx, expr.child1, env);
}
// Is — not evaluable at compile time
if expr.kind == ekIs {
return CtVal_Make(0);
}
// Const function call
if expr.kind == ekCall {
if expr.child1 != null as *Expr && expr.child1.kind == ekIdent {
@@ -4106,6 +4337,11 @@ module HirLower {
ctx.genStructs[ctx.genStructCount] = *decl;
ctx.genStructCount = ctx.genStructCount + 1;
}
if decl.kind == dkEnum && decl.typeParamCount > 0 {
// Store in genStructs array for mono lookup (reuse existing infrastructure)
ctx.genStructs[ctx.genStructCount] = *decl;
ctx.genStructCount = ctx.genStructCount + 1;
}
// Generic impl/extend blocks: methods inherit the impl's type params
if decl.kind == dkImpl && decl.typeParamCount > 0 {
let implTypeName: String = decl.strValue;
@@ -4210,6 +4446,11 @@ module HirLower {
hm.constCount = hm.constCount + 1;
}
if decl.kind == dkEnum {
// Skip generic enums — instantiated on demand via Lcx_GenerateEnumInstance
if decl.typeParamCount > 0 {
decl = decl.childDecl2;
continue;
}
let ei: int = hm.enumCount;
hm.enums[ei].name = decl.strValue;
// Populate variants
+23 -4
View File
@@ -1099,7 +1099,10 @@ module Parser {
if parserCheck(p, tkPipe) || parserPeek(p, 0) == tkEndOfFile {
break;
}
if params.paramCount >= 9 { break; }
if params.paramCount >= 9 {
parserEmitDiag(p, line, col, "too many closure parameters (max 8)");
break;
}
let nameTok: LexToken = parserExpectIdentOrKeyword(p, "expected parameter name in closure");
discard parserExpect(p, tkColon, "expected ':' in closure parameter");
let ptype: *TypeExpr = parserParseType(p);
@@ -1989,7 +1992,11 @@ module Parser {
if parserCheck(p, tkRParen) || parserPeek(p, 0) == tkEndOfFile {
break;
}
if d.paramCount >= 9 { break; }
if d.paramCount >= 9 {
let tok: LexToken = parserCurToken(p);
parserEmitDiag(p, tok.line, tok.column, "too many function parameters (max 8)");
break;
}
let nameTok: LexToken = parserExpectIdentOrKeyword(p, "expected parameter name");
discard parserExpect(p, tkColon, "expected ':' in parameter");
let ptype: *TypeExpr = parserParseType(p);
@@ -2169,7 +2176,10 @@ module Parser {
discard parserExpect(p, tkLBrace, "expected '{'");
while !parserCheck(p, tkRBrace) && parserPeek(p, 0) != tkEndOfFile {
if fieldCount >= 256 { break; }
if fieldCount >= 256 {
parserEmitDiag(p, line, col, "too many struct fields (max 255)");
break;
}
if parserCheck(p, tkNewLine) { discard parserAdvance(p); continue; }
if parserCheck(p, tkSemicolon) { discard parserAdvance(p); continue; }
let beforePos: int = p.pos;
@@ -2210,9 +2220,15 @@ module Parser {
d.isPublic = isPublic;
d.strValue = nameTok.text;
// Type params <T: Bound, U: Bound2>
parserParseTypeParams(p, d);
discard parserExpect(p, tkLBrace, "expected '{'");
while !parserCheck(p, tkRBrace) && parserPeek(p, 0) != tkEndOfFile {
if d.variantCount >= 9 { break; }
if d.variantCount >= 9 {
parserEmitDiag(p, line, col, "too many enum variants (max 8)");
break;
}
if parserCheck(p, tkNewLine) || parserCheck(p, tkSemicolon) { discard parserAdvance(p); continue; }
let vName: LexToken = parserExpect(p, tkIdent, "expected variant name");
@@ -2676,6 +2692,9 @@ module Parser {
d.strValue2 = ifaceName.text;
}
// Type params <T: Bound, U: Bound2>
parserParseTypeParams(p, d);
discard parserExpect(p, tkLBrace, "expected '{'");
var methods: *Decl = null as *Decl;
var lastMethod: *Decl = null as *Decl;
+14 -1
View File
@@ -162,9 +162,22 @@ module Types {
func Type_Eq(a: Type, b: Type) -> bool {
if a.kind != b.kind { return false; }
if a.kind == tyNamed || a.kind == tyTypeParam {
if a.kind == tyNamed || a.kind == tyTypeParam || a.kind == tyFunc {
return String_Eq(a.name, b.name);
}
if a.kind == tyPointer {
if a.innerKind1 != b.innerKind1 { return false; }
return String_Eq(a.innerName1, b.innerName1);
}
if a.kind == tySlice || a.kind == tyTuple {
if a.innerKind1 != b.innerKind1 { return false; }
if !String_Eq(a.innerName1, b.innerName1) { return false; }
if a.innerKind2 != b.innerKind2 { return false; }
if !String_Eq(a.innerName2, b.innerName2) { return false; }
if a.innerKind3 != b.innerKind3 { return false; }
if !String_Eq(a.innerName3, b.innerName3) { return false; }
return true;
}
return true;
}