Skip to content

Commit

Permalink
FEAT: added support for string concatenation and basic builtin functi…
Browse files Browse the repository at this point in the history
…ons ('len' for now)
  • Loading branch information
MKaczkow committed Oct 12, 2024
1 parent c14952c commit b387e89
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 11 deletions.
3 changes: 3 additions & 0 deletions monkey/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -109,6 +109,9 @@ is represented as:
* `REPL` - `Read-Eval-Print Loop`
* sometimes - 'console', 'interactive mode', etc.

### built-in functions
* design choice - should they be evalueated in top-level environment or in their own environment? (`object.Environment`)

### compiler
* goal is turning `source code` into `bytecode` and then use VM to execute it

Expand Down
22 changes: 22 additions & 0 deletions monkey/interpreter/evaluator/builtins.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,22 @@
package evaluator

import (
"monkey/interpreter/object"
)

var builtins = map[string]*object.Builtin{
"len": &object.Builtin{
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))}
default:
return newError("argument to `len` not supported, got %s", args[0].Type())
}
},
},
}
48 changes: 37 additions & 11 deletions monkey/interpreter/evaluator/evaluator.go
Original file line number Diff line number Diff line change
Expand Up @@ -96,14 +96,19 @@ func Eval(node ast.Node, env *object.Environment) object.Object {
}

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("not a function: %s", fn.Type())
}

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

func extendFunctionEnv(
Expand Down Expand Up @@ -178,13 +183,19 @@ func evalBlockStatement(block *ast.BlockStatement, env *object.Environment) obje
return result
}

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

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

return val
return newError("identifier not found: " + node.Value)
}

func nativeBooltoBooleanObject(input bool) *object.Boolean {
Expand Down Expand Up @@ -237,6 +248,8 @@ func evalInfixExpression(
// so we can compare them directly (checking for identity)
// this assumption does not hold for other types, because creating integer object allocates new memory
// so we need to compare their values (otherwise 5 == 5 would yield false)
case left.Type() == object.STRING_OBJ && right.Type() == object.STRING_OBJ:
return evalStringInfixExpression(operator, left, right)
case operator == "==":
return nativeBooltoBooleanObject(left == right)
case operator == "!=":
Expand Down Expand Up @@ -277,6 +290,19 @@ func evalIntegerInfixExpression(
}
}

func evalStringInfixExpression(operator string, left, right object.Object) object.Object {
leftVal := left.(*object.String).Value
rightVal := right.(*object.String).Value

switch operator {
case "+":
return &object.String{Value: leftVal + rightVal}
// TODO: maybe add more string operations (like comparison)
default:
return newError("unknown operator: %s %s %s", left.Type(), operator, right.Type())
}
}

func evalIfExpression(ie *ast.IfExpression, env *object.Environment) object.Object {
condition := Eval(ie.Condition, env)
if isError(condition) {
Expand Down
47 changes: 47 additions & 0 deletions monkey/interpreter/evaluator/evaluator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,10 @@ func TestErrorHandling(t *testing.T) {
"foobar",
"identifier not found: foobar",
},
{
`"Hello" - "World"`,
"unknown operator: STRING - STRING",
},
{
`
if (10 > 1) {
Expand Down Expand Up @@ -273,6 +277,49 @@ func TestClosures(t *testing.T) {
testIntegerObject(t, testEval(input), 4)
}

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"},
}
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("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)
}
}
}
}

func testEval(input string) object.Object {
l := lexer.New(input)
p := parser.New(l)
Expand Down
10 changes: 10 additions & 0 deletions monkey/interpreter/object/object.go
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@ import (

type ObjectType string

type BuiltinFunction func(args ...Object) Object

const (
INTEGER_OBJ = "INTEGER"
BOOLEAN_OBJ = "BOOLEAN"
Expand All @@ -17,6 +19,7 @@ const (
ERROR_OBJ = "ERROR"
FUNCTION_OBJ = "FUNCTION"
STRING_OBJ = "STRING"
BUILTIN_OBJ = "BUILTIN"
)

type Object interface {
Expand Down Expand Up @@ -88,3 +91,10 @@ func (f *Function) Inspect() string {

return out.String()
}

type Builtin struct {
Fn BuiltinFunction
}

func (b *Builtin) Inspect() string { return "builtin function" }
func (b *Builtin) Type() ObjectType { return BUILTIN_OBJ }

0 comments on commit b387e89

Please sign in to comment.