From a6440a8ff52ac45d7b491ae365799f4437a2a5a4 Mon Sep 17 00:00:00 2001 From: Franck Marcia Date: Wed, 13 Dec 2023 22:40:22 +0100 Subject: [PATCH 1/2] Add duration literal support --- ast/node.go | 7 ++++ ast/print.go | 4 ++ ast/visitor.go | 1 + builtin/builtin.go | 49 +++++++++++++++------- builtin/builtin_test.go | 7 ++++ builtin/func.go | 24 +++++------ checker/checker.go | 9 ++++ compiler/compiler.go | 6 +++ docs/Language-Definition.md | 30 +++++++++++++ expr_test.go | 84 +++++++++++++++++++++++++++++++++++++ optimizer/fold.go | 16 +++++-- parser/lexer/state.go | 55 +++++++++++++++++++++++- parser/lexer/token.go | 1 + parser/parser.go | 12 ++++++ parser/parser_test.go | 60 ++++++++++++++++++++++++++ vm/runtime/generated.go | 55 +++++++++++++++++++++++- vm/runtime/helpers/main.go | 10 +++-- vm/runtime/runtime.go | 3 ++ 18 files changed, 398 insertions(+), 35 deletions(-) diff --git a/ast/node.go b/ast/node.go index 7aabf064c..cf24a2301 100644 --- a/ast/node.go +++ b/ast/node.go @@ -3,6 +3,7 @@ package ast import ( "reflect" "regexp" + "time" "github.com/expr-lang/expr/file" ) @@ -93,6 +94,12 @@ type BoolNode struct { Value bool // Value of the boolean. } +// DurationNode represents a duration. +type DurationNode struct { + base + Value time.Duration // Value of the duration. +} + // StringNode represents a string. type StringNode struct { base diff --git a/ast/print.go b/ast/print.go index 9a7d12391..27b536604 100644 --- a/ast/print.go +++ b/ast/print.go @@ -29,6 +29,10 @@ func (n *BoolNode) String() string { return fmt.Sprintf("%t", n.Value) } +func (n *DurationNode) String() string { + return fmt.Sprintf("%v", n.Value) +} + func (n *StringNode) String() string { return fmt.Sprintf("%q", n.Value) } diff --git a/ast/visitor.go b/ast/visitor.go index 287a75589..9c43ea4dd 100644 --- a/ast/visitor.go +++ b/ast/visitor.go @@ -12,6 +12,7 @@ func Walk(node *Node, v Visitor) { case *IdentifierNode: case *IntegerNode: case *FloatNode: + case *DurationNode: case *BoolNode: case *StringNode: case *ConstantNode: diff --git a/builtin/builtin.go b/builtin/builtin.go index 8576160a6..f0527a83f 100644 --- a/builtin/builtin.go +++ b/builtin/builtin.go @@ -4,6 +4,7 @@ import ( "encoding/base64" "encoding/json" "fmt" + "math" "reflect" "sort" "strings" @@ -154,20 +155,6 @@ var Builtins = []*ast.Function{ return anyType, fmt.Errorf("invalid argument for floor (type %s)", args[0]) }, }, - { - Name: "round", - Fast: Round, - Validate: func(args []reflect.Type) (reflect.Type, error) { - if len(args) != 1 { - return anyType, fmt.Errorf("invalid number of arguments (expected 1, got %d)", len(args)) - } - switch kind(args[0]) { - case reflect.Float32, reflect.Float64, reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64, reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64, reflect.Interface: - return floatType, nil - } - return anyType, fmt.Errorf("invalid argument for floor (type %s)", args[0]) - }, - }, { Name: "int", Fast: Int, @@ -209,6 +196,40 @@ var Builtins = []*ast.Function{ Fast: String, Types: types(new(func(any any) string)), }, + { + Name: "round", + Func: func(args ...any) (any, error) { + switch l := len(args); l { + case 1: + a := args[0] + switch a := a.(type) { + case float32: + return math.Round(float64(a)), nil + case float64: + return math.Round(a), nil + case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: + return Float(a), nil + } + return nil, fmt.Errorf("invalid argument for round (type %T)", a) + case 2: + a, ok := args[0].(time.Duration) + if !ok { + return nil, fmt.Errorf("invalid argument for round (type %T)", args[0]) + } + b, ok := args[1].(time.Duration) + if !ok { + return nil, fmt.Errorf("invalid argument for round (type %T)", args[1]) + } + return a.Round(b), nil + default: + return nil, fmt.Errorf("invalid number of arguments for round (expected 1 or 2, got %d)", l) + } + }, + Types: types( + math.Round, + new(func(time.Duration, time.Duration) time.Duration), + ), + }, { Name: "trim", Func: func(args ...any) (any, error) { diff --git a/builtin/builtin_test.go b/builtin/builtin_test.go index 40f6e32ee..dd1359ce4 100644 --- a/builtin/builtin_test.go +++ b/builtin/builtin_test.go @@ -36,6 +36,7 @@ func TestBuiltin(t *testing.T) { {`abs(-5)`, 5}, {`abs(.5)`, .5}, {`abs(-.5)`, .5}, + {`abs(-24h)`, 24 * time.Hour}, {`ceil(5.5)`, 6.0}, {`ceil(5)`, 5.0}, {`floor(5.5)`, 5.0}, @@ -43,15 +44,19 @@ func TestBuiltin(t *testing.T) { {`round(5.5)`, 6.0}, {`round(5)`, 5.0}, {`round(5.49)`, 5.0}, + {`round(24h2m, 1h)`, 24 * time.Hour}, {`int(5.5)`, 5}, {`int(5)`, 5}, {`int("5")`, 5}, + {`int(2ns)`, 2}, {`float(5)`, 5.0}, {`float(5.5)`, 5.5}, {`float("5.5")`, 5.5}, + {`float(2ns)`, 2.0}, {`string(5)`, "5"}, {`string(5.5)`, "5.5"}, {`string("5.5")`, "5.5"}, + {`string(1m2s)`, "1m2s"}, {`trim(" foo ")`, "foo"}, {`trim("__foo___", "_")`, "foo"}, {`trimPrefix("prefix_foo", "prefix_")`, "foo"}, @@ -75,8 +80,10 @@ func TestBuiltin(t *testing.T) { {`hasSuffix("foo,bar,baz", "baz")`, true}, {`max(1, 2, 3)`, 3}, {`max(1.5, 2.5, 3.5)`, 3.5}, + {`max(3h, 2m, 1s)`, 3 * time.Hour}, {`min(1, 2, 3)`, 1}, {`min(1.5, 2.5, 3.5)`, 1.5}, + {`min(3h, 2m, 1s)`, time.Second}, {`sum(1..9)`, 45}, {`sum([.5, 1.5, 2.5])`, 4.5}, {`sum([])`, 0}, diff --git a/builtin/func.go b/builtin/func.go index 9bcd7827b..71c90d7a0 100644 --- a/builtin/func.go +++ b/builtin/func.go @@ -5,6 +5,7 @@ import ( "math" "reflect" "strconv" + "time" "github.com/expr-lang/expr/vm/runtime" ) @@ -35,6 +36,8 @@ func Type(arg any) any { } if v.Type().Name() != "" && v.Type().PkgPath() != "" { return fmt.Sprintf("%s.%s", v.Type().PkgPath(), v.Type().Name()) + } else if v.Type().String() == "time.Duration" { + return "duration" } switch v.Type().Kind() { case reflect.Invalid: @@ -135,6 +138,11 @@ func Abs(x any) any { } else { return x } + case time.Duration: + if x.(time.Duration) < 0 { + return -x.(time.Duration) + } + return x } panic(fmt.Sprintf("invalid argument for abs (type %T)", x)) } @@ -163,18 +171,6 @@ func Floor(x any) any { panic(fmt.Sprintf("invalid argument for floor (type %T)", x)) } -func Round(x any) any { - switch x := x.(type) { - case float32: - return math.Round(float64(x)) - case float64: - return math.Round(x) - case int, int8, int16, int32, int64, uint, uint8, uint16, uint32, uint64: - return Float(x) - } - panic(fmt.Sprintf("invalid argument for round (type %T)", x)) -} - func Int(x any) any { switch x := x.(type) { case float32: @@ -201,6 +197,8 @@ func Int(x any) any { return int(x) case uint64: return int(x) + case time.Duration: + return int(x) case string: i, err := strconv.Atoi(x) if err != nil { @@ -244,6 +242,8 @@ func Float(x any) any { panic(fmt.Sprintf("invalid operation: float(%s)", x)) } return f + case time.Duration: + return float64(x) default: panic(fmt.Sprintf("invalid operation: float(%T)", x)) } diff --git a/checker/checker.go b/checker/checker.go index c214acf3b..74327209c 100644 --- a/checker/checker.go +++ b/checker/checker.go @@ -94,6 +94,8 @@ func (v *checker) visit(node ast.Node) (reflect.Type, info) { t, i = v.IntegerNode(n) case *ast.FloatNode: t, i = v.FloatNode(n) + case *ast.DurationNode: + t, i = v.DurationNode(n) case *ast.BoolNode: t, i = v.BoolNode(n) case *ast.StringNode: @@ -202,6 +204,10 @@ func (v *checker) FloatNode(*ast.FloatNode) (reflect.Type, info) { return floatType, info{} } +func (v *checker) DurationNode(*ast.DurationNode) (reflect.Type, info) { + return durationType, info{} +} + func (v *checker) BoolNode(*ast.BoolNode) (reflect.Type, info) { return boolType, info{} } @@ -233,6 +239,9 @@ func (v *checker) UnaryNode(node *ast.UnaryNode) (reflect.Type, info) { if isNumber(t) { return t, info{} } + if isDuration(t) { + return t, info{} + } if isAny(t) { return anyType, info{} } diff --git a/compiler/compiler.go b/compiler/compiler.go index 4831a3103..28d067364 100644 --- a/compiler/compiler.go +++ b/compiler/compiler.go @@ -200,6 +200,8 @@ func (c *compiler) compile(node ast.Node) { c.IntegerNode(n) case *ast.FloatNode: c.FloatNode(n) + case *ast.DurationNode: + c.DurationNode(n) case *ast.BoolNode: c.BoolNode(n) case *ast.StringNode: @@ -319,6 +321,10 @@ func (c *compiler) FloatNode(node *ast.FloatNode) { } } +func (c *compiler) DurationNode(node *ast.DurationNode) { + c.emitPush(node.Value) +} + func (c *compiler) BoolNode(node *ast.BoolNode) { if node.Value { c.emit(OpTrue) diff --git a/docs/Language-Definition.md b/docs/Language-Definition.md index 46bad9322..73b2d8451 100644 --- a/docs/Language-Definition.md +++ b/docs/Language-Definition.md @@ -33,6 +33,12 @@ "foo", 'bar' + + Duration + + 1h16m7ms + + Array @@ -358,6 +364,30 @@ date("2023-08-14T00:00:00Z") date("2023-08-14 00:00:00", "2006-01-02 15:04:05", "Europe/Zurich") ``` +## Duration Functions + +The following operators can be used to manipulate durations: + +```expr +-1h == -1 * 1h ++1h == 1h +2 * 1h == 2h +1h + 1m == 1h1m +1h - 1m == 59m +1h / 10m == 6 +1h / 2 == 30m +``` + +Some number functions (max, min and abs) are compatible with durations as well. + +### round(d1, d2) + +Returns the result of rounding d1 to the nearest multiple of d2. + +```expr +round(24h2m, 1h) == 24h +``` + ## Number Functions ### max(n1, n2) diff --git a/expr_test.go b/expr_test.go index eb77408b1..0288a9535 100644 --- a/expr_test.go +++ b/expr_test.go @@ -1031,6 +1031,90 @@ func TestExpr(t *testing.T) { `duration("1s") * .5`, 5e8, }, + { + `1h == 1h`, + true, + }, + { + `TimePlusDay - Time >= 24h`, + true, + }, + { + `1h > 1m`, + true, + }, + { + `1h < 1m`, + false, + }, + { + `1h >= 1m`, + true, + }, + { + `1h <= 1m`, + false, + }, + { + `1h > 1m`, + true, + }, + { + `1h + 1m`, + time.Hour + time.Minute, + }, + { + `7 * 1h`, + 7 * time.Hour, + }, + { + `1h * 7`, + 7 * time.Hour, + }, + { + `1s * .5`, + 5e8, + }, + { + "-1h", + -time.Hour, + }, + { + "+1h", + time.Hour, + }, + { + "1h - 1m", + 59 * time.Minute, + }, + { + "1h - -1m", + time.Hour + time.Minute, + }, + { + "1h / 2 * 2", + time.Hour, + }, + { + "1h * 2 / 2", + time.Hour, + }, + { + "1h5m / 10m", + 6.5, + }, + { + `date("2023-08-14") - 24h == date("2023-08-13")`, + true, + }, + { + `date("2023-08-13") + 24h == date("2023-08-14")`, + true, + }, + { + `let res = 24h; date("2023-08-14") - date("2023-08-13") == res`, + true, + }, { `1 /* one */ + 2 // two`, 3, diff --git a/optimizer/fold.go b/optimizer/fold.go index 910c92402..da140fc84 100644 --- a/optimizer/fold.go +++ b/optimizer/fold.go @@ -4,15 +4,17 @@ import ( "fmt" "math" "reflect" + "time" . "github.com/expr-lang/expr/ast" "github.com/expr-lang/expr/file" ) var ( - integerType = reflect.TypeOf(0) - floatType = reflect.TypeOf(float64(0)) - stringType = reflect.TypeOf("") + integerType = reflect.TypeOf(0) + floatType = reflect.TypeOf(float64(0)) + durationType = reflect.TypeOf(time.Nanosecond) + stringType = reflect.TypeOf("") ) type fold struct { @@ -32,6 +34,8 @@ func (fold *fold) Visit(node *Node) { newNode.SetType(integerType) case *FloatNode: newNode.SetType(floatType) + case *DurationNode: + newNode.SetType(durationType) case *StringNode: newNode.SetType(stringType) default: @@ -49,6 +53,9 @@ func (fold *fold) Visit(node *Node) { if i, ok := n.Node.(*FloatNode); ok { patchWithType(&FloatNode{Value: -i.Value}) } + if i, ok := n.Node.(*DurationNode); ok { + patchWithType(&DurationNode{Value: -i.Value}) + } case "+": if i, ok := n.Node.(*IntegerNode); ok { patchWithType(&IntegerNode{Value: i.Value}) @@ -56,6 +63,9 @@ func (fold *fold) Visit(node *Node) { if i, ok := n.Node.(*FloatNode); ok { patchWithType(&FloatNode{Value: i.Value}) } + if i, ok := n.Node.(*DurationNode); ok { + patchWithType(&DurationNode{Value: i.Value}) + } case "!", "not": if a := toBool(n.Node); a != nil { patch(&BoolNode{Value: !a.Value}) diff --git a/parser/lexer/state.go b/parser/lexer/state.go index 5d82329eb..b57ae52cc 100644 --- a/parser/lexer/state.go +++ b/parser/lexer/state.go @@ -57,10 +57,18 @@ func root(l *lexer) stateFn { } func number(l *lexer) stateFn { + kind := Number + loc, prev, end := l.loc, l.prev, l.end if !l.scanNumber() { - return l.error("bad number syntax: %q", l.word()) + loc2, prev2, end2 := l.loc, l.prev, l.end + l.loc, l.prev, l.end = loc, prev, end + if !l.scanDuration() { + l.loc, l.prev, l.end = loc2, prev2, end2 + return l.error("bad number syntax: %q", l.word()) + } + kind = Duration } - l.emit(Number) + l.emit(kind) return root } @@ -102,6 +110,49 @@ func (l *lexer) scanNumber() bool { return true } +func (l *lexer) scanDuration() bool { + digits := "0123456789" + ok := false + for { + l.acceptRun(digits) + if l.accept(".") { + l.acceptRun(digits) + } + if l.accept("h") { + ok = true + continue + } else if l.accept("m") { + l.accept("s") + ok = true + continue + } else if l.accept("s") { + ok = true + continue + } else if l.accept("uµ") { + if l.accept("s") { + ok = true + continue + } else { + break + } + } else if l.accept("n") { + if l.accept("s") { + ok = true + continue + } else { + break + } + } else { + break + } + } + if !ok || utils.IsAlphaNumeric(l.peek()) { + l.next() + return false + } + return true +} + func dot(l *lexer) stateFn { l.next() if l.accept("0123456789") { diff --git a/parser/lexer/token.go b/parser/lexer/token.go index 459fa6905..96e082c54 100644 --- a/parser/lexer/token.go +++ b/parser/lexer/token.go @@ -11,6 +11,7 @@ type Kind string const ( Identifier Kind = "Identifier" Number Kind = "Number" + Duration Kind = "Duration" String Kind = "String" Operator Kind = "Operator" Bracket Kind = "Bracket" diff --git a/parser/parser.go b/parser/parser.go index 5f29279bc..e2012a431 100644 --- a/parser/parser.go +++ b/parser/parser.go @@ -5,6 +5,7 @@ import ( "math" "strconv" "strings" + "time" . "github.com/expr-lang/expr/ast" "github.com/expr-lang/expr/builtin" @@ -344,6 +345,17 @@ func (p *parser) parseSecondary() Node { node.SetLocation(token.Location) } return node + + case Duration: + p.next() + duration, err := time.ParseDuration(token.Value) + if err != nil { + p.error("invalid duration literal: %v", err) + } + var node Node = &DurationNode{Value: duration} + node.SetLocation(token.Location) + return node + case String: p.next() node := &StringNode{Value: token.Value} diff --git a/parser/parser_test.go b/parser/parser_test.go index 2af7635ed..d5418f35d 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -4,6 +4,7 @@ import ( "fmt" "strings" "testing" + "time" . "github.com/expr-lang/expr/ast" "github.com/expr-lang/expr/parser" @@ -68,6 +69,55 @@ func TestParse(t *testing.T) { "1e9", &FloatNode{Value: 1e9}, }, + { + "1ns", + &DurationNode{Value: time.Nanosecond}, + }, + { + "1us", + &DurationNode{Value: time.Microsecond}, + }, + { + "1µs", + &DurationNode{Value: time.Microsecond}, + }, + { + "1ms", + &DurationNode{Value: time.Millisecond}, + }, + { + "1s", + &DurationNode{Value: time.Second}, + }, + { + "1m", + &DurationNode{Value: time.Minute}, + }, + { + "1h", + &DurationNode{Value: time.Hour}, + }, + { + "-1h", + &UnaryNode{Operator: "-", + Node: &DurationNode{Value: time.Hour}}, + }, + { + ".2m", + &DurationNode{Value: 12 * time.Second}, + }, + { + "5m3ms", + &DurationNode{Value: 5*time.Minute + 3*time.Millisecond}, + }, + { + "1h.2m", + &DurationNode{Value: time.Hour + 12*time.Second}, + }, + { + "2m2h2m2h", + &DurationNode{Value: 4*time.Hour + 4*time.Minute}, + }, { "true", &BoolNode{Value: true}, @@ -609,6 +659,16 @@ invalid float literal: strconv.ParseFloat: parsing "0o1E+1": invalid syntax (1:6 invalid float literal: strconv.ParseFloat: parsing "1E": invalid syntax (1:2) | 1E | .^ + +1mt +bad number syntax: "1m" (1:3) + | 1mt + | ..^ + +1h1x1s +bad number syntax: "1h" (1:3) + | 1h1mt1s + | ..^ ` func TestParse_error(t *testing.T) { diff --git a/vm/runtime/generated.go b/vm/runtime/generated.go index 720feb455..2872de8b7 100644 --- a/vm/runtime/generated.go +++ b/vm/runtime/generated.go @@ -2808,7 +2808,7 @@ func Multiply(a, b interface{}) interface{} { panic(fmt.Sprintf("invalid operation: %T * %T", a, b)) } -func Divide(a, b interface{}) float64 { +func Divide(a, b interface{}) interface{} { switch x := a.(type) { case uint: switch y := b.(type) { @@ -2836,6 +2836,8 @@ func Divide(a, b interface{}) float64 { return float64(x) / float64(y) case float64: return float64(x) / float64(y) + case time.Duration: + return float64(x) / float64(y) } case uint8: switch y := b.(type) { @@ -2863,6 +2865,8 @@ func Divide(a, b interface{}) float64 { return float64(x) / float64(y) case float64: return float64(x) / float64(y) + case time.Duration: + return float64(x) / float64(y) } case uint16: switch y := b.(type) { @@ -2890,6 +2894,8 @@ func Divide(a, b interface{}) float64 { return float64(x) / float64(y) case float64: return float64(x) / float64(y) + case time.Duration: + return float64(x) / float64(y) } case uint32: switch y := b.(type) { @@ -2917,6 +2923,8 @@ func Divide(a, b interface{}) float64 { return float64(x) / float64(y) case float64: return float64(x) / float64(y) + case time.Duration: + return float64(x) / float64(y) } case uint64: switch y := b.(type) { @@ -2944,6 +2952,8 @@ func Divide(a, b interface{}) float64 { return float64(x) / float64(y) case float64: return float64(x) / float64(y) + case time.Duration: + return float64(x) / float64(y) } case int: switch y := b.(type) { @@ -2971,6 +2981,8 @@ func Divide(a, b interface{}) float64 { return float64(x) / float64(y) case float64: return float64(x) / float64(y) + case time.Duration: + return float64(x) / float64(y) } case int8: switch y := b.(type) { @@ -2998,6 +3010,8 @@ func Divide(a, b interface{}) float64 { return float64(x) / float64(y) case float64: return float64(x) / float64(y) + case time.Duration: + return float64(x) / float64(y) } case int16: switch y := b.(type) { @@ -3025,6 +3039,8 @@ func Divide(a, b interface{}) float64 { return float64(x) / float64(y) case float64: return float64(x) / float64(y) + case time.Duration: + return float64(x) / float64(y) } case int32: switch y := b.(type) { @@ -3052,6 +3068,8 @@ func Divide(a, b interface{}) float64 { return float64(x) / float64(y) case float64: return float64(x) / float64(y) + case time.Duration: + return float64(x) / float64(y) } case int64: switch y := b.(type) { @@ -3079,6 +3097,8 @@ func Divide(a, b interface{}) float64 { return float64(x) / float64(y) case float64: return float64(x) / float64(y) + case time.Duration: + return float64(x) / float64(y) } case float32: switch y := b.(type) { @@ -3106,6 +3126,8 @@ func Divide(a, b interface{}) float64 { return float64(x) / float64(y) case float64: return float64(x) / float64(y) + case time.Duration: + return float64(x) / float64(y) } case float64: switch y := b.(type) { @@ -3133,6 +3155,37 @@ func Divide(a, b interface{}) float64 { return float64(x) / float64(y) case float64: return float64(x) / float64(y) + case time.Duration: + return float64(x) / float64(y) + } + case time.Duration: + switch y := b.(type) { + case uint: + return float64(x) / float64(y) + case uint8: + return float64(x) / float64(y) + case uint16: + return float64(x) / float64(y) + case uint32: + return float64(x) / float64(y) + case uint64: + return float64(x) / float64(y) + case int: + return time.Duration(x) / time.Duration(y) + case int8: + return time.Duration(x) / time.Duration(y) + case int16: + return time.Duration(x) / time.Duration(y) + case int32: + return time.Duration(x) / time.Duration(y) + case int64: + return time.Duration(x) / time.Duration(y) + case float32: + return time.Duration(x) / time.Duration(y) + case float64: + return time.Duration(x) / time.Duration(y) + case time.Duration: + return float64(x) / float64(y) } } panic(fmt.Sprintf("invalid operation: %T / %T", a, b)) diff --git a/vm/runtime/helpers/main.go b/vm/runtime/helpers/main.go index 66eb68dba..b6164c531 100644 --- a/vm/runtime/helpers/main.go +++ b/vm/runtime/helpers/main.go @@ -79,7 +79,11 @@ func cases(op string, xs ...[]string) string { } echo(`case %v:`, b) if op == "/" { - echo(`return float64(x) / float64(y)`) + if isDuration(a) && (isFloat(b) || isInt(b)) { + echo(`return time.Duration(x) / time.Duration(y)`) + } else { + echo(`return float64(x) / float64(y)`) + } } else { echo(`return %v(x) %v %v(y)`, t, op, t) } @@ -274,9 +278,9 @@ func Multiply(a, b interface{}) interface{} { panic(fmt.Sprintf("invalid operation: %T * %T", a, b)) } -func Divide(a, b interface{}) float64 { +func Divide(a, b interface{}) interface{} { switch x := a.(type) { - {{ cases "/" }} + {{ cases_with_duration "/" }} } panic(fmt.Sprintf("invalid operation: %T / %T", a, b)) } diff --git a/vm/runtime/runtime.go b/vm/runtime/runtime.go index 406f85096..ccc26a0a8 100644 --- a/vm/runtime/runtime.go +++ b/vm/runtime/runtime.go @@ -6,6 +6,7 @@ import ( "fmt" "math" "reflect" + "time" ) func deref(kind reflect.Kind, value reflect.Value) (reflect.Kind, reflect.Value) { @@ -294,6 +295,8 @@ func Negate(i any) any { return -v case uint64: return -v + case time.Duration: + return -v default: panic(fmt.Sprintf("invalid operation: - %T", v)) } From 30f2f22b0afb29d955aa415cd64b8896cdd979b8 Mon Sep 17 00:00:00 2001 From: Franck Marcia Date: Wed, 13 Dec 2023 22:52:46 +0100 Subject: [PATCH 2/2] Fix error message --- parser/parser_test.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/parser/parser_test.go b/parser/parser_test.go index d5418f35d..efadca243 100644 --- a/parser/parser_test.go +++ b/parser/parser_test.go @@ -667,7 +667,7 @@ bad number syntax: "1m" (1:3) 1h1x1s bad number syntax: "1h" (1:3) - | 1h1mt1s + | 1h1x1s | ..^ `