feat: full match expressions in bootstrap and selfhost

Lower match to if-else for literals, ranges, enum tags, and wildcards.
Fix bootstrap literal arms that always matched; port real arm AST/parser
and Lcx_LowerMatch to selfhost with last-expression return. Expand
pattern_matching example and add parse/use-after-move/double-mut golden
diagnostics. Selfhost-loop remains binary-identical.
This commit is contained in:
2026-07-16 16:19:43 +03:00
parent 2cbbccc508
commit f619316470
18 changed files with 786 additions and 110 deletions
+2 -1
View File
@@ -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"
+102 -67
View File
@@ -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)
+34 -10
View File
@@ -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)
+38 -5
View File
@@ -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;
}
+15 -1
View File
@@ -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,
}
// ---------------------------------------------------------------------------
+299 -3
View File
@@ -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;
}
+180 -19
View File
@@ -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;
}
+41 -1
View File
@@ -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;
@@ -0,0 +1,7 @@
[Package]
Name = "double_mut_borrow"
Version = "0.1.0"
Type = "bin"
[Build]
Output = "Bin"
@@ -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);
| ^
@@ -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;
}
+7
View File
@@ -0,0 +1,7 @@
[Package]
Name = "parse_error"
Version = "0.1.0"
Type = "bin"
[Build]
Output = "Bin"
@@ -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 ';')
@@ -0,0 +1,4 @@
func Main() -> int {
let x = ;
return 0;
}
+6 -3
View File
@@ -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 <path>" 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 '}'
}
@@ -0,0 +1,7 @@
[Package]
Name = "use_after_move"
Version = "0.1.0"
Type = "bin"
[Build]
Output = "Bin"
@@ -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
@@ -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;
}