From db41ba4d84d6373e713c28377ac80c103a843752 Mon Sep 17 00:00:00 2001 From: dimgigov Date: Sat, 18 Jul 2026 00:58:44 +0300 Subject: [PATCH] feat: pattern bindings, empty closures, match-as-expr, string interp MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- Makefile | 2 +- bootstrap/c_backend.nim | 12 +- bootstrap/hir_lower.nim | 153 +++++++++++++- bootstrap/lexer.nim | 3 + bootstrap/lir_c_backend.nim | 10 +- bootstrap/parser.nim | 23 ++- bootstrap/sema.nim | 69 +++++-- docs/LanguageRef.md | 24 ++- docs/QUALITY_PLAN.md | 47 ++++- examples/closure_control.bux | 63 ++++++ examples/match_let.bux | 43 ++++ examples/pattern_matching.bux | 38 +++- examples/string_interp.bux | 23 +++ src/ast.bux | 2 + src/hir_lower.bux | 367 +++++++++++++++++++++++++++++++++- src/lexer.bux | 45 +++-- src/parser.bux | 224 ++++++++++++++++++++- src/sema.bux | 111 ++++++++++ 18 files changed, 1192 insertions(+), 67 deletions(-) create mode 100644 examples/closure_control.bux create mode 100644 examples/match_let.bux create mode 100644 examples/string_interp.bux diff --git a/Makefile b/Makefile index 9d2137a..86fba4c 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,7 @@ SRC := bootstrap/main.nim OUT := buxc BUILD_DIR := build -EXAMPLES := hello fibonacci factorial structs enums methods algebraic_enums generics generics_struct generic_infer generic_infer2 extend_generic pattern_matching strings strings2 map result_option try_operator ownership ctfe async concurrency os_time process json iter trait_bounds channel sync jwt stdlib_ergonomics tuples func_ptr map_remove array_iter_extra string_extra multi_closure iter_hof +EXAMPLES := hello fibonacci factorial structs enums methods algebraic_enums generics generics_struct generic_infer generic_infer2 extend_generic pattern_matching strings strings2 map result_option try_operator ownership ctfe async concurrency os_time process json iter trait_bounds channel sync jwt stdlib_ergonomics tuples func_ptr map_remove array_iter_extra string_extra multi_closure iter_hof closure_control match_let string_interp .PHONY: all build dev debug test clean clean-all test-examples selfhost test-golden test-errors selfhost-loop lsp diff --git a/bootstrap/c_backend.nim b/bootstrap/c_backend.nim index bb87656..ac84fc5 100644 --- a/bootstrap/c_backend.nim +++ b/bootstrap/c_backend.nim @@ -595,11 +595,19 @@ proc emitEnum*(be: var CBackend, name: string, variants: seq[HirEnumVariant]) = be.emitLine(&"typedef union {{") inc be.indent for v in variants: - if v.fields.len > 0: - # Positional fields + if v.fields.len == 1: + # Single positional field — flat union member (compat: data.Variant_0) + 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};") elif v.namedFields.len > 0: # Named fields - generate as struct be.emitLine(&"struct {{") diff --git a/bootstrap/hir_lower.nim b/bootstrap/hir_lower.nim index 088be0d..6fc426a 100644 --- a/bootstrap/hir_lower.nim +++ b/bootstrap/hir_lower.nim @@ -30,6 +30,9 @@ type funcAdapterSigs*: Table[string, Type] ## All func types that need BuxFn_* typedefs (including locals) seenFatTypes*: seq[Type] + ## Pattern-binding names already alloca'd in the current function + ## (avoids `int v;` twice when two matches bind the same name) + patternBoundNames*: HashSet[string] proc freshName(ctx: var LowerCtx): string = inc ctx.varCounter @@ -73,6 +76,8 @@ proc patternLiteralNode(pat: Pattern, loc: SourceLocation): HirNode = proc matchAlwaysTrue(loc: SourceLocation): HirNode = hirLit(Token(kind: tkBoolLiteral, text: "true", loc: loc), makeBool(), loc) +proc resolveTypeExpr(ctx: var LowerCtx, te: TypeExpr): Type + proc matchPatternCond(ctx: var LowerCtx, subject: HirNode, pattern: Pattern, subjectEnumName: string, subjectHasData: bool, loc: SourceLocation): HirNode = @@ -126,9 +131,99 @@ proc matchPatternCond(ctx: var LowerCtx, subject: HirNode, pattern: Pattern, # Struct/tuple patterns: not yet fully lowered — always-true return nil +proc matchPatternBindings(ctx: var LowerCtx, subject: HirNode, pattern: Pattern, + subjectEnumName: string, subjectHasData: bool, + loc: SourceLocation): seq[HirNode] = + ## Emit alloca+store for identifiers bound by a match pattern. + ## Enum payload: `Option::Some(value)` → `value = subject.data.Some_0` + ## Ident catch-all: `x` → `x = subject` + result = @[] + if pattern == nil: return + case pattern.kind + of pkIdent: + let ty = if subject.typ != nil: subject.typ else: makeUnknown() + if pattern.patIdent notin ctx.patternBoundNames: + result.add(hirAlloca(pattern.patIdent, ty, loc)) + ctx.patternBoundNames.incl(pattern.patIdent) + result.add(hirStore(hirVar(pattern.patIdent, ty, loc), subject, loc)) + of pkEnum: + if not subjectHasData: + return + var enumName = "" + var variantName = "" + if pattern.patEnumPath.len >= 2: + enumName = pattern.patEnumPath[0] + variantName = pattern.patEnumPath[^1] + elif pattern.patEnumPath.len == 1: + variantName = pattern.patEnumPath[0] + enumName = subjectEnumName + if enumName == "" or variantName == "": + return + # Look up field types from enum declaration + var fieldTypes: seq[Type] = @[] + var namedFields: seq[tuple[name: string, typ: Type]] = @[] + let enumSym = ctx.globalScope.lookup(enumName) + if enumSym != nil and enumSym.decl != nil and enumSym.decl.kind == dkEnum: + for v in enumSym.decl.declEnumVariants: + if v.name == variantName: + for f in v.fields: + fieldTypes.add(ctx.resolveTypeExpr(f)) + for nf in v.namedFields: + namedFields.add((nf.name, ctx.resolveTypeExpr(nf.ftype))) + break + let dataType = makeNamed(enumName & "_Data") + let dataPtr = HirNode(kind: hFieldPtr, fieldPtrBase: subject, fieldName: "data", + typ: makePointer(dataType), loc: loc) + let dataLoad = HirNode(kind: hLoad, loadPtr: dataPtr, typ: dataType, loc: loc) + # Multi-field positional variants live in a nested struct data.Variant.{Variant_i} + # Single-field stay flat as data.Variant_0 for ABI compat. + let multiField = fieldTypes.len > 1 + var payloadBase = dataLoad + if multiField: + let variantStructTy = makeNamed(variantName) + let variantPtr = HirNode(kind: hFieldPtr, fieldPtrBase: dataLoad, fieldName: variantName, + typ: makePointer(variantStructTy), loc: loc) + payloadBase = HirNode(kind: hLoad, loadPtr: variantPtr, typ: variantStructTy, loc: loc) + for i, arg in pattern.patEnumArgs: + if arg == nil or arg.kind != pkIdent: + continue + let fieldName = variantName & "_" & $i + let fieldTy = if i < fieldTypes.len: fieldTypes[i] else: makeInt() + let fieldPtr = HirNode(kind: hFieldPtr, fieldPtrBase: payloadBase, fieldName: fieldName, + typ: makePointer(fieldTy), loc: loc) + let fieldLoad = HirNode(kind: hLoad, loadPtr: fieldPtr, typ: fieldTy, loc: loc) + if arg.patIdent notin ctx.patternBoundNames: + result.add(hirAlloca(arg.patIdent, fieldTy, loc)) + ctx.patternBoundNames.incl(arg.patIdent) + result.add(hirStore(hirVar(arg.patIdent, fieldTy, loc), fieldLoad, loc)) + for nf in pattern.patEnumNamed: + if nf.pattern == nil or nf.pattern.kind != pkIdent: + continue + var fieldTy = makeInt() + for entry in namedFields: + if entry.name == nf.name: + fieldTy = entry.typ + break + # Named payload fields live under data.VariantName.name + 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) + 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) + if nf.pattern.patIdent notin ctx.patternBoundNames: + result.add(hirAlloca(nf.pattern.patIdent, fieldTy, loc)) + ctx.patternBoundNames.incl(nf.pattern.patIdent) + result.add(hirStore(hirVar(nf.pattern.patIdent, fieldTy, loc), fieldLoad, loc)) + of pkGuarded: + result.add(ctx.matchPatternBindings(subject, pattern.patGuardedInner, subjectEnumName, subjectHasData, loc)) + else: + discard + proc lowerMatch(ctx: var LowerCtx, subject: HirNode, arms: seq[HirMatchArm], typ: Type, loc: SourceLocation): HirNode = ## Lower match expression to a block with if-else chain. - ## Supports: enum tags, integer/bool/char/string literals, ranges, wildcard/ident. + ## Supports: enum tags + payload bindings, integer/bool/char/string literals, + ## ranges, wildcard/ident catch-all. let hasResult = typ != nil and typ.kind != tkVoid and typ.kind != tkUnknown let resultName = ctx.freshName() var stmts: seq[HirNode] = @[] @@ -143,8 +238,8 @@ proc lowerMatch(ctx: var LowerCtx, subject: HirNode, arms: seq[HirMatchArm], typ subjectEnumName = subject.typ.name subjectHasData = ctx.enumHasDataVariants(subjectEnumName) - proc makeArmBlock(body: HirNode): HirNode = - var armStmts: seq[HirNode] = @[] + proc makeArmBlock(body: HirNode, bindStmts: seq[HirNode]): HirNode = + var armStmts: seq[HirNode] = bindStmts if hasResult: armStmts.add(hirStore(hirVar(resultName, typ, loc), body, loc)) elif body != nil: @@ -157,7 +252,8 @@ proc lowerMatch(ctx: var LowerCtx, subject: HirNode, arms: seq[HirMatchArm], typ for i in countdown(arms.len - 1, 0): let arm = arms[i] - let armBlock = makeArmBlock(arm.body) + let binds = matchPatternBindings(ctx, subject, arm.pattern, subjectEnumName, subjectHasData, loc) + let armBlock = makeArmBlock(arm.body, binds) let cond = matchPatternCond(ctx, subject, arm.pattern, subjectEnumName, subjectHasData, loc) if cond == nil: @@ -202,6 +298,7 @@ proc initLowerCtx*(module: Module, sema: Sema): LowerCtx = result.funcAdapters = initHashSet[string]() result.funcAdapterSigs = initTable[string, Type]() result.seenFatTypes = @[] + result.patternBoundNames = initHashSet[string]() proc sanitizeFatPart(s: string): string = result = s.replace("const char*", "cstr").replace("unsigned int", "uint") @@ -244,8 +341,6 @@ proc hirFuncFatTypeName*(typ: Type): string = parts.add("void") return "BuxFn_" & parts.join("_") -proc resolveTypeExpr(ctx: var LowerCtx, te: TypeExpr): Type - proc substituteType(ctx: var LowerCtx, te: TypeExpr, subst: Table[string, Type]): Type = if te == nil: return makeUnknown() case te.kind @@ -1314,10 +1409,51 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode = of ekMatch: let subject = ctx.lowerExpr(expr.exprMatchSubject) + var subjectEnumName = "" + var subjectHasData = false + if subject.typ != nil and subject.typ.kind == tkNamed: + subjectEnumName = subject.typ.name + subjectHasData = ctx.enumHasDataVariants(subjectEnumName) + # Prefer resolved match type; fall back to function return type when arms + # only reference pattern bindings (not yet in varTypeExprs during resolve). + var matchTyp = typ + if matchTyp == nil or matchTyp.kind == tkUnknown: + if ctx.currentFuncRetType != nil and ctx.currentFuncRetType.kind notin {tkVoid, tkUnknown}: + matchTyp = ctx.currentFuncRetType var arms: seq[HirMatchArm] = @[] for arm in expr.exprMatchArms: + # Register bind types only (do NOT call matchPatternBindings here — that + # would mark names as already alloca'd and lowerMatch would skip them). + if arm.pattern != nil and arm.pattern.kind == pkEnum and subjectHasData: + var enumName = "" + var variantName = "" + if arm.pattern.patEnumPath.len >= 2: + enumName = arm.pattern.patEnumPath[0] + variantName = arm.pattern.patEnumPath[^1] + elif arm.pattern.patEnumPath.len == 1: + variantName = arm.pattern.patEnumPath[0] + enumName = subjectEnumName + var fieldTypes: seq[Type] = @[] + let enumSym = ctx.globalScope.lookup(enumName) + if enumSym != nil and enumSym.decl != nil and enumSym.decl.kind == dkEnum: + for v in enumSym.decl.declEnumVariants: + if v.name == variantName: + for f in v.fields: + fieldTypes.add(ctx.resolveTypeExpr(f)) + break + for i, arg in arm.pattern.patEnumArgs: + if arg != nil and arg.kind == pkIdent: + let ft = if i < fieldTypes.len: fieldTypes[i] else: makeInt() + ctx.varTypeExprs[arg.patIdent] = typeToTypeExpr(ft) + elif arm.pattern != nil and arm.pattern.kind == pkIdent: + let ty = if subject.typ != nil: subject.typ else: makeUnknown() + ctx.varTypeExprs[arm.pattern.patIdent] = typeToTypeExpr(ty) arms.add(HirMatchArm(pattern: arm.pattern, body: ctx.lowerExpr(arm.body))) - return lowerMatch(ctx, subject, arms, typ, loc) + # Re-resolve match type now that bindings are registered + if matchTyp == nil or matchTyp.kind == tkUnknown: + if arms.len > 0 and arms[0].body != nil and arms[0].body.typ != nil: + matchTyp = arms[0].body.typ + return lowerMatch(ctx, subject, arms, matchTyp, loc) of ekSizeOf: let ty = ctx.resolveTypeExpr(expr.exprSizeOfType) @@ -1796,9 +1932,11 @@ proc lowerFunc*(ctx: var LowerCtx, decl: Decl): HirFunc = let oldFuncDecl = ctx.currentFuncDecl let oldFuncRetType = ctx.currentFuncRetType let oldVarTypeExprs = ctx.varTypeExprs + let oldPatternBound = ctx.patternBoundNames ctx.currentFuncRetType = retType ctx.currentFuncDecl = decl ctx.varTypeExprs = initTable[string, TypeExpr]() # Clear local vars for new function + ctx.patternBoundNames = initHashSet[string]() # Add parameters to varTypeExprs after clearing so they are visible in the body. for p in funcParams: if p.ptype != nil: @@ -1824,6 +1962,7 @@ proc lowerFunc*(ctx: var LowerCtx, decl: Decl): HirFunc = ctx.currentFuncDecl = oldFuncDecl ctx.currentFuncRetType = oldFuncRetType ctx.varTypeExprs = oldVarTypeExprs + ctx.patternBoundNames = oldPatternBound result = HirFunc(name: funcName, params: params, retType: retType, body: body, isPublic: decl.isPublic) diff --git a/bootstrap/lexer.nim b/bootstrap/lexer.nim index 8ba9b34..2db82ab 100644 --- a/bootstrap/lexer.nim +++ b/bootstrap/lexer.nim @@ -228,6 +228,9 @@ proc scanEscapeSequence(lex: var Lexer): string = of 'r': result = "\r" of 't': result = "\t" of '0': result = "\0" + of '{', '}': + # Preserve \{ and \} so f"..." interpolation can treat them as literal braces + result = "\\" & $c of 'x': var hexVal = "" for _ in 0..<2: diff --git a/bootstrap/lir_c_backend.nim b/bootstrap/lir_c_backend.nim index b60ce9d..c2ad825 100644 --- a/bootstrap/lir_c_backend.nim +++ b/bootstrap/lir_c_backend.nim @@ -496,9 +496,17 @@ proc emitEnumDef(be: var LirCBackend, name: string, variants: seq[HirEnumVariant be.emitLine(&"typedef union {{") be.indent += 1 for v in variants: - if v.fields.len > 0: + if v.fields.len == 1: + # 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};") elif v.namedFields.len > 0: be.emitLine(&"struct {{") be.indent += 1 diff --git a/bootstrap/parser.nim b/bootstrap/parser.nim index 6a08232..95aebb5 100644 --- a/bootstrap/parser.nim +++ b/bootstrap/parser.nim @@ -463,7 +463,10 @@ proc parseStringInterpolation(p: var Parser, tok: Token): Expr = i += 1 texts.add(currentText) if exprs.len == 0: - return newLiteralExpr(tok) + # f"plain" / f"use \{x\}" with no real {expr} — use processed text (escapes applied) + var litTok = tok + litTok.text = "\"" & texts[0] & "\"" + return newLiteralExpr(litTok) return newStringInterpExpr(texts, exprs, tok.loc) proc parsePrimary(p: var Parser): Expr = @@ -524,9 +527,16 @@ proc parsePrimary(p: var Parser): Expr = p.structInitAllowed = false let subject = p.parseExpr() p.structInitAllowed = true + # Same newline rules as statement-form match (needed for `let x = match ...`) + while p.check(tkNewLine): + discard p.advance() discard p.expect(tkLBrace, "expected '{' to start match") var arms: seq[MatchArm] = @[] while not p.check(tkRBrace) and not p.isAtEnd: + while p.check(tkNewLine): + discard p.advance() + if p.check(tkRBrace) or p.isAtEnd: + break let armLoc = p.currentLoc let pat = p.parsePattern() discard p.expect(tkFatArrow, "expected '=>' in match arm") @@ -581,6 +591,17 @@ proc parsePrimary(p: var Parser): Expr = of tkNull: discard p.advance() return newLiteralExpr(Token(kind: tkNull, text: "null", loc: loc)) + of tkPipePipe: + # Empty-param closure written as `||` — lexer merges two '|' into tkPipePipe. + # Disambiguate from logical-or (which only appears mid-expression): as a primary, + # `|| -> T { ... }` / `|| { ... }` is always a zero-param closure. + discard p.advance() # || + var retTypePP: TypeExpr = nil + if p.check(tkArrow): + discard p.advance() # -> + retTypePP = p.parseType() + let bodyPP = p.parseBlock() + return Expr(kind: ekClosure, loc: loc, exprClosureParams: @[], exprClosureBody: bodyPP, exprClosureReturnType: retTypePP) of tkPipe: # Closure: |params| -> Ret { body } discard p.advance() # | diff --git a/bootstrap/sema.nim b/bootstrap/sema.nim index 7be7632..76d607e 100644 --- a/bootstrap/sema.nim +++ b/bootstrap/sema.nim @@ -757,26 +757,67 @@ proc checkTraitBounds(sema: var Sema, funcDecl: Decl, inferredTypes: seq[Type], if not sema.typeImplements(inferredTypes[i], bound): sema.emitError(loc, &"type '{inferredTypes[i].toString}' does not implement trait '{bound}'") -proc extractPatternBindings(sema: var Sema, pat: Pattern, scope: Scope) = - ## Add pattern-bound identifiers to scope with unknown type (best-effort) +proc extractPatternBindings(sema: var Sema, pat: Pattern, scope: Scope, subjectType: Type = nil) = + ## Add pattern-bound identifiers to scope. For enum payloads, resolve field types + ## from the matched enum variant so arm bodies type-check correctly. if pat == nil: return case pat.kind of pkIdent: - let sym = Symbol(kind: skVar, name: pat.patIdent, typ: makeUnknown(), isMutable: false) + let bindTy = if subjectType != nil and not subjectType.isUnknown: subjectType else: makeUnknown() + let sym = Symbol(kind: skVar, name: pat.patIdent, typ: bindTy, isMutable: false) discard scope.define(sym) of pkEnum: - for arg in pat.patEnumArgs: - sema.extractPatternBindings(arg, scope) + # Resolve variant field types from enum declaration + var enumName = "" + var variantName = "" + if pat.patEnumPath.len >= 2: + enumName = pat.patEnumPath[0] + variantName = pat.patEnumPath[^1] + elif pat.patEnumPath.len == 1: + variantName = pat.patEnumPath[0] + if subjectType != nil and subjectType.kind == tkNamed: + enumName = subjectType.name + var fieldTypes: seq[Type] = @[] + var namedFieldTypes: Table[string, Type] + if enumName != "": + let enumSym = sema.globalScope.lookup(enumName) + if enumSym != nil and enumSym.decl != nil and enumSym.decl.kind == dkEnum: + for v in enumSym.decl.declEnumVariants: + if v.name == variantName: + for f in v.fields: + fieldTypes.add(sema.resolveType(f)) + for nf in v.namedFields: + namedFieldTypes[nf.name] = sema.resolveType(nf.ftype) + break + for i, arg in pat.patEnumArgs: + let argTy = if i < fieldTypes.len: fieldTypes[i] else: makeUnknown() + if arg.kind == pkIdent: + let sym = Symbol(kind: skVar, name: arg.patIdent, typ: argTy, isMutable: false) + discard scope.define(sym) + else: + sema.extractPatternBindings(arg, scope, argTy) for nf in pat.patEnumNamed: - sema.extractPatternBindings(nf.pattern, scope) + let argTy = if namedFieldTypes.hasKey(nf.name): namedFieldTypes[nf.name] else: makeUnknown() + if nf.pattern.kind == pkIdent: + let sym = Symbol(kind: skVar, name: nf.pattern.patIdent, typ: argTy, isMutable: false) + discard scope.define(sym) + else: + sema.extractPatternBindings(nf.pattern, scope, argTy) of pkTuple: - for elem in pat.patTupleElements: - sema.extractPatternBindings(elem, scope) + for i, elem in pat.patTupleElements: + let elemTy = if subjectType != nil and subjectType.kind == tkTuple and i < subjectType.inner.len: + subjectType.inner[i] + else: makeUnknown() + if elem.kind == pkIdent: + let sym = Symbol(kind: skVar, name: elem.patIdent, typ: elemTy, isMutable: false) + discard scope.define(sym) + else: + sema.extractPatternBindings(elem, scope, elemTy) of pkStruct: for f in pat.patStructFields: sema.extractPatternBindings(f.pattern, scope) of pkGuarded: - sema.extractPatternBindings(pat.patGuardedInner, scope) + sema.extractPatternBindings(pat.patGuardedInner, scope, subjectType) else: discard @@ -1381,11 +1422,11 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type = lastType = sema.checkStmt(stmt, blockScope) return lastType of ekMatch: - discard sema.checkExpr(expr.exprMatchSubject, scope) + let subjectType = sema.checkExpr(expr.exprMatchSubject, scope) var resultType = makeUnknown() for arm in expr.exprMatchArms: var armScope = newScope(scope) - sema.extractPatternBindings(arm.pattern, armScope) + sema.extractPatternBindings(arm.pattern, armScope, subjectType) let armType = sema.checkExpr(arm.body, armScope) if resultType.isUnknown: resultType = armType @@ -1541,9 +1582,11 @@ proc checkStmt(sema: var Sema, stmt: Stmt, scope: Scope): Type = discard sema.checkStmt(Stmt(kind: skExpr, loc: stmt.stmtForBody.loc, stmtExpr: Expr(kind: ekBlock, loc: stmt.stmtForBody.loc, exprBlock: stmt.stmtForBody)), forScope) return makeVoid() of skMatch: - discard sema.checkExpr(stmt.stmtMatchSubject, scope) + let subjectType = sema.checkExpr(stmt.stmtMatchSubject, scope) for arm in stmt.stmtMatchArms: - discard sema.checkExpr(arm.body, scope) + var armScope = newScope(scope) + sema.extractPatternBindings(arm.pattern, armScope, subjectType) + discard sema.checkExpr(arm.body, armScope) return makeVoid() of skReturn: if stmt.stmtReturnValue != nil: diff --git a/docs/LanguageRef.md b/docs/LanguageRef.md index a0922ef..8fff3bc 100644 --- a/docs/LanguageRef.md +++ b/docs/LanguageRef.md @@ -361,21 +361,29 @@ func Main() -> int { ## Pattern Matching ```bux -match opt { - Option::Some(value) => PrintInt(value), - Option::None => PrintLine("none") +// Payload bindings: names in Variant(args) are bound in the arm body +func GetValue(opt: Option) -> int { + match opt { + Option::Some(value) => value, + Option::None => 0 + } +} + +match n { + 0 => 100, + 1..5 => 200, + 6..=10 => 300, + _ => -1 } ``` Supported patterns: - Wildcard: `_` - Literal: `42`, `"hello"`, `true` -- Identifier: `name` +- Identifier catch-all: `name` (binds whole subject) - Range: `1..9`, `1..=9` -- Enum destructuring: `Shape::Circle(r)` -- Struct destructuring: `Point { x: 0, y: 0 }` -- Tuple: `(a, b, c)` -- Guard: `t if t < 0` +- Enum tags + **payload bindings**: `Option::Some(value)`, `Pair::Two(a, b)` +- Struct / tuple / guard patterns: parsed; full lowering still evolving --- diff --git a/docs/QUALITY_PLAN.md b/docs/QUALITY_PLAN.md index 34d6bbb..7a21cfd 100644 --- a/docs/QUALITY_PLAN.md +++ b/docs/QUALITY_PLAN.md @@ -1,7 +1,7 @@ # Bux — План към „добър“ език (v0.5 → v1.0) -> **Дата:** 2026-07-16 (вечерта) -> **Текущо:** v0.5.x — selfhost loop, gradual ownership, green threads, **40+ examples**, match expr **bootstrap+selfhost** ✅ +> **Дата:** 2026-07-18 +> **Текущо:** v0.5.x — selfhost loop, gradual ownership, green threads, **41+ examples**, match + pattern bindings + **`f"..."` interp** bootstrap+selfhost ✅ > **Цел:** Език, с който се пишат реални проекти комфортно, безопасно (по избор) и с надежден toolchain. --- @@ -58,10 +58,11 @@ | B.1 | Proper tuple types в C backend | `(T,U)` → `Tuple_T_U` struct + `.0`/`.1` | ✅ bootstrap + selfhost | | B.2 | Function pointer types | `func(T)->U` fat ABI | ✅ bootstrap + selfhost | | B.3 | Match expression lowering (literals, ranges, enums) | Expression-context match → if-else | ✅ bootstrap + selfhost | +| B.3b | Pattern bindings (`Some(value) => value`) | Payload idents bound in arm body | ✅ bootstrap + selfhost | | B.4 | Closures multi-instance | Fat `BuxFn` + heap env | ✅ bootstrap + selfhost | -| B.4b | Closures: loop/return edge cases in body | По-сложни body control-flow | ⏳ | +| B.4b | Closures: `\|\|` empty params + loop/return body | Lexer `\|\|` vs empty closure; while/break/return | ✅ bootstrap + selfhost | | B.5 | По-добри diagnostics (snippet + hint) | DX #1 за нови потребители | ✅ | -| B.6 | Bootstrap ↔ selfhost feature parity | Tuples/closures/**match** done; string interp / some ops still bootstrap-heavy | 🔄 | +| B.6 | Bootstrap ↔ selfhost feature parity | empty `\|\|`, match-as-expr, **string interp `f"..."`** bootstrap+selfhost | ✅ | ### C — Gradual Ownership 2.0 (P1) @@ -200,14 +201,44 @@ A (stdlib ergonomics) → B (compiler holes) → C (ownership depth) 5. **Last-expr return:** `Lcx_LowerBlock` converts final `skExpr` into `return` (needed for `func F() -> T { match ... }`) 6. Verified: `pattern_matching` via **buxc2**; simple enum + ranges; **selfhost-loop IDENTICAL ✓** +## Сесия 10 (pattern bindings — B.3b) + +1. **Bootstrap sema:** `extractPatternBindings` resolves enum payload field types from variant decl +2. **Bootstrap HIR:** `matchPatternBindings` → `alloca name; name = subject.data.Variant_i` before arm body +3. **Match result type:** fall back to `currentFuncRetType` when arm bodies only use bindings +4. **Multi-field enum layout:** positional `fields.len > 1` → nested struct in `_Data` union (no overlay) +5. **Selfhost:** parse `patArgs` linked list; `Sema_BindPattern`; `Lcx_PatternBindings` in match arms +6. **Example:** `pattern_matching.bux` uses `Option::Some(value) => value` (real binding, not `opt.data.Some_0`) +7. Verified: bootstrap + **buxc2** + all examples + error goldens + **selfhost-loop IDENTICAL ✓** + +## Сесия 11 (empty `||` closures + match-as-expr — B.4b) + +1. **Bug:** `|| -> int { ... }` lexed as `tkPipePipe` (logical-or), not two `tkPipe` → empty-param closures failed to parse +2. **Fix bootstrap:** primary `of tkPipePipe:` → zero-param `ekClosure` +3. **Fix selfhost:** `parserParseEmptyClosure` for `tkPipePipe` +4. **Match-as-expr:** expression-form match now skips newlines (same as statement form) → `let x = match n { ... }` works +5. **Verified control-flow in closure body:** while/break, early return from loop, multi-return if-chain +6. **Pattern bind reuse:** one alloca per binding name per function (`patternBoundNames`) so two matches can both use `v` +7. **Selfhost match-as-expr:** let-init + binary operands expand yield blocks (`Lcx_IsMatchYield` / `__binop_N`) +8. Examples: `closure_control.bux`, `match_let.bux` +9. Verified: bootstrap + **buxc2** + all examples + error goldens + **selfhost-loop IDENTICAL ✓** + +## Сесия 12 (string interpolation selfhost — B.6) + +1. **Selfhost parser:** `parserParseStringInterp` — interleaved lit/expr parts in `callArgs`, nested fragment parse via `Lexer_Tokenize` + sub-parser +2. **Selfhost sema/HIR:** `ekStringInterp` → `String_Concat` + `String_FromInt`/`FromBool`/`FromFloat` +3. **Bootstrap fix:** `f"plain"` no longer keeps the `f` prefix in the literal; `\{`/`\}` preserved by lexer and unescaped by interp parser +4. **Lexer:** `\{` / `\}` allowed (bootstrap + selfhost) so LanguageRef escape rules work +5. **Compiler hygiene:** original dense `&&`/`||` in the large interp loop caused bootstrap OOM (~27 GB) when compiling `parser.bux` — rewrite with simpler control flow +6. Example: `examples/string_interp.bux` (name/int/bool/plain/escaped braces) +7. Verified: bootstrap + **buxc2** + all 41 examples + error goldens + **selfhost-loop IDENTICAL ✓** + --- ## Следващи стъпки 1. **LSP hover / go-to-def** (над текущите diagnostics) 2. **Generic Iter map** (не само int), ако monomorphization с `func` params е стабилна -3. **Closures B.4b** — loop/return edge cases in closure body -4. Struct/tuple patterns + pattern bindings (`Some(value)` binds `value`) -5. `let x = match ...` multi-stmt yield (beyond tail-position return) -6. String interpolation / remaining bootstrap-only features (B.6) +3. Struct/tuple patterns (`Point { x, y }`, `(a, b)`) + nested bindings +4. Match arm multi-stmt bodies (beyond single expr) diff --git a/examples/closure_control.bux b/examples/closure_control.bux new file mode 100644 index 0000000..2f4991b --- /dev/null +++ b/examples/closure_control.bux @@ -0,0 +1,63 @@ +// Closures with control flow: empty `||` params, while/break, early return +import Std::Io::{PrintLine, PrintInt}; +import Std::Test::{Test_AssertEqInt, Test_Pass}; + +// Zero-param capturing closure written as `||` (not `| |`) +func MakeCounter(limit: int) -> func() -> int { + return || -> int { + var sum: int = 0; + var i: int = 0; + while i < limit { + sum = sum + i; + i = i + 1; + if i == 3 { + break; + } + } + return sum; + }; +} + +func MakeClamp(n: int) -> func(int) -> int { + return |x: int| -> int { + if x < 0 { + return 0; + } + if x > n { + return n; + } + return x; + }; +} + +// return from inside a loop inside a closure +func MakeLoopReturn(n: int) -> func() -> int { + return || -> int { + var i: int = 0; + while i < n { + if i == 2 { + return 99; + } + i = i + 1; + } + return i; + }; +} + +func Main() -> int { + let c: func() -> int = MakeCounter(10); + Test_AssertEqInt(c(), 3); // 0+1+2 + + let f: func(int) -> int = MakeClamp(5); + Test_AssertEqInt(f(-1), 0); + Test_AssertEqInt(f(3), 3); + Test_AssertEqInt(f(100), 5); + + let g: func() -> int = MakeLoopReturn(10); + Test_AssertEqInt(g(), 99); + + PrintInt(c()); + PrintLine(""); + Test_Pass("closure_control"); + return 0; +} diff --git a/examples/match_let.bux b/examples/match_let.bux new file mode 100644 index 0000000..9a14330 --- /dev/null +++ b/examples/match_let.bux @@ -0,0 +1,43 @@ +// Match as expression: let-init, inline arms, combined with operators +import Std::Io::{PrintLine, PrintInt}; +import Std::Test::{Test_AssertEqInt, Test_Pass}; + +enum Option { + Some(int), + None +} + +func Main() -> int { + let opt: Option = Option { tag: Option_Some }; + opt.data.Some_0 = 21; + + let x: int = match opt { + Option::Some(v) => v + v, + Option::None => 0 + }; + Test_AssertEqInt(x, 42); + + let n: int = 3; + let y: int = match n { + 0 => 100, + 1..5 => 200, + _ => -1 + }; + Test_AssertEqInt(y, 200); + + let z: int = match n { 3 => 1, _ => 0 } + match n { 3 => 2, _ => 0 }; + Test_AssertEqInt(z, 3); + + let none: Option = Option { tag: Option_None }; + // Same binding name as above — one alloca, reassigned per arm + let w: int = match none { + Option::Some(v) => v, + Option::None => -7 + }; + Test_AssertEqInt(w, -7); + + PrintInt(x); + PrintLine(""); + Test_Pass("match_let"); + return 0; +} diff --git a/examples/pattern_matching.bux b/examples/pattern_matching.bux index e2d4bf3..d808e6f 100644 --- a/examples/pattern_matching.bux +++ b/examples/pattern_matching.bux @@ -1,19 +1,41 @@ -// Pattern Matching — enum tags, literals, ranges, wildcard +// Pattern Matching — enum tags, payload bindings, literals, ranges, wildcard import Std::Io::{PrintLine, PrintInt}; - +import Std::Test::{Test_AssertEqInt, Test_Pass}; enum Option { Some(int), None } +enum Msg { + Quit, + Move(int), + Write(String) +} + func GetValue(opt: Option) -> int { + // Pattern binding: value is bound from Option::Some payload match opt { - Option::Some(value) => opt.data.Some_0, + Option::Some(value) => value, Option::None => 0 } } +func DoubleSome(opt: Option) -> int { + match opt { + Option::Some(n) => n + n, + Option::None => -1 + } +} + +func MsgCode(m: Msg) -> int { + match m { + Msg::Quit => 0, + Msg::Move(x) => x, + Msg::Write(s) => 1 + } +} + func Classify(n: int) -> int { // literal arms + exclusive range + inclusive range + wildcard match n { @@ -40,6 +62,15 @@ func Main() -> int { let opt2: Option = Option { tag: Option_None }; + Test_AssertEqInt(GetValue(opt1), 42); + Test_AssertEqInt(GetValue(opt2), 0); + Test_AssertEqInt(DoubleSome(opt1), 84); + Test_AssertEqInt(DoubleSome(opt2), -1); + + let m: Msg = Msg { tag: Msg_Move }; + m.data.Move_0 = 7; + Test_AssertEqInt(MsgCode(m), 7); + PrintLine("opt1 value: "); PrintInt(GetValue(opt1)); PrintLine(""); @@ -61,5 +92,6 @@ func Main() -> int { PrintLine("color:"); PrintLine(ColorName(2)); + Test_Pass("pattern_matching"); return 0; } diff --git a/examples/string_interp.bux b/examples/string_interp.bux new file mode 100644 index 0000000..a6157aa --- /dev/null +++ b/examples/string_interp.bux @@ -0,0 +1,23 @@ +import Std::Io::{PrintLine}; +import Std::Test::{Test_AssertEqString, Test_Pass}; + +func Main() -> int { + let name: String = "Bux"; + let n: int = 42; + let msg: String = f"Hello, {name}! count={n}"; + PrintLine(msg); + Test_AssertEqString(msg, "Hello, Bux! count=42"); + + let flag: bool = true; + let bmsg: String = f"flag={flag}"; + Test_AssertEqString(bmsg, "flag=true"); + + let empty: String = f"plain"; + Test_AssertEqString(empty, "plain"); + + let raw: String = f"use \{braces\} literally"; + Test_AssertEqString(raw, "use {braces} literally"); + + Test_Pass("string_interp"); + return 0; +} diff --git a/src/ast.bux b/src/ast.bux index 01af564..a607c78 100644 --- a/src/ast.bux +++ b/src/ast.bux @@ -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 diff --git a/src/hir_lower.bux b/src/hir_lower.bux index f474a27..a10e24d 100644 --- a/src/hir_lower.bux +++ b/src/hir_lower.bux @@ -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 = ;` + 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); } } diff --git a/src/lexer.bux b/src/lexer.bux index 883eeb6..0f8f6ce 100644 --- a/src/lexer.bux +++ b/src/lexer.bux @@ -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); diff --git a/src/parser.bux b/src/parser.bux index b019da7..c9ffa97 100644 --- a/src/parser.bux +++ b/src/parser.bux @@ -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; diff --git a/src/sema.bux b/src/sema.bux index 0781ec9..ae29a9b 100644 --- a/src/sema.bux +++ b/src/sema.bux @@ -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;