From 6d234099d1e4d984e3abe7baee4797fb41940a91 Mon Sep 17 00:00:00 2001 From: Chuck Smith Date: Mon, 25 Mar 2024 16:18:08 -0400 Subject: [PATCH] type checking and error handling for builtins improved. --- .drone.yml | 2 +- builtins/abs.go | 26 +++++++++-------- builtins/args.go | 14 ++++++++-- builtins/assert.go | 22 +++++++-------- builtins/bin.go | 18 ++++++------ builtins/bool.go | 42 +++++++--------------------- builtins/chr.go | 16 ++++++----- builtins/divmod.go | 32 ++++++++++----------- builtins/exit.go | 17 +++++++---- builtins/ffi.go | 29 +++++++++---------- builtins/find.go | 36 ++++++++++++++++-------- builtins/first.go | 18 ++++++------ builtins/hash.go | 16 +++++++---- builtins/hex.go | 16 ++++++----- builtins/id.go | 13 +++++---- builtins/input.go | 25 ++++++++++------- builtins/int.go | 15 ++++++---- builtins/join.go | 32 ++++++++++----------- builtins/last.go | 18 ++++++------ builtins/len.go | 26 ++++++++--------- builtins/lower.go | 20 +++++++------ builtins/max.go | 32 +++++++++++---------- builtins/min.go | 32 +++++++++++---------- builtins/oct.go | 16 ++++++----- builtins/ord.go | 27 +++++++++++------- builtins/pop.go | 20 +++++++------ builtins/pow.go | 28 +++++++++---------- builtins/print.go | 15 ++++++++-- builtins/push.go | 32 +++++++++------------ builtins/read.go | 26 ++++++++--------- builtins/rest.go | 29 +++++++++---------- builtins/reversed.go | 20 +++++++------ builtins/sorted.go | 20 +++++++------ builtins/split.go | 45 ++++++++++++++--------------- builtins/str.go | 19 ++++++------- builtins/typeof.go | 14 +++++++--- builtins/upper.go | 22 ++++++++------- builtins/write.go | 32 +++++++++------------ evaluator/evaluator_test.go | 12 ++++---- object/array.go | 36 ++++++++++++++++++++++-- object/bool.go | 4 +++ object/builtin.go | 6 ++++ object/closure.go | 12 ++++++++ object/error.go | 7 +++++ object/function.go | 12 ++++++++ object/hash.go | 9 ++++++ object/int.go | 4 +++ object/null.go | 4 +++ object/object.go | 12 +++++++- object/str.go | 13 ++++++++- typing/typing.go | 56 +++++++++++++++++++++++++++++++++++++ vm/vm_test.go | 14 +++++----- 52 files changed, 650 insertions(+), 433 deletions(-) create mode 100644 typing/typing.go diff --git a/.drone.yml b/.drone.yml index 9f36c6b..e7e337d 100644 --- a/.drone.yml +++ b/.drone.yml @@ -5,7 +5,7 @@ steps: - name: build image: golang:latest commands: - - make test + - go test -v -short -cover -coverprofile=coverage.txt ./... - name: coverage image: plugins/codecov diff --git a/builtins/abs.go b/builtins/abs.go index eeb8399..f767ed5 100644 --- a/builtins/abs.go +++ b/builtins/abs.go @@ -1,20 +1,24 @@ package builtins -import "monkey/object" +import ( + "monkey/object" + "monkey/typing" +) // Abs ... func Abs(args ...object.Object) object.Object { - if len(args) != 1 { - return newError("wrong number of arguments. got=%d, want=1", - len(args)) + if err := typing.Check( + "abs", args, + typing.ExactArgs(1), + typing.WithTypes(object.INTEGER_OBJ), + ); err != nil { + return newError(err.Error()) } - if i, ok := args[0].(*object.Integer); ok { - value := i.Value - if value < 0 { - value = value * -1 - } - return &object.Integer{Value: value} + i := args[0].(*object.Integer) + value := i.Value + if value < 0 { + value = value * -1 } - return newError("argument to `abs` not supported, got %s", args[0].Type()) + return &object.Integer{Value: value} } diff --git a/builtins/args.go b/builtins/args.go index 9c92379..1fcc398 100644 --- a/builtins/args.go +++ b/builtins/args.go @@ -1,9 +1,19 @@ package builtins -import "monkey/object" +import ( + "monkey/object" + "monkey/typing" +) -// Args ... +// Args ... func Args(args ...object.Object) object.Object { + if err := typing.Check( + "args", args, + typing.ExactArgs(0), + ); err != nil { + return newError(err.Error()) + } + elements := make([]object.Object, len(object.Arguments)) for i, arg := range object.Arguments { elements[i] = &object.String{Value: arg} diff --git a/builtins/assert.go b/builtins/assert.go index 5f52e54..bee2b19 100644 --- a/builtins/assert.go +++ b/builtins/assert.go @@ -1,6 +1,9 @@ package builtins -import "monkey/object" +import ( + "monkey/object" + "monkey/typing" +) import ( "fmt" @@ -9,17 +12,12 @@ import ( // Assert ... func Assert(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.BOOLEAN_OBJ { - return newError("argument #1 to `assert` must be BOOLEAN, got %s", - args[0].Type()) - } - if args[1].Type() != object.STRING_OBJ { - return newError("argument #2 to `assert` must be STRING, got %s", - args[0].Type()) + if err := typing.Check( + "assert", args, + typing.ExactArgs(2), + typing.WithTypes(object.BOOLEAN_OBJ, object.STRING_OBJ), + ); err != nil { + return newError(err.Error()) } if !args[0].(*object.Boolean).Value { diff --git a/builtins/bin.go b/builtins/bin.go index e7ae87e..535ad13 100644 --- a/builtins/bin.go +++ b/builtins/bin.go @@ -3,18 +3,20 @@ package builtins import ( "fmt" "monkey/object" + "monkey/typing" "strconv" ) -// Bin ... +// Bin ... func Bin(args ...object.Object) object.Object { - if len(args) != 1 { - return newError("wrong number of arguments. got=%d, want=1", - len(args)) + if err := typing.Check( + "bin", args, + typing.ExactArgs(1), + typing.WithTypes(object.INTEGER_OBJ), + ); err != nil { + return newError(err.Error()) } - if i, ok := args[0].(*object.Integer); ok { - return &object.String{Value: fmt.Sprintf("0b%s", strconv.FormatInt(i.Value, 2))} - } - return newError("argument to `bin` not supported, got %s", args[0].Type()) + i := args[0].(*object.Integer) + return &object.String{Value: fmt.Sprintf("0b%s", strconv.FormatInt(i.Value, 2))} } diff --git a/builtins/bool.go b/builtins/bool.go index 8dc6cdb..6744e61 100644 --- a/builtins/bool.go +++ b/builtins/bool.go @@ -1,40 +1,18 @@ package builtins -import "monkey/object" +import ( + "monkey/object" + "monkey/typing" +) // Bool ... func Bool(args ...object.Object) object.Object { - if len(args) != 1 { - return newError("wrong number of arguments. got=%d, want=1", len(args)) + if err := typing.Check( + "bool", args, + typing.ExactArgs(1), + ); err != nil { + return newError(err.Error()) } - switch arg := args[0].(type) { - case *object.Null: - return &object.Boolean{Value: false} - case *object.Boolean: - return arg - case *object.Integer: - if arg.Value == 0 { - return &object.Boolean{Value: false} - } - return &object.Boolean{Value: true} - case *object.String: - if len(arg.Value) > 0 { - return &object.Boolean{Value: true} - } - return &object.Boolean{Value: false} - case *object.Array: - if len(arg.Elements) > 0 { - return &object.Boolean{Value: true} - } - return &object.Boolean{Value: false} - case *object.Hash: - if len(arg.Pairs) > 0 { - return &object.Boolean{Value: true} - } - return &object.Boolean{Value: false} - - default: - return newError("argument to `bool` not supported, got=%s", args[0].Type()) - } + return &object.Boolean{Value: args[0].Bool()} } diff --git a/builtins/chr.go b/builtins/chr.go index 6619110..f647560 100644 --- a/builtins/chr.go +++ b/builtins/chr.go @@ -3,17 +3,19 @@ package builtins import ( "fmt" "monkey/object" + "monkey/typing" ) // Chr ... func Chr(args ...object.Object) object.Object { - if len(args) != 1 { - return newError("wrong number of arguments. got=%d, want=1", - len(args)) + if err := typing.Check( + "chr", args, + typing.ExactArgs(1), + typing.WithTypes(object.INTEGER_OBJ), + ); err != nil { + return newError(err.Error()) } - if i, ok := args[0].(*object.Integer); ok { - return &object.String{Value: fmt.Sprintf("%c", rune(i.Value))} - } - return newError("argument to `chr` not supported, got %s", args[0].Type()) + i := args[0].(*object.Integer) + return &object.String{Value: fmt.Sprintf("%c", rune(i.Value))} } diff --git a/builtins/divmod.go b/builtins/divmod.go index 01701b1..b1906d1 100644 --- a/builtins/divmod.go +++ b/builtins/divmod.go @@ -1,24 +1,24 @@ package builtins -import "monkey/object" +import ( + "monkey/object" + "monkey/typing" +) // Divmod ... func Divmod(args ...object.Object) object.Object { - if len(args) != 2 { - return newError("wrong number of arguments. got=%d, want=2", - len(args)) + if err := typing.Check( + "divmod", args, + typing.ExactArgs(2), + typing.WithTypes(object.INTEGER_OBJ, object.INTEGER_OBJ), + ); err != nil { + return newError(err.Error()) } - if a, ok := args[0].(*object.Integer); ok { - if b, ok := args[1].(*object.Integer); ok { - elements := make([]object.Object, 2) - elements[0] = &object.Integer{Value: a.Value / b.Value} - elements[1] = &object.Integer{Value: a.Value % b.Value} - return &object.Array{Elements: elements} - } else { - return newError("expected argument #2 to `divmod` to be `int` got=%s", args[1].Type()) - } - } else { - return newError("expected argument #1 to `divmod` to be `int` got=%s", args[0].Type()) - } + a := args[0].(*object.Integer) + b := args[1].(*object.Integer) + elements := make([]object.Object, 2) + elements[0] = &object.Integer{Value: a.Value / b.Value} + elements[1] = &object.Integer{Value: a.Value % b.Value} + return &object.Array{Elements: elements} } diff --git a/builtins/exit.go b/builtins/exit.go index 0bcf42c..b84c662 100644 --- a/builtins/exit.go +++ b/builtins/exit.go @@ -1,15 +1,22 @@ package builtins -import "monkey/object" +import ( + "monkey/object" + "monkey/typing" +) // Exit ... func Exit(args ...object.Object) object.Object { + if err := typing.Check( + "exit", args, + typing.RangeOfArgs(0, 1), + typing.WithTypes(object.INTEGER_OBJ), + ); err != nil { + return newError(err.Error()) + } + var status int if len(args) == 1 { - if args[0].Type() != object.INTEGER_OBJ { - return newError("argument to `exit` must be INTEGER, got %s", - args[0].Type()) - } status = int(args[0].(*object.Integer).Value) } diff --git a/builtins/ffi.go b/builtins/ffi.go index c237abd..6f5c136 100644 --- a/builtins/ffi.go +++ b/builtins/ffi.go @@ -1,6 +1,9 @@ package builtins -import "monkey/object" +import ( + "monkey/object" + "monkey/typing" +) import ( "fmt" @@ -9,22 +12,16 @@ import ( // FFI ... func FFI(args ...object.Object) object.Object { - if len(args) != 2 { - return newError("wrong number of arguments. got=%d, want=2", - len(args)) + if err := typing.Check( + "ffi", args, + typing.ExactArgs(2), + typing.WithTypes(object.STRING_OBJ, object.STRING_OBJ), + ); err != nil { + return newError(err.Error()) } - arg, ok := args[0].(*object.String) - if !ok { - return newError("argument #1 to `ffi` expected to be `str` got=%T", args[0].Type()) - } - name := arg.Value - - arg, ok = args[1].(*object.String) - if !ok { - return newError("argument #2 to `ffi` expected to be `str` got=%T", args[0].Type()) - } - symbol := arg.Value + name := args[0].(*object.String).Value + symbol := args[1].(*object.String).Value p, err := plugin.Open(fmt.Sprintf("%s.so", name)) if err != nil { @@ -36,5 +33,5 @@ func FFI(args ...object.Object) object.Object { return newError("error finding symbol: %s", err) } - return &object.Builtin{Name: symbol, Fn: v.(func(...object.Object) object.Object)} + return &object.Builtin{Name: symbol, Fn: v.(object.BuiltinFunction)} } diff --git a/builtins/find.go b/builtins/find.go index 29b3780..dc2e7be 100644 --- a/builtins/find.go +++ b/builtins/find.go @@ -2,6 +2,7 @@ package builtins import ( "monkey/object" + "monkey/typing" "sort" ) @@ -11,19 +12,29 @@ import ( // Find ... func Find(args ...object.Object) object.Object { - if len(args) != 2 { - return newError("wrong number of arguments. got=%d, want=2", - len(args)) + if err := typing.Check( + "find", args, + typing.ExactArgs(2), + ); err != nil { + return newError(err.Error()) } + // find substring in string if haystack, ok := args[0].(*object.String); ok { - if needle, ok := args[1].(*object.String); ok { - index := strings.Index(haystack.Value, needle.Value) - return &object.Integer{Value: int64(index)} - } else { - return newError("expected arg #2 to be `str` got got=%T", args[1]) + if err := typing.Check( + "find", args, + typing.WithTypes(object.STRING_OBJ, object.STRING_OBJ), + ); err != nil { + return newError(err.Error()) } - } else if haystack, ok := args[0].(*object.Array); ok { + + needle := args[1].(*object.String) + index := strings.Index(haystack.Value, needle.Value) + return &object.Integer{Value: int64(index)} + } + + // find in array + if haystack, ok := args[0].(*object.Array); ok { needle := args[1].(object.Comparable) i := sort.Search(len(haystack.Elements), func(i int) bool { return needle.Compare(haystack.Elements[i]) == 0 @@ -32,7 +43,10 @@ func Find(args ...object.Object) object.Object { return &object.Integer{Value: int64(i)} } return &object.Integer{Value: -1} - } else { - return newError("expected arg #1 to be `str` or `array` got got=%T", args[0]) } + + return newError( + "TypeError: find() expected argument #1 to be `array` or `str` got `%s`", + args[0].Type(), + ) } diff --git a/builtins/first.go b/builtins/first.go index b25bac3..a82149e 100644 --- a/builtins/first.go +++ b/builtins/first.go @@ -1,16 +1,18 @@ package builtins -import "monkey/object" +import ( + "monkey/object" + "monkey/typing" +) // First ... func First(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()) + if err := typing.Check( + "first", args, + typing.ExactArgs(1), + typing.WithTypes(object.ARRAY_OBJ), + ); err != nil { + return newError(err.Error()) } arr := args[0].(*object.Array) diff --git a/builtins/hash.go b/builtins/hash.go index c13a170..d7433ae 100644 --- a/builtins/hash.go +++ b/builtins/hash.go @@ -1,16 +1,22 @@ package builtins -import "monkey/object" +import ( + "monkey/object" + "monkey/typing" +) // HashOf ... func HashOf(args ...object.Object) object.Object { - if len(args) != 1 { - return newError("wrong number of arguments. got=%d, want=1", - len(args)) + if err := typing.Check( + "hash", args, + typing.ExactArgs(1), + ); err != nil { + return newError(err.Error()) } if hash, ok := args[0].(object.Hashable); ok { return &object.Integer{Value: int64(hash.HashKey().Value)} } - return newError("argument #1 to `hash()` is not hashable: %s", args[0].Inspect()) + + return newError("TypeError: hash() expected argument #1 to be hashable") } diff --git a/builtins/hex.go b/builtins/hex.go index 952ce1d..8483014 100644 --- a/builtins/hex.go +++ b/builtins/hex.go @@ -3,18 +3,20 @@ package builtins import ( "fmt" "monkey/object" + "monkey/typing" "strconv" ) // Hex ... func Hex(args ...object.Object) object.Object { - if len(args) != 1 { - return newError("wrong number of arguments. got=%d, want=1", - len(args)) + if err := typing.Check( + "hex", args, + typing.ExactArgs(1), + typing.WithTypes(object.INTEGER_OBJ), + ); err != nil { + return newError(err.Error()) } - if i, ok := args[0].(*object.Integer); ok { - return &object.String{Value: fmt.Sprintf("0x%s", strconv.FormatInt(i.Value, 16))} - } - return newError("argument to `hex` not supported, got %s", args[0].Type()) + i := args[0].(*object.Integer) + return &object.String{Value: fmt.Sprintf("0x%s", strconv.FormatInt(i.Value, 16))} } diff --git a/builtins/id.go b/builtins/id.go index 7214ac0..f620af6 100644 --- a/builtins/id.go +++ b/builtins/id.go @@ -3,13 +3,16 @@ package builtins import ( "fmt" "monkey/object" + "monkey/typing" ) // IdOf ... func IdOf(args ...object.Object) object.Object { - if len(args) != 1 { - return newError("wrong number of arguments. got=%d, want=1", - len(args)) + if err := typing.Check( + "id", args, + typing.ExactArgs(1), + ); err != nil { + return newError(err.Error()) } arg := args[0] @@ -32,7 +35,7 @@ func IdOf(args ...object.Object) object.Object { return &object.String{Value: fmt.Sprintf("%p", c)} } else if b, ok := arg.(*object.Builtin); ok { return &object.String{Value: fmt.Sprintf("%p", b)} - } else { - return newError("argument 31 to `id()` unsupported got=%T", arg.Type()) } + + return nil } diff --git a/builtins/input.go b/builtins/input.go index f75a449..b5bd2f1 100644 --- a/builtins/input.go +++ b/builtins/input.go @@ -1,6 +1,9 @@ package builtins -import "monkey/object" +import ( + "monkey/object" + "monkey/typing" +) import ( "bufio" @@ -11,15 +14,17 @@ import ( // Input ... func Input(args ...object.Object) object.Object { - if len(args) > 0 { - obj, ok := args[0].(*object.String) - if !ok { - return newError( - "argument to `input` not supported, got %s", - args[0].Type(), - ) - } - fmt.Fprintf(os.Stdout, obj.Value) + if err := typing.Check( + "input", args, + typing.RangeOfArgs(0, 1), + typing.WithTypes(object.STRING_OBJ), + ); err != nil { + return newError(err.Error()) + } + + if len(args) == 1 { + prompt := args[0].(*object.String).Value + fmt.Fprintf(os.Stdout, prompt) } buffer := bufio.NewReader(os.Stdin) diff --git a/builtins/int.go b/builtins/int.go index d0cc2a5..5e3cbc7 100644 --- a/builtins/int.go +++ b/builtins/int.go @@ -1,13 +1,19 @@ package builtins -import "monkey/object" +import ( + "monkey/object" + "monkey/typing" +) import "strconv" // Int ... func Int(args ...object.Object) object.Object { - if len(args) != 1 { - return newError("wrong number of arguments. got=%d, want=1", len(args)) + if err := typing.Check( + "int", args, + typing.ExactArgs(1), + ); err != nil { + return newError(err.Error()) } switch arg := args[0].(type) { @@ -23,9 +29,8 @@ func Int(args ...object.Object) object.Object { if err != nil { return newError("could not parse string to int: %s", err) } - return &object.Integer{Value: n} default: - return newError("argument to `int` not supported, got=%s", args[0].Type()) + return &object.Integer{} } } diff --git a/builtins/join.go b/builtins/join.go index 22b879d..c814c43 100644 --- a/builtins/join.go +++ b/builtins/join.go @@ -1,6 +1,9 @@ package builtins -import "monkey/object" +import ( + "monkey/object" + "monkey/typing" +) import ( "strings" @@ -8,22 +11,19 @@ import ( // Join ... func Join(args ...object.Object) object.Object { - if len(args) != 2 { - return newError("wrong number of arguments. got=%d, want=1", - len(args)) + if err := typing.Check( + "join", args, + typing.ExactArgs(2), + typing.WithTypes(object.ARRAY_OBJ, object.STRING_OBJ), + ); err != nil { + return newError(err.Error()) } - if arr, ok := args[0].(*object.Array); ok { - if sep, ok := args[1].(*object.String); ok { - a := make([]string, len(arr.Elements)) - for i, el := range arr.Elements { - a[i] = el.String() - } - return &object.String{Value: strings.Join(a, sep.Value)} - } else { - return newError("expected arg #2 to be `str` got got=%T", args[1]) - } - } else { - return newError("expected arg #1 to be `array` got got=%T", args[0]) + arr := args[0].(*object.Array) + sep := args[1].(*object.String) + a := make([]string, len(arr.Elements)) + for i, el := range arr.Elements { + a[i] = el.String() } + return &object.String{Value: strings.Join(a, sep.Value)} } diff --git a/builtins/last.go b/builtins/last.go index c04b1aa..0fafeec 100644 --- a/builtins/last.go +++ b/builtins/last.go @@ -1,16 +1,18 @@ package builtins -import "monkey/object" +import ( + "monkey/object" + "monkey/typing" +) // Last ... func Last(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()) + if err := typing.Check( + "last", args, + typing.ExactArgs(1), + typing.WithTypes(object.ARRAY_OBJ), + ); err != nil { + return newError(err.Error()) } arr := args[0].(*object.Array) diff --git a/builtins/len.go b/builtins/len.go index 03c9667..980eafd 100644 --- a/builtins/len.go +++ b/builtins/len.go @@ -1,23 +1,21 @@ package builtins -import "monkey/object" - -import "unicode/utf8" +import ( + "monkey/object" + "monkey/typing" +) // Len ... func Len(args ...object.Object) object.Object { - if len(args) != 1 { - return newError("wrong number of arguments. got=%d, want=1", - len(args)) + if err := typing.Check( + "len", args, + typing.ExactArgs(1), + ); err != nil { + return newError(err.Error()) } - switch arg := args[0].(type) { - case *object.Array: - return &object.Integer{Value: int64(len(arg.Elements))} - case *object.String: - return &object.Integer{Value: int64(utf8.RuneCountInString(arg.Value))} - default: - return newError("argument to `len` not supported, got %s", - args[0].Type()) + if size, ok := args[0].(object.Sizeable); ok { + return &object.Integer{Value: int64(size.Len())} } + return newError("TypeError: object of type '%s' has no len()", args[0].Type()) } diff --git a/builtins/lower.go b/builtins/lower.go index de8babf..2dcfa7b 100644 --- a/builtins/lower.go +++ b/builtins/lower.go @@ -1,18 +1,22 @@ package builtins -import "monkey/object" +import ( + "monkey/object" + "monkey/typing" +) import "strings" // Lower ... func Lower(args ...object.Object) object.Object { - if len(args) != 1 { - return newError("wrong number of arguments. got=%d, want=1", - len(args)) + if err := typing.Check( + "lower", args, + typing.ExactArgs(1), + typing.WithTypes(object.STRING_OBJ), + ); err != nil { + return newError(err.Error()) } - if str, ok := args[0].(*object.String); ok { - return &object.String{Value: strings.ToLower(str.Value)} - } - return newError("expected `str` argument to `lower` got=%T", args[0]) + str := args[0].(*object.String) + return &object.String{Value: strings.ToLower(str.Value)} } diff --git a/builtins/max.go b/builtins/max.go index 0267762..47489fd 100644 --- a/builtins/max.go +++ b/builtins/max.go @@ -2,28 +2,30 @@ package builtins import ( "monkey/object" + "monkey/typing" "sort" ) // Max ... func Max(args ...object.Object) object.Object { - if len(args) != 1 { - return newError("wrong number of arguments. got=%d, want=1", - len(args)) + if err := typing.Check( + "max", args, + typing.ExactArgs(1), + typing.WithTypes(object.ARRAY_OBJ), + ); err != nil { + return newError(err.Error()) } - if a, ok := args[0].(*object.Array); ok { - // TODO: Make this more generic - xs := make([]int, len(a.Elements)) - for n, e := range a.Elements { - if i, ok := e.(*object.Integer); ok { - xs = append(xs, int(i.Value)) - } else { - return newError("item #%d not an `int` got=%s", n, e.Type()) - } + a := args[0].(*object.Array) + // TODO: Make this more generic + xs := make([]int, len(a.Elements)) + for n, e := range a.Elements { + if i, ok := e.(*object.Integer); ok { + xs = append(xs, int(i.Value)) + } else { + return newError("item #%d not an `int` got=%s", n, e.Type()) } - sort.Ints(xs) - return &object.Integer{Value: int64(xs[len(xs)-1])} } - return newError("argument #1 to `max` expected to be `array` got=%T", args[0].Type()) + sort.Ints(xs) + return &object.Integer{Value: int64(xs[len(xs)-1])} } diff --git a/builtins/min.go b/builtins/min.go index e9aa539..a4b6406 100644 --- a/builtins/min.go +++ b/builtins/min.go @@ -2,28 +2,30 @@ package builtins import ( "monkey/object" + "monkey/typing" "sort" ) // Min ... func Min(args ...object.Object) object.Object { - if len(args) != 1 { - return newError("wrong number of arguments. got=%d, want=1", - len(args)) + if err := typing.Check( + "min", args, + typing.ExactArgs(1), + typing.WithTypes(object.ARRAY_OBJ), + ); err != nil { + return newError(err.Error()) } - if a, ok := args[0].(*object.Array); ok { - // TODO: Make this more generic - xs := make([]int, len(a.Elements)) - for n, e := range a.Elements { - if i, ok := e.(*object.Integer); ok { - xs = append(xs, int(i.Value)) - } else { - return newError("item #%d not an `int` got=%s", n, e.Type()) - } + a := args[0].(*object.Array) + // TODO: Make this more generic + xs := make([]int, len(a.Elements)) + for n, e := range a.Elements { + if i, ok := e.(*object.Integer); ok { + xs = append(xs, int(i.Value)) + } else { + return newError("item #%d not an `int` got=%s", n, e.Type()) } - sort.Ints(xs) - return &object.Integer{Value: int64(xs[0])} } - return newError("argument #1 to `min` expected to be `array` got=%T", args[0].Type()) + sort.Ints(xs) + return &object.Integer{Value: int64(xs[0])} } diff --git a/builtins/oct.go b/builtins/oct.go index e107ad8..6c9f949 100644 --- a/builtins/oct.go +++ b/builtins/oct.go @@ -3,18 +3,20 @@ package builtins import ( "fmt" "monkey/object" + "monkey/typing" "strconv" ) // Oct ... func Oct(args ...object.Object) object.Object { - if len(args) != 1 { - return newError("wrong number of arguments. got=%d, want=1", - len(args)) + if err := typing.Check( + "oct", args, + typing.ExactArgs(1), + typing.WithTypes(object.INTEGER_OBJ), + ); err != nil { + return newError(err.Error()) } - if i, ok := args[0].(*object.Integer); ok { - return &object.String{Value: fmt.Sprintf("0%s", strconv.FormatInt(i.Value, 8))} - } - return newError("argument to `oct` not supported, got %s", args[0].Type()) + i := args[0].(*object.Integer) + return &object.String{Value: fmt.Sprintf("0%s", strconv.FormatInt(i.Value, 8))} } diff --git a/builtins/ord.go b/builtins/ord.go index 0dc21f4..c12e71b 100644 --- a/builtins/ord.go +++ b/builtins/ord.go @@ -1,19 +1,26 @@ package builtins -import "monkey/object" +import ( + "monkey/object" + "monkey/typing" +) // Ord ... func Ord(args ...object.Object) object.Object { - if len(args) != 1 { - return newError("wrong number of arguments. got=%d, want=1", - len(args)) + if err := typing.Check( + "ord", args, + typing.ExactArgs(1), + typing.WithTypes(object.STRING_OBJ), + ); err != nil { + return newError(err.Error()) } - if s, ok := args[0].(*object.String); ok { - if len(s.Value) == 1 { - return &object.Integer{Value: int64(s.Value[0])} - } - return newError("`ord()` expected a character but got string of length %d", len(s.Value)) + s := args[0].(*object.String) + if len(s.Value) == 1 { + return &object.Integer{Value: int64(s.Value[0])} } - return newError("argument to `ord` not supported, got %s", args[0].Type()) + return newError( + "TypeError: ord() expected a single character `str` got=%s", + s.Inspect(), + ) } diff --git a/builtins/pop.go b/builtins/pop.go index cbfaab6..de2557c 100644 --- a/builtins/pop.go +++ b/builtins/pop.go @@ -1,23 +1,25 @@ package builtins -import "monkey/object" +import ( + "monkey/object" + "monkey/typing" +) // Pop ... func Pop(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 `pop` must be array, got %s", - args[0].Type()) + if err := typing.Check( + "pop", args, + typing.ExactArgs(1), + typing.WithTypes(object.ARRAY_OBJ), + ); err != nil { + return newError(err.Error()) } arr := args[0].(*object.Array) length := len(arr.Elements) if length == 0 { - return newError("cannot pop from an empty array") + return newError("IndexError: pop from an empty array") } element := arr.Elements[length-1] diff --git a/builtins/pow.go b/builtins/pow.go index 63f02c8..f25c3e5 100644 --- a/builtins/pow.go +++ b/builtins/pow.go @@ -1,6 +1,9 @@ package builtins -import "monkey/object" +import ( + "monkey/object" + "monkey/typing" +) func pow(x, y int64) int64 { p := int64(1) @@ -16,19 +19,16 @@ func pow(x, y int64) int64 { // Pow ... func Pow(args ...object.Object) object.Object { - if len(args) != 2 { - return newError("wrong number of arguments. got=%d, want=2", - len(args)) + if err := typing.Check( + "pow", args, + typing.ExactArgs(2), + typing.WithTypes(object.INTEGER_OBJ, object.INTEGER_OBJ), + ); err != nil { + return newError(err.Error()) } - if x, ok := args[0].(*object.Integer); ok { - if y, ok := args[1].(*object.Integer); ok { - value := pow(x.Value, y.Value) - return &object.Integer{Value: value} - } else { - return newError("expected argument #2 to `pow` to be `int` got=%s", args[1].Type()) - } - } else { - return newError("expected argument #1 to `pow` to be `int` got=%s", args[0].Type()) - } + x := args[0].(*object.Integer) + y := args[1].(*object.Integer) + value := pow(x.Value, y.Value) + return &object.Integer{Value: value} } diff --git a/builtins/print.go b/builtins/print.go index 6a4257e..c37d757 100644 --- a/builtins/print.go +++ b/builtins/print.go @@ -1,14 +1,23 @@ package builtins -import "monkey/object" +import ( + "monkey/object" + "monkey/typing" +) import "fmt" // Print ... func Print(args ...object.Object) object.Object { - for _, arg := range args { - fmt.Println(arg.String()) + if err := typing.Check( + "print", args, + typing.MinimumArgs(1), + typing.WithTypes(object.STRING_OBJ), + ); err != nil { + return newError(err.Error()) } + fmt.Println(args[0].String()) + return nil } diff --git a/builtins/push.go b/builtins/push.go index 9e7b93e..1c660da 100644 --- a/builtins/push.go +++ b/builtins/push.go @@ -1,28 +1,22 @@ package builtins -import "monkey/object" +import ( + "monkey/object" + "monkey/typing" +) // Push ... func Push(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()) + if err := typing.Check( + "push", args, + typing.ExactArgs(2), + typing.WithTypes(object.ARRAY_OBJ), + ); err != nil { + return newError(err.Error()) } arr := args[0].(*object.Array) - length := len(arr.Elements) - - newElements := make([]object.Object, length+1) - copy(newElements, arr.Elements) - if immutable, ok := args[1].(object.Immutable); ok { - newElements[length] = immutable.Clone() - } else { - newElements[length] = args[1] - } - - return &object.Array{Elements: newElements} + newArray := arr.Copy() + newArray.Append(args[1]) + return newArray } diff --git a/builtins/read.go b/builtins/read.go index b996025..759bba9 100644 --- a/builtins/read.go +++ b/builtins/read.go @@ -1,27 +1,25 @@ package builtins -import "monkey/object" - import ( - "os" + "io/ioutil" + "monkey/object" + "monkey/typing" ) // Read ... func Read(args ...object.Object) object.Object { - if len(args) != 1 { - return newError("wrong number of arguments. got=%d, want=1", - len(args)) + if err := typing.Check( + "read", args, + typing.ExactArgs(1), + typing.WithTypes(object.STRING_OBJ), + ); err != nil { + return newError(err.Error()) } - arg, ok := args[0].(*object.String) - if !ok { - return newError("argument to `read` expected to be `str` got=%T", args[0].Type()) - } - - filename := arg.Value - data, err := os.ReadFile(filename) + filename := args[0].(*object.String).Value + data, err := ioutil.ReadFile(filename) if err != nil { - return newError("error reading file: %s", err) + return newError("IOError: error reading from file %s: %s", filename, err) } return &object.String{Value: string(data)} diff --git a/builtins/rest.go b/builtins/rest.go index 15f730a..21c80e1 100644 --- a/builtins/rest.go +++ b/builtins/rest.go @@ -1,25 +1,22 @@ package builtins -import "monkey/object" +import ( + "monkey/object" + "monkey/typing" +) // Rest ... func Rest(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()) + if err := typing.Check( + "rest", args, + typing.ExactArgs(1), + typing.WithTypes(object.ARRAY_OBJ), + ); err != nil { + return newError(err.Error()) } arr := args[0].(*object.Array) - length := len(arr.Elements) - if length > 0 { - newElements := make([]object.Object, length-1, length-1) - copy(newElements, arr.Elements[1:length]) - return &object.Array{Elements: newElements} - } - - return nil + newArray := arr.Copy() + newArray.PopLeft() + return newArray } diff --git a/builtins/reversed.go b/builtins/reversed.go index bb33a4d..f5ee375 100644 --- a/builtins/reversed.go +++ b/builtins/reversed.go @@ -2,19 +2,21 @@ package builtins import ( "monkey/object" + "monkey/typing" ) // Reversed ... func Reversed(args ...object.Object) object.Object { - if len(args) != 1 { - return newError("wrong number of arguments. got=%d, want=1", - len(args)) + if err := typing.Check( + "reversed", args, + typing.ExactArgs(1), + typing.WithTypes(object.ARRAY_OBJ), + ); err != nil { + return newError(err.Error()) } - if a, ok := args[0].(*object.Array); ok { - newArray := a.Copy() - newArray.Reverse() - return newArray - } - return newError("argument #1 to `reversed` expected to be `array` got=%T", args[0].Type()) + arr := args[0].(*object.Array) + newArray := arr.Copy() + newArray.Reverse() + return newArray } diff --git a/builtins/sorted.go b/builtins/sorted.go index aabfed3..0280b05 100644 --- a/builtins/sorted.go +++ b/builtins/sorted.go @@ -2,20 +2,22 @@ package builtins import ( "monkey/object" + "monkey/typing" "sort" ) // Sorted ... func Sorted(args ...object.Object) object.Object { - if len(args) != 1 { - return newError("wrong number of arguments. got=%d, want=1", - len(args)) + if err := typing.Check( + "sort", args, + typing.ExactArgs(1), + typing.WithTypes(object.ARRAY_OBJ), + ); err != nil { + return newError(err.Error()) } - if a, ok := args[0].(*object.Array); ok { - newArray := a.Copy() - sort.Sort(newArray) - return newArray - } - return newError("argument #1 to `sorted` expected to be `array` got=%T", args[0].Type()) + arr := args[0].(*object.Array) + newArray := arr.Copy() + sort.Sort(newArray) + return newArray } diff --git a/builtins/split.go b/builtins/split.go index fb05c76..dcef498 100644 --- a/builtins/split.go +++ b/builtins/split.go @@ -1,6 +1,9 @@ package builtins -import "monkey/object" +import ( + "monkey/object" + "monkey/typing" +) import ( "strings" @@ -8,31 +11,25 @@ import ( // Split ... func Split(args ...object.Object) object.Object { - if len(args) < 1 { - return newError("wrong number of arguments. got=%d, want=1", - len(args)) + if err := typing.Check( + "split", args, + typing.RangeOfArgs(1, 2), + typing.WithTypes(object.STRING_OBJ, object.STRING_OBJ), + ); err != nil { + return newError(err.Error()) } - if obj, ok := args[0].(*object.String); ok { - var sep string + var sep string + s := args[0].(*object.String).Value - s := obj.Value - - if len(args) == 2 { - if obj, ok := args[1].(*object.String); ok { - sep = obj.Value - } else { - return newError("expected arg #2 to be `str` got=%T", args[1]) - } - } - - tokens := strings.Split(s, sep) - elements := make([]object.Object, len(tokens)) - for i, token := range tokens { - elements[i] = &object.String{Value: token} - } - return &object.Array{Elements: elements} - } else { - return newError("expected arg #1 to be `str` got got=%T", args[0]) + if len(args) == 2 { + sep = args[1].(*object.String).Value } + + tokens := strings.Split(s, sep) + elements := make([]object.Object, len(tokens)) + for i, token := range tokens { + elements[i] = &object.String{Value: token} + } + return &object.Array{Elements: elements} } diff --git a/builtins/str.go b/builtins/str.go index 8c77a8f..a2e56ed 100644 --- a/builtins/str.go +++ b/builtins/str.go @@ -1,21 +1,18 @@ package builtins -import "monkey/object" - import ( - "fmt" + "monkey/object" + "monkey/typing" ) // Str ... func Str(args ...object.Object) object.Object { - if len(args) != 1 { - return newError("wrong number of arguments. got=%d, want=1", len(args)) + if err := typing.Check( + "str", args, + typing.ExactArgs(1), + ); err != nil { + return newError(err.Error()) } - arg, ok := args[0].(fmt.Stringer) - if !ok { - return newError("argument to `str` not supported, got %s", args[0].Type()) - } - - return &object.String{Value: arg.String()} + return &object.String{Value: args[0].String()} } diff --git a/builtins/typeof.go b/builtins/typeof.go index c093254..ace5799 100644 --- a/builtins/typeof.go +++ b/builtins/typeof.go @@ -1,11 +1,17 @@ package builtins -import "monkey/object" +import ( + "monkey/object" + "monkey/typing" +) -// TypeOf ... +// TypeOf ... func TypeOf(args ...object.Object) object.Object { - if len(args) != 1 { - return newError("wrong number of arguments. got=%d, want=1", len(args)) + if err := typing.Check( + "type", args, + typing.ExactArgs(1), + ); err != nil { + return newError(err.Error()) } return &object.String{Value: string(args[0].Type())} diff --git a/builtins/upper.go b/builtins/upper.go index 45c0880..711ea79 100644 --- a/builtins/upper.go +++ b/builtins/upper.go @@ -1,18 +1,20 @@ package builtins -import "monkey/object" - -import "strings" +import ( + "monkey/object" + "monkey/typing" + "strings" +) // Upper ... func Upper(args ...object.Object) object.Object { - if len(args) != 1 { - return newError("wrong number of arguments. got=%d, want=1", - len(args)) + if err := typing.Check( + "upper", args, + typing.ExactArgs(1), + typing.WithTypes(object.STRING_OBJ), + ); err != nil { + return newError(err.Error()) } - if str, ok := args[0].(*object.String); ok { - return &object.String{Value: strings.ToUpper(str.Value)} - } - return newError("expected `str` argument to `upper` got=%T", args[0]) + return &object.String{Value: strings.ToUpper(args[0].(*object.String).Value)} } diff --git a/builtins/write.go b/builtins/write.go index c2ff56f..0f907bb 100644 --- a/builtins/write.go +++ b/builtins/write.go @@ -1,33 +1,27 @@ package builtins -import "monkey/object" - import ( - "os" + "io/ioutil" + "monkey/object" + "monkey/typing" ) // Write ... func Write(args ...object.Object) object.Object { - if len(args) != 2 { - return newError("wrong number of arguments. got=%d, want=2", - len(args)) + if err := typing.Check( + "write", args, + typing.ExactArgs(2), + typing.WithTypes(object.STRING_OBJ, object.STRING_OBJ), + ); err != nil { + return newError(err.Error()) } - arg, ok := args[0].(*object.String) - if !ok { - return newError("argument #1 to `write` expected to be `str` got=%T", args[0].Type()) - } - filename := arg.Value + filename := args[0].(*object.String).Value + data := []byte(args[1].(*object.String).Value) - arg, ok = args[1].(*object.String) - if !ok { - return newError("argument #2 to `write` expected to be `str` got=%T", args[1].Type()) - } - data := []byte(arg.Value) - - err := os.WriteFile(filename, data, 0755) + err := ioutil.WriteFile(filename, data, 0755) if err != nil { - return newError("error writing file: %s", err) + return newError("IOError: error writing file %s: %s", filename, err) } return &object.Null{} diff --git a/evaluator/evaluator_test.go b/evaluator/evaluator_test.go index e57cbb4..b759c85 100644 --- a/evaluator/evaluator_test.go +++ b/evaluator/evaluator_test.go @@ -398,24 +398,24 @@ func TestBuiltinFunctions(t *testing.T) { {`len("")`, 0}, {`len("four")`, 4}, {`len("hello world")`, 11}, - {`len(1)`, errors.New("argument to `len` not supported, got int")}, - {`len("one", "two")`, errors.New("wrong number of arguments. got=2, want=1")}, + {`len(1)`, errors.New("TypeError: object of type 'int' has no len()")}, + {`len("one", "two")`, errors.New("TypeError: len() takes exactly 1 argument (2 given)")}, {`len("∑")`, 1}, {`len([1, 2, 3])`, 3}, {`len([])`, 0}, {`first([1, 2, 3])`, 1}, {`first([])`, nil}, - {`first(1)`, errors.New("argument to `first` must be array, got int")}, + {`first(1)`, errors.New("TypeError: first() expected argument #1 to be `array` got `int`")}, {`last([1, 2, 3])`, 3}, {`last([])`, nil}, - {`last(1)`, errors.New("argument to `last` must be array, got int")}, + {`last(1)`, errors.New("TypeError: last() expected argument #1 to be `array` got `int`")}, {`rest([1, 2, 3])`, []int{2, 3}}, {`rest([])`, nil}, {`push([], 1)`, []int{1}}, - {`push(1, 1)`, errors.New("argument to `push` must be array, got int")}, + {`push(1, 1)`, errors.New("TypeError: push() expected argument #1 to be `array` got `int`")}, {`print("Hello World")`, nil}, {`input()`, ""}, - {`pop([])`, errors.New("cannot pop from an empty array")}, + {`pop([])`, errors.New("IndexError: pop from an empty array")}, {`pop([1])`, 1}, {`bool(1)`, true}, {`bool(0)`, false}, diff --git a/object/array.go b/object/array.go index b47bb37..7616977 100644 --- a/object/array.go +++ b/object/array.go @@ -14,6 +14,36 @@ func (ao *Array) Type() ObjectType { return ARRAY_OBJ } +func (ao *Array) Bool() bool { + return len(ao.Elements) > 0 +} + +func (a *Array) PopLeft() Object { + if len(a.Elements) > 0 { + e := a.Elements[0] + a.Elements = a.Elements[1:] + return e + } + return &Null{} +} + +func (a *Array) PopRight() Object { + if len(a.Elements) > 0 { + e := a.Elements[(len(a.Elements) - 1)] + a.Elements = a.Elements[:(len(a.Elements) - 1)] + return e + } + return &Null{} +} + +func (a *Array) Prepend(obj Object) { + a.Elements = append([]Object{obj}, a.Elements...) +} + +func (a *Array) Append(obj Object) { + a.Elements = append(a.Elements, obj) +} + func (ao *Array) Inspect() string { var out bytes.Buffer @@ -32,9 +62,9 @@ func (ao *Array) String() string { return ao.Inspect() } -func (a *Array) Less(i, j int) bool { - if cmp, ok := a.Elements[i].(Comparable); ok { - return cmp.Compare(a.Elements[j]) == -1 +func (ao *Array) Less(i, j int) bool { + if cmp, ok := ao.Elements[i].(Comparable); ok { + return cmp.Compare(ao.Elements[j]) == -1 } return false } diff --git a/object/bool.go b/object/bool.go index f7565ec..3d7cc4b 100644 --- a/object/bool.go +++ b/object/bool.go @@ -8,6 +8,10 @@ type Boolean struct { Value bool } +func (b *Boolean) Bool() bool { + return b.Value +} + func (b *Boolean) Type() ObjectType { return BOOLEAN_OBJ } diff --git a/object/builtin.go b/object/builtin.go index c443ac0..627fadc 100644 --- a/object/builtin.go +++ b/object/builtin.go @@ -7,12 +7,18 @@ type Builtin struct { Fn BuiltinFunction } +func (b *Builtin) Bool() bool { + return true +} + func (b *Builtin) Type() ObjectType { return BUILTIN_OBJ } + func (b *Builtin) Inspect() string { return fmt.Sprintf("", b.Name) } + func (b *Builtin) String() string { return b.Inspect() } diff --git a/object/closure.go b/object/closure.go index 3375539..2eb3ad3 100644 --- a/object/closure.go +++ b/object/closure.go @@ -11,12 +11,18 @@ type CompiledFunction struct { NumParameters int } +func (cf *CompiledFunction) Bool() bool { + return true +} + func (cf *CompiledFunction) Type() ObjectType { return COMPILED_FUNCTION_OBJ } + func (cf *CompiledFunction) Inspect() string { return fmt.Sprintf("CompiledFunction[%p]", cf) } + func (cf *CompiledFunction) String() string { return cf.Inspect() } @@ -26,12 +32,18 @@ type Closure struct { Free []Object } +func (c *Closure) Bool() bool { + return true +} + func (c *Closure) Type() ObjectType { return CLOSURE_OBJ } + func (c *Closure) Inspect() string { return fmt.Sprintf("Closure[%p]", c) } + func (c *Closure) String() string { return c.Inspect() } diff --git a/object/error.go b/object/error.go index d50e0b1..cb2f2ba 100644 --- a/object/error.go +++ b/object/error.go @@ -4,15 +4,22 @@ type Error struct { Message string } +func (e *Error) Bool() bool { + return false +} + func (e *Error) Type() ObjectType { return ERROR_OBJ } + func (e *Error) Inspect() string { return "Error: " + e.Message } + func (e *Error) Clone() Object { return &Error{Message: e.Message} } + func (e *Error) String() string { return e.Message } diff --git a/object/function.go b/object/function.go index 7677e95..160815a 100644 --- a/object/function.go +++ b/object/function.go @@ -12,9 +12,14 @@ type Function struct { Env *Environment } +func (f *Function) Bool() bool { + return false +} + func (f *Function) Type() ObjectType { return FUNCTION_OBJ } + func (f *Function) Inspect() string { var out bytes.Buffer @@ -32,6 +37,7 @@ func (f *Function) Inspect() string { return out.String() } + func (f *Function) String() string { return f.Inspect() } @@ -40,12 +46,18 @@ type ReturnValue struct { Value Object } +func (rv *ReturnValue) Bool() bool { + return true +} + func (rv *ReturnValue) Type() ObjectType { return RETURN_VALUE_OBJ } + func (rv *ReturnValue) Inspect() string { return rv.Value.Inspect() } + func (rv *ReturnValue) String() string { return rv.Inspect() } diff --git a/object/hash.go b/object/hash.go index d0d50db..07f6a1a 100644 --- a/object/hash.go +++ b/object/hash.go @@ -44,6 +44,14 @@ type Hash struct { Pairs map[HashKey]HashPair } +func (h *Hash) Len() int { + return len(h.Pairs) +} + +func (h *Hash) Bool() bool { + return len(h.Pairs) > 0 +} + func (h *Hash) Type() ObjectType { return HASH_OBJ } @@ -62,6 +70,7 @@ func (h *Hash) Inspect() string { return out.String() } + func (h *Hash) String() string { return h.Inspect() } diff --git a/object/int.go b/object/int.go index ae6d300..3edd8b3 100644 --- a/object/int.go +++ b/object/int.go @@ -6,6 +6,10 @@ type Integer struct { Value int64 } +func (i *Integer) Bool() bool { + return i.Value != 0 +} + func (i *Integer) Type() ObjectType { return INTEGER_OBJ } diff --git a/object/null.go b/object/null.go index 9440e6c..91d8d19 100644 --- a/object/null.go +++ b/object/null.go @@ -2,6 +2,10 @@ package object type Null struct{} +func (n *Null) Bool() bool { + return false +} + func (n *Null) Type() ObjectType { return NULL_OBJ } diff --git a/object/object.go b/object/object.go index 2ee6b74..0f25d5f 100644 --- a/object/object.go +++ b/object/object.go @@ -1,5 +1,7 @@ package object +import "fmt" + // Type represents the type of an object type ObjectType string @@ -25,6 +27,13 @@ type Comparable interface { Compare(other Object) int } +// Sizeable is the interface for returning the size of an Object. +// Object(s) that have a valid size must implement this interface and the +// Len() method. +type Sizeable interface { + Len() int +} + // Immutable is the interface for all immutable objects which must implement // the Clone() method used by binding names to values. type Immutable interface { @@ -34,8 +43,9 @@ type Immutable interface { // Object represents a value and implementations are expected to implement // `Type()` and `Inspect()` functions type Object interface { + fmt.Stringer Type() ObjectType - String() string + Bool() bool Inspect() string } diff --git a/object/str.go b/object/str.go index e3a10e0..2bcb889 100644 --- a/object/str.go +++ b/object/str.go @@ -1,11 +1,22 @@ package object -import "fmt" +import ( + "fmt" + "unicode/utf8" +) type String struct { Value string } +func (s *String) Len() int { + return utf8.RuneCountInString(s.Value) +} + +func (s *String) Bool() bool { + return s.Value != "" +} + func (s *String) Type() ObjectType { return STRING_OBJ } diff --git a/typing/typing.go b/typing/typing.go new file mode 100644 index 0000000..5bd88c7 --- /dev/null +++ b/typing/typing.go @@ -0,0 +1,56 @@ +package typing + +import ( + "fmt" + "monkey/object" +) + +type CheckFunc func(name string, args []object.Object) error + +func Check(name string, args []object.Object, checks ...CheckFunc) error { + for _, check := range checks { + if err := check(name, args); err != nil { + return err + } + } + + return nil +} + +func ExactArgs(n int) CheckFunc { + return func(name string, args []object.Object) error { + if len(args) != n { + return fmt.Errorf("TypeError: %s() takes exactly %d argument (%d given)", name, n, len(args)) + } + return nil + } +} + +func MinimumArgs(n int) CheckFunc { + return func(name string, args []object.Object) error { + if len(args) < n { + return fmt.Errorf("TypeError: %s() takes a minimum %d arguments (%d given)", name, n, len(args)) + } + return nil + } +} + +func RangeOfArgs(n, m int) CheckFunc { + return func(name string, args []object.Object) error { + if len(args) < n || len(args) > m { + return fmt.Errorf("TypeError: %s() takes at least %d arguments at most %d (%d given)", name, n, m, len(args)) + } + return nil + } +} + +func WithTypes(types ...object.ObjectType) CheckFunc { + return func(name string, args []object.Object) error { + for i, t := range types { + if i < len(args) && args[i].Type() != t { + return fmt.Errorf("TypeError: %s() expected argument #%d to be `%s` got `%s`", name, i+1, t, args[i].Type()) + } + } + return nil + } +} diff --git a/vm/vm_test.go b/vm/vm_test.go index fa9b323..c54137b 100644 --- a/vm/vm_test.go +++ b/vm/vm_test.go @@ -709,12 +709,12 @@ func TestBuiltinFunctions(t *testing.T) { { `len(1)`, &object.Error{ - Message: "argument to `len` not supported, got int", + Message: "TypeError: object of type 'int' has no len()", }, }, {`len("one", "two")`, &object.Error{ - Message: "wrong number of arguments. got=2, want=1", + Message: "TypeError: len() takes exactly 1 argument (2 given)", }, }, {`len([1, 2, 3])`, 3}, @@ -724,27 +724,27 @@ func TestBuiltinFunctions(t *testing.T) { {`first([])`, Null}, {`first(1)`, &object.Error{ - Message: "argument to `first` must be array, got int", + Message: "TypeError: first() expected argument #1 to be `array` got `int`", }, }, {`last([1, 2, 3])`, 3}, {`last([])`, Null}, {`last(1)`, &object.Error{ - Message: "argument to `last` must be array, got int", + Message: "TypeError: last() expected argument #1 to be `array` got `int`", }, }, {`rest([1, 2, 3])`, []int{2, 3}}, - {`rest([])`, Null}, + {`rest([])`, []int{}}, {`push([], 1)`, []int{1}}, {`push(1, 1)`, &object.Error{ - Message: "argument to `push` must be array, got int", + Message: "TypeError: push() expected argument #1 to be `array` got `int`", }, }, {`input()`, ""}, {`pop([])`, &object.Error{ - Message: "cannot pop from an empty array", + Message: "IndexError: pop from an empty array", }, }, {`pop([1])`, 1},