All Downloads are FREE. Search and download functionalities are using the official Maven repository.

com.google.javascript.jscomp.graph.LowestCommonAncestorFinder Maven / Gradle / Ivy

There is a newer version: 9.0.8
Show newest version
/*
 * Copyright 2008 The Closure Compiler 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 com.google.javascript.jscomp.graph;

import static com.google.common.base.Preconditions.checkArgument;
import static com.google.common.base.Preconditions.checkNotNull;
import static com.google.common.base.Preconditions.checkState;

import com.google.common.collect.ImmutableSet;
import com.google.errorprone.annotations.Immutable;
import com.google.javascript.jscomp.graph.DiGraph.DiGraphEdge;
import com.google.javascript.jscomp.graph.DiGraph.DiGraphNode;
import java.util.ArrayDeque;
import java.util.Arrays;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Set;

/**
 * Implements a lowest common ancestor search algorithm.
 *
 * 

The LCA of a set of nodes is the node that is an ancestor to all of them but is not an * ancestor of any other common ancestor. In a non-tree, There may be multiple LCAs for a given set * of search nodes. * *

In a cyclic graph, the LCAs may not be well defined. Within a cycle, all elements are both * above and below one another, so there is no uniquely lowest element. If the set of common * ancestors is rooted on a cycle, this implementation returns one or more elements of that cycle. * Those elements are chosen arbitrarily but deterministically (as long as the underlying graph has * deterministic iteration). */ public class LowestCommonAncestorFinder { /** * An abstraction for {@code LowestCommonAncestorFinder::new}. * *

This interface allows injection in tests. */ @FunctionalInterface public interface Factory { LowestCommonAncestorFinder create(DiGraph graph); } /** A "color" for a node, encoded as a combination of other colors using a one-hot scheme. */ @Immutable private static final class Color { /** * A set of commonly used colors to minimize allocations. * *

An array is used to prevent bounds checking overhead. */ private static final Color[] COMMON_COLOR_CACHE = new Color[2 << 5]; static { Arrays.setAll(COMMON_COLOR_CACHE, Color::new); } /** * A color that when mixed with any other color {@code x} returns {@code x}. * *

Mixing this color is idempotent. */ static final Color BLANK = checkNotNull(COMMON_COLOR_CACHE[0]); /** * A color for common ancestors that indicates that there are lower common ancestors. * *

Because this color sets its MSB high, it can never equal any other color. Also notice that * mixing this {@link Color} with any other produces this {@link Color} again; mixing it is * nullipotent. */ static final Color NOT_LOWEST = new Color(-1); static Color create(int bitset) { if (bitset < 0) { checkArgument(bitset == -1); return NOT_LOWEST; } else if (bitset < COMMON_COLOR_CACHE.length) { return COMMON_COLOR_CACHE[bitset]; } return new Color(bitset); } final int bitset; private Color(int bitset) { this.bitset = bitset; } Color mix(Color other) { if (this.bitset == other.bitset) { return this; } return create(this.bitset | other.bitset); } public boolean contains(Color other) { return (this.bitset & other.bitset) == other.bitset; } @Override @SuppressWarnings({"EqualsUnsafeCast"}) public boolean equals(Object other) { return this.bitset == ((Color) other).bitset; } @Override public int hashCode() { return this.bitset; } } private final DiGraph graph; private final LinkedHashMap, Color> searchColoring = new LinkedHashMap<>(); private final ArrayDeque> searchQueue = new ArrayDeque<>(); public LowestCommonAncestorFinder(DiGraph graph) { this.graph = graph; } /** * Execute a search on all the elements of {@code roots}. * *

This is a general-purpose, bare-bones implementation. There are lots of special case * optimizations that could be applied. */ public ImmutableSet findAll(Set roots) { // We reserved the MSB of each Color for bookkeeping. checkArgument(roots.size() <= Integer.SIZE - 1, "Too many roots."); checkState(this.searchColoring.isEmpty()); // In two's-complement, (2^n - 1) sets the lowest n bits high. Color allColor = Color.create((1 << roots.size()) - 1); /** * Paint up from each root using the color associated with that root. * *

When done, the set of common ancestors is the set of nodes painted `allColor`. */ int bitForRoot = 1; for (N root : roots) { DiGraphNode rootNode = this.graph.getNode(root); checkNotNull(rootNode, "Root not present in graph: %s", root); Color color = Color.create(bitForRoot); this.searchColoring.merge(rootNode, color, Color::mix); // Preserve any existing colors. this.paintAncestors(rootNode, color); bitForRoot <<= 1; } /** * For every common ancestor, paint all of its ancestors with a color indicating it is not the * lowest. */ this.searchColoring.forEach( (node, color) -> { if (color.equals(allColor)) { this.paintAncestors(node, Color.NOT_LOWEST); } }); ImmutableSet.Builder results = ImmutableSet.builder(); this.searchColoring.forEach( (node, color) -> { if (color.equals(allColor)) { results.add(node.getValue()); } }); this.searchColoring.clear(); this.searchQueue.clear(); return results.build(); } /** * Paint all nodes above {@code root} with {@code color}. * *

{@code root} itself will not have its color changed. {@code color} will be mixed with all * existing colors on ancestor nodes. */ private void paintAncestors(DiGraphNode root, Color color) { checkState(this.searchQueue.isEmpty()); this.searchQueue.addLast(root); while (!this.searchQueue.isEmpty()) { DiGraphNode curr = this.searchQueue.removeFirst(); List> parentEdges = curr.getInEdges(); for (DiGraphEdge parentEdge : parentEdges) { DiGraphNode parent = parentEdge.getSource(); if (parent.equals(root)) { continue; // Don't paint `root`. } Color oldColor = this.searchColoring.getOrDefault(parent, Color.BLANK); if (!oldColor.contains(color)) { // Only explore in directions that have not yet been painted. this.searchQueue.addLast(parent); this.searchColoring.put(parent, oldColor.mix(color)); } } } } }





© 2015 - 2024 Weber Informatics LLC | Privacy Policy