mirror of
https://github.com/hanzoai/expr.git
synced 2026-09-24 16:58:58 +00:00
Add len() builtin
This commit is contained in:
+2
-2
@@ -4,6 +4,7 @@ import (
|
||||
"reflect"
|
||||
"regexp"
|
||||
|
||||
"github.com/antonmedv/expr/builtin"
|
||||
"github.com/antonmedv/expr/file"
|
||||
)
|
||||
|
||||
@@ -124,8 +125,7 @@ type CallNode struct {
|
||||
Arguments []Node
|
||||
Typed int
|
||||
Fast bool
|
||||
Name string
|
||||
Func func(params ...interface{}) (interface{}, error)
|
||||
Func *builtin.Function
|
||||
}
|
||||
|
||||
type BuiltinNode struct {
|
||||
|
||||
@@ -0,0 +1,29 @@
|
||||
package builtin
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/antonmedv/expr/vm/runtime"
|
||||
)
|
||||
|
||||
type Function struct {
|
||||
Name string
|
||||
Func func(params ...interface{}) (interface{}, error)
|
||||
Types []reflect.Type
|
||||
Validate func(args []reflect.Type) error
|
||||
}
|
||||
|
||||
var Builtins = []*Function{
|
||||
{
|
||||
Name: "len",
|
||||
Func: runtime.Len,
|
||||
Validate: func(args []reflect.Type) error {
|
||||
switch args[0].Kind() {
|
||||
case reflect.Array, reflect.Map, reflect.Slice, reflect.String, reflect.Interface:
|
||||
return nil
|
||||
}
|
||||
return fmt.Errorf("invalid argument for len (type %s)", args[0])
|
||||
},
|
||||
},
|
||||
}
|
||||
+20
-15
@@ -6,6 +6,7 @@ import (
|
||||
"regexp"
|
||||
|
||||
"github.com/antonmedv/expr/ast"
|
||||
"github.com/antonmedv/expr/builtin"
|
||||
"github.com/antonmedv/expr/conf"
|
||||
"github.com/antonmedv/expr/file"
|
||||
"github.com/antonmedv/expr/parser"
|
||||
@@ -60,7 +61,7 @@ type visitor struct {
|
||||
|
||||
type info struct {
|
||||
method bool
|
||||
fn *conf.Function
|
||||
fn *builtin.Function
|
||||
}
|
||||
|
||||
func (v *visitor) visit(node ast.Node) (reflect.Type, info) {
|
||||
@@ -473,11 +474,26 @@ func (v *visitor) CallNode(node *ast.CallNode) (reflect.Type, info) {
|
||||
|
||||
if fnInfo.fn != nil {
|
||||
f := fnInfo.fn
|
||||
node.Name = f.Name
|
||||
node.Func = f.Func
|
||||
node.Func = f
|
||||
if f.Validate != nil {
|
||||
args := make([]reflect.Type, len(node.Arguments))
|
||||
for i, arg := range node.Arguments {
|
||||
args[i], _ = v.visit(arg)
|
||||
}
|
||||
if err := f.Validate(args); err != nil {
|
||||
return v.error(node, "%v", err)
|
||||
}
|
||||
}
|
||||
if len(f.Types) == 0 {
|
||||
t, err := v.checkFunc(f.Name, functionType, false, node)
|
||||
if err != nil {
|
||||
if v.err == nil {
|
||||
v.err = err
|
||||
}
|
||||
return anyType, info{}
|
||||
}
|
||||
// No type was specified, so we assume the function returns any.
|
||||
return anyType, info{}
|
||||
return t, info{}
|
||||
}
|
||||
var lastErr *file.Error
|
||||
for _, t := range f.Types {
|
||||
@@ -627,17 +643,6 @@ func (v *visitor) checkFunc(name string, fn reflect.Type, method bool, node *ast
|
||||
|
||||
func (v *visitor) BuiltinNode(node *ast.BuiltinNode) (reflect.Type, info) {
|
||||
switch node.Name {
|
||||
|
||||
case "len":
|
||||
param, _ := v.visit(node.Arguments[0])
|
||||
if isArray(param) || isMap(param) || isString(param) {
|
||||
return integerType, info{}
|
||||
}
|
||||
if isAny(param) {
|
||||
return anyType, info{}
|
||||
}
|
||||
return v.error(node, "invalid argument for len (type %v)", param)
|
||||
|
||||
case "all", "none", "any", "one":
|
||||
collection, _ := v.visit(node.Arguments[0])
|
||||
if !isArray(collection) && !isAny(collection) {
|
||||
|
||||
@@ -492,10 +492,6 @@ func TestCheck_error(t *testing.T) {
|
||||
tree, err := parser.Parse(input[0])
|
||||
assert.NoError(t, err)
|
||||
|
||||
if input[0] == "1 + ''" {
|
||||
fmt.Println(tree)
|
||||
}
|
||||
|
||||
_, err = checker.Check(tree, conf.New(mock.Env{}))
|
||||
if err == nil {
|
||||
err = fmt.Errorf("<nil>")
|
||||
@@ -834,8 +830,8 @@ func TestCheck_Function_types_are_checked(t *testing.T) {
|
||||
|
||||
_, err = checker.Check(tree, config)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "add", tree.Node.(*ast.CallNode).Name)
|
||||
require.NotNil(t, tree.Node.(*ast.CallNode).Func)
|
||||
require.Equal(t, "add", tree.Node.(*ast.CallNode).Func.Name)
|
||||
})
|
||||
}
|
||||
|
||||
@@ -869,6 +865,6 @@ func TestCheck_Function_without_types(t *testing.T) {
|
||||
|
||||
_, err = checker.Check(tree, config)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "add", tree.Node.(*ast.CallNode).Name)
|
||||
require.NotNil(t, tree.Node.(*ast.CallNode).Func)
|
||||
require.Equal(t, "add", tree.Node.(*ast.CallNode).Func.Name)
|
||||
}
|
||||
|
||||
@@ -19,6 +19,7 @@ var (
|
||||
anyType = reflect.TypeOf(new(interface{})).Elem()
|
||||
timeType = reflect.TypeOf(time.Time{})
|
||||
durationType = reflect.TypeOf(time.Duration(0))
|
||||
functionType = reflect.TypeOf(new(func(...interface{}) (interface{}, error))).Elem()
|
||||
errorType = reflect.TypeOf((*error)(nil)).Elem()
|
||||
)
|
||||
|
||||
|
||||
+3
-12
@@ -127,18 +127,15 @@ func (c *compiler) addConstant(constant interface{}) int {
|
||||
}
|
||||
|
||||
func (c *compiler) addFunction(node *ast.CallNode) int {
|
||||
if node.Name == "" {
|
||||
panic("function name is empty")
|
||||
}
|
||||
if node.Func == nil {
|
||||
panic("function is nil")
|
||||
}
|
||||
if p, ok := c.functionsIndex[node.Name]; ok {
|
||||
if p, ok := c.functionsIndex[node.Func.Name]; ok {
|
||||
return p
|
||||
}
|
||||
p := len(c.functions)
|
||||
c.functions = append(c.functions, node.Func)
|
||||
c.functionsIndex[node.Name] = p
|
||||
c.functions = append(c.functions, node.Func.Func)
|
||||
c.functionsIndex[node.Func.Name] = p
|
||||
return p
|
||||
}
|
||||
|
||||
@@ -558,12 +555,6 @@ func (c *compiler) CallNode(node *ast.CallNode) {
|
||||
|
||||
func (c *compiler) BuiltinNode(node *ast.BuiltinNode) {
|
||||
switch node.Name {
|
||||
case "len":
|
||||
c.compile(node.Arguments[0])
|
||||
c.emit(OpLen)
|
||||
c.emit(OpRot)
|
||||
c.emit(OpPop)
|
||||
|
||||
case "all":
|
||||
c.compile(node.Arguments[0])
|
||||
c.emit(OpBegin)
|
||||
|
||||
+8
-3
@@ -5,6 +5,7 @@ import (
|
||||
"reflect"
|
||||
|
||||
"github.com/antonmedv/expr/ast"
|
||||
"github.com/antonmedv/expr/builtin"
|
||||
"github.com/antonmedv/expr/vm/runtime"
|
||||
)
|
||||
|
||||
@@ -19,17 +20,21 @@ type Config struct {
|
||||
Strict bool
|
||||
ConstFns map[string]reflect.Value
|
||||
Visitors []ast.Visitor
|
||||
Functions map[string]*Function
|
||||
Functions map[string]*builtin.Function
|
||||
}
|
||||
|
||||
// CreateNew creates new config with default values.
|
||||
func CreateNew() *Config {
|
||||
return &Config{
|
||||
c := &Config{
|
||||
Operators: make(map[string][]string),
|
||||
ConstFns: make(map[string]reflect.Value),
|
||||
Functions: make(map[string]*Function),
|
||||
Functions: make(map[string]*builtin.Function),
|
||||
Optimize: true,
|
||||
}
|
||||
for _, builtin := range builtin.Builtins {
|
||||
c.Functions[builtin.Name] = builtin
|
||||
}
|
||||
return c
|
||||
}
|
||||
|
||||
// New creates new config with environment.
|
||||
|
||||
@@ -1,9 +1 @@
|
||||
package conf
|
||||
|
||||
import "reflect"
|
||||
|
||||
type Function struct {
|
||||
Name string
|
||||
Func func(params ...interface{}) (interface{}, error)
|
||||
Types []reflect.Type
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ import (
|
||||
"reflect"
|
||||
|
||||
"github.com/antonmedv/expr/ast"
|
||||
"github.com/antonmedv/expr/builtin"
|
||||
"github.com/antonmedv/expr/checker"
|
||||
"github.com/antonmedv/expr/compiler"
|
||||
"github.com/antonmedv/expr/conf"
|
||||
@@ -17,30 +18,6 @@ import (
|
||||
// Option for configuring config.
|
||||
type Option func(c *conf.Config)
|
||||
|
||||
// Eval parses, compiles and runs given input.
|
||||
func Eval(input string, env interface{}) (interface{}, error) {
|
||||
if _, ok := env.(Option); ok {
|
||||
return nil, fmt.Errorf("misused expr.Eval: second argument (env) should be passed without expr.Env")
|
||||
}
|
||||
|
||||
tree, err := parser.Parse(input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
program, err := compiler.Compile(tree, nil)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
output, err := vm.Run(program, env)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return output, nil
|
||||
}
|
||||
|
||||
// Env specifies expected input of env for type checks.
|
||||
// If struct is passed, all fields will be treated as variables,
|
||||
// as well as all fields of embedded structs and struct itself.
|
||||
@@ -138,7 +115,7 @@ func Function(name string, fn func(params ...interface{}) (interface{}, error),
|
||||
}
|
||||
ts[i] = t
|
||||
}
|
||||
c.Functions[name] = &conf.Function{
|
||||
c.Functions[name] = &builtin.Function{
|
||||
Name: name,
|
||||
Func: fn,
|
||||
Types: ts,
|
||||
@@ -207,3 +184,22 @@ func Compile(input string, ops ...Option) (*vm.Program, error) {
|
||||
func Run(program *vm.Program, env interface{}) (interface{}, error) {
|
||||
return vm.Run(program, env)
|
||||
}
|
||||
|
||||
// Eval parses, compiles and runs given input.
|
||||
func Eval(input string, env interface{}) (interface{}, error) {
|
||||
if _, ok := env.(Option); ok {
|
||||
return nil, fmt.Errorf("misused expr.Eval: second argument (env) should be passed without expr.Env")
|
||||
}
|
||||
|
||||
program, err := Compile(input)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
output, err := Run(program, env)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return output, nil
|
||||
}
|
||||
|
||||
+8
-6
@@ -1048,10 +1048,12 @@ func TestExpr(t *testing.T) {
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
got, err := expr.Eval(tt.code, env)
|
||||
require.NoError(t, err, "eval error: "+tt.code)
|
||||
t.Run(tt.code, func(t *testing.T) {
|
||||
got, err := expr.Eval(tt.code, env)
|
||||
require.NoError(t, err, "eval error: "+tt.code)
|
||||
|
||||
assert.Equal(t, tt.want, got, "eval: "+tt.code)
|
||||
assert.Equal(t, tt.want, got, "eval: "+tt.code)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1300,8 +1302,8 @@ func (p *lengthPatcher) Visit(node *ast.Node) {
|
||||
switch n := (*node).(type) {
|
||||
case *ast.MemberNode:
|
||||
if prop, ok := n.Property.(*ast.StringNode); ok && prop.Value == "length" {
|
||||
ast.Patch(node, &ast.BuiltinNode{
|
||||
Name: "len",
|
||||
ast.Patch(node, &ast.CallNode{
|
||||
Callee: &ast.IdentifierNode{Value: "len"},
|
||||
Arguments: []ast.Node{n.Node},
|
||||
})
|
||||
}
|
||||
@@ -1396,7 +1398,7 @@ func TestEval_exposed_error(t *testing.T) {
|
||||
|
||||
fileError, ok := err.(*file.Error)
|
||||
require.True(t, ok, "error should be of type *file.Error")
|
||||
require.Equal(t, "runtime error: integer divide by zero (1:3)\n | 1 % 0\n | ..^", fileError.Error())
|
||||
require.Equal(t, "integer divide by zero (1:3)\n | 1 % 0\n | ..^", fileError.Error())
|
||||
require.Equal(t, 2, fileError.Column)
|
||||
require.Equal(t, 1, fileError.Line)
|
||||
}
|
||||
|
||||
@@ -61,7 +61,6 @@ var binaryOperators = map[string]operator{
|
||||
}
|
||||
|
||||
var builtins = map[string]builtin{
|
||||
"len": {1},
|
||||
"all": {2},
|
||||
"none": {2},
|
||||
"any": {2},
|
||||
|
||||
@@ -264,8 +264,12 @@ func TestParse(t *testing.T) {
|
||||
},
|
||||
{
|
||||
"len(foo)",
|
||||
&BuiltinNode{Name: "len",
|
||||
Arguments: []Node{&IdentifierNode{Value: "foo"}}},
|
||||
&CallNode{
|
||||
Callee: &IdentifierNode{Value: "len"},
|
||||
Arguments: []Node{
|
||||
&IdentifierNode{Value: "foo"},
|
||||
},
|
||||
},
|
||||
},
|
||||
{
|
||||
`foo matches "foo"`,
|
||||
|
||||
@@ -1,3 +0,0 @@
|
||||
package vm
|
||||
|
||||
type Function = func(params ...interface{}) (interface{}, error)
|
||||
@@ -242,13 +242,16 @@ func In(needle interface{}, array interface{}) bool {
|
||||
panic(fmt.Sprintf(`operator "in"" not defined on %T`, array))
|
||||
}
|
||||
|
||||
func Length(a interface{}) int {
|
||||
v := reflect.ValueOf(a)
|
||||
func Len(args ...interface{}) (interface{}, error) {
|
||||
if len(args) != 1 {
|
||||
return 0, fmt.Errorf("invalid number of arguments for len (expected 1, got %d)", len(args))
|
||||
}
|
||||
v := reflect.ValueOf(args[0])
|
||||
switch v.Kind() {
|
||||
case reflect.Array, reflect.Slice, reflect.Map, reflect.String:
|
||||
return v.Len()
|
||||
return v.Len(), nil
|
||||
default:
|
||||
panic(fmt.Sprintf("invalid argument for len (type %T)", a))
|
||||
return 0, fmt.Errorf("invalid argument for len (type %T)", args[0])
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,6 +15,8 @@ import (
|
||||
var MemoryBudget int = 1e6
|
||||
var errorType = reflect.TypeOf((*error)(nil)).Elem()
|
||||
|
||||
type Function = func(params ...interface{}) (interface{}, error)
|
||||
|
||||
func Run(program *Program, env interface{}) (interface{}, error) {
|
||||
if program == nil {
|
||||
return nil, fmt.Errorf("program is nil")
|
||||
@@ -397,7 +399,11 @@ func (vm *VM) Run(program *Program, env interface{}) (_ interface{}, err error)
|
||||
}
|
||||
|
||||
case OpLen:
|
||||
vm.push(runtime.Length(vm.current()))
|
||||
l, err := runtime.Len(vm.current())
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
vm.push(l)
|
||||
|
||||
case OpCast:
|
||||
t := arg
|
||||
|
||||
Reference in New Issue
Block a user