wai-quantum: any exercise is checked as a lesson is (engine::course::check_exercise) #645

Merged
dcharlot merged 11 commits from wai/quantum-check-exercise into main 2026-11-06 15:15:59 -05:00
8 changed files with 656 additions and 82 deletions

View file

@ -125,7 +125,7 @@ jobs:
run: |
set -euo pipefail
cargo test --locked --release --features full --lib -- engine::course:: 2>&1 | tee "$RUNNER_TEMP/course.log"
grep -qE '^test result: ok\. 9 passed; 0 failed' "$RUNNER_TEMP/course.log" || { echo "::error::the course's tests did not all pass"; exit 1; }
grep -qE '^test result: ok\. 14 passed; 0 failed' "$RUNNER_TEMP/course.log" || { echo "::error::the course's tests did not all pass"; exit 1; }
for t in every_solution_passes_its_exercise_and_every_known_wrong_answer_fails_it \
a_check_says_what_went_wrong_from_the_engine_and_never_shows_the_solution \
no_hint_or_lesson_gives_the_solution \
@ -134,7 +134,12 @@ jobs:
the_schemas_name_the_fields_the_reader_takes \
a_check_is_the_same_bytes_every_time \
the_estimate_lesson_prices_the_program \
a_locale_tag_picks_the_course_s_locale; do
a_locale_tag_picks_the_course_s_locale \
an_exercise_outside_the_course_is_checked_as_a_lesson_is \
a_malformed_exercise_is_refused_before_anything_is_checked \
every_case_binds_its_inputs_and_is_held_to_the_constraints \
a_t_count_counts_t_gates_however_they_are_written \
an_injected_error_is_corrected_from_the_qubits_and_never_read; do
grep -q "^test engine::course::tests::$t \.\.\. ok$" "$RUNNER_TEMP/course.log" || { echo "::error::$t did not run"; exit 1; }
done
- name: The command line's course and its languages

View file

@ -0,0 +1,28 @@
### wai-quantum: any exercise is checked as a lesson is
`engine::course::check_exercise` checks a program against an exercise document in the course's form
(`wai.quantum.exercise/1`): the same verdicts, constraints and words as a lesson's check, with `lesson` null. It is how
a task that is not in the course is held to what it must do, the first step toward the job service writing a program
for an intent and checking each attempt. `check` now looks up its lesson and calls it, and its answers are the same
bytes for a program that passes.
- **The exercise is read first.** `engine::course::exercise_form` holds a document to the form a lesson's exercise is
held to, as far as a check reads it, and `check_exercise` refuses what it refuses before anything runs: a bound that
is not whole numbers (`{"max": 1.5}`, `{"max": "0"}`, a bare number), a constraint or a field the course does not
name, a job with more than shots, seed and noise (so a task cannot ask for a repair the transcript does not show),
more than 16 cases (`MOST_CASES`) or 8 intents in one place (`MOST_INTENTS`). Before, such a constraint held
vacuously, an unknown one was dropped, and a job that was not an object stopped the check with a panic.
- **Every case binds its inputs.** A program that does not declare an input a case gives is refused in that case
(`Q114`), where it was run without the input: the bit-flip code's lesson passed with no flip at all. A case whose
`intent` is null takes the exercise's, as one with none does.
- **The constraints hold in every case**, not only the first: a gate one case alone adds is counted, and the case is
named in what went wrong.
- **An injected error is corrected from the qubits.** A new constraint, `inject`, names the input whose value is the
qubit an error flips: the cases' programs may differ in that one x alone, so the program cannot read the error and
must find and correct it with its quantum operations. Another, `qubits` (`{"max": n}`), bounds the qubits a program
declares. The bit-flip and syndrome lessons set both: before, a program that declared the error and never applied
it passed the bit-flip lesson, and one that branched on it classically passed the syndrome lesson.
- **A T count counts T gates however they are written:** a `p` or `rz` by an odd multiple of π/4 counts one, a
Clifford gate none, and any other gate (a rotation by another angle, a Toffoli) does not hold to a T count, since
its count is not fixed. A gate on three qubits does not hold to a count of two-qubit gates. Before, `t_count`
counted gate names, so the t-count lesson passed with every T written as `p(pi/4)`.
- **Breaking:** `api:rust:wai_quantum::engine::course::CONSTRAINTS`: the array gains `qubits` and `inject`, so its length goes from 6 to 8. Code that names the array's length should read `CONSTRAINTS.len()`; code that iterates it needs no change.

View file

@ -27,7 +27,7 @@
{ "inputs": { "err": 2 }, "name": { "en": "a flip of q[2]", "zh-CN": "q[2] 被翻转" } }
],
"intent": { "distribution": { "1": 1.0 } },
"constraints": { "allow": ["x", "cx", "ccx"], "counts": { "x": { "max": 2 } } },
"constraints": { "allow": ["x", "cx", "ccx"], "counts": { "x": { "max": 2 } }, "qubits": { "max": 3 }, "inject": "err" },
"hints": {
"en": [
"Encode with two controlled-NOTs from q[0]: |1⟩ becomes |111⟩, and one flip leaves two copies right.",

View file

@ -24,7 +24,7 @@
{ "inputs": { "err": 1 }, "name": { "en": "a flip of q[1]", "zh-CN": "q[1] 被翻转" }, "intent": { "distribution": { "11": 1.0 } } },
{ "inputs": { "err": 2 }, "name": { "en": "a flip of q[2]", "zh-CN": "q[2] 被翻转" }, "intent": { "distribution": { "10": 1.0 } } }
],
"constraints": { "allow": ["x", "cx"], "counts": { "x": { "max": 1 }, "cx": { "max": 4 } } },
"constraints": { "allow": ["x", "cx"], "counts": { "x": { "max": 1 }, "cx": { "max": 4 } }, "qubits": { "max": 5 }, "inject": "err" },
"hints": {
"en": [
"A controlled-NOT from a data qubit onto an ancilla in |0⟩ adds that qubit's value to the ancilla, mod 2.",

View file

@ -24,6 +24,8 @@
"constraint.between": "between {min} and {max} {gate}",
"constraint.t_count": "at most {n} T and T† gates",
"constraint.two_qubit": "at most {n} two-qubit gates",
"constraint.qubits": "at most {n} qubits",
"constraint.inject": "the error {input} flips qubit {input} with one x, and the program uses {input} nowhere else",
"constraint.no_smells": "no smell of these kinds from the critique: {kinds}",
"constraint.estimate": "a resource estimate counting at most {n} {what}",
"estimate.toffolis": "Toffoli gates",
@ -46,6 +48,12 @@
"wrong.count": "{gate}: your program has {found}, and the exercise asks for {limit}.",
"wrong.t_count": "Your program has {found} T and T† gates, and the exercise allows {limit}.",
"wrong.two_qubit": "Your program has {found} two-qubit gates, and the exercise allows {limit}.",
"wrong.t_count_other": "Your program has a {gate} gate, which is neither a Clifford nor a T gate, so its T count is not fixed: the exercise allows {limit} T and T† gates.",
"wrong.two_qubit_wide": "Your program has a {gate} gate, which acts on three qubits, so a count of two-qubit gates does not bound it: the exercise allows {limit} two-qubit gates.",
"wrong.qubits": "Your program has {found} qubits, and the exercise allows {limit}.",
"wrong.inject_unused": "Your program never flips qubit {input} with {input}: put `x q[{input}];` where the error happens, and correct it after.",
"wrong.inject_read": "Your program uses {input} beyond flipping qubit {input} once with x, so it could know the error without looking at the qubits: find the error from the qubits.",
"wrong.inject_where": "The gate {input} changes in your program is not an x on qubit {input}: flip that qubit with `x q[{input}];`.",
"wrong.smell": "The critique finds a smell the exercise rules out, at line(s) {lines}.",
"wrong.note": "The critique notes something at line(s) {lines}.",
"wrong.estimate": "The resource estimate counts {found} {what}, and the exercise allows {limit}.",
@ -60,6 +68,10 @@
"pointer.count": "Count your {gate} gates.",
"pointer.t_count": "Look for T and T† gates that combine into Clifford gates.",
"pointer.two_qubit": "Look for two-qubit gates you can do without.",
"pointer.t_count_other": "Write each {gate} with T, T† and Clifford gates.",
"pointer.two_qubit_wide": "Write each {gate} with one- and two-qubit gates.",
"pointer.qubits": "Use only the qubits the exercise needs.",
"pointer.inject": "Look at every line that uses {input}.",
"pointer.smell": "Look at line(s) {lines}: the critique flags them.",
"pointer.estimate": "Look for work the program does and then undoes.",
"pointer.case": "Try the case {name} first.",

View file

@ -24,6 +24,8 @@
"constraint.between": "{min} 到 {max} 个 {gate}",
"constraint.t_count": "T 与 T† 门合计至多 {n} 个",
"constraint.two_qubit": "双比特门至多 {n} 个",
"constraint.qubits": "至多 {n} 个量子比特",
"constraint.inject": "错误 {input} 只由一个 x 翻转第 {input} 个量子比特,程序在别处不使用 {input}",
"constraint.no_smells": "评析中不出现以下类型的问题模式:{kinds}",
"constraint.estimate": "资源估算计入的 {what} 至多 {n} 个",
"estimate.toffolis": "Toffoli 门",
@ -46,6 +48,12 @@
"wrong.count": "{gate}:你的程序有 {found} 个,练习要求 {limit}。",
"wrong.t_count": "你的程序有 {found} 个 T 与 T† 门,练习只允许 {limit}。",
"wrong.two_qubit": "你的程序有 {found} 个双比特门,练习只允许 {limit}。",
"wrong.t_count_other": "你的程序有 {gate} 门,它既不是 Clifford 门也不是 T 门,所以它的 T 门数不确定:练习只允许 {limit} 个 T 与 T† 门。",
"wrong.two_qubit_wide": "你的程序有 {gate} 门,它作用于三个量子比特,双比特门的计数约束不了它:练习只允许 {limit} 个双比特门。",
"wrong.qubits": "你的程序有 {found} 个量子比特,练习只允许 {limit}。",
"wrong.inject_unused": "你的程序从未用 {input} 翻转第 {input} 个量子比特:在错误发生处写 `x q[{input}];`,再在之后纠正它。",
"wrong.inject_read": "你的程序除了用一个 x 翻转第 {input} 个量子比特之外还用到了 {input},于是不看量子比特也能知道错误:请从量子比特中找出错误。",
"wrong.inject_where": "{input} 在你的程序中改变的门不是作用在第 {input} 个量子比特上的 x:请用 `x q[{input}];` 翻转该量子比特。",
"wrong.smell": "评析在第 {lines} 行发现了这道练习所排除的问题模式。",
"wrong.note": "评析在第 {lines} 行有一条提示。",
"wrong.estimate": "资源估算计入了 {found} 个{what},练习只允许 {limit}。",
@ -60,6 +68,10 @@
"pointer.count": "数一数你的 {gate} 门。",
"pointer.t_count": "找一找可以合并成 Clifford 门的 T 与 T† 门。",
"pointer.two_qubit": "找一找可以省掉的双比特门。",
"pointer.t_count_other": "用 T、T† 与 Clifford 门写出每个 {gate}。",
"pointer.two_qubit_wide": "用单比特门与双比特门写出每个 {gate}。",
"pointer.qubits": "只使用练习需要的量子比特。",
"pointer.inject": "检查每一行用到 {input} 的地方。",
"pointer.smell": "看第 {lines} 行:评析标出了它们。",
"pointer.estimate": "找一找程序先做了又撤销掉的工作。",
"pointer.case": "先试试{name}的情况。",

View file

@ -78,6 +78,20 @@
"minimum": 0
}
}
},
"at_most": {
"type": "object",
"description": "A bound the course says as at most n: its max alone",
"additionalProperties": false,
"required": [
"max"
],
"properties": {
"max": {
"type": "integer",
"minimum": 0
}
}
}
},
"properties": {
@ -133,7 +147,8 @@
"cases": {
"type": "array",
"minItems": 1,
"description": "A generator's inputs to check the program at, each a case with its own verdict; a case without an intent takes the exercise's",
"maxItems": 16,
"description": "A generator's inputs to check the program at, each a case with its own verdict, at most 16 (engine::course::MOST_CASES); a case without an intent takes the exercise's. Every input a case gives must be one the program declares, and the constraints hold in every case",
"items": {
"type": "object",
"additionalProperties": false,
@ -178,12 +193,12 @@
"description": "Bounds on how many of a gate the program has"
},
"t_count": {
"$ref": "#/$defs/bound",
"description": "A bound on the T and T† gates, as the critique counts them"
"$ref": "#/$defs/at_most",
"description": "A bound on the T gates: a t or tdg counts one, a p or rz by an odd multiple of π/4 one (a T up to Clifford gates), a Clifford gate none; a program with any other gate holds to no T count"
},
"two_qubit": {
"$ref": "#/$defs/bound",
"description": "A bound on the two-qubit gates"
"$ref": "#/$defs/at_most",
"description": "A bound on the two-qubit gates; a program with a gate on three qubits holds to none"
},
"no_smells": {
"type": "array",
@ -205,21 +220,30 @@
"additionalProperties": false,
"properties": {
"toffolis": {
"$ref": "#/$defs/bound"
"$ref": "#/$defs/at_most"
},
"t_gates": {
"$ref": "#/$defs/bound"
"$ref": "#/$defs/at_most"
},
"rotations": {
"$ref": "#/$defs/bound"
"$ref": "#/$defs/at_most"
},
"logical_qubits": {
"$ref": "#/$defs/bound"
"$ref": "#/$defs/at_most"
},
"t_states": {
"$ref": "#/$defs/bound"
"$ref": "#/$defs/at_most"
}
}
},
"qubits": {
"$ref": "#/$defs/at_most",
"description": "A bound on the qubits the program declares"
},
"inject": {
"type": "string",
"minLength": 1,
"description": "The input whose value, in each case, is the qubit an error flips: the cases' programs may differ only in that one x, so the program finds and corrects the error from the qubits and cannot read it otherwise. Every case gives it, and at least two give different ones"
}
}
},

View file

@ -58,13 +58,18 @@ pub const EXERCISE_FIELDS: [&str; 13] = ["schema", "id", "translations", "title"
/// The fields an exercise must have.
pub const EXERCISE_REQUIRED: [&str; 8] = ["schema", "id", "title", "task", "starter", "hints", "reference", "wrong"];
/// The constraints an exercise may set.
pub const CONSTRAINTS: [&str; 6] = ["allow", "counts", "t_count", "two_qubit", "no_smells", "estimate"];
pub const CONSTRAINTS: [&str; 8] = ["allow", "counts", "t_count", "two_qubit", "no_smells", "estimate", "qubits", "inject"];
/// What an exercise's job may give.
pub const JOB_FIELDS: [&str; 3] = ["shots", "seed", "noise"];
/// The resource estimate's counts a constraint may bound.
pub const ESTIMATE_COUNTS: [&str; 5] = ["toffolis", "t_gates", "rotations", "logical_qubits", "t_states"];
/// Why a known wrong answer fails.
pub const FAILS: [&str; 3] = ["intent", "constraint", "refused"];
/// The most cases an exercise may have: each is a run of the program, and its
/// verdict, in every check.
pub const MOST_CASES: usize = 16;
/// The most intents an exercise, or one of its cases, may give.
pub const MOST_INTENTS: usize = 8;
const COURSE: &str = include_str!("../../course/course.json");
const MESSAGES: [(&str, &str); 2] = [("en", include_str!("../../course/locales/en.json")), ("zh-CN", include_str!("../../course/locales/zh-CN.json"))];
@ -179,6 +184,185 @@ fn bound(v: &Value, what: &str) -> Result<(), String> {
Ok(())
}
/// A bound the course says as "at most n" (a T count, a two-qubit count, an
/// estimate's count): `{"max": n}` alone, so what the check holds a program to
/// is what its words say.
fn at_most(v: &Value, what: &str) -> Result<(), String> {
bound(v, what)?;
if v.as_object().is_none_or(|o| o.len() != 1 || !o.contains_key("max")) {
return Err(format!("{what}: {{\"max\": n}} alone: the course says this bound as at most n"));
}
Ok(())
}
/// A field of an exercise or a case, null read as absent.
fn present<'a>(o: &'a Map<String, Value>, k: &str) -> Option<&'a Value> {
o.get(k).filter(|v| !v.is_null())
}
/// An intent (one, or a list) checked in its shape and its number.
fn intent_list(v: &Value, what: &str) -> Result<(), String> {
let list = verdict::intents(v).map_err(|e| format!("{what}: {e}"))?;
if list.len() > MOST_INTENTS {
return Err(format!("{what}: {} intents, and one may give at most {MOST_INTENTS}", list.len()));
}
Ok(())
}
/// What an exercise asks of a program, checked in its form as a check reads
/// it: its job (shots, seed and noise, nothing else), its intent, its cases
/// (at most [`MOST_CASES`], each with its inputs as whole numbers and an
/// intent of its own or the exercise's) and its constraints (each one the
/// course names, each bound whole numbers). A field that is null is absent.
/// Nothing malformed is read as no bound, or dropped: a check of an exercise
/// that does not read here would hold vacuously where it reads nothing.
fn asks(o: &Map<String, Value>, id: &str) -> Result<(), String> {
if let Some(job) = present(o, "job") {
let j = job.as_object().ok_or_else(|| format!("{id}: job is an object"))?;
if let Some(k) = j.keys().find(|k| !JOB_FIELDS.contains(&k.as_str())) {
return Err(format!("{id}: job takes {}, not {k}", JOB_FIELDS.join(", ")));
}
if present(j, "shots").is_some_and(|s| !s.as_u64().is_some_and(|n| n >= 1)) {
return Err(format!("{id}: job: shots is a whole number, at least 1"));
}
if present(j, "seed").is_some_and(|s| s.as_u64().is_none()) {
return Err(format!("{id}: job: seed is a whole number"));
}
if let Some(n) = present(j, "noise") {
let n = n.as_object().ok_or_else(|| format!("{id}: job: noise is an object of channels and their probabilities"))?;
if let Some((k, _)) = n.iter().find(|(_, p)| !p.as_f64().is_some_and(|p| (0.0..=1.0).contains(&p))) {
return Err(format!("{id}: job: noise: {k} takes a probability, 0 to 1"));
}
}
}
let intent = present(o, "intent");
let cases = present(o, "cases");
if intent.is_none() && cases.is_none() {
return Err(format!("{id}: an exercise needs an intent, or cases that each carry one"));
}
if let Some(i) = intent {
intent_list(i, &format!("{id}: intent"))?;
}
if let Some(cases) = cases {
let list = cases.as_array().filter(|a| !a.is_empty()).ok_or_else(|| format!("{id}: cases is a list of at least one case"))?;
if list.len() > MOST_CASES {
return Err(format!("{id}: {} cases, and an exercise may have at most {MOST_CASES}", list.len()));
}
for (n, c) in list.iter().enumerate() {
let what = format!("{id}: case {}", n + 1);
let co = c.as_object().ok_or_else(|| format!("{what} is an object"))?;
if let Some(k) = co.keys().find(|k| !["inputs", "name", "intent"].contains(&k.as_str())) {
return Err(format!("{what}: `{k}` is not a field of a case (inputs, name, intent)"));
}
if let Some(ins) = present(co, "inputs") {
let ins = ins.as_object().ok_or_else(|| format!("{what}: inputs is an object of whole numbers"))?;
if ins.values().any(|x| x.as_i64().is_none()) {
return Err(format!("{what}: inputs is an object of whole numbers"));
}
}
if let Some(nm) = present(co, "name")
&& !(nm.is_string() || nm.as_object().is_some_and(|m| m.values().all(Value::is_string)))
{
return Err(format!("{what}: name is a text, or one for each locale"));
}
match present(co, "intent") {
Some(i) => intent_list(i, &format!("{what}: intent"))?,
None if intent.is_none() => return Err(format!("{what}: no intent, and the exercise has none")),
None => {}
}
}
}
if let Some(c) = present(o, "constraints") {
let co = c.as_object().ok_or_else(|| format!("{id}: constraints is an object"))?;
if let Some(k) = co.keys().find(|k| !CONSTRAINTS.contains(&k.as_str())) {
return Err(format!("{id}: `{k}` is not a constraint ({})", CONSTRAINTS.join(", ")));
}
if let Some(a) = co.get("allow") {
let gates = a.as_array().filter(|a| !a.is_empty() && a.iter().all(Value::is_string)).ok_or_else(|| format!("{id}: allow is a list of gate names"))?;
if let Some(g) = gates.iter().filter_map(Value::as_str).find(|g| !GATES.contains(g)) {
return Err(format!("{id}: allow: {g} is not a gate the reader names"));
}
}
if let Some(cs) = co.get("counts") {
for (g, b) in cs.as_object().ok_or_else(|| format!("{id}: counts is an object of gates and bounds"))? {
if !GATES.contains(&g.as_str()) {
return Err(format!("{id}: counts: {g} is not a gate the reader names"));
}
bound(b, &format!("{id}: counts: {g}"))?;
}
}
for k in ["t_count", "two_qubit"] {
if let Some(b) = co.get(k) {
at_most(b, &format!("{id}: {k}"))?;
}
}
if let Some(s) = co.get("no_smells") {
let kinds = s.as_array().filter(|a| !a.is_empty()).ok_or_else(|| format!("{id}: no_smells is a list of the critique's smell kinds"))?;
if let Some(k) = kinds.iter().find(|k| !k.as_str().is_some_and(|k| SMELLS.contains(&k))) {
return Err(format!("{id}: no_smells: {k} is not a smell the critique names ({})", SMELLS.join(", ")));
}
}
if let Some(e) = co.get("estimate") {
for (k, b) in e.as_object().ok_or_else(|| format!("{id}: estimate is an object of the estimate's counts and bounds"))? {
if !ESTIMATE_COUNTS.contains(&k.as_str()) {
return Err(format!("{id}: estimate: {k} is not a count the estimate gives ({})", ESTIMATE_COUNTS.join(", ")));
}
at_most(b, &format!("{id}: estimate: {k}"))?;
}
}
if let Some(b) = co.get("qubits") {
at_most(b, &format!("{id}: qubits"))?;
}
if let Some(i) = co.get("inject") {
let name = i.as_str().filter(|n| !n.is_empty()).ok_or_else(|| format!("{id}: inject names the input whose value is the qubit an error flips, as \"err\""))?;
let values: Vec<Option<i64>> = cases.and_then(Value::as_array).into_iter().flatten().map(|c| c.pointer(&format!("/inputs/{name}")).and_then(Value::as_i64)).collect();
if values.iter().any(|v| v.is_none_or(|v| v < 0)) || values.iter().collect::<std::collections::BTreeSet<_>>().len() < 2 {
return Err(format!("{id}: inject: every case gives {name}, a qubit (a whole number from 0), and at least two cases give different ones"));
}
}
}
Ok(())
}
/// Whether `exercise` is in the form [`EXERCISE_SCHEMA`] names, as far as a
/// check reads it: a JSON object of an exercise's fields only, naming that
/// form if it names one, with its texts and programs where it gives them, and
/// what it asks of a program (its job, intent, cases and constraints) in the
/// form a lesson's exercise is held to. An exercise that is no lesson's (a
/// task a program is written for) need not give a lesson's title in every
/// locale, its hints, solution or known wrong answers, nor name each case.
/// [`check_exercise`] refuses what this refuses, before it runs anything.
pub fn exercise_form(exercise: &Value) -> Result<(), String> {
let id = "the exercise";
let o = exercise.as_object().ok_or_else(|| format!("{id} is not a JSON object"))?;
if let Some(k) = o.keys().find(|k| !EXERCISE_FIELDS.contains(&k.as_str())) {
return Err(format!("{id}: `{k}` is not a field of an exercise ({})", EXERCISE_FIELDS.join(", ")));
}
if let Some(s) = present(o, "schema")
&& *s != EXERCISE_SCHEMA
{
return Err(format!("{id}: its schema is {s}, not \"{EXERCISE_SCHEMA}\""));
}
for k in ["title", "task"] {
if let Some(t) = present(o, k)
&& !(t.is_string() || t.as_object().is_some_and(|m| m.values().all(Value::is_string)))
{
return Err(format!("{id}: {k} is a text, or one for each locale"));
}
}
for k in ["starter", "reference"] {
if let Some(p) = present(o, k) {
program_text(p, &format!("{id}: {k}"))?;
}
}
for (k, ok) in [("id", Value::is_string as fn(&Value) -> bool), ("translations", Value::is_object), ("hints", Value::is_object), ("wrong", Value::is_array)] {
if present(o, k).is_some_and(|v| !ok(v)) {
return Err(format!("{id}: {k} is not in an exercise's form"));
}
}
asks(o, id)
}
/// An exercise read from its JSON and checked in its form, its programs
/// joined from their lines.
fn read_exercise(id: &str, text: &str) -> Result<Value, String> {
@ -200,76 +384,14 @@ fn read_exercise(id: &str, text: &str) -> Result<Value, String> {
if counts[0] == 0 || counts.iter().any(|c| *c != counts[0]) || hints.len() != LOCALES.len() {
return Err(format!("{id}: hints needs the same number of hints, at least one, in each locale ({counts:?})"));
}
if let Some(job) = o.get("job") {
let j = job.as_object().ok_or_else(|| format!("{id}: job is an object"))?;
if let Some(k) = j.keys().find(|k| !JOB_FIELDS.contains(&k.as_str())) {
return Err(format!("{id}: job takes {}, not {k}", JOB_FIELDS.join(", ")));
}
}
if !o.contains_key("intent") && !o.contains_key("cases") {
return Err(format!("{id}: an exercise needs an intent, or cases that each carry one"));
}
if let Some(i) = o.get("intent") {
verdict::intents(i).map_err(|e| format!("{id}: intent: {e}"))?;
}
if let Some(cases) = o.get("cases") {
let list = cases.as_array().filter(|a| !a.is_empty()).ok_or_else(|| format!("{id}: cases is a list of at least one case"))?;
for (n, c) in list.iter().enumerate() {
let what = format!("{id}: case {}", n + 1);
let co = c.as_object().ok_or_else(|| format!("{what} is an object"))?;
if let Some(k) = co.keys().find(|k| !["inputs", "name", "intent"].contains(&k.as_str())) {
return Err(format!("{what}: `{k}` is not a field of a case (inputs, name, intent)"));
}
let ins = co.get("inputs").and_then(Value::as_object).ok_or_else(|| format!("{what}: inputs is an object of whole numbers"))?;
if ins.values().any(|x| x.as_i64().is_none()) {
return Err(format!("{what}: inputs is an object of whole numbers"));
}
localized(co.get("name"), &format!("{what}: name"))?;
match co.get("intent") {
Some(i) => drop(verdict::intents(i).map_err(|e| format!("{what}: intent: {e}"))?),
None if !o.contains_key("intent") => return Err(format!("{what}: no intent, and the exercise has none")),
None => {}
}
}
}
if let Some(c) = o.get("constraints") {
let co = c.as_object().ok_or_else(|| format!("{id}: constraints is an object"))?;
if let Some(k) = co.keys().find(|k| !CONSTRAINTS.contains(&k.as_str())) {
return Err(format!("{id}: `{k}` is not a constraint ({})", CONSTRAINTS.join(", ")));
}
if let Some(a) = co.get("allow") {
let gates = a.as_array().filter(|a| !a.is_empty() && a.iter().all(Value::is_string)).ok_or_else(|| format!("{id}: allow is a list of gate names"))?;
if let Some(g) = gates.iter().filter_map(Value::as_str).find(|g| !GATES.contains(g)) {
return Err(format!("{id}: allow: {g} is not a gate the reader names"));
}
}
if let Some(cs) = co.get("counts") {
for (g, b) in cs.as_object().ok_or_else(|| format!("{id}: counts is an object of gates and bounds"))? {
if !GATES.contains(&g.as_str()) {
return Err(format!("{id}: counts: {g} is not a gate the reader names"));
}
bound(b, &format!("{id}: counts: {g}"))?;
}
}
for k in ["t_count", "two_qubit"] {
if let Some(b) = co.get(k) {
bound(b, &format!("{id}: {k}"))?;
}
}
if let Some(s) = co.get("no_smells") {
let kinds = s.as_array().filter(|a| !a.is_empty()).ok_or_else(|| format!("{id}: no_smells is a list of the critique's smell kinds"))?;
if let Some(k) = kinds.iter().find(|k| !k.as_str().is_some_and(|k| SMELLS.contains(&k))) {
return Err(format!("{id}: no_smells: {k} is not a smell the critique names ({})", SMELLS.join(", ")));
}
}
if let Some(e) = co.get("estimate") {
for (k, b) in e.as_object().ok_or_else(|| format!("{id}: estimate is an object of the estimate's counts and bounds"))? {
if !ESTIMATE_COUNTS.contains(&k.as_str()) {
return Err(format!("{id}: estimate: {k} is not a count the estimate gives ({})", ESTIMATE_COUNTS.join(", ")));
}
bound(b, &format!("{id}: estimate: {k}"))?;
}
asks(o, id)?;
// A lesson's cases each name their inputs, and are named in every locale.
for (n, c) in present(o, "cases").and_then(Value::as_array).into_iter().flatten().enumerate() {
let what = format!("{id}: case {}", n + 1);
if !c.get("inputs").is_some_and(Value::is_object) {
return Err(format!("{what}: inputs is an object of whole numbers"));
}
localized(c.get("name"), &format!("{what}: name"))?;
}
let wrong = o["wrong"].as_array().filter(|w| !w.is_empty()).ok_or_else(|| format!("{id}: wrong is a list of at least one known wrong answer"))?;
for (n, w) in wrong.iter().enumerate() {
@ -367,6 +489,12 @@ fn word<'a>(v: &'a Value, loc: &str) -> &'a str {
v.get(loc).or_else(|| v.get(LOCALES[0])).and_then(Value::as_str).unwrap_or("")
}
/// A text given once, or in each locale ([`word`]): an exercise that is no
/// lesson's may name a case with one string.
fn text_in<'a>(v: &'a Value, loc: &str) -> &'a str {
v.as_str().unwrap_or_else(|| word(v, loc))
}
/// A probability or a distance, as the course writes one: up to four
/// decimals, without trailing zeros, or in scientific notation when small.
fn num(x: f64) -> String {
@ -451,18 +579,57 @@ fn constraint_texts(c: &Value, loc: &str) -> Vec<(String, String, String)> {
for (k, b) in o.get("estimate").and_then(Value::as_object).into_iter().flatten() {
out.push(("estimate".into(), k.clone(), say(loc, "constraint.estimate", &[("n", b["max"].as_u64().map_or("?".into(), |n| n.to_string())), ("what", say(loc, &format!("estimate.{k}"), &[]))])));
}
if let Some(b) = o.get("qubits") {
out.push(("qubits".into(), String::new(), say(loc, "constraint.qubits", &[("n", b["max"].as_u64().map_or("?".into(), |n| n.to_string()))])));
}
if let Some(i) = o.get("inject").and_then(Value::as_str) {
out.push(("inject".into(), i.to_string(), say(loc, "constraint.inject", &[("input", i.to_string())])));
}
out
}
/// Whether the programs of a task's cases (each with the value its case gives the input an
/// error is injected by) differ only where the error is injected: one gate, an x on the qubit
/// the case's value names, every other gate, measurement and step of every case's program the
/// same. Then the error is in the quantum state and nothing else in the program reads it, so
/// what corrects or detects it is the program's quantum operations, the same in every case.
/// Otherwise, the locale key of why not.
fn injected(programs: &[(&qasm::Program, i64)]) -> Result<(), &'static str> {
let Some((first, _)) = programs.first() else { return Ok(()) };
if programs.iter().any(|(p, _)| p.ops.len() != first.ops.len()) {
return Err("wrong.inject_read");
}
let differ: Vec<usize> = (0..first.ops.len()).filter(|&j| programs.iter().any(|(p, _)| p.ops[j] != first.ops[j])).collect();
let at = match differ[..] {
[] => return Err("wrong.inject_unused"),
[at] => at,
_ => return Err("wrong.inject_read"),
};
if programs.iter().any(|(p, v)| p.ops[at].kind != qasm::Kind::X || u32::try_from(*v).ok().is_none_or(|q| p.ops[at].qubits != [q])) {
return Err("wrong.inject_where");
}
let rest = |p: &qasm::Program| {
let mut ops = p.ops.clone();
ops[at] = first.ops[at].clone();
(p.n_qubits, p.n_bits, ops, p.measured.clone(), p.dynamic.clone())
};
if programs.iter().any(|(p, _)| rest(p) != rest(first)) {
return Err("wrong.inject_read");
}
Ok(())
}
/// The cases a program is checked in: each with its name, the inputs it
/// gives a generator, and its intent.
fn cases_of(ex: &Value) -> Vec<(Option<&Value>, qasm::Inputs, Value)> {
fn cases_of<'a>(ex: &'a Value) -> Vec<(Option<&'a Value>, qasm::Inputs, Value)> {
match ex.get("cases").and_then(Value::as_array) {
Some(list) => list
.iter()
.map(|c| {
let ins: qasm::Inputs = c["inputs"].as_object().into_iter().flatten().map(|(k, v)| (k.clone(), v.as_i64().unwrap_or(0))).collect();
(Some(&c["name"]), ins, c.get("intent").or(ex.get("intent")).cloned().unwrap_or(Value::Null))
// A case's intent, or the exercise's when it gives none (null is none).
let own = |v: &'a Value| v.get("intent").filter(|i| !i.is_null());
(Some(&c["name"]), ins, own(c).or(own(ex)).cloned().unwrap_or(Value::Null))
})
.collect(),
None => vec![(None, qasm::Inputs::new(), ex["intent"].clone())],
@ -694,23 +861,83 @@ fn limit_text(b: &Value, loc: &str) -> String {
/// proof or as evidence, and every constraint holds.
pub fn check(id: &str, src: &str, job_bytes: u64, loc: &str) -> Result<Value, String> {
let (c, i) = find(id)?;
check_exercise_as(&c.lessons[i].exercise, Some(id), src, job_bytes, loc)
}
/// A program checked against any exercise in the form [`EXERCISE_SCHEMA`]
/// names, as [`check`] checks a lesson's: the same verdicts, constraints and
/// words, with `lesson` null. It is how a task that is not in the course (a
/// program a person or the job service is asked to write) is held to what it
/// must do.
pub fn check_exercise(exercise: &Value, src: &str, job_bytes: u64, loc: &str) -> Result<Value, String> {
exercise_form(exercise)?;
check_exercise_as(exercise, None, src, job_bytes, loc)
}
/// The T gates a program comes to, as a T count bounds it: a T or T† one each,
/// a Clifford gate none, and a phase rotation (`p`, `rz`) by an odd multiple of
/// π/4 one, since it is a T or T† up to Clifford gates and a global phase. Any
/// other gate's T count is not fixed (it depends on how the gate is written in
/// Clifford and T gates, or it has no exact form in them): that gate's name.
fn t_gates(p: &qasm::Program) -> Result<u64, &'static str> {
let mut n = 0;
for op in &p.ops {
let odd_quarter = |theta: f64| {
let q = theta / std::f64::consts::FRAC_PI_4;
(q - q.round()).abs() < 1e-9 && (q.round() as i64).rem_euclid(2) == 1
};
if qasm::Program::lower_clifford(std::slice::from_ref(op)).is_ok() {
continue;
}
match op.kind {
qasm::Kind::T | qasm::Kind::Tdg => n += 1,
qasm::Kind::P | qasm::Kind::Rz if op.params.first().is_some_and(|t| odd_quarter(*t)) => n += 1,
k => return Err(critique::kind_name(k)),
}
}
Ok(n)
}
/// The two-qubit gates of a program, as a two-qubit count bounds it; a gate on
/// three qubits is not one, and is not bounded by their count: its name.
fn two_qubit_gates(p: &qasm::Program) -> Result<u64, &'static str> {
if let Some(op) = p.ops.iter().find(|o| o.qubits.len() > 2) {
return Err(critique::kind_name(op.kind));
}
Ok(p.ops.iter().filter(|o| o.qubits.len() == 2).count() as u64)
}
/// One case's program as the constraints measure it: the case's number and
/// name where the cases are named, its text, the critique of it, and it read.
struct Measured<'a> {
case: Option<usize>,
name: Option<String>,
inputs: qasm::Inputs,
text: &'a str,
critique: Value,
program: Option<qasm::Program>,
}
fn check_exercise_as(ex: &Value, id: Option<&str>, src: &str, job_bytes: u64, loc: &str) -> Result<Value, String> {
let loc = locale(loc);
let ex = &c.lessons[i].exercise;
let cases = cases_of(ex);
let named = cases.len() > 1 || ex.get("cases").is_some();
let declared = qasm::inputs(src).unwrap_or_default();
let named = cases.len() > 1 || ex.get("cases").is_some_and(Value::is_array);
let mut wrongs: Vec<Wrong> = Vec::new();
let mut case_out = Vec::new();
let mut first_text: Option<String> = None;
// Each case's program as it reads there (a generator at the case's inputs),
// with the case's number and name: what the constraints are held to.
let mut texts: Vec<(Option<usize>, Option<String>, String, qasm::Inputs)> = Vec::new();
let mut decided = true;
let mut levels: Vec<verdict::Level> = Vec::new();
for (n, (name, inputs, intent)) in cases.iter().enumerate() {
let ins: qasm::Inputs = inputs.iter().filter(|(k, _)| declared.contains(k)).map(|(k, v)| (k.clone(), *v)).collect();
let case = named.then_some(n);
let v = match qasm::read_with(src, &ins) {
// Every input a case gives binds: the reader refuses a value for an input
// the program does not declare (Q114), so a program that ignores the
// perturbation a case is about does not pass that case.
let v = match qasm::read_with(src, inputs) {
Err(d) => verdict::of_diagnostic(&d),
Ok(_) => {
let text = if ins.is_empty() { Ok(src.to_string()) } else { qasm::instantiate(src, &ins) };
let text = if inputs.is_empty() { Ok(src.to_string()) } else { qasm::instantiate(src, inputs) };
let text = match text {
Ok(t) => t,
Err(e) => {
@ -718,14 +945,12 @@ pub fn check(id: &str, src: &str, job_bytes: u64, loc: &str) -> Result<Value, St
levels.push(verdict::Level::Refused);
decided = false;
wrongs.extend(verdict_wrongs(&v, case, loc));
case_out.push(json!({ "name": name.map(|x| word(x, loc)), "inputs": inputs, "passed": false, "level": v["level"], "holds": v["holds"], "statement": v["statement"], "verdict": v }));
case_out.push(json!({ "name": name.map(|x| text_in(x, loc)), "inputs": inputs, "passed": false, "level": v["level"], "holds": v["holds"], "statement": v["statement"], "verdict": v }));
continue;
}
};
if first_text.is_none() {
first_text = Some(text.clone());
}
let mut req = ex.get("job").cloned().unwrap_or(json!({}));
texts.push((case, name.map(|x| text_in(x, loc).to_string()), text.clone(), inputs.clone()));
let mut req = ex.get("job").filter(|j| j.is_object()).cloned().unwrap_or(json!({}));
req["qasm"] = json!(text);
req["intent"] = intent.clone();
if req.get("shots").is_none() {
@ -746,7 +971,7 @@ pub fn check(id: &str, src: &str, job_bytes: u64, loc: &str) -> Result<Value, St
if v["holds"].is_null() {
decided = false;
}
let name_text = name.map(|x| word(x, loc).to_string());
let name_text = name.map(|x| text_in(x, loc).to_string());
for mut w in verdict_wrongs(&v, case, loc) {
if let Some(nm) = &name_text {
w.text = say(loc, "wrong.case", &[("name", nm.clone()), ("text", w.text)]);
@ -763,18 +988,42 @@ pub fn check(id: &str, src: &str, job_bytes: u64, loc: &str) -> Result<Value, St
"verdict": v,
}));
}
// The constraints, on the program as it reads (a generator at its first case).
// The constraints, held on the program as it reads in every case: a gate
// that one case alone uses is held to them as the first case's are. A
// constraint holds when it holds in every case; what it found is the first
// case's, or the first case's where it does not hold.
let first_text = texts.first().map(|(_, _, t, _)| t.clone());
let mut constraints = Vec::new();
let mut measures = Value::Null;
let mut estimate_out = Value::Null;
let cons = ex.get("constraints").cloned().unwrap_or(json!({}));
if let Some(text) = &first_text {
let m = critique::critique(text);
if m["ok"] == true {
measures = json!({ "qubits": m["qubits"], "gates": m["gates"], "two_qubit": m["two_qubit"], "t_count": m["t_count"], "depth": m["depth"], "by_gate": m["by_gate"] });
let by = m["by_gate"].as_object().cloned().unwrap_or_default();
let count = |g: &str| by.get(g).and_then(Value::as_u64).unwrap_or(0);
for (rule, subject, text_c) in constraint_texts(&cons, loc) {
let cons = ex.get("constraints").filter(|c| c.is_object()).cloned().unwrap_or(json!({}));
let measured: Vec<Measured> = texts.iter().map(|(c, nm, t, ins)| Measured { case: *c, name: nm.clone(), inputs: ins.clone(), text: t, critique: critique::critique(t), program: qasm::parse(t).ok() }).collect();
if let Some(m0) = measured.first().map(|x| &x.critique)
&& m0["ok"] == true
{
measures = json!({ "qubits": m0["qubits"], "gates": m0["gates"], "two_qubit": m0["two_qubit"], "t_count": m0["t_count"], "depth": m0["depth"], "by_gate": m0["by_gate"] });
let mut estimates: Vec<Value> = Vec::new();
for (rule, subject, text_c) in constraint_texts(&cons, loc) {
// An injected error is held across the cases' programs at once.
if rule == "inject" {
let programs: Vec<(&qasm::Program, i64)> = measured.iter().filter_map(|x| Some((x.program.as_ref()?, *x.inputs.get(&subject)?))).collect();
if programs.len() < 2 {
continue;
}
let holds = injected(&programs);
if let Err(why) = holds {
wrongs.push(Wrong { kind: "constraint", text: say(loc, why, &[("input", subject.clone())]), engine: None, lines: Vec::new(), case: None, pointer: say(loc, "pointer.inject", &[("input", subject.clone())]) });
}
constraints.push(json!({ "rule": rule, "subject": subject, "text": text_c, "holds": holds.is_ok(), "found": holds.err().map(|w| w.trim_start_matches("wrong.inject_")) }));
continue;
}
let mut row: Option<(bool, Value, Option<Wrong>)> = None;
for (k, Measured { case, name: case_name, critique: m, program, .. }) in measured.iter().enumerate() {
if m["ok"] != true {
continue;
}
let by = m["by_gate"].as_object().cloned().unwrap_or_default();
let count = |g: &str| by.get(g).and_then(Value::as_u64).unwrap_or(0);
let (holds, found, wrong) = match rule.as_str() {
"allow" => {
let allowed: Vec<&str> = cons["allow"].as_array().into_iter().flatten().filter_map(Value::as_str).collect();
@ -804,14 +1053,49 @@ pub fn check(id: &str, src: &str, job_bytes: u64, loc: &str) -> Result<Value, St
}
"t_count" | "two_qubit" => {
let b = &cons[&rule];
let n = m[&rule].as_u64().unwrap_or(0);
let limit = b["max"].as_u64().map_or("?".into(), |x| x.to_string());
let counted = match program {
Some(p) if rule == "t_count" => t_gates(p),
Some(p) => two_qubit_gates(p),
None => Ok(m[&rule].as_u64().unwrap_or(0)),
};
match counted {
Ok(n) => {
let w = (!within(n, b)).then(|| Wrong {
kind: "constraint",
text: say(loc, &format!("wrong.{rule}"), &[("found", n.to_string()), ("limit", limit.clone())]),
engine: None,
lines: Vec::new(),
case: None,
pointer: say(loc, &format!("pointer.{rule}"), &[]),
});
(within(n, b), json!(n), w)
}
// A gate the count cannot bound: the constraint does not hold.
Err(gate) => {
let other = if rule == "t_count" { "t_count_other" } else { "two_qubit_wide" };
let w = Wrong {
kind: "constraint",
text: say(loc, &format!("wrong.{other}"), &[("gate", gate.to_string()), ("limit", limit)]),
engine: None,
lines: Vec::new(),
case: None,
pointer: say(loc, &format!("pointer.{other}"), &[("gate", gate.to_string())]),
};
(false, Value::Null, Some(w))
}
}
}
"qubits" => {
let b = &cons["qubits"];
let n = m["qubits"].as_u64().unwrap_or(0);
let w = (!within(n, b)).then(|| Wrong {
kind: "constraint",
text: say(loc, &format!("wrong.{rule}"), &[("found", n.to_string()), ("limit", b["max"].as_u64().map_or("?".into(), |x| x.to_string()))]),
text: say(loc, "wrong.qubits", &[("found", n.to_string()), ("limit", b["max"].as_u64().map_or("?".into(), |x| x.to_string()))]),
engine: None,
lines: Vec::new(),
case: None,
pointer: say(loc, &format!("pointer.{rule}"), &[]),
pointer: say(loc, "pointer.qubits", &[]),
});
(within(n, b), json!(n), w)
}
@ -827,10 +1111,14 @@ pub fn check(id: &str, src: &str, job_bytes: u64, loc: &str) -> Result<Value, St
}
"estimate" => {
let b = &cons["estimate"][&subject];
if estimate_out.is_null() {
estimate_out = estimate_of(text, loc);
while estimates.len() <= k {
estimates.push(estimate_of(measured[estimates.len()].text, loc));
}
match estimate_out["counts"][&subject].as_u64() {
if estimate_out.is_null() {
estimate_out = estimates[0].clone();
}
let e = &estimates[k];
match e["counts"][&subject].as_u64() {
Some(n) => {
let w = (!within(n, b)).then(|| Wrong {
kind: "constraint",
@ -845,16 +1133,32 @@ pub fn check(id: &str, src: &str, job_bytes: u64, loc: &str) -> Result<Value, St
None => (
false,
Value::Null,
Some(Wrong { kind: "constraint", text: say(loc, "wrong.estimate_refused", &[]), engine: estimate_out["error"].as_str().map(str::to_string), lines: Vec::new(), case: None, pointer: say(loc, "pointer.unchecked", &[]) }),
Some(Wrong { kind: "constraint", text: say(loc, "wrong.estimate_refused", &[]), engine: e["error"].as_str().map(str::to_string), lines: Vec::new(), case: None, pointer: say(loc, "pointer.unchecked", &[]) }),
),
}
}
_ => continue,
};
constraints.push(json!({ "rule": rule, "subject": (!subject.is_empty()).then_some(subject), "text": text_c, "holds": holds, "found": found }));
if let Some(w) = wrong {
wrongs.push(w);
// Where the cases are named, a constraint broken in one says which.
let wrong = wrong.map(|mut w| {
if let (Some(c), Some(nm)) = (case, case_name) {
w.case = Some(*c);
w.text = say(loc, "wrong.case", &[("name", nm.clone()), ("text", w.text)]);
}
w
});
let fails = !holds;
if row.is_none() || fails {
row = Some((holds, found, wrong));
}
if fails {
break;
}
}
let Some((holds, found, wrong)) = row else { continue };
constraints.push(json!({ "rule": rule, "subject": (!subject.is_empty()).then_some(subject), "text": text_c, "holds": holds, "found": found }));
if let Some(w) = wrong {
wrongs.push(w);
}
}
}
@ -1035,6 +1339,195 @@ mod tests {
}
}
#[test]
fn an_exercise_outside_the_course_is_checked_as_a_lesson_is() {
// A lesson's own exercise, given as a document: the same check, byte for byte, but for
// the lesson it names (none).
let (c, i) = find("first-circuit").unwrap();
let ex = &c.lessons[i].exercise;
let right = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nbit[1] c;\nx q[0];\nc = measure q;\n";
let wrong = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[1] q;\nbit[1] c;\nh q[0];\nc = measure q;\n";
for src in [right, wrong] {
let mut lesson = check("first-circuit", src, CAP, "en").unwrap();
let doc = check_exercise(ex, src, CAP, "en").unwrap();
assert_eq!(doc["lesson"], Value::Null, "{doc:#}");
lesson["lesson"] = Value::Null;
assert_eq!(lesson, doc);
}
// An exercise written from nothing but what the program must do.
let mine = json!({ "schema": EXERCISE_SCHEMA, "intent": { "distribution": { "00": 0.5, "11": 0.5 } } });
let bell = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\nqubit[2] q;\nbit[2] c;\nh q[0];\ncx q[0], q[1];\nc = measure q;\n";
let ok = check_exercise(&mine, bell, CAP, "en").unwrap();
assert_eq!((ok["passed"].as_bool(), ok["level"].as_str()), (Some(true), Some("proof")), "{ok:#}");
let no = check_exercise(&mine, &bell.replace("cx q[0], q[1];\n", ""), CAP, "en").unwrap();
assert_eq!(no["passed"], false, "{no:#}");
assert_eq!(no["what_went_wrong"][0]["kind"], "distribution", "{no:#}");
}
const HEAD: &str = "OPENQASM 3.0;\ninclude \"stdgates.inc\";\n";
/// An exercise that is no lesson's is held to the form a lesson's is before
/// anything runs: a bound that is not whole numbers, a constraint the
/// course does not name, a job that is not shots, seed and noise, too many
/// cases or intents, or a field an exercise does not have, is refused, never
/// read as no bound or dropped. Each program here does what the intent
/// asks, so a vacuous constraint would let it pass.
#[test]
fn a_malformed_exercise_is_refused_before_anything_is_checked() {
let src = format!("{HEAD}qubit[1] q;\nbit[1] c;\nx q[0];\nt q[0];\nt q[0];\nc = measure q;\n");
let with = |k: &str, v: Value| {
let mut ex = json!({ "schema": EXERCISE_SCHEMA, "intent": { "distribution": { "1": 1.0 } } });
ex[k] = v;
ex
};
// Well formed, it reads and the program passes; bounded, it fails.
let ok = check_exercise(&with("constraints", json!({})), &src, CAP, "en").unwrap();
assert_eq!(ok["passed"], true, "{ok:#}");
let bounded = check_exercise(&with("constraints", json!({ "t_count": { "max": 0 } })), &src, CAP, "en").unwrap();
assert_eq!((bounded["passed"].as_bool(), bounded["constraints"][0]["found"].as_u64()), (Some(false), Some(2)), "{bounded:#}");
let bad_constraints = [
json!({ "t_count": 0 }),
json!({ "t_count": { "max": -1 } }),
json!({ "t_count": { "max": 1.5 } }),
json!({ "t_count": { "max": 0.0 } }),
json!({ "t_count": { "max": "0" } }),
json!({ "t_count": { "maximum": 0 } }),
json!({ "t_count": { "min": 3, "max": 1 } }),
json!({ "t_count": { "min": 1 } }),
json!({ "two_qubit": { "min": 0, "max": 3 } }),
json!({ "estimate": { "toffolis": { "min": 1 } } }),
json!({ "two_qubit": {} }),
json!({ "counts": { "CX": { "max": 0 } } }),
json!({ "counts": 5 }),
json!({ "estimate": 5 }),
json!({ "estimate": { "t_gates": { "max": 1.0 } } }),
json!({ "depth": { "max": 3 } }),
json!({ "no_smells": "cancels" }),
json!({ "no_smells": ["everything"] }),
json!(5),
];
let many = |n: usize, one: Value| Value::Array(vec![one; n]);
let bad: Vec<Value> = bad_constraints
.into_iter()
.map(|c| with("constraints", c))
.chain([json!(1), json!("x"), json!([]), json!(true), json!({ "repair": true }), json!({ "dyadic_bits": 3 }), json!({ "compile": { "level": 2 } }), json!({ "shots": 2.5 }), json!({ "shots": 0 }), json!({ "seed": -1 }), json!({ "noise": { "depolarizing_1q": 2 } })].map(|j| with("job", j)))
.chain([
with("cases", many(100, json!({ "inputs": {} }))),
with("intent", many(100, json!({ "distribution": { "1": 1.0 } }))),
with("cases", json!([{ "inputs": { "err": 3.0 } }])),
with("cases", json!([{ "inputs": {}, "colour": 1 }])),
with("cases", json!(5)),
with("constrains", json!({ "t_count": { "max": 0 } })),
with("schema", json!("wai.quantum.job/1")),
json!({ "cases": [{ "inputs": {} }] }),
json!([1]),
])
.collect();
for ex in bad {
let r = std::panic::catch_unwind(|| check_exercise(&ex, &src, CAP, "en"));
let r = r.unwrap_or_else(|_| panic!("the check panicked on {ex}"));
assert!(r.is_err(), "{ex} was checked, not refused: {:#}", r.unwrap());
}
}
/// A case's inputs bind: a program that does not declare an input a case
/// gives is refused there, not run without it. And the constraints are held
/// in every case, not only the first: a gate one case alone adds is counted.
#[test]
fn every_case_binds_its_inputs_and_is_held_to_the_constraints() {
// The bit-flip code with no flip at all: it would pass every case unbound.
let no_flip = format!("{HEAD}qubit[3] q;\nbit[1] c;\nx q[0];\nc[0] = measure q[0];\n");
let c = check("bit-flip-code", &no_flip, CAP, "en").unwrap();
assert_eq!(c["passed"], false, "{c:#}");
assert!(c["what_went_wrong"].as_array().unwrap().iter().any(|w| w["text"].as_str().unwrap().contains("Q114")), "{c:#}");
// Two x gates more in the second case only: the intent holds in both.
let ex = json!({
"intent": { "distribution": { "1": 1.0 } },
"cases": [{ "name": "no flip", "inputs": { "err": 0 } }, { "name": "a flip", "inputs": { "err": 1 } }],
"constraints": { "counts": { "x": { "max": 1 } } },
});
let src = format!("{HEAD}input uint err;\nqubit[1] q;\nbit[1] c;\nx q[0];\nif (err == 1) {{\n x q[0];\n x q[0];\n}}\nc[0] = measure q[0];\n");
let c = check_exercise(&ex, &src, CAP, "en").unwrap();
assert!(c["cases"].as_array().unwrap().iter().all(|k| k["passed"] == true), "{c:#}");
assert_eq!((c["passed"].as_bool(), c["constraints"][0]["holds"].as_bool(), c["constraints"][0]["found"].as_u64()), (Some(false), Some(false), Some(3)), "{c:#}");
let w = &c["what_went_wrong"][0];
assert_eq!((w["kind"].as_str(), w["case"].as_u64()), (Some("constraint"), Some(1)), "{c:#}");
assert!(w["text"].as_str().unwrap().starts_with("With a flip:"), "{w}");
// Within the bound in every case, it passes, and says what the first case found.
let ok = check_exercise(&ex, &src.replace(" x q[0];\n x q[0];\n", " z q[0];\n"), CAP, "en").unwrap();
assert_eq!((ok["passed"].as_bool(), ok["constraints"][0]["found"].as_u64()), (Some(true), Some(1)), "{ok:#}");
// A case whose intent is null takes the exercise's, as one without does.
let null = json!({ "intent": { "distribution": { "1": 1.0 } }, "cases": [{ "name": "a", "inputs": {}, "intent": null }] });
let one = format!("{HEAD}qubit[1] q;\nbit[1] c;\nx q[0];\nc = measure q;\n");
let c = check_exercise(&null, &one, CAP, "en").unwrap();
assert_eq!((c["passed"].as_bool(), c["level"].as_str()), (Some(true), Some("proof")), "{c:#}");
}
/// An error a case injects is corrected from the qubits: the cases' programs differ only in
/// the one x the error is, so a program that declares the error and never applies it, reads
/// it classically, or flips another qubit does not pass, nor one that keeps its answer on a
/// qubit the error never reaches.
#[test]
fn an_injected_error_is_corrected_from_the_qubits_and_never_read() {
let inject = |c: &Value| c["constraints"].as_array().unwrap().iter().find(|r| r["rule"] == "inject").cloned().unwrap_or(Value::Null);
// Declared, never applied: every case's program the same, the answer 1 at once.
let unused = format!("{HEAD}input uint err;\nqubit[3] q;\nbit[1] c;\nx q[0];\nc[0] = measure q[0];\n");
let c = check("bit-flip-code", &unused, CAP, "en").unwrap();
assert_eq!((c["passed"].as_bool(), inject(&c)["holds"].as_bool(), inject(&c)["found"].as_str()), (Some(false), Some(false), Some("unused")), "{c:#}");
// The syndrome decided on the classical error, not measured from the qubits.
let oracle = format!("{HEAD}input uint err;\nqubit[5] q;\nbit[2] s;\nx q[err];\nif (err == 0) {{ cx q[0], q[3]; }}\nif (err == 1) {{ cx q[1], q[3]; cx q[1], q[4]; }}\nif (err == 2) {{ cx q[2], q[4]; }}\ns[0] = measure q[3];\ns[1] = measure q[4];\n");
let c = check("syndrome", &oracle, CAP, "en").unwrap();
assert!(c["cases"].as_array().unwrap().iter().all(|k| k["passed"] == true), "the oracle's syndromes are right: {c:#}");
assert_eq!((c["passed"].as_bool(), inject(&c)["found"].as_str()), (Some(false), Some("read")), "{c:#}");
// Another qubit flipped than the case names.
let elsewhere = format!("{HEAD}input uint err;\nqubit[3] q;\nbit[1] c;\nx q[0];\ncx q[0], q[1];\ncx q[0], q[2];\nx q[2 - err];\ncx q[0], q[1];\ncx q[0], q[2];\nccx q[2], q[1], q[0];\nc[0] = measure q[0];\n");
let c = check("bit-flip-code", &elsewhere, CAP, "en").unwrap();
assert_eq!((c["passed"].as_bool(), inject(&c)["found"].as_str()), (Some(false), Some("where")), "{c:#}");
// The answer kept on a fourth qubit, which no error reaches.
let aside = format!("{HEAD}input uint err;\nqubit[4] q;\nbit[1] c;\nx q[3];\nx q[err];\nc[0] = measure q[3];\n");
let c = check("bit-flip-code", &aside, CAP, "en").unwrap();
assert_eq!(inject(&c)["holds"], true, "{c:#}");
assert!(c["passed"] == false && c["constraints"].as_array().unwrap().iter().any(|r| r["rule"] == "qubits" && r["holds"] == false), "{c:#}");
// The lesson's own solution holds to it.
let (l, i) = find("bit-flip-code").unwrap();
let c = check("bit-flip-code", l.lessons[i].exercise["reference"].as_str().unwrap(), CAP, "en").unwrap();
assert_eq!((c["passed"].as_bool(), inject(&c)["holds"].as_bool()), (Some(true), Some(true)), "{c:#}");
// An injection the cases cannot make is refused.
let bad = json!({ "intent": { "distribution": { "1": 1.0 } }, "cases": [{ "inputs": { "err": 0 } }, { "inputs": { "err": 0 } }], "constraints": { "inject": "err" } });
assert!(check_exercise(&bad, &unused, CAP, "en").unwrap_err().contains("inject"));
}
/// A T count counts T gates however they are written: a phase rotation by
/// an odd multiple of π/4 is a T up to Clifford gates, and a gate whose T
/// count is not fixed does not hold to a bound on it. A gate on three qubits
/// is not bounded by a count of two-qubit gates.
#[test]
fn a_t_count_counts_t_gates_however_they_are_written() {
// The t-count lesson's own intent, each T written as p(π/4): the same circuit.
let (c, i) = find("t-count").unwrap();
let reference = c.lessons[i].exercise["intent"]["equivalent"].as_str().unwrap();
let respelled = reference.replace("tdg q[1];", "p(-pi/4) q[1];").replace("t q[0];", "p(pi/4) q[0];").replace("t q[1];", "p(pi/4) q[1];");
assert!(!respelled.contains("\nt ") && !respelled.contains("tdg q"), "{respelled}");
let r = check("t-count", &respelled, CAP, "en").unwrap();
assert_eq!(r["cases"][0]["passed"], true, "the respelling is the same circuit: {r:#}");
assert_eq!((r["passed"].as_bool(), r["constraints"][0]["found"].as_u64()), (Some(false), Some(8)), "{r:#}");
let task = |c: Value, intent: &str| json!({ "intent": { "distribution": { intent: 1.0 } }, "constraints": c });
// A rotation that is no T: its T count is not fixed, so no bound holds.
let rz = check_exercise(&task(json!({ "t_count": { "max": 0 } }), "0"), &format!("{HEAD}qubit[1] q;\nbit[1] c;\nrz(0.3) q[0];\nc = measure q;\n"), CAP, "en").unwrap();
assert_eq!((rz["passed"].as_bool(), rz["constraints"][0]["holds"].as_bool()), (Some(false), Some(false)), "{rz:#}");
assert!(rz["what_went_wrong"][0]["text"].as_str().unwrap().contains("rz gate"), "{rz:#}");
// A Clifford rotation counts none; T and p(π/4) count one each.
let mixed = check_exercise(&task(json!({ "t_count": { "max": 1 } }), "0"), &format!("{HEAD}qubit[1] q;\nbit[1] c;\np(pi/2) q[0];\nt q[0];\np(pi/4) q[0];\nc = measure q;\n"), CAP, "en").unwrap();
assert_eq!((mixed["passed"].as_bool(), mixed["constraints"][0]["found"].as_u64()), (Some(false), Some(2)), "{mixed:#}");
// A Toffoli is not a two-qubit gate, so a bound on them does not bound it.
let ccx = format!("{HEAD}qubit[3] q;\nbit[3] c;\nx q[0];\nx q[1];\nccx q[0], q[1], q[2];\nc = measure q;\n");
let wide = check_exercise(&task(json!({ "two_qubit": { "max": 0 } }), "111"), &ccx, CAP, "en").unwrap();
assert_eq!((wide["passed"].as_bool(), wide["constraints"][0]["holds"].as_bool()), (Some(false), Some(false)), "{wide:#}");
assert!(wide["what_went_wrong"][0]["text"].as_str().unwrap().contains("ccx gate"), "{wide:#}");
let two = check_exercise(&task(json!({ "two_qubit": { "max": 1 } }), "11"), &format!("{HEAD}qubit[2] q;\nbit[2] c;\nx q[0];\ncx q[0], q[1];\nc = measure q;\n"), CAP, "en").unwrap();
assert_eq!((two["passed"].as_bool(), two["constraints"][0]["found"].as_u64()), (Some(true), Some(1)), "{two:#}");
}
#[test]
fn a_check_says_what_went_wrong_from_the_engine_and_never_shows_the_solution() {
// A distribution: the largest gap is named, and the pointer is to it.