From 8d6a3136e6c007052d741d9589695fd9b475c82d Mon Sep 17 00:00:00 2001 From: Damian Rovara <93778306+DRovara@users.noreply.github.com> Date: Thu, 19 Sep 2024 16:54:01 +0200 Subject: [PATCH] :sparkles: Upgrade Diagnosis Capabilities (#20) * test: :test_tube: Add test cases for this feature * feat: :sparkles: Data dependency checks now reach into custom gate definitions * feat: :sparkles: Getting interactions now also works inside custom gate definitions, but it is not yet possible to "inspect out" of a custom gate * feat: :sparkles: Data dependency diagnosis can now step out of the current function call, even if the diagnosis was started in the same scope Previously, data dependency analysis was only able to step outside of functions that were entered during the analysis (i.e. where the analysis knows from where the function was entered). Now, the parameter `includeCallers` can be passed to allow the analysis to step out. In this case, all possible callers will be included as dependencies. * feat: :sparkles: Dependency/Interaction analysis now also recognises uses of the full register (e.g. `x q` rather than `x q[0]`) * fix: :bug: Fix codecov issues * style: :recycle: Fix linter issues * fix: :bug: Fix bug that makes debugger access element -1 if no custom gates are defined --- include/backend/dd/DDSimDebug.hpp | 11 +- include/backend/dd/DDSimDiagnostics.hpp | 6 +- include/backend/diagnostics.h | 2 +- include/common/parsing/CodePreprocessing.hpp | 8 +- include/common/parsing/Utils.hpp | 2 + src/backend/dd/DDSimDebug.cpp | 96 ++++++++- src/backend/dd/DDSimDiagnostics.cpp | 201 ++++++++++++++++-- src/common/parsing/CodePreprocessing.cpp | 93 +++++--- src/common/parsing/Utils.cpp | 13 ++ src/frontend/cli/CliFrontEnd.cpp | 3 +- .../messages/set_breakpoints_dap_message.py | 2 - src/mqt/debug/pydebug.pyi | 2 +- src/python/InterfaceBindings.cpp | 34 +-- test/circuits/diagnose-with-jumps.qasm | 26 +++ test/circuits/runtime-interaction.qasm | 10 + .../data-dependencies-with-callers.qasm | 15 ++ test/python/test_diagnosis.py | 21 +- test/test_custom_code.cpp | 146 ++++++++++++- test/test_diagnostics.cpp | 99 ++++++++- 19 files changed, 704 insertions(+), 86 deletions(-) create mode 100644 test/circuits/diagnose-with-jumps.qasm create mode 100644 test/circuits/runtime-interaction.qasm create mode 100644 test/python/resources/diagnosis/data-dependencies-with-callers.qasm diff --git a/include/backend/dd/DDSimDebug.hpp b/include/backend/dd/DDSimDebug.hpp index 3696a5e..777b1f6 100644 --- a/include/backend/dd/DDSimDebug.hpp +++ b/include/backend/dd/DDSimDebug.hpp @@ -57,9 +57,10 @@ struct DDSimulationState { std::vector callReturnStack; std::map> callSubstitutions; std::vector> restoreCallReturnStack; - std::map> dataDependencies; + std::map>> dataDependencies; + std::map> functionCallers; std::set breakpoints; - std::vector> targetQubits; + std::vector> targetQubits; bool paused; @@ -127,8 +128,14 @@ bool checkAssertion(DDSimulationState* ddsim, std::unique_ptr& assertion); std::string getClassicalBitName(DDSimulationState* ddsim, size_t index); size_t variableToQubit(DDSimulationState* ddsim, const std::string& variable); +std::pair variableToQubitAt(DDSimulationState* ddsim, + const std::string& variable, + size_t instruction); bool isSubStateVectorLegal(const Statevector& full, std::vector& targetQubits); std::vector> getPartialTraceFromStateVector(const Statevector& sv, const std::vector& traceOut); + +std::vector getTargetVariables(DDSimulationState* ddsim, + size_t instruction); diff --git a/include/backend/dd/DDSimDiagnostics.hpp b/include/backend/dd/DDSimDiagnostics.hpp index f7ae200..4be6b59 100644 --- a/include/backend/dd/DDSimDiagnostics.hpp +++ b/include/backend/dd/DDSimDiagnostics.hpp @@ -8,6 +8,7 @@ #include #include #include +#include struct DDSimulationState; @@ -17,12 +18,15 @@ struct DDDiagnostics { DDSimulationState* simulationState; std::map> zeroControls; std::map> nonZeroControls; + + std::map>> actualQubits; }; size_t dddiagnosticsGetNumQubits(Diagnostics* self); size_t dddiagnosticsGetInstructionCount(Diagnostics* self); -Result dddiagnosticsInit([[maybe_unused]] Diagnostics* self); +Result dddiagnosticsInit(Diagnostics* self); Result dddiagnosticsGetDataDependencies(Diagnostics* self, size_t instruction, + bool includeCallers, bool* instructions); Result dddiagnosticsGetInteractions(Diagnostics* self, size_t beforeInstruction, size_t qubit, bool* qubitsAreInteracting); diff --git a/include/backend/diagnostics.h b/include/backend/diagnostics.h index 5bef392..0685234 100644 --- a/include/backend/diagnostics.h +++ b/include/backend/diagnostics.h @@ -24,7 +24,7 @@ struct Diagnostics { size_t (*getNumQubits)(Diagnostics* self); size_t (*getInstructionCount)(Diagnostics* self); Result (*getDataDependencies)(Diagnostics* self, size_t instruction, - bool* instructions); + bool includeCallers, bool* instructions); Result (*getInteractions)(Diagnostics* self, size_t beforeInstruction, size_t qubit, bool* qubitsAreInteracting); Result (*getZeroControlInstructions)(Diagnostics* self, bool* instructions); diff --git a/include/common/parsing/CodePreprocessing.hpp b/include/common/parsing/CodePreprocessing.hpp index 0855d71..b3d84db 100644 --- a/include/common/parsing/CodePreprocessing.hpp +++ b/include/common/parsing/CodePreprocessing.hpp @@ -5,8 +5,8 @@ #include #include #include -#include #include +#include #include struct Block { @@ -18,7 +18,7 @@ struct Instruction { size_t lineNumber; std::string code; std::unique_ptr assertion; - std::set targets; + std::vector targets; size_t originalCodeStartPosition; size_t originalCodeEndPosition; @@ -32,13 +32,13 @@ struct Instruction { std::map callSubstitution; - std::vector dataDependencies; + std::vector> dataDependencies; Block block; std::vector childInstructions; Instruction(size_t inputLineNumber, std::string inputCode, std::unique_ptr& inputAssertion, - std::set inputTargets, size_t startPos, + std::vector inputTargets, size_t startPos, size_t endPos, size_t successor, bool isFuncCall, std::string function, bool inFuncDef, bool isFuncDef, Block inputBlock); diff --git a/include/common/parsing/Utils.hpp b/include/common/parsing/Utils.hpp index 830ec8f..f0552e4 100644 --- a/include/common/parsing/Utils.hpp +++ b/include/common/parsing/Utils.hpp @@ -14,3 +14,5 @@ std::string replaceString(std::string str, const std::string& from, const std::string& to); std::string removeWhitespace(std::string str); + +bool variablesEqual(const std::string& v1, const std::string& v2); diff --git a/src/backend/dd/DDSimDebug.cpp b/src/backend/dd/DDSimDebug.cpp index 52e5b90..f90bd2b 100644 --- a/src/backend/dd/DDSimDebug.cpp +++ b/src/backend/dd/DDSimDebug.cpp @@ -24,6 +24,7 @@ #include #include #include +#include #include #include #include @@ -854,6 +855,52 @@ Result destroyDDSimulationState(DDSimulationState* self) { } //----------------------------------------------------------------------------------------- + +std::vector getTargetVariables(DDSimulationState* ddsim, + size_t instruction) { + std::vector result; + size_t parentFunction = -1ULL; + size_t i = instruction; + while (true) { + if (ddsim->functionDefinitions.find(i) != + ddsim->functionDefinitions.end()) { + parentFunction = i; + break; + } + if (ddsim->instructionTypes[i] == RETURN) { + break; + } + if (i == 0) { + break; + } + i--; + } + + const auto parameters = parentFunction != -1ULL + ? ddsim->targetQubits[parentFunction] + : std::vector{}; + for (const auto& target : ddsim->targetQubits[instruction]) { + if (std::find(parameters.begin(), parameters.end(), target) != + parameters.end()) { + result.push_back(target); + continue; + } + const auto foundRegister = + std::find_if(ddsim->qubitRegisters.begin(), ddsim->qubitRegisters.end(), + [target](const QubitRegisterDefinition& reg) { + return reg.name == target; + }); + if (foundRegister != ddsim->qubitRegisters.end()) { + for (size_t j = 0; j < foundRegister->size; j++) { + result.push_back(target + "[" + std::to_string(j) + "]"); + } + } else { + result.push_back(target); + } + } + return result; +} + size_t variableToQubit(DDSimulationState* ddsim, const std::string& variable) { auto declaration = replaceString(variable, " ", ""); declaration = replaceString(declaration, "\t", ""); @@ -892,6 +939,42 @@ size_t variableToQubit(DDSimulationState* ddsim, const std::string& variable) { throw std::runtime_error("Unknown variable name " + var); } +std::pair variableToQubitAt(DDSimulationState* ddsim, + const std::string& variable, + size_t instruction) { + size_t sweep = instruction; + size_t functionDef = -1ULL; + while (sweep < ddsim->instructionTypes.size()) { + if (std::find(ddsim->functionDefinitions.begin(), + ddsim->functionDefinitions.end(), + sweep) != ddsim->functionDefinitions.end()) { + functionDef = sweep; + break; + } + if (ddsim->instructionTypes[sweep] == RETURN) { + break; + } + sweep--; + } + + if (functionDef == -1ULL) { + // In the global scope, we can just use the register's index. + return {variableToQubit(ddsim, variable), functionDef}; + } + + // In a gate-local scope, we have to define qubit indices relative to the + // gate. + const auto& targets = ddsim->targetQubits[functionDef]; + + const auto found = std::find(targets.begin(), targets.end(), variable); + if (found == targets.end()) { + throw std::runtime_error("Unknown variable name " + variable); + } + + return {static_cast(std::distance(targets.begin(), found)), + functionDef}; +} + double complexMagnitude(Complex& c) { return std::sqrt(c.real * c.real + c.imaginary * c.imaginary); } @@ -1283,6 +1366,7 @@ std::string preprocessAssertionCode(const char* code, ddsim->qubitRegisters.clear(); ddsim->successorInstructions.clear(); ddsim->dataDependencies.clear(); + ddsim->functionCallers.clear(); ddsim->targetQubits.clear(); for (auto& instruction : instructions) { @@ -1293,7 +1377,17 @@ std::string preprocessAssertionCode(const char* code, ddsim->instructionEnds.push_back(instruction.originalCodeEndPosition); ddsim->dataDependencies.insert({instruction.lineNumber, {}}); for (const auto& dependency : instruction.dataDependencies) { - ddsim->dataDependencies[instruction.lineNumber].push_back(dependency); + ddsim->dataDependencies[instruction.lineNumber].emplace_back( + dependency.first, dependency.second); + } + if (instruction.isFunctionCall) { + const size_t successorInFunction = instruction.successorIndex; + const size_t functionIndex = successorInFunction - 1; + if (ddsim->functionCallers.find(functionIndex) == + ddsim->functionCallers.end()) { + ddsim->functionCallers.insert({functionIndex, {}}); + } + ddsim->functionCallers[functionIndex].insert(instruction.lineNumber); } // what exactly we do with each instruction depends on its type: diff --git a/src/backend/dd/DDSimDiagnostics.cpp b/src/backend/dd/DDSimDiagnostics.cpp index af1edbc..a89d1d8 100644 --- a/src/backend/dd/DDSimDiagnostics.cpp +++ b/src/backend/dd/DDSimDiagnostics.cpp @@ -9,6 +9,7 @@ #include #include #include +#include #include #include #include @@ -59,9 +60,90 @@ size_t dddiagnosticsGetInstructionCount(Diagnostics* self) { &ddd->simulationState->interface); } -Result dddiagnosticsInit([[maybe_unused]] Diagnostics* self) { return OK; } +Result dddiagnosticsInit(Diagnostics* self) { + auto* ddd = toDDDiagnostics(self); + ddd->zeroControls.clear(); + ddd->nonZeroControls.clear(); + ddd->actualQubits.clear(); + return OK; +} + +size_t findReturn(DDSimulationState* state, size_t instruction) { + size_t current = instruction; + while (state->instructionTypes[current] != RETURN) { + current++; + } + return current; +} + +void visitCall(DDSimulationState* ddsim, size_t current, size_t qubitIndex, + std::set& visited, std::set& toVisit) { + const auto gateStart = ddsim->successorInstructions[current]; + const auto gateDefinition = gateStart - 1; + const std::string stringToSearch = + ddsim->targetQubits[gateDefinition][qubitIndex]; + auto checkInstruction = findReturn(ddsim, gateStart); + while (checkInstruction >= gateStart) { + const auto found = + std::find(ddsim->targetQubits[checkInstruction].begin(), + ddsim->targetQubits[checkInstruction].end(), stringToSearch); + if (ddsim->instructionTypes[checkInstruction] != RETURN && + found != ddsim->targetQubits[checkInstruction].end()) { + if (visited.find(checkInstruction) == visited.end()) { + toVisit.insert(checkInstruction); + } + if (ddsim->instructionTypes[checkInstruction] == CALL) { + const auto position = + std::distance(ddsim->targetQubits[checkInstruction].begin(), found); + visitCall(ddsim, checkInstruction, static_cast(position), + visited, toVisit); + } + break; + } + if (checkInstruction == 0) { + break; + } + checkInstruction--; + } +} + +std::set getUnknownCallers(DDSimulationState* ddsim, + size_t instruction) { + std::set unknownCallers; + + std::set toVisit{}; + std::set visited{}; + + while (true) { + instruction--; + if (ddsim->functionDefinitions.find(instruction) != + ddsim->functionDefinitions.end()) { + unknownCallers.insert(instruction); + for (const auto caller : ddsim->functionCallers[instruction]) { + if (visited.find(caller) == visited.end()) { + toVisit.insert(caller); + } + } + } + + if (instruction == 0 || ddsim->instructionTypes[instruction] == RETURN || + ddsim->functionDefinitions.find(instruction) != + ddsim->functionDefinitions.end()) { + if (toVisit.empty()) { + break; + } + + instruction = *toVisit.begin(); + toVisit.erase(instruction); + visited.insert(instruction); + } + } + + return unknownCallers; +} Result dddiagnosticsGetDataDependencies(Diagnostics* self, size_t instruction, + bool includeCallers, bool* instructions) { auto* ddd = toDDDiagnostics(self); auto* ddsim = ddd->simulationState; @@ -69,14 +151,37 @@ Result dddiagnosticsGetDataDependencies(Diagnostics* self, size_t instruction, instructions, ddsim->interface.getInstructionCount(&ddsim->interface)); std::set toVisit{instruction}; std::set visited; + + // Stores all functions whose callers are unknown (because analysis started + // inside them) + const std::set unknownCallers = + includeCallers ? getUnknownCallers(ddsim, instruction) + : std::set{}; + while (!toVisit.empty()) { auto current = *toVisit.begin(); isDependency[current] = true; toVisit.erase(toVisit.begin()); visited.insert(current); + for (auto dep : ddsim->dataDependencies[current]) { - if (visited.find(dep) == visited.end()) { - toVisit.insert(dep); + const auto depInstruction = dep.first; + if (ddsim->instructionTypes[depInstruction] == NOP) { + continue; // We don't want variable declarations as dependencies. + } + if (visited.find(depInstruction) == visited.end()) { + toVisit.insert(depInstruction); + } + if (ddsim->instructionTypes[depInstruction] == CALL) { + visitCall(ddsim, depInstruction, dep.second, visited, toVisit); + } + } + + if (unknownCallers.find(current - 1) != unknownCallers.end()) { + for (auto caller : ddsim->functionCallers[current - 1]) { + if (visited.find(caller) == visited.end()) { + toVisit.insert(caller); + } } } } @@ -95,14 +200,20 @@ Result dddiagnosticsGetInteractions(Diagnostics* self, size_t beforeInstruction, while (found) { found = false; for (auto i = beforeInstruction - 1; i < beforeInstruction; i--) { + if (std::find(ddsim->functionDefinitions.begin(), + ddsim->functionDefinitions.end(), + i) != ddsim->functionDefinitions.end()) { + break; + } if (ddsim->instructionTypes[i] != SIMULATE && ddsim->instructionTypes[i] != CALL) { continue; } - auto& targets = ddsim->targetQubits[i]; + + auto targets = getTargetVariables(ddsim, i); std::set targetQubits; for (const auto& target : targets) { - targetQubits.insert(variableToQubit(ddsim, target)); + targetQubits.insert(variableToQubitAt(ddsim, target, i).first); } if (!std::none_of(targetQubits.begin(), targetQubits.end(), [&interactions](size_t elem) { @@ -150,6 +261,44 @@ size_t dddiagnosticsPotentialErrorCauses(Diagnostics* self, ErrorCause* output, return index; } +std::set getInteractionsAtRuntime(DDDiagnostics* ddd, size_t qubit) { + auto* ddsim = ddd->simulationState; + std::set interactions; + interactions.insert(qubit); + bool found = true; + + while (found) { + found = false; + + for (size_t i = 0; i < ddsim->instructionTypes.size(); i++) { + if (ddsim->instructionTypes[i] != SIMULATE) { + continue; + } + if (ddd->actualQubits.find(i) == ddd->actualQubits.end()) { + continue; + } + + auto& actualQubits = ddd->actualQubits[i]; + for (const auto& actualQubitVector : actualQubits) { + if (!std::none_of(actualQubitVector.begin(), actualQubitVector.end(), + [&interactions](size_t elem) { + return interactions.find(elem) != + interactions.end(); + })) { + for (const auto& target : actualQubitVector) { + if (interactions.find(target) == interactions.end()) { + found = true; + } + interactions.insert(target); + } + } + } + } + } + + return interactions; +} + size_t tryFindMissingInteraction(DDDiagnostics* diagnostics, DDSimulationState* state, size_t instruction, const std::unique_ptr& assertion, @@ -168,20 +317,18 @@ size_t tryFindMissingInteraction(DDDiagnostics* diagnostics, return variableToQubit(state, target); }); - std::map> allInteractions; + std::map> allInteractions; for (size_t i = 0; i < targets.size(); i++) { - std::vector interactions( - diagnostics->interface.getNumQubits(&diagnostics->interface)); - diagnostics->interface.getInteractions(&diagnostics->interface, instruction, - targetQubits[i], - toBoolArray(interactions.data())); - allInteractions.insert({targetQubits[i], interactions}); + allInteractions.insert( + {targetQubits[i], + getInteractionsAtRuntime(diagnostics, targetQubits[i])}); } + for (size_t i = 0; i < targets.size(); i++) { for (size_t j = i + 1; j < targets.size(); j++) { - if (allInteractions[targetQubits[i]][targetQubits[j]] == 0 && - allInteractions[targetQubits[j]][targetQubits[i]] == 0) { + if (allInteractions[targetQubits[i]].find(targetQubits[j]) == + allInteractions[targetQubits[i]].end()) { outputs[index].type = ErrorCauseType::MissingInteraction; outputs[index].instruction = instruction; index++; @@ -203,8 +350,9 @@ size_t tryFindZeroControls(DDDiagnostics* diagnostics, size_t instruction, std::vector dependencies( diagnostics->interface.getInstructionCount(&diagnostics->interface)); - diagnostics->interface.getDataDependencies( - &diagnostics->interface, instruction, toBoolArray(dependencies.data())); + diagnostics->interface.getDataDependencies(&diagnostics->interface, + instruction, true, + toBoolArray(dependencies.data())); auto outputs = Span(output, count); size_t index = 0; @@ -270,10 +418,29 @@ Result dddiagnosticsGetZeroControlInstructions(Diagnostics* self, void dddiagnosticsOnStepForward(DDDiagnostics* diagnostics, size_t instruction) { auto* ddsim = diagnostics->simulationState; + const auto targets = getTargetVariables(ddsim, instruction); + + // Add actual qubits to tracker. + if (ddsim->instructionTypes[instruction] == SIMULATE || + ddsim->instructionTypes[instruction] == CALL) { + std::vector targetQubits(targets.size()); + std::transform(targets.begin(), targets.end(), targetQubits.begin(), + [&ddsim](const std::string& target) { + return variableToQubit(ddsim, target); + }); + if (diagnostics->actualQubits.find(instruction) == + diagnostics->actualQubits.end()) { + diagnostics->actualQubits[instruction] = std::set>(); + } + diagnostics->actualQubits[instruction].insert(targetQubits); + } + + // Check for zero controls. if (ddsim->instructionTypes[instruction] != SIMULATE) { return; } - const auto numQubits = ddsim->interface.getNumQubits(&ddsim->interface); + const auto numQubits = + diagnostics->interface.getNumQubits(&diagnostics->interface); if (numQubits > 16) { return; } diff --git a/src/common/parsing/CodePreprocessing.cpp b/src/common/parsing/CodePreprocessing.cpp index 810ea52..289b70f 100644 --- a/src/common/parsing/CodePreprocessing.cpp +++ b/src/common/parsing/CodePreprocessing.cpp @@ -9,14 +9,13 @@ #include #include #include -#include #include #include #include Instruction::Instruction(size_t inputLineNumber, std::string inputCode, std::unique_ptr& inputAssertion, - std::set inputTargets, size_t startPos, + std::vector inputTargets, size_t startPos, size_t endPos, size_t successor, bool isFuncCall, std::string function, bool inFuncDef, bool isFuncDef, Block inputBlock) @@ -77,52 +76,82 @@ bool isFunctionDefinition(const std::string& line) { return startsWith(trim(line), "gate "); } -std::vector parseParameters(const std::string& instruction) { +bool isClassicControlledGate(const std::string& line) { + return startsWith(trim(line), "if") && + (line.find('(') != std::string::npos) && + (line.find(')') != std::string::npos); +} + +FunctionDefinition parseFunctionDefinition(const std::string& signature) { auto parts = splitString( - replaceString( - replaceString(replaceString(instruction, ";", " "), "\n", " "), "\t", - " "), - ' '); + replaceString(replaceString(signature, "\n", " "), "\t", " "), ' '); + std::string name; size_t index = 0; for (auto& part : parts) { index++; - if (!part.empty()) { + if (part != "gate" && !part.empty()) { + name = part; break; } } std::string parameterParts; for (size_t i = index; i < parts.size(); i++) { - if (parts[i].empty()) { - continue; - } parameterParts += parts[i]; } auto parameters = splitString(removeWhitespace(parameterParts), ','); - return parameters; + return {name, parameters}; } -FunctionDefinition parseFunctionDefinition(const std::string& signature) { +std::vector parseParameters(const std::string& instruction) { + if (isFunctionDefinition(instruction)) { + const auto fd = parseFunctionDefinition(instruction); + return fd.parameters; + } + + if (instruction.find("->") != std::string::npos) { + // We only add the quantum variable to the measurement's targets. + return parseParameters(splitString(instruction, '-')[0]); + } + + if (isClassicControlledGate(instruction)) { + const auto end = instruction.find(')'); + + return parseParameters( + instruction.substr(end + 1, instruction.length() - end - 1)); + } + auto parts = splitString( - replaceString(replaceString(signature, "\n", " "), "\t", " "), ' '); - std::string name; + replaceString( + replaceString(replaceString(instruction, ";", " "), "\n", " "), "\t", + " "), + ' '); size_t index = 0; + size_t openBrackets = 0; for (auto& part : parts) { index++; - if (part != "gate" && !part.empty()) { - name = part; + openBrackets += + static_cast(std::count(part.begin(), part.end(), '(')); + openBrackets -= + static_cast(std::count(part.begin(), part.end(), ')')); + if (!part.empty() && openBrackets == 0) { break; } } std::string parameterParts; for (size_t i = index; i < parts.size(); i++) { + if (parts[i].empty()) { + continue; + } parameterParts += parts[i]; } auto parameters = splitString(removeWhitespace(parameterParts), ','); - - return {name, parameters}; + if (parameters.size() == 1 && parameters[0].empty()) { + return {}; + } + return parameters; } std::vector sweepFunctionNames(const std::string& code) { @@ -178,9 +207,6 @@ preprocessCode(const std::string& code, size_t startIndex, auto tokens = splitString(trimmedLine, ' '); auto isAssert = isAssertion(line); auto blockPos = line.find("$__block"); - const auto targetsVector = parseParameters(line); - const std::set targets(targetsVector.begin(), - targetsVector.end()); const size_t trueStart = pos + blocksOffset; @@ -199,6 +225,8 @@ preprocessCode(const std::string& code, size_t startIndex, line.replace(blockPos, endPos - blockPos + 1, ""); } + const auto targets = parseParameters(line); + const size_t trueEnd = end + blocksOffset; if (isFunctionDefinition(line)) { @@ -271,20 +299,23 @@ preprocessCode(const std::string& code, size_t startIndex, for (auto& instr : instructions) { auto vars = parseParameters(instr.code); size_t idx = instr.lineNumber - 1; - while (!vars.empty() && (instr.lineNumber < instructions.size() || - idx > instr.lineNumber - instructions.size())) { - bool found = false; + while (instr.lineNumber != 0 && !vars.empty() && + (instr.lineNumber < instructions.size() || + idx > instr.lineNumber - instructions.size())) { + size_t foundIndex = 0; for (const auto& var : variableUsages[idx]) { - if (std::find(vars.begin(), vars.end(), var) != vars.end()) { - found = true; + const auto found = + std::find_if(vars.begin(), vars.end(), [&var](const auto& v) { + return variablesEqual(v, var); + }); + if (found != vars.end()) { const auto newEnd = std::remove(vars.begin(), vars.end(), var); vars.erase(newEnd, vars.end()); + instr.dataDependencies.emplace_back(idx, foundIndex); } + foundIndex++; } - if (found) { - instr.dataDependencies.push_back(idx); - } - if (idx - 1 == instr.lineNumber - instructions.size()) { + if (idx - 1 == instr.lineNumber - instructions.size() || idx == 0) { break; } idx--; diff --git a/src/common/parsing/Utils.cpp b/src/common/parsing/Utils.cpp index e6de922..f5f6e3a 100644 --- a/src/common/parsing/Utils.cpp +++ b/src/common/parsing/Utils.cpp @@ -55,3 +55,16 @@ std::string removeWhitespace(std::string str) { str.erase(std::remove_if(str.begin(), str.end(), ::isspace), str.end()); return str; } + +bool variablesEqual(const std::string& v1, const std::string& v2) { + if (v1.find('[') != std::string::npos && v2.find('[') != std::string::npos) { + return v1 == v2; + } + if (v1.find('[') != std::string::npos) { + return variablesEqual(splitString(v1, '[')[0], v2); + } + if (v2.find('[') != std::string::npos) { + return variablesEqual(splitString(v2, '[')[0], v1); + } + return v1 == v2; +} diff --git a/src/frontend/cli/CliFrontEnd.cpp b/src/frontend/cli/CliFrontEnd.cpp index e3066a7..0d43053 100644 --- a/src/frontend/cli/CliFrontEnd.cpp +++ b/src/frontend/cli/CliFrontEnd.cpp @@ -8,7 +8,6 @@ #include "backend/diagnostics.h" #include "common.h" -#include #include #include #include @@ -127,7 +126,7 @@ void CliFrontEnd::printState(SimulationState* state, size_t inspecting, auto* deps = inspectingDependencies.data(); // NOLINTBEGIN(cppcoreguidelines-pro-type-reinterpret-cast) state->getDiagnostics(state)->getDataDependencies( - state->getDiagnostics(state), inspecting, + state->getDiagnostics(state), inspecting, true, reinterpret_cast(deps)); // NOLINTEND(cppcoreguidelines-pro-type-reinterpret-cast) uint8_t on = 0; diff --git a/src/mqt/debug/dap/messages/set_breakpoints_dap_message.py b/src/mqt/debug/dap/messages/set_breakpoints_dap_message.py index a6633fa..b1a2077 100644 --- a/src/mqt/debug/dap/messages/set_breakpoints_dap_message.py +++ b/src/mqt/debug/dap/messages/set_breakpoints_dap_message.py @@ -9,8 +9,6 @@ if TYPE_CHECKING: from .. import DAPServer -# TODO this fails if another file also has breakpoints - class SetBreakpointsDAPMessage(DAPMessage): """Represents the 'setBreakpoints' DAP request.""" diff --git a/src/mqt/debug/pydebug.pyi b/src/mqt/debug/pydebug.pyi index 0e9b40d..f6235ea 100644 --- a/src/mqt/debug/pydebug.pyi +++ b/src/mqt/debug/pydebug.pyi @@ -89,7 +89,7 @@ class Diagnostics: def init(self) -> None: ... def get_num_qubits(self) -> int: ... def get_instruction_count(self) -> int: ... - def get_data_dependencies(self, instruction: int) -> list[int]: ... + def get_data_dependencies(self, instruction: int, include_callers: bool = False) -> list[int]: ... def get_interactions(self, before_instruction: int, qubit: int) -> list[int]: ... def get_zero_control_instructions(self) -> list[int]: ... def potential_error_causes(self) -> list[ErrorCause]: ... diff --git a/src/python/InterfaceBindings.cpp b/src/python/InterfaceBindings.cpp index a34f8e4..d1a3a2e 100644 --- a/src/python/InterfaceBindings.cpp +++ b/src/python/InterfaceBindings.cpp @@ -259,22 +259,24 @@ void bindDiagnostics(py::module& m) { [](Diagnostics* self) { return self->getNumQubits(self); }) .def("get_instruction_count", [](Diagnostics* self) { return self->getInstructionCount(self); }) - .def("get_data_dependencies", - [](Diagnostics* self, size_t instruction) { - std::vector instructions(self->getInstructionCount(self)); - // NOLINTBEGIN(cppcoreguidelines-pro-type-reinterpret-cast) - checkOrThrow(self->getDataDependencies( - self, instruction, - reinterpret_cast(instructions.data()))); - // NOLINTEND(cppcoreguidelines-pro-type-reinterpret-cast) - std::vector result; - for (size_t i = 0; i < instructions.size(); i++) { - if (instructions[i] != 0) { - result.push_back(i); - } - } - return result; - }) + .def( + "get_data_dependencies", + [](Diagnostics* self, size_t instruction, bool includeCallers) { + std::vector instructions(self->getInstructionCount(self)); + // NOLINTBEGIN(cppcoreguidelines-pro-type-reinterpret-cast) + checkOrThrow(self->getDataDependencies( + self, instruction, includeCallers, + reinterpret_cast(instructions.data()))); + // NOLINTEND(cppcoreguidelines-pro-type-reinterpret-cast) + std::vector result; + for (size_t i = 0; i < instructions.size(); i++) { + if (instructions[i] != 0) { + result.push_back(i); + } + } + return result; + }, + py::arg("instruction"), py::arg("include_callers") = false) .def("get_interactions", [](Diagnostics* self, size_t beforeInstruction, size_t qubit) { std::vector qubits(self->getNumQubits(self)); diff --git a/test/circuits/diagnose-with-jumps.qasm b/test/circuits/diagnose-with-jumps.qasm new file mode 100644 index 0000000..8c0d512 --- /dev/null +++ b/test/circuits/diagnose-with-jumps.qasm @@ -0,0 +1,26 @@ +gate level_one q0, q1, q2 { // 0 + level_two q0, q1; // 1 + level_two q1, q2; // 2 +} // 3 + +gate level_two q0, q1 { // 4 + cx q0, q1; // 5 + level_three_a q0; // 6 + + level_three_b q1; // 7 +} // 8 + +gate level_three_a q { // 9 + x q; // 10 +} // 11 + +gate level_three_b q { // 12 + z q; // 13 +} // 14 + +qreg q[4]; // 15 + +x q[0]; // 16 +level_one q[2], q[1], q[0]; // 17 + +assert-eq q[0], q[1], q[2], q[3] { qreg x[4]; } // 18 diff --git a/test/circuits/runtime-interaction.qasm b/test/circuits/runtime-interaction.qasm new file mode 100644 index 0000000..25efc48 --- /dev/null +++ b/test/circuits/runtime-interaction.qasm @@ -0,0 +1,10 @@ +gate gate1 q0, q1 { // 0 + x q0; // 1 + x q1; // 2 + assert-ent q0, q1; // 3 +} // 4 + +qreg q[2]; // 5 + +cx q[0], q[1]; // 6 +gate1 q[0], q[1]; // 7 diff --git a/test/python/resources/diagnosis/data-dependencies-with-callers.qasm b/test/python/resources/diagnosis/data-dependencies-with-callers.qasm new file mode 100644 index 0000000..03fe2bb --- /dev/null +++ b/test/python/resources/diagnosis/data-dependencies-with-callers.qasm @@ -0,0 +1,15 @@ +qreg q[3]; // 0 + +gate test q { // 1 + x q; // 2 +} // 3 + +x q[0]; // 4 +x q[1]; // 5 + +test q[0]; // 6 + +x q[0]; // 7 +x q[2]; // 8 + +test q[2]; // 9 diff --git a/test/python/test_diagnosis.py b/test/python/test_diagnosis.py index facba39..b4aa97c 100644 --- a/test/python/test_diagnosis.py +++ b/test/python/test_diagnosis.py @@ -44,7 +44,7 @@ def test_data_dependencies_jumps() -> None: dependencies = s.get_diagnostics().get_data_dependencies(4) assert dependencies == [2, 3, 4] dependencies = s.get_diagnostics().get_data_dependencies(9) - assert dependencies == [6, 7, 8, 9] + assert dependencies == [2, 3, 4, 6, 7, 8, 9] destroy_ddsim_simulation_state(s) @@ -54,10 +54,13 @@ def test_control_always_zero() -> None: s.run_simulation() causes = s.get_diagnostics().potential_error_causes() - assert len(causes) == 1 # once diagnosis can step into jumps, this should be 2 + assert len(causes) == 2 assert causes[0].type == ErrorCauseType.ControlAlwaysZero - assert causes[0].instruction == 12 + assert causes[0].instruction == 3 + + assert causes[1].type == ErrorCauseType.ControlAlwaysZero + assert causes[1].instruction == 12 def test_missing_interaction() -> None: @@ -78,3 +81,15 @@ def test_zero_control_listing() -> None: s.run_simulation() zero_controls = s.get_diagnostics().get_zero_control_instructions() assert zero_controls == [3, 12] + + +def test_data_dependencies_with_callers() -> None: + """Tests the data dependency analysis with enabling callers.""" + s = load_instance("data-dependencies-with-callers") + s.run_simulation() + dependencies = s.get_diagnostics().get_data_dependencies(2, include_callers=True) + assert dependencies == [2, 4, 6, 8, 9] + + dependencies = s.get_diagnostics().get_data_dependencies(7, include_callers=True) + assert dependencies == [2, 4, 6, 7] + # 8 and 9 are not included `test` doesn't have unknown callers in this case, so the analysis won't include all callers. diff --git a/test/test_custom_code.cpp b/test/test_custom_code.cpp index 36aa2ff..fbcb732 100644 --- a/test/test_custom_code.cpp +++ b/test/test_custom_code.cpp @@ -1,5 +1,6 @@ #include "backend/dd/DDSimDebug.hpp" #include "backend/debug.h" +#include "backend/diagnostics.h" #include "common.h" #include "utils_test.hpp" @@ -58,9 +59,9 @@ class CustomCodeTest : public testing::Test { } }; -TEST_F(CustomCodeTest, ClassicControlledOperation) { +TEST_F(CustomCodeTest, ClassicControlledOperationFalse) { loadCode(2, 1, - "h q[0];" + "z q[0];" "cx q[0], q[1];" "measure q[0] -> c[0];" "if(c==1) x q[1];" @@ -70,8 +71,24 @@ TEST_F(CustomCodeTest, ClassicControlledOperation) { std::array amplitudes{}; Statevector sv{2, 4, amplitudes.data()}; state->getStateVectorFull(state, &sv); - ASSERT_TRUE(complexEquality(amplitudes[0], 1, 0.0) || - complexEquality(amplitudes[1], 1, 0.0)); + ASSERT_TRUE(complexEquality(amplitudes[0], 1, 0.0)); + + ASSERT_EQ(state->stepBackward(state), OK); +} + +TEST_F(CustomCodeTest, ClassicControlledOperationTrue) { + loadCode(2, 1, + "x q[0];" + "cx q[0], q[1];" + "measure q[0] -> c[0];" + "if(c==1) x q[1];" + "if(c==0) z q[1];"); + ASSERT_EQ(state->runSimulation(state), OK); + + std::array amplitudes{}; + Statevector sv{2, 4, amplitudes.data()}; + state->getStateVectorFull(state, &sv); + ASSERT_TRUE(complexEquality(amplitudes[1], 1, 0.0)); ASSERT_EQ(state->stepBackward(state), OK); } @@ -272,3 +289,124 @@ TEST_F(CustomCodeTest, LargeProgram) { ASSERT_EQ(errors, 0); ASSERT_EQ(state->getCurrentInstruction(state), 4); } + +TEST_F(CustomCodeTest, CollectiveGateAsDependency) { + loadCode(2, 0, "x q; barrier q[0];"); + auto* diagnosis = state->getDiagnostics(state); + std::array dependencies{}; + ASSERT_EQ( + diagnosis->getDataDependencies(diagnosis, 3, false, dependencies.data()), + OK); + ASSERT_EQ(dependencies[0], false); + ASSERT_EQ(dependencies[1], false); + ASSERT_EQ(dependencies[2], true); + ASSERT_EQ(dependencies[3], true); +} + +TEST_F(CustomCodeTest, CollectiveGateAsInteraction) { + loadCode(1, 0, "qreg p[1]; cx q, p; assert-ent q[0], p[0];"); + ASSERT_EQ(state->runSimulation(state), OK); + ASSERT_TRUE(state->didAssertionFail(state)); + + auto* diagnosis = state->getDiagnostics(state); + + std::array interactions{}; + ASSERT_EQ(diagnosis->getInteractions(diagnosis, 4, 0, interactions.data()), + OK); + + ASSERT_TRUE(interactions[0]); + ASSERT_TRUE(interactions[1]); + + std::array causes{}; + ASSERT_EQ( + diagnosis->potentialErrorCauses(diagnosis, causes.data(), causes.size()), + 1); + ASSERT_EQ(causes[0].type, ControlAlwaysZero); + ASSERT_EQ(causes[0].instruction, 3); +} + +TEST_F(CustomCodeTest, NonZeroControlsInErrorSearch) { + loadCode(2, 0, + "gate test q1, q2 { cx q1, q2; } x q[0]; test q[1], q[0]; test " + "q[0], q[1]; assert-sup q[0];"); + auto* diagnosis = state->getDiagnostics(state); + ASSERT_EQ(state->runSimulation(state), OK); + ASSERT_TRUE(state->didAssertionFail(state)); + std::array errors{}; + ASSERT_TRUE(diagnosis->potentialErrorCauses(diagnosis, errors.data(), + errors.size()) == 0); +} + +TEST_F(CustomCodeTest, PaperExampleGrover) { + loadCode(3, 3, + "gate oracle q0, q1, q2, flag {" + "assert-sup q0, q1, q2;" + "ccz q1, q2, flag;" + "assert-ent q0, q1, q2;" + "}" + "gate diffusion q0, q1, q2 {" + "h q0; h q1; h q2;" + "x q0; x q1; x q2;" + "ccz q0, q1, q2;" + "x q2; x q1; x q0;" + "h q2; h q1; h q0;" + "}" + "qreg flag[1];" + "x flag;" + "oracle q[0], q[1], q[2], flag;" + "diffusion q[0], q[1], q[2];" + "assert-eq 0.8, q[0], q[1], q[2] { 0, 0, 0, 0, 0, 0, 0, 1 }" + "oracle q[0], q[1], q[2], flag;" + "diffusion q[0], q[1], q[2];" + "assert-eq 0.9, q[0], q[1], q[2] { 0, 0, 0, 0, 0, 0, 0, 1 }", + false, "OPENQASM 2.0;\ninclude \"qelib1.inc\";\n"); + + auto* diagnosis = state->getDiagnostics(state); + std::array causes{}; + + ASSERT_EQ(state->runSimulation(state), OK); + ASSERT_EQ(state->didAssertionFail(state), true); + ASSERT_EQ(state->getCurrentInstruction(state), 5); + // We expect no potential errors yet: + ASSERT_EQ( + diagnosis->potentialErrorCauses(diagnosis, causes.data(), causes.size()), + 0); + + ASSERT_EQ(state->runSimulation(state), OK); + ASSERT_EQ(state->didAssertionFail(state), true); + ASSERT_EQ(state->getCurrentInstruction(state), 7); + // We expect three potential errors: + // 2 missing interactions: q0 <-> q1 and q0 <-> q2 + // 1 control always zero: q1 & q2 in instruction 6 + ASSERT_EQ( + diagnosis->potentialErrorCauses(diagnosis, causes.data(), causes.size()), + 3); + ASSERT_EQ(causes[0].type, MissingInteraction); + ASSERT_EQ(causes[0].instruction, 7); + ASSERT_EQ(causes[1].type, MissingInteraction); + ASSERT_EQ(causes[1].instruction, 7); + ASSERT_EQ(causes[2].type, ControlAlwaysZero); + ASSERT_EQ(causes[2].instruction, 6); + + ASSERT_EQ(state->runSimulation(state), OK); + ASSERT_EQ(state->didAssertionFail(state), true); + ASSERT_EQ(state->getCurrentInstruction(state), 28); + // We expect one potential error: Control always zero in instruction 6 + ASSERT_EQ( + diagnosis->potentialErrorCauses(diagnosis, causes.data(), causes.size()), + 1); + ASSERT_EQ(causes[0].type, ControlAlwaysZero); + ASSERT_EQ(causes[0].instruction, 6); + + ASSERT_EQ(state->runSimulation(state), OK); + ASSERT_EQ(state->didAssertionFail(state), true); + ASSERT_EQ(state->getCurrentInstruction(state), 31); + // We expect no potential errors, as instruction 6 is no longer always 0 + ASSERT_EQ( + diagnosis->potentialErrorCauses(diagnosis, causes.data(), causes.size()), + 0); + + ASSERT_EQ(state->runSimulation(state), OK); + ASSERT_EQ(state->didAssertionFail(state), false); + ASSERT_EQ(state->isFinished(state), true); +} diff --git a/test/test_diagnostics.cpp b/test/test_diagnostics.cpp index d93b430..16f0f58 100644 --- a/test/test_diagnostics.cpp +++ b/test/test_diagnostics.cpp @@ -1,6 +1,7 @@ #include "backend/dd/DDSimDebug.hpp" #include "backend/debug.h" #include "backend/diagnostics.h" +#include "common.h" #include "utils_test.hpp" #include @@ -49,7 +50,8 @@ TEST_F(DiagnosticsTest, DataDependencies) { std::vector dependencies(state->getInstructionCount(state), 0); // NOLINTBEGIN(cppcoreguidelines-pro-type-reinterpret-cast) diagnostics->getDataDependencies( - diagnostics, instruction, reinterpret_cast(dependencies.data())); + diagnostics, instruction, false, + reinterpret_cast(dependencies.data())); // NOLINTEND(cppcoreguidelines-pro-type-reinterpret-cast) std::set dependenciesSet; for (size_t i = 0; i < dependencies.size(); ++i) { @@ -171,3 +173,98 @@ TEST_F(DiagnosticsTest, ZeroControlsWithJumps) { ASSERT_FALSE(zeroControls.at(i) ^ (i == 3 || i == 12)); } } + +TEST_F(DiagnosticsTest, DataDependenciesWithJumps) { + loadFromFile("diagnose-with-jumps"); + const std::map> expected = { + {1, {1}}, + {2, {2, 13, 7, 5, 1}}, + {3, {3}}, + + {5, {5}}, + {6, {6, 5}}, + {7, {7, 5}}, + {8, {8}}, + + {10, {10}}, + {13, {13}}, + {11, {11}}, + {9, {9}}, + {14, {14}}, + {12, {12}}, + + {15, {15}}, + + {16, {16}}, + + {17, {17, 16}}, + + {18, {18, 13, 10, 7, 6, 5, 2, 1, 17, 16}}}; + + for (const auto& pair : expected) { + std::vector dependencies(state->getInstructionCount(state), 0); + // NOLINTBEGIN(cppcoreguidelines-pro-type-reinterpret-cast) + diagnostics->getDataDependencies( + diagnostics, pair.first, false, + reinterpret_cast(dependencies.data())); + // NOLINTEND(cppcoreguidelines-pro-type-reinterpret-cast) + std::set dependenciesSet; + for (size_t i = 0; i < dependencies.size(); ++i) { + if (dependencies[i] != 0) { + dependenciesSet.insert(i); + } + } + ASSERT_EQ(dependenciesSet, pair.second) + << "Failed for instruction " << pair.first; + } +} + +TEST_F(DiagnosticsTest, InteractionsWithJumps) { + loadFromFile("diagnose-with-jumps"); + + const std::map, std::set> expected = { + {{1, 0}, {0}}, {{1, 1}, {1}}, {{1, 2}, {2}}, + {{2, 0}, {0, 1}}, {{2, 1}, {0, 1}}, {{2, 2}, {2}}, + + {{5, 0}, {0}}, {{6, 0}, {1, 0}}, {{7, 1}, {0, 1}}, + + {{10, 0}, {0}}, + + {{17, 0}, {0}}, {{18, 0}, {1, 2, 0}}, {{18, 1}, {0, 2, 1}}, + {{18, 2}, {0, 1, 2}}, {{18, 3}, {3}}}; + + for (const auto& pair : expected) { + std::vector interactions(state->getNumQubits(state), 0); + // NOLINTBEGIN(cppcoreguidelines-pro-type-reinterpret-cast) + diagnostics->getInteractions(diagnostics, pair.first.first, + pair.first.second, + reinterpret_cast(interactions.data())); + // NOLINTEND(cppcoreguidelines-pro-type-reinterpret-cast) + std::set interactionsSet; + for (size_t i = 0; i < interactions.size(); ++i) { + if (interactions[i] != 0) { + interactionsSet.insert(i); + } + } + ASSERT_EQ(interactionsSet, pair.second) + << "Failed for instruction " << pair.first.first << " qubit " + << pair.first.second; + } +} + +TEST_F(DiagnosticsTest, RuntimeInteractions) { + loadFromFile("runtime-interaction"); + ASSERT_EQ(state->runSimulation(state), OK); + ASSERT_TRUE(state->didAssertionFail(state)); + ASSERT_EQ(state->getCurrentInstruction(state), 3); + + std::array errors{}; + ASSERT_EQ(diagnostics->potentialErrorCauses(diagnostics, errors.data(), 10), + 1); + + // Interaction happens outside of the instruction, so we don't expect a + // missing interaction. + + ASSERT_EQ(errors[0].type, ErrorCauseType::ControlAlwaysZero); + ASSERT_EQ(errors[0].instruction, 6); +}