diff --git a/README.md b/README.md index 90b6b6c7..f949b77b 100644 --- a/README.md +++ b/README.md @@ -71,7 +71,7 @@ _* See [here](https://github.com/d5/tengobench) for commands/codes used_ - [Builtin Functions](https://github.com/d5/tengo/blob/master/docs/builtins.md) - [Interoperability](https://github.com/d5/tengo/blob/master/docs/interoperability.md) - [Tengo CLI](https://github.com/d5/tengo/blob/master/docs/tengo-cli.md) -- [Standard Library](https://github.com/d5/tengo/blob/master/docs/stdlib.md) _(experimental)_ +- [Standard Library](https://github.com/d5/tengo/blob/master/docs/stdlib.md) ## Roadmap diff --git a/assert/assert.go b/assert/assert.go index bb9cb06d..5b16289c 100644 --- a/assert/assert.go +++ b/assert/assert.go @@ -15,8 +15,6 @@ import ( // NoError asserts err is not an error. func NoError(t *testing.T, err error, msg ...interface{}) bool { - t.Helper() - if err == nil { return true } @@ -26,8 +24,6 @@ func NoError(t *testing.T, err error, msg ...interface{}) bool { // Error asserts err is an error. func Error(t *testing.T, err error, msg ...interface{}) bool { - t.Helper() - if err != nil { return true } @@ -37,8 +33,6 @@ func Error(t *testing.T, err error, msg ...interface{}) bool { // Nil asserts v is nil. func Nil(t *testing.T, v interface{}, msg ...interface{}) bool { - t.Helper() - if v == nil { return true } @@ -48,8 +42,6 @@ func Nil(t *testing.T, v interface{}, msg ...interface{}) bool { // True asserts v is true. func True(t *testing.T, v bool, msg ...interface{}) bool { - t.Helper() - if v { return true } @@ -59,8 +51,6 @@ func True(t *testing.T, v bool, msg ...interface{}) bool { // False asserts vis false. func False(t *testing.T, v bool, msg ...interface{}) bool { - t.Helper() - if !v { return true } @@ -70,8 +60,6 @@ func False(t *testing.T, v bool, msg ...interface{}) bool { // NotNil asserts v is not nil. func NotNil(t *testing.T, v interface{}, msg ...interface{}) bool { - t.Helper() - if v != nil { return true } @@ -81,8 +69,6 @@ func NotNil(t *testing.T, v interface{}, msg ...interface{}) bool { // IsType asserts expected and actual are of the same type. func IsType(t *testing.T, expected, actual interface{}, msg ...interface{}) bool { - t.Helper() - if reflect.TypeOf(expected) == reflect.TypeOf(actual) { return true } @@ -92,15 +78,13 @@ func IsType(t *testing.T, expected, actual interface{}, msg ...interface{}) bool // Equal asserts expected and actual are equal. func Equal(t *testing.T, expected, actual interface{}, msg ...interface{}) bool { - t.Helper() - if expected == nil { return Nil(t, actual, "expected nil, but got not nil") } if !NotNil(t, actual, "expected not nil, but got nil") { return false } - if !IsType(t, expected, actual) { + if !IsType(t, expected, actual, msg...) { return false } @@ -123,7 +107,7 @@ func Equal(t *testing.T, expected, actual interface{}, msg ...interface{}) bool } case []byte: if bytes.Compare(expected, actual.([]byte)) != 0 { - return failExpectedActual(t, expected, actual, msg...) + return failExpectedActual(t, string(expected), string(actual.([]byte)), msg...) } case []int: if !equalIntSlice(expected, actual.([]int)) { @@ -150,39 +134,47 @@ func Equal(t *testing.T, expected, actual interface{}, msg ...interface{}) bool return failExpectedActual(t, expected, actual, msg...) } case []objects.Object: - return equalObjectSlice(t, expected, actual.([]objects.Object)) + return equalObjectSlice(t, expected, actual.([]objects.Object), msg...) case *objects.Int: - return Equal(t, expected.Value, actual.(*objects.Int).Value) + return Equal(t, expected.Value, actual.(*objects.Int).Value, msg...) case *objects.Float: - return Equal(t, expected.Value, actual.(*objects.Float).Value) + return Equal(t, expected.Value, actual.(*objects.Float).Value, msg...) case *objects.String: - return Equal(t, expected.Value, actual.(*objects.String).Value) + return Equal(t, expected.Value, actual.(*objects.String).Value, msg...) case *objects.Char: - return Equal(t, expected.Value, actual.(*objects.Char).Value) + return Equal(t, expected.Value, actual.(*objects.Char).Value, msg...) case *objects.Bool: - return Equal(t, expected.Value, actual.(*objects.Bool).Value) + if expected != actual { + return failExpectedActual(t, expected, actual, msg...) + } case *objects.ReturnValue: - return Equal(t, expected.Value, actual.(objects.ReturnValue).Value) + return Equal(t, expected.Value, actual.(objects.ReturnValue).Value, msg...) case *objects.Array: - return equalObjectSlice(t, expected.Value, actual.(*objects.Array).Value) + return equalObjectSlice(t, expected.Value, actual.(*objects.Array).Value, msg...) case *objects.ImmutableArray: - return equalObjectSlice(t, expected.Value, actual.(*objects.ImmutableArray).Value) + return equalObjectSlice(t, expected.Value, actual.(*objects.ImmutableArray).Value, msg...) case *objects.Bytes: if bytes.Compare(expected.Value, actual.(*objects.Bytes).Value) != 0 { - return failExpectedActual(t, expected.Value, actual.(*objects.Bytes).Value, msg...) + return failExpectedActual(t, string(expected.Value), string(actual.(*objects.Bytes).Value), msg...) } case *objects.Map: - return equalObjectMap(t, expected.Value, actual.(*objects.Map).Value) + return equalObjectMap(t, expected.Value, actual.(*objects.Map).Value, msg...) case *objects.ImmutableMap: - return equalObjectMap(t, expected.Value, actual.(*objects.ImmutableMap).Value) + return equalObjectMap(t, expected.Value, actual.(*objects.ImmutableMap).Value, msg...) case *objects.CompiledFunction: - return equalCompiledFunction(t, expected, actual.(*objects.CompiledFunction)) + return equalCompiledFunction(t, expected, actual.(*objects.CompiledFunction), msg...) case *objects.Closure: - return equalClosure(t, expected, actual.(*objects.Closure)) + return equalClosure(t, expected, actual.(*objects.Closure), msg...) case *objects.Undefined: - return true + if expected != actual { + return failExpectedActual(t, expected, actual, msg...) + } case *objects.Error: - return Equal(t, expected.Value, actual.(*objects.Error).Value) + return Equal(t, expected.Value, actual.(*objects.Error).Value, msg...) + case objects.Object: + if !expected.Equals(actual.(objects.Object)) { + return failExpectedActual(t, expected, actual, msg...) + } case error: if expected != actual.(error) { return failExpectedActual(t, expected, actual, msg...) @@ -196,8 +188,6 @@ func Equal(t *testing.T, expected, actual interface{}, msg ...interface{}) bool // Fail marks the function as having failed but continues execution. func Fail(t *testing.T, msg ...interface{}) bool { - t.Helper() - t.Logf("\nError trace:\n\t%s\n%s", strings.Join(errorTrace(), "\n\t"), message(msg...)) t.Fail() @@ -206,8 +196,6 @@ func Fail(t *testing.T, msg ...interface{}) bool { } func failExpectedActual(t *testing.T, expected, actual interface{}, msg ...interface{}) bool { - t.Helper() - var addMsg string if len(msg) > 0 { addMsg = "\nMessage: " + message(msg...) @@ -256,15 +244,15 @@ func equalSymbol(a, b compiler.Symbol) bool { a.Scope == b.Scope } -func equalObjectSlice(t *testing.T, expected, actual []objects.Object) bool { +func equalObjectSlice(t *testing.T, expected, actual []objects.Object, msg ...interface{}) bool { // TODO: this test does not differentiate nil vs empty slice - if !Equal(t, len(expected), len(actual)) { + if !Equal(t, len(expected), len(actual), msg...) { return false } for i := 0; i < len(expected); i++ { - if !Equal(t, expected[i], actual[i]) { + if !Equal(t, expected[i], actual[i], msg...) { return false } } @@ -272,15 +260,15 @@ func equalObjectSlice(t *testing.T, expected, actual []objects.Object) bool { return true } -func equalObjectMap(t *testing.T, expected, actual map[string]objects.Object) bool { - if !Equal(t, len(expected), len(actual)) { +func equalObjectMap(t *testing.T, expected, actual map[string]objects.Object, msg ...interface{}) bool { + if !Equal(t, len(expected), len(actual), msg...) { return false } for key, expectedVal := range expected { actualVal := actual[key] - if !Equal(t, expectedVal, actualVal) { + if !Equal(t, expectedVal, actualVal, msg...) { return false } } @@ -288,27 +276,27 @@ func equalObjectMap(t *testing.T, expected, actual map[string]objects.Object) bo return true } -func equalCompiledFunction(t *testing.T, expected, actual objects.Object) bool { +func equalCompiledFunction(t *testing.T, expected, actual objects.Object, msg ...interface{}) bool { expectedT := expected.(*objects.CompiledFunction) actualT := actual.(*objects.CompiledFunction) - return Equal(t, expectedT.Instructions, actualT.Instructions) + return Equal(t, expectedT.Instructions, actualT.Instructions, msg...) } -func equalClosure(t *testing.T, expected, actual objects.Object) bool { +func equalClosure(t *testing.T, expected, actual objects.Object, msg ...interface{}) bool { expectedT := expected.(*objects.Closure) actualT := actual.(*objects.Closure) - if !Equal(t, expectedT.Fn, actualT.Fn) { + if !Equal(t, expectedT.Fn, actualT.Fn, msg...) { return false } - if !Equal(t, len(expectedT.Free), len(actualT.Free)) { + if !Equal(t, len(expectedT.Free), len(actualT.Free), msg...) { return false } for i := 0; i < len(expectedT.Free); i++ { - if !Equal(t, *expectedT.Free[i], *actualT.Free[i]) { + if !Equal(t, *expectedT.Free[i], *actualT.Free[i], msg...) { return false } } diff --git a/compiler/bytecode.go b/compiler/bytecode.go index 6e87585c..df0d2f22 100644 --- a/compiler/bytecode.go +++ b/compiler/bytecode.go @@ -21,7 +21,16 @@ func (b *Bytecode) Decode(r io.Reader) error { return err } - return dec.Decode(&b.Constants) + if err := dec.Decode(&b.Constants); err != nil { + return err + } + + // replace Bool and Undefined with known value + for i, v := range b.Constants { + b.Constants[i] = cleanupObjects(v) + } + + return nil } // Encode writes Bytecode data to the writer. @@ -32,9 +41,32 @@ func (b *Bytecode) Encode(w io.Writer) error { return err } + // constants return enc.Encode(b.Constants) } +func cleanupObjects(o objects.Object) objects.Object { + switch o := o.(type) { + case *objects.Bool: + if o.IsFalsy() { + return objects.FalseValue + } + return objects.TrueValue + case *objects.Undefined: + return objects.UndefinedValue + case *objects.Array: + for i, v := range o.Value { + o.Value[i] = cleanupObjects(v) + } + case *objects.Map: + for k, v := range o.Value { + o.Value[k] = cleanupObjects(v) + } + } + + return o +} + func init() { gob.Register(&objects.Int{}) gob.Register(&objects.Float{}) @@ -52,4 +84,5 @@ func init() { gob.Register(&objects.StringIterator{}) gob.Register(&objects.MapIterator{}) gob.Register(&objects.ArrayIterator{}) + gob.Register(&objects.Time{}) } diff --git a/compiler/bytecode_test.go b/compiler/bytecode_test.go index b899f2c7..86ac2acc 100644 --- a/compiler/bytecode_test.go +++ b/compiler/bytecode_test.go @@ -3,6 +3,7 @@ package compiler_test import ( "bytes" "testing" + "time" "github.com/d5/tengo/assert" "github.com/d5/tengo/compiler" @@ -14,16 +15,20 @@ func TestBytecode(t *testing.T) { testBytecodeSerialization(t, bytecode( concat(), objectsArray( + objects.UndefinedValue, + &objects.Time{Value: time.Now()}, &objects.Array{ Value: objectsArray( &objects.Int{Value: 12}, &objects.String{Value: "foo"}, - &objects.Bool{Value: true}, + objects.TrueValue, + objects.FalseValue, &objects.Float{Value: 93.11}, &objects.Char{Value: 'x'}, + objects.UndefinedValue, ), }, - &objects.Bool{Value: false}, + objects.FalseValue, &objects.Char{Value: 'y'}, &objects.Float{Value: 93.11}, compiledFunction(1, 0, @@ -36,11 +41,12 @@ func TestBytecode(t *testing.T) { &objects.Map{ Value: map[string]objects.Object{ "a": &objects.Float{Value: -93.1}, - "b": &objects.Bool{Value: false}, + "b": objects.FalseValue, + "c": objects.UndefinedValue, }, }, &objects.String{Value: "bar"}, - &objects.Undefined{}))) + objects.UndefinedValue))) testBytecodeSerialization(t, bytecode( concat( diff --git a/compiler/stdlib/func_typedefs.go b/compiler/stdlib/func_typedefs.go index ab078454..78fffaeb 100644 --- a/compiler/stdlib/func_typedefs.go +++ b/compiler/stdlib/func_typedefs.go @@ -43,7 +43,11 @@ func FuncARB(fn func() bool) *objects.UserFunction { return nil, objects.ErrWrongNumArguments } - return &objects.Bool{Value: fn()}, nil + if fn() { + return objects.TrueValue, nil + } + + return objects.FalseValue, nil }, } } @@ -340,7 +344,11 @@ func FuncAFIRB(fn func(float64, int) bool) *objects.UserFunction { return nil, objects.ErrInvalidTypeConversion } - return &objects.Bool{Value: fn(f1, i2)}, nil + if fn(f1, i2) { + return objects.TrueValue, nil + } + + return objects.FalseValue, nil }, } } @@ -359,7 +367,11 @@ func FuncAFRB(fn func(float64) bool) *objects.UserFunction { return nil, objects.ErrInvalidTypeConversion } - return &objects.Bool{Value: fn(f1)}, nil + if fn(f1) { + return objects.TrueValue, nil + } + + return objects.FalseValue, nil }, } } @@ -383,6 +395,31 @@ func FuncASRS(fn func(string) string) *objects.UserFunction { } } +// FuncASRSs transform a function of 'func(string) []string' signature into a user function object. +func FuncASRSs(fn func(string) []string) *objects.UserFunction { + return &objects.UserFunction{ + Value: func(args ...objects.Object) (objects.Object, error) { + if len(args) != 1 { + return nil, objects.ErrWrongNumArguments + } + + s1, ok := objects.ToString(args[0]) + if !ok { + return nil, objects.ErrInvalidTypeConversion + } + + res := fn(s1) + + arr := &objects.Array{} + for _, osArg := range res { + arr.Value = append(arr.Value, &objects.String{Value: osArg}) + } + + return arr, nil + }, + } +} + // FuncASRSE transform a function of 'func(string) (string, error)' signature into a user function object. // User function will return 'true' if underlying native function returns nil. func FuncASRSE(fn func(string) (string, error)) *objects.UserFunction { @@ -450,6 +487,171 @@ func FuncASSRE(fn func(string, string) error) *objects.UserFunction { } } +// FuncASSRSs transform a function of 'func(string, string) []string' signature into a user function object. +func FuncASSRSs(fn func(string, string) []string) *objects.UserFunction { + return &objects.UserFunction{ + Value: func(args ...objects.Object) (objects.Object, error) { + if len(args) != 2 { + return nil, objects.ErrWrongNumArguments + } + + s1, ok := objects.ToString(args[0]) + if !ok { + return nil, objects.ErrInvalidTypeConversion + } + + s2, ok := objects.ToString(args[1]) + if !ok { + return nil, objects.ErrInvalidTypeConversion + } + + arr := &objects.Array{} + for _, res := range fn(s1, s2) { + arr.Value = append(arr.Value, &objects.String{Value: res}) + } + + return arr, nil + }, + } +} + +// FuncASSIRSs transform a function of 'func(string, string, int) []string' signature into a user function object. +func FuncASSIRSs(fn func(string, string, int) []string) *objects.UserFunction { + return &objects.UserFunction{ + Value: func(args ...objects.Object) (objects.Object, error) { + if len(args) != 3 { + return nil, objects.ErrWrongNumArguments + } + + s1, ok := objects.ToString(args[0]) + if !ok { + return nil, objects.ErrInvalidTypeConversion + } + + s2, ok := objects.ToString(args[1]) + if !ok { + return nil, objects.ErrInvalidTypeConversion + } + + i3, ok := objects.ToInt(args[2]) + if !ok { + return nil, objects.ErrInvalidTypeConversion + } + + arr := &objects.Array{} + for _, res := range fn(s1, s2, i3) { + arr.Value = append(arr.Value, &objects.String{Value: res}) + } + + return arr, nil + }, + } +} + +// FuncASSRI transform a function of 'func(string, string) int' signature into a user function object. +func FuncASSRI(fn func(string, string) int) *objects.UserFunction { + return &objects.UserFunction{ + Value: func(args ...objects.Object) (objects.Object, error) { + if len(args) != 2 { + return nil, objects.ErrWrongNumArguments + } + + s1, ok := objects.ToString(args[0]) + if !ok { + return nil, objects.ErrInvalidTypeConversion + } + + s2, ok := objects.ToString(args[1]) + if !ok { + return nil, objects.ErrInvalidTypeConversion + } + + return &objects.Int{Value: int64(fn(s1, s2))}, nil + }, + } +} + +// FuncASSRS transform a function of 'func(string, string) string' signature into a user function object. +func FuncASSRS(fn func(string, string) string) *objects.UserFunction { + return &objects.UserFunction{ + Value: func(args ...objects.Object) (objects.Object, error) { + if len(args) != 2 { + return nil, objects.ErrWrongNumArguments + } + + s1, ok := objects.ToString(args[0]) + if !ok { + return nil, objects.ErrInvalidTypeConversion + } + + s2, ok := objects.ToString(args[1]) + if !ok { + return nil, objects.ErrInvalidTypeConversion + } + + return &objects.String{Value: fn(s1, s2)}, nil + }, + } +} + +// FuncASSRB transform a function of 'func(string, string) bool' signature into a user function object. +func FuncASSRB(fn func(string, string) bool) *objects.UserFunction { + return &objects.UserFunction{ + Value: func(args ...objects.Object) (objects.Object, error) { + if len(args) != 2 { + return nil, objects.ErrWrongNumArguments + } + + s1, ok := objects.ToString(args[0]) + if !ok { + return nil, objects.ErrInvalidTypeConversion + } + + s2, ok := objects.ToString(args[1]) + if !ok { + return nil, objects.ErrInvalidTypeConversion + } + + if fn(s1, s2) { + return objects.TrueValue, nil + } + + return objects.FalseValue, nil + }, + } +} + +// FuncASsSRS transform a function of 'func([]string, string) string' signature into a user function object. +func FuncASsSRS(fn func([]string, string) string) *objects.UserFunction { + return &objects.UserFunction{ + Value: func(args ...objects.Object) (objects.Object, error) { + if len(args) != 2 { + return nil, objects.ErrWrongNumArguments + } + + var ss1 []string + arr, ok := args[0].(*objects.Array) + if !ok { + return nil, objects.ErrInvalidTypeConversion + } + for _, a := range arr.Value { + as, ok := objects.ToString(a) + if !ok { + return nil, objects.ErrInvalidTypeConversion + } + ss1 = append(ss1, as) + } + + s2, ok := objects.ToString(args[1]) + if !ok { + return nil, objects.ErrInvalidTypeConversion + } + + return &objects.String{Value: fn(ss1, s2)}, nil + }, + } +} + // FuncASI64RE transform a function of 'func(string, int64) error' signature // into a user function object. func FuncASI64RE(fn func(string, int64) error) *objects.UserFunction { @@ -498,6 +700,30 @@ func FuncAIIRE(fn func(int, int) error) *objects.UserFunction { } } +// FuncASIRS transform a function of 'func(string, int) string' signature +// into a user function object. +func FuncASIRS(fn func(string, int) string) *objects.UserFunction { + return &objects.UserFunction{ + Value: func(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 2 { + return nil, objects.ErrWrongNumArguments + } + + s1, ok := objects.ToString(args[0]) + if !ok { + return nil, objects.ErrInvalidTypeConversion + } + + i2, ok := objects.ToInt(args[1]) + if !ok { + return nil, objects.ErrInvalidTypeConversion + } + + return &objects.String{Value: fn(s1, i2)}, nil + }, + } +} + // FuncASIIRE transform a function of 'func(string, int, int) error' signature // into a user function object. func FuncASIIRE(fn func(string, int, int) error) *objects.UserFunction { @@ -595,11 +821,30 @@ func FuncAIRSsE(fn func(int) ([]string, error)) *objects.UserFunction { } arr := &objects.Array{} - for _, osArg := range res { - arr.Value = append(arr.Value, &objects.String{Value: osArg}) + for _, r := range res { + arr.Value = append(arr.Value, &objects.String{Value: r}) } return arr, nil }, } } + +// FuncAIRS transform a function of 'func(int) string' signature +// into a user function object. +func FuncAIRS(fn func(int) string) *objects.UserFunction { + return &objects.UserFunction{ + Value: func(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 1 { + return nil, objects.ErrWrongNumArguments + } + + i1, ok := objects.ToInt(args[0]) + if !ok { + return nil, objects.ErrInvalidTypeConversion + } + + return &objects.String{Value: fn(i1)}, nil + }, + } +} diff --git a/compiler/stdlib/func_typedefs_test.go b/compiler/stdlib/func_typedefs_test.go index 610e132e..aca7bfe3 100644 --- a/compiler/stdlib/func_typedefs_test.go +++ b/compiler/stdlib/func_typedefs_test.go @@ -2,6 +2,8 @@ package stdlib_test import ( "errors" + "strconv" + "strings" "testing" "github.com/d5/tengo/assert" @@ -13,7 +15,7 @@ func TestFuncAIR(t *testing.T) { uf := stdlib.FuncAIR(func(int) {}) ret, err := uf.Call(&objects.Int{Value: 10}) assert.NoError(t, err) - assert.Equal(t, &objects.Undefined{}, ret) + assert.Equal(t, objects.UndefinedValue, ret) ret, err = uf.Call() assert.Equal(t, objects.ErrWrongNumArguments, err) } @@ -22,7 +24,7 @@ func TestFuncAR(t *testing.T) { uf := stdlib.FuncAR(func() {}) ret, err := uf.Call() assert.NoError(t, err) - assert.Equal(t, &objects.Undefined{}, ret) + assert.Equal(t, objects.UndefinedValue, ret) ret, err = uf.Call(objects.TrueValue) assert.Equal(t, objects.ErrWrongNumArguments, err) } @@ -115,6 +117,15 @@ func TestFuncASRS(t *testing.T) { assert.Equal(t, objects.ErrWrongNumArguments, err) } +func TestFuncASRSs(t *testing.T) { + uf := stdlib.FuncASRSs(func(a string) []string { return []string{a} }) + ret, err := uf.Call(&objects.String{Value: "foo"}) + assert.NoError(t, err) + assert.Equal(t, array(&objects.String{Value: "foo"}), ret) + ret, err = uf.Call() + assert.Equal(t, objects.ErrWrongNumArguments, err) +} + func TestFuncASI64RE(t *testing.T) { uf := stdlib.FuncASI64RE(func(a string, b int64) error { return nil }) ret, err := uf.Call(&objects.String{Value: "foo"}, &objects.Int{Value: 5}) @@ -168,7 +179,24 @@ func TestFuncASRSE(t *testing.T) { } func TestFuncASSRE(t *testing.T) { + uf := stdlib.FuncASSRE(func(a, b string) error { return nil }) + ret, err := uf.Call(&objects.String{Value: "foo"}, &objects.String{Value: "bar"}) + assert.NoError(t, err) + uf = stdlib.FuncASSRE(func(a, b string) error { return errors.New("some error") }) + ret, err = uf.Call(&objects.String{Value: "foo"}, &objects.String{Value: "bar"}) + assert.NoError(t, err) + assert.Equal(t, &objects.Error{Value: &objects.String{Value: "some error"}}, ret) + ret, err = uf.Call(&objects.String{Value: "foo"}) + assert.Equal(t, objects.ErrWrongNumArguments, err) +} +func TestFuncASsRS(t *testing.T) { + uf := stdlib.FuncASsSRS(func(a []string, b string) string { return strings.Join(a, b) }) + ret, err := uf.Call(array(&objects.String{Value: "foo"}, &objects.String{Value: "bar"}), &objects.String{Value: " "}) + assert.NoError(t, err) + assert.Equal(t, &objects.String{Value: "foo bar"}, ret) + ret, err = uf.Call(&objects.String{Value: "foo"}) + assert.Equal(t, objects.ErrWrongNumArguments, err) } func TestFuncARF(t *testing.T) { @@ -227,7 +255,7 @@ func TestFuncAFRB(t *testing.T) { }) ret, err := uf.Call(&objects.Float{Value: 0.1}) assert.NoError(t, err) - assert.Equal(t, &objects.Bool{Value: true}, ret) + assert.Equal(t, objects.TrueValue, ret) ret, err = uf.Call() assert.Equal(t, objects.ErrWrongNumArguments, err) ret, err = uf.Call(objects.TrueValue, objects.TrueValue) @@ -247,6 +275,17 @@ func TestFuncAFFRF(t *testing.T) { assert.Equal(t, objects.ErrWrongNumArguments, err) } +func TestFuncASIRS(t *testing.T) { + uf := stdlib.FuncASIRS(func(a string, b int) string { return strings.Repeat(a, b) }) + ret, err := uf.Call(&objects.String{Value: "ab"}, &objects.Int{Value: 2}) + assert.NoError(t, err) + assert.Equal(t, &objects.String{Value: "abab"}, ret) + ret, err = uf.Call() + assert.Equal(t, objects.ErrWrongNumArguments, err) + ret, err = uf.Call(objects.TrueValue) + assert.Equal(t, objects.ErrWrongNumArguments, err) +} + func TestFuncAIFRF(t *testing.T) { uf := stdlib.FuncAIFRF(func(a int, b float64) float64 { return float64(a) + b @@ -279,7 +318,7 @@ func TestFuncAFIRB(t *testing.T) { }) ret, err := uf.Call(&objects.Float{Value: 10.0}, &objects.Int{Value: 20}) assert.NoError(t, err) - assert.Equal(t, &objects.Bool{Value: true}, ret) + assert.Equal(t, objects.TrueValue, ret) ret, err = uf.Call() assert.Equal(t, objects.ErrWrongNumArguments, err) ret, err = uf.Call(objects.TrueValue) @@ -303,6 +342,24 @@ func TestFuncAIRSsE(t *testing.T) { assert.Equal(t, objects.ErrWrongNumArguments, err) } +func TestFuncASSRSs(t *testing.T) { + uf := stdlib.FuncASSRSs(func(a, b string) []string { return []string{a, b} }) + ret, err := uf.Call(&objects.String{Value: "foo"}, &objects.String{Value: "bar"}) + assert.NoError(t, err) + assert.Equal(t, array(&objects.String{Value: "foo"}, &objects.String{Value: "bar"}), ret) + ret, err = uf.Call() + assert.Equal(t, objects.ErrWrongNumArguments, err) +} + +func TestFuncASSIRSs(t *testing.T) { + uf := stdlib.FuncASSIRSs(func(a, b string, c int) []string { return []string{a, b, strconv.Itoa(c)} }) + ret, err := uf.Call(&objects.String{Value: "foo"}, &objects.String{Value: "bar"}, &objects.Int{Value: 5}) + assert.NoError(t, err) + assert.Equal(t, array(&objects.String{Value: "foo"}, &objects.String{Value: "bar"}, &objects.String{Value: "5"}), ret) + ret, err = uf.Call() + assert.Equal(t, objects.ErrWrongNumArguments, err) +} + func TestFuncARB(t *testing.T) { uf := stdlib.FuncARB(func() bool { return true }) ret, err := uf.Call() @@ -355,6 +412,42 @@ func TestFuncAYRIE(t *testing.T) { assert.Equal(t, objects.ErrWrongNumArguments, err) } +func TestFuncASSRI(t *testing.T) { + uf := stdlib.FuncASSRI(func(a, b string) int { return len(a) + len(b) }) + ret, err := uf.Call(&objects.String{Value: "foo"}, &objects.String{Value: "bar"}) + assert.NoError(t, err) + assert.Equal(t, &objects.Int{Value: 6}, ret) + ret, err = uf.Call(&objects.String{Value: "foo"}) + assert.Equal(t, objects.ErrWrongNumArguments, err) +} + +func TestFuncASSRS(t *testing.T) { + uf := stdlib.FuncASSRS(func(a, b string) string { return a + b }) + ret, err := uf.Call(&objects.String{Value: "foo"}, &objects.String{Value: "bar"}) + assert.NoError(t, err) + assert.Equal(t, &objects.String{Value: "foobar"}, ret) + ret, err = uf.Call(&objects.String{Value: "foo"}) + assert.Equal(t, objects.ErrWrongNumArguments, err) +} + +func TestFuncASSRB(t *testing.T) { + uf := stdlib.FuncASSRB(func(a, b string) bool { return len(a) > len(b) }) + ret, err := uf.Call(&objects.String{Value: "123"}, &objects.String{Value: "12"}) + assert.NoError(t, err) + assert.Equal(t, objects.TrueValue, ret) + ret, err = uf.Call(&objects.String{Value: "foo"}) + assert.Equal(t, objects.ErrWrongNumArguments, err) +} + +func TestFuncAIRS(t *testing.T) { + uf := stdlib.FuncAIRS(func(a int) string { return strconv.Itoa(a) }) + ret, err := uf.Call(&objects.Int{Value: 55}) + assert.NoError(t, err) + assert.Equal(t, &objects.String{Value: "55"}, ret) + ret, err = uf.Call() + assert.Equal(t, objects.ErrWrongNumArguments, err) +} + func array(elements ...objects.Object) *objects.Array { return &objects.Array{Value: elements} } diff --git a/compiler/stdlib/math.go b/compiler/stdlib/math.go index c92dce2d..5c519d72 100644 --- a/compiler/stdlib/math.go +++ b/compiler/stdlib/math.go @@ -67,14 +67,8 @@ var mathModule = map[string]objects.Object{ "sqrt": FuncAFRF(math.Sqrt), "tan": FuncAFRF(math.Tan), "tanh": FuncAFRF(math.Tanh), - "runct": FuncAFRF(math.Trunc), + "trunc": FuncAFRF(math.Trunc), "y0": FuncAFRF(math.Y0), "y1": FuncAFRF(math.Y1), "yn": FuncAIFRF(math.Yn), - // TODO: functions that have multiple returns - // Should these be tuple assignment? Or Map return? - //"frexp": nil, - //"lgamma": nil, - //"modf": nil, - //"sincos": nil, } diff --git a/compiler/stdlib/os.go b/compiler/stdlib/os.go index 21ad6894..d032b506 100644 --- a/compiler/stdlib/os.go +++ b/compiler/stdlib/os.go @@ -3,6 +3,7 @@ package stdlib import ( "io" "os" + "os/exec" "github.com/d5/tengo/objects" ) @@ -36,82 +37,110 @@ var osModule = map[string]objects.Object{ "seek_set": &objects.Int{Value: int64(io.SeekStart)}, "seek_cur": &objects.Int{Value: int64(io.SeekCurrent)}, "seek_end": &objects.Int{Value: int64(io.SeekEnd)}, - // args() => array(string) - "args": &objects.UserFunction{Value: osArgs}, - // chdir(dir string) => error - "chdir": FuncASRE(os.Chdir), - // chmod(name string, mode int) => error - "chmod": osFuncASFmRE(os.Chmod), - // chown(name string, uid int, gid int) => error - "chown": FuncASIIRE(os.Chown), - // clearenv() - "clearenv": FuncAR(os.Clearenv), - // environ() => array(string) - "environ": FuncARSs(os.Environ), - // exit(code int) - "exit": FuncAIR(os.Exit), - // expand_env(s string) => string - "expand_env": FuncASRS(os.ExpandEnv), - // getegid() => int - "getegid": FuncARI(os.Getegid), - // getenv(s string) => string - "getenv": FuncASRS(os.Getenv), - // geteuid() => int - "geteuid": FuncARI(os.Geteuid), - // getgid() => int - "getgid": FuncARI(os.Getgid), - // getgroups() => array(string)/error - "getgroups": FuncARIsE(os.Getgroups), - // getpagesize() => int - "getpagesize": FuncARI(os.Getpagesize), - // getpid() => int - "getpid": FuncARI(os.Getpid), - // getppid() => int - "getppid": FuncARI(os.Getppid), - // getuid() => int - "getuid": FuncARI(os.Getuid), - // getwd() => string/error - "getwd": FuncARSE(os.Getwd), - // hostname() => string/error - "hostname": FuncARSE(os.Hostname), - // lchown(name string, uid int, gid int) => error - "lchown": FuncASIIRE(os.Lchown), - // link(oldname string, newname string) => error - "link": FuncASSRE(os.Link), - // lookup_env(key string) => string/false - "lookup_env": &objects.UserFunction{Value: osLookupEnv}, - // mkdir(name string, perm int) => error - "mkdir": osFuncASFmRE(os.Mkdir), - // mkdir_all(name string, perm int) => error - "mkdir_all": osFuncASFmRE(os.MkdirAll), - // readlink(name string) => string/error - "readlink": FuncASRSE(os.Readlink), - // remove(name string) => error - "remove": FuncASRE(os.Remove), - // remove_all(name string) => error - "remove_all": FuncASRE(os.RemoveAll), - // rename(oldpath string, newpath string) => error - "rename": FuncASSRE(os.Rename), - // setenv(key string, value string) => error - "setenv": FuncASSRE(os.Setenv), - // symlink(oldname string newname string) => error - "symlink": FuncASSRE(os.Symlink), - // temp_dir() => string - "temp_dir": FuncARS(os.TempDir), - // truncate(name string, size int) => error - "truncate": FuncASI64RE(os.Truncate), - // unsetenv(key string) => error - "unsetenv": FuncASRE(os.Unsetenv), - // create(name string) => imap(file)/error - "create": &objects.UserFunction{Value: osCreate}, - // open(name string) => imap(file)/error - "open": &objects.UserFunction{Value: osOpen}, - // open_file(name string, flag int, perm int) => imap(file)/error - "open_file": &objects.UserFunction{Value: osOpenFile}, - // find_process(pid int) => imap(process)/error - "find_process": &objects.UserFunction{Value: osFindProcess}, - // start_process(name string, argv array(string), dir string, env array(string)) => imap(process)/error - "start_process": &objects.UserFunction{Value: osStartProcess}, + "args": &objects.UserFunction{Value: osArgs}, // args() => array(string) + "chdir": FuncASRE(os.Chdir), // chdir(dir string) => error + "chmod": osFuncASFmRE(os.Chmod), // chmod(name string, mode int) => error + "chown": FuncASIIRE(os.Chown), // chown(name string, uid int, gid int) => error + "clearenv": FuncAR(os.Clearenv), // clearenv() + "environ": FuncARSs(os.Environ), // environ() => array(string) + "exit": FuncAIR(os.Exit), // exit(code int) + "expand_env": FuncASRS(os.ExpandEnv), // expand_env(s string) => string + "getegid": FuncARI(os.Getegid), // getegid() => int + "getenv": FuncASRS(os.Getenv), // getenv(s string) => string + "geteuid": FuncARI(os.Geteuid), // geteuid() => int + "getgid": FuncARI(os.Getgid), // getgid() => int + "getgroups": FuncARIsE(os.Getgroups), // getgroups() => array(string)/error + "getpagesize": FuncARI(os.Getpagesize), // getpagesize() => int + "getpid": FuncARI(os.Getpid), // getpid() => int + "getppid": FuncARI(os.Getppid), // getppid() => int + "getuid": FuncARI(os.Getuid), // getuid() => int + "getwd": FuncARSE(os.Getwd), // getwd() => string/error + "hostname": FuncARSE(os.Hostname), // hostname() => string/error + "lchown": FuncASIIRE(os.Lchown), // lchown(name string, uid int, gid int) => error + "link": FuncASSRE(os.Link), // link(oldname string, newname string) => error + "lookup_env": &objects.UserFunction{Value: osLookupEnv}, // lookup_env(key string) => string/false + "mkdir": osFuncASFmRE(os.Mkdir), // mkdir(name string, perm int) => error + "mkdir_all": osFuncASFmRE(os.MkdirAll), // mkdir_all(name string, perm int) => error + "readlink": FuncASRSE(os.Readlink), // readlink(name string) => string/error + "remove": FuncASRE(os.Remove), // remove(name string) => error + "remove_all": FuncASRE(os.RemoveAll), // remove_all(name string) => error + "rename": FuncASSRE(os.Rename), // rename(oldpath string, newpath string) => error + "setenv": FuncASSRE(os.Setenv), // setenv(key string, value string) => error + "symlink": FuncASSRE(os.Symlink), // symlink(oldname string newname string) => error + "temp_dir": FuncARS(os.TempDir), // temp_dir() => string + "truncate": FuncASI64RE(os.Truncate), // truncate(name string, size int) => error + "unsetenv": FuncASRE(os.Unsetenv), // unsetenv(key string) => error + "create": &objects.UserFunction{Value: osCreate}, // create(name string) => imap(file)/error + "open": &objects.UserFunction{Value: osOpen}, // open(name string) => imap(file)/error + "open_file": &objects.UserFunction{Value: osOpenFile}, // open_file(name string, flag int, perm int) => imap(file)/error + "find_process": &objects.UserFunction{Value: osFindProcess}, // find_process(pid int) => imap(process)/error + "start_process": &objects.UserFunction{Value: osStartProcess}, // start_process(name string, argv array(string), dir string, env array(string)) => imap(process)/error + "exec_look_path": FuncASRSE(exec.LookPath), // exec_look_path(file) => string/error + "exec": &objects.UserFunction{Value: osExec}, // exec(name, args...) => command +} + +func osCreate(args ...objects.Object) (objects.Object, error) { + if len(args) != 1 { + return nil, objects.ErrWrongNumArguments + } + + s1, ok := objects.ToString(args[0]) + if !ok { + return nil, objects.ErrInvalidTypeConversion + } + + res, err := os.Create(s1) + if err != nil { + return wrapError(err), nil + } + + return makeOSFile(res), nil +} + +func osOpen(args ...objects.Object) (objects.Object, error) { + if len(args) != 1 { + return nil, objects.ErrWrongNumArguments + } + + s1, ok := objects.ToString(args[0]) + if !ok { + return nil, objects.ErrInvalidTypeConversion + } + + res, err := os.Open(s1) + if err != nil { + return wrapError(err), nil + } + + return makeOSFile(res), nil +} + +func osOpenFile(args ...objects.Object) (objects.Object, error) { + if len(args) != 3 { + return nil, objects.ErrWrongNumArguments + } + + s1, ok := objects.ToString(args[0]) + if !ok { + return nil, objects.ErrInvalidTypeConversion + } + + i2, ok := objects.ToInt(args[1]) + if !ok { + return nil, objects.ErrInvalidTypeConversion + } + + i3, ok := objects.ToInt(args[2]) + if !ok { + return nil, objects.ErrInvalidTypeConversion + } + + res, err := os.OpenFile(s1, i2, os.FileMode(i3)) + if err != nil { + return wrapError(err), nil + } + + return makeOSFile(res), nil } func osArgs(args ...objects.Object) (objects.Object, error) { @@ -148,33 +177,116 @@ func osFuncASFmRE(fn func(string, os.FileMode) error) *objects.UserFunction { } } -func osExecutable(args ...objects.Object) (objects.Object, error) { - if len(args) != 0 { +func osLookupEnv(args ...objects.Object) (objects.Object, error) { + if len(args) != 1 { return nil, objects.ErrWrongNumArguments } - res, err := os.Executable() - if err != nil { - return wrapError(err), nil + s1, ok := objects.ToString(args[0]) + if !ok { + return nil, objects.ErrInvalidTypeConversion + } + + res, ok := os.LookupEnv(s1) + if !ok { + return objects.FalseValue, nil } return &objects.String{Value: res}, nil } -func osLookupEnv(args ...objects.Object) (objects.Object, error) { +func osExec(args ...objects.Object) (objects.Object, error) { + if len(args) == 0 { + return nil, objects.ErrWrongNumArguments + } + + name, ok := objects.ToString(args[0]) + if !ok { + return nil, objects.ErrInvalidTypeConversion + } + + var execArgs []string + for _, arg := range args[1:] { + execArg, ok := objects.ToString(arg) + if !ok { + return nil, objects.ErrInvalidTypeConversion + } + + execArgs = append(execArgs, execArg) + } + + return makeOSExecCommand(exec.Command(name, execArgs...)), nil +} + +func osFindProcess(args ...objects.Object) (objects.Object, error) { if len(args) != 1 { return nil, objects.ErrWrongNumArguments } - s1, ok := objects.ToString(args[0]) + i1, ok := objects.ToInt(args[0]) if !ok { return nil, objects.ErrInvalidTypeConversion } - res, ok := os.LookupEnv(s1) + proc, err := os.FindProcess(i1) + if err != nil { + return wrapError(err), nil + } + + return makeOSProcess(proc), nil +} + +func osStartProcess(args ...objects.Object) (objects.Object, error) { + if len(args) != 4 { + return nil, objects.ErrWrongNumArguments + } + + name, ok := objects.ToString(args[0]) if !ok { - return objects.FalseValue, nil + return nil, objects.ErrInvalidTypeConversion } - return &objects.String{Value: res}, nil + argv, err := stringArray(args[1]) + if err != nil { + return nil, err + } + + dir, ok := objects.ToString(args[2]) + if !ok { + return nil, objects.ErrInvalidTypeConversion + } + + env, err := stringArray(args[3]) + if err != nil { + return nil, err + } + + proc, err := os.StartProcess(name, argv, &os.ProcAttr{ + Dir: dir, + Env: env, + }) + if err != nil { + return wrapError(err), nil + } + + return makeOSProcess(proc), nil +} + +func stringArray(o objects.Object) ([]string, error) { + arr, ok := o.(*objects.Array) + if !ok { + return nil, objects.ErrInvalidTypeConversion + } + + var sarr []string + for _, elem := range arr.Value { + str, ok := elem.(*objects.String) + if !ok { + return nil, objects.ErrInvalidTypeConversion + } + + sarr = append(sarr, str.Value) + } + + return sarr, nil } diff --git a/compiler/stdlib/exec.go b/compiler/stdlib/os_exec.go similarity index 69% rename from compiler/stdlib/exec.go rename to compiler/stdlib/os_exec.go index 2577d10e..b2bc7433 100644 --- a/compiler/stdlib/exec.go +++ b/compiler/stdlib/os_exec.go @@ -6,14 +6,7 @@ import ( "github.com/d5/tengo/objects" ) -var execModule = map[string]objects.Object{ - // look_path(file string) => string/error - "look_path": FuncASRSE(exec.LookPath), - // command(name string, args array(string)) => imap(cmd) - "command": &objects.UserFunction{Value: execCommand}, -} - -func execCmdImmutableMap(cmd *exec.Cmd) *objects.ImmutableMap { +func makeOSExecCommand(cmd *exec.Cmd) *objects.ImmutableMap { return &objects.ImmutableMap{ Value: map[string]objects.Object{ // combined_output() => bytes/error @@ -84,33 +77,9 @@ func execCmdImmutableMap(cmd *exec.Cmd) *objects.ImmutableMap { return nil, objects.ErrWrongNumArguments } - return osProcessImmutableMap(cmd.Process), nil + return makeOSProcess(cmd.Process), nil }, }, - // TODO: implement pipes - //"stderr_pipe": nil, - //"stdin_pipe": nil, - //"stdout_pipe": nil, }, } } - -func execCommand(args ...objects.Object) (objects.Object, error) { - if len(args) != 2 { - return nil, objects.ErrWrongNumArguments - } - - name, ok := objects.ToString(args[0]) - if !ok { - return nil, objects.ErrInvalidTypeConversion - } - - arg, err := stringArray(args[1]) - if err != nil { - return nil, err - } - - res := exec.Command(name, arg...) - - return execCmdImmutableMap(res), nil -} diff --git a/compiler/stdlib/os_file.go b/compiler/stdlib/os_file.go index 5c0007b7..3b9c4b66 100644 --- a/compiler/stdlib/os_file.go +++ b/compiler/stdlib/os_file.go @@ -6,7 +6,7 @@ import ( "github.com/d5/tengo/objects" ) -func osFileImmutableMap(file *os.File) *objects.ImmutableMap { +func makeOSFile(file *os.File) *objects.ImmutableMap { return &objects.ImmutableMap{ Value: map[string]objects.Object{ // chdir() => true/error @@ -66,79 +66,6 @@ func osFileImmutableMap(file *os.File) *objects.ImmutableMap { return &objects.Int{Value: res}, nil }, }, - // TODO: implement more functions - //"fd": nil, - //"read_at": nil, - //"readdir": nil, - //"set_deadline": nil, - //"set_read_deadline": nil, - //"set_write_deadline": nil, - //"stat": nil, - //"write_at": nil, }, } } - -func osCreate(args ...objects.Object) (objects.Object, error) { - if len(args) != 1 { - return nil, objects.ErrWrongNumArguments - } - - s1, ok := objects.ToString(args[0]) - if !ok { - return nil, objects.ErrInvalidTypeConversion - } - - res, err := os.Create(s1) - if err != nil { - return wrapError(err), nil - } - - return osFileImmutableMap(res), nil -} - -func osOpen(args ...objects.Object) (objects.Object, error) { - if len(args) != 1 { - return nil, objects.ErrWrongNumArguments - } - - s1, ok := objects.ToString(args[0]) - if !ok { - return nil, objects.ErrInvalidTypeConversion - } - - res, err := os.Open(s1) - if err != nil { - return wrapError(err), nil - } - - return osFileImmutableMap(res), nil -} - -func osOpenFile(args ...objects.Object) (objects.Object, error) { - if len(args) != 3 { - return nil, objects.ErrWrongNumArguments - } - - s1, ok := objects.ToString(args[0]) - if !ok { - return nil, objects.ErrInvalidTypeConversion - } - - i2, ok := objects.ToInt(args[1]) - if !ok { - return nil, objects.ErrInvalidTypeConversion - } - - i3, ok := objects.ToInt(args[2]) - if !ok { - return nil, objects.ErrInvalidTypeConversion - } - - res, err := os.OpenFile(s1, i2, os.FileMode(i3)) - if err != nil { - return wrapError(err), nil - } - - return osFileImmutableMap(res), nil -} diff --git a/compiler/stdlib/os_process.go b/compiler/stdlib/os_process.go index 45872d7b..5cdc856e 100644 --- a/compiler/stdlib/os_process.go +++ b/compiler/stdlib/os_process.go @@ -7,7 +7,7 @@ import ( "github.com/d5/tengo/objects" ) -func osProcessStateImmutableMap(state *os.ProcessState) *objects.ImmutableMap { +func makeOSProcessState(state *os.ProcessState) *objects.ImmutableMap { return &objects.ImmutableMap{ Value: map[string]objects.Object{ "exited": FuncARB(state.Exited), @@ -18,7 +18,7 @@ func osProcessStateImmutableMap(state *os.ProcessState) *objects.ImmutableMap { } } -func osProcessImmutableMap(proc *os.Process) *objects.ImmutableMap { +func makeOSProcess(proc *os.Process) *objects.ImmutableMap { return &objects.ImmutableMap{ Value: map[string]objects.Object{ "kill": FuncARE(proc.Kill), @@ -48,82 +48,9 @@ func osProcessImmutableMap(proc *os.Process) *objects.ImmutableMap { return wrapError(err), nil } - return osProcessStateImmutableMap(state), nil + return makeOSProcessState(state), nil }, }, }, } } - -func osFindProcess(args ...objects.Object) (objects.Object, error) { - if len(args) != 1 { - return nil, objects.ErrWrongNumArguments - } - - i1, ok := objects.ToInt(args[0]) - if !ok { - return nil, objects.ErrInvalidTypeConversion - } - - proc, err := os.FindProcess(i1) - if err != nil { - return wrapError(err), nil - } - - return osProcessImmutableMap(proc), nil -} - -func osStartProcess(args ...objects.Object) (objects.Object, error) { - if len(args) != 4 { - return nil, objects.ErrWrongNumArguments - } - - name, ok := objects.ToString(args[0]) - if !ok { - return nil, objects.ErrInvalidTypeConversion - } - - argv, err := stringArray(args[1]) - if err != nil { - return nil, err - } - - dir, ok := objects.ToString(args[2]) - if !ok { - return nil, objects.ErrInvalidTypeConversion - } - - env, err := stringArray(args[3]) - if err != nil { - return nil, err - } - - proc, err := os.StartProcess(name, argv, &os.ProcAttr{ - Dir: dir, - Env: env, - }) - if err != nil { - return wrapError(err), nil - } - - return osProcessImmutableMap(proc), nil -} - -func stringArray(o objects.Object) ([]string, error) { - arr, ok := o.(*objects.Array) - if !ok { - return nil, objects.ErrInvalidTypeConversion - } - - var sarr []string - for _, elem := range arr.Value { - str, ok := elem.(*objects.String) - if !ok { - return nil, objects.ErrInvalidTypeConversion - } - - sarr = append(sarr, str.Value) - } - - return sarr, nil -} diff --git a/compiler/stdlib/stdlib.go b/compiler/stdlib/stdlib.go index 3ca5dd37..412ce62d 100644 --- a/compiler/stdlib/stdlib.go +++ b/compiler/stdlib/stdlib.go @@ -4,7 +4,8 @@ import "github.com/d5/tengo/objects" // Modules contain the standard modules. var Modules = map[string]*objects.ImmutableMap{ - "math": {Value: mathModule}, - "os": {Value: osModule}, - "exec": {Value: execModule}, + "math": {Value: mathModule}, + "os": {Value: osModule}, + "text": {Value: textModule}, + "times": {Value: timesModule}, } diff --git a/compiler/stdlib/stdlib_test.go b/compiler/stdlib/stdlib_test.go new file mode 100644 index 00000000..37b936a5 --- /dev/null +++ b/compiler/stdlib/stdlib_test.go @@ -0,0 +1,128 @@ +package stdlib_test + +import ( + "fmt" + "testing" + "time" + + "github.com/d5/tengo/assert" + "github.com/d5/tengo/compiler/stdlib" + "github.com/d5/tengo/objects" +) + +type ARR = []interface{} +type MAP = map[string]interface{} +type IARR []interface{} +type IMAP map[string]interface{} + +type callres struct { + t *testing.T + o objects.Object + e error +} + +func (c callres) call(funcName string, args ...interface{}) callres { + if c.e != nil { + return c + } + + imap, ok := c.o.(*objects.ImmutableMap) + if !ok { + return c + } + + m, ok := imap.Value[funcName] + if !ok { + return callres{t: c.t, e: fmt.Errorf("function not found: %s", funcName)} + } + + f, ok := m.(*objects.UserFunction) + if !ok { + return callres{t: c.t, e: fmt.Errorf("non-callable: %s", funcName)} + } + + var oargs []objects.Object + for _, v := range args { + oargs = append(oargs, object(v)) + } + + res, err := f.Value(oargs...) + + return callres{t: c.t, o: res, e: err} +} + +func (c callres) expect(expected interface{}, msgAndArgs ...interface{}) bool { + return assert.NoError(c.t, c.e, msgAndArgs...) && + assert.Equal(c.t, object(expected), c.o, msgAndArgs...) +} + +func (c callres) expectError() bool { + return assert.Error(c.t, c.e) +} + +func module(t *testing.T, moduleName string) callres { + mod, ok := stdlib.Modules[moduleName] + if !ok { + return callres{t: t, e: fmt.Errorf("module not found: %s", moduleName)} + } + + return callres{t: t, o: mod} +} + +func object(v interface{}) objects.Object { + switch v := v.(type) { + case objects.Object: + return v + case string: + return &objects.String{Value: v} + case int64: + return &objects.Int{Value: v} + case int: // for convenience + return &objects.Int{Value: int64(v)} + case bool: + if v { + return objects.TrueValue + } + return objects.FalseValue + case rune: + return &objects.Char{Value: v} + case byte: // for convenience + return &objects.Char{Value: rune(v)} + case float64: + return &objects.Float{Value: v} + case []byte: + return &objects.Bytes{Value: v} + case MAP: + objs := make(map[string]objects.Object) + for k, v := range v { + objs[k] = object(v) + } + + return &objects.Map{Value: objs} + case ARR: + var objs []objects.Object + for _, e := range v { + objs = append(objs, object(e)) + } + + return &objects.Array{Value: objs} + case IMAP: + objs := make(map[string]objects.Object) + for k, v := range v { + objs[k] = object(v) + } + + return &objects.ImmutableMap{Value: objs} + case IARR: + var objs []objects.Object + for _, e := range v { + objs = append(objs, object(e)) + } + + return &objects.ImmutableArray{Value: objs} + case time.Time: + return &objects.Time{Value: v} + } + + panic(fmt.Errorf("unknown type: %T", v)) +} diff --git a/compiler/stdlib/text.go b/compiler/stdlib/text.go new file mode 100644 index 00000000..b68b43bd --- /dev/null +++ b/compiler/stdlib/text.go @@ -0,0 +1,469 @@ +package stdlib + +import ( + "regexp" + "strconv" + "strings" + + "github.com/d5/tengo/objects" +) + +var textModule = map[string]objects.Object{ + "re_match": &objects.UserFunction{Value: textREMatch}, // re_match(pattern, text) => bool/error + "re_find": &objects.UserFunction{Value: textREFind}, // re_find(pattern, text, count) => [[{text:,begin:,end:}]]/undefined + "re_replace": &objects.UserFunction{Value: textREReplace}, // re_replace(pattern, text, repl) => string/error + "re_split": &objects.UserFunction{Value: textRESplit}, // re_split(pattern, text, count) => [string]/error + "re_compile": &objects.UserFunction{Value: textRECompile}, // re_compile(pattern) => Regexp/error + "compare": FuncASSRI(strings.Compare), // compare(a, b) => int + "contains": FuncASSRB(strings.Contains), // contains(s, substr) => bool + "contains_any": FuncASSRB(strings.ContainsAny), // contains_any(s, chars) => bool + "count": FuncASSRI(strings.Count), // count(s, substr) => int + "equal_fold": FuncASSRB(strings.EqualFold), // "equal_fold(s, t) => bool + "fields": FuncASRSs(strings.Fields), // fields(s) => [string] + "has_prefix": FuncASSRB(strings.HasPrefix), // has_prefix(s, prefix) => bool + "has_suffix": FuncASSRB(strings.HasSuffix), // has_suffix(s, suffix) => bool + "index": FuncASSRI(strings.Index), // index(s, substr) => int + "index_any": FuncASSRI(strings.IndexAny), // index_any(s, chars) => int + "join": FuncASsSRS(strings.Join), // join(arr, sep) => string + "last_index": FuncASSRI(strings.LastIndex), // last_index(s, substr) => int + "last_index_any": FuncASSRI(strings.LastIndexAny), // last_index_any(s, chars) => int + "repeat": FuncASIRS(strings.Repeat), // repeat(s, count) => string + "replace": &objects.UserFunction{Value: textReplace}, // replace(s, old, new, n) => string + "split": FuncASSRSs(strings.Split), // split(s, sep) => [string] + "split_after": FuncASSRSs(strings.SplitAfter), // split_after(s, sep) => [string] + "split_after_n": FuncASSIRSs(strings.SplitAfterN), // split_after_n(s, sep, n) => [string] + "split_n": FuncASSIRSs(strings.SplitN), // split_n(s, sep, n) => [string] + "title": FuncASRS(strings.Title), // title(s) => string + "to_lower": FuncASRS(strings.ToLower), // to_lower(s) => string + "to_title": FuncASRS(strings.ToTitle), // to_title(s) => string + "to_upper": FuncASRS(strings.ToUpper), // to_upper(s) => string + "trim_left": FuncASSRS(strings.TrimLeft), // trim_left(s, cutset) => string + "trim_prefix": FuncASSRS(strings.TrimPrefix), // trim_prefix(s, prefix) => string + "trim_right": FuncASSRS(strings.TrimRight), // trim_right(s, cutset) => string + "trim_space": FuncASRS(strings.TrimSpace), // trim_space(s) => string + "trim_suffix": FuncASSRS(strings.TrimSuffix), // trim_suffix(s, suffix) => string + "atoi": FuncASRIE(strconv.Atoi), // atoi(str) => int/error + "format_bool": &objects.UserFunction{Value: textFormatBool}, // format_bool(b) => string + "format_float": &objects.UserFunction{Value: textFormatFloat}, // format_float(f, fmt, prec, bits) => string + "format_int": &objects.UserFunction{Value: textFormatInt}, // format_int(i, base) => string + "itoa": FuncAIRS(strconv.Itoa), // itoa(i) => string + "parse_bool": &objects.UserFunction{Value: textParseBool}, // parse_bool(str) => bool/error + "parse_float": &objects.UserFunction{Value: textParseFloat}, // parse_float(str, bits) => float/error + "parse_int": &objects.UserFunction{Value: textParseInt}, // parse_int(str, base, bits) => int/error + "quote": FuncASRS(strconv.Quote), // quote(str) => string + "unquote": FuncASRSE(strconv.Unquote), // unquote(str) => string/error +} + +func textREMatch(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 2 { + err = objects.ErrWrongNumArguments + return + } + + s1, ok := objects.ToString(args[0]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + s2, ok := objects.ToString(args[1]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + matched, err := regexp.MatchString(s1, s2) + if err != nil { + ret = wrapError(err) + return + } + + if matched { + ret = objects.TrueValue + } else { + ret = objects.FalseValue + } + + return +} + +func textREFind(args ...objects.Object) (ret objects.Object, err error) { + numArgs := len(args) + if numArgs != 2 && numArgs != 3 { + err = objects.ErrWrongNumArguments + return + } + + s1, ok := objects.ToString(args[0]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + re, err := regexp.Compile(s1) + if err != nil { + ret = wrapError(err) + return + } + + s2, ok := objects.ToString(args[1]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + if numArgs < 3 { + m := re.FindStringSubmatchIndex(s2) + if m == nil { + ret = objects.UndefinedValue + return + } + + arr := &objects.Array{} + for i := 0; i < len(m); i += 2 { + arr.Value = append(arr.Value, &objects.ImmutableMap{Value: map[string]objects.Object{ + "text": &objects.String{Value: s2[m[i]:m[i+1]]}, + "begin": &objects.Int{Value: int64(m[i])}, + "end": &objects.Int{Value: int64(m[i+1])}, + }}) + } + + ret = &objects.Array{Value: []objects.Object{arr}} + + return + } + + i3, ok := objects.ToInt(args[2]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + m := re.FindAllStringSubmatchIndex(s2, i3) + if m == nil { + ret = objects.UndefinedValue + return + } + + arr := &objects.Array{} + for _, m := range m { + subMatch := &objects.Array{} + for i := 0; i < len(m); i += 2 { + subMatch.Value = append(subMatch.Value, &objects.ImmutableMap{Value: map[string]objects.Object{ + "text": &objects.String{Value: s2[m[i]:m[i+1]]}, + "begin": &objects.Int{Value: int64(m[i])}, + "end": &objects.Int{Value: int64(m[i+1])}, + }}) + } + + arr.Value = append(arr.Value, subMatch) + } + + ret = arr + + return +} + +func textREReplace(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 3 { + err = objects.ErrWrongNumArguments + return + } + + s1, ok := objects.ToString(args[0]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + s2, ok := objects.ToString(args[1]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + s3, ok := objects.ToString(args[2]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + re, err := regexp.Compile(s1) + if err != nil { + ret = wrapError(err) + } else { + ret = &objects.String{Value: re.ReplaceAllString(s2, s3)} + } + + return +} + +func textRESplit(args ...objects.Object) (ret objects.Object, err error) { + numArgs := len(args) + if numArgs != 2 && numArgs != 3 { + err = objects.ErrWrongNumArguments + return + } + + s1, ok := objects.ToString(args[0]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + s2, ok := objects.ToString(args[1]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + var i3 = -1 + if numArgs > 2 { + i3, ok = objects.ToInt(args[2]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + } + + re, err := regexp.Compile(s1) + if err != nil { + ret = wrapError(err) + return + } + + arr := &objects.Array{} + for _, s := range re.Split(s2, i3) { + arr.Value = append(arr.Value, &objects.String{Value: s}) + } + + ret = arr + + return +} + +func textRECompile(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 1 { + err = objects.ErrWrongNumArguments + return + } + + s1, ok := objects.ToString(args[0]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + re, err := regexp.Compile(s1) + if err != nil { + ret = wrapError(err) + } else { + ret = makeTextRegexp(re) + } + + return +} + +func textReplace(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 4 { + err = objects.ErrWrongNumArguments + return + } + + s1, ok := objects.ToString(args[0]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + s2, ok := objects.ToString(args[1]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + s3, ok := objects.ToString(args[2]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + i4, ok := objects.ToInt(args[3]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + ret = &objects.String{Value: strings.Replace(s1, s2, s3, i4)} + + return +} + +func textFormatBool(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 1 { + err = objects.ErrWrongNumArguments + return + } + + b1, ok := args[0].(*objects.Bool) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + if b1 == objects.TrueValue { + ret = &objects.String{Value: "true"} + } else { + ret = &objects.String{Value: "false"} + } + + return +} + +func textFormatFloat(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 4 { + err = objects.ErrWrongNumArguments + return + } + + f1, ok := args[0].(*objects.Float) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + s2, ok := objects.ToString(args[1]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + i3, ok := objects.ToInt(args[2]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + i4, ok := objects.ToInt(args[3]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + ret = &objects.String{Value: strconv.FormatFloat(f1.Value, s2[0], i3, i4)} + + return +} + +func textFormatInt(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 2 { + err = objects.ErrWrongNumArguments + return + } + + i1, ok := args[0].(*objects.Int) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + i2, ok := objects.ToInt(args[1]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + ret = &objects.String{Value: strconv.FormatInt(i1.Value, i2)} + + return +} + +func textParseBool(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 1 { + err = objects.ErrWrongNumArguments + return + } + + s1, ok := args[0].(*objects.String) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + parsed, err := strconv.ParseBool(s1.Value) + if err != nil { + ret = wrapError(err) + return + } + + if parsed { + ret = objects.TrueValue + } else { + ret = objects.FalseValue + } + + return +} + +func textParseFloat(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 2 { + err = objects.ErrWrongNumArguments + return + } + + s1, ok := args[0].(*objects.String) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + i2, ok := objects.ToInt(args[1]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + parsed, err := strconv.ParseFloat(s1.Value, i2) + if err != nil { + ret = wrapError(err) + return + } + + ret = &objects.Float{Value: parsed} + + return +} + +func textParseInt(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 3 { + err = objects.ErrWrongNumArguments + return + } + + s1, ok := args[0].(*objects.String) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + i2, ok := objects.ToInt(args[1]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + i3, ok := objects.ToInt(args[2]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + parsed, err := strconv.ParseInt(s1.Value, i2, i3) + if err != nil { + ret = wrapError(err) + return + } + + ret = &objects.Int{Value: parsed} + + return +} diff --git a/compiler/stdlib/text_regexp.go b/compiler/stdlib/text_regexp.go new file mode 100644 index 00000000..94bf2ed1 --- /dev/null +++ b/compiler/stdlib/text_regexp.go @@ -0,0 +1,167 @@ +package stdlib + +import ( + "regexp" + + "github.com/d5/tengo/objects" +) + +func makeTextRegexp(re *regexp.Regexp) *objects.ImmutableMap { + return &objects.ImmutableMap{ + Value: map[string]objects.Object{ + // match(text) => bool + "match": &objects.UserFunction{ + Value: func(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 1 { + err = objects.ErrWrongNumArguments + return + } + + s1, ok := objects.ToString(args[0]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + if re.MatchString(s1) { + ret = objects.TrueValue + } else { + ret = objects.FalseValue + } + + return + }, + }, + + // find(text) => array(array({text:,begin:,end:}))/undefined + // find(text, maxCount) => array(array({text:,begin:,end:}))/undefined + "find": &objects.UserFunction{ + Value: func(args ...objects.Object) (ret objects.Object, err error) { + numArgs := len(args) + if numArgs != 1 && numArgs != 2 { + err = objects.ErrWrongNumArguments + return + } + + s1, ok := objects.ToString(args[0]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + if numArgs == 1 { + m := re.FindStringSubmatchIndex(s1) + if m == nil { + ret = objects.UndefinedValue + return + } + + arr := &objects.Array{} + for i := 0; i < len(m); i += 2 { + arr.Value = append(arr.Value, &objects.ImmutableMap{Value: map[string]objects.Object{ + "text": &objects.String{Value: s1[m[i]:m[i+1]]}, + "begin": &objects.Int{Value: int64(m[i])}, + "end": &objects.Int{Value: int64(m[i+1])}, + }}) + } + + ret = &objects.Array{Value: []objects.Object{arr}} + + return + } + + i2, ok := objects.ToInt(args[1]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + m := re.FindAllStringSubmatchIndex(s1, i2) + if m == nil { + ret = objects.UndefinedValue + return + } + + arr := &objects.Array{} + for _, m := range m { + subMatch := &objects.Array{} + for i := 0; i < len(m); i += 2 { + subMatch.Value = append(subMatch.Value, &objects.ImmutableMap{Value: map[string]objects.Object{ + "text": &objects.String{Value: s1[m[i]:m[i+1]]}, + "begin": &objects.Int{Value: int64(m[i])}, + "end": &objects.Int{Value: int64(m[i+1])}, + }}) + } + + arr.Value = append(arr.Value, subMatch) + } + + ret = arr + + return + }, + }, + + // replace(src, repl) => string + "replace": &objects.UserFunction{ + Value: func(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 2 { + err = objects.ErrWrongNumArguments + return + } + + s1, ok := objects.ToString(args[0]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + s2, ok := objects.ToString(args[1]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + ret = &objects.String{Value: re.ReplaceAllString(s1, s2)} + + return + }, + }, + + // split(text) => array(string) + // split(text, maxCount) => array(string) + "split": &objects.UserFunction{ + Value: func(args ...objects.Object) (ret objects.Object, err error) { + numArgs := len(args) + if numArgs != 1 && numArgs != 2 { + err = objects.ErrWrongNumArguments + return + } + + s1, ok := objects.ToString(args[0]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + var i2 = -1 + if numArgs > 1 { + i2, ok = objects.ToInt(args[1]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + } + + arr := &objects.Array{} + for _, s := range re.Split(s1, i2) { + arr.Value = append(arr.Value, &objects.String{Value: s}) + } + + ret = arr + + return + }, + }, + }, + } +} diff --git a/compiler/stdlib/text_test.go b/compiler/stdlib/text_test.go new file mode 100644 index 00000000..5509fa6a --- /dev/null +++ b/compiler/stdlib/text_test.go @@ -0,0 +1,200 @@ +package stdlib_test + +import ( + "testing" + + "github.com/d5/tengo/objects" +) + +func TestTextRE(t *testing.T) { + // re_match(pattern, text) + for _, d := range []struct { + pattern string + text string + expected interface{} + }{ + {"abc", "", false}, + {"abc", "abc", true}, + {"a", "abc", true}, + {"b", "abc", true}, + {"^a", "abc", true}, + {"^b", "abc", false}, + } { + module(t, "text").call("re_match", d.pattern, d.text).expect(d.expected, "pattern: %q, src: %q", d.pattern, d.text) + module(t, "text").call("re_compile", d.pattern).call("match", d.text).expect(d.expected, "patter: %q, src: %q", d.pattern, d.text) + } + + // re_find(pattern, text) + for _, d := range []struct { + pattern string + text string + expected interface{} + }{ + {"a(b)", "", objects.UndefinedValue}, + {"a(b)", "ab", ARR{ + ARR{ + IMAP{"text": "ab", "begin": 0, "end": 2}, + IMAP{"text": "b", "begin": 1, "end": 2}, + }, + }}, + {"a(bc)d", "abcdefgabcd", ARR{ + ARR{ + IMAP{"text": "abcd", "begin": 0, "end": 4}, + IMAP{"text": "bc", "begin": 1, "end": 3}, + }, + }}, + {"(a)b(c)d", "abcdefgabcd", ARR{ + ARR{ + IMAP{"text": "abcd", "begin": 0, "end": 4}, + IMAP{"text": "a", "begin": 0, "end": 1}, + IMAP{"text": "c", "begin": 2, "end": 3}, + }, + }}, + } { + module(t, "text").call("re_find", d.pattern, d.text).expect(d.expected, "pattern: %q, text: %q", d.pattern, d.text) + module(t, "text").call("re_compile", d.pattern).call("find", d.text).expect(d.expected, "pattern: %q, text: %q", d.pattern, d.text) + } + + // re_find(pattern, text, count)) + for _, d := range []struct { + pattern string + text string + count int + expected interface{} + }{ + {"a(b)", "", -1, objects.UndefinedValue}, + {"a(b)", "ab", -1, ARR{ + ARR{ + IMAP{"text": "ab", "begin": 0, "end": 2}, + IMAP{"text": "b", "begin": 1, "end": 2}, + }, + }}, + {"a(bc)d", "abcdefgabcd", -1, ARR{ + ARR{ + IMAP{"text": "abcd", "begin": 0, "end": 4}, + IMAP{"text": "bc", "begin": 1, "end": 3}, + }, + ARR{ + IMAP{"text": "abcd", "begin": 7, "end": 11}, + IMAP{"text": "bc", "begin": 8, "end": 10}, + }, + }}, + {"(a)b(c)d", "abcdefgabcd", -1, ARR{ + ARR{ + IMAP{"text": "abcd", "begin": 0, "end": 4}, + IMAP{"text": "a", "begin": 0, "end": 1}, + IMAP{"text": "c", "begin": 2, "end": 3}, + }, + ARR{ + IMAP{"text": "abcd", "begin": 7, "end": 11}, + IMAP{"text": "a", "begin": 7, "end": 8}, + IMAP{"text": "c", "begin": 9, "end": 10}, + }, + }}, + {"(a)b(c)d", "abcdefgabcd", 0, objects.UndefinedValue}, + {"(a)b(c)d", "abcdefgabcd", 1, ARR{ + ARR{ + IMAP{"text": "abcd", "begin": 0, "end": 4}, + IMAP{"text": "a", "begin": 0, "end": 1}, + IMAP{"text": "c", "begin": 2, "end": 3}, + }, + }}, + } { + module(t, "text").call("re_find", d.pattern, d.text, d.count).expect(d.expected, "pattern: %q, text: %q", d.pattern, d.text) + module(t, "text").call("re_compile", d.pattern).call("find", d.text, d.count).expect(d.expected, "pattern: %q, text: %q", d.pattern, d.text) + } + + // re_replace(pattern, text, repl) + for _, d := range []struct { + pattern string + text string + repl string + expected interface{} + }{ + {"a", "", "b", ""}, + {"a", "a", "b", "b"}, + {"a", "acac", "b", "bcbc"}, + {"a", "acac", "123", "123c123c"}, + {"ac", "acac", "99", "9999"}, + {"ac$", "acac", "foo", "acfoo"}, + } { + module(t, "text").call("re_replace", d.pattern, d.text, d.repl).expect(d.expected, "pattern: %q, text: %q, repl: %q", d.pattern, d.text, d.repl) + module(t, "text").call("re_compile", d.pattern).call("replace", d.text, d.repl).expect(d.expected, "pattern: %q, text: %q, repl: %q", d.pattern, d.text, d.repl) + } + + // re_split(pattern, text) + for _, d := range []struct { + pattern string + text string + expected interface{} + }{ + {"a", "", ARR{""}}, + {"a", "abcabc", ARR{"", "bc", "bc"}}, + {"ab", "abcabc", ARR{"", "c", "c"}}, + {"^a", "abcabc", ARR{"", "bcabc"}}, + } { + module(t, "text").call("re_split", d.pattern, d.text).expect(d.expected, "pattern: %q, text: %q", d.pattern, d.text) + module(t, "text").call("re_compile", d.pattern).call("split", d.text).expect(d.expected, "pattern: %q, text: %q", d.pattern, d.text) + } + + // re_split(pattern, text, count)) + for _, d := range []struct { + pattern string + text string + count int + expected interface{} + }{ + {"a", "", -1, ARR{""}}, + {"a", "abcabc", -1, ARR{"", "bc", "bc"}}, + {"ab", "abcabc", -1, ARR{"", "c", "c"}}, + {"^a", "abcabc", -1, ARR{"", "bcabc"}}, + {"a", "abcabc", 0, ARR{}}, + {"a", "abcabc", 1, ARR{"abcabc"}}, + {"a", "abcabc", 2, ARR{"", "bcabc"}}, + {"a", "abcabc", 3, ARR{"", "bc", "bc"}}, + {"b", "abcabc", 1, ARR{"abcabc"}}, + {"b", "abcabc", 2, ARR{"a", "cabc"}}, + {"b", "abcabc", 3, ARR{"a", "ca", "c"}}, + } { + module(t, "text").call("re_split", d.pattern, d.text, d.count).expect(d.expected, "pattern: %q, text: %q", d.pattern, d.text) + module(t, "text").call("re_compile", d.pattern).call("split", d.text, d.count).expect(d.expected, "pattern: %q, text: %q", d.pattern, d.text) + } +} + +func TestText(t *testing.T) { + module(t, "text").call("compare", "", "").expect(0) + module(t, "text").call("compare", "", "a").expect(-1) + module(t, "text").call("compare", "a", "").expect(1) + module(t, "text").call("compare", "a", "a").expect(0) + module(t, "text").call("compare", "a", "b").expect(-1) + module(t, "text").call("compare", "b", "a").expect(1) + module(t, "text").call("compare", "abcde", "abcde").expect(0) + module(t, "text").call("compare", "abcde", "abcdf").expect(-1) + module(t, "text").call("compare", "abcdf", "abcde").expect(1) + + module(t, "text").call("contains", "", "").expect(true) + module(t, "text").call("contains", "", "a").expect(false) + module(t, "text").call("contains", "a", "").expect(true) + module(t, "text").call("contains", "a", "a").expect(true) + module(t, "text").call("contains", "abcde", "a").expect(true) + module(t, "text").call("contains", "abcde", "abcde").expect(true) + module(t, "text").call("contains", "abc", "abcde").expect(false) + module(t, "text").call("contains", "ab cd", "bc").expect(false) + + module(t, "text").call("replace", "", "", "", -1).expect("") + module(t, "text").call("replace", "abcd", "a", "x", -1).expect("xbcd") + module(t, "text").call("replace", "aaaa", "a", "x", -1).expect("xxxx") + module(t, "text").call("replace", "aaaa", "a", "x", 0).expect("aaaa") + module(t, "text").call("replace", "aaaa", "a", "x", 2).expect("xxaa") + module(t, "text").call("replace", "abcd", "bc", "x", -1).expect("axd") + + module(t, "text").call("format_bool", true).expect("true") + module(t, "text").call("format_bool", false).expect("false") + module(t, "text").call("format_float", -19.84, 'f', -1, 64).expect("-19.84") + module(t, "text").call("format_int", -1984, 10).expect("-1984") + module(t, "text").call("format_int", 1984, 8).expect("3700") + module(t, "text").call("parse_bool", "true").expect(true) + module(t, "text").call("parse_bool", "0").expect(false) + module(t, "text").call("parse_float", "-19.84", 64).expect(-19.84) + module(t, "text").call("parse_int", "-1984", 10, 64).expect(-1984) +} diff --git a/compiler/stdlib/times.go b/compiler/stdlib/times.go new file mode 100644 index 00000000..fd4f7cc2 --- /dev/null +++ b/compiler/stdlib/times.go @@ -0,0 +1,782 @@ +package stdlib + +import ( + "time" + + "github.com/d5/tengo/objects" +) + +var timesModule = map[string]objects.Object{ + "format_ansic": &objects.String{Value: time.ANSIC}, + "format_unix_date": &objects.String{Value: time.UnixDate}, + "format_ruby_date": &objects.String{Value: time.RubyDate}, + "format_rfc822": &objects.String{Value: time.RFC822}, + "format_rfc822z": &objects.String{Value: time.RFC822Z}, + "format_rfc850": &objects.String{Value: time.RFC850}, + "format_rfc1123": &objects.String{Value: time.RFC1123}, + "format_rfc1123z": &objects.String{Value: time.RFC1123Z}, + "format_rfc3339": &objects.String{Value: time.RFC3339}, + "format_rfc3339_nano": &objects.String{Value: time.RFC3339Nano}, + "format_kitchen": &objects.String{Value: time.Kitchen}, + "format_stamp": &objects.String{Value: time.Stamp}, + "format_stamp_milli": &objects.String{Value: time.StampMilli}, + "format_stamp_micro": &objects.String{Value: time.StampMicro}, + "format_stamp_nano": &objects.String{Value: time.StampNano}, + "nanosecond": &objects.Int{Value: int64(time.Nanosecond)}, + "microsecond": &objects.Int{Value: int64(time.Microsecond)}, + "millisecond": &objects.Int{Value: int64(time.Millisecond)}, + "second": &objects.Int{Value: int64(time.Second)}, + "minute": &objects.Int{Value: int64(time.Minute)}, + "hour": &objects.Int{Value: int64(time.Hour)}, + "january": &objects.Int{Value: int64(time.January)}, + "february": &objects.Int{Value: int64(time.February)}, + "march": &objects.Int{Value: int64(time.March)}, + "april": &objects.Int{Value: int64(time.April)}, + "may": &objects.Int{Value: int64(time.May)}, + "june": &objects.Int{Value: int64(time.June)}, + "july": &objects.Int{Value: int64(time.July)}, + "august": &objects.Int{Value: int64(time.August)}, + "september": &objects.Int{Value: int64(time.September)}, + "october": &objects.Int{Value: int64(time.October)}, + "november": &objects.Int{Value: int64(time.November)}, + "december": &objects.Int{Value: int64(time.December)}, + "sleep": &objects.UserFunction{Value: timesSleep}, // sleep(int) + "parse_duration": &objects.UserFunction{Value: timesParseDuration}, // parse_duration(str) => int + "since": &objects.UserFunction{Value: timesSince}, // since(time) => int + "until": &objects.UserFunction{Value: timesUntil}, // until(time) => int + "duration_hours": &objects.UserFunction{Value: timesDurationHours}, // duration_hours(int) => float + "duration_minutes": &objects.UserFunction{Value: timesDurationMinutes}, // duration_minutes(int) => float + "duration_nanoseconds": &objects.UserFunction{Value: timesDurationNanoseconds}, // duration_nanoseconds(int) => int + "duration_seconds": &objects.UserFunction{Value: timesDurationSeconds}, // duration_seconds(int) => float + "duration_string": &objects.UserFunction{Value: timesDurationString}, // duration_string(int) => string + "month_string": &objects.UserFunction{Value: timesMonthString}, // month_string(int) => string + "date": &objects.UserFunction{Value: timesDate}, // date(year, month, day, hour, min, sec, nsec) => time + "now": &objects.UserFunction{Value: timesNow}, // now() => time + "parse": &objects.UserFunction{Value: timesParse}, // parse(format, str) => time + "unix": &objects.UserFunction{Value: timesUnix}, // unix(sec, nsec) => time + "add": &objects.UserFunction{Value: timesAdd}, // add(time, int) => time + "add_date": &objects.UserFunction{Value: timesAddDate}, // add_date(time, years, months, days) => time + "sub": &objects.UserFunction{Value: timesSub}, // sub(t time, u time) => int + "after": &objects.UserFunction{Value: timesAfter}, // after(t time, u time) => bool + "before": &objects.UserFunction{Value: timesBefore}, // before(t time, u time) => bool + "time_year": &objects.UserFunction{Value: timesTimeYear}, // time_year(time) => int + "time_month": &objects.UserFunction{Value: timesTimeMonth}, // time_month(time) => int + "time_day": &objects.UserFunction{Value: timesTimeDay}, // time_day(time) => int + "time_weekday": &objects.UserFunction{Value: timesTimeWeekday}, // time_weekday(time) => int + "time_hour": &objects.UserFunction{Value: timesTimeHour}, // time_hour(time) => int + "time_minute": &objects.UserFunction{Value: timesTimeMinute}, // time_minute(time) => int + "time_second": &objects.UserFunction{Value: timesTimeSecond}, // time_second(time) => int + "time_nanosecond": &objects.UserFunction{Value: timesTimeNanosecond}, // time_nanosecond(time) => int + "time_unix": &objects.UserFunction{Value: timesTimeUnix}, // time_unix(time) => int + "time_unix_nano": &objects.UserFunction{Value: timesTimeUnixNano}, // time_unix_nano(time) => int + "time_format": &objects.UserFunction{Value: timesTimeFormat}, // time_format(time, format) => string + "time_location": &objects.UserFunction{Value: timesTimeLocation}, // time_location(time) => string + "time_string": &objects.UserFunction{Value: timesTimeString}, // time_string(time) => string + "is_zero": &objects.UserFunction{Value: timesIsZero}, // is_zero(time) => bool + "to_local": &objects.UserFunction{Value: timesToLocal}, // to_local(time) => time + "to_utc": &objects.UserFunction{Value: timesToUTC}, // to_utc(time) => time +} + +func timesSleep(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 1 { + err = objects.ErrWrongNumArguments + return + } + + i1, ok := objects.ToInt64(args[0]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + time.Sleep(time.Duration(i1)) + ret = objects.UndefinedValue + + return +} + +func timesParseDuration(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 1 { + err = objects.ErrWrongNumArguments + return + } + + s1, ok := objects.ToString(args[0]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + dur, err := time.ParseDuration(s1) + if err != nil { + ret = wrapError(err) + return + } + + ret = &objects.Int{Value: int64(dur)} + + return +} + +func timesSince(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 1 { + err = objects.ErrWrongNumArguments + return + } + + t1, ok := objects.ToTime(args[0]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + ret = &objects.Int{Value: int64(time.Since(t1))} + + return +} + +func timesUntil(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 1 { + err = objects.ErrWrongNumArguments + return + } + + t1, ok := objects.ToTime(args[0]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + ret = &objects.Int{Value: int64(time.Until(t1))} + + return +} + +func timesDurationHours(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 1 { + err = objects.ErrWrongNumArguments + return + } + + i1, ok := objects.ToInt64(args[0]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + ret = &objects.Float{Value: time.Duration(i1).Hours()} + + return +} + +func timesDurationMinutes(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 1 { + err = objects.ErrWrongNumArguments + return + } + + i1, ok := objects.ToInt64(args[0]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + ret = &objects.Float{Value: time.Duration(i1).Minutes()} + + return +} + +func timesDurationNanoseconds(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 1 { + err = objects.ErrWrongNumArguments + return + } + + i1, ok := objects.ToInt64(args[0]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + ret = &objects.Int{Value: time.Duration(i1).Nanoseconds()} + + return +} + +func timesDurationSeconds(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 1 { + err = objects.ErrWrongNumArguments + return + } + + i1, ok := objects.ToInt64(args[0]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + ret = &objects.Float{Value: time.Duration(i1).Seconds()} + + return +} + +func timesDurationString(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 1 { + err = objects.ErrWrongNumArguments + return + } + + i1, ok := objects.ToInt64(args[0]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + ret = &objects.String{Value: time.Duration(i1).String()} + + return +} + +func timesMonthString(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 1 { + err = objects.ErrWrongNumArguments + return + } + + i1, ok := objects.ToInt64(args[0]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + ret = &objects.String{Value: time.Month(i1).String()} + + return +} + +func timesDate(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 7 { + err = objects.ErrWrongNumArguments + return + } + + i1, ok := objects.ToInt(args[0]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + i2, ok := objects.ToInt(args[1]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + i3, ok := objects.ToInt(args[2]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + i4, ok := objects.ToInt(args[3]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + i5, ok := objects.ToInt(args[4]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + i6, ok := objects.ToInt(args[5]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + i7, ok := objects.ToInt(args[6]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + ret = &objects.Time{Value: time.Date(i1, time.Month(i2), i3, i4, i5, i6, i7, time.Now().Location())} + + return +} + +func timesNow(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 0 { + err = objects.ErrWrongNumArguments + return + } + + ret = &objects.Time{Value: time.Now()} + + return +} + +func timesParse(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 2 { + err = objects.ErrWrongNumArguments + return + } + + s1, ok := objects.ToString(args[0]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + s2, ok := objects.ToString(args[1]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + parsed, err := time.Parse(s1, s2) + if err != nil { + ret = wrapError(err) + return + } + + ret = &objects.Time{Value: parsed} + + return +} + +func timesUnix(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 2 { + err = objects.ErrWrongNumArguments + return + } + + i1, ok := objects.ToInt64(args[0]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + i2, ok := objects.ToInt64(args[1]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + ret = &objects.Time{Value: time.Unix(i1, i2)} + + return +} + +func timesAdd(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 2 { + err = objects.ErrWrongNumArguments + return + } + + t1, ok := objects.ToTime(args[0]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + i2, ok := objects.ToInt64(args[1]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + ret = &objects.Time{Value: t1.Add(time.Duration(i2))} + + return +} + +func timesSub(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 2 { + err = objects.ErrWrongNumArguments + return + } + + t1, ok := objects.ToTime(args[0]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + t2, ok := objects.ToTime(args[1]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + ret = &objects.Int{Value: int64(t1.Sub(t2))} + + return +} + +func timesAddDate(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 4 { + err = objects.ErrWrongNumArguments + return + } + + t1, ok := objects.ToTime(args[0]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + i2, ok := objects.ToInt(args[1]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + i3, ok := objects.ToInt(args[2]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + i4, ok := objects.ToInt(args[3]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + ret = &objects.Time{Value: t1.AddDate(i2, i3, i4)} + + return +} + +func timesAfter(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 2 { + err = objects.ErrWrongNumArguments + return + } + + t1, ok := objects.ToTime(args[0]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + t2, ok := objects.ToTime(args[1]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + if t1.After(t2) { + ret = objects.TrueValue + } else { + ret = objects.FalseValue + } + + return +} + +func timesBefore(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 2 { + err = objects.ErrWrongNumArguments + return + } + + t1, ok := objects.ToTime(args[0]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + t2, ok := objects.ToTime(args[1]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + if t1.Before(t2) { + ret = objects.TrueValue + } else { + ret = objects.FalseValue + } + + return +} + +func timesTimeYear(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 1 { + err = objects.ErrWrongNumArguments + return + } + + t1, ok := objects.ToTime(args[0]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + ret = &objects.Int{Value: int64(t1.Year())} + + return +} + +func timesTimeMonth(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 1 { + err = objects.ErrWrongNumArguments + return + } + + t1, ok := objects.ToTime(args[0]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + ret = &objects.Int{Value: int64(t1.Month())} + + return +} + +func timesTimeDay(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 1 { + err = objects.ErrWrongNumArguments + return + } + + t1, ok := objects.ToTime(args[0]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + ret = &objects.Int{Value: int64(t1.Day())} + + return +} + +func timesTimeWeekday(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 1 { + err = objects.ErrWrongNumArguments + return + } + + t1, ok := objects.ToTime(args[0]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + ret = &objects.Int{Value: int64(t1.Weekday())} + + return +} + +func timesTimeHour(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 1 { + err = objects.ErrWrongNumArguments + return + } + + t1, ok := objects.ToTime(args[0]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + ret = &objects.Int{Value: int64(t1.Hour())} + + return +} + +func timesTimeMinute(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 1 { + err = objects.ErrWrongNumArguments + return + } + + t1, ok := objects.ToTime(args[0]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + ret = &objects.Int{Value: int64(t1.Minute())} + + return +} + +func timesTimeSecond(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 1 { + err = objects.ErrWrongNumArguments + return + } + + t1, ok := objects.ToTime(args[0]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + ret = &objects.Int{Value: int64(t1.Second())} + + return +} + +func timesTimeNanosecond(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 1 { + err = objects.ErrWrongNumArguments + return + } + + t1, ok := objects.ToTime(args[0]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + ret = &objects.Int{Value: int64(t1.Nanosecond())} + + return +} + +func timesTimeUnix(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 1 { + err = objects.ErrWrongNumArguments + return + } + + t1, ok := objects.ToTime(args[0]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + ret = &objects.Int{Value: int64(t1.Unix())} + + return +} + +func timesTimeUnixNano(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 1 { + err = objects.ErrWrongNumArguments + return + } + + t1, ok := objects.ToTime(args[0]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + ret = &objects.Int{Value: int64(t1.UnixNano())} + + return +} + +func timesTimeFormat(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 2 { + err = objects.ErrWrongNumArguments + return + } + + t1, ok := objects.ToTime(args[0]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + s2, ok := objects.ToString(args[1]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + ret = &objects.String{Value: t1.Format(s2)} + + return +} + +func timesIsZero(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 1 { + err = objects.ErrWrongNumArguments + return + } + + t1, ok := objects.ToTime(args[0]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + if t1.IsZero() { + ret = objects.TrueValue + } else { + ret = objects.FalseValue + } + + return +} + +func timesToLocal(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 1 { + err = objects.ErrWrongNumArguments + return + } + + t1, ok := objects.ToTime(args[0]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + ret = &objects.Time{Value: t1.Local()} + + return +} + +func timesToUTC(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 1 { + err = objects.ErrWrongNumArguments + return + } + + t1, ok := objects.ToTime(args[0]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + ret = &objects.Time{Value: t1.UTC()} + + return +} + +func timesTimeLocation(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 1 { + err = objects.ErrWrongNumArguments + return + } + + t1, ok := objects.ToTime(args[0]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + ret = &objects.String{Value: t1.Location().String()} + + return +} + +func timesTimeString(args ...objects.Object) (ret objects.Object, err error) { + if len(args) != 1 { + err = objects.ErrWrongNumArguments + return + } + + t1, ok := objects.ToTime(args[0]) + if !ok { + err = objects.ErrInvalidTypeConversion + return + } + + ret = &objects.String{Value: t1.String()} + + return +} diff --git a/compiler/stdlib/times_test.go b/compiler/stdlib/times_test.go new file mode 100644 index 00000000..11551dee --- /dev/null +++ b/compiler/stdlib/times_test.go @@ -0,0 +1,62 @@ +package stdlib_test + +import ( + "testing" + "time" + + "github.com/d5/tengo/assert" + "github.com/d5/tengo/objects" +) + +func TestTimes(t *testing.T) { + time1 := time.Date(1982, 9, 28, 19, 21, 44, 999, time.Now().Location()) + time2 := time.Now() + + module(t, "times").call("sleep", 1).expect(objects.UndefinedValue) + + assert.True(t, module(t, "times").call("since", time.Now().Add(-time.Hour)).o.(*objects.Int).Value > 3600000000000) + assert.True(t, module(t, "times").call("until", time.Now().Add(time.Hour)).o.(*objects.Int).Value < 3600000000000) + + module(t, "times").call("parse_duration", "1ns").expect(1) + module(t, "times").call("parse_duration", "1ms").expect(1000000) + module(t, "times").call("parse_duration", "1h").expect(3600000000000) + module(t, "times").call("duration_hours", 1800000000000).expect(0.5) + module(t, "times").call("duration_minutes", 1800000000000).expect(30.0) + module(t, "times").call("duration_nanoseconds", 100).expect(100) + module(t, "times").call("duration_seconds", 1000000).expect(0.001) + module(t, "times").call("duration_string", 1800000000000).expect("30m0s") + + module(t, "times").call("month_string", 1).expect("January") + module(t, "times").call("month_string", 12).expect("December") + + module(t, "times").call("date", 1982, 9, 28, 19, 21, 44, 999).expect(time1) + assert.True(t, module(t, "times").call("now").o.(*objects.Time).Value.Sub(time.Now()).Nanoseconds() < 100000000) // within 100ms + parsed, _ := time.Parse(time.RFC3339, "1982-09-28T19:21:44+07:00") + module(t, "times").call("parse", time.RFC3339, "1982-09-28T19:21:44+07:00").expect(parsed) + module(t, "times").call("unix", 1234325, 94493).expect(time.Unix(1234325, 94493)) + + module(t, "times").call("add", time2, 3600000000000).expect(time2.Add(time.Duration(3600000000000))) + module(t, "times").call("sub", time2, time2.Add(-time.Hour)).expect(3600000000000) + module(t, "times").call("add_date", time2, 1, 2, 3).expect(time2.AddDate(1, 2, 3)) + module(t, "times").call("after", time2, time2.Add(time.Hour)).expect(false) + module(t, "times").call("after", time2, time2.Add(-time.Hour)).expect(true) + module(t, "times").call("before", time2, time2.Add(time.Hour)).expect(true) + module(t, "times").call("before", time2, time2.Add(-time.Hour)).expect(false) + + module(t, "times").call("time_year", time1).expect(time1.Year()) + module(t, "times").call("time_month", time1).expect(int(time1.Month())) + module(t, "times").call("time_day", time1).expect(time1.Day()) + module(t, "times").call("time_hour", time1).expect(time1.Hour()) + module(t, "times").call("time_minute", time1).expect(time1.Minute()) + module(t, "times").call("time_second", time1).expect(time1.Second()) + module(t, "times").call("time_nanosecond", time1).expect(time1.Nanosecond()) + module(t, "times").call("time_unix", time1).expect(time1.Unix()) + module(t, "times").call("time_unix_nano", time1).expect(time1.UnixNano()) + module(t, "times").call("time_format", time1, time.RFC3339).expect(time1.Format(time.RFC3339)) + module(t, "times").call("is_zero", time1).expect(false) + module(t, "times").call("is_zero", time.Time{}).expect(true) + module(t, "times").call("to_local", time1).expect(time1.Local()) + module(t, "times").call("to_utc", time1).expect(time1.UTC()) + module(t, "times").call("time_location", time1).expect(time1.Location().String()) + module(t, "times").call("time_string", time1).expect(time1.String()) +} diff --git a/docs/runtime-types.md b/docs/runtime-types.md index 1863d2c5..eb1e6a95 100644 --- a/docs/runtime-types.md +++ b/docs/runtime-types.md @@ -7,15 +7,17 @@ - **Char**: character (`rune` in Go) - **Bytes**: byte array (`[]byte` in Go) - **Array**: objects array (`[]Object` in Go) +- **ImmutableArray**: immutable object array (`[]Object` in Go) - **Map**: objects map with string keys (`map[string]Object` in Go) - **ImmutableMap**: immutable object map with string keys (`map[string]Object` in Go) +- **Time**: time (`time.Time` in Go) - **Error**: an error with underlying Object value of any type - **Undefined**: undefined ## Type Conversion/Coercion Table -|src\dst |Int |String |Float |Bool |Char |Bytes |Array |Map |IMap|Error |Undefined| +|src\dst |Int |String |Float |Bool |Char |Bytes |Array |Map |Time |Error |Undefined| | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | :---: | -|Int | - |_strconv_ |float64(v)|!IsFalsy()| rune(v)|**X**|**X**|**X**|**X**|**X**|**X**| +|Int | - |_strconv_ |float64(v)|!IsFalsy()| rune(v)|**X**|**X**|**X**|_time.Unix()_|**X**|**X**| |String |_strconv_| - |_strconv_|!IsFalsy()|**X**|[]byte(s)|**X**|**X**|**X**|**X**|**X**| |Float |int64(f) |_strconv_ | - |!IsFalsy()|**X**|**X**|**X**|**X**|**X**|**X**|**X**| |Bool |1 / 0 |"true" / "false"|**X** | - |**X**|**X**|**X**|**X**|**X**|**X**|**X**| @@ -23,13 +25,15 @@ |Bytes |**X** |string(y)|**X** |!IsFalsy()|**X**| - |**X**|**X**|**X**|**X**|**X**| |Array |**X** |"[...]" |**X** |!IsFalsy()|**X**|**X**| - |**X**|**X**|**X**|**X**| |Map |**X** |"{...}" |**X** |!IsFalsy()|**X**|**X**|**X**| - |**X**|**X**|**X**| -|IMap |**X** |"{...}" |**X** |!IsFalsy()|**X**|**X**|**X**|**X**| - |**X**|**X**| +|Time |**X** |String() |**X** |!IsFalsy()|**X**|**X**|**X**|**X**| - |**X**|**X**| |Error |**X** |"error: ..." |**X** |false|**X**|**X**|**X**|**X**|**X**| - |**X**| |Undefined|**X** |**X**|**X** |false|**X**|**X**|**X**|**X**|**X**|**X**| - | _* **X**: No conversion; Typed value functions for `script.Variable` will return zero values._ _* strconv: converted using Go's conversion functions from `strconv` package._ -_* IsFalsy(): use [Object.IsFalsy()](#objectisfalsy) function_ +_* IsFalsy(): use [Object.IsFalsy()](#objectisfalsy) function_ +_* String(): use `Object.String()` function_ +_* time.Unix(): use `time.Unix(v, 0)` to convert to Time_ ## Object.IsFalsy() @@ -43,7 +47,7 @@ _* IsFalsy(): use [Object.IsFalsy()](#objectisfalsy) function_ - **Bytes**: `len(bytes) == 0` - **Array**: `len(arr) == 0` - **Map**: `len(map) == 0` -- **ImmutableMap**: `len(map) == 0` +- **Time**: `Time.IsZero()` - **Error**: `true` _(Error is always falsy)_ - **Undefined**: `true` _(Undefined is always falsy)_ @@ -56,6 +60,7 @@ _* IsFalsy(): use [Object.IsFalsy()](#objectisfalsy) function_ - `char(x)`: tries to convert `x` into char; returns `undefined` if failed - `bytes(x)`: tries to convert `x` into bytes; returns `undefined` if failed - `bytes(N)`: as a special case this will create a Bytes variable with the given size `N` (only if `N` is int) +- `time(x)`: tries to convert `x` into time; returns `undefined` if failed ## Type Checking Builtin Functions @@ -65,5 +70,10 @@ _* IsFalsy(): use [Object.IsFalsy()](#objectisfalsy) function_ - `is_float(x)`: returns `true` if `x` is float; `false` otherwise - `is_char(x)`: returns `true` if `x` is char; `false` otherwise - `is_bytes(x)`: returns `true` if `x` is bytes; `false` otherwise +- `is_array(x)`: return `true` if `x` is array; `false` otherwise +- `is_immutable_array(x)`: return `true` if `x` is immutable array; `false` otherwise +- `is_map(x)`: return `true` if `x` is map; `false` otherwise +- `is_immutable_map(x)`: return `true` if `x` is immutable map; `false` otherwise +- `is_time(x)`: return `true` if `x` is time; `false` otherwise - `is_error(x)`: returns `true` if `x` is error; `false` otherwise - `is_undefined(x)`: returns `true` if `x` is undefined; `false` otherwise \ No newline at end of file diff --git a/docs/stdlib-exec.md b/docs/stdlib-exec.md deleted file mode 100644 index 1d6c6845..00000000 --- a/docs/stdlib-exec.md +++ /dev/null @@ -1,28 +0,0 @@ -# Module - "exec" - -```golang -exec := import("exec") -``` - -## Module Functions - -- `look_path(file string) => string/error`: port of `exec.LookPath` function -- `command(name string, args array(string)) => `Cmd/error`: port of `exec.Command` function - -## Cmd Functions - -```golang -cmd := exec.command("echo", ["foo", "bar"]) -output := cmd.output() -``` - -- `combined_output() => bytes/error`: port of `exec.Cmd.CombinedOutput` function -- `output() => bytes/error`: port of `exec.Cmd.Output` function -- `combined_output() => bytes/error`: port of `exec.Cmd.CombinedOutput` function -- `run() => error`: port of `exec.Cmd.Run` function -- `start() => error`: port of `exec.Cmd.Start` function -- `wait() => error`: port of `exec.Cmd.Wait` function -- `set_path(path string)`: sets `Path` of `exec.Cmd` -- `set_dir(dir string)`: sets `Dir` of `exec.Cmd` -- `set_env(env array(string))`: sets `Env` of `exec.Cmd` -- `process() => Process`: returns Process (`Process` of `exec.Cmd`) \ No newline at end of file diff --git a/docs/stdlib-math.md b/docs/stdlib-math.md index d6b1c90e..9796b9d5 100644 --- a/docs/stdlib-math.md +++ b/docs/stdlib-math.md @@ -4,76 +4,72 @@ math := import("math") ``` -## Variables +## Constants -- `e`: equivalent of Go's `math.E` -- `pi`: equivalent of Go's `math.Pi` -- `phi`: equivalent of Go's `math.Phi` -- `sqrt2`: equivalent of Go's `math.Sqrt2` -- `sqrtE`: equivalent of Go's `math.SqrtE` -- `sprtPi`: equivalent of Go's `math.SqrtPi` -- `sqrtPhi`: equivalent of Go's `math.SqrtPhi` -- `ln2`: equivalent of Go's `math.Ln2` -- `log2E`: equivalent of Go's `math.Log2E` -- `ln10`: equivalent of Go's `math.Ln10` -- `ln10E`: equivalent of Go's `math.Log10E` +- `e` +- `pi` +- `phi` +- `sqrt2` +- `sqrtE` +- `sprtPi` +- `sqrtPhi` +- `ln2` +- `log2E` +- `ln10` +- `ln10E` ## Functions -- `abs(float) => float`: port of Go's `math.Abs` function -- `acos(float) => float`: port of Go's `math.Acos` function -- `acosh(float) => float`: port of Go's `math.Acosh` function -- `asin(float) => float`: port of Go's `math.Asin` function -- `asinh(float) => float`: port of Go's `math.Asinh` function -- `atan(float) => float`: port of Go's `math.Atan` function -- `atan2(float, float) => float`: port of Go's `math.Atan2` function -- `atanh(float) => float`: port of Go's `math.Atanh` function -- `cbrt(float) => float`: port of Go's `math.Cbrt` function -- `ceil(float) => float`: port of Go's `math.Ceil` function -- `copysign(float, float) => float`: port of Go's `math.Copysign` function -- `cos(float) => float`: port of Go's `math.Cos` function -- `cosh(float) => float`: port of Go's `math.Cosh` function -- `dim(float, float) => float`: port of Go's `math.Dim` function -- `erf(float) => float`: port of Go's `math.Erf` function -- `erfc(float) => float`: port of Go's `math.Erfc` function -- `erfcinv(float) => float`: port of Go's `math.Erfcinv` function -- `erfinv(float) => float`: port of Go's `math.Erfinv` function -- `exp(float) => float`: port of Go's `math.Exp` function -- `exp2(float) => float`: port of Go's `math.Exp2` function -- `expm1(float) => float`: port of Go's `math.Expm1` function -- `floor(float) => float`: port of Go's `math.Floor` function -- `gamma(float) => float`: port of Go's `math.Gamma` function -- `hypot(float, float) => float`: port of Go's `math.Hypot` function -- `ilogb(float) => float`: port of Go's `math.Ilogb` function -- `inf(int) => float`: port of Go's `math.Inf` function -- `is_inf(float, int) => float`: port of Go's `math.IsInf` function -- `is_nan(float) => float`: port of Go's `math.IsNaN` function -- `j0(float) => float`: port of Go's `math.J0` function -- `j1(float) => float`: port of Go's `math.J1` function -- `jn(int, float) => float`: port of Go's `math.Jn` function -- `ldexp(float, int) => float`: port of Go's `math.Ldexp` function -- `log(float) => float`: port of Go's `math.Log` function -- `log10(float) => float`: port of Go's `math.Log10` function -- `log1p(float) => float`: port of Go's `math.Log1p` function -- `log2(float) => float`: port of Go's `math.Log2` function -- `logb(float) => float`: port of Go's `math.Logb` function -- `max(float, float) => float`: port of Go's `math.Max` function -- `min(float, float) => float`: port of Go's `math.Min` function -- `mod(float, float) => float`: port of Go's `math.Mod` function -- `nan() => float`: port of Go's `math.NaN` function -- `nextafter(float, float) => float`: port of Go's `math.Nextafter` function -- `pow(float, float) => float`: port of Go's `math.Pow` function -- `pow10(int) => float`: port of Go's `math.Pow10` function -- `remainder(float, float) => float`: port of Go's `math.Remainder` function -- `round(float) => float`: port of Go's `math.Round` function -- `round_to_even(float) => float`: port of Go's `math.RoundToEven` function -- `signbit(float) => float`: port of Go's `math.Signbit` function -- `sin(float) => float`: port of Go's `math.Sin` function -- `sinh(float) => float`: port of Go's `math.Sinh` function -- `sqrt(float) => float`: port of Go's `math.Sqrt` function -- `tan(float) => float`: port of Go's `math.Tan` function -- `tanh(float) => float`: port of Go's `math.Tanh` function -- `runct(float) => float`: port of Go's `math.Trunc` function -- `y0(float) => float`: port of Go's `math.Y0` function -- `y1(float) => float`: port of Go's `math.Y1` function -- `yn(int, float) => float`: port of Go's `math.Yn` function \ No newline at end of file +- `abs(x float) => float`: returns the absolute value of x. +- `acos(x float) => float`: returns the arccosine, in radians, of x. +- `acosh(x float) => float`: returns the inverse hyperbolic cosine of x. +- `asin(x float) => float`: returns the arcsine, in radians, of x. +- `asinh(x float) => float`: returns the inverse hyperbolic sine of x. +- `atan(x float) => float`: returns the arctangent, in radians, of x. +- `atan2(y float, xfloat) => float`: returns the arc tangent of y/x, using the signs of the two to determine the quadrant of the return value. +- `atanh(x float) => float`: returns the inverse hyperbolic tangent of x. +- `cbrt(x float) => float`: returns the cube root of x. +- `ceil(x float) => float`: returns the least integer value greater than or equal to x. +- `copysign(x float, y float) => float`: returns a value with the magnitude of x and the sign of y. +- `cos(x float) => float`: returns the cosine of the radian argument x. +- `cosh(x float) => float`: returns the hyperbolic cosine of x. +- `dim(x float, y float) => float`: returns the maximum of x-y or 0. +- `erf(x float) => float`: returns the error function of x. +- `erfc(x float) => float`: returns the complementary error function of x. +- `exp(x float) => float`: returns e**x, the base-e exponential of x. +- `exp2(x float) => float`: returns 2**x, the base-2 exponential of x. +- `expm1(x float) => float`: returns e**x - 1, the base-e exponential of x minus 1. It is more accurate than Exp(x) - 1 when x is near zero. +- `floor(x float) => float`: returns the greatest integer value less than or equal to x. +- `gamma(x float) => float`: returns the Gamma function of x. +- `hypot(p float, q float) => float`: returns Sqrt(p * p + q * q), taking care to avoid unnecessary overflow and underflow. +- `ilogb(x float) => float`: returns the binary exponent of x as an integer. +- `inf(sign int) => float`: returns positive infinity if sign >= 0, negative infinity if sign < 0. +- `is_inf(f float, sign int) => float`: reports whether f is an infinity, according to sign. If sign > 0, IsInf reports whether f is positive infinity. If sign < 0, IsInf reports whether f is negative infinity. If sign == 0, IsInf reports whether f is either infinity. +- `is_nan(f float) => float`: reports whether f is an IEEE 754 ``not-a-number'' value. +- `j0(x float) => float`: returns the order-zero Bessel function of the first kind. +- `j1(x float) => float`: returns the order-one Bessel function of the first kind. +- `jn(n int, x float) => float`: returns the order-n Bessel function of the first kind. +- `ldexp(frac float, exp int) => float`: is the inverse of frexp. It returns frac × 2**exp. +- `log(x float) => float`: returns the natural logarithm of x. +- `log10(x float) => float`: returns the decimal logarithm of x. +- `log1p(x float) => float`: returns the natural logarithm of 1 plus its argument x. It is more accurate than Log(1 + x) when x is near zero. +- `log2(x float) => float`: returns the binary logarithm of x. +- `logb(x float) => float`: returns the binary exponent of x. +- `max(x float, y float) => float`: returns the larger of x or y. +- `min(x float, y float) => float`: returns the smaller of x or y. +- `mod(x float, y float) => float`: returns the floating-point remainder of x/y. +- `nan() => float`: returns an IEEE 754 ``not-a-number'' value. +- `nextafter(x float, y float) => float`: returns the next representable float64 value after x towards y. +- `pow(x float, y float) => float`: returns x**y, the base-x exponential of y. +- `pow10(n int) => float`: returns 10**n, the base-10 exponential of n. +- `remainder(x float, y float) => float`: returns the IEEE 754 floating-point remainder of x/y. +- `signbit(x float) => float`: returns true if x is negative or negative zero. +- `sin(x float) => float`: returns the sine of the radian argument x. +- `sinh(x float) => float`: returns the hyperbolic sine of x. +- `sqrt(x float) => float`: returns the square root of x. +- `tan(x float) => float`: returns the tangent of the radian argument x. +- `tanh(x float) => float`: returns the hyperbolic tangent of x. +- `trunc(x float) => float`: returns the integer value of x. +- `y0(x float) => float`: returns the order-zero Bessel function of the second kind. +- `y1(x float) => float`: returns the order-one Bessel function of the second kind. +- `yn(n int, x float) => float`: returns the order-n Bessel function of the second kind. \ No newline at end of file diff --git a/docs/stdlib-os.md b/docs/stdlib-os.md index 4e2324b0..b39fb26b 100644 --- a/docs/stdlib-os.md +++ b/docs/stdlib-os.md @@ -4,82 +4,84 @@ os := import("os") ``` -## Module Variables - -- `o_rdonly`: equivalent of Go's `os.O_RDONLY` -- `o_wronly`: equivalent of Go's `os.O_WRONLY` -- `o_rdwr`: equivalent of Go's `os.O_RDWR` -- `o_append`: equivalent of Go's `os.O_APPEND` -- `o_create`: equivalent of Go's `os.O_CREATE` -- `o_excl`: equivalent of Go's `os.O_EXCL` -- `o_sync`: equivalent of Go's `os.O_SYNC` -- `o_trunc`: equivalent of Go's `os.O_TRUNC` -- `mode_dir`: equivalent of Go's `os.ModeDir` -- `mode_append`: equivalent of Go's `os.ModeAppend` -- `mode_exclusive`: equivalent of Go's `os.ModeExclusive` -- `mode_temporary`: equivalent of Go's `os.ModeTemporary` -- `mode_symlink`: equivalent of Go's `os.ModeSymlink` -- `mode_device`: equivalent of Go's `os.ModeDevice` -- `mode_named_pipe`: equivalent of Go's `os.ModeNamedPipe` -- `mode_socket`: equivalent of Go's `os.ModeSocket` -- `mode_setuid`: equivalent of Go's `os.ModeSetuid` -- `mode_setgui`: equivalent of Go's `os.ModeSetgid` -- `mode_char_device`: equivalent of Go's `os.ModeCharDevice` -- `mode_sticky`: equivalent of Go's `os.ModeSticky` -- `mode_irregular`: equivalent of Go's `os.ModeIrregular` -- `mode_type`: equivalent of Go's `os.ModeType` -- `mode_perm`: equivalent of Go's `os.ModePerm` -- `seek_set`: equivalent of Go's `os.SEEK_SET` -- `seek_cur`: equivalent of Go's `os.SEEK_CUR` -- `seek_end`: equivalent of Go's `os.SEEK_END` -- `path_separator`: equivalent of Go's `os.PathSeparator` -- `path_list_separator`: equivalent of Go's `os.PathListSeparator` -- `dev_null`: equivalent of Go's `os.DevNull` - -## Module Functions - -- `args() => array(string)`: returns `os.Args` -- `chdir(dir string) => error`: port of `os.Chdir` function -- `chmod(name string, mode int) => error `: port of Go's `os.Chmod` function -- `chown(name string, uid int, gid int) => error `: port of Go's `os.Chown` function -- `clearenv() `: port of Go's `os.Clearenv` function -- `environ() => array(string) `: port of Go's `os.Environ` function -- `executable() => string/error`: port of Go's `os.Executable()` function -- `exit(code int) `: port of Go's `os.Exit` function -- `expand_env(s string) => string `: port of Go's `os.ExpandEnv` function -- `getegid() => int `: port of Go's `os.Getegid` function -- `getenv(s string) => string `: port of Go's `os.Getenv` function -- `geteuid() => int `: port of Go's `os.Geteuid` function -- `getgid() => int `: port of Go's `os.Getgid` function -- `getgroups() => array(string)/error `: port of Go's `os.Getgroups` function -- `getpagesize() => int `: port of Go's `os.Getpagesize` function -- `getpid() => int `: port of Go's `os.Getpid` function -- `getppid() => int `: port of Go's `os.Getppid` function -- `getuid() => int `: port of Go's `os.Getuid` function -- `getwd() => string/error `: port of Go's `os.Getwd` function -- `hostname() => string/error `: port of Go's `os.Hostname` function -- `lchown(name string, uid int, gid int) => error `: port of Go's `os.Lchown` function -- `link(oldname string, newname string) => error `: port of Go's `os.Link` function -- `lookup_env(key string) => string/false`: port of Go's `os,LookupEnv` function -- `mkdir(name string, perm int) => error `: port of Go's `os.Mkdir` function -- `mkdir_all(name string, perm int) => error `: port of Go's `os.MkdirAll` function -- `readlink(name string) => string/error `: port of Go's `os.Readlink` function -- `remove(name string) => error `: port of Go's `os.Remove` function -- `remove_all(name string) => error `: port of Go's `os.RemoveAll` function -- `rename(oldpath string, newpath string) => error `: port of Go's `os.Rename` function -- `setenv(key string, value string) => error `: port of Go's `os.Setenv` function -- `symlink(oldname string newname string) => error `: port of Go's `os.Symlink` function -- `temp_dir() => string `: port of Go's `os.TempDir` function -- `truncate(name string, size int) => error `: port of Go's `os.Truncate` function -- `unsetenv(key string) => error `: port of Go's `os.Unsetenv` function -- `user_cache_dir() => string/error `: port of Go's `os.UserCacheDir` function -- `create(name string) => File/error`: port of Go's `os.Create` function -- `open(name string) => File/error`: port of Go's `os.Open` function -- `open_file(name string, flag int, perm int) => File/error`: port of Go's `os.OpenFile` function -- `find_process(pid int) => Process/error`: port of Go's `os.FindProcess` function -- `start_process(name string, argv array(string), dir string, env array(string)) => Process/error`: port of Go's `os.StartProcess` function - -## File Functions +## Constants + +- `o_rdonly` +- `o_wronly` +- `o_rdwr` +- `o_append` +- `o_create` +- `o_excl` +- `o_sync` +- `o_trunc` +- `mode_dir` +- `mode_append` +- `mode_exclusive` +- `mode_temporary` +- `mode_symlink` +- `mode_device` +- `mode_named_pipe` +- `mode_socket` +- `mode_setuid` +- `mode_setgui` +- `mode_char_device` +- `mode_sticky` +- `mode_irregular` +- `mode_type` +- `mode_perm` +- `seek_set` +- `seek_cur` +- `seek_end` +- `path_separator` +- `path_list_separator` +- `dev_null` + + +## Functions + +- `args() => [string]`: returns command-line arguments, starting with the program name. +- `chdir(dir string) => error`: changes the current working directory to the named directory. +- `chmod(name string, mode int) => error `: changes the mode of the named file to mode. +- `chown(name string, uid int, gid int) => error `: changes the numeric uid and gid of the named file. +- `clearenv()`: deletes all environment variables. +- `environ() => [string] `: returns a copy of strings representing the environment. +- `exit(code int) `: causes the current program to exit with the given status code. +- `expand_env(s string) => string `: replaces ${var} or $var in the string according to the values of the current environment variables. +- `getegid() => int `: returns the numeric effective group id of the caller. +- `getenv(key string) => string `: retrieves the value of the environment variable named by the key. +- `geteuid() => int `: returns the numeric effective user id of the caller. +- `getgid() => int `: returns the numeric group id of the caller. +- `getgroups() => [int]/error `: returns a list of the numeric ids of groups that the caller belongs to. +- `getpagesize() => int `: returns the underlying system's memory page size. +- `getpid() => int `: returns the process id of the caller. +- `getppid() => int `: returns the process id of the caller's parent. +- `getuid() => int `: returns the numeric user id of the caller. +- `getwd() => string/error `: returns a rooted path name corresponding to the current directory. +- `hostname() => string/error `: returns the host name reported by the kernel. +- `lchown(name string, uid int, gid int) => error `: changes the numeric uid and gid of the named file. +- `link(oldname string, newname string) => error `: creates newname as a hard link to the oldname file. +- `lookup_env(key string) => string/false`: retrieves the value of the environment variable named by the key. +- `mkdir(name string, perm int) => error `: creates a new directory with the specified name and permission bits (before umask). +- `mkdir_all(name string, perm int) => error `: creates a directory named path, along with any necessary parents, and returns nil, or else returns an error. +- `readlink(name string) => string/error `: returns the destination of the named symbolic link. +- `remove(name string) => error `: removes the named file or (empty) directory. +- `remove_all(name string) => error `: removes path and any children it contains. +- `rename(oldpath string, newpath string) => error `: renames (moves) oldpath to newpath. +- `setenv(key string, value string) => error `: sets the value of the environment variable named by the key. +- `symlink(oldname string newname string) => error `: creates newname as a symbolic link to oldname. +- `temp_dir() => string `: returns the default directory to use for temporary files. +- `truncate(name string, size int) => error `: changes the size of the named file. +- `unsetenv(key string) => error `: unsets a single environment variable. +- `create(name string) => File/error`: creates the named file with mode 0666 (before umask), truncating it if it already exists. +- `open(name string) => File/error`: opens the named file for reading. If successful, methods on the returned file can be used for reading; the associated file descriptor has mode O_RDONLY. +- `open_file(name string, flag int, perm int) => File/error`: is the generalized open call; most users will use Open or Create instead. It opens the named file with specified flag (O_RDONLY etc.) and perm (before umask), if applicable. +- `find_process(pid int) => Process/error`: looks for a running process by its pid. +- `start_process(name string, argv [string], dir string, env [string]) => Process/error`: starts a new process with the program, arguments and attributes specified by name, argv and attr. The argv slice will become os.Args in the new process, so it normally starts with the program name. +- `exec_look_path(file string) => string/error`: searches for an executable named file in the directories named by the PATH environment variable. +- `exec(name string, args...) => Command/error`: returns the Command to execute the named program with the given arguments. + + +## File ```golang file := os.create("myfile") @@ -87,31 +89,31 @@ file.write_string("some data") file.close() ``` -- `chdir() => true/error`: port of `os.File.Chdir` function -- `chown(uid int, gid int) => true/error`: port of `os.File.Chown` function -- `close() => error`: port of `os.File.Close` function -- `name() => string`: port of `os.File.Name` function -- `readdirnames() => array(string)/error`: port of `os.File.Readdirnames` function -- `sync() => error`: port of `os.File.Sync` function -- `write(bytes) => int/error`: port of `os.File.Write` function -- `write_string(string) => int/error`: port of `os.File.WriteString` function -- `read(bytes) => int/error`: port of `os.File.Read` function -- `chmod(mode int) => error`: port of `os.File.Chmod` function -- `seek(offset int, whence int) => int/error`: port of `os.File.Seek` function +- `chdir() => true/error`: changes the current working directory to the file, +- `chown(uid int, gid int) => true/error`: changes the numeric uid and gid of the named file. +- `close() => error`: closes the File, rendering it unusable for I/O. +- `name() => string`: returns the name of the file as presented to Open. +- `readdirnames(n int) => [string]/error`: reads and returns a slice of names from the directory. +- `sync() => error`: commits the current contents of the file to stable storage. +- `write(bytes) => int/error`: writes len(b) bytes to the File. +- `write_string(string) => int/error`: is like 'write', but writes the contents of string s rather than a slice of bytes. +- `read(bytes) => int/error`: reads up to len(b) bytes from the File. +- `chmod(mode int) => error`: changes the mode of the file to mode. +- `seek(offset int, whence int) => int/error`: sets the offset for the next Read or Write on file to offset, interpreted according to whence: 0 means relative to the origin of the file, 1 means relative to the current offset, and 2 means relative to the end. -## Process Functions +## Process ```golang proc := start_process("app", ["arg1", "arg2"], "dir", []) proc.wait() ``` -- `kill() => error`: port of `os.Process.Kill` function -- `release() => error`: port of `os.Process.Release` function -- `signal(signal int) => error`: port of `os.Process.Signal` function -- `wait() => ProcessState/error`: port of `os.Process.Wait` function +- `kill() => error`: causes the Process to exit immediately. +- `release() => error`: releases any resources associated with the process, rendering it unusable in the future. +- `signal(signal int) => error`: sends a signal to the Process. +- `wait() => ProcessState/error`: waits for the Process to exit, and then returns a ProcessState describing its status and an error, if any. -## ProcessState Functions +## ProcessState ```golang proc := start_process("app", ["arg1", "arg2"], "dir", []) @@ -119,7 +121,24 @@ stat := proc.wait() pid := stat.pid() ``` -- `exited() => bool`: port of `os.ProcessState.Exited` function -- `pid() => int`: port of `os.ProcessState.Pid` function -- `string() => string`: port of `os.ProcessState.String` function -- `success() => bool`: port of `os.ProcessState.Success` function \ No newline at end of file +- `exited() => bool`: reports whether the program has exited. +- `pid() => int`: returns the process id of the exited process. +- `string() => string`: returns a string representation of the process. +- `success() => bool`: reports whether the program exited successfully, such as with exit status 0 on Unix. + +```golang +cmd := exec.command("echo", ["foo", "bar"]) +output := cmd.output() +``` + +## Command + +- `combined_output() => bytes/error`: runs the command and returns its combined standard output and standard error. +- `output() => bytes/error`: runs the command and returns its standard output. +- `run() => error`: starts the specified command and waits for it to complete. +- `start() => error`: starts the specified command but does not wait for it to complete. +- `wait() => error`: waits for the command to exit and waits for any copying to stdin or copying from stdout or stderr to complete. +- `set_path(path string)`: sets the path of the command to run. +- `set_dir(dir string)`: sets the working directory of the process. +- `set_env(env [string])`: sets the environment of the process. +- `process() => Process`: returns the underlying process, once started. \ No newline at end of file diff --git a/docs/stdlib-text.md b/docs/stdlib-text.md new file mode 100644 index 00000000..db7d1b40 --- /dev/null +++ b/docs/stdlib-text.md @@ -0,0 +1,58 @@ +# Module - "text" + +```golang +text := import("text") +``` + +## Functions + +- `re_match(pattern string, text string) => bool/error`: reports whether the string s contains any match of the regular expression pattern. +- `re_find(pattern string, text string, count int) => [[{text: string, begin: int, end: int}]]/undefined`: returns an array holding all matches, each of which is an array of map object that contains matching text, begin and end (exclusive) index. +- `re_replace(pattern string, text string, repl string) => string/error`: returns a copy of src, replacing matches of the pattern with the replacement string repl. +- `re_split(pattern string, text string, count int) => [string]/error`: slices s into substrings separated by the expression and returns a slice of the substrings between those expression matches. +- `re_compile(pattern string) => Regexp/error`: parses a regular expression and returns, if successful, a Regexp object that can be used to match against text. +- `compare(a string, b string) => int`: returns an integer comparing two strings lexicographically. The result will be 0 if a==b, -1 if a < b, and +1 if a > b. +- `contains(s string, substr string) => bool`: reports whether substr is within s. +- `contains_any(s string, chars string) => bool`: reports whether any Unicode code points in chars are within s. +- `count(s string, substr string) => int`: counts the number of non-overlapping instances of substr in s. +- `equal_fold(s string, t string) => bool`: reports whether s and t, interpreted as UTF-8 strings, +- `fields(s string) => [string]`: splits the string s around each instance of one or more consecutive white space characters, as defined by unicode.IsSpace, returning a slice of substrings of s or an empty slice if s contains only white space. +- `has_prefix(s string, prefix string) => bool`: tests whether the string s begins with prefix. +- `has_suffix(s string, suffix string) => bool`: tests whether the string s ends with suffix. +- `index(s string, substr string) => int`: returns the index of the first instance of substr in s, or -1 if substr is not present in s. +- `index_any(s string, chars string) => int`: returns the index of the first instance of any Unicode code point from chars in s, or -1 if no Unicode code point from chars is present in s. +- `join(arr string, sep string) => string`: concatenates the elements of a to create a single string. The separator string sep is placed between elements in the resulting string. +- `last_index(s string, substr string) => int`: returns the index of the last instance of substr in s, or -1 if substr is not present in s. +- `last_index_any(s string, chars string) => int`: returns the index of the last instance of any Unicode code point from chars in s, or -1 if no Unicode code point from chars is present in s. +- `repeat(s string, count int) => string`: returns a new string consisting of count copies of the string s. +- `replace(s string, old string, new string, n int) => string`: returns a copy of the string s with the first n non-overlapping instances of old replaced by new. +- `split(s string, sep string) => [string]`: slices s into all substrings separated by sep and returns a slice of the substrings between those separators. +- `split_after(s string, sep string) => [string]`: slices s into all substrings after each instance of sep and returns a slice of those substrings. +- `split_after_n(s string, sep string, n int) => [string]`: slices s into substrings after each instance of sep and returns a slice of those substrings. +- `split_n(s string, sep string, n int) => [string]`: slices s into substrings separated by sep and returns a slice of the substrings between those separators. +- `title(s string) => string`: returns a copy of the string s with all Unicode letters that begin words mapped to their title case. +- `to_lower(s string) => string`: returns a copy of the string s with all Unicode letters mapped to their lower case. +- `to_title(s string) => string`: returns a copy of the string s with all Unicode letters mapped to their title case. +- `to_upper(s string) => string`: returns a copy of the string s with all Unicode letters mapped to their upper case. +- `trim_left(s string, cutset string) => string`: returns a slice of the string s with all leading Unicode code points contained in cutset removed. +- `trim_prefix(s string, prefix string) => string`: returns s without the provided leading prefix string. +- `trim_right(s string, cutset string) => string`: returns a slice of the string s, with all trailing Unicode code points contained in cutset removed. +- `trim_space(s string) => string`: returns a slice of the string s, with all leading and trailing white space removed, as defined by Unicode. +- `trim_suffix(s string, suffix string) => string`: returns s without the provided trailing suffix string. +- `atoi(str string) => int/error`: returns the result of ParseInt(s, 10, 0) converted to type int. +- `format_bool(b bool) => string`: returns "true" or "false" according to the value of b. +- `format_float(f float, fmt string, prec int, bits int) => string`: converts the floating-point number f to a string, according to the format fmt and precision prec. +- `format_int(i int, base int) => string`: returns the string representation of i in the given base, for 2 <= base <= 36. The result uses the lower-case letters 'a' to 'z' for digit values >= 10. +- `itoa(i int) => string`: is shorthand for format_int(i, 10). +- `parse_bool(s string) => bool/error`: returns the boolean value represented by the string. It accepts 1, t, T, TRUE, true, True, 0, f, F, FALSE, false, False. Any other value returns an error. +- `parse_float(s string, bits int) => float/error`: converts the string s to a floating-point number with the precision specified by bitSize: 32 for float32, or 64 for float64. When bitSize=32, the result still has type float64, but it will be convertible to float32 without changing its value. +- `parse_int(s string, base int, bits int) => int/error`: interprets a string s in the given base (0, 2 to 36) and bit size (0 to 64) and returns the corresponding value i. +- `quote(s string) => string`: returns a double-quoted Go string literal representing s. The returned string uses Go escape sequences (\t, \n, \xFF, \u0100) for control characters and non-printable characters as defined by IsPrint. +- `unquote(s string) => string/error`: interprets s as a single-quoted, double-quoted, or backquoted Go string literal, returning the string value that s quotes. (If s is single-quoted, it would be a Go character literal; Unquote returns the corresponding one-character string.) + +## Regexp + +- `match(text string) => bool`: reports whether the string s contains any match of the regular expression pattern. +- `find(text string, count int) => [[{text: string, begin: int, end: int}]]/undefined`: returns an array holding all matches, each of which is an array of map object that contains matching text, begin and end (exclusive) index. +- `replace(src string, repl string) => string`: returns a copy of src, replacing matches of the pattern with the replacement string repl. +- `split(text string, count int) => [string]`: slices s into substrings separated by the expression and returns a slice of the substrings between those expression matches. \ No newline at end of file diff --git a/docs/stdlib-times.md b/docs/stdlib-times.md new file mode 100644 index 00000000..bd03ecc0 --- /dev/null +++ b/docs/stdlib-times.md @@ -0,0 +1,79 @@ +# Module - "times" + +```golang +times := import("times") +``` + +## Constants + +- `format_ansic`: time format "Mon Jan _2 15:04:05 2006" +- `format_unix_date`: time format "Mon Jan _2 15:04:05 MST 2006" +- `format_ruby_date`: time format "Mon Jan 02 15:04:05 -0700 2006" +- `format_rfc822`: time format "02 Jan 06 15:04 MST" +- `format_rfc822z`: time format "02 Jan 06 15:04 -0700" +- `format_rfc850`: time format "Monday, 02-Jan-06 15:04:05 MST" +- `format_rfc1123`: time format "Mon, 02 Jan 2006 15:04:05 MST" +- `format_rfc1123z`: time format "Mon, 02 Jan 2006 15:04:05 -0700" +- `format_rfc3339`: time format "2006-01-02T15:04:05Z07:00" +- `format_rfc3339_nano`: time format "2006-01-02T15:04:05.999999999Z07:00" +- `format_kitchen`: time format "3:04PM" +- `format_stamp`: time format "Jan _2 15:04:05" +- `format_stamp_milli`: time format "Jan _2 15:04:05.000" +- `format_stamp_micro`: time format "Jan _2 15:04:05.000000" +- `format_stamp_nano`: time format "Jan _2 15:04:05.000000000" +- `nanosecond` +- `microsecond` +- `millisecond` +- `second` +- `minute` +- `hour` +- `january` +- `february` +- `march` +- `april` +- `may` +- `june` +- `july` +- `august` +- `september` +- `october` +- `november` +- `december` + +## Functions + +- `sleep(duration int)`: pauses the current goroutine for at least the duration d. A negative or zero duration causes Sleep to return immediately. +- `parse_duration(s string) => int`: parses a duration string. A duration string is a possibly signed sequence of decimal numbers, each with optional fraction and a unit suffix, such as "300ms", "-1.5h" or "2h45m". Valid time units are "ns", "us" (or "µs"), "ms", "s", "m", "h". +- `since(t time) => int`: returns the time elapsed since t. +- `until(t time) => int`: returns the duration until t. +- `duration_hours(duration int) => float`: returns the duration as a floating point number of hours. +- `duration_minutes(duration int) => float`: returns the duration as a floating point number of minutes. +- `duration_nanoseconds(duration int) => int`: returns the duration as an integer of nanoseconds. +- `duration_seconds(duration int) => float`: returns the duration as a floating point number of seconds. +- `duration_string(duration int) => string`: returns a string representation of duration. +- `month_string(month int) => string`: returns the English name of the month ("January", "February", ...). +- `date(year int, month int, day int, hour int, min int, sec int, nsec int) => time`: returns the Time corresponding to "yyyy-mm-dd hh:mm:ss + nsec nanoseconds". Current location is used. +- `now() => time`: returns the current local time. +- `parse(format string, s string) => time`: parses a formatted string and returns the time value it represents. The layout defines the format by showing how the reference time, defined to be "Mon Jan 2 15:04:05 -0700 MST 2006" would be interpreted if it were the value; it serves as an example of the input format. The same interpretation will then be made to the input string. +- `unix(sec int, nsec int) => time`: returns the local Time corresponding to the given Unix time, sec seconds and nsec nanoseconds since January 1, 1970 UTC. +- `add(t time, duration int) => time`: returns the time t+d. +- `add_date(t time, years int, months int, days int) => time`: returns the time corresponding to adding the given number of years, months, and days to t. For example, AddDate(-1, 2, 3) applied to January 1, 2011 returns March 4, 2010. +- `sub(t time, u time) => int`: returns the duration t-u. +- `after(t time, u time) => bool`: reports whether the time instant t is after u. +- `before(t time, u time) => bool`: reports whether the time instant t is before u. +- `time_year(t time) => int`: returns the year in which t occurs. +- `time_month(t time) => int`: returns the month of the year specified by t. +- `time_day(t time) => int`: returns the day of the month specified by t. +- `time_weekday(t time) => int`: returns the day of the week specified by t. +- `time_hour(t time) => int`: returns the hour within the day specified by t, in the range [0, 23]. +- `time_minute(t time) => int`: returns the minute offset within the hour specified by t, in the range [0, 59]. +- `time_second(t time) => int`: returns the second offset within the minute specified by t, in the range [0, 59]. +- `time_nanosecond(t time) => int`: returns the nanosecond offset within the second specified by t, in the range [0, 999999999]. +- `time_unix(t time) => int`: returns t as a Unix time, the number of seconds elapsed since January 1, 1970 UTC. The result does not depend on the location associated with t. +- `time_unix_nano(t time) => int`: returns t as a Unix time, the number of nanoseconds elapsed since January 1, 1970 UTC. The result is undefined if the Unix time in nanoseconds cannot be represented by an int64 (a date before the year 1678 or after 2262). Note that this means the result of calling UnixNano on the zero Time is undefined. The result does not depend on the location associated with t. +- `time_format(t time, format) => string`: returns a textual representation of the time value formatted according to layout, which defines the format by showing how the reference time, defined to be "Mon Jan 2 15:04:05 -0700 MST 2006" would be displayed if it were the value; it serves as an example of the desired output. The same display rules will then be applied to the time value. +- `time_location(t time) => string`: returns the time zone name associated with t. +- `time_string(t time) => string`: returns the time formatted using the format string "2006-01-02 15:04:05.999999999 -0700 MST". +- `is_zero(t time) => bool`: reports whether t represents the zero time instant, January 1, year 1, 00:00:00 UTC. +- `to_local(t time) => time`: returns t with the location set to local time. +- `to_utc(t time) => time`: returns t with the location set to UTC. \ No newline at end of file diff --git a/docs/stdlib.md b/docs/stdlib.md index e5c5940e..facce101 100644 --- a/docs/stdlib.md +++ b/docs/stdlib.md @@ -1,7 +1,6 @@ # Standard Library -_Warning: standard library implementations/interfaces are **experimental** and subject to change in the future release._ - -- [math](https://github.com/d5/tengo/blob/master/docs/stdlib-math.md) -- [os](https://github.com/d5/tengo/blob/master/docs/stdlib-os.md) -- [exec](https://github.com/d5/tengo/blob/master/docs/stdlib-exec.md) \ No newline at end of file +- [os](https://github.com/d5/tengo/blob/master/docs/stdlib-os.md): platform-independent interface to operating system functionality. +- [text](https://github.com/d5/tengo/blob/master/docs/stdlib-text.md): regular expressions, string conversion, and manipulation +- [math](https://github.com/d5/tengo/blob/master/docs/stdlib-math.md): mathematical constants and functions +- [times](https://github.com/d5/tengo/blob/master/docs/stdlib-times.md): time-related functions \ No newline at end of file diff --git a/objects/bool.go b/objects/bool.go index b7ca19cf..ac9949e4 100644 --- a/objects/bool.go +++ b/objects/bool.go @@ -6,11 +6,12 @@ import ( // Bool represents a boolean value. type Bool struct { - Value bool + // this is intentionally non-public to force using objects.TrueValue and FalseValue always + value bool } func (o *Bool) String() string { - if o.Value { + if o.value { return "true" } @@ -30,22 +31,34 @@ func (o *Bool) BinaryOp(op token.Token, rhs Object) (Object, error) { // Copy returns a copy of the type. func (o *Bool) Copy() Object { - v := Bool{Value: o.Value} - return &v + return o } // IsFalsy returns true if the value of the type is falsy. func (o *Bool) IsFalsy() bool { - return !o.Value + return !o.value } // Equals returns true if the value of the type // is equal to the value of another object. func (o *Bool) Equals(x Object) bool { - t, ok := x.(*Bool) - if !ok { - return false + return o == x +} + +// GobDecode decodes bool value from input bytes. +func (o *Bool) GobDecode(b []byte) (err error) { + o.value = b[0] == 1 + + return +} + +// GobEncode encodes bool values into bytes. +func (o *Bool) GobEncode() (b []byte, err error) { + if o.value { + b = []byte{1} + } else { + b = []byte{0} } - return o.Value == t.Value + return } diff --git a/objects/builtin_convert.go b/objects/builtin_convert.go index cd2823d1..7d9a8733 100644 --- a/objects/builtin_convert.go +++ b/objects/builtin_convert.go @@ -77,7 +77,11 @@ func builtinBool(args ...Object) (Object, error) { v, ok := ToBool(args[0]) if ok { - return &Bool{Value: v}, nil + if v { + return TrueValue, nil + } + + return FalseValue, nil } return UndefinedValue, nil @@ -127,3 +131,25 @@ func builtinBytes(args ...Object) (Object, error) { return UndefinedValue, nil } + +func builtinTime(args ...Object) (Object, error) { + argsLen := len(args) + if !(argsLen == 1 || argsLen == 2) { + return nil, ErrWrongNumArguments + } + + if _, ok := args[0].(*Time); ok { + return args[0], nil + } + + v, ok := ToTime(args[0]) + if ok { + return &Time{Value: v}, nil + } + + if argsLen == 2 { + return args[1], nil + } + + return UndefinedValue, nil +} diff --git a/objects/builtin_type_checks.go b/objects/builtin_type_checks.go index b2af0580..e39cc9f0 100644 --- a/objects/builtin_type_checks.go +++ b/objects/builtin_type_checks.go @@ -72,6 +72,66 @@ func builtinIsBytes(args ...Object) (Object, error) { return FalseValue, nil } +func builtinIsArray(args ...Object) (Object, error) { + if len(args) != 1 { + return nil, ErrWrongNumArguments + } + + if _, ok := args[0].(*Array); ok { + return TrueValue, nil + } + + return FalseValue, nil +} + +func builtinIsImmutableArray(args ...Object) (Object, error) { + if len(args) != 1 { + return nil, ErrWrongNumArguments + } + + if _, ok := args[0].(*ImmutableArray); ok { + return TrueValue, nil + } + + return FalseValue, nil +} + +func builtinIsMap(args ...Object) (Object, error) { + if len(args) != 1 { + return nil, ErrWrongNumArguments + } + + if _, ok := args[0].(*Map); ok { + return TrueValue, nil + } + + return FalseValue, nil +} + +func builtinIsImmutableMap(args ...Object) (Object, error) { + if len(args) != 1 { + return nil, ErrWrongNumArguments + } + + if _, ok := args[0].(*ImmutableMap); ok { + return TrueValue, nil + } + + return FalseValue, nil +} + +func builtinIsTime(args ...Object) (Object, error) { + if len(args) != 1 { + return nil, ErrWrongNumArguments + } + + if _, ok := args[0].(*Time); ok { + return TrueValue, nil + } + + return FalseValue, nil +} + func builtinIsError(args ...Object) (Object, error) { if len(args) != 1 { return nil, ErrWrongNumArguments @@ -89,7 +149,7 @@ func builtinIsUndefined(args ...Object) (Object, error) { return nil, ErrWrongNumArguments } - if _, ok := args[0].(*Undefined); ok { + if args[0] == UndefinedValue { return TrueValue, nil } diff --git a/objects/builtins.go b/objects/builtins.go index 11b7f223..30049e9d 100644 --- a/objects/builtins.go +++ b/objects/builtins.go @@ -56,6 +56,10 @@ var Builtins = []NamedBuiltinFunc{ Name: "bytes", Func: builtinBytes, }, + { + Name: "time", + Func: builtinTime, + }, { Name: "is_int", Func: builtinIsInt, @@ -80,6 +84,26 @@ var Builtins = []NamedBuiltinFunc{ Name: "is_bytes", Func: builtinIsBytes, }, + { + Name: "is_array", + Func: builtinIsArray, + }, + { + Name: "is_immutable_array", + Func: builtinIsImmutableArray, + }, + { + Name: "is_map", + Func: builtinIsMap, + }, + { + Name: "is_immutable_map", + Func: builtinIsImmutableMap, + }, + { + Name: "is_time", + Func: builtinIsTime, + }, { Name: "is_error", Func: builtinIsError, diff --git a/objects/conversion.go b/objects/conversion.go index 6b762a37..5cdb8b92 100644 --- a/objects/conversion.go +++ b/objects/conversion.go @@ -3,11 +3,12 @@ package objects import ( "fmt" "strconv" + "time" ) // ToString will try to convert object o to string value. func ToString(o Object) (v string, ok bool) { - if _, isUndefined := o.(*Undefined); isUndefined { + if o == UndefinedValue { //ok = false return } @@ -36,7 +37,7 @@ func ToInt(o Object) (v int, ok bool) { v = int(o.Value) ok = true case *Bool: - if o.Value { + if o == TrueValue { v = 1 } ok = true @@ -65,7 +66,7 @@ func ToInt64(o Object) (v int64, ok bool) { v = int64(o.Value) ok = true case *Bool: - if o.Value { + if o == TrueValue { v = 1 } ok = true @@ -140,6 +141,21 @@ func ToByteSlice(o Object) (v []byte, ok bool) { return } +// ToTime will try to convert object o to time.Time value. +func ToTime(o Object) (v time.Time, ok bool) { + switch o := o.(type) { + case *Time: + v = o.Value + ok = true + case *Int: + v = time.Unix(o.Value, 0) + ok = true + } + + //ok = false + return +} + // objectToInterface attempts to convert an object o to an interface{} value func objectToInterface(o Object) (res interface{}) { switch o := o.(type) { @@ -150,7 +166,7 @@ func objectToInterface(o Object) (res interface{}) { case *Float: res = o.Value case *Bool: - res = o.Value + res = o == TrueValue case *Char: res = o.Value case *Bytes: @@ -176,7 +192,7 @@ func objectToInterface(o Object) (res interface{}) { func FromInterface(v interface{}) (Object, error) { switch v := v.(type) { case nil: - return &Undefined{}, nil + return UndefinedValue, nil case string: return &String{Value: v}, nil case int64: @@ -184,7 +200,10 @@ func FromInterface(v interface{}) (Object, error) { case int: return &Int{Value: int64(v)}, nil case bool: - return &Bool{Value: v}, nil + if v { + return TrueValue, nil + } + return FalseValue, nil case rune: return &Char{Value: v}, nil case byte: @@ -220,6 +239,8 @@ func FromInterface(v interface{}) (Object, error) { arr[i] = vo } return &Array{Value: arr}, nil + case time.Time: + return &Time{Value: v}, nil case Object: return v, nil } diff --git a/objects/float_test.go b/objects/float_test.go index 971b26e5..c7f89c2c 100644 --- a/objects/float_test.go +++ b/objects/float_test.go @@ -41,28 +41,28 @@ func TestFloat_BinaryOp(t *testing.T) { // float < float for l := float64(-2); l <= 2.1; l += 0.4 { for r := float64(-2); r <= 2.1; r += 0.4 { - testBinaryOp(t, &objects.Float{Value: l}, token.Less, &objects.Float{Value: r}, &objects.Bool{Value: l < r}) + testBinaryOp(t, &objects.Float{Value: l}, token.Less, &objects.Float{Value: r}, boolValue(l < r)) } } // float > float for l := float64(-2); l <= 2.1; l += 0.4 { for r := float64(-2); r <= 2.1; r += 0.4 { - testBinaryOp(t, &objects.Float{Value: l}, token.Greater, &objects.Float{Value: r}, &objects.Bool{Value: l > r}) + testBinaryOp(t, &objects.Float{Value: l}, token.Greater, &objects.Float{Value: r}, boolValue(l > r)) } } // float <= float for l := float64(-2); l <= 2.1; l += 0.4 { for r := float64(-2); r <= 2.1; r += 0.4 { - testBinaryOp(t, &objects.Float{Value: l}, token.LessEq, &objects.Float{Value: r}, &objects.Bool{Value: l <= r}) + testBinaryOp(t, &objects.Float{Value: l}, token.LessEq, &objects.Float{Value: r}, boolValue(l <= r)) } } // float >= float for l := float64(-2); l <= 2.1; l += 0.4 { for r := float64(-2); r <= 2.1; r += 0.4 { - testBinaryOp(t, &objects.Float{Value: l}, token.GreaterEq, &objects.Float{Value: r}, &objects.Bool{Value: l >= r}) + testBinaryOp(t, &objects.Float{Value: l}, token.GreaterEq, &objects.Float{Value: r}, boolValue(l >= r)) } } @@ -99,28 +99,28 @@ func TestFloat_BinaryOp(t *testing.T) { // float < int for l := float64(-2); l <= 2.1; l += 0.4 { for r := int64(-2); r <= 2; r++ { - testBinaryOp(t, &objects.Float{Value: l}, token.Less, &objects.Int{Value: r}, &objects.Bool{Value: l < float64(r)}) + testBinaryOp(t, &objects.Float{Value: l}, token.Less, &objects.Int{Value: r}, boolValue(l < float64(r))) } } // float > int for l := float64(-2); l <= 2.1; l += 0.4 { for r := int64(-2); r <= 2; r++ { - testBinaryOp(t, &objects.Float{Value: l}, token.Greater, &objects.Int{Value: r}, &objects.Bool{Value: l > float64(r)}) + testBinaryOp(t, &objects.Float{Value: l}, token.Greater, &objects.Int{Value: r}, boolValue(l > float64(r))) } } // float <= int for l := float64(-2); l <= 2.1; l += 0.4 { for r := int64(-2); r <= 2; r++ { - testBinaryOp(t, &objects.Float{Value: l}, token.LessEq, &objects.Int{Value: r}, &objects.Bool{Value: l <= float64(r)}) + testBinaryOp(t, &objects.Float{Value: l}, token.LessEq, &objects.Int{Value: r}, boolValue(l <= float64(r))) } } // float >= int for l := float64(-2); l <= 2.1; l += 0.4 { for r := int64(-2); r <= 2; r++ { - testBinaryOp(t, &objects.Float{Value: l}, token.GreaterEq, &objects.Int{Value: r}, &objects.Bool{Value: l >= float64(r)}) + testBinaryOp(t, &objects.Float{Value: l}, token.GreaterEq, &objects.Int{Value: r}, boolValue(l >= float64(r))) } } } diff --git a/objects/int_test.go b/objects/int_test.go index 3e896c4b..8e7b3501 100644 --- a/objects/int_test.go +++ b/objects/int_test.go @@ -114,28 +114,28 @@ func TestInt_BinaryOp(t *testing.T) { // int < int for l := int64(-2); l <= 2; l++ { for r := int64(-2); r <= 2; r++ { - testBinaryOp(t, &objects.Int{Value: l}, token.Less, &objects.Int{Value: r}, &objects.Bool{Value: l < r}) + testBinaryOp(t, &objects.Int{Value: l}, token.Less, &objects.Int{Value: r}, boolValue(l < r)) } } // int > int for l := int64(-2); l <= 2; l++ { for r := int64(-2); r <= 2; r++ { - testBinaryOp(t, &objects.Int{Value: l}, token.Greater, &objects.Int{Value: r}, &objects.Bool{Value: l > r}) + testBinaryOp(t, &objects.Int{Value: l}, token.Greater, &objects.Int{Value: r}, boolValue(l > r)) } } // int <= int for l := int64(-2); l <= 2; l++ { for r := int64(-2); r <= 2; r++ { - testBinaryOp(t, &objects.Int{Value: l}, token.LessEq, &objects.Int{Value: r}, &objects.Bool{Value: l <= r}) + testBinaryOp(t, &objects.Int{Value: l}, token.LessEq, &objects.Int{Value: r}, boolValue(l <= r)) } } // int >= int for l := int64(-2); l <= 2; l++ { for r := int64(-2); r <= 2; r++ { - testBinaryOp(t, &objects.Int{Value: l}, token.GreaterEq, &objects.Int{Value: r}, &objects.Bool{Value: l >= r}) + testBinaryOp(t, &objects.Int{Value: l}, token.GreaterEq, &objects.Int{Value: r}, boolValue(l >= r)) } } @@ -172,28 +172,28 @@ func TestInt_BinaryOp(t *testing.T) { // int < float for l := int64(-2); l <= 2; l++ { for r := float64(-2); r <= 2.1; r += 0.5 { - testBinaryOp(t, &objects.Int{Value: l}, token.Less, &objects.Float{Value: r}, &objects.Bool{Value: float64(l) < r}) + testBinaryOp(t, &objects.Int{Value: l}, token.Less, &objects.Float{Value: r}, boolValue(float64(l) < r)) } } // int > float for l := int64(-2); l <= 2; l++ { for r := float64(-2); r <= 2.1; r += 0.5 { - testBinaryOp(t, &objects.Int{Value: l}, token.Greater, &objects.Float{Value: r}, &objects.Bool{Value: float64(l) > r}) + testBinaryOp(t, &objects.Int{Value: l}, token.Greater, &objects.Float{Value: r}, boolValue(float64(l) > r)) } } // int <= float for l := int64(-2); l <= 2; l++ { for r := float64(-2); r <= 2.1; r += 0.5 { - testBinaryOp(t, &objects.Int{Value: l}, token.LessEq, &objects.Float{Value: r}, &objects.Bool{Value: float64(l) <= r}) + testBinaryOp(t, &objects.Int{Value: l}, token.LessEq, &objects.Float{Value: r}, boolValue(float64(l) <= r)) } } // int >= float for l := int64(-2); l <= 2; l++ { for r := float64(-2); r <= 2.1; r += 0.5 { - testBinaryOp(t, &objects.Int{Value: l}, token.GreaterEq, &objects.Float{Value: r}, &objects.Bool{Value: float64(l) >= r}) + testBinaryOp(t, &objects.Int{Value: l}, token.GreaterEq, &objects.Float{Value: r}, boolValue(float64(l) >= r)) } } } diff --git a/objects/objects.go b/objects/objects.go index eaa29dd7..f3878b11 100644 --- a/objects/objects.go +++ b/objects/objects.go @@ -2,10 +2,10 @@ package objects var ( // TrueValue represents a true value. - TrueValue Object = &Bool{Value: true} + TrueValue Object = &Bool{value: true} // FalseValue represents a false value. - FalseValue Object = &Bool{Value: false} + FalseValue Object = &Bool{value: false} // UndefinedValue represents an undefined value. UndefinedValue Object = &Undefined{} diff --git a/objects/objects_test.go b/objects/objects_test.go index 23e8872c..c411736c 100644 --- a/objects/objects_test.go +++ b/objects/objects_test.go @@ -15,3 +15,11 @@ func testBinaryOp(t *testing.T, lhs objects.Object, op token.Token, rhs objects. return assert.NoError(t, err) && assert.Equal(t, expected, actual) } + +func boolValue(b bool) objects.Object { + if b { + return objects.TrueValue + } + + return objects.FalseValue +} diff --git a/objects/time.go b/objects/time.go new file mode 100644 index 00000000..4e783cc8 --- /dev/null +++ b/objects/time.go @@ -0,0 +1,89 @@ +package objects + +import ( + "time" + + "github.com/d5/tengo/compiler/token" +) + +// Time represents a time value. +type Time struct { + Value time.Time +} + +func (o *Time) String() string { + return o.Value.String() +} + +// TypeName returns the name of the type. +func (o *Time) TypeName() string { + return "time" +} + +// BinaryOp returns another object that is the result of +// a given binary operator and a right-hand side object. +func (o *Time) BinaryOp(op token.Token, rhs Object) (Object, error) { + switch rhs := rhs.(type) { + case *Int: + switch op { + case token.Add: // time + int => time + if rhs.Value == 0 { + return o, nil + } + return &Time{Value: o.Value.Add(time.Duration(rhs.Value))}, nil + case token.Sub: // time - int => time + if rhs.Value == 0 { + return o, nil + } + return &Time{Value: o.Value.Add(time.Duration(-rhs.Value))}, nil + } + case *Time: + switch op { + case token.Sub: // time - time => int (duration) + return &Int{Value: int64(o.Value.Sub(rhs.Value))}, nil + case token.Less: // time < time => bool + if o.Value.Before(rhs.Value) { + return TrueValue, nil + } + return FalseValue, nil + case token.Greater: + if o.Value.After(rhs.Value) { + return TrueValue, nil + } + return FalseValue, nil + case token.LessEq: + if o.Value.Equal(rhs.Value) || o.Value.Before(rhs.Value) { + return TrueValue, nil + } + return FalseValue, nil + case token.GreaterEq: + if o.Value.Equal(rhs.Value) || o.Value.After(rhs.Value) { + return TrueValue, nil + } + return FalseValue, nil + } + } + + return nil, ErrInvalidOperator +} + +// Copy returns a copy of the type. +func (o *Time) Copy() Object { + return &Time{Value: o.Value} +} + +// IsFalsy returns true if the value of the type is falsy. +func (o *Time) IsFalsy() bool { + return o.Value.IsZero() +} + +// Equals returns true if the value of the type +// is equal to the value of another object. +func (o *Time) Equals(x Object) bool { + t, ok := x.(*Time) + if !ok { + return false + } + + return o.Value.Equal(t.Value) +} diff --git a/objects/undefined.go b/objects/undefined.go index fbad34c6..ab6f549b 100644 --- a/objects/undefined.go +++ b/objects/undefined.go @@ -22,7 +22,7 @@ func (o *Undefined) BinaryOp(op token.Token, rhs Object) (Object, error) { // Copy returns a copy of the type. func (o *Undefined) Copy() Object { - return &Undefined{} + return o } // IsFalsy returns true if the value of the type is falsy. @@ -33,7 +33,5 @@ func (o *Undefined) IsFalsy() bool { // Equals returns true if the value of the type // is equal to the value of another object. func (o *Undefined) Equals(x Object) bool { - _, ok := x.(*Undefined) - - return ok + return o == x } diff --git a/runtime/vm.go b/runtime/vm.go index 47164ed1..b4be7141 100644 --- a/runtime/vm.go +++ b/runtime/vm.go @@ -617,23 +617,21 @@ func (v *VM) Run() error { left := v.stack[v.sp-3] v.sp -= 3 - var lowIdx, highIdx int64 - - switch low := (*low).(type) { - case *objects.Undefined: - //lowIdx = 0 - case *objects.Int: - lowIdx = low.Value - default: - return fmt.Errorf("non-integer slice index: %s", low.TypeName()) + var lowIdx int64 + if *low != objects.UndefinedValue { + if low, ok := (*low).(*objects.Int); ok { + lowIdx = low.Value + } else { + return fmt.Errorf("non-integer slice index: %s", low.TypeName()) + } } - switch high := (*high).(type) { - case *objects.Undefined: - highIdx = -1 // will be replaced by number of elements - case *objects.Int: + var highIdx int64 + if *high == objects.UndefinedValue { + highIdx = -1 + } else if high, ok := (*high).(*objects.Int); ok { highIdx = high.Value - default: + } else { return fmt.Errorf("non-integer slice index: %s", high.TypeName()) } diff --git a/runtime/vm_builtin_test.go b/runtime/vm_builtin_test.go index 5e853f2c..aebbe788 100644 --- a/runtime/vm_builtin_test.go +++ b/runtime/vm_builtin_test.go @@ -2,6 +2,8 @@ package runtime_test import ( "testing" + + "github.com/d5/tengo/objects" ) func TestBuiltinFunction(t *testing.T) { @@ -24,14 +26,14 @@ func TestBuiltinFunction(t *testing.T) { expect(t, `out = int(true)`, 1) expect(t, `out = int(false)`, 0) expect(t, `out = int('8')`, 56) - expect(t, `out = int([1])`, undefined()) - expect(t, `out = int({a: 1})`, undefined()) - expect(t, `out = int(undefined)`, undefined()) + expect(t, `out = int([1])`, objects.UndefinedValue) + expect(t, `out = int({a: 1})`, objects.UndefinedValue) + expect(t, `out = int(undefined)`, objects.UndefinedValue) expect(t, `out = int("-522", 1)`, -522) expect(t, `out = int(undefined, 1)`, 1) expect(t, `out = int(undefined, 1.8)`, 1.8) expect(t, `out = int(undefined, string(1))`, "1") - expect(t, `out = int(undefined, undefined)`, undefined()) + expect(t, `out = int(undefined, undefined)`, objects.UndefinedValue) expect(t, `out = string(1)`, "1") expect(t, `out = string(1.8)`, "1.8") @@ -41,40 +43,40 @@ func TestBuiltinFunction(t *testing.T) { expect(t, `out = string('8')`, "8") expect(t, `out = string([1,8.1,true,3])`, "[1, 8.1, true, 3]") expect(t, `out = string({b: "foo"})`, `{b: "foo"}`) - expect(t, `out = string(undefined)`, undefined()) // not "undefined" + expect(t, `out = string(undefined)`, objects.UndefinedValue) // not "undefined" expect(t, `out = string(1, "-522")`, "1") expect(t, `out = string(undefined, "-522")`, "-522") // not "undefined" expect(t, `out = float(1)`, 1.0) expect(t, `out = float(1.8)`, 1.8) expect(t, `out = float("-52.2")`, -52.2) - expect(t, `out = float(true)`, undefined()) - expect(t, `out = float(false)`, undefined()) - expect(t, `out = float('8')`, undefined()) - expect(t, `out = float([1,8.1,true,3])`, undefined()) - expect(t, `out = float({a: 1, b: "foo"})`, undefined()) - expect(t, `out = float(undefined)`, undefined()) + expect(t, `out = float(true)`, objects.UndefinedValue) + expect(t, `out = float(false)`, objects.UndefinedValue) + expect(t, `out = float('8')`, objects.UndefinedValue) + expect(t, `out = float([1,8.1,true,3])`, objects.UndefinedValue) + expect(t, `out = float({a: 1, b: "foo"})`, objects.UndefinedValue) + expect(t, `out = float(undefined)`, objects.UndefinedValue) expect(t, `out = float("-52.2", 1.8)`, -52.2) expect(t, `out = float(undefined, 1)`, 1) expect(t, `out = float(undefined, 1.8)`, 1.8) expect(t, `out = float(undefined, "-52.2")`, "-52.2") expect(t, `out = float(undefined, char(56))`, '8') - expect(t, `out = float(undefined, undefined)`, undefined()) + expect(t, `out = float(undefined, undefined)`, objects.UndefinedValue) expect(t, `out = char(56)`, '8') - expect(t, `out = char(1.8)`, undefined()) - expect(t, `out = char("-52.2")`, undefined()) - expect(t, `out = char(true)`, undefined()) - expect(t, `out = char(false)`, undefined()) + expect(t, `out = char(1.8)`, objects.UndefinedValue) + expect(t, `out = char("-52.2")`, objects.UndefinedValue) + expect(t, `out = char(true)`, objects.UndefinedValue) + expect(t, `out = char(false)`, objects.UndefinedValue) expect(t, `out = char('8')`, '8') - expect(t, `out = char([1,8.1,true,3])`, undefined()) - expect(t, `out = char({a: 1, b: "foo"})`, undefined()) - expect(t, `out = char(undefined)`, undefined()) + expect(t, `out = char([1,8.1,true,3])`, objects.UndefinedValue) + expect(t, `out = char({a: 1, b: "foo"})`, objects.UndefinedValue) + expect(t, `out = char(undefined)`, objects.UndefinedValue) expect(t, `out = char(56, 'a')`, '8') expect(t, `out = char(undefined, '8')`, '8') expect(t, `out = char(undefined, 56)`, 56) expect(t, `out = char(undefined, "-52.2")`, "-52.2") - expect(t, `out = char(undefined, undefined)`, undefined()) + expect(t, `out = char(undefined, undefined)`, objects.UndefinedValue) expect(t, `out = bool(1)`, true) // non-zero integer: true expect(t, `out = bool(0)`, false) // zero: true @@ -93,20 +95,20 @@ func TestBuiltinFunction(t *testing.T) { expect(t, `out = bool(undefined)`, false) // undefined: false expect(t, `out = bytes(1)`, []byte{0}) - expect(t, `out = bytes(1.8)`, undefined()) + expect(t, `out = bytes(1.8)`, objects.UndefinedValue) expect(t, `out = bytes("-522")`, []byte{'-', '5', '2', '2'}) - expect(t, `out = bytes(true)`, undefined()) - expect(t, `out = bytes(false)`, undefined()) - expect(t, `out = bytes('8')`, undefined()) - expect(t, `out = bytes([1])`, undefined()) - expect(t, `out = bytes({a: 1})`, undefined()) - expect(t, `out = bytes(undefined)`, undefined()) + expect(t, `out = bytes(true)`, objects.UndefinedValue) + expect(t, `out = bytes(false)`, objects.UndefinedValue) + expect(t, `out = bytes('8')`, objects.UndefinedValue) + expect(t, `out = bytes([1])`, objects.UndefinedValue) + expect(t, `out = bytes({a: 1})`, objects.UndefinedValue) + expect(t, `out = bytes(undefined)`, objects.UndefinedValue) expect(t, `out = bytes("-522", ['8'])`, []byte{'-', '5', '2', '2'}) expect(t, `out = bytes(undefined, "-522")`, "-522") expect(t, `out = bytes(undefined, 1)`, 1) expect(t, `out = bytes(undefined, 1.8)`, 1.8) expect(t, `out = bytes(undefined, int("-522"))`, -522) - expect(t, `out = bytes(undefined, undefined)`, undefined()) + expect(t, `out = bytes(undefined, undefined)`, objects.UndefinedValue) expect(t, `out = is_error(error(1))`, true) expect(t, `out = is_error(1)`, false) diff --git a/runtime/vm_function_test.go b/runtime/vm_function_test.go index e5417007..4d4d115b 100644 --- a/runtime/vm_function_test.go +++ b/runtime/vm_function_test.go @@ -2,13 +2,15 @@ package runtime_test import ( "testing" + + "github.com/d5/tengo/objects" ) func TestFunction(t *testing.T) { // function with no "return" statement returns "invalid" value. - expect(t, `f1 := func() {}; out = f1();`, undefined()) - expect(t, `f1 := func() {}; f2 := func() { return f1(); }; f1(); out = f2();`, undefined()) - expect(t, `f := func(x) { x; }; out = f(5);`, undefined()) + expect(t, `f1 := func() {}; out = f1();`, objects.UndefinedValue) + expect(t, `f1 := func() {}; f2 := func() { return f1(); }; f1(); out = f2();`, objects.UndefinedValue) + expect(t, `f := func(x) { x; }; out = f(5);`, objects.UndefinedValue) expect(t, `f := func(x) { return x; }; out = f(5);`, 5) expect(t, `f := func(x) { return x * 2; }; out = f(5);`, 10) diff --git a/runtime/vm_if_test.go b/runtime/vm_if_test.go index 34ab117a..648f0640 100644 --- a/runtime/vm_if_test.go +++ b/runtime/vm_if_test.go @@ -2,16 +2,18 @@ package runtime_test import ( "testing" + + "github.com/d5/tengo/objects" ) func TestIf(t *testing.T) { expect(t, `if (true) { out = 10 }`, 10) - expect(t, `if (false) { out = 10 }`, undefined()) + expect(t, `if (false) { out = 10 }`, objects.UndefinedValue) expect(t, `if (false) { out = 10 } else { out = 20 }`, 20) expect(t, `if (1) { out = 10 }`, 10) expect(t, `if (0) { out = 10 } else { out = 20 }`, 20) expect(t, `if (1 < 2) { out = 10 }`, 10) - expect(t, `if (1 > 2) { out = 10 }`, undefined()) + expect(t, `if (1 > 2) { out = 10 }`, objects.UndefinedValue) expect(t, `if (1 < 2) { out = 10 } else { out = 20 }`, 10) expect(t, `if (1 > 2) { out = 10 } else { out = 20 }`, 20) diff --git a/runtime/vm_indexable_test.go b/runtime/vm_indexable_test.go index 19959cad..a98a8e34 100644 --- a/runtime/vm_indexable_test.go +++ b/runtime/vm_indexable_test.go @@ -159,7 +159,7 @@ func TestIndexable(t *testing.T) { dict := func() *StringDict { return &StringDict{Value: map[string]string{"a": "foo", "b": "bar"}} } expectWithSymbols(t, `out = dict["a"]`, "foo", SYM{"dict": dict()}) expectWithSymbols(t, `out = dict["B"]`, "bar", SYM{"dict": dict()}) - expectWithSymbols(t, `out = dict["x"]`, undefined(), SYM{"dict": dict()}) + expectWithSymbols(t, `out = dict["x"]`, objects.UndefinedValue, SYM{"dict": dict()}) expectErrorWithSymbols(t, `out = dict[0]`, SYM{"dict": dict()}) strCir := func() *StringCircle { return &StringCircle{Value: []string{"one", "two", "three"}} } @@ -173,7 +173,7 @@ func TestIndexable(t *testing.T) { strArr := func() *StringArray { return &StringArray{Value: []string{"one", "two", "three"}} } expectWithSymbols(t, `out = arr["one"]`, 0, SYM{"arr": strArr()}) expectWithSymbols(t, `out = arr["three"]`, 2, SYM{"arr": strArr()}) - expectWithSymbols(t, `out = arr["four"]`, undefined(), SYM{"arr": strArr()}) + expectWithSymbols(t, `out = arr["four"]`, objects.UndefinedValue, SYM{"arr": strArr()}) expectWithSymbols(t, `out = arr[0]`, "one", SYM{"arr": strArr()}) expectWithSymbols(t, `out = arr[1]`, "two", SYM{"arr": strArr()}) expectErrorWithSymbols(t, `out = arr[-1]`, SYM{"arr": strArr()}) diff --git a/runtime/vm_map_test.go b/runtime/vm_map_test.go index 740d559f..35090b3c 100644 --- a/runtime/vm_map_test.go +++ b/runtime/vm_map_test.go @@ -2,6 +2,8 @@ package runtime_test import ( "testing" + + "github.com/d5/tengo/objects" ) func TestMap(t *testing.T) { @@ -17,9 +19,9 @@ out = { }) expect(t, `out = {foo: 5}["foo"]`, 5) - expect(t, `out = {foo: 5}["bar"]`, undefined()) + expect(t, `out = {foo: 5}["bar"]`, objects.UndefinedValue) expect(t, `key := "foo"; out = {foo: 5}[key]`, 5) - expect(t, `out = {}["foo"]`, undefined()) + expect(t, `out = {}["foo"]`, objects.UndefinedValue) expect(t, ` m := { diff --git a/runtime/vm_module_test.go b/runtime/vm_module_test.go index 19fcf671..e059968b 100644 --- a/runtime/vm_module_test.go +++ b/runtime/vm_module_test.go @@ -47,17 +47,11 @@ os.remove("./temp") // exec.command expect(t, ` -exec := import("exec") - -echo := func(args) { - cmd := exec.command("echo", args) - if is_error(cmd) { return cmd.value } - output := cmd.output() - if is_error(output) { return output.value } - return output +os := import("os") +cmd := os.exec("echo", "foo", "bar") +if !is_error(cmd) { + out = cmd.output() } - -out = echo(["foo", "bar"]) `, []byte("foo bar\n")) // user modules diff --git a/runtime/vm_selector_test.go b/runtime/vm_selector_test.go index 94107736..1212d209 100644 --- a/runtime/vm_selector_test.go +++ b/runtime/vm_selector_test.go @@ -2,12 +2,14 @@ package runtime_test import ( "testing" + + "github.com/d5/tengo/objects" ) func TestSelector(t *testing.T) { expect(t, `a := {k1: 5, k2: "foo"}; out = a.k1`, 5) expect(t, `a := {k1: 5, k2: "foo"}; out = a.k2`, "foo") - expect(t, `a := {k1: 5, k2: "foo"}; out = a.k3`, undefined()) + expect(t, `a := {k1: 5, k2: "foo"}; out = a.k3`, objects.UndefinedValue) expect(t, ` a := { diff --git a/runtime/vm_test.go b/runtime/vm_test.go index 6a11697a..4b21e826 100644 --- a/runtime/vm_test.go +++ b/runtime/vm_test.go @@ -133,7 +133,10 @@ func toObject(v interface{}) objects.Object { case int: // for convenience return &objects.Int{Value: int64(v)} case bool: - return &objects.Bool{Value: v} + if v { + return objects.TrueValue + } + return objects.FalseValue case rune: return &objects.Char{Value: v} case byte: // for convenience @@ -326,7 +329,7 @@ func objectZeroCopy(o objects.Object) objects.Object { case *objects.Map: return &objects.Map{} case *objects.Undefined: - return &objects.Undefined{} + return objects.UndefinedValue case *objects.Error: return &objects.Error{} case *objects.Bytes: @@ -341,7 +344,3 @@ func objectZeroCopy(o objects.Object) objects.Object { panic(fmt.Errorf("unknown object type: %s", o.TypeName())) } } - -func undefined() *objects.Undefined { - return &objects.Undefined{} -} diff --git a/runtime/vm_undefined_test.go b/runtime/vm_undefined_test.go index 4e445b07..4e8b75e2 100644 --- a/runtime/vm_undefined_test.go +++ b/runtime/vm_undefined_test.go @@ -1,9 +1,13 @@ package runtime_test -import "testing" +import ( + "testing" + + "github.com/d5/tengo/objects" +) func TestUndefined(t *testing.T) { - expect(t, `out = undefined`, undefined()) + expect(t, `out = undefined`, objects.UndefinedValue) expect(t, `out = undefined == undefined`, true) expect(t, `out = undefined == 1`, false) expect(t, `out = 1 == undefined`, false) diff --git a/script/compiled.go b/script/compiled.go index 64d6fa82..c26b8f24 100644 --- a/script/compiled.go +++ b/script/compiled.go @@ -8,8 +8,6 @@ import ( "github.com/d5/tengo/runtime" ) -var undefined objects.Object = &objects.Undefined{} - // Compiled is a compiled instance of the user script. // Use Script.Compile() to create Compiled object. type Compiled struct { @@ -53,20 +51,18 @@ func (c *Compiled) IsDefined(name string) bool { return false } - _, isUndefined := (*v).(*objects.Undefined) - - return !isUndefined + return *v != objects.UndefinedValue } // Get returns a variable identified by the name. func (c *Compiled) Get(name string) *Variable { - value := &undefined + value := &objects.UndefinedValue symbol, _, ok := c.symbolTable.Resolve(name) if ok && symbol.Scope == compiler.ScopeGlobal { value = c.machine.Globals()[symbol.Index] if value == nil { - value = &undefined + value = &objects.UndefinedValue } } @@ -84,7 +80,7 @@ func (c *Compiled) GetAll() []*Variable { if ok && symbol.Scope == compiler.ScopeGlobal { value := c.machine.Globals()[symbol.Index] if value == nil { - value = &undefined + value = &objects.UndefinedValue } vars = append(vars, &Variable{ diff --git a/script/conversion.go b/script/conversion.go index 440ef9a3..c35c1411 100644 --- a/script/conversion.go +++ b/script/conversion.go @@ -15,7 +15,10 @@ func objectToInterface(o objects.Object) interface{} { case *objects.Float: return val.Value case *objects.Bool: - return val.Value + if val == objects.TrueValue { + return true + } + return false case *objects.Char: return val.Value case *objects.String: diff --git a/script/variable.go b/script/variable.go index 13aa210f..c5e01bd9 100644 --- a/script/variable.go +++ b/script/variable.go @@ -145,7 +145,5 @@ func (v *Variable) Object() objects.Object { // IsUndefined returns true if the underlying value is undefined. func (v *Variable) IsUndefined() bool { - _, isUndefined := (*v.value).(*objects.Undefined) - - return isUndefined + return *v.value == objects.UndefinedValue } diff --git a/script/variable_test.go b/script/variable_test.go index 45894f80..8f67c323 100644 --- a/script/variable_test.go +++ b/script/variable_test.go @@ -54,13 +54,12 @@ func TestVariable(t *testing.T) { FloatValue: 0, BoolValue: true, StringValue: "true", - Object: &objects.Bool{Value: true}, + Object: objects.TrueValue, }, { Name: "d", Value: nil, ValueType: "undefined", - StringValue: "", Object: objects.UndefinedValue, IsUndefined: true, },