Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: implement the interpreter for the language #10

Merged
merged 21 commits into from
May 8, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 16 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -24,13 +24,16 @@ Monkey is an interpreted language written in Go. *This project is still under de
+ Prefix operators (binary expressions)
+ Infix operators (unary expressions)
+ Postfix operators
+ Tree-walking interpreter
+ Works on 64-bit machines

## Components

+ Token set
+ Lexer
+ Abstract Syntax Tree (AST)
+ Pratt parser based on context-free grammars and the Backus-Naur-Form
+ Tree-walking interpreter (evaluator), future: bytecode, VM, and JIT compilation

## TODOs

Expand Down Expand Up @@ -64,10 +67,22 @@ Monkey is an interpreted language written in Go. *This project is still under de
+ [ ] feat: PrettyPrint, color, AST, etc
+ [ ] feat: sys call such as print(...)
+ [ ] feat: add a helper for available functions
+ [ ] feat: consider how to write a GC
+ [ ] feat: switch between multiple value representation system using some flag
+ [ ] feat: class (object system)
+ [ ] feat: configuration with yaml or envrc
+ [ ] feat: edge cases for those operators
+ [ ] feat: integer division operator and float division operator
+ [ ] feat: reference integer literal as constant, simulate some static memory space for literals of integer, strings, etc.
+ [ ] feat: immutable types
+ [ ] feat: integer overflow problem
+ [ ] feat: command history and navigate in REPL using left, right, up, bottom

## References

+ *Top Down Operator Precedence*, Vaughan Pratt
+ *Writing An Interpreter In Go, Thorsten Ball*
+ *Top Down Operator Precedence, Vaughan Pratt*
+ *The Structure and Interpretation of Computer Programs (SICP), Harold Abelson*

## License

Expand Down
40 changes: 40 additions & 0 deletions cmd/build.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
Copyright © 2024 NAME HERE <EMAIL ADDRESS>

*/
package cmd

import (
"fmt"

"github.com/spf13/cobra"
)

// buildCmd represents the build command
var buildCmd = &cobra.Command{
Use: "build",
Short: "A brief description of your command",
Long: `A longer description that spans multiple lines and likely contains examples
and usage of using your command. For example:

Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("build called")
},
}

func init() {
rootCmd.AddCommand(buildCmd)

// Here you will define your flags and configuration settings.

// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// buildCmd.PersistentFlags().String("foo", "", "A help for foo")

// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// buildCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}
40 changes: 40 additions & 0 deletions cmd/run.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
/*
Copyright © 2024 NAME HERE <EMAIL ADDRESS>

*/
package cmd

import (
"fmt"

"github.com/spf13/cobra"
)

// runCmd represents the run command
var runCmd = &cobra.Command{
Use: "run",
Short: "A brief description of your command",
Long: `A longer description that spans multiple lines and likely contains examples
and usage of using your command. For example:

Cobra is a CLI library for Go that empowers applications.
This application is a tool to generate the needed files
to quickly create a Cobra application.`,
Run: func(cmd *cobra.Command, args []string) {
fmt.Println("run called")
},
}

func init() {
rootCmd.AddCommand(runCmd)

// Here you will define your flags and configuration settings.

// Cobra supports Persistent Flags which will work for this command
// and all subcommands, e.g.:
// runCmd.PersistentFlags().String("foo", "", "A help for foo")

// Cobra supports local flags which will only run when this command
// is called directly, e.g.:
// runCmd.Flags().BoolP("toggle", "t", false, "Help message for toggle")
}
Empty file added examples/hello.mk
Empty file.
6 changes: 6 additions & 0 deletions internal/ast/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,15 @@ var _ Node = (*Program)(nil)
var _ Expression = (*IdentifierExpression)(nil)
var _ Expression = (*IntegerExpression)(nil)
var _ Expression = (*BooleanExpression)(nil)
var _ Expression = (*IfExpression)(nil)
var _ Expression = (*FuncExpression)(nil)
var _ Expression = (*CallExpression)(nil)
var _ Expression = (*PrefixExpression)(nil)
var _ Expression = (*InfixExpression)(nil)
var _ Statement = (*LetStatement)(nil)
var _ Statement = (*ReturnStatement)(nil)
var _ Statement = (*ExpressionStatement)(nil)
var _ Statement = (*BlockStatement)(nil)

// Node is a common interface for nodes in AST
type Node interface {
Expand Down
12 changes: 12 additions & 0 deletions internal/evaluator/errors.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
package evaluator

import (
"errors"
)

var (
ErrEmptyNodeInput = errors.New("empty node input")
ErrUnexpectedNodeType = errors.New("unexpected node type")
ErrUnexpectedObjectType = errors.New("unexpected object type")
ErrUnexpectedOperatorType = errors.New("unexpected operator type")
)
187 changes: 187 additions & 0 deletions internal/evaluator/evaluator.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,187 @@
package evaluator

import (
"reflect"

"github.com/aden-q/monkey/internal/ast"
"github.com/aden-q/monkey/internal/object"
)

// interface compliance check
var _ Evaluator = (*evaluator)(nil)

// An interpreter/evaluator interface
type Evaluator interface {
Eval(node ast.Node) (object.Object, error)
}

type evaluator struct {
}

func New() Evaluator {
return &evaluator{}
}

// Eval evaluate an AST node recursively
func (e *evaluator) Eval(node ast.Node) (object.Object, error) {
if node == nil {
return object.NIL, ErrEmptyNodeInput
}

switch node := node.(type) {
// evaluate the program
case *ast.Program:
return e.evalStatements(node.Statements)
// evaluate statements
case *ast.ExpressionStatement:
return e.Eval(node.Expression)
case *ast.BlockStatement:
return e.evalStatements(node.Statements)
// evaluate expressions
case *ast.IntegerExpression:
return object.NewInteger(node.Value), nil
case *ast.BooleanExpression:
return booleanConv(node.Value), nil
case *ast.IfExpression:
return e.evalIfExpression(node)
case *ast.PrefixExpression:
return e.evalPrefixExpression(node)
case *ast.InfixExpression:
return e.evalInfixExpression(node)
}

// no match, unexpected path
return object.NIL, ErrUnexpectedNodeType
}

func (e *evaluator) evalStatements(stmts []ast.Statement) (object.Object, error) {
var result object.Object
var err error

// iteratively evaluate each statement and return the result from the last one
for _, stmt := range stmts {
result, err = e.Eval(stmt)
if err != nil {
return nil, err
}
}

return result, nil
}

func (e *evaluator) evalIfExpression(ie *ast.IfExpression) (object.Object, error) {
condition, err := e.Eval(ie.Condition)
if err != nil {
return object.NIL, nil
}

if condition.IsTruthy() {
return e.Eval(ie.Consequence)
}

if ie.Alternative != nil {
return e.Eval(ie.Alternative)
}

return object.NIL, nil
}

func (e *evaluator) evalPrefixExpression(pe *ast.PrefixExpression) (object.Object, error) {
operandObj, err := e.Eval(pe.Operand)
if err != nil {
return nil, nil
}

switch pe.Operator {
case "!":
return e.evalBangOperatorExpression(operandObj)
case "-":
return e.evalMinuxPrefixOperatorExpression(operandObj)
default:
return object.NIL, nil
}
}

// evalBangOperatorExpression evaluates a prefix expression with a '!' token as the prefix
func (e *evaluator) evalBangOperatorExpression(o object.Object) (object.Object, error) {
switch o {
case object.FALSE, object.NewInteger(0):
return object.TRUE, nil
default:
return object.FALSE, nil
}
}

// evalMinuxPrefixOperatorExpression evaluates a prefix expression with a '-' token as the prefix
func (e *evaluator) evalMinuxPrefixOperatorExpression(o object.Object) (object.Object, error) {
if o.Type() != object.INTEGER_OBJ {
return object.NIL, ErrUnexpectedObjectType
}

return object.NewInteger(-o.(*object.Integer).Value), nil
}

// evalInfixExpression evaluates an infix expression
func (e *evaluator) evalInfixExpression(ie *ast.InfixExpression) (object.Object, error) {
leftOperandObj, err := e.Eval(ie.LeftOperand)
if err != nil {
return nil, err
}

rightOperandObj, err := e.Eval(ie.RightOperand)
if err != nil {
return nil, err
}

switch {
case leftOperandObj.Type() == object.INTEGER_OBJ && rightOperandObj.Type() == object.INTEGER_OBJ:
return e.evalIntegerInfixExpression(ie.Operator, leftOperandObj.(*object.Integer), rightOperandObj.(*object.Integer))
// equality test
case ie.Operator == "==":
return booleanConv(reflect.DeepEqual(leftOperandObj, rightOperandObj)), nil
case ie.Operator == "!=":
return booleanConv(!reflect.DeepEqual(leftOperandObj, rightOperandObj)), nil
default:
// TODO: check infix expressions involving boolean operands and operators that result in boolean values
return object.NIL, ErrUnexpectedObjectType
}
}

// evalIntegerInfixExpression evaluates an infix expression involving two integer operators
func (e *evaluator) evalIntegerInfixExpression(operator string, left, right *object.Integer) (object.Object, error) {
leftVal, rightVal := left.Value, right.Value

switch operator {
case "+":
return object.NewInteger(leftVal + rightVal), nil
case "-":
return object.NewInteger(leftVal - rightVal), nil
case "*":
return object.NewInteger(leftVal * rightVal), nil
case "/":
return object.NewInteger(leftVal / rightVal), nil
case "<":
return booleanConv(leftVal < rightVal), nil
case "<=":
return booleanConv(leftVal <= rightVal), nil
case ">":
return booleanConv(leftVal > rightVal), nil
case ">=":
return booleanConv(leftVal >= rightVal), nil
case "==":
return booleanConv(leftVal == rightVal), nil
case "!=":
return booleanConv(leftVal != rightVal), nil
default:
return object.NIL, ErrUnexpectedOperatorType
}
}

// booleanConv converts a boolean literal to a boolean object in the object system
func booleanConv(input bool) object.Object {
if input {
return object.TRUE
}

return object.FALSE
}
13 changes: 13 additions & 0 deletions internal/evaluator/evaluator_suite_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package evaluator_test

import (
"testing"

. "github.com/onsi/ginkgo/v2"
. "github.com/onsi/gomega"
)

func TestEvaluator(t *testing.T) {
RegisterFailHandler(Fail)
RunSpecs(t, "Evaluator Suite")
}
Loading