fix: C backend, parser, sema, HIR lowering for all examples
- C backend: strip c8/c16/c32 string prefixes in emitted C - C backend: resolve imports to fully-qualified names (Std_Io_PrintLine) - Parser: fix infinite loop on < in comparisons vs generic calls - Parser: fix enum patterns without parens (Option::None) - Parser: fix match expressions inside blocks - Sema: extract pattern bindings for match arms - HIR: lower match expressions to if-else chains with enum tag checks - HIR/C backend: support block expressions as function return values - Makefile: add integration tests for all 9 examples - Add pattern_matching.bux example
This commit is contained in:
+10
@@ -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/
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
+16
-1
@@ -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,6 +348,13 @@ proc emitFunc*(be: var CBackend, hfunc: HirFunc) =
|
||||
be.emitLine(&"{retType} {hfunc.name}({paramsStr}) {{")
|
||||
inc be.indent
|
||||
if hfunc.body != nil:
|
||||
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("}")
|
||||
|
||||
+123
-6
@@ -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:
|
||||
|
||||
+34
-3
@@ -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<int>(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
|
||||
|
||||
+26
-1
@@ -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:
|
||||
|
||||
Reference in New Issue
Block a user