feat: parser + AST (Phase 1)

This commit is contained in:
2026-05-30 21:19:43 +03:00
parent 1b708ec755
commit 713ab8e4d6
6 changed files with 1598 additions and 7 deletions
+388
View File
@@ -0,0 +1,388 @@
import source_location, token
type
CallingConvention* = enum
ccDefault
ccWin64
IntrinsicKind* = enum
ikLine
ikColumn
ikFile
ikFunction
ikDate
ikTime
ikModule
UseKind* = enum
ukSingle
ukGlob
ukMulti
# ---------------------------------------------------------------------------
# Type expressions
# ---------------------------------------------------------------------------
TypeExprKind* = enum
tekNamed
tekPath
tekSlice
tekPointer
tekTuple
tekSelf
TypeExpr* = ref object
loc*: SourceLocation
case kind*: TypeExprKind
of tekNamed:
typeName*: string
typeArgs*: seq[TypeExpr]
of tekPath:
pathSegments*: seq[string]
of tekSlice:
sliceElement*: TypeExpr
sliceSize*: Expr ## nil for unsized slices T[]
of tekPointer:
pointerPointee*: TypeExpr
of tekTuple:
tupleElements*: seq[TypeExpr]
of tekSelf:
discard
# ---------------------------------------------------------------------------
# Patterns
# ---------------------------------------------------------------------------
PatternKind* = enum
pkWildcard
pkLiteral
pkIdent
pkRange
pkEnum
pkStruct
pkTuple
pkGuarded
Pattern* = ref object
loc*: SourceLocation
case kind*: PatternKind
of pkWildcard:
discard
of pkLiteral:
patLit*: Token
of pkIdent:
patIdent*: string
of pkRange:
patRangeLo*: Pattern
patRangeHi*: Pattern
patRangeInclusive*: bool
of pkEnum:
patEnumPath*: seq[string]
patEnumArgs*: seq[Pattern]
patEnumNamed*: seq[tuple[name: string, pattern: Pattern]]
of pkStruct:
patStructName*: string
patStructFields*: seq[tuple[name: string, pattern: Pattern]]
of pkTuple:
patTupleElements*: seq[Pattern]
of pkGuarded:
patGuardedInner*: Pattern
patGuardedExpr*: Expr
# ---------------------------------------------------------------------------
# Expressions
# ---------------------------------------------------------------------------
ExprKind* = enum
ekLiteral
ekIdent
ekSelf
ekPath
ekSizeOf
ekIntrinsic
ekUnary
ekPostfix
ekBinary
ekAssign
ekTernary
ekRange
ekCall
ekIndex
ekField
ekStructInit
ekSlice
ekSpread
ekTuple
ekCast
ekIs
ekBlock
ekMatch
MatchArm* = object
loc*: SourceLocation
pattern*: Pattern
body*: Expr
Expr* = ref object
loc*: SourceLocation
case kind*: ExprKind
of ekLiteral:
exprLit*: Token
of ekIdent:
exprIdent*: string
of ekSelf:
discard
of ekPath:
exprPath*: seq[string]
of ekSizeOf:
exprSizeOfType*: TypeExpr
of ekIntrinsic:
exprIntrinsic*: IntrinsicKind
of ekUnary:
exprUnaryOp*: TokenKind
exprUnaryOperand*: Expr
of ekPostfix:
exprPostfixOp*: TokenKind
exprPostfixOperand*: Expr
of ekBinary:
exprBinaryOp*: TokenKind
exprBinaryLeft*: Expr
exprBinaryRight*: Expr
of ekAssign:
exprAssignOp*: TokenKind
exprAssignTarget*: Expr
exprAssignValue*: Expr
of ekTernary:
exprTernaryCond*: Expr
exprTernaryThen*: Expr
exprTernaryElse*: Expr
of ekRange:
exprRangeLo*: Expr
exprRangeHi*: Expr
exprRangeInclusive*: bool
of ekCall:
exprCallCallee*: Expr
exprCallArgs*: seq[Expr]
of ekIndex:
exprIndexObj*: Expr
exprIndexIdx*: Expr
of ekField:
exprFieldObj*: Expr
exprFieldName*: string
of ekStructInit:
exprStructInitName*: string
exprStructInitTypeArgs*: seq[TypeExpr]
exprStructInitFields*: seq[tuple[name: string, value: Expr]]
of ekSlice:
exprSliceElements*: seq[Expr]
of ekSpread:
exprSpreadOperand*: Expr
of ekTuple:
exprTupleElements*: seq[Expr]
of ekCast:
exprCastOperand*: Expr
exprCastType*: TypeExpr
of ekIs:
exprIsOperand*: Expr
exprIsType*: TypeExpr
of ekBlock:
exprBlock*: Block
of ekMatch:
exprMatchSubject*: Expr
exprMatchArms*: seq[MatchArm]
# ---------------------------------------------------------------------------
# Statements
# ---------------------------------------------------------------------------
StmtKind* = enum
skExpr
skLet
skIf
skWhile
skDoWhile
skLoop
skFor
skMatch
skReturn
skBreak
skContinue
skDecl
ElseIf* = object
loc*: SourceLocation
cond*: Expr
blk*: Block
Block* = ref object
loc*: SourceLocation
stmts*: seq[Stmt]
Stmt* = ref object
loc*: SourceLocation
case kind*: StmtKind
of skExpr:
stmtExpr*: Expr
of skLet:
stmtLetMut*: bool
stmtLetName*: string
stmtLetPattern*: Pattern
stmtLetType*: TypeExpr ## nil if inferred
stmtLetInit*: Expr
of skIf:
stmtIfCond*: Expr
stmtIfThen*: Block
stmtIfElseIfs*: seq[ElseIf]
stmtIfElse*: Block ## nil if no else
of skWhile:
stmtWhileLabel*: string
stmtWhileCond*: Expr
stmtWhileBody*: Block
of skDoWhile:
stmtDoWhileLabel*: string
stmtDoWhileBody*: Block
stmtDoWhileCond*: Expr
of skLoop:
stmtLoopLabel*: string
stmtLoopBody*: Block
of skFor:
stmtForLabel*: string
stmtForVar*: string
stmtForIter*: Expr
stmtForBody*: Block
of skMatch:
stmtMatchSubject*: Expr
stmtMatchArms*: seq[MatchArm]
of skReturn:
stmtReturnValue*: Expr ## nil for bare return
of skBreak:
stmtBreakLabel*: string
of skContinue:
stmtContinueLabel*: string
of skDecl:
stmtDecl*: Decl
# ---------------------------------------------------------------------------
# Declarations
# ---------------------------------------------------------------------------
DeclKind* = enum
dkFunc
dkStruct
dkEnum
dkUnion
dkInterface
dkImpl
dkModule
dkUse
dkConst
dkTypeAlias
dkExternFunc
dkExternVar
dkExternBlock
Param* = object
loc*: SourceLocation
name*: string
ptype*: TypeExpr
isVariadic*: bool
defaultValue*: Expr
StructField* = object
loc*: SourceLocation
isPublic*: bool
name*: string
ftype*: TypeExpr
EnumVariant* = object
loc*: SourceLocation
name*: string
fields*: seq[TypeExpr]
namedFields*: seq[tuple[name: string, ftype: TypeExpr]]
discriminant*: string
UnionField* = object
loc*: SourceLocation
name*: string
ftype*: TypeExpr
Decl* = ref object
loc*: SourceLocation
isPublic*: bool
case kind*: DeclKind
of dkFunc:
declFuncAsm*: bool
declFuncCallConv*: CallingConvention
declFuncName*: string
declFuncTypeParams*: seq[string]
declFuncParams*: seq[Param]
declFuncReturnType*: TypeExpr ## nil if void/inferred
declFuncBody*: Block ## nil for signature-only
of dkStruct:
declStructName*: string
declStructTypeParams*: seq[string]
declStructFields*: seq[StructField]
of dkEnum:
declEnumName*: string
declEnumBaseType*: TypeExpr
declEnumVariants*: seq[EnumVariant]
of dkUnion:
declUnionName*: string
declUnionFields*: seq[UnionField]
of dkInterface:
declInterfaceName*: string
declInterfaceMethods*: seq[Decl] ## FuncDecl signatures only
of dkImpl:
declImplTypeName*: string
declImplInterface*: string ## empty if not for interface
declImplMethods*: seq[Decl]
of dkModule:
declModuleName*: string
declModuleItems*: seq[Decl]
of dkUse:
declUsePath*: seq[string]
declUseKind*: UseKind
declUseNames*: seq[string] ## for multi-import
declUseTargetOs*: string
of dkConst:
declConstName*: string
declConstType*: TypeExpr
declConstValue*: Expr
of dkTypeAlias:
declAliasName*: string
declAliasType*: TypeExpr
of dkExternFunc:
declExtFuncName*: string
declExtFuncDll*: string
declExtFuncCallConv*: CallingConvention
declExtFuncParams*: seq[Param]
declExtFuncVariadic*: bool
declExtFuncReturnType*: TypeExpr
of dkExternVar:
declExtVarName*: string
declExtVarType*: TypeExpr
of dkExternBlock:
declExtBlockDll*: string
declExtBlockCallConv*: CallingConvention
declExtBlockItems*: seq[Decl]
# ---------------------------------------------------------------------------
# Module (AST root)
# ---------------------------------------------------------------------------
Module* = ref object
name*: string
items*: seq[Decl]
# Convenience constructors
proc newModule*(name: string): Module =
result = Module(name: name)
proc newBlock*(loc: SourceLocation): Block =
result = Block(loc: loc)
proc newLiteralExpr*(tok: Token): Expr =
result = Expr(kind: ekLiteral, loc: tok.loc, exprLit: tok)
proc newIdentExpr*(name: string, loc: SourceLocation): Expr =
result = Expr(kind: ekIdent, loc: loc, exprIdent: name)
proc newBinaryExpr*(op: TokenKind, left, right: Expr, loc: SourceLocation): Expr =
result = Expr(kind: ekBinary, loc: loc, exprBinaryOp: op, exprBinaryLeft: left, exprBinaryRight: right)
proc newUnaryExpr*(op: TokenKind, operand: Expr, loc: SourceLocation): Expr =
result = Expr(kind: ekUnary, loc: loc, exprUnaryOp: op, exprUnaryOperand: operand)
+12 -6
View File
@@ -1,5 +1,5 @@
import std/[os, strutils, terminal, strformat] import std/[os, strutils, terminal, strformat]
import lexer, manifest import lexer, parser, manifest
type type
ColorMode* = enum ColorMode* = enum
@@ -158,14 +158,20 @@ proc cmdCheck*(args: seq[string], opts: GlobalOptions): int =
for kind, path in walkDir(srcDir): for kind, path in walkDir(srcDir):
if kind == pcFile and path.endsWith(".bux"): if kind == pcFile and path.endsWith(".bux"):
let source = readFile(path) let source = readFile(path)
let res = tokenize(source, path) let lexRes = tokenize(source, path)
if res.hasErrors: if lexRes.hasErrors:
printError(&"lex errors in {path}", useColor) printError(&"lex errors in {path}", useColor)
for d in res.diagnostics: for d in lexRes.diagnostics:
echo $d echo $d
return 1 return 1
let parseRes = parse(lexRes.tokens, path)
if parseRes.diagnostics.len > 0:
printError(&"parse errors in {path}", useColor)
for d in parseRes.diagnostics:
echo &"error: {d.message} at {d.loc}"
return 1
if opts.verbose: if opts.verbose:
printInfo(&"lexed {path} ({res.tokens.len} tokens)", useColor) printInfo(&"parsed {path} ({lexRes.tokens.len} tokens, {parseRes.module.items.len} top-level declarations)", useColor)
if splitFile(path).name == "Main": if splitFile(path).name == "Main":
foundMain = true foundMain = true
@@ -174,7 +180,7 @@ proc cmdCheck*(args: seq[string], opts: GlobalOptions): int =
return 1 return 1
if not opts.quiet: if not opts.quiet:
printInfo("check passed (lexer only)", useColor) printInfo("check passed", useColor)
return 0 return 0
proc cmdBuild*(args: seq[string], opts: GlobalOptions): int = proc cmdBuild*(args: seq[string], opts: GlobalOptions): int =
+3 -1
View File
@@ -142,7 +142,9 @@ proc scanIdent(lex: var Lexer, startLoc: SourceLocation): Token =
while not lex.isAtEnd() and isIdentChar(lex.peek()): while not lex.isAtEnd() and isIdentChar(lex.peek()):
discard lex.advance() discard lex.advance()
let text = lex.source[startPos ..< lex.pos] let text = lex.source[startPos ..< lex.pos]
let kind = keywordKind(text) var kind = keywordKind(text)
if text == "_":
kind = tkUnderscore
result = Token(kind: kind, text: text, loc: startLoc) result = Token(kind: kind, text: text, loc: startLoc)
# --------------------------------------------------------------------------- # ---------------------------------------------------------------------------
+1068
View File
File diff suppressed because it is too large Load Diff
+5
View File
@@ -11,6 +11,7 @@ type
##Identifiers ##Identifiers
tkIdent # foo Bar _x tkIdent # foo Bar _x
tkUnderscore # _
##Control flow keywords ##Control flow keywords
tkIf # if tkIf # if
@@ -47,6 +48,7 @@ type
tkNull # null tkNull # null
tkSelf # self tkSelf # self
tkSuper # super tkSuper # super
tkSizeOf # sizeof
##Punctuation ##Punctuation
tkLParen # ( tkLParen # (
@@ -191,6 +193,7 @@ proc keywordKind*(text: string): TokenKind =
of "null": tkNull of "null": tkNull
of "self": tkSelf of "self": tkSelf
of "super": tkSuper of "super": tkSuper
of "sizeof": tkSizeOf
of "true", "false": tkBoolLiteral of "true", "false": tkBoolLiteral
else: tkIdent else: tkIdent
@@ -202,6 +205,8 @@ proc tokenKindName*(kind: TokenKind): string =
of tkCharLiteral: "char literal" of tkCharLiteral: "char literal"
of tkBoolLiteral: "boolean literal" of tkBoolLiteral: "boolean literal"
of tkIdent: "identifier" of tkIdent: "identifier"
of tkUnderscore: "'_'"
of tkSizeOf: "'sizeof'"
of tkIf: "'if'" of tkIf: "'if'"
of tkElse: "'else'" of tkElse: "'else'"
of tkWhile: "'while'" of tkWhile: "'while'"
+122
View File
@@ -0,0 +1,122 @@
import std/[unittest, os, strutils]
import ../src/[lexer, parser, ast, token]
proc parseSource(source: string): ParseResult =
let lexRes = tokenize(source, "<test>")
check not lexRes.hasErrors
result = parse(lexRes.tokens, "<test>")
suite "Parser":
test "empty module":
let res = parseSource("")
check res.diagnostics.len == 0
check res.module.items.len == 0
test "simple function":
let res = parseSource("func Main() -> int { return 0; }")
check res.diagnostics.len == 0
check res.module.items.len == 1
check res.module.items[0].kind == dkFunc
check res.module.items[0].declFuncName == "Main"
test "function with parameters":
let res = parseSource("func Add(a: int32, b: int32) -> int32 { return a + b; }")
check res.diagnostics.len == 0
check res.module.items[0].declFuncParams.len == 2
check res.module.items[0].declFuncParams[0].name == "a"
check res.module.items[0].declFuncParams[1].name == "b"
test "struct declaration":
let res = parseSource("struct Point { x: float64; y: float64; }")
check res.diagnostics.len == 0
check res.module.items[0].kind == dkStruct
check res.module.items[0].declStructName == "Point"
check res.module.items[0].declStructFields.len == 2
test "enum declaration":
let res = parseSource("enum Color { Red, Green, Blue }")
check res.diagnostics.len == 0
check res.module.items[0].kind == dkEnum
check res.module.items[0].declEnumName == "Color"
check res.module.items[0].declEnumVariants.len == 3
test "import declaration":
let res = parseSource("import Std::Io::PrintLine;")
check res.diagnostics.len == 0
check res.module.items[0].kind == dkUse
check res.module.items[0].declUsePath == @["Std", "Io", "PrintLine"]
test "const declaration":
let res = parseSource("const PI: float64 = 3.14;")
check res.diagnostics.len == 0
check res.module.items[0].kind == dkConst
check res.module.items[0].declConstName == "PI"
test "type alias":
let res = parseSource("type MyInt = int32;")
check res.diagnostics.len == 0
check res.module.items[0].kind == dkTypeAlias
check res.module.items[0].declAliasName == "MyInt"
test "let statement in function":
let res = parseSource("func Main() -> int { let x: int32 = 42; return x; }")
check res.diagnostics.len == 0
check res.module.items[0].declFuncBody.stmts.len == 2
check res.module.items[0].declFuncBody.stmts[0].kind == skLet
check res.module.items[0].declFuncBody.stmts[0].stmtLetName == "x"
test "if statement":
let res = parseSource("func Main() -> int { if true { return 1; } else { return 0; } }")
check res.diagnostics.len == 0
let body = res.module.items[0].declFuncBody
check body.stmts[0].kind == skIf
check body.stmts[0].stmtIfThen.stmts.len == 1
check body.stmts[0].stmtIfElse.stmts.len == 1
test "while loop":
let res = parseSource("func Main() -> int { while true { break; } return 0; }")
check res.diagnostics.len == 0
let body = res.module.items[0].declFuncBody
check body.stmts[0].kind == skWhile
check body.stmts[0].stmtWhileBody.stmts[0].kind == skBreak
test "for loop":
let res = parseSource("func Main() -> int { for i in items { continue; } return 0; }")
check res.diagnostics.len == 0
let body = res.module.items[0].declFuncBody
check body.stmts[0].kind == skFor
check body.stmts[0].stmtForVar == "i"
test "match expression":
let res = parseSource("func Main() -> int { let x = match 1 { 1 => 10, _ => 20 }; return x; }")
check res.diagnostics.len == 0
let body = res.module.items[0].declFuncBody
check body.stmts[0].kind == skLet
check body.stmts[0].stmtLetInit.kind == ekMatch
test "binary expression":
let res = parseSource("func Main() -> int { return 1 + 2 * 3; }")
check res.diagnostics.len == 0
let ret = res.module.items[0].declFuncBody.stmts[0]
check ret.kind == skReturn
check ret.stmtReturnValue.kind == ekBinary
# 1 + (2 * 3)
check ret.stmtReturnValue.exprBinaryOp == tkPlus
test "call expression":
let res = parseSource("func Main() -> int { return Add(10, 20); }")
check res.diagnostics.len == 0
let ret = res.module.items[0].declFuncBody.stmts[0]
check ret.stmtReturnValue.kind == ekCall
check ret.stmtReturnValue.exprCallCallee.kind == ekIdent
test "full sample file":
let source = readFile("tests/testdata/sample.bux")
let lexRes = tokenize(source, "sample.bux")
check not lexRes.hasErrors
let parseRes = parse(lexRes.tokens, "sample.bux")
check parseRes.diagnostics.len == 0
check parseRes.module.items.len == 3
check parseRes.module.items[0].kind == dkUse
check parseRes.module.items[1].kind == dkFunc
check parseRes.module.items[2].kind == dkFunc