/*
* Copyright (c) 2016, Oracle and/or its affiliates.
*
* All rights reserved.
*
* Redistribution and use in source and binary forms, with or without modification, are
* permitted provided that the following conditions are met:
*
* 1. Redistributions of source code must retain the above copyright notice, this list of
* conditions and the following disclaimer.
*
* 2. Redistributions in binary form must reproduce the above copyright notice, this list of
* conditions and the following disclaimer in the documentation and/or other materials provided
* with the distribution.
*
* 3. Neither the name of the copyright holder nor the names of its contributors may be used to
* endorse or promote products derived from this software without specific prior written
* permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" AND ANY EXPRESS
* OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF
* MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE
* COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL,
* EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE
* GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED
* AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING
* NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED
* OF THE POSSIBILITY OF SUCH DAMAGE.
*/
package com.oracle.truffle.llvm.test.alpha;
import java.io.BufferedReader;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.PrintStream;
import java.nio.file.Path;
import java.util.Arrays;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
import com.oracle.truffle.api.frame.FrameDescriptor;
import com.oracle.truffle.api.frame.FrameSlot;
import com.oracle.truffle.api.source.Source;
import com.oracle.truffle.llvm.parser.BitcodeParserResult;
import com.oracle.truffle.llvm.parser.LLVMLifetimeAnalysis;
import com.oracle.truffle.llvm.parser.model.blocks.InstructionBlock;
import com.oracle.truffle.llvm.parser.model.functions.FunctionDefinition;
import com.oracle.truffle.llvm.parser.model.visitors.ModelVisitor;
import com.oracle.truffle.llvm.runtime.LLVMLogger;
import com.oracle.truffle.llvm.test.options.SulongTestOptions;
import com.oracle.truffle.llvm.test.util.LifetimeFileFormat;
import com.oracle.truffle.llvm.test.util.LifetimeFileParserEventListener;
public final class LifetimeAnalysisTest {
private final LifetimeFileFormat.Writer fileWriter;
private final Map<String, LLVMLifetimeAnalysis> referenceResults;
private LifetimeAnalysisTest(LifetimeFileFormat.Writer fileWriter, Map<String, LLVMLifetimeAnalysis> referenceResults) {
this.fileWriter = fileWriter;
this.referenceResults = referenceResults;
}
public static void test(Path bcFile, Path ltaFile, Path ltaGenDir) throws Throwable {
LifetimeFileFormat.Writer fileWriter;
Map<String, LLVMLifetimeAnalysis> referenceResults = null;
BufferedReader referenceFileReader;
if (SulongTestOptions.TEST.generateLifetimeReferenceOutput()) {
final File ltaGenDirFile = ltaGenDir.toFile();
if (!ltaGenDirFile.exists()) {
// noinspection ResultOfMethodCallIgnored
ltaGenDirFile.getParentFile().mkdirs();
final boolean fileCreated = ltaGenDirFile.createNewFile();
if (!fileCreated) {
throw new AssertionError();
}
}
fileWriter = new LifetimeFileFormat.Writer(new PrintStream(ltaGenDirFile));
} else {
fileWriter = null;
FileInputStream fis = new FileInputStream(ltaFile.toFile());
referenceFileReader = new BufferedReader(new InputStreamReader(fis));
referenceResults = parseReferenceResults(referenceFileReader);
}
new LifetimeAnalysisTest(fileWriter, referenceResults).test(bcFile);
}
private void test(Path bcFile) throws Throwable {
try {
LLVMLogger.info("original file: " + bcFile.toString());
final BitcodeParserResult parserResult = BitcodeParserResult.getFromSource(Source.newBuilder(bcFile.toFile().getAbsoluteFile()).build());
parserResult.getModel().accept(new ModelVisitor() {
@Override
public void ifVisitNotOverwritten(Object obj) {
}
@Override
public void visit(FunctionDefinition method) {
final String functionName = method.getName();
final LLVMLifetimeAnalysis lifetimes = LLVMLifetimeAnalysis.getResult(method, parserResult.getStackAllocation().getFrame(functionName),
parserResult.getPhis().getPhiMap(functionName));
if (SulongTestOptions.TEST.generateLifetimeReferenceOutput()) {
fileWriter.writeFunctionName(functionName);
fileWriter.writeBeginDead();
writeInstructionBlockVariables(lifetimes.getNullableBefore());
fileWriter.writeEndDead();
writeInstructionBlockVariables(lifetimes.getNullableAfter());
} else {
LLVMLifetimeAnalysis expected = referenceResults.get(functionName);
assertResultsEqual(functionName, expected, lifetimes, bcFile);
}
}
private void writeInstructionBlockVariables(Map<InstructionBlock, FrameSlot[]> beginDead) {
for (InstructionBlock b : beginDead.keySet()) {
fileWriter.writeBasicBlockIndent(b.getName());
for (FrameSlot slot : beginDead.get(b)) {
if (slot != null) {
fileWriter.writeVariableIndent(slot.getIdentifier());
}
}
}
}
});
} catch (Throwable e) {
throw e;
}
}
private static void assertResultsEqual(String functionName, LLVMLifetimeAnalysis expected, LLVMLifetimeAnalysis actual, Path bcFile) {
if (expected == null) {
LLVMLogger.unconditionalInfo(String.format("No reference result for test %s", bcFile.toFile().getAbsolutePath()));
return;
}
final Map<String, Set<String>> expectedBeginDead = getCommonFromInstructionBlocks(expected.getNullableBefore());
final Map<String, Set<String>> actualBeginDead = getCommonFromInstructionBlocks(actual.getNullableBefore());
assertMapsEqual(functionName, expectedBeginDead, actualBeginDead, bcFile);
final Map<String, Set<String>> expectedEndDead = getCommonFromInstructionBlocks(expected.getNullableAfter());
final Map<String, Set<String>> actualEndDead = getCommonFromInstructionBlocks(actual.getNullableAfter());
assertMapsEqual(functionName, expectedEndDead, actualEndDead, bcFile);
}
private static Map<String, Set<String>> getCommonFromInstructionBlocks(Map<InstructionBlock, FrameSlot[]> original) {
final Map<String, Set<String>> commonMap = new HashMap<>(original.size());
for (Map.Entry<InstructionBlock, FrameSlot[]> entry : original.entrySet()) {
final String name = getQuotedName(entry.getKey().getName());
final Set<String> slots = getQuotedNames(entry.getValue());
commonMap.put(name, slots);
}
return commonMap;
}
private static Set<String> getQuotedNames(FrameSlot[] slots) {
return Arrays.stream(slots).map(frameSlot -> getQuotedName((String) frameSlot.getIdentifier())).collect(Collectors.toSet());
}
private static String getQuotedName(String originalName) {
// make sure all names are quoted to avoid naming differences between parsers
if (originalName.charAt(1) == '"') {
return originalName;
} else {
return String.format("%%\"%s\"", originalName.substring(1));
}
}
private static void assertMapsEqual(String functionName, Map<String, Set<String>> expected, Map<String, Set<String>> actual, Path bcFile) {
if (expected.size() != actual.size()) {
throw new AssertionError(buildErrorMessage(functionName, String.format("Different Map Sizes, should be %d, but is %d!", expected.size(), actual.size()), bcFile, expected, actual));
}
for (final String name : expected.keySet()) {
if (!actual.containsKey(name)) {
throw new AssertionError(buildErrorMessage(functionName, String.format("Cannot find block %s in %s", name, asString(actual.keySet())), bcFile, expected, actual));
}
final Set<String> expectedFrameSlots = expected.get(name);
final Set<String> actualFrameSlots = actual.get(name);
if (!setsEqual(expectedFrameSlots, actualFrameSlots)) {
throw new AssertionError(
buildErrorMessage(functionName, String.format("Nullers do not match: should be %s, but are %s", asString(expectedFrameSlots), asString(actualFrameSlots)), bcFile,
expected, actual));
}
}
}
private static boolean setsEqual(Set<String> expected, Set<String> actual) {
for (String expectedString : expected) {
if (!actual.contains(expectedString)) {
return false;
}
}
return expected.size() == actual.size();
}
private static String asString(Set<String> names) {
return names.stream().collect(Collectors.joining(", ", "[", "]"));
}
private static String buildErrorMessage(String functionName, String message, Path bcFile, Map<String, Set<String>> expected, Map<String, Set<String>> actual) {
return String.format(
"Error in Function %s in File %s: %s\nexpected: \n%s\nactual: \n%s\n",
functionName,
bcFile.toFile().getAbsolutePath(),
message,
printResult(expected),
printResult(actual));
}
private static String printResult(Map<String, Set<String>> result) {
return result.entrySet().stream().map(e -> String.format(" %s : %s", e.getKey(), asString(e.getValue()))).collect(Collectors.joining(",\n", "{\n", "\n}"));
}
private static InstructionBlock createInstructionBlock(String name) {
InstructionBlock namedBlock = new InstructionBlock(null, 0);
namedBlock.setName(name);
return namedBlock;
}
private static Map<String, LLVMLifetimeAnalysis> parseReferenceResults(BufferedReader referenceFileReader) throws IOException {
Map<String, LLVMLifetimeAnalysis> results = new HashMap<>();
LifetimeFileFormat.parse(referenceFileReader, new LifetimeFileParserEventListener() {
private FrameDescriptor descr = new FrameDescriptor();
private Map<InstructionBlock, FrameSlot[]> beginDead = new HashMap<>();
private Map<InstructionBlock, FrameSlot[]> endDead = new HashMap<>();
Set<FrameSlot> slots = new HashSet<>();
private boolean deadAtBeginning = true;
@Override
public void startFile() {
}
@Override
public void endFile(String functionName) {
results.put(functionName, result(beginDead, endDead));
}
@Override
public void variableIndent(String variableName) {
slots.add(descr.findOrAddFrameSlot(variableName));
}
@Override
public void functionIndent(String functionName) {
results.put(functionName, result(beginDead, endDead));
beginDead = new HashMap<>();
endDead = new HashMap<>();
}
@Override
public void finishEntry(String block) {
String entryName = block.substring(1); // Remove '%' at beginning of name
if (deadAtBeginning) {
beginDead.put(createInstructionBlock(entryName), slots.toArray(new FrameSlot[slots.size()]));
} else {
endDead.put(createInstructionBlock(entryName), slots.toArray(new FrameSlot[slots.size()]));
}
slots.clear();
}
@Override
public void endDead() {
deadAtBeginning = false;
}
@Override
public void beginDead() {
deadAtBeginning = true;
}
});
return results;
}
private static LLVMLifetimeAnalysis result(Map<InstructionBlock, FrameSlot[]> beginDead, Map<InstructionBlock, FrameSlot[]> endDead) {
return new LLVMLifetimeAnalysis(beginDead, endDead);
}
}