You cannot select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

699 lines
17 KiB
Go

// FILE: interpreter_test.go
package interpreter
import (
"bytes"
"golox/ast"
"golox/errors"
"golox/token"
"io"
"os"
"testing"
)
func TestInterpretLiteralExpr(t *testing.T) {
i := New(errors.NewMockErrorLogger())
literal := &ast.LiteralExpr{Value: 42}
result := i.VisitLiteralExpr(literal)
if result != 42 {
t.Errorf("expected 42, got %v", result)
}
}
func TestInterpretGroupingExpr(t *testing.T) {
i := New(errors.NewMockErrorLogger())
literal := &ast.LiteralExpr{Value: 42}
grouping := &ast.GroupingExpr{Expression: literal}
result := i.VisitGroupingExpr(grouping)
if result != 42 {
t.Errorf("expected 42, got %v", result)
}
}
func TestInterpretUnaryExpr(t *testing.T) {
i := New(errors.NewMockErrorLogger())
literal := &ast.LiteralExpr{Value: 42.0}
unary := &ast.UnaryExpr{
Operator: token.Token{Type: token.MINUS, Lexeme: "-"},
Right: literal,
}
result := i.VisitUnaryExpr(unary)
if result != -42.0 {
t.Errorf("expected -42, got %v", result)
}
}
func TestInterpretUnaryExprBang(t *testing.T) {
i := New(errors.NewMockErrorLogger())
literal := &ast.LiteralExpr{Value: true}
unary := &ast.UnaryExpr{
Operator: token.Token{Type: token.BANG, Lexeme: "!"},
Right: literal,
}
result := i.VisitUnaryExpr(unary)
if result != false {
t.Errorf("expected false, got %v", result)
}
}
func TestInterpretErrorExpr(t *testing.T) {
i := New(errors.NewMockErrorLogger())
errorExpr := &ast.ErrorExpr{Value: "error"}
defer func() {
if r := recover(); r != "error" {
t.Errorf("expected panic with 'error', got %v", r)
}
}()
i.VisitErrorExpr(errorExpr)
}
func TestInterpretExpr(t *testing.T) {
i := New(errors.NewMockErrorLogger())
literal := &ast.LiteralExpr{Value: 42.0}
defer func() {
if r := recover(); r != nil {
t.Errorf("unexpected panic: %v", r)
}
}()
result := i.InterpretExpr(literal)
if result != "42" {
t.Errorf("expected '42', got %v", result)
}
}
func TestInterpretBinaryExpr(t *testing.T) {
i := New(errors.NewMockErrorLogger())
left := &ast.LiteralExpr{Value: 42.0}
right := &ast.LiteralExpr{Value: 2.0}
binary := &ast.BinaryExpr{
Left: left,
Operator: token.Token{Type: token.STAR, Lexeme: "*"},
Right: right,
}
result := i.VisitBinaryExpr(binary)
if result != 84.0 {
t.Errorf("expected 84, got %v", result)
}
}
func TestInterpretBinaryExprDivisionByZero(t *testing.T) {
i := New(errors.NewMockErrorLogger())
left := &ast.LiteralExpr{Value: 42.0}
right := &ast.LiteralExpr{Value: 0.0}
binary := &ast.BinaryExpr{
Left: left,
Operator: token.Token{Type: token.SLASH, Lexeme: "/"},
Right: right,
}
defer func() {
if r := recover(); r != "Division by zero [line 0]" {
t.Errorf("expected panic with 'division by zero', got %v", r)
}
}()
i.VisitBinaryExpr(binary)
}
func TestInterpretBinaryExprAddition(t *testing.T) {
i := New(errors.NewMockErrorLogger())
left := &ast.LiteralExpr{Value: 42.0}
right := &ast.LiteralExpr{Value: 2.0}
binary := &ast.BinaryExpr{
Left: left,
Operator: token.Token{Type: token.PLUS, Lexeme: "+"},
Right: right,
}
result := i.VisitBinaryExpr(binary)
if result != 44.0 {
t.Errorf("expected 44, got %v", result)
}
}
func TestInterpretBinaryExprSubtraction(t *testing.T) {
i := New(errors.NewMockErrorLogger())
left := &ast.LiteralExpr{Value: 42.0}
right := &ast.LiteralExpr{Value: 2.0}
binary := &ast.BinaryExpr{
Left: left,
Operator: token.Token{Type: token.MINUS, Lexeme: "-"},
Right: right,
}
result := i.VisitBinaryExpr(binary)
if result != 40.0 {
t.Errorf("expected 40, got %v", result)
}
}
func TestInterpretBinaryExprStringConcatenation(t *testing.T) {
i := New(errors.NewMockErrorLogger())
left := &ast.LiteralExpr{Value: "foo"}
right := &ast.LiteralExpr{Value: "bar"}
binary := &ast.BinaryExpr{
Left: left,
Operator: token.Token{Type: token.PLUS, Lexeme: "+"},
Right: right,
}
result := i.VisitBinaryExpr(binary)
if result != "foobar" {
t.Errorf("expected 'foobar', got %v", result)
}
}
func TestInterpretBinaryExprInvalidOperands(t *testing.T) {
i := New(errors.NewMockErrorLogger())
left := &ast.LiteralExpr{Value: "foo"}
right := &ast.LiteralExpr{Value: 42.0}
binary := &ast.BinaryExpr{
Left: left,
Operator: token.Token{Type: token.PLUS, Lexeme: "+"},
Right: right,
}
defer func() {
if r := recover(); r != "Operands must be two numbers or two strings [line 0]" {
t.Errorf("expected panic with 'operands must be two numbers or two strings', got %v", r)
}
}()
i.VisitBinaryExpr(binary)
}
func TestInterpretBinaryExprComparison(t *testing.T) {
i := New(errors.NewMockErrorLogger())
left := &ast.LiteralExpr{Value: 42.0}
right := &ast.LiteralExpr{Value: 2.0}
binary := &ast.BinaryExpr{
Left: left,
Operator: token.Token{Type: token.GREATER, Lexeme: ">"},
Right: right,
}
result := i.VisitBinaryExpr(binary)
if result != true {
t.Errorf("expected true, got %v", result)
}
}
func TestInterpretBinaryExprComparisonEqual(t *testing.T) {
i := New(errors.NewMockErrorLogger())
left := &ast.LiteralExpr{Value: 42.0}
right := &ast.LiteralExpr{Value: 42.0}
binary := &ast.BinaryExpr{
Left: left,
Operator: token.Token{Type: token.EQUAL_EQUAL, Lexeme: "=="},
Right: right,
}
result := i.VisitBinaryExpr(binary)
if result != true {
t.Errorf("expected true, got %v", result)
}
}
func TestInterpretBinaryExprComparisonNotEqual(t *testing.T) {
i := New(errors.NewMockErrorLogger())
left := &ast.LiteralExpr{Value: 42.0}
right := &ast.LiteralExpr{Value: 2.0}
binary := &ast.BinaryExpr{
Left: left,
Operator: token.Token{Type: token.BANG_EQUAL, Lexeme: "!="},
Right: right,
}
result := i.VisitBinaryExpr(binary)
if result != true {
t.Errorf("expected true, got %v", result)
}
}
func TestInterpretBinaryExprComparisonInvalidOperands(t *testing.T) {
i := New(errors.NewMockErrorLogger())
left := &ast.LiteralExpr{Value: "foo"}
right := &ast.LiteralExpr{Value: 42.0}
binary := &ast.BinaryExpr{
Left: left,
Operator: token.Token{Type: token.GREATER, Lexeme: ">"},
Right: right,
}
defer func() {
if r := recover(); r != "Operands of operator '>' must be numbers [line 0]" {
t.Errorf("expected panic with 'operands must be numbers', got %v", r)
}
}()
i.VisitBinaryExpr(binary)
}
func TestInterpretBinaryExprInvalidOperatorType(t *testing.T) {
i := New(errors.NewMockErrorLogger())
left := &ast.LiteralExpr{Value: 42.0}
right := &ast.LiteralExpr{Value: 2.0}
binary := &ast.BinaryExpr{
Left: left,
Operator: token.Token{Type: token.EOF, Lexeme: ""},
Right: right,
}
defer func() {
if r := recover(); r != "Unknown binary operator '' [line 0]" {
t.Errorf("expected panic with 'unknown operator type', got %v", r)
}
}()
i.VisitBinaryExpr(binary)
}
func TestInterpretLogicalExpr(t *testing.T) {
i := New(errors.NewMockErrorLogger())
left := &ast.LiteralExpr{Value: true}
right := &ast.LiteralExpr{Value: false}
logical := &ast.LogicalExpr{
Left: left,
Operator: token.Token{Type: token.AND, Lexeme: "and"},
Right: right,
}
result := i.VisitLogicalExpr(logical)
if result != false {
t.Errorf("expected false, got %v", result)
}
}
func TestInterpretErrorStatement(t *testing.T) {
i := New(errors.NewMockErrorLogger())
errorStmt := &ast.ErrorStmt{Value: "error"}
defer func() {
if r := recover(); r != "error" {
t.Errorf("expected panic with 'error', got %v", r)
}
}()
i.VisitErrorStmt(errorStmt)
}
func TestInterpretExprStatement(t *testing.T) {
i := New(errors.NewMockErrorLogger())
literal := &ast.LiteralExpr{Value: 42.0}
exprStmt := &ast.ExpressionStmt{Expression: literal}
result := i.VisitExpressionStmt(exprStmt)
if result != nil {
t.Errorf("expected nil, got %v", result)
}
}
func TestInterpretPrintStatement(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()
}()
i := New(errors.NewMockErrorLogger())
literal := &ast.LiteralExpr{Value: 42.0}
printStmt := &ast.PrintStmt{Expression: literal}
result := i.VisitPrintStmt(printStmt)
if result != nil {
t.Errorf("expected nil, got %v", result)
}
// back to normal state
w.Close()
os.Stdout = old // restoring the real stdout
out := <-outC
// reading our temp stdout
expected := "42\n"
if out != expected {
t.Errorf("run() = %v; want %v", out, expected)
}
}
func TestInterpretVarStatement(t *testing.T) {
i := New(errors.NewMockErrorLogger())
varStmt := &ast.VarStmt{
Name: token.Token{Type: token.IDENTIFIER, Lexeme: "foo"},
Initializer: &ast.LiteralExpr{Value: 42.0},
}
i.VisitVarStmt(varStmt)
result := i.env.get("foo")
if result != 42.0 {
t.Errorf("expected 42, got %v", result)
}
}
func TestInterpretVarStatementNoInitializer(t *testing.T) {
i := New(errors.NewMockErrorLogger())
varStmt := &ast.VarStmt{
Name: token.Token{Type: token.IDENTIFIER, Lexeme: "foo"},
}
i.VisitVarStmt(varStmt)
result := i.env.get("foo")
if result != nil {
t.Errorf("expected nil, got %v", result)
}
}
func TestInterpretAssignment(t *testing.T) {
i := New(errors.NewMockErrorLogger())
varStmt := &ast.VarStmt{
Name: token.Token{Type: token.IDENTIFIER, Lexeme: "foo"},
}
i.VisitVarStmt(varStmt)
result := i.env.get("foo")
if result != nil {
t.Errorf("expected nil, got %v", result)
}
assign := &ast.AssignExpr{
Name: token.Token{Type: token.IDENTIFIER, Lexeme: "foo"},
Value: &ast.LiteralExpr{Value: 42.0},
}
i.VisitAssignExpr(assign)
result = i.env.get("foo")
if result != 42.0 {
t.Errorf("expected 42, got %v", result)
}
}
func TestInterpretAssignmentUndefinedVariable(t *testing.T) {
i := New(errors.NewMockErrorLogger())
assign := &ast.AssignExpr{
Name: token.Token{Type: token.IDENTIFIER, Lexeme: "foo"},
Value: &ast.LiteralExpr{Value: 42.0},
}
defer func() {
if r := recover(); r != "Undefined variable 'foo'." {
t.Errorf("expected panic with 'undefined variable', got %v", r)
}
}()
i.VisitAssignExpr(assign)
}
func TestInterpretBlockStatement(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()
}()
// begin unit test
i := New(errors.NewMockErrorLogger())
block := &ast.BlockStmt{
Statements: []ast.Stmt{
&ast.VarStmt{
Name: token.Token{Type: token.IDENTIFIER, Lexeme: "foo"},
Initializer: &ast.LiteralExpr{Value: 42.0},
},
&ast.PrintStmt{
Expression: &ast.VariableExpr{
Name: token.Token{Type: token.IDENTIFIER, Lexeme: "foo"},
},
},
},
}
i.VisitBlockStmt(block)
_, found := i.env.values["foo"]
if found {
t.Errorf("expected to not find 'foo' in environment")
}
// end unit test
// back to normal state
w.Close()
os.Stdout = old // restoring the real stdout
out := <-outC
// reading our temp stdout
expected := "42\n"
if out != expected {
t.Errorf("run() = %v; want %v", out, expected)
}
}
func TestInterpretIfStatement(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()
}()
// begin unit test
i := New(errors.NewMockErrorLogger())
ifStmt := &ast.IfStmt{
Condition: &ast.LiteralExpr{Value: true},
ThenBranch: &ast.PrintStmt{
Expression: &ast.LiteralExpr{Value: 42.0},
},
}
i.VisitIfStmt(ifStmt)
// end unit test
// back to normal state
w.Close()
os.Stdout = old // restoring the real stdout
out := <-outC
// reading our temp stdout
expected := "42\n"
if out != expected {
t.Errorf("run() = %v; want %v", out, expected)
}
}
func TestInterpretIfStatementElseBranch(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()
}()
// begin unit test
i := New(errors.NewMockErrorLogger())
ifStmt := &ast.IfStmt{
Condition: &ast.LiteralExpr{Value: false},
ThenBranch: &ast.PrintStmt{
Expression: &ast.LiteralExpr{Value: 42.0},
},
ElseBranch: &ast.PrintStmt{
Expression: &ast.LiteralExpr{Value: 24.0},
},
}
i.VisitIfStmt(ifStmt)
// end unit test
// back to normal state
w.Close()
os.Stdout = old // restoring the real stdout
out := <-outC
// reading our temp stdout
expected := "24\n"
if out != expected {
t.Errorf("run() = %v; want %v", out, expected)
}
}
func TestInterpretWhileStatement(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()
}()
// begin unit test
i := New(errors.NewMockErrorLogger())
varStmt := &ast.VarStmt{
Name: token.Token{Type: token.IDENTIFIER, Lexeme: "i"},
Initializer: &ast.LiteralExpr{Value: 3.0},
}
i.VisitVarStmt(varStmt)
whileStmt := &ast.WhileStmt{
Condition: &ast.BinaryExpr{
Left: &ast.VariableExpr{Name: token.Token{Type: token.IDENTIFIER, Lexeme: "i"}},
Operator: token.Token{Type: token.GREATER, Lexeme: ">"},
Right: &ast.LiteralExpr{Value: 0.0},
},
Body: &ast.BlockStmt{
Statements: []ast.Stmt{
&ast.PrintStmt{
Expression: &ast.VariableExpr{Name: token.Token{Type: token.IDENTIFIER, Lexeme: "i"}},
},
&ast.ExpressionStmt{
Expression: &ast.AssignExpr{
Name: token.Token{Type: token.IDENTIFIER, Lexeme: "i"},
Value: &ast.BinaryExpr{
Left: &ast.VariableExpr{Name: token.Token{Type: token.IDENTIFIER, Lexeme: "i"}},
Operator: token.Token{Type: token.MINUS, Lexeme: "-"},
Right: &ast.LiteralExpr{Value: 1.0},
},
},
},
},
},
}
i.VisitWhileStmt(whileStmt)
// end unit test
// back to normal state
w.Close()
os.Stdout = old // restoring the real stdout
out := <-outC
// reading our temp stdout
expected := "3\n2\n1\n"
if out != expected {
t.Errorf("run() = %v; want %v", out, expected)
}
}
func TestInterpretFunctionStatement(t *testing.T) {
i := New(errors.NewMockErrorLogger())
functionStmt := &ast.FunctionStmt{
Name: token.Token{Type: token.IDENTIFIER, Lexeme: "foo"},
Params: []token.Token{},
Body: []ast.Stmt{&ast.BlockStmt{Statements: []ast.Stmt{}}},
}
i.VisitFunctionStmt(functionStmt)
result := i.env.get("foo")
if result == nil {
t.Errorf("expected function, got nil")
}
}
func TestInterpretFunctionCall(t *testing.T) {
i := New(errors.NewMockErrorLogger())
functionStmt := &ast.FunctionStmt{
Name: token.Token{Type: token.IDENTIFIER, Lexeme: "foo"},
Params: []token.Token{},
Body: []ast.Stmt{&ast.BlockStmt{Statements: []ast.Stmt{&ast.PrintStmt{Expression: &ast.LiteralExpr{Value: 42.0}}}}},
}
i.VisitFunctionStmt(functionStmt)
callExpr := &ast.CallExpr{
Callee: &ast.VariableExpr{Name: token.Token{Type: token.IDENTIFIER, Lexeme: "foo"}},
Arguments: []ast.Expr{},
}
i.VisitCallExpr(callExpr)
}
func TestInterpretFunctionCallWithArguments(t *testing.T) {
i := New(errors.NewMockErrorLogger())
functionStmt := &ast.FunctionStmt{
Name: token.Token{Type: token.IDENTIFIER, Lexeme: "foo"},
Params: []token.Token{{Type: token.IDENTIFIER, Lexeme: "a"}},
Body: []ast.Stmt{&ast.BlockStmt{Statements: []ast.Stmt{&ast.PrintStmt{Expression: &ast.VariableExpr{Name: token.Token{Type: token.IDENTIFIER, Lexeme: "a"}}}}}},
}
i.VisitFunctionStmt(functionStmt)
callExpr := &ast.CallExpr{
Callee: &ast.VariableExpr{Name: token.Token{Type: token.IDENTIFIER, Lexeme: "foo"}},
Arguments: []ast.Expr{
&ast.LiteralExpr{Value: 42.0},
},
}
i.VisitCallExpr(callExpr)
}
func TestInterpretFunctionCallWithWrongNumberOfArguments(t *testing.T) {
i := New(errors.NewMockErrorLogger())
functionStmt := &ast.FunctionStmt{
Name: token.Token{Type: token.IDENTIFIER, Lexeme: "foo"},
Params: []token.Token{{Type: token.IDENTIFIER, Lexeme: "a"}},
Body: []ast.Stmt{&ast.BlockStmt{Statements: []ast.Stmt{}}},
}
i.VisitFunctionStmt(functionStmt)
callExpr := &ast.CallExpr{
Callee: &ast.VariableExpr{Name: token.Token{Type: token.IDENTIFIER, Lexeme: "foo"}},
Arguments: []ast.Expr{},
}
defer func() {
if r := recover(); r != "Expected 1 arguments but got 0 [line 0]" {
t.Errorf("expected panic with 'expected 1 arguments but got 0', got %v", r)
}
}()
i.VisitCallExpr(callExpr)
}