cuelang.org/go@v0.13.0/internal/core/adt/typocheck.go (about)

     1  // Copyright 2025 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  //     https://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 CUE's algorithm to detect misspelled field names.
    18  //
    19  // ## Outline
    20  //
    21  // Typo checking MAY be enabled whenever a node is unified with a definition or
    22  // a struct returned from the close builtin. Each distinct unification of such a
    23  // struct triggers a separate "typo check", which is associated with a unique
    24  // identifier. This identifier is passed along all values that result from
    25  // unification with this struct.
    26  //
    27  // Once the processing of a node is complete, it is checked that for all its
    28  // fields there is supportive evidence that the field is allowed for all structs
    29  // for which typo checking is enabled.
    30  //
    31  // ## Selecting which Structs to Typo Check
    32  //
    33  // The algorithm is quite general and allows many types of heuristics to be
    34  // applied. The initial goal is to mimic V2 semantics as closely as possible to
    35  // facilitate migration to V3.
    36  //
    37  // ### Embeddings
    38  //
    39  // Embeddings provide evidence allowing fields for their enclosing scopes. Even
    40  // when an embedding references a definition, it will not start its own typo
    41  // check.
    42  //
    43  // ### Definitions
    44  //
    45  // By default, all references to non-embedded definitions are typo checked. The
    46  // following situations qualify.
    47  //
    48  //      #A: b: {a: int}
    49  //      a: #A
    50  //
    51  //      // Trigger typo check
    52  //      r1: #A
    53  //      r2: a
    54  //
    55  //      // Do NOT trigger typo check
    56  //      r3: a.b
    57  //
    58  // In the case of r3, no typo check is triggered as the inserted value does not
    59  // reference a definition directly. This choice is somewhat arbitrary. The main
    60  // reason to pick this semantics is to be compatible with V2.
    61  //
    62  // ### Inline structs
    63  //
    64  // Like in V2, inline structs are generally not typo checked. We can add this
    65  // later if necessary.
    66  //
    67  // ## Tracking Evidence
    68  //
    69  // The basic principle of this algorithm is that each (dynamic) reference is
    70  // associated with a unique identifier. When a node is unified with a definition
    71  // all its descendant nodes are tagged with this number if any of the conjuncts
    72  // of this definition end up being unified with such nodes. Once a node finished
    73  // processing, it is checked, recursively, that all its fields adhere to this
    74  // schema by checking that there is supportive evidence that this schema allows
    75  // such fields.
    76  //
    77  // Consider, for instance, the following CUE. Here, the reference to #Schema
    78  // will be assigned the unique identifier 7.
    79  //
    80  // 	foo: #Schema & {
    81  // 		a: 1  // marked with 7 as #Schema has a field `a`.
    82  // 		b: 1  // marked with 7 as the pattern of #Schema allows `b`.
    83  // 		c: 1  // not marked with 7 as #Schema does not have a field `c`.
    84  // 	}
    85  //
    86  // 	#Schema: {
    87  // 		a?:      int
    88  // 		[<="b"]: int
    89  // 	}
    90  //
    91  // Details of this algorithm are included below.
    92  //
    93  // ### Evidence sets and multiple insertions
    94  //
    95  // The same struct may be referred to within a node multiple times. If it is not
    96  // itself a typo-checked definition, it may provide evidence for multiple other
    97  // typo-checked definitions. To avoid having to process the conjuncts of such
    98  // structs multiple times, we will still assign a unique identifier to such
    99  // structs, and will annotate each typo-checked definition to which this applies
   100  // that it is satisfied by this struct.
   101  //
   102  // In other words, each typo-checked struct may be associated with multiple
   103  // unique identifiers that provide evidence for an allowed field. See the
   104  // section below for a more detailed example.
   105  //
   106  // ### Embeddings
   107  //
   108  // Embeddings are a special case of structs that are not typo checked, but may
   109  // provide evidence for other typo-checked definitions. As a struct associated
   110  // with an embedding may be referred to multiple times, we will also assign a
   111  // unique identifier to embeddings. This identifier is then also added to the
   112  // evidence set of the typo-checked definition.
   113  //
   114  // Note that if a definition is embedded within a struct, that struct is
   115  // considered closed after unifying all embeddings. We need to track this
   116  // separately. This is discussed in the code below where appropriate. Ideally,
   117  // we would get rid of this behavior by only allowing "open" or "closed"
   118  // unifications, without considering any of the other context.
   119  //
   120  // Consider the following example:
   121  //
   122  // 	a: #A & { #A, b: int, c: int }
   123  //  #A: #B
   124  //  #B: { b: int }
   125  //
   126  // In this case, for `a`, we need find evidence that all fields of `#A` are
   127  // allowed. If `a` were to be referenced by another field, we would also
   128  // need to check that the struct with the embedded definition, which is
   129  // closed after unification, is valid.
   130  //
   131  // So, for `a` we track two requirements, say `1`, which corresponds to the
   132  // reference to `#A` and `2`, which corresponds to the literal struct.
   133  // During evaluation, we additionally assign `3` to `#B`.
   134  //
   135  // The algorithm now proceeds as follows. We start with the requirement sets
   136  // `{1}` and `{2}`. For the first, while evaluating definition `#A`, we find
   137  // that this redirects to `#B`. In this case, as `#B` is at least as strict
   138  // as `#A`, we can rewrite the set as `{3}`. This means we need to find
   139  // evidence that each field of `a` is marked with a `3`.
   140  // For the second case, `{2}`, we find that `#A` is embedded. As this is not
   141  // a further restriction, we add the ID for `#A` to the set, resulting in
   142  // `{2, 1}`. Subsequently, `#A` maps to `#B` again, but in this case, as `#A`
   143  // is embedded, it is also additive, resulting in `{2, 1, 3}`, where each field
   144  // in `a` needs to be marked with at least one of these values.
   145  //
   146  // After `a` is fully unified, field `b` will be marked with `3` and thus has
   147  // evidence for both requirements. Field `c`, however, is marked with `2`, which
   148  // is only supports the second requirement, and thus will result in a typo
   149  // error.
   150  //
   151  // NOTE 1: that the second set is strictly more permissive then the first
   152  // requirement and could be elided.
   153  // NOTE 2: a reference to the same node within one field is only assigned a
   154  // single ID and only needs to be processed once per node.
   155  //
   156  // ### Pruning of sub nodes
   157  //
   158  // For definitions, typo checking proceeds recursively. However, typo checking
   159  // is disabled for certain fields, even when a node still has subfields. Typo
   160  // checking is disabled if a field has:
   161  //
   162  // - a "naked" top `_` (so not, for instance `{_}`), - an ellipsis `...`, or -
   163  // it has a "non-concrete" validator, like matchn.
   164  //
   165  // In the latter case, the builtin takes over the responsibility of checking the
   166  // type.
   167  //
   168  import (
   169  	"math"
   170  	"slices"
   171  
   172  	"cuelang.org/go/cue/ast"
   173  )
   174  
   175  type defID uint32
   176  
   177  const deleteID defID = math.MaxUint32
   178  
   179  func (c *OpContext) getNextDefID() defID {
   180  	c.stats.NumCloseIDs++
   181  	c.nextDefID++
   182  	return c.nextDefID
   183  }
   184  
   185  type refInfo struct {
   186  	v  *Vertex
   187  	id defID
   188  
   189  	// exclude defines a subtree of CloseInfo.def that should not be
   190  	// transitively added to the set of allowed fields.
   191  	//
   192  	// It would probably be more efficient to track a separate allow and deny
   193  	// list, rather than tracking the entire tree except for the excluded.
   194  	exclude defID
   195  
   196  	// ignore defines whether we should not do typo checking for this defID.
   197  	ignore bool
   198  
   199  	// isOuterStruct indicates that this struct is marked as "outerID" in a
   200  	// CloseInfo. The debug visualization in ./debug.go uses this to show a
   201  	// mark ("S") next to the defID to visualize that this fact.
   202  	isOuterStruct bool
   203  }
   204  
   205  type conjunctFlags uint8
   206  
   207  const (
   208  	cHasEllipsis conjunctFlags = 1 << iota
   209  	cHasTop
   210  	cHasStruct
   211  	cHasOpenValidator
   212  )
   213  
   214  type conjunctInfo struct {
   215  	id    defID
   216  	kind  Kind
   217  	flags conjunctFlags
   218  }
   219  
   220  func (c conjunctFlags) hasTop() bool {
   221  	return c&(cHasTop) != 0
   222  }
   223  
   224  func (c conjunctFlags) hasStruct() bool {
   225  	return c&(cHasStruct) != 0
   226  }
   227  
   228  func (c conjunctFlags) forceOpen() bool {
   229  	return c&(cHasEllipsis|cHasOpenValidator) != 0
   230  }
   231  
   232  type replaceID struct {
   233  	from defID
   234  	to   defID
   235  	add  bool // If true, add to the set. If false, replace from with to.
   236  }
   237  
   238  func (n *nodeContext) addReplacement(x replaceID) {
   239  	if x.from == x.to {
   240  		return
   241  	}
   242  	// TODO: we currently may compute n.reqSets too early in some rare
   243  	// circumstances. We clear the set if it needs to be recomputed.
   244  	n.computedCloseInfo = false
   245  	n.reqSets = n.reqSets[:0]
   246  
   247  	n.replaceIDs = append(n.replaceIDs, x)
   248  }
   249  
   250  func (n *nodeContext) updateConjunctInfo(k Kind, id CloseInfo, flags conjunctFlags) {
   251  	if n.ctx.OpenDef {
   252  		return
   253  	}
   254  
   255  	for i, c := range n.conjunctInfo {
   256  		if c.id == id.defID {
   257  			n.conjunctInfo[i].kind &= k
   258  			n.conjunctInfo[i].flags |= flags
   259  			return
   260  		}
   261  	}
   262  	n.conjunctInfo = append(n.conjunctInfo, conjunctInfo{
   263  		id:    id.defID,
   264  		kind:  k,
   265  		flags: flags,
   266  	})
   267  }
   268  
   269  // addResolver adds a resolver to typo checking. Both definitions and
   270  // non-definitions should typically be added: non-definitions may be added
   271  // multiple times to a single node. As we only want to insert each conjunct
   272  // once, we need to ensure that within all contexts a single ID assigned to such
   273  // a resolver is tracked.
   274  func (n *nodeContext) addResolver(v *Vertex, id CloseInfo, forceIgnore bool) CloseInfo {
   275  	if n.ctx.OpenDef {
   276  		return id
   277  	}
   278  
   279  	isClosed := id.FromDef || v.ClosedNonRecursive
   280  
   281  	if isClosed && !forceIgnore {
   282  		for i, x := range n.reqDefIDs {
   283  			if x.id == id.outerID && id.outerID != 0 {
   284  				n.reqDefIDs[i].ignore = false
   285  				break
   286  			}
   287  		}
   288  	}
   289  
   290  	var ignore bool
   291  	switch {
   292  	case forceIgnore:
   293  		// Special mode to always ignore the outer enclosing group.
   294  		// This is the case, for instance, if a resolver resolves to a
   295  		// non-definition.
   296  		ignore = true
   297  		// TODO: Consider resetting FromDef.
   298  		// id.FromDef = false
   299  	case id.enclosingEmbed != 0 || id.outerID == 0:
   300  		// We have a reference within an inner embedding group. If this is
   301  		// a definition, or otherwise typo checked struct, we need to track
   302  		// the embedding for mutual compatibility.
   303  		// is a definition:
   304  		// 		a: {
   305  		// 			// Even though #A and #B do not constraint `a` individually,
   306  		// 			// they need to be checked for mutual consistency within
   307  		// 			// the embedding.
   308  		// 			#A & #B
   309  		// 		}
   310  		ignore = !isClosed
   311  	default:
   312  		// In the default case we can disable typo checking this type if it is
   313  		// an embedding.
   314  		ignore = id.FromEmbed
   315  	}
   316  
   317  	srcID := id.defID
   318  	dstID := defID(0)
   319  	for _, x := range n.reqDefIDs {
   320  		if x.v == v {
   321  			dstID = x.id
   322  			break
   323  		}
   324  	}
   325  
   326  	if dstID == 0 || id.enclosingEmbed != 0 {
   327  		next := n.ctx.getNextDefID()
   328  		if dstID != 0 {
   329  			// If we need to activate an enclosing embed group, and the added
   330  			// resolver was already before, we need to allocate a new ID and
   331  			// add the original ID to the set of the new one.
   332  			n.addReplacement(replaceID{from: next, to: dstID, add: true})
   333  		}
   334  		dstID = next
   335  
   336  		n.reqDefIDs = append(n.reqDefIDs, refInfo{
   337  			v:       v,
   338  			id:      dstID,
   339  			exclude: id.enclosingEmbed,
   340  			ignore:  ignore,
   341  		})
   342  	}
   343  	id.defID = dstID
   344  
   345  	// TODO: consider using add: !isClosed
   346  	n.addReplacement(replaceID{from: srcID, to: dstID, add: true})
   347  	if id.enclosingEmbed != 0 && !ignore {
   348  		ph := id.outerID
   349  		n.addReplacement(replaceID{from: dstID, to: ph, add: true})
   350  		_, dstID = n.newGroup(id, false)
   351  		id.enclosingEmbed = dstID
   352  	}
   353  
   354  	return id
   355  }
   356  
   357  // subField updates a CloseInfo for subfields of a struct.
   358  func (c *OpContext) subField(ci CloseInfo) CloseInfo {
   359  	ci.outerID = 0
   360  	ci.enclosingEmbed = 0
   361  	return ci
   362  }
   363  
   364  func (n *nodeContext) newGroup(id CloseInfo, placeholder bool) (CloseInfo, defID) {
   365  	srcID := id.defID
   366  	dstID := n.ctx.getNextDefID()
   367  	// TODO: consider only adding when record || OpenGraph
   368  	n.reqDefIDs = append(n.reqDefIDs, refInfo{
   369  		v:             emptyNode,
   370  		id:            dstID,
   371  		ignore:        true,
   372  		isOuterStruct: placeholder,
   373  	})
   374  	id.defID = dstID
   375  	n.addReplacement(replaceID{from: srcID, to: dstID, add: true})
   376  	return id, dstID
   377  }
   378  
   379  // AddOpenConjunct adds w as a conjunct of v and disables typo checking for w,
   380  // even if it is a definition.
   381  func (v *Vertex) AddOpenConjunct(ctx *OpContext, w *Vertex) {
   382  	n := v.getBareState(ctx)
   383  	ci := n.injectEmbedNode(w, CloseInfo{})
   384  	c := MakeConjunct(nil, w, ci)
   385  	v.AddConjunct(c)
   386  }
   387  
   388  // injectEmbedNode is used to track typo checking within an embedding.
   389  // Consider, for instance:
   390  //
   391  //	#A: {a: int}
   392  //	#B: {b: int}
   393  //	#C: {
   394  //		#A & #B // fails
   395  //		c: int
   396  //	}
   397  //
   398  // In this case, even though #A and #B are both embedded, they are intended
   399  // to be mutually exclusive. We track this by introducing a separate defID
   400  // for the embedding. Suppose that the embedding #A&#B is assigned defID 2,
   401  // where its parent is defID 1. Then #A is assigned 3 and #B is assigned 4.
   402  //
   403  // We can then say that requirement 3 (node A) holds if all fields contain
   404  // either label 3, or any field within 1 that is not 2.
   405  func (n *nodeContext) injectEmbedNode(x Decl, id CloseInfo) CloseInfo {
   406  	id.FromEmbed = true
   407  
   408  	// Filter cases where we do not need to track the definition.
   409  	switch x := x.(type) {
   410  	case *DisjunctionExpr, *Disjunction, *Comprehension:
   411  		return id
   412  	case *BinaryExpr:
   413  		if x.Op != AndOp {
   414  			return id
   415  		}
   416  	}
   417  
   418  	id, dstID := n.newGroup(id, false)
   419  	id.enclosingEmbed = dstID
   420  
   421  	return id
   422  }
   423  
   424  // splitStruct is used to mark the outer struct of a field in which embeddings
   425  // occur. The significance is that a reference to this node expects a node
   426  // to be closed, even if it only has embeddings. Consider for instance:
   427  //
   428  //	// A is closed  and allows the fields of #B plus c.
   429  //	A: { { #B }, c: int }
   430  //
   431  // TODO(flatclose): this is a temporary solution to handle the case where a
   432  // definition is embedded within a struct. It can be removed if we implement
   433  // the #A vs #A... semantics.
   434  func (n *nodeContext) splitStruct(s *StructLit, id CloseInfo) CloseInfo {
   435  	if n.ctx.OpenDef {
   436  		return id
   437  	}
   438  
   439  	if _, ok := s.Src.(*ast.File); ok {
   440  		// If this is not a file, the struct indicates the scope/
   441  		// boundary at which closedness should apply. This is not true
   442  		// for files.
   443  		// We should also not spawn if this is a nested Comprehension,
   444  		// where the spawn is already done as it may lead to spurious
   445  		// field not allowed errors. We can detect this with a nil s.Src.
   446  		// TODO(evalv3): use a more principled detection mechanism.
   447  		// TODO: set this as a flag in StructLit so as to not have to
   448  		// do the somewhat dangerous cast here.
   449  		return id
   450  	}
   451  
   452  	return n.splitScope(id)
   453  }
   454  
   455  func (n *nodeContext) splitScope(id CloseInfo) CloseInfo {
   456  	id, dstID := n.newGroup(id, true)
   457  
   458  	if id.outerID == 0 {
   459  		id.outerID = dstID
   460  	}
   461  
   462  	return id
   463  }
   464  
   465  func (n *nodeContext) checkTypos() {
   466  	ctx := n.ctx
   467  	if ctx.OpenDef {
   468  		return
   469  	}
   470  	noDeref := n.node // noDeref retained for debugging purposes.
   471  	v := noDeref.DerefValue()
   472  
   473  	// Stop early, avoiding the work in appendRequired below, if we have no arcs to check.
   474  	if len(v.Arcs) == 0 {
   475  		return
   476  	}
   477  
   478  	// Avoid unnecessary errors.
   479  	if b, ok := v.BaseValue.(*Bottom); ok && !b.CloseCheck {
   480  		return
   481  	}
   482  
   483  	required := getReqSets(n)
   484  	if len(required) == 0 {
   485  		return
   486  	}
   487  
   488  	// TODO(perf): reuse buffers via OpContext
   489  	requiredCopy := make(reqSets, 0, len(required))
   490  	var replacements []replaceID
   491  
   492  	var err *Bottom
   493  	// outer:
   494  	for _, a := range v.Arcs {
   495  		f := a.Label
   496  		if a.IsFromDisjunction() {
   497  			continue // Already checked in disjuncts.
   498  		}
   499  
   500  		// TODO(mem): child states of uncompleted nodes must have a state.
   501  		na := a.state
   502  
   503  		replacements = na.getReplacements(replacements[:0])
   504  		required := append(requiredCopy[:0], required...)
   505  		// do the right thing in appendRequired either way.
   506  		required.replaceIDs(n.ctx, replacements...)
   507  
   508  		a = a.DerefDisjunct()
   509  		// TODO(perf): somehow prevent error generation of recursive structures,
   510  		// or at least make it cheap. Right now if this field is a typo, likely
   511  		// all descendents will be regarded as typos.
   512  		if b, ok := a.BaseValue.(*Bottom); ok {
   513  			if !b.CloseCheck {
   514  				continue
   515  			}
   516  		}
   517  
   518  		if allowedInClosed(f) {
   519  			continue
   520  		}
   521  
   522  		if n.hasEvidenceForAll(required, na.conjunctInfo) {
   523  			continue
   524  		}
   525  
   526  		// TODO: do not descend on optional?
   527  
   528  		// openDebugGraph(ctx, a, "NOT ALLOWED") // Uncomment for debugging.
   529  		if b := ctx.notAllowedError(a); b != nil && a.ArcType <= ArcRequired {
   530  			err = CombineErrors(nil, err, b)
   531  		}
   532  	}
   533  
   534  	if err != nil {
   535  		n.AddChildError(err) // TODO: should not be necessary.
   536  	}
   537  }
   538  
   539  // hasEvidenceForAll reports whether there is evidence in a set of
   540  // conjuncts for each of the typo-checked structs represented by
   541  // the reqSets.
   542  func (n *nodeContext) hasEvidenceForAll(a reqSets, conjuncts []conjunctInfo) bool {
   543  	for i := uint32(0); int(i) < len(a); i += a[i].size {
   544  		if a[i].size == 0 {
   545  			panic("unexpected set length")
   546  		}
   547  		if !hasEvidenceForOne(a[i:i+a[i].size], conjuncts) {
   548  			return false
   549  		}
   550  	}
   551  	return true
   552  }
   553  
   554  // hasEvidenceForOne reports whether a single typo-checked set has evidence for
   555  // any of its defIDs.
   556  func hasEvidenceForOne(a reqSets, conjuncts []conjunctInfo) bool {
   557  	for _, c := range conjuncts {
   558  		for _, ri := range a {
   559  			if c.id == ri.id {
   560  				return true
   561  			}
   562  		}
   563  	}
   564  	return false
   565  }
   566  
   567  // reqSets defines a set of sets of defIDs that can each satisfy a required id.
   568  //
   569  // A reqSet holds a sequence of a defID "representative", or "head" for a
   570  // requirement, followed by all defIDs that satisfy this requirement. For head
   571  // elements, size indicates the number of entries in the set, including the
   572  // head. For non-head elements, size is 0.
   573  type reqSets []reqSet
   574  
   575  // A single reqID might be satisfied by multiple defIDs, if the definition
   576  // associated with the reqID embeds other definitions, for instance. In this
   577  // case we keep a list of defIDs that may also be satisfied.
   578  //
   579  // This type is used in [nodeContext.equivalences] as follows:
   580  //   - if an embedding is used by a definition, it is inserted in the list
   581  //     pointed to by its refInfo. Similarly,
   582  //     refInfo is added to the list
   583  type reqSet struct {
   584  	id defID
   585  	// size is the number of elements in the set. This is only set for the head.
   586  	// Entries with equivalence IDs have size set to 0.
   587  	size uint32
   588  	del  defID // TODO(flatclose): can be removed later.
   589  	once bool
   590  }
   591  
   592  // assert checks the invariants of a reqSets. It can be used for debugging.
   593  func (a reqSets) assert() {
   594  	for i := 0; i < len(a); {
   595  		e := a[i]
   596  		if e.size == 0 {
   597  			panic("head element with 0 size")
   598  		}
   599  		if i+int(e.size) > len(a) {
   600  			panic("set extends beyond end of slice")
   601  		}
   602  		for j := 1; j < int(e.size); j++ {
   603  			if a[i+j].size != 0 {
   604  				panic("non-head element with non-zero size")
   605  			}
   606  		}
   607  
   608  		i += int(e.size)
   609  	}
   610  }
   611  
   612  // replaceIDs replaces defIDs mappings in the receiving reqSets in place.
   613  //
   614  // The following rules apply:
   615  //
   616  //   - Mapping a typo-checked definition to a new typo-checked definition replaces
   617  //     the set from the "from" definition in its entirety. It is assumed that the
   618  //     set is already added to the requirements.
   619  //
   620  //   - A mapping from a typo-checked definition to 0 removes the set from the
   621  //     requirements. This typically comes from _ or ....
   622  //
   623  //   - A mapping from an equivalence (non-head) value to 0 removes the
   624  //     equivalence. This does not change the outcome of typo checking, but
   625  //     reduces the size of the equivalence list, which helps performance.
   626  //
   627  //   - A mapping from an equivalence (non-head) value to a new value widens the
   628  //     allowed values for the respective set by adding the new value to the
   629  //     equivalence list.
   630  //
   631  // In words;
   632  // - Definition: if not in embed, create new group
   633  // - If in active definition, replace old definition
   634  // - If in embed, replace embed in respective sets. definition starts new group
   635  // - child definition replaces parent definition
   636  func (a *reqSets) replaceIDs(ctx *OpContext, b ...replaceID) {
   637  	temp := *a
   638  	temp = temp[:0]
   639  	buf := ctx.reqSetsBuf[:0]
   640  outer:
   641  	for i := 0; i < len(*a); {
   642  		e := (*a)[i]
   643  		if e.size != 0 {
   644  			// If the head is dropped, the entire group is deleted.
   645  			for _, x := range b {
   646  				if e.id == x.from && !x.add {
   647  					i += int(e.size)
   648  					continue outer
   649  				}
   650  			}
   651  			if len(buf) > 0 {
   652  				buf[0].size = uint32(len(buf))
   653  				if len(temp)+len(buf) > i {
   654  					*a = slices.Replace(*a, len(temp), i, buf...)
   655  					i = len(temp) + len(buf)
   656  					temp = (*a)[:i]
   657  				} else {
   658  					temp = append(temp, buf...)
   659  				}
   660  				buf = buf[:0]
   661  			}
   662  		}
   663  
   664  		buf = transitiveMapping(ctx, buf, e, b)
   665  
   666  		i++
   667  	}
   668  	if len(buf) > 0 {
   669  		buf[0].size = uint32(len(buf))
   670  		temp = append(temp, buf...)
   671  	}
   672  	ctx.reqSetsBuf = buf[:0] // to be reused later on
   673  	*a = temp
   674  }
   675  
   676  // TODO: this is a polynomial algorithm. At some point, if this turns out to be
   677  // a bottleneck, we should consider filtering unnecessary entries and/or using a
   678  // more efficient algorithm.
   679  func transitiveMapping(ctx *OpContext, buf reqSets, x reqSet, b []replaceID) reqSets {
   680  	// Trim subtree for embedded conjunctions.
   681  	if len(buf) > 0 && buf[0].del == x.id {
   682  		return buf
   683  	}
   684  
   685  	// do not add duplicates
   686  	for _, y := range buf {
   687  		if x.id == y.id {
   688  			return buf
   689  		}
   690  	}
   691  
   692  	buf = append(buf, x)
   693  	ctx.stats.CloseIDElems++
   694  
   695  	for _, y := range b {
   696  		if x.id == y.from {
   697  			if y.to == deleteID { // do we need this?
   698  				buf = buf[:len(buf)-1]
   699  				return buf
   700  			}
   701  			buf = transitiveMapping(ctx, buf, reqSet{
   702  				id:   y.to,
   703  				once: x.once,
   704  			}, b)
   705  		}
   706  	}
   707  	return buf
   708  }
   709  
   710  // mergeCloseInfo merges the conjunctInfo of nw that is missing from nv into nv.
   711  //
   712  // This is used to merge conjunctions from a disjunct to the Vertex that
   713  // originated the disjunct.
   714  // TODO: consider whether we can do without. We usually aim to not check
   715  // such nodes, but sometimes we do.
   716  func mergeCloseInfo(nv, nw *nodeContext) {
   717  	v := nv.node
   718  	w := nw.node
   719  	if w == nil {
   720  		return
   721  	}
   722  	// Merge missing closeInfos
   723  outer:
   724  	for _, wci := range nw.conjunctInfo {
   725  		for _, vci := range nv.conjunctInfo {
   726  			if wci.id == vci.id {
   727  				continue outer
   728  			}
   729  		}
   730  		nv.conjunctInfo = append(nv.conjunctInfo, wci)
   731  	}
   732  
   733  outer2:
   734  	for _, d := range nw.replaceIDs {
   735  		for _, vd := range nv.replaceIDs {
   736  			if d == vd {
   737  				continue outer2
   738  			}
   739  		}
   740  		nv.replaceIDs = append(nv.replaceIDs, d)
   741  	}
   742  
   743  	for _, wa := range w.Arcs {
   744  		for _, va := range v.Arcs {
   745  			if va.Label == wa.Label {
   746  				mergeCloseInfo(va.state, wa.state)
   747  				break
   748  			}
   749  		}
   750  	}
   751  }
   752  
   753  func (n *nodeContext) getReplacements(a []replaceID) []replaceID {
   754  	for p := n.node; p != nil && p.state != nil; p = p.Parent {
   755  		a = append(a, p.state.replaceIDs...)
   756  	}
   757  	return a
   758  }
   759  
   760  // getReqSets initializes, if necessary, and returns the reqSets for n.
   761  func getReqSets(n *nodeContext) reqSets {
   762  	if n == nil {
   763  		return nil
   764  	}
   765  
   766  	if n.computedCloseInfo {
   767  		return n.reqSets
   768  	}
   769  	n.reqSets = n.reqSets[:0]
   770  	n.computedCloseInfo = true
   771  
   772  	a := n.reqSets
   773  	v := n.node
   774  
   775  	if p := v.Parent; p != nil {
   776  		aReq := getReqSets(p.state)
   777  		if !n.dropParentRequirements {
   778  			a = append(a, aReq...)
   779  		}
   780  	}
   781  	a.filterNonRecursive()
   782  
   783  outer:
   784  	for _, y := range n.reqDefIDs {
   785  		if y.ignore {
   786  			continue
   787  		}
   788  		for _, x := range a {
   789  			if x.id == y.id {
   790  				continue outer
   791  			}
   792  		}
   793  		once := false
   794  		if y.v != nil {
   795  			once = !y.v.ClosedRecursive
   796  		}
   797  
   798  		a = append(a, reqSet{
   799  			id:   y.id,
   800  			once: once,
   801  			del:  y.exclude,
   802  			size: 1,
   803  		})
   804  	}
   805  
   806  	a.replaceIDs(n.ctx, n.replaceIDs...)
   807  
   808  	// If 'v' is a hidden field, then all reqSets in 'a' for which there is no
   809  	// corresponding entry in conjunctInfo should be removed from 'a'.
   810  	if allowedInClosed(v.Label) {
   811  		a.filterSets(func(a []reqSet) bool {
   812  			for _, e := range a {
   813  				for _, c := range n.conjunctInfo {
   814  					if c.id == e.id {
   815  						return true // keep the set
   816  					}
   817  				}
   818  			}
   819  			return false // discard the set
   820  		})
   821  	}
   822  
   823  	a.filterTop(n.conjunctInfo)
   824  
   825  	n.reqSets = a
   826  	return a
   827  }
   828  
   829  // If there is a top or ellipsis for all supported conjuncts, we have
   830  // evidence that this node can be dropped.
   831  func (a *reqSets) filterTop(conjuncts []conjunctInfo) {
   832  	a.filterSets(func(a []reqSet) bool {
   833  		var f conjunctFlags
   834  		for _, e := range a {
   835  			for _, c := range conjuncts {
   836  				if e.id != c.id {
   837  					continue
   838  				}
   839  				flags := c.flags
   840  				if c.id < a[0].id {
   841  					flags &^= cHasStruct
   842  				}
   843  				f |= flags
   844  			}
   845  		}
   846  		if (f.hasTop() && !f.hasStruct()) || f.forceOpen() {
   847  			return false
   848  		}
   849  		return true
   850  	})
   851  }
   852  
   853  func (a *reqSets) filterNonRecursive() {
   854  	a.filterSets(func(e []reqSet) bool {
   855  		x := e[0]
   856  		if x.once { //  || x.id == 0
   857  			return false // discard the entry
   858  		}
   859  		return true // keep the entry
   860  	})
   861  }
   862  
   863  // filter keeps all reqSets e in a for which f(e) and removes the rest.
   864  func (a *reqSets) filterSets(f func(e []reqSet) bool) {
   865  	temp := (*a)[:0]
   866  	for i := 0; i < len(*a); {
   867  		e := (*a)[i]
   868  		set := (*a)[i : i+int(e.size)]
   869  
   870  		if f(set) {
   871  			temp = append(temp, set...)
   872  		}
   873  
   874  		i += int(e.size)
   875  	}
   876  	*a = temp
   877  }