diff --git a/cmd/repl/main.go b/cmd/repl/main.go new file mode 100644 index 0000000..949c7e9 --- /dev/null +++ b/cmd/repl/main.go @@ -0,0 +1,19 @@ +package main + +import ( + "fmt" + "os" + "os/user" + + "code.jmug.me/jmug/interpreter-in-go/pkg/repl" +) + +func main() { + user, err := user.Current() + if err != nil { + panic(err) + } + fmt.Printf("Hello %s, this is the Monkey programming language!\n", user.Username) + fmt.Println("Go ahead, type something :)") + repl.Start(os.Stdin, os.Stdout) +} diff --git a/go.mod b/go.mod new file mode 100644 index 0000000..0cb3e05 --- /dev/null +++ b/go.mod @@ -0,0 +1,3 @@ +module code.jmug.me/jmug/interpreter-in-go + +go 1.23.3 diff --git a/pkg/ast/array.go b/pkg/ast/array.go new file mode 100644 index 0000000..fbc7b15 --- /dev/null +++ b/pkg/ast/array.go @@ -0,0 +1,29 @@ +package ast + +import ( + "bytes" + "strings" + + "code.jmug.me/jmug/interpreter-in-go/pkg/token" +) + +type ArrayLiteral struct { + Token token.Token // The '[' token + Elements []Expression +} + +func (al *ArrayLiteral) expressionNode() {} +func (al *ArrayLiteral) TokenLiteral() string { + return al.Token.Literal +} +func (al *ArrayLiteral) String() string { + var out bytes.Buffer + elements := []string{} + for _, el := range al.Elements { + elements = append(elements, el.String()) + } + out.WriteString("[") + out.WriteString(strings.Join(elements, ", ")) + out.WriteString("]") + return out.String() +} diff --git a/pkg/ast/ast.go b/pkg/ast/ast.go new file mode 100644 index 0000000..62e26fd --- /dev/null +++ b/pkg/ast/ast.go @@ -0,0 +1,39 @@ +package ast + +import ( + "bytes" +) + +type Node interface { + TokenLiteral() string + String() string +} + +type Statement interface { + Node + statementNode() +} + +type Expression interface { + Node + expressionNode() +} + +type Program struct { + Statements []Statement +} + +func (p *Program) TokenLiteral() string { + if len(p.Statements) > 0 { + return p.Statements[0].TokenLiteral() + } + return "" +} + +func (p *Program) String() string { + var out bytes.Buffer + for _, stmt := range p.Statements { + out.WriteString(stmt.String()) + } + return out.String() +} diff --git a/pkg/ast/ast_test.go b/pkg/ast/ast_test.go new file mode 100644 index 0000000..3912b37 --- /dev/null +++ b/pkg/ast/ast_test.go @@ -0,0 +1,29 @@ +package ast + +import ( + "testing" + + "code.jmug.me/jmug/interpreter-in-go/pkg/token" +) + +func TestString(t *testing.T) { + program := &Program{ + Statements: []Statement{ + &LetStatement{ + Token: token.Token{Type: token.LET, Literal: "let"}, + Name: &Identifier{ + Token: token.Token{Type: token.IDENT, Literal: "myVar"}, + Value: "myVar", + }, + Value: &Identifier{ + Token: token.Token{Type: token.IDENT, Literal: "anotherVar"}, + Value: "anotherVar", + }, + }, + }, + } + + if program.String() != "let myVar = anotherVar;" { + t.Errorf("program.String() wrong. got=%q", program.String()) + } +} diff --git a/pkg/ast/block.go b/pkg/ast/block.go new file mode 100644 index 0000000..108dadc --- /dev/null +++ b/pkg/ast/block.go @@ -0,0 +1,24 @@ +package ast + +import ( + "bytes" + + "code.jmug.me/jmug/interpreter-in-go/pkg/token" +) + +type BlockStatement struct { + Token token.Token // The `{` token. + Statements []Statement +} + +func (bs *BlockStatement) statementNode() {} +func (bs *BlockStatement) TokenLiteral() string { + return bs.Token.Literal +} +func (bs *BlockStatement) String() string { + var out bytes.Buffer + for _, s := range bs.Statements { + out.WriteString(s.String()) + } + return out.String() +} diff --git a/pkg/ast/boolean.go b/pkg/ast/boolean.go new file mode 100644 index 0000000..256cde5 --- /dev/null +++ b/pkg/ast/boolean.go @@ -0,0 +1,16 @@ +package ast + +import "code.jmug.me/jmug/interpreter-in-go/pkg/token" + +type Boolean struct { + Token token.Token + Value bool +} + +func (bl *Boolean) expressionNode() {} +func (bl *Boolean) TokenLiteral() string { + return bl.Token.Literal +} +func (bl *Boolean) String() string { + return bl.Token.Literal +} diff --git a/pkg/ast/call.go b/pkg/ast/call.go new file mode 100644 index 0000000..5d87790 --- /dev/null +++ b/pkg/ast/call.go @@ -0,0 +1,31 @@ +package ast + +import ( + "bytes" + "strings" + + "code.jmug.me/jmug/interpreter-in-go/pkg/token" +) + +type CallExpression struct { + Token token.Token // The ( token + Function Expression + Arguments []Expression +} + +func (ce *CallExpression) expressionNode() {} +func (ce *CallExpression) TokenLiteral() string { + return ce.Token.Literal +} +func (ce *CallExpression) String() string { + var out bytes.Buffer + out.WriteString(ce.Function.String()) + out.WriteString("(") + args := []string{} + for _, arg := range ce.Arguments { + args = append(args, arg.String()) + } + out.WriteString(strings.Join(args, ", ")) + out.WriteString(")") + return out.String() +} diff --git a/pkg/ast/expression_statement.go b/pkg/ast/expression_statement.go new file mode 100644 index 0000000..4766371 --- /dev/null +++ b/pkg/ast/expression_statement.go @@ -0,0 +1,23 @@ +package ast + +import "code.jmug.me/jmug/interpreter-in-go/pkg/token" + +// ExpressionStatement is a simple wrapper of an expression in a statement +// This is common in scripting languages and allows you to have a source line +// that is solely an expression, think of the Python REPL and how you can +// type `1 + 1` and get a result. +type ExpressionStatement struct { + Token token.Token // The first token in the expression. + Expression Expression +} + +func (es *ExpressionStatement) statementNode() {} +func (es *ExpressionStatement) TokenLiteral() string { + return es.Token.Literal +} +func (es *ExpressionStatement) String() string { + if es.Expression != nil { + return es.Expression.String() + } + return "" +} diff --git a/pkg/ast/function.go b/pkg/ast/function.go new file mode 100644 index 0000000..8cdfb9e --- /dev/null +++ b/pkg/ast/function.go @@ -0,0 +1,32 @@ +package ast + +import ( + "bytes" + "strings" + + "code.jmug.me/jmug/interpreter-in-go/pkg/token" +) + +type FunctionLiteral struct { + Token token.Token // The fn token + Parameters []*Identifier + Body *BlockStatement +} + +func (fl *FunctionLiteral) expressionNode() {} +func (fl *FunctionLiteral) TokenLiteral() string { + return fl.Token.Literal +} +func (fl *FunctionLiteral) String() string { + var out bytes.Buffer + params := []string{} + for _, p := range fl.Parameters { + params = append(params, p.String()) + } + out.WriteString(fl.TokenLiteral()) + out.WriteString("(") + out.WriteString(strings.Join(params, ", ")) + out.WriteString(") ") + out.WriteString(fl.Body.String()) + return out.String() +} diff --git a/pkg/ast/hash.go b/pkg/ast/hash.go new file mode 100644 index 0000000..7d48cb8 --- /dev/null +++ b/pkg/ast/hash.go @@ -0,0 +1,24 @@ +package ast + +import ( + "strings" + + "code.jmug.me/jmug/interpreter-in-go/pkg/token" +) + +type HashLiteral struct { + Token token.Token // The "{" token + Pairs map[Expression]Expression +} + +func (hl *HashLiteral) expressionNode() {} +func (hl *HashLiteral) TokenLiteral() string { + return hl.Token.Literal +} +func (hl *HashLiteral) String() string { + pairs := []string{} + for k, v := range hl.Pairs { + pairs = append(pairs, k.String()+":"+v.String()) + } + return "{" + strings.Join(pairs, ", ") + "}" +} diff --git a/pkg/ast/identifier.go b/pkg/ast/identifier.go new file mode 100644 index 0000000..4635ddc --- /dev/null +++ b/pkg/ast/identifier.go @@ -0,0 +1,20 @@ +package ast + +import "code.jmug.me/jmug/interpreter-in-go/pkg/token" + +// Identifier is treated as an expression because in certain +// circumstances they can return values (think `let some = other` where `other` +// is actually an expression returning a value) and this makes them easier to +// handle (according to the author). +type Identifier struct { + Token token.Token + Value string +} + +func (i *Identifier) expressionNode() {} +func (i *Identifier) TokenLiteral() string { + return i.Token.Literal +} +func (i *Identifier) String() string { + return i.Value +} diff --git a/pkg/ast/if_expression.go b/pkg/ast/if_expression.go new file mode 100644 index 0000000..e28d122 --- /dev/null +++ b/pkg/ast/if_expression.go @@ -0,0 +1,31 @@ +package ast + +import ( + "bytes" + + "code.jmug.me/jmug/interpreter-in-go/pkg/token" +) + +type IfExpression struct { + Token token.Token // The `if` token. + Condition Expression + Consequence *BlockStatement + Alternative *BlockStatement +} + +func (ie *IfExpression) expressionNode() {} +func (ie *IfExpression) TokenLiteral() string { + return ie.Token.Literal +} +func (ie *IfExpression) String() string { + var out bytes.Buffer + out.WriteString("if") + out.WriteString(ie.Condition.String()) + out.WriteString(" ") + out.WriteString(ie.Consequence.String()) + if ie.Alternative != nil { + out.WriteString("else ") + out.WriteString(ie.Alternative.String()) + } + return out.String() +} diff --git a/pkg/ast/index.go b/pkg/ast/index.go new file mode 100644 index 0000000..7484917 --- /dev/null +++ b/pkg/ast/index.go @@ -0,0 +1,21 @@ +package ast + +import ( + "fmt" + + "code.jmug.me/jmug/interpreter-in-go/pkg/token" +) + +type IndexExpression struct { + Token token.Token // The "[" token + Left Expression + Index Expression +} + +func (ie *IndexExpression) expressionNode() {} +func (ie *IndexExpression) TokenLiteral() string { + return ie.Token.Literal +} +func (ie *IndexExpression) String() string { + return fmt.Sprintf("(%s[%s])", ie.Left.String(), ie.Index.String()) +} diff --git a/pkg/ast/infix_expression.go b/pkg/ast/infix_expression.go new file mode 100644 index 0000000..53d4bf9 --- /dev/null +++ b/pkg/ast/infix_expression.go @@ -0,0 +1,18 @@ +package ast + +import "code.jmug.me/jmug/interpreter-in-go/pkg/token" + +type InfixExpression struct { + Token token.Token // The operator token + Operator string + Left Expression + Right Expression +} + +func (ie *InfixExpression) expressionNode() {} +func (ie *InfixExpression) TokenLiteral() string { + return ie.Token.Literal +} +func (ie *InfixExpression) String() string { + return "(" + ie.Left.String() + " " + ie.Operator + " " + ie.Right.String() + ")" +} diff --git a/pkg/ast/integer.go b/pkg/ast/integer.go new file mode 100644 index 0000000..4a04bef --- /dev/null +++ b/pkg/ast/integer.go @@ -0,0 +1,16 @@ +package ast + +import "code.jmug.me/jmug/interpreter-in-go/pkg/token" + +type IntegerLiteral struct { + Token token.Token + Value int64 +} + +func (il *IntegerLiteral) expressionNode() {} +func (il *IntegerLiteral) TokenLiteral() string { + return il.Token.Literal +} +func (il *IntegerLiteral) String() string { + return il.Token.Literal +} diff --git a/pkg/ast/let.go b/pkg/ast/let.go new file mode 100644 index 0000000..4f13337 --- /dev/null +++ b/pkg/ast/let.go @@ -0,0 +1,28 @@ +package ast + +import ( + "bytes" + + "code.jmug.me/jmug/interpreter-in-go/pkg/token" +) + +type LetStatement struct { + Token token.Token // TODO: This is a little redundant, figure out if I can get rid of it. + Name *Identifier + Value Expression +} + +func (ls *LetStatement) statementNode() {} +func (ls *LetStatement) TokenLiteral() string { + return ls.Token.Literal +} +func (ls *LetStatement) String() string { + var out bytes.Buffer + out.WriteString(ls.TokenLiteral() + " ") + out.WriteString(ls.Name.String() + " = ") + if ls.Value != nil { + out.WriteString(ls.Value.String()) + } + out.WriteString(";") + return out.String() +} diff --git a/pkg/ast/prefix_expression.go b/pkg/ast/prefix_expression.go new file mode 100644 index 0000000..942f0cf --- /dev/null +++ b/pkg/ast/prefix_expression.go @@ -0,0 +1,17 @@ +package ast + +import "code.jmug.me/jmug/interpreter-in-go/pkg/token" + +type PrefixExpression struct { + Token token.Token // The operator token + Operator string + Right Expression +} + +func (pe *PrefixExpression) expressionNode() {} +func (pe *PrefixExpression) TokenLiteral() string { + return pe.Token.Literal +} +func (pe *PrefixExpression) String() string { + return "(" + pe.Operator + pe.Right.String() + ")" +} diff --git a/pkg/ast/return.go b/pkg/ast/return.go new file mode 100644 index 0000000..1fa7101 --- /dev/null +++ b/pkg/ast/return.go @@ -0,0 +1,26 @@ +package ast + +import ( + "bytes" + + "code.jmug.me/jmug/interpreter-in-go/pkg/token" +) + +type ReturnStatement struct { + Token token.Token // TODO: This is a little redundant, figure out if I can get rid of it. + ReturnValue Expression +} + +func (rs *ReturnStatement) statementNode() {} +func (rs *ReturnStatement) TokenLiteral() string { + return rs.Token.Literal +} +func (rs *ReturnStatement) String() string { + var out bytes.Buffer + out.WriteString(rs.TokenLiteral()) + if rs.ReturnValue != nil { + out.WriteString(" " + rs.ReturnValue.String()) + } + out.WriteString(";") + return out.String() +} diff --git a/pkg/ast/string.go b/pkg/ast/string.go new file mode 100644 index 0000000..2b9edc6 --- /dev/null +++ b/pkg/ast/string.go @@ -0,0 +1,16 @@ +package ast + +import "code.jmug.me/jmug/interpreter-in-go/pkg/token" + +type StringLiteral struct { + Token token.Token + Value string +} + +func (s *StringLiteral) expressionNode() {} +func (s *StringLiteral) TokenLiteral() string { + return s.Token.Literal +} +func (s *StringLiteral) String() string { + return s.Token.Literal +} diff --git a/pkg/evaluator/builtins.go b/pkg/evaluator/builtins.go new file mode 100644 index 0000000..c36b6ae --- /dev/null +++ b/pkg/evaluator/builtins.go @@ -0,0 +1,121 @@ +package evaluator + +import ( + "fmt" + "os" + + "code.jmug.me/jmug/interpreter-in-go/pkg/object" +) + +var builtins = map[string]*object.Builtin{ + "puts": { + Fn: func(args ...object.Object) object.Object { + for _, arg := range args { + fmt.Println(arg.Inspect()) + } + return _NULL + }, + }, + "len": { + Fn: func(args ...object.Object) object.Object { + if len(args) != 1 { + return newError("wrong number of arguments. got=%d, want=1", + len(args)) + } + + switch arg := args[0].(type) { + case *object.String: + return &object.Integer{Value: int64(len(arg.Value))} + case *object.Array: + return &object.Integer{Value: int64(len(arg.Elements))} + default: + return newError("argument to `len` not supported, got %s", + args[0].Type()) + } + }, + }, + "first": { + Fn: func(args ...object.Object) object.Object { + if len(args) != 1 { + return newError("wrong number of arguments. got=%d, want=1", + len(args)) + } + if args[0].Type() != object.ARRAY_OBJ { + return newError("argument to `first` must be ARRAY, got %s", + args[0].Type()) + } + + arr := args[0].(*object.Array) + if len(arr.Elements) > 0 { + return arr.Elements[0] + } + + return _NULL + }, + }, + "last": { + Fn: func(args ...object.Object) object.Object { + if len(args) != 1 { + return newError("wrong number of arguments. got=%d, want=1", + len(args)) + } + if args[0].Type() != object.ARRAY_OBJ { + return newError("argument to `last` must be ARRAY, got %s", + args[0].Type()) + } + + arr := args[0].(*object.Array) + if len(arr.Elements) > 0 { + return arr.Elements[len(arr.Elements)-1] + } + return _NULL + }, + }, + "rest": { + Fn: func(args ...object.Object) object.Object { + if len(args) != 1 { + return newError("wrong number of arguments. got=%d, want=1", + len(args)) + } + if args[0].Type() != object.ARRAY_OBJ { + return newError("argument to `rest` must be ARRAY, got %s", + args[0].Type()) + } + arr := args[0].(*object.Array).Elements + arrLen := len(arr) + if arrLen > 0 { + newArr := make([]object.Object, arrLen-1) + copy(newArr, arr[1:]) + return &object.Array{Elements: newArr} + } + return _NULL + }, + }, + "push": { + Fn: func(args ...object.Object) object.Object { + if len(args) != 2 { + return newError("wrong number of arguments. got=%d, want=2", + len(args)) + } + if args[0].Type() != object.ARRAY_OBJ { + return newError("argument to `push` must be ARRAY, got %s", + args[0].Type()) + } + arr := args[0].(*object.Array).Elements + arrLen := len(arr) + newArr := make([]object.Object, arrLen+1) + copy(newArr, arr) + newArr[arrLen] = args[1] + return &object.Array{Elements: newArr} + }, + }, + "exit": { + Fn: func(args ...object.Object) object.Object { + if len(args) != 0 { + return newError("exit takes no arguments...") + } + os.Exit(0) + return nil // Make the compiler happy. + }, + }, +} diff --git a/pkg/evaluator/evaluator.go b/pkg/evaluator/evaluator.go new file mode 100644 index 0000000..a2770ff --- /dev/null +++ b/pkg/evaluator/evaluator.go @@ -0,0 +1,358 @@ +package evaluator + +import ( + "fmt" + + "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, env *object.Environment) object.Object { + switch node := node.(type) { + // Statements. + case *ast.Program: + return evalProgram(node.Statements, env) + case *ast.ExpressionStatement: + return Eval(node.Expression, env) + // Expressions. + case *ast.IntegerLiteral: + return &object.Integer{Value: node.Value} + case *ast.Boolean: + return nativeBoolToBooleanObject(node.Value) + case *ast.StringLiteral: + return &object.String{Value: node.Value} + case *ast.PrefixExpression: + right := Eval(node.Right, env) + if isError(right) { + return right + } + return evalPrefixExpression(node.Operator, right) + case *ast.InfixExpression: + left := Eval(node.Left, env) + if isError(left) { + return left + } + right := Eval(node.Right, env) + if isError(right) { + return right + } + return evalInfixExpression(node.Operator, left, right) + case *ast.BlockStatement: + return evalBlockStatement(node.Statements, env) + case *ast.IfExpression: + return evalIfExpression(node, env) + case *ast.ReturnStatement: + ret := Eval(node.ReturnValue, env) + if isError(ret) { + return ret + } + return &object.ReturnValue{Value: ret} + case *ast.LetStatement: + val := Eval(node.Value, env) + if isError(val) { + return val + } + env.Set(node.Name.Value, val) + case *ast.Identifier: + return evalIdentifier(node, env) + case *ast.FunctionLiteral: + params := node.Parameters + body := node.Body + return &object.Function{Parameters: params, Body: body, Env: env} + case *ast.CallExpression: + fn := Eval(node.Function, env) + if isError(fn) { + return fn + } + args := evalExpressions(node.Arguments, env) + if len(args) == 1 && isError(args[0]) { + return args[0] + } + return applyFunction(fn, args) + case *ast.ArrayLiteral: + els := evalExpressions(node.Elements, env) + if len(els) == 1 && isError(els[0]) { + return els[0] + } + return &object.Array{Elements: els} + case *ast.IndexExpression: + left := Eval(node.Left, env) + if isError(left) { + return left + } + index := Eval(node.Index, env) + if isError(index) { + return index + } + return evalIndexExpression(left, index) + case *ast.HashLiteral: + return evalHashLiteral(node, env) + } + return nil +} + +func evalProgram(stmts []ast.Statement, env *object.Environment) object.Object { + var res object.Object + for _, stmt := range stmts { + res = Eval(stmt, env) + switch res := res.(type) { + case *object.ReturnValue: + return res.Value + case *object.Error: + return res + } + } + return res +} + +func evalBlockStatement(stmts []ast.Statement, env *object.Environment) object.Object { + var res object.Object + for _, stmt := range stmts { + res = Eval(stmt, env) + if res != nil && (res.Type() == object.RETURN_VALUE_OBJ || res.Type() == object.ERROR_OBJ) { + return res + } + } + return res +} + +func evalPrefixExpression(op string, right object.Object) object.Object { + switch op { + case "!": + return evalBangOperatorExpression(right) + case "-": + return evalMinusPrefixOperatorExpression(right) + default: + return newError("unknown operator: %s%s", op, right.Type()) + } +} + +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 newError("unknown operator: -%s", obj.Type()) + } + val := obj.(*object.Integer).Value + return &object.Integer{Value: -val} +} + +func evalInfixExpression(op string, left, right object.Object) object.Object { + switch { + case left.Type() == object.INTEGER_OBJ && right.Type() == object.INTEGER_OBJ: + return evalIntegerInfixExpression(op, left, right) + case left.Type() == object.STRING_OBJ && right.Type() == object.STRING_OBJ: + return evalStringInfixExpression(op, left, right) + case op == "==": + return nativeBoolToBooleanObject(left == right) + case op == "!=": + return nativeBoolToBooleanObject(left != right) + case left.Type() != right.Type(): + return newError("type mismatch: %s %s %s", + left.Type(), op, right.Type()) + default: + return newError("unknown operator: %s %s %s", + left.Type(), op, right.Type()) + } +} + +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} + case "<": + return nativeBoolToBooleanObject(l < r) + case ">": + return nativeBoolToBooleanObject(l > r) + case "==": + return nativeBoolToBooleanObject(l == r) + case "!=": + return nativeBoolToBooleanObject(l != r) + default: + return newError("unknown operator: %s %s %s", + left.Type(), op, right.Type()) + } +} + +func evalStringInfixExpression(op string, left, right object.Object) object.Object { + if op != "+" { + return newError( + "unknown operator: %s %s %s", + left.Type(), op, right.Type(), + ) + } + l := left.(*object.String).Value + r := right.(*object.String).Value + return &object.String{Value: l + r} +} + +func evalIfExpression(ifExp *ast.IfExpression, env *object.Environment) object.Object { + cond := Eval(ifExp.Condition, env) + if isError(cond) { + return cond + } + if isTruthy(cond) { + return Eval(ifExp.Consequence, env) + } else if ifExp.Alternative != nil { + return Eval(ifExp.Alternative, env) + } + return _NULL +} + +func evalIdentifier(exp *ast.Identifier, env *object.Environment) object.Object { + if val, ok := env.Get(exp.Value); ok { + return val + } + if val, ok := builtins[exp.Value]; ok { + return val + } + return newError("identifier not found: " + exp.Value) +} + +func evalExpressions( + exps []ast.Expression, + env *object.Environment, +) []object.Object { + var res []object.Object + for _, exp := range exps { + ev := Eval(exp, env) + if isError(ev) { + return []object.Object{ev} + } + res = append(res, ev) + } + return res +} + +func evalIndexExpression(left, index object.Object) object.Object { + switch { + case left.Type() == object.ARRAY_OBJ && index.Type() == object.INTEGER_OBJ: + return evalArrayIndexExpression(left, index) + case left.Type() == object.HASH_OBJ: + return evalHashIndexExpression(left, index) + default: + return newError("index operator not supported: %s", left.Type()) + } +} + +func evalArrayIndexExpression(arrayObj, indexObj object.Object) object.Object { + array := arrayObj.(*object.Array).Elements + index := indexObj.(*object.Integer).Value + if index < 0 || index >= int64(len(array)) { + return _NULL + } + return array[index] +} + +func evalHashIndexExpression(hashObj, index object.Object) object.Object { + hash := hashObj.(*object.Hash) + hashable, okHash := index.(object.Hashable) + if !okHash { + return newError("unusable as hash key: %s", index.Type()) + } + pair, okPair := hash.Pairs[hashable.HashKey()] + if !okPair { + return _NULL + } + return pair.Value +} + +func evalHashLiteral(hash *ast.HashLiteral, env *object.Environment) object.Object { + pairs := map[object.HashKey]object.HashPair{} + for ke, ve := range hash.Pairs { + k := Eval(ke, env) + if isError(k) { + return k + } + hashable, ok := k.(object.Hashable) + if !ok { + return newError("unusable as hash key: %s", k.Type()) + } + v := Eval(ve, env) + if isError(v) { + return v + } + hashKey := hashable.HashKey() + pairs[hashKey] = object.HashPair{Key: k, Value: v} + } + return &object.Hash{Pairs: pairs} +} + +func applyFunction(fnObj object.Object, args []object.Object) object.Object { + switch fn := fnObj.(type) { + case *object.Function: + env := extendFunctionEnv(fn, args) + ret := Eval(fn.Body, env) + return unwrapReturnValue(ret) + case *object.Builtin: + return fn.Fn(args...) + } + return newError("not a function: %s", fnObj.Type()) +} + +func extendFunctionEnv(fn *object.Function, args []object.Object) *object.Environment { + env := object.NewEnclosedEnvironment(fn.Env) + for pi, param := range fn.Parameters { + env.Set(param.Value, args[pi]) + } + return env +} + +func unwrapReturnValue(obj object.Object) object.Object { + if ret, ok := obj.(*object.ReturnValue); ok { + return ret.Value + } + return obj +} + +func isTruthy(obj object.Object) bool { + switch obj { + case _TRUE: + return true + case _FALSE: + return false + case _NULL: + return false + } + return true +} + +func nativeBoolToBooleanObject(b bool) object.Object { + if b { + return _TRUE + } + return _FALSE +} + +func newError(format string, a ...any) *object.Error { + return &object.Error{Message: fmt.Sprintf(format, a...)} +} + +func isError(obj object.Object) bool { + return obj != nil && obj.Type() == object.ERROR_OBJ +} diff --git a/pkg/evaluator/evaluator_test.go b/pkg/evaluator/evaluator_test.go new file mode 100644 index 0000000..35f9993 --- /dev/null +++ b/pkg/evaluator/evaluator_test.go @@ -0,0 +1,630 @@ +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}, + {"1 < 2", true}, + {"1 > 2", false}, + {"1 < 1", false}, + {"1 > 1", false}, + {"1 == 1", true}, + {"1 != 1", false}, + {"1 == 2", false}, + {"1 != 2", true}, + {"true == true", true}, + {"false == false", true}, + {"true == false", false}, + {"true != false", true}, + {"false != true", true}, + {"(1 < 2) == true", true}, + {"(1 < 2) == false", false}, + {"(1 > 2) == true", false}, + {"(1 > 2) == false", true}, + } + + 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 TestIfElseExpressions(t *testing.T) { + tests := []struct { + input string + expected interface{} + }{ + {"if (true) { 10 }", 10}, + {"if (false) { 10 }", nil}, + {"if (1) { 10 }", 10}, + {"if (1 < 2) { 10 }", 10}, + {"if (1 > 2) { 10 }", nil}, + {"if (1 > 2) { 10 } else { 20 }", 20}, + {"if (1 < 2) { 10 } else { 20 }", 10}, + } + + for _, tt := range tests { + evaluated := testEval(tt.input) + integer, ok := tt.expected.(int) + if ok { + testIntegerObject(t, evaluated, int64(integer)) + } else { + testNullObject(t, evaluated) + } + } +} + +func TestReturnStatements(t *testing.T) { + tests := []struct { + input string + expected int64 + }{ + {"return 10;", 10}, + {"return 10; 9;", 10}, + {"return 2 * 5; 9;", 10}, + {"9; return 2 * 5; 9;", 10}, + {"if (10 > 1) { return 10; }", 10}, + { + ` +if (10 > 1) { + if (10 > 1) { + return 10; + } + + return 1; +} +`, + 10, + }, + { + ` +let f = fn(x) { + return x; + x + 10; +}; +f(10);`, + 10, + }, + { + ` +let f = fn(x) { + let result = x + 10; + return result; + return 10; +}; +f(10);`, + 20, + }, + } + + for _, tt := range tests { + evaluated := testEval(tt.input) + testIntegerObject(t, evaluated, tt.expected) + } +} + +func TestErrorHandling(t *testing.T) { + tests := []struct { + input string + expectedMessage string + }{ + { + "5 + true;", + "type mismatch: INTEGER + BOOLEAN", + }, + { + "5 + true; 5;", + "type mismatch: INTEGER + BOOLEAN", + }, + { + "-true", + "unknown operator: -BOOLEAN", + }, + { + "true + false;", + "unknown operator: BOOLEAN + BOOLEAN", + }, + { + "true + false + true + false;", + "unknown operator: BOOLEAN + BOOLEAN", + }, + { + "5; true + false; 5", + "unknown operator: BOOLEAN + BOOLEAN", + }, + { + `"Hello" - "World"`, + "unknown operator: STRING - STRING", + }, + { + "if (10 > 1) { true + false; }", + "unknown operator: BOOLEAN + BOOLEAN", + }, + { + ` +if (10 > 1) { + if (10 > 1) { + return true + false; + } + + return 1; +} +`, + "unknown operator: BOOLEAN + BOOLEAN", + }, + { + "foobar", + "identifier not found: foobar", + }, + { + `{"name": "Monkey"}[fn(x) { x }];`, + "unusable as hash key: FUNCTION", + }, + { + `999[1]`, + "index operator not supported: INTEGER", + }, + } + + for _, tt := range tests { + evaluated := testEval(tt.input) + + errObj, ok := evaluated.(*object.Error) + if !ok { + t.Errorf("no error object returned. got=%T(%+v)", + evaluated, evaluated) + continue + } + + if errObj.Message != tt.expectedMessage { + t.Errorf("wrong error message. expected=%q, got=%q", + tt.expectedMessage, errObj.Message) + } + } +} + +func TestLetStatements(t *testing.T) { + tests := []struct { + input string + expected int64 + }{ + {"let a = 5; a;", 5}, + {"let a = 5 * 5; a;", 25}, + {"let a = 5; let b = a; b;", 5}, + {"let a = 5; let b = a; let c = a + b + 5; c;", 15}, + } + + for _, tt := range tests { + testIntegerObject(t, testEval(tt.input), tt.expected) + } +} + +func TestFunctionObject(t *testing.T) { + input := "fn(x) { x + 2; };" + + evaluated := testEval(input) + fn, ok := evaluated.(*object.Function) + if !ok { + t.Fatalf("object is not Function. got=%T (%+v)", evaluated, evaluated) + } + + if len(fn.Parameters) != 1 { + t.Fatalf("function has wrong parameters. Parameters=%+v", + fn.Parameters) + } + + if fn.Parameters[0].String() != "x" { + t.Fatalf("parameter is not 'x'. got=%q", fn.Parameters[0]) + } + + expectedBody := "(x + 2)" + + if fn.Body.String() != expectedBody { + t.Fatalf("body is not %q. got=%q", expectedBody, fn.Body.String()) + } +} + +func TestFunctionApplication(t *testing.T) { + tests := []struct { + input string + expected int64 + }{ + {"let identity = fn(x) { x; }; identity(5);", 5}, + {"let identity = fn(x) { return x; }; identity(5);", 5}, + {"let double = fn(x) { x * 2; }; double(5);", 10}, + {"let add = fn(x, y) { x + y; }; add(5, 5);", 10}, + {"let add = fn(x, y) { x + y; }; add(5 + 5, add(5, 5));", 20}, + {"fn(x) { x; }(5)", 5}, + } + + for _, tt := range tests { + testIntegerObject(t, testEval(tt.input), tt.expected) + } +} + +func TestEnclosingEnvironments(t *testing.T) { + input := ` +let first = 10; +let second = 10; +let third = 10; + +let ourFunction = fn(first) { + let second = 20; + + first + second + third; +}; + +ourFunction(20) + first + second;` + + testIntegerObject(t, testEval(input), 70) +} + +func TestClosures(t *testing.T) { + input := ` +let newAdder = fn(x) { + fn(y) { x + y }; +}; + +let addTwo = newAdder(2); +addTwo(2);` + + testIntegerObject(t, testEval(input), 4) +} + +func TestStringLiteral(t *testing.T) { + input := `"Hello World!"` + + evaluated := testEval(input) + str, ok := evaluated.(*object.String) + if !ok { + t.Fatalf("object is not String. got=%T (%+v)", evaluated, evaluated) + } + + if str.Value != "Hello World!" { + t.Errorf("String has wrong value. got=%q", str.Value) + } +} + +func TestStringConcatenation(t *testing.T) { + input := `"Hello" + " " + "World!"` + + evaluated := testEval(input) + str, ok := evaluated.(*object.String) + if !ok { + t.Fatalf("object is not String. got=%T (%+v)", evaluated, evaluated) + } + + if str.Value != "Hello World!" { + t.Errorf("String has wrong value. got=%q", str.Value) + } +} + +func TestBuiltinFunctions(t *testing.T) { + tests := []struct { + input string + expected interface{} + }{ + {`len("")`, 0}, + {`len("four")`, 4}, + {`len("hello world")`, 11}, + {`len(1)`, "argument to `len` not supported, got INTEGER"}, + {`len("one", "two")`, "wrong number of arguments. got=2, want=1"}, + {`len([1, 2, 3])`, 3}, + {`len([])`, 0}, + {`puts("hello", "world!")`, nil}, + {`first([1, 2, 3])`, 1}, + {`first([])`, nil}, + {`first(1)`, "argument to `first` must be ARRAY, got INTEGER"}, + {`last([1, 2, 3])`, 3}, + {`last([])`, nil}, + {`last(1)`, "argument to `last` must be ARRAY, got INTEGER"}, + {`rest([1, 2, 3])`, []int{2, 3}}, + {`rest([])`, nil}, + {`push([], 1)`, []int{1}}, + {`push(1, 1)`, "argument to `push` must be ARRAY, got INTEGER"}, + } + + for _, tt := range tests { + evaluated := testEval(tt.input) + + switch expected := tt.expected.(type) { + case int: + testIntegerObject(t, evaluated, int64(expected)) + case nil: + testNullObject(t, evaluated) + case string: + errObj, ok := evaluated.(*object.Error) + if !ok { + t.Errorf("object is not Error. got=%T (%+v)", + evaluated, evaluated) + continue + } + if errObj.Message != expected { + t.Errorf("wrong error message. expected=%q, got=%q", + expected, errObj.Message) + } + case []int: + array, ok := evaluated.(*object.Array) + if !ok { + t.Errorf("obj not Array. got=%T (%+v)", evaluated, evaluated) + continue + } + + if len(array.Elements) != len(expected) { + t.Errorf("wrong num of elements. want=%d, got=%d", + len(expected), len(array.Elements)) + continue + } + + for i, expectedElem := range expected { + testIntegerObject(t, array.Elements[i], int64(expectedElem)) + } + } + } +} + +func TestArrayLiterals(t *testing.T) { + input := "[1, 2 * 2, 3 + 3]" + + evaluated := testEval(input) + result, ok := evaluated.(*object.Array) + if !ok { + t.Fatalf("object is not Array. got=%T (%+v)", evaluated, evaluated) + } + + if len(result.Elements) != 3 { + t.Fatalf("array has wrong num of elements. got=%d", + len(result.Elements)) + } + + testIntegerObject(t, result.Elements[0], 1) + testIntegerObject(t, result.Elements[1], 4) + testIntegerObject(t, result.Elements[2], 6) +} + +func TestArrayIndexExpressions(t *testing.T) { + tests := []struct { + input string + expected interface{} + }{ + { + "[1, 2, 3][0]", + 1, + }, + { + "[1, 2, 3][1]", + 2, + }, + { + "[1, 2, 3][2]", + 3, + }, + { + "let i = 0; [1][i];", + 1, + }, + { + "[1, 2, 3][1 + 1];", + 3, + }, + { + "let myArray = [1, 2, 3]; myArray[2];", + 3, + }, + { + "let myArray = [1, 2, 3]; myArray[0] + myArray[1] + myArray[2];", + 6, + }, + { + "let myArray = [1, 2, 3]; let i = myArray[0]; myArray[i]", + 2, + }, + { + "[1, 2, 3][3]", + nil, + }, + { + "[1, 2, 3][-1]", + nil, + }, + } + + for _, tt := range tests { + evaluated := testEval(tt.input) + integer, ok := tt.expected.(int) + if ok { + testIntegerObject(t, evaluated, int64(integer)) + } else { + testNullObject(t, evaluated) + } + } +} + +func TestHashLiterals(t *testing.T) { + input := `let two = "two"; + { + "one": 10 - 9, + two: 1 + 1, + "thr" + "ee": 6 / 2, + 4: 4, + true: 5, + false: 6 + }` + + evaluated := testEval(input) + result, ok := evaluated.(*object.Hash) + if !ok { + t.Fatalf("Eval didn't return Hash. got=%T (%+v)", evaluated, evaluated) + } + + expected := map[object.HashKey]int64{ + (&object.String{Value: "one"}).HashKey(): 1, + (&object.String{Value: "two"}).HashKey(): 2, + (&object.String{Value: "three"}).HashKey(): 3, + (&object.Integer{Value: 4}).HashKey(): 4, + _TRUE.HashKey(): 5, + _FALSE.HashKey(): 6, + } + + if len(result.Pairs) != len(expected) { + t.Fatalf("Hash has wrong num of pairs. got=%d", len(result.Pairs)) + } + + for expectedKey, expectedValue := range expected { + pair, ok := result.Pairs[expectedKey] + if !ok { + t.Errorf("no pair for given key in Pairs") + } + + testIntegerObject(t, pair.Value, expectedValue) + } +} + +func TestHashIndexExpressions(t *testing.T) { + tests := []struct { + input string + expected interface{} + }{ + { + `{"foo": 5}["foo"]`, + 5, + }, + { + `{"foo": 5}["bar"]`, + nil, + }, + { + `let key = "foo"; {"foo": 5}[key]`, + 5, + }, + { + `{}["foo"]`, + nil, + }, + { + `{5: 5}[5]`, + 5, + }, + { + `{true: 5}[true]`, + 5, + }, + { + `{false: 5}[false]`, + 5, + }, + } + + for _, tt := range tests { + evaluated := testEval(tt.input) + integer, ok := tt.expected.(int) + if ok { + testIntegerObject(t, evaluated, int64(integer)) + } else { + testNullObject(t, evaluated) + } + } +} +func testEval(input string) object.Object { + l := lexer.New(input) + p := parser.New(l) + program := p.ParseProgram() + env := object.NewEnvironment() + + return Eval(program, env) +} + +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 +} + +func testNullObject(t *testing.T, obj object.Object) bool { + if obj != _NULL { + t.Errorf("object is not NULL. got=%T (%+v)", obj, obj) + return false + } + return true +} diff --git a/pkg/lexer/lexer.go b/pkg/lexer/lexer.go new file mode 100644 index 0000000..1efe8e8 --- /dev/null +++ b/pkg/lexer/lexer.go @@ -0,0 +1,160 @@ +package lexer + +import "code.jmug.me/jmug/interpreter-in-go/pkg/token" + +type Lexer struct { + input string + position int + readPosition int + ch byte +} + +func New(input string) *Lexer { + l := &Lexer{input: input} + l.readChar() + return l +} + +func (l *Lexer) NextToken() token.Token { + l.skipWhitespace() + var tok token.Token + switch l.ch { + case '=': + if l.peekChar() == '=' { + ch := l.ch + l.readChar() + literal := string(ch) + string(l.ch) + tok.Type = token.EQ + tok.Literal = literal + } else { + tok = newToken(token.ASSIGN, l.ch) + } + case '+': + tok = newToken(token.PLUS, l.ch) + case '-': + tok = newToken(token.MINUS, l.ch) + case '!': + if l.peekChar() == '=' { + ch := l.ch + l.readChar() + literal := string(ch) + string(l.ch) + tok.Type = token.NOT_EQ + tok.Literal = literal + } else { + tok = newToken(token.BANG, l.ch) + } + case '*': + tok = newToken(token.ASTERISK, l.ch) + case '/': + tok = newToken(token.SLASH, l.ch) + case '<': + tok = newToken(token.LT, l.ch) + case '>': + tok = newToken(token.GT, l.ch) + case ',': + tok = newToken(token.COMMA, l.ch) + case ';': + tok = newToken(token.SEMICOLON, l.ch) + case ':': + tok = newToken(token.COLON, l.ch) + case '(': + tok = newToken(token.LPAREN, l.ch) + case ')': + tok = newToken(token.RPAREN, l.ch) + case '{': + tok = newToken(token.LBRACE, l.ch) + case '}': + tok = newToken(token.RBRACE, l.ch) + case '[': + tok = newToken(token.LBRACKET, l.ch) + case ']': + tok = newToken(token.RBRACKET, l.ch) + case '"': + tok.Type = token.STRING + tok.Literal = l.readString() + case 0: + tok.Literal = "" + tok.Type = token.EOF + default: + if isLetter(l.ch) { + tok.Literal = l.readIdentifier() + tok.Type = token.LookupIdent(tok.Literal) + // Don't let it fall through because readIdentifier calls readChar. + return tok + } else if isDigit(l.ch) { + tok.Literal = l.readNumber() + tok.Type = token.INT + // Don't let it fall through because readNumber calls readChar. + return tok + } else { + tok = newToken(token.ILLEGAL, l.ch) + } + } + l.readChar() + return tok +} + +func (l *Lexer) readChar() { + if l.readPosition >= len(l.input) { + l.ch = 0 + } else { + l.ch = l.input[l.readPosition] + } + l.position = l.readPosition + l.readPosition += 1 +} + +func (l *Lexer) peekChar() byte { + if l.readPosition >= len(l.input) { + return 0 + } + return l.input[l.readPosition] +} + +func (l *Lexer) readIdentifier() string { + position := l.position + for isLetter(l.ch) { + l.readChar() + } + // Slicing until l.position instead of readPosition because the last read + // char was not a letter. + return l.input[position:l.position] +} + +func (l *Lexer) readNumber() string { + position := l.position + for isDigit(l.ch) { + l.readChar() + } + // Slicing until l.position instead of readPosition because the last read + // char was not a letter. + return l.input[position:l.position] +} + +func (l *Lexer) readString() string { + // Don't include the quotes in the literal. + position := l.position + 1 + l.readChar() + for l.ch != '"' && l.ch != 0 { + l.readChar() + } + return l.input[position:l.position] +} + +func (l *Lexer) skipWhitespace() { + for l.ch == ' ' || l.ch == '\t' || l.ch == '\n' || l.ch == '\r' { + l.readChar() + } +} + +func newToken(tokenType token.TokenType, ch byte) token.Token { + return token.Token{Type: tokenType, Literal: string(ch)} +} + +func isLetter(ch byte) bool { + return ('a' <= ch && ch <= 'z') || ('A' <= ch && ch <= 'Z') || ch == '_' +} + +func isDigit(ch byte) bool { + return '0' <= ch && ch <= '9' +} diff --git a/pkg/lexer/lexer_test.go b/pkg/lexer/lexer_test.go new file mode 100644 index 0000000..564f0d3 --- /dev/null +++ b/pkg/lexer/lexer_test.go @@ -0,0 +1,144 @@ +package lexer + +import ( + "testing" + + "code.jmug.me/jmug/interpreter-in-go/pkg/token" +) + +func TestNextToken(t *testing.T) { + input := `let five = 5; +let ten = 10; + +let add = fn(x, y) { + x + y; +}; + +let result = add(five, ten); +!-/*5; +5 < 10 > 5; + +if (5 < 10) { + return true; +} else { + return false; +} + +10 == 10; +10 != 9; +"foobar" +"foo bar" +[1, 2]; +{1: 2}; +` + + tests := []struct { + expectedType token.TokenType + expectedLiteral string + }{ + {token.LET, "let"}, + {token.IDENT, "five"}, + {token.ASSIGN, "="}, + {token.INT, "5"}, + {token.SEMICOLON, ";"}, + {token.LET, "let"}, + {token.IDENT, "ten"}, + {token.ASSIGN, "="}, + {token.INT, "10"}, + {token.SEMICOLON, ";"}, + {token.LET, "let"}, + {token.IDENT, "add"}, + {token.ASSIGN, "="}, + {token.FUNCTION, "fn"}, + {token.LPAREN, "("}, + {token.IDENT, "x"}, + {token.COMMA, ","}, + {token.IDENT, "y"}, + {token.RPAREN, ")"}, + {token.LBRACE, "{"}, + {token.IDENT, "x"}, + {token.PLUS, "+"}, + {token.IDENT, "y"}, + {token.SEMICOLON, ";"}, + {token.RBRACE, "}"}, + {token.SEMICOLON, ";"}, + {token.LET, "let"}, + {token.IDENT, "result"}, + {token.ASSIGN, "="}, + {token.IDENT, "add"}, + {token.LPAREN, "("}, + {token.IDENT, "five"}, + {token.COMMA, ","}, + {token.IDENT, "ten"}, + {token.RPAREN, ")"}, + {token.SEMICOLON, ";"}, + {token.BANG, "!"}, + {token.MINUS, "-"}, + {token.SLASH, "/"}, + {token.ASTERISK, "*"}, + {token.INT, "5"}, + {token.SEMICOLON, ";"}, + {token.INT, "5"}, + {token.LT, "<"}, + {token.INT, "10"}, + {token.GT, ">"}, + {token.INT, "5"}, + {token.SEMICOLON, ";"}, + {token.IF, "if"}, + {token.LPAREN, "("}, + {token.INT, "5"}, + {token.LT, "<"}, + {token.INT, "10"}, + {token.RPAREN, ")"}, + {token.LBRACE, "{"}, + {token.RETURN, "return"}, + {token.TRUE, "true"}, + {token.SEMICOLON, ";"}, + {token.RBRACE, "}"}, + {token.ELSE, "else"}, + {token.LBRACE, "{"}, + {token.RETURN, "return"}, + {token.FALSE, "false"}, + {token.SEMICOLON, ";"}, + {token.RBRACE, "}"}, + {token.INT, "10"}, + {token.EQ, "=="}, + {token.INT, "10"}, + {token.SEMICOLON, ";"}, + {token.INT, "10"}, + {token.NOT_EQ, "!="}, + {token.INT, "9"}, + {token.SEMICOLON, ";"}, + {token.STRING, "foobar"}, + {token.STRING, "foo bar"}, + {token.LBRACKET, "["}, + {token.INT, "1"}, + {token.COMMA, ","}, + {token.INT, "2"}, + {token.RBRACKET, "]"}, + {token.SEMICOLON, ";"}, + {token.LBRACE, "{"}, + {token.INT, "1"}, + {token.COLON, ":"}, + {token.INT, "2"}, + {token.RBRACE, "}"}, + {token.SEMICOLON, ";"}, + {token.EOF, ""}, + } + + l := New(input) + + for i, tt := range tests { + tok := l.NextToken() + + if tok.Type != tt.expectedType { + t.Fatalf("tests[%d] - tokentype wrong. expected=%q, got=%q", + i, tt.expectedType, tok.Type) + } + + if tok.Literal != tt.expectedLiteral { + t.Fatalf("tests[%d] - literal wrong. expected=%q, got=%q", + i, tt.expectedLiteral, tok.Literal) + } + } +} diff --git a/pkg/object/environment.go b/pkg/object/environment.go new file mode 100644 index 0000000..04ace5e --- /dev/null +++ b/pkg/object/environment.go @@ -0,0 +1,30 @@ +package object + +func NewEnvironment() *Environment { + return &Environment{store: map[string]Object{}} +} + +func NewEnclosedEnvironment(outer *Environment) *Environment { + return &Environment{ + store: map[string]Object{}, + outer: outer, + } +} + +type Environment struct { + store map[string]Object + outer *Environment +} + +func (e *Environment) Get(name string) (Object, bool) { + obj, ok := e.store[name] + if !ok && e.outer != nil { + obj, ok = e.outer.Get(name) + } + return obj, ok +} + +func (e *Environment) Set(name string, obj Object) Object { + e.store[name] = obj + return obj +} diff --git a/pkg/object/hash_key.go b/pkg/object/hash_key.go new file mode 100644 index 0000000..18c28ba --- /dev/null +++ b/pkg/object/hash_key.go @@ -0,0 +1,30 @@ +package object + +import "hash/fnv" + +type Hashable interface { + HashKey() HashKey +} + +type HashKey struct { + Type ObjectType + Value uint64 +} + +func (b *Boolean) HashKey() HashKey { + var val uint64 = 0 + if b.Value { + val = 1 + } + return HashKey{Type: b.Type(), Value: val} +} + +func (i *Integer) HashKey() HashKey { + return HashKey{Type: i.Type(), Value: uint64(i.Value)} +} + +func (s *String) HashKey() HashKey { + h := fnv.New64() + h.Write([]byte(s.Value)) + return HashKey{Type: s.Type(), Value: h.Sum64()} +} diff --git a/pkg/object/object.go b/pkg/object/object.go new file mode 100644 index 0000000..396c19b --- /dev/null +++ b/pkg/object/object.go @@ -0,0 +1,164 @@ +package object + +import ( + "bytes" + "fmt" + "strings" + + "code.jmug.me/jmug/interpreter-in-go/pkg/ast" +) + +type ObjectType string + +const ( + INTEGER_OBJ = "INTEGER" + BOOLEAN_OBJ = "BOOLEAN" + NULL_OBJ = "NULL" + RETURN_VALUE_OBJ = "RETURN" + ERROR_OBJ = "ERROR" + FUNCTION_OBJ = "FUNCTION" + STRING_OBJ = "STRING" + BUILTIN_OBJ = "BUILTIN" + ARRAY_OBJ = "ARRAY" + HASH_OBJ = "HASH" +) + +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" +} + +type ReturnValue struct { + Value Object +} + +func (rv *ReturnValue) Type() ObjectType { + return RETURN_VALUE_OBJ +} +func (rv *ReturnValue) Inspect() string { + return rv.Value.Inspect() +} + +type Error struct { + Message string +} + +func (e *Error) Type() ObjectType { + return ERROR_OBJ +} +func (e *Error) Inspect() string { + return "ERROR: " + e.Message +} + +type Function struct { + Parameters []*ast.Identifier + Body *ast.BlockStatement + Env *Environment +} + +func (f *Function) Type() ObjectType { + return FUNCTION_OBJ +} +func (f *Function) Inspect() string { + var out bytes.Buffer + params := []string{} + for _, p := range f.Parameters { + params = append(params, p.Value) + } + out.WriteString("fn") + out.WriteString("(" + strings.Join(params, ", ") + ")") + out.WriteString(" {\n" + f.Body.String() + "\n}") + return out.String() +} + +type String struct { + Value string +} + +func (s *String) Type() ObjectType { + return STRING_OBJ +} +func (s *String) Inspect() string { + return s.Value +} + +type BuiltinFunction func(args ...Object) Object +type Builtin struct { + Fn BuiltinFunction +} + +func (b *Builtin) Type() ObjectType { + return BUILTIN_OBJ +} +func (b *Builtin) Inspect() string { + return "builtin function" +} + +type Array struct { + Elements []Object +} + +func (a *Array) Type() ObjectType { + return ARRAY_OBJ +} +func (a *Array) Inspect() string { + elements := []string{} + for _, el := range a.Elements { + elements = append(elements, el.Inspect()) + } + return fmt.Sprintf("[%s]", strings.Join(elements, ", ")) +} + +type HashPair struct { + Key Object + Value Object +} + +type Hash struct { + Pairs map[HashKey]HashPair +} + +func (h *Hash) Type() ObjectType { + return HASH_OBJ +} +func (h *Hash) Inspect() string { + pairs := []string{} + for _, p := range h.Pairs { + pairs = append( + pairs, + fmt.Sprintf("%s: %s", p.Key.Inspect(), p.Value.Inspect()), + ) + } + return "{" + strings.Join(pairs, ", ") + "}" +} diff --git a/pkg/object/object_test.go b/pkg/object/object_test.go new file mode 100644 index 0000000..34d8882 --- /dev/null +++ b/pkg/object/object_test.go @@ -0,0 +1,22 @@ +package object + +import "testing" + +func TestStringHashKey(t *testing.T) { + hello1 := &String{Value: "Hello World"} + hello2 := &String{Value: "Hello World"} + diff1 := &String{Value: "My name is johnny"} + diff2 := &String{Value: "My name is johnny"} + + if hello1.HashKey() != hello2.HashKey() { + t.Errorf("strings with same content have different hash keys") + } + + if diff1.HashKey() != diff2.HashKey() { + t.Errorf("strings with same content have different hash keys") + } + + if hello1.HashKey() == diff1.HashKey() { + t.Errorf("strings with different content have same hash keys") + } +} diff --git a/pkg/parser/parser.go b/pkg/parser/parser.go new file mode 100644 index 0000000..f45d8c0 --- /dev/null +++ b/pkg/parser/parser.go @@ -0,0 +1,408 @@ +package parser + +import ( + "fmt" + "strconv" + + "code.jmug.me/jmug/interpreter-in-go/pkg/ast" + "code.jmug.me/jmug/interpreter-in-go/pkg/lexer" + "code.jmug.me/jmug/interpreter-in-go/pkg/token" +) + +type ( + prefixParseFn func() ast.Expression + infixParseFn func(ast.Expression) ast.Expression +) + +type Parser struct { + l *lexer.Lexer + errors []string + curToken token.Token + peekToken token.Token + prefixParseFns map[token.TokenType]prefixParseFn + infixParseFns map[token.TokenType]infixParseFn +} + +func New(l *lexer.Lexer) *Parser { + p := &Parser{ + l: l, + errors: []string{}, + prefixParseFns: map[token.TokenType]prefixParseFn{}, + infixParseFns: map[token.TokenType]infixParseFn{}, + } + // Prefix registrations + p.registerPrefix(token.IDENT, p.parseIdentifier) + p.registerPrefix(token.INT, p.parseIntegerLiteral) + p.registerPrefix(token.MINUS, p.parsePrefixExpression) + p.registerPrefix(token.BANG, p.parsePrefixExpression) + p.registerPrefix(token.TRUE, p.parseBoolean) + p.registerPrefix(token.FALSE, p.parseBoolean) + p.registerPrefix(token.LPAREN, p.parseGroupedExpression) + p.registerPrefix(token.IF, p.parseIfExpression) + p.registerPrefix(token.FUNCTION, p.parseFunctionLiteral) + p.registerPrefix(token.STRING, p.parseStringLiteral) + p.registerPrefix(token.LBRACKET, p.parseArrayLiteral) + p.registerPrefix(token.LBRACE, p.parseHashLiteral) + // Infix registrations + p.registerInfix(token.PLUS, p.parseInfixExpression) + p.registerInfix(token.MINUS, p.parseInfixExpression) + p.registerInfix(token.ASTERISK, p.parseInfixExpression) + p.registerInfix(token.SLASH, p.parseInfixExpression) + p.registerInfix(token.GT, p.parseInfixExpression) + p.registerInfix(token.LT, p.parseInfixExpression) + p.registerInfix(token.EQ, p.parseInfixExpression) + p.registerInfix(token.NOT_EQ, p.parseInfixExpression) + p.registerInfix(token.LPAREN, p.parseCallExpression) + p.registerInfix(token.LBRACKET, p.parseIndexExpression) + // TODO: figure out why this can't be done from `parseProgram` + p.nextToken() + p.nextToken() + return p +} + +func (p *Parser) ParseProgram() *ast.Program { + program := &ast.Program{} + program.Statements = []ast.Statement{} + for !p.curTokenIs(token.EOF) { + stmt := p.parseStatement() + if stmt != nil { + program.Statements = append(program.Statements, stmt) + } + // NOTE: For now, this is not only eating the semicolon, it is also + // eating every and all tokens until parse statement finds something + // it deems valid. + p.nextToken() + } + return program +} + +func (p *Parser) parseStatement() ast.Statement { + switch p.curToken.Type { + case token.LET: + return p.parseLetStatement() + case token.RETURN: + return p.parseReturnStatement() + } + return p.parseExpressionStatement() +} + +func (p *Parser) parseBlockStatement() *ast.BlockStatement { + block := &ast.BlockStatement{Token: p.curToken} + block.Statements = []ast.Statement{} + p.nextToken() + for !p.curTokenIs(token.RBRACE) && !p.curTokenIs(token.EOF) { + stmt := p.parseStatement() + if stmt != nil { + block.Statements = append(block.Statements, stmt) + } + // Consume the last token in the statement. + p.nextToken() + } + return block +} + +func (p *Parser) parseLetStatement() ast.Statement { + stmt := &ast.LetStatement{Token: p.curToken} + if !p.nextTokenIfPeekIs(token.IDENT) { + return nil + } + stmt.Name = &ast.Identifier{Token: p.curToken, Value: p.curToken.Literal} + if !p.nextTokenIfPeekIs(token.ASSIGN) { + return nil + } + // Consume the assign. + p.nextToken() + stmt.Value = p.parseExpression(LOWEST) + if p.peekTokenIs(token.SEMICOLON) { + p.nextToken() + } + return stmt +} + +func (p *Parser) parseReturnStatement() ast.Statement { + stmt := &ast.ReturnStatement{Token: p.curToken} + p.nextToken() + stmt.ReturnValue = p.parseExpression(LOWEST) + if p.peekTokenIs(token.SEMICOLON) { + p.nextToken() + } + return stmt +} + +func (p *Parser) parseExpressionStatement() ast.Statement { + stmt := &ast.ExpressionStatement{Token: p.curToken} + stmt.Expression = p.parseExpression(LOWEST) + // The semicolon is optional for expression statements so they're easier + // to type on the REPL. NOTE: It is weird that the last token parsed by + // parseExpression does not get consumed. + if p.peekTokenIs(token.SEMICOLON) { + p.nextToken() + } + return stmt +} + +func (p *Parser) parseExpression(precedence int) ast.Expression { + // TODO: Could this be replaced with an `ok` check? + prefix := p.prefixParseFns[p.curToken.Type] + if prefix == nil { + p.noPrefixParseFnError(p.curToken.Type) + return nil + } + curExpr := prefix() + for !p.peekTokenIs(token.SEMICOLON) && precedence < p.peekPrecedence() { + infix := p.infixParseFns[p.peekToken.Type] + if infix == nil { + return curExpr + } + p.nextToken() + curExpr = infix(curExpr) + } + return curExpr +} + +func (p *Parser) parseIdentifier() ast.Expression { + return &ast.Identifier{Token: p.curToken, Value: p.curToken.Literal} +} + +func (p *Parser) parseIntegerLiteral() ast.Expression { + exp := &ast.IntegerLiteral{Token: p.curToken} + literal, err := strconv.ParseInt(p.curToken.Literal, 0, 64) + if err != nil { + p.errors = append(p.errors, fmt.Sprintf("could not parse %q as an integer", p.curToken.Literal)) + return nil + } + exp.Value = literal + return exp +} + +func (p *Parser) parseBoolean() ast.Expression { + return &ast.Boolean{Token: p.curToken, Value: p.curTokenIs(token.TRUE)} +} + +func (p *Parser) parsePrefixExpression() ast.Expression { + exp := &ast.PrefixExpression{ + Token: p.curToken, + Operator: p.curToken.Literal, + } + p.nextToken() + exp.Right = p.parseExpression(PREFIX) + return exp +} + +func (p *Parser) parseInfixExpression(left ast.Expression) ast.Expression { + exp := &ast.InfixExpression{ + Token: p.curToken, + Operator: p.curToken.Literal, + Left: left, + } + precedence := p.curPrecedence() + p.nextToken() + exp.Right = p.parseExpression(precedence) + return exp +} + +func (p *Parser) parseGroupedExpression() ast.Expression { + p.nextToken() + exp := p.parseExpression(LOWEST) + if !p.nextTokenIfPeekIs(token.RPAREN) { + // TODO: Would probably be good to emit an error here? + return nil + } + return exp +} + +func (p *Parser) parseIfExpression() ast.Expression { + exp := &ast.IfExpression{Token: p.curToken} + if !p.nextTokenIfPeekIs(token.LPAREN) { + // TODO: Would be good to emit an error here. + return nil + } + p.nextToken() + exp.Condition = p.parseExpression(LOWEST) + if !p.nextTokenIfPeekIs(token.RPAREN) { + // TODO: Would be good to emit an error here. + return nil + } + if !p.nextTokenIfPeekIs(token.LBRACE) { + // TODO: Would be good to emit an error here. + return nil + } + exp.Consequence = p.parseBlockStatement() + if p.peekTokenIs(token.ELSE) { + p.nextToken() + if !p.nextTokenIfPeekIs(token.LBRACE) { + // TODO: Would be good to emit an error here. + return nil + } + exp.Alternative = p.parseBlockStatement() + } + // We don't consume the RBRACE because it acts as our "end of statement" + // token, and it's consumed by parseProgram. + return exp +} + +func (p *Parser) parseFunctionLiteral() ast.Expression { + fn := &ast.FunctionLiteral{Token: p.curToken} + if !p.nextTokenIfPeekIs(token.LPAREN) { + // TODO: Would be good to emit an error here. + return nil + } + fn.Parameters = p.parseFunctionParameters() + if !p.nextTokenIfPeekIs(token.LBRACE) { + // TODO: Would be good to emit an error here. + return nil + } + fn.Body = p.parseBlockStatement() + return fn +} + +func (p *Parser) parseFunctionParameters() []*ast.Identifier { + params := []*ast.Identifier{} + if p.peekTokenIs(token.RPAREN) { + p.nextToken() + return params + } + // Consume the LPAREN + p.nextToken() + params = append(params, &ast.Identifier{Token: p.curToken, Value: p.curToken.Literal}) + for p.peekTokenIs(token.COMMA) { + // Consume the previous identifier. + p.nextToken() + // Consume the comma. + p.nextToken() + params = append(params, &ast.Identifier{Token: p.curToken, Value: p.curToken.Literal}) + } + if !p.nextTokenIfPeekIs(token.RPAREN) { + // TODO: Would be good to emit an error here. + return nil + } + return params +} + +func (p *Parser) parseCallExpression(function ast.Expression) ast.Expression { + call := &ast.CallExpression{Token: p.curToken, Function: function} + call.Arguments = p.parseExpressionList(token.RPAREN) + return call +} + +func (p *Parser) parseExpressionList(end token.TokenType) []ast.Expression { + args := []ast.Expression{} + if p.peekTokenIs(end) { + p.nextToken() + return args + } + // Consume the LPAREN + p.nextToken() + args = append(args, p.parseExpression(LOWEST)) + for p.peekTokenIs(token.COMMA) { + // Consume last token of the previous expression. + p.nextToken() + // Consume the comma. + p.nextToken() + args = append(args, p.parseExpression(LOWEST)) + } + if !p.nextTokenIfPeekIs(end) { + // TODO: Would be good to emit an error here. + return nil + } + return args +} + +func (p *Parser) parseStringLiteral() ast.Expression { + return &ast.StringLiteral{Token: p.curToken, Value: p.curToken.Literal} +} + +func (p *Parser) parseArrayLiteral() ast.Expression { + array := &ast.ArrayLiteral{Token: p.curToken} + array.Elements = p.parseExpressionList(token.RBRACKET) + return array +} + +func (p *Parser) parseIndexExpression(left ast.Expression) ast.Expression { + ie := &ast.IndexExpression{Token: p.curToken, Left: left} + p.nextToken() + ie.Index = p.parseExpression(LOWEST) + if !p.nextTokenIfPeekIs(token.RBRACKET) { + return nil + } + return ie +} + +func (p *Parser) parseHashLiteral() ast.Expression { + hash := &ast.HashLiteral{ + Token: p.curToken, + Pairs: map[ast.Expression]ast.Expression{}, + } + for !p.peekTokenIs(token.RBRACE) { + p.nextToken() + k := p.parseExpression(LOWEST) + if !p.nextTokenIfPeekIs(token.COLON) { + return nil + } + p.nextToken() + v := p.parseExpression(LOWEST) + hash.Pairs[k] = v + if !p.peekTokenIs(token.RBRACE) && !p.nextTokenIfPeekIs(token.COMMA) { + break + } + } + if !p.nextTokenIfPeekIs(token.RBRACE) { + return nil + } + return hash +} + +func (p *Parser) curTokenIs(typ token.TokenType) bool { + return p.curToken.Type == typ +} + +func (p *Parser) peekTokenIs(typ token.TokenType) bool { + return p.peekToken.Type == typ +} + +// NOTE: I'll leave the name as-is to avoid deviating from the book (maybe a +// rename at the end?), but I think `nextTokenIfPeek` would be a much better +// name for this. +func (p *Parser) nextTokenIfPeekIs(typ token.TokenType) bool { + if p.peekTokenIs(typ) { + p.nextToken() + return true + } + p.peekError(typ) + return false +} + +func (p *Parser) nextToken() { + p.curToken = p.peekToken + p.peekToken = p.l.NextToken() +} + +func (p *Parser) Errors() []string { + return p.errors +} + +func (p *Parser) peekError(typ token.TokenType) { + p.errors = append( + p.errors, + fmt.Sprintf( + "expected next token to be %q, got %q instead", + typ, + p.peekToken.Type, + ), + ) +} + +func (p *Parser) noPrefixParseFnError(t token.TokenType) { + p.errors = append( + p.errors, + fmt.Sprintf("no prefix parse function found for %q", t), + ) +} + +func (p *Parser) registerPrefix(typ token.TokenType, fn prefixParseFn) { + p.prefixParseFns[typ] = fn +} + +func (p *Parser) registerInfix(typ token.TokenType, fn infixParseFn) { + p.infixParseFns[typ] = fn +} diff --git a/pkg/parser/parser_test.go b/pkg/parser/parser_test.go new file mode 100644 index 0000000..9403229 --- /dev/null +++ b/pkg/parser/parser_test.go @@ -0,0 +1,1084 @@ +package parser + +import ( + "fmt" + "testing" + + "code.jmug.me/jmug/interpreter-in-go/pkg/ast" + "code.jmug.me/jmug/interpreter-in-go/pkg/lexer" +) + +func TestLetStatements(t *testing.T) { + tests := []struct { + input string + expectedIdentifier string + expectedValue interface{} + }{ + {"let x = 5;", "x", 5}, + {"let y = true;", "y", true}, + {"let foobar = y;", "foobar", "y"}, + } + + for _, tt := range tests { + l := lexer.New(tt.input) + p := New(l) + program := p.ParseProgram() + checkParserErrors(t, p) + + if len(program.Statements) != 1 { + t.Fatalf("program.Statements does not contain 1 statements. got=%d", + len(program.Statements)) + } + + stmt := program.Statements[0] + if !testLetStatement(t, stmt, tt.expectedIdentifier) { + return + } + + val := stmt.(*ast.LetStatement).Value + if !testLiteralExpression(t, val, tt.expectedValue) { + return + } + } +} + +func TestReturnStatements(t *testing.T) { + tests := []struct { + input string + expectedValue interface{} + }{ + {"return 5;", 5}, + {"return true;", true}, + {"return foobar;", "foobar"}, + } + + for _, tt := range tests { + l := lexer.New(tt.input) + p := New(l) + program := p.ParseProgram() + checkParserErrors(t, p) + + if len(program.Statements) != 1 { + t.Fatalf("program.Statements does not contain 1 statements. got=%d", + len(program.Statements)) + } + + stmt := program.Statements[0] + returnStmt, ok := stmt.(*ast.ReturnStatement) + if !ok { + t.Fatalf("stmt not *ast.ReturnStatement. got=%T", stmt) + } + if returnStmt.TokenLiteral() != "return" { + t.Fatalf("returnStmt.TokenLiteral not 'return', got %q", + returnStmt.TokenLiteral()) + } + if testLiteralExpression(t, returnStmt.ReturnValue, tt.expectedValue) { + return + } + } +} + +func TestIdentifierExpression(t *testing.T) { + input := "foobar;" + + l := lexer.New(input) + p := New(l) + program := p.ParseProgram() + checkParserErrors(t, p) + + if len(program.Statements) != 1 { + t.Fatalf("program has not enough statements. got=%d", + len(program.Statements)) + } + stmt, ok := program.Statements[0].(*ast.ExpressionStatement) + if !ok { + t.Fatalf("program.Statements[0] is not ast.ExpressionStatement. got=%T", + program.Statements[0]) + } + + ident, ok := stmt.Expression.(*ast.Identifier) + if !ok { + t.Fatalf("exp not *ast.Identifier. got=%T", stmt.Expression) + } + if ident.Value != "foobar" { + t.Errorf("ident.Value not %s. got=%s", "foobar", ident.Value) + } + if ident.TokenLiteral() != "foobar" { + t.Errorf("ident.TokenLiteral not %s. got=%s", "foobar", + ident.TokenLiteral()) + } +} + +func TestIntegerLiteralExpression(t *testing.T) { + input := "5;" + + l := lexer.New(input) + p := New(l) + program := p.ParseProgram() + checkParserErrors(t, p) + + if len(program.Statements) != 1 { + t.Fatalf("program has not enough statements. got=%d", + len(program.Statements)) + } + stmt, ok := program.Statements[0].(*ast.ExpressionStatement) + if !ok { + t.Fatalf("program.Statements[0] is not ast.ExpressionStatement. got=%T", + program.Statements[0]) + } + + literal, ok := stmt.Expression.(*ast.IntegerLiteral) + if !ok { + t.Fatalf("exp not *ast.IntegerLiteral. got=%T", stmt.Expression) + } + if literal.Value != 5 { + t.Errorf("literal.Value not %d. got=%d", 5, literal.Value) + } + if literal.TokenLiteral() != "5" { + t.Errorf("literal.TokenLiteral not %s. got=%s", "5", + literal.TokenLiteral()) + } +} + +func TestParsingPrefixExpressions(t *testing.T) { + prefixTests := []struct { + input string + operator string + value interface{} + }{ + {"!5;", "!", 5}, + {"-15;", "-", 15}, + {"!foobar;", "!", "foobar"}, + {"-foobar;", "-", "foobar"}, + {"!true;", "!", true}, + {"!false;", "!", false}, + } + + for _, tt := range prefixTests { + l := lexer.New(tt.input) + p := New(l) + program := p.ParseProgram() + checkParserErrors(t, p) + + if len(program.Statements) != 1 { + t.Fatalf("program.Statements does not contain %d statements. got=%d\n", + 1, len(program.Statements)) + } + + stmt, ok := program.Statements[0].(*ast.ExpressionStatement) + if !ok { + t.Fatalf("program.Statements[0] is not ast.ExpressionStatement. got=%T", + program.Statements[0]) + } + + exp, ok := stmt.Expression.(*ast.PrefixExpression) + if !ok { + t.Fatalf("stmt is not ast.PrefixExpression. got=%T", stmt.Expression) + } + if exp.Operator != tt.operator { + t.Fatalf("exp.Operator is not '%s'. got=%s", + tt.operator, exp.Operator) + } + if !testLiteralExpression(t, exp.Right, tt.value) { + return + } + } +} + +func TestParsingInfixExpressions(t *testing.T) { + infixTests := []struct { + input string + leftValue interface{} + operator string + rightValue interface{} + }{ + {"5 + 5;", 5, "+", 5}, + {"5 - 5;", 5, "-", 5}, + {"5 * 5;", 5, "*", 5}, + {"5 / 5;", 5, "/", 5}, + {"5 > 5;", 5, ">", 5}, + {"5 < 5;", 5, "<", 5}, + {"5 == 5;", 5, "==", 5}, + {"5 != 5;", 5, "!=", 5}, + {"foobar + barfoo;", "foobar", "+", "barfoo"}, + {"foobar - barfoo;", "foobar", "-", "barfoo"}, + {"foobar * barfoo;", "foobar", "*", "barfoo"}, + {"foobar / barfoo;", "foobar", "/", "barfoo"}, + {"foobar > barfoo;", "foobar", ">", "barfoo"}, + {"foobar < barfoo;", "foobar", "<", "barfoo"}, + {"foobar == barfoo;", "foobar", "==", "barfoo"}, + {"foobar != barfoo;", "foobar", "!=", "barfoo"}, + {"true == true", true, "==", true}, + {"true != false", true, "!=", false}, + {"false == false", false, "==", false}, + } + + for _, tt := range infixTests { + l := lexer.New(tt.input) + p := New(l) + program := p.ParseProgram() + checkParserErrors(t, p) + + if len(program.Statements) != 1 { + t.Fatalf("program.Statements does not contain %d statements. got=%d\n", + 1, len(program.Statements)) + } + + stmt, ok := program.Statements[0].(*ast.ExpressionStatement) + if !ok { + t.Fatalf("program.Statements[0] is not ast.ExpressionStatement. got=%T", + program.Statements[0]) + } + + if !testInfixExpression(t, stmt.Expression, tt.leftValue, + tt.operator, tt.rightValue) { + return + } + } +} + +func TestOperatorPrecedenceParsing(t *testing.T) { + tests := []struct { + input string + expected string + }{ + { + "-a * b", + "((-a) * b)", + }, + { + "!-a", + "(!(-a))", + }, + { + "a + b + c", + "((a + b) + c)", + }, + { + "a + b - c", + "((a + b) - c)", + }, + { + "a * b * c", + "((a * b) * c)", + }, + { + "a * b / c", + "((a * b) / c)", + }, + { + "a + b / c", + "(a + (b / c))", + }, + { + "a + b * c + d / e - f", + "(((a + (b * c)) + (d / e)) - f)", + }, + { + "3 + 4; -5 * 5", + "(3 + 4)((-5) * 5)", + }, + { + "5 > 4 == 3 < 4", + "((5 > 4) == (3 < 4))", + }, + { + "5 < 4 != 3 > 4", + "((5 < 4) != (3 > 4))", + }, + { + "3 + 4 * 5 == 3 * 1 + 4 * 5", + "((3 + (4 * 5)) == ((3 * 1) + (4 * 5)))", + }, + { + "true", + "true", + }, + { + "false", + "false", + }, + { + "3 > 5 == false", + "((3 > 5) == false)", + }, + { + "3 < 5 == true", + "((3 < 5) == true)", + }, + { + "1 + (2 + 3) + 4", + "((1 + (2 + 3)) + 4)", + }, + { + "(5 + 5) * 2", + "((5 + 5) * 2)", + }, + { + "2 / (5 + 5)", + "(2 / (5 + 5))", + }, + { + "(5 + 5) * 2 * (5 + 5)", + "(((5 + 5) * 2) * (5 + 5))", + }, + { + "-(5 + 5)", + "(-(5 + 5))", + }, + { + "!(true == true)", + "(!(true == true))", + }, + { + "a + add(b * c) + d", + "((a + add((b * c))) + d)", + }, + { + "add(a, b, 1, 2 * 3, 4 + 5, add(6, 7 * 8))", + "add(a, b, 1, (2 * 3), (4 + 5), add(6, (7 * 8)))", + }, + { + "add(a + b + c * d / f + g)", + "add((((a + b) + ((c * d) / f)) + g))", + }, + { + "a * [1, 2, 3, 4][b * c] * d", + "((a * ([1, 2, 3, 4][(b * c)])) * d)", + }, + { + "add(a * b[2], b[1], 2 * [1, 2][1])", + "add((a * (b[2])), (b[1]), (2 * ([1, 2][1])))", + }, + } + + for _, tt := range tests { + l := lexer.New(tt.input) + p := New(l) + program := p.ParseProgram() + checkParserErrors(t, p) + + actual := program.String() + if actual != tt.expected { + t.Errorf("expected=%q, got=%q", tt.expected, actual) + } + } +} + +func TestBooleanExpression(t *testing.T) { + tests := []struct { + input string + expectedBoolean bool + }{ + {"true;", true}, + {"false;", false}, + } + + for _, tt := range tests { + l := lexer.New(tt.input) + p := New(l) + program := p.ParseProgram() + checkParserErrors(t, p) + + if len(program.Statements) != 1 { + t.Fatalf("program has not enough statements. got=%d", + len(program.Statements)) + } + + stmt, ok := program.Statements[0].(*ast.ExpressionStatement) + if !ok { + t.Fatalf("program.Statements[0] is not ast.ExpressionStatement. got=%T", + program.Statements[0]) + } + + boolean, ok := stmt.Expression.(*ast.Boolean) + if !ok { + t.Fatalf("exp not *ast.Boolean. got=%T", stmt.Expression) + } + if boolean.Value != tt.expectedBoolean { + t.Errorf("boolean.Value not %t. got=%t", tt.expectedBoolean, + boolean.Value) + } + } +} + +func TestIfExpression(t *testing.T) { + input := `if (x < y) { x }` + + l := lexer.New(input) + p := New(l) + program := p.ParseProgram() + checkParserErrors(t, p) + + if len(program.Statements) != 1 { + t.Fatalf("program.Statements does not contain %d statements. got=%d\n", + 1, len(program.Statements)) + } + + stmt, ok := program.Statements[0].(*ast.ExpressionStatement) + if !ok { + t.Fatalf("program.Statements[0] is not ast.ExpressionStatement. got=%T", + program.Statements[0]) + } + + exp, ok := stmt.Expression.(*ast.IfExpression) + if !ok { + t.Fatalf("stmt.Expression is not ast.IfExpression. got=%T", + stmt.Expression) + } + + if !testInfixExpression(t, exp.Condition, "x", "<", "y") { + return + } + + if len(exp.Consequence.Statements) != 1 { + t.Errorf("consequence is not 1 statements. got=%d\n", + len(exp.Consequence.Statements)) + } + + consequence, ok := exp.Consequence.Statements[0].(*ast.ExpressionStatement) + if !ok { + t.Fatalf("Statements[0] is not ast.ExpressionStatement. got=%T", + exp.Consequence.Statements[0]) + } + + if !testIdentifier(t, consequence.Expression, "x") { + return + } + + if exp.Alternative != nil { + t.Errorf("exp.Alternative.Statements was not nil. got=%+v", exp.Alternative) + } +} + +func TestIfElseExpression(t *testing.T) { + input := `if (x < y) { x } else { y }` + + l := lexer.New(input) + p := New(l) + program := p.ParseProgram() + checkParserErrors(t, p) + + if len(program.Statements) != 1 { + t.Fatalf("program.Statements does not contain %d statements. got=%d\n", + 1, len(program.Statements)) + } + + stmt, ok := program.Statements[0].(*ast.ExpressionStatement) + if !ok { + t.Fatalf("program.Statements[0] is not ast.ExpressionStatement. got=%T", + program.Statements[0]) + } + + exp, ok := stmt.Expression.(*ast.IfExpression) + if !ok { + t.Fatalf("stmt.Expression is not ast.IfExpression. got=%T", stmt.Expression) + } + + if !testInfixExpression(t, exp.Condition, "x", "<", "y") { + return + } + + if len(exp.Consequence.Statements) != 1 { + t.Errorf("consequence is not 1 statements. got=%d\n", + len(exp.Consequence.Statements)) + } + + consequence, ok := exp.Consequence.Statements[0].(*ast.ExpressionStatement) + if !ok { + t.Fatalf("Statements[0] is not ast.ExpressionStatement. got=%T", + exp.Consequence.Statements[0]) + } + + if !testIdentifier(t, consequence.Expression, "x") { + return + } + + if len(exp.Alternative.Statements) != 1 { + t.Errorf("exp.Alternative.Statements does not contain 1 statements. got=%d\n", + len(exp.Alternative.Statements)) + } + + alternative, ok := exp.Alternative.Statements[0].(*ast.ExpressionStatement) + if !ok { + t.Fatalf("Statements[0] is not ast.ExpressionStatement. got=%T", + exp.Alternative.Statements[0]) + } + + if !testIdentifier(t, alternative.Expression, "y") { + return + } +} + +func TestFunctionLiteralParsing(t *testing.T) { + input := `fn(x, y) { x + y; }` + + l := lexer.New(input) + p := New(l) + program := p.ParseProgram() + checkParserErrors(t, p) + + if len(program.Statements) != 1 { + t.Fatalf("program.Statements does not contain %d statements. got=%d\n", + 1, len(program.Statements)) + } + + stmt, ok := program.Statements[0].(*ast.ExpressionStatement) + if !ok { + t.Fatalf("program.Statements[0] is not ast.ExpressionStatement. got=%T", + program.Statements[0]) + } + + function, ok := stmt.Expression.(*ast.FunctionLiteral) + if !ok { + t.Fatalf("stmt.Expression is not ast.FunctionLiteral. got=%T", + stmt.Expression) + } + + if len(function.Parameters) != 2 { + t.Fatalf("function literal parameters wrong. want 2, got=%d\n", + len(function.Parameters)) + } + + testLiteralExpression(t, function.Parameters[0], "x") + testLiteralExpression(t, function.Parameters[1], "y") + + if len(function.Body.Statements) != 1 { + t.Fatalf("function.Body.Statements has not 1 statements. got=%d\n", + len(function.Body.Statements)) + } + + bodyStmt, ok := function.Body.Statements[0].(*ast.ExpressionStatement) + if !ok { + t.Fatalf("function body stmt is not ast.ExpressionStatement. got=%T", + function.Body.Statements[0]) + } + + testInfixExpression(t, bodyStmt.Expression, "x", "+", "y") +} + +func TestFunctionParameterParsing(t *testing.T) { + tests := []struct { + input string + expectedParams []string + }{ + {input: "fn() {};", expectedParams: []string{}}, + {input: "fn(x) {};", expectedParams: []string{"x"}}, + {input: "fn(x, y, z) {};", expectedParams: []string{"x", "y", "z"}}, + } + + for _, tt := range tests { + l := lexer.New(tt.input) + p := New(l) + program := p.ParseProgram() + checkParserErrors(t, p) + + stmt := program.Statements[0].(*ast.ExpressionStatement) + function := stmt.Expression.(*ast.FunctionLiteral) + + if len(function.Parameters) != len(tt.expectedParams) { + t.Errorf("length parameters wrong. want %d, got=%d\n", + len(tt.expectedParams), len(function.Parameters)) + } + + for i, ident := range tt.expectedParams { + testLiteralExpression(t, function.Parameters[i], ident) + } + } +} + +func TestCallExpressionParsing(t *testing.T) { + input := "add(1, 2 * 3, 4 + 5);" + + l := lexer.New(input) + p := New(l) + program := p.ParseProgram() + checkParserErrors(t, p) + + if len(program.Statements) != 1 { + t.Fatalf("program.Statements does not contain %d statements. got=%d\n", + 1, len(program.Statements)) + } + + stmt, ok := program.Statements[0].(*ast.ExpressionStatement) + if !ok { + t.Fatalf("stmt is not ast.ExpressionStatement. got=%T", + program.Statements[0]) + } + + exp, ok := stmt.Expression.(*ast.CallExpression) + if !ok { + t.Fatalf("stmt.Expression is not ast.CallExpression. got=%T", + stmt.Expression) + } + + if !testIdentifier(t, exp.Function, "add") { + return + } + + if len(exp.Arguments) != 3 { + t.Fatalf("wrong length of arguments. got=%d", len(exp.Arguments)) + } + + testLiteralExpression(t, exp.Arguments[0], 1) + testInfixExpression(t, exp.Arguments[1], 2, "*", 3) + testInfixExpression(t, exp.Arguments[2], 4, "+", 5) +} + +func TestCallExpressionParameterParsing(t *testing.T) { + tests := []struct { + input string + expectedIdent string + expectedArgs []string + }{ + { + input: "add();", + expectedIdent: "add", + expectedArgs: []string{}, + }, + { + input: "add(1);", + expectedIdent: "add", + expectedArgs: []string{"1"}, + }, + { + input: "add(1, 2 * 3, 4 + 5);", + expectedIdent: "add", + expectedArgs: []string{"1", "(2 * 3)", "(4 + 5)"}, + }, + } + + for _, tt := range tests { + l := lexer.New(tt.input) + p := New(l) + program := p.ParseProgram() + checkParserErrors(t, p) + + stmt := program.Statements[0].(*ast.ExpressionStatement) + exp, ok := stmt.Expression.(*ast.CallExpression) + if !ok { + t.Fatalf("stmt.Expression is not ast.CallExpression. got=%T", + stmt.Expression) + } + + if !testIdentifier(t, exp.Function, tt.expectedIdent) { + return + } + + if len(exp.Arguments) != len(tt.expectedArgs) { + t.Fatalf("wrong number of arguments. want=%d, got=%d", + len(tt.expectedArgs), len(exp.Arguments)) + } + + for i, arg := range tt.expectedArgs { + if exp.Arguments[i].String() != arg { + t.Errorf("argument %d wrong. want=%q, got=%q", i, + arg, exp.Arguments[i].String()) + } + } + } +} + +func TestStringLiteralExpression(t *testing.T) { + input := `"hello world";` + + l := lexer.New(input) + p := New(l) + program := p.ParseProgram() + checkParserErrors(t, p) + + stmt := program.Statements[0].(*ast.ExpressionStatement) + literal, ok := stmt.Expression.(*ast.StringLiteral) + if !ok { + t.Fatalf("exp not *ast.StringLiteral. got=%T", stmt.Expression) + } + + if literal.Value != "hello world" { + t.Errorf("literal.Value not %q. got=%q", "hello world", literal.Value) + } +} + +func TestParsingEmptyArrayLiterals(t *testing.T) { + input := "[]" + + l := lexer.New(input) + p := New(l) + program := p.ParseProgram() + checkParserErrors(t, p) + + stmt, ok := program.Statements[0].(*ast.ExpressionStatement) + array, ok := stmt.Expression.(*ast.ArrayLiteral) + if !ok { + t.Fatalf("exp not ast.ArrayLiteral. got=%T", stmt.Expression) + } + + if len(array.Elements) != 0 { + t.Errorf("len(array.Elements) not 0. got=%d", len(array.Elements)) + } +} + +func TestParsingArrayLiterals(t *testing.T) { + input := "[1, 2 * 2, 3 + 3]" + + l := lexer.New(input) + p := New(l) + program := p.ParseProgram() + checkParserErrors(t, p) + + stmt, ok := program.Statements[0].(*ast.ExpressionStatement) + array, ok := stmt.Expression.(*ast.ArrayLiteral) + if !ok { + t.Fatalf("exp not ast.ArrayLiteral. got=%T", stmt.Expression) + } + + if len(array.Elements) != 3 { + t.Fatalf("len(array.Elements) not 3. got=%d", len(array.Elements)) + } + + testIntegerLiteral(t, array.Elements[0], 1) + testInfixExpression(t, array.Elements[1], 2, "*", 2) + testInfixExpression(t, array.Elements[2], 3, "+", 3) +} + +func TestParsingIndexExpressions(t *testing.T) { + input := "myArray[1 + 1]" + + l := lexer.New(input) + p := New(l) + program := p.ParseProgram() + checkParserErrors(t, p) + + stmt, ok := program.Statements[0].(*ast.ExpressionStatement) + indexExp, ok := stmt.Expression.(*ast.IndexExpression) + if !ok { + t.Fatalf("exp not *ast.IndexExpression. got=%T", stmt.Expression) + } + + if !testIdentifier(t, indexExp.Left, "myArray") { + return + } + + if !testInfixExpression(t, indexExp.Index, 1, "+", 1) { + return + } +} + +func TestParsingEmptyHashLiteral(t *testing.T) { + input := "{}" + + l := lexer.New(input) + p := New(l) + program := p.ParseProgram() + checkParserErrors(t, p) + + stmt := program.Statements[0].(*ast.ExpressionStatement) + hash, ok := stmt.Expression.(*ast.HashLiteral) + if !ok { + t.Fatalf("exp is not ast.HashLiteral. got=%T", stmt.Expression) + } + + if len(hash.Pairs) != 0 { + t.Errorf("hash.Pairs has wrong length. got=%d", len(hash.Pairs)) + } +} + +func TestParsingHashLiteralsStringKeys(t *testing.T) { + input := `{"one": 1, "two": 2, "three": 3}` + + l := lexer.New(input) + p := New(l) + program := p.ParseProgram() + checkParserErrors(t, p) + + stmt := program.Statements[0].(*ast.ExpressionStatement) + hash, ok := stmt.Expression.(*ast.HashLiteral) + if !ok { + t.Fatalf("exp is not ast.HashLiteral. got=%T", stmt.Expression) + } + + expected := map[string]int64{ + "one": 1, + "two": 2, + "three": 3, + } + + if len(hash.Pairs) != len(expected) { + t.Errorf("hash.Pairs has wrong length. got=%d", len(hash.Pairs)) + } + + for key, value := range hash.Pairs { + literal, ok := key.(*ast.StringLiteral) + if !ok { + t.Errorf("key is not ast.StringLiteral. got=%T", key) + continue + } + + expectedValue := expected[literal.String()] + testIntegerLiteral(t, value, expectedValue) + } +} + +func TestParsingHashLiteralsBooleanKeys(t *testing.T) { + input := `{true: 1, false: 2}` + + l := lexer.New(input) + p := New(l) + program := p.ParseProgram() + checkParserErrors(t, p) + + stmt := program.Statements[0].(*ast.ExpressionStatement) + hash, ok := stmt.Expression.(*ast.HashLiteral) + if !ok { + t.Fatalf("exp is not ast.HashLiteral. got=%T", stmt.Expression) + } + + expected := map[string]int64{ + "true": 1, + "false": 2, + } + + if len(hash.Pairs) != len(expected) { + t.Errorf("hash.Pairs has wrong length. got=%d", len(hash.Pairs)) + } + + for key, value := range hash.Pairs { + boolean, ok := key.(*ast.Boolean) + if !ok { + t.Errorf("key is not ast.BooleanLiteral. got=%T", key) + continue + } + + expectedValue := expected[boolean.String()] + testIntegerLiteral(t, value, expectedValue) + } +} + +func TestParsingHashLiteralsIntegerKeys(t *testing.T) { + input := `{1: 1, 2: 2, 3: 3}` + + l := lexer.New(input) + p := New(l) + program := p.ParseProgram() + checkParserErrors(t, p) + + stmt := program.Statements[0].(*ast.ExpressionStatement) + hash, ok := stmt.Expression.(*ast.HashLiteral) + if !ok { + t.Fatalf("exp is not ast.HashLiteral. got=%T", stmt.Expression) + } + + expected := map[string]int64{ + "1": 1, + "2": 2, + "3": 3, + } + + if len(hash.Pairs) != len(expected) { + t.Errorf("hash.Pairs has wrong length. got=%d", len(hash.Pairs)) + } + + for key, value := range hash.Pairs { + integer, ok := key.(*ast.IntegerLiteral) + if !ok { + t.Errorf("key is not ast.IntegerLiteral. got=%T", key) + continue + } + + expectedValue := expected[integer.String()] + + testIntegerLiteral(t, value, expectedValue) + } +} + +func TestParsingHashLiteralsWithExpressions(t *testing.T) { + input := `{"one": 0 + 1, "two": 10 - 8, "three": 15 / 5}` + + l := lexer.New(input) + p := New(l) + program := p.ParseProgram() + checkParserErrors(t, p) + + stmt := program.Statements[0].(*ast.ExpressionStatement) + hash, ok := stmt.Expression.(*ast.HashLiteral) + if !ok { + t.Fatalf("exp is not ast.HashLiteral. got=%T", stmt.Expression) + } + + if len(hash.Pairs) != 3 { + t.Errorf("hash.Pairs has wrong length. got=%d", len(hash.Pairs)) + } + + tests := map[string]func(ast.Expression){ + "one": func(e ast.Expression) { + testInfixExpression(t, e, 0, "+", 1) + }, + "two": func(e ast.Expression) { + testInfixExpression(t, e, 10, "-", 8) + }, + "three": func(e ast.Expression) { + testInfixExpression(t, e, 15, "/", 5) + }, + } + + for key, value := range hash.Pairs { + literal, ok := key.(*ast.StringLiteral) + if !ok { + t.Errorf("key is not ast.StringLiteral. got=%T", key) + continue + } + + testFunc, ok := tests[literal.String()] + if !ok { + t.Errorf("No test function for key %q found", literal.String()) + continue + } + + testFunc(value) + } +} + +func testLetStatement(t *testing.T, s ast.Statement, name string) bool { + if s.TokenLiteral() != "let" { + t.Errorf("s.TokenLiteral not 'let'. got=%q", s.TokenLiteral()) + return false + } + + letStmt, ok := s.(*ast.LetStatement) + if !ok { + t.Errorf("s not *ast.LetStatement. got=%T", s) + return false + } + + if letStmt.Name.Value != name { + t.Errorf("letStmt.Name.Value not '%s'. got=%s", name, letStmt.Name.Value) + return false + } + + if letStmt.Name.TokenLiteral() != name { + t.Errorf("letStmt.Name.TokenLiteral() not '%s'. got=%s", + name, letStmt.Name.TokenLiteral()) + return false + } + + return true +} + +func testInfixExpression(t *testing.T, exp ast.Expression, left interface{}, + operator string, right interface{}) bool { + + opExp, ok := exp.(*ast.InfixExpression) + if !ok { + t.Errorf("exp is not ast.InfixExpression. got=%T(%s)", exp, exp) + return false + } + + if !testLiteralExpression(t, opExp.Left, left) { + return false + } + + if opExp.Operator != operator { + t.Errorf("exp.Operator is not '%s'. got=%q", operator, opExp.Operator) + return false + } + + if !testLiteralExpression(t, opExp.Right, right) { + return false + } + + return true +} + +func testLiteralExpression( + t *testing.T, + exp ast.Expression, + expected interface{}, +) bool { + switch v := expected.(type) { + case int: + return testIntegerLiteral(t, exp, int64(v)) + case int64: + return testIntegerLiteral(t, exp, v) + case string: + return testIdentifier(t, exp, v) + case bool: + return testBooleanLiteral(t, exp, v) + } + t.Errorf("type of exp not handled. got=%T", exp) + return false +} + +func testIntegerLiteral(t *testing.T, il ast.Expression, value int64) bool { + integ, ok := il.(*ast.IntegerLiteral) + if !ok { + t.Errorf("il not *ast.IntegerLiteral. got=%T", il) + return false + } + + if integ.Value != value { + t.Errorf("integ.Value not %d. got=%d", value, integ.Value) + return false + } + + if integ.TokenLiteral() != fmt.Sprintf("%d", value) { + t.Errorf("integ.TokenLiteral not %d. got=%s", value, + integ.TokenLiteral()) + return false + } + + return true +} + +func testIdentifier(t *testing.T, exp ast.Expression, value string) bool { + ident, ok := exp.(*ast.Identifier) + if !ok { + t.Errorf("exp not *ast.Identifier. got=%T", exp) + return false + } + + if ident.Value != value { + t.Errorf("ident.Value not %s. got=%s", value, ident.Value) + return false + } + + if ident.TokenLiteral() != value { + t.Errorf("ident.TokenLiteral not %s. got=%s", value, + ident.TokenLiteral()) + return false + } + + return true +} + +func testBooleanLiteral(t *testing.T, exp ast.Expression, value bool) bool { + bo, ok := exp.(*ast.Boolean) + if !ok { + t.Errorf("exp not *ast.Boolean. got=%T", exp) + return false + } + + if bo.Value != value { + t.Errorf("bo.Value not %t. got=%t", value, bo.Value) + return false + } + + if bo.TokenLiteral() != fmt.Sprintf("%t", value) { + t.Errorf("bo.TokenLiteral not %t. got=%s", + value, bo.TokenLiteral()) + return false + } + + return true +} + +func checkParserErrors(t *testing.T, p *Parser) { + errors := p.Errors() + if len(errors) == 0 { + return + } + + t.Errorf("parser has %d errors", len(errors)) + for _, msg := range errors { + t.Errorf("parser error: %q", msg) + } + t.FailNow() +} diff --git a/pkg/parser/precedence.go b/pkg/parser/precedence.go new file mode 100644 index 0000000..aa792da --- /dev/null +++ b/pkg/parser/precedence.go @@ -0,0 +1,44 @@ +package parser + +import ( + "code.jmug.me/jmug/interpreter-in-go/pkg/token" +) + +const ( + _ int = iota + LOWEST + EQUALS // == + LESSGREATER // > or < + SUM // + + PRODUCT // * + PREFIX // -X or !X + CALL // myFunction(X) + INDEX // array[index] +) + +var precedences = map[token.TokenType]int{ + token.EQ: EQUALS, + token.NOT_EQ: EQUALS, + token.GT: LESSGREATER, + token.LT: LESSGREATER, + token.PLUS: SUM, + token.MINUS: SUM, + token.ASTERISK: PRODUCT, + token.SLASH: PRODUCT, + token.LPAREN: CALL, + token.LBRACKET: INDEX, +} + +func (p *Parser) peekPrecedence() int { + if pr, ok := precedences[p.peekToken.Type]; ok { + return pr + } + return LOWEST +} + +func (p *Parser) curPrecedence() int { + if pr, ok := precedences[p.curToken.Type]; ok { + return pr + } + return LOWEST +} diff --git a/pkg/repl/repl.go b/pkg/repl/repl.go new file mode 100644 index 0000000..0878839 --- /dev/null +++ b/pkg/repl/repl.go @@ -0,0 +1,59 @@ +package repl + +import ( + "bufio" + "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/object" + "code.jmug.me/jmug/interpreter-in-go/pkg/parser" +) + +const PROMPT = ">> " + +func Start(in io.Reader, out io.Writer) { + scanner := bufio.NewScanner(in) + env := object.NewEnvironment() + for { + fmt.Fprint(out, PROMPT) + if !scanner.Scan() { + return + } + l := lexer.New(scanner.Text()) + p := parser.New(l) + program := p.ParseProgram() + if len(p.Errors()) != 0 { + printParserErrors(out, p.Errors()) + continue + } + res := evaluator.Eval(program, env) + if res != nil { + io.WriteString(out, res.Inspect()) + io.WriteString(out, "\n") + } + } +} + +const MONKEY_FACE = ` __,__ + .--. .-" "-. .--. + / .. \/ .-. .-. \/ .. \ + | | '| / Y \ |' | | + | \ \ \ 0 | 0 / / / | + \ '- ,\.-"""""""-./, -' / + ''-' /_ ^ ^ _\ '-'' + | \._ _./ | + \ \ '~' / / + '._ '-=-' _.' + '-----' +` + +func printParserErrors(out io.Writer, errors []string) { + io.WriteString(out, MONKEY_FACE) + io.WriteString(out, "Woops! We ran into some monkey business here!\n") + io.WriteString(out, " parser errors:\n") + for _, msg := range errors { + io.WriteString(out, "\t"+msg+"\n") + } +} diff --git a/pkg/token/token.go b/pkg/token/token.go new file mode 100644 index 0000000..0ccca72 --- /dev/null +++ b/pkg/token/token.go @@ -0,0 +1,68 @@ +package token + +type TokenType string + +const ( + ILLEGAL = "ILLEGAL" + EOF = "EOF" + + // Identifiers + Literals + IDENT = "IDENT" + INT = "INT" + STRING = "STRING" + + // Operators + ASSIGN = "=" + PLUS = "+" + MINUS = "-" + BANG = "!" + ASTERISK = "*" + SLASH = "/" + LT = "<" + GT = ">" + EQ = "==" + NOT_EQ = "!=" + + // Delimiters + COMMA = "," + SEMICOLON = ";" + COLON = ":" + + LPAREN = "(" + RPAREN = ")" + LBRACE = "{" + RBRACE = "}" + LBRACKET = "[" + RBRACKET = "]" + + // Keywords + FUNCTION = "FUNCTION" + LET = "LET" + TRUE = "TRUE" + FALSE = "FALSE" + IF = "IF" + ELSE = "ELSE" + RETURN = "RETURN" +) + +var keywords = map[string]TokenType{ + "fn": FUNCTION, + "let": LET, + "true": TRUE, + "false": FALSE, + "if": IF, + "else": ELSE, + "return": RETURN, +} + +type Token struct { + Type TokenType + Literal string +} + +func LookupIdent(ident string) TokenType { + if typ, ok := keywords[ident]; ok { + return typ + } + return IDENT +}