Skip to content

Commit d3ff814

Browse files
committed
Add len() builtin
1 parent 48b12f1 commit d3ff814

File tree

15 files changed

+114
-88
lines changed

15 files changed

+114
-88
lines changed

ast/node.go

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@ import (
44
"reflect"
55
"regexp"
66

7+
"github.com/antonmedv/expr/builtin"
78
"github.com/antonmedv/expr/file"
89
)
910

@@ -124,8 +125,7 @@ type CallNode struct {
124125
Arguments []Node
125126
Typed int
126127
Fast bool
127-
Name string
128-
Func func(params ...interface{}) (interface{}, error)
128+
Func *builtin.Function
129129
}
130130

131131
type BuiltinNode struct {

builtin/builtin.go

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
package builtin
2+
3+
import (
4+
"fmt"
5+
"reflect"
6+
7+
"github.com/antonmedv/expr/vm/runtime"
8+
)
9+
10+
type Function struct {
11+
Name string
12+
Func func(params ...interface{}) (interface{}, error)
13+
Types []reflect.Type
14+
Validate func(args []reflect.Type) error
15+
}
16+
17+
var Builtins = []*Function{
18+
{
19+
Name: "len",
20+
Func: runtime.Len,
21+
Validate: func(args []reflect.Type) error {
22+
switch args[0].Kind() {
23+
case reflect.Array, reflect.Map, reflect.Slice, reflect.String, reflect.Interface:
24+
return nil
25+
}
26+
return fmt.Errorf("invalid argument for len (type %s)", args[0])
27+
},
28+
},
29+
}

checker/checker.go

Lines changed: 20 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"regexp"
77

88
"github.com/antonmedv/expr/ast"
9+
"github.com/antonmedv/expr/builtin"
910
"github.com/antonmedv/expr/conf"
1011
"github.com/antonmedv/expr/file"
1112
"github.com/antonmedv/expr/parser"
@@ -60,7 +61,7 @@ type visitor struct {
6061

6162
type info struct {
6263
method bool
63-
fn *conf.Function
64+
fn *builtin.Function
6465
}
6566

6667
func (v *visitor) visit(node ast.Node) (reflect.Type, info) {
@@ -473,11 +474,26 @@ func (v *visitor) CallNode(node *ast.CallNode) (reflect.Type, info) {
473474

474475
if fnInfo.fn != nil {
475476
f := fnInfo.fn
476-
node.Name = f.Name
477-
node.Func = f.Func
477+
node.Func = f
478+
if f.Validate != nil {
479+
args := make([]reflect.Type, len(node.Arguments))
480+
for i, arg := range node.Arguments {
481+
args[i], _ = v.visit(arg)
482+
}
483+
if err := f.Validate(args); err != nil {
484+
return v.error(node, "%v", err)
485+
}
486+
}
478487
if len(f.Types) == 0 {
488+
t, err := v.checkFunc(f.Name, functionType, false, node)
489+
if err != nil {
490+
if v.err == nil {
491+
v.err = err
492+
}
493+
return anyType, info{}
494+
}
479495
// No type was specified, so we assume the function returns any.
480-
return anyType, info{}
496+
return t, info{}
481497
}
482498
var lastErr *file.Error
483499
for _, t := range f.Types {
@@ -627,17 +643,6 @@ func (v *visitor) checkFunc(name string, fn reflect.Type, method bool, node *ast
627643

628644
func (v *visitor) BuiltinNode(node *ast.BuiltinNode) (reflect.Type, info) {
629645
switch node.Name {
630-
631-
case "len":
632-
param, _ := v.visit(node.Arguments[0])
633-
if isArray(param) || isMap(param) || isString(param) {
634-
return integerType, info{}
635-
}
636-
if isAny(param) {
637-
return anyType, info{}
638-
}
639-
return v.error(node, "invalid argument for len (type %v)", param)
640-
641646
case "all", "none", "any", "one":
642647
collection, _ := v.visit(node.Arguments[0])
643648
if !isArray(collection) && !isAny(collection) {

checker/checker_test.go

Lines changed: 2 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -492,10 +492,6 @@ func TestCheck_error(t *testing.T) {
492492
tree, err := parser.Parse(input[0])
493493
assert.NoError(t, err)
494494

495-
if input[0] == "1 + ''" {
496-
fmt.Println(tree)
497-
}
498-
499495
_, err = checker.Check(tree, conf.New(mock.Env{}))
500496
if err == nil {
501497
err = fmt.Errorf("<nil>")
@@ -834,8 +830,8 @@ func TestCheck_Function_types_are_checked(t *testing.T) {
834830

835831
_, err = checker.Check(tree, config)
836832
require.NoError(t, err)
837-
require.Equal(t, "add", tree.Node.(*ast.CallNode).Name)
838833
require.NotNil(t, tree.Node.(*ast.CallNode).Func)
834+
require.Equal(t, "add", tree.Node.(*ast.CallNode).Func.Name)
839835
})
840836
}
841837

@@ -869,6 +865,6 @@ func TestCheck_Function_without_types(t *testing.T) {
869865

870866
_, err = checker.Check(tree, config)
871867
require.NoError(t, err)
872-
require.Equal(t, "add", tree.Node.(*ast.CallNode).Name)
873868
require.NotNil(t, tree.Node.(*ast.CallNode).Func)
869+
require.Equal(t, "add", tree.Node.(*ast.CallNode).Func.Name)
874870
}

checker/types.go

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,7 @@ var (
1919
anyType = reflect.TypeOf(new(interface{})).Elem()
2020
timeType = reflect.TypeOf(time.Time{})
2121
durationType = reflect.TypeOf(time.Duration(0))
22+
functionType = reflect.TypeOf(new(func(...interface{}) (interface{}, error))).Elem()
2223
errorType = reflect.TypeOf((*error)(nil)).Elem()
2324
)
2425

compiler/compiler.go

Lines changed: 3 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -127,18 +127,15 @@ func (c *compiler) addConstant(constant interface{}) int {
127127
}
128128

129129
func (c *compiler) addFunction(node *ast.CallNode) int {
130-
if node.Name == "" {
131-
panic("function name is empty")
132-
}
133130
if node.Func == nil {
134131
panic("function is nil")
135132
}
136-
if p, ok := c.functionsIndex[node.Name]; ok {
133+
if p, ok := c.functionsIndex[node.Func.Name]; ok {
137134
return p
138135
}
139136
p := len(c.functions)
140-
c.functions = append(c.functions, node.Func)
141-
c.functionsIndex[node.Name] = p
137+
c.functions = append(c.functions, node.Func.Func)
138+
c.functionsIndex[node.Func.Name] = p
142139
return p
143140
}
144141

@@ -558,12 +555,6 @@ func (c *compiler) CallNode(node *ast.CallNode) {
558555

559556
func (c *compiler) BuiltinNode(node *ast.BuiltinNode) {
560557
switch node.Name {
561-
case "len":
562-
c.compile(node.Arguments[0])
563-
c.emit(OpLen)
564-
c.emit(OpRot)
565-
c.emit(OpPop)
566-
567558
case "all":
568559
c.compile(node.Arguments[0])
569560
c.emit(OpBegin)

conf/config.go

Lines changed: 8 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"reflect"
66

77
"github.com/antonmedv/expr/ast"
8+
"github.com/antonmedv/expr/builtin"
89
"github.com/antonmedv/expr/vm/runtime"
910
)
1011

@@ -19,17 +20,21 @@ type Config struct {
1920
Strict bool
2021
ConstFns map[string]reflect.Value
2122
Visitors []ast.Visitor
22-
Functions map[string]*Function
23+
Functions map[string]*builtin.Function
2324
}
2425

2526
// CreateNew creates new config with default values.
2627
func CreateNew() *Config {
27-
return &Config{
28+
c := &Config{
2829
Operators: make(map[string][]string),
2930
ConstFns: make(map[string]reflect.Value),
30-
Functions: make(map[string]*Function),
31+
Functions: make(map[string]*builtin.Function),
3132
Optimize: true,
3233
}
34+
for _, builtin := range builtin.Builtins {
35+
c.Functions[builtin.Name] = builtin
36+
}
37+
return c
3338
}
3439

3540
// New creates new config with environment.

conf/functions.go

Lines changed: 0 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1 @@
11
package conf
2-
3-
import "reflect"
4-
5-
type Function struct {
6-
Name string
7-
Func func(params ...interface{}) (interface{}, error)
8-
Types []reflect.Type
9-
}

expr.go

Lines changed: 21 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -5,6 +5,7 @@ import (
55
"reflect"
66

77
"github.com/antonmedv/expr/ast"
8+
"github.com/antonmedv/expr/builtin"
89
"github.com/antonmedv/expr/checker"
910
"github.com/antonmedv/expr/compiler"
1011
"github.com/antonmedv/expr/conf"
@@ -17,30 +18,6 @@ import (
1718
// Option for configuring config.
1819
type Option func(c *conf.Config)
1920

20-
// Eval parses, compiles and runs given input.
21-
func Eval(input string, env interface{}) (interface{}, error) {
22-
if _, ok := env.(Option); ok {
23-
return nil, fmt.Errorf("misused expr.Eval: second argument (env) should be passed without expr.Env")
24-
}
25-
26-
tree, err := parser.Parse(input)
27-
if err != nil {
28-
return nil, err
29-
}
30-
31-
program, err := compiler.Compile(tree, nil)
32-
if err != nil {
33-
return nil, err
34-
}
35-
36-
output, err := vm.Run(program, env)
37-
if err != nil {
38-
return nil, err
39-
}
40-
41-
return output, nil
42-
}
43-
4421
// Env specifies expected input of env for type checks.
4522
// If struct is passed, all fields will be treated as variables,
4623
// 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),
138115
}
139116
ts[i] = t
140117
}
141-
c.Functions[name] = &conf.Function{
118+
c.Functions[name] = &builtin.Function{
142119
Name: name,
143120
Func: fn,
144121
Types: ts,
@@ -207,3 +184,22 @@ func Compile(input string, ops ...Option) (*vm.Program, error) {
207184
func Run(program *vm.Program, env interface{}) (interface{}, error) {
208185
return vm.Run(program, env)
209186
}
187+
188+
// Eval parses, compiles and runs given input.
189+
func Eval(input string, env interface{}) (interface{}, error) {
190+
if _, ok := env.(Option); ok {
191+
return nil, fmt.Errorf("misused expr.Eval: second argument (env) should be passed without expr.Env")
192+
}
193+
194+
program, err := Compile(input)
195+
if err != nil {
196+
return nil, err
197+
}
198+
199+
output, err := Run(program, env)
200+
if err != nil {
201+
return nil, err
202+
}
203+
204+
return output, nil
205+
}

expr_test.go

Lines changed: 8 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1048,10 +1048,12 @@ func TestExpr(t *testing.T) {
10481048
}
10491049

10501050
for _, tt := range tests {
1051-
got, err := expr.Eval(tt.code, env)
1052-
require.NoError(t, err, "eval error: "+tt.code)
1051+
t.Run(tt.code, func(t *testing.T) {
1052+
got, err := expr.Eval(tt.code, env)
1053+
require.NoError(t, err, "eval error: "+tt.code)
10531054

1054-
assert.Equal(t, tt.want, got, "eval: "+tt.code)
1055+
assert.Equal(t, tt.want, got, "eval: "+tt.code)
1056+
})
10551057
}
10561058
}
10571059

@@ -1300,8 +1302,8 @@ func (p *lengthPatcher) Visit(node *ast.Node) {
13001302
switch n := (*node).(type) {
13011303
case *ast.MemberNode:
13021304
if prop, ok := n.Property.(*ast.StringNode); ok && prop.Value == "length" {
1303-
ast.Patch(node, &ast.BuiltinNode{
1304-
Name: "len",
1305+
ast.Patch(node, &ast.CallNode{
1306+
Callee: &ast.IdentifierNode{Value: "len"},
13051307
Arguments: []ast.Node{n.Node},
13061308
})
13071309
}
@@ -1396,7 +1398,7 @@ func TestEval_exposed_error(t *testing.T) {
13961398

13971399
fileError, ok := err.(*file.Error)
13981400
require.True(t, ok, "error should be of type *file.Error")
1399-
require.Equal(t, "runtime error: integer divide by zero (1:3)\n | 1 % 0\n | ..^", fileError.Error())
1401+
require.Equal(t, "integer divide by zero (1:3)\n | 1 % 0\n | ..^", fileError.Error())
14001402
require.Equal(t, 2, fileError.Column)
14011403
require.Equal(t, 1, fileError.Line)
14021404
}

0 commit comments

Comments
 (0)