diff --git a/bootstrap/c_backend.nim b/bootstrap/c_backend.nim index 98584b3..bb87656 100644 --- a/bootstrap/c_backend.nim +++ b/bootstrap/c_backend.nim @@ -401,7 +401,8 @@ proc emitExpr(be: var CBackend, node: HirNode): string = return "0" of hMatch: - return "0" # TODO: match expression lowering + # Match should be desugared in hir_lower to if-else. Fallback: 0. + return "0" else: return "0" diff --git a/bootstrap/hir_lower.nim b/bootstrap/hir_lower.nim index 6e567b6..088be0d 100644 --- a/bootstrap/hir_lower.nim +++ b/bootstrap/hir_lower.nim @@ -55,14 +55,86 @@ proc enumHasDataVariants(ctx: var LowerCtx, enumName: string): bool = return true return false +proc litTokenType(tok: Token): Type = + case tok.kind + of tkIntLiteral: makeInt() + of tkFloatLiteral: makeFloat64() + of tkStringLiteral: makeStr() + of tkCharLiteral: makeChar32() + of tkBoolLiteral: makeBool() + else: makeUnknown() + +proc patternLiteralNode(pat: Pattern, loc: SourceLocation): HirNode = + ## Convert a pkLiteral pattern into an hLit node, or nil if not a literal. + if pat == nil or pat.kind != pkLiteral: + return nil + return hirLit(pat.patLit, litTokenType(pat.patLit), loc) + +proc matchAlwaysTrue(loc: SourceLocation): HirNode = + hirLit(Token(kind: tkBoolLiteral, text: "true", loc: loc), makeBool(), loc) + +proc matchPatternCond(ctx: var LowerCtx, subject: HirNode, pattern: Pattern, + subjectEnumName: string, subjectHasData: bool, + loc: SourceLocation): HirNode = + ## Build a boolean condition for a match pattern. + ## Returns nil for always-true arms (wildcard / catch-all). + if pattern == nil: + return nil + case pattern.kind + of pkWildcard, pkIdent: + return nil + of pkLiteral: + let litNode = patternLiteralNode(pattern, loc) + if litNode == nil: + return nil + return hirBinary(tkEq, subject, litNode, makeBool(), loc) + of pkRange: + let loNode = patternLiteralNode(pattern.patRangeLo, loc) + let hiNode = patternLiteralNode(pattern.patRangeHi, loc) + if loNode == nil or hiNode == nil: + # Non-literal range endpoints — treat as always-true (best-effort) + return nil + let loOk = hirBinary(tkGe, subject, loNode, makeBool(), loc) + let hiOp = if pattern.patRangeInclusive: tkLe else: tkLt + let hiOk = hirBinary(hiOp, subject, hiNode, makeBool(), loc) + return hirBinary(tkAmpAmp, loOk, hiOk, makeBool(), loc) + of pkEnum: + let path = pattern.patEnumPath + if path.len >= 2: + let enumName = path[0] + let variantName = path[^1] + let tagName = enumName & "_" & variantName + if subjectHasData and enumName == subjectEnumName: + # Algebraic enum: compare subject.tag + let tagField = HirNode(kind: hFieldPtr, fieldPtrBase: subject, fieldName: "tag", + typ: makePointer(makeNamed(enumName & "_Tag")), loc: loc) + let tagLoad = HirNode(kind: hLoad, loadPtr: tagField, typ: makeNamed(enumName & "_Tag"), loc: loc) + let tagConst = hirLit(Token(kind: tkIdent, text: tagName, loc: loc), makeNamed(enumName & "_Tag"), loc) + return hirBinary(tkEq, tagLoad, tagConst, makeBool(), loc) + else: + # Simple enum or cross-enum match: compare subject directly + let tagConst = hirLit(Token(kind: tkIdent, text: tagName, loc: loc), makeNamed(enumName), loc) + return hirBinary(tkEq, subject, tagConst, makeBool(), loc) + # Single-segment enum path — always-true fallback + return nil + of pkGuarded: + # Guard: inner pattern AND guard expression (lowered later if needed) + # For now only support always-true inner + guard as bool expr via lowerExpr path. + # Guards are not yet fully lowered here (require expr lowering of guard). + return nil + else: + # Struct/tuple patterns: not yet fully lowered — always-true + return nil + 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. - # For now, supports enum tag matching and wildcard/ident fallbacks. + ## Lower match expression to a block with if-else chain. + ## Supports: enum tags, integer/bool/char/string literals, ranges, wildcard/ident. + let hasResult = typ != nil and typ.kind != tkVoid and typ.kind != tkUnknown let resultName = ctx.freshName() var stmts: seq[HirNode] = @[] - # Allocate result variable - stmts.add(hirAlloca(resultName, typ, loc)) + if hasResult: + stmts.add(hirAlloca(resultName, typ, loc)) # Determine whether the matched enum has data variants (needs .tag access). var subjectEnumName = "" @@ -71,81 +143,44 @@ 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] = @[] + if hasResult: + armStmts.add(hirStore(hirVar(resultName, typ, loc), body, loc)) + elif body != nil: + # Void match: evaluate body for side effects + armStmts.add(body) + return hirBlock(armStmts, nil, makeVoid(), loc) + # Build if-else chain from arms (last arm is the outermost else) var ifChain: HirNode = nil for i in countdown(arms.len - 1, 0): let arm = arms[i] - let body = arm.body + let armBlock = makeArmBlock(arm.body) + let cond = matchPatternCond(ctx, subject, arm.pattern, subjectEnumName, subjectHasData, loc) - case arm.pattern.kind - of pkEnum: - let path = arm.pattern.patEnumPath - if path.len >= 2: - let enumName = path[0] - let variantName = path[^1] - let tagName = enumName & "_" & variantName - - var cond: HirNode - if subjectHasData and enumName == subjectEnumName: - # Algebraic enum: compare subject.tag - let tagField = HirNode(kind: hFieldPtr, fieldPtrBase: subject, fieldName: "tag", - typ: makePointer(makeNamed(enumName & "_Tag")), loc: loc) - let tagLoad = HirNode(kind: hLoad, loadPtr: tagField, typ: makeNamed(enumName & "_Tag"), loc: loc) - let tagConst = hirLit(Token(kind: tkIdent, text: tagName, loc: loc), makeNamed(enumName & "_Tag"), loc) - cond = hirBinary(tkEq, tagLoad, tagConst, makeBool(), loc) - else: - # Simple enum or cross-enum match: compare subject directly - let tagConst = hirLit(Token(kind: tkIdent, text: tagName, loc: loc), makeNamed(enumName), loc) - cond = hirBinary(tkEq, subject, tagConst, makeBool(), loc) - - # body: result = arm_body - var armStmts: seq[HirNode] = @[] - armStmts.add(hirStore(hirVar(resultName, typ, loc), body, loc)) - let armBlock = hirBlock(armStmts, nil, makeVoid(), loc) - - if ifChain == nil: - ifChain = HirNode(kind: hIf, ifCond: cond, ifThen: armBlock, ifElse: nil, - typ: makeVoid(), loc: loc) - else: - ifChain = HirNode(kind: hIf, ifCond: cond, ifThen: armBlock, ifElse: ifChain, - typ: makeVoid(), loc: loc) - else: - var armStmts: seq[HirNode] = @[] - armStmts.add(hirStore(hirVar(resultName, typ, loc), body, loc)) - let armBlock = hirBlock(armStmts, nil, makeVoid(), loc) - if ifChain == nil: - ifChain = armBlock - else: - ifChain = HirNode(kind: hIf, - ifCond: hirLit(Token(kind: tkBoolLiteral, text: "true", loc: loc), makeBool(), loc), - ifThen: armBlock, ifElse: ifChain, typ: makeVoid(), loc: loc) - of pkWildcard, pkIdent: - # Default arm — always matches - var armStmts: seq[HirNode] = @[] - armStmts.add(hirStore(hirVar(resultName, typ, loc), body, loc)) - let armBlock = hirBlock(armStmts, nil, makeVoid(), loc) + if cond == nil: + # Always-true arm (wildcard / incomplete pattern) if ifChain == nil: ifChain = armBlock else: - ifChain = HirNode(kind: hIf, - ifCond: hirLit(Token(kind: tkBoolLiteral, text: "true", loc: loc), makeBool(), loc), - ifThen: armBlock, ifElse: ifChain, typ: makeVoid(), loc: loc) + ifChain = HirNode(kind: hIf, ifCond: matchAlwaysTrue(loc), ifThen: armBlock, + ifElse: ifChain, typ: makeVoid(), loc: loc) else: - var armStmts: seq[HirNode] = @[] - armStmts.add(hirStore(hirVar(resultName, typ, loc), body, loc)) - let armBlock = hirBlock(armStmts, nil, makeVoid(), loc) if ifChain == nil: - ifChain = armBlock + ifChain = HirNode(kind: hIf, ifCond: cond, ifThen: armBlock, ifElse: nil, + typ: makeVoid(), loc: loc) else: - ifChain = HirNode(kind: hIf, - ifCond: hirLit(Token(kind: tkBoolLiteral, text: "true", loc: loc), makeBool(), loc), - ifThen: armBlock, ifElse: ifChain, typ: makeVoid(), loc: loc) + ifChain = HirNode(kind: hIf, ifCond: cond, ifThen: armBlock, ifElse: ifChain, + typ: makeVoid(), loc: loc) - stmts.add(ifChain) + if ifChain != nil: + stmts.add(ifChain) - # Return the result variable as the block expression - return hirBlock(stmts, hirVar(resultName, typ, loc), typ, loc) + if hasResult: + return hirBlock(stmts, hirVar(resultName, typ, loc), typ, loc) + return hirBlock(stmts, nil, makeVoid(), loc) proc initLowerCtx*(module: Module, sema: Sema): LowerCtx = result.module = module @@ -1672,8 +1707,8 @@ proc lowerStmt(ctx: var LowerCtx, stmt: Stmt): HirNode = var arms: seq[HirMatchArm] = @[] for arm in stmt.stmtMatchArms: arms.add(HirMatchArm(pattern: arm.pattern, body: ctx.lowerExpr(arm.body))) - return ctx.flushPending(HirNode(kind: hMatch, matchSubject: subject, matchArms: arms, - typ: makeVoid(), loc: loc)) + # Statement match: lower to if-else chain (void result) + return ctx.flushPending(lowerMatch(ctx, subject, arms, makeVoid(), loc)) of skSwitch: let subject = ctx.lowerExpr(stmt.stmtSwitchExpr) diff --git a/docs/QUALITY_PLAN.md b/docs/QUALITY_PLAN.md index 8146941..34d6bbb 100644 --- a/docs/QUALITY_PLAN.md +++ b/docs/QUALITY_PLAN.md @@ -1,7 +1,7 @@ # Bux — План към „добър“ език (v0.5 → v1.0) -> **Дата:** 2026-07-15 (обновено вечерта) -> **Текущо:** v0.5.x — selfhost loop, gradual ownership, green threads, **40+ examples** ✅ +> **Дата:** 2026-07-16 (вечерта) +> **Текущо:** v0.5.x — selfhost loop, gradual ownership, green threads, **40+ examples**, match expr **bootstrap+selfhost** ✅ > **Цел:** Език, с който се пишат реални проекти комфортно, безопасно (по избор) и с надежден toolchain. --- @@ -57,11 +57,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 до край в C (не `return "0"`) | Expression-context match | ⏳ | +| B.3 | Match expression lowering (literals, ranges, enums) | Expression-context match → if-else | ✅ 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.5 | По-добри diagnostics (snippet + hint) | DX #1 за нови потребители | ✅ | -| B.6 | Bootstrap ↔ selfhost feature parity | Tuples/closures done; string interp / ops still bootstrap-heavy | 🔄 | +| B.6 | Bootstrap ↔ selfhost feature parity | Tuples/closures/**match** done; string interp / some ops still bootstrap-heavy | 🔄 | ### C — Gradual Ownership 2.0 (P1) @@ -177,13 +177,37 @@ A (stdlib ergonomics) → B (compiler holes) → C (ownership depth) 3. Example `iter_hof.bux` (sum=15, product=120) 4. Selfhost fix: pointer `->` field access; no bogus bounds check on `arr.data[len]` (Push) +## Сесия 8 (match expression + error goldens) + +1. **Match expression lowering** (B.3): `pkLiteral`, `pkRange` (`a..b` / `a..=b`), enum tags, wildcard + - Bugfix: literal arms were always-true (`else` branch) → `match 1 { 1=>10, 2=>20 }` returned 10 for all + - `skMatch` now also lowers via `lowerMatch` (void result) +2. Expanded `examples/pattern_matching.bux` (enums + classify ranges + string arms) +3. **Error golden cases** (`tests/error_golden/`): + - `parse_error` — incomplete `let x = ;` + - `use_after_move` — `@[Checked]` + `own String` + - `double_mut_borrow` — two `&mut` of same var +4. `run.sh` normalize: absolute paths in "parse errors in …" lines + +## Сесия 9 (selfhost match — B.6 parity) + +1. **AST:** `MatchArm` linked list; `Pattern.patChild1/2` for ranges; `Expr.matchArms` +2. **Parser:** full `parserParsePattern` / `parserParseMatchExpr` (was skip-arms stub) + - literals, `_`, ident, `Enum::Variant` / `Enum::Variant(...)`, ranges + - statement `match` → `skExpr` + `ekMatch` +3. **Sema:** type-check subject + arms; propagate first-arm type to `expr.refType` +4. **HIR lower:** `Lcx_LowerMatch` → alloca result + if-else stores (enum `.tag` / simple / literal / range) +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 ✓** + --- -## Утре — предложени следващи стъпки +## Следващи стъпки -1. **Match expression** lowering докрай (B.3) -2. **Още error golden cases** (parse error, use-after-move) -3. **LSP hover / go-to-def** (над текущите diagnostics) -4. **Generic Iter map** (не само int), ако monomorphization с `func` params е стабилна -5. **Selfhost-loop** re-check след fat-func + tuples промените +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) diff --git a/examples/pattern_matching.bux b/examples/pattern_matching.bux index d3f1b96..e2d4bf3 100644 --- a/examples/pattern_matching.bux +++ b/examples/pattern_matching.bux @@ -1,4 +1,4 @@ -// Pattern Matching - Match expressions with algebraic enums +// Pattern Matching — enum tags, literals, ranges, wildcard import Std::Io::{PrintLine, PrintInt}; @@ -14,19 +14,52 @@ func GetValue(opt: Option) -> int { } } +func Classify(n: int) -> int { + // literal arms + exclusive range + inclusive range + wildcard + match n { + 0 => 100, + 1 => 101, + 2..5 => 200, + 6..=10 => 300, + _ => -1 + } +} + +func ColorName(c: int) -> String { + match c { + 1 => "red", + 2 => "green", + 3 => "blue", + _ => "unknown" + } +} + func Main() -> int { let opt1: Option = Option { tag: Option_Some }; opt1.data.Some_0 = 42; - + let opt2: Option = Option { tag: Option_None }; - + PrintLine("opt1 value: "); PrintInt(GetValue(opt1)); PrintLine(""); - + PrintLine("opt2 value: "); PrintInt(GetValue(opt2)); PrintLine(""); - + + PrintLine("classify:"); + PrintInt(Classify(0)); + PrintLine(""); + PrintInt(Classify(3)); + PrintLine(""); + PrintInt(Classify(8)); + PrintLine(""); + PrintInt(Classify(99)); + PrintLine(""); + + PrintLine("color:"); + PrintLine(ColorName(2)); + return 0; } diff --git a/src/ast.bux b/src/ast.bux index 9f44590..01af564 100644 --- a/src/ast.bux +++ b/src/ast.bux @@ -75,8 +75,19 @@ struct Pattern { patLitKind: int, // for pkLiteral (token kind) patLitText: String, // for pkLiteral (token text) patRangeInclusive: bool, // for pkRange - patEnumPath: String, // for pkEnum (path joined) + patEnumPath: String, // for pkEnum: "Enum::Variant" patStructName: String, // for pkStruct + patChild1: *Pattern, // range lo / nested + patChild2: *Pattern, // range hi / nested +} + +// Match arm: pattern => body +struct MatchArm { + line: uint32, + column: uint32, + pattern: *Pattern, + body: *Expr, + next: *MatchArm, } // --------------------------------------------------------------------------- @@ -165,6 +176,9 @@ struct Expr { // Call arguments (linked list for multi-arg support) callArgs: *ExprList, callArgCount: int, + // Match arms (for ekMatch) + matchArms: *MatchArm, + matchArmCount: int, } // --------------------------------------------------------------------------- diff --git a/src/hir_lower.bux b/src/hir_lower.bux index 55f5bfc..f474a27 100644 --- a/src/hir_lower.bux +++ b/src/hir_lower.bux @@ -406,6 +406,251 @@ func Lcx_GetArrayElemType(te: *TypeExpr) -> String { return ""; } +// --------------------------------------------------------------------------- +// Match lowering helpers +// --------------------------------------------------------------------------- + +func Lcx_EnumHasData(ctx: *LowerCtx, enumName: String) -> bool { + if String_Eq(enumName, "") { return false; } + let sym: Symbol = Scope_Lookup(ctx.scope, enumName); + if sym.decl == null as *Decl || sym.decl.kind != dkEnum { return false; } + if sym.decl.variantCount > 0 && sym.decl.variant0.fieldCount > 0 { return true; } + if sym.decl.variantCount > 1 && sym.decl.variant1.fieldCount > 0 { return true; } + if sym.decl.variantCount > 2 && sym.decl.variant2.fieldCount > 0 { return true; } + if sym.decl.variantCount > 3 && sym.decl.variant3.fieldCount > 0 { return true; } + if sym.decl.variantCount > 4 && sym.decl.variant4.fieldCount > 0 { return true; } + if sym.decl.variantCount > 5 && sym.decl.variant5.fieldCount > 0 { return true; } + if sym.decl.variantCount > 6 && sym.decl.variant6.fieldCount > 0 { return true; } + if sym.decl.variantCount > 7 && sym.decl.variant7.fieldCount > 0 { return true; } + if sym.decl.variantCount > 8 && sym.decl.variant8.fieldCount > 0 { return true; } + return false; +} + +func Lcx_MakeLitHir(litKind: int, litText: String, line: uint32, col: uint32) -> *HirNode { + let n: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; + n.kind = hLit; + n.line = line; + n.column = col; + n.intValue = litKind; + n.strValue = litText; + return n; +} + +func Lcx_MakeBinHir(op: int, left: *HirNode, right: *HirNode, line: uint32, col: uint32) -> *HirNode { + let n: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; + n.kind = hBinary; + n.line = line; + n.column = col; + n.intValue = op; + n.child1 = left; + n.child2 = right; + return n; +} + +func Lcx_MakeTrueHir(line: uint32, col: uint32) -> *HirNode { + return Lcx_MakeLitHir(tkBoolLiteral, "true", line, col); +} + +// Build condition HirNode for a match pattern. Returns null = always-true. +func Lcx_PatternCond(ctx: *LowerCtx, subject: *HirNode, pat: *Pattern, + subjectEnumName: String, subjectHasData: bool, + line: uint32, col: uint32) -> *HirNode { + if pat == null as *Pattern { return null as *HirNode; } + let kind: int = pat.kind; + + if kind == pkWildcard || kind == pkIdent { + return null as *HirNode; + } + + if kind == pkLiteral { + let lit: *HirNode = Lcx_MakeLitHir(pat.patLitKind, pat.patLitText, line, col); + return Lcx_MakeBinHir(tkEq, subject, lit, line, col); + } + + if kind == pkRange { + let loPat: *Pattern = pat.patChild1; + let hiPat: *Pattern = pat.patChild2; + if loPat == null as *Pattern || hiPat == null as *Pattern { return null as *HirNode; } + if loPat.kind != pkLiteral || hiPat.kind != pkLiteral { return null as *HirNode; } + let lo: *HirNode = Lcx_MakeLitHir(loPat.patLitKind, loPat.patLitText, line, col); + let hi: *HirNode = Lcx_MakeLitHir(hiPat.patLitKind, hiPat.patLitText, line, col); + let loOk: *HirNode = Lcx_MakeBinHir(tkGe, subject, lo, line, col); + var hiOp: int = tkLt; + if pat.patRangeInclusive { hiOp = tkLe; } + let hiOk: *HirNode = Lcx_MakeBinHir(hiOp, subject, hi, line, col); + return Lcx_MakeBinHir(tkAmpAmp, loOk, hiOk, line, col); + } + + if kind == pkEnum { + let path: String = pat.patEnumPath; + // path is "Enum::Variant" or just "Variant" + var enumName: String = ""; + var variantName: String = path; + if String_Contains(path, "::") { + enumName = String_SplitPart(path, "::", 0); + variantName = String_SplitPart(path, "::", 1); + } + let tagName: String = String_Concat(String_Concat(enumName, "_"), variantName); + // Prefer subject enum name when path is full + var useEnum: String = enumName; + if String_Eq(useEnum, "") { useEnum = subjectEnumName; } + let fullTag: String = String_Concat(String_Concat(useEnum, "_"), variantName); + + if subjectHasData && (String_Eq(enumName, subjectEnumName) || String_Eq(enumName, "")) { + // Algebraic: subject.tag == Enum_Variant + let tagPtr: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; + tagPtr.kind = hFieldPtr; + tagPtr.line = line; + tagPtr.column = col; + tagPtr.strValue = "tag"; + tagPtr.child1 = subject; + let tagLoad: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; + tagLoad.kind = hLoad; + tagLoad.line = line; + tagLoad.column = col; + tagLoad.child1 = tagPtr; + let tagConst: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; + tagConst.kind = hVar; + tagConst.line = line; + tagConst.column = col; + tagConst.strValue = fullTag; + return Lcx_MakeBinHir(tkEq, tagLoad, tagConst, line, col); + } else { + // Simple enum: subject == Enum_Variant + let tagConst: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; + tagConst.kind = hVar; + tagConst.line = line; + tagConst.column = col; + if !String_Eq(enumName, "") { + tagConst.strValue = fullTag; + } else { + tagConst.strValue = tagName; + } + return Lcx_MakeBinHir(tkEq, subject, tagConst, line, col); + } + } + + return null as *HirNode; +} + +// 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; + let col: uint32 = expr.column; + let subject: *HirNode = Lcx_LowerExpr(ctx, expr.child1); + + ctx.varCounter = ctx.varCounter + 1; + let resultName: String = String_Concat("__match_", String_FromInt(ctx.varCounter as int64)); + + // Result type from sema refType, default int + var typeName: String = "int"; + if expr.refType != null as *TypeExpr && !String_Eq(expr.refType.typeName, "") { + typeName = expr.refType.typeName; + } + + // Subject enum info + var subjectEnumName: String = ""; + var subjectHasData: bool = false; + if expr.child1 != null as *Expr && expr.child1.refType != null as *TypeExpr { + if expr.child1.refType.kind == tekNamed { + subjectEnumName = expr.child1.refType.typeName; + subjectHasData = Lcx_EnumHasData(ctx, subjectEnumName); + } + } + + // Alloca result + let allocaNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; + allocaNode.kind = hAlloca; + allocaNode.line = line; + allocaNode.column = col; + allocaNode.strValue = resultName; + allocaNode.typeName = typeName; + + // Collect arms into a temporary array via reverse build of if-chain + // First count arms and build from last to first + var armCount: int = 0; + var arm: *MatchArm = expr.matchArms; + while arm != null as *MatchArm { + armCount = armCount + 1; + arm = arm.next; + } + + // Build if-else from last arm to first + var ifChain: *HirNode = null as *HirNode; + var ai: int = armCount - 1; + while ai >= 0 { + // Find arm at index ai + var cur: *MatchArm = expr.matchArms; + var j: int = 0; + while j < ai && cur != null as *MatchArm { + cur = cur.next; + j = j + 1; + } + if cur == null as *MatchArm { + ai = ai - 1; + continue; + } + + let bodyHir: *HirNode = Lcx_LowerExpr(ctx, cur.body); + // result = body + let storeNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; + storeNode.kind = hStore; + storeNode.line = line; + storeNode.column = col; + let resVar: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; + resVar.kind = hVar; + resVar.strValue = resultName; + storeNode.child1 = resVar; + storeNode.child2 = bodyHir; + + let armBlock: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; + armBlock.kind = hBlock; + armBlock.line = line; + armBlock.column = col; + armBlock.child1 = storeNode; + + let cond: *HirNode = Lcx_PatternCond(ctx, subject, cur.pattern, subjectEnumName, subjectHasData, line, col); + if cond == null as *HirNode { + // Always-true arm + if ifChain == null as *HirNode { + ifChain = armBlock; + } else { + let ifNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; + ifNode.kind = hIf; + ifNode.line = line; + ifNode.column = col; + ifNode.child1 = Lcx_MakeTrueHir(line, col); + ifNode.child2 = armBlock; + ifNode.extraData = ifChain as *void; + ifChain = ifNode; + } + } else { + let ifNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; + ifNode.kind = hIf; + ifNode.line = line; + ifNode.column = col; + ifNode.child1 = cond; + ifNode.child2 = armBlock; + ifNode.extraData = ifChain as *void; + ifChain = ifNode; + } + ai = ai - 1; + } + + // Chain: alloca → ifChain + allocaNode.child3 = ifChain; + + let block: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; + block.kind = hBlock; + block.line = line; + block.column = col; + block.child1 = allocaNode; + // Yield marker: strValue = result var name for last-expr return / let init + block.strValue = resultName; + block.typeName = typeName; + return block; +} + // --------------------------------------------------------------------------- // Expression lowering // --------------------------------------------------------------------------- @@ -422,6 +667,11 @@ func Lcx_LowerExpr(ctx: *LowerCtx, expr: *Expr) -> *HirNode { let kind: int = expr.kind; + // Match expression + if kind == ekMatch { + return Lcx_LowerMatch(ctx, expr); + } + // Literal if kind == ekLiteral { n.kind = hLit; @@ -2286,8 +2536,54 @@ func Lcx_LowerBlock(ctx: *LowerCtx, block: *Block, retTypeKind: int) -> *HirNode var firstNode: *HirNode = null as *HirNode; var prevNode: *HirNode = null as *HirNode; var stmt: *Stmt = block.firstStmt; + var lastStmt: *Stmt = null as *Stmt; while stmt != null as *Stmt { - let lowered: *HirNode = Lcx_LowerStmt(ctx, stmt); + lastStmt = stmt; + let isLast: bool = stmt.nextStmt == null as *Stmt; + // Last expression statement in a non-void function → implicit return + // (supports `func F() -> T { match ... }` / bare value as body) + var lowered: *HirNode = null as *HirNode; + if isLast && retTypeKind != tyVoid && retTypeKind != tyUnknown && retTypeKind >= 0 + && stmt.kind == skExpr && stmt.child1 != null as *Expr { + let exprNode: *HirNode = Lcx_LowerExpr(ctx, stmt.child1); + if exprNode != null as *HirNode { + // Match (and similar multi-stmt yields): hBlock with strValue = result var + if exprNode.kind == hBlock && !String_Eq(exprNode.strValue, "") { + let retVar: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; + retVar.kind = hVar; + retVar.strValue = exprNode.strValue; + let retNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; + retNode.kind = hReturn; + retNode.line = stmt.line; + retNode.column = stmt.column; + retNode.child1 = retVar; + // Append return at end of match block's child3 chain + var lastInBlock: *HirNode = exprNode.child1; + if lastInBlock == null as *HirNode { + exprNode.child1 = retNode; + } else { + while lastInBlock.child3 != null as *HirNode { + lastInBlock = lastInBlock.child3; + } + lastInBlock.child3 = retNode; + } + // Clear yield marker so emit treats it as plain block + exprNode.strValue = ""; + lowered = exprNode; + } else if exprNode.kind == hReturn { + lowered = exprNode; + } else { + let retNode: *HirNode = bux_alloc(sizeof(HirNode)) as *HirNode; + retNode.kind = hReturn; + retNode.line = stmt.line; + retNode.column = stmt.column; + retNode.child1 = exprNode; + lowered = retNode; + } + } + } else { + lowered = Lcx_LowerStmt(ctx, stmt); + } if lowered != null as *HirNode { if firstNode == null as *HirNode { firstNode = lowered; @@ -2430,11 +2726,11 @@ func Lcx_LowerFunc(ctx: *LowerCtx, decl: *Decl) -> *HirFunc { pi = pi + 1; } - // Lower body with function scope active + // Lower body with function scope active (pass ret kind for last-expr return) let prevScope: *Scope = ctx.scope; ctx.scope = &funcScope; if decl.refBody != null as *Block { - f.body = Lcx_LowerBlock(ctx, decl.refBody, -1); + f.body = Lcx_LowerBlock(ctx, decl.refBody, f.retTypeKind); } else { f.body = null as *HirNode; } diff --git a/src/parser.bux b/src/parser.bux index c1f162e..b019da7 100644 --- a/src/parser.bux +++ b/src/parser.bux @@ -13,6 +13,8 @@ func parserParsePrimary(p: *Parser) -> *Expr; func parserParsePostfixExpr(p: *Parser) -> *Expr; func parserParseUnary(p: *Parser) -> *Expr; func parserParseBinaryPrec(p: *Parser, minPrec: int) -> *Expr; +func parserParsePattern(p: *Parser) -> *Pattern; +func parserParseMatchExpr(p: *Parser) -> *Expr; // --------------------------------------------------------------------------- // Parser state @@ -346,6 +348,8 @@ func parserMakeExpr(kind: int, line: uint32, col: uint32) -> *Expr { e.structFieldCount = 0; e.callArgs = null as *ExprList; e.callArgCount = 0; + e.matchArms = null as *MatchArm; + e.matchArmCount = 0; return e; } @@ -490,10 +494,182 @@ func parserParsePrimary(p: *Parser) -> *Expr { return e; } + // match expr { arms } + if kind == tkMatch { + return parserParseMatchExpr(p); + } + parserEmitDiag(p, line, col, "expected expression"); return parserMakeExpr(ekLiteral, line, col); } +// --------------------------------------------------------------------------- +// Patterns (for match arms) +// --------------------------------------------------------------------------- + +func parserMakePattern(kind: int, line: uint32, col: uint32) -> *Pattern { + let pat: *Pattern = bux_alloc(sizeof(Pattern)) as *Pattern; + pat.kind = kind; + pat.line = line; + pat.column = col; + pat.patIdent = ""; + pat.patLitKind = 0; + pat.patLitText = ""; + pat.patRangeInclusive = false; + pat.patEnumPath = ""; + pat.patStructName = ""; + pat.patChild1 = null as *Pattern; + pat.patChild2 = null as *Pattern; + return pat; +} + +func parserParsePrimaryPattern(p: *Parser) -> *Pattern { + while parserCheck(p, tkNewLine) { discard parserAdvance(p); } + let tok: LexToken = parserCurToken(p); + let line: uint32 = tok.line; + let col: uint32 = tok.column; + let kind: int = tok.kind; + + // _ + if kind == tkUnderscore { + discard parserAdvance(p); + return parserMakePattern(pkWildcard, line, col); + } + + // Literals + if kind == tkIntLiteral || kind == tkFloatLiteral || kind == tkStringLiteral + || kind == tkCharLiteral || kind == tkBoolLiteral { + discard parserAdvance(p); + let pat: *Pattern = parserMakePattern(pkLiteral, line, col); + pat.patLitKind = kind; + pat.patLitText = tok.text; + return pat; + } + + // Ident / enum path / true|false as names + if kind == tkIdent { + discard parserAdvance(p); + let name: String = tok.text; + if String_Eq(name, "true") || String_Eq(name, "false") { + let pat: *Pattern = parserMakePattern(pkLiteral, line, col); + pat.patLitKind = tkBoolLiteral; + pat.patLitText = name; + return pat; + } + // Enum path: Enum::Variant or Enum::Variant(...) + if parserCheck(p, tkColonColon) { + var path: String = name; + while parserCheck(p, tkColonColon) { + discard parserAdvance(p); + let seg: LexToken = parserExpectIdentOrKeyword(p, "expected identifier in pattern path"); + path = String_Concat(path, "::"); + path = String_Concat(path, seg.text); + } + // Optional (args) for algebraic variants — parse and ignore bindings for now + if parserCheck(p, tkLParen) { + discard parserAdvance(p); + while !parserCheck(p, tkRParen) && parserPeek(p, 0) != tkEndOfFile { + discard parserParsePattern(p); + if parserCheck(p, tkComma) { discard parserAdvance(p); } + else { break; } + } + discard parserExpect(p, tkRParen, "expected ')' to close enum pattern"); + } + let pat: *Pattern = parserMakePattern(pkEnum, line, col); + pat.patEnumPath = path; + return pat; + } + // Bare name with (args): Variant(...) treated as single-segment enum + if parserCheck(p, tkLParen) { + discard parserAdvance(p); + while !parserCheck(p, tkRParen) && parserPeek(p, 0) != tkEndOfFile { + discard parserParsePattern(p); + 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; + return pat; + } + // Ident binding / catch-all name + let pat: *Pattern = parserMakePattern(pkIdent, line, col); + pat.patIdent = name; + return pat; + } + + parserEmitDiag(p, line, col, "expected pattern"); + return parserMakePattern(pkWildcard, line, col); +} + +func parserParsePattern(p: *Parser) -> *Pattern { + let locTok: LexToken = parserCurToken(p); + let line: uint32 = locTok.line; + let col: uint32 = locTok.column; + let left: *Pattern = parserParsePrimaryPattern(p); + // Range pattern: lo..hi or lo..=hi + if parserCheck(p, tkDotDot) || parserCheck(p, tkDotDotEqual) { + let inclusive: bool = parserCheck(p, tkDotDotEqual); + discard parserAdvance(p); + let right: *Pattern = parserParsePrimaryPattern(p); + let pat: *Pattern = parserMakePattern(pkRange, line, col); + pat.patRangeInclusive = inclusive; + pat.patChild1 = left; + pat.patChild2 = right; + return pat; + } + return left; +} + +// match subject { pat => body, ... } +func parserParseMatchExpr(p: *Parser) -> *Expr { + let line: uint32 = parserCurToken(p).line; + let col: uint32 = parserCurToken(p).column; + discard parserAdvance(p); // match + p.structInitAllowed = false; + let subject: *Expr = parserParseExpr(p); + p.structInitAllowed = true; + discard parserExpect(p, tkLBrace, "expected '{' to start match body"); + + var firstArm: *MatchArm = null as *MatchArm; + var lastArm: *MatchArm = null as *MatchArm; + var armCount: int = 0; + + while !parserCheck(p, tkRBrace) && parserPeek(p, 0) != tkEndOfFile { + while parserCheck(p, tkNewLine) { discard parserAdvance(p); } + if parserCheck(p, tkRBrace) || parserPeek(p, 0) == tkEndOfFile { break; } + let armLine: uint32 = parserCurToken(p).line; + let armCol: uint32 = parserCurToken(p).column; + let mp: int = p.pos; + let pat: *Pattern = parserParsePattern(p); + discard parserExpect(p, tkFatArrow, "expected '=>' in match arm"); + let body: *Expr = parserParseExpr(p); + let arm: *MatchArm = bux_alloc(sizeof(MatchArm)) as *MatchArm; + arm.line = armLine; + arm.column = armCol; + arm.pattern = pat; + arm.body = body; + arm.next = null as *MatchArm; + if firstArm == null as *MatchArm { + firstArm = arm; + lastArm = arm; + } else { + lastArm.next = arm; + lastArm = arm; + } + armCount = armCount + 1; + if parserCheck(p, tkComma) { discard parserAdvance(p); } + if p.pos == mp { discard parserAdvance(p); } + } + discard parserExpect(p, tkRBrace, "expected '}' to close match"); + + let e: *Expr = parserMakeExpr(ekMatch, line, col); + e.child1 = subject; + e.matchArms = firstArm; + e.matchArmCount = armCount; + return e; +} + // --------------------------------------------------------------------------- // Closure: |params| -> Ret { body } // --------------------------------------------------------------------------- @@ -1117,29 +1293,14 @@ func parserParseStmt(p: *Parser) -> *Stmt { return s; } - // match + // match — expression statement (arms fully parsed) if kind == tkMatch { - discard parserAdvance(p); - p.structInitAllowed = false; - let subject: *Expr = parserParseExpr(p); - p.structInitAllowed = true; - discard parserExpect(p, tkLBrace, "expected '{' to start match body"); - // Skip match body (simplified — just parse arms as empty) - while !parserCheck(p, tkRBrace) && parserPeek(p, 0) != tkEndOfFile { - if parserCheck(p, tkNewLine) { discard parserAdvance(p); continue; } - let mp: int = p.pos; - discard parserParseExpr(p); // pattern - if parserMatch(p, tkFatArrow) { - discard parserParseExpr(p); // body - } - if p.pos == mp { discard parserAdvance(p); } - } - discard parserExpect(p, tkRBrace, "expected '}' to close match"); + let matchExpr: *Expr = parserParseMatchExpr(p); let s: *Stmt = bux_alloc(sizeof(Stmt)) as *Stmt; - s.kind = skMatch; + s.kind = skExpr; s.line = line; s.column = col; - s.child1 = subject; + s.child1 = matchExpr; return s; } diff --git a/src/sema.bux b/src/sema.bux index 9755658..0781ec9 100644 --- a/src/sema.bux +++ b/src/sema.bux @@ -783,6 +783,46 @@ func Sema_CheckExpr(sema: *Sema, expr: *Expr) -> int { return tyVoid; } + // Match expression + if kind == ekMatch { + discard Sema_CheckExpr(sema, expr.child1); // subject + var armType: int = tyUnknown; + var first: bool = true; + var arm: *MatchArm = expr.matchArms; + while arm != null as *MatchArm { + let bt: int = Sema_CheckExpr(sema, arm.body); + if first { + armType = bt; + first = false; + } + arm = arm.next; + } + // Propagate type of first arm for codegen (result temp type) + if armType == tyStr { + let te: *TypeExpr = bux_alloc(sizeof(TypeExpr)) as *TypeExpr; + te.kind = tekNamed; + te.typeName = "String"; + expr.refType = te; + } else if armType == tyBool { + let te: *TypeExpr = bux_alloc(sizeof(TypeExpr)) as *TypeExpr; + te.kind = tekNamed; + te.typeName = "bool"; + expr.refType = te; + } else if armType == tyInt || armType == tyUnknown { + let te: *TypeExpr = bux_alloc(sizeof(TypeExpr)) as *TypeExpr; + te.kind = tekNamed; + te.typeName = "int"; + expr.refType = te; + if armType == tyUnknown { armType = tyInt; } + } else { + let te: *TypeExpr = bux_alloc(sizeof(TypeExpr)) as *TypeExpr; + te.kind = tekNamed; + te.typeName = "int"; + expr.refType = te; + } + return armType; + } + // Closure: |params| -> Ret { body } if kind == ekClosure { let savedRetType: int = sema.currentRetType; @@ -1052,7 +1092,7 @@ func Sema_CheckStmt(sema: *Sema, stmt: *Stmt) { return; } - // Match + // Match (legacy skMatch — prefer ekMatch via skExpr) if kind == skMatch { discard Sema_CheckExpr(sema, stmt.child1); return; diff --git a/tests/error_golden/double_mut_borrow/bux.toml b/tests/error_golden/double_mut_borrow/bux.toml new file mode 100644 index 0000000..4d50469 --- /dev/null +++ b/tests/error_golden/double_mut_borrow/bux.toml @@ -0,0 +1,7 @@ +[Package] +Name = "double_mut_borrow" +Version = "0.1.0" +Type = "bin" + +[Build] +Output = "Bin" diff --git a/tests/error_golden/double_mut_borrow/expected.err b/tests/error_golden/double_mut_borrow/expected.err new file mode 100644 index 0000000..556f93e --- /dev/null +++ b/tests/error_golden/double_mut_borrow/expected.err @@ -0,0 +1,6 @@ +error: type errors in project +error: mutable borrow conflict: arguments 1 and 2 both borrow '&mut x' + --> FILE:10:11 + | + 10 | TwoMut(&x, &x); + | ^ diff --git a/tests/error_golden/double_mut_borrow/src/Main.bux b/tests/error_golden/double_mut_borrow/src/Main.bux new file mode 100644 index 0000000..2a404e4 --- /dev/null +++ b/tests/error_golden/double_mut_borrow/src/Main.bux @@ -0,0 +1,12 @@ +@[Checked] +func TwoMut(a: &mut int, b: &mut int) { + *a = 1; + *b = 2; +} + +@[Checked] +func Main() -> int { + var x: int = 0; + TwoMut(&x, &x); + return 0; +} diff --git a/tests/error_golden/parse_error/bux.toml b/tests/error_golden/parse_error/bux.toml new file mode 100644 index 0000000..5e093e1 --- /dev/null +++ b/tests/error_golden/parse_error/bux.toml @@ -0,0 +1,7 @@ +[Package] +Name = "parse_error" +Version = "0.1.0" +Type = "bin" + +[Build] +Output = "Bin" diff --git a/tests/error_golden/parse_error/expected.err b/tests/error_golden/parse_error/expected.err new file mode 100644 index 0000000..4ca46eb --- /dev/null +++ b/tests/error_golden/parse_error/expected.err @@ -0,0 +1,7 @@ +error: parse errors in FILE +error: expected expression + --> FILE:2:13 + | + 2 | let x = ; + | ^ + = help: the previous statement may be incomplete (missing value or ';') diff --git a/tests/error_golden/parse_error/src/Main.bux b/tests/error_golden/parse_error/src/Main.bux new file mode 100644 index 0000000..2edf804 --- /dev/null +++ b/tests/error_golden/parse_error/src/Main.bux @@ -0,0 +1,4 @@ +func Main() -> int { + let x = ; + return 0; +} diff --git a/tests/error_golden/run.sh b/tests/error_golden/run.sh index 32dcdb3..b6a3896 100755 --- a/tests/error_golden/run.sh +++ b/tests/error_golden/run.sh @@ -16,10 +16,13 @@ passed=0 failed=0 normalize() { - # Replace absolute path prefix with FILE, drop trailing blank lines + # Replace absolute paths with FILE (location markers and "in " lines). + # Drop trailing blank lines. sed -E \ - -e "s|$DIR/[^:]+:|FILE:|g" \ - -e "s|$ROOT/[^:]+:|FILE:|g" \ + -e "s|$DIR/[^:[:space:]]+:|FILE:|g" \ + -e "s|$ROOT/[^:[:space:]]+:|FILE:|g" \ + -e "s|$DIR/[^:[:space:]]+|FILE|g" \ + -e "s|$ROOT/[^:[:space:]]+|FILE|g" \ -e "s|//+|/|g" \ | sed -e :a -e '/^\n*$/{$d;N;ba' -e '}' } diff --git a/tests/error_golden/use_after_move/bux.toml b/tests/error_golden/use_after_move/bux.toml new file mode 100644 index 0000000..ec94dd6 --- /dev/null +++ b/tests/error_golden/use_after_move/bux.toml @@ -0,0 +1,7 @@ +[Package] +Name = "use_after_move" +Version = "0.1.0" +Type = "bin" + +[Build] +Output = "Bin" diff --git a/tests/error_golden/use_after_move/expected.err b/tests/error_golden/use_after_move/expected.err new file mode 100644 index 0000000..edfee7b --- /dev/null +++ b/tests/error_golden/use_after_move/expected.err @@ -0,0 +1,7 @@ +error: type errors in project +error: use of moved value 'a' + --> FILE:10:25 + | + 10 | let c: own String = a; + | ^ + = help: the value was moved; clone it or restructure ownership diff --git a/tests/error_golden/use_after_move/src/Main.bux b/tests/error_golden/use_after_move/src/Main.bux new file mode 100644 index 0000000..c4f6d53 --- /dev/null +++ b/tests/error_golden/use_after_move/src/Main.bux @@ -0,0 +1,12 @@ +@[Checked] +func Take(s: own String) -> own String { + return s; +} + +@[Checked] +func Main() -> int { + let a: own String = "hello"; + let b: own String = Take(a); + let c: own String = a; + return 0; +}