Add len() builtin

This commit is contained in:
Anton Medvedev
2023-01-29 20:34:15 +01:00
parent 75edfd2b79
commit 72a4ce18e9
15 changed files with 114 additions and 88 deletions
+2 -2
View File
@@ -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 {
+29
View File
@@ -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
View File
@@ -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) {
+2 -6
View File
@@ -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)
}
+1
View File
@@ -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
View File
@@ -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
View File
@@ -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.
-8
View File
@@ -1,9 +1 @@
package conf
import "reflect"
type Function struct {
Name string
Func func(params ...interface{}) (interface{}, error)
Types []reflect.Type
}
+21 -25
View File
@@ -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
View File
@@ -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)
}
-1
View File
@@ -61,7 +61,6 @@ var binaryOperators = map[string]operator{
}
var builtins = map[string]builtin{
"len": {1},
"all": {2},
"none": {2},
"any": {2},
+6 -2
View File
@@ -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"`,
-3
View File
@@ -1,3 +0,0 @@
package vm
type Function = func(params ...interface{}) (interface{}, error)
+7 -4
View File
@@ -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])
}
}
+7 -1
View File
@@ -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