about summary refs log tree commit diff
diff options
context:
space:
mode:
authorMel <einebeere@gmail.com>2022-05-09 00:01:02 +0200
committerMel <einebeere@gmail.com>2022-05-09 00:01:02 +0200
commitb09a14147d397904722ee7c25e4defc56135b96f (patch)
tree694dc725528310f3a65d04785b8eea33908ce69f
parentb5a9660b6ac42bce27c746e76013c3ce5992743a (diff)
downloadjinx-b09a14147d397904722ee7c25e4defc56135b96f.tar.zst
jinx-b09a14147d397904722ee7c25e4defc56135b96f.zip
Extract source walk part of scanner
-rw-r--r--pkg/lang/ast/expr.go4
-rw-r--r--pkg/lang/ast/nodes.go8
-rw-r--r--pkg/lang/ast/stmt.go4
-rw-r--r--pkg/lang/parser/parser_test.go92
-rw-r--r--pkg/lang/scanner/errors.go9
-rw-r--r--pkg/lang/scanner/scanner.go119
-rw-r--r--pkg/lang/scanner/scanner_test.go187
-rw-r--r--pkg/lang/scanner/token/token.go8
-rw-r--r--pkg/libs/source/errors.go10
-rw-r--r--pkg/libs/source/loc.go (renamed from pkg/lang/scanner/token/loc.go)2
-rw-r--r--pkg/libs/source/walker.go80
11 files changed, 272 insertions, 251 deletions
diff --git a/pkg/lang/ast/expr.go b/pkg/lang/ast/expr.go
index ab6578a..f98f9b3 100644
--- a/pkg/lang/ast/expr.go
+++ b/pkg/lang/ast/expr.go
@@ -1,6 +1,6 @@
 package ast
 
-import "jinx/pkg/lang/scanner/token"
+import "jinx/pkg/libs/source"
 
 type ExprKind int
 
@@ -25,7 +25,7 @@ const (
 type Expr ExprT[any]
 
 type ExprT[T any] struct {
-	At    token.Loc
+	At    source.Loc
 	Kind  ExprKind
 	Value T
 }
diff --git a/pkg/lang/ast/nodes.go b/pkg/lang/ast/nodes.go
index 55dbb5b..117d8bf 100644
--- a/pkg/lang/ast/nodes.go
+++ b/pkg/lang/ast/nodes.go
@@ -1,19 +1,19 @@
 package ast
 
-import "jinx/pkg/lang/scanner/token"
+import "jinx/pkg/libs/source"
 
 type IdentNode struct {
-	At    token.Loc
+	At    source.Loc
 	Value string
 }
 
 type BlockNode struct {
-	At    token.Loc
+	At    source.Loc
 	Stmts []Stmt
 }
 
 type CondNode struct {
-	At   token.Loc
+	At   source.Loc
 	Cond Expr
 	Then BlockNode
 }
diff --git a/pkg/lang/ast/stmt.go b/pkg/lang/ast/stmt.go
index 4f24cc5..e25dee9 100644
--- a/pkg/lang/ast/stmt.go
+++ b/pkg/lang/ast/stmt.go
@@ -1,6 +1,6 @@
 package ast
 
-import "jinx/pkg/lang/scanner/token"
+import "jinx/pkg/libs/source"
 
 type StmtKind int
 
@@ -22,7 +22,7 @@ const (
 type Stmt StmtT[any]
 
 type StmtT[T any] struct {
-	At    token.Loc
+	At    source.Loc
 	Kind  StmtKind
 	Value T
 }
diff --git a/pkg/lang/parser/parser_test.go b/pkg/lang/parser/parser_test.go
index 969e878..c3794e9 100644
--- a/pkg/lang/parser/parser_test.go
+++ b/pkg/lang/parser/parser_test.go
@@ -4,7 +4,7 @@ import (
 	"jinx/pkg/lang/ast"
 	"jinx/pkg/lang/parser"
 	"jinx/pkg/lang/scanner"
-	"jinx/pkg/lang/scanner/token"
+	"jinx/pkg/libs/source"
 	"strings"
 	"testing"
 
@@ -12,8 +12,8 @@ import (
 )
 
 func TestIntLit(t *testing.T) {
-	source := `123`
-	p := cheatWithScanner(t, source)
+	src := `123`
+	p := cheatWithScanner(t, src)
 	program, err := p.Parse()
 	require.NoError(t, err)
 
@@ -30,8 +30,8 @@ func TestIntLit(t *testing.T) {
 }
 
 func TestSimpleBinaryExpr(t *testing.T) {
-	source := `1 + 2`
-	p := cheatWithScanner(t, source)
+	src := `1 + 2`
+	p := cheatWithScanner(t, src)
 	program, err := p.Parse()
 	require.NoError(t, err)
 
@@ -48,7 +48,7 @@ func TestSimpleBinaryExpr(t *testing.T) {
 					},
 					Op: ast.BinOpPlus,
 					Right: ast.Expr{
-						At:    token.NewLoc(0, 4),
+						At:    source.NewLoc(0, 4),
 						Kind:  ast.ExprKindIntLit,
 						Value: ast.ExprIntLit{Value: 2},
 					},
@@ -59,8 +59,8 @@ func TestSimpleBinaryExpr(t *testing.T) {
 }
 
 func TestLeftAssocBinaryExpr(t *testing.T) {
-	source := `1 + 2 + 3`
-	p := cheatWithScanner(t, source)
+	src := `1 + 2 + 3`
+	p := cheatWithScanner(t, src)
 	program, err := p.Parse()
 	require.NoError(t, err)
 
@@ -80,7 +80,7 @@ func TestLeftAssocBinaryExpr(t *testing.T) {
 							},
 							Op: ast.BinOpPlus,
 							Right: ast.Expr{
-								At:    token.NewLoc(0, 4),
+								At:    source.NewLoc(0, 4),
 								Kind:  ast.ExprKindIntLit,
 								Value: ast.ExprIntLit{Value: 2},
 							},
@@ -88,7 +88,7 @@ func TestLeftAssocBinaryExpr(t *testing.T) {
 					},
 					Op: ast.BinOpPlus,
 					Right: ast.Expr{
-						At:    token.NewLoc(0, 8),
+						At:    source.NewLoc(0, 8),
 						Kind:  ast.ExprKindIntLit,
 						Value: ast.ExprIntLit{Value: 3},
 					},
@@ -99,8 +99,8 @@ func TestLeftAssocBinaryExpr(t *testing.T) {
 }
 
 func TestRightAssocBinaryExpr(t *testing.T) {
-	source := `x = y = z`
-	p := cheatWithScanner(t, source)
+	src := `x = y = z`
+	p := cheatWithScanner(t, src)
 	program, err := p.Parse()
 	require.NoError(t, err)
 
@@ -117,19 +117,19 @@ func TestRightAssocBinaryExpr(t *testing.T) {
 					},
 					Op: ast.BinOpAssign,
 					Right: ast.Expr{
-						At:   token.NewLoc(0, 4),
+						At:   source.NewLoc(0, 4),
 						Kind: ast.ExprKindBinary,
 						Value: ast.ExprBinary{
 							Left: ast.Expr{
-								At:    token.NewLoc(0, 4),
+								At:    source.NewLoc(0, 4),
 								Kind:  ast.ExprKindIdent,
-								Value: ast.ExprIdent{Value: ast.IdentNode{At: token.NewLoc(0, 4), Value: "y"}},
+								Value: ast.ExprIdent{Value: ast.IdentNode{At: source.NewLoc(0, 4), Value: "y"}},
 							},
 							Op: ast.BinOpAssign,
 							Right: ast.Expr{
-								At:    token.NewLoc(0, 8),
+								At:    source.NewLoc(0, 8),
 								Kind:  ast.ExprKindIdent,
-								Value: ast.ExprIdent{Value: ast.IdentNode{At: token.NewLoc(0, 8), Value: "z"}},
+								Value: ast.ExprIdent{Value: ast.IdentNode{At: source.NewLoc(0, 8), Value: "z"}},
 							},
 						},
 					},
@@ -140,8 +140,8 @@ func TestRightAssocBinaryExpr(t *testing.T) {
 }
 
 func TestVarDecl(t *testing.T) {
-	source := `var x = 123`
-	p := cheatWithScanner(t, source)
+	src := `var x = 123`
+	p := cheatWithScanner(t, src)
 	program, err := p.Parse()
 	require.NoError(t, err)
 
@@ -149,9 +149,9 @@ func TestVarDecl(t *testing.T) {
 	require.Equal(t, ast.Stmt{
 		Kind: ast.StmtKindVarDecl,
 		Value: ast.StmtVarDecl{
-			Name: ast.IdentNode{At: token.NewLoc(0, 4), Value: "x"},
+			Name: ast.IdentNode{At: source.NewLoc(0, 4), Value: "x"},
 			Value: ast.Expr{
-				At:    token.NewLoc(0, 8),
+				At:    source.NewLoc(0, 8),
 				Kind:  ast.ExprKindIntLit,
 				Value: ast.ExprIntLit{Value: 123},
 			},
@@ -160,14 +160,14 @@ func TestVarDecl(t *testing.T) {
 }
 
 func TestEmptyStmt(t *testing.T) {
-	source := `
+	src := `
 	
 	
 	;
 
 
 	`
-	p := cheatWithScanner(t, source)
+	p := cheatWithScanner(t, src)
 	program, err := p.Parse()
 	require.NoError(t, err)
 
@@ -179,17 +179,17 @@ func TestEmptyStmt(t *testing.T) {
 			Value: ast.StmtEmpty{},
 		},
 		{
-			At:    token.NewLoc(3, 1),
+			At:    source.NewLoc(3, 1),
 			Kind:  ast.StmtKindEmpty,
 			Value: ast.StmtEmpty{},
 		},
 		{
-			At:    token.NewLoc(3, 2),
+			At:    source.NewLoc(3, 2),
 			Kind:  ast.StmtKindEmpty,
 			Value: ast.StmtEmpty{},
 		},
 		{
-			At:    token.NewLoc(6, 1),
+			At:    source.NewLoc(6, 1),
 			Kind:  ast.StmtKindEmpty,
 			Value: ast.StmtEmpty{},
 		},
@@ -199,13 +199,13 @@ func TestEmptyStmt(t *testing.T) {
 }
 
 func TestMultipleStmts(t *testing.T) {
-	source := sourceify(
+	src := sourceify(
 		`var x = 1`,
 		`var y = 2`,
 		`z = 3`,
 	)
 
-	p := cheatWithScanner(t, source)
+	p := cheatWithScanner(t, src)
 	program, err := p.Parse()
 	require.NoError(t, err)
 
@@ -215,42 +215,42 @@ func TestMultipleStmts(t *testing.T) {
 		{
 			Kind: ast.StmtKindVarDecl,
 			Value: ast.StmtVarDecl{
-				Name: ast.IdentNode{At: token.NewLoc(0, 4), Value: "x"},
+				Name: ast.IdentNode{At: source.NewLoc(0, 4), Value: "x"},
 				Value: ast.Expr{
-					At:    token.NewLoc(0, 8),
+					At:    source.NewLoc(0, 8),
 					Kind:  ast.ExprKindIntLit,
 					Value: ast.ExprIntLit{Value: 1},
 				},
 			},
 		},
 		{
-			At:   token.NewLoc(1, 0),
+			At:   source.NewLoc(1, 0),
 			Kind: ast.StmtKindVarDecl,
 			Value: ast.StmtVarDecl{
-				Name: ast.IdentNode{At: token.NewLoc(1, 4), Value: "y"},
+				Name: ast.IdentNode{At: source.NewLoc(1, 4), Value: "y"},
 				Value: ast.Expr{
-					At:    token.NewLoc(1, 8),
+					At:    source.NewLoc(1, 8),
 					Kind:  ast.ExprKindIntLit,
 					Value: ast.ExprIntLit{Value: 2},
 				},
 			},
 		},
 		{
-			At:   token.NewLoc(2, 0),
+			At:   source.NewLoc(2, 0),
 			Kind: ast.StmtKindExpr,
 			Value: ast.StmtExpr{
 				Value: ast.Expr{
-					At:   token.NewLoc(2, 0),
+					At:   source.NewLoc(2, 0),
 					Kind: ast.ExprKindBinary,
 					Value: ast.ExprBinary{
 						Left: ast.Expr{
-							At:    token.NewLoc(2, 0),
+							At:    source.NewLoc(2, 0),
 							Kind:  ast.ExprKindIdent,
-							Value: ast.ExprIdent{Value: ast.IdentNode{At: token.NewLoc(2, 0), Value: "z"}},
+							Value: ast.ExprIdent{Value: ast.IdentNode{At: source.NewLoc(2, 0), Value: "z"}},
 						},
 						Op: ast.BinOpAssign,
 						Right: ast.Expr{
-							At:    token.NewLoc(2, 4),
+							At:    source.NewLoc(2, 4),
 							Kind:  ast.ExprKindIntLit,
 							Value: ast.ExprIntLit{Value: 3},
 						},
@@ -263,12 +263,12 @@ func TestMultipleStmts(t *testing.T) {
 }
 
 func TestIfStmt(t *testing.T) {
-	source := sourceify(
+	src := sourceify(
 		`if false {`,
 		`	var x = 2`,
 		`}`,
 	)
-	p := cheatWithScanner(t, source)
+	p := cheatWithScanner(t, src)
 	program, err := p.Parse()
 	require.NoError(t, err)
 
@@ -277,25 +277,25 @@ func TestIfStmt(t *testing.T) {
 		Kind: ast.StmtKindIf,
 		Value: ast.StmtIf{
 			Cond: ast.Expr{
-				At:    token.NewLoc(0, 3),
+				At:    source.NewLoc(0, 3),
 				Kind:  ast.ExprKindBoolLit,
 				Value: ast.ExprBoolLit{Value: false},
 			},
 			Then: ast.BlockNode{
-				At: token.NewLoc(0, 9),
+				At: source.NewLoc(0, 9),
 				Stmts: []ast.Stmt{
 					{
-						At:    token.NewLoc(0, 10),
+						At:    source.NewLoc(0, 10),
 						Kind:  ast.StmtKindEmpty,
 						Value: ast.StmtEmpty{},
 					},
 					{
-						At:   token.NewLoc(1, 1),
+						At:   source.NewLoc(1, 1),
 						Kind: ast.StmtKindVarDecl,
 						Value: ast.StmtVarDecl{
-							Name: ast.IdentNode{At: token.NewLoc(1, 5), Value: "x"},
+							Name: ast.IdentNode{At: source.NewLoc(1, 5), Value: "x"},
 							Value: ast.Expr{
-								At:    token.NewLoc(1, 9),
+								At:    source.NewLoc(1, 9),
 								Kind:  ast.ExprKindIntLit,
 								Value: ast.ExprIntLit{Value: 2},
 							},
diff --git a/pkg/lang/scanner/errors.go b/pkg/lang/scanner/errors.go
index dd9aae3..6a6f9d9 100644
--- a/pkg/lang/scanner/errors.go
+++ b/pkg/lang/scanner/errors.go
@@ -7,15 +7,6 @@ var (
 	ErrUnclosedString  = errors.New("unclosed string")
 )
 
-type ErrUnexpectedChar struct {
-	Expected rune
-	Actual   rune
-}
-
-func (e ErrUnexpectedChar) Error() string {
-	return "unexpected character: expected " + string(e.Expected) + ", actual " + string(e.Actual)
-}
-
 type ErrUnknownChar struct {
 	Char rune
 }
diff --git a/pkg/lang/scanner/scanner.go b/pkg/lang/scanner/scanner.go
index 19e3462..2991083 100644
--- a/pkg/lang/scanner/scanner.go
+++ b/pkg/lang/scanner/scanner.go
@@ -1,29 +1,24 @@
 package scanner
 
 import (
-	"bufio"
-	"errors"
 	"io"
 	"jinx/pkg/lang/scanner/token"
+	"jinx/pkg/libs/source"
 	"strconv"
 	"strings"
 	"unicode"
 )
 
 type Scanner struct {
-	source *bufio.Reader
-	row    int
-	col    int
+	src    source.Walker
 	indent int
 
 	finished bool
 }
 
-func New(source io.Reader) *Scanner {
+func New(src io.Reader) *Scanner {
 	return &Scanner{
-		source:   bufio.NewReader(source),
-		row:      0,
-		col:      0,
+		src:      *source.NewWalker(src),
 		indent:   0,
 		finished: false,
 	}
@@ -62,14 +57,14 @@ func (s *Scanner) scanToken() (token.Token, error) {
 		return token.New(token.EOL, firstNewline, nil), nil
 	}
 
-	c, eof, err := s.peek()
+	c, eof, err := s.src.Peek()
 	if err != nil {
 		return token.Token{}, err
 	}
 
 	if eof {
 		s.finished = true
-		return token.Simple(token.EOF, s.loc()), nil
+		return token.Simple(token.EOF, s.src.Loc()), nil
 	}
 
 	if c == '"' {
@@ -80,8 +75,8 @@ func (s *Scanner) scanToken() (token.Token, error) {
 		return s.scanNumber()
 	}
 
-	loc := s.loc()
-	c, _, err = s.next()
+	loc := s.src.Loc()
+	c, _, err = s.src.Next()
 	if err != nil {
 		return token.Token{}, err
 	}
@@ -91,7 +86,7 @@ func (s *Scanner) scanToken() (token.Token, error) {
 	case '\n':
 		kind = token.EOL
 	case '=':
-		if cont, err := s.consume('='); cont && err == nil {
+		if cont, err := s.src.Consume('='); cont && err == nil {
 			kind = token.Eq
 		} else if cont && err != nil {
 			kind = token.Assign
@@ -110,7 +105,7 @@ func (s *Scanner) scanToken() (token.Token, error) {
 		kind = token.Percent
 
 	case '<':
-		if cont, err := s.consume('='); cont && err == nil {
+		if cont, err := s.src.Consume('='); cont && err == nil {
 			kind = token.Lte
 		} else if cont && err != nil {
 			kind = token.Lt
@@ -118,7 +113,7 @@ func (s *Scanner) scanToken() (token.Token, error) {
 			return token.Token{}, err
 		}
 	case '>':
-		if cont, err := s.consume('='); cont && err == nil {
+		if cont, err := s.src.Consume('='); cont && err == nil {
 			kind = token.Gte
 		} else if cont && err != nil {
 			kind = token.Gt
@@ -126,7 +121,7 @@ func (s *Scanner) scanToken() (token.Token, error) {
 			return token.Token{}, err
 		}
 	case '!':
-		if cont, err := s.consume('='); cont && err == nil {
+		if cont, err := s.src.Consume('='); cont && err == nil {
 			kind = token.Neq
 		} else if cont && err != nil {
 			kind = token.Bang
@@ -162,15 +157,15 @@ func (s *Scanner) scanToken() (token.Token, error) {
 }
 
 func (s *Scanner) scanString() (token.Token, error) {
-	loc := s.loc()
-	if _, err := s.consume('"'); err != nil {
+	loc := s.src.Loc()
+	if _, err := s.src.Consume('"'); err != nil {
 		return token.Token{}, err
 	}
 
 	var buf strings.Builder
 
 	for {
-		c, eof, err := s.next()
+		c, eof, err := s.src.Next()
 		if err != nil {
 			return token.Token{}, err
 		}
@@ -190,12 +185,12 @@ func (s *Scanner) scanString() (token.Token, error) {
 }
 
 func (s *Scanner) scanIdentifierOrKeyword() (token.Token, error) {
-	loc := s.loc()
+	loc := s.src.Loc()
 
 	var buf strings.Builder
 
 	for {
-		c, eof, err := s.peek()
+		c, eof, err := s.src.Peek()
 		if err != nil {
 			return token.Token{}, err
 		}
@@ -204,7 +199,7 @@ func (s *Scanner) scanIdentifierOrKeyword() (token.Token, error) {
 			break
 		}
 
-		if _, _, err = s.next(); err != nil {
+		if _, _, err = s.src.Next(); err != nil {
 			return token.Token{}, err
 		}
 
@@ -270,12 +265,12 @@ func (s *Scanner) scanIdentifierOrKeyword() (token.Token, error) {
 }
 
 func (s *Scanner) scanNumber() (token.Token, error) {
-	loc := s.loc()
+	loc := s.src.Loc()
 
 	var buf strings.Builder
 
 	for {
-		c, eof, err := s.peek()
+		c, eof, err := s.src.Peek()
 		if err != nil {
 			return token.Token{}, err
 		}
@@ -284,7 +279,7 @@ func (s *Scanner) scanNumber() (token.Token, error) {
 			break
 		}
 
-		if _, _, err = s.next(); err != nil {
+		if _, _, err = s.src.Next(); err != nil {
 			return token.Token{}, err
 		}
 
@@ -299,18 +294,18 @@ func (s *Scanner) scanNumber() (token.Token, error) {
 	return token.New(token.Int, loc, num), nil
 }
 
-func (s *Scanner) skipWhitespace() (bool, token.Loc, error) {
+func (s *Scanner) skipWhitespace() (bool, source.Loc, error) {
 	hadNewline := false
-	firstNewline := token.Loc{}
+	firstNewline := source.Loc{}
 
 	for {
-		c, eof, err := s.peek()
+		c, eof, err := s.src.Peek()
 		if err != nil {
-			return false, token.Loc{}, err
+			return false, source.Loc{}, err
 		}
 
 		if c == '\n' && !hadNewline {
-			firstNewline = s.loc()
+			firstNewline = s.src.Loc()
 			hadNewline = true
 		}
 
@@ -318,68 +313,10 @@ func (s *Scanner) skipWhitespace() (bool, token.Loc, error) {
 			break
 		}
 
-		if _, _, err = s.next(); err != nil {
-			return false, token.Loc{}, err
+		if _, _, err = s.src.Next(); err != nil {
+			return false, source.Loc{}, err
 		}
 	}
 
 	return hadNewline, firstNewline, nil
 }
-
-func (s *Scanner) loc() token.Loc {
-	return token.Loc{
-		Row: s.row,
-		Col: s.col,
-	}
-}
-
-func (s *Scanner) next() (rune, bool, error) {
-	r, _, err := s.source.ReadRune()
-	if err != nil {
-		if errors.Is(err, io.EOF) {
-			return 0, true, nil
-		}
-
-		return 0, false, err
-	}
-
-	if r == '\n' {
-		s.row++
-		s.col = 0
-	} else {
-		s.col++
-	}
-
-	return r, false, nil
-}
-
-func (s *Scanner) consume(want rune) (bool, error) {
-	c, _, err := s.next()
-	if err != nil {
-		return false, err
-	}
-
-	if c != want {
-		return true, ErrUnexpectedChar{
-			Expected: want,
-			Actual:   c,
-		}
-	}
-
-	return true, nil
-}
-
-func (s *Scanner) peek() (rune, bool, error) {
-	r, _, err := s.source.ReadRune()
-	defer s.source.UnreadRune()
-
-	if err != nil {
-		if errors.Is(err, io.EOF) {
-			return 0, true, nil
-		}
-
-		return 0, false, err
-	}
-
-	return r, false, nil
-}
diff --git a/pkg/lang/scanner/scanner_test.go b/pkg/lang/scanner/scanner_test.go
index 99e77fd..4df4b23 100644
--- a/pkg/lang/scanner/scanner_test.go
+++ b/pkg/lang/scanner/scanner_test.go
@@ -3,6 +3,7 @@ package scanner_test
 import (
 	"jinx/pkg/lang/scanner"
 	"jinx/pkg/lang/scanner/token"
+	"jinx/pkg/libs/source"
 	"strings"
 	"testing"
 
@@ -10,7 +11,7 @@ import (
 )
 
 func TestBasic(t *testing.T) {
-	source := `
+	src := `
 	fn basic() {
 		var x = 1
 		var y = x + 1
@@ -22,158 +23,158 @@ func TestBasic(t *testing.T) {
 		return true
 	}`
 
-	s := scanner.New(strings.NewReader(source))
+	s := scanner.New(strings.NewReader(src))
 
 	tokens, err := s.Scan()
 	require.NoError(t, err)
 
 	expected := []token.Token{
-		token.Simple(token.EOL, token.NewLoc(0, 0)),
-
-		token.Simple(token.KwFn, token.NewLoc(1, 1)),
-		token.New(token.Ident, token.NewLoc(1, 4), "basic"),
-		token.Simple(token.LParen, token.NewLoc(1, 9)),
-		token.Simple(token.RParen, token.NewLoc(1, 10)),
-		token.Simple(token.LBrace, token.NewLoc(1, 12)),
-		token.Simple(token.EOL, token.NewLoc(1, 13)),
-
-		token.Simple(token.KwVar, token.NewLoc(2, 2)),
-		token.New(token.Ident, token.NewLoc(2, 6), "x"),
-		token.Simple(token.Assign, token.NewLoc(2, 8)),
-		token.New(token.Int, token.NewLoc(2, 10), uint64(1)),
-		token.Simple(token.EOL, token.NewLoc(2, 11)),
-
-		token.Simple(token.KwVar, token.NewLoc(3, 2)),
-		token.New(token.Ident, token.NewLoc(3, 6), "y"),
-		token.Simple(token.Assign, token.NewLoc(3, 8)),
-		token.New(token.Ident, token.NewLoc(3, 10), "x"),
-		token.Simple(token.Plus, token.NewLoc(3, 12)),
-		token.New(token.Int, token.NewLoc(3, 14), uint64(1)),
-		token.Simple(token.EOL, token.NewLoc(3, 15)),
-
-		token.Simple(token.KwIf, token.NewLoc(4, 2)),
-		token.New(token.Ident, token.NewLoc(4, 5), "x"),
-		token.Simple(token.Lt, token.NewLoc(4, 7)),
-		token.New(token.Ident, token.NewLoc(4, 9), "y"),
-		token.Simple(token.LBrace, token.NewLoc(4, 11)),
-		token.Simple(token.EOL, token.NewLoc(4, 12)),
-
-		token.New(token.Ident, token.NewLoc(5, 3), "say"),
-		token.Simple(token.LParen, token.NewLoc(5, 6)),
-		token.New(token.String, token.NewLoc(5, 7), "x is less than y"),
-		token.Simple(token.RParen, token.NewLoc(5, 25)),
-		token.Simple(token.EOL, token.NewLoc(5, 26)),
-
-		token.Simple(token.RBrace, token.NewLoc(6, 2)),
-		token.Simple(token.KwElse, token.NewLoc(6, 4)),
-		token.Simple(token.LBrace, token.NewLoc(6, 9)),
-		token.Simple(token.EOL, token.NewLoc(6, 10)),
-
-		token.New(token.Ident, token.NewLoc(7, 3), "say"),
-		token.Simple(token.LParen, token.NewLoc(7, 6)),
-		token.New(token.String, token.NewLoc(7, 7), "x is greater than or equal to y"),
-		token.Simple(token.RParen, token.NewLoc(7, 40)),
-		token.Simple(token.EOL, token.NewLoc(7, 41)),
-
-		token.Simple(token.RBrace, token.NewLoc(8, 2)),
-		token.Simple(token.EOL, token.NewLoc(8, 3)),
-
-		token.Simple(token.KwReturn, token.NewLoc(9, 2)),
-		token.Simple(token.KwTrue, token.NewLoc(9, 9)),
-		token.Simple(token.EOL, token.NewLoc(9, 13)),
-
-		token.Simple(token.RBrace, token.NewLoc(10, 1)),
-
-		token.Simple(token.EOF, token.NewLoc(10, 2)),
+		token.Simple(token.EOL, source.NewLoc(0, 0)),
+
+		token.Simple(token.KwFn, source.NewLoc(1, 1)),
+		token.New(token.Ident, source.NewLoc(1, 4), "basic"),
+		token.Simple(token.LParen, source.NewLoc(1, 9)),
+		token.Simple(token.RParen, source.NewLoc(1, 10)),
+		token.Simple(token.LBrace, source.NewLoc(1, 12)),
+		token.Simple(token.EOL, source.NewLoc(1, 13)),
+
+		token.Simple(token.KwVar, source.NewLoc(2, 2)),
+		token.New(token.Ident, source.NewLoc(2, 6), "x"),
+		token.Simple(token.Assign, source.NewLoc(2, 8)),
+		token.New(token.Int, source.NewLoc(2, 10), uint64(1)),
+		token.Simple(token.EOL, source.NewLoc(2, 11)),
+
+		token.Simple(token.KwVar, source.NewLoc(3, 2)),
+		token.New(token.Ident, source.NewLoc(3, 6), "y"),
+		token.Simple(token.Assign, source.NewLoc(3, 8)),
+		token.New(token.Ident, source.NewLoc(3, 10), "x"),
+		token.Simple(token.Plus, source.NewLoc(3, 12)),
+		token.New(token.Int, source.NewLoc(3, 14), uint64(1)),
+		token.Simple(token.EOL, source.NewLoc(3, 15)),
+
+		token.Simple(token.KwIf, source.NewLoc(4, 2)),
+		token.New(token.Ident, source.NewLoc(4, 5), "x"),
+		token.Simple(token.Lt, source.NewLoc(4, 7)),
+		token.New(token.Ident, source.NewLoc(4, 9), "y"),
+		token.Simple(token.LBrace, source.NewLoc(4, 11)),
+		token.Simple(token.EOL, source.NewLoc(4, 12)),
+
+		token.New(token.Ident, source.NewLoc(5, 3), "say"),
+		token.Simple(token.LParen, source.NewLoc(5, 6)),
+		token.New(token.String, source.NewLoc(5, 7), "x is less than y"),
+		token.Simple(token.RParen, source.NewLoc(5, 25)),
+		token.Simple(token.EOL, source.NewLoc(5, 26)),
+
+		token.Simple(token.RBrace, source.NewLoc(6, 2)),
+		token.Simple(token.KwElse, source.NewLoc(6, 4)),
+		token.Simple(token.LBrace, source.NewLoc(6, 9)),
+		token.Simple(token.EOL, source.NewLoc(6, 10)),
+
+		token.New(token.Ident, source.NewLoc(7, 3), "say"),
+		token.Simple(token.LParen, source.NewLoc(7, 6)),
+		token.New(token.String, source.NewLoc(7, 7), "x is greater than or equal to y"),
+		token.Simple(token.RParen, source.NewLoc(7, 40)),
+		token.Simple(token.EOL, source.NewLoc(7, 41)),
+
+		token.Simple(token.RBrace, source.NewLoc(8, 2)),
+		token.Simple(token.EOL, source.NewLoc(8, 3)),
+
+		token.Simple(token.KwReturn, source.NewLoc(9, 2)),
+		token.Simple(token.KwTrue, source.NewLoc(9, 9)),
+		token.Simple(token.EOL, source.NewLoc(9, 13)),
+
+		token.Simple(token.RBrace, source.NewLoc(10, 1)),
+
+		token.Simple(token.EOF, source.NewLoc(10, 2)),
 	}
 
 	require.Equal(t, expected, tokens)
 }
 
 func TestTightIdent(t *testing.T) {
-	source := `say(message)`
+	src := `say(message)`
 
-	s := scanner.New(strings.NewReader(source))
+	s := scanner.New(strings.NewReader(src))
 
 	tokens, err := s.Scan()
 	require.NoError(t, err)
 
 	expected := []token.Token{
-		token.New(token.Ident, token.NewLoc(0, 0), "say"),
-		token.Simple(token.LParen, token.NewLoc(0, 3)),
-		token.New(token.Ident, token.NewLoc(0, 4), "message"),
-		token.Simple(token.RParen, token.NewLoc(0, 11)),
-		token.Simple(token.EOF, token.NewLoc(0, 12)),
+		token.New(token.Ident, source.NewLoc(0, 0), "say"),
+		token.Simple(token.LParen, source.NewLoc(0, 3)),
+		token.New(token.Ident, source.NewLoc(0, 4), "message"),
+		token.Simple(token.RParen, source.NewLoc(0, 11)),
+		token.Simple(token.EOF, source.NewLoc(0, 12)),
 	}
 
 	require.Equal(t, expected, tokens)
 }
 
 func TestTightNumber(t *testing.T) {
-	source := `1+2+3`
+	src := `1+2+3`
 
-	s := scanner.New(strings.NewReader(source))
+	s := scanner.New(strings.NewReader(src))
 
 	tokens, err := s.Scan()
 	require.NoError(t, err)
 
 	expected := []token.Token{
-		token.New(token.Int, token.NewLoc(0, 0), uint64(1)),
-		token.Simple(token.Plus, token.NewLoc(0, 1)),
-		token.New(token.Int, token.NewLoc(0, 2), uint64(2)),
-		token.Simple(token.Plus, token.NewLoc(0, 3)),
-		token.New(token.Int, token.NewLoc(0, 4), uint64(3)),
-		token.Simple(token.EOF, token.NewLoc(0, 5)),
+		token.New(token.Int, source.NewLoc(0, 0), uint64(1)),
+		token.Simple(token.Plus, source.NewLoc(0, 1)),
+		token.New(token.Int, source.NewLoc(0, 2), uint64(2)),
+		token.Simple(token.Plus, source.NewLoc(0, 3)),
+		token.New(token.Int, source.NewLoc(0, 4), uint64(3)),
+		token.Simple(token.EOF, source.NewLoc(0, 5)),
 	}
 
 	require.Equal(t, expected, tokens)
 }
 
 func TestNewlineStacking(t *testing.T) {
-	source := `
+	src := `
 	x
 
 
 	y
 	`
 
-	s := scanner.New(strings.NewReader(source))
+	s := scanner.New(strings.NewReader(src))
 
 	tokens, err := s.Scan()
 	require.NoError(t, err)
 
 	expected := []token.Token{
-		token.Simple(token.EOL, token.NewLoc(0, 0)),
-		token.New(token.Ident, token.NewLoc(1, 1), "x"),
-		token.Simple(token.EOL, token.NewLoc(1, 2)),
-		token.New(token.Ident, token.NewLoc(4, 1), "y"),
-		token.Simple(token.EOL, token.NewLoc(4, 2)),
-		token.Simple(token.EOF, token.NewLoc(5, 1)),
+		token.Simple(token.EOL, source.NewLoc(0, 0)),
+		token.New(token.Ident, source.NewLoc(1, 1), "x"),
+		token.Simple(token.EOL, source.NewLoc(1, 2)),
+		token.New(token.Ident, source.NewLoc(4, 1), "y"),
+		token.Simple(token.EOL, source.NewLoc(4, 2)),
+		token.Simple(token.EOF, source.NewLoc(5, 1)),
 	}
 
 	require.Equal(t, expected, tokens)
 }
 
 func TestEmojiInStrings(t *testing.T) {
-	source := `
+	src := `
 	say("🇺🇦" + "❤️!")
 	`
 
-	s := scanner.New(strings.NewReader(source))
+	s := scanner.New(strings.NewReader(src))
 
 	tokens, err := s.Scan()
 	require.NoError(t, err)
 
 	expected := []token.Token{
-		token.Simple(token.EOL, token.NewLoc(0, 0)),
-		token.New(token.Ident, token.NewLoc(1, 1), "say"),
-		token.Simple(token.LParen, token.NewLoc(1, 4)),
-		token.New(token.String, token.NewLoc(1, 5), "🇺🇦"),
-		token.Simple(token.Plus, token.NewLoc(1, 10)),
-		token.New(token.String, token.NewLoc(1, 12), "❤️!"),
-		token.Simple(token.RParen, token.NewLoc(1, 17)),
-		token.Simple(token.EOL, token.NewLoc(1, 18)),
-		token.Simple(token.EOF, token.NewLoc(2, 1)),
+		token.Simple(token.EOL, source.NewLoc(0, 0)),
+		token.New(token.Ident, source.NewLoc(1, 1), "say"),
+		token.Simple(token.LParen, source.NewLoc(1, 4)),
+		token.New(token.String, source.NewLoc(1, 5), "🇺🇦"),
+		token.Simple(token.Plus, source.NewLoc(1, 10)),
+		token.New(token.String, source.NewLoc(1, 12), "❤️!"),
+		token.Simple(token.RParen, source.NewLoc(1, 17)),
+		token.Simple(token.EOL, source.NewLoc(1, 18)),
+		token.Simple(token.EOF, source.NewLoc(2, 1)),
 	}
 
 	require.Equal(t, expected, tokens)
diff --git a/pkg/lang/scanner/token/token.go b/pkg/lang/scanner/token/token.go
index 1ccd864..a39df8a 100644
--- a/pkg/lang/scanner/token/token.go
+++ b/pkg/lang/scanner/token/token.go
@@ -1,19 +1,21 @@
 package token
 
+import "jinx/pkg/libs/source"
+
 type Token struct {
 	Kind TokenKind
-	At   Loc
+	At   source.Loc
 	Data any
 }
 
-func Simple(kind TokenKind, at Loc) Token {
+func Simple(kind TokenKind, at source.Loc) Token {
 	return Token{
 		Kind: kind,
 		At:   at,
 	}
 }
 
-func New(kind TokenKind, at Loc, data any) Token {
+func New(kind TokenKind, at source.Loc, data any) Token {
 	return Token{
 		Kind: kind,
 		At:   at,
diff --git a/pkg/libs/source/errors.go b/pkg/libs/source/errors.go
new file mode 100644
index 0000000..cfcd9e3
--- /dev/null
+++ b/pkg/libs/source/errors.go
@@ -0,0 +1,10 @@
+package source
+
+type ErrUnexpectedChar struct {
+	Expected rune
+	Actual   rune
+}
+
+func (e ErrUnexpectedChar) Error() string {
+	return "unexpected character: expected " + string(e.Expected) + ", actual " + string(e.Actual)
+}
diff --git a/pkg/lang/scanner/token/loc.go b/pkg/libs/source/loc.go
index 7936cba..3089d3b 100644
--- a/pkg/lang/scanner/token/loc.go
+++ b/pkg/libs/source/loc.go
@@ -1,4 +1,4 @@
-package token
+package source
 
 type Loc struct {
 	Row int
diff --git a/pkg/libs/source/walker.go b/pkg/libs/source/walker.go
new file mode 100644
index 0000000..aeac82a
--- /dev/null
+++ b/pkg/libs/source/walker.go
@@ -0,0 +1,80 @@
+package source
+
+import (
+	"bufio"
+	"errors"
+	"io"
+)
+
+type Walker struct {
+	source *bufio.Reader
+
+	row int
+	col int
+}
+
+func NewWalker(source io.Reader) *Walker {
+	return &Walker{
+		source: bufio.NewReader(source),
+		row:    0,
+		col:    0,
+	}
+}
+
+func (w *Walker) Loc() Loc {
+	return Loc{
+		Row: w.row,
+		Col: w.col,
+	}
+}
+
+func (w *Walker) Next() (rune, bool, error) {
+	r, _, err := w.source.ReadRune()
+	if err != nil {
+		if errors.Is(err, io.EOF) {
+			return 0, true, nil
+		}
+
+		return 0, false, err
+	}
+
+	if r == '\n' {
+		w.row++
+		w.col = 0
+	} else {
+		w.col++
+	}
+
+	return r, false, nil
+}
+
+func (w *Walker) Consume(want rune) (bool, error) {
+	c, _, err := w.Next()
+	if err != nil {
+		return false, err
+	}
+
+	if c != want {
+		return true, ErrUnexpectedChar{
+			Expected: want,
+			Actual:   c,
+		}
+	}
+
+	return true, nil
+}
+
+func (w *Walker) Peek() (rune, bool, error) {
+	r, _, err := w.source.ReadRune()
+	defer w.source.UnreadRune()
+
+	if err != nil {
+		if errors.Is(err, io.EOF) {
+			return 0, true, nil
+		}
+
+		return 0, false, err
+	}
+
+	return r, false, nil
+}