diff --git a/cli/test.yaml b/cli/test.yaml index c669f75..809d68c 100644 --- a/cli/test.yaml +++ b/cli/test.yaml @@ -2407,29 +2407,6 @@ "/a,b/c/d/e,f/" ",/ /a/,/b/,/ /c/,/ /d/,/ /e/,/f/,/ " -- name: split/1 function against empty string - args: - - -c - - '.[] as $t | .[] as $s | $s | split($t)' - input: '["abc", "a", "b", ""]' - expected: | - ["",""] - ["a"] - ["b"] - [] - ["","bc"] - ["",""] - ["b"] - [] - ["a","c"] - ["a"] - ["",""] - [] - ["a","b","c"] - ["a"] - ["b"] - [] - - name: join function args: - 'join(",")' @@ -3608,16 +3585,6 @@ expected: | 8 -- name: reduce with assignment of multiple paths in update - args: - - -c - - 'reduce (1,2) as $i (.; (.[0], .[.[0]]) = $i), - reduce (1,2) as $i (.; (.[0], .[.[0]]) |= $i)' - input: '[0,0,0]' - expected: | - [2,2,0] - [2,2,0] - - name: reduce with query in start args: - 'reduce range(5) as $x (range(3); . + $x)' @@ -3642,68 +3609,6 @@ expected: | [[1,1],[1]] -- name: reduce with assignment in update - args: - - -c - - 'reduce range(3) as $i ({}; .[$i|tostring] = {}), - reduce range(3) as $i ({}; .a[$i|tostring] = $i), - reduce range(3) as $i ([]; .[$i] = $i), - reduce range(3) as $i ({}; .[$i|tostring] += 1)' - input: 'null' - expected: | - {"0":{},"1":{},"2":{}} - {"a":{"0":0,"1":1,"2":2}} - [0,1,2] - {"0":1,"1":1,"2":1} - -- name: reduce with assignment of the accumulated value - args: - - -c - - 'reduce range(3) as $i ({}; .a = .), - reduce range(3) as $i ({}; .a = {v: .}), - reduce range(3) as $i ({}; . as $p | .a = $i | .b = $p)' - input: 'null' - expected: | - {"a":{"a":{"a":{}}}} - {"a":{"v":{"a":{"v":{"a":{"v":{}}}}}}} - {"a":2,"b":{"a":1,"b":{"a":0,"b":{}}}} - -- name: reduce with assignment of multiple values - args: - - -c - - 'reduce range(2) as $i ({"k":"k"}; .[.k] = ("x","y")), - reduce range(2) as $i ({}; (.a = 1), (.b = 2))' - input: 'null' - expected: | - {"k":"y","y":"y"} - {"b":2} - -- name: reduce with assignment of a shared value - args: - - -c - - '{"a":1} as $x | reduce range(2) as $i ({}; .[$i|tostring] = $x) | - [(.["0"].a = 9), $x]' - input: 'null' - expected: | - [{"0":{"a":9},"1":{"a":1}},{"a":1}] - -- name: reduce with assignment result reused - args: - - -c - - 'reduce range(3) as $i ({}; .[$i|tostring] = $i) as $r | - [($r | .["0"] = 9), $r]' - input: 'null' - expected: | - [{"0":9,"1":1,"2":2},{"0":0,"1":1,"2":2}] - -- name: foreach with assignment in update - args: - - -c - - '[foreach range(3) as $i ({}; .[$i|tostring] = $i)]' - input: 'null' - expected: | - [{"0":0},{"0":0,"1":1},{"0":0,"1":1,"2":2}] - - name: foreach iterator args: - 'foreach range(5) as $item (0; $item)' @@ -5084,46 +4989,6 @@ expected: | {"a":[{},{"b":[1,2,3]}]} -- name: getpath function against string - args: - - -c - - '[getpath([{"start":1,"end":3}]), getpath([{"start":-2,"end":null}]), getpath([1]), getpath([])]' - input: '"abcde"' - expected: | - ["bc","de","b","abcde"] - -- name: getpath function against string in a container - args: - - -c - - 'getpath(["a",{"start":0,"end":2}]), getpath(["a",1])' - input: '{"a":"abc"}' - expected: | - "ab" - "b" - -- name: getpath function against string round trip with path function - args: - - -c - - '[getpath(path(.[1:3])), getpath(path(.[1]))]' - input: '"abcde"' - expected: | - ["bc","b"] - -- name: getpath function against string round trip in a container - args: - - -c - - '. as $d | [path(.a[0:2]), path(.b[1])] | map(. as $p | $d | getpath($p))' - input: '{"a":"abc","b":"xyz"}' - expected: | - ["ab","y"] - -- name: getpath function against string with object key - args: - - 'getpath(["k"])' - input: '"abc"' - error: | - getpath(["k"]) cannot be applied to "abc": expected an object but got: string ("abc") - - name: getpath function error args: - 'getpath(["a",1,"b","c"])' @@ -5156,9 +5021,9 @@ expected: | {"a":[{"b":5}]} {"a":[{"b":5}],"b":0} - "getpath([\"a\",0,\"b\"]) cannot be applied to {\"a\":0}: expected an array but got: number (0)" + "getpath([\"a\",0,\"b\"]) cannot be applied to: object ({\"a\":0})" {"a":[{"b":5}]} - "getpath([\"a\",0,\"b\"]) cannot be applied to {\"a\":[0,1]}: expected an object but got: number (0)" + "getpath([\"a\",0,\"b\"]) cannot be applied to: object ({\"a\":[0,1]})" "getpath([\"a\",0,\"b\"]) cannot be applied to {\"a\":{\"b\":1}}: expected an array but got: object ({\"b\":1})" {"a":[{"b":5}]} {"a":[{"b":5,"c":3}]} @@ -5572,14 +5437,6 @@ expected: | {"a":{"p":{"b":{"p":1,"q":1},"z":{"p":null,"q":null}},"q":{"b":{"p":1,"q":1}}}} -- name: update-assignment operator against nested object with overlapping paths - args: - - -c - - '(.a.b.c,.a,.a.p.b.z) |= {p: ., q: .}' - input: '{"a":{"b":{"c":1}}}' - expected: | - {"a":{"p":{"b":{"c":{"p":1,"q":1},"z":{"p":null,"q":null}}},"q":{"b":{"c":{"p":1,"q":1}}}}} - - name: update-assignment operator with optional operator args: - -c diff --git a/compiler.go b/compiler.go index 7747077..477650c 100644 --- a/compiler.go +++ b/compiler.go @@ -132,13 +132,18 @@ func (c *compiler) compile(q *Query) error { } func (c *compiler) compileImport(i *Import) error { - path, alias := cmp.Or(i.ImportPath, i.IncludePath), i.ImportAlias + var path, alias string + var err error + if i.ImportPath != "" { + path, alias = i.ImportPath, i.ImportAlias + } else { + path = i.IncludePath + } if c.moduleLoader == nil { return fmt.Errorf("cannot load module: %q", path) } if strings.HasPrefix(alias, "$") { var vals any - var err error if moduleLoader, ok := c.moduleLoader.(interface { LoadJSONWithMeta(string, map[string]any) (any, error) }); ok { @@ -161,7 +166,6 @@ func (c *compiler) compileImport(i *Import) error { return nil } var q *Query - var err error if moduleLoader, ok := c.moduleLoader.(interface { LoadModuleWithMeta(string, map[string]any) (*Query, error) }); ok { @@ -393,7 +397,7 @@ func (c *compiler) compileQuery(e *Query) error { return c.compileAlt(e.Left, e.Right) case OpAssign, OpModify, OpUpdateAdd, OpUpdateSub, OpUpdateMul, OpUpdateDiv, OpUpdateMod, OpUpdateAlt: - return c.compileQueryUpdate(e.Left, e.Right, e.Op, nil) + return c.compileQueryUpdate(e.Left, e.Right, e.Op) case OpOr: return c.compileIf( &If{ @@ -467,10 +471,7 @@ func (c *compiler) compileAlt(l, r *Query) error { return c.compileQuery(r) } -// compileQueryUpdate compiles an assignment operator. alloc is nil unless the -// assignment may update the input value in place, in which case it yields the -// allocator of the enclosing reduce expression. -func (c *compiler) compileQueryUpdate(l, r *Query, op Operator, alloc *Query) error { +func (c *compiler) compileQueryUpdate(l, r *Query, op Operator) error { switch op { case OpAssign: // optimize assignment operator with constant indexing and slicing @@ -490,7 +491,12 @@ func (c *compiler) compileQueryUpdate(l, r *Query, op Operator, alloc *Query) er } fallthrough case OpModify: - return c.compileUpdateFunc(op.getFunc(), []*Query{l, r}, alloc) + return c.compileFunc( + &Func{ + Name: op.getFunc(), + Args: []*Query{l, r}, + }, + ) default: name := "$%0" c.append(&code{op: opdup}) @@ -498,53 +504,27 @@ func (c *compiler) compileQueryUpdate(l, r *Query, op Operator, alloc *Query) er return err } c.append(&code{op: opstore, v: c.pushVariable(name)}) - return c.compileUpdateFunc( - "_modify", - []*Query{ - l, - {Term: &Term{ - Type: TermTypeFunc, - Func: &Func{ - Name: op.getFunc(), - Args: []*Query{ - {Term: &Term{Type: TermTypeIdentity}}, - {Term: &Term{Type: TermTypeFunc, Func: &Func{Name: name}}}, + return c.compileFunc( + &Func{ + Name: "_modify", + Args: []*Query{ + l, + {Term: &Term{ + Type: TermTypeFunc, + Func: &Func{ + Name: op.getFunc(), + Args: []*Query{ + {Term: &Term{Type: TermTypeIdentity}}, + {Term: &Term{Type: TermTypeFunc, Func: &Func{Name: name}}}, + }, }, - }, - }}, + }}, + }, }, - alloc, ) } } -const ( - // Both names are deliberately invalid identifiers, so that a query cannot - // reach the allocator variants directly. - allocatorArgSuffix = "/3" - allocatorName = "$%allocator" -) - -// compileUpdateFunc calls _assign or _modify, or the variant taking an -// allocator as its third argument. -func (c *compiler) compileUpdateFunc(name string, args []*Query, alloc *Query) error { - if alloc == nil { - return c.compileFunc(&Func{Name: name, Args: args}) - } - args = append(args, alloc) - fn := c.lookupBuiltin(name+allocatorArgSuffix, len(args)) - if fn == nil { - switch name { - case "_assign": - c.compileAssign(true) - case "_modify": - c.compileModify(true) - } - fn = c.lookupBuiltin(name+allocatorArgSuffix, len(args)) - } - return c.compileCallPc(fn, args) -} - func (c *compiler) compileBind(l, r *Query, patterns []*Pattern) error { defer c.newScopeDepth()() c.append(&code{op: opdup}) @@ -740,21 +720,6 @@ func (c *compiler) compileReduce(e *Reduce) error { } f() c.append(&code{op: opstore, v: v}) - // An assignment forming the whole update query is the only thing that - // observes the accumulated value, so it may modify it in place; see - // Query#inPlaceUpdate. The allocator lives across the whole reduction. - l, r, op, inPlace := e.Update.inPlaceUpdate() - var alloc *Query - if inPlace { - c.appends( - &code{op: oppush, v: nil}, - &code{op: opcall, v: [3]any{funcAllocator, 0, "_allocator"}}, - &code{op: opstore, v: c.pushVariable(allocatorName)}, - ) - alloc = &Query{Term: &Term{ - Type: TermTypeFunc, Func: &Func{Name: allocatorName}, - }} - } setfork := c.lazy(func() *code { return &code{op: opfork, v: len(c.codes)} }) @@ -766,13 +731,7 @@ func (c *compiler) compileReduce(e *Reduce) error { } c.append(&code{op: opload, v: v}) f = c.newScopeDepth() - var err error - if inPlace { - err = c.compileQueryUpdate(l, r, op, alloc) - } else { - err = c.compileQuery(e.Update) - } - if err != nil { + if err := c.compileQuery(e.Update); err != nil { return err } f() @@ -977,9 +936,9 @@ func (c *compiler) compileFunc(e *Func) error { if !compiled { switch e.Name { case "_assign": - c.compileAssign(false) + c.compileAssign() case "_modify": - c.compileModify(false) + c.compileModify() case "_last": c.compileLast() } @@ -1075,54 +1034,29 @@ func (c *compiler) compileFunc(e *Func) error { // // To overcome the difficulty of reducing allocations on `setpath`, we use the // `allocator` type and track the allocated addresses during the reduction. -// -// The shared variant reuses the allocator of the enclosing reduce expression -// instead of creating a new one, modifying the value in place. -func (c *compiler) compileAssign(shared bool) { - name, argcnt := "_assign", 2 - if shared { - name, argcnt = name+allocatorArgSuffix, 3 - } - defer c.appendBuiltin(name, argcnt)() +func (c *compiler) compileAssign() { + defer c.appendBuiltin("_assign", 2)() scope := c.newScope() v, p := [2]int{scope.id, 0}, [2]int{scope.id, 1} x, a := [2]int{scope.id, 2}, [2]int{scope.id, 3} // Cannot reuse v, p due to backtracking in x. w, q := [2]int{scope.id, 4}, [2]int{scope.id, 5} c.appends( - &code{op: opscope, v: [3]int{scope.id, 6, argcnt}}, + &code{op: opscope, v: [3]int{scope.id, 6, 2}}, &code{op: opstore, v: v}, // def _assign(p; $x): &code{op: opstore, v: p}, &code{op: opstore, v: x}, - ) - if shared { - c.append(&code{op: opstore, v: a}) // the allocator argument - } - c.appends( &code{op: opload, v: v}, &code{op: opexpbegin}, &code{op: opload, v: x}, &code{op: opcallpc}, &code{op: opstore, v: x}, &code{op: opexpend}, - ) - if shared { - c.appends( - &code{op: opload, v: v}, - &code{op: opload, v: a}, - &code{op: opcallpc}, - &code{op: opstore, v: a}, - ) - } else { - c.appends( - &code{op: oppush, v: nil}, - &code{op: opcall, v: [3]any{funcAllocator, 0, "_allocator"}}, - &code{op: opstore, v: a}, - ) - } - c.appends( + &code{op: oppush, v: nil}, + &code{op: opcall, v: [3]any{funcAllocator, 0, "_allocator"}}, + &code{op: opstore, v: a}, &code{op: opload, v: v}, - &code{op: opfork, v: len(c.codes) + 17}, // reduce [L1] + &code{op: opfork, v: len(c.codes) + 30}, // reduce [L1] &code{op: opdup}, &code{op: opstore, v: w}, &code{op: oppathbegin}, // path(p) @@ -1146,49 +1080,24 @@ func (c *compiler) compileAssign(shared bool) { // Appends the compiled code for the update-assignment operator (`|=`) to // maximize performance. We use the `allocator` type, just like `_assign/2`. -// -// The shared variant reuses the allocator of the enclosing reduce expression; -// see compiler#compileAssign. -func (c *compiler) compileModify(shared bool) { - name, argcnt := "_modify", 2 - if shared { - name, argcnt = name+allocatorArgSuffix, 3 - } - defer c.appendBuiltin(name, argcnt)() +func (c *compiler) compileModify() { + defer c.appendBuiltin("_modify", 2)() scope := c.newScope() v, p := [2]int{scope.id, 0}, [2]int{scope.id, 1} f, d := [2]int{scope.id, 2}, [2]int{scope.id, 3} a, l := [2]int{scope.id, 4}, [2]int{scope.id, 5} c.appends( - &code{op: opscope, v: [3]int{scope.id, 6, argcnt}}, + &code{op: opscope, v: [3]int{scope.id, 6, 2}}, &code{op: opstore, v: v}, // def _modify(p; f): &code{op: opstore, v: p}, &code{op: opstore, v: f}, - ) - if shared { - c.append(&code{op: opstore, v: a}) // the allocator argument - } - c.appends( &code{op: oppush, v: []any{}}, &code{op: opstore, v: d}, - ) - if shared { - c.appends( - &code{op: opload, v: v}, - &code{op: opload, v: a}, - &code{op: opcallpc}, - &code{op: opstore, v: a}, - ) - } else { - c.appends( - &code{op: oppush, v: nil}, - &code{op: opcall, v: [3]any{funcAllocator, 0, "_allocator"}}, - &code{op: opstore, v: a}, - ) - } - c.appends( + &code{op: oppush, v: nil}, + &code{op: opcall, v: [3]any{funcAllocator, 0, "_allocator"}}, + &code{op: opstore, v: a}, &code{op: opload, v: v}, - &code{op: opfork, v: len(c.codes) + 31}, // reduce [L1] + &code{op: opfork, v: len(c.codes) + 40}, // reduce [L1] &code{op: oppathbegin}, // path(p) &code{op: opload, v: p}, &code{op: opcallpc}, @@ -1197,7 +1106,7 @@ func (c *compiler) compileModify(shared bool) { &code{op: opstore, v: p}, // as $p (.; &code{op: opforklabel, v: l}, // label $l | &code{op: opload, v: v}, // - &code{op: opfork, v: len(c.codes) + 28}, // [L2] + &code{op: opfork, v: len(c.codes) + 37}, // [L2] &code{op: oppop}, // (getpath($p) | &code{op: opload, v: a}, &code{op: opload, v: a}, @@ -1211,8 +1120,8 @@ func (c *compiler) compileModify(shared bool) { &code{op: opcall, v: [3]any{funcSetpathWithAllocator, 3, "_setpath"}}, &code{op: opstore, v: v}, &code{op: opload, v: v}, // ., break $l - &code{op: opfork, v: len(c.codes) + 26}, // [L4] - &code{op: opjump, v: len(c.codes) + 30}, // [L3] + &code{op: opfork, v: len(c.codes) + 35}, // [L4] + &code{op: opjump, v: len(c.codes) + 39}, // [L3] &code{op: opload, v: l}, // [L4] &code{op: opcall, v: [3]any{funcBreak(""), 0, "_break"}}, &code{op: opload, v: p}, // append $p to $d [L2] @@ -1365,7 +1274,11 @@ func listModuleDeps(q *Query) []any { if v == nil { v = make(map[string]any) } - v["relpath"] = cmp.Or(i.ImportPath, i.IncludePath) + relpath := i.ImportPath + if relpath == "" { + relpath = i.IncludePath + } + v["relpath"] = relpath if i.ImportAlias != "" { v["as"] = strings.TrimPrefix(i.ImportAlias, "$") } diff --git a/compiler_test.go b/compiler_test.go index 6f7c1f8..fe67bf5 100644 --- a/compiler_test.go +++ b/compiler_test.go @@ -7,7 +7,6 @@ import ( "log" "os" "reflect" - "runtime" "sync" "testing" "time" @@ -375,57 +374,6 @@ func TestCodeRun_RaceRegexp(t *testing.T) { wg.Wait() } -// Building an object or an array of n elements must allocate memory -// proportional to n, not to n squared. -func TestCodeRun_ReduceAssignmentAllocation(t *testing.T) { - for _, src := range []string{ - `reduce .[] as $x ({}; .[$x|tostring] = $x)`, - `reduce .[] as $x ({}; .[$x|tostring] |= $x)`, - `reduce .[] as $x ({}; .[$x|tostring] += 1)`, - `reduce .[] as $x ([]; .[$x] = $x)`, - } { - t.Run(src, func(t *testing.T) { - query, err := gojq.Parse(src) - if err != nil { - t.Fatal(err) - } - code, err := gojq.Compile(query) - if err != nil { - t.Fatal(err) - } - small, large := allocatedBytes(t, code, 1000), allocatedBytes(t, code, 4000) - // Quadratic growth would be about sixteen times as much. - if expected := small * 8; large > expected { - t.Errorf("expected at most %v bytes for 4000 elements, got %v (%v bytes for 1000 elements)", - expected, large, small) - } - }) - } -} - -func allocatedBytes(t *testing.T, code *gojq.Code, n int) uint64 { - t.Helper() - input := make([]any, n) - for i := range input { - input[i] = i - } - var before, after runtime.MemStats - runtime.GC() - runtime.ReadMemStats(&before) - iter := code.Run(input) - for { - v, ok := iter.Next() - if !ok { - break - } - if err, ok := v.(error); ok { - t.Fatal(err) - } - } - runtime.ReadMemStats(&after) - return after.TotalAlloc - before.TotalAlloc -} - func BenchmarkCompile(b *testing.B) { cnt, err := os.ReadFile("builtin.jq") if err != nil { diff --git a/inplace.go b/inplace.go deleted file mode 100644 index 442babb..0000000 --- a/inplace.go +++ /dev/null @@ -1,204 +0,0 @@ -package gojq - -// Static analysis deciding whether the accumulated value of a reduce -// expression may be updated in place; see compiler#compileReduce. The checks -// are conservative: failing them only keeps the previous copying behavior. - -// inPlaceUpdate reports whether the query is a single assignment applied to -// the input value, and returns its operands. -func (e *Query) inPlaceUpdate() (l, r *Query, op Operator, ok bool) { - if e.Term != nil || len(e.FuncDefs) > 0 { - return nil, nil, 0, false - } - switch e.Op { - case OpModify: - // _modify releases the allocator before calling the update function. - return e.Left, e.Right, e.Op, true - case OpAssign: - // A constant path compiles to setpath, which uses no allocator. - if e.Left.toIndices(nil) != nil { - return nil, nil, 0, false - } - fallthrough - case OpUpdateAdd, OpUpdateSub, OpUpdateMul, - OpUpdateDiv, OpUpdateMod, OpUpdateAlt: - // The right-hand side must neither observe the input value, which it - // could otherwise alias, nor yield multiple values, which would run - // the assignment against an already modified input. - if !e.Right.readsInput() && e.Right.singleValued() { - return e.Left, e.Right, e.Op, true - } - } - return nil, nil, 0, false -} - -// readsInput reports whether evaluating the query may observe its input value. -// It answers true unless it can prove otherwise. -func (e *Query) readsInput() bool { - if len(e.FuncDefs) > 0 { - return true - } - if e.Term != nil { - return e.Term.readsInput() - } - switch e.Op { - case OpPipe: - // Binding variables also exposes the input to the right-hand side. - if len(e.Patterns) > 0 { - return e.Left.readsInput() || e.Right.readsInput() - } - return e.Left.readsInput() - case OpComma, OpAdd, OpSub, OpMul, OpDiv, OpMod, OpEq, OpNe, - OpGt, OpLt, OpGe, OpLe, OpAnd, OpOr, OpAlt: - return e.Left.readsInput() || e.Right.readsInput() - default: - return true - } -} - -// singleValued reports whether the query yields at most one value. -// It answers false unless it can prove otherwise. -func (e *Query) singleValued() bool { - if len(e.FuncDefs) > 0 { - return false - } - if e.Term != nil { - return e.Term.singleValued() - } - switch e.Op { - case OpPipe: - return len(e.Patterns) == 0 && - e.Left.singleValued() && e.Right.singleValued() - case OpAdd, OpSub, OpMul, OpDiv, OpMod, OpEq, OpNe, - OpGt, OpLt, OpGe, OpLe, OpAnd, OpOr: - return e.Left.singleValued() && e.Right.singleValued() - default: - return false - } -} - -func (e *Term) readsInput() bool { - for _, s := range e.SuffixList { - if s.readsInput() { - return true - } - } - switch e.Type { - case TermTypeNull, TermTypeTrue, TermTypeFalse, TermTypeNumber: - return false - case TermTypeString: - return e.Str.readsInput() - case TermTypeObject: - for _, kv := range e.Object.KeyVals { - if kv.readsInput() { - return true - } - } - return false - case TermTypeArray: - return e.Array.Query != nil && e.Array.Query.readsInput() - case TermTypeUnary: - return e.Unary.Term.readsInput() - case TermTypeFunc: - // Any function may be redefined, so only $x is known not to read it. - return e.Func.Name[0] != '$' || len(e.Func.Args) > 0 - case TermTypeQuery: - return e.Query.readsInput() - default: - return true - } -} - -func (e *Term) singleValued() bool { - for _, s := range e.SuffixList { - if !s.singleValued() { - return false - } - } - switch e.Type { - case TermTypeNull, TermTypeTrue, TermTypeFalse, TermTypeNumber: - return true - case TermTypeString: - return e.Str.singleValued() - case TermTypeObject: - for _, kv := range e.Object.KeyVals { - if !kv.singleValued() { - return false - } - } - return true - case TermTypeArray: - return true - case TermTypeUnary: - return e.Unary.Term.singleValued() - case TermTypeFunc: - return e.Func.Name[0] == '$' && len(e.Func.Args) == 0 - case TermTypeQuery: - return e.Query.singleValued() - default: - return false - } -} - -func (e *Suffix) readsInput() bool { - // Only an index is evaluated against the input value. - return e.Index != nil && e.Index.readsInput() -} - -func (e *Suffix) singleValued() bool { - return !e.Iter && (e.Index == nil || e.Index.singleValued()) -} - -func (e *Index) readsInput() bool { - if e.Str != nil { - return e.Str.readsInput() - } - return e.Start != nil && e.Start.readsInput() || - e.End != nil && e.End.readsInput() -} - -func (e *Index) singleValued() bool { - if e.Str != nil { - return e.Str.singleValued() - } - return (e.Start == nil || e.Start.singleValued()) && - (e.End == nil || e.End.singleValued()) -} - -func (e *String) readsInput() bool { - for _, q := range e.Queries { - if q.readsInput() { - return true - } - } - return false -} - -func (e *String) singleValued() bool { - for _, q := range e.Queries { - if !q.singleValued() { - return false - } - } - return true -} - -func (e *ObjectKeyVal) readsInput() bool { - if e.Val == nil { - // {foo} reads the input value, {$foo} does not. - return e.Key == "" || e.Key[0] != '$' - } - if e.KeyString != nil && e.KeyString.readsInput() || - e.KeyQuery != nil && e.KeyQuery.readsInput() { - return true - } - return e.Val.readsInput() -} - -func (e *ObjectKeyVal) singleValued() bool { - if e.KeyString != nil && !e.KeyString.singleValued() || - e.KeyQuery != nil && !e.KeyQuery.singleValued() { - return false - } - return e.Val == nil || e.Val.singleValued() -}