about summary refs log tree commit diff
path: root/pkg
diff options
context:
space:
mode:
authorMel <einebeere@gmail.com>2022-04-21 03:19:06 +0200
committerMel <einebeere@gmail.com>2022-04-21 03:19:06 +0200
commitb5a9660b6ac42bce27c746e76013c3ce5992743a (patch)
tree0e9bd21a30aae25fe21a7a3994a48ac2cea8c955 /pkg
parent5267c0e8653b431cfd2c06212cdba4f22225bd02 (diff)
downloadjinx-b5a9660b6ac42bce27c746e76013c3ce5992743a.tar.zst
jinx-b5a9660b6ac42bce27c746e76013c3ce5992743a.zip
Lang parser prototype
Diffstat (limited to 'pkg')
-rw-r--r--pkg/lang/ast/op.go102
-rw-r--r--pkg/lang/parser/exprs.go390
-rw-r--r--pkg/lang/parser/parser.go99
-rw-r--r--pkg/lang/parser/parser_test.go321
-rw-r--r--pkg/lang/parser/stmts.go372
-rw-r--r--pkg/lang/scanner/token/token.go9
6 files changed, 1286 insertions, 7 deletions
diff --git a/pkg/lang/ast/op.go b/pkg/lang/ast/op.go
index a708c78..f5fce51 100644
--- a/pkg/lang/ast/op.go
+++ b/pkg/lang/ast/op.go
@@ -1,13 +1,18 @@
 package ast
 
+import (
+	"fmt"
+	"jinx/pkg/lang/scanner/token"
+)
+
 type BinOp int
 
 const (
-	BinOpAdd BinOp = iota
-	BinOpSub
-	BinOpMul
-	BinOpDiv
-	BinOpMod
+	BinOpPlus BinOp = iota
+	BinOpMinus
+	BinOpStar
+	BinOpSlash
+	BinOpPercent
 
 	BinOpAssign
 	BinOpEq
@@ -18,9 +23,92 @@ const (
 	BinOpGte
 )
 
+func BinOpFromToken(tok token.Token) (BinOp, bool) {
+	var op BinOp
+	switch tok.Kind {
+	case token.Plus:
+		op = BinOpPlus
+	case token.Minus:
+		op = BinOpMinus
+	case token.Star:
+		op = BinOpStar
+	case token.Slash:
+		op = BinOpSlash
+	case token.Percent:
+		op = BinOpPercent
+	case token.Assign:
+		op = BinOpAssign
+	case token.Eq:
+		op = BinOpEq
+	case token.Neq:
+		op = BinOpNeq
+	case token.Lt:
+		op = BinOpLt
+	case token.Lte:
+		op = BinOpLte
+	case token.Gt:
+		op = BinOpGt
+	case token.Gte:
+		op = BinOpGte
+	default:
+		return 0, false
+	}
+
+	return op, true
+}
+
+func (op BinOp) Precedence() int {
+	switch op {
+	case BinOpPlus, BinOpMinus:
+		return 1
+	case BinOpStar, BinOpSlash, BinOpPercent:
+		return 2
+	case BinOpAssign:
+		return 3
+	case BinOpEq, BinOpNeq, BinOpLt, BinOpLte, BinOpGt, BinOpGte:
+		return 4
+	default:
+		panic(fmt.Sprintf("unknown binary operator: %d", op))
+	}
+}
+
+type Associativity int
+
+const (
+	AssociativityLeft Associativity = iota
+	AssociativityRight
+)
+
+func (op BinOp) Associativity() Associativity {
+	switch op {
+	case BinOpPlus, BinOpMinus, BinOpStar, BinOpSlash, BinOpPercent:
+		return AssociativityLeft
+	case BinOpAssign:
+		return AssociativityRight
+	case BinOpEq, BinOpNeq, BinOpLt, BinOpLte, BinOpGt, BinOpGte:
+		return AssociativityLeft
+	default:
+		panic(fmt.Sprintf("unknown binary operator: %d", op))
+	}
+}
+
 type UnOp int
 
 const (
-	UnOpNot UnOp = iota
-	UnOpNeg
+	UnOpBang UnOp = iota
+	UnOpMinus
 )
+
+func UnOpFromToken(tok token.Token) (UnOp, bool) {
+	var op UnOp
+	switch tok.Kind {
+	case token.Bang:
+		op = UnOpBang
+	case token.Minus:
+		op = UnOpMinus
+	default:
+		return 0, false
+	}
+
+	return op, true
+}
diff --git a/pkg/lang/parser/exprs.go b/pkg/lang/parser/exprs.go
new file mode 100644
index 0000000..b08864a
--- /dev/null
+++ b/pkg/lang/parser/exprs.go
@@ -0,0 +1,390 @@
+package parser
+
+import (
+	"fmt"
+	"jinx/pkg/lang/ast"
+	"jinx/pkg/lang/scanner/token"
+)
+
+func (p *Parser) parseExpr() (ast.Expr, error) {
+	return p.parseBinaryExpr()
+}
+
+func (p *Parser) parseBinaryExpr() (ast.Expr, error) {
+	// BinaryExpr = UnaryExpr (BinOp BinaryExpr)?
+
+	left, err := p.parseUnaryExpr()
+	if err != nil {
+		return ast.Expr{}, err
+	}
+
+	if op, ok := ast.BinOpFromToken(p.peek()); ok {
+		p.next()
+
+		right, err := p.parseBinaryExpr()
+		if err != nil {
+			return ast.Expr{}, err
+		}
+
+		// Check precedence and associativity.
+		if right.Kind == ast.ExprKindBinary {
+			rightBin := right.Value.(ast.ExprBinary)
+
+			needsSwitch := (op.Precedence() > rightBin.Op.Precedence()) ||
+				(op.Precedence() == rightBin.Op.Precedence() && op.Associativity() == ast.AssociativityLeft)
+
+			if needsSwitch {
+				left = ast.Expr{
+					At:   left.At,
+					Kind: ast.ExprKindBinary,
+					Value: ast.ExprBinary{
+						Left:  left,
+						Op:    op,
+						Right: rightBin.Left,
+					},
+				}
+
+				right = rightBin.Right
+
+				op = rightBin.Op
+			}
+		}
+
+		left = ast.Expr{
+			At:   left.At,
+			Kind: ast.ExprKindBinary,
+			Value: ast.ExprBinary{
+				Left:  left,
+				Op:    op,
+				Right: right,
+			},
+		}
+	}
+
+	return left, nil
+}
+
+func (p *Parser) parseUnaryExpr() (ast.Expr, error) {
+	// UnaryExpr = PostfixExpr (UnOp UnaryExpr)?
+	if op, ok := ast.UnOpFromToken(p.peek()); ok {
+		tok := p.next()
+
+		expr, err := p.parseUnaryExpr()
+		if err != nil {
+			return ast.Expr{}, err
+		}
+
+		return ast.Expr{
+			At:   tok.At,
+			Kind: ast.ExprKindUnary,
+			Value: ast.ExprUnary{
+				Op:    op,
+				Value: expr,
+			},
+		}, nil
+	}
+
+	return p.parsePostfixExpr()
+}
+
+func (p *Parser) parsePostfixExpr() (ast.Expr, error) {
+	// PostfixExpr = SubscriptionExpr | CallExpr
+
+	obj, err := p.parseUnitExpr()
+	if err != nil {
+		return ast.Expr{}, err
+	}
+
+	switch p.peek().Kind {
+	case token.LBracket:
+		return p.parseArrayIndexExpr(obj)
+	case token.LParen:
+		return p.parseCallExpr(obj)
+	default:
+		return obj, nil
+	}
+}
+
+func (p *Parser) parseArrayIndexExpr(obj ast.Expr) (ast.Expr, error) {
+	// SubscriptionExpr = UnitExpr ( "[" Expr "]" )*
+
+	if _, err := p.expect(token.LBracket); err != nil {
+		return ast.Expr{}, err
+	}
+
+	index, err := p.parseExpr()
+	if err != nil {
+		return ast.Expr{}, err
+	}
+
+	if _, err := p.expect(token.RBracket); err != nil {
+		return ast.Expr{}, err
+	}
+
+	return ast.Expr{
+		At:   obj.At,
+		Kind: ast.ExprKindSubscription,
+		Value: ast.ExprSubscription{
+			Obj: obj,
+			Key: index,
+		},
+	}, nil
+
+}
+
+func (p *Parser) parseCallExpr(callee ast.Expr) (ast.Expr, error) {
+	// CallExpr = UnitExpr ( "(" ( Expr ( "," Expr )* )? ")" )*
+
+	if _, err := p.expect(token.LParen); err != nil {
+		return ast.Expr{}, err
+	}
+
+	args := []ast.Expr{}
+	if p.peek().Kind != token.RParen {
+		for {
+			arg, err := p.parseExpr()
+			if err != nil {
+				return ast.Expr{}, err
+			}
+
+			args = append(args, arg)
+
+			if p.peek().Kind == token.RParen {
+				break
+			}
+
+			if _, err := p.expect(token.Comma); err != nil {
+				return ast.Expr{}, err
+			}
+		}
+	}
+
+	if _, err := p.expect(token.RParen); err != nil {
+		return ast.Expr{}, err
+	}
+
+	return ast.Expr{
+		At:   callee.At,
+		Kind: ast.ExprKindCall,
+		Value: ast.ExprCall{
+			Callee: callee,
+			Args:   args,
+		},
+	}, nil
+
+}
+
+func (p *Parser) parseUnitExpr() (ast.Expr, error) {
+	// UnitExpr = GroupExpr |
+	//        FnLitExpr     |
+	//        ArrayLitExpr  |
+	//        Ident         |
+	//        IntLitExpr    |
+	//        FloatLitExpr  |
+	//        StringLitExpr |
+	//        ValueLitExpr  |
+	switch p.peek().Kind {
+	case token.LParen:
+		return p.parseGroupExpr()
+	case token.KwFn:
+		return p.parseFnLitExpr()
+	case token.LBracket:
+		return p.parseArrayLitExpr()
+	case token.Ident:
+		return p.parseIdentExpr()
+	case token.Int:
+		return p.parseIntLitExpr()
+	case token.Float:
+		return p.parseFloatLitExpr()
+	case token.String:
+		return p.parseStringLitExpr()
+	case token.KwTrue, token.KwFalse, token.KwNull, token.KwThis:
+		return p.parseValueLitExpr()
+	default:
+		panic(fmt.Errorf("unexpected token %+v, wanted unit expression start", p.peek()))
+	}
+}
+
+func (p *Parser) parseGroupExpr() (ast.Expr, error) {
+	// GroupExpr = "(" Expr ")"
+
+	if _, err := p.expect(token.LParen); err != nil {
+		return ast.Expr{}, err
+	}
+
+	expr, err := p.parseExpr()
+	if err != nil {
+		return ast.Expr{}, err
+	}
+
+	if _, err := p.expect(token.RParen); err != nil {
+		return ast.Expr{}, err
+	}
+
+	return expr, nil
+}
+
+func (p *Parser) parseFnLitExpr() (ast.Expr, error) {
+	// FnLitExpr = "fn" FnParams (Block | Expr)
+	fnTok, err := p.expect(token.KwFn)
+	if err != nil {
+		return ast.Expr{}, err
+	}
+
+	params, err := p.parseFnParams()
+	if err != nil {
+		return ast.Expr{}, err
+	}
+
+	// TODO: Also parse just an expression
+	block, err := p.parseBlock()
+	if err != nil {
+		return ast.Expr{}, err
+	}
+
+	return ast.Expr{
+		At:   fnTok.At,
+		Kind: ast.ExprKindFnLit,
+		Value: ast.ExprFnLit{
+			Args: params,
+			Body: block,
+		},
+	}, nil
+}
+
+func (p *Parser) parseArrayLitExpr() (ast.Expr, error) {
+	// ArrayLitExpr = "[" Expr ("," Expr)* "]"
+	bracketTok, err := p.expect(token.LBracket)
+	if err != nil {
+		return ast.Expr{}, err
+	}
+
+	elems := []ast.Expr{}
+	if p.peek().Kind != token.RBracket {
+		for {
+			elem, err := p.parseExpr()
+			if err != nil {
+				return ast.Expr{}, err
+			}
+
+			elems = append(elems, elem)
+
+			if p.peek().Kind == token.RBracket {
+				break
+			}
+
+			if _, err := p.expect(token.Comma); err != nil {
+				return ast.Expr{}, err
+			}
+		}
+	}
+
+	return ast.Expr{
+		At:   bracketTok.At,
+		Kind: ast.ExprKindArrayLit,
+		Value: ast.ExprArrayLit{
+			Values: elems,
+		},
+	}, nil
+}
+
+func (p *Parser) parseIdentExpr() (ast.Expr, error) {
+	// IdentExpr = IDENT
+	node, err := p.parseIdent()
+	if err != nil {
+		return ast.Expr{}, err
+	}
+
+	return ast.Expr{
+		At:   node.At,
+		Kind: ast.ExprKindIdent,
+		Value: ast.ExprIdent{
+			Value: node,
+		},
+	}, nil
+}
+
+func (p *Parser) parseIntLitExpr() (ast.Expr, error) {
+	// IntLitExpr = INT
+	tok, err := p.expect(token.Int)
+	if err != nil {
+		return ast.Expr{}, err
+	}
+
+	return ast.Expr{
+		At:   tok.At,
+		Kind: ast.ExprKindIntLit,
+		Value: ast.ExprIntLit{
+			Value: tok.Data.(uint64),
+		},
+	}, nil
+}
+
+func (p *Parser) parseFloatLitExpr() (ast.Expr, error) {
+	// FloatLitExpr = FLOAT
+	tok, err := p.expect(token.Float)
+	if err != nil {
+		return ast.Expr{}, err
+	}
+
+	return ast.Expr{
+		At:   tok.At,
+		Kind: ast.ExprKindFloatLit,
+		Value: ast.ExprFloatLit{
+			Value: tok.Data.(float64),
+		},
+	}, nil
+}
+
+func (p *Parser) parseStringLitExpr() (ast.Expr, error) {
+	// StringLitExpr = STRING
+	tok, err := p.expect(token.String)
+	if err != nil {
+		return ast.Expr{}, err
+	}
+
+	return ast.Expr{
+		At:   tok.At,
+		Kind: ast.ExprKindStringLit,
+		Value: ast.ExprStringLit{
+			Value: tok.Data.(string),
+		},
+	}, nil
+}
+
+func (p *Parser) parseValueLitExpr() (ast.Expr, error) {
+	// ValueLitExpr = "true" | "false" | "null" | "this"
+	tok := p.next()
+	switch tok.Kind {
+	case token.KwTrue:
+		return ast.Expr{
+			At:   tok.At,
+			Kind: ast.ExprKindBoolLit,
+			Value: ast.ExprBoolLit{
+				Value: true,
+			},
+		}, nil
+	case token.KwFalse:
+		return ast.Expr{
+			At:   tok.At,
+			Kind: ast.ExprKindBoolLit,
+			Value: ast.ExprBoolLit{
+				Value: false,
+			},
+		}, nil
+	case token.KwNull:
+		return ast.Expr{
+			At:    tok.At,
+			Kind:  ast.ExprKindNullLit,
+			Value: ast.ExprNullLit{},
+		}, nil
+	case token.KwThis:
+		return ast.Expr{
+			At:    tok.At,
+			Kind:  ast.ExprKindThis,
+			Value: ast.ExprThis{},
+		}, nil
+	default:
+		panic(fmt.Errorf("unexpected token %+v, wanted value literal", tok))
+	}
+}
diff --git a/pkg/lang/parser/parser.go b/pkg/lang/parser/parser.go
new file mode 100644
index 0000000..1a9d1d0
--- /dev/null
+++ b/pkg/lang/parser/parser.go
@@ -0,0 +1,99 @@
+package parser
+
+import (
+	"fmt"
+	"jinx/pkg/lang/ast"
+	"jinx/pkg/lang/scanner/token"
+)
+
+type Parser struct {
+	tokens []token.Token
+	pos    int
+}
+
+func New(tokens []token.Token) *Parser {
+	return &Parser{tokens: tokens, pos: 0}
+}
+
+func (p *Parser) Parse() (ast.Program, error) {
+	program := ast.Program{}
+
+	for p.pos < len(p.tokens) {
+		stmt, err := p.parseStmt()
+		if err != nil {
+			return ast.Program{}, err
+		}
+
+		if err = p.parseStmtEnd(); err != nil {
+			return ast.Program{}, err
+		}
+
+		program.Stmts = append(program.Stmts, stmt)
+	}
+
+	return program, nil
+}
+
+func (p *Parser) parseBlock() (ast.BlockNode, error) {
+	braceTok, err := p.expect(token.LBrace)
+	if err != nil {
+		return ast.BlockNode{}, err
+	}
+
+	stmts := []ast.Stmt{}
+	for {
+		stmt, err := p.parseStmt()
+		if err != nil {
+			return ast.BlockNode{}, err
+		}
+
+		stmts = append(stmts, stmt)
+
+		if err = p.parseStmtEnd(); err != nil {
+			return ast.BlockNode{}, err
+		}
+
+		if p.peek().Kind == token.RBrace {
+			break
+		}
+	}
+
+	if _, err := p.expect(token.RBrace); err != nil {
+		return ast.BlockNode{}, err
+	}
+
+	return ast.BlockNode{
+		At:    braceTok.At,
+		Stmts: stmts,
+	}, nil
+}
+
+func (p *Parser) parseIdent() (ast.IdentNode, error) {
+	tok, err := p.expect(token.Ident)
+	if err != nil {
+		return ast.IdentNode{}, err
+	}
+
+	return ast.IdentNode{
+		At:    tok.At,
+		Value: tok.Data.(string),
+	}, nil
+}
+
+func (p *Parser) next() token.Token {
+	p.pos++
+	return p.tokens[p.pos-1]
+}
+
+func (p *Parser) expect(kind token.TokenKind) (token.Token, error) {
+	tok := p.next()
+	if tok.Kind != kind {
+		return token.Token{}, fmt.Errorf("expected %v, got %v", kind, tok.Kind)
+	}
+
+	return tok, nil
+}
+
+func (p *Parser) peek() token.Token {
+	return p.tokens[p.pos]
+}
diff --git a/pkg/lang/parser/parser_test.go b/pkg/lang/parser/parser_test.go
new file mode 100644
index 0000000..969e878
--- /dev/null
+++ b/pkg/lang/parser/parser_test.go
@@ -0,0 +1,321 @@
+package parser_test
+
+import (
+	"jinx/pkg/lang/ast"
+	"jinx/pkg/lang/parser"
+	"jinx/pkg/lang/scanner"
+	"jinx/pkg/lang/scanner/token"
+	"strings"
+	"testing"
+
+	"github.com/stretchr/testify/require"
+)
+
+func TestIntLit(t *testing.T) {
+	source := `123`
+	p := cheatWithScanner(t, source)
+	program, err := p.Parse()
+	require.NoError(t, err)
+
+	require.Equal(t, 1, len(program.Stmts))
+	require.Equal(t, ast.Stmt{
+		Kind: ast.StmtKindExpr,
+		Value: ast.StmtExpr{
+			Value: ast.Expr{
+				Kind:  ast.ExprKindIntLit,
+				Value: ast.ExprIntLit{Value: 123},
+			},
+		},
+	}, program.Stmts[0])
+}
+
+func TestSimpleBinaryExpr(t *testing.T) {
+	source := `1 + 2`
+	p := cheatWithScanner(t, source)
+	program, err := p.Parse()
+	require.NoError(t, err)
+
+	require.Equal(t, 1, len(program.Stmts))
+	require.Equal(t, ast.Stmt{
+		Kind: ast.StmtKindExpr,
+		Value: ast.StmtExpr{
+			Value: ast.Expr{
+				Kind: ast.ExprKindBinary,
+				Value: ast.ExprBinary{
+					Left: ast.Expr{
+						Kind:  ast.ExprKindIntLit,
+						Value: ast.ExprIntLit{Value: 1},
+					},
+					Op: ast.BinOpPlus,
+					Right: ast.Expr{
+						At:    token.NewLoc(0, 4),
+						Kind:  ast.ExprKindIntLit,
+						Value: ast.ExprIntLit{Value: 2},
+					},
+				},
+			},
+		},
+	}, program.Stmts[0])
+}
+
+func TestLeftAssocBinaryExpr(t *testing.T) {
+	source := `1 + 2 + 3`
+	p := cheatWithScanner(t, source)
+	program, err := p.Parse()
+	require.NoError(t, err)
+
+	require.Equal(t, 1, len(program.Stmts))
+	require.Equal(t, ast.Stmt{
+		Kind: ast.StmtKindExpr,
+		Value: ast.StmtExpr{
+			Value: ast.Expr{
+				Kind: ast.ExprKindBinary,
+				Value: ast.ExprBinary{
+					Left: ast.Expr{
+						Kind: ast.ExprKindBinary,
+						Value: ast.ExprBinary{
+							Left: ast.Expr{
+								Kind:  ast.ExprKindIntLit,
+								Value: ast.ExprIntLit{Value: 1},
+							},
+							Op: ast.BinOpPlus,
+							Right: ast.Expr{
+								At:    token.NewLoc(0, 4),
+								Kind:  ast.ExprKindIntLit,
+								Value: ast.ExprIntLit{Value: 2},
+							},
+						},
+					},
+					Op: ast.BinOpPlus,
+					Right: ast.Expr{
+						At:    token.NewLoc(0, 8),
+						Kind:  ast.ExprKindIntLit,
+						Value: ast.ExprIntLit{Value: 3},
+					},
+				},
+			},
+		},
+	}, program.Stmts[0])
+}
+
+func TestRightAssocBinaryExpr(t *testing.T) {
+	source := `x = y = z`
+	p := cheatWithScanner(t, source)
+	program, err := p.Parse()
+	require.NoError(t, err)
+
+	require.Equal(t, 1, len(program.Stmts))
+	require.Equal(t, ast.Stmt{
+		Kind: ast.StmtKindExpr,
+		Value: ast.StmtExpr{
+			Value: ast.Expr{
+				Kind: ast.ExprKindBinary,
+				Value: ast.ExprBinary{
+					Left: ast.Expr{
+						Kind:  ast.ExprKindIdent,
+						Value: ast.ExprIdent{Value: ast.IdentNode{Value: "x"}},
+					},
+					Op: ast.BinOpAssign,
+					Right: ast.Expr{
+						At:   token.NewLoc(0, 4),
+						Kind: ast.ExprKindBinary,
+						Value: ast.ExprBinary{
+							Left: ast.Expr{
+								At:    token.NewLoc(0, 4),
+								Kind:  ast.ExprKindIdent,
+								Value: ast.ExprIdent{Value: ast.IdentNode{At: token.NewLoc(0, 4), Value: "y"}},
+							},
+							Op: ast.BinOpAssign,
+							Right: ast.Expr{
+								At:    token.NewLoc(0, 8),
+								Kind:  ast.ExprKindIdent,
+								Value: ast.ExprIdent{Value: ast.IdentNode{At: token.NewLoc(0, 8), Value: "z"}},
+							},
+						},
+					},
+				},
+			},
+		},
+	}, program.Stmts[0])
+}
+
+func TestVarDecl(t *testing.T) {
+	source := `var x = 123`
+	p := cheatWithScanner(t, source)
+	program, err := p.Parse()
+	require.NoError(t, err)
+
+	require.Equal(t, 1, len(program.Stmts))
+	require.Equal(t, ast.Stmt{
+		Kind: ast.StmtKindVarDecl,
+		Value: ast.StmtVarDecl{
+			Name: ast.IdentNode{At: token.NewLoc(0, 4), Value: "x"},
+			Value: ast.Expr{
+				At:    token.NewLoc(0, 8),
+				Kind:  ast.ExprKindIntLit,
+				Value: ast.ExprIntLit{Value: 123},
+			},
+		},
+	}, program.Stmts[0])
+}
+
+func TestEmptyStmt(t *testing.T) {
+	source := `
+	
+	
+	;
+
+
+	`
+	p := cheatWithScanner(t, source)
+	program, err := p.Parse()
+	require.NoError(t, err)
+
+	require.Equal(t, 4, len(program.Stmts))
+
+	expected := []ast.Stmt{
+		{
+			Kind:  ast.StmtKindEmpty,
+			Value: ast.StmtEmpty{},
+		},
+		{
+			At:    token.NewLoc(3, 1),
+			Kind:  ast.StmtKindEmpty,
+			Value: ast.StmtEmpty{},
+		},
+		{
+			At:    token.NewLoc(3, 2),
+			Kind:  ast.StmtKindEmpty,
+			Value: ast.StmtEmpty{},
+		},
+		{
+			At:    token.NewLoc(6, 1),
+			Kind:  ast.StmtKindEmpty,
+			Value: ast.StmtEmpty{},
+		},
+	}
+
+	require.Equal(t, expected, program.Stmts)
+}
+
+func TestMultipleStmts(t *testing.T) {
+	source := sourceify(
+		`var x = 1`,
+		`var y = 2`,
+		`z = 3`,
+	)
+
+	p := cheatWithScanner(t, source)
+	program, err := p.Parse()
+	require.NoError(t, err)
+
+	require.Equal(t, 3, len(program.Stmts))
+
+	expected := []ast.Stmt{
+		{
+			Kind: ast.StmtKindVarDecl,
+			Value: ast.StmtVarDecl{
+				Name: ast.IdentNode{At: token.NewLoc(0, 4), Value: "x"},
+				Value: ast.Expr{
+					At:    token.NewLoc(0, 8),
+					Kind:  ast.ExprKindIntLit,
+					Value: ast.ExprIntLit{Value: 1},
+				},
+			},
+		},
+		{
+			At:   token.NewLoc(1, 0),
+			Kind: ast.StmtKindVarDecl,
+			Value: ast.StmtVarDecl{
+				Name: ast.IdentNode{At: token.NewLoc(1, 4), Value: "y"},
+				Value: ast.Expr{
+					At:    token.NewLoc(1, 8),
+					Kind:  ast.ExprKindIntLit,
+					Value: ast.ExprIntLit{Value: 2},
+				},
+			},
+		},
+		{
+			At:   token.NewLoc(2, 0),
+			Kind: ast.StmtKindExpr,
+			Value: ast.StmtExpr{
+				Value: ast.Expr{
+					At:   token.NewLoc(2, 0),
+					Kind: ast.ExprKindBinary,
+					Value: ast.ExprBinary{
+						Left: ast.Expr{
+							At:    token.NewLoc(2, 0),
+							Kind:  ast.ExprKindIdent,
+							Value: ast.ExprIdent{Value: ast.IdentNode{At: token.NewLoc(2, 0), Value: "z"}},
+						},
+						Op: ast.BinOpAssign,
+						Right: ast.Expr{
+							At:    token.NewLoc(2, 4),
+							Kind:  ast.ExprKindIntLit,
+							Value: ast.ExprIntLit{Value: 3},
+						},
+					},
+				},
+			},
+		},
+	}
+	require.Equal(t, expected, program.Stmts)
+}
+
+func TestIfStmt(t *testing.T) {
+	source := sourceify(
+		`if false {`,
+		`	var x = 2`,
+		`}`,
+	)
+	p := cheatWithScanner(t, source)
+	program, err := p.Parse()
+	require.NoError(t, err)
+
+	require.Equal(t, 1, len(program.Stmts))
+	require.Equal(t, ast.Stmt{
+		Kind: ast.StmtKindIf,
+		Value: ast.StmtIf{
+			Cond: ast.Expr{
+				At:    token.NewLoc(0, 3),
+				Kind:  ast.ExprKindBoolLit,
+				Value: ast.ExprBoolLit{Value: false},
+			},
+			Then: ast.BlockNode{
+				At: token.NewLoc(0, 9),
+				Stmts: []ast.Stmt{
+					{
+						At:    token.NewLoc(0, 10),
+						Kind:  ast.StmtKindEmpty,
+						Value: ast.StmtEmpty{},
+					},
+					{
+						At:   token.NewLoc(1, 1),
+						Kind: ast.StmtKindVarDecl,
+						Value: ast.StmtVarDecl{
+							Name: ast.IdentNode{At: token.NewLoc(1, 5), Value: "x"},
+							Value: ast.Expr{
+								At:    token.NewLoc(1, 9),
+								Kind:  ast.ExprKindIntLit,
+								Value: ast.ExprIntLit{Value: 2},
+							},
+						},
+					},
+				},
+			},
+			Elifs: []ast.CondNode{},
+		},
+	}, program.Stmts[0])
+}
+
+func sourceify(lines ...string) string {
+	return strings.Join(lines, "\n")
+}
+
+func cheatWithScanner(t *testing.T, source string) *parser.Parser {
+	s := scanner.New(strings.NewReader(source))
+	tokens, err := s.Scan()
+	require.NoError(t, err, "scanner error occurred while testing parser :(")
+
+	return parser.New(tokens)
+}
diff --git a/pkg/lang/parser/stmts.go b/pkg/lang/parser/stmts.go
new file mode 100644
index 0000000..52e590c
--- /dev/null
+++ b/pkg/lang/parser/stmts.go
@@ -0,0 +1,372 @@
+package parser
+
+import (
+	"fmt"
+	"jinx/pkg/lang/ast"
+	"jinx/pkg/lang/scanner/token"
+)
+
+func (p *Parser) parseStmt() (ast.Stmt, error) {
+	switch p.peek().Kind {
+	case token.KwUse:
+		return p.parseUseStmt()
+	case token.KwFn:
+		return p.parseFnDeclStmt()
+	case token.KwObject:
+		return p.parseObjectDeclStmt()
+	case token.KwVar:
+		return p.parseVarDeclStmt()
+	case token.KwIf:
+		return p.parseIfStmt()
+	case token.KwTry:
+		return p.parseTryStmt()
+	case token.KwReturn:
+		return p.parseReturnStmt()
+	case token.KwContinue:
+		return p.parseContinueStmt()
+	case token.KwBreak:
+		return p.parseBreakStmt()
+	case token.KwThrow:
+		return p.parseThrowStmt()
+	default:
+		if p.peek().CanEndStmt() {
+			return p.parseEmptyStmt()
+		} else {
+			return p.parseExprStmt()
+		}
+	}
+}
+
+func (p *Parser) parseUseStmt() (ast.Stmt, error) {
+	panic("not implemented")
+}
+
+func (p *Parser) parseFnDeclStmt() (ast.Stmt, error) {
+	fnTok, err := p.expect(token.KwFn)
+	if err != nil {
+		return ast.Stmt{}, err
+	}
+
+	name, err := p.parseIdent()
+	if err != nil {
+		return ast.Stmt{}, err
+	}
+
+	params, err := p.parseFnParams()
+	if err != nil {
+		return ast.Stmt{}, err
+	}
+
+	block, err := p.parseBlock()
+	if err != nil {
+		return ast.Stmt{}, err
+	}
+
+	return ast.Stmt{
+		At:   fnTok.At,
+		Kind: ast.StmtKindFnDecl,
+		Value: ast.StmtFnDecl{
+			Name: name,
+			Args: params,
+			Body: block,
+		},
+	}, nil
+}
+
+func (p *Parser) parseFnParams() ([]ast.IdentNode, error) {
+	// FnParams = "(" ( IDENT ("," IDENT)* )? ")"
+
+	if _, err := p.expect(token.LParen); err != nil {
+		return nil, err
+	}
+
+	params := []ast.IdentNode{}
+
+	if p.peek().Kind != token.RParen {
+		for {
+			param, err := p.parseIdent()
+			if err != nil {
+				return nil, err
+			}
+
+			params = append(params, param)
+
+			if p.peek().Kind == token.RParen {
+				break
+			}
+
+			if _, err := p.expect(token.Comma); err != nil {
+				return nil, err
+			}
+		}
+	}
+
+	if _, err := p.expect(token.RParen); err != nil {
+		return nil, err
+	}
+
+	return params, nil
+}
+
+func (p *Parser) parseObjectDeclStmt() (ast.Stmt, error) {
+	panic("not implemented")
+}
+
+func (p *Parser) parseVarDeclStmt() (ast.Stmt, error) {
+	// VarDeclStmt = "var" IDENT "=" Expr
+
+	varTok, err := p.expect(token.KwVar)
+	if err != nil {
+		return ast.Stmt{}, err
+	}
+
+	name, err := p.parseIdent()
+	if err != nil {
+		return ast.Stmt{}, err
+	}
+
+	if _, err := p.expect(token.Assign); err != nil {
+		return ast.Stmt{}, err
+	}
+
+	expr, err := p.parseExpr()
+	if err != nil {
+		return ast.Stmt{}, err
+	}
+
+	return ast.Stmt{
+		At:   varTok.At,
+		Kind: ast.StmtKindVarDecl,
+		Value: ast.StmtVarDecl{
+			Name:  name,
+			Value: expr,
+		},
+	}, nil
+}
+
+func (p *Parser) parseIfStmt() (ast.Stmt, error) {
+	// IfStmt = "if" Expr Block ("elif" Expr Block)* ("else" Block)?
+	ifTok, err := p.expect(token.KwIf)
+	if err != nil {
+		return ast.Stmt{}, err
+	}
+
+	cond, err := p.parseExpr()
+	if err != nil {
+		return ast.Stmt{}, err
+	}
+
+	then, err := p.parseBlock()
+	if err != nil {
+		return ast.Stmt{}, err
+	}
+
+	elifs := []ast.CondNode{}
+
+	for p.peek().Kind == token.KwElif {
+		elifTok, err := p.expect(token.KwElif)
+		if err != nil {
+			return ast.Stmt{}, err
+		}
+
+		elifCond, err := p.parseExpr()
+		if err != nil {
+			return ast.Stmt{}, err
+		}
+
+		elifThen, err := p.parseBlock()
+		if err != nil {
+			return ast.Stmt{}, err
+		}
+
+		elifs = append(elifs, ast.CondNode{
+			At:   elifTok.At,
+			Cond: elifCond,
+			Then: elifThen,
+		})
+	}
+
+	elseThen := ast.BlockNode{}
+
+	if p.peek().Kind == token.KwElse {
+		_, err := p.expect(token.KwElse)
+		if err != nil {
+			return ast.Stmt{}, err
+		}
+
+		elseThen, err = p.parseBlock()
+		if err != nil {
+			return ast.Stmt{}, err
+		}
+	}
+
+	return ast.Stmt{
+		At:   ifTok.At,
+		Kind: ast.StmtKindIf,
+		Value: ast.StmtIf{
+			Cond:  cond,
+			Then:  then,
+			Elifs: elifs,
+			Else:  elseThen,
+		},
+	}, nil
+}
+
+func (p *Parser) parseTryStmt() (ast.Stmt, error) {
+	// TryStmt = "try" Block "catch" Ident ("finally" Block)?
+
+	tryTok, err := p.expect(token.KwTry)
+	if err != nil {
+		return ast.Stmt{}, err
+	}
+
+	tryBlock, err := p.parseBlock()
+	if err != nil {
+		return ast.Stmt{}, err
+	}
+
+	if _, err = p.expect(token.KwCatch); err != nil {
+		return ast.Stmt{}, err
+	}
+
+	catchName, err := p.parseIdent()
+	if err != nil {
+		return ast.Stmt{}, err
+	}
+
+	catchBlock, err := p.parseBlock()
+	if err != nil {
+		return ast.Stmt{}, err
+	}
+
+	finallyBlock := ast.BlockNode{}
+	if p.peek().Kind == token.KwFinally {
+		_, err := p.expect(token.KwFinally)
+		if err != nil {
+			return ast.Stmt{}, err
+		}
+
+		finallyBlock, err = p.parseBlock()
+		if err != nil {
+			return ast.Stmt{}, err
+		}
+	}
+
+	return ast.Stmt{
+		At:   tryTok.At,
+		Kind: ast.StmtKindTry,
+		Value: ast.StmtTry{
+			Try:         tryBlock,
+			CatchedName: catchName,
+			Catch:       catchBlock,
+			Finally:     finallyBlock,
+		},
+	}, nil
+}
+
+func (p *Parser) parseReturnStmt() (ast.Stmt, error) {
+	// ReturnStmt = "return" (Expr)?
+	returnTok, err := p.expect(token.KwReturn)
+	if err != nil {
+		return ast.Stmt{}, err
+	}
+
+	expr := ast.Expr{}
+	if !p.peek().CanEndStmt() {
+		expr, err = p.parseExpr()
+		if err != nil {
+			return ast.Stmt{}, err
+		}
+	}
+
+	return ast.Stmt{
+		At:   returnTok.At,
+		Kind: ast.StmtKindReturn,
+		Value: ast.StmtReturn{
+			Value: expr,
+		},
+	}, nil
+}
+
+func (p *Parser) parseContinueStmt() (ast.Stmt, error) {
+	// ContinueStmt = "continue"
+	continueTok, err := p.expect(token.KwContinue)
+	if err != nil {
+		return ast.Stmt{}, err
+	}
+
+	return ast.Stmt{
+		At:    continueTok.At,
+		Kind:  ast.StmtKindContinue,
+		Value: ast.StmtContinue{},
+	}, nil
+}
+
+func (p *Parser) parseBreakStmt() (ast.Stmt, error) {
+	// BreakStmt = "break"
+
+	breakTok, err := p.expect(token.KwBreak)
+	if err != nil {
+		return ast.Stmt{}, err
+	}
+
+	return ast.Stmt{
+		At:    breakTok.At,
+		Kind:  ast.StmtKindBreak,
+		Value: ast.StmtBreak{},
+	}, nil
+}
+
+func (p *Parser) parseThrowStmt() (ast.Stmt, error) {
+	// ThrowStmt = "throw" Expr
+
+	throwTok, err := p.expect(token.KwThrow)
+	if err != nil {
+		return ast.Stmt{}, err
+	}
+
+	expr, err := p.parseExpr()
+	if err != nil {
+		return ast.Stmt{}, err
+	}
+
+	return ast.Stmt{
+		At:   throwTok.At,
+		Kind: ast.StmtKindThrow,
+		Value: ast.StmtThrow{
+			Value: expr,
+		},
+	}, nil
+}
+
+func (p *Parser) parseExprStmt() (ast.Stmt, error) {
+	expr, err := p.parseExpr()
+	if err != nil {
+		return ast.Stmt{}, err
+	}
+
+	return ast.Stmt{
+		At:   expr.At,
+		Kind: ast.StmtKindExpr,
+		Value: ast.StmtExpr{
+			Value: expr,
+		},
+	}, nil
+}
+
+func (p *Parser) parseEmptyStmt() (ast.Stmt, error) {
+	return ast.Stmt{
+		At:    p.peek().At,
+		Kind:  ast.StmtKindEmpty,
+		Value: ast.StmtEmpty{},
+	}, nil
+}
+
+func (p *Parser) parseStmtEnd() error {
+	tok := p.peek()
+	if !tok.CanEndStmt() {
+		panic(fmt.Errorf("wanted statement end, received: %+v", tok))
+	}
+	p.next()
+	return nil
+}
diff --git a/pkg/lang/scanner/token/token.go b/pkg/lang/scanner/token/token.go
index 840a420..1ccd864 100644
--- a/pkg/lang/scanner/token/token.go
+++ b/pkg/lang/scanner/token/token.go
@@ -20,3 +20,12 @@ func New(kind TokenKind, at Loc, data any) Token {
 		Data: data,
 	}
 }
+
+func (t Token) CanEndStmt() bool {
+	switch t.Kind {
+	case EOF, EOL, SemiColon:
+		return true
+	default:
+		return false
+	}
+}