Skip to content

Commit

Permalink
Parsing upcoming let-expression (#66)
Browse files Browse the repository at this point in the history
* Fixed mismatched pretty printed parens

Signed-off-by: Springcomp <[email protected]>

* [lexical-scoping] Parsing let-expression.

Signed-off-by: Springcomp <[email protected]>

* [lexical-scoping] Refactor comma-separated lists.

Signed-off-by: Springcomp <[email protected]>

* [lexical-scoping] 'let' and 'in' should be valid identifiers as well.

Signed-off-by: Springcomp <[email protected]>

* [lexical-scoping] Fixed file is not 'gofumpt' ed

Signed-off-by: Springcomp <[email protected]>

* [lexical-scoping] Refactored tests.

Signed-off-by: Springcomp <[email protected]>

* [lexical-scoping] Ensure sustained code coverage.

Signed-off-by: Springcomp <[email protected]>

* lexing

Signed-off-by: Charles-Edouard Brétéché <[email protected]>

* interpreter

Signed-off-by: Charles-Edouard Brétéché <[email protected]>

* error

Signed-off-by: Charles-Edouard Brétéché <[email protected]>

* fix

Signed-off-by: Charles-Edouard Brétéché <[email protected]>

* Re-introduced tests that now succeed.

Signed-off-by: Springcomp <[email protected]>

* Include compliance tests.

Signed-off-by: Springcomp <[email protected]>

* let() is deprecated.

Signed-off-by: Springcomp <[email protected]>

* unit tests

Signed-off-by: Charles-Edouard Brétéché <[email protected]>

---------

Signed-off-by: Springcomp <[email protected]>
Signed-off-by: Charles-Edouard Brétéché <[email protected]>
Co-authored-by: Charles-Edouard Brétéché <[email protected]>
  • Loading branch information
springcomp and eddycharly authored May 31, 2023
1 parent 7d8d162 commit 4a4bfbf
Show file tree
Hide file tree
Showing 14 changed files with 532 additions and 54 deletions.
2 changes: 1 addition & 1 deletion compliance
5 changes: 0 additions & 5 deletions jp_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -28,11 +28,6 @@ type TestCase struct {
var excludeList = []string{
"legacy/legacy-literal.json",
"benchmarks.json",
"function_let.json",
"lexical_scoping.json",

// this test currently fails
"literal.json",
}

func excluded(path string) bool {
Expand Down
2 changes: 1 addition & 1 deletion pkg/api/api.go
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ func MustCompile(expression string, funcs ...functions.FunctionEntry) JMESPath {

// Search evaluates a JMESPath expression against input data and returns the result.
func (jp jmesPath) Search(data interface{}) (interface{}, error) {
intr := interpreter.NewInterpreter(data, jp.caller)
intr := interpreter.NewInterpreter(data, jp.caller, nil)
return intr.Execute(jp.node, data)
}

Expand Down
8 changes: 8 additions & 0 deletions pkg/api/api_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,14 @@ func TestSearch(t *testing.T) {
want: map[string]interface{}{
"foo": nil,
},
}, {
args: args{
expression: "let $root = @ in $root.a",
data: map[string]interface{}{
"a": 42.0,
},
},
want: 42.0,
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
Expand Down
37 changes: 37 additions & 0 deletions pkg/binding/binding.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package binding

import "fmt"

// Bindings stores let expression bindings by name.
type Bindings interface {
// Get returns the value bound for a given name.
Get(string) (interface{}, error)
// Register registers a value associated with a given name, it returns a new binding
Register(string, interface{}) Bindings
}

type bindings struct {
values map[string]interface{}
}

func NewBindings() Bindings {
return bindings{}
}

func (b bindings) Get(name string) (interface{}, error) {
if value, ok := b.values[name]; ok {
return value, nil
}
return nil, fmt.Errorf("variable not defined: %s", name)
}

func (b bindings) Register(name string, value interface{}) Bindings {
values := map[string]interface{}{}
for k, v := range b.values {
values[k] = v
}
values[name] = value
return bindings{
values: values,
}
}
143 changes: 143 additions & 0 deletions pkg/binding/binding_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,143 @@
package binding

import (
"reflect"
"testing"
)

func TestNewBindings(t *testing.T) {
tests := []struct {
name string
want Bindings
}{{
want: bindings{},
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
if got := NewBindings(); !reflect.DeepEqual(got, tt.want) {
t.Errorf("NewBindings() = %v, want %v", got, tt.want)
}
})
}
}

func Test_bindings_Get(t *testing.T) {
type fields struct {
values map[string]interface{}
}
type args struct {
name string
}
tests := []struct {
name string
fields fields
args args
want interface{}
wantErr bool
}{{
fields: fields{
values: nil,
},
args: args{
name: "$root",
},
wantErr: true,
}, {
fields: fields{
values: map[string]interface{}{},
},
args: args{
name: "$root",
},
wantErr: true,
}, {
fields: fields{
values: map[string]interface{}{
"$root": 42.0,
},
},
args: args{
name: "$root",
},
want: 42.0,
}, {
fields: fields{
values: map[string]interface{}{
"$foot": 42.0,
},
},
args: args{
name: "$root",
},
wantErr: true,
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
b := bindings{
values: tt.fields.values,
}
got, err := b.Get(tt.args.name)
if (err != nil) != tt.wantErr {
t.Errorf("bindings.Get() error = %v, wantErr %v", err, tt.wantErr)
return
}
if !reflect.DeepEqual(got, tt.want) {
t.Errorf("bindings.Get() = %v, want %v", got, tt.want)
}
})
}
}

func Test_bindings_Register(t *testing.T) {
type fields struct {
values map[string]interface{}
}
type args struct {
name string
value interface{}
}
tests := []struct {
name string
fields fields
args args
want Bindings
}{{
fields: fields{
values: nil,
},
args: args{
name: "$root",
value: 42.0,
},
want: bindings{
values: map[string]interface{}{
"$root": 42.0,
},
},
}, {
fields: fields{
values: map[string]interface{}{
"$root": 21.0,
},
},
args: args{
name: "$root",
value: 42.0,
},
want: bindings{
values: map[string]interface{}{
"$root": 42.0,
},
},
}}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
b := bindings{
values: tt.fields.values,
}
if got := b.Register(tt.args.name, tt.args.value); !reflect.DeepEqual(got, tt.want) {
t.Errorf("bindings.Register() = %v, want %v", got, tt.want)
}
})
}
}
49 changes: 44 additions & 5 deletions pkg/interpreter/interpreter.go
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@ import (
"unicode"
"unicode/utf8"

"github.com/jmespath-community/go-jmespath/pkg/binding"
"github.com/jmespath-community/go-jmespath/pkg/parsing"
"github.com/jmespath-community/go-jmespath/pkg/util"
)
Expand All @@ -21,14 +22,19 @@ type Interpreter interface {
}

type treeInterpreter struct {
caller FunctionCaller
root interface{}
caller FunctionCaller
root interface{}
bindings binding.Bindings
}

func NewInterpreter(data interface{}, caller FunctionCaller) Interpreter {
func NewInterpreter(data interface{}, caller FunctionCaller, bindings binding.Bindings) Interpreter {
if bindings == nil {
bindings = binding.NewBindings()
}
return &treeInterpreter{
caller: caller,
root: data,
caller: caller,
root: data,
bindings: bindings,
}
}

Expand Down Expand Up @@ -211,6 +217,39 @@ func (intr *treeInterpreter) Execute(node parsing.ASTNode, value interface{}) (i
return value, nil
case parsing.ASTRootNode:
return intr.root, nil
case parsing.ASTBindings:
bindings := intr.bindings
for _, child := range node.Children {
if value, err := intr.Execute(child.Children[1], value); err != nil {
return nil, err
} else {
bindings = bindings.Register(child.Children[0].Value.(string), value)
}
}
intr.bindings = bindings
// doesn't mutate value
return value, nil
case parsing.ASTLetExpression:
// save bindings state
bindings := intr.bindings
// retore bindings state
defer func() {
intr.bindings = bindings
}()
// evalute bindings first, then evaluate expression
if _, err := intr.Execute(node.Children[0], value); err != nil {
return nil, err
} else if value, err := intr.Execute(node.Children[1], value); err != nil {
return nil, err
} else {
return value, nil
}
case parsing.ASTVariable:
if value, err := intr.bindings.Get(node.Value.(string)); err != nil {
return nil, err
} else {
return value, nil
}
case parsing.ASTIndex:
if sliceType, ok := value.([]interface{}); ok {
index := node.Value.(int)
Expand Down
8 changes: 4 additions & 4 deletions pkg/interpreter/interpreter_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -52,7 +52,7 @@ func search(t *testing.T, expression string, data interface{}) (interface{}, err
return nil, err
}
caller := NewFunctionCaller(functions.GetDefaultFunctions()...)
intr := NewInterpreter(nil, caller)
intr := NewInterpreter(nil, caller, nil)
return intr.Execute(ast, data)
}

Expand Down Expand Up @@ -198,7 +198,7 @@ func TestCanSupportSliceOfStructsWithFunctions(t *testing.T) {
func BenchmarkInterpretSingleFieldStruct(b *testing.B) {
assert := assert.New(b)
caller := NewFunctionCaller(functions.GetDefaultFunctions()...)
intr := NewInterpreter(nil, caller)
intr := NewInterpreter(nil, caller, nil)
parser := parsing.NewParser()
ast, _ := parser.Parse("fooasdfasdfasdfasdf")
data := benchmarkStruct{"foobarbazqux"}
Expand All @@ -213,7 +213,7 @@ func BenchmarkInterpretSingleFieldStruct(b *testing.B) {
func BenchmarkInterpretNestedStruct(b *testing.B) {
assert := assert.New(b)
caller := NewFunctionCaller(functions.GetDefaultFunctions()...)
intr := NewInterpreter(nil, caller)
intr := NewInterpreter(nil, caller, nil)
parser := parsing.NewParser()
ast, _ := parser.Parse("fooasdfasdfasdfasdf.fooasdfasdfasdfasdf.fooasdfasdfasdfasdf.fooasdfasdfasdfasdf")
data := benchmarkNested{
Expand All @@ -238,7 +238,7 @@ func BenchmarkInterpretNestedMaps(b *testing.B) {
err := json.Unmarshal(jsonData, &data)
assert.Nil(err)
caller := NewFunctionCaller(functions.GetDefaultFunctions()...)
intr := NewInterpreter(nil, caller)
intr := NewInterpreter(nil, caller, nil)
parser := parsing.NewParser()
ast, _ := parser.Parse("fooasdfasdfasdfasdf.fooasdfasdfasdfasdf.fooasdfasdfasdfasdf.fooasdfasdfasdfasdf")
for i := 0; i < b.N; i++ {
Expand Down
8 changes: 6 additions & 2 deletions pkg/parsing/astnodetype_string.go

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading

0 comments on commit 4a4bfbf

Please sign in to comment.