From b27b21869efc5b1e45daa5feedecfa73f3c1a3a8 Mon Sep 17 00:00:00 2001 From: Ondra Lengal Date: Tue, 27 Oct 2026 12:53:13 +0100 Subject: [PATCH] Reject misaligned kets instead of aborting (#34) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit A ket shorter or longer than the other kets for the same qubits (in one set, across "∪", or across pre.hsl and post.hsl) made the unit rewrite index past the end of the ket, and autoq aborted with an uncaught std::out_of_range. - In the rewrite pass, every ket of a tensor segment must have the same number of qubits as the first one; otherwise report "Not aligned" naming both kets. This covers shorter kets, longer kets and kets that end inside a constant unit. - Guard the unit loop against running out of characters. - Share the ket-length computation between the collect and rewrite passes (ket_units). A unit test checks that the three shapes from the issue, and a post.hsl shorter than pre.hsl, throw AutoQError, and that aligned input parses. Co-Authored-By: Claude Opus 5.5 Claude-Session: https://claude.ai/code/session_013gtFQRPe1eSSpmA7EJ2KNY --- .../parsing/ExtendedDirac/EvaluationVisitor.h | 4 ++ .../ExtendedDirac/EvaluationVisitorDirac.h | 68 +++++++++++++------ .../ExtendedDirac/EvaluationVisitorTset.h | 1 + unit_tests/qasm_parsing_inclusion_cli_test.cc | 14 ++++ unit_tests/testcase/KetAlignment/aligned.hsl | 2 + unit_tests/testcase/KetAlignment/circuit.qasm | 3 + .../KetAlignment/ket_ends_inside_constant.hsl | 2 + .../testcase/KetAlignment/longer_ket.hsl | 2 + unit_tests/testcase/KetAlignment/post.hsl | 2 + .../testcase/KetAlignment/post_shorter.hsl | 2 + unit_tests/testcase/KetAlignment/pre.hsl | 2 + .../testcase/KetAlignment/shorter_ket.hsl | 2 + 12 files changed, 82 insertions(+), 22 deletions(-) create mode 100644 unit_tests/testcase/KetAlignment/aligned.hsl create mode 100644 unit_tests/testcase/KetAlignment/circuit.qasm create mode 100644 unit_tests/testcase/KetAlignment/ket_ends_inside_constant.hsl create mode 100644 unit_tests/testcase/KetAlignment/longer_ket.hsl create mode 100644 unit_tests/testcase/KetAlignment/post.hsl create mode 100644 unit_tests/testcase/KetAlignment/post_shorter.hsl create mode 100644 unit_tests/testcase/KetAlignment/pre.hsl create mode 100644 unit_tests/testcase/KetAlignment/shorter_ket.hsl diff --git a/include/autoq/parsing/ExtendedDirac/EvaluationVisitor.h b/include/autoq/parsing/ExtendedDirac/EvaluationVisitor.h index 06291621ab..dbfa526dbb 100644 --- a/include/autoq/parsing/ExtendedDirac/EvaluationVisitor.h +++ b/include/autoq/parsing/ExtendedDirac/EvaluationVisitor.h @@ -58,6 +58,8 @@ struct EvaluationVisitor : public ExtendedDiracParserBaseVisitor { std::set usedVars; // record all variables currently used in {diracs : varcons} for preventing naming collision. segment2split_t segment2split; currentSplit_t currentSplit; + std::string currentSegmentFirstKet; // the first ket rewritten in the current tensor segment + int currentSegmentKetLength; // its number of qubits, or -1 before the first ket segment2perm_t segment2perm; perm_t currentPerm; std::vector remember_the_lengths_of_each_unit_position; @@ -77,6 +79,8 @@ struct EvaluationVisitor : public ExtendedDiracParserBaseVisitor { usedVars(), segment2split(), currentSplit(), + currentSegmentFirstKet(), + currentSegmentKetLength(-1), segment2perm(), currentPerm(), remember_the_lengths_of_each_unit_position(), diff --git a/include/autoq/parsing/ExtendedDirac/EvaluationVisitorDirac.h b/include/autoq/parsing/ExtendedDirac/EvaluationVisitorDirac.h index 213323d647..c41bac8058 100644 --- a/include/autoq/parsing/ExtendedDirac/EvaluationVisitorDirac.h +++ b/include/autoq/parsing/ExtendedDirac/EvaluationVisitorDirac.h @@ -143,6 +143,33 @@ } } + // Splits a ket into its variable units (qubit intervals) and sets len to + // its total number of qubits. + strsplit_t ket_units(const std::string &vstr, const std::map &localVar2len, int &len) { + strsplit_t intervals; + len = 0; + for (const auto &c : vstr) { + if ((c == '0') || (c == '1')) { + len++; + continue; + } + auto it = localVar2len.find(static_cast(std::tolower(c))); + if (it != localVar2len.end()) { + intervals.push_back(unitsplit_t(len, len+it->second)); + len += it->second; + } else { + it = globalVar2len.find(static_cast(std::tolower(c))); + if (it != globalVar2len.end()) { + intervals.push_back(unitsplit_t(len, len+it->second)); + len += it->second; + } else { + THROW_AUTOQ_ERROR("Undefined variable!"); + } + } + } + return intervals; + } + std::any visitTerm(ExtendedDiracParser::TermContext *ctx) override { if (mode == EXPAND_POWER_AND_DIRACS_AND_REWRITE_COMPLEMENT) { auto vstr = ctx->VStr->getText(); @@ -161,32 +188,12 @@ return std::any_cast((ctx->complex() == nullptr ? (ctx->SUB() != nullptr ? "-1" : "") : ctx->complex()->getText()) + "∑" + ctx->varcons()->getText() + "|" + vstr2 + "⟩"); } } else if (mode == COLLECT_KETS_AND_COMPUTE_UNIT_DECOMPOSITION_INDICES) { - strsplit_t intervals; std::map localVar2len; if (ctx->varcons() != nullptr) { localVar2len = std::any_cast>(visit(ctx->varcons())); } int len = 0; - for (const auto &c : ctx->VStr->getText()) { - if ((c == '0') || (c == '1')) { - len++; - continue; - } - auto it = localVar2len.find(static_cast(std::tolower(c))); - if (it != localVar2len.end()) { - intervals.push_back(unitsplit_t(len, len+it->second)); - len += it->second; - } else { - it = globalVar2len.find(static_cast(std::tolower(c))); - if (it != globalVar2len.end()) { - intervals.push_back(unitsplit_t(len, len+it->second)); - len += it->second; - } else { - THROW_AUTOQ_ERROR("Undefined variable!"); - } - } - } - return intervals; + return ket_units(ctx->VStr->getText(), localVar2len, len); } else if (mode == REWRITE_BY_UNIT_INDICES_AND_MAKE_ALL_VARS_DISTINCT) { /***************************************************************/ // Check if there is some local variable using global characters. @@ -214,6 +221,18 @@ } /***************************************************************/ std::string vstr = ctx->VStr->getText(); + /* All kets for the same qubits (in one set, across "∪", and across + the files of one verification task) must have the same length. */ + int ketLength = 0; + ket_units(vstr, localVar2len, ketLength); + if (currentSegmentKetLength == -1) { + currentSegmentKetLength = ketLength; + currentSegmentFirstKet = vstr; + } else if (ketLength != currentSegmentKetLength) { + THROW_AUTOQ_ERROR("Not aligned: the ket |" + vstr + "⟩ has " + std::to_string(ketLength) + + " qubit(s), but the ket |" + currentSegmentFirstKet + "⟩ for the same qubits has " + + std::to_string(currentSegmentKetLength) + "."); + } std::string vstr2; // the rewritten output remember_the_lengths_of_each_unit_position.clear(); /*************************************************/ @@ -230,9 +249,14 @@ /***********************************************/ std::map localVal2const; for (const auto &[l, r] : completeCurrentSplit) { + if (vstr.empty()) { + if (r == -1) continue; // no remaining part + THROW_AUTOQ_ERROR("Not aligned!"); + } if (r == -1 || // the last remaining part not classified as a unit must be a constant vstr.at(0)=='0' || vstr.at(0)=='1') { - if (vstr.empty()) continue; // reject if there is no remaining part + if (r != -1 && vstr.size() < static_cast(r - l)) + THROW_AUTOQ_ERROR("Not aligned!"); std::string this_constant_string = ((r == -1) ? vstr : vstr.substr(0, r-l)); if (!std::all_of(this_constant_string.begin(), this_constant_string.end(), [](char ch) { return ch == '0' || ch == '1'; })) { THROW_AUTOQ_ERROR("Not aligned!"); diff --git a/include/autoq/parsing/ExtendedDirac/EvaluationVisitorTset.h b/include/autoq/parsing/ExtendedDirac/EvaluationVisitorTset.h index eb381e40db..43345610f9 100644 --- a/include/autoq/parsing/ExtendedDirac/EvaluationVisitorTset.h +++ b/include/autoq/parsing/ExtendedDirac/EvaluationVisitorTset.h @@ -51,6 +51,7 @@ if (ctx->scset() != nullptr) { // set currentSplit = segment2split.front(); // access the first element segment2split.erase(segment2split.begin()); // remove it (O(n) operation) + currentSegmentKetLength = -1; return std::any_cast(visit(ctx->scset())); } else if (ctx->MUL() != nullptr || ctx->PROD() != nullptr) { std::string str0 = std::any_cast(visit(ctx->tset(0))); diff --git a/unit_tests/qasm_parsing_inclusion_cli_test.cc b/unit_tests/qasm_parsing_inclusion_cli_test.cc index 67cedc02d4..7eae7b7ce9 100644 --- a/unit_tests/qasm_parsing_inclusion_cli_test.cc +++ b/unit_tests/qasm_parsing_inclusion_cli_test.cc @@ -57,6 +57,20 @@ BOOST_AUTO_TEST_CASE(qasm_parsing_minimal_circuit) + AUTOQ::Serialization::TimbukSerializer::Serialize(spec)); } +// Kets for the same qubits must have the same length; otherwise parsing +// reports an AutoQError instead of aborting with std::out_of_range (#34). +BOOST_AUTO_TEST_CASE(misaligned_kets_are_rejected) +{ + std::string folder = test_dir_from_file(__FILE__, "testcase/KetAlignment/"); + using Parser = AUTOQ::Parsing::TimbukParser; + for (const std::string f : {"shorter_ket.hsl", "longer_ket.hsl", "ket_ends_inside_constant.hsl"}) + BOOST_CHECK_THROW(Parser::ReadAutomaton(folder + f), AutoQError); + BOOST_CHECK_THROW(Parser::ReadTwoAutomata(folder + "pre.hsl", folder + "post_shorter.hsl", folder + "circuit.qasm"), AutoQError); + + BOOST_CHECK_NO_THROW(Parser::ReadAutomaton(folder + "aligned.hsl")); + BOOST_CHECK_NO_THROW(Parser::ReadTwoAutomata(folder + "pre.hsl", folder + "post.hsl", folder + "circuit.qasm")); +} + BOOST_AUTO_TEST_CASE(inclusion_checker_equal_automata) { auto aut = AUTOQ::ConcreteAutomata::basis(3); diff --git a/unit_tests/testcase/KetAlignment/aligned.hsl b/unit_tests/testcase/KetAlignment/aligned.hsl new file mode 100644 index 0000000000..2c129768b9 --- /dev/null +++ b/unit_tests/testcase/KetAlignment/aligned.hsl @@ -0,0 +1,2 @@ +Extended Dirac +{|0x> : |x|=1} ∪ {|10>} diff --git a/unit_tests/testcase/KetAlignment/circuit.qasm b/unit_tests/testcase/KetAlignment/circuit.qasm new file mode 100644 index 0000000000..450318aea6 --- /dev/null +++ b/unit_tests/testcase/KetAlignment/circuit.qasm @@ -0,0 +1,3 @@ +OPENQASM 2.0; +include "qelib1.inc"; +qreg qubits[2]; diff --git a/unit_tests/testcase/KetAlignment/ket_ends_inside_constant.hsl b/unit_tests/testcase/KetAlignment/ket_ends_inside_constant.hsl new file mode 100644 index 0000000000..596dbe5779 --- /dev/null +++ b/unit_tests/testcase/KetAlignment/ket_ends_inside_constant.hsl @@ -0,0 +1,2 @@ +Extended Dirac +{|0x> : |x|=2} ∪ {|00>} diff --git a/unit_tests/testcase/KetAlignment/longer_ket.hsl b/unit_tests/testcase/KetAlignment/longer_ket.hsl new file mode 100644 index 0000000000..1955daf78b --- /dev/null +++ b/unit_tests/testcase/KetAlignment/longer_ket.hsl @@ -0,0 +1,2 @@ +Extended Dirac +{|x> : |x|=1} ∪ {|00>} diff --git a/unit_tests/testcase/KetAlignment/post.hsl b/unit_tests/testcase/KetAlignment/post.hsl new file mode 100644 index 0000000000..0ad7913cd6 --- /dev/null +++ b/unit_tests/testcase/KetAlignment/post.hsl @@ -0,0 +1,2 @@ +Extended Dirac +{|00>} diff --git a/unit_tests/testcase/KetAlignment/post_shorter.hsl b/unit_tests/testcase/KetAlignment/post_shorter.hsl new file mode 100644 index 0000000000..cdf60b305d --- /dev/null +++ b/unit_tests/testcase/KetAlignment/post_shorter.hsl @@ -0,0 +1,2 @@ +Extended Dirac +{|0>} diff --git a/unit_tests/testcase/KetAlignment/pre.hsl b/unit_tests/testcase/KetAlignment/pre.hsl new file mode 100644 index 0000000000..03019594f7 --- /dev/null +++ b/unit_tests/testcase/KetAlignment/pre.hsl @@ -0,0 +1,2 @@ +Extended Dirac +{|xy> : |x|=1, |y|=1} diff --git a/unit_tests/testcase/KetAlignment/shorter_ket.hsl b/unit_tests/testcase/KetAlignment/shorter_ket.hsl new file mode 100644 index 0000000000..8d443e43bc --- /dev/null +++ b/unit_tests/testcase/KetAlignment/shorter_ket.hsl @@ -0,0 +1,2 @@ +Extended Dirac +{|0x> : |x|=1} ∪ {|0>}