Add Scanner
parent
5063cdbf40
commit
d8050c1699
@ -0,0 +1,180 @@
|
||||
package lox
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestRun(t *testing.T) {
|
||||
old := os.Stdout // keep backup of the real stdout
|
||||
r, w, err := os.Pipe()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
os.Stdout = w
|
||||
|
||||
outC := make(chan string)
|
||||
// copy the output in a separate goroutine so printing can't block indefinitely
|
||||
go func() {
|
||||
var buf bytes.Buffer
|
||||
io.Copy(&buf, r)
|
||||
outC <- buf.String()
|
||||
}()
|
||||
|
||||
source := "print('Hello, World!');"
|
||||
run(source)
|
||||
|
||||
// back to normal state
|
||||
w.Close()
|
||||
os.Stdout = old // restoring the real stdout
|
||||
out := <-outC
|
||||
|
||||
// reading our temp stdout
|
||||
expected := source + "\n"
|
||||
if out != expected {
|
||||
t.Errorf("run() = %v; want %v", out, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunFile(t *testing.T) {
|
||||
old := os.Stdout // keep backup of the real stdout
|
||||
r, w, err := os.Pipe()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
os.Stdout = w
|
||||
|
||||
outC := make(chan string)
|
||||
// copy the output in a separate goroutine so printing can't block indefinitely
|
||||
go func() {
|
||||
var buf bytes.Buffer
|
||||
io.Copy(&buf, r)
|
||||
outC <- buf.String()
|
||||
}()
|
||||
|
||||
// Create a temporary file with some content
|
||||
tmpfile, err := os.CreateTemp("", "example.*.txt")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer os.Remove(tmpfile.Name())
|
||||
|
||||
content := "print('Hello, World!');"
|
||||
if _, err := tmpfile.Write([]byte(content)); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := tmpfile.Close(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
RunFile(tmpfile.Name())
|
||||
|
||||
// back to normal state
|
||||
w.Close()
|
||||
os.Stdout = old // restoring the real stdout
|
||||
out := <-outC
|
||||
|
||||
// reading our temp stdout
|
||||
expected := "print('Hello, World!');\n"
|
||||
if out != expected {
|
||||
t.Errorf("RunFile() = %v; want %v", out, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestError(t *testing.T) {
|
||||
old := os.Stdout // keep backup of the real stdout
|
||||
r, w, err := os.Pipe()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
os.Stdout = w
|
||||
|
||||
outC := make(chan string)
|
||||
// copy the output in a separate goroutine so printing can't block indefinitely
|
||||
go func() {
|
||||
var buf bytes.Buffer
|
||||
io.Copy(&buf, r)
|
||||
outC <- buf.String()
|
||||
}()
|
||||
|
||||
line := 1
|
||||
message := "Unexpected character."
|
||||
Error(line, message)
|
||||
|
||||
// back to normal state
|
||||
w.Close()
|
||||
os.Stdout = old // restoring the real stdout
|
||||
out := <-outC
|
||||
|
||||
// reading our temp stdout
|
||||
expected := fmt.Sprintf("[line %d] Error : %s\n", line, message)
|
||||
if out != expected {
|
||||
t.Errorf("Error() = %v; want %v", out, expected)
|
||||
}
|
||||
}
|
||||
|
||||
func TestReport(t *testing.T) {
|
||||
old := os.Stdout // keep backup of the real stdout
|
||||
r, w, err := os.Pipe()
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
os.Stdout = w
|
||||
|
||||
outC := make(chan string)
|
||||
// copy the output in a separate goroutine so printing can't block indefinitely
|
||||
go func() {
|
||||
var buf bytes.Buffer
|
||||
io.Copy(&buf, r)
|
||||
outC <- buf.String()
|
||||
}()
|
||||
|
||||
line := 1
|
||||
where := "at 'foo'"
|
||||
message := "Unexpected character."
|
||||
report(line, where, message)
|
||||
|
||||
// back to normal state
|
||||
w.Close()
|
||||
os.Stdout = old // restoring the real stdout
|
||||
out := <-outC
|
||||
|
||||
// reading our temp stdout
|
||||
expected := fmt.Sprintf("[line %d] Error %s: %s\n", line, where, message)
|
||||
if out != expected {
|
||||
t.Errorf("report() = %v; want %v", out, expected)
|
||||
}
|
||||
}
|
||||
func TestRunPrompt(t *testing.T) {
|
||||
oldStdin := os.Stdin
|
||||
oldStdout := os.Stdout
|
||||
rIn, wIn, _ := os.Pipe()
|
||||
rOut, wOut, _ := os.Pipe()
|
||||
os.Stdin = rIn
|
||||
os.Stdout = wOut
|
||||
|
||||
outC := make(chan string)
|
||||
go func() {
|
||||
var buf bytes.Buffer
|
||||
io.Copy(&buf, rOut)
|
||||
outC <- buf.String()
|
||||
}()
|
||||
|
||||
input := "print('Hello, World!');\n\n"
|
||||
wIn.Write([]byte(input))
|
||||
wIn.Close()
|
||||
|
||||
RunPrompt()
|
||||
|
||||
wOut.Close()
|
||||
os.Stdin = oldStdin
|
||||
os.Stdout = oldStdout
|
||||
out := <-outC
|
||||
|
||||
expected := "> print('Hello, World!');\n\n> "
|
||||
if out != expected {
|
||||
t.Errorf("RunPrompt() = %v; want %v", out, expected)
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,244 @@
|
||||
package scanner
|
||||
|
||||
import (
|
||||
"golox/lox"
|
||||
"golox/token"
|
||||
"strconv"
|
||||
)
|
||||
|
||||
// Scanner is a struct that holds the source code, the start and current position
|
||||
// of the scanner, the current line, and the tokens that have been scanned.
|
||||
type Scanner struct {
|
||||
source string
|
||||
start int
|
||||
current int
|
||||
line int
|
||||
tokens []token.Token
|
||||
}
|
||||
|
||||
// New creates a new Scanner struct with the given source code.
|
||||
func New(source string) *Scanner {
|
||||
return &Scanner{
|
||||
source: source, // The source code to scan.
|
||||
start: 0, // The start position of the scanner.
|
||||
current: 0, // The current position of the scanner.
|
||||
line: 1, // The current line number.
|
||||
tokens: []token.Token{}, // The tokens that have been scanned.
|
||||
}
|
||||
}
|
||||
|
||||
// scanToken scans the next token in the source code.
|
||||
func (s *Scanner) ScanTokens() []token.Token {
|
||||
for !s.isAtEnd() {
|
||||
s.start = s.current
|
||||
s.scanToken()
|
||||
}
|
||||
|
||||
s.tokens = append(s.tokens, token.New(token.EOF, "", nil, s.line))
|
||||
return s.tokens
|
||||
}
|
||||
|
||||
// isAtEnd returns true if the scanner has reached the end of the source code.
|
||||
func (s *Scanner) isAtEnd() bool {
|
||||
return s.current >= len(s.source)
|
||||
}
|
||||
|
||||
// scanToken scans the next token in the source code.
|
||||
func (s *Scanner) scanToken() {
|
||||
c := s.advance()
|
||||
|
||||
switch c {
|
||||
case '(':
|
||||
s.addToken(token.LEFT_PAREN)
|
||||
case ')':
|
||||
s.addToken(token.RIGHT_PAREN)
|
||||
case '{':
|
||||
s.addToken(token.LEFT_BRACE)
|
||||
case '}':
|
||||
s.addToken(token.RIGHT_BRACE)
|
||||
case ',':
|
||||
s.addToken(token.COMMA)
|
||||
case '.':
|
||||
s.addToken(token.DOT)
|
||||
case '-':
|
||||
s.addToken(token.MINUS)
|
||||
case '+':
|
||||
s.addToken(token.PLUS)
|
||||
case ';':
|
||||
s.addToken(token.SEMICOLON)
|
||||
case '*':
|
||||
s.addToken(token.STAR)
|
||||
case '!':
|
||||
if s.match('=') {
|
||||
s.addToken(token.BANG_EQUAL)
|
||||
} else {
|
||||
s.addToken(token.BANG)
|
||||
}
|
||||
case '=':
|
||||
if s.match('=') {
|
||||
s.addToken(token.EQUAL_EQUAL)
|
||||
} else {
|
||||
s.addToken(token.EQUAL)
|
||||
}
|
||||
case '<':
|
||||
if s.match('=') {
|
||||
s.addToken(token.LESS_EQUAL)
|
||||
} else {
|
||||
s.addToken(token.LESS)
|
||||
}
|
||||
case '>':
|
||||
if s.match('=') {
|
||||
s.addToken(token.GREATER_EQUAL)
|
||||
} else {
|
||||
s.addToken(token.GREATER)
|
||||
}
|
||||
case '/':
|
||||
if s.match('/') {
|
||||
// A comment goes until the end of the line.
|
||||
for s.peek() != '\n' && !s.isAtEnd() {
|
||||
s.advance()
|
||||
}
|
||||
} else {
|
||||
s.addToken(token.SLASH)
|
||||
}
|
||||
case ' ', '\r', '\t':
|
||||
// Ignore whitespace.
|
||||
case '\n':
|
||||
s.line++
|
||||
case '"':
|
||||
s.string()
|
||||
case '0', '1', '2', '3', '4', '5', '6', '7', '8', '9':
|
||||
s.number()
|
||||
default:
|
||||
if isAlpha(c) {
|
||||
s.identifier()
|
||||
} else {
|
||||
lox.Error(s.line, "Unexpected character.")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// identifier scans an identifier token.
|
||||
func (s *Scanner) identifier() {
|
||||
for isAlpha(s.peek()) || isDigit(s.peek()) {
|
||||
s.advance()
|
||||
}
|
||||
|
||||
text := s.source[s.start:s.current]
|
||||
|
||||
// Get the token type for the identifier (keyword or identifier).
|
||||
t := token.LookupKeyword(text)
|
||||
|
||||
s.addToken(t)
|
||||
}
|
||||
|
||||
// number scans a number token.
|
||||
func (s *Scanner) number() {
|
||||
for isDigit(s.peek()) {
|
||||
s.advance()
|
||||
}
|
||||
|
||||
// Look for a fractional part.
|
||||
if s.peek() == '.' && isDigit(s.peekNext()) {
|
||||
// Consume the "."
|
||||
s.advance()
|
||||
|
||||
for isDigit(s.peek()) {
|
||||
s.advance()
|
||||
}
|
||||
}
|
||||
|
||||
f, err := strconv.ParseFloat(s.source[s.start:s.current], 64)
|
||||
|
||||
if err != nil {
|
||||
lox.Error(s.line, "Could not parse number.")
|
||||
return
|
||||
}
|
||||
|
||||
s.addTokenLiteral(token.NUMBER, f)
|
||||
}
|
||||
|
||||
// string scans a string token.
|
||||
func (s *Scanner) string() {
|
||||
for s.peek() != '"' && !s.isAtEnd() {
|
||||
if s.peek() == '\n' {
|
||||
s.line++
|
||||
}
|
||||
s.advance()
|
||||
}
|
||||
|
||||
if s.isAtEnd() {
|
||||
lox.Error(s.line, "Unterminated string.")
|
||||
return
|
||||
}
|
||||
|
||||
// The closing ".
|
||||
s.advance()
|
||||
|
||||
// Trim the surrounding quotes.
|
||||
value := s.source[s.start+1 : s.current-1]
|
||||
s.addTokenLiteral(token.STRING, value)
|
||||
}
|
||||
|
||||
// match returns true if the current character matches the expected character.
|
||||
// If the current character matches the expected character, the character is consumed.
|
||||
// If not, there is no side effect.
|
||||
func (s *Scanner) match(expected byte) bool {
|
||||
if s.isAtEnd() {
|
||||
return false
|
||||
}
|
||||
|
||||
if s.source[s.current] != expected {
|
||||
return false
|
||||
}
|
||||
|
||||
s.current++
|
||||
return true
|
||||
}
|
||||
|
||||
// peek returns the character at the current position without consuming it.
|
||||
func (s *Scanner) peek() byte {
|
||||
if s.isAtEnd() {
|
||||
return '\000'
|
||||
}
|
||||
|
||||
return s.source[s.current]
|
||||
}
|
||||
|
||||
// peekNext returns the character at the next position without consuming it.
|
||||
func (s *Scanner) peekNext() byte {
|
||||
if s.current+1 >= len(s.source) {
|
||||
return '\000'
|
||||
}
|
||||
|
||||
return s.source[s.current+1]
|
||||
}
|
||||
|
||||
// isAlpha returns true if the character is an alphabetic character.
|
||||
func isAlpha(c byte) bool {
|
||||
return (c >= 'a' && c <= 'z') || (c >= 'A' && c <= 'Z') || c == '_'
|
||||
}
|
||||
|
||||
// isDigit returns true if the character is a digit.
|
||||
func isDigit(c byte) bool {
|
||||
return c >= '0' && c <= '9'
|
||||
}
|
||||
|
||||
// advance increments the current position of the scanner and
|
||||
// returns the character at that position.
|
||||
func (s *Scanner) advance() byte {
|
||||
c := s.source[s.current]
|
||||
s.current++
|
||||
return c
|
||||
}
|
||||
|
||||
// addToken adds a token to the list of tokens.
|
||||
func (s *Scanner) addToken(t token.TokenType) {
|
||||
s.addTokenLiteral(t, nil)
|
||||
}
|
||||
|
||||
// addTokenLiteral adds a token with a literal value to the list of tokens.
|
||||
func (s *Scanner) addTokenLiteral(t token.TokenType, literal interface{}) {
|
||||
text := s.source[s.start:s.current] // This selects a half-open range which includes the first element, but excludes the last one
|
||||
s.tokens = append(s.tokens, token.New(t, text, literal, s.line))
|
||||
}
|
||||
@ -0,0 +1,381 @@
|
||||
package scanner
|
||||
|
||||
import (
|
||||
"golox/token"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestScanTokens(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
source string
|
||||
tokens []token.TokenType
|
||||
}{
|
||||
{
|
||||
name: "Single character tokens",
|
||||
source: "(){}.,-+;*",
|
||||
tokens: []token.TokenType{
|
||||
token.LEFT_PAREN, token.RIGHT_PAREN, token.LEFT_BRACE, token.RIGHT_BRACE,
|
||||
token.DOT, token.COMMA, token.MINUS, token.PLUS, token.SEMICOLON, token.STAR,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Operators",
|
||||
source: "! != = == < <= > >=",
|
||||
tokens: []token.TokenType{
|
||||
token.BANG, token.BANG_EQUAL, token.EQUAL, token.EQUAL_EQUAL,
|
||||
token.LESS, token.LESS_EQUAL, token.GREATER, token.GREATER_EQUAL,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Comments",
|
||||
source: "// this is a comment\n+",
|
||||
tokens: []token.TokenType{
|
||||
token.PLUS,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Whitespace",
|
||||
source: " \r\t\n",
|
||||
tokens: []token.TokenType{},
|
||||
},
|
||||
{
|
||||
name: "String literals",
|
||||
source: `"hello world"`,
|
||||
tokens: []token.TokenType{
|
||||
token.STRING,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Number literals",
|
||||
source: "123 45.67",
|
||||
tokens: []token.TokenType{
|
||||
token.NUMBER, token.NUMBER,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Identifiers and keywords",
|
||||
source: "and class else false for fun if nil or print return super this true var while",
|
||||
tokens: []token.TokenType{
|
||||
token.AND, token.CLASS, token.ELSE, token.FALSE, token.FOR, token.FUN, token.IF,
|
||||
token.NIL, token.OR, token.PRINT, token.RETURN, token.SUPER, token.THIS, token.TRUE,
|
||||
token.VAR, token.WHILE,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "Unterminated string",
|
||||
source: `"unterminated string`,
|
||||
tokens: []token.TokenType{},
|
||||
},
|
||||
{
|
||||
name: "Unexpected character",
|
||||
source: "@",
|
||||
tokens: []token.TokenType{},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
scanner := New(tt.source)
|
||||
tokens := scanner.ScanTokens()
|
||||
|
||||
if len(tokens) != len(tt.tokens)+1 { // +1 for EOF token
|
||||
t.Fatalf("expected %d tokens, got %d", len(tt.tokens)+1, len(tokens))
|
||||
}
|
||||
|
||||
for i, tokenType := range tt.tokens {
|
||||
if tokens[i].Type != tokenType {
|
||||
t.Errorf("expected token %v, got %v", tokenType, tokens[i].Type)
|
||||
}
|
||||
}
|
||||
|
||||
if tokens[len(tokens)-1].Type != token.EOF {
|
||||
t.Errorf("expected EOF token, got %v", tokens[len(tokens)-1].Type)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsAtEnd(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
source string
|
||||
expected bool
|
||||
}{
|
||||
{"Not at end", "abc", false},
|
||||
{"At end", "", true},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
scanner := New(tt.source)
|
||||
if got := scanner.isAtEnd(); got != tt.expected {
|
||||
t.Errorf("expected %v, got %v", tt.expected, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestMatch(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
source string
|
||||
expected bool
|
||||
char byte
|
||||
}{
|
||||
{"Match character", "=", true, '='},
|
||||
{"No match character", "!", false, '='},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
scanner := New(tt.source)
|
||||
if got := scanner.match(tt.char); got != tt.expected {
|
||||
t.Errorf("expected %v, got %v", tt.expected, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPeek(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
source string
|
||||
expected byte
|
||||
}{
|
||||
{"Peek character", "abc", 'a'},
|
||||
{"Peek at end", "", '\000'},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
scanner := New(tt.source)
|
||||
if got := scanner.peek(); got != tt.expected {
|
||||
t.Errorf("expected %v, got %v", tt.expected, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestPeekNext(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
source string
|
||||
expected byte
|
||||
}{
|
||||
{"Peek next character", "abc", 'b'},
|
||||
{"Peek next at end", "a", '\000'},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
scanner := New(tt.source)
|
||||
if got := scanner.peekNext(); got != tt.expected {
|
||||
t.Errorf("expected %v, got %v", tt.expected, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestAdvance(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
source string
|
||||
expected byte
|
||||
}{
|
||||
{"Advance character", "abc", 'a'},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
scanner := New(tt.source)
|
||||
if got := scanner.advance(); got != tt.expected {
|
||||
t.Errorf("expected %v, got %v", tt.expected, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsAlpha(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
char byte
|
||||
expected bool
|
||||
}{
|
||||
{"Is alpha", 'a', true},
|
||||
{"Is not alpha", '1', false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := isAlpha(tt.char); got != tt.expected {
|
||||
t.Errorf("expected %v, got %v", tt.expected, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIsDigit(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
char byte
|
||||
expected bool
|
||||
}{
|
||||
{"Is digit", '1', true},
|
||||
{"Is not digit", 'a', false},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
if got := isDigit(tt.char); got != tt.expected {
|
||||
t.Errorf("expected %v, got %v", tt.expected, got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestString(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
source string
|
||||
expected string
|
||||
}{
|
||||
{"Valid string", `"hello"`, "hello"},
|
||||
{"Unterminated string", `"hello`, ""},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
scanner := New(tt.source)
|
||||
scanner.advance() // Move to the first character of the string
|
||||
scanner.string()
|
||||
if tt.expected == "" {
|
||||
if len(scanner.tokens) != 0 {
|
||||
t.Errorf("expected no tokens, got %d", len(scanner.tokens))
|
||||
}
|
||||
} else {
|
||||
if len(scanner.tokens) != 1 {
|
||||
t.Errorf("expected 1 token, got %d", len(scanner.tokens))
|
||||
} else if scanner.tokens[0].Literal != tt.expected {
|
||||
t.Errorf("expected %v, got %v", tt.expected, scanner.tokens[0].Literal)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestNumber(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
source string
|
||||
expected float64
|
||||
}{
|
||||
{"Integer number", "123", 123},
|
||||
{"Floating point number", "45.67", 45.67},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
scanner := New(tt.source)
|
||||
scanner.number()
|
||||
if tt.expected == 0 {
|
||||
if len(scanner.tokens) != 0 {
|
||||
t.Errorf("expected no tokens, got %d", len(scanner.tokens))
|
||||
}
|
||||
} else {
|
||||
if len(scanner.tokens) != 1 {
|
||||
t.Errorf("expected 1 token, got %d", len(scanner.tokens))
|
||||
} else if scanner.tokens[0].Literal != tt.expected {
|
||||
t.Errorf("expected %v, got %v", tt.expected, scanner.tokens[0].Literal)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestScanToken(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
source string
|
||||
expected token.TokenType
|
||||
}{
|
||||
{"Left paren", "(", token.LEFT_PAREN},
|
||||
{"Right paren", ")", token.RIGHT_PAREN},
|
||||
{"Left brace", "{", token.LEFT_BRACE},
|
||||
{"Right brace", "}", token.RIGHT_BRACE},
|
||||
{"Comma", ",", token.COMMA},
|
||||
{"Dot", ".", token.DOT},
|
||||
{"Minus", "-", token.MINUS},
|
||||
{"Plus", "+", token.PLUS},
|
||||
{"Semicolon", ";", token.SEMICOLON},
|
||||
{"Star", "*", token.STAR},
|
||||
{"Bang", "!", token.BANG},
|
||||
{"Bang equal", "!=", token.BANG_EQUAL},
|
||||
{"Equal", "=", token.EQUAL},
|
||||
{"Equal equal", "==", token.EQUAL_EQUAL},
|
||||
{"Less", "<", token.LESS},
|
||||
{"Less equal", "<=", token.LESS_EQUAL},
|
||||
{"Greater", ">", token.GREATER},
|
||||
{"Greater equal", ">=", token.GREATER_EQUAL},
|
||||
{"Slash", "/", token.SLASH},
|
||||
{"Comment", "// comment\n", token.EOF},
|
||||
{"Whitespace", " \r\t\n", token.EOF},
|
||||
{"String", `"hello"`, token.STRING},
|
||||
{"Number", "123", token.NUMBER},
|
||||
{"Identifier", "var", token.VAR},
|
||||
{"Unexpected character", "@", token.EOF},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
scanner := New(tt.source)
|
||||
scanner.scanToken()
|
||||
if len(scanner.tokens) > 0 {
|
||||
if scanner.tokens[0].Type != tt.expected {
|
||||
t.Errorf("expected %v, got %v", tt.expected, scanner.tokens[0].Type)
|
||||
}
|
||||
} else if tt.expected != token.EOF {
|
||||
t.Errorf("expected %v, got no tokens", tt.expected)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestIdentifier(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
source string
|
||||
expected token.TokenType
|
||||
}{
|
||||
{"Keyword and", "and", token.AND},
|
||||
{"Keyword class", "class", token.CLASS},
|
||||
{"Keyword else", "else", token.ELSE},
|
||||
{"Keyword false", "false", token.FALSE},
|
||||
{"Keyword for", "for", token.FOR},
|
||||
{"Keyword fun", "fun", token.FUN},
|
||||
{"Keyword if", "if", token.IF},
|
||||
{"Keyword nil", "nil", token.NIL},
|
||||
{"Keyword or", "or", token.OR},
|
||||
{"Keyword print", "print", token.PRINT},
|
||||
{"Keyword return", "return", token.RETURN},
|
||||
{"Keyword super", "super", token.SUPER},
|
||||
{"Keyword this", "this", token.THIS},
|
||||
{"Keyword true", "true", token.TRUE},
|
||||
{"Keyword var", "var", token.VAR},
|
||||
{"Keyword while", "while", token.WHILE},
|
||||
{"Identifier", "myVar", token.IDENTIFIER},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
scanner := New(tt.source)
|
||||
scanner.identifier()
|
||||
if len(scanner.tokens) != 1 {
|
||||
t.Fatalf("expected 1 token, got %d", len(scanner.tokens))
|
||||
}
|
||||
if scanner.tokens[0].Type != tt.expected {
|
||||
t.Errorf("expected %v, got %v", tt.expected, scanner.tokens[0].Type)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,102 @@
|
||||
package token
|
||||
|
||||
// TokenType represents the type of a token.
|
||||
type TokenType int
|
||||
|
||||
// Token types.
|
||||
const (
|
||||
// Single-character tokens.
|
||||
LEFT_PAREN TokenType = iota
|
||||
RIGHT_PAREN
|
||||
LEFT_BRACE
|
||||
RIGHT_BRACE
|
||||
COMMA
|
||||
DOT
|
||||
MINUS
|
||||
PLUS
|
||||
SEMICOLON
|
||||
SLASH
|
||||
STAR
|
||||
|
||||
// One or two character tokens.
|
||||
BANG
|
||||
BANG_EQUAL
|
||||
EQUAL
|
||||
EQUAL_EQUAL
|
||||
GREATER
|
||||
GREATER_EQUAL
|
||||
LESS
|
||||
LESS_EQUAL
|
||||
|
||||
// Literals.
|
||||
IDENTIFIER
|
||||
STRING
|
||||
NUMBER
|
||||
|
||||
// Keywords.
|
||||
AND
|
||||
CLASS
|
||||
ELSE
|
||||
FALSE
|
||||
FUN
|
||||
FOR
|
||||
IF
|
||||
NIL
|
||||
OR
|
||||
PRINT
|
||||
RETURN
|
||||
SUPER
|
||||
THIS
|
||||
TRUE
|
||||
VAR
|
||||
WHILE
|
||||
|
||||
EOF
|
||||
)
|
||||
|
||||
// Token represents a token in the source code.
|
||||
type Token struct {
|
||||
Type TokenType
|
||||
Lexeme string
|
||||
Literal interface{}
|
||||
Line int
|
||||
}
|
||||
|
||||
// New creates a new Token.
|
||||
func New(t TokenType, lexeme string, literal interface{}, line int) Token {
|
||||
return Token{t, lexeme, literal, line}
|
||||
}
|
||||
|
||||
// String returns the string representation of the token.
|
||||
func (t Token) String() string {
|
||||
return t.Lexeme
|
||||
}
|
||||
|
||||
// keywords maps keywords to their respective TokenType.
|
||||
var keywords = map[string]TokenType{
|
||||
"and": AND,
|
||||
"class": CLASS,
|
||||
"else": ELSE,
|
||||
"false": FALSE,
|
||||
"for": FOR,
|
||||
"fun": FUN,
|
||||
"if": IF,
|
||||
"nil": NIL,
|
||||
"or": OR,
|
||||
"print": PRINT,
|
||||
"return": RETURN,
|
||||
"super": SUPER,
|
||||
"this": THIS,
|
||||
"true": TRUE,
|
||||
"var": VAR,
|
||||
"while": WHILE,
|
||||
}
|
||||
|
||||
// LookupKeyword returns the TokenType for the given identifier.
|
||||
// If the identifier is not a keyword, it returns IDENTIFIER.
|
||||
func LookupKeyword(identifier string) TokenType {
|
||||
if t, ok := keywords[identifier]; ok {
|
||||
return t
|
||||
}
|
||||
return IDENTIFIER
|
||||
}
|
||||
@ -0,0 +1,76 @@
|
||||
package token
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestTokenCreation(t *testing.T) {
|
||||
tests := []struct {
|
||||
tokenType TokenType
|
||||
lexeme string
|
||||
literal interface{}
|
||||
line int
|
||||
}{
|
||||
{LEFT_PAREN, "(", nil, 1},
|
||||
{RIGHT_PAREN, ")", nil, 1},
|
||||
{IDENTIFIER, "foo", nil, 1},
|
||||
{STRING, "\"bar\"", "bar", 1},
|
||||
{NUMBER, "123", 123, 1},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
token := New(tt.tokenType, tt.lexeme, tt.literal, tt.line)
|
||||
if token.Type != tt.tokenType {
|
||||
t.Errorf("expected token type %v, got %v", tt.tokenType, token.Type)
|
||||
}
|
||||
if token.Lexeme != tt.lexeme {
|
||||
t.Errorf("expected lexeme %v, got %v", tt.lexeme, token.Lexeme)
|
||||
}
|
||||
if token.Literal != tt.literal {
|
||||
t.Errorf("expected literal %v, got %v", tt.literal, token.Literal)
|
||||
}
|
||||
if token.Line != tt.line {
|
||||
t.Errorf("expected line %v, got %v", tt.line, token.Line)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestTokenString(t *testing.T) {
|
||||
token := New(IDENTIFIER, "foo", nil, 1)
|
||||
expected := "foo"
|
||||
if token.String() != expected {
|
||||
t.Errorf("expected %v, got %v", expected, token.String())
|
||||
}
|
||||
}
|
||||
|
||||
func TestLookupKeyword(t *testing.T) {
|
||||
tests := []struct {
|
||||
identifier string
|
||||
expected TokenType
|
||||
}{
|
||||
{"and", AND},
|
||||
{"class", CLASS},
|
||||
{"else", ELSE},
|
||||
{"false", FALSE},
|
||||
{"for", FOR},
|
||||
{"fun", FUN},
|
||||
{"if", IF},
|
||||
{"nil", NIL},
|
||||
{"or", OR},
|
||||
{"print", PRINT},
|
||||
{"return", RETURN},
|
||||
{"super", SUPER},
|
||||
{"this", THIS},
|
||||
{"true", TRUE},
|
||||
{"var", VAR},
|
||||
{"while", WHILE},
|
||||
{"foobar", IDENTIFIER},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
tokenType := LookupKeyword(tt.identifier)
|
||||
if tokenType != tt.expected {
|
||||
t.Errorf("expected %v, got %v", tt.expected, tokenType)
|
||||
}
|
||||
}
|
||||
}
|
||||
Loading…
Reference in New Issue