Repository navigation
New Mellea post on LintQ for IVR #80
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Merged
Merged
Changes from all commits
Commits
Show all changes
16 commits
Select commit
Hold shift + click to select a range
fbcd1eb
New Mellea post on LintQ for IVR
gretadolcetti fbaf4da
Use simple_validate, start_session and remove attempts
gretadolcetti 2b46bf7
Run LintQ inside validate_lintq
gretadolcetti 463615e
More details for results reproduction
gretadolcetti 8108f08
How to install CodeQL and LintQ
gretadolcetti 9c9799d
Pointer to the prev example folder
gretadolcetti 202463d
Update content/blogs/qiskit-ivr-lintq.md
gretadolcetti 8cafe97
Update content/blogs/qiskit-ivr-lintq.md
gretadolcetti d7a0b7b
Update content/blogs/qiskit-ivr-lintq.md
gretadolcetti c3447a1
Mention LintQ Paper
gretadolcetti 95968a2
Add codehelm reference
gretadolcetti 33975a7
Update content/blogs/qiskit-ivr-lintq.md
gretadolcetti 7615aad
Update content/blogs/qiskit-ivr-lintq.md
gretadolcetti 589243a
Update release date
gretadolcetti b6f3d19
Update content/blogs/qiskit-ivr-lintq.md
gretadolcetti d305c3f
Clarify where validate_lintq comes from in the LintQ blog post
gretadolcetti File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Some comments aren't visible on the classic Files Changed page.
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,239 @@ | ||
| --- | ||
| title: "Tests Pass, Smells Stay: Adding a Quantum Linter to Qiskit IVR" | ||
| date: "2026-10-23" | ||
| author: "Greta Dolcetti" | ||
| excerpt: "Quantum programs can pass every test and still carry quantum-specific code smells. We wired LintQ into Mellea's Instruct-Validate-Repair loop next to the test suite and roughly tripled the share of generations that are both correct and smell-free." | ||
| tags: ["qiskit", "IVR", "validation", "static-analysis", "LLM", "quantum", "code-generation"] | ||
| --- | ||
|
|
||
| This series has been adding validators to Mellea's Instruct-Validate-Repair | ||
| (IVR) loop for Qiskit, one gap at a time. The | ||
| [first post](https://mellea.ai/blogs/qiskit-ivr-code-validation/) used | ||
| `flake8-qiskit-migration` to catch deprecated APIs. The | ||
| [second](https://mellea.ai/blogs/qiskit-ivr-functional-validation/) put each benchmark | ||
| problem's `check()` test in the loop, so the model could see when its code | ||
| ran but gave the wrong answer. | ||
|
|
||
| This post covers a third gap: code that uses the right APIs, passes its | ||
| tests, and still contains code smells that might silently impact the | ||
| program execution. | ||
|
|
||
| ## Code smells that tests don't see | ||
|
|
||
| A *code smell* is code that runs without error but is likely to cause | ||
| incorrect or unexpected behavior. Python developers have a mature linting | ||
| culture for these. However, quantum programs have their own class of smells, which | ||
| classical linters and unit tests are not able to catch: a | ||
| gate applied after a qubit has already been measured, a register that is | ||
| allocated and never used, etc. | ||
|
|
||
| [LintQ](https://github.com/sola-st/LintQ) is a static analyzer for Qiskit | ||
| built on [CodeQL](https://codeql.github.com/). It models circuits, | ||
| registers, and gates as CodeQL abstractions and ships queries for ten | ||
| quantum-specific smells described in the [LintQ paper](https://arxiv.org/abs/2310.00718), including: | ||
|
|
||
| | LintQ rule | What it flags | | ||
| | --- | --- | | ||
| | `measure-all-abuse` | `measure_all()` used although a classical register already exists | | ||
| | `operation-after-measurement` | Gate applied to a qubit after it was measured | | ||
| | `constant-classic-bit` | A qubit is measured but was never transformed | | ||
| | `oversized-circuit` | Register with allocated but unused qubits | | ||
| | `double-measurement` | Two measurements in a row on the same qubit | | ||
|
|
||
| LintQ is neither sound nor complete, like most linters. But it is | ||
| deterministic and reproducible, and when it flags something it names the | ||
| rule and the circuit involved. As the earlier posts showed, that is the | ||
| property that matters for a repair loop. | ||
|
|
||
| ## Two validators, one requirement | ||
|
|
||
| The setup follows the earlier posts: Mellea's `m.instruct()` with a | ||
| requirement and a `MultiTurnStrategy`. The difference is that the | ||
| requirement's validation function now runs two checks in sequence: | ||
|
|
||
| ```python | ||
| from mellea.stdlib.requirements import req, simple_validate | ||
| from validation_helpers import extract_code_from_markdown, validate_correctness | ||
|
|
||
| # validate_lintq() is defined in the next snippet | ||
|
|
||
|
|
||
| def tests_and_lintq(problem: dict): | ||
| def check(output: str) -> tuple[bool, str]: | ||
| code = extract_code_from_markdown(output) | ||
| passed, reason = validate_correctness(problem, code) | ||
| return validate_lintq(code) if passed else (passed, reason) | ||
|
|
||
| return req( | ||
| "The code must pass the problem's test suite and raise no LintQ warnings", | ||
| validation_fn=simple_validate(check), | ||
| ) | ||
| ``` | ||
|
|
||
| `simple_validate` hands `check()` the model's latest output as a string, and | ||
| turns the `(passed, reason)` tuple it returns into a validation result whose | ||
| reason becomes the repair feedback. | ||
|
|
||
| Correctness goes first, because there's no point linting a program that | ||
| doesn't run. `validate_correctness()` executes the code against the | ||
| problem's own test suite with a 30-second timeout. In order to keep functional | ||
|
ajbozarth marked this conversation as resolved.
|
||
| correctness as a first order priority, only a program that | ||
| passes the tests gets linted. | ||
|
|
||
| `validate_lintq()` writes the code to a temporary directory, builds a | ||
| CodeQL database from it, runs the `LintQ-all.qls` query suite, and turns the | ||
| SARIF results into repair feedback, one `[rule-id] message` line per finding: | ||
|
|
||
| ```python | ||
| import json | ||
| import os | ||
| import subprocess | ||
| import tempfile | ||
|
|
||
|
|
||
| def validate_lintq(code: str) -> tuple[bool, str]: | ||
| lintq = os.environ["LINTQ_DIR"] | ||
| with tempfile.TemporaryDirectory() as tmp: | ||
| os.makedirs(f"{tmp}/src") | ||
| with open(f"{tmp}/src/candidate.py", "w") as f: | ||
| f.write(code) | ||
| for cmd in ( | ||
| ["database", "create", f"{tmp}/db", "--language=python", f"--source-root={tmp}/src"], | ||
| [ | ||
| "database", "analyze", f"{tmp}/db", f"{lintq}/LintQ-all.qls", | ||
| "--format=sarifv2.1.0", f"--output={tmp}/out.sarif", | ||
| f"--additional-packs={lintq}/qlint/codeql/src:{lintq}/qlint/codeql/lib", | ||
| ], | ||
| ): | ||
| subprocess.run(["codeql", *cmd], check=True, capture_output=True) | ||
| with open(f"{tmp}/out.sarif") as f: | ||
| results = [r for run in json.load(f)["runs"] for r in run["results"]] | ||
|
|
||
| if not results: | ||
| return True, "" | ||
| findings = "\n".join(f"[{r['ruleId']}] {r['message']['text']}" for r in results) | ||
| return False, f"LintQ warnings:\n{findings}" | ||
| ``` | ||
|
|
||
| Wiring it into Mellea is the same `m.instruct()` call as before. Each problem | ||
| gets its own session with a fresh `ChatContext`, so one task's repair | ||
| conversation never leaks into the next: | ||
|
|
||
| ```python | ||
| from mellea import start_session | ||
| from mellea.stdlib.context import ChatContext | ||
| from mellea.stdlib.sampling import MultiTurnStrategy | ||
|
|
||
| with start_session(backend_name="<backend>", model_id="<model-id>", ctx=ChatContext()) as m: | ||
| result = m.instruct( | ||
| problem["prompt"], | ||
| requirements=[tests_and_lintq(problem)], | ||
| strategy=MultiTurnStrategy(loop_budget=3), | ||
| return_sampling_results=True, | ||
| ) | ||
| ``` | ||
|
|
||
| ## Results | ||
|
|
||
| We ran `gpt-oss-120b` on | ||
|
ajbozarth marked this conversation as resolved.
|
||
| the *hard* variant of Qiskit HumanEval, where the prompt is a | ||
| natural-language instruction with no code stub, so the model also has to | ||
| write the function signature. We also ran it on the full Quantum Katas set. | ||
| The loop budget was three attempts: the first generation plus up to two | ||
| repairs. "First pass" is generation 0 on its own; "post repair" is the | ||
| outcome of the whole loop. | ||
|
|
||
| To reproduce these numbers, this is the setup we used: | ||
|
|
||
| | Component | Version or setting | | ||
| | --- | --- | | ||
| | Model | `gpt-oss-120b`, served through an OpenAI-compatible endpoint and called with Mellea's `OpenAIBackend` | | ||
| | Strategy | `MultiTurnStrategy(loop_budget=3)` | | ||
| | Python | 3.12.8 | | ||
| | Mellea | 0.7.0 | | ||
| | Qiskit | `qiskit` 2.4.2, `qiskit-aer` 0.17.2, `qiskit-ibm-runtime` 0.45.1, `qiskit-ibm-transpiler` 0.18.0 | | ||
| | CodeQL CLI | 2.26.3 | | ||
| | IBM Quantum account | Saved using (`QiskitRuntimeService.save_account(...)`) | | ||
|
|
||
| > **Note:** LintQ only runs on programs that pass their tests. If the | ||
| > "Flagged by LintQ" or "LintQ warnings raised" count goes up after repair, | ||
| > it's only because the loop fixed correctness for more programs, so more of | ||
| > them reached LintQ. It doesn't mean repair made the code smellier. | ||
|
|
||
| ### Qiskit HumanEval (hard), 151 tasks | ||
|
|
||
| | Metric | First pass | Post repair | | ||
| | --- | --- | --- | | ||
| | Passed both validators | 24/151 (16%) | **70/151 (46%)** | | ||
| | Failed | 127/151 (84%) | 81/151 (54%) | | ||
| | Failed the test suite | 113 | 77 | | ||
| | Flagged by LintQ | 14 | 4 | | ||
| | LintQ warnings raised | 17 | 4 | | ||
| | Fixed by the repair loop | — | 46 | | ||
|
|
||
| ### Qiskit Quantum Katas, 350 tasks | ||
|
|
||
| | Metric | First pass | Post repair | | ||
| | --- | --- | --- | | ||
| | Passed both validators | 25/350 (7%) | **163/350 (47%)** | | ||
| | Failed | 325/350 (93%) | 187/350 (53%) | | ||
| | Failed the test suite | 297 | 148 | | ||
| | Flagged by LintQ | 28 | 39 | | ||
| | LintQ warnings raised | 30 | 47 | | ||
| | Fixed by the repair loop | — | 138 | | ||
|
|
||
| With a budget of three attempts, the share of tasks that are both correct and | ||
| smell-free went from 16% to 46% on Qiskit HumanEval (hard) and from 7% to | ||
| 47% on Quantum Katas. | ||
|
|
||
| ## Takeaways | ||
|
|
||
| The three posts in this series use three kinds of validator, and each one | ||
| catches problems the others miss: | ||
|
|
||
| | Validator | Catches | Misses | | ||
| | --- | --- | --- | | ||
| | `flake8-qiskit-migration` | Deprecated and removed APIs | Behavior | | ||
| | Functional tests (`check()`) | Wrong answers, crashes | Quantum-specific smells | | ||
| | LintQ | Quantum code smells | Behavior, and intent | | ||
|
|
||
| None of them required changes to Mellea. Each is a function that returns | ||
| `(is_valid, error_message)`, wrapped in a requirement. The loop takes care of | ||
| turning failures into repair prompts. The more validators you stack, the | ||
| more aspects are checked and enforced during the generation. | ||
|
|
||
| A few practical notes if you want to try LintQ in your own loop: | ||
|
|
||
| - **Check correctness first.** LintQ builds a CodeQL database for every | ||
| candidate, which takes a few seconds. Skipping that for programs that fail | ||
| their tests saves time and keeps the repair feedback focused on one | ||
| problem at a time. | ||
| - **Keep the rule ID in the message.** `[ql-unmeasurable-qubits] Circuit | ||
| 'qc' has more qubits (1) than classical bits (0)` tells the model which | ||
| rule fired and on which circuit. | ||
|
|
||
| ## Try it | ||
|
|
||
| LintQ is a set of CodeQL queries, so it needs the CodeQL CLI and a checkout | ||
| of the LintQ repository: | ||
|
|
||
| 1. Install the | ||
| [CodeQL CLI](https://docs.github.com/en/code-security/codeql-cli/getting-started-with-the-codeql-cli/setting-up-the-codeql-cli) | ||
| and put `codeql` on your `PATH` (`brew install codeql` works on macOS). | ||
| Check it with `codeql version`. | ||
| 2. Clone LintQ and install its query pack dependencies: | ||
|
|
||
| ```bash | ||
| git clone --depth 1 https://github.com/sola-st/LintQ.git | ||
| (cd LintQ/qlint/codeql/src && codeql pack install) | ||
| ``` | ||
|
|
||
| 3. Point `LINTQ_DIR` at the checkout: | ||
|
|
||
| ```bash | ||
| export LINTQ_DIR="$(pwd)/LintQ" | ||
| ``` | ||
|
|
||
| This blog post is built as an extension of the [Mellea IVR example](https://github.com/generative-computing/mellea/tree/main/docs/examples/instruct_validate_repair/qiskit_code_validation). | ||
| The full reproduction code, including the LintQ validator, will be released in the IBM open-source project [CodeHelm](https://github.com/IBM/codehelm). | ||
| Adding LintQ to it means adding one more validation function with the code shown above. | ||
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.