{
  "schema_version": 1,
  "project": "Souffle-LintQ",
  "reference": "Paltenghi, M. and Pradel, M. Analyzing Quantum Programs with LintQ: A Static Analysis Framework for Qiskit (FSE 2024). DOI: 10.1145/3660802.",
  "engines": {
    "A": {
      "dl": "engine/lintq.dl",
      "fact_schema": "extractor (Stmt/CFGEdge/Assign/CircuitAlloc/GateOp/MeasureOp/CircuitCall)",
      "detectors": ["OpAfterMeas", "DoubleMeas", "GhostCompose"]
    },
    "B": {
      "dl": "rules/lintq.dl",
      "fact_schema": "compact (Op/ActsOn/Succ)",
      "detectors": ["OpAfterMeas", "DoubleMeas", "MissingReset"]
    }
  },
  "label_legend": {
    "FIRE": "bug class is present; detector is expected to emit a warning",
    "SILENT": "bug class is intentionally ABSENT; detector must stay silent (negative example)",
    "FALSE_POS": "known engine limitation: a reset-blind detector will erroneously fire here (documented)",
    "N/A": "the engine/detector is structurally unable to observe this bug class"
  },
  "examples": [
    {
      "id": "ex01_op_after_meas_basic",
      "file": "ex01_op_after_meas_basic.py",
      "title": "Basic OpAfterMeas (single qubit)",
      "category": "correctness",
      "bug_class": "OpAfterMeas",
      "description": "A gate (h) is applied to q0 after q0 was measured, with no reset between. Both engines should flag it.",
      "ground_truth": {
        "A_OpAfterMeas": "FIRE", "A_DoubleMeas": "N/A", "A_GhostCompose": "N/A",
        "B_OpAfterMeas": "FIRE", "B_DoubleMeas": "N/A", "B_MissingReset": "SILENT"
      },
      "notes": "Resets at the top only initialise the qubit; the post-measurement gate is the bug."
    },
    {
      "id": "ex02_op_after_meas_reset_safe",
      "file": "ex02_op_after_meas_reset_safe.py",
      "title": "OpAfterMeas negative: reset between measure and gate",
      "category": "correctness",
      "bug_class": "OpAfterMeas",
      "description": "A reset lies between the measure and the later gate, so the gate operates on a re-initialised qubit. A reset-AWARE detector (Engine B) must stay silent; the reset-BLIND Engine A falsely fires -- a documented limitation.",
      "ground_truth": {
        "A_OpAfterMeas": "FALSE_POS", "A_DoubleMeas": "N/A", "A_GhostCompose": "N/A",
        "B_OpAfterMeas": "SILENT", "B_DoubleMeas": "N/A", "B_MissingReset": "SILENT"
      },
      "notes": "Key cross-engine example: discriminates reset-aware vs reset-blind OpAfterMeas."
    },
    {
      "id": "basic_reset_initialised",
      "file": "ex03_op_reach_2qubit.py",
      "title": "OpAfterMeas via 2-qubit gate (cx touches measured qubit)",
      "category": "correctness",
      "bug_class": "OpAfterMeas",
      "description": "cx(0,1) acts on both q0 and q1, but only q1 was measured earlier. Detector must flag the gate on q1 (the measured qubit) and NOT on q0.",
      "ground_truth": {
        "A_OpAfterMeas": "FIRE", "A_DoubleMeas": "N/A", "A_GhostCompose": "N/A",
        "B_OpAfterMeas": "FIRE", "B_DoubleMeas": "N/A", "B_MissingReset": "SILENT"
      },
      "notes": "Tests that ActsOn propagation over 2-qubit gates does not over-report to the unmeasured control qubit."
    },
    {
      "id": "ex04_op_after_meas_branch",
      "file": "ex04_op_after_meas_branch.py",
      "title": "OpAfterMeas only on the if-true branch",
      "category": "correctness",
      "bug_class": "OpAfterMeas",
      "description": "A gate on q0 after the measurement appears only inside the if-body; the else branch resets q0. Both engines must follow the if-true path and flag the gate.",
      "ground_truth": {
        "A_OpAfterMeas": "FIRE", "A_DoubleMeas": "N/A", "A_GhostCompose": "N/A",
        "B_OpAfterMeas": "FIRE", "B_DoubleMeas": "N/A", "B_MissingReset": "SILENT"
      },
      "notes": "Exercises CFG branching: the measure->gate reachability must hold along the taken branch."
    },
    {
      "id": "ex05_op_after_meas_loop",
      "file": "ex05_op_after_meas_loop.py",
      "title": "OpAfterMeas inside a loop body",
      "category": "correctness",
      "bug_class": "OpAfterMeas",
      "description": "The loop body applies a gate to q0 after the measurement, reachable on every iteration via the CFG back-edge. Both engines must traverse the loop and flag the in-body gate.",
      "ground_truth": {
        "A_OpAfterMeas": "FIRE", "A_DoubleMeas": "N/A", "A_GhostCompose": "N/A",
        "B_OpAfterMeas": "FIRE", "B_DoubleMeas": "N/A", "B_MissingReset": "SILENT"
      },
      "notes": "Exercises CFG cycle handling (transitive closure over the back-edge)."
    },
    {
      "id": "ex06_double_meas_basic",
      "file": "ex06_double_meas_basic.py",
      "title": "Basic DoubleMeas (consecutive measure of same qubit)",
      "category": "correctness",
      "bug_class": "DoubleMeas",
      "description": "q0 is measured twice with no operation on q0 between. Both engines should flag the redundant consecutive measure.",
      "ground_truth": {
        "A_OpAfterMeas": "N/A", "A_DoubleMeas": "FIRE", "A_GhostCompose": "N/A",
        "B_OpAfterMeas": "N/A", "B_DoubleMeas": "FIRE", "B_MissingReset": "SILENT"
      },
      "notes": "The canonical redundant-measurement pattern."
    },
    {
      "id": "ex07_double_meas_reset_between",
      "file": "ex07_double_meas_reset_between.py",
      "title": "DoubleMeas negative: reset between the two measures",
      "category": "correctness",
      "bug_class": "DoubleMeas",
      "description": "A reset on q0 sits between the two measures, so the second measure is NOT redundant. Both engines must stay silent (an op on the qubit between suppresses DoubleMeas).",
      "ground_truth": {
        "A_OpAfterMeas": "N/A", "A_DoubleMeas": "SILENT", "A_GhostCompose": "N/A",
        "B_OpAfterMeas": "N/A", "B_DoubleMeas": "SILENT", "B_MissingReset": "SILENT"
      },
      "notes": "Confirms the OpBetween / ModifiedQubit guard works as the dual of the reset case in OpAfterMeas."
    },
    {
      "id": "ex08_double_meas_diff_qubits",
      "file": "ex08_double_meas_diff_qubits.py",
      "title": "DoubleMeas negative: measures target different qubits",
      "category": "correctness",
      "bug_class": "DoubleMeas",
      "description": "The two measures target q0 then q1, so there is no redundant re-measure of the same qubit. Both engines must stay silent (tests qubit-index sensitivity).",
      "ground_truth": {
        "A_OpAfterMeas": "N/A", "A_DoubleMeas": "SILENT", "A_GhostCompose": "N/A",
        "B_OpAfterMeas": "N/A", "B_DoubleMeas": "SILENT", "B_MissingReset": "SILENT"
      },
      "notes": "Guards against qubit-insensitive index collisions."
    },
    {
      "id": "ex09_double_meas_branch",
      "file": "ex09_double_meas_branch.py",
      "title": "DoubleMeas inside a branch",
      "category": "correctness",
      "bug_class": "DoubleMeas",
      "description": "The second measure of q0 is reachable from the first along the if-true path with no operation on q0 between. Both engines should flag it.",
      "ground_truth": {
        "A_OpAfterMeas": "N/A", "A_DoubleMeas": "FIRE", "A_GhostCompose": "N/A",
        "B_OpAfterMeas": "N/A", "B_DoubleMeas": "FIRE", "B_MissingReset": "SILENT"
      },
      "notes": "DoubleMeas with CFG branching."
    },
    {
      "id": "ex10_double_meas_2qubitgate",
      "file": "ex10_double_meas_2qubitgate.py",
      "title": "DoubleMeas suppressed but OpAfterMeas fires on the same cx",
      "category": "correctness",
      "bug_class": "OpAfterMeas+DoubleMeas",
      "description": "A 2-qubit gate cx(0,1) on q0 lies between two measures of q0: that op correctly SUPPRESSES DoubleMeas, yet cx is itself a gate on q0 applied after the first measure, so it TRIGGERS OpAfterMeas. One detector must fire (OpAfterMeas) while the other stays silent (DoubleMeas).",
      "ground_truth": {
        "A_OpAfterMeas": "FIRE", "A_DoubleMeas": "SILENT", "A_GhostCompose": "N/A",
        "B_OpAfterMeas": "FIRE", "B_DoubleMeas": "SILENT", "B_MissingReset": "SILENT"
      },
      "notes": "Cross-detector interaction: the same statement is relevant to two analyses with opposite expectations."
    },
    {
      "id": "ex11_missing_reset_basic",
      "file": "ex11_missing_reset_basic.py",
      "title": "Basic MissingReset (first op is a gate, not a reset)",
      "category": "correctness",
      "bug_class": "MissingReset",
      "description": "The first operation that touches q0 is h (a gate), not a reset, so q0 is used while uninitialised. Engine B only.",
      "ground_truth": {
        "A_OpAfterMeas": "N/A", "A_DoubleMeas": "N/A", "A_GhostCompose": "N/A",
        "B_OpAfterMeas": "N/A", "B_DoubleMeas": "N/A", "B_MissingReset": "FIRE"
      },
      "notes": "Engine A has no MissingReset rule; marked N/A there."
    },
    {
      "id": "ex12_missing_reset_multi",
      "file": "ex12_missing_reset_multi.py",
      "title": "MissingReset on multiple qubits",
      "category": "correctness",
      "bug_class": "MissingReset",
      "description": "q1 and q2 are used before any reset, while q0 is reset first. Engine B must flag q1 and q2 only. (Implemented by Engine B's MissingReset emitting one row per uninitialised qubit.)",
      "ground_truth": {
        "A_OpAfterMeas": "N/A", "A_DoubleMeas": "N/A", "A_GhostCompose": "N/A",
        "B_OpAfterMeas": "N/A", "B_DoubleMeas": "N/A", "B_MissingReset": "FIRE"
      },
      "notes": "Tests Multi-arity of MissingReset (several qubits reported)."
    },
    {
      "id": "ex13_missing_reset_meas_first",
      "file": "ex13_missing_reset_meas_first.py",
      "title": "MissingReset where the first op is a measurement",
      "category": "correctness",
      "bug_class": "MissingReset+OpAfterMeas",
      "description": "qc.measure(0,0) precedes qc.h(0). Engine B sees MissingReset (first op on q0 was not a reset) and OpAfterMeas (gate after measure); Engine A sees only OpAfterMeas. Different engines, same qubit.",
      "ground_truth": {
        "A_OpAfterMeas": "FIRE", "A_DoubleMeas": "N/A", "A_GhostCompose": "N/A",
        "B_OpAfterMeas": "FIRE", "B_DoubleMeas": "N/A", "B_MissingReset": "FIRE"
      },
      "notes": "Demonstrates that a single statement sequence feeds two different analyses across the two engines."
    },
    {
      "id": "ex14_missing_reset_2qubit_gate",
      "file": "ex14_missing_reset_2qubit_gate.py",
      "title": "MissingReset via a 2-qubit gate (cx uses uninitialised qubits)",
      "category": "correctness",
      "bug_class": "MissingReset",
      "description": "cx(0,1) is the first operation touching both q0 and q1, so both are used while uninitialised. Engine B must flag q0 and q1 (the cx is a gate, not a reset).",
      "ground_truth": {
        "A_OpAfterMeas": "N/A", "A_DoubleMeas": "N/A", "A_GhostCompose": "N/A",
        "B_OpAfterMeas": "N/A", "B_DoubleMeas": "N/A", "B_MissingReset": "FIRE"
      },
      "notes": "MissingReset over a multi-target gate."
    },
    {
      "id": "ex15_ghost_compose_basic",
      "file": "ex15_ghost_compose_basic.py",
      "title": "Basic GhostCompose (discarded compose return value)",
      "category": "silent-noop",
      "bug_class": "GhostCompose",
      "description": "qc.compose(sub) is non-mutating in Qiskit, but its return value is discarded, so the composed circuit is silently dropped. Engine A only.",
      "ground_truth": {
        "A_OpAfterMeas": "N/A", "A_DoubleMeas": "N/A", "A_GhostCompose": "FIRE",
        "B_OpAfterMeas": "N/A", "B_DoubleMeas": "N/A", "B_MissingReset": "N/A"
      },
      "notes": "Engine B has no GhostCompose rule; marked N/A there."
    },
    {
      "id": "ex16_ghost_compose_assigned",
      "file": "ex16_ghost_compose_assigned.py",
      "title": "GhostCompose negative: return value captured",
      "category": "silent-noop",
      "bug_class": "GhostCompose",
      "description": "qc2 = qc.compose(sub) keeps the return value, so the circuit is intentionally retained. Engine A must NOT fire (negative example).",
      "ground_truth": {
        "A_OpAfterMeas": "N/A", "A_DoubleMeas": "N/A", "A_GhostCompose": "SILENT",
        "B_OpAfterMeas": "N/A", "B_DoubleMeas": "N/A", "B_MissingReset": "N/A"
      },
      "notes": "Confirms the AssignedStmt guard: only non-assigned compose() calls are flagged."
    },
    {
      "id": "ex17_ghost_compose_loop",
      "file": "ex17_ghost_compose_loop.py",
      "title": "GhostCompose inside a loop",
      "category": "silent-noop",
      "bug_class": "GhostCompose",
      "description": "compose() is called repeatedly across loop iterations, each discarding its return value. Engine A should flag every iteration (one warning per discarded call).",
      "ground_truth": {
        "A_OpAfterMeas": "N/A", "A_DoubleMeas": "N/A", "A_GhostCompose": "FIRE",
        "B_OpAfterMeas": "N/A", "B_DoubleMeas": "N/A", "B_MissingReset": "N/A"
      },
      "notes": "GhostCompose with control-flow iteration (each iteration is a distinct statement)."
    }
  ]
}
