Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
179 changes: 102 additions & 77 deletions src/common/parsing/CodePreprocessing.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -23,16 +23,17 @@
#include <algorithm>
#include <array>
#include <cctype>
#include <charconv>
#include <cstddef>
#include <exception>
#include <iterator>
#include <map>
#include <memory>
#include <optional>
#include <sstream>
#include <stdexcept>
#include <string>
#include <string_view>
#include <system_error>
#include <utility>
#include <vector>

Expand All @@ -53,6 +54,71 @@ bool isDigits(const std::string& text) {
text, [](unsigned char c) { return std::isdigit(c) != 0; });
}

/**
* @brief Parse the entire text as an unsigned integer.
*
* The text must contain only characters accepted by `std::from_chars` for the
* target type (digits, no leading sign, no whitespace, no trailing garbage).
* Anything else, including partial matches, fails.
* @param text The text to parse.
* @return The parsed value, or `std::nullopt` if parsing fails or does not
* consume the whole input.
*/
std::optional<size_t> parseUnsignedInt(std::string_view text) {
size_t value = 0;
const char* const begin = std::to_address(text.begin());
const char* const end = std::to_address(text.end());
const auto result = std::from_chars(begin, end, value);
if (result.ec != std::errc{} || result.ptr != end) {
return std::nullopt;
}
return value;
}

/**
* @brief A reference to a register, or to a single element within a register.
*
* `index` can hold the following values:
* - `std::nullopt`, when the reference targets the whole register (`c`, `q`).
* - the position within the register, when the reference targets a single
* element (`c[k]`, `q[k]`).
*/
struct RegisterRef {
std::string name;
std::optional<size_t> index;
};

/**
* @brief Parse a register reference from the given text.
*
* Accepts either the bare register name (`c`, `q`), which resolves to
* `{name = "c", index = std::nullopt}` and targets the whole register,
* or the indexed form (`c[k]`), which resolves to a single-element reference.
* @param text The already-trimmed text to parse.
* @return The parsed reference, or `std::nullopt` if the shape is invalid.
*/
std::optional<RegisterRef> parseRegisterRef(const std::string& text) {
if (text.empty()) {
return std::nullopt;
}
const auto bracketPos = text.find('[');
if (bracketPos == std::string::npos) {
return RegisterRef{.name = text, .index = std::nullopt};
}
const auto closePos = text.find(']', bracketPos + 1);
if (bracketPos == 0 || closePos == std::string::npos ||
closePos != text.size() - 1) {
return std::nullopt;
}
auto base = text.substr(0, bracketPos);
const auto indexText = text.substr(bracketPos + 1, closePos - bracketPos - 1);

if (const auto index = parseUnsignedInt(indexText); index.has_value()) {
return RegisterRef{.name = std::move(base), .index = index};
}
return std::nullopt;
}

/**
* @brief 1-based line/column location within source text.
*/
Expand Down Expand Up @@ -173,37 +239,20 @@ void validateTargets(const std::string& code, size_t instructionStart,
detail += ".";
throw makeParseError(code, instructionStart, detail);
}
const auto open = target.find('[');
if (open == std::string::npos) {
continue;
}
const auto close = target.find(']', open + 1);
if (open == 0 || close == std::string::npos || close != target.size() - 1) {
throw makeParseError(code, instructionStart,
invalidTargetDetail(target, context), target);
}
const auto registerName = target.substr(0, open);
const auto indexText = target.substr(open + 1, close - open - 1);
if (!isDigits(indexText)) {
const auto ref = parseRegisterRef(target);
if (!ref.has_value()) {
throw makeParseError(code, instructionStart,
invalidTargetDetail(target, context), target);
}
size_t registerIndex = 0;
try {
registerIndex = std::stoul(indexText);
} catch (const std::invalid_argument&) {
throw makeParseError(code, instructionStart,
invalidTargetDetail(target, context), target);
} catch (const std::out_of_range&) {
throw makeParseError(code, instructionStart,
invalidTargetDetail(target, context), target);
if (!ref->index.has_value()) {
continue;
}
if (std::ranges::find(shadowedRegisters, registerName) !=
if (std::ranges::find(shadowedRegisters, ref->name) !=
shadowedRegisters.end()) {
continue;
}
const auto found = definedRegisters.find(registerName);
if (found == definedRegisters.end() || found->second <= registerIndex) {
const auto found = definedRegisters.find(ref->name);
if (found == definedRegisters.end() || found->second <= *ref->index) {
throw makeParseError(code, instructionStart,
invalidTargetDetail(target, context), target);
}
Expand Down Expand Up @@ -409,11 +458,6 @@ ClassicControlledGate parseClassicControlledGate(const std::string& code) {

std::optional<ClassicCondition>
parseClassicConditionExpression(const std::string& condition) {
auto normalized = removeWhitespace(condition);
if (!normalized.empty() && normalized.front() == '(') {
normalized.erase(0, 1);
}

// Operators must be scanned longest-first so that "<=" is not misread as "<".
struct OperatorMatch {
std::string_view text;
Expand All @@ -429,66 +473,47 @@ parseClassicConditionExpression(const std::string& condition) {
{.text = ">", .kind = qc::Gt},
}};

auto normalized = removeWhitespace(condition);
if (!normalized.empty() && normalized.front() == '(') {
normalized.erase(0, 1);
}

// Default values for the bare form (`c`, `c[k]`): implicit `!= 0`.
std::string operand{normalized};
size_t expected = 0;
qc::ComparisonKind kind = qc::Neq;

// Comparator form (`c` <op> integer).
std::optional<OperatorMatch> match;
for (const auto& op : OPERATORS) {
if (normalized.find(op.text) != std::string::npos) {
match = op;
break;
}
}
if (!match.has_value()) {
return std::nullopt;
}
const auto opPos = normalized.find(match->text);
const auto lhs = normalized.substr(0, opPos);
const auto rhs = normalized.substr(opPos + match->text.size());
if (lhs.empty() || rhs.empty()) {
return std::nullopt;
}

if (!isDigits(rhs)) {
return std::nullopt;
}
size_t expected = 0;
try {
expected = std::stoull(rhs);
} catch (const std::invalid_argument&) {
return std::nullopt;
} catch (const std::out_of_range&) {
return std::nullopt;
}

const auto bracketPos = lhs.find('[');
if (bracketPos != std::string::npos) {
const auto closePos = lhs.find(']', bracketPos + 1);
if (bracketPos == 0 || closePos == std::string::npos ||
closePos != lhs.size() - 1) {
return std::nullopt;
}
const auto base = lhs.substr(0, bracketPos);
const auto indexText =
lhs.substr(bracketPos + 1, closePos - bracketPos - 1);
if (!isDigits(indexText)) {
if (match.has_value()) {
const auto opPos = normalized.find(match->text);
const auto lhs = normalized.substr(0, opPos);
const auto rhs = normalized.substr(opPos + match->text.size());
if (lhs.empty() || rhs.empty()) {
return std::nullopt;
}
size_t bitIndex = 0;
try {
bitIndex = std::stoull(indexText);
} catch (const std::invalid_argument&) {
return std::nullopt;
} catch (const std::out_of_range&) {
const auto parsed = parseUnsignedInt(rhs);
if (!parsed.has_value()) {
return std::nullopt;
}
return ClassicCondition{.registerName = base,
.bitIndex = bitIndex,
.expectedValue = expected,
.kind = match->kind};
expected = *parsed;
operand = lhs;
kind = match->kind;
}

return ClassicCondition{.registerName = lhs,
.bitIndex = std::nullopt,
.expectedValue = expected,
.kind = match->kind};
if (const auto ref = parseRegisterRef(operand); ref.has_value()) {
return ClassicCondition{.registerName = ref->name,
.bitIndex = ref->index,
.expectedValue = expected,
.kind = kind};
}
return std::nullopt;
}

std::optional<ClassicCondition>
Expand Down
78 changes: 78 additions & 0 deletions test/test_custom_code.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -259,6 +259,84 @@ TEST_F(CustomCodeTest, IfElseOperationBackwardStep) {
ASSERT_TRUE(complexEquality(amplitudes[3], 1, 0.0));
}

/**
* @test Test classic-controlled operations that use a bare classical register
* as the condition (no explicit comparator).
* The measured value of `c` is 1, so `if (c)` triggers and `x q[1]` is applied.
*/
TEST_F(CustomCodeTest, IfElseOperationBareRegisterTrue) {
loadCode(2, 1,
"x q[0];"
"cx q[0], q[1];"
"measure q[0] -> c[0];"
"if(c) x q[1];");
ASSERT_EQ(state->runSimulation(state), OK);

std::array<Complex, 4> amplitudes{};
Statevector sv{2, 4, amplitudes.data()};
state->getStateVectorFull(state, &sv);
// q[0] = 1, q[1] = 0 after the `if` flips q[1] from 1 to 0.
ASSERT_TRUE(complexEquality(amplitudes[1], 1, 0.0));
}

/**
* @test Same as `IfElseOperationBareRegisterTrue` but with the measurement
* producing 0, so `if (c)` does not trigger.
*/
TEST_F(CustomCodeTest, IfElseOperationBareRegisterFalse) {
loadCode(2, 1,
"measure q[0] -> c[0];"
"if(c) x q[1];");
ASSERT_EQ(state->runSimulation(state), OK);

std::array<Complex, 4> amplitudes{};
Statevector sv{2, 4, amplitudes.data()};
state->getStateVectorFull(state, &sv);
// Both qubits stay at |0>; the `if` is skipped.
ASSERT_TRUE(complexEquality(amplitudes[0], 1, 0.0));
}

/**
* @test Same as `IfElseOperationBareRegisterTrue` but using a single-bit
* reference (`c[0]`) as the bare condition.
*/
TEST_F(CustomCodeTest, IfElseOperationBareBit) {
loadCode(2, 1,
"x q[0];"
"cx q[0], q[1];"
"measure q[0] -> c[0];"
"if(c[0]) x q[1];");
ASSERT_EQ(state->runSimulation(state), OK);

std::array<Complex, 4> amplitudes{};
Statevector sv{2, 4, amplitudes.data()};
state->getStateVectorFull(state, &sv);
ASSERT_TRUE(complexEquality(amplitudes[1], 1, 0.0));
}

/**
* @test Exercise the backward-execution branch for a bare-register condition.
* Stepping back should undo the `x q[1]` that fired on the forward pass.
*/
TEST_F(CustomCodeTest, IfElseOperationBareRegisterBackwardStep) {
loadCode(2, 1,
"x q[0];"
"cx q[0], q[1];"
"measure q[0] -> c[0];"
"if(c) x q[1];");
ASSERT_EQ(state->runSimulation(state), OK);

std::array<Complex, 4> 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);
state->getStateVectorFull(state, &sv);
// Undoing the `if(c) x q[1]` restores q[0] = 1, q[1] = 1, i.e. index 3.
ASSERT_TRUE(complexEquality(amplitudes[3], 1, 0.0));
}

/**
* @test Test the `reset` instruction.
*/
Expand Down
Loading