diff --git a/cli/test.yaml b/cli/test.yaml index 4818048..a2ef24f 100644 --- a/cli/test.yaml +++ b/cli/test.yaml @@ -5502,6 +5502,22 @@ expected: | [1,3,6,5,5] +- name: update-assignment operator against object with overlapping paths + args: + - -c + - '(.a.b,.a,.a.p.z) |= {p: ., q: .}' + input: '{"a":{"b":1}}' + 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 6b51e0e..050951b 100644 --- a/compiler.go +++ b/compiler.go @@ -1093,7 +1093,7 @@ func (c *compiler) compileModify() { &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) + 39}, // 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}, @@ -1102,12 +1102,13 @@ func (c *compiler) compileModify() { &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) + 36}, // [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}, &code{op: opload, v: p}, &code{op: opload, v: v}, - &code{op: opcall, v: [3]any{internalFuncs["getpath"].callback, 1, "getpath"}}, + &code{op: opcall, v: [3]any{funcGetpathWithAllocator, 2, "getpath"}}, &code{op: opload, v: f}, // f) &code{op: opcallpc}, &code{op: opload, v: p}, // setpath($p; ...) @@ -1115,8 +1116,8 @@ func (c *compiler) compileModify() { &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) + 34}, // [L4] - &code{op: opjump, v: len(c.codes) + 38}, // [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] diff --git a/func.go b/func.go index 81dbe31..d352716 100644 --- a/func.go +++ b/func.go @@ -1488,6 +1488,17 @@ func (a allocator) allocated(v any) bool { return ok } +// release forgets every allocated address when the value is one of them, so +// that a value handed to a query is copied on update again. +func (a allocator) release(v any) { + switch v.(type) { + case map[string]any, []any: + if a.allocated(v) { + clear(a) + } + } +} + func (a allocator) makeObject(l int) map[string]any { v := make(map[string]any, l) if a != nil { @@ -1514,6 +1525,15 @@ func funcSetpathWithAllocator(v any, args []any) any { return setpath(v, args[0], args[1], args[2].(allocator)) } +// Used in compiler#compileModify: the update function may store the value +// anywhere, so it must not be modified in place afterwards. Registered as +// getpath to keep tracking paths through the call. +func funcGetpathWithAllocator(v any, args []any) any { + w := funcGetpath(v, args[0]) + args[1].(allocator).release(w) + return w +} + func setpath(v, p, n any, a allocator) any { path, ok := p.([]any) if !ok {