feat: pattern bindings, empty closures, match-as-expr, string interp

Sessions 10–12 from QUALITY_PLAN:

- Pattern payload bindings (Some(value) => value) in bootstrap and selfhost
- Empty-param closures via || (tkPipePipe) with loop/return bodies
- Expression-form match: let x = match …; newline before arms
- f"…" string interpolation desugared to String_Concat + conversions
- Lexer preserves \{ \} for literal braces in f-strings
- Bootstrap fix: f"plain" strips the f prefix after escape processing
- Examples: pattern_matching, closure_control, match_let, string_interp

Selfhost-loop remains binary-identical; all examples and error goldens pass.
This commit is contained in:
2026-07-18 00:58:44 +03:00
parent f619316470
commit db41ba4d84
18 changed files with 1192 additions and 67 deletions
+111
View File
@@ -384,6 +384,97 @@ func Sema_AddCapture(closureExpr: *Expr, name: String, typeKind: int) {
// Expression type checking
// ---------------------------------------------------------------------------
// Bind identifiers from a match pattern into the current scope.
// Enum payloads: Option::Some(value) → value:int (from variant field type).
func Sema_BindPattern(sema: *Sema, pat: *Pattern, subject: *Expr) {
if pat == null as *Pattern { return; }
if pat.kind == pkIdent {
var sym: Symbol;
Sema_ZeroInitSymbol(&sym);
sym.kind = skVar;
sym.name = pat.patIdent;
sym.typeKind = tyInt;
sym.typeName = "int";
if subject != null as *Expr && subject.refType != null as *TypeExpr {
sym.refType = subject.refType;
sym.typeName = subject.refType.typeName;
sym.typeKind = Sema_ResolveType(sema, subject.refType);
}
sym.isMutable = false;
sym.isPublic = false;
sym.decl = null as *Decl;
discard Scope_Define(sema.scope, sym);
return;
}
if pat.kind == pkEnum {
// Resolve enum + variant field types for payload bindings
var enumName: String = "";
var variantName: String = pat.patEnumPath;
if String_Contains(pat.patEnumPath, "::") {
enumName = String_SplitPart(pat.patEnumPath, "::", 0);
variantName = String_SplitPart(pat.patEnumPath, "::", 1);
} else if subject != null as *Expr && subject.refType != null as *TypeExpr {
enumName = subject.refType.typeName;
}
var fieldType0: String = "int";
var fieldType1: String = "int";
var fieldCount: int = 0;
if !String_Eq(enumName, "") {
let enumSym: Symbol = Scope_Lookup(sema.scope, enumName);
if enumSym.decl != null as *Decl && enumSym.decl.kind == dkEnum {
var vi: int = 0;
while vi < enumSym.decl.variantCount {
var v: *EnumVariant = null as *EnumVariant;
if vi == 0 { v = &enumSym.decl.variant0; }
else if vi == 1 { v = &enumSym.decl.variant1; }
else if vi == 2 { v = &enumSym.decl.variant2; }
else if vi == 3 { v = &enumSym.decl.variant3; }
else if vi == 4 { v = &enumSym.decl.variant4; }
else if vi == 5 { v = &enumSym.decl.variant5; }
else if vi == 6 { v = &enumSym.decl.variant6; }
else if vi == 7 { v = &enumSym.decl.variant7; }
else if vi == 8 { v = &enumSym.decl.variant8; }
if v != null as *EnumVariant && String_Eq(v.name, variantName) {
fieldCount = v.fieldCount;
if !String_Eq(v.fieldTypeName0, "") { fieldType0 = v.fieldTypeName0; }
if !String_Eq(v.fieldTypeName1, "") { fieldType1 = v.fieldTypeName1; }
}
vi = vi + 1;
}
}
}
var arg: *Pattern = pat.patArgs;
var ai: int = 0;
while arg != null as *Pattern {
if arg.kind == pkIdent {
var ftype: String = "int";
if ai == 0 { ftype = fieldType0; }
else if ai == 1 { ftype = fieldType1; }
var bsym: Symbol;
Sema_ZeroInitSymbol(&bsym);
bsym.kind = skVar;
bsym.name = arg.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; }
else if String_Eq(ftype, "float64") || String_Eq(ftype, "float") { bsym.typeKind = tyFloat64; }
let te: *TypeExpr = bux_alloc(sizeof(TypeExpr)) as *TypeExpr;
te.kind = tekNamed;
te.typeName = ftype;
bsym.refType = te;
bsym.isMutable = false;
bsym.isPublic = false;
bsym.decl = null as *Decl;
discard Scope_Define(sema.scope, bsym);
}
arg = arg.patNext;
ai = ai + 1;
}
return;
}
}
func Sema_IsMutRefDeref(target: *Expr) -> bool {
if target == null as *Expr { return false; }
if target.kind != ekUnary { return false; }
@@ -790,7 +881,13 @@ func Sema_CheckExpr(sema: *Sema, expr: *Expr) -> int {
var first: bool = true;
var arm: *MatchArm = expr.matchArms;
while arm != null as *MatchArm {
// Pattern bindings: Option::Some(value) → define value in arm scope
let armScope: Scope = Scope_NewChild(sema.scope);
let savedScope: *Scope = sema.scope;
sema.scope = &armScope;
Sema_BindPattern(sema, arm.pattern, expr.child1);
let bt: int = Sema_CheckExpr(sema, arm.body);
sema.scope = savedScope;
if first {
armType = bt;
first = false;
@@ -823,6 +920,20 @@ func Sema_CheckExpr(sema: *Sema, expr: *Expr) -> int {
return armType;
}
// String interpolation f"...{expr}..." → String
if kind == ekStringInterp {
var part: *ExprList = expr.callArgs;
while part != null as *ExprList {
discard Sema_CheckExpr(sema, part.expr);
part = part.next;
}
let te: *TypeExpr = bux_alloc(sizeof(TypeExpr)) as *TypeExpr;
te.kind = tekNamed;
te.typeName = "String";
expr.refType = te;
return tyStr;
}
// Closure: |params| -> Ret { body }
if kind == ekClosure {
let savedRetType: int = sema.currentRetType;