feat: struct and tuple patterns in match (bootstrap + selfhost)

Support destructuring in match arms:
- Tuple: (a, b) binds subject._0 / _1
- Struct: Point { x: px, y: py } and shorthand Point { x, y }

Bootstrap: matchPatternBindings for pkTuple/pkStruct; register local
tuple typedefs from function bodies. Selfhost: parse, Sema_BindPattern,
Lcx_PatternBindings with scope defines. Fix operator-overload path that
crashed when typeName was null after pattern binds.

Example: examples/struct_tuple_pat.bux. Selfhost-loop IDENTICAL.
This commit is contained in:
2026-07-18 01:15:44 +03:00
parent eac78f28c1
commit e3ca724bfa
12 changed files with 494 additions and 16 deletions
+3 -2
View File
@@ -76,10 +76,11 @@ struct Pattern {
patLitText: String, // for pkLiteral (token text)
patRangeInclusive: bool, // for pkRange
patEnumPath: String, // for pkEnum: "Enum::Variant"
patStructName: String, // for pkStruct
patStructName: String, // for pkStruct (type name)
patFieldName: String, // for struct field entry: field name in Point { x: a }
patChild1: *Pattern, // range lo / nested
patChild2: *Pattern, // range hi / nested
patArgs: *Pattern, // pkEnum payload args (head)
patArgs: *Pattern, // pkEnum/pkTuple/pkStruct field list (head)
patNext: *Pattern, // next sibling in patArgs list
}
+145 -4
View File
@@ -569,9 +569,11 @@ func Lcx_PatternBindings(ctx: *LowerCtx, subject: *HirNode, pat: *Pattern,
line: uint32, col: uint32) -> *HirNode {
if pat == null as *Pattern { return null as *HirNode; }
if pat.kind == pkIdent {
// `_` is wildcard, not a binding
if String_Eq(pat.patIdent, "_") { return null as *HirNode; }
let ty: String = "int";
if subject != null as *HirNode && !String_Eq(subject.typeName, "") {
// keep int default for catch-all unless subject has a type name
ty = subject.typeName;
}
let alloca: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
alloca.kind = hAlloca;
@@ -589,8 +591,144 @@ func Lcx_PatternBindings(ctx: *LowerCtx, subject: *HirNode, pat: *Pattern,
store.child1 = v;
store.child2 = subject;
alloca.child3 = store;
var bsym: Symbol;
bsym.kind = skVar;
bsym.name = pat.patIdent;
bsym.typeKind = tyInt;
bsym.typeName = ty;
bsym.refType = null as *TypeExpr;
bsym.isMutable = false;
bsym.isPublic = false;
bsym.decl = null as *Decl;
discard Scope_Define(ctx.scope, bsym);
return alloca;
}
// Tuple: (a, b) → a = subject._0; b = subject._1
if pat.kind == pkTuple {
var head: *HirNode = null as *HirNode;
var tail: *HirNode = null as *HirNode;
var ei: int = 0;
var elem: *Pattern = pat.patArgs;
while elem != null as *Pattern {
if elem.kind == pkIdent && !String_Eq(elem.patIdent, "_") {
var fieldName: String = "_0";
if ei == 1 { fieldName = "_1"; }
else if ei == 2 { fieldName = "_2"; }
else if ei == 3 { fieldName = "_3"; }
else if ei > 3 { fieldName = String_Concat("_", String_FromInt(ei as int64)); }
let fPtr: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
fPtr.kind = hFieldPtr;
fPtr.line = line;
fPtr.column = col;
fPtr.strValue = fieldName;
fPtr.child1 = subject;
let fLoad: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
fLoad.kind = hLoad;
fLoad.line = line;
fLoad.column = col;
fLoad.child1 = fPtr;
fLoad.typeName = "int";
let alloca: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
alloca.kind = hAlloca;
alloca.line = line;
alloca.column = col;
alloca.strValue = elem.patIdent;
alloca.typeName = "int";
let store: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
store.kind = hStore;
store.line = line;
store.column = col;
let v: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
v.kind = hVar;
v.strValue = elem.patIdent;
store.child1 = v;
store.child2 = fLoad;
alloca.child3 = store;
var bsym: Symbol;
bsym.kind = skVar;
bsym.name = elem.patIdent;
bsym.typeKind = tyInt;
bsym.typeName = "int";
bsym.refType = null as *TypeExpr;
bsym.isMutable = false;
bsym.isPublic = false;
bsym.decl = null as *Decl;
discard Scope_Define(ctx.scope, bsym);
if head == null as *HirNode {
head = alloca;
tail = store;
} else {
tail.child3 = alloca;
tail = store;
}
}
elem = elem.patNext;
ei = ei + 1;
}
return head;
}
// Struct: Point { x: a } → a = subject.x
if pat.kind == pkStruct {
var head: *HirNode = null as *HirNode;
var tail: *HirNode = null as *HirNode;
var field: *Pattern = pat.patArgs;
while field != null as *Pattern {
if field.kind == pkIdent && !String_Eq(field.patIdent, "_") {
var fname: String = field.patFieldName;
if String_Eq(fname, "") { fname = field.patIdent; }
let fPtr: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
fPtr.kind = hFieldPtr;
fPtr.line = line;
fPtr.column = col;
fPtr.strValue = fname;
fPtr.child1 = subject;
let fLoad: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
fLoad.kind = hLoad;
fLoad.line = line;
fLoad.column = col;
fLoad.child1 = fPtr;
fLoad.typeName = "int";
let alloca: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
alloca.kind = hAlloca;
alloca.line = line;
alloca.column = col;
alloca.strValue = field.patIdent;
alloca.typeName = "int";
let store: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
store.kind = hStore;
store.line = line;
store.column = col;
let v: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
v.kind = hVar;
v.strValue = field.patIdent;
store.child1 = v;
store.child2 = fLoad;
alloca.child3 = store;
var bsym: Symbol;
bsym.kind = skVar;
bsym.name = field.patIdent;
bsym.typeKind = tyInt;
bsym.typeName = "int";
bsym.refType = null as *TypeExpr;
bsym.isMutable = false;
bsym.isPublic = false;
bsym.decl = null as *Decl;
discard Scope_Define(ctx.scope, bsym);
if head == null as *HirNode {
head = alloca;
tail = store;
} else {
tail.child3 = alloca;
tail = store;
}
}
field = field.patNext;
}
return head;
}
if pat.kind != pkEnum || !subjectHasData { return null as *HirNode; }
var enumName: String = "";
@@ -1115,12 +1253,15 @@ func Lcx_LowerExpr(ctx: *LowerCtx, expr: *Expr) -> *HirNode {
if expr.child1 != null as *Expr && expr.child1.refType != null as *TypeExpr {
let refTe: *TypeExpr = expr.child1.refType;
if refTe.kind == tekNamed {
receiverTypeName = refTe.typeName;
if refTe.typeName != null as String { receiverTypeName = refTe.typeName; }
} else if refTe.kind == tekPointer && refTe.pointerPointee != null as *TypeExpr && refTe.pointerPointee.kind == tekNamed {
receiverTypeName = refTe.pointerPointee.typeName;
if refTe.pointerPointee.typeName != null as String {
receiverTypeName = refTe.pointerPointee.typeName;
}
}
}
if !String_Eq(receiverTypeName, "") {
// Note: String_Eq(null, "") is false — must also reject null type names
if receiverTypeName != null as String && !String_Eq(receiverTypeName, "") {
let funcName: String = String_Concat(String_Concat(receiverTypeName, "_"), opMethodName);
let sym: Symbol = Scope_Lookup(ctx.scope, funcName);
if sym.kind == skFunc && sym.decl != null as *Decl {
+60
View File
@@ -694,6 +694,7 @@ func parserMakePattern(kind: int, line: uint32, col: uint32) -> *Pattern {
pat.patRangeInclusive = false;
pat.patEnumPath = "";
pat.patStructName = "";
pat.patFieldName = "";
pat.patChild1 = null as *Pattern;
pat.patChild2 = null as *Pattern;
pat.patArgs = null as *Pattern;
@@ -767,6 +768,42 @@ func parserParsePrimaryPattern(p: *Parser) -> *Pattern {
pat.patArgs = enumArgs;
return pat;
}
// Struct pattern: Point { x: a, y: b } or shorthand Point { x, y }
if parserCheck(p, tkLBrace) {
discard parserAdvance(p);
var fieldHead: *Pattern = null as *Pattern;
var fieldTail: *Pattern = null as *Pattern;
while !parserCheck(p, tkRBrace) && parserPeek(p, 0) != tkEndOfFile {
while parserCheck(p, tkNewLine) { discard parserAdvance(p); }
if parserCheck(p, tkRBrace) { break; }
let ftok: LexToken = parserExpectIdentOrKeyword(p, "expected field name in struct pattern");
let fieldName: String = ftok.text;
var fieldPat: *Pattern = null as *Pattern;
if parserCheck(p, tkColon) {
discard parserAdvance(p);
fieldPat = parserParsePattern(p);
} else {
// Shorthand { x } → { x: x }
fieldPat = parserMakePattern(pkIdent, line, col);
fieldPat.patIdent = fieldName;
}
fieldPat.patFieldName = fieldName;
if fieldHead == null as *Pattern {
fieldHead = fieldPat;
fieldTail = fieldPat;
} else {
fieldTail.patNext = fieldPat;
fieldTail = fieldPat;
}
if parserCheck(p, tkComma) { discard parserAdvance(p); }
else { break; }
}
discard parserExpect(p, tkRBrace, "expected '}' to close struct pattern");
let spat: *Pattern = parserMakePattern(pkStruct, line, col);
spat.patStructName = name;
spat.patArgs = fieldHead;
return spat;
}
// Bare name with (args): Variant(...) treated as single-segment enum
if parserCheck(p, tkLParen) {
discard parserAdvance(p);
@@ -796,6 +833,29 @@ func parserParsePrimaryPattern(p: *Parser) -> *Pattern {
return pat;
}
// Tuple pattern: (a, b)
if kind == tkLParen {
discard parserAdvance(p);
var head: *Pattern = null as *Pattern;
var tail: *Pattern = null as *Pattern;
while !parserCheck(p, tkRParen) && parserPeek(p, 0) != tkEndOfFile {
let elem: *Pattern = parserParsePattern(p);
if head == null as *Pattern {
head = elem;
tail = elem;
} else {
tail.patNext = elem;
tail = elem;
}
if parserCheck(p, tkComma) { discard parserAdvance(p); }
else { break; }
}
discard parserExpect(p, tkRParen, "expected ')' to close tuple pattern");
let tpat: *Pattern = parserMakePattern(pkTuple, line, col);
tpat.patArgs = head;
return tpat;
}
parserEmitDiag(p, line, col, "expected pattern");
return parserMakePattern(pkWildcard, line, col);
}
+82
View File
@@ -473,6 +473,88 @@ func Sema_BindPattern(sema: *Sema, pat: *Pattern, subject: *Expr) {
}
return;
}
// Tuple pattern: (a, b) — bind elements from subject tuple types
if pat.kind == pkTuple {
var ei: int = 0;
var elem: *Pattern = pat.patArgs;
while elem != null as *Pattern {
if elem.kind == pkIdent {
var ety: String = "int";
// Tuple_int_int → fields are ints by default; prefer subject type args if present
if subject != null as *Expr && subject.refType != null as *TypeExpr {
if subject.refType.kind == tekTuple {
// typeArgName0 / typeArgName1 store element type names when available
if ei == 0 && !String_Eq(subject.refType.typeArgName0, "") {
ety = subject.refType.typeArgName0;
} else if ei == 1 && !String_Eq(subject.refType.typeArgName1, "") {
ety = subject.refType.typeArgName1;
}
}
}
var bsym: Symbol;
Sema_ZeroInitSymbol(&bsym);
bsym.kind = skVar;
bsym.name = elem.patIdent;
bsym.typeName = ety;
bsym.typeKind = tyInt;
if String_Eq(ety, "String") || String_Eq(ety, "str") { bsym.typeKind = tyStr; }
else if String_Eq(ety, "bool") { bsym.typeKind = tyBool; }
let te: *TypeExpr = bux_alloc(sizeof(TypeExpr)) as *TypeExpr;
te.kind = tekNamed;
te.typeName = ety;
bsym.refType = te;
bsym.isMutable = false;
discard Scope_Define(sema.scope, bsym);
}
elem = elem.patNext;
ei = ei + 1;
}
return;
}
// Struct pattern: Point { x: a, y: b }
if pat.kind == pkStruct {
var structName: String = pat.patStructName;
if String_Eq(structName, "") && subject != null as *Expr && subject.refType != null as *TypeExpr {
structName = subject.refType.typeName;
}
var field: *Pattern = pat.patArgs;
while field != null as *Pattern {
if field.kind == pkIdent {
var ftype: String = "int";
if !String_Eq(structName, "") {
let ssym: Symbol = Scope_Lookup(sema.scope, structName);
if ssym.decl != null as *Decl && ssym.decl.kind == dkStruct && ssym.decl.fields != null as *StructField {
var fi: int = 0;
while fi < ssym.decl.fieldCount {
let sf: StructField = ssym.decl.fields[fi];
if String_Eq(sf.name, field.patFieldName) {
if sf.refFieldType != null as *TypeExpr && !String_Eq(sf.refFieldType.typeName, "") {
ftype = sf.refFieldType.typeName;
}
}
fi = fi + 1;
}
}
}
var bsym: Symbol;
Sema_ZeroInitSymbol(&bsym);
bsym.kind = skVar;
bsym.name = field.patIdent;
bsym.typeName = ftype;
bsym.typeKind = tyInt;
if String_Eq(ftype, "String") || String_Eq(ftype, "str") { bsym.typeKind = tyStr; }
else if String_Eq(ftype, "bool") { bsym.typeKind = tyBool; }
let te: *TypeExpr = bux_alloc(sizeof(TypeExpr)) as *TypeExpr;
te.kind = tekNamed;
te.typeName = ftype;
bsym.refType = te;
bsym.isMutable = false;
discard Scope_Define(sema.scope, bsym);
}
field = field.patNext;
}
return;
}
}
func Sema_IsMutRefDeref(target: *Expr) -> bool {