this repo has no description
0
fork

Configure Feed

Select the types of activity you want to include in your feed.

internal/core/adt: add new closedness implementation

This involved inserting fields, data structures,
closedness and pattern constraints.

Only support fields in CloseInfo are added, but
this is not hooked in yet otherwise and thus does
not affect the existing implementation.

As this change increases the size of some data structures,
it may lead to a small performance hit.

The change in adt.go is needed to pass the tests using the
FieldTester, as this may create conjuncts without Evaluators.

Signed-off-by: Marcel van Lohuizen <mpvl@gmail.com>
Change-Id: I068fc4937b51dc01286be60658b6eab92caac112
Reviewed-on: https://review.gerrithub.io/c/cue-lang/cue/+/1167822
Unity-Result: CUE porcuepine <cue.porcuepine@gmail.com>
TryBot-Result: CUEcueckoo <cueckoo@cuelang.org>
Reviewed-by: Daniel Martí <mvdan@mvdan.cc>
Reviewed-by: Roger Peppe <rogpeppe@gmail.com>

+1758 -4
+14 -1
internal/core/adt/closed.go
··· 108 108 109 109 // TODO: merge with closeInfo: this is a leftover of the refactoring. 110 110 type CloseInfo struct { 111 - *closeInfo 111 + *closeInfo // old implementation (TODO: remove) 112 + cc *closeContext // new implementation (TODO: rename field to closeCtx) 112 113 113 114 // IsClosed is true if this conjunct represents a single level of closing 114 115 // as indicated by the closed builtin. 115 116 IsClosed bool 117 + 118 + // FromEmbed indicates whether this conjunct was inserted because of an 119 + // embedding. This flag is sticky: it will be set for conjuncts created 120 + // from fields defined by this conjunct. 121 + // NOTE: only used when using closeContext. 122 + FromEmbed bool 123 + 124 + // FromDef indicates whether this conjunct was inserted because of a 125 + // definition. This flag is sticky: it will be set for conjuncts created 126 + // from fields defined by this conjunct. 127 + // NOTE: only used when using closeContext. 128 + FromDef bool 116 129 117 130 // FieldTypes indicates which kinds of fields (optional, dynamic, patterns, 118 131 // etc.) are contained in this conjunct.
+12
internal/core/adt/composite.go
··· 192 192 // or any other operation that relies on the set of arcs being constant. 193 193 LockArcs bool 194 194 195 + // disallowedField means that this arc is not allowed according 196 + // to the closedness rules. This is used to avoid duplicate error reporting. 197 + // TODO: perhaps rename to notAllowedErrorEmitted. 198 + disallowedField bool 199 + 195 200 // IsDynamic signifies whether this struct is computed as part of an 196 201 // expression and not part of the static evaluation tree. 197 202 // Used for cycle detection. ··· 220 225 // configuration of this node. 221 226 // Value Value 222 227 Arcs []*Vertex // arcs are sorted in display order. 228 + 229 + // PatternConstraints are additional constraints that match more nodes. 230 + // Constraints that match existing Arcs already have their conjuncts 231 + // mixed in. 232 + // TODO: either put in StructMarker/ListMarker or integrate with Arcs 233 + // so that this pointer is unnecessary. 234 + PatternConstraints *Constraints 223 235 224 236 // Conjuncts lists the structs that ultimately formed this Composite value. 225 237 // This includes all selected disjuncts.
+229
internal/core/adt/constraints.go
··· 1 + // Copyright 2023 CUE Authors 2 + // 3 + // Licensed under the Apache License, Version 2.0 (the "License"); 4 + // you may not use this file except in compliance with the License. 5 + // You may obtain a copy of the License at 6 + // 7 + // http://www.apache.org/licenses/LICENSE-2.0 8 + // 9 + // Unless required by applicable law or agreed to in writing, software 10 + // distributed under the License is distributed on an "AS IS" BASIS, 11 + // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 + // See the License for the specific language governing permissions and 13 + // limitations under the License. 14 + 15 + package adt 16 + 17 + // This file contains functionality for pattern constraints. 18 + 19 + // Constraints keeps track of pattern constraints and the set of allowed 20 + // fields. 21 + type Constraints struct { 22 + // Pairs lists Pattern-Constraint pairs. 23 + Pairs []PatternConstraint // TODO(perf): move to Arcs? 24 + 25 + // Allowed is a Value that defines the set of all allowed fields. 26 + // To check if a field is allowed, its correpsonding CUE value can be 27 + // unified with this value. 28 + Allowed Value 29 + } 30 + 31 + // A PatternConstraint represents a single 32 + // 33 + // [pattern]: T. 34 + // 35 + // The Vertex holds a list of conjuncts to represent the constraints. We use 36 + // a Vertex so that these can be evaluated and compared for equality. 37 + // Unlike for regular Vertex values, CloseInfo.closeContext is set for 38 + // constraints: it is needed when matching subfields to ensure that conjuncts 39 + // get inserted into the proper groups. 40 + type PatternConstraint struct { 41 + Pattern Value 42 + Constraint *Vertex 43 + } 44 + 45 + // insertListEllipsis inserts the given list ellipsis as a pattern constraint on 46 + // n, applying it to all elements at indexes >= offset. 47 + func (n *nodeContext) insertListEllipsis(offset int, ellipsis Conjunct) { 48 + ctx := n.ctx 49 + 50 + var p Value 51 + if offset == 0 { 52 + p = &BasicType{ 53 + Src: ellipsis.Field().Source(), 54 + K: IntKind, 55 + } 56 + } else { 57 + p = &BoundValue{ 58 + Src: nil, // TODO: field source. 59 + Op: GreaterEqualOp, 60 + Value: ctx.NewInt64(int64(offset)), 61 + } 62 + } 63 + n.insertConstraint(p, ellipsis) 64 + } 65 + 66 + // insertConstraint ensures a given pattern constraint is present in the 67 + // constraints of n and reports whether the pair was added newly. 68 + // 69 + // The given conjunct must have a closeContext associated with it. This ensures 70 + // that different pattern constraints pairs originating from the same 71 + // closeContext will be collated properly in fields to which these constraints 72 + // are applied. 73 + func (n *nodeContext) insertConstraint(pattern Value, c Conjunct) bool { 74 + if c.CloseInfo.cc == nil { 75 + panic("constraint conjunct must have closeContext associated with it") 76 + } 77 + 78 + ctx := n.ctx 79 + v := n.node 80 + 81 + pcs := v.PatternConstraints 82 + if pcs == nil { 83 + pcs = &Constraints{} 84 + v.PatternConstraints = pcs 85 + } 86 + 87 + var constraint *Vertex 88 + for _, pc := range pcs.Pairs { 89 + if Equal(ctx, pc.Pattern, pattern, 0) { 90 + constraint = pc.Constraint 91 + break 92 + } 93 + } 94 + 95 + if constraint == nil { 96 + constraint = &Vertex{} 97 + pcs.Pairs = append(pcs.Pairs, PatternConstraint{ 98 + Pattern: pattern, 99 + Constraint: constraint, 100 + }) 101 + } else if constraint.hasConjunct(c) { 102 + // The constraint already existed and the conjunct was already added. 103 + return false 104 + } 105 + 106 + constraint.addConjunctUnchecked(c) 107 + return true 108 + } 109 + 110 + // matchPattern reports whether f matches pattern. The result reflects 111 + // whether unification of pattern with f converted to a CUE value succeeds. 112 + func matchPattern(n *nodeContext, pattern Value, f Feature) bool { 113 + if pattern == nil { 114 + return false 115 + } 116 + 117 + // TODO(perf): this assumes that comparing an int64 against apd.Decimal 118 + // is faster than converting this to a Num and using that for comparison. 119 + // This may very well not be the case. But it definitely will be if we 120 + // special-case integers that can fit in an int64 (or int32 if we want to 121 + // avoid many bound checks), which we probably should. Especially when we 122 + // allow list constraints, like [<10]: T. 123 + var label Value 124 + if int64(f.Index()) == MaxIndex { 125 + f = 0 126 + } else if f.IsString() { 127 + label = f.ToValue(n.ctx) 128 + } 129 + 130 + return matchPatternValue(n.ctx, pattern, f, label) 131 + } 132 + 133 + // matchPatternValue matches a concrete value against f. label must be the 134 + // CUE value that is obtained from converting f. 135 + // 136 + // This is an optimization an intended to be faster than regular CUE evaluation 137 + // for the majority of cases where pattern constraints are used. 138 + func matchPatternValue(ctx *OpContext, pattern Value, f Feature, label Value) (result bool) { 139 + pattern = Unwrap(pattern) 140 + label = Unwrap(label) 141 + 142 + if pattern == label { 143 + return true 144 + } 145 + 146 + // Fast track for the majority of cases. 147 + switch x := pattern.(type) { 148 + case *Bottom: 149 + // TODO: hoist and reuse with the identical code in optional.go. 150 + if x == cycle { 151 + err := ctx.NewPosf(pos(pattern), "cyclic pattern constraint") 152 + for _, c := range ctx.vertex.Conjuncts { 153 + err.AddPosition(c.Elem()) 154 + } 155 + ctx.AddBottom(&Bottom{ 156 + Err: err, 157 + }) 158 + } 159 + if ctx.errs == nil { 160 + ctx.AddBottom(x) 161 + } 162 + return false 163 + 164 + case *Top: 165 + return true 166 + 167 + case *BasicType: 168 + k := label.Kind() 169 + return x.K&k == k 170 + 171 + case *BoundValue: 172 + switch x.Kind() { 173 + case StringKind: 174 + if label == nil { 175 + return false 176 + } 177 + str := label.(*String).Str 178 + return x.validateStr(ctx, str) 179 + 180 + case NumKind: 181 + return x.validateInt(ctx, int64(f.Index())) 182 + } 183 + 184 + case *Num: 185 + if !f.IsInt() { 186 + return false 187 + } 188 + yi := int64(f.Index()) 189 + xi, err := x.X.Int64() 190 + return err == nil && xi == yi 191 + 192 + case *String: 193 + y, ok := label.(*String) 194 + return ok && x.Str == y.Str 195 + 196 + case *Conjunction: 197 + for _, a := range x.Values { 198 + if !matchPatternValue(ctx, a, f, label) { 199 + return false 200 + } 201 + } 202 + return true 203 + 204 + case *Disjunction: 205 + for _, a := range x.Values { 206 + if matchPatternValue(ctx, a, f, label) { 207 + return true 208 + } 209 + } 210 + return false 211 + } 212 + 213 + // Slow track. 214 + // 215 + // TODO(perf): if a pattern tree has many values that are not handled in the 216 + // fast track, it is probably more efficient to handle everything in the 217 + // slow track. One way to signal this would be to have a "value thunk" at 218 + // the root that causes the fast track to be bypassed altogether. 219 + 220 + if label == nil { 221 + label = f.ToValue(ctx) 222 + } 223 + 224 + n := ctx.newInlineVertex(nil, nil, 225 + MakeConjunct(ctx.e, pattern, ctx.ci), 226 + MakeConjunct(ctx.e, label, ctx.ci)) 227 + n.Finalize(ctx) 228 + return n.Err(ctx) == nil 229 + }
+150
internal/core/adt/constraints_test.go
··· 1 + // Copyright 2023 CUE Authors 2 + // 3 + // Licensed under the Apache License, Version 2.0 (the "License"); 4 + // you may not use this file except in compliance with the License. 5 + // You may obtain a copy of the License at 6 + // 7 + // http://www.apache.org/licenses/LICENSE-2.0 8 + // 9 + // Unless required by applicable law or agreed to in writing, software 10 + // distributed under the License is distributed on an "AS IS" BASIS, 11 + // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 + // See the License for the specific language governing permissions and 13 + // limitations under the License. 14 + 15 + package adt_test 16 + 17 + import ( 18 + "testing" 19 + 20 + "cuelang.org/go/cue/ast" 21 + "cuelang.org/go/internal/core/adt" 22 + "cuelang.org/go/internal/core/compile" 23 + "cuelang.org/go/internal/core/eval" 24 + "cuelang.org/go/internal/core/runtime" 25 + "cuelang.org/go/internal/cuetest" 26 + ) 27 + 28 + func TestMatchPatternValue(t *testing.T) { 29 + type testCase struct { 30 + expr string 31 + label string 32 + index int64 33 + value string // overrides value from label 34 + result bool 35 + } 36 + 37 + r := runtime.New() 38 + ctx := eval.NewContext(r, nil) 39 + 40 + pv := func(t *testing.T, s string) adt.Value { 41 + switch s { 42 + case "_": 43 + return &adt.Top{} 44 + } 45 + cfg := compile.Config{ 46 + Imports: func(x *ast.Ident) (pkgPath string) { 47 + return r.BuiltinPackagePath(x.Name) 48 + }, 49 + } 50 + v, b := r.Compile(&runtime.Config{Config: cfg}, s) 51 + if b.Err != nil { 52 + t.Fatal(b.Err) 53 + } 54 + v.Finalize(ctx) 55 + ctx.Err() // clear errors 56 + return v.Value() 57 + } 58 + 59 + str := func(s string) (adt.Feature, adt.Value) { 60 + f := r.StrLabel(s) 61 + return f, ctx.NewString(s) 62 + } 63 + 64 + idx := func(i int64) adt.Feature { 65 + return adt.MakeIntLabel(adt.IntLabel, i) 66 + } 67 + 68 + testCases := []testCase{{ 69 + expr: "string", 70 + label: "foo", 71 + result: true, 72 + }, { 73 + expr: "_", 74 + label: "foo", 75 + result: true, 76 + }, { 77 + expr: "_|_", 78 + label: "foo", 79 + result: false, 80 + }, { 81 + expr: `<"h"`, 82 + label: "foo", 83 + result: true, 84 + }, { 85 + expr: `"foo"`, 86 + label: "bar", 87 + result: false, 88 + }, { 89 + expr: `"foo"`, 90 + label: "foo", 91 + result: true, 92 + }, { 93 + expr: `<4`, 94 + index: 5, 95 + result: false, 96 + }, { 97 + expr: `>=4`, 98 + index: 5, 99 + result: true, 100 + }, { 101 + expr: `5`, 102 + index: 5, 103 + result: true, 104 + }, { 105 + expr: `5`, 106 + label: "str", 107 + result: false, 108 + }, { 109 + expr: `>1 & <10`, 110 + index: 5, 111 + result: true, 112 + }, { 113 + expr: `>1 & <10`, 114 + index: 10, 115 + result: false, 116 + }, { 117 + expr: `<1 | >10`, 118 + index: 0, 119 + result: true, 120 + }, { 121 + expr: `<1 | >10`, 122 + index: 5, 123 + result: false, 124 + }, { 125 + expr: `strings.HasPrefix("foo")`, 126 + label: "foo", 127 + result: true, 128 + }, { 129 + expr: `strings.HasPrefix("foo")`, 130 + label: "bar", 131 + result: false, 132 + }} 133 + 134 + cuetest.Run(t, testCases, func(t *cuetest.T, tc *testCase) { 135 + expr := pv(t.T, tc.expr) 136 + 137 + var f adt.Feature 138 + var label adt.Value 139 + if tc.label != "" { 140 + f, label = str(tc.label) 141 + } else { 142 + f = idx(tc.index) 143 + } 144 + if tc.value != "" { 145 + label = pv(t.T, tc.value) 146 + } 147 + 148 + t.Equal(adt.MatchPatternValue(ctx, expr, f, label), tc.result) 149 + }) 150 + }
+5
internal/core/adt/eval.go
··· 960 960 961 961 nodeContextState 962 962 963 + // rootCloseContext should not be cloned as clones need to get their own 964 + // copies of this. For this reason it is not included in nodeContextState, 965 + // as it prevents it from being set "by default". 966 + rootCloseContext *closeContext 967 + 963 968 // Below are slices that need to be managed when cloning and reclaiming 964 969 // nodeContexts for reuse. We want to ensure that, instead of setting 965 970 // slices to nil, we truncate the existing buffers so that they do not
+187
internal/core/adt/export_test.go
··· 1 + // Copyright 2023 CUE Authors 2 + // 3 + // Licensed under the Apache License, Version 2.0 (the "License"); 4 + // you may not use this file except in compliance with the License. 5 + // You may obtain a copy of the License at 6 + // 7 + // http://www.apache.org/licenses/LICENSE-2.0 8 + // 9 + // Unless required by applicable law or agreed to in writing, software 10 + // distributed under the License is distributed on an "AS IS" BASIS, 11 + // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 + // See the License for the specific language governing permissions and 13 + // limitations under the License. 14 + 15 + package adt 16 + 17 + // The functions and types in this file are use to construct test cases for 18 + // fields_test.go and constraints_test. 19 + 20 + // MatchPatternValue exports matchPatternValue for testing. 21 + func MatchPatternValue(ctx *OpContext, p Value, f Feature, label Value) bool { 22 + return matchPatternValue(ctx, p, f, label) 23 + } 24 + 25 + // FieldTester is used low-level testing of field insertion. It simulates 26 + // how the evaluator inserts fields. This allows the closedness algorithm to be 27 + // tested independently of the underlying evaluator implementation. 28 + type FieldTester struct { 29 + *OpContext 30 + n *nodeContext 31 + cc *closeContext 32 + Root *Vertex 33 + } 34 + 35 + func NewFieldTester(r Runtime) *FieldTester { 36 + v := &Vertex{} 37 + ctx := New(v, &Config{Runtime: r}) 38 + n := v.getNodeContext(ctx, 1) 39 + 40 + n.rootCloseContext = &closeContext{ 41 + src: v, 42 + } 43 + 44 + return &FieldTester{ 45 + OpContext: ctx, 46 + n: n, 47 + cc: n.rootCloseContext, 48 + Root: v, 49 + } 50 + } 51 + 52 + func (x *FieldTester) Error() string { 53 + if b, ok := x.n.node.BaseValue.(*Bottom); ok && b.Err != nil { 54 + return b.Err.Error() 55 + } 56 + return "" 57 + } 58 + 59 + type declaration func(cc *closeContext) 60 + 61 + // Run simulates a CUE evaluation of the given declarations. 62 + func (x *FieldTester) Run(sub ...declaration) { 63 + x.cc.incDependent() 64 + for _, s := range sub { 65 + s(x.cc) 66 + } 67 + x.cc.decDependent(x.n) 68 + } 69 + 70 + // Def represents fields that define a definition, such that 71 + // Def(Field("a", "foo"), Field("b", "bar")) represents: 72 + // 73 + // #D: { 74 + // a: "foo" 75 + // b: "bar" 76 + // } 77 + // 78 + // For some unique #D. 79 + func (x *FieldTester) Def(sub ...declaration) declaration { 80 + return func(cc *closeContext) { 81 + ci := CloseInfo{cc: cc} 82 + ci, dc := ci.spawnCloseContext(closeDef) 83 + 84 + dc.incDependent() 85 + for _, sfn := range sub { 86 + sfn(dc) 87 + } 88 + dc.decDependent(x.n) 89 + } 90 + } 91 + 92 + // Embed represents fields embedded within the current node, such that 93 + // Embed(Field("a", "foo"), Def(Field("b", "bar"))) represents: 94 + // 95 + // { 96 + // { 97 + // a: "foo" 98 + // #D 99 + // } 100 + // } 101 + // 102 + // For some #D: b: "bar". 103 + func (x *FieldTester) Embed(sub ...declaration) declaration { 104 + return func(cc *closeContext) { 105 + ci := CloseInfo{cc: cc} 106 + ci, dc := ci.spawnCloseContext(closeEmbed) 107 + 108 + dc.incDependent() 109 + for _, sfn := range sub { 110 + sfn(dc) 111 + } 112 + dc.decDependent(x.n) 113 + } 114 + } 115 + 116 + // EmbedDef represents fields that define a struct and embedded within the 117 + // current node. 118 + func (x *FieldTester) EmbedDef(sub ...declaration) declaration { 119 + return x.Embed(x.Def(sub...)) 120 + } 121 + 122 + // Field defines a field declaration such that Field("a", "foo") represents 123 + // 124 + // a: "foo" 125 + // 126 + // The value can be of type string, int64, bool, or Expr. 127 + // Duplicate values (conjuncts) are retained as the deduplication check is 128 + // bypassed for this. 129 + func (x *FieldTester) Field(label string, a any) declaration { 130 + return x.field(label, a, false) 131 + } 132 + 133 + // FieldDedup is like Field, but enables conjunct deduplication. 134 + func (x *FieldTester) FieldDedup(label string, a any) declaration { 135 + return x.field(label, a, true) 136 + } 137 + 138 + func (x *FieldTester) field(label string, a any, dedup bool) declaration { 139 + f := x.StringLabel(label) 140 + 141 + var v Expr 142 + switch a := a.(type) { 143 + case Expr: 144 + v = a 145 + case string: 146 + v = x.NewString(a) 147 + case int: 148 + v = x.NewInt64(int64(a)) 149 + case bool: 150 + v = x.newBool(a) 151 + default: 152 + panic("type not supported") 153 + } 154 + 155 + return func(cc *closeContext) { 156 + var c Conjunct 157 + c.Env = &Environment{Vertex: x.Root} 158 + c.CloseInfo.cc = cc 159 + c.x = v 160 + c.CloseInfo.FromDef = cc.isDef 161 + c.CloseInfo.FromEmbed = cc.isEmbed 162 + 163 + x.n.insertArc(f, ArcMember, c, dedup) 164 + } 165 + } 166 + 167 + // Pat represents a pattern constraint, such that Pat(`<"a"`, "foo") represents 168 + // 169 + // [<"a"]: "foo" 170 + func (x *FieldTester) Pat(pattern Value, v Expr) declaration { 171 + if pattern == nil { 172 + panic("nil pattern") 173 + } 174 + if v == nil { 175 + panic("nil expr") 176 + } 177 + return func(cc *closeContext) { 178 + var c Conjunct 179 + c.Env = &Environment{Vertex: x.Root} 180 + c.CloseInfo.cc = cc 181 + c.x = v 182 + c.CloseInfo.FromDef = cc.isDef 183 + c.CloseInfo.FromEmbed = cc.isEmbed 184 + 185 + x.n.insertPattern(pattern, c) 186 + } 187 + }
+575
internal/core/adt/fields.go
··· 1 + // Copyright 2023 CUE Authors 2 + // 3 + // Licensed under the Apache License, Version 2.0 (the "License"); 4 + // you may not use this file except in compliance with the License. 5 + // You may obtain a copy of the License at 6 + // 7 + // http://www.apache.org/licenses/LICENSE-2.0 8 + // 9 + // Unless required by applicable law or agreed to in writing, software 10 + // distributed under the License is distributed on an "AS IS" BASIS, 11 + // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 + // See the License for the specific language governing permissions and 13 + // limitations under the License. 14 + 15 + package adt 16 + 17 + // This file holds the logic for the insertion of fields and pattern 18 + // constraints, including tracking closedness. 19 + // 20 + // 21 + // DESIGN GOALS 22 + // 23 + // Key to performance is to fail early during evaluation. This is especially 24 + // true for disjunctions. In CUE evaluation, conjuncts may be evaluated in a 25 + // fairly arbitrary order. We want to retain this flexibility while also failing 26 + // on disallowed fields as soon as we have enough data to tell for certain. 27 + // 28 + // Keeping track of which fields are allowed means keeping provenance data on 29 + // whether certain conjuncts originate from embeddings or definitions, as well 30 + // as how they group together with other conjuncts. These data structure should 31 + // allow for a "mark and unwind" approach to allow for backtracking when 32 + // computing disjunctions. 33 + // 34 + // References to the same CUE value may be added as conjuncts through various 35 + // paths. For instance, a reference to a definition may be added directly, or 36 + // through embedding. How they are added affects which set of fields are 37 + // allowed. This can make the removal of duplicate conjuncts hard. A solution 38 + // should make it straightforward to deduplicate conjuncts if they have the same 39 + // impact on field inclusion. 40 + // 41 + // All conjuncts associated with field constraints, including optional fields 42 + // and pattern constraints, should be collated, deduplicated, and evaluated as 43 + // if they were regular fields. This allows comparisons between values to be 44 + // meaningful and helps to filter disjuncts. 45 + // 46 + // The provenance data generated by this algorithm should ideally be easily 47 + // usable in external APIs. 48 + // 49 + // 50 + // DATA STRUCTURES 51 + // 52 + // Conjuncts 53 + // 54 + // To keep track of conjunct provenance, each conjunct has a few flags that 55 + // indicates whether it orignates from 56 + // - an embedding 57 + // - a definition 58 + // - a reference (optional and unimplemented) 59 + // 60 + // Conjuncts with the same origin are represented as a single Conjunct in the 61 + // Vertex, where this conjunct is a list of these conjuncts. In other words, the 62 + // conjuncts of a Vertex are really a forest (group of trees) of conjuncts that, 63 + // recursively, reflect the provenance of the conjuncts contained within it. 64 + // 65 + // The current implementation uses a Vertex for listing conjuncts with the same 66 + // origin. This Vertex is marked as "Dynamic", as it does not have a CUE path 67 + // that leads to them. 68 + // 69 + // 70 + // Constraints 71 + // 72 + // Vertex values separately keep track of pattern constraints. These consist of 73 + // a list of patterns with associated conjuncts, and a CUE expression that 74 + // represents the set of allowed fields. This information is mostly for equality 75 + // checking: by the time this data is produced, conjuncts associated with 76 + // patterns are already inserted into the computed subfields. 77 + // 78 + // Note that this representation assumes that patterns are always accrued 79 + // cumulatively: a field that is allowed will accrue the conjuncts of any 80 + // matched pattern, even if it originates from an embedding that itself does not 81 + // allow this field. 82 + // 83 + // 84 + // ALGORITHM 85 + // 86 + // When processing the conjuncts of a Vertex, subfields are tracked per 87 + // "grouping" (the list of conjuncts of the same origin). Each grouping keeps a 88 + // counter of the number of unprocessed conjuncts and subgroups associated with 89 + // it. Field inclusion (closedness) can be computed as soon as all subconjuncts 90 + // and subgroups are processed. 91 + // 92 + // Conjuncts of subfields are inserted in such a way that they reflect the same 93 + // grouping as the parent Vertex, plus any grouping that may be added by the 94 + // subfield itself. 95 + // 96 + // It would be possible, though, to collapse certain (combinations of) groups 97 + // that contain only a single conjunct. This can limit the size of such conjunct 98 + // trees. 99 + // 100 + // As conjuncts are added within their grouping context, it is possible to 101 + // uniquely identify conjuncts only by Vertex and expression pointer, 102 + // disregarding the Environment. 103 + // 104 + // 105 + // EXAMPLE DATA STRUCTURE 106 + // 107 + // a: #A 108 + // #A: { 109 + // #B 110 + // x: r1 111 + // } 112 + // #B: y: r2 113 + // r1: z: r3 114 + // r2: 2 115 + // r3: foo: 2 116 + // 117 + // gets evaluated into: 118 + // 119 + // V_a: Arcs{ 120 + // x: V_x [ V_def(#A)[ r1 ] ] 121 + // y: V_y [ V_def(#A)[ V_embed(#B)[ r2 ] ] ] 122 + // } 123 + // 124 + // When evaluating V_x, its Arcs, in turn become: 125 + // 126 + // V_x: Arcs{ 127 + // z: V_z [ V_def(#A)[ V_ref(r1)[ r3 ]) ]] 128 + // } 129 + // 130 + // The V_def(#A) is necessary here to ensure that closedness information can be 131 + // computed, if necessary. The V_ref's, however, are optional, and can be 132 + // omitted if provenance is less important: 133 + // 134 + // V_x: Arcs{ 135 + // z: V_z [ V_def(#A)[ r3 ]] 136 + // } 137 + // 138 + // Another possible optimization is to eliminate Vertices if there is only one 139 + // conjunct: the embedding and definition flags in the conjunct can be 140 + // sufficient in that case. The provenance data could potentially be derived 141 + // from the Environment in that case. If an embedding conjunct is itself the 142 + // only conjunct in a list, the embedding bit can be eliminated. So V_y in the 143 + // above example could be reduced to 144 + // 145 + // V_y [ V_def(#A)[ r2 ] ] 146 + // 147 + 148 + // TODO(perf): 149 + // - the data structures could probably be collapsed with Conjunct. and the 150 + // Vertex inserted into the Conjuncts could be a special ConjunctGroup. 151 + 152 + type closeContext struct { 153 + // Used to recursively insert Vertices. 154 + parent *closeContext 155 + 156 + // child links to a sequence which additional patterns need to be verified 157 + // against (&&). If there are more than one, these additional nodes are 158 + // linked with next. Only closed nodes with patterns are added. Arc sets are 159 + // already merged during processing. 160 + child *closeContext 161 + 162 + // next holds a linked list of nodes to process. 163 + // See comments above and see linkPatterns. 164 + next *closeContext 165 + 166 + // if conjunctCount is 0, pattern constraints can be merged and the 167 + // closedness can be checked. To ensure that this is true, there should 168 + // be an additional increment at the start before any processing is done. 169 + conjunctCount int 170 + 171 + src *Vertex 172 + 173 + // isDef indicates whether the closeContext is created as part of a 174 + // definition. 175 + isDef bool 176 + 177 + // isEmbed indicates whether the closeContext is created as part of an 178 + // embedding. 179 + isEmbed bool 180 + 181 + // isClosed is true if a node is a def, it became closed because of a 182 + // reference or if it is closed by the close builtin. 183 + // 184 + // isClosed must only be set to true if all fields and pattern constraints 185 + // that define the domain of the node have been added. 186 + isClosed bool 187 + 188 + // isTotal is true if a node contains an ellipsis and is defined for all 189 + // values. 190 + isTotal bool 191 + 192 + Arcs []*Vertex // TODO: also link to parent.src Vertex? 193 + 194 + // Patterns contains all patterns of the current closeContext. 195 + // It is used in the construction of Expr. 196 + Patterns []Value 197 + 198 + // Expr contains the Expr that is used for checking whether a Feature 199 + // is allowed in this context. It is only complete after the full 200 + // context has been completed, but it can be used for initial checking 201 + // once isClosed is true. 202 + Expr Value 203 + } 204 + 205 + // spawnCloseContext wraps the closeContext in c with a new one and returns 206 + // this new context along with an updated CloseInfo. The new values reflect 207 + // that the set of fields represented by c are now, for instance, enclosed in 208 + // an embedding or a definition. 209 + // 210 + // This call is used when preparing ADT values for evaluation. 211 + func (c CloseInfo) spawnCloseContext(t closeNodeType) (CloseInfo, *closeContext) { 212 + c.cc.incDependent() 213 + 214 + c.cc = &closeContext{ 215 + parent: c.cc, 216 + } 217 + 218 + switch t { 219 + case closeDef: 220 + c.cc.isDef = true 221 + case closeEmbed: 222 + c.cc.isEmbed = true 223 + } 224 + 225 + return c, c.cc 226 + } 227 + 228 + // incDependent needs to be called for any conjunct or child closeContext 229 + // scheduled for c that is queued for later processing and not scheduled 230 + // immediately. 231 + func (c *closeContext) incDependent() { 232 + c.conjunctCount++ 233 + } 234 + 235 + // decDependent needs to be called for any conjunct or child closeContext for 236 + // which a corresponding incDependent was called after it has been successfully 237 + // processed. 238 + func (c *closeContext) decDependent(n *nodeContext) { 239 + c.conjunctCount-- 240 + if c.conjunctCount > 0 { 241 + return 242 + } 243 + 244 + c.finalizePattern(n) 245 + 246 + if c.isDef { 247 + c.isClosed = true 248 + } 249 + 250 + p := c.parent 251 + if p == nil { 252 + // Root pattern, set allowed patterns. 253 + if pcs := n.node.PatternConstraints; pcs != nil { 254 + if pcs.Allowed != nil { 255 + panic("unexpected allowed set") 256 + } 257 + pcs.Allowed = c.Expr 258 + return 259 + } 260 + return 261 + } 262 + 263 + if !c.isEmbed && c.isClosed { 264 + // Merge the two closeContexts and ensure that the patterns and fields 265 + // are mutually compatible according to the closedness rules. 266 + injectClosed(n, c, p) 267 + p.Expr = mergeConjunctions(p.Expr, c.Expr) 268 + } else { 269 + // Do not check closedness of fields for embeddings. 270 + // The pattern constraints of the embedding still need to be added 271 + // to the current context. 272 + p.linkPatterns(c) 273 + } 274 + 275 + p.decDependent(n) 276 + } 277 + 278 + // linkPatterns merges the patterns of child into c, if needed. 279 + func (c *closeContext) linkPatterns(child *closeContext) { 280 + if len(child.Patterns) > 0 { 281 + child.next = c.child 282 + c.child = child 283 + } 284 + } 285 + 286 + // insertArc inserts conjunct c into n. If check is true it will not add c if it 287 + // was already added. 288 + func (n *nodeContext) insertArc(f Feature, mode ArcType, c Conjunct, check bool) { 289 + if n == nil { 290 + panic("nil nodeContext") 291 + } 292 + if n.node == nil { 293 + panic("nil node") 294 + } 295 + cc := c.CloseInfo.cc 296 + if cc == nil { 297 + panic("nil closeContext") 298 + } 299 + 300 + if _, isNew := n.insertArc1(f, mode, c, check); !isNew { 301 + return // Patterns were already added. 302 + } 303 + 304 + // Match and insert patterns. 305 + if pcs := n.node.PatternConstraints; pcs != nil { 306 + for _, pc := range pcs.Pairs { 307 + if matchPattern(n, pc.Pattern, f) { 308 + for _, c := range pc.Constraint.Conjuncts { 309 + n.insertArc1(f, mode, c, check) 310 + } 311 + } 312 + } 313 + } 314 + } 315 + 316 + // insertArc1 inserts conjunct c into its associated closeContext. If the 317 + // closeContext did not yet have a Vertex for f, it is created and it is ensured 318 + // that the grouping is associated with all the parent closeContexts. If it is 319 + // newly added to the root closeContext, the outer grouping is also added to 320 + // n.node, the top-level Vertex itself. 321 + // 322 + // insertArc1 is exclusively used by insertArc to insert conjuncts for regular 323 + // fields as well as pattern constraints. 324 + func (n *nodeContext) insertArc1(f Feature, mode ArcType, c Conjunct, check bool) (v *Vertex, isNew bool) { 325 + cc := c.CloseInfo.cc 326 + 327 + // Locate or create the arc in the current context. 328 + v, isNew = cc.insertArc(n, f, mode, c, check) 329 + if !isNew { 330 + return v, false 331 + } 332 + 333 + i := cc.parent 334 + for prev := cc; i != nil && isNew; prev, i = i, i.parent { 335 + vc := MakeRootConjunct(nil, v) 336 + vc.CloseInfo.FromDef = prev.isDef 337 + vc.CloseInfo.FromEmbed = prev.isEmbed 338 + v, isNew = i.insertArc(n, f, mode, vc, check) 339 + } 340 + if isNew && i == nil { 341 + n.node.Arcs = append(n.node.Arcs, v) 342 + } 343 + 344 + if cc.isClosed && !v.disallowedField && !matchPattern(n, cc.Expr, f) { 345 + n.notAllowedError(f) 346 + } 347 + 348 + return v, isNew 349 + } 350 + 351 + // insertArc is exclusively called from nodeContext.insertArc1 and is used to 352 + // insert a conjunct in closeContext, along with other conjuncts from the same 353 + // origin. It does not recursively insert conjuncts into parent closeContexts, 354 + // which is done by insertArc1. 355 + // 356 + // If check is true it will not add c if it was already added. 357 + func (cc *closeContext) insertArc( 358 + n *nodeContext, f Feature, mode ArcType, c Conjunct, check bool) (v *Vertex, isNew bool) { 359 + c.CloseInfo.cc = nil 360 + 361 + for _, a := range cc.Arcs { 362 + if a.Label != f { 363 + continue 364 + } 365 + 366 + if f.IsLet() { 367 + a.MultiLet = true 368 + return a, false 369 + } 370 + if check { 371 + a.AddConjunct(c) 372 + } else { 373 + a.addConjunctUnchecked(c) 374 + } 375 + // TODO: possibly add positions to error. 376 + return a, false 377 + } 378 + 379 + v = &Vertex{Parent: cc.src, Label: f, ArcType: mode} 380 + if mode == ArcPending { 381 + cc.src.hasPendingArc = true 382 + } 383 + v.Conjuncts = append(v.Conjuncts, c) 384 + cc.Arcs = append(cc.Arcs, v) 385 + 386 + return v, true 387 + } 388 + 389 + func (n *nodeContext) insertPattern(pattern Value, c Conjunct) { 390 + ctx := n.ctx 391 + cc := c.CloseInfo.cc 392 + 393 + // Collect patterns in root vertex. This allows comparing disjuncts for 394 + // equality as well as inserting new arcs down the line as they are 395 + // inserted. 396 + if !n.insertConstraint(pattern, c) { 397 + return 398 + } 399 + 400 + // Match against full set of arcs from root, but insert in current vertex. 401 + // Hypothesis: this may not be necessary. Maybe for closedness. 402 + // TODO: may need to replicate the closedContext for patterns. 403 + // Also: Conjuncts for matching other arcs in this node may be different 404 + // for matching arcs using v.foo?, if we need to ensure that conjuncts 405 + // from arcs and patterns are grouped under the same vertex. 406 + // TODO: verify. See test Pattern 1b 407 + for _, a := range n.node.Arcs { 408 + if matchPattern(n, pattern, a.Label) { 409 + // TODO: is it necessary to check for uniqueness here? 410 + n.insertArc(a.Label, a.ArcType, c, true) 411 + } 412 + } 413 + 414 + if cc.isTotal { 415 + return 416 + } 417 + if isTotal(pattern) { 418 + cc.isTotal = true 419 + cc.Patterns = cc.Patterns[:0] 420 + return 421 + } 422 + 423 + // insert pattern in current set. 424 + // TODO: normalize patterns 425 + // TODO: do we only need to do this for closed contexts? 426 + for _, pc := range cc.Patterns { 427 + if Equal(ctx, pc, pattern, 0) { 428 + return 429 + } 430 + } 431 + cc.Patterns = append(cc.Patterns, pattern) 432 + } 433 + 434 + // isTotal reports whether pattern value p represents a full domain, that is, 435 + // whether it is of type BasicType or Top. 436 + func isTotal(p Value) bool { 437 + switch p.(type) { 438 + case *BasicType: 439 + return true 440 + case *Top: 441 + return true 442 + } 443 + return false 444 + } 445 + 446 + // injectClosed updates dst so that it only allows fields allowed by closed. 447 + // 448 + // It first ensures that the fields contained in dst are allowed by the fields 449 + // and patterns defined in closed. It reports an error in the nodeContext if 450 + // this is not the case. 451 + func injectClosed(n *nodeContext, closed, dst *closeContext) { 452 + // TODO: check that fields are not void arcs. 453 + outer: 454 + for _, a := range dst.Arcs { 455 + for _, b := range closed.Arcs { 456 + if a.Label == b.Label { 457 + if b.disallowedField { 458 + // Error was already reported. 459 + break 460 + } 461 + continue outer 462 + } 463 + } 464 + if !matchPattern(n, closed.Expr, a.Label) { 465 + n.notAllowedError(a.Label) 466 + continue 467 + } 468 + } 469 + 470 + if !dst.isClosed { 471 + // Since dst is not closed, it is safe to take all patterns from 472 + // closed. 473 + // This is only necessary for passing up patterns into embeddings. For 474 + // (the conjunction of) definitions the construction is handled 475 + // elsewhere. 476 + // TODO(perf): reclaim slice memory 477 + dst.Patterns = closed.Patterns 478 + 479 + dst.isClosed = true 480 + } 481 + } 482 + 483 + // notAllowedError reports a "field not allowed" error in n and sets the value 484 + // for arc f to that error. 485 + func (n *nodeContext) notAllowedError(f Feature) { 486 + // Set the error on the same arc as the old implementation 487 + // and using the same path. 488 + arc := n.node.Lookup(f) 489 + v := n.ctx.PushArc(arc) 490 + n.node.SetValue(n.ctx, n.ctx.NewErrf("field not allowed")) 491 + arc.disallowedField = true // Is this necessary? 492 + n.ctx.PopArc(v) 493 + 494 + // TODO: create a special kind of error that gets the positions 495 + // of the relevant locations upon request from the arc. 496 + } 497 + 498 + // mergeConjunctions combines two values into one. It never modifies an 499 + // existing conjunction. 500 + func mergeConjunctions(a, b Value) Value { 501 + if a == nil { 502 + return b 503 + } 504 + if b == nil { 505 + return a 506 + } 507 + ca, _ := a.(*Conjunction) 508 + cb, _ := b.(*Conjunction) 509 + n := 2 510 + if ca != nil { 511 + n += len(ca.Values) - 1 512 + } 513 + if cb != nil { 514 + n += len(cb.Values) - 1 515 + } 516 + vs := make([]Value, 0, n) 517 + if ca != nil { 518 + vs = append(vs, ca.Values...) 519 + } else { 520 + vs = append(vs, a) 521 + } 522 + if cb != nil { 523 + vs = append(vs, cb.Values...) 524 + } else { 525 + vs = append(vs, b) 526 + } 527 + // TODO: potentially order conjuncts to make matching more likely. 528 + return &Conjunction{Values: vs} 529 + } 530 + 531 + // finalizePattern updates c.Expr to a CUE Value representing all fields allowed 532 + // by the pattern constraints of c. If this context or any of its direct 533 + // children is closed, the result will be a conjunction of all these closed 534 + // values. Otherwise it will be a disjunction of all its children. A nil value 535 + // represents all values. 536 + func (c *closeContext) finalizePattern(n *nodeContext) { 537 + switch { 538 + case c.Expr != nil: // Patterns and expression are already set. 539 + if !c.isClosed { 540 + panic("c.Expr set unexpectedly") 541 + } 542 + return 543 + case c.isTotal: // All values are allowed always. 544 + return 545 + } 546 + 547 + // As this context is not closed, the pattern is somewhat meaningless. 548 + // It may still be useful for analysis. 549 + or := c.Patterns 550 + 551 + for cc := c.child; cc != nil; cc = cc.next { 552 + if cc.isTotal { 553 + return 554 + } 555 + // Could be closed, in which case it must also be an embedding. 556 + 557 + // TODO: simplify the values. 558 + switch x := cc.Expr.(type) { 559 + case nil: 560 + case *Disjunction: 561 + or = append(or, x.Values...) 562 + default: 563 + or = append(or, x) 564 + } 565 + } 566 + 567 + switch len(or) { 568 + case 0: 569 + case 1: 570 + c.Expr = or[0] 571 + default: 572 + // TODO: potentially order conjuncts to make matching more likely. 573 + c.Expr = &Disjunction{Values: or} 574 + } 575 + }
+581
internal/core/adt/fields_test.go
··· 1 + // Copyright 2023 CUE Authors 2 + // 3 + // Licensed under the Apache License, Version 2.0 (the "License"); 4 + // you may not use this file except in compliance with the License. 5 + // You may obtain a copy of the License at 6 + // 7 + // http://www.apache.org/licenses/LICENSE-2.0 8 + // 9 + // Unless required by applicable law or agreed to in writing, software 10 + // distributed under the License is distributed on an "AS IS" BASIS, 11 + // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 12 + // See the License for the specific language governing permissions and 13 + // limitations under the License. 14 + 15 + package adt_test 16 + 17 + import ( 18 + "fmt" 19 + "strings" 20 + "testing" 21 + 22 + "cuelang.org/go/cue/format" 23 + "cuelang.org/go/internal/core/adt" 24 + "cuelang.org/go/internal/core/debug" 25 + "cuelang.org/go/internal/core/eval" 26 + "cuelang.org/go/internal/core/export" 27 + "cuelang.org/go/internal/core/runtime" 28 + "cuelang.org/go/internal/cuetest" 29 + ) 30 + 31 + // TestCloseContext tests the intricacies of the closedness algorithm. 32 + // Much of this could be tested using the existing testing framework, but the 33 + // code is intricate enough that it is hard to map real-life cases onto 34 + // specific edge cases. Also, changes in the higher-level order of evaluation 35 + // may cause certain test cases to go untested. 36 + // 37 + // This test enables covering such edge cases with confidence by allowing 38 + // fine control over the order of execution of conjuncts 39 + // 40 + // NOTE: much of the code is in export_test.go. This test needs access to 41 + // higher level functionality which prevents it from being defined in 42 + // package adt. The code in export_test.go provides access to the 43 + // low-level functionality that this test needs. 44 + func TestCloseContext(t *testing.T) { 45 + adt.Verbosity = 1 46 + 47 + r := runtime.New() 48 + ctx := eval.NewContext(r, nil) 49 + 50 + v := func(s string) adt.Value { 51 + v, _ := r.Compile(nil, s) 52 + if err := v.Err(ctx); err != nil { 53 + t.Fatal(err.Err) 54 + } 55 + v.Finalize(ctx) 56 + return v.Value() 57 + } 58 + ref := func(s string) adt.Expr { 59 + f := r.StrLabel(s) 60 + return &adt.FieldReference{Label: f} 61 + } 62 + // TODO: this may be needed once we optimize inserting scalar values. 63 + // pe := func(s string) adt.Expr { 64 + // x, err := parser.ParseExpr("", s) 65 + // if err != nil { 66 + // t.Fatal(err) 67 + // } 68 + // c, err := compile.Expr(nil, r, "test", x) 69 + // if err != nil { 70 + // t.Fatal(err) 71 + // } 72 + // return c.Expr() 73 + // } 74 + type testCase struct { 75 + name string 76 + run func(*adt.FieldTester) 77 + 78 + // arcs shows a hierarchical representation of the arcs below the node. 79 + arcs string 80 + 81 + // patters shows a list of patterns and associated conjuncts 82 + patterns string 83 + 84 + // allowed is the computed allowed expression. 85 + allowed string 86 + 87 + // err holds all errors or "" if none. 88 + err string 89 + } 90 + cases := []testCase{{ 91 + name: "one", 92 + run: func(x *adt.FieldTester) { 93 + x.Run(x.Field("a", "foo")) 94 + }, 95 + arcs: `a: {"foo"}`, 96 + }, { 97 + name: "two", 98 + run: func(x *adt.FieldTester) { 99 + x.Run( 100 + x.Field("a", "foo"), 101 + x.Field("a", "bar"), 102 + ) 103 + }, 104 + arcs: `a: {"foo" "bar"}`, 105 + }, { 106 + // This could be optimized as both nestings have a single value. 107 + // This cause some provenance data to be lost, so this could be an 108 + // option instead. 109 + name: "double nested", 110 + // a: "foo" 111 + // #A 112 + // 113 + // where 114 + // 115 + // #A: { 116 + // #B 117 + // b: "foo" 118 + // } 119 + // #B: a: "foo" 120 + run: func(x *adt.FieldTester) { 121 + x.Run( 122 + x.Field("a", "foo"), 123 + x.EmbedDef( 124 + x.EmbedDef(x.Field("a", "foo")), 125 + x.Field("b", "foo"), 126 + ), 127 + ) 128 + }, 129 + arcs: ` 130 + a: { 131 + "foo" 132 + [e]{ 133 + [d]{ 134 + [e]{ 135 + [d]{"foo"} 136 + } 137 + } 138 + } 139 + } 140 + b: { 141 + [e]{ 142 + [d]{"foo"} 143 + } 144 + }`, 145 + }, { 146 + name: "single pattern", 147 + run: func(x *adt.FieldTester) { 148 + x.Run(x.Pat(v(`<="foo"`), v("1"))) 149 + }, 150 + arcs: "", 151 + patterns: `<="foo": {1}`, 152 + allowed: `<="foo"`, 153 + }, { 154 + name: "total patterns", 155 + run: func(x *adt.FieldTester) { 156 + x.Run( 157 + x.Pat(v(`<="foo"`), x.NewString("foo")), 158 + x.Pat(v(`string`), v("1")), 159 + x.Pat(v(`string`), v("1")), 160 + x.Pat(v(`<="foo"`), v("2")), 161 + ) 162 + }, 163 + 164 + arcs: "", 165 + patterns: ` 166 + <="foo": {"foo" 2} 167 + string: {1 1}`, 168 + 169 + // Should be empty or string only, as all fields match. 170 + allowed: "", 171 + }, { 172 + name: "multi patterns", 173 + run: func(x *adt.FieldTester) { 174 + shared := v("100") 175 + x.Run( 176 + x.Pat(v(`<="foo"`), x.NewString("foo")), 177 + x.Pat(v(`>"bar"`), shared), 178 + x.Pat(v(`>"bar"`), shared), 179 + x.Pat(v(`<="foo"`), v("1")), 180 + ) 181 + }, 182 + 183 + // should have only a single 100 184 + patterns: ` 185 + <="foo": {"foo" 1} 186 + >"bar": {100}`, 187 + 188 + // TODO: normalize the output, Greater than first. 189 + allowed: `|(<="foo", >"bar")`, 190 + }, { 191 + name: "pattern defined after matching field", 192 + run: func(x *adt.FieldTester) { 193 + x.Run( 194 + x.Field("a", "foo"), 195 + x.Pat(v(`string`), v(`string`)), 196 + ) 197 + }, 198 + arcs: `a: {"foo" string}`, 199 + patterns: `string: {string}`, 200 + allowed: "", // all fields 201 + }, { 202 + name: "pattern defined before matching field", 203 + run: func(x *adt.FieldTester) { 204 + x.Run( 205 + x.Pat(v(`string`), v(`string`)), 206 + x.Field("a", "foo"), 207 + ) 208 + }, 209 + arcs: `a: {"foo" string}`, 210 + patterns: `string: {string}`, 211 + allowed: "", // all fields 212 + }, { 213 + name: "shared on one level", 214 + run: func(x *adt.FieldTester) { 215 + shared := ref("shared") 216 + x.Run( 217 + x.Pat(v(`string`), v(`1`)), 218 + x.Pat(v(`>"a"`), shared), 219 + x.Pat(v(`>"a"`), shared), 220 + x.Field("m", "foo"), 221 + x.Pat(v(`string`), v(`2`)), 222 + x.Pat(v(`>"a"`), shared), 223 + x.Pat(v(`>"b"`), shared), 224 + ) 225 + }, 226 + arcs: `m: {"foo" 1 shared 2}`, 227 + patterns: ` 228 + string: {1 2} 229 + >"a": {shared} 230 + >"b": {shared}`, 231 + }, { 232 + // The same conjunct in different groups could result in the different 233 + // closedness rules. Hence they should not be shared. 234 + name: "do not share between groups", 235 + run: func(x *adt.FieldTester) { 236 + notShared := ref("notShared") 237 + x.Run( 238 + x.Def(x.Field("m", notShared)), 239 + 240 + // TODO(perf): since the nodes in Def have strictly more 241 + // restrictive closedness requirements, this node could be 242 + // eliminated from arcs. 243 + x.Field("m", notShared), 244 + 245 + // This could be shared with the first entry, but since there 246 + // is no mechanism to identify equality for tis case, it is 247 + // not shared. 248 + x.Def(x.Field("m", notShared)), 249 + 250 + // Under some conditions the same holds for embeddings. 251 + x.Embed(x.Field("m", notShared)), 252 + ) 253 + }, 254 + arcs: `m: { 255 + [d]{notShared} 256 + notShared 257 + [d]{notShared} 258 + [e]{notShared} 259 + }`, 260 + }, { 261 + name: "conjunction of patterns", 262 + run: func(x *adt.FieldTester) { 263 + x.Run( 264 + x.Def(x.Pat(v(`>"a"`), v("1"))), 265 + x.Def(x.Pat(v(`<"m"`), v("2"))), 266 + ) 267 + }, 268 + patterns: ` 269 + >"a": {1} 270 + <"m": {2}`, 271 + // allowed reflects explicitly matched fields, even if node is not closed. 272 + allowed: `&(>"a", <"m")`, 273 + }, { 274 + name: "pattern in definition in embedding", 275 + run: func(x *adt.FieldTester) { 276 + x.Run( 277 + x.Embed(x.Def(x.Pat(v(`>"a"`), v("1")))), 278 + ) 279 + }, 280 + patterns: `>"a": {1}`, 281 + allowed: `>"a"`, 282 + }, { 283 + name: "avoid duplicate pattern entries", 284 + run: func(x *adt.FieldTester) { 285 + x.Run( 286 + x.Field("b", "bar"), 287 + x.Field("b", "baz"), 288 + x.Pat(v(`string`), v("2")), 289 + x.Pat(v(`string`), v("3")), 290 + x.Field("c", "bar"), 291 + x.Field("c", "baz"), 292 + ) 293 + }, 294 + arcs: ` 295 + b: {"bar" "baz" 2 3} 296 + c: {"bar" 2 3 "baz"}`, 297 + 298 + patterns: `string: {2 3}`, 299 + }, { 300 + name: "conjunction in embedding", 301 + run: func(x *adt.FieldTester) { 302 + x.Run( 303 + x.Field("b", "foo"), 304 + x.Embed( 305 + x.Def(x.Pat(v(`>"a"`), v("1"))), 306 + x.Def(x.Pat(v(`<"z"`), v("2"))), 307 + ), 308 + x.Field("c", "bar"), 309 + x.Pat(v(`<"q"`), v("3")), 310 + ) 311 + }, 312 + arcs: ` 313 + b: { 314 + "foo" 315 + [e]{ 316 + [d]{1} 317 + [d]{2} 318 + } 319 + 3 320 + } 321 + c: { 322 + "bar" 323 + [e]{ 324 + [d]{1} 325 + [d]{2} 326 + } 327 + 3 328 + }`, // 329 + patterns: ` 330 + >"a": {1} 331 + <"z": {2} 332 + <"q": {3}`, 333 + allowed: `|(<"q", &(>"a", <"z"))`, 334 + }, { 335 + // The point of this test is to see if the "allow" expression nests 336 + // properly. 337 + name: "conjunctions in embedding", 338 + run: func(x *adt.FieldTester) { 339 + x.Run( 340 + x.Embed( 341 + x.Def( 342 + x.Field("b", "foo"), 343 + x.Embed( 344 + x.Def(x.Pat(v(`>"a"`), v("1"))), 345 + x.Def(x.Pat(v(`<"g"`), v("2"))), 346 + x.Pat(v(`<"z"`), v("3")), 347 + ), 348 + x.Embed( 349 + x.Def(x.Pat(v(`>"b"`), v("3"))), 350 + x.Def(x.Pat(v(`<"h"`), v("4"))), 351 + ), 352 + x.Field("c", "bar"), 353 + x.Pat(v(`<"q"`), v("5")), 354 + ), 355 + x.Def( 356 + x.Embed( 357 + x.Def(x.Pat(v(`>"m"`), v("6"))), 358 + x.Def(x.Pat(v(`<"y"`), v("7"))), 359 + x.Pat(v(`<"z"`), v("8")), 360 + ), 361 + x.Embed( 362 + x.Def(x.Pat(v(`>"n"`), v("9"))), 363 + x.Def(x.Pat(v(`<"z"`), v("10"))), 364 + ), 365 + x.Field("c", "bar"), 366 + x.Pat(v(`<"q"`), v("11")), 367 + ), 368 + ), 369 + x.Pat(v(`<"h"`), v("12")), 370 + ) 371 + }, 372 + arcs: ` 373 + b: { 374 + [e]{ 375 + [d]{ 376 + "foo" 377 + [e]{ 378 + [d]{1} 379 + [d]{2} 380 + 3 381 + } 382 + [e]{ 383 + [d]{4} 384 + } 385 + 5 386 + } 387 + [d]{ 388 + [e]{ 389 + [d]{7} 390 + 8 391 + } 392 + [e]{ 393 + [d]{10} 394 + } 395 + 11 396 + } 397 + } 398 + 12 399 + } 400 + c: { 401 + [e]{ 402 + [d]{ 403 + "bar" 404 + [e]{ 405 + [d]{1} 406 + [d]{2} 407 + 3 408 + } 409 + [e]{ 410 + [d]{3} 411 + [d]{4} 412 + } 413 + 5 414 + } 415 + [d]{ 416 + [e]{ 417 + [d]{7} 418 + 8 419 + } 420 + [e]{ 421 + [d]{10} 422 + } 423 + "bar" 424 + 11 425 + } 426 + } 427 + 12 428 + }`, 429 + patterns: ` 430 + >"a": {1} 431 + <"g": {2} 432 + <"z": {3 8 10} 433 + >"b": {3} 434 + <"h": {4 12} 435 + <"q": {5 11} 436 + >"m": {6} 437 + <"y": {7} 438 + >"n": {9}`, 439 + allowed: `|(<"h", &(|(<"q", &(>"b", <"h"), &(>"a", <"g")), |(<"q", &(>"n", <"z"), &(>"m", <"y"))))`, 440 + }, { 441 + name: "dedup equal", 442 + run: func(x *adt.FieldTester) { 443 + shared := v("1") 444 + x.Run( 445 + x.FieldDedup("a", shared), 446 + x.FieldDedup("a", shared), 447 + ) 448 + }, 449 + patterns: "", 450 + allowed: "", 451 + arcs: `a: {1}`, 452 + }, { 453 + // Effectively {a: 1} & #D, where #D is {b: 1} 454 + name: "disallowed before", 455 + run: func(x *adt.FieldTester) { 456 + x.Run( 457 + x.Field("a", v("1")), 458 + x.Def(x.Field("b", v("1"))), 459 + ) 460 + }, 461 + arcs: ` 462 + a: {1} 463 + b: { 464 + [d]{1} 465 + }`, 466 + err: `a: field not allowed`, 467 + }, { 468 + // Effectively #D & {a: 1}, where #D is {b: 1} 469 + name: "disallowed after", 470 + run: func(x *adt.FieldTester) { 471 + x.Run( 472 + x.Def(x.Field("b", v("1"))), 473 + x.Field("a", v("1")), 474 + ) 475 + }, 476 + arcs: ` 477 + b: { 478 + [d]{1} 479 + } 480 + a: {1}`, 481 + err: `a: field not allowed`, 482 + }} 483 + 484 + cuetest.Run(t, cases, func(t *cuetest.T, tc *testCase) { 485 + x := adt.NewFieldTester(r) 486 + tc.run(x) 487 + 488 + t.Equal(writeArcs(x, x.Root), tc.arcs) 489 + t.Equal(x.Error(), tc.err) 490 + 491 + var patterns, allowed string 492 + if pcs := x.Root.PatternConstraints; pcs != nil { 493 + patterns = writePatterns(x, pcs.Pairs) 494 + if pcs.Allowed != nil { 495 + allowed = debug.NodeString(r, pcs.Allowed, nil) 496 + // TODO: output is nicer, but need either parenthesis or 497 + // removed spaces around more tightly bound expressions. 498 + // allowed = pExpr(x, pcs.Allowed) 499 + } 500 + } 501 + 502 + t.Equal(patterns, tc.patterns) 503 + t.Equal(allowed, tc.allowed) 504 + }) 505 + } 506 + 507 + const ( 508 + initialIndent = 3 509 + indentString = "\t" 510 + ) 511 + 512 + func writeArcs(x adt.Runtime, v *adt.Vertex) string { 513 + b := &strings.Builder{} 514 + for _, a := range v.Arcs { 515 + if len(v.Arcs) > 1 { 516 + fmt.Fprint(b, "\n", strings.Repeat(indentString, initialIndent)) 517 + } 518 + fmt.Fprintf(b, "%s: ", a.Label.RawString(x)) 519 + vertexString(x, b, a, initialIndent) 520 + } 521 + return b.String() 522 + } 523 + 524 + func writePatterns(x adt.Runtime, pairs []adt.PatternConstraint) string { 525 + b := &strings.Builder{} 526 + for _, pc := range pairs { 527 + if len(pairs) > 1 { 528 + fmt.Fprint(b, "\n", strings.Repeat(indentString, initialIndent)) 529 + } 530 + b.WriteString(pExpr(x, pc.Pattern)) 531 + b.WriteString(": ") 532 + vertexString(x, b, pc.Constraint, initialIndent) 533 + } 534 + return b.String() 535 + } 536 + 537 + func vertexString(x adt.Runtime, b *strings.Builder, v *adt.Vertex, indent int) { 538 + hasVertex := false 539 + for _, c := range v.Conjuncts { 540 + if _, ok := c.Expr().(*adt.Vertex); ok { 541 + hasVertex = true 542 + } 543 + } 544 + 545 + b.WriteString("{") 546 + for i, c := range v.Conjuncts { 547 + if g, ok := c.Expr().(*adt.Vertex); ok { 548 + doIndent(b, indent+1) 549 + b.WriteString("[") 550 + if c.CloseInfo.FromEmbed { 551 + b.WriteString("e") 552 + } 553 + if c.CloseInfo.FromDef { 554 + b.WriteString("d") 555 + } 556 + b.WriteString("]") 557 + vertexString(x, b, g, indent+1) 558 + } else { 559 + if hasVertex { 560 + doIndent(b, indent+1) 561 + } else if i > 0 { 562 + b.WriteString(" ") 563 + } 564 + b.WriteString(pExpr(x, c.Expr())) 565 + } 566 + } 567 + if hasVertex { 568 + doIndent(b, indent) 569 + } 570 + b.WriteString("}") 571 + } 572 + 573 + func doIndent(b *strings.Builder, indent int) { 574 + fmt.Fprint(b, "\n", strings.Repeat(indentString, indent)) 575 + } 576 + 577 + func pExpr(x adt.Runtime, e adt.Expr) string { 578 + a, _ := export.All.Expr(x, "test", e) 579 + b, _ := format.Node(a) 580 + return string(b) 581 + }
+5 -3
internal/core/export/adt.go
··· 328 328 // cannot be properly resolved, throwing off the sanitize. Also, 329 329 // comprehensions originate from a single source and do not need to be 330 330 // handled. 331 - if v := env.Vertex; !v.IsDynamic { 332 - if v = v.Lookup(x.Label); v != nil { 333 - e.linkIdentifier(v, ident) 331 + if env != nil { // for generated stuff 332 + if v := env.Vertex; !v.IsDynamic { 333 + if v = v.Lookup(x.Label); v != nil { 334 + e.linkIdentifier(v, ident) 335 + } 334 336 } 335 337 } 336 338