File: overlay.go

package info (click to toggle)
golang-github-cue-lang-cue 0.12.0.-1
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 19,072 kB
  • sloc: sh: 57; makefile: 17
file content (590 lines) | stat: -rw-r--r-- 17,764 bytes parent folder | download
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
// Copyright 2024 CUE Authors
//
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
//     http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.

package adt

import "slices"

// This file implements a Vertex overlay. This is used by the disjunction
// algorithm to fork an existing Vertex value without modifying the original.
//
// At the moment, the forked value is a complete copy of the original.
// The copy points to the original to keep track of pointer equivalence.
// Conversely, while a copy is evaluated, the value of which it is a copy
// references the copy. Dereferencing will then take care that the copy is used
// during evaluation.
//
//   nodeContext (main)  <-
//   - deref               \
//     |                    \
//     |  nodeContext (d1)  | <-
//     \  - overlays -------/   \
//      \                        \
//       ->   nodeContext (d2)    |
//            - overlays --------/
//
// TODO: implement dereferencing
// TODO(perf): implement copy on write: instead of copying the entire tree, we
// could get by with only copying arcs to that are modified in the copy.

var nextGeneration int

func newOverlayContext(ctx *OpContext) *overlayContext {
	nextGeneration++
	return &overlayContext{ctx: ctx, generation: nextGeneration}
}

// An overlayContext keeps track of copied vertices, closeContexts, and tasks.
// This allows different passes to know which of each were created, without
// having to walk the entire tree.
type overlayContext struct {
	ctx *OpContext

	// generation is used to identify the current overlayContext. All
	// closeContexts created by this overlayContext will have this generation.
	// Whenever a counter of a closedContext is changed, this may only cause
	// a cascade of changes if the generation is the same.
	generation int

	// closeContexts holds the allocated closeContexts created by allocCC.
	//
	// In the first pass, closeContexts are copied using allocCC. This also
	// walks the parent tree, and allocates copies for ConjunctGroups.
	//
	// In the second pass, initCloneCC can be finalized by initializing each
	// closeContext in this slice.
	//
	// Note that after the copy is completed, the overlay pointer should be
	// deleted.
	closeContexts []*closeContext

	// vertices holds the original, non-overlay vertices. The overlay for a
	// vertex v can be obtained by looking up v.cc.overlay.src.
	vertices []*Vertex
}

// cloneRoot clones the Vertex in which disjunctions are defined to allow
// inserting selected disjuncts into a new Vertex.
func (ctx *overlayContext) cloneRoot(root *nodeContext) *nodeContext {
	// Clone all vertices that need to be cloned to support the overlay.
	v := ctx.cloneVertex(root.node)
	v.IsDisjunct = true

	// At this point we have copied all the mandatory closeContexts. There
	// may be derivative closeContexts copied as well.

	// TODO: patch notifications to any node that is within the disjunct to
	// point to the new vertex instead.

	// Initialize closeContexts: at this point, all closeContexts that need to
	// be cloned have been allocated and stored in closeContexts and can now be
	// initialized.
	// Use an explicit index as initCloneCC uses allocCC, which MAY allocate a
	// new closeContext. It probably does not, but we use an index in case.
	for i := 0; i < len(ctx.closeContexts); i++ {
		cc := ctx.closeContexts[i]
		ctx.initCloneCC(cc)
	}

	for _, cc := range ctx.closeContexts {
		ctx.finishDependencies(cc)
	}

	// TODO: walk overlay vertices and decrement counters of non-disjunction
	// running tasks?
	// TODO: find a faster way to do this. Walking over vertices would
	// probably be faster.
	for _, cc := range ctx.closeContexts {
		for _, d := range cc.dependencies {
			if d.task == nil {
				// The test case that makes this necessary:
				// #A: ["a" | "b"] | {}
				// #A: ["a" | "b"] | {}
				// b:  #A & ["b"]
				//
				// TODO: invalidate task instead?
				continue
			}
			if d.kind == TASK && d.task.state == taskRUNNING && !d.task.defunct {
				cc.overlay.decDependent(ctx.ctx, TASK, nil)
			}
		}
	}

	return v.state
}

// unlinkOverlay unlinks helper pointers. This should be done after the
// evaluation of a disjunct is complete. Keeping the linked pointers around
// will allow for dereferencing a vertex to its overlay, which, in turn,
// allows a disjunct to refer to parents vertices of the disjunct that
// recurse into the disjunct.
//
// TODO(perf): consider using generation counters.
func (ctx *overlayContext) unlinkOverlay() {
	for _, cc := range ctx.closeContexts {
		cc.overlay = nil
	}
}

// cloneVertex copies the contents of x into a new Vertex.
//
// It copies all Arcs, Conjuncts, and Structs, recursively.
//
// TODO(perf): it would probably be faster to copy vertices on demand. But this
// is more complicated and it would be worth measuring how much of a performance
// benefit this gives. More importantly, we should first implement the filter
// to eliminate disjunctions pre-copy based on discriminator fields and what
// have you. This is not unlikely to eliminate
func (ctx *overlayContext) cloneVertex(x *Vertex) *Vertex {
	xcc := x.rootCloseContext(ctx.ctx) // may be uninitialized for constraints.
	if o := xcc.overlay; o != nil && o.src != nil {
		// This path could happen with structure sharing or user-constructed
		// values.
		return o.src
	}

	v := &Vertex{}
	*v = *x

	ctx.vertices = append(ctx.vertices, v)

	v._cc = ctx.allocCC(x.cc())

	v._cc.src = v
	v._cc.parentConjuncts = v

	// The group of the root closeContext should point to the Conjuncts field
	// of the Vertex. As we already allocated the group, we use that allocation,
	// but "move" it to v.Conjuncts.
	v.Conjuncts = *v._cc.group
	v._cc.group = &v.Conjuncts

	if a := x.Arcs; len(a) > 0 {
		// TODO(perf): reuse buffer.
		v.Arcs = make([]*Vertex, len(a))
		for i, arc := range a {
			// TODO(perf): reuse when finalized.
			arc := ctx.cloneVertex(arc)
			v.Arcs[i] = arc
			arc.Parent = v
		}
	}

	v.Structs = slices.Clone(v.Structs)

	if pc := v.PatternConstraints; pc != nil {
		npc := &Constraints{Allowed: pc.Allowed}
		v.PatternConstraints = npc

		npc.Pairs = make([]PatternConstraint, len(pc.Pairs))
		for i, p := range pc.Pairs {
			npc.Pairs[i] = PatternConstraint{
				Pattern:    p.Pattern,
				Constraint: ctx.cloneVertex(p.Constraint),
			}
		}
	}

	if v.state != nil {
		v.state = ctx.cloneNodeContext(x.state)
		v.state.node = v

		ctx.cloneScheduler(v.state, x.state)
	}

	return v
}

func (ctx *overlayContext) cloneNodeContext(n *nodeContext) *nodeContext {
	if !n.node.isInitialized() {
		panic("unexpected uninitialized node")
	}
	d := n.ctx.newNodeContext(n.node)
	d.underlying = n.underlying
	if n.underlying == nil {
		panic("unexpected nil underlying")
	}

	d.refCount++

	d.ctx = n.ctx
	d.node = n.node

	d.nodeContextState = n.nodeContextState

	d.arcMap = append(d.arcMap, n.arcMap...)
	d.checks = append(d.checks, n.checks...)

	for _, s := range n.sharedIDs {
		s.cc = ctx.allocCC(s.cc)
		d.sharedIDs = append(d.sharedIDs, s)
	}

	// TODO: do we need to add cyclicConjuncts? Typically, cyclicConjuncts
	// gets cleared at the end of a unify call. There are cases, however, where
	// this is possible. We should decide whether cyclicConjuncts should be
	// forced to be processed in the parent node, or that we allow it to be
	// copied to the disjunction. By taking no action here, we assume it is
	// processed in the parent node. Investigate whether this always will lead
	// to correct results.
	// d.cyclicConjuncts = append(d.cyclicConjuncts, n.cyclicConjuncts...)

	if len(n.disjunctions) > 0 {
		// Do not clone cc in disjunctions, as it is identified by underlying.
		// We only need to clone the cc in disjunctCCs.
		d.disjunctions = append(d.disjunctions, n.disjunctions...)
		for _, h := range n.disjunctCCs {
			h.cc = ctx.allocCC(h.cc)
			d.disjunctCCs = append(d.disjunctCCs, h)
		}
	}

	return d
}

// cloneConjunct prepares a tree of conjuncts for copying by first allocating
// a clone for each closeContext.
func (ctx *overlayContext) copyConjunct(c Conjunct) Conjunct {
	cc := c.CloseInfo.cc
	if cc == nil {
		return c
	}
	// TODO: see if we can avoid this allocation. It seems that this should
	// not be necessary, and evaluation attains correct results without it.
	// Removing this, though, will cause some of the assertions to fail. These
	// assertions are overly strict and could be relaxed, but keeping them as
	// they are makes reasoning about them easier.
	overlay := ctx.allocCC(cc)
	c.CloseInfo.cc = overlay
	return c
}

// Phase 1: alloc
func (ctx *overlayContext) allocCC(cc *closeContext) *closeContext {
	// TODO(perf): if the original is "done", it can no longer be modified and
	// we can use the original, even if the values will not be correct.
	if cc.overlay != nil {
		return cc.overlay
	}

	o := &closeContext{generation: ctx.generation}
	cc.overlay = o
	o.depth = cc.depth
	o.holeID = cc.holeID

	if cc.parent != nil {
		o.parent = ctx.allocCC(cc.parent)
	}

	// Copy the conjunct group if it exists.
	if cc.group != nil {
		// Copy the group of conjuncts.
		g := make([]Conjunct, len(*cc.group))
		o.group = (*ConjunctGroup)(&g)
		for i, c := range *cc.group {
			g[i] = ctx.copyConjunct(c)
		}

		if o.parent != nil {
			// validate invariants.
			// TODO: the group can sometimes be empty. Investigate why and
			// whether this is valid.
			if ca := *cc.parent.group; len(ca) > 0 {
				if ca[cc.parentIndex].x != cc.group {
					panic("group misaligned")
				}

				(*o.parent.group)[cc.parentIndex].x = o.group
			}
		}
	}

	// This must come after allocating the parent so that we can always read
	// the src vertex from the parent during initialization. This assumes that
	// src is set in the root closeContext when cloning a vertex.
	ctx.closeContexts = append(ctx.closeContexts, cc)

	// We only explicitly tag dependencies of type ARC. Notifications that
	// point within the disjunct overlay will be tagged elsewhere.
	for _, a := range cc.arcs {
		ctx.allocCC(a.dst)
	}

	return o
}

func (ctx *overlayContext) initCloneCC(x *closeContext) {
	o := x.overlay

	if p := x.parent; p != nil {
		o.parent = p.overlay
		o.src = o.parent.src
	}

	o.depth = x.depth
	o.conjunctCount = x.conjunctCount
	o.disjunctCount = x.disjunctCount
	o.isDef = x.isDef
	o.isDefOrig = x.isDefOrig
	o.hasTop = x.hasTop
	o.hasNonTop = x.hasNonTop
	o.isClosedOnce = x.isClosedOnce
	o.isEmbed = x.isEmbed
	o.isClosed = x.isClosed
	o.isTotal = x.isTotal
	o.done = x.done
	o.isDecremented = x.isDecremented
	o.parentIndex = x.parentIndex
	o.Expr = x.Expr
	o.Patterns = append(o.Patterns, x.Patterns...)

	// needsCloseInSchedule is a separate mechanism to signal nodes that have
	// completed that corresponds to the EVAL mechanism. Since we have not
	// processed the conjuncts yet, these are inherently initiated outside of
	// this conjunct. By now, if a closeContext needs to remain open, other
	// counters should have been added. As an example, the parent node of this
	// disjunct is still processing. The disjunction will be fully added before
	// processing, and thus their will be no direct EVAL dependency. However,
	// this disjunct may depend on a NOTIFY that is kept open by an ancestor
	// EVAL.
	if x.needsCloseInSchedule != nil {
		o.needsCloseInSchedule = nil
	}

	// child and next always point to completed closeContexts. Moreover, only
	// fields that are immutable, such as Expr, are used. It is therefore not
	// necessary to use overlays.
	o.child = x.child
	if x.child != nil && x.child.overlay != nil {
		// TODO(evalv3): there seem to be situations where this is possible
		// after all. See if this is really true, and we should remove this
		// panic, or if this underlies a bug of sorts.
		// panic("unexpected overlay in child")
	}
	o.next = x.next
	if x.next != nil && x.next.overlay != nil {
		// TODO(evalv3): there seem to be situations where this is possible
		// after all. See if this is really true, and we should remove this
		// panic, or if this underlies a bug of sorts.
		// See Issue #3434.
		// panic("unexpected overlay in next")
	}

	switch p := x.parentConjuncts.(type) {
	case *closeContext:
		if p.overlay == nil {
			panic("expected overlay")
		}
		o.parentConjuncts = p.overlay

	case *Vertex:
		o.parentConjuncts = o.src
	}

	if o.src == nil {
		// fall back to original vertex.
		// FIXME: this is incorrect, as it may lead to evaluating nodes that
		// are not part of the disjunction with values of the disjunction.
		// TODO: try eliminating EVAL dependencies of arcs that are the parent
		// of the disjunction root.
		o.src = x.src
	}

	if o.parentConjuncts == nil {
		panic("expected parentConjuncts")
	}
}

func (ctx *overlayContext) finishDependencies(x *closeContext) {
	o := x.overlay

	for _, a := range x.arcs {
		// If an arc does not have an overlay, we should not decrement the
		// dependency counter. We simply remove the dependency in that case.
		if a.dst.overlay == nil || a.root.overlay == nil {
			panic("arcs should always point inwards and thus included in the overlay")
		}
		if a.decremented {
			continue
		}
		a.root = a.root.overlay // TODO: is this necessary?
		a.dst = a.dst.overlay
		o.arcs = append(o.arcs, a)

		root := a.dst.src.cc()
		root.externalDeps = append(root.externalDeps, ccDepRef{
			src:   o,
			kind:  ARC,
			index: len(o.arcs) - 1,
		})
	}

	for _, a := range x.notify {
		// If a notification does not have an overlay, we should not decrement
		// the dependency counter. We simply remove the dependency in that case.
		// TODO: however, the original closeContext that it point to now will
		// never be "filled". We should insert top in this gat or render it as
		// "defunct", for instance, so that it will not leave an nondecremented
		// counter.
		if a.dst.overlay == nil {
			for c := a.dst; c != nil; c = c.parent {
				c.disjunctCount++
			}
			continue
		}
		if a.decremented {
			continue
		}
		a.dst = a.dst.overlay
		o.notify = append(o.notify, a)

		root := a.dst.src.cc()
		root.externalDeps = append(root.externalDeps, ccDepRef{
			src:   o,
			kind:  NOTIFY,
			index: len(o.notify) - 1,
		})
	}

	for _, d := range x.dependencies {
		if d.decremented {
			continue
		}

		if d.kind == DEFER {
			o.decDependentNoMatch(ctx.ctx, DEFER, nil)
			continue
		}

		// Since have not started processing the disjunct yet, all EVAL
		// dependencies will have been initiated outside of this disjunct.
		if d.kind == EVAL {
			o.decDependentNoMatch(ctx.ctx, EVAL, nil)
			continue
		}

		if d.dependency.overlay == nil {
			// This dependency is irrelevant for the current overlay. We can
			// eliminate it as long as we decrement the accompanying counter.
			if o.conjunctCount < 2 {
				// This node can only be relevant if it has at least one other
				// dependency. Check that we are not decrementing the counter
				// to 0.
				// TODO: this currently panics for some tests. Disabling does
				// not seem to harm, though. Reconsider whether this is an issue.
				// panic("unexpected conjunctCount: must be at least 2")
			}
			o.conjunctCount--
			continue
		}

		dep := d.dependency
		dep = dep.overlay
		o.dependencies = append(o.dependencies, &ccDep{
			dependency:  dep,
			kind:        d.kind,
			decremented: false,
		})
	}
}

func (ctx *overlayContext) cloneScheduler(dst, src *nodeContext) {
	ss := &src.scheduler
	ds := &dst.scheduler

	ds.state = ss.state
	ds.completed = ss.completed
	ds.needs = ss.needs
	ds.provided = ss.provided
	ds.counters = ss.counters

	ss.blocking = ss.blocking[:0]

	for _, t := range ss.tasks {
		switch t.state {
		case taskWAITING:
			// Do not unblock previously blocked tasks, unless they are
			// associated with this node.
			// TODO: an edge case is when a task is blocked on another node
			// within the same disjunction. We could solve this by associating
			// each nodeContext with a unique ID (like a generation counter) for
			// the disjunction.
			if t.node != src || t.blockedOn != ss {
				break
			}
			t.defunct = true
			t := ctx.cloneTask(t, ds, ss)
			ds.tasks = append(ds.tasks, t)
			ds.blocking = append(ds.blocking, t)
			ctx.ctx.blocking = append(ctx.ctx.blocking, t)

		case taskREADY:
			t.defunct = true
			t := ctx.cloneTask(t, ds, ss)
			ds.tasks = append(ds.tasks, t)

		case taskRUNNING:
			if t.run != handleResolver && t.run != handleExpr {
				// TODO: consider whether this is also necessary for other
				// types of tasks.
				break
			}

			t.defunct = true
			t := ctx.cloneTask(t, ds, ss)
			t.state = taskREADY
			ds.tasks = append(ds.tasks, t)
		}
	}
}

func (ctx *overlayContext) cloneTask(t *task, dst, src *scheduler) *task {
	if t.node != src.node {
		panic("misaligned node")
	}

	id := t.id
	if id.cc != nil {
		id.cc = ctx.allocCC(t.id.cc) // TODO: may be nil for disjunctions.
	}

	// TODO: alloc from buffer.
	d := &task{
		run:            t.run,
		state:          t.state,
		completes:      t.completes,
		unblocked:      t.unblocked,
		blockCondition: t.blockCondition,
		err:            t.err,
		env:            t.env,
		x:              t.x,
		id:             id,

		node: dst.node,

		// TODO: need to copy closeContexts?
		comp: t.comp,
		leaf: t.leaf,
	}

	if t.blockedOn != nil {
		if t.blockedOn != src {
			panic("invalid scheduler")
		}
		d.blockedOn = dst
	}

	return d
}