From cf8b0033c9c76eed94face4747658caa24c6953a Mon Sep 17 00:00:00 2001 From: jmug Date: Mon, 6 Jan 2025 18:24:23 -0800 Subject: [PATCH] Introduced the evaluator (and integrated it with the REPL) It supports integer and boolean literals, prefix expressions and infix expressions with integer operands. Signed-off-by: jmug --- pkg/evaluator/evaluator.go | 104 ++++++++++++++++++++++++++++++ pkg/evaluator/evaluator_test.go | 108 ++++++++++++++++++++++++++++++++ pkg/object/object.go | 47 ++++++++++++++ pkg/repl/repl.go | 8 ++- 4 files changed, 265 insertions(+), 2 deletions(-) create mode 100644 pkg/evaluator/evaluator.go create mode 100644 pkg/evaluator/evaluator_test.go create mode 100644 pkg/object/object.go diff --git a/pkg/evaluator/evaluator.go b/pkg/evaluator/evaluator.go new file mode 100644 index 0000000..859af83 --- /dev/null +++ b/pkg/evaluator/evaluator.go @@ -0,0 +1,104 @@ +package evaluator + +import ( + "code.jmug.me/jmug/interpreter-in-go/pkg/ast" + "code.jmug.me/jmug/interpreter-in-go/pkg/object" +) + +var ( + _NULL = &object.Null{} + _TRUE = &object.Boolean{Value: true} + _FALSE = &object.Boolean{Value: false} +) + +func Eval(node ast.Node) object.Object { + switch node := node.(type) { + // Statements. + case *ast.Program: + return evalStatements(node.Statements) + case *ast.ExpressionStatement: + return Eval(node.Expression) + // Expressions. + case *ast.IntegerLiteral: + return &object.Integer{Value: node.Value} + case *ast.Boolean: + return nativeBoolToBooleanObject(node.Value) + case *ast.PrefixExpression: + right := Eval(node.Right) + return evalPrefixExpression(node.Operator, right) + case *ast.InfixExpression: + left := Eval(node.Left) + right := Eval(node.Right) + return evalInfixExpression(node.Operator, left, right) + } + return nil +} + +func evalStatements(stmts []ast.Statement) object.Object { + var res object.Object + for _, stmt := range stmts { + res = Eval(stmt) + } + return res +} + +func evalPrefixExpression(op string, right object.Object) object.Object { + switch op { + case "!": + return evalBangOperatorExpression(right) + case "-": + return evalMinusPrefixOperatorExpression(right) + } + return _NULL +} + +func evalBangOperatorExpression(obj object.Object) object.Object { + switch obj { + case _TRUE: + return _FALSE + case _FALSE: + return _TRUE + case _NULL: + return _TRUE + default: + return _FALSE + } +} + +func evalMinusPrefixOperatorExpression(obj object.Object) object.Object { + if obj.Type() != object.INTEGER_OBJ { + return _NULL + } + val := obj.(*object.Integer).Value + return &object.Integer{Value: -val} +} + +func evalInfixExpression(op string, left, right object.Object) object.Object { + if left.Type() == object.INTEGER_OBJ && right.Type() == object.INTEGER_OBJ { + return evalIntegerInfixExpression(op, left, right) + } + return _NULL +} + +func evalIntegerInfixExpression(op string, left, right object.Object) object.Object { + l := left.(*object.Integer).Value + r := right.(*object.Integer).Value + switch op { + case "+": + return &object.Integer{Value: l + r} + case "-": + return &object.Integer{Value: l - r} + case "*": + return &object.Integer{Value: l * r} + case "/": + return &object.Integer{Value: l / r} + } + return _NULL +} + +func nativeBoolToBooleanObject(b bool) object.Object { + if b { + return _TRUE + } + return _FALSE +} diff --git a/pkg/evaluator/evaluator_test.go b/pkg/evaluator/evaluator_test.go new file mode 100644 index 0000000..7b11567 --- /dev/null +++ b/pkg/evaluator/evaluator_test.go @@ -0,0 +1,108 @@ +package evaluator + +import ( + "testing" + + "code.jmug.me/jmug/interpreter-in-go/pkg/lexer" + "code.jmug.me/jmug/interpreter-in-go/pkg/object" + "code.jmug.me/jmug/interpreter-in-go/pkg/parser" +) + +func TestEvalIntegerExpression(t *testing.T) { + tests := []struct { + input string + expected int64 + }{ + {"5", 5}, + {"10", 10}, + {"-5", -5}, + {"-10", -10}, + {"5 + 5 + 5 + 5 - 10", 10}, + {"2 * 2 * 2 * 2 * 2", 32}, + {"-50 + 100 + -50", 0}, + {"5 * 2 + 10", 20}, + {"5 + 2 * 10", 25}, + {"20 + 2 * -10", 0}, + {"50 / 2 * 2 + 10", 60}, + {"2 * (5 + 10)", 30}, + {"3 * 3 * 3 + 10", 37}, + {"3 * (3 * 3) + 10", 37}, + {"(5 + 10 * 2 + 15 / 3) * 2 + -10", 50}, + } + + for _, tt := range tests { + evaluated := testEval(tt.input) + testIntegerObject(t, evaluated, tt.expected) + } +} + +func TestEvalBooleanExpression(t *testing.T) { + tests := []struct { + input string + expected bool + }{ + {"true", true}, + {"false", false}, + } + + for _, tt := range tests { + evaluated := testEval(tt.input) + testBooleanObject(t, evaluated, tt.expected) + } +} + +func TestBangOperator(t *testing.T) { + tests := []struct { + input string + expected bool + }{ + {"!true", false}, + {"!false", true}, + {"!5", false}, + {"!!true", true}, + {"!!false", false}, + {"!!5", true}, + } + + for _, tt := range tests { + evaluated := testEval(tt.input) + testBooleanObject(t, evaluated, tt.expected) + } +} + +func testEval(input string) object.Object { + l := lexer.New(input) + p := parser.New(l) + program := p.ParseProgram() + + return Eval(program) +} + +func testIntegerObject(t *testing.T, obj object.Object, expected int64) bool { + result, ok := obj.(*object.Integer) + if !ok { + t.Errorf("object is not Integer. got=%T (%+v)", obj, obj) + return false + } + if result.Value != expected { + t.Errorf("object has wrong value. got=%d, want=%d", + result.Value, expected) + return false + } + + return true +} + +func testBooleanObject(t *testing.T, obj object.Object, expected bool) bool { + result, ok := obj.(*object.Boolean) + if !ok { + t.Errorf("object is not Boolean. got=%T (%+v)", obj, obj) + return false + } + if result.Value != expected { + t.Errorf("object has wrong value. got=%t, want=%t", + result.Value, expected) + return false + } + return true +} diff --git a/pkg/object/object.go b/pkg/object/object.go new file mode 100644 index 0000000..5f3d2b7 --- /dev/null +++ b/pkg/object/object.go @@ -0,0 +1,47 @@ +package object + +import "fmt" + +type ObjectType string + +const ( + INTEGER_OBJ = "INTEGER" + BOOLEAN_OBJ = "BOOLEAN" + NULL_OBJ = "NULL" +) + +type Object interface { + Type() ObjectType + Inspect() string +} + +type Integer struct { + Value int64 +} + +func (i *Integer) Type() ObjectType { + return INTEGER_OBJ +} +func (i *Integer) Inspect() string { + return fmt.Sprintf("%d", i.Value) +} + +type Boolean struct { + Value bool +} + +func (b *Boolean) Type() ObjectType { + return BOOLEAN_OBJ +} +func (b *Boolean) Inspect() string { + return fmt.Sprintf("%t", b.Value) +} + +type Null struct{} + +func (n *Null) Type() ObjectType { + return NULL_OBJ +} +func (n *Null) Inspect() string { + return "null" +} diff --git a/pkg/repl/repl.go b/pkg/repl/repl.go index dd47f22..e311a7d 100644 --- a/pkg/repl/repl.go +++ b/pkg/repl/repl.go @@ -5,6 +5,7 @@ import ( "fmt" "io" + "code.jmug.me/jmug/interpreter-in-go/pkg/evaluator" "code.jmug.me/jmug/interpreter-in-go/pkg/lexer" "code.jmug.me/jmug/interpreter-in-go/pkg/parser" ) @@ -25,8 +26,11 @@ func Start(in io.Reader, out io.Writer) { printParserErrors(out, p.Errors()) continue } - io.WriteString(out, program.String()) - io.WriteString(out, "\n") + res := evaluator.Eval(program) + if res != nil { + io.WriteString(out, res.Inspect()) + io.WriteString(out, "\n") + } } }