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

org.aya.util.terck.CallGraph Maven / Gradle / Ivy

There is a newer version: 0.36.0
Show newest version
// Copyright (c) 2020-2023 Tesla (Yinsen) Zhang.
// Use of this source code is governed by the MIT license that can be found in the LICENSE.md file.
package org.aya.util.terck;

import kala.collection.immutable.ImmutableSeq;
import kala.collection.mutable.MutableLinkedHashMap;
import kala.collection.mutable.MutableList;
import kala.collection.mutable.MutableMap;
import kala.tuple.Tuple;
import kala.tuple.Tuple2;
import org.jetbrains.annotations.NotNull;

import java.util.function.BiConsumer;
import java.util.function.Consumer;

/**
 * Call graph is a multi-graph; each vertex represents a definition and each edge from vertex `f`
 * to vertex `g` represents a call to `g` within `f`. The edges are labeled with call matrices,
 * and can be labelled with several call matrices if there are several paths from `f` to `g`.
 *
 * @author kiva
 * @see CallMatrix
 */
public record CallGraph(
  @NotNull MutableMap>>> graph
) {
  public static  @NotNull CallGraph create() {
    return new CallGraph<>(MutableLinkedHashMap.of());
  }

  public void put(@NotNull CallMatrix matrix) {
    var caller = matrix.domain();
    var callee = matrix.codomain();
    var calls = graph.getOrPut(caller, MutableLinkedHashMap::of)
      .getOrPut(callee, MutableList::create);
    calls.append(matrix);
  }

  /** @return true if there's no edge */
  public boolean isEmpty() {
    return graph.allMatch((_, ts) -> ts.allMatch((_, t) -> t.isEmpty()));
  }

  /** completing a call graph is just finding its transitive closure */
  private static  @NotNull CallGraph complete(@NotNull CallGraph initial) {
    var step = initial;
    while (true) {
      var comb = indirect(initial, step);
      var tup = merge(comb, step);
      if (tup.component1().isEmpty()) return step; // no better matrices are found, we are complete
      step = tup.component2(); // got a partially completed call graph, try complete more
    }
  }

  /** find all indirect calls and combine them together */
  private static  @NotNull CallGraph indirect(@NotNull CallGraph initial, @NotNull CallGraph step) {
    var comb = CallGraph.create();
    initial.graph.forEach((_, codomains) -> codomains.forEach((_, mats) -> mats.forEach(mat -> {
      var indirect = step.graph.getOrNull(mat.codomain());
      if (indirect != null) indirect.forEach((_, indMats) -> indMats.forEach(ind -> {
        var combine = CallMatrix.combine(mat, ind);
        comb.put(combine);
      }));
    })));
    return comb;
  }

  /**
   * merge newly discovered indirect matrices with old ones.
   * CallGraph.hs
   */
  private static  @NotNull Tuple2, CallGraph> merge(
    @NotNull CallGraph comb, @NotNull CallGraph cs
  ) {
    var newG = CallGraph.create(); // all accepted new matrices go here, used for indicating whether we are done.
    var oldG = CallGraph.create(); // all old matrices and accepted new matrices go here
    forEachGraph(comb.graph, cs.graph,
      // If the matrix is really new (no old matrices describing the same call -- we find a new call path), accept it
      n -> {
        n.forEach(newG::put);
        n.forEach(oldG::put);
      },
      // If no new matrix is replacing the old one, keep the old one
      o -> o.forEach(oldG::put),
      // If `n` is replacing `o`, compare one by one
      (n, o) -> {
        // check if there's still old ones better than new ones...
        // note: the idea of "better" is not the same as "decrease more", see comments on `Selector.select()`
        var cmp = Selector.select(n.view(), o.view());
        cmp.component1().forEach(newG::put); // filtered really better new ones,
        cmp.component1().forEach(oldG::put); // ... and accept them.
        cmp.component2().forEach(oldG::put); // filtered old ones that still better than new ones.
      });
    return Tuple.of(newG, oldG);
  }

  public static  void forEachGraph(
    @NotNull MutableMap> a,
    @NotNull MutableMap> b,
    @NotNull Consumer inA,
    @NotNull Consumer inB,
    @NotNull BiConsumer both
  ) {
    forEachMap(a, b,
      v1 -> v1.forEach((_, v) -> inA.accept(v)),
      v2 -> v2.forEach((_, v) -> inB.accept(v)),
      (v1, v2) -> forEachMap(v1, v2, inA, inB, both));
  }

  public static  void forEachMap(
    @NotNull MutableMap a,
    @NotNull MutableMap b,
    @NotNull Consumer inA,
    @NotNull Consumer inB,
    @NotNull BiConsumer both
  ) {
    var union = MutableLinkedHashMap.of();
    a.forEach(union::put);
    b.forEach((k, bv) -> {
      var av = union.remove(k);
      if (av.isEmpty()) inB.accept(bv);
      else both.accept(av.get(), bv);
    });
    union.forEach((_, av) -> inA.accept(av));
  }

  /** find bad recursive calls in current SCC */
  public @NotNull ImmutableSeq> findBadRecursion() {
    var complete = complete(this);
    var bads = MutableList.>create();
    for (var key : complete.graph.keysView()) {
      var matrix = complete.graph.getOption(key)
        .flatMap(g -> g.getOption(key));
      if (matrix.isEmpty()) continue;
      // idempotent calls can never get worse after completion --- they are already at the bottom.
      var idempotent = matrix.get().view()
        .filter(m -> CallMatrix.combine(m, m).notWorseThan(m));
      // size-change principle: each idempotent call matrix must have a decreasing argument.
      var bad = idempotent
        .map(Diagonal::create)
        .filterNot(diag -> diag.diagonal().anyMatch(Relation::isDecreasing))
        .toImmutableSeq();
      if (bad.isNotEmpty()) bads.appendAll(bad);
    }
    return bads.toImmutableSeq();
  }
}




© 2015 - 2025 Weber Informatics LLC | Privacy Policy