Fix uses/augment linkage 14/99414/32
authorRobert Varga <robert.varga@pantheon.tech>
Fri, 21 Jan 2022 06:10:37 +0000 (07:10 +0100)
committerRobert Varga <robert.varga@pantheon.tech>
Sun, 13 Feb 2022 23:12:11 +0000 (00:12 +0100)
Our previous fix for MDSAL-715 switched the resolution logic to a unified
approach based on linked augmentation. Unfortunately it missed the fact
that linking the groupings also had the side-effect of setting the
augment target, which in turn populates 'augments' list.

Our failure to do so ends up wrecking lookups in the case where we have
an uses-augmented node further augmented by a module-augment.

Fix this by intertwining original and augment linkage, so that a subtree
root (such as module) pays attention to augments which need to resolve
before descending to recursive linkage.

JIRA: MDSAL-718
Change-Id: I7bd6cbed636267d35113888ddd35f0c6d9411043
Signed-off-by: Robert Varga <robert.varga@pantheon.tech>
13 files changed:
binding/mdsal-binding-generator/src/main/java/org/opendaylight/mdsal/binding/generator/impl/reactor/AbstractAugmentGenerator.java
binding/mdsal-binding-generator/src/main/java/org/opendaylight/mdsal/binding/generator/impl/reactor/AbstractCompositeGenerator.java
binding/mdsal-binding-generator/src/main/java/org/opendaylight/mdsal/binding/generator/impl/reactor/AbstractExplicitGenerator.java
binding/mdsal-binding-generator/src/main/java/org/opendaylight/mdsal/binding/generator/impl/reactor/AugmentRequirement.java [new file with mode: 0644]
binding/mdsal-binding-generator/src/main/java/org/opendaylight/mdsal/binding/generator/impl/reactor/GeneratorReactor.java
binding/mdsal-binding-generator/src/main/java/org/opendaylight/mdsal/binding/generator/impl/reactor/LinkageProgress.java [new file with mode: 0644]
binding/mdsal-binding-generator/src/main/java/org/opendaylight/mdsal/binding/generator/impl/reactor/ModuleAugmentGenerator.java
binding/mdsal-binding-generator/src/main/java/org/opendaylight/mdsal/binding/generator/impl/reactor/UsesAugmentGenerator.java
binding/mdsal-binding-generator/src/test/java/org/opendaylight/mdsal/binding/generator/impl/Mdsal718Test.java [new file with mode: 0644]
binding/mdsal-binding-generator/src/test/resources/mdsal718/bar.yang [new file with mode: 0644]
binding/mdsal-binding-generator/src/test/resources/mdsal718/baz.yang [new file with mode: 0644]
binding/mdsal-binding-generator/src/test/resources/mdsal718/foo.yang [new file with mode: 0644]
binding/mdsal-binding-generator/src/test/resources/mdsal718/xyzzy.yang [new file with mode: 0644]

index 4875bd00c4f601ece3bbd07e83fc611eea09fd7a..b9c92cd1cb89bece58f184a5e15971c901224e9e 100644 (file)
@@ -9,6 +9,7 @@ package org.opendaylight.mdsal.binding.generator.impl.reactor;
 
 import static com.google.common.base.Verify.verify;
 import static com.google.common.base.Verify.verifyNotNull;
+import static java.util.Objects.requireNonNull;
 
 import java.util.Comparator;
 import java.util.Iterator;
@@ -143,21 +144,12 @@ abstract class AbstractAugmentGenerator extends AbstractCompositeGenerator<Augme
         // Augments are never added as getters, as they are handled via Augmentable mechanics
     }
 
-    final void setTargetGenerator(final AbstractExplicitGenerator<?> target) {
-        verify(target instanceof AbstractCompositeGenerator, "Unexpected target %s", target);
-        targetGen = (AbstractCompositeGenerator<?>) target;
-        targetGen.addAugment(this);
+    final void setTargetGenerator(final AbstractCompositeGenerator<?> targetGenerator) {
+        verify(targetGen == null, "Attempted to relink %s, already have target %s", this, targetGen);
+        targetGen = requireNonNull(targetGenerator);
     }
 
     final @NonNull AbstractCompositeGenerator<?> targetGenerator() {
-        final AbstractCompositeGenerator<?> existing = targetGen;
-        if (existing != null) {
-            return existing.getOriginal();
-        }
-
-        loadTargetGenerator();
-        return verifyNotNull(targetGen, "No target for %s", this).getOriginal();
+        return verifyNotNull(targetGen, "No target for %s", this);
     }
-
-    abstract void loadTargetGenerator();
 }
index 6dae9ac58423207f2d426e9f13d6523fbccda3d2..93b8debced4c228300c99a0f15b6712e948e9754 100644 (file)
@@ -8,7 +8,6 @@
 package org.opendaylight.mdsal.binding.generator.impl.reactor;
 
 import static com.google.common.base.Verify.verify;
-import static com.google.common.base.Verify.verifyNotNull;
 import static java.util.Objects.requireNonNull;
 
 import java.util.ArrayList;
@@ -23,7 +22,6 @@ import org.opendaylight.mdsal.binding.model.api.GeneratedType;
 import org.opendaylight.mdsal.binding.model.api.type.builder.GeneratedTypeBuilder;
 import org.opendaylight.mdsal.binding.model.ri.BindingTypes;
 import org.opendaylight.yangtools.yang.common.QName;
-import org.opendaylight.yangtools.yang.common.QNameModule;
 import org.opendaylight.yangtools.yang.model.api.AddedByUsesAware;
 import org.opendaylight.yangtools.yang.model.api.CopyableNode;
 import org.opendaylight.yangtools.yang.model.api.meta.EffectiveStatement;
@@ -43,7 +41,6 @@ import org.opendaylight.yangtools.yang.model.api.stmt.ListEffectiveStatement;
 import org.opendaylight.yangtools.yang.model.api.stmt.NotificationEffectiveStatement;
 import org.opendaylight.yangtools.yang.model.api.stmt.OutputEffectiveStatement;
 import org.opendaylight.yangtools.yang.model.api.stmt.RpcEffectiveStatement;
-import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier;
 import org.opendaylight.yangtools.yang.model.api.stmt.SchemaTreeEffectiveStatement;
 import org.opendaylight.yangtools.yang.model.api.stmt.TypedefEffectiveStatement;
 import org.opendaylight.yangtools.yang.model.api.stmt.UsesEffectiveStatement;
@@ -55,19 +52,93 @@ import org.slf4j.LoggerFactory;
  * A composite generator. Composite generators may contain additional children, which end up being mapped into
  * the naming hierarchy 'under' the composite generator. To support this use case, each composite has a Java package
  * name assigned.
+ *
+ * <p>
+ * State tracking for resolution of children to their original declaration, i.e. back along the 'uses' and 'augment'
+ * axis. This is quite convoluted because we are traversing the generator tree recursively in the iteration order of
+ * children, but actual dependencies may require resolution in a different order, for example in the case of:
+ * <pre>
+ *   container foo {
+ *     uses bar {             // A
+ *       augment bar {        // B
+ *         container xyzzy;   // C
+ *       }
+ *     }
+ *
+ *     grouping bar {
+ *       container bar {      // D
+ *         uses baz;          // E
+ *       }
+ *     }
+ *
+ *     grouping baz {
+ *       leaf baz {           // F
+ *         type string;
+ *       }
+ *     }
+ *   }
+ *
+ *   augment /foo/bar/xyzzy { // G
+ *     leaf xyzzy {           // H
+ *       type string;
+ *     }
+ *   }
+ * </pre>
+ *
+ * <p>
+ * In this case we have three manifestations of 'leaf baz' -- marked A, E and F in the child iteration order. In order
+ * to perform a resolution, we first have to determine that F is the original definition, then establish that E is using
+ * the definition made by F and finally establish that A is using the definition made by F.
+ *
+ * <p>
+ * Dealing with augmentations is harder still, because we need to attach them to the original definition, hence for the
+ * /foo/bar container at A, we need to understand that its original definition is at D and we need to attach the augment
+ * at B to D. Futhermore we also need to establish that the augmentation at G attaches to container defined in C, so
+ * that the 'leaf xyzzy' existing as /foo/bar/xyzzy/xyzzy under C has its original definition at H.
+ *
+ * <p>
+ * Finally realize that the augment at G can actually exist in a different module and is shown in this example only
+ * the simplified form. That also means we could encounter G well before 'container foo' as well as we can have multiple
+ * such augments sprinkled across multiple modules having the same dependency rules as between C and G -- but they still
+ * have to form a directed acyclic graph and we partially deal with those complexities by having modules sorted by their
+ * dependencies.
+ *
+ * <p>
+ * For further details see {@link #linkOriginalGenerator()} and {@link #linkOriginalGeneratorRecursive()}, which deal
+ * with linking original instances in the tree iteration order. The part dealing with augment attachment lives mostly
+ * in {@link AugmentRequirement}.
  */
 abstract class AbstractCompositeGenerator<T extends EffectiveStatement<?, ?>> extends AbstractExplicitGenerator<T> {
     private static final Logger LOG = LoggerFactory.getLogger(AbstractCompositeGenerator.class);
 
+    // FIXME: we want to allocate this lazily to lower memory footprint
     private final @NonNull CollisionDomain domain = new CollisionDomain(this);
     private final List<Generator> children;
 
+    /**
+     * List of {@code augment} statements targeting this generator. This list is maintained only for the primary
+     * incarnation. This list is an evolving entity until after we have finished linkage of original statements. It is
+     * expected to be stable at the start of {@code step 2} in {@link GeneratorReactor#execute(TypeBuilderFactory)}.
+     */
     private List<AbstractAugmentGenerator> augments = List.of();
+
+    /**
+     * List of {@code grouping} statements this statement references. This field is set once by
+     * {@link #linkUsesDependencies(GeneratorContext)}.
+     */
     private List<GroupingGenerator> groupings;
 
-    // Performance optimization: if this is true, we have ascertained our original definition as well that of all our
-    // children
-    private boolean originalsResolved;
+    /**
+     * List of composite children which have not been recursively processed. This may become a mutable list when we
+     * have some children which have not completed linking. Once we have completed linking of all children, including
+     * {@link #unlinkedChildren}, this will be set to {@code null}.
+     */
+    private List<AbstractCompositeGenerator<?>> unlinkedComposites = List.of();
+    /**
+     * List of children which have not had their original linked. This list starts of as null. When we first attempt
+     * linkage, it becomes non-null.
+     */
+    private List<Generator> unlinkedChildren;
 
     AbstractCompositeGenerator(final T statement) {
         super(statement);
@@ -146,16 +217,41 @@ abstract class AbstractCompositeGenerator<T extends EffectiveStatement<?, ?>> ex
     }
 
     final void linkUsesDependencies(final GeneratorContext context) {
-        // We are resolving 'uses' statements to their corresponding 'grouping' definitions
+        // We are establishing two linkages here:
+        // - we are resolving 'uses' statements to their corresponding 'grouping' definitions
+        // - we propagate those groupings as anchors to any augment statements, which takes out some amount of guesswork
+        //   from augment+uses resolution case, as groupings know about their immediate augments as soon as uses linkage
+        //   is resolved
         final List<GroupingGenerator> tmp = new ArrayList<>();
         for (EffectiveStatement<?, ?> stmt : statement().effectiveSubstatements()) {
             if (stmt instanceof UsesEffectiveStatement) {
-                tmp.add(context.resolveTreeScoped(GroupingGenerator.class, ((UsesEffectiveStatement) stmt).argument()));
+                final UsesEffectiveStatement uses = (UsesEffectiveStatement) stmt;
+                final GroupingGenerator grouping = context.resolveTreeScoped(GroupingGenerator.class, uses.argument());
+                tmp.add(grouping);
+
+                // Trigger resolution of uses/augment statements. This looks like guesswork, but there may be multiple
+                // 'augment' statements in a 'uses' statement and keeping a ListMultimap here seems wasteful.
+                for (Generator gen : this) {
+                    if (gen instanceof UsesAugmentGenerator) {
+                        ((UsesAugmentGenerator) gen).resolveGrouping(uses, grouping);
+                    }
+                }
             }
         }
         groupings = List.copyOf(tmp);
     }
 
+    final void startUsesAugmentLinkage(final List<AugmentRequirement> requirements) {
+        for (Generator child : children) {
+            if (child instanceof UsesAugmentGenerator) {
+                requirements.add(((UsesAugmentGenerator) child).startLinkage());
+            }
+            if (child instanceof AbstractCompositeGenerator) {
+                ((AbstractCompositeGenerator<?>) child).startUsesAugmentLinkage(requirements);
+            }
+        }
+    }
+
     final void addAugment(final AbstractAugmentGenerator augment) {
         if (augments.isEmpty()) {
             augments = new ArrayList<>(2);
@@ -163,24 +259,73 @@ abstract class AbstractCompositeGenerator<T extends EffectiveStatement<?, ?>> ex
         augments.add(requireNonNull(augment));
     }
 
-    @Override
-    long linkOriginalGenerator() {
-        if (originalsResolved) {
-            return 0;
+    /**
+     * Attempt to link the generator corresponding to the original definition for this generator's statements as well as
+     * to all child generators.
+     *
+     * @return Progress indication
+     */
+    final @NonNull LinkageProgress linkOriginalGeneratorRecursive() {
+        if (unlinkedComposites == null) {
+            // We have unset this list (see below), and there is nothing left to do
+            return LinkageProgress.DONE;
         }
 
-        long remaining = super.linkOriginalGenerator();
-        if (remaining == 0) {
-            for (Generator child : children) {
+        if (unlinkedChildren == null) {
+            unlinkedChildren = children.stream()
+                .filter(AbstractExplicitGenerator.class::isInstance)
+                .map(child -> (AbstractExplicitGenerator<?>) child)
+                .collect(Collectors.toList());
+        }
+
+        var progress = LinkageProgress.NONE;
+        if (!unlinkedChildren.isEmpty()) {
+            // Attempt to make progress on child linkage
+            final var it = unlinkedChildren.iterator();
+            while (it.hasNext()) {
+                final var child = it.next();
                 if (child instanceof AbstractExplicitGenerator) {
-                    remaining += ((AbstractExplicitGenerator<?>) child).linkOriginalGenerator();
+                    if (((AbstractExplicitGenerator<?>) child).linkOriginalGenerator()) {
+                        progress = LinkageProgress.SOME;
+                        it.remove();
+
+                        // If this is a composite generator we need to process is further
+                        if (child instanceof AbstractCompositeGenerator) {
+                            if (unlinkedComposites.isEmpty()) {
+                                unlinkedComposites = new ArrayList<>();
+                            }
+                            unlinkedComposites.add((AbstractCompositeGenerator<?>) child);
+                        }
+                    }
                 }
             }
-            if (remaining == 0) {
-                originalsResolved = true;
+
+            if (unlinkedChildren.isEmpty()) {
+                // Nothing left to do, make sure any previously-allocated list can be scavenged
+                unlinkedChildren = List.of();
             }
         }
-        return remaining;
+
+        // Process children of any composite children we have.
+        final var it = unlinkedComposites.iterator();
+        while (it.hasNext()) {
+            final var tmp = it.next().linkOriginalGeneratorRecursive();
+            if (tmp != LinkageProgress.NONE) {
+                progress = LinkageProgress.SOME;
+            }
+            if (tmp == LinkageProgress.DONE) {
+                it.remove();
+            }
+        }
+
+        if (unlinkedChildren.isEmpty() && unlinkedComposites.isEmpty()) {
+            // All done, set the list to null to indicate there is nothing left to do in this generator or any of our
+            // children.
+            unlinkedComposites = null;
+            return LinkageProgress.DONE;
+        }
+
+        return progress;
     }
 
     @Override
@@ -188,20 +333,29 @@ abstract class AbstractCompositeGenerator<T extends EffectiveStatement<?, ?>> ex
         return (AbstractCompositeGenerator<?>) super.getOriginal();
     }
 
-    final @NonNull OriginalLink getOriginalChild(final QName childQName) {
+    @Override
+    final AbstractCompositeGenerator<?> tryOriginal() {
+        return (AbstractCompositeGenerator<?>) super.tryOriginal();
+    }
+
+    final @Nullable OriginalLink originalChild(final QName childQName) {
         // First try groupings/augments ...
-        final AbstractExplicitGenerator<?> found = findInferredGenerator(childQName);
+        var found = findInferredGenerator(childQName);
         if (found != null) {
             return OriginalLink.partial(found);
         }
 
         // ... no luck, we really need to start looking at our origin
-        final AbstractExplicitGenerator<?> prev = verifyNotNull(previous(),
-            "Failed to find %s in scope of %s", childQName, this);
+        final var prev = previous();
+        if (prev != null) {
+            final QName prevQName = childQName.bindTo(prev.getQName().getModule());
+            found = prev.findSchemaTreeGenerator(prevQName);
+            if (found != null) {
+                return  found.originalLink();
+            }
+        }
 
-        final QName prevQName = childQName.bindTo(prev.getQName().getModule());
-        return verifyNotNull(prev.findSchemaTreeGenerator(prevQName),
-            "Failed to find child %s (proxy for %s) in %s", prevQName, childQName, prev).originalLink();
+        return null;
     }
 
     @Override
@@ -210,73 +364,42 @@ abstract class AbstractCompositeGenerator<T extends EffectiveStatement<?, ?>> ex
         return found != null ? found : findInferredGenerator(qname);
     }
 
-    private @Nullable AbstractExplicitGenerator<?> findInferredGenerator(final QName qname) {
-        // First search our local groupings ...
-        for (GroupingGenerator grouping : groupings) {
-            final AbstractExplicitGenerator<?> gen = grouping.findSchemaTreeGenerator(
-                qname.bindTo(grouping.statement().argument().getModule()));
+    final @Nullable AbstractAugmentGenerator findAugmentForGenerator(final QName qname) {
+        for (var augment : augments) {
+            final var gen = augment.findSchemaTreeGenerator(qname);
             if (gen != null) {
-                return gen;
+                return augment;
             }
         }
-        // ... next try local augments, which may have groupings themselves
-        for (AbstractAugmentGenerator augment : augments) {
-            final AbstractExplicitGenerator<?> gen = augment.findSchemaTreeGenerator(qname);
+        return null;
+    }
+
+    final @Nullable GroupingGenerator findGroupingForGenerator(final QName qname) {
+        for (GroupingGenerator grouping : groupings) {
+            final var gen = grouping.findSchemaTreeGenerator(qname.bindTo(grouping.statement().argument().getModule()));
             if (gen != null) {
-                return gen;
+                return grouping;
             }
         }
         return null;
     }
 
-    final @NonNull AbstractExplicitGenerator<?> resolveSchemaNode(final SchemaNodeIdentifier path) {
-        // This is not quite straightforward. 'path' works on top of schema tree, which is instantiated view. Since we
-        // do not generate duplicate instantiations along 'uses' path, findSchemaTreeGenerator() would satisfy our
-        // request by returning a child of the source 'grouping'.
-        //
-        // When that happens, our subsequent lookups need to adjust the namespace being looked up to the grouping's
-        // namespace... except for the case when the step is actually an augmentation, in which case we must not make
-        // that adjustment.
-        //
-        // Hence we deal with this lookup recursively, dropping namespace hints when we cross into groupings.
-        return resolveSchemaNode(path.getNodeIdentifiers().iterator(), null);
-    }
-
-    private @NonNull AbstractExplicitGenerator<?> resolveSchemaNode(final Iterator<QName> qnames,
-            final @Nullable QNameModule localNamespace) {
-        final QName qname = qnames.next();
-
-        // First try local augments, as those are guaranteed to match namespace exactly
-        for (AbstractAugmentGenerator augment : augments) {
-            final AbstractExplicitGenerator<?> gen = augment.findSchemaTreeGenerator(qname);
+    private @Nullable AbstractExplicitGenerator<?> findInferredGenerator(final QName qname) {
+        // First search our local groupings ...
+        for (var grouping : groupings) {
+            final var gen = grouping.findSchemaTreeGenerator(qname.bindTo(grouping.statement().argument().getModule()));
             if (gen != null) {
-                return resolveNext(gen, qnames, null);
+                return gen;
             }
         }
-
-        // Second try local groupings, as those perform their own adjustment
-        for (GroupingGenerator grouping : groupings) {
-            final QNameModule ns = grouping.statement().argument().getModule();
-            final AbstractExplicitGenerator<?> gen = grouping.findSchemaTreeGenerator(qname.bindTo(ns));
+        // ... next try local augments, which may have groupings themselves
+        for (var augment : augments) {
+            final var gen = augment.findSchemaTreeGenerator(qname);
             if (gen != null) {
-                return resolveNext(gen, qnames, ns);
+                return gen;
             }
         }
-
-        // Lastly try local statements adjusted with namespace, if applicable
-        final QName lookup = localNamespace == null ? qname : qname.bindTo(localNamespace);
-        final AbstractExplicitGenerator<?> gen = verifyNotNull(super.findSchemaTreeGenerator(lookup),
-            "Failed to find %s as %s in %s", qname, lookup, this);
-        return resolveNext(gen, qnames, localNamespace);
-    }
-
-    private static @NonNull AbstractExplicitGenerator<?> resolveNext(final @NonNull AbstractExplicitGenerator<?> gen,
-            final Iterator<QName> qnames, final QNameModule localNamespace) {
-        if (qnames.hasNext()) {
-            verify(gen instanceof AbstractCompositeGenerator, "Unexpected generator %s", gen);
-            return ((AbstractCompositeGenerator<?>) gen).resolveSchemaNode(qnames, localNamespace);
-        }
-        return gen;
+        return null;
     }
 
     /**
@@ -404,7 +527,7 @@ abstract class AbstractCompositeGenerator<T extends EffectiveStatement<?, ?>> ex
                 final UsesEffectiveStatement uses = (UsesEffectiveStatement) stmt;
                 for (EffectiveStatement<?, ?> usesSub : uses.effectiveSubstatements()) {
                     if (usesSub instanceof AugmentEffectiveStatement) {
-                        tmpAug.add(new UsesAugmentGenerator((AugmentEffectiveStatement) usesSub, this));
+                        tmpAug.add(new UsesAugmentGenerator((AugmentEffectiveStatement) usesSub, uses, this));
                     }
                 }
             } else {
@@ -414,7 +537,9 @@ abstract class AbstractCompositeGenerator<T extends EffectiveStatement<?, ?>> ex
         }
 
         // Sort augments and add them last. This ensures child iteration order always reflects potential
-        // interdependencies, hence we do not need to worry about them.
+        // interdependencies, hence we do not need to worry about them. This is extremely important, as there are a
+        // number of places where we would have to either move the logic to parent statement and explicitly filter/sort
+        // substatements to establish this order.
         tmpAug.sort(AbstractAugmentGenerator.COMPARATOR);
         tmp.addAll(tmpAug);
 
index 69a2f217e51e242d9408940958e1f232935ec471..fca1b5bacb8cad53b94b1e42d473a8bc8e11e34f 100644 (file)
@@ -85,15 +85,14 @@ public abstract class AbstractExplicitGenerator<T extends EffectiveStatement<?,
     }
 
     /**
-     * Attempt to link the generator corresponding to the original definition for this generator's statements as well as
-     * to all child generators.
+     * Attempt to link the generator corresponding to the original definition for this generator.
      *
-     * @return Number of generators that remain unlinked.
+     * @return {@code true} if this generator is linked
      */
-    long linkOriginalGenerator() {
+    final boolean linkOriginalGenerator() {
         if (orig != null) {
             // Original already linked
-            return 0;
+            return true;
         }
 
         if (prev == null) {
@@ -102,27 +101,32 @@ public abstract class AbstractExplicitGenerator<T extends EffectiveStatement<?,
             if (!isAddedByUses() && !isAugmenting()) {
                 orig = prev = this;
                 LOG.trace("Linked {} to self", this);
-                return 0;
+                return true;
+            }
+
+            final var link = getParent().originalChild(getQName());
+            if (link == null) {
+                LOG.trace("Cannot link {} yet", this);
+                return false;
             }
 
-            final var link = getParent().getOriginalChild(getQName());
             prev = link.previous();
             orig = link.original();
             if (orig != null) {
                 LOG.trace("Linked {} to {} original {}", this, prev, orig);
-                return 0;
+                return true;
             }
 
             LOG.trace("Linked {} to intermediate {}", this, prev);
-            return 1;
+            return false;
         }
 
         orig = prev.originalLink().original();
         if (orig != null) {
             LOG.trace("Linked {} to original {}", this, orig);
-            return 0;
+            return true;
         }
-        return 1;
+        return false;
     }
 
     /**
@@ -144,6 +148,10 @@ public abstract class AbstractExplicitGenerator<T extends EffectiveStatement<?,
         return verifyNotNull(orig, "Generator %s does not have linkage to original instance resolved", this);
     }
 
+    @Nullable AbstractExplicitGenerator<?> tryOriginal() {
+        return orig;
+    }
+
     /**
      * Return the link towards the original generator.
      *
@@ -161,6 +169,10 @@ public abstract class AbstractExplicitGenerator<T extends EffectiveStatement<?,
     }
 
     @Nullable AbstractExplicitGenerator<?> findSchemaTreeGenerator(final QName qname) {
+        return findLocalSchemaTreeGenerator(qname);
+    }
+
+    final @Nullable AbstractExplicitGenerator<?> findLocalSchemaTreeGenerator(final QName qname) {
         for (Generator child : this) {
             if (child instanceof AbstractExplicitGenerator) {
                 final AbstractExplicitGenerator<?> gen = (AbstractExplicitGenerator<?>) child;
diff --git a/binding/mdsal-binding-generator/src/main/java/org/opendaylight/mdsal/binding/generator/impl/reactor/AugmentRequirement.java b/binding/mdsal-binding-generator/src/main/java/org/opendaylight/mdsal/binding/generator/impl/reactor/AugmentRequirement.java
new file mode 100644 (file)
index 0000000..ee1b940
--- /dev/null
@@ -0,0 +1,128 @@
+/*
+ * Copyright (c) 2022 PANTHEON.tech, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.mdsal.binding.generator.impl.reactor;
+
+import static com.google.common.base.Verify.verify;
+import static com.google.common.base.Verify.verifyNotNull;
+import static java.util.Objects.requireNonNull;
+
+import java.util.HashSet;
+import java.util.Iterator;
+import java.util.Set;
+import org.eclipse.jdt.annotation.NonNull;
+import org.opendaylight.yangtools.concepts.Mutable;
+import org.opendaylight.yangtools.yang.common.QName;
+import org.opendaylight.yangtools.yang.common.QNameModule;
+
+/**
+ * Class tracking state of resolution of an {@code augment} statement's target generator.
+ *
+ * <p>
+ * This is not quite straightforward. 'path' works on top of schema tree, which is instantiated view. Since we
+ * do not generate duplicate instantiations along 'uses' path, findSchemaTreeGenerator() would satisfy our
+ * request by returning a child of the source 'grouping'.
+ *
+ * <p>
+ * When that happens, our subsequent lookups need to adjust the namespace being looked up to the grouping's
+ * namespace... except for the case when the step is actually an augmentation, in which case we must not make
+ * that adjustment.
+ *
+ * <p>
+ * Hence we deal with this lookup recursively, dropping namespace hints when we cross into groupings. Note we
+ * take an initial hint -- which UsesAugmentGenerator provides, but ModuleAugmentGenerator does not -- and that
+ * accounts for the difference.
+ */
+final class AugmentRequirement implements Mutable {
+    private final @NonNull Set<QNameModule> squashNamespaces = new HashSet<>(4);
+    private final @NonNull AbstractAugmentGenerator augment;
+    private final @NonNull Iterator<QName> remaining;
+
+    private @NonNull AbstractCompositeGenerator<?> target;
+    private QNameModule localNamespace;
+    private QName qname;
+
+    private AugmentRequirement(final AbstractAugmentGenerator augment, final AbstractCompositeGenerator<?> target) {
+        this.augment = requireNonNull(augment);
+        this.target = requireNonNull(target);
+        remaining = augment.statement().argument().getNodeIdentifiers().iterator();
+        qname = remaining.next();
+    }
+
+    AugmentRequirement(final ModuleAugmentGenerator augment, final ModuleGenerator module) {
+        this((AbstractAugmentGenerator) augment, module);
+    }
+
+    AugmentRequirement(final UsesAugmentGenerator augment, final GroupingGenerator grouping) {
+        this((AbstractAugmentGenerator) augment, grouping);
+        // Starting in a grouping: squash namespace references to the grouping's namespace
+        localNamespace = grouping.getQName().getModule();
+        squashNamespaces.add(qname.getModule());
+    }
+
+    @NonNull LinkageProgress resolve() {
+        return qname == null ? resolveAsTarget() : resolveAsChild();
+    }
+
+    private @NonNull LinkageProgress resolveAsTarget() {
+        // Resolved requirement, if we also have original we end resolution here and now
+        final var original = target.tryOriginal();
+        if (original != null) {
+            augment.setTargetGenerator(original);
+            original.addAugment(augment);
+            return LinkageProgress.DONE;
+        }
+        return LinkageProgress.NONE;
+    }
+
+    private @NonNull LinkageProgress resolveAsChild() {
+        // First try local statements without adjustment
+        var gen = target.findLocalSchemaTreeGenerator(qname);
+        if (gen != null) {
+            return progressTo(gen);
+        }
+
+        // Second try local augments, as those are guaranteed to match namespace exactly
+        final var aug = target.findAugmentForGenerator(qname);
+        if (aug != null) {
+            return moveTo(aug);
+        }
+
+        // Third try local groupings, as those perform their own adjustment
+        final var grp = target.findGroupingForGenerator(qname);
+        if (grp != null) {
+            squashNamespaces.add(qname.getModule());
+            localNamespace = grp.getQName().getModule();
+            return moveTo(grp);
+        }
+
+        // Lastly try local statements adjusted with namespace, if applicable
+        gen = target.findLocalSchemaTreeGenerator(squashNamespaces.contains(qname.getModule())
+            ? qname.bindTo(verifyNotNull(localNamespace)) : qname);
+        if (gen != null) {
+            return progressTo(gen);
+        }
+        return LinkageProgress.NONE;
+    }
+
+    private @NonNull LinkageProgress moveTo(final @NonNull AbstractCompositeGenerator<?> newTarget) {
+        target = newTarget;
+        return tryProgress();
+    }
+
+    private @NonNull LinkageProgress progressTo(final @NonNull AbstractExplicitGenerator<?> newTarget) {
+        verify(newTarget instanceof AbstractCompositeGenerator, "Unexpected generator %s", newTarget);
+        target = (AbstractCompositeGenerator<?>) newTarget;
+        qname = remaining.hasNext() ? remaining.next() : null;
+        return tryProgress();
+    }
+
+    private @NonNull LinkageProgress tryProgress() {
+        final var progress = resolve();
+        return progress != LinkageProgress.NONE ? progress : LinkageProgress.SOME;
+    }
+}
index 8eec089c175edcb0565bffd9954f8e03b851de5e..4cfde54f751f866bd885059b4500016198aad7e6 100644 (file)
@@ -21,6 +21,7 @@ import java.util.Deque;
 import java.util.Iterator;
 import java.util.List;
 import java.util.Map;
+import java.util.function.Function;
 import java.util.stream.Collectors;
 import org.eclipse.jdt.annotation.NonNull;
 import org.eclipse.jdt.annotation.Nullable;
@@ -122,33 +123,48 @@ public final class GeneratorReactor extends GeneratorContext implements Mutable
         // Start measuring time...
         final Stopwatch sw = Stopwatch.createStarted();
 
-        // Step 1a: walk all composite generators and resolve 'uses' statements to the corresponding grouping node,
-        //          establishing implied inheritance ...
+        // Step 1a: Walk all composite generators and resolve 'uses' statements to the corresponding grouping generator,
+        //          establishing implied inheritance. During this walk we maintain 'stack' to aid this process.
+        //          This indirectly triggers resolution of UsesAugmentGenerators' targets by hooking a requirement
+        //          on the resolved grouping's child nodes as needed.
         linkUsesDependencies(children);
 
-        // Step 1b: ... and also link augments and their targets in a separate pass, as we need groupings fully resolved
-        //          before we attempt augmentation lookups ...
+        // Step 1b: Walk all module generators and start ModuleAugmentGenerators' target resolution by linking the first
+        //          step of each 'augment' statement to its corresponding instantiated site.
+        //          Then start all UsesAugmentGenerators' target resolution.
+        final var augments = new ArrayList<AugmentRequirement>();
         for (ModuleGenerator module : children) {
-            for (Generator child : module) {
-                if (child instanceof ModuleAugmentGenerator) {
-                    ((ModuleAugmentGenerator) child).linkAugmentationTarget(this);
+            for (Generator gen : module) {
+                if (gen instanceof ModuleAugmentGenerator) {
+                    augments.add(((ModuleAugmentGenerator) gen).startLinkage(this));
                 }
             }
         }
+        for (ModuleGenerator module : children) {
+            module.startUsesAugmentLinkage(augments);
+        }
+        LOG.trace("Processing linkage of {} augment generators", augments.size());
 
         // Step 1c: ... finally establish linkage along the reverse uses/augment axis. This is needed to route generated
         //          type manifestations (isAddedByUses/isAugmenting) to their type generation sites. Since generator
         //          tree iteration order does not match dependencies, we may need to perform multiple passes.
-        long unlinkedOriginals = Long.MAX_VALUE;
-        do {
-            long remaining = 0;
-            for (ModuleGenerator module : children) {
-                remaining += module.linkOriginalGenerator();
+        for (ModuleGenerator module : children) {
+            verify(module.linkOriginalGenerator(), "Module %s failed to link", module);
+        }
+
+        final var unlinkedModules = new ArrayList<>(children);
+        while (true) {
+            final boolean progress =
+                progressAndClean(unlinkedModules, ModuleGenerator::linkOriginalGeneratorRecursive)
+                // not '||' because we need the side-effects, which would get short-circuited
+                | progressAndClean(augments, AugmentRequirement::resolve);
+
+            if (augments.isEmpty() && unlinkedModules.isEmpty()) {
+                break;
             }
-            verify(remaining < unlinkedOriginals, "Failed to make progress on linking of remaining %s originals",
-                remaining);
-            unlinkedOriginals = remaining;
-        } while (unlinkedOriginals != 0);
+
+            verify(progress, "Failed to make progress on linking of original generators");
+        }
 
         /*
          * Step 2: link typedef statements, so that typedef's 'type' axis is fully established
@@ -343,6 +359,23 @@ public final class GeneratorReactor extends GeneratorContext implements Mutable
         }
     }
 
+    private static <T> boolean progressAndClean(final List<T> items, final Function<T, LinkageProgress> function) {
+        boolean progress = false;
+
+        final var it = items.iterator();
+        while (it.hasNext()) {
+            final var tmp = function.apply(it.next());
+            if (tmp != LinkageProgress.NONE) {
+                progress = true;
+            }
+            if (tmp == LinkageProgress.DONE) {
+                it.remove();
+            }
+        }
+
+        return progress;
+    }
+
     private void linkDependencies(final Iterable<? extends Generator> parent) {
         for (Generator child : parent) {
             if (child instanceof AbstractDependentGenerator) {
diff --git a/binding/mdsal-binding-generator/src/main/java/org/opendaylight/mdsal/binding/generator/impl/reactor/LinkageProgress.java b/binding/mdsal-binding-generator/src/main/java/org/opendaylight/mdsal/binding/generator/impl/reactor/LinkageProgress.java
new file mode 100644 (file)
index 0000000..23a2818
--- /dev/null
@@ -0,0 +1,26 @@
+/*
+ * Copyright (c) 2022 PANTHEON.tech, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.mdsal.binding.generator.impl.reactor;
+
+/**
+ * Enumeration of progress indicators.
+ */
+enum LinkageProgress {
+    /**
+     * No progress made and there is work to do.
+     */
+    NONE,
+    /**
+     * Some progress made, but there is more work to do.
+     */
+    SOME,
+    /**
+     * There is no more work to do.
+     */
+    DONE;
+}
index f4c01a3e7e0f38bab1a9135925e0eb0620033a9b..ee590e72bf2db69aabe15d06229d3b18be0489e9 100644 (file)
@@ -7,8 +7,8 @@
  */
 package org.opendaylight.mdsal.binding.generator.impl.reactor;
 
+import org.eclipse.jdt.annotation.NonNull;
 import org.opendaylight.yangtools.yang.model.api.stmt.AugmentEffectiveStatement;
-import org.opendaylight.yangtools.yang.model.api.stmt.SchemaNodeIdentifier;
 
 /**
  * Generator corresponding to a {@code augment} statement used as a child of a {@code module} statement.
@@ -18,14 +18,8 @@ final class ModuleAugmentGenerator extends AbstractAugmentGenerator {
         super(statement, parent);
     }
 
-    @Override
-    void loadTargetGenerator() {
-        throw new UnsupportedOperationException();
-    }
-
-    void linkAugmentationTarget(final GeneratorContext context) {
-        final SchemaNodeIdentifier path = statement().argument();
-        final ModuleGenerator module = context.resolveModule(path.firstNodeIdentifier().getModule());
-        setTargetGenerator(module.resolveSchemaNode(path));
+    @NonNull AugmentRequirement startLinkage(final GeneratorContext context) {
+        return new AugmentRequirement(this,
+            context.resolveModule(statement().argument().firstNodeIdentifier().getModule()));
     }
 }
index f8a9eb7fb2ca843465ae44cf1723147fdbb011ef..81c93f723ef157a94728f4682905a654982ef719 100644 (file)
@@ -7,26 +7,43 @@
  */
 package org.opendaylight.mdsal.binding.generator.impl.reactor;
 
+import static com.google.common.base.Verify.verify;
+import static com.google.common.base.Verify.verifyNotNull;
+import static java.util.Objects.requireNonNull;
+
+import org.eclipse.jdt.annotation.NonNull;
 import org.opendaylight.yangtools.yang.model.api.stmt.AugmentEffectiveStatement;
+import org.opendaylight.yangtools.yang.model.api.stmt.UsesEffectiveStatement;
 
 /**
  * Generator corresponding to a {@code augment} statement used as a child of a {@code uses} statement.
  */
 final class UsesAugmentGenerator extends AbstractAugmentGenerator {
-    UsesAugmentGenerator(final AugmentEffectiveStatement statement, final AbstractCompositeGenerator<?> parent) {
+    private final UsesEffectiveStatement uses;
+
+    private GroupingGenerator grouping;
+
+    UsesAugmentGenerator(final AugmentEffectiveStatement statement, final UsesEffectiveStatement uses,
+            final AbstractCompositeGenerator<?> parent) {
         super(statement, parent);
+        this.uses = requireNonNull(uses);
+    }
+
+    void resolveGrouping(final UsesEffectiveStatement resolvedUses, final GroupingGenerator resolvedGrouping) {
+        if (resolvedUses == uses) {
+            verify(grouping == null, "Attempted to re-resolve grouping of %s", this);
+            grouping = requireNonNull(resolvedGrouping);
+        }
     }
 
-    @Override
-    void loadTargetGenerator() {
+    @NonNull AugmentRequirement startLinkage() {
         // Here we are going in the opposite direction of RFC7950, section 7.13:
         //
         //    The effect of a "uses" reference to a grouping is that the nodes
         //    defined by the grouping are copied into the current schema tree and
         //    are then updated according to the "refine" and "augment" statements.
         //
-        // Our parent here is *not* the uses statement, but rather the statement which contains uses -- and its
-        // getSchemaTreeGenerator() is well equipped to deal with the namespace hopping needed to perform the lookups
-        setTargetGenerator(getParent().resolveSchemaNode(statement().argument()));
+        // Our parent here is *not* the 'uses' statement, but rather the statement which contains it.
+        return new AugmentRequirement(this, verifyNotNull(grouping, "Unresolved grouping in %s", this));
     }
 }
diff --git a/binding/mdsal-binding-generator/src/test/java/org/opendaylight/mdsal/binding/generator/impl/Mdsal718Test.java b/binding/mdsal-binding-generator/src/test/java/org/opendaylight/mdsal/binding/generator/impl/Mdsal718Test.java
new file mode 100644 (file)
index 0000000..cd71ef9
--- /dev/null
@@ -0,0 +1,22 @@
+/*
+ * Copyright (c) 2022 PANTHEON.tech, s.r.o. and others.  All rights reserved.
+ *
+ * This program and the accompanying materials are made available under the
+ * terms of the Eclipse Public License v1.0 which accompanies this distribution,
+ * and is available at http://www.eclipse.org/legal/epl-v10.html
+ */
+package org.opendaylight.mdsal.binding.generator.impl;
+
+import static org.junit.Assert.assertEquals;
+
+import org.junit.Test;
+import org.opendaylight.yangtools.yang.test.util.YangParserTestUtils;
+
+public class Mdsal718Test {
+    @Test
+    public void testModuleUsesAugmentLinking() {
+        final var generatedTypes = DefaultBindingGenerator.generateFor(
+            YangParserTestUtils.parseYangResourceDirectory("/mdsal718"));
+        assertEquals(10, generatedTypes.size());
+    }
+}
diff --git a/binding/mdsal-binding-generator/src/test/resources/mdsal718/bar.yang b/binding/mdsal-binding-generator/src/test/resources/mdsal718/bar.yang
new file mode 100644 (file)
index 0000000..5dd6013
--- /dev/null
@@ -0,0 +1,6 @@
+module bar {
+  namespace bar;
+  prefix bar;
+
+  container bar;
+}
diff --git a/binding/mdsal-binding-generator/src/test/resources/mdsal718/baz.yang b/binding/mdsal-binding-generator/src/test/resources/mdsal718/baz.yang
new file mode 100644 (file)
index 0000000..37b6990
--- /dev/null
@@ -0,0 +1,15 @@
+module baz {
+  namespace baz;
+  prefix baz;
+
+  import foo { prefix foo; }
+  import bar { prefix bar; }
+
+  augment /bar:bar {
+    uses foo:foo {
+      augment foo/nested {
+        container baz;
+      }
+    }
+  }
+}
diff --git a/binding/mdsal-binding-generator/src/test/resources/mdsal718/foo.yang b/binding/mdsal-binding-generator/src/test/resources/mdsal718/foo.yang
new file mode 100644 (file)
index 0000000..fba948f
--- /dev/null
@@ -0,0 +1,10 @@
+module foo {
+  namespace foo;
+  prefix foo;
+
+  grouping foo {
+    container foo {
+      container nested;
+    }
+  }
+}
diff --git a/binding/mdsal-binding-generator/src/test/resources/mdsal718/xyzzy.yang b/binding/mdsal-binding-generator/src/test/resources/mdsal718/xyzzy.yang
new file mode 100644 (file)
index 0000000..a8e7b3d
--- /dev/null
@@ -0,0 +1,11 @@
+module xyzzy {
+  namespace xyzzy;
+  prefix xyzzy;
+
+  import bar { prefix bar; }
+  import baz { prefix baz; }
+
+  augment /bar:bar/baz:foo/baz:nested/baz:baz {
+    container xyzzy;
+  }
+}