From 726947ddfc3a6e522b81699d83baf4f556de8a0f Mon Sep 17 00:00:00 2001 From: Zecheng Date: Sun, 12 May 2024 13:30:03 -0500 Subject: [PATCH] feat: add a builtin function type to the object system (#14) ## What does this PR do? add a builtin function type to the object system --- .github/workflows/ci.yaml | 3 +- .github/workflows/release.yaml | 4 +++ internal/evaluator/evaluator.go | 47 +++++++++++++----------- internal/evaluator/evaluator_test.go | 53 ++++++++++++++++++++++++++++ internal/object/errors.go | 10 ++++++ internal/object/object.go | 35 +++++++++++++++++- 6 files changed, 129 insertions(+), 23 deletions(-) create mode 100644 internal/object/errors.go diff --git a/.github/workflows/ci.yaml b/.github/workflows/ci.yaml index afabcf5..d69cb54 100644 --- a/.github/workflows/ci.yaml +++ b/.github/workflows/ci.yaml @@ -13,7 +13,8 @@ concurrency: cancel-in-progress: true jobs: - lint-build-test: + ci: + name: lint-build-test runs-on: ubuntu-latest steps: - uses: actions/checkout@v4 diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index bdca7cf..538eff8 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -7,6 +7,10 @@ on: branches: - main +concurrency: + group: ${{ github.workflow }}-${{ github.ref }} + cancel-in-progress: true + jobs: tag-release: if: github.event.pull_request.merged == true diff --git a/internal/evaluator/evaluator.go b/internal/evaluator/evaluator.go index 701dc17..8889611 100644 --- a/internal/evaluator/evaluator.go +++ b/internal/evaluator/evaluator.go @@ -113,12 +113,15 @@ func (e *evaluator) evalReturnStatement(stmt *ast.ReturnStatement) (object.Objec } func (e *evaluator) evalIdentifierExpression(ie *ast.IdentifierExpression) (object.Object, error) { - val, ok := e.env.Get(ie.Value) - if !ok { - return object.NIL, ErrIdentifierNotFound + if val, ok := e.env.Get(ie.Value); ok { + return val, nil } - return val, nil + if builtinFunc, ok := object.BuiltinFuncs[ie.Value]; ok { + return builtinFunc, nil + } + + return object.NIL, ErrIdentifierNotFound } func (e *evaluator) evalIfExpression(ie *ast.IfExpression) (object.Object, error) { @@ -164,26 +167,28 @@ func (e *evaluator) evalCallExpression(ce *ast.CallExpression) (object.Object, e } func applyFunc(fn object.Object, args []object.Object, env object.Environment) (object.Object, error) { - function, ok := fn.(*object.Func) - if !ok { - return object.NIL, ErrNotAFunction - } - - // create a new environment for the function scope - funcEvaluator := New(object.NewClosureEnvironment(env)) - // extend the closure environment with arguments passed to the function - funcEvaluator.extendFunctionEnv(function, args) + switch fn := fn.(type) { + case *object.Func: + // create a new environment for the function scope + funcEvaluator := New(object.NewClosureEnvironment(env)) + // extend the closure environment with arguments passed to the function + funcEvaluator.extendFunctionEnv(fn, args) + + val, err := funcEvaluator.Eval(fn.Body) + if err != nil { + return object.NIL, err + } - val, err := funcEvaluator.Eval(function.Body) - if err != nil { - return object.NIL, err - } + if returnVal, ok := val.(*object.ReturnValue); ok { + return returnVal.Value, nil + } - if returnVal, ok := val.(*object.ReturnValue); ok { - return returnVal.Value, nil + return val, nil + case object.BuiltinFunc: + return fn(args...) + default: + return object.NIL, ErrNotAFunction } - - return val, nil } func (e *evaluator) extendFunctionEnv(fn *object.Func, args []object.Object) { diff --git a/internal/evaluator/evaluator_test.go b/internal/evaluator/evaluator_test.go index 24fc74a..4ba1dec 100644 --- a/internal/evaluator/evaluator_test.go +++ b/internal/evaluator/evaluator_test.go @@ -936,6 +936,59 @@ var _ = Describe("Evaluator", func() { Expect(obj).To(Equal(expectedObject)) }) }) + + Context("builtin functions", func() { + It("length of an empty string", func() { + text = ` + len(""); + ` + expectedObject := object.NewInteger(0) + expectedParseErrors := []error{} + + // parse the program + program, errs = p.ParseProgram(text) + Expect(errs).To(Equal(expectedParseErrors)) + + // evaluate the AST tree + obj, err := e.Eval(program) + Expect(err).ToNot(HaveOccurred()) + Expect(obj).To(Equal(expectedObject)) + }) + + It("length of a non-empty string", func() { + text = ` + len("hello world"); + ` + expectedObject := object.NewInteger(11) + expectedParseErrors := []error{} + + // parse the program + program, errs = p.ParseProgram(text) + Expect(errs).To(Equal(expectedParseErrors)) + + // evaluate the AST tree + obj, err := e.Eval(program) + Expect(err).ToNot(HaveOccurred()) + Expect(obj).To(Equal(expectedObject)) + }) + + It("length on integer unsupported", func() { + text = ` + len(1); + ` + expectedObject := object.NIL + expectedParseErrors := []error{} + + // parse the program + program, errs = p.ParseProgram(text) + Expect(errs).To(Equal(expectedParseErrors)) + + // evaluate the AST tree + obj, err := e.Eval(program) + Expect(err).To(HaveOccurred()) + Expect(obj).To(Equal(expectedObject)) + }) + }) }) }) }) diff --git a/internal/object/errors.go b/internal/object/errors.go new file mode 100644 index 0000000..e05c4df --- /dev/null +++ b/internal/object/errors.go @@ -0,0 +1,10 @@ +package object + +import ( + "errors" +) + +var ( + ErrWrongNumberArguments = errors.New("wrong number of argument(s)") + ErrUnsupportedArgumentType = errors.New("unsupported argument type") +) diff --git a/internal/object/object.go b/internal/object/object.go index ebda9f3..81b5d52 100644 --- a/internal/object/object.go +++ b/internal/object/object.go @@ -15,6 +15,7 @@ var _ Object = (*Nil)(nil) var _ Object = (*ReturnValue)(nil) var _ Object = (*Error)(nil) var _ Object = (*Func)(nil) +var _ Object = (BuiltinFunc)(nil) type ObjectType string @@ -26,6 +27,7 @@ var ( RETURN_VALUE_OBJ = ObjectType("RETURN_VALUE") ERROR_OBJ = ObjectType("ERROR") FUNCTION_OBJ = ObjectType("FUNCTION") + BUILTINFUNC_OBJ = ObjectType("BUILTINFUNC") ) // boolean literal objects @@ -35,6 +37,21 @@ var ( NIL = NewNil() ) +var BuiltinFuncs = map[string]BuiltinFunc{ + "len": func(args ...Object) (Object, error) { + if len(args) != 1 { + return NIL, ErrWrongNumberArguments + } + + switch arg := args[0].(type) { + case *String: + return NewInteger(int64(len(arg.Value))), nil + default: + return NIL, ErrUnsupportedArgumentType + } + }, +} + type Object interface { Type() ObjectType Inspect() string @@ -210,7 +227,23 @@ func (f *Func) Inspect() string { return builder.String() } -// FIXME: this behavior is undined, not sure an error is truthy or not +// FIXME: this behavior is undined, not sure whether a function is truthy or not func (f *Func) IsTruthy() bool { return false } + +// BuiltinFunc represents a builtin function object +type BuiltinFunc func(args ...Object) (Object, error) + +func (b BuiltinFunc) Type() ObjectType { + return FUNCTION_OBJ +} + +func (b BuiltinFunc) Inspect() string { + return "builtin function" +} + +// FIXME: this behavior is undined, not sure whether a builtin function is truthy or not +func (b BuiltinFunc) IsTruthy() bool { + return false +}