diff --git a/README.md b/README.md index f088ad2..573e2c7 100644 --- a/README.md +++ b/README.md @@ -24,6 +24,8 @@ 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 @@ -31,6 +33,7 @@ Monkey is an interpreted language written in Go. *This project is still under de + 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 @@ -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 diff --git a/cmd/build.go b/cmd/build.go new file mode 100644 index 0000000..84c7614 --- /dev/null +++ b/cmd/build.go @@ -0,0 +1,40 @@ +/* +Copyright © 2024 NAME HERE + +*/ +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") +} diff --git a/cmd/run.go b/cmd/run.go new file mode 100644 index 0000000..a75b636 --- /dev/null +++ b/cmd/run.go @@ -0,0 +1,40 @@ +/* +Copyright © 2024 NAME HERE + +*/ +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") +} diff --git a/examples/hello.mk b/examples/hello.mk new file mode 100644 index 0000000..e69de29 diff --git a/internal/ast/ast.go b/internal/ast/ast.go index 9528191..ed97dc0 100644 --- a/internal/ast/ast.go +++ b/internal/ast/ast.go @@ -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 { diff --git a/internal/evaluator/errors.go b/internal/evaluator/errors.go new file mode 100644 index 0000000..24faa36 --- /dev/null +++ b/internal/evaluator/errors.go @@ -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") +) diff --git a/internal/evaluator/evaluator.go b/internal/evaluator/evaluator.go new file mode 100644 index 0000000..ae1723f --- /dev/null +++ b/internal/evaluator/evaluator.go @@ -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 +} diff --git a/internal/evaluator/evaluator_suite_test.go b/internal/evaluator/evaluator_suite_test.go new file mode 100644 index 0000000..00cf99b --- /dev/null +++ b/internal/evaluator/evaluator_suite_test.go @@ -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") +} diff --git a/internal/evaluator/evaluator_test.go b/internal/evaluator/evaluator_test.go new file mode 100644 index 0000000..29e677f --- /dev/null +++ b/internal/evaluator/evaluator_test.go @@ -0,0 +1,639 @@ +package evaluator_test + +import ( + "github.com/aden-q/monkey/internal/ast" + "github.com/aden-q/monkey/internal/evaluator" + "github.com/aden-q/monkey/internal/lexer" + "github.com/aden-q/monkey/internal/object" + "github.com/aden-q/monkey/internal/parser" + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +var _ = Describe("Evaluator", func() { + var ( + text string + l lexer.Lexer + p parser.Parser + e evaluator.Evaluator + program *ast.Program + errs []error + ) + + BeforeEach(func() { + l = lexer.New() + p = parser.New(l) + e = evaluator.New() + }) + + Describe("Eval", func() { + Context("integer object", func() { + It("integer expression", func() { + text = ` + 5; + ` + expectedObject := object.NewInteger(5) + expectedErrors := []error{} + + // parse the program + program, errs = p.ParseProgram(text) + Expect(errs).To(Equal(expectedErrors)) + + // evaluate the AST tree + obj, err := e.Eval(program) + Expect(err).To(BeNil()) + Expect(obj).To(Equal(expectedObject)) + }) + + It("prefix minus operator expression", func() { + text = ` + -5; + ` + expectedObject := object.NewInteger(-5) + expectedErrors := []error{} + + // parse the program + program, errs = p.ParseProgram(text) + Expect(errs).To(Equal(expectedErrors)) + + // evaluate the AST tree + obj, err := e.Eval(program) + Expect(err).To(BeNil()) + Expect(obj).To(Equal(expectedObject)) + }) + + It("infix plus operator expression", func() { + text = ` + 5 + 5; + ` + expectedObject := object.NewInteger(10) + expectedErrors := []error{} + + // parse the program + program, errs = p.ParseProgram(text) + Expect(errs).To(Equal(expectedErrors)) + + // evaluate the AST tree + obj, err := e.Eval(program) + Expect(err).To(BeNil()) + Expect(obj).To(Equal(expectedObject)) + }) + + It("infix minus operator expression", func() { + text = ` + 5 - 5; + ` + expectedObject := object.NewInteger(0) + expectedErrors := []error{} + + // parse the program + program, errs = p.ParseProgram(text) + Expect(errs).To(Equal(expectedErrors)) + + // evaluate the AST tree + obj, err := e.Eval(program) + Expect(err).To(BeNil()) + Expect(obj).To(Equal(expectedObject)) + }) + + It("infix multiply operator expression", func() { + text = ` + 5 * 5; + ` + expectedObject := object.NewInteger(25) + expectedErrors := []error{} + + // parse the program + program, errs = p.ParseProgram(text) + Expect(errs).To(Equal(expectedErrors)) + + // evaluate the AST tree + obj, err := e.Eval(program) + Expect(err).To(BeNil()) + Expect(obj).To(Equal(expectedObject)) + }) + + It("infix division operator expression", func() { + text = ` + 10 / 5; + ` + expectedObject := object.NewInteger(2) + expectedErrors := []error{} + + // parse the program + program, errs = p.ParseProgram(text) + Expect(errs).To(Equal(expectedErrors)) + + // evaluate the AST tree + obj, err := e.Eval(program) + Expect(err).To(BeNil()) + Expect(obj).To(Equal(expectedObject)) + }) + + It("infix mix-operator expression", func() { + text = ` + 5 + 5 - 2 + 10 * 3 / 5; + ` + expectedObject := object.NewInteger(14) + expectedErrors := []error{} + + // parse the program + program, errs = p.ParseProgram(text) + Expect(errs).To(Equal(expectedErrors)) + + // evaluate the AST tree + obj, err := e.Eval(program) + Expect(err).To(BeNil()) + Expect(obj).To(Equal(expectedObject)) + }) + }) + + Context("boolean object", func() { + It("boolean expression", func() { + text = ` + true; + ` + expectedObject := object.TRUE + expectedErrors := []error{} + + // parse the program + program, errs = p.ParseProgram(text) + Expect(errs).To(Equal(expectedErrors)) + + // evaluate the AST tree + obj, err := e.Eval(program) + Expect(err).To(BeNil()) + Expect(obj).To(Equal(expectedObject)) + }) + + It("prefix bang boolean expression", func() { + text = ` + !true; + ` + expectedObject := object.FALSE + expectedErrors := []error{} + + // parse the program + program, errs = p.ParseProgram(text) + Expect(errs).To(Equal(expectedErrors)) + + // evaluate the AST tree + obj, err := e.Eval(program) + Expect(err).To(BeNil()) + Expect(obj).To(Equal(expectedObject)) + }) + + It("prefix bang boolean expression", func() { + text = ` + !false; + ` + expectedObject := object.TRUE + expectedErrors := []error{} + + // parse the program + program, errs = p.ParseProgram(text) + Expect(errs).To(Equal(expectedErrors)) + + // evaluate the AST tree + obj, err := e.Eval(program) + Expect(err).To(BeNil()) + Expect(obj).To(Equal(expectedObject)) + }) + + It("prefix bang boolean expression", func() { + text = ` + !5; + ` + expectedObject := object.FALSE + expectedErrors := []error{} + + // parse the program + program, errs = p.ParseProgram(text) + Expect(errs).To(Equal(expectedErrors)) + + // evaluate the AST tree + obj, err := e.Eval(program) + Expect(err).To(BeNil()) + Expect(obj).To(Equal(expectedObject)) + }) + + It("prefix bang boolean expression", func() { + text = ` + !!true; + ` + expectedObject := object.TRUE + expectedErrors := []error{} + + // parse the program + program, errs = p.ParseProgram(text) + Expect(errs).To(Equal(expectedErrors)) + + // evaluate the AST tree + obj, err := e.Eval(program) + Expect(err).To(BeNil()) + Expect(obj).To(Equal(expectedObject)) + }) + + It("prefix bang boolean expression", func() { + text = ` + !!false; + ` + expectedObject := object.FALSE + expectedErrors := []error{} + + // parse the program + program, errs = p.ParseProgram(text) + Expect(errs).To(Equal(expectedErrors)) + + // evaluate the AST tree + obj, err := e.Eval(program) + Expect(err).To(BeNil()) + Expect(obj).To(Equal(expectedObject)) + }) + + It("prefix bang boolean expression", func() { + text = ` + !!5; + ` + expectedObject := object.TRUE + expectedErrors := []error{} + + // parse the program + program, errs = p.ParseProgram(text) + Expect(errs).To(Equal(expectedErrors)) + + // evaluate the AST tree + obj, err := e.Eval(program) + Expect(err).To(BeNil()) + Expect(obj).To(Equal(expectedObject)) + }) + + It("infix comparision expression", func() { + text = ` + 1 < 2; + ` + expectedObject := object.TRUE + expectedErrors := []error{} + + // parse the program + program, errs = p.ParseProgram(text) + Expect(errs).To(Equal(expectedErrors)) + + // evaluate the AST tree + obj, err := e.Eval(program) + Expect(err).To(BeNil()) + Expect(obj).To(Equal(expectedObject)) + }) + + It("infix comparision expression", func() { + text = ` + 1 <= 2; + ` + expectedObject := object.TRUE + expectedErrors := []error{} + + // parse the program + program, errs = p.ParseProgram(text) + Expect(errs).To(Equal(expectedErrors)) + + // evaluate the AST tree + obj, err := e.Eval(program) + Expect(err).To(BeNil()) + Expect(obj).To(Equal(expectedObject)) + }) + + It("infix comparision expression", func() { + text = ` + 2 < 1; + ` + expectedObject := object.FALSE + expectedErrors := []error{} + + // parse the program + program, errs = p.ParseProgram(text) + Expect(errs).To(Equal(expectedErrors)) + + // evaluate the AST tree + obj, err := e.Eval(program) + Expect(err).To(BeNil()) + Expect(obj).To(Equal(expectedObject)) + }) + + It("infix comparision expression", func() { + text = ` + 2 <= 1; + ` + expectedObject := object.FALSE + expectedErrors := []error{} + + // parse the program + program, errs = p.ParseProgram(text) + Expect(errs).To(Equal(expectedErrors)) + + // evaluate the AST tree + obj, err := e.Eval(program) + Expect(err).To(BeNil()) + Expect(obj).To(Equal(expectedObject)) + }) + + It("infix comparision expression", func() { + text = ` + 1 > 2; + ` + expectedObject := object.FALSE + expectedErrors := []error{} + + // parse the program + program, errs = p.ParseProgram(text) + Expect(errs).To(Equal(expectedErrors)) + + // evaluate the AST tree + obj, err := e.Eval(program) + Expect(err).To(BeNil()) + Expect(obj).To(Equal(expectedObject)) + }) + + It("infix comparision expression", func() { + text = ` + 1 >= 2; + ` + expectedObject := object.FALSE + expectedErrors := []error{} + + // parse the program + program, errs = p.ParseProgram(text) + Expect(errs).To(Equal(expectedErrors)) + + // evaluate the AST tree + obj, err := e.Eval(program) + Expect(err).To(BeNil()) + Expect(obj).To(Equal(expectedObject)) + }) + + It("infix comparision expression", func() { + text = ` + 2 > 1; + ` + expectedObject := object.TRUE + expectedErrors := []error{} + + // parse the program + program, errs = p.ParseProgram(text) + Expect(errs).To(Equal(expectedErrors)) + + // evaluate the AST tree + obj, err := e.Eval(program) + Expect(err).To(BeNil()) + Expect(obj).To(Equal(expectedObject)) + }) + + It("infix comparision expression", func() { + text = ` + 2 >= 1; + ` + expectedObject := object.TRUE + expectedErrors := []error{} + + // parse the program + program, errs = p.ParseProgram(text) + Expect(errs).To(Equal(expectedErrors)) + + // evaluate the AST tree + obj, err := e.Eval(program) + Expect(err).To(BeNil()) + Expect(obj).To(Equal(expectedObject)) + }) + + It("infix comparision expression", func() { + text = ` + 1 == 1; + ` + expectedObject := object.TRUE + expectedErrors := []error{} + + // parse the program + program, errs = p.ParseProgram(text) + Expect(errs).To(Equal(expectedErrors)) + + // evaluate the AST tree + obj, err := e.Eval(program) + Expect(err).To(BeNil()) + Expect(obj).To(Equal(expectedObject)) + }) + + It("infix comparision expression", func() { + text = ` + 2 == 1; + ` + expectedObject := object.FALSE + expectedErrors := []error{} + + // parse the program + program, errs = p.ParseProgram(text) + Expect(errs).To(Equal(expectedErrors)) + + // evaluate the AST tree + obj, err := e.Eval(program) + Expect(err).To(BeNil()) + Expect(obj).To(Equal(expectedObject)) + }) + + It("infix comparision expression", func() { + text = ` + 2 != 1; + ` + expectedObject := object.TRUE + expectedErrors := []error{} + + // parse the program + program, errs = p.ParseProgram(text) + Expect(errs).To(Equal(expectedErrors)) + + // evaluate the AST tree + obj, err := e.Eval(program) + Expect(err).To(BeNil()) + Expect(obj).To(Equal(expectedObject)) + }) + + It("infix comparision expression", func() { + text = ` + 1 != 1; + ` + expectedObject := object.FALSE + expectedErrors := []error{} + + // parse the program + program, errs = p.ParseProgram(text) + Expect(errs).To(Equal(expectedErrors)) + + // evaluate the AST tree + obj, err := e.Eval(program) + Expect(err).To(BeNil()) + Expect(obj).To(Equal(expectedObject)) + }) + + It("infix comparision expression", func() { + text = ` + true == true; + ` + expectedObject := object.TRUE + expectedErrors := []error{} + + // parse the program + program, errs = p.ParseProgram(text) + Expect(errs).To(Equal(expectedErrors)) + + // evaluate the AST tree + obj, err := e.Eval(program) + Expect(err).To(BeNil()) + Expect(obj).To(Equal(expectedObject)) + }) + + It("infix comparision expression", func() { + text = ` + false == false; + ` + expectedObject := object.TRUE + expectedErrors := []error{} + + // parse the program + program, errs = p.ParseProgram(text) + Expect(errs).To(Equal(expectedErrors)) + + // evaluate the AST tree + obj, err := e.Eval(program) + Expect(err).To(BeNil()) + Expect(obj).To(Equal(expectedObject)) + }) + + It("infix comparision expression", func() { + text = ` + true == false; + ` + expectedObject := object.FALSE + expectedErrors := []error{} + + // parse the program + program, errs = p.ParseProgram(text) + Expect(errs).To(Equal(expectedErrors)) + + // evaluate the AST tree + obj, err := e.Eval(program) + Expect(err).To(BeNil()) + Expect(obj).To(Equal(expectedObject)) + }) + + It("infix comparision expression", func() { + text = ` + true != true; + ` + expectedObject := object.FALSE + expectedErrors := []error{} + + // parse the program + program, errs = p.ParseProgram(text) + Expect(errs).To(Equal(expectedErrors)) + + // evaluate the AST tree + obj, err := e.Eval(program) + Expect(err).To(BeNil()) + Expect(obj).To(Equal(expectedObject)) + }) + + It("infix comparision expression", func() { + text = ` + true != false; + ` + expectedObject := object.TRUE + expectedErrors := []error{} + + // parse the program + program, errs = p.ParseProgram(text) + Expect(errs).To(Equal(expectedErrors)) + + // evaluate the AST tree + obj, err := e.Eval(program) + Expect(err).To(BeNil()) + Expect(obj).To(Equal(expectedObject)) + }) + }) + + Context("if conditionals", func() { + It("if condition is truthy", func() { + text = ` + if (true) { + 10; + }; + ` + expectedObject := object.NewInteger(10) + expectedErrors := []error{} + + // parse the program + program, errs = p.ParseProgram(text) + Expect(errs).To(Equal(expectedErrors)) + + // evaluate the AST tree + obj, err := e.Eval(program) + Expect(err).To(BeNil()) + Expect(obj).To(Equal(expectedObject)) + }) + + It("if condition is truthy", func() { + text = ` + if (5) { + 10; + }; + ` + expectedObject := object.NewInteger(10) + expectedErrors := []error{} + + // parse the program + program, errs = p.ParseProgram(text) + Expect(errs).To(Equal(expectedErrors)) + + // evaluate the AST tree + obj, err := e.Eval(program) + Expect(err).To(BeNil()) + Expect(obj).To(Equal(expectedObject)) + }) + + It("if condition is truthy", func() { + text = ` + if (1 < 2) { + 10; + }; + ` + expectedObject := object.NewInteger(10) + expectedErrors := []error{} + + // parse the program + program, errs = p.ParseProgram(text) + Expect(errs).To(Equal(expectedErrors)) + + // evaluate the AST tree + obj, err := e.Eval(program) + Expect(err).To(BeNil()) + Expect(obj).To(Equal(expectedObject)) + }) + + It("if condition is false", func() { + text = ` + if (false) { + 10; + }; + ` + expectedObject := object.NIL + expectedErrors := []error{} + + // parse the program + program, errs = p.ParseProgram(text) + Expect(errs).To(Equal(expectedErrors)) + + // evaluate the AST tree + obj, err := e.Eval(program) + Expect(err).To(BeNil()) + Expect(obj).To(Equal(expectedObject)) + }) + }) + }) +}) diff --git a/internal/object/object.go b/internal/object/object.go new file mode 100644 index 0000000..6cdde1b --- /dev/null +++ b/internal/object/object.go @@ -0,0 +1,93 @@ +package object + +import "strconv" + +// interface compliance check +var _ Object = (*Integer)(nil) +var _ Object = (*Boolean)(nil) +var _ Object = (*Nil)(nil) + +type ObjectType string + +var ( + INTEGER_OBJ = ObjectType("INTEGER") + BOOLEAN_OBJ = ObjectType("BOOLEAN") + NIL_OBJ = ObjectType("NIL") +) + +// boolean literal objects +var ( + TRUE = NewBoolean(true) + FALSE = NewBoolean(false) + NIL = NewNil() +) + +type Object interface { + Type() ObjectType + Inspect() string + IsTruthy() bool +} + +// Integer +type Integer struct { + Value int64 +} + +func NewInteger(value int64) *Integer { + return &Integer{ + Value: value, + } +} + +func (i *Integer) Type() ObjectType { + return INTEGER_OBJ +} + +func (i *Integer) Inspect() string { + return strconv.FormatInt(i.Value, 10) +} + +func (i *Integer) IsTruthy() bool { + return i.Value != 0 +} + +type Boolean struct { + Value bool +} + +func NewBoolean(value bool) *Boolean { + return &Boolean{ + Value: value, + } +} + +func (b *Boolean) Type() ObjectType { + return BOOLEAN_OBJ +} + +func (b *Boolean) Inspect() string { + return strconv.FormatBool(b.Value) +} + +func (b *Boolean) IsTruthy() bool { + return b.Value +} + +// Nil represents the absence of any value +type Nil struct{} + +func NewNil() *Nil { + return &Nil{} +} + +func (n *Nil) Type() ObjectType { + return NIL_OBJ +} + +func (n *Nil) Inspect() string { + return "nil" +} + +func (n *Nil) IsTruthy() bool { + return false +} diff --git a/internal/object/object_suite_test.go b/internal/object/object_suite_test.go new file mode 100644 index 0000000..a0e9cec --- /dev/null +++ b/internal/object/object_suite_test.go @@ -0,0 +1,13 @@ +package object_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestObject(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Object Suite") +} diff --git a/internal/object/object_test.go b/internal/object/object_test.go new file mode 100644 index 0000000..b81c494 --- /dev/null +++ b/internal/object/object_test.go @@ -0,0 +1,80 @@ +package object_test + +import ( + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" + + "github.com/aden-q/monkey/internal/object" +) + +var _ = Describe("Object", func() { + Describe("Integer", func() { + Context("integer object", func() { + It("truthy integer object", func() { + var val int64 = 5 + + expectedIntegerObj := &object.Integer{ + Value: val, + } + obj := object.NewInteger(val) + + Expect(obj).To(Equal(expectedIntegerObj)) + Expect(obj.Inspect()).To(Equal("5")) + Expect(obj.Type()).To(Equal(object.INTEGER_OBJ)) + Expect(obj.IsTruthy()).To(Equal(true)) + }) + + It("false integer object", func() { + var val int64 = 0 + + expectedIntegerObj := &object.Integer{ + Value: val, + } + obj := object.NewInteger(val) + + Expect(obj).To(Equal(expectedIntegerObj)) + Expect(obj.Inspect()).To(Equal("0")) + Expect(obj.Type()).To(Equal(object.INTEGER_OBJ)) + Expect(obj.IsTruthy()).To(Equal(false)) + }) + }) + }) + + Describe("Boolean", func() { + It("truthy boolean object", func() { + var val bool = true + + expectedBooleanObj := object.TRUE + obj := object.NewBoolean(val) + + Expect(obj).To(Equal(expectedBooleanObj)) + Expect(obj.Inspect()).To(Equal("true")) + Expect(obj.Type()).To(Equal(object.BOOLEAN_OBJ)) + Expect(obj.IsTruthy()).To(Equal(true)) + }) + + It("false boolean object", func() { + var val bool = false + + expectedBooleanObj := object.FALSE + obj := object.NewBoolean(val) + + Expect(obj).To(Equal(expectedBooleanObj)) + Expect(obj.Inspect()).To(Equal("false")) + Expect(obj.Type()).To(Equal(object.BOOLEAN_OBJ)) + Expect(obj.IsTruthy()).To(Equal(false)) + }) + }) + + Describe("Boolean", func() { + It("false nil object", func() { + expectedNilObj := object.NIL + obj := object.NewNil() + + Expect(obj).To(Equal(expectedNilObj)) + Expect(obj.Inspect()).To(Equal("nil")) + Expect(obj.Type()).To(Equal(object.NIL_OBJ)) + Expect(obj.IsTruthy()).To(Equal(false)) + }) + }) +}) diff --git a/internal/parser/parser.go b/internal/parser/parser.go index 758194d..6a5e176 100644 --- a/internal/parser/parser.go +++ b/internal/parser/parser.go @@ -106,6 +106,14 @@ func (p *parser) ParseProgram(text string) (*ast.Program, []error) { // in each iteration of the for loop here, we parse a single statement separated by a semicolon ; for p.curToken.Type != token.EOF { + if p.curToken.Type == token.SEMICOLON { + // this is an empty statement (i.e. only a single ; token in the whole statement) + // treat it as an empty expression statement, instead of ignoring it + program.Statements = append(program.Statements, ast.NewExpressionStatement(nil)) + p.nextToken() + continue + } + // parse a single statement every time stmt, err := p.parseStatment() if err != nil { @@ -157,7 +165,7 @@ func (p *parser) parseStatment() (ast.Statement, error) { // so that we can continue parsing the following statements p.nextToken() - return stmt, err + return stmt, nil } // parseLetStatement parses a single let statement diff --git a/internal/parser/parser_test.go b/internal/parser/parser_test.go index 5524182..5ad27a8 100644 --- a/internal/parser/parser_test.go +++ b/internal/parser/parser_test.go @@ -392,23 +392,22 @@ var _ = Describe("Parser", func() { }) }) - // TODO: parse empty statement - // Context("special statements", func() { - // It("empty statement", func() { - // text = ` - // ; - // ` - // expectedProgram := &ast.Program{ - // Statements: []ast.Statement{ - // ast.NewExpressionStatement(nil), - // }, - // } - // expectedErrors := []error{} - - // program, errs = p.ParseProgram(text) - // Expect(program).To(Equal(expectedProgram)) - // Expect(errs).To(Equal(expectedErrors)) - // }) - // }) + Context("special statements", func() { + It("empty statement", func() { + text = ` + ; + ` + expectedProgram := &ast.Program{ + Statements: []ast.Statement{ + ast.NewExpressionStatement(nil), + }, + } + expectedErrors := []error{} + + program, errs = p.ParseProgram(text) + Expect(program).To(Equal(expectedProgram)) + Expect(errs).To(Equal(expectedErrors)) + }) + }) }) }) diff --git a/internal/repl/repl.go b/internal/repl/repl.go index 32b44fd..e125ac0 100644 --- a/internal/repl/repl.go +++ b/internal/repl/repl.go @@ -5,6 +5,7 @@ import ( "fmt" "io" + "github.com/aden-q/monkey/internal/evaluator" "github.com/aden-q/monkey/internal/lexer" "github.com/aden-q/monkey/internal/parser" ) @@ -42,6 +43,7 @@ func (r *repl) Start(in io.ReadCloser, out io.WriteCloser, userName string) { scanner := bufio.NewScanner(in) l := lexer.New() p := parser.New(l) + e := evaluator.New() fmt.Print(MONKEY_FACE) fmt.Printf("Hello %s! This is the Monkey programming language!\n", userName) @@ -59,10 +61,17 @@ func (r *repl) Start(in io.ReadCloser, out io.WriteCloser, userName string) { program, errs := p.ParseProgram(line) if len(errs) != 0 { printParserErrors(out, errs) + continue + } + + res, err := e.Eval(program) + if err != nil { + fmt.Printf("Error: %v\n", err) + continue } // TODO: PrettyPrint - fmt.Println(program.String()) + fmt.Println(res.Inspect()) } } diff --git a/internal/token/precedence_test.go b/internal/token/precedence_test.go new file mode 100644 index 0000000..acb74ad --- /dev/null +++ b/internal/token/precedence_test.go @@ -0,0 +1,10 @@ +package token_test + +import ( + . "github.com/onsi/ginkgo/v2" + _ "github.com/onsi/gomega" +) + +var _ = Describe("Precedence", func() { + +}) diff --git a/internal/token/token_suite_test.go b/internal/token/token_suite_test.go new file mode 100644 index 0000000..a9beb41 --- /dev/null +++ b/internal/token/token_suite_test.go @@ -0,0 +1,13 @@ +package token_test + +import ( + "testing" + + . "github.com/onsi/ginkgo/v2" + . "github.com/onsi/gomega" +) + +func TestToken(t *testing.T) { + RegisterFailHandler(Fail) + RunSpecs(t, "Token Suite") +} diff --git a/internal/token/token_test.go b/internal/token/token_test.go new file mode 100644 index 0000000..a788774 --- /dev/null +++ b/internal/token/token_test.go @@ -0,0 +1,10 @@ +package token_test + +import ( + . "github.com/onsi/ginkgo/v2" + _ "github.com/onsi/gomega" +) + +var _ = Describe("Token", func() { + +})