Add OpCallBuiltin opcode to avoid an extra allocation

This commit is contained in:
Anton Medvedev
2023-08-14 11:50:42 +02:00
parent 8d959ea2fe
commit 8177d8bd9f
13 changed files with 395 additions and 295 deletions
+18 -8
View File
@@ -8,14 +8,24 @@ import (
type Function struct {
Name string
Func func(args ...interface{}) (interface{}, error)
Builtin1 func(arg interface{}) interface{}
Types []reflect.Type
Validate func(args []reflect.Type) (reflect.Type, error)
}
var Index map[string]int
func init() {
Index = make(map[string]int)
for i, fn := range Builtins {
Index[fn.Name] = i
}
}
var Builtins = []*Function{
{
Name: "len",
Func: Len,
Name: "len",
Builtin1: Len,
Validate: func(args []reflect.Type) (reflect.Type, error) {
if len(args) != 1 {
return anyType, fmt.Errorf("invalid number of arguments for len (expected 1, got %d)", len(args))
@@ -28,8 +38,8 @@ var Builtins = []*Function{
},
},
{
Name: "abs",
Func: Abs,
Name: "abs",
Builtin1: Abs,
Validate: func(args []reflect.Type) (reflect.Type, error) {
if len(args) != 1 {
return anyType, fmt.Errorf("invalid number of arguments for abs (expected 1, got %d)", len(args))
@@ -42,8 +52,8 @@ var Builtins = []*Function{
},
},
{
Name: "int",
Func: Int,
Name: "int",
Builtin1: Int,
Validate: func(args []reflect.Type) (reflect.Type, error) {
if len(args) != 1 {
return anyType, fmt.Errorf("invalid number of arguments for int (expected 1, got %d)", len(args))
@@ -60,8 +70,8 @@ var Builtins = []*Function{
},
},
{
Name: "float",
Func: Float,
Name: "float",
Builtin1: Float,
Validate: func(args []reflect.Type) (reflect.Type, error) {
if len(args) != 1 {
return anyType, fmt.Errorf("invalid number of arguments for float (expected 1, got %d)", len(args))
+61 -65
View File
@@ -6,168 +6,164 @@ import (
"strconv"
)
func Len(xs ...interface{}) (interface{}, error) {
x := xs[0]
func Len(x interface{}) interface{} {
v := reflect.ValueOf(x)
switch v.Kind() {
case reflect.Array, reflect.Slice, reflect.Map, reflect.String:
return v.Len(), nil
return v.Len()
default:
return nil, fmt.Errorf("invalid argument for len (type %T)", x)
panic(fmt.Sprintf("invalid argument for len (type %T)", x))
}
}
func Abs(xs ...interface{}) (interface{}, error) {
x := xs[0]
func Abs(x interface{}) interface{} {
switch x.(type) {
case float32:
if x.(float32) < 0 {
return -x.(float32), nil
return -x.(float32)
} else {
return x, nil
return x
}
case float64:
if x.(float64) < 0 {
return -x.(float64), nil
return -x.(float64)
} else {
return x, nil
return x
}
case int:
if x.(int) < 0 {
return -x.(int), nil
return -x.(int)
} else {
return x, nil
return x
}
case int8:
if x.(int8) < 0 {
return -x.(int8), nil
return -x.(int8)
} else {
return x, nil
return x
}
case int16:
if x.(int16) < 0 {
return -x.(int16), nil
return -x.(int16)
} else {
return x, nil
return x
}
case int32:
if x.(int32) < 0 {
return -x.(int32), nil
return -x.(int32)
} else {
return x, nil
return x
}
case int64:
if x.(int64) < 0 {
return -x.(int64), nil
return -x.(int64)
} else {
return x, nil
return x
}
case uint:
if x.(uint) < 0 {
return -x.(uint), nil
return -x.(uint)
} else {
return x, nil
return x
}
case uint8:
if x.(uint8) < 0 {
return -x.(uint8), nil
return -x.(uint8)
} else {
return x, nil
return x
}
case uint16:
if x.(uint16) < 0 {
return -x.(uint16), nil
return -x.(uint16)
} else {
return x, nil
return x
}
case uint32:
if x.(uint32) < 0 {
return -x.(uint32), nil
return -x.(uint32)
} else {
return x, nil
return x
}
case uint64:
if x.(uint64) < 0 {
return -x.(uint64), nil
return -x.(uint64)
} else {
return x, nil
return x
}
}
return nil, fmt.Errorf("invalid argument for abs (type %T)", x)
panic(fmt.Sprintf("invalid argument for abs (type %T)", x))
}
func Int(xs ...interface{}) (interface{}, error) {
x := xs[0]
func Int(x interface{}) interface{} {
switch x := x.(type) {
case float32:
return int(x), nil
return int(x)
case float64:
return int(x), nil
return int(x)
case int:
return x, nil
return x
case int8:
return int(x), nil
return int(x)
case int16:
return int(x), nil
return int(x)
case int32:
return int(x), nil
return int(x)
case int64:
return int(x), nil
return int(x)
case uint:
return int(x), nil
return int(x)
case uint8:
return int(x), nil
return int(x)
case uint16:
return int(x), nil
return int(x)
case uint32:
return int(x), nil
return int(x)
case uint64:
return int(x), nil
return int(x)
case string:
i, err := strconv.Atoi(x)
if err != nil {
return nil, fmt.Errorf("invalid operation: int(%s)", x)
panic(fmt.Sprintf("invalid operation: int(%s)", x))
}
return i, nil
return i
default:
return nil, fmt.Errorf("invalid operation: int(%T)", x)
panic(fmt.Sprintf("invalid operation: int(%T)", x))
}
}
func Float(xs ...interface{}) (interface{}, error) {
x := xs[0]
func Float(x interface{}) interface{} {
switch x := x.(type) {
case float32:
return float64(x), nil
return float64(x)
case float64:
return x, nil
return x
case int:
return float64(x), nil
return float64(x)
case int8:
return float64(x), nil
return float64(x)
case int16:
return float64(x), nil
return float64(x)
case int32:
return float64(x), nil
return float64(x)
case int64:
return float64(x), nil
return float64(x)
case uint:
return float64(x), nil
return float64(x)
case uint8:
return float64(x), nil
return float64(x)
case uint16:
return float64(x), nil
return float64(x)
case uint32:
return float64(x), nil
return float64(x)
case uint64:
return float64(x), nil
return float64(x)
case string:
f, err := strconv.ParseFloat(x, 64)
if err != nil {
return nil, fmt.Errorf("invalid operation: float(%s)", x)
panic(fmt.Sprintf("invalid operation: float(%s)", x))
}
return f, nil
return f
default:
return nil, fmt.Errorf("invalid operation: float(%T)", x)
panic(fmt.Sprintf("invalid operation: float(%T)", x))
}
}
+135 -127
View File
@@ -493,45 +493,8 @@ func (v *visitor) CallNode(node *ast.CallNode) (reflect.Type, info) {
fn, fnInfo := v.visit(node.Callee)
if fnInfo.fn != nil {
f := fnInfo.fn
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)
}
t, err := f.Validate(args)
if err != nil {
return v.error(node, "%v", err)
}
return t, info{}
}
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 t, info{}
}
var lastErr *file.Error
for _, t := range f.Types {
outType, err := v.checkFunc(f.Name, t, false, node)
if err != nil {
lastErr = err
continue
}
return outType, info{}
}
if lastErr != nil {
if v.err == nil {
v.err = lastErr
}
return anyType, info{}
}
node.Func = fnInfo.fn
return v.checkFunction(fnInfo.fn, node.Arguments, node)
}
fnName := "function"
@@ -564,7 +527,7 @@ func (v *visitor) CallNode(node *ast.CallNode) (reflect.Type, info) {
}
}
outType, err := v.checkFunc(fnName, fn, fnInfo.method, node)
outType, err := v.checkArguments(fnName, fn, fnInfo.method, node.Arguments, node)
if err != nil {
if v.err == nil {
v.err = err
@@ -579,90 +542,6 @@ func (v *visitor) CallNode(node *ast.CallNode) (reflect.Type, info) {
return v.error(node, "%v is not callable", fn)
}
func (v *visitor) checkFunc(name string, fn reflect.Type, method bool, node *ast.CallNode) (reflect.Type, *file.Error) {
if isAny(fn) {
return anyType, nil
}
if fn.NumOut() == 0 {
return anyType, &file.Error{
Location: node.Location(),
Message: fmt.Sprintf("func %v doesn't return value", name),
}
}
if numOut := fn.NumOut(); numOut > 2 {
return anyType, &file.Error{
Location: node.Location(),
Message: fmt.Sprintf("func %v returns more then two values", name),
}
}
// If func is method on an env, first argument should be a receiver,
// and actual arguments less than fnNumIn by one.
fnNumIn := fn.NumIn()
if method {
fnNumIn--
}
// Skip first argument in case of the receiver.
fnInOffset := 0
if method {
fnInOffset = 1
}
if fn.IsVariadic() {
if len(node.Arguments) < fnNumIn-1 {
return anyType, &file.Error{
Location: node.Location(),
Message: fmt.Sprintf("not enough arguments to call %v", name),
}
}
} else {
if len(node.Arguments) > fnNumIn {
return anyType, &file.Error{
Location: node.Location(),
Message: fmt.Sprintf("too many arguments to call %v", name),
}
}
if len(node.Arguments) < fnNumIn {
return anyType, &file.Error{
Location: node.Location(),
Message: fmt.Sprintf("not enough arguments to call %v", name),
}
}
}
for i, arg := range node.Arguments {
t, _ := v.visit(arg)
var in reflect.Type
if fn.IsVariadic() && i >= fnNumIn-1 {
// For variadic arguments fn(xs ...int), go replaces type of xs (int) with ([]int).
// As we compare arguments one by one, we need underling type.
in = fn.In(fn.NumIn() - 1).Elem()
} else {
in = fn.In(i + fnInOffset)
}
if isIntegerOrArithmeticOperation(arg) && (isInteger(in) || isFloat(in)) {
t = in
setTypeForIntegers(arg, t)
}
if t == nil {
continue
}
if !t.AssignableTo(in) && t.Kind() != reflect.Interface {
return anyType, &file.Error{
Location: arg.Location(),
Message: fmt.Sprintf("cannot use %v as argument (type %v) to call %v ", t, in, name),
}
}
}
return fn.Out(0), nil
}
func (v *visitor) BuiltinNode(node *ast.BuiltinNode) (reflect.Type, info) {
switch node.Name {
case "all", "none", "any", "one":
@@ -748,10 +627,139 @@ func (v *visitor) BuiltinNode(node *ast.BuiltinNode) (reflect.Type, info) {
return integerType, info{}
}
return v.error(node.Arguments[1], "closure should has one input and one output param")
default:
return v.error(node, "unknown builtin %v", node.Name)
}
if id, ok := builtin.Index[node.Name]; ok {
return v.checkFunction(builtin.Builtins[id], node.Arguments, node)
}
return v.error(node, "unknown builtin %v", node.Name)
}
func (v *visitor) checkFunction(f *builtin.Function, arguments []ast.Node, node ast.Node) (reflect.Type, info) {
if f.Validate != nil {
args := make([]reflect.Type, len(arguments))
for i, arg := range arguments {
args[i], _ = v.visit(arg)
}
t, err := f.Validate(args)
if err != nil {
return v.error(node, "%v", err)
}
return t, info{}
}
if len(f.Types) == 0 {
t, err := v.checkArguments(f.Name, functionType, false, arguments, 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 t, info{}
}
var lastErr *file.Error
for _, t := range f.Types {
outType, err := v.checkArguments(f.Name, t, false, arguments, node)
if err != nil {
lastErr = err
continue
}
return outType, info{}
}
if lastErr != nil {
if v.err == nil {
v.err = lastErr
}
return anyType, info{}
}
return v.error(node, "no matching overload for %v", f.Name)
}
func (v *visitor) checkArguments(name string, fn reflect.Type, method bool, arguments []ast.Node, node ast.Node) (reflect.Type, *file.Error) {
if isAny(fn) {
return anyType, nil
}
if fn.NumOut() == 0 {
return anyType, &file.Error{
Location: node.Location(),
Message: fmt.Sprintf("func %v doesn't return value", name),
}
}
if numOut := fn.NumOut(); numOut > 2 {
return anyType, &file.Error{
Location: node.Location(),
Message: fmt.Sprintf("func %v returns more then two values", name),
}
}
// If func is method on an env, first argument should be a receiver,
// and actual arguments less than fnNumIn by one.
fnNumIn := fn.NumIn()
if method {
fnNumIn--
}
// Skip first argument in case of the receiver.
fnInOffset := 0
if method {
fnInOffset = 1
}
if fn.IsVariadic() {
if len(arguments) < fnNumIn-1 {
return anyType, &file.Error{
Location: node.Location(),
Message: fmt.Sprintf("not enough arguments to call %v", name),
}
}
} else {
if len(arguments) > fnNumIn {
return anyType, &file.Error{
Location: node.Location(),
Message: fmt.Sprintf("too many arguments to call %v", name),
}
}
if len(arguments) < fnNumIn {
return anyType, &file.Error{
Location: node.Location(),
Message: fmt.Sprintf("not enough arguments to call %v", name),
}
}
}
for i, arg := range arguments {
t, _ := v.visit(arg)
var in reflect.Type
if fn.IsVariadic() && i >= fnNumIn-1 {
// For variadic arguments fn(xs ...int), go replaces type of xs (int) with ([]int).
// As we compare arguments one by one, we need underling type.
in = fn.In(fn.NumIn() - 1).Elem()
} else {
in = fn.In(i + fnInOffset)
}
if isIntegerOrArithmeticOperation(arg) && (isInteger(in) || isFloat(in)) {
t = in
setTypeForIntegers(arg, t)
}
if t == nil {
continue
}
if !t.AssignableTo(in) && t.Kind() != reflect.Interface {
return anyType, &file.Error{
Location: arg.Location(),
Message: fmt.Sprintf("cannot use %v as argument (type %v) to call %v ", t, in, name),
}
}
}
return fn.Out(0), nil
}
func (v *visitor) ClosureNode(node *ast.ClosureNode) (reflect.Type, info) {
+18 -3
View File
@@ -5,6 +5,7 @@ import (
"reflect"
"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"
@@ -603,6 +604,7 @@ func (c *compiler) BuiltinNode(node *ast.BuiltinNode) {
c.emit(OpTrue)
c.patchJump(loopBreak)
c.emit(OpEnd)
return
case "none":
c.compile(node.Arguments[0])
@@ -617,6 +619,7 @@ func (c *compiler) BuiltinNode(node *ast.BuiltinNode) {
c.emit(OpTrue)
c.patchJump(loopBreak)
c.emit(OpEnd)
return
case "any":
c.compile(node.Arguments[0])
@@ -630,6 +633,7 @@ func (c *compiler) BuiltinNode(node *ast.BuiltinNode) {
c.emit(OpFalse)
c.patchJump(loopBreak)
c.emit(OpEnd)
return
case "one":
c.compile(node.Arguments[0])
@@ -644,6 +648,7 @@ func (c *compiler) BuiltinNode(node *ast.BuiltinNode) {
c.emitPush(1)
c.emit(OpEqual)
c.emit(OpEnd)
return
case "filter":
c.compile(node.Arguments[0])
@@ -658,6 +663,7 @@ func (c *compiler) BuiltinNode(node *ast.BuiltinNode) {
c.emit(OpGetCount)
c.emit(OpEnd)
c.emit(OpArray)
return
case "map":
c.compile(node.Arguments[0])
@@ -668,6 +674,7 @@ func (c *compiler) BuiltinNode(node *ast.BuiltinNode) {
c.emit(OpGetLen)
c.emit(OpEnd)
c.emit(OpArray)
return
case "count":
c.compile(node.Arguments[0])
@@ -680,10 +687,18 @@ func (c *compiler) BuiltinNode(node *ast.BuiltinNode) {
})
c.emit(OpGetCount)
c.emit(OpEnd)
default:
panic(fmt.Sprintf("unknown builtin %v", node.Name))
return
}
if id, ok := builtin.Index[node.Name]; ok {
for _, arg := range node.Arguments {
c.compile(arg)
}
c.emit(OpCallBuiltin, id)
return
}
panic(fmt.Sprintf("unknown builtin %v", node.Name))
}
func (c *compiler) emitCond(body func()) {
-70
View File
@@ -1308,34 +1308,6 @@ func TestPatch(t *testing.T) {
require.Equal(t, true, output)
}
type lengthPatcher struct{}
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.CallNode{
Callee: &ast.IdentifierNode{Value: "len"},
Arguments: []ast.Node{n.Node},
})
}
}
}
func TestPatch_length(t *testing.T) {
program, err := expr.Compile(
`String.length == 5`,
expr.Env(mockEnv{}),
expr.Patch(&lengthPatcher{}),
)
require.NoError(t, err)
env := mockEnv{String: "hello"}
output, err := expr.Run(program, env)
require.NoError(t, err)
require.Equal(t, true, output)
}
func TestCompile_exposed_error(t *testing.T) {
_, err := expr.Compile(`1 == true`)
require.Error(t, err)
@@ -1864,48 +1836,6 @@ func TestEnv_keyword(t *testing.T) {
}
type Bar interface {
Bar() int
}
type Foo interface {
Foo() Bar
}
type FooImpl struct{}
func (f FooImpl) Foo() Bar {
return BarImpl{}
}
type BarImpl struct{}
// Aba is a special method that is not part of the Bar interface,
// but is used to test that the correct method is called. "Aba" name
// is chosen to be before "Bar" in the alphabet.
func (b BarImpl) Aba() bool {
return true
}
func (b BarImpl) Bar() int {
return 42
}
func TestEval_interface_method(t *testing.T) {
require.True(t, BarImpl{}.Aba())
require.True(t, BarImpl{}.Bar() == 42)
env := map[string]interface{}{
"var": FooImpl{},
}
p, err := expr.Compile(`var.Foo().Bar()`, expr.Env(env))
assert.NoError(t, err)
out, err := expr.Run(p, env)
assert.NoError(t, err)
assert.Equal(t, 42, out)
}
// Mock types
type mockEnv struct {
+19 -6
View File
@@ -7,6 +7,7 @@ import (
"unicode/utf8"
. "github.com/antonmedv/expr/ast"
"github.com/antonmedv/expr/builtin"
"github.com/antonmedv/expr/file"
. "github.com/antonmedv/expr/parser/lexer"
)
@@ -57,11 +58,9 @@ var binaryOperators = map[string]operator{
"??": {500, left},
}
type builtin struct {
var predicates = map[string]struct {
arity int
}
var builtins = map[string]builtin{
}{
"all": {2},
"none": {2},
"any": {2},
@@ -355,7 +354,7 @@ func (p *parser) parseCall(token Token) Node {
if p.current.Is(Bracket, "(") {
var arguments []Node
if b, ok := builtins[token.Value]; ok {
if b, ok := predicates[token.Value]; ok {
p.expect(Bracket, "(")
if b.arity == 1 {
arguments = make([]Node, 1)
@@ -373,6 +372,12 @@ func (p *parser) parseCall(token Token) Node {
Arguments: arguments,
}
node.SetLocation(token.Location)
} else if _, ok := builtin.Index[token.Value]; ok {
node = &BuiltinNode{
Name: token.Value,
Arguments: p.parseArguments(),
}
node.SetLocation(token.Location)
} else {
callee := &IdentifierNode{Value: token.Value}
callee.SetLocation(token.Location)
@@ -588,13 +593,21 @@ func (p *parser) parsePipe(node Node) Node {
arguments := []Node{node}
if b, ok := builtins[identifier.Value]; ok {
if b, ok := predicates[identifier.Value]; ok {
p.expect(Bracket, "(")
if b.arity == 2 {
arguments = append(arguments, p.parseClosure())
}
p.expect(Bracket, ")")
node = &BuiltinNode{
Name: identifier.Value,
Arguments: arguments,
}
node.SetLocation(identifier.Location)
} else if _, ok := builtin.Index[identifier.Value]; ok {
arguments = append(arguments, p.parseArguments()...)
node = &BuiltinNode{
Name: identifier.Value,
Arguments: arguments,
+4 -4
View File
@@ -265,8 +265,8 @@ func TestParse(t *testing.T) {
},
{
"len(foo)",
&CallNode{
Callee: &IdentifierNode{Value: "len"},
&BuiltinNode{
Name: "len",
Arguments: []Node{
&IdentifierNode{Value: "foo"},
},
@@ -644,8 +644,8 @@ func TestParse_pipe_operator(t *testing.T) {
expect := &CallNode{
Callee: &IdentifierNode{Value: "Foo"},
Arguments: []Node{
&CallNode{
Callee: &IdentifierNode{Value: "len"},
&BuiltinNode{
Name: "len",
Arguments: []Node{
&BuiltinNode{
Name: "map",
+42 -12
View File
@@ -2,37 +2,67 @@ package main
import (
"fmt"
"strings"
"github.com/antonmedv/expr"
"github.com/antonmedv/expr/vm"
"github.com/chzyer/readline"
)
func main() {
rl, err := readline.New("> ")
rl, err := readline.NewEx(&readline.Config{
Prompt: "> ",
AutoComplete: readline.NewPrefixCompleter(
readline.PcItem("exit"),
readline.PcItem("opcodes"),
readline.PcItem("len"),
readline.PcItem("abs"),
readline.PcItem("int"),
readline.PcItem("float"),
readline.PcItem("map"),
readline.PcItem("filter"),
readline.PcItem("all"),
readline.PcItem("any"),
readline.PcItem("none"),
readline.PcItem("one"),
readline.PcItem("count"),
readline.PcItem("sum"),
),
})
if err != nil {
panic(err)
}
defer rl.Close()
var program *vm.Program
for {
line, err := rl.Readline()
if err != nil { // io.EOF when Ctrl-D is pressed
break
}
line = strings.TrimSpace(line)
if line == "exit" {
break
}
if line == "opcodes" {
if program == nil {
fmt.Println("no program")
continue
}
fmt.Println(program.Disassemble())
continue
}
result := evaluate(line)
fmt.Println(result)
program, err = expr.Compile(line, expr.Env(nil))
if err != nil {
fmt.Printf("compile error: %s\n", err)
continue
}
output, err := expr.Run(program, nil)
if err != nil {
fmt.Printf("runtime error: %s\n", err)
continue
}
fmt.Println(output)
}
}
func evaluate(input string) string {
result, err := expr.Eval(input, nil)
if err != nil {
return err.Error()
}
return fmt.Sprintf("%v", result)
}
@@ -0,0 +1,48 @@
package interface_method_test
import (
"testing"
"github.com/antonmedv/expr"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"
)
type Bar interface {
Bar() int
}
type FooImpl struct{}
func (f FooImpl) Foo() Bar {
return BarImpl{}
}
type BarImpl struct{}
// Aba is a special method that is not part of the Bar interface,
// but is used to test that the correct method is called. "Aba" name
// is chosen to be before "Bar" in the alphabet.
func (b BarImpl) Aba() bool {
return true
}
func (b BarImpl) Bar() int {
return 42
}
func TestInterfaceMethod(t *testing.T) {
require.True(t, BarImpl{}.Aba())
require.True(t, BarImpl{}.Bar() == 42)
env := map[string]interface{}{
"var": FooImpl{},
}
p, err := expr.Compile(`var.Foo().Bar()`, expr.Env(env))
assert.NoError(t, err)
out, err := expr.Run(p, env)
assert.NoError(t, err)
assert.Equal(t, 42, out)
}
+38
View File
@@ -0,0 +1,38 @@
package patch_test
import (
"testing"
"github.com/antonmedv/expr"
"github.com/antonmedv/expr/ast"
"github.com/antonmedv/expr/test/mock"
"github.com/stretchr/testify/require"
)
type lengthPatcher struct{}
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",
Arguments: []ast.Node{n.Node},
})
}
}
}
func TestPatch_length(t *testing.T) {
program, err := expr.Compile(
`String.length == 5`,
expr.Env(mock.Env{}),
expr.Patch(&lengthPatcher{}),
)
require.NoError(t, err)
env := mock.Env{String: "hello"}
output, err := expr.Run(program, env)
require.NoError(t, err)
require.Equal(t, true, output)
}
+1
View File
@@ -57,6 +57,7 @@ const (
OpCallN
OpCallFast
OpCallTyped
OpCallBuiltin
OpArray
OpMap
OpLen
+7
View File
@@ -9,6 +9,7 @@ import (
"text/tabwriter"
"github.com/antonmedv/expr/ast"
"github.com/antonmedv/expr/builtin"
"github.com/antonmedv/expr/file"
"github.com/antonmedv/expr/vm/runtime"
)
@@ -63,6 +64,9 @@ func (program *Program) Disassemble() string {
}
_, _ = fmt.Fprintf(w, "%v\t%v\t<%v>\t%v\n", pp, label, arg, c)
}
builtin := func(label string) {
_, _ = fmt.Fprintf(w, "%v\t%v\t<%v>\t%v\n", pp, label, arg, builtin.Builtins[arg].Name)
}
switch op {
case OpInvalid:
@@ -228,6 +232,9 @@ func (program *Program) Disassemble() string {
signature := reflect.TypeOf(FuncTypes[arg]).Elem().String()
_, _ = fmt.Fprintf(w, "%v\t%v\t<%v>\t%v\n", pp, "OpCallTyped", arg, signature)
case OpCallBuiltin:
builtin("OpCallBuiltin")
case OpArray:
code("OpArray")
+4
View File
@@ -8,6 +8,7 @@ import (
"regexp"
"strings"
"github.com/antonmedv/expr/builtin"
"github.com/antonmedv/expr/file"
"github.com/antonmedv/expr/vm/runtime"
)
@@ -380,6 +381,9 @@ func (vm *VM) Run(program *Program, env interface{}) (_ interface{}, err error)
out := vm.call(fn, arg)
vm.push(out)
case OpCallBuiltin:
vm.push(builtin.Builtins[arg].Builtin1(vm.pop()))
case OpArray:
size := vm.pop().(int)
array := make([]interface{}, size)