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 <u.g.a.mariano@gmail.com>
This commit is contained in:
Mariano Uvalle 2025-01-06 18:24:23 -08:00
parent acc6707740
commit cf8b0033c9
4 changed files with 265 additions and 2 deletions

104
pkg/evaluator/evaluator.go Normal file
View file

@ -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
}

View file

@ -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
}

47
pkg/object/object.go Normal file
View file

@ -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"
}

View file

@ -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,9 +26,12 @@ func Start(in io.Reader, out io.Writer) {
printParserErrors(out, p.Errors())
continue
}
io.WriteString(out, program.String())
res := evaluator.Eval(program)
if res != nil {
io.WriteString(out, res.Inspect())
io.WriteString(out, "\n")
}
}
}
const MONKEY_FACE = ` __,__