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:
@@ -79,6 +79,8 @@ struct Pattern {
|
||||
patStructName: String, // for pkStruct
|
||||
patChild1: *Pattern, // range lo / nested
|
||||
patChild2: *Pattern, // range hi / nested
|
||||
patArgs: *Pattern, // pkEnum payload args (head)
|
||||
patNext: *Pattern, // next sibling in patArgs list
|
||||
}
|
||||
|
||||
// Match arm: pattern => body
|
||||
|
||||
+362
-5
@@ -533,6 +533,202 @@ func Lcx_PatternCond(ctx: *LowerCtx, subject: *HirNode, pat: *Pattern,
|
||||
return null as *HirNode;
|
||||
}
|
||||
|
||||
// Emit binding stmts for pattern payload: Option::Some(value) → alloca value; value = subject.data.Some_0
|
||||
// Returns head of child3-linked list of HirNodes (may be null).
|
||||
func Lcx_PatternBindings(ctx: *LowerCtx, subject: *HirNode, pat: *Pattern,
|
||||
subjectEnumName: String, subjectHasData: bool,
|
||||
line: uint32, col: uint32) -> *HirNode {
|
||||
if pat == null as *Pattern { return null as *HirNode; }
|
||||
if pat.kind == pkIdent {
|
||||
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
|
||||
}
|
||||
let alloca: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
||||
alloca.kind = hAlloca;
|
||||
alloca.line = line;
|
||||
alloca.column = col;
|
||||
alloca.strValue = pat.patIdent;
|
||||
alloca.typeName = ty;
|
||||
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 = pat.patIdent;
|
||||
store.child1 = v;
|
||||
store.child2 = subject;
|
||||
alloca.child3 = store;
|
||||
return alloca;
|
||||
}
|
||||
if pat.kind != pkEnum || !subjectHasData { return null as *HirNode; }
|
||||
|
||||
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 {
|
||||
enumName = subjectEnumName;
|
||||
}
|
||||
if String_Eq(enumName, "") || String_Eq(variantName, "") { return null as *HirNode; }
|
||||
|
||||
// Look up field type names from enum decl
|
||||
var fieldType0: String = "int";
|
||||
var fieldType1: String = "int";
|
||||
var fieldCount: int = 0;
|
||||
let enumSym: Symbol = Scope_Lookup(ctx.scope, enumName);
|
||||
if enumSym.decl != null as *Decl && enumSym.decl.kind == dkEnum {
|
||||
var vi: int = 0;
|
||||
while vi < enumSym.decl.variantCount {
|
||||
var vv: *EnumVariant = null as *EnumVariant;
|
||||
if vi == 0 { vv = &enumSym.decl.variant0; }
|
||||
else if vi == 1 { vv = &enumSym.decl.variant1; }
|
||||
else if vi == 2 { vv = &enumSym.decl.variant2; }
|
||||
else if vi == 3 { vv = &enumSym.decl.variant3; }
|
||||
else if vi == 4 { vv = &enumSym.decl.variant4; }
|
||||
else if vi == 5 { vv = &enumSym.decl.variant5; }
|
||||
else if vi == 6 { vv = &enumSym.decl.variant6; }
|
||||
else if vi == 7 { vv = &enumSym.decl.variant7; }
|
||||
else if vi == 8 { vv = &enumSym.decl.variant8; }
|
||||
if vv != null as *EnumVariant && String_Eq(vv.name, variantName) {
|
||||
fieldCount = vv.fieldCount;
|
||||
if !String_Eq(vv.fieldTypeName0, "") { fieldType0 = vv.fieldTypeName0; }
|
||||
if !String_Eq(vv.fieldTypeName1, "") { fieldType1 = vv.fieldTypeName1; }
|
||||
}
|
||||
vi = vi + 1;
|
||||
}
|
||||
}
|
||||
|
||||
// dataLoad = subject.data
|
||||
let dataPtr: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
||||
dataPtr.kind = hFieldPtr;
|
||||
dataPtr.line = line;
|
||||
dataPtr.column = col;
|
||||
dataPtr.strValue = "data";
|
||||
dataPtr.child1 = subject;
|
||||
let dataLoad: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
||||
dataLoad.kind = hLoad;
|
||||
dataLoad.line = line;
|
||||
dataLoad.column = col;
|
||||
dataLoad.child1 = dataPtr;
|
||||
dataLoad.typeName = String_Concat(enumName, "_Data");
|
||||
|
||||
// Multi-field: nested struct data.Variant; single-field: flat data.Variant_0
|
||||
var payloadBase: *HirNode = dataLoad;
|
||||
if fieldCount > 1 {
|
||||
let vPtr: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
||||
vPtr.kind = hFieldPtr;
|
||||
vPtr.line = line;
|
||||
vPtr.column = col;
|
||||
vPtr.strValue = variantName;
|
||||
vPtr.child1 = dataLoad;
|
||||
let vLoad: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
||||
vLoad.kind = hLoad;
|
||||
vLoad.line = line;
|
||||
vLoad.column = col;
|
||||
vLoad.child1 = vPtr;
|
||||
vLoad.typeName = variantName;
|
||||
payloadBase = vLoad;
|
||||
}
|
||||
|
||||
var head: *HirNode = null as *HirNode;
|
||||
var tail: *HirNode = null as *HirNode;
|
||||
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; }
|
||||
let fieldName: String = String_Concat(String_Concat(variantName, "_"), String_FromInt(ai 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 = payloadBase;
|
||||
let fLoad: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
||||
fLoad.kind = hLoad;
|
||||
fLoad.line = line;
|
||||
fLoad.column = col;
|
||||
fLoad.child1 = fPtr;
|
||||
fLoad.typeName = ftype;
|
||||
|
||||
let alloca: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
||||
alloca.kind = hAlloca;
|
||||
alloca.line = line;
|
||||
alloca.column = col;
|
||||
alloca.strValue = arg.patIdent;
|
||||
alloca.typeName = ftype;
|
||||
|
||||
let store: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
||||
store.kind = hStore;
|
||||
store.line = line;
|
||||
store.column = col;
|
||||
let bv: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
||||
bv.kind = hVar;
|
||||
bv.strValue = arg.patIdent;
|
||||
store.child1 = bv;
|
||||
store.child2 = fLoad;
|
||||
alloca.child3 = store;
|
||||
|
||||
// Define in scope so body idents resolve
|
||||
var bsym: Symbol;
|
||||
bsym.kind = skVar;
|
||||
bsym.name = arg.patIdent;
|
||||
bsym.typeKind = tyInt;
|
||||
bsym.typeName = ftype;
|
||||
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;
|
||||
}
|
||||
}
|
||||
arg = arg.patNext;
|
||||
ai = ai + 1;
|
||||
}
|
||||
return head;
|
||||
}
|
||||
|
||||
// True when n is a multi-stmt yield block (match result, etc.)
|
||||
func Lcx_IsMatchYield(n: *HirNode) -> bool {
|
||||
if n == null as *HirNode { return false; }
|
||||
if n.kind != hBlock { return false; }
|
||||
return !String_Eq(n.strValue, "");
|
||||
}
|
||||
|
||||
func Lcx_YieldVarOf(n: *HirNode) -> *HirNode {
|
||||
let v: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
||||
v.kind = hVar;
|
||||
v.strValue = n.strValue;
|
||||
v.typeName = n.typeName;
|
||||
return v;
|
||||
}
|
||||
|
||||
// Append `node` at the end of a child3-linked chain starting at `head` (or its child1 if head is hBlock).
|
||||
func Lcx_AppendToChain(head: *HirNode, node: *HirNode) {
|
||||
if head == null as *HirNode || node == null as *HirNode { return; }
|
||||
var cur: *HirNode = head;
|
||||
if head.kind == hBlock && head.child1 != null as *HirNode {
|
||||
cur = head.child1;
|
||||
}
|
||||
while cur.child3 != null as *HirNode {
|
||||
cur = cur.child3;
|
||||
}
|
||||
cur.child3 = node;
|
||||
}
|
||||
|
||||
// Lower match expr → hBlock: alloca result; if-else stores; strValue = result name
|
||||
func Lcx_LowerMatch(ctx: *LowerCtx, expr: *Expr) -> *HirNode {
|
||||
let line: uint32 = expr.line;
|
||||
@@ -591,6 +787,8 @@ func Lcx_LowerMatch(ctx: *LowerCtx, expr: *Expr) -> *HirNode {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Pattern bindings before body (so body idents resolve)
|
||||
let bindHead: *HirNode = Lcx_PatternBindings(ctx, subject, cur.pattern, subjectEnumName, subjectHasData, line, col);
|
||||
let bodyHir: *HirNode = Lcx_LowerExpr(ctx, cur.body);
|
||||
// result = body
|
||||
let storeNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
||||
@@ -603,11 +801,22 @@ func Lcx_LowerMatch(ctx: *LowerCtx, expr: *Expr) -> *HirNode {
|
||||
storeNode.child1 = resVar;
|
||||
storeNode.child2 = bodyHir;
|
||||
|
||||
// armBlock = bindings... → storeNode
|
||||
let armBlock: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
||||
armBlock.kind = hBlock;
|
||||
armBlock.line = line;
|
||||
armBlock.column = col;
|
||||
armBlock.child1 = storeNode;
|
||||
if bindHead != null as *HirNode {
|
||||
armBlock.child1 = bindHead;
|
||||
// find tail of bind chain
|
||||
var bt: *HirNode = bindHead;
|
||||
while bt.child3 != null as *HirNode {
|
||||
bt = bt.child3;
|
||||
}
|
||||
bt.child3 = storeNode;
|
||||
} else {
|
||||
armBlock.child1 = storeNode;
|
||||
}
|
||||
|
||||
let cond: *HirNode = Lcx_PatternCond(ctx, subject, cur.pattern, subjectEnumName, subjectHasData, line, col);
|
||||
if cond == null as *HirNode {
|
||||
@@ -672,6 +881,72 @@ func Lcx_LowerExpr(ctx: *LowerCtx, expr: *Expr) -> *HirNode {
|
||||
return Lcx_LowerMatch(ctx, expr);
|
||||
}
|
||||
|
||||
// String interpolation: desugar to String_Concat + String_FromInt/Bool/Float
|
||||
// Parts in callArgs are interleaved text lits and expressions.
|
||||
if kind == ekStringInterp {
|
||||
var result: *HirNode = null as *HirNode;
|
||||
var part: *ExprList = expr.callArgs;
|
||||
while part != null as *ExprList {
|
||||
let pe: *Expr = part.expr;
|
||||
var piece: *HirNode = null as *HirNode;
|
||||
if pe != null as *Expr && pe.kind == ekLiteral && pe.tokKind == tkStringLiteral {
|
||||
piece = Lcx_LowerExpr(ctx, pe);
|
||||
} else {
|
||||
let lowered: *HirNode = Lcx_LowerExpr(ctx, pe);
|
||||
// Convert non-string to String
|
||||
var needConv: bool = true;
|
||||
var convName: String = "String_FromInt";
|
||||
if pe != null as *Expr && pe.refType != null as *TypeExpr {
|
||||
let tn: String = pe.refType.typeName;
|
||||
if String_Eq(tn, "String") || String_Eq(tn, "str") {
|
||||
needConv = false;
|
||||
} else if String_Eq(tn, "bool") {
|
||||
convName = "String_FromBool";
|
||||
} else if String_Eq(tn, "float64") || String_Eq(tn, "float") || String_Eq(tn, "float32") {
|
||||
convName = "String_FromFloat";
|
||||
} else {
|
||||
convName = "String_FromInt";
|
||||
}
|
||||
}
|
||||
if needConv {
|
||||
let callN: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
||||
callN.kind = hCall;
|
||||
callN.line = line;
|
||||
callN.column = col;
|
||||
callN.strValue = convName;
|
||||
callN.child1 = lowered;
|
||||
piece = callN;
|
||||
} else {
|
||||
piece = lowered;
|
||||
}
|
||||
}
|
||||
if result == null as *HirNode {
|
||||
result = piece;
|
||||
} else {
|
||||
let cat: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
||||
cat.kind = hCall;
|
||||
cat.line = line;
|
||||
cat.column = col;
|
||||
cat.strValue = "String_Concat";
|
||||
cat.child1 = result;
|
||||
cat.child2 = piece;
|
||||
result = cat;
|
||||
}
|
||||
part = part.next;
|
||||
}
|
||||
if result == null as *HirNode {
|
||||
// empty f""
|
||||
let empty: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
||||
empty.kind = hLit;
|
||||
empty.line = line;
|
||||
empty.column = col;
|
||||
empty.intValue = tkStringLiteral;
|
||||
empty.strValue = "\"\"";
|
||||
return empty;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
// Literal
|
||||
if kind == ekLiteral {
|
||||
n.kind = hLit;
|
||||
@@ -874,8 +1149,69 @@ func Lcx_LowerExpr(ctx: *LowerCtx, expr: *Expr) -> *HirNode {
|
||||
|
||||
n.kind = hBinary;
|
||||
n.intValue = expr.intValue; // operator
|
||||
n.child1 = Lcx_LowerExpr(ctx, expr.child1);
|
||||
n.child2 = Lcx_LowerExpr(ctx, expr.child2);
|
||||
let leftHir: *HirNode = Lcx_LowerExpr(ctx, expr.child1);
|
||||
let rightHir: *HirNode = Lcx_LowerExpr(ctx, expr.child2);
|
||||
// If either side is a match yield block, expand to:
|
||||
// match stmts...; int __binop_N = leftVal op rightVal; yield __binop_N
|
||||
if Lcx_IsMatchYield(leftHir) || Lcx_IsMatchYield(rightHir) {
|
||||
ctx.varCounter = ctx.varCounter + 1;
|
||||
let tmpName: String = String_Concat("__binop_", String_FromInt(ctx.varCounter as int64));
|
||||
var leftVal: *HirNode = leftHir;
|
||||
var rightVal: *HirNode = rightHir;
|
||||
let outer: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
||||
outer.kind = hBlock;
|
||||
outer.line = line;
|
||||
outer.column = col;
|
||||
outer.strValue = tmpName;
|
||||
outer.typeName = "int";
|
||||
var first: *HirNode = null as *HirNode;
|
||||
if Lcx_IsMatchYield(leftHir) {
|
||||
leftVal = Lcx_YieldVarOf(leftHir);
|
||||
leftHir.strValue = "";
|
||||
first = leftHir;
|
||||
}
|
||||
if Lcx_IsMatchYield(rightHir) {
|
||||
rightVal = Lcx_YieldVarOf(rightHir);
|
||||
rightHir.strValue = "";
|
||||
if first == null as *HirNode {
|
||||
first = rightHir;
|
||||
} else {
|
||||
Lcx_AppendToChain(first, rightHir);
|
||||
}
|
||||
}
|
||||
let tmpAlloca: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
||||
tmpAlloca.kind = hAlloca;
|
||||
tmpAlloca.line = line;
|
||||
tmpAlloca.column = col;
|
||||
tmpAlloca.strValue = tmpName;
|
||||
tmpAlloca.typeName = "int";
|
||||
let binNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
||||
binNode.kind = hBinary;
|
||||
binNode.line = line;
|
||||
binNode.column = col;
|
||||
binNode.intValue = expr.intValue;
|
||||
binNode.child1 = leftVal;
|
||||
binNode.child2 = rightVal;
|
||||
let tmpStore: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
||||
tmpStore.kind = hStore;
|
||||
tmpStore.line = line;
|
||||
tmpStore.column = col;
|
||||
let tmpVar: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
||||
tmpVar.kind = hVar;
|
||||
tmpVar.strValue = tmpName;
|
||||
tmpStore.child1 = tmpVar;
|
||||
tmpStore.child2 = binNode;
|
||||
tmpAlloca.child3 = tmpStore;
|
||||
if first == null as *HirNode {
|
||||
outer.child1 = tmpAlloca;
|
||||
} else {
|
||||
outer.child1 = first;
|
||||
Lcx_AppendToChain(first, tmpAlloca);
|
||||
}
|
||||
return outer;
|
||||
}
|
||||
n.child1 = leftHir;
|
||||
n.child2 = rightHir;
|
||||
return n;
|
||||
}
|
||||
|
||||
@@ -1875,6 +2211,26 @@ func Lcx_LowerStmt(ctx: *LowerCtx, stmt: *Stmt) -> *HirNode {
|
||||
sym.isPublic = false;
|
||||
sym.decl = null as *Decl;
|
||||
discard Scope_Define(ctx.scope, sym);
|
||||
|
||||
// Match (or other multi-stmt yield) as let initializer:
|
||||
// match stmts...; Type x = __match_N;
|
||||
// instead of illegal `Type x = <block>;`
|
||||
if Lcx_IsMatchYield(init) {
|
||||
let yieldName: String = init.strValue;
|
||||
init.strValue = "";
|
||||
let yieldVar: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
||||
yieldVar.kind = hVar;
|
||||
yieldVar.strValue = yieldName;
|
||||
let storeNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
||||
storeNode.kind = hStore;
|
||||
storeNode.line = line;
|
||||
storeNode.column = col;
|
||||
storeNode.child1 = alloca;
|
||||
storeNode.child2 = yieldVar;
|
||||
Lcx_AppendToChain(init, storeNode);
|
||||
return init;
|
||||
}
|
||||
|
||||
// store the init value
|
||||
let storeNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode;
|
||||
storeNode.kind = hStore;
|
||||
@@ -3365,11 +3721,12 @@ func HirLower_LowerModule(mod: *Module, sema: *Sema) -> *HirModule {
|
||||
hm.enums[ei].variants[vi].name = v.name;
|
||||
hm.enums[ei].variants[vi].fieldCount = v.fieldCount;
|
||||
if v.fieldCount > 0 {
|
||||
hm.enums[ei].variants[vi].fieldName0 = "value";
|
||||
// Positional field names: Variant_0, Variant_1 (matches data.Variant_i / nested struct)
|
||||
hm.enums[ei].variants[vi].fieldName0 = String_Concat(v.name, "_0");
|
||||
hm.enums[ei].variants[vi].fieldType0 = Lcx_ResolveTypeKindFromName(v.fieldTypeName0);
|
||||
}
|
||||
if v.fieldCount > 1 {
|
||||
hm.enums[ei].variants[vi].fieldName1 = "value2";
|
||||
hm.enums[ei].variants[vi].fieldName1 = String_Concat(v.name, "_1");
|
||||
hm.enums[ei].variants[vi].fieldType1 = Lcx_ResolveTypeKindFromName(v.fieldTypeName1);
|
||||
}
|
||||
}
|
||||
|
||||
+30
-15
@@ -381,8 +381,8 @@ func lexScanBacktickString(lex: *Lexer) {
|
||||
lexEmitToken(lex, tkStringLiteral);
|
||||
}
|
||||
|
||||
func lexScanString(lex: *Lexer) {
|
||||
lexMarkStart(lex);
|
||||
// Assumes lex.startPos already marked (may include f/c8/… prefix before the quote).
|
||||
func lexScanStringFrom(lex: *Lexer) {
|
||||
// Collect the prefix (before opening quote) for the token text
|
||||
var prefix: String = "";
|
||||
var prefixLen: int = 0;
|
||||
@@ -412,16 +412,24 @@ func lexScanString(lex: *Lexer) {
|
||||
discard lexAdvance(lex);
|
||||
if !lexIsAtEnd(lex) {
|
||||
let ec: uint32 = lexAdvance(lex);
|
||||
var rc: char8 = ec as char8;
|
||||
if ec == 110 { rc = 10 as char8; } // \n
|
||||
else if ec == 114 { rc = 13 as char8; } // \r
|
||||
else if ec == 116 { rc = 9 as char8; } // \t
|
||||
else if ec == 48 { rc = 0 as char8; } // \0
|
||||
else if ec == 92 { rc = 92 as char8; } // \\
|
||||
else if ec == 34 { rc = 34 as char8; } // \"
|
||||
else if ec == 39 { rc = 39 as char8; } // \'
|
||||
resolved[rpos] = rc;
|
||||
rpos = rpos + 1;
|
||||
// Preserve \{ and \} as two chars for f"..." brace escaping
|
||||
if ec == 123 || ec == 125 {
|
||||
resolved[rpos] = 92 as char8;
|
||||
rpos = rpos + 1;
|
||||
resolved[rpos] = ec as char8;
|
||||
rpos = rpos + 1;
|
||||
} else {
|
||||
var rc: char8 = ec as char8;
|
||||
if ec == 110 { rc = 10 as char8; } // \n
|
||||
else if ec == 114 { rc = 13 as char8; } // \r
|
||||
else if ec == 116 { rc = 9 as char8; } // \t
|
||||
else if ec == 48 { rc = 0 as char8; } // \0
|
||||
else if ec == 92 { rc = 92 as char8; } // \\
|
||||
else if ec == 34 { rc = 34 as char8; } // \"
|
||||
else if ec == 39 { rc = 39 as char8; } // \'
|
||||
resolved[rpos] = rc;
|
||||
rpos = rpos + 1;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
let c: uint32 = lexAdvance(lex);
|
||||
@@ -450,6 +458,11 @@ func lexScanString(lex: *Lexer) {
|
||||
lexSetLastTokenText(lex, finalBuf);
|
||||
}
|
||||
|
||||
func lexScanString(lex: *Lexer) {
|
||||
lexMarkStart(lex);
|
||||
lexScanStringFrom(lex);
|
||||
}
|
||||
|
||||
func lexScanChar(lex: *Lexer) {
|
||||
lexMarkStart(lex);
|
||||
// Collect the prefix for the token text
|
||||
@@ -678,10 +691,12 @@ func lexNextToken(lex: *Lexer) {
|
||||
}
|
||||
|
||||
// String prefixes: f" c8" c16" c32"
|
||||
// Keep `f` in token text so the parser can detect interpolating strings.
|
||||
if c == 102 && lexPeek(lex, 1) == 34 { // f"
|
||||
discard lexAdvance(lex); // f
|
||||
lexMarkStart(lex); // treat as plain string literal in selfhost
|
||||
lexScanString(lex); return;
|
||||
lexMarkStart(lex); // start at 'f'
|
||||
discard lexAdvance(lex); // consume f; startPos still at f
|
||||
lexScanStringFrom(lex); // does not re-mark — prefix = "f"
|
||||
return;
|
||||
}
|
||||
if c == 99 { // 'c'
|
||||
let d: uint32 = lexPeek(lex, 1);
|
||||
|
||||
+221
-3
@@ -4,6 +4,7 @@ module Parser {
|
||||
|
||||
extern func bux_strlen(s: String) -> uint;
|
||||
extern func bux_str_to_int(s: String) -> int64;
|
||||
extern func bux_str_slice(s: String, start: uint, len: uint) -> String;
|
||||
|
||||
// Forward declarations for mutual recursion
|
||||
func parserParseExpr(p: *Parser) -> *Expr;
|
||||
@@ -353,6 +354,171 @@ func parserMakeExpr(kind: int, line: uint32, col: uint32) -> *Expr {
|
||||
return e;
|
||||
}
|
||||
|
||||
|
||||
func parserMakeStringLitExpr(text: String, line: uint32, col: uint32) -> *Expr {
|
||||
let quoted: String = String_Concat(String_Concat("\"", text), "\"");
|
||||
let e: *Expr = parserMakeExpr(ekLiteral, line, col);
|
||||
e.tokKind = tkStringLiteral;
|
||||
e.tokText = quoted;
|
||||
return e;
|
||||
}
|
||||
|
||||
func parserParseInterpFragment(exprStr: String) -> *Expr {
|
||||
let lex: *Lexer = Lexer_Tokenize(exprStr);
|
||||
var sub: Parser;
|
||||
sub.tokens = lex.tokens;
|
||||
sub.tokenCount = lex.tokenCount;
|
||||
sub.pos = 0;
|
||||
sub.diagCount = 0;
|
||||
sub.diags = null as *ParserDiag;
|
||||
sub.structInitAllowed = true;
|
||||
return parserParseExpr(&sub);
|
||||
}
|
||||
|
||||
func parserAppendPart(head: *ExprList, tail: *ExprList, e: *Expr) -> *ExprList {
|
||||
// returns new tail; head updated via pointer trick not possible — return pair as side effect on first arg using double pointer?
|
||||
// simpler: just inline in main
|
||||
return tail;
|
||||
}
|
||||
|
||||
func parserParseStringInterp(p: *Parser, tok: LexToken) -> *Expr {
|
||||
let text: String = tok.text;
|
||||
let tlen: uint = bux_strlen(text);
|
||||
if tlen < 3 as uint {
|
||||
let e: *Expr = parserMakeExpr(ekLiteral, tok.line, tok.column);
|
||||
e.tokKind = tkStringLiteral;
|
||||
e.tokText = text;
|
||||
return e;
|
||||
}
|
||||
if text[0] as int != 102 {
|
||||
let e: *Expr = parserMakeExpr(ekLiteral, tok.line, tok.column);
|
||||
e.tokKind = tkStringLiteral;
|
||||
e.tokText = text;
|
||||
return e;
|
||||
}
|
||||
if text[1] as int != 34 {
|
||||
let e: *Expr = parserMakeExpr(ekLiteral, tok.line, tok.column);
|
||||
e.tokKind = tkStringLiteral;
|
||||
e.tokText = text;
|
||||
return e;
|
||||
}
|
||||
let inner: String = bux_str_slice(text, 2, tlen - 3);
|
||||
let innerLen: uint = bux_strlen(inner);
|
||||
var head: *ExprList = null as *ExprList;
|
||||
var tail: *ExprList = null as *ExprList;
|
||||
var currentText: String = "";
|
||||
var i: uint = 0;
|
||||
var partCount: int = 0;
|
||||
while i < innerLen {
|
||||
let ch: int = inner[i] as int;
|
||||
var handled: bool = false;
|
||||
if ch == 92 {
|
||||
if i + 1 < innerLen {
|
||||
let nch: int = inner[i + 1] as int;
|
||||
if nch == 123 {
|
||||
currentText = String_Concat(currentText, "{");
|
||||
i = i + 2;
|
||||
handled = true;
|
||||
} else {
|
||||
if nch == 125 {
|
||||
currentText = String_Concat(currentText, "}");
|
||||
i = i + 2;
|
||||
handled = true;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if handled {
|
||||
continue;
|
||||
}
|
||||
if ch == 123 {
|
||||
let textPart: *Expr = parserMakeStringLitExpr(currentText, tok.line, tok.column);
|
||||
let textNode: *ExprList = bux_alloc(sizeof(ExprList)) as *ExprList;
|
||||
textNode.expr = textPart;
|
||||
textNode.next = null as *ExprList;
|
||||
textNode.argName = "";
|
||||
if head == null as *ExprList {
|
||||
head = textNode;
|
||||
tail = textNode;
|
||||
} else {
|
||||
tail.next = textNode;
|
||||
tail = textNode;
|
||||
}
|
||||
partCount = partCount + 1;
|
||||
currentText = "";
|
||||
var j: uint = i + 1;
|
||||
var depth: int = 1;
|
||||
while j < innerLen {
|
||||
if depth <= 0 {
|
||||
break;
|
||||
}
|
||||
let cj: int = inner[j] as int;
|
||||
if cj == 123 {
|
||||
depth = depth + 1;
|
||||
}
|
||||
if cj == 125 {
|
||||
depth = depth - 1;
|
||||
}
|
||||
j = j + 1;
|
||||
}
|
||||
if depth != 0 {
|
||||
parserEmitDiag(p, tok.line, tok.column, "unmatched brace in string interpolation");
|
||||
let bad: *Expr = parserMakeExpr(ekLiteral, tok.line, tok.column);
|
||||
bad.tokKind = tkStringLiteral;
|
||||
bad.tokText = "\"\"";
|
||||
return bad;
|
||||
}
|
||||
let exprLen: uint = j - i - 2;
|
||||
let exprStr: String = bux_str_slice(inner, i + 1, exprLen);
|
||||
if bux_strlen(exprStr) == 0 {
|
||||
parserEmitDiag(p, tok.line, tok.column, "empty interpolation");
|
||||
let bad: *Expr = parserMakeExpr(ekLiteral, tok.line, tok.column);
|
||||
bad.tokKind = tkStringLiteral;
|
||||
bad.tokText = "\"\"";
|
||||
return bad;
|
||||
}
|
||||
let frag: *Expr = parserParseInterpFragment(exprStr);
|
||||
let fragNode: *ExprList = bux_alloc(sizeof(ExprList)) as *ExprList;
|
||||
fragNode.expr = frag;
|
||||
fragNode.next = null as *ExprList;
|
||||
fragNode.argName = "";
|
||||
if head == null as *ExprList {
|
||||
head = fragNode;
|
||||
tail = fragNode;
|
||||
} else {
|
||||
tail.next = fragNode;
|
||||
tail = fragNode;
|
||||
}
|
||||
partCount = partCount + 1;
|
||||
i = j;
|
||||
continue;
|
||||
}
|
||||
let one: String = bux_str_slice(inner, i, 1);
|
||||
currentText = String_Concat(currentText, one);
|
||||
i = i + 1;
|
||||
}
|
||||
let lastPart: *Expr = parserMakeStringLitExpr(currentText, tok.line, tok.column);
|
||||
let lastNode: *ExprList = bux_alloc(sizeof(ExprList)) as *ExprList;
|
||||
lastNode.expr = lastPart;
|
||||
lastNode.next = null as *ExprList;
|
||||
lastNode.argName = "";
|
||||
if head == null as *ExprList {
|
||||
head = lastNode;
|
||||
tail = lastNode;
|
||||
} else {
|
||||
tail.next = lastNode;
|
||||
tail = lastNode;
|
||||
}
|
||||
partCount = partCount + 1;
|
||||
if partCount == 1 {
|
||||
return head.expr;
|
||||
}
|
||||
let e: *Expr = parserMakeExpr(ekStringInterp, tok.line, tok.column);
|
||||
e.callArgs = head;
|
||||
e.callArgCount = partCount;
|
||||
return e;
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------------------
|
||||
// Primary expressions
|
||||
// ---------------------------------------------------------------------------
|
||||
@@ -370,6 +536,10 @@ func parserParsePrimary(p: *Parser) -> *Expr {
|
||||
if kind == tkIntLiteral || kind == tkFloatLiteral || kind == tkStringLiteral
|
||||
|| kind == tkCharLiteral || kind == tkBoolLiteral {
|
||||
discard parserAdvance(p);
|
||||
if kind == tkStringLiteral && bux_strlen(tok.text) >= 2 as uint
|
||||
&& tok.text[0] as int == 102 && tok.text[1] as int == 34 {
|
||||
return parserParseStringInterp(p, tok);
|
||||
}
|
||||
let e: *Expr = parserMakeExpr(ekLiteral, line, col);
|
||||
e.tokKind = kind;
|
||||
e.tokText = tok.text;
|
||||
@@ -480,6 +650,12 @@ func parserParsePrimary(p: *Parser) -> *Expr {
|
||||
return first;
|
||||
}
|
||||
|
||||
// Empty-param closure: `||` is lexed as tkPipePipe (logical-or token).
|
||||
// As a primary it can only mean a zero-param closure: || -> T { ... }
|
||||
if kind == tkPipePipe {
|
||||
return parserParseEmptyClosure(p);
|
||||
}
|
||||
|
||||
// Closure: |params| -> Ret { body }
|
||||
if kind == tkPipe {
|
||||
return parserParseClosure(p);
|
||||
@@ -520,6 +696,8 @@ func parserMakePattern(kind: int, line: uint32, col: uint32) -> *Pattern {
|
||||
pat.patStructName = "";
|
||||
pat.patChild1 = null as *Pattern;
|
||||
pat.patChild2 = null as *Pattern;
|
||||
pat.patArgs = null as *Pattern;
|
||||
pat.patNext = null as *Pattern;
|
||||
return pat;
|
||||
}
|
||||
|
||||
@@ -565,11 +743,20 @@ func parserParsePrimaryPattern(p: *Parser) -> *Pattern {
|
||||
path = String_Concat(path, "::");
|
||||
path = String_Concat(path, seg.text);
|
||||
}
|
||||
// Optional (args) for algebraic variants — parse and ignore bindings for now
|
||||
// Optional (args) for algebraic variants — store as patArgs linked list
|
||||
var enumArgs: *Pattern = null as *Pattern;
|
||||
var lastArg: *Pattern = null as *Pattern;
|
||||
if parserCheck(p, tkLParen) {
|
||||
discard parserAdvance(p);
|
||||
while !parserCheck(p, tkRParen) && parserPeek(p, 0) != tkEndOfFile {
|
||||
discard parserParsePattern(p);
|
||||
let argPat: *Pattern = parserParsePattern(p);
|
||||
if enumArgs == null as *Pattern {
|
||||
enumArgs = argPat;
|
||||
lastArg = argPat;
|
||||
} else {
|
||||
lastArg.patNext = argPat;
|
||||
lastArg = argPat;
|
||||
}
|
||||
if parserCheck(p, tkComma) { discard parserAdvance(p); }
|
||||
else { break; }
|
||||
}
|
||||
@@ -577,19 +764,30 @@ func parserParsePrimaryPattern(p: *Parser) -> *Pattern {
|
||||
}
|
||||
let pat: *Pattern = parserMakePattern(pkEnum, line, col);
|
||||
pat.patEnumPath = path;
|
||||
pat.patArgs = enumArgs;
|
||||
return pat;
|
||||
}
|
||||
// Bare name with (args): Variant(...) treated as single-segment enum
|
||||
if parserCheck(p, tkLParen) {
|
||||
discard parserAdvance(p);
|
||||
var bareArgs: *Pattern = null as *Pattern;
|
||||
var bareLast: *Pattern = null as *Pattern;
|
||||
while !parserCheck(p, tkRParen) && parserPeek(p, 0) != tkEndOfFile {
|
||||
discard parserParsePattern(p);
|
||||
let argPat: *Pattern = parserParsePattern(p);
|
||||
if bareArgs == null as *Pattern {
|
||||
bareArgs = argPat;
|
||||
bareLast = argPat;
|
||||
} else {
|
||||
bareLast.patNext = argPat;
|
||||
bareLast = argPat;
|
||||
}
|
||||
if parserCheck(p, tkComma) { discard parserAdvance(p); }
|
||||
else { break; }
|
||||
}
|
||||
discard parserExpect(p, tkRParen, "expected ')' to close pattern");
|
||||
let pat: *Pattern = parserMakePattern(pkEnum, line, col);
|
||||
pat.patEnumPath = name;
|
||||
pat.patArgs = bareArgs;
|
||||
return pat;
|
||||
}
|
||||
// Ident binding / catch-all name
|
||||
@@ -629,6 +827,8 @@ func parserParseMatchExpr(p: *Parser) -> *Expr {
|
||||
p.structInitAllowed = false;
|
||||
let subject: *Expr = parserParseExpr(p);
|
||||
p.structInitAllowed = true;
|
||||
// Allow newline before '{' (needed for `let x = match n \n { ... }`)
|
||||
while parserCheck(p, tkNewLine) { discard parserAdvance(p); }
|
||||
discard parserExpect(p, tkLBrace, "expected '{' to start match body");
|
||||
|
||||
var firstArm: *MatchArm = null as *MatchArm;
|
||||
@@ -674,6 +874,24 @@ func parserParseMatchExpr(p: *Parser) -> *Expr {
|
||||
// Closure: |params| -> Ret { body }
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
// Zero-param closure when written as `||` (single tkPipePipe token from lexer)
|
||||
func parserParseEmptyClosure(p: *Parser) -> *Expr {
|
||||
let line: uint32 = parserCurToken(p).line;
|
||||
let col: uint32 = parserCurToken(p).column;
|
||||
discard parserAdvance(p); // ||
|
||||
let e: *Expr = parserMakeExpr(ekClosure, line, col);
|
||||
let params: *Decl = bux_alloc(sizeof(Decl)) as *Decl;
|
||||
params.kind = dkFunc;
|
||||
params.paramCount = 0;
|
||||
e.closureParams = params;
|
||||
if parserCheck(p, tkArrow) {
|
||||
discard parserAdvance(p);
|
||||
e.refType = parserParseType(p);
|
||||
}
|
||||
e.refBlock = parserParseBlock(p);
|
||||
return e;
|
||||
}
|
||||
|
||||
func parserParseClosure(p: *Parser) -> *Expr {
|
||||
let line: uint32 = parserCurToken(p).line;
|
||||
let col: uint32 = parserCurToken(p).column;
|
||||
|
||||
+111
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user