// 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) }