diff --git a/.gitlab-ci.yml b/.gitlab-ci.yml
index 07648c5824a9aa6c5d34534ca9a2186946474525..4526552a83f85c1affdbda45416e52eb8671913c 100644
--- a/.gitlab-ci.yml
+++ b/.gitlab-ci.yml
@@ -73,6 +73,8 @@ stages:
# - dast
- staging
- canary
+ - jar
+ - publish
- production
- incremental rollout 10%
- incremental rollout 25%
@@ -127,17 +129,71 @@ trampoline-feedback:
expire_in: 1 week
checkstyle:
- stage: test
+# image: $CI_REGISTRY/$CI_PROJECT_PATH/$CI_COMMIT_REF_NAME:$CI_COMMIT_SHA
image: wukl/auta-build:0.1.0
+ stage: test
script:
- ./gradlew checkstyleMain checkstyleTest
spotbugs:
- stage: test
+# image: $CI_REGISTRY/$CI_PROJECT_PATH/$CI_COMMIT_REF_NAME:$CI_COMMIT_SHA
image: wukl/auta-build:0.1.0
+ stage: test
script:
- ./gradlew :spotbugsMain :core:spotbugsMain :worker:spotbugsMain
+jar-core:
+# image: $CI_REGISTRY/$CI_PROJECT_PATH/$CI_COMMIT_REF_NAME:$CI_COMMIT_SHA
+ image: wukl/auta-build:0.1.0
+ stage: jar
+ script:
+ - ./gradlew bootJar
+ artifacts:
+ paths:
+ - core/build/libs/core-*.jar
+ expire_in: 1h
+
+jar-worker:
+# image: $CI_REGISTRY/$CI_PROJECT_PATH/$CI_COMMIT_REF_NAME:$CI_COMMIT_SHA
+ image: wukl/auta-build:0.1.0
+ stage: jar
+ script:
+ - ./gradlew :worker:jar
+ artifacts:
+ paths:
+ - worker/build/libs/worker-*.jar
+ expire_in: 1h
+
+publish-core:
+ image: registry.gitlab.com/gitlab-org/cluster-integration/auto-build-image/master:stable
+ variables:
+ DOCKER_TLS_CERTDIR: ""
+ services:
+ - docker:19.03.5-dind
+ only:
+ - branches
+ stage: publish
+ dependencies:
+ - jar-core
+ script:
+ - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
+ - devops/build-slim-image core $CI_COMMIT_REF_NAME
+
+publish-worker:
+ image: registry.gitlab.com/gitlab-org/cluster-integration/auto-build-image/master:stable
+ variables:
+ DOCKER_TLS_CERTDIR: ""
+ services:
+ - docker:19.03.5-dind
+ only:
+ - branches
+ stage: publish
+ dependencies:
+ - jar-worker
+ script:
+ - docker login -u $CI_REGISTRY_USER -p $CI_REGISTRY_PASSWORD $CI_REGISTRY
+ - devops/build-slim-image worker $CI_COMMIT_REF_NAME
+
# Override DAST job to exclude master branch
#dast:
# except:
diff --git a/devops/build-slim-image b/devops/build-slim-image
new file mode 100755
index 0000000000000000000000000000000000000000..3670308e02fa9f4844389702d5fa7f757fde87c6
--- /dev/null
+++ b/devops/build-slim-image
@@ -0,0 +1,44 @@
+#!/bin/bash
+set -e
+
+COMPONENT=$1
+TAG=$2
+
+if [ -z "$COMPONENT" ] || [ "$COMPONENT" = all ]
+then
+ ./$0 core $TAG
+ ./$0 worker $TAG
+ exit
+fi
+
+if [ -n "$TAG" ]
+then
+ TAG=-$TAG
+fi
+
+VERSION=$(grep -o " version = '[^']*'" build.gradle | cut -d "'" -f 2 | tr -d '\n')
+
+/bin/echo -e "\033[33;1mReleasing AuTA $COMPONENT v$VERSION$TAG\033[0m"
+
+if [ $COMPONENT = core ]
+then
+ ARGS=
+else
+ ARGS='host=$COREADDRESS name=$WORKERNAME api-token=$APIKEY api-protocol=$APIPROTOCOL api-port=$APIPORT docker-api=$DOCKERAPI'
+fi
+
+docker build --no-cache --pull -t $CI_REGISTRY/$CI_PROJECT_PATH/$COMPONENT -f slim-$COMPONENT.Dockerfile . \
+ --build-arg VERSION=$VERSION --build-arg COMPONENT=$COMPONENT --build-arg ARGS="$ARGS"
+
+if [ "$TAG" = -local ]
+then
+ exit
+fi
+
+if [ "$TAG" = -latest ] || [ "$TAG" = -master ]
+then
+ docker push $CI_REGISTRY/$CI_PROJECT_PATH/$COMPONENT
+else
+ docker tag $CI_REGISTRY/$CI_PROJECT_PATH/$COMPONENT $CI_REGISTRY/$CI_PROJECT_PATH/$COMPONENT:$VERSION$TAG
+ docker push $CI_REGISTRY/$CI_PROJECT_PATH/$COMPONENT:$VERSION$TAG
+fi
diff --git a/src/main/java/nl/tudelft/ewi/auta/common/model/metric/MetricName.java b/src/main/java/nl/tudelft/ewi/auta/common/model/metric/MetricName.java
index 08df3757d2f7f9038de7a456303b3b6a90bbf248..1cfe53ddf9077c5aff8292157f3beed1d042f06d 100644
--- a/src/main/java/nl/tudelft/ewi/auta/common/model/metric/MetricName.java
+++ b/src/main/java/nl/tudelft/ewi/auta/common/model/metric/MetricName.java
@@ -393,7 +393,12 @@ public enum MetricName {
/**
* SpotBugs/FindBugs complaints.
*/
- SPOTBUGS(Metric.class);
+ SPOTBUGS(Metric.class),
+
+ /**
+ * Call graph formatted as an SVG.
+ */
+ SVG_FLOW_GRAPH(StringMetric.class);
/**
* The type of the metric produced by instances with this name.
diff --git a/worker/src/main/antlr/nl/tudelft/ewi/auta/checker/grammar/AttAsm.g4 b/worker/src/main/antlr/nl/tudelft/ewi/auta/checker/grammar/AttAsm.g4
index 09c170843d154bb01242ca680781b9658a33e8c3..d4a96d72569bb4db7dc3f6e016206764fc5c5a07 100644
--- a/worker/src/main/antlr/nl/tudelft/ewi/auta/checker/grammar/AttAsm.g4
+++ b/worker/src/main/antlr/nl/tudelft/ewi/auta/checker/grammar/AttAsm.g4
@@ -151,6 +151,11 @@ LineComment
-> channel(HIDDEN)
;
+LineCommentCStyle
+ : '//' ~'\n'*
+ -> channel(HIDDEN)
+ ;
+
Whitespace
: [ \t]+
-> skip
diff --git a/worker/src/main/java/nl/tudelft/ewi/auta/checker/asm/AttAsmAnalyzer.java b/worker/src/main/java/nl/tudelft/ewi/auta/checker/asm/AttAsmAnalyzer.java
index 0af74976aadefda83aeb49fe45f46e5402e24633..a2be70939835554c5601932f041c34609a7647a1 100644
--- a/worker/src/main/java/nl/tudelft/ewi/auta/checker/asm/AttAsmAnalyzer.java
+++ b/worker/src/main/java/nl/tudelft/ewi/auta/checker/asm/AttAsmAnalyzer.java
@@ -1,56 +1,37 @@
package nl.tudelft.ewi.auta.checker.asm;
+import java.io.ByteArrayOutputStream;
import java.io.IOException;
+import java.nio.charset.StandardCharsets;
import java.util.Arrays;
+import java.util.Collections;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
-import java.util.function.Function;
+import net.sourceforge.plantuml.FileFormat;
+import net.sourceforge.plantuml.FileFormatOption;
+import net.sourceforge.plantuml.SourceStringReader;
import nl.tudelft.ewi.auta.checker.EntityAnalyzer;
import nl.tudelft.ewi.auta.common.model.entity.FileEntity;
import nl.tudelft.ewi.auta.common.model.metric.IntegerMetric;
import nl.tudelft.ewi.auta.common.model.metric.MetricName;
+import nl.tudelft.ewi.auta.common.model.metric.StringMetric;
import nl.tudelft.ewi.auta.common.model.metric.StringSetMetric;
import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
import nl.tudelft.ewi.auta.checker.grammar.AttAsmLexer;
import nl.tudelft.ewi.auta.checker.grammar.AttAsmParser;
import nl.tudelft.ewi.auta.common.model.metric.MetricCriteriaScript;
+import javax.annotation.Nullable;
+
/**
* An analyzer for AT&T-style x86_64 assembly.
*/
public class AttAsmAnalyzer extends EntityAnalyzer {
- private static final Logger logger = LoggerFactory.getLogger(AttAsmAnalyzer.class);
-
- /**
- * The set of functions used to extract metrics from an assembly info instance.
- */
- private static final Map<MetricName, Function<AttAsmReader.Info, Object>> METRIC_EXTRACTORS;
-
- static {
- METRIC_EXTRACTORS = new HashMap<>();
-
- METRIC_EXTRACTORS.put(MetricName.COMMENTED_LINE_COUNT, info -> {
- final var map = new HashMap<>();
-
- map.put("comments", info.getCommentCount());
- map.put("instructions", info.getInstructionCount());
-
- return map;
- });
-
- METRIC_EXTRACTORS.put(
- MetricName.ASSEMBLY_RECURSION_TARGET,
- AttAsmReader.Info::getRecursionTargets
- );
- }
-
/**
* The assembly reader.
*/
@@ -89,6 +70,75 @@ public class AttAsmAnalyzer extends EntityAnalyzer {
MetricName.ASSEMBLY_INSTRUCTION_COUNT));
victim.addMetric(new StringSetMetric(info.getRecursionTargets(),
MetricName.ASSEMBLY_RECURSION_TARGET));
+
+ try (var os = new ByteArrayOutputStream()) {
+ final var pumlReader = new SourceStringReader(this.formatAsPuml(info));
+ pumlReader.generateImage(os, new FileFormatOption(FileFormat.SVG));
+ victim.addMetric(new StringMetric(
+ os.toString(StandardCharsets.UTF_8), MetricName.SVG_FLOW_GRAPH
+ ));
+ }
+ }
+
+ /**
+ * Formats the flow graph as PlantUML.
+ *
+ * @param info the file info
+ *
+ * @return the call graph as PlantUML
+ */
+ private String formatAsPuml(final AttAsmReader.Info info) {
+ final var builder = new StringBuilder("@startuml\n");
+
+ info.getBlocks().forEach(b -> {
+ final var name = b.getName();
+ final var sname = this.makePumlSafe(name);
+ builder.append("state \"").append(name).append("\" as ").append(sname)
+ .append('\n');
+ b.getInstructions().forEach(i ->
+ builder.append(sname).append(" : ").append(i).append('\n')
+ );
+ });
+
+ info.getBlocks().forEach(b -> {
+ final var sname = this.makePumlSafe(b.getName());
+ b.getCallTargets().forEach(t ->
+ builder.append(sname).append(" --> ")
+ .append(this.makePumlSafe(t)).append(" : call")
+ .append('\n'));
+
+ @Nullable
+ final var jumpTargets = b.getJumpTargets();
+ if (jumpTargets == null || jumpTargets.isRet()) {
+ return;
+ }
+
+ if (jumpTargets.isConditional()) {
+ builder.append(sname).append(" --> ")
+ .append(this.makePumlSafe(jumpTargets.getFalsyTarget())).append(" : false")
+ .append('\n')
+ .append(sname).append(" --> ")
+ .append(this.makePumlSafe(jumpTargets.getTruthyTarget())).append(" : true")
+ .append('\n');
+ } else {
+ builder.append(sname).append(" --> ")
+ .append(this.makePumlSafe(jumpTargets.getTarget()))
+ .append('\n');
+ }
+ });
+
+ return builder.append("@enduml\n").toString();
+ }
+
+ /**
+ * Translates a block label into one that is safe to use as a PlantUML label.
+ *
+ * @param name the unsafe label
+ *
+ * @return the safe label
+ */
+ private String makePumlSafe(final String name) {
+ return "S" + name.replaceAll("[.$\\-()?]", "_");
}
/**
@@ -108,7 +158,12 @@ public class AttAsmAnalyzer extends EntityAnalyzer {
*/
@Override
public Set<MetricName> getMetricNames() {
- return METRIC_EXTRACTORS.keySet();
+ return Set.of(
+ MetricName.COMMENTED_LINE_COUNT,
+ MetricName.ASSEMBLY_INSTRUCTION_COUNT,
+ MetricName.ASSEMBLY_RECURSION_TARGET,
+ MetricName.SVG_FLOW_GRAPH
+ );
}
/**
@@ -185,6 +240,15 @@ public class AttAsmAnalyzer extends EntityAnalyzer {
)
));
+ map.put(MetricName.SVG_FLOW_GRAPH, Collections.singletonList(
+ new MetricCriteriaScript("Emit as image",
+ "(() => svg => {\n"
+ + " // TODO: add support for script artifacts...\n"
+ + " info(EDU_STAFF, svg);\n"
+ + "})()\n"
+ )
+ ));
+
return map;
}
}
diff --git a/worker/src/main/java/nl/tudelft/ewi/auta/checker/asm/AttAsmReader.java b/worker/src/main/java/nl/tudelft/ewi/auta/checker/asm/AttAsmReader.java
index 65c7dbfbe25f48ca5265f0dee62f189846cddf22..e86ab4cf9675942437b8a661a6a94a807e5e8d44 100644
--- a/worker/src/main/java/nl/tudelft/ewi/auta/checker/asm/AttAsmReader.java
+++ b/worker/src/main/java/nl/tudelft/ewi/auta/checker/asm/AttAsmReader.java
@@ -1,17 +1,24 @@
package nl.tudelft.ewi.auta.checker.asm;
import java.util.ArrayList;
+import java.util.Collections;
+import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
+import java.util.Random;
import java.util.Set;
+import java.util.Stack;
import java.util.regex.Pattern;
+import nl.tudelft.ewi.auta.common.annotation.Unmodifiable;
import org.antlr.v4.runtime.CommonTokenStream;
+import org.antlr.v4.runtime.misc.Interval;
import org.antlr.v4.runtime.tree.ParseTreeWalker;
import nl.tudelft.ewi.auta.checker.grammar.AttAsmBaseListener;
import nl.tudelft.ewi.auta.checker.grammar.AttAsmLexer;
import nl.tudelft.ewi.auta.checker.grammar.AttAsmParser;
+import org.jetbrains.annotations.Contract;
import javax.annotation.Nullable;
@@ -24,17 +31,22 @@ public class AttAsmReader {
/**
* The virtual call target for incomplete instructions.
*/
- public static final String INVALID_CALL = "(invalid?)";
+ public static final String INVALID_JUMP = "(invalid?)";
/**
* The virtual call target for calls through registers.
*/
- public static final String DYNAMIC_CALL = "(dynamic?)";
+ public static final String DYNAMIC_JUMP = "(dynamic?)";
+
+ /**
+ * The virtual jump target used by return instructions.
+ */
+ private static final JumpTargets RET_TARGET = new JumpTargets("#ret");
/**
* A regular expression matching an empty line comment.
*/
- private static final String EMPTY_COMMENT_REGEX = "\\s*#\\s*$";
+ private static final String EMPTY_COMMENT_REGEX = "\\s*(#|//)\\s*$";
/**
* A pattern matching an empty line comment.
@@ -74,7 +86,35 @@ public class AttAsmReader {
/**
* Function call instructions.
*/
- private static final Set<String> CALL_INSTRUCTIONS = Set.of("call");
+ private static final Set<String> CALL_INSTRUCTIONS = Collections.singleton("call");
+
+ /**
+ * Unconditional jump instructions.
+ */
+ private static final Set<String> JMP_INSTRUCTIONS = Collections.singleton("jmp");
+
+ /**
+ * Conditional jump instructions.
+ */
+ private static final Set<String> JCC_INSTRUCTIONS = Set.of(
+ "ja", "jae", "jb", "jbe", "jc", "jcxz", "jecxz", "jrcxz", "je", "jg", "jge",
+ "jl", "jle", "jna", "jnae", "jnb", "jnbe", "jnc", "jne", "jng", "jnge", "jnl",
+ "jnle", "jno", "jnp", "jns", "jnz", "jo", "jp", "jpe", "jpo", "js"
+ );
+
+ /**
+ * Function return instructions.
+ */
+ private static final Set<String> RET_INSTRUCTIONS = Collections.singleton("ret");
+
+ /**
+ * The random number generator to generate fairly non-colliding anonymous block identifiers.
+ */
+ private final Random random;
+
+ public AttAsmReader(final Random random) {
+ this.random = random;
+ }
/**
* Analyzes a file parsed by the AT&T assembly grammar.
@@ -97,6 +137,20 @@ public class AttAsmReader {
return info;
}
+ /**
+ * Creates an anonymous block unreachable instructions are placed in.
+ *
+ * Technically these instructions do not have to be unreachable due to the nature of assembly,
+ * but such trickery is asking for trouble.
+ *
+ * @return a new anonymous block
+ */
+ @Contract(" -> new")
+ private Block generateAnonymousBlock() {
+ // Generate multiples of 2 so that each block has no direct neighbors.
+ return new Block("", "", this.random.nextLong() * 2);
+ }
+
/**
* Walks the AST, recording useful information along the way.
*
@@ -111,18 +165,52 @@ public class AttAsmReader {
public void exitInstruction(final AttAsmParser.InstructionContext ctx) {
++info.instructionCount;
- if (CALL_INSTRUCTIONS.contains(ctx.Symbol().getText().toLowerCase())) {
- info.lastBlock().callTargets.add(AttAsmReader.this.getCallTarget(ctx, info));
+ final var lastBlock = info.lastBlock();
+ final var start = ctx.start;
+ final var end = ctx.stop;
+ final var cs = start.getTokenSource().getInputStream();
+ lastBlock.instructions.add(cs.getText(
+ new Interval(start.getStartIndex(), end != null ? end.getStopIndex() : -1)
+ ));
+
+ final var instr = ctx.Symbol().getText().toLowerCase();
+ if (RET_INSTRUCTIONS.contains(instr)) {
+ // If this is a return, end the current block and move to anonymous space
+ lastBlock.jumpTargets = RET_TARGET;
+ info.blocks.add(AttAsmReader.this.generateAnonymousBlock());
+ }
+
+ final var jumpTarget = AttAsmReader.this.getJumpTarget(ctx, info);
+ if (CALL_INSTRUCTIONS.contains(instr)) {
+ // Function calls return to the current block (in theory)
+ lastBlock.callTargets.add(jumpTarget);
+ } else if (JMP_INSTRUCTIONS.contains(instr)) {
+ // Unconditional jumps end the current block
+ lastBlock.jumpTargets = new JumpTargets(jumpTarget);
+ info.blocks.add(AttAsmReader.this.generateAnonymousBlock());
+ } else if (JCC_INSTRUCTIONS.contains(instr)) {
+ // Conditional jumps split the flow in two
+ final var nextBlock = info.lastBlock().advanceId();
+ lastBlock.jumpTargets = new JumpTargets(nextBlock.getName(), jumpTarget);
+ info.blocks.add(nextBlock);
}
}
@Override
public void exitLabel(final AttAsmParser.LabelContext ctx) {
+ final var prevBlock = info.lastBlock();
+
if (ctx.getText().startsWith(".")) {
info.blocks.add(new Block(info.lastBlock().label, ctx.Symbol().getText()));
} else {
info.blocks.add(new Block(ctx.Symbol().getText(), null));
}
+
+ // If the previous block ended without jump instructions, simulate a jump to this
+ // block
+ if (prevBlock.jumpTargets == null) {
+ prevBlock.jumpTargets = new JumpTargets(info.lastBlock().getName());
+ }
}
}, parser.file());
}
@@ -132,17 +220,43 @@ public class AttAsmReader {
*
* This can not detect cross-file recursion.
*
- * @param info the info object to read class from and write conclusions to
+ * @param info the info object to read calls from and write conclusions to
*/
private void detectRecursion(final Info info) {
- final var visited = new HashSet<String>();
+ final var blocksByName = new HashMap<String, Block>();
+ info.getBlocks().forEach(block -> blocksByName.put(block.getName(), block));
+
+ for (final var iblock : info.getBlocks()) {
+ final var toVisit = new Stack<Block>();
+ final var visited = new HashSet<String>();
+ final var blocksCalled = new HashSet<String>();
+
+ toVisit.push(iblock);
+
+ while (!toVisit.empty()) {
+ System.out.println(toVisit);
- for (final var block : info.blocks) {
- visited.add(block.getName());
+ final var block = toVisit.pop();
+ if (visited.contains(block.getName())) {
+ continue;
+ }
- final var intersection = new HashSet<>(visited);
- intersection.retainAll(block.callTargets);
- info.recursionTargets.addAll(intersection);
+ visited.add(block.getName());
+ blocksCalled.addAll(block.callTargets);
+
+ if (block.jumpTargets != null) {
+ if (block.jumpTargets.isConditional()) {
+ toVisit.push(blocksByName.get(block.jumpTargets.getFalsyTarget()));
+ toVisit.push(blocksByName.get(block.jumpTargets.getTruthyTarget()));
+ } else if (!block.jumpTargets.isRet()) {
+ toVisit.push(blocksByName.get(block.jumpTargets.getTarget()));
+ }
+ }
+ }
+
+ if (blocksCalled.contains(iblock.getName())) {
+ info.recursionTargets.add(iblock.getName());
+ }
}
}
@@ -183,19 +297,19 @@ public class AttAsmReader {
}
/**
- * Extracts the call target from an instruction.
+ * Extracts the call or jump target from an instruction.
*
- * May return special values {@link #INVALID_CALL} or {@link #DYNAMIC_CALL} if the call target
+ * May return special values {@link #INVALID_JUMP} or {@link #DYNAMIC_JUMP} if the target
* is special.
* *
- * @param ctx the call instruction context
+ * @param ctx the call or jump instruction context
* @param info the info block used to find which block a local label belongs to
*
- * @return the call target
+ * @return the target
*/
- private String getCallTarget(final AttAsmParser.InstructionContext ctx, final Info info) {
+ private String getJumpTarget(final AttAsmParser.InstructionContext ctx, final Info info) {
if (ctx.getChildCount() == 1) {
- return INVALID_CALL;
+ return INVALID_JUMP;
}
final var operands = ctx.operands();
final var first = operands.expression(0).getChild(0);
@@ -211,7 +325,7 @@ public class AttAsmReader {
return name;
}
- return DYNAMIC_CALL;
+ return DYNAMIC_JUMP;
}
/**
@@ -274,12 +388,26 @@ public class AttAsmReader {
public Set<String> getRecursionTargets() {
return this.recursionTargets;
}
+
+ /**
+ * Returns the logical blocks in the assembly file.
+ *
+ * @return the blocks
+ */
+ @Unmodifiable
+ public List<Block> getBlocks() {
+ return Collections.unmodifiableList(this.blocks);
+ }
}
/**
* A logical block of assembly code.
+ *
+ * Blocks always have a label and always end with a jump or a return instructions. The label
+ * is derived from the label specified in the input, plus the number branches not taken
+ * within that block, called the block ID.
*/
- private static final class Block {
+ public static final class Block {
/**
* The top-level label of the block.
*/
@@ -291,11 +419,36 @@ public class AttAsmReader {
@Nullable
private final String subLabel;
+ /**
+ * The full name of the block, generated on demand.
+ */
+ @Nullable
+ private transient String fullName;
+
+ /**
+ * The internal block branch identifier.
+ *
+ * This is a unique counter for each block and increases for every jump not taken within
+ * this block.
+ */
+ private final long branchId;
+
/**
* The set of blocks this block calls.
*/
private final Set<String> callTargets = new HashSet<>();
+ /**
+ * The blocks this block jumps to at the end of its execution.
+ */
+ @Nullable
+ private JumpTargets jumpTargets;
+
+ /**
+ * The instructions in this block.
+ */
+ private final List<String> instructions = new ArrayList<>();
+
/**
* Creates a new block.
*
@@ -303,24 +456,190 @@ public class AttAsmReader {
* @param subLabel the sub-label
*/
private Block(final String label, final @Nullable String subLabel) {
+ this(label, subLabel, 0);
+ }
+
+ /**
+ * Creates a new block.
+ *
+ * @param label the top-level label
+ * @param subLabel the sub-label
+ * @param branchId the jump-not-taken counter for the block with this name
+ */
+ private Block(final String label, final @Nullable String subLabel, final long branchId) {
this.label = label;
this.subLabel = subLabel;
+ this.branchId = branchId;
}
/**
* Returns the logical name of the block.
*
* This is formed by taking the top-level label and appending a period and the sub-label,
- * if it exists.
+ * if it exists. If this block is not the primary branch, a dollar sign and a branch
+ * identifier are appended.
*
* @return the name
*/
- private String getName() {
+ @Contract(pure = true)
+ public String getName() {
+ if (this.fullName != null) {
+ return this.fullName;
+ }
+
if (this.subLabel == null) {
- return this.label;
+ if (this.branchId == 0) {
+ return this.label;
+ }
+ return this.label + '$' + this.branchId;
+ }
+
+ if (this.branchId == 0) {
+ return this.label + '.' + this.subLabel;
}
- return this.label + '.' + this.subLabel;
+ this.fullName = this.label + '.' + this.subLabel + '$' + this.branchId;
+ return this.fullName;
+ }
+
+ /**
+ * Returns the names of the functions this block calls.
+ *
+ * @return the call targets
+ */
+ @Contract(pure = true)
+ @Unmodifiable
+ public Set<String> getCallTargets() {
+ return Collections.unmodifiableSet(this.callTargets);
+ }
+
+ /**
+ * Returns the instructions in this block.
+ *
+ * @return the instructions
+ */
+ @Contract(pure = true)
+ @Unmodifiable
+ public List<String> getInstructions() {
+ return Collections.unmodifiableList(this.instructions);
+ }
+
+ /**
+ * Returns the jump targets this block ends with.
+ *
+ * @return the jump targets
+ */
+ @Nullable
+ @Contract(pure = true)
+ public JumpTargets getJumpTargets() {
+ return this.jumpTargets;
+ }
+
+ /**
+ * Creates a block with this block's label and the next branch ID.
+ *
+ * @return the next block
+ */
+ @Contract(" -> new")
+ private Block advanceId() {
+ return new Block(this.label, this.subLabel, this.branchId + 1);
+ }
+ }
+
+ /**
+ * A set of jump targets for the end of a block.
+ *
+ * A special jump target for blocks returning from a call is {@link #RET_TARGET}. There is only
+ * one instance of this target.
+ */
+ public static final class JumpTargets {
+ /**
+ * The target the block jumps to if the condition is false.
+ */
+ private final String falsyTarget;
+
+ /**
+ * The target the block jumps to if the condition is true.
+ */
+ private final String truthyTarget;
+
+ /**
+ * Creates a new jump target for a conditional jump.
+ *
+ * @param falsyTarget the target the block jumps to if the condition is false
+ * @param truthyTarget the target the block jumps to if the condition is true
+ */
+ @Contract(pure = true)
+ private JumpTargets(final String falsyTarget, final String truthyTarget) {
+ this.falsyTarget = falsyTarget;
+ this.truthyTarget = truthyTarget;
+ }
+
+ /**
+ * Creates a new jump target for an unconditional jump.
+ *
+ * @param target the target the block jumps to
+ */
+ @Contract(pure = true)
+ private JumpTargets(final String target) {
+ this.falsyTarget = target;
+ this.truthyTarget = target;
+ }
+
+ /**
+ * Checks whether these targets are for a conditional jump.
+ *
+ * @return {@code true} if these targets are for a conditional jump, {@code false} if this
+ * is a target for an unconditional jump or a return instruction
+ */
+ @Contract(pure = true)
+ public boolean isConditional() {
+ // noinspection StringEquality - identity is ensured by constructor
+ return this.falsyTarget != this.truthyTarget;
+ }
+
+ /**
+ * Checks whether this target is a return target.
+ *
+ * @return {@code true} if this target is a return target, {@code false} if this is a
+ * target for an unconditional jump or if these targets are for a conditional jump
+ */
+ @Contract(pure = true)
+ public boolean isRet() {
+ return this == RET_TARGET;
+ }
+
+ /**
+ * Returns the target for an unconditional jump.
+ *
+ * @return the target
+ */
+ @Contract(pure = true)
+ public String getTarget() {
+ assert !this.isRet() && !this.isConditional() : "target is not unconditional";
+ return this.falsyTarget;
+ }
+
+ /**
+ * Returns the target for a true conditional jump.
+ *
+ * @return the target
+ */
+ @Contract(pure = true)
+ public String getTruthyTarget() {
+ assert this.isConditional() : "target is not conditional";
+ return this.truthyTarget;
+ }
+
+ /**
+ * Returns the target for a false conditional jump.
+ *
+ * @return the target
+ */
+ @Contract(pure = true)
+ public String getFalsyTarget() {
+ assert this.isConditional() : "target is not conditional";
+ return this.falsyTarget;
}
}
}
diff --git a/worker/src/test/java/nl/tudelft/ewi/auta/checker/asm/AttAsmAnalyzerTest.java b/worker/src/test/java/nl/tudelft/ewi/auta/checker/asm/AttAsmAnalyzerTest.java
index 05d3692b6905f1c213cb1d22cccc6a05af2ebe5c..4c5f8058e2e6c16f92000d22e55f669a37703b2e 100644
--- a/worker/src/test/java/nl/tudelft/ewi/auta/checker/asm/AttAsmAnalyzerTest.java
+++ b/worker/src/test/java/nl/tudelft/ewi/auta/checker/asm/AttAsmAnalyzerTest.java
@@ -14,6 +14,7 @@ import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.HashSet;
+import java.util.Random;
import static org.assertj.core.api.Assertions.assertThat;
@@ -24,7 +25,7 @@ public class AttAsmAnalyzerTest {
@BeforeEach
public void before() throws IOException {
- this.analyzer = new AttAsmAnalyzer(new AttAsmReader());
+ this.analyzer = new AttAsmAnalyzer(new AttAsmReader(new Random(4)));
this.temp = Files.createTempFile("test-", ".s");
}
@@ -61,7 +62,7 @@ public class AttAsmAnalyzerTest {
this.analyzer.analyze(entity, "option");
assertThat(project.getChildren()).isNotEmpty();
final var child = project.getChildren().iterator().next();
- assertThat(child.getMetrics()).containsExactlyInAnyOrder(
+ assertThat(child.getMetrics()).contains(
new IntegerMetric(1, MetricName.COMMENTED_LINE_COUNT),
new IntegerMetric(1, MetricName.ASSEMBLY_INSTRUCTION_COUNT),
new StringSetMetric(new HashSet<>(), MetricName.ASSEMBLY_RECURSION_TARGET)
diff --git a/worker/src/test/java/nl/tudelft/ewi/auta/checker/asm/AttAsmReaderTest.java b/worker/src/test/java/nl/tudelft/ewi/auta/checker/asm/AttAsmReaderTest.java
index 122bdc60b380d654c738a5a81fb3caa0bcb00dd7..f4fbacb378dd135e451b8070ad2f32365719f552 100644
--- a/worker/src/test/java/nl/tudelft/ewi/auta/checker/asm/AttAsmReaderTest.java
+++ b/worker/src/test/java/nl/tudelft/ewi/auta/checker/asm/AttAsmReaderTest.java
@@ -6,15 +6,21 @@ import org.antlr.v4.runtime.CharStreams;
import org.antlr.v4.runtime.CommonTokenStream;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Random;
import static org.assertj.core.api.Assertions.assertThat;
public class AttAsmReaderTest {
+ private static final Logger logger = LoggerFactory.getLogger(AttAsmReaderTest.class);
+
private AttAsmReader reader;
@BeforeEach
public void before() {
- this.reader = new AttAsmReader();
+ this.reader = new AttAsmReader(new Random(4));
}
@Test