Skip to content

Commit b13ddd4

Browse files
cty: Use WrangleMarksDeep for UnmarkDeep and UnmarkDeepWithPaths
These functions were previously using the "Transformer" mechanism, which is very general but as a result ends up always conservatively rebuilding the entire data structure it's given. The recently-added WrangleMarksDeep relies on the fact that it can only possibly change marks (not the actual values) to avoid building a new value at all in the common case where there are no marks, to rebuild only the parts of the structure that have mark changes when changes are being made, and to reuse the internal "payload" values from the source values while just wrapping them in a new outer cty.Value. Testing with a benchmark in another codebase that makes heavy use of both UnmarkDeep and UnmarkDeepWithPaths shows this being a material performance improvement. This also includes a bonus fast path for MarkWithPaths where it will skip doing anything at all if the given slice of ParkValueMarks is empty.
1 parent 4453ac2 commit b13ddd4

File tree

2 files changed

+33
-32
lines changed

2 files changed

+33
-32
lines changed

CHANGELOG.md

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212

1313
- `cty.ValueMarksOfType` and `cty.ValueMarksOfTypeDeep` make it easier to use type-based rather than value-based mark schemes, where different values of a common type are used to track a specific kind of relationship with multiple external values.
1414
- `cty.Value.HasMarkDeep` provides a "deep" version of the existing `cty.Value.HasMark`, searching throughout a possibly-nested structure for any values that have the given mark.
15+
- `cty.Value.UnmarkDeep` and `cty.Value.UnmarkDeepWithPaths` are now implemented in terms of `cty.Value.WrangleMarksDeep`, so they benefit from its reduced overhead. In particular they avoid reconstructing a data structure that contains no marks at all.
16+
- `cty.Value.MarkWithPaths` now has a fast path when it's given a zero-length `PathValueMarks`, in which case it just returns the value it was given with no modifications.
1517

1618
# 1.16.4 (August 20, 2025)
1719

cty/marks.go

Lines changed: 31 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,8 @@ import (
44
"fmt"
55
"iter"
66
"strings"
7+
8+
"github.com/zclconf/go-cty/cty/ctymarks"
79
)
810

911
// marker is an internal wrapper type used to add special "marks" to values.
@@ -284,6 +286,11 @@ func (t *applyPathValueMarksTransformer) Exit(p Path, v Value) (Value, error) {
284286
// markers to particular paths and returns the marked
285287
// Value.
286288
func (val Value) MarkWithPaths(pvm []PathValueMarks) Value {
289+
if len(pvm) == 0 {
290+
// If we have no marks to apply then there's nothing to do, so we'll
291+
// just return the same value rather than wastefully rebuilding it.
292+
return val
293+
}
287294
ret, _ := TransformWithTransformer(val, &applyPathValueMarksTransformer{pvm})
288295
return ret
289296
}
@@ -305,52 +312,44 @@ func (val Value) Unmark() (Value, ValueMarks) {
305312
}, marks
306313
}
307314

308-
type unmarkTransformer struct {
309-
pvm []PathValueMarks
310-
}
311-
312-
func (t *unmarkTransformer) Enter(p Path, v Value) (Value, error) {
313-
unmarkedVal, marks := v.Unmark()
314-
if len(marks) > 0 {
315-
path := make(Path, len(p), len(p)+1)
316-
copy(path, p)
317-
t.pvm = append(t.pvm, PathValueMarks{path, marks})
318-
}
319-
return unmarkedVal, nil
320-
}
321-
322-
func (t *unmarkTransformer) Exit(p Path, v Value) (Value, error) {
323-
return v, nil
324-
}
325-
326315
// UnmarkDeep is similar to Unmark, but it works with an entire nested structure
327316
// rather than just the given value directly.
328317
//
329318
// The result is guaranteed to contain no nested values that are marked, and
330319
// the returned marks set includes the superset of all of the marks encountered
331320
// during the operation.
332321
func (val Value) UnmarkDeep() (Value, ValueMarks) {
333-
t := unmarkTransformer{}
334-
ret, _ := TransformWithTransformer(val, &t)
335-
336-
marks := make(ValueMarks)
337-
for _, pvm := range t.pvm {
338-
for m, s := range pvm.Marks {
339-
marks[m] = s
340-
}
341-
}
342-
343-
return ret, marks
322+
retMarks := make(ValueMarks)
323+
retVal, _ := val.WrangleMarksDeep(func(mark any, path Path) (ctymarks.WrangleAction, error) {
324+
retMarks[mark] = struct{}{}
325+
return ctymarks.WrangleDrop, nil
326+
})
327+
return retVal, retMarks
344328
}
345329

346330
// UnmarkDeepWithPaths is like UnmarkDeep, except it returns a slice
347331
// of PathValueMarks rather than a superset of all marks. This allows
348332
// a caller to know which marks are associated with which paths
349333
// in the Value.
350334
func (val Value) UnmarkDeepWithPaths() (Value, []PathValueMarks) {
351-
t := unmarkTransformer{}
352-
ret, _ := TransformWithTransformer(val, &t)
353-
return ret, t.pvm
335+
var pvm []PathValueMarks
336+
retVal, _ := val.WrangleMarksDeep(func(mark any, path Path) (ctymarks.WrangleAction, error) {
337+
if len(pvm) != 0 {
338+
// We'll try to modify the most recent item instead of adding
339+
// a new one, if the path hasn't changed.
340+
latest := &pvm[len(pvm)-1]
341+
if latest.Path.Equals(path) {
342+
latest.Marks[mark] = struct{}{}
343+
return ctymarks.WrangleDrop, nil
344+
}
345+
}
346+
pvm = append(pvm, PathValueMarks{
347+
Path: path.Copy(),
348+
Marks: NewValueMarks(mark),
349+
})
350+
return ctymarks.WrangleDrop, nil
351+
})
352+
return retVal, pvm
354353
}
355354

356355
func (val Value) unmarkForce() Value {

0 commit comments

Comments
 (0)