Skip to content

Commit

Permalink
Strings & builtin functions
Browse files Browse the repository at this point in the history
  • Loading branch information
kscarlett committed Jan 1, 2018
1 parent c458451 commit bf32f30
Show file tree
Hide file tree
Showing 10 changed files with 277 additions and 48 deletions.
15 changes: 13 additions & 2 deletions ast/ast.go
Original file line number Diff line number Diff line change
Expand Up @@ -122,7 +122,7 @@ func (i *Identifier) expressionNode() {}
func (i *Identifier) TokenLiteral() string { return i.Token.Literal }
func (i *Identifier) String() string { return i.Value }

// === INTEGERLITERAL ===
// === INTEGER LITERAL ===

type IntegerLiteral struct {
Token token.Token
Expand All @@ -133,7 +133,18 @@ func (il *IntegerLiteral) expressionNode() {}
func (il *IntegerLiteral) TokenLiteral() string { return il.Token.Literal }
func (il *IntegerLiteral) String() string { return il.Token.Literal }

// === PREFIXEXPRESSION ===
// === STRING LITERAL ===

type StringLiteral struct {
Token token.Token
Value string
}

func (sl *StringLiteral) expressionNode() {}
func (sl *StringLiteral) TokenLiteral() string { return sl.Token.Literal }
func (sl *StringLiteral) String() string { return sl.Token.Literal }

// === PREFIX EXPRESSION ===

type PrefixExpression struct {
Token token.Token
Expand Down
22 changes: 22 additions & 0 deletions evaluator/builtins.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package evaluator

import (
"github.com/kscarlett/kmonkey/object"
)

var builtins = map[string]*object.Builtin{
"len": &object.Builtin{
Fn: func(args ...object.Object) object.Object {
if len(args) != 1 {
return newError("error", "Wrong number of arguments passed to `len`. Expected: %d, got: %d", 1, len(args))
}

switch arg := args[0].(type) {
case *object.String:
return &object.Integer{Value: int64(len(arg.Value))}
default:
return newError("error", "Argument %s is not supported by `len`", args[0].Type())
}
},
},
}
74 changes: 64 additions & 10 deletions evaluator/evaluator.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@ package evaluator

import (
"fmt"
"strconv"

"github.com/kscarlett/kmonkey/ast"
"github.com/kscarlett/kmonkey/object"
Expand Down Expand Up @@ -43,6 +44,9 @@ func Eval(node ast.Node, env *object.Environment) object.Object {
case *ast.IntegerLiteral:
return &object.Integer{Value: node.Value}

case *ast.StringLiteral:
return &object.String{Value: node.Value}

case *ast.Boolean:
return nativeBoolToBooleanObject(node.Value)

Expand Down Expand Up @@ -148,6 +152,8 @@ func evalInfixExpression(operator string, left object.Object, right object.Objec
return nativeBoolToBooleanObject(left == right)
case operator == "!=":
return nativeBoolToBooleanObject(left != right)
case left.Type() == object.STRING_OBJ || right.Type() == object.STRING_OBJ:
return evalStringInfixExpression(operator, left, right)
case left.Type() != right.Type():
return newError("error", "Type mismatch: %s %s %s", left.Type(), operator, right.Type())
default:
Expand Down Expand Up @@ -206,6 +212,26 @@ func evalIntegerInfixExpression(operator string, left object.Object, right objec
}
}

func evalStringInfixExpression(operator string, left object.Object, right object.Object) object.Object {
if operator != "+" {
return newError("error", "Unknown operator: %s %s %s", left.Type(), operator, right.Type())
}

left = getStringValue(left)
if isError(left) {
return left
}
leftVal := left.(*object.String).Value

right = getStringValue(right)
if isError(right) {
return right
}
rightVal := right.(*object.String).Value

return &object.String{Value: leftVal + rightVal}
}

func evalIfExpression(ie *ast.IfExpression, env *object.Environment) object.Object {
condition := Eval(ie.Condition, env)

Expand Down Expand Up @@ -244,12 +270,15 @@ func evalWhileExpression(we *ast.WhileExpression, env *object.Environment) objec
}

func evalIdentifier(node *ast.Identifier, env *object.Environment) object.Object {
val, ok := env.Get(node.Value)
if !ok {
return newError("warn", "Identifier not found: %s", node.Value)
if val, ok := env.Get(node.Value); ok {
return val
}

if builtin, ok := builtins[node.Value]; ok {
return builtin
}

return val
return newError("warn", "Identifier not found: %s", node.Value)
}

func evalExpressions(exps []ast.Expression, env *object.Environment) []object.Object {
Expand All @@ -267,14 +296,19 @@ func evalExpressions(exps []ast.Expression, env *object.Environment) []object.Ob
}

func applyFunction(fn object.Object, args []object.Object) object.Object {
function, ok := fn.(*object.Function)
if !ok {
switch fn := fn.(type) {

case *object.Function:
extendedEnv := extendFunctionEnv(fn, args)
evaluated := Eval(fn.Body, extendedEnv)
return unwrapReturnValue(evaluated)

case *object.Builtin:
return fn.Fn(args...)

default:
return newError("error", "Not a function: %s", fn.Type())
}

extendedEnv := extendFunctionEnv(function, args)
evaluated := Eval(function.Body, extendedEnv)
return unwrapReturnValue(evaluated)
}

func extendFunctionEnv(fn *object.Function, args []object.Object) *object.Environment {
Expand All @@ -287,6 +321,26 @@ func extendFunctionEnv(fn *object.Function, args []object.Object) *object.Enviro
return env
}

func getStringValue(obj object.Object) object.Object {
switch obj.Type() {
case object.STRING_OBJ:
str, ok := obj.(*object.String)
if !ok {
return newError("error", "Unable to convert to String: %T (%+v)", obj, obj)
}
return str
case object.INTEGER_OBJ:
val, ok := obj.(*object.Integer)
if !ok {
return newError("error", "Unable to convert to String: %T (%+v)", obj, obj)
}
str := strconv.FormatInt(val.Value, 10)
return &object.String{Value: str}
default:
return newError("error", "Unable to convert to String: %T (%+v)", obj, obj)
}
}

func unwrapReturnValue(obj object.Object) object.Object {
if returnValue, ok := obj.(*object.ReturnValue); ok {
return returnValue.Value
Expand Down
79 changes: 79 additions & 0 deletions evaluator/evaluator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,20 @@ func TestEvalIntegerExpression(t *testing.T) {
}
}

func TestStringLiteral(t *testing.T) {
input := `"Hello World!"`

evaluated := testEval(input)
str, ok := evaluated.(*object.String)
if !ok {
t.Fatalf("Object is incorrect type. Expected: %s, got: %T (%+v)", "string", evaluated, evaluated)
}

if str.Value != "Hello World!" {
t.Errorf("String has incorrect value. Expected: %s, got: %q", "Hello World!", str.Value)
}
}

func TestEvalBooleanExpression(t *testing.T) {
tests := []struct {
input string
Expand Down Expand Up @@ -182,6 +196,18 @@ func TestErrorHandling(t *testing.T) {
"foobar",
"Identifier not found: foobar",
},
{
`"Hello" - "World"`,
"Unknown operator: STRING - STRING",
},
{
`"test" + true`,
"Unable to convert to String: *object.Boolean (&{Value:true})",
},
{
`false + "test"`,
"Unable to convert to String: *object.Boolean (&{Value:false})",
},
}

for _, tt := range tests {
Expand Down Expand Up @@ -269,6 +295,59 @@ func TestClosures(t *testing.T) {
testIntegerObject(t, testEval(input), 4)
}

func TestStringConcatenation(t *testing.T) {
tests := []struct {
input string
expected string
}{
{`"Hello" + " " + "World!"`, "Hello World!"},
{`"Hello " + 12`, "Hello 12"},
}

for _, tt := range tests {
evaluated := testEval(tt.input)
str, ok := evaluated.(*object.String)
if !ok {
t.Fatalf("Object is incorrect type. Expected: %s, got: %T (%+v)", "String", evaluated, evaluated)
}

if str.Value != tt.expected {
t.Errorf("String has incorrect value. Expected: %s, got: %q", tt.expected, 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 INTEGER is not supported by `len`"},
{`len("one", "two")`, "Wrong number of arguments passed to `len`. Expected: 1, got: 2"},
}

for _, tt := range tests {
evaluated := testEval(tt.input)

switch expected := tt.expected.(type) {
case int:
testIntegerObject(t, evaluated, int64(expected))
case string:
errObj, ok := evaluated.(*object.Error)
if !ok {
t.Errorf("Error did not get thrown. Got: %T (%+v)", evaluated, evaluated)
continue
}
if errObj.Message != expected {
t.Errorf("Incorrect error message. Expected: %q, got %q", tt.expected, errObj.Message)
}
}
}
}

func testEval(input string) object.Object {
l := lexer.New(input)
p := parser.New(l)
Expand Down
82 changes: 48 additions & 34 deletions lexer/lexer.go
Original file line number Diff line number Diff line change
Expand Up @@ -20,40 +20,6 @@ func New(input string) *Lexer {
return l
}

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

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()
}
return l.input[position:l.position]
}

func (l *Lexer) readNumber() string {
position := l.position
for isDigit(l.ch) {
l.readChar()
}
return l.input[position:l.position]
}

func (l *Lexer) NextToken() token.Token {
var tok token.Token

Expand Down Expand Up @@ -102,6 +68,9 @@ func (l *Lexer) NextToken() token.Token {
tok = newToken(token.LBRACE, l.ch)
case '}':
tok = newToken(token.RBRACE, l.ch)
case '"':
tok.Type = token.STRING
tok.Literal = l.readString()
case 0:
tok.Literal = ""
tok.Type = token.EOF
Expand All @@ -127,6 +96,51 @@ func newToken(tokenType token.TokenType, ch byte) token.Token {
return token.Token{Type: tokenType, Literal: string(ch)}
}

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

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()
}
return l.input[position:l.position]
}

func (l *Lexer) readNumber() string {
position := l.position
for isDigit(l.ch) {
l.readChar()
}
return l.input[position:l.position]
}

func (l *Lexer) readString() string {
position := l.position + 1
for {
l.readChar()
if l.ch == '"' || l.ch == '0' { // TODO: consider not breaking on literal 0
break
}
}
return l.input[position:l.position]
}

func (l *Lexer) skipWhitespace() {
for l.ch == ' ' || l.ch == '\t' || l.ch == '\n' || l.ch == '\r' {
l.readChar()
Expand Down
4 changes: 4 additions & 0 deletions lexer/lexer_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,8 @@ func TestNextToken(t *testing.T) {
10 == 10;
10 != 9;
"foobar"
"foo bar"
`

tests := []struct {
Expand Down Expand Up @@ -106,6 +108,8 @@ func TestNextToken(t *testing.T) {
{token.NOT_EQ, "!="},
{token.INT, "9"},
{token.SEMICOLON, ";"},
{token.STRING, "foobar"},
{token.STRING, "foo bar"},
{token.EOF, ""},
}

Expand Down
Loading

0 comments on commit bf32f30

Please sign in to comment.