Skip to content

Commit 02b8bcf

Browse files
committed
4.3 Built-in Functions (len)
Add built-in functions to our interpreter. The first built-in function we’re going to add to our interpreter is len.
1 parent 8afb5f2 commit 02b8bcf

File tree

4 files changed

+111
-11
lines changed

4 files changed

+111
-11
lines changed

evaluator/builtins.go

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
package evaluator
2+
3+
import (
4+
"github.com/cedrickchee/hou/object"
5+
)
6+
7+
var builtins = map[string]*object.Builtin{
8+
"len": &object.Builtin{
9+
Fn: func(args ...object.Object) object.Object {
10+
// Error checking that makes sure that we can't call this function
11+
// with the wrong number of arguments.
12+
if len(args) != 1 {
13+
return newError("wrong number of arguments. got=%d, want=1",
14+
len(args))
15+
}
16+
17+
switch arg := args[0].(type) {
18+
case *object.String:
19+
return &object.Integer{Value: int64(len(arg.Value))}
20+
default:
21+
// Error checking that makes sure that we can't call this
22+
// function with an argument of an unsupported type.
23+
return newError("argument to `len` not supported, got %s",
24+
args[0].Type())
25+
}
26+
},
27+
},
28+
}

evaluator/evaluator.go

Lines changed: 24 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -337,12 +337,17 @@ func evalIdentifier(
337337
node *ast.Identifier,
338338
env *object.Environment,
339339
) object.Object {
340-
val, ok := env.Get(node.Value)
341-
if !ok {
342-
return newError("identifier not found: " + node.Value)
340+
if val, ok := env.Get(node.Value); ok {
341+
return val
343342
}
344343

345-
return val
344+
// Lookup built-in functions as a fallback when the given identifier is not
345+
// bound to a value in the current environment.
346+
if builtin, ok := builtins[node.Value]; ok {
347+
return builtin
348+
}
349+
350+
return newError("identifier not found: " + node.Value)
346351
}
347352

348353
func isTruthy(obj object.Object) bool {
@@ -400,15 +405,23 @@ func evalExpressions(
400405
}
401406

402407
func applyFunction(fn object.Object, args []object.Object) object.Object {
403-
// Convert the fn parameter to a *object.Function reference.
404-
function, ok := fn.(*object.Function)
405-
if !ok {
408+
switch fn := fn.(type) {
409+
case *object.Function:
410+
// Here, fn is the converted fn parameter to a *object.Function
411+
// reference.
412+
extendedEnv := extendFunctionEnv(fn, args)
413+
evaluated := Eval(fn.Body, extendedEnv)
414+
return unwrapReturnValue(evaluated)
415+
416+
case *object.Builtin:
417+
// Call the object.BuiltinFunction. Note that we don’t need to
418+
// unwrapReturnValue when calling a built-in function. That’s because we
419+
// never return an *object.ReturnValue from these functions.
420+
return fn.Fn(args...)
421+
422+
default:
406423
return newError("not a function: %s", fn.Type())
407424
}
408-
409-
extendedEnv := extendFunctionEnv(function, args)
410-
evaluated := Eval(function.Body, extendedEnv)
411-
return unwrapReturnValue(evaluated)
412425
}
413426

414427
func extendFunctionEnv(

evaluator/evaluator_test.go

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -363,6 +363,45 @@ func TestStringConcatenation(t *testing.T) {
363363
}
364364
}
365365

366+
func TestBuiltinFunctions(t *testing.T) {
367+
// Test cases that run len through its paces: an empty string, a normal
368+
// string and a string containing whitespace.
369+
// The last two test cases are more interesting: we want to make sure that
370+
// len returns an *object.Error when called with an integer or with the
371+
// wrong number of arguments.
372+
373+
tests := []struct {
374+
input string
375+
expected interface{}
376+
}{
377+
{`len("")`, 0},
378+
{`len("four")`, 4},
379+
{`len("hello world")`, 11},
380+
{`len(1)`, "argument to `len` not supported, got INTEGER"},
381+
{`len("one", "two")`, "wrong number of arguments. got=2, want=1"},
382+
}
383+
384+
for _, tt := range tests {
385+
evaluated := testEval(tt.input)
386+
387+
switch expected := tt.expected.(type) {
388+
case int:
389+
testIntegerObject(t, evaluated, int64(expected))
390+
case string:
391+
errObj, ok := evaluated.(*object.Error)
392+
if !ok {
393+
t.Errorf("object is not Error. got=%T (%+v)",
394+
evaluated, evaluated)
395+
continue
396+
}
397+
if errObj.Message != expected {
398+
t.Errorf("wrong error message. expected=%q, got=%q",
399+
expected, errObj.Message)
400+
}
401+
}
402+
}
403+
}
404+
366405
func testEval(input string) object.Object {
367406
l := lexer.New(input)
368407
p := parser.New(l)

object/object.go

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -33,8 +33,15 @@ const (
3333

3434
// FUNCTION_OBJ is the Function object type.
3535
FUNCTION_OBJ = "FUNCTION"
36+
37+
// BUILTIN_OBJ is the Builtin object type.
38+
BUILTIN_OBJ = "BUILTIN"
3639
)
3740

41+
// BuiltinFunction represents the builtin function type.
42+
// It's the type definition of a callable Go function.
43+
type BuiltinFunction func(args ...Object) Object
44+
3845
// ObjectType represents the type of an object.
3946
type ObjectType string
4047

@@ -155,3 +162,16 @@ func (s *String) Type() ObjectType { return STRING_OBJ }
155162

156163
// Inspect returns a stringified version of the object for debugging.
157164
func (s *String) Inspect() string { return s.Value }
165+
166+
// Builtin is the builtin object type that simply holds a reference to a
167+
// BuiltinFunction type that takes zero or more objects as arguments and returns
168+
// an object.
169+
type Builtin struct {
170+
Fn BuiltinFunction
171+
}
172+
173+
// Type returns the type of the object.
174+
func (b *Builtin) Type() ObjectType { return BUILTIN_OBJ }
175+
176+
// Inspect returns a stringified version of the object for debugging.
177+
func (b *Builtin) Inspect() string { return "builtin function" }

0 commit comments

Comments
 (0)