Add expression interpreter
parent
e0e921602d
commit
95ab9d78d5
@ -0,0 +1,185 @@
|
||||
package interpreter
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"golox/ast"
|
||||
"golox/errors"
|
||||
"golox/token"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Interpreter interprets the AST.
|
||||
type Interpreter struct {
|
||||
errLogger errors.Logger
|
||||
}
|
||||
|
||||
// New creates a new Interpreter.
|
||||
func New() *Interpreter {
|
||||
return &Interpreter{}
|
||||
}
|
||||
|
||||
// Interpret interprets the AST.
|
||||
func (i *Interpreter) InterpretExpr(expr ast.Expr) string {
|
||||
defer i.afterPanic()
|
||||
|
||||
value := i.evaluate(expr)
|
||||
return stringify(value)
|
||||
}
|
||||
|
||||
// VisitErrorExpr visits an ErrorExpr.
|
||||
func (i *Interpreter) VisitErrorExpr(e *ast.ErrorExpr) any {
|
||||
panic(e.Value)
|
||||
}
|
||||
|
||||
// VisitLiteralExpr visits a LiteralExpr.
|
||||
func (i *Interpreter) VisitLiteralExpr(l *ast.LiteralExpr) any {
|
||||
return l.Value
|
||||
}
|
||||
|
||||
// VisitGroupingExpr visits a GroupingExpr.
|
||||
func (i *Interpreter) VisitGroupingExpr(g *ast.GroupingExpr) any {
|
||||
return i.evaluate(g.Expression)
|
||||
}
|
||||
|
||||
// VisitUnaryExpr visits a UnaryExpr.
|
||||
func (i *Interpreter) VisitUnaryExpr(u *ast.UnaryExpr) any {
|
||||
right := i.evaluate(u.Right)
|
||||
|
||||
switch u.Operator.Type {
|
||||
case token.MINUS:
|
||||
checkNumberOperands(u.Operator, right)
|
||||
return -right.(float64)
|
||||
case token.BANG:
|
||||
return !isTruthy(right)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// VisitBinaryExpr visits a BinaryExpr.
|
||||
func (i *Interpreter) VisitBinaryExpr(b *ast.BinaryExpr) any {
|
||||
left := i.evaluate(b.Left)
|
||||
right := i.evaluate(b.Right)
|
||||
|
||||
switch b.Operator.Type {
|
||||
case token.MINUS:
|
||||
checkNumberOperands(b.Operator, left, right)
|
||||
return left.(float64) - right.(float64)
|
||||
case token.SLASH:
|
||||
checkNumberOperands(b.Operator, left, right)
|
||||
denominator := right.(float64)
|
||||
if denominator == 0.0 {
|
||||
panic(fmt.Sprintf("Division by zero [line %d]", b.Operator.Line))
|
||||
}
|
||||
return left.(float64) / denominator
|
||||
case token.STAR:
|
||||
checkNumberOperands(b.Operator, left, right)
|
||||
return left.(float64) * right.(float64)
|
||||
case token.PLUS:
|
||||
if l, ok := left.(float64); ok {
|
||||
if r, ok := right.(float64); ok {
|
||||
return l + r
|
||||
}
|
||||
}
|
||||
|
||||
if l, ok := left.(string); ok {
|
||||
if r, ok := right.(string); ok {
|
||||
return l + r
|
||||
}
|
||||
}
|
||||
|
||||
panic(fmt.Sprintf("Operands must be two numbers or two strings [line %d]", b.Operator.Line))
|
||||
case token.GREATER:
|
||||
checkNumberOperands(b.Operator, left, right)
|
||||
return left.(float64) > right.(float64)
|
||||
case token.GREATER_EQUAL:
|
||||
checkNumberOperands(b.Operator, left, right)
|
||||
return left.(float64) >= right.(float64)
|
||||
case token.LESS:
|
||||
checkNumberOperands(b.Operator, left, right)
|
||||
return left.(float64) < right.(float64)
|
||||
case token.LESS_EQUAL:
|
||||
checkNumberOperands(b.Operator, left, right)
|
||||
return left.(float64) <= right.(float64)
|
||||
case token.BANG_EQUAL:
|
||||
return !isEqual(left, right)
|
||||
case token.EQUAL_EQUAL:
|
||||
return isEqual(left, right)
|
||||
}
|
||||
|
||||
panic(fmt.Sprintf("Unknown binary operator '%s' [line %d]", b.Operator.Lexeme, b.Operator.Line))
|
||||
}
|
||||
|
||||
// checkNumberOperands checks if the operands are numbers.
|
||||
func checkNumberOperands(operator token.Token, operands ...any) {
|
||||
for _, operand := range operands {
|
||||
if _, ok := operand.(float64); !ok {
|
||||
panic(fmt.Sprintf("Operands of operator '%s' must be numbers [line %d]", operator.Lexeme, operator.Line))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// isTruthy checks if a value is truthy.
|
||||
func isTruthy(v any) bool {
|
||||
if v == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
if b, ok := v.(bool); ok {
|
||||
return b
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
// isEqual checks if two values are equal.
|
||||
func isEqual(a, b any) bool {
|
||||
if a == nil && b == nil {
|
||||
return true
|
||||
}
|
||||
|
||||
if a == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
return a == b
|
||||
}
|
||||
|
||||
// evaluate evaluates an expression.
|
||||
func (i *Interpreter) evaluate(e ast.Expr) any {
|
||||
return e.Accept(i)
|
||||
}
|
||||
|
||||
// stringify returns a string representation of a value.
|
||||
func stringify(v any) string {
|
||||
if v == nil {
|
||||
return "nil"
|
||||
}
|
||||
|
||||
if b, ok := v.(bool); ok {
|
||||
if b {
|
||||
return "true"
|
||||
}
|
||||
|
||||
return "false"
|
||||
}
|
||||
|
||||
s := fmt.Sprintf("%v", v)
|
||||
|
||||
if f, ok := v.(float64); ok {
|
||||
if strings.HasSuffix(s, ".0") {
|
||||
return fmt.Sprintf("%d", int(f))
|
||||
}
|
||||
|
||||
return fmt.Sprintf("%g", f)
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
// afterPanic handles a panic.
|
||||
func (i *Interpreter) afterPanic() {
|
||||
if r := recover(); r != nil {
|
||||
i.errLogger.RuntimeError(r.(string))
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,275 @@
|
||||
// FILE: interpreter_test.go
|
||||
package interpreter_test
|
||||
|
||||
import (
|
||||
"golox/ast"
|
||||
"golox/interpreter"
|
||||
"golox/token"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestInterpretLiteralExpr(t *testing.T) {
|
||||
i := interpreter.New()
|
||||
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 := interpreter.New()
|
||||
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 := interpreter.New()
|
||||
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 := interpreter.New()
|
||||
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 := interpreter.New()
|
||||
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 := interpreter.New()
|
||||
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 := interpreter.New()
|
||||
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 := interpreter.New()
|
||||
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 := interpreter.New()
|
||||
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 := interpreter.New()
|
||||
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 := interpreter.New()
|
||||
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 := interpreter.New()
|
||||
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 := interpreter.New()
|
||||
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 := interpreter.New()
|
||||
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 := interpreter.New()
|
||||
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 := interpreter.New()
|
||||
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 := interpreter.New()
|
||||
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)
|
||||
}
|
||||
Loading…
Reference in New Issue