Skip to content

Simplify ast nodes #503

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 3 commits into from
Dec 26, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 4 additions & 35 deletions ast/node.go
Original file line number Diff line number Diff line change
Expand Up @@ -58,21 +58,7 @@ type NilNode struct {
// IdentifierNode represents an identifier.
type IdentifierNode struct {
base
Value string // Name of the identifier. Like "foo" in "foo.bar".
FieldIndex []int // Internal. Index of the field in the list of fields.
Method bool // Internal. If true then the identifier is a method call.
MethodIndex int // Internal. Index of the method in the list of methods.
}

// SetFieldIndex sets the field index of the identifier.
func (n *IdentifierNode) SetFieldIndex(field []int) {
n.FieldIndex = field
}

// SetMethodIndex sets the method index of the identifier.
func (n *IdentifierNode) SetMethodIndex(methodIndex int) {
n.Method = true
n.MethodIndex = methodIndex
Value string // Name of the identifier. Like "foo" in "foo.bar".
}

// IntegerNode represents an integer.
Expand Down Expand Up @@ -146,26 +132,9 @@ type ChainNode struct {
// array[0]
type MemberNode struct {
base
Node Node // Node of the member access. Like "foo" in "foo.bar".
Property Node // Property of the member access. For property access it is a StringNode.
Optional bool // If true then the member access is optional. Like "foo?.bar".
Name string // Internal. Name of the filed or method. Used for error reporting.
FieldIndex []int // Internal. Index sequence of fields. Generated by type checker.

// TODO: Combine Method and MethodIndex into a single MethodIndex field of &int type.
Method bool // Internal. If true then the member access is a method call.
MethodIndex int // Internal. Index of the method in the list of methods. Generated by type checker.
}

// SetFieldIndex sets the field index of the member access.
func (n *MemberNode) SetFieldIndex(field []int) {
n.FieldIndex = field
}

// SetMethodIndex sets the method index of the member access.
func (n *MemberNode) SetMethodIndex(methodIndex int) {
n.Method = true
n.MethodIndex = methodIndex
Node Node // Node of the member access. Like "foo" in "foo.bar".
Property Node // Property of the member access. For property access it is a StringNode.
Optional bool // If true then the member access is optional. Like "foo?.bar".
}

// SliceNode represents access to a slice of an array.
Expand Down
16 changes: 1 addition & 15 deletions checker/checker.go
Original file line number Diff line number Diff line change
Expand Up @@ -166,23 +166,13 @@ func (v *checker) IdentifierNode(node *ast.IdentifierNode) (reflect.Type, info)
return v.env(node, node.Value, true)
}

type NodeWithIndexes interface {
ast.Node
SetFieldIndex(field []int)
SetMethodIndex(methodIndex int)
}

// env method returns type of environment variable. env only lookups for
// environment variables, no builtins, no custom functions.
func (v *checker) env(node NodeWithIndexes, name string, strict bool) (reflect.Type, info) {
func (v *checker) env(node ast.Node, name string, strict bool) (reflect.Type, info) {
if t, ok := v.config.Types[name]; ok {
if t.Ambiguous {
return v.error(node, "ambiguous identifier %v", name)
}
node.SetFieldIndex(t.FieldIndex)
if t.Method {
node.SetMethodIndex(t.MethodIndex)
}
return t.Type, info{method: t.Method}
}
if v.config.Strict && strict {
Expand Down Expand Up @@ -477,8 +467,6 @@ func (v *checker) MemberNode(node *ast.MemberNode) (reflect.Type, info) {
// the same interface.
return m.Type, info{}
} else {
node.SetMethodIndex(m.Index)
node.Name = name.Value
return m.Type, info{method: true}
}
}
Expand Down Expand Up @@ -508,8 +496,6 @@ func (v *checker) MemberNode(node *ast.MemberNode) (reflect.Type, info) {
if name, ok := node.Property.(*ast.StringNode); ok {
propertyName := name.Value
if field, ok := fetchField(base, propertyName); ok {
node.FieldIndex = field.Index
node.Name = propertyName
return field.Type, info{}
}
if len(v.parents) > 1 {
Expand Down
50 changes: 50 additions & 0 deletions checker/info.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
package checker

import (
"reflect"

"github.com/expr-lang/expr/ast"
"github.com/expr-lang/expr/conf"
)

func FieldIndex(types conf.TypesTable, node ast.Node) (bool, []int, string) {
switch n := node.(type) {
case *ast.IdentifierNode:
if t, ok := types[n.Value]; ok && len(t.FieldIndex) > 0 {
return true, t.FieldIndex, n.Value
}
case *ast.MemberNode:
base := n.Node.Type()
if kind(base) == reflect.Ptr {
base = base.Elem()
}
if kind(base) == reflect.Struct {
if prop, ok := n.Property.(*ast.StringNode); ok {
name := prop.Value
if field, ok := fetchField(base, name); ok {
return true, field.Index, name
}
}
}
}
return false, nil, ""
}

func MethodIndex(types conf.TypesTable, node ast.Node) (bool, int, string) {
switch n := node.(type) {
case *ast.IdentifierNode:
if t, ok := types[n.Value]; ok {
return t.Method, t.MethodIndex, n.Value
}
case *ast.MemberNode:
if name, ok := n.Property.(*ast.StringNode); ok {
base := n.Node.Type()
if base != nil && base.Kind() != reflect.Interface {
if m, ok := base.MethodByName(name.Value); ok {
return true, m.Index, name.Value
}
}
}
}
return false, 0, ""
}
62 changes: 36 additions & 26 deletions compiler/compiler.go
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,7 @@ import (

"github.com/expr-lang/expr/ast"
"github.com/expr-lang/expr/builtin"
"github.com/expr-lang/expr/checker"
"github.com/expr-lang/expr/conf"
"github.com/expr-lang/expr/file"
"github.com/expr-lang/expr/parser"
Expand Down Expand Up @@ -34,6 +35,7 @@ func Compile(tree *parser.Tree, config *conf.Config) (program *Program, err erro
if config != nil {
c.mapEnv = config.MapEnv
c.cast = config.Expect
c.types = config.Types
}

c.compile(tree.Node)
Expand Down Expand Up @@ -75,6 +77,7 @@ type compiler struct {
nodes []ast.Node
chains [][]int
arguments []int
types conf.TypesTable
}

type scope struct {
Expand Down Expand Up @@ -254,15 +257,15 @@ func (c *compiler) IdentifierNode(node *ast.IdentifierNode) {
}
if c.mapEnv {
c.emit(OpLoadFast, c.addConstant(node.Value))
} else if len(node.FieldIndex) > 0 {
} else if ok, index, name := checker.FieldIndex(c.types, node); ok {
c.emit(OpLoadField, c.addConstant(&runtime.Field{
Index: node.FieldIndex,
Path: []string{node.Value},
Index: index,
Path: []string{name},
}))
} else if node.Method {
} else if ok, index, name := checker.MethodIndex(c.types, node); ok {
c.emit(OpLoadMethod, c.addConstant(&runtime.Method{
Name: node.Value,
Index: node.MethodIndex,
Name: name,
Index: index,
}))
} else {
c.emit(OpLoadConst, c.addConstant(node.Value))
Expand Down Expand Up @@ -559,36 +562,43 @@ func (c *compiler) ChainNode(node *ast.ChainNode) {
}

func (c *compiler) MemberNode(node *ast.MemberNode) {
if node.Method {
if ok, index, name := checker.MethodIndex(c.types, node); ok {
c.compile(node.Node)
c.emit(OpMethod, c.addConstant(&runtime.Method{
Name: node.Name,
Index: node.MethodIndex,
Name: name,
Index: index,
}))
return
}
op := OpFetch
index := node.FieldIndex
path := []string{node.Name}
base := node.Node
if len(node.FieldIndex) > 0 {

ok, index, nodeName := checker.FieldIndex(c.types, node)
path := []string{nodeName}

if ok {
op = OpFetchField
for !node.Optional {
ident, ok := base.(*ast.IdentifierNode)
if ok && len(ident.FieldIndex) > 0 {
index = append(ident.FieldIndex, index...)
path = append([]string{ident.Value}, path...)
c.emitLocation(ident.Location(), OpLoadField, c.addConstant(
&runtime.Field{Index: index, Path: path},
))
return
if ident, isIdent := base.(*ast.IdentifierNode); isIdent {
if ok, identIndex, name := checker.FieldIndex(c.types, ident); ok {
index = append(identIndex, index...)
path = append([]string{name}, path...)
c.emitLocation(ident.Location(), OpLoadField, c.addConstant(
&runtime.Field{Index: index, Path: path},
))
return
}
}
member, ok := base.(*ast.MemberNode)
if ok && len(member.FieldIndex) > 0 {
index = append(member.FieldIndex, index...)
path = append([]string{member.Name}, path...)
node = member
base = member.Node

if member, isMember := base.(*ast.MemberNode); isMember {
if ok, memberIndex, name := checker.FieldIndex(c.types, member); ok {
index = append(memberIndex, index...)
path = append([]string{name}, path...)
node = member
base = member.Node
} else {
break
}
} else {
break
}
Expand Down
68 changes: 60 additions & 8 deletions compiler/compiler_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,10 @@ type B struct {
}
}

func (B) FuncInB() int {
return 0
}

type Env struct {
A struct {
_ byte
Expand All @@ -33,12 +37,20 @@ type Env struct {
}
}

// AFunc is a method what goes before Func in the alphabet.
func (e Env) AFunc() int {
return 0
}

func (e Env) Func() B {
return B{}
}

func TestCompile(t *testing.T) {
type test struct {
input string
program vm.Program
}
var tests = []test{
var tests = []struct {
code string
want vm.Program
}{
{
`65535`,
vm.Program{
Expand Down Expand Up @@ -271,13 +283,53 @@ func TestCompile(t *testing.T) {
Arguments: []int{0, 0, 1, 0},
},
},
{
`Func()`,
vm.Program{
Constants: []any{
&runtime.Method{
Index: 1,
Name: "Func",
},
},
Bytecode: []vm.Opcode{
vm.OpLoadMethod,
vm.OpCall,
},
Arguments: []int{0, 0},
},
},
{
`Func().FuncInB()`,
vm.Program{
Constants: []any{
&runtime.Method{
Index: 1,
Name: "Func",
},
&runtime.Method{
Index: 0,
Name: "FuncInB",
},
},
Bytecode: []vm.Opcode{
vm.OpLoadMethod,
vm.OpCall,
vm.OpMethod,
vm.OpCallTyped,
},
Arguments: []int{0, 0, 1, 10},
},
},
}

for _, test := range tests {
program, err := expr.Compile(test.input, expr.Env(Env{}), expr.Optimize(false))
require.NoError(t, err, test.input)
t.Run(test.code, func(t *testing.T) {
program, err := expr.Compile(test.code, expr.Env(Env{}), expr.Optimize(false))
require.NoError(t, err)

assert.Equal(t, test.program.Disassemble(), program.Disassemble(), test.input)
assert.Equal(t, test.want.Disassemble(), program.Disassemble())
})
}
}

Expand Down
4 changes: 2 additions & 2 deletions test/interface_method/interface_method_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -3,9 +3,10 @@ package interface_method_test
import (
"testing"

"github.com/expr-lang/expr"
"github.com/stretchr/testify/assert"
"github.com/stretchr/testify/require"

"github.com/expr-lang/expr"
)

type Bar interface {
Expand Down Expand Up @@ -39,7 +40,6 @@ func TestInterfaceMethod(t *testing.T) {
"var": FooImpl{},
}
p, err := expr.Compile(`var.Foo().Bar()`, expr.Env(env))

assert.NoError(t, err)

out, err := expr.Run(p, env)
Expand Down
3 changes: 2 additions & 1 deletion test/operator/operator_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,10 @@ import (
"testing"
"time"

"github.com/stretchr/testify/require"

"github.com/expr-lang/expr"
"github.com/expr-lang/expr/test/mock"
"github.com/stretchr/testify/require"
)

func TestOperator_struct(t *testing.T) {
Expand Down