Chromium Code Reviews
chromiumcodereview-hr@appspot.gserviceaccount.com (chromiumcodereview-hr) | Please choose your nickname with Settings | Help | Chromium Project | Gerrit Changes | Sign out
(566)

Side by Side Diff: mojom/mojom_parser/utils/wellfounded_graphs.go

Issue 1908273003: Mojom frontend: An algorithm for detecting ill-founded nodes in a graph. (Closed) Base URL: https://github.com/domokit/mojo.git@master
Patch Set: Responding to code review. Created 4 years, 8 months ago
Use n/p to move between diff chunks; N/P to move between comments. Draft comments are only viewable by you.
Jump to:
View unified diff | Download patch
« no previous file with comments | « mojom/mojom_parser/BUILD.gn ('k') | mojom/mojom_parser/utils/wellfounded_graphs_test.go » ('j') | no next file with comments »
Toggle Intra-line Diffs ('i') | Expand Comments ('e') | Collapse Comments ('c') | Show Comments Hide Comments ('s')
OLDNEW
(Empty)
1 // Copyright 2016 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
4
5 package utils
6
7 import (
8 "bytes"
9 "fmt"
10 )
11
12 // This file contains an implementation of an algorithm to check the well-founde dness
13 // of two-sorted directed graphs. See the paper
14 // "Well-Founded Two-Sorted Directed Graphs" (https://goo.gl/ipxFKu) for backgro und
15 // and detailed definitions. Here we give only a high-level overview.
16 //
17 // A two-sorted graph is a directed graph that contains two sorts of nodes calle d circle nodes and
18 // square nodes. A node in a two-sorted graph is *well-founded* iff it satisfies the
19 // following recursive definition:
20 // (i) Leaf nodes are well-founded
21 // (ii) A circle node is well-founded iff all of its children are well-founded
22 // (iii) A non-leaf square node is well-founded iff at least one of its children is well-founded.
23 // (See the paper for a more logically correct definition.)
24 //
25 // See the comments on the main function |CheckWellFounded| below for a descript ion
26 // of the algorithm.
27
28 ///////////////////////////////////////////////////
29 // Node type
30 //
31 // A |Node| is a node in a directed graph.
32 ///////////////////////////////////////////////////
33
34 type Node interface {
35 // Returns the list of children of this node.
36 OutEdges() []Node
37
38 // Returns whether or not this node is a square node.
39 IsSquare() bool
40
41 // SetKnownWellFounded is invoked by the algorithm in order to set the
42 // fact that the algorithm has determined that this node is well-founded .
43 // An implementation of |Node| should cache this--even between different
44 // invocations of the algorithm on different starting nodes--and return
45 // |true| from the method |KnownWellFounded| just in case this method ha s
46 // ever been invoked. In this way any nodes verified to be well-founded during
47 // one run of the algorithm do not need to be checked during a later run of the
48 // algorithm on a different starting node.
49 SetKnownWellFounded()
50
51 // Returns whether or not the function |SetKnownWellFounded| has ever
52 // been invoked on this node, during the lifetime of a program using
53 // this library.
54 KnownWellFounded() bool
55
56 // Returns the name of this node, appropriate for debugging.
57 DebugName() string
58 }
59
60 ///////////////////////////////////////////////////
61 // CheckWellFounded function
62 //
63 // This is the main public function.
64 ///////////////////////////////////////////////////
65
66 // CheckWellFounded checks whether the sub-graph rooted at |root| contains any i ll-founded nodes.
67 // If any ill-founded nodes are detected then a non-nil |CycleDescription| is re turned containing
68 // a cycle of ill-founded nodes. If every node is well-founded then nil is retur ned.
69 //
70 // If the graph contains only circle nodes then well-foundedness is equivalent t o
71 // acyclicality and so the returned cycle is a proof that the nodes contained in it
72 // are ill-founded. But in general (if the graph contains square nodes) the retu rned
73 // |CycleDescription| is only meant to serve as an example of some of the
74 // ill-foundedness contained in the subgraph. The cycle is guaranteed to contain only
75 // ill-founded nodes and the cycle may be considered part of a proof that thes n odes
76 // are in fact ill-founded. But the cycle does not necessarily contain every
77 // ill-founded node and it does not
78 // necessarily constitute a complete proof of the ill-foundedness
79 // because a graph that contains square nodes is allowed to contain some cycles and still be
80 // well-founded. The intended application of the returned CycleDescription is to be used to
81 // describe to a user the location of the ill-foundedness in order to allow the user to modify
82 // the graph in order to make it well-founded.
83 //
84 // This function may be invoked multiple times on different starting nodes durin g the life
85 // of a program. If the function is invoked once on node |x| and no ill-foundedn ess is
86 // found and then the function is invoked later on node |y| and if node |z| is r eachable
87 // from both |x| and |y| then node |z| will be marked |KnownWellFounded| during the
88 // first run of the function and so the graph below |z| will not have to be insp ected
89 // during the second run of the function.
90 //
91 // Our algorithm proceeds in three phases:
92 // (1) Phase 1 consists of a depth-first traversal of the graph whose purpose is two-fold:
93 // (a) To prove directly that as many nodes as possible are well-founded and mark them
94 // so by invoking SetKnownWellFounded().
95 // (b) To prepare for phase 2 by constructing two sets of nodes called the | foundationSet| and
96 // the |pendingSet|
97 // See the comments at |checkWellFoundedPhase1| for more details.
98 //
99 // In many cases phase 1 will be able to prove that every node is well-founded a nd so the algorithm
100 // will terminate without entering phase 2. This is the case for example if the graph has no
101 // cycles at all.
102 //
103 // (2) The purpose of phase 2 is to propogate the |KnownWellFounded| property to the remaining well-founded
104 // nodes (the ones that could not be verified as well-founded during phase 1 .) Phase 2 proceeds in
105 // multiple rounds. During each round the |KnownWellFounded| property is pro pogated from the
106 // |foundationSet| to the |pendingSet|. See the comments at |checkWellFounde dPhase2| for more details.
107 //
108 // If there are no ill-founded nodes then the algorithm terminates after phase 2.
109 //
110 // (3) Phase 3 of the algorithm consists of building a |CycleDescription| in the case that there are ill-founded
111 // nodes. See the method |findKnownIllFoundedCycle|.
112
113 func CheckWellFounded(root Node) *CycleDescription {
114 return checkWellFounded(root, nil)
115 }
116
117 // checkWellFounded is a package-private version of CheckWellFounded intendend t o be invoked by tests.
118 // It offers a second parameter |debugDataRequest|. If this is non-nil then its fields will be filled
119 // in with debugging data about the output of phase 1.
120 func checkWellFounded(root Node, debugDataRequest *debugData) *CycleDescription {
121 if root == nil {
122 return nil
123 }
124 finder := makeCycleFinder()
125 finder.debugDataRequest = debugDataRequest
126 holder := finder.holderForNode(root)
127 return finder.checkWellFounded(holder)
128 }
129
130 ///////////////////////////////////////////////////
131 /// cycleFinder type
132 ///
133 /// A cycleFinder is an object that holds state during the execution of the algo rithm.
134 ///////////////////////////////////////////////////
135 type cycleFinder struct {
136
137 // Maps each node seen by the computation to its holder.
138 nodeToHolder map[Node]*nodeHolder
139
140 foundationSet, pendingSet NodeSet
141
142 visitationIndex int
143 currentPath nodeStack
144
145 debugDataRequest *debugData
146 }
147
148 type debugData struct {
149 initialPendingSet []Node
150 initialFoundationSet []Node
151 }
152
153 func makeCycleFinder() cycleFinder {
154 finder := cycleFinder{}
155 finder.nodeToHolder = make(map[Node]*nodeHolder)
156 finder.foundationSet = MakeNodeSet()
157 finder.pendingSet = MakeNodeSet()
158 finder.currentPath = nodeStack{}
159 return finder
160 }
161
162 // checkWellFounded contains the top-level implementation of the algorithm.
163 func (finder *cycleFinder) checkWellFounded(nodeHolder *nodeHolder) *CycleDescri ption {
164 if nodeHolder.node.KnownWellFounded() {
165 // This node is already known to be well-founded because of an e arlier
166 // execution of the algorithm.
167 return nil
168 }
169
170 // Perform phase 1.
171 finder.checkWellFoundedPhase1(nodeHolder)
172
173 // In tests we pass back some debugging information here.
174 if finder.debugDataRequest != nil {
175 finder.debugDataRequest.initialPendingSet = finder.pendingSet.To NodeSlice()
176 finder.debugDataRequest.initialFoundationSet = finder.foundation Set.ToNodeSlice()
177 }
178
179 // All nodes have been verified as well-founded.
180 if finder.pendingSet.Empty() {
181 return nil
182 }
183
184 // Perform phase 2.
185 finder.checkWellFoundedPhase2()
186
187 // All nodes have been verified as well-founded.
188 if finder.pendingSet.Empty() {
189 return nil
190 }
191
192 // If we are here then there is at least one ill-founded node.
193
194 // In order to build a canonical cycle description, find the illfounded
195 // node with the least visitation order.
196 var minVisitationOrder int
197 var minIllfoundedNode Node = nil
198 for n, _ := range finder.pendingSet.elements {
199 if minIllfoundedNode == nil || n.visitationOrder < minVisitation Order {
200 minVisitationOrder = n.visitationOrder
201 minIllfoundedNode = n.node
202 }
203 }
204
205 // Starting from the ill-founded node with the least visitation order,
206 // build a canonical cycle.
207 freshCycleFinder := makeCycleFinder()
208 holder := freshCycleFinder.holderForNode(minIllfoundedNode)
209 return freshCycleFinder.findKnownIllFoundedCycle(holder)
210 }
211
212 // checkWellFoundedPhase1 is a recursive helper function that does a depth-first traversal
213 // of the graph rooted at |nodeHolder|. The goal of phase 1 is to mark as many
214 // of the nodes as possible as |KnownWellFounded| and to set up the |pendingSet| ,
215 // the |foundationSet|, and the |parentsToBeNotified| sets so that the remaining
216 // well-founded nodes will be marked as |KnownWellFounded| in phase 2.
217 //
218 // In more detail the following steps are performed for each node x:
219 // (a1) If it can be verified during the traversal that x is well-founded then
220 // x will be marked as |KnownWellFounded|. This occurs if x is a leaf, or x
221 // is a circle and each child node of x is known well-founded before being
222 // visited as a child of x, or x is a square and at-least one child node of x
223 // is known well-founded before being visited as a child of x.
224 // (a2) Otherwise if it cannot be determined during traveral that x is well-foun ded then
225 // (i) x is added to the |pendingSet|.
226 // (ii) x is added to the |parentsToBeNotified| set of all of its children.
227 // (b) In step (a1) if at the time x is found to be well-founded x already has
228 // some parent node x' in its |parentsToBeNotified| set (meaning that step a2 oc curred
229 // earlier for x' and so x' is in the |pendingSet|) then x is added to the |foun dationSet|.
230 // In phase 2, the fact that x is in the foundation set and x' is in the pending set will be
231 // used to propogate known-wellfoundedness to x'.
232 func (finder *cycleFinder) checkWellFoundedPhase1(nodeHolder *nodeHolder) {
233 if nodeHolder.node.KnownWellFounded() {
234 // This node is known to be well-founded before being visited.
235 // This occurs when the node was marked |KnownWellFounded| durin g a
236 // previous run of the algorithm. It follows that all nodes reac hable
237 // from this node have also been so marked. We therefore don't n eed
238 // to traverse the part of the graph below this node during this run
239 // of the algorithm and so we treat this node as a leaf node.
240 nodeHolder.state = vsVisitEnded
241 return
242 }
243
244 // Mark the visit as started.
245 nodeHolder.state = vsVisitStarted
246
247 // Next we examine each of the children and recurse into the unvisited o nes.
248 sawUnverifiedChild := false
249 for _, child := range nodeHolder.node.OutEdges() {
250 childHolder := finder.holderForNode(child)
251 if childHolder.state == vsUnvisited {
252 // Recursively visit this child.
253 finder.checkWellFoundedPhase1(childHolder)
254 }
255
256 // After having visited a child we use the results to update the status of this node.
257 // We could express the logic here more concisely, but the logi c is easier
258 // to understand if we treat circles and squares seperately,
259 if nodeHolder.node.IsSquare() {
260 if nodeHolder.node.KnownWellFounded() {
261 // This square node has already been marked |Kno wnWellFounded| becuase
262 // of an earlier iteration through this loop. Th ere is nothing else to do.
263 continue
264 }
265 if childHolder.node.KnownWellFounded() {
266 // We mark a square node as |KnownWellFounded| a s soon as we can so
267 // that if any of its descendants are also paren ts, the well-foundedness
268 // has a chance to propogate to the descendant i n a recursive call.
269 nodeHolder.node.SetKnownWellFounded()
270 } else {
271 // This square node is not yet known to be well- founded and the child node
272 // is not yet known to be well-founded. Set up a back link from the child.
273 childHolder.parentsToBeNotified.Add(nodeHolder)
274 sawUnverifiedChild = true
275 }
276 continue // Done handling the square case.
277 }
278
279 // Else the node is a circle. If the child is not yet known to b e well-founded
280 // set up a back link from the child to this node.
281 if !childHolder.node.KnownWellFounded() {
282 childHolder.parentsToBeNotified.Add(nodeHolder)
283 sawUnverifiedChild = true
284 }
285 }
286
287 // If a circle node has only well-founded children, or a square node has no children at all,
288 // then the node is well-founded.
289 if !sawUnverifiedChild && !nodeHolder.node.KnownWellFounded() {
290 nodeHolder.node.SetKnownWellFounded()
291 }
292
293 // Possibly add this node to the |foundationSet| or the |pendingSet|.
294 if nodeHolder.node.KnownWellFounded() {
295 if !nodeHolder.parentsToBeNotified.Empty() {
296 finder.foundationSet.Add(nodeHolder)
297 }
298 } else {
299 finder.pendingSet.Add(nodeHolder)
300 }
301
302 // Mark the visit as ended.
303 nodeHolder.state = vsVisitEnded
304 return
305 }
306
307 // checkWellFoundedPhase2 performs phase 2 of the algorithm. The goal is to
308 // propogate known well-foundedness along the back-links that were established
309 // during phase 1. We have two sets of nodes: the |foundationSet| and the
310 // |pendingSet|. The |pendingSet| consists of all nodes that are not currently
311 // known to be well-founded. If the |pendingSet| is not empty when this method
312 // returns, then the nodes in the |pendingSet| are ill-founded. The |foundationS et|
313 // consists of the current frontier of the propogation. That is, the |foundation Set|
314 // consists of the nodes discovered to be well-founded in recent iterations and not yet
315 // used to propogate well-foundedness. (The |foundationSet| starts with the node s discovered
316 // to be well-founded during phase 1, pruned to nodes that have parents that
317 // are in the |pendingSet|.) We iteratively remove a node n from the foundation set and
318 // for each of its parents p for which p is in the pending set and for which we can now verify
319 // well-foundedness, we remove p from the pending set and add it to the foundati on set.
320 func (finder *cycleFinder) checkWellFoundedPhase2() {
321 for n := finder.foundationSet.removeRandomElement(); n != nil; n = finde r.foundationSet.removeRandomElement() {
322 for p, _ := range n.parentsToBeNotified.elements {
323 if finder.pendingSet.Contains(p) {
324 knownWellFounded := true
325 if !p.node.IsSquare() {
326 for _, child := range p.node.OutEdges() {
327 if child != p.node && !child.Kno wnWellFounded() {
328 knownWellFounded = false
329 break
330 }
331 }
332 }
333 if knownWellFounded {
334 p.node.SetKnownWellFounded()
335 finder.foundationSet.Add(p)
336 finder.pendingSet.Remove(p)
337 }
338 }
339 }
340 }
341 }
342
343 // findKnownIllFoundedCycle finds and returns a |CycleDescription| starting from a node that is known
344 // to be ill-founded. This proceeds by following edges from an ill-founded node to
345 // an ill-founded child node until a cycle is formed. We return a *canonical* cy cle,
346 // meaning we start from the node with the least possible visit index and follow edges to
347 // the child node with the least possible visit index. This is done in order to make testing of the algorithm easier.
348 // We are not concerned with optimizing the performance of phase 3 because in th e intended application
349 // phase 3 can occur at most once in the lifetime of a program: Once an ill-foun ded node is detected the
350 // program exits with a cycle description allowing the user to fix the ill-found edness.
351 func (finder *cycleFinder) findKnownIllFoundedCycle(nodeHolder *nodeHolder) *Cyc leDescription {
352 // Mark the current node as started
353 nodeHolder.state = vsVisitStarted
354 finder.currentPath.Push(nodeHolder)
355 for _, child := range nodeHolder.node.OutEdges() {
356 childHolder := finder.holderForNode(child)
357 if childHolder.state == vsVisitStarted {
358 // If the child has been started but not finished then w e have found a cycle
359 // from the child to the current node back to the child.
360 return newCycleDescription(finder.currentPath.elements, childHolder, nodeHolder)
361 } else if !childHolder.node.KnownWellFounded() {
362 return finder.findKnownIllFoundedCycle(childHolder)
363 }
364 }
365 panic("Program logic error: Could not find a known ill-founded cycle.")
366 }
367
368 // Returns the nodeHolder for the given node.
369 func (finder *cycleFinder) holderForNode(node Node) *nodeHolder {
370 if holder, found := finder.nodeToHolder[node]; found {
371 return holder
372 }
373
374 // This is the first time we have seen this node. Assign it a new
375 // visitor order.
376 holder := newNodeHolder(node, finder.visitationIndex)
377 finder.visitationIndex++
378 finder.nodeToHolder[node] = holder
379 return holder
380 }
381
382 ////////////////////////////////////////////////////
383 // nodeHolder type
384 ////////////////////////////////////////////////////
385
386 type visitationState int
387
388 const (
389 vsUnvisited visitationState = iota
390 vsVisitStarted
391 vsVisitEnded
392 )
393
394 // A nodeHolder is an internal data structure used by the algorithm.
395 // It holds one node plus data about that node used by the algorithm.
396 type nodeHolder struct {
397 // The node
398 node Node
399
400 parentsToBeNotified NodeSet
401
402 visitationOrder int
403
404 state visitationState
405 }
406
407 func newNodeHolder(node Node, visitationOrder int) *nodeHolder {
408 nodeHolder := new(nodeHolder)
409 nodeHolder.node = node
410 nodeHolder.parentsToBeNotified = MakeNodeSet()
411 nodeHolder.state = vsUnvisited
412 nodeHolder.visitationOrder = visitationOrder
413 return nodeHolder
414 }
415
416 //////////////////////////////////////////////
417 /// nodeStack type
418 //////////////////////////////////////////////
419
420 // A nodeStack is a stack of *nodeHolders
421 type nodeStack struct {
422 elements []*nodeHolder
423 }
424
425 func (stack *nodeStack) Push(n *nodeHolder) {
426 stack.elements = append(stack.elements, n)
427 }
428
429 func (stack *nodeStack) Size() int {
430 return len(stack.elements)
431 }
432
433 func (stack *nodeStack) Pop() (n *nodeHolder) {
434 lastIndex := stack.Size() - 1
435 n = stack.elements[lastIndex]
436 stack.elements = stack.elements[:lastIndex]
437 return
438 }
439
440 func (stack *nodeStack) Peek() (n *nodeHolder) {
441 return stack.elements[stack.Size()-1]
442 }
443
444 func (stack *nodeStack) String() string {
445 var buffer bytes.Buffer
446 fmt.Fprintf(&buffer, "[")
447 first := true
448 for _, e := range stack.elements {
449 if !first {
450 fmt.Fprintf(&buffer, ", ")
451 }
452 fmt.Fprintf(&buffer, "%s", e.node.DebugName())
453 first = false
454 }
455 fmt.Fprintln(&buffer, "]")
456 return buffer.String()
457 }
458
459 ///////////////////////////////////////////////////
460 /// NodeSet type
461 ///////////////////////////////////////////////////
462
463 // A NodeSet is a set of nodeHolders.
464 type NodeSet struct {
465 elements map[*nodeHolder]bool
466 }
467
468 // MakeNodeSet makes a new empty NodeSet.
469 func MakeNodeSet() NodeSet {
470 nodeSet := NodeSet{}
471 nodeSet.elements = make(map[*nodeHolder]bool)
472 return nodeSet
473 }
474
475 // Add adds a Node to a NodeSet.
476 func (set *NodeSet) Add(node *nodeHolder) {
477 set.elements[node] = true
478 }
479
480 // AddAll adds all the nodes from |otherSet| to |set|.
481 func (set *NodeSet) AddAll(otherSet NodeSet) {
482 for e, _ := range otherSet.elements {
483 set.elements[e] = true
484 }
485 }
486
487 // Contains returns whether or not |node| is an element of |set|.
488 func (set *NodeSet) Contains(node *nodeHolder) bool {
489 _, ok := set.elements[node]
490 return ok
491 }
492
493 func (set *NodeSet) Remove(node *nodeHolder) {
494 delete(set.elements, node)
495 }
496
497 func (set *NodeSet) Empty() bool {
498 return len(set.elements) == 0
499 }
500
501 // doUntilEmpty repeatedly iterates through the elements of |set| removing
502 // them and invoking |f|. More precisely, for each element x of |set|,
503 // x is removed from |set| and then f(x) is invoked.
504 //
505 // The function |f| is allowed to mutate |set|. If |f| adds new elements to
506 // |set| those will also eventually be removed and operated on. Whether or
507 // not this process converges to an empty set depends entirely on the behavior
508 // of |f| and it is the caller's responsibility to ensure that it does.
509 func (set *NodeSet) doUntilEmpty(f func(node *nodeHolder)) {
510 for !set.Empty() {
511 for n, _ := range set.elements {
512 set.Remove(n)
513 f(n)
514 }
515 }
516 }
517
518 // removeRandomElement removes and returns an arbitrary element of |set|
519 // or nil of |set| is empty.
520 func (set *NodeSet) removeRandomElement() *nodeHolder {
521 for n, _ := range set.elements {
522 delete(set.elements, n)
523 return n
524 }
525 return nil
526 }
527
528 func (set *NodeSet) ToNodeSlice() []Node {
529 slice := make([]Node, 0, len(set.elements))
530 for n, _ := range set.elements {
531 slice = append(slice, n.node)
532 }
533 return slice
534 }
535
536 func (set *NodeSet) Size() int {
537 return len(set.elements)
538 }
539
540 // compareNodeSets is a package-private method used in our tests. It returns
541 // a non-nil error in case expected is not equal to actual.
542 func compareNodeSets(expected, actual *NodeSet) error {
543 for n, _ := range expected.elements {
544 if !actual.Contains(n) {
545 return fmt.Errorf("%s is in expected but not actual", n. node.DebugName())
546 }
547 }
548 for n, _ := range actual.elements {
549 if !expected.Contains(n) {
550 return fmt.Errorf("%s is in actual but not expected", n. node.DebugName())
551 }
552 }
553 return nil
554 }
555
556 // String returns a human readable string representation of |set|.
557 func (set *NodeSet) String() string {
558 var buffer bytes.Buffer
559 fmt.Fprintf(&buffer, "{")
560 first := true
561 for e, _ := range set.elements {
562 if !first {
563 fmt.Fprintf(&buffer, ", ")
564 }
565 fmt.Fprintf(&buffer, "%s", e.node.DebugName())
566 first = false
567 }
568 fmt.Fprintln(&buffer, "}")
569 return buffer.String()
570 }
571
572 ///////////////////////////////////////////////////
573 // CycleDescription type
574 //
575 // A |CycleDescription| describes a cycle in a directed graph.
576 ///////////////////////////////////////////////////
577
578 type CycleDescription struct {
579 first, last Node
580 path []Node
581 }
582
583 func (c *CycleDescription) String() string {
584 var buffer bytes.Buffer
585 fmt.Fprintf(&buffer, "first:%s", c.first.DebugName())
586 fmt.Fprintf(&buffer, ", last:%s", c.last.DebugName())
587 fmt.Fprintf(&buffer, ", path:{")
588 first := true
589 for _, n := range c.path {
590 if !first {
591 fmt.Fprintf(&buffer, ", ")
592 }
593 fmt.Fprintf(&buffer, "%s", n.DebugName())
594 first = false
595 }
596 fmt.Fprintln(&buffer, "}")
597 return buffer.String()
598 }
599
600 func newCycleDescription(path []*nodeHolder, first, last *nodeHolder) *CycleDesc ription {
601 description := CycleDescription{}
602 description.first = first.node
603 description.last = last.node
604 description.path = make([]Node, 0, len(path))
605 for _, n := range path {
606 if len(description.path) > 0 || n.node == first.node {
607 description.path = append(description.path, n.node)
608 }
609 }
610 if description.path[len(description.path)-1] != last.node {
611 panic(fmt.Sprintf("%s != %s", description.path[len(description.p ath)-1], last.node))
612 }
613 return &description
614 }
OLDNEW
« no previous file with comments | « mojom/mojom_parser/BUILD.gn ('k') | mojom/mojom_parser/utils/wellfounded_graphs_test.go » ('j') | no next file with comments »

Powered by Google App Engine
This is Rietveld 408576698