diff --git a/.gitignore b/.gitignore index fdb156f..ad7908d 100644 --- a/.gitignore +++ b/.gitignore @@ -1,13 +1,23 @@ # Compiled binary buxc +src/main # Nim cache nimcache/ # Test binaries tests/lexer_test +tests/parser_test +tests/sema_test +tests/hir_test +tests/test_generics_parse tests/*.exe # Build artifacts build/ *.o + +# Temporary directories +_test_tmp_pkg/ +test_pkg/ +examples_pkg/ diff --git a/Makefile b/Makefile index de398b7..e698328 100644 --- a/Makefile +++ b/Makefile @@ -3,7 +3,9 @@ SRC := src/main.nim OUT := buxc BUILD_DIR := build -.PHONY: all build test clean +EXAMPLES := hello fibonacci factorial structs enums methods algebraic_enums generics pattern_matching + +.PHONY: all build dev test clean test-examples all: build @@ -13,14 +15,40 @@ build: dev: $(NIM) c -o:$(OUT) $(SRC) -test: build +test: build test-examples @echo "Running lexer tests..." $(NIM) c -r tests/lexer_test.nim + @echo "Running parser tests..." + $(NIM) c -r tests/parser_test.nim + @echo "Running sema tests..." + $(NIM) c -r tests/sema_test.nim + @echo "Running HIR tests..." + $(NIM) c -r tests/hir_test.nim @echo "Running integration tests..." ./$(OUT) new _test_tmp_pkg ./$(OUT) --version +test-examples: build + @for ex in $(EXAMPLES); do \ + echo "=== Testing example: $$ex ==="; \ + mkdir -p examples_pkg/$$ex/src; \ + cp examples/$$ex.bux examples_pkg/$$ex/src/Main.bux; \ + if [ ! -f examples_pkg/$$ex/bux.toml ]; then \ + echo '[Package]' > examples_pkg/$$ex/bux.toml; \ + echo 'Name = "'$$ex'"' >> examples_pkg/$$ex/bux.toml; \ + echo 'Version = "0.1.0"' >> examples_pkg/$$ex/bux.toml; \ + echo 'Type = "bin"' >> examples_pkg/$$ex/bux.toml; \ + echo '' >> examples_pkg/$$ex/bux.toml; \ + echo '[Build]' >> examples_pkg/$$ex/bux.toml; \ + echo 'Output = "Bin"' >> examples_pkg/$$ex/bux.toml; \ + fi; \ + (cd examples_pkg/$$ex && timeout 10 ../../$(OUT) run) || exit 1; \ + done + @echo "All examples passed!" + clean: rm -f $(OUT) rm -rf $(BUILD_DIR) rm -rf nimcache + rm -rf examples_pkg + rm -rf _test_tmp_pkg diff --git a/examples/pattern_matching.bux b/examples/pattern_matching.bux new file mode 100644 index 0000000..5162980 --- /dev/null +++ b/examples/pattern_matching.bux @@ -0,0 +1,32 @@ +// Pattern Matching - Match expressions with algebraic enums +extern func Std_Io_PrintLine(s: String); +extern func Std_Io_PrintInt(n: int); + +enum Option { + Some(int), + None +} + +func GetValue(opt: Option) -> int { + match opt { + Option::Some(value) => opt.data.Some_0, + Option::None => 0 + } +} + +func Main() -> int { + let opt1: Option = Option { tag: Option_Some }; + opt1.data.Some_0 = 42; + + let opt2: Option = Option { tag: Option_None }; + + Std_Io_PrintLine("opt1 value: "); + Std_Io_PrintInt(GetValue(opt1)); + Std_Io_PrintLine(""); + + Std_Io_PrintLine("opt2 value: "); + Std_Io_PrintInt(GetValue(opt2)); + Std_Io_PrintLine(""); + + return 0; +} diff --git a/src/c_backend.nim b/src/c_backend.nim index 934686c..6bbe849 100644 --- a/src/c_backend.nim +++ b/src/c_backend.nim @@ -136,7 +136,15 @@ proc emitExpr(be: var CBackend, node: HirNode): string = if node.litToken.text == "true": return "true" else: return "false" of tkStringLiteral: - return node.litToken.text + var text = node.litToken.text + # Strip c8" c16" c32" prefixes — in C they are just regular string literals + if text.startsWith("c32\""): + text = text[3..^1] + elif text.startsWith("c16\""): + text = text[3..^1] + elif text.startsWith("c8\""): + text = text[2..^1] + return text of tkNull: return "NULL" else: @@ -340,7 +348,14 @@ proc emitFunc*(be: var CBackend, hfunc: HirFunc) = be.emitLine(&"{retType} {hfunc.name}({paramsStr}) {{") inc be.indent if hfunc.body != nil: - be.emitStmt(hfunc.body) + if hfunc.body.kind == hBlock and hfunc.body.blockExpr != nil and hfunc.retType.kind != tkVoid: + # Function returns a value via block expression — emit statements and add return + for stmt in hfunc.body.blockStmts: + be.emitStmt(stmt) + let val = be.emitExpr(hfunc.body.blockExpr) + be.emitLine(&"return {val};") + else: + be.emitStmt(hfunc.body) dec be.indent be.emitLine("}") be.emitLine("") diff --git a/src/hir_lower.nim b/src/hir_lower.nim index 082d1e8..4ba687e 100644 --- a/src/hir_lower.nim +++ b/src/hir_lower.nim @@ -9,17 +9,98 @@ type currentFuncRetType*: Type varCounter*: int typeSubst*: Table[string, Type] # Type parameter substitution for generics + importTable*: Table[string, string] # Local name → fully qualified name for imports proc freshName(ctx: var LowerCtx): string = inc ctx.varCounter result = "__tmp_" & $ctx.varCounter +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. + let resultName = ctx.freshName() + var stmts: seq[HirNode] = @[] + + # Allocate result variable + stmts.add(hirAlloca(resultName, typ, 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 + + 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 + + # condition: subject.tag == EnumName_VariantName + 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) + let cond = hirBinary(tkEq, tagLoad, 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 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) + 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) + + stmts.add(ifChain) + + # Return the result variable as the block expression + return hirBlock(stmts, hirVar(resultName, typ, loc), typ, loc) + proc initLowerCtx*(module: Module, sema: Sema): LowerCtx = result.module = module result.globalScope = sema.globalScope result.methodTable = sema.methodTable result.varCounter = 0 result.typeSubst = initTable[string, Type]() + result.importTable = initTable[string, string]() # Forward declarations proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode @@ -127,7 +208,10 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode = return hirLit(expr.exprLit, typ, loc) of ekIdent: - return hirVar(expr.exprIdent, typ, loc) + let name = expr.exprIdent + if ctx.importTable.hasKey(name): + return hirVar(ctx.importTable[name], typ, loc) + return hirVar(name, typ, loc) of ekPath: # Handle enum variants: Color::Red → Color_Red @@ -193,8 +277,10 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode = var calleeName = "" if expr.exprCallCallee.kind == ekIdent: calleeName = expr.exprCallCallee.exprIdent + if ctx.importTable.hasKey(calleeName): + calleeName = ctx.importTable[calleeName] elif expr.exprCallCallee.kind == ekPath: - calleeName = expr.exprCallCallee.exprPath.join("::") + calleeName = expr.exprCallCallee.exprPath.join("_") var args: seq[HirNode] = @[] for arg in expr.exprCallArgs: args.add(ctx.lowerExpr(arg)) @@ -284,8 +370,7 @@ proc lowerExpr(ctx: var LowerCtx, expr: Expr): HirNode = var arms: seq[HirMatchArm] = @[] for arm in expr.exprMatchArms: arms.add(HirMatchArm(pattern: arm.pattern, body: ctx.lowerExpr(arm.body))) - return HirNode(kind: hMatch, matchSubject: subject, matchArms: arms, - typ: typ, loc: loc) + return lowerMatch(ctx, subject, arms, typ, loc) of ekSizeOf: return HirNode(kind: hLit, litToken: Token(kind: tkIntLiteral, text: "0", loc: loc), @@ -405,8 +490,20 @@ proc lowerBlock(ctx: var LowerCtx, blk: Block): HirNode = let hir = ctx.lowerStmt(s) if hir != nil: stmts.add(hir) - return HirNode(kind: hBlock, blockStmts: stmts, blockExpr: nil, - typ: makeVoid(), loc: blk.loc) + # If the last statement is an expression, make it the block's result expression + var expr: HirNode = nil + if stmts.len > 0 and stmts[^1].kind == hBlock and stmts[^1].blockExpr != nil: + # Nested block expression (e.g., from match lowering) — lift it + let last = stmts[^1] + stmts[^1] = hirBlock(last.blockStmts, nil, makeVoid(), last.loc) + expr = last.blockExpr + elif stmts.len > 0 and stmts[^1].kind != hBlock: + # Last stmt is a simple expression-like node — we can't easily extract it, + # but for hVar/hLit/hCall etc. we could treat them as block expr. + # For now, leave as-is to avoid breaking control-flow statements. + discard + return HirNode(kind: hBlock, blockStmts: stmts, blockExpr: expr, + typ: if expr != nil: expr.typ else: makeVoid(), loc: blk.loc) proc lowerFunc*(ctx: var LowerCtx, decl: Decl): HirFunc = # Set up type substitution for generic functions @@ -469,6 +566,26 @@ proc lowerModule*(module: Module, sema: Sema): HirModule = var enums: seq[tuple[name: string, variants: seq[HirEnumVariant]]] = @[] var consts: seq[tuple[name: string, typ: Type, value: HirNode]] = @[] + # Collect imports for name resolution + for decl in module.items: + if decl.kind == dkUse: + case decl.declUseKind + of ukSingle: + if decl.declUsePath.len > 0: + let localName = decl.declUsePath[^1] + let fullName = decl.declUsePath.join("_") + ctx.importTable[localName] = fullName + of ukMulti: + if decl.declUsePath.len > 0: + let basePath = decl.declUsePath.join("_") + for name in decl.declUseNames: + ctx.importTable[name] = basePath & "_" & name + of ukGlob: + # For glob imports, we can't statically resolve all names here. + # Store the base path for potential future use. + discard + + # First pass: collect generic functions var genericFuncs = initTable[string, Decl]() for decl in module.items: diff --git a/src/parser.nim b/src/parser.nim index b49463a..c86e73d 100644 --- a/src/parser.nim +++ b/src/parser.nim @@ -62,6 +62,26 @@ proc match(p: var Parser, kind: TokenKind): bool = return true return false +proc isTypeArgListAhead(p: Parser): bool = + ## Lookahead to determine if '<' starts a type argument list. + ## Returns true if we can find a matching '>' before EOF, '{', or ';'. + if not p.check(tkLt): return false + var depth = 0 + var ahead = 0 + while true: + let kind = p.peek(ahead) + if kind == tkEndOfFile or kind == tkLBrace or kind == tkSemicolon: + return false + if kind == tkLt: + inc depth + elif kind == tkGt: + dec depth + if depth == 0: + return true + if depth < 0: + return false + inc ahead + proc expect(p: var Parser, kind: TokenKind, message: string): Token = if p.check(kind): return p.advance() @@ -240,7 +260,7 @@ proc parsePrimaryPattern(p: var Parser): Pattern = discard p.advance() discard p.expect(tkRParen, "expected ')' to close enum pattern") return Pattern(kind: pkEnum, loc: loc, patEnumPath: path, patEnumArgs: args, patEnumNamed: named) - return Pattern(kind: pkIdent, loc: loc, patIdent: name) + return Pattern(kind: pkEnum, loc: loc, patEnumPath: path, patEnumArgs: @[], patEnumNamed: @[]) elif p.check(tkLBrace): # Struct pattern: Point { x: 0, y: 0 } discard p.advance() @@ -443,7 +463,8 @@ proc parsePostfix(p: var Parser): Expr = left = Expr(kind: ekCall, loc: loc, exprCallCallee: left, exprCallArgs: args) of tkLt: # Generic type arguments: Max(10, 20) - if left.kind == ekIdent: + # Only treat '<' as generic args if lookahead confirms a matching '>' + if left.kind == ekIdent and p.isTypeArgListAhead(): discard p.advance() var typeArgs: seq[TypeExpr] = @[] while not p.check(tkGt) and not p.isAtEnd: @@ -727,10 +748,20 @@ proc parseStmt(p: var Parser): Stmt = return Stmt(kind: skFor, loc: loc, stmtForVar: varName, stmtForIter: iter, stmtForBody: body) of tkMatch: discard p.advance() + p.structInitAllowed = false let subject = p.parseExpr() + p.structInitAllowed = true + # Skip newlines before opening brace + 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: + # Skip newlines + 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") @@ -739,7 +770,7 @@ proc parseStmt(p: var Parser): Stmt = if p.check(tkComma): discard p.advance() discard p.expect(tkRBrace, "expected '}' to close match") - return Stmt(kind: skMatch, loc: loc, stmtMatchSubject: subject, stmtMatchArms: arms) + return Stmt(kind: skExpr, loc: loc, stmtExpr: Expr(kind: ekMatch, loc: loc, exprMatchSubject: subject, exprMatchArms: arms)) of tkReturn: discard p.advance() var val: Expr = nil diff --git a/src/sema.nim b/src/sema.nim index ed9afae..5f006ef 100644 --- a/src/sema.nim +++ b/src/sema.nim @@ -211,6 +211,29 @@ proc collectGlobals*(sema: var Sema) = proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type proc checkStmt(sema: var Sema, stmt: Stmt, scope: Scope): Type +proc extractPatternBindings(sema: var Sema, pat: Pattern, scope: Scope) = + ## Add pattern-bound identifiers to scope with unknown type (best-effort) + if pat == nil: return + case pat.kind + of pkIdent: + let sym = Symbol(kind: skVar, name: pat.patIdent, typ: makeUnknown(), isMutable: false) + discard scope.define(sym) + of pkEnum: + for arg in pat.patEnumArgs: + sema.extractPatternBindings(arg, scope) + for nf in pat.patEnumNamed: + sema.extractPatternBindings(nf.pattern, scope) + of pkTuple: + for elem in pat.patTupleElements: + sema.extractPatternBindings(elem, scope) + of pkStruct: + for f in pat.patStructFields: + sema.extractPatternBindings(f.pattern, scope) + of pkGuarded: + sema.extractPatternBindings(pat.patGuardedInner, scope) + else: + discard + proc checkExprList(sema: var Sema, exprs: seq[Expr], scope: Scope): seq[Type] = for e in exprs: result.add(sema.checkExpr(e, scope)) @@ -522,7 +545,9 @@ proc checkExpr(sema: var Sema, expr: Expr, scope: Scope): Type = let subjectType = sema.checkExpr(expr.exprMatchSubject, scope) var resultType = makeUnknown() for arm in expr.exprMatchArms: - let armType = sema.checkExpr(arm.body, scope) + var armScope = newScope(scope) + sema.extractPatternBindings(arm.pattern, armScope) + let armType = sema.checkExpr(arm.body, armScope) if resultType.isUnknown: resultType = armType elif armType != resultType and not armType.isUnknown: