Skip to content

Commit f559e60

Browse files
authored
Cleanup (#315)
* Regenerate doc tests and prune stale generated files * Auto-include CUDA Python packages when toolkit and NVIDIA GPU detected * Surface CUDA Python packages in pecos setup summary * Stop pytest from shelling out to cargo, fix shadowed slr_tests module collision * Sanitize generated doc test paths and fix cmake-setup example * Introduce VariableState; fix SLR linearity bugs at use-after-unpack sites * Add AST -> Guppy v1 acceptance tests as xfail spec * Tighten v1 acceptance tests: strict xfail; defer rejection tests * Add Guppy-only linearity helper for AST -> Guppy lowering * Rewrite AST Guppy emitter for v1 linearity * Clean up legacy AST Guppy tests after v1 emitter rewrite * Tighten _harness lint: TYPE_CHECKING import, Error suffix on exception * Black formatting on AST -> Guppy v1 work * Fix Guppy bool emission for CReg bit assignments * Add Selene behavioral test harness for AST -> Guppy v1 (Workstream A) * Workstream B audit runner skeleton (waiting on _force_ast kwarg) * Add audit-only AST HUGR route * Expand audit runner with legacy HUGR test corpus (9 cases all pass) * Harden AST Guppy behavioral coverage * Add examples/ corpus to audit runner (pass 2) * Add qeclib corpus + ExpectedFailure XFAIL mechanism to audit runner (pass 3) * Harden Guppy v1 preflight + inline-CReg inference; lock down pass 4 audit (4 green, 5 XFAIL) * Cutover: route SlrConverter.hugr() through AST by default; remove _force_ast kwarg * Cutover: delete legacy gen_codes/guppy/ and dependent tests * Apply Codex review fixes: no-arg entrypoint wrap + compile diagnostics + stale comments + 3-cycle and cross-register Permute coverage * Fix Codex review v2 findings: entry wrapper mirrors emitter return shape (explicit Return passthrough + inline CReg result capture); broaden compile diagnostic catch; promote wrapper to shared entry_wrapper module * Mirror emitter for non-result-CReg Return values; cover nested-control-flow inline Measure in regression tests * Widen compile-error diagnostics to module-load failures; factor out _load_and_compile_entry helper * v2 Phase 1: add Print(value, *, tag=None, namespace='result') with SLR class, PrintOp AST node, Guppy result() emission, QASM comment fallback, and 15 Selene-roundtrip behavioral tests * Phase 1 Print: apply Codex tracer-bullet review (validate derived tags, document Selene tag-mode flip, strengthen loop/multi-Print tests with event-list shape assertions, explicit Print-skip in QIR/Stim/QuantumCircuit, import result from guppylang.std.builtins) * Phase 1 Print: path-signature validator rejects asymmetric If branches + non-static loops with Print bodies (9 new tests) * Phase 1 Print: inline-CReg definite-assignment dataflow validator + 8 tests (Codex's Print-before-Measure soundness gap closed) * Phase 1 Print: tighten inline-CReg validator to bit-level (Codex tracer-bullet finding Q4) + add missing coverage tests (While/Parallel/Repeat(0)/partial-CReg) * Phase 1 Print: reject whole-CReg Print of inline CReg outright (Codex v2 review Q2 -- prevents silent register shrink); rewrite affected tests at bit-level; add Codex shrink-case regression + direct AST non-static For test * Phase 1 Print: adversarial tests (cross-codegen byte-identity + Selene shape edge cases + QASM exactly-one-comment assertion) * Phase 2 deprecation warnings: CReg(result=False) construction-time warning + Main(...) implicit-return warning fired SLR-wide across all codegen entry points (cached per converter, stacklevel attributes to user, walker tightened to skip false-positive no-result Measure and result=False-only targets) * Land Phase 3a.0-3a.3 iters 1-4: BlockDecl/BlockCall AST + Guppy emitter + 7 Steane Block conversions. * Phase 3a.3 iter 5a-prep: typed BlockArg sum type for BlockCall arg/out bindings (AllocatorArg + four shape stubs). * Fix Codex review of BlockArg refactor: flatten path now rejects deferred BlockArg shapes in out_bindings (symmetric with arg_bindings); add 4 lock-in tests across Guppy + flatten paths. * Phase 3a.3 iter 5b: single-qubit BlockInput type + SingleQubitArg call-site shape end-to-end in Guppy emitter. * Fix Codex iter-5b review: reject mismatched arg/out bindings for LIVE_PRESERVED inputs (would emit invalid Guppy); add flatten-rejection lock-in for SingleQubitArg + clarify test docstring. * Phase 3a.3 iter 5c: single-bit BlockInput via array[bool,1] write-back proxy + SingleBitArg call-site shape end-to-end in Guppy emitter. * Phase 3a.3 iter 5d: QubitBundleArg non-contiguous slot-bundle call-site shape end-to-end in Guppy emitter (pack/unpack across arbitrary allocators, duplicate/bounds/size validation). * Iter-5d Codex polish: end-to-end Selene lock-in for cross-allocator bundle; pre-consume cross-input qubit-slot alias check; refresh stale _emit_block_call docstring + mismatch message. * Iter-5d r2 polish: add asymmetric bundle test that genuinely pins unpack order; reword Bell test to not over-claim order-sensitivity; fix stale validated_args tag comment. * Phase 3a.3 iter 5e.1: unify BlockDecl-body substitution into shared BodyRemap + recursive substitute_stmt (slot/bit-level, reject-on-partial); converter + flatten both delegate to it; 24 synthetic non-identity tests. Byte-identical on the 7 Steane conversions. * Iter-5e.1 Codex early-fixes: token-boundary permute reject (no substring false-positive); BodyRemap rejects conflicting whole/partial binds at build time; exact prepare-slot-order lock-in + reversed-map case + builder-conflict tests. * Phase 3a.3 iter 5e.2: SLR var->typed BlockArg detection (single Qubit/Bit, list[Qubit] bundle) + flatten shape promotion + cross-input qubit/bit aliasing guard at both converter and BodyRemap layers; strip review-attribution from code comments. * Scratch-ancilla S1: ResourceEffect.SCRATCH + converter detection (excluded from out_bindings, kept for O2 alias guard) + R4 Prep->Measure segment-lifecycle validator (every Prep closed by Measure) + 13 lock-in tests. * Scratch-ancilla S1: reject any ReturnOp in a scratch-bearing block (R4 clause 4, Codex S1 r2 -- scratch slot must not be handed out); drop dead _expr_mentions_allocator; +return-rejected test. * Scratch-ancilla S2: Guppy emitter lowers SCRATCH as an internal qubit() allocation (no param/arg/return, seeded CONSUMED); fix live_arg_index to index validated_args; reuse pattern now compiles + runs (original blocker dead). * Scratch-ancilla S2 fixes (Codex r1): whole-program guard rejecting non-scratch use of a scratch-bound outer slot (Prep carve-out for the corpus, reuse still allowed); validate scratch BlockArgs before excluding so malformed bindings fail loudly; 3 lock-in tests. * Scratch-ancilla S2 R5-completeness (Codex r2): per-scope purity guard (main + every BlockDecl.body) covering PermuteOp + ReturnOp observation paths; 3 lock-in tests. * Scratch-ancilla S4: convert qeclib Check (a:scratch) -- SynExtractBare reused-ancilla path now compiles to Guppy (original 5e.3 blocker dead); Check Selene records byte-identical (qubits 4->5 for internal alloc, design R2); Steane production lock-in. * Scratch-ancilla S4: add Color488 serial SynExtractBare production lock-in (Codex S4 non-blocking finding -- design specifies Steane + Color488). * Scratch-ancilla S5: convert qeclib Check1Flag (a,flag:scratch; Prep(a,flag)->Prep(a),Prep(flag) per O1 option a) -- SynExtractFlagged reused-ancilla+flag path now compiles; Check1Flag Selene records byte-identical (qubits 5->7); SynExtractFlagged + CH-branch lock-ins. * Scratch-ancilla S5: fix corpus tracked-block table doc drift (Check/Check1Flag ancilla now scratch, was consumed) -- Codex S5 non-blocking finding. * v1 cutover cleanup: port surface_code_slr_exploration notebook off the deleted IRGuppyGenerator to SlrConverter.guppy(); drop stale deleted-test reference in audit_runner comment. * Centralize AST visitor dispatch: replace 37 per-node accept() double-dispatch stubs with one BaseVisitor _DISPATCH map; drop vacuous ABC from base nodes; nodes.py now visitor-decoupled; +completeness safety-net test (byte-identity + public API unchanged). * Visitor dispatch: walk type(node).__mro__ so subclasses of concrete AST nodes resolve to the base node's visit_* (preserves old inherited-accept() semantics, Codex review); scope completeness test to the nodes module + add MRO-subclass regression test. * Fix two pre-existing qeclib construction bugs (audit XFAIL->accepted, 8->6 XFAIL): PrepProjectZ now preps its list[Qubit] via the Q.pz primitive (dead/broken (QReg,indices) PrepZ removed); Color488 SynExtractBareParallel uses int math.ceil for the range() count instead of the f64 numpy-drop-in pecos_rslib.num.ceil. * Add Phase 3b S1 codemod (libcst): append explicit Return(<result CRegs>) to implicit-return Main() calls, trailing-comma-preserving so the formatter keeps multi-line shape; validated byte-identical on test_v1_behavioral. Not yet rolled out (harness-first + chunked-black-gated plan pending Codex review). * Phase 3b S1a: generalize _selene_harness to accept explicit Return (derive measurement_N keys from returned CRegs OR -- pre-3b -- implicit result CRegs; strict on non-CReg returns); codemod now also matches qualified pecos.slr.Main + mirrors its qualifier for Return. No-op for current tree (1129 pass, audit 31/31). * Phase 3b gate-hardening: implicit-return warning detector now also catches whole-register inline CRegs (Measure(q) > CReg('c',n) stores the CReg itself in cout, not a Bit) -- was under-detecting; suite green (1129), warnings non-fatal. * Phase 3b S1b: harden implicit-return detector (recursive cout -- whole-register + slice inline CRegs); re-apply per-case audit warning enforcement; migrate implicit-return programs to explicit Return across ast_guppy/QASM-regression/legacy-guppy/unit-slr/test_partial (+scoped block.extend(Return) for qeclib QASM-byte-identity, +Stim/QC helper returns). Audit 31/31; strict gate 1131 pass; QASM byte-identical; ruff+black clean. * Phase 3b S1c: fix logical_steane_code_program example -- stale pecos.qeclib import -> pecos.slr.qeclib, prog.qasm() -> SlrConverter(prog).qasm() (was doubly-broken/non-runnable), + explicit Return(m_bell,m_out)/(m_reject,m_t,m_out) so it is S3-safe. Now runs end-to-end; wrapped regression test (own copies) still green. * Phase 3b S2: remove Phase-2 implicit-return warning machinery (slr_converter) + CReg(result=False) warning (vars.py, kwarg kept for S3); revert now-moot per-case audit enforcement to plain hugr() (option 5a); rewrite test_deprecation_warnings into post-S2 no-warning contract tests. gen_codes legacy deprecations untouched. Independently verified: audit 31/31, suite 1121 pass byte-identical, ruff/black clean. * Phase 3b S4: port qir_bc() to AST path (binding.parse_assembly bitcode + split diagnostics), fix invalid // builder comment so qir() emits parseable LLVM IR, sever legacy gen_qir creg.result (S3 blocker; QIRGenerator symbol kept). * Phase 3b S4 fix: wrap binding.parse_assembly().as_bitcode() in the diagnostics try so malformed IR raises the wrapped 'Failed to compile QIR to bitcode' error (Codex post-review blocker). * Phase 3b S3: remove CReg(result=) kwarg + RegisterDecl.is_result field + the v1 implicit-return code path (D-S3-1 b: drop EntryWrapperInfo.result_cregs, explicit-Return-only); no-Return -> main()->None; rewrite test surface to hard contracts; Codex post-review fixes (ISC001 x2 + 4 stale-doc/dead-code). * Phase 3b S5: M2 converter-time elision of flattened composite block-boundary Returns (provenance flag, not position/count); steane_pz compiles (audit accepted, compile-scope); behavioral lock-in is strict-xfail pinning the orthogonal pre-existing v1 FT-RUS pz() non-codeword defect (tracked post-3b, task #72); EncodingCircuit provenance guard. * #72: fix _selene_harness measurement-mapping via opt-in named return tags (D-72-B). entry_wrapper gains keyword-only emit_return_result_tags (default False -> production wrapper byte-identical); harness emits result('__pecos_return.<creg>',..) and _shot_records reads tags by name, re-exporting the historical measurement_N shape (immune to internal RUS measurements). Remove the wrong S5 strict-xfail (steane pz now genuinely passes; stim-verified); add internal-measurement regression guard. * #72 close-out: fail-loud on unexpected Selene result-tag shape in _shot_records (Pickle non-blocking) -- explicit list/int dispatch, TypeError on any other type instead of silent single-bit wrap; behaviour-preserving (1123 passed, audit unchanged). * #71 Stage A: add qir-qis test-group dep + QIR spec-compliance baseline gate over the audit corpus. Honest non-faked baseline (0 compliant; uniform missing-output_labeling_schema cause; build-failure set pinned so a new qir_bc regression trips it); trips deliberately on Stage-B progress. Codex blocker folded + re-confirmed; Pickle signed off. Test-only, production byte-identical. * #71 Stage B1: emit required QIR module metadata in _finalize_module (output_labeling_schema=labeled, qir_profiles=adaptive_profile, !llvm.module.flags qir_*_version + dynamic_*/arrays=false). validate_qir 0->27/28; honest two-tier gate (Tier-1 validate, Tier-1b exact entry-attr value pins, Tier-2 qir_to_qis still create_creg until B2; build-fails pinned). Dual post-review: Pickle YES; Codex blocker (presence-only value pin) folded+re-confirmed. QASM/Stim/QC byte-identical; audit/optional_dep unchanged. * #71 B2a: declare standard QIR classical-model functions (__quantum__qis__mz__body, __quantum__rt__read_result) for B2b to wire. Decls-only, zero behavior/gate change; verified inert (validate/qir_to_qis split, gate, broad 1124, audit all unchanged). Helper methods deferred to B2b (no dead code). * #75: extend pecos_rslib_llvm with LLVM memory ops to unblock #71 B2 -- LLIRBuilder::{alloca,load,store,zext,trunc,icmp_unsigned} + LLConstant::zero (Array->zeroinitializer, Pointer->null) in crates/pecos-llvm; LLType PartialEq/Eq + manual Hash consistent with inkwell's LLVMTypeRef Eq (inkwell 0.8.0 derives Hash only for IntType); 6 pyo3 #[pymethods] + Constant(ty,value=None)->zero + PyLLValue.type getter + structural __richcmp__/__hash__ on Py*Type (PyAnyType gains IntoPyObject so the getter is returnable). Additive only: end-to-end B2-shaped module via the new ops round-trips binding.parse_assembly + qir_qis.validate_qir + qir_to_qis; #71 gate 1 passed unchanged, slr_tests 368 passed, broad qir/qasm/codegen sweep 1785 passed 0 fail; cargo fmt/clippy clean. * #75 post-review nit #1 (Pickle): type __richcmp__ ordering ops (Lt/Le/Gt/Ge) now return NotImplemented so Python raises TypeError instead of a silently-wrong False; Eq/Ne unchanged (lltype_richcmp -> Py<PyAny>). Verified i1<i64 raises, eq/hash/dedup intact, #75 e2e + #71 gate + slr_tests 368 + broad sweep 1785 all green; clippy/fmt clean. * #71 B2 (B2b+B2c) on the #75-extended binding: replace bespoke CReg runtime helpers with the standard M-B2-static model. CReg -> entry-block alloca [N x i1] + zeroinitializer; measure -> __quantum__qis__mz__body + static %Result* + read_result + store (no-result mz; slot=measurement_count-1); BitRef/whole-CReg c.set(int) -> store/per-bit lshr+trunc unpack; BitExpr -> load+zext (i64-canonical); _as_i1/_as_i64 via #75 value.type; record pack load/zext/shl/or -> int_record_output (kept); point-of-use GEP. B2c removes the now-uncalled create_creg/get_creg_bit/set_creg_bit/get_int_from_creg/set_creg_to_int/mz_to_creg_bit decls. #71 structural gate honestly rebaselined: _EXPECTED_QIS_OK 7->27, qis_failed empty, BUILD_FAILED 3->2, qeclib.steane_pz triaged build->validate-fail (B2 makes it build; then hits qir-qis allowlist on PECOS's non-standard __quantum__qis__barrierN__body -- pre-existing barrier gap, out of B2 scope). New tier2_semantic.py: real qir-qis-compiler acceptance + emitted-QIR structural invariants + deterministic AST->Guppy->selene cross-anchor (set_int/zero_init A+B-only; Guppy can't return a set(int) CReg). Executable qir_to_qis->selene differential blocked by LLVM 14<->21 / opaque-vs-typed gap -> tracked separately. slr_tests 368, broad sweep 1785/0-fail, ruff/format clean. * #71 B2 post-review fold: fix Codex blocker + 2 non-blocking. BLOCKER: >64-bit CReg silently miscompiled (creg_map recorded size but alloca/output only when <=64, so CReg(65) built+validated+qir_to_qis-lowered with storage/output silently dropped) -> _process_declarations now raises NotImplementedError for size>64 (single-i64 pack cap; fail-loud, not a silent fallback) + regression test test_oversize_creg_raises_loud (CReg(64) builds, CReg(65) raises). Non-blocking: register `slow` marker in tests/conftest.py::pytest_configure (invocation-independent; canonical sweep 0 warnings; residual single-file pytest-path warning is a conftest-discovery quirk, cosmetic); tighten tier2_semantic Layer B from read_result-subset-of-mz to per-measurement adjacency (_assert_mz_rr_pairing: each read_result immediately preceded by its own same-slot mz), drop dead _MZ_SLOT/_RR_SLOT. Gates: tier2 PASS (8 A/B + tightened pairing + 2 C), oversize regression passes, #71 gate green, slr_tests 368, broad sweep 1785/0-fail, ruff/format clean. Pickle YES; Codex re-confirm pending (blocker fix + regression test were its exact sign-off condition). * #71 B2 Codex re-confirm fold (cosmetic): raise the >64-bit CReg NotImplementedError BEFORE writing context.creg_map (fail before any partial state). Behaviour-equivalent (size>64 raises regardless; size<=64 unchanged); Codex minor note from codex-71-stageB2-postreview-reconfirm.md. Codex re-confirm: YES (0 blockers); both reviewers now YES -> B2 dual-signed-off. * #74: fail loud on the 3 silent AST->QIR miscompiles (B3). VarExpr (was ->constant 0), While (was silent one-pass dropping condition+iteration), Print (was silent drop losing observable output) now raise NotImplementedError with clear messages -- each was valid-QIR-but-wrong-semantics that qir-qis cannot catch. Bounded scope: real While/scalar-SSA/Print->record are explicitly deferred (v1-feature-matrix: real While "too large for first sound emitter"); min bar = fail-fast not silently wrong (same pattern as B2 >64 cap). qir_bc() does not wrap them (only parse_assembly RuntimeError caught) -> surface at QIR-gen. Deliberate #71 gate rebaseline (triaged like steane_pz): only docs.while_loop exercises a B3 site -> _EXPECTED_QIS_OK 27->26, _EXPECTED_BUILD_FAILED 2->3 (NotImplementedError, "does not support While loops"); aligns QIR path with the Guppy path which already rejects While; 0 corpus Print, VarExpr only via for_loopvar_symbolic (build-fails earlier). Fixed enshrined-bug test: test_print.py test_qir_byte_identical asserted the old silent drop -> test_qir_raises_loud_on_print (Stim/QC byte-identical siblings left, documented out-of-scope). 4 fast regression tests in tier2_semantic.py. Gates: 5 fast + #71 gate green, cross-codegen Print 4 passed, slr_tests 368, ruff/format clean; broad sweep in-flight. * #74 dual-review fold (Codex blocker): fix the 4th silent AST->QIR miscompile -- static `For` body dropped. `_process_for`'s `isinstance(node.start, int)` guard was always false (converter wraps For range bounds in LiteralExpr), so every static-For body was silently dropped: valid QIR, wrong semantics, qir-qis-uncatchable -- and docs.for_static_indexing sat in _EXPECTED_QIS_OK on body-dropped QIR (gate dishonest). Proper fix (static fixed-bound For is v1-supported + small, unlike deferred While/SSA): _static_int_bound resolves LiteralExpr(int) start/stop/step; _process_for unrolls range(start,stop,step) exclusive (matches canonical gen_quantum_circuit); symbolic/non-static bound -> NotImplementedError (never silent-drop). _process_repeat left as-is (RepeatStmt.count SLR-enforced int -- never the same bug; bounded fix to _process_for only). Honest resolution, NO baseline shuffle: docs.for_static_indexing stays in _EXPECTED_QIS_OK (26) but its QIR is now correct (body unrolled) -- gate honest by fixing code not the pin. New regression test_static_for_unrolls_body_not_dropped (3 call sites, not the declare; unrolled QIR still qir-qis-lowered). Gates: 6 fast tests + #71 gate green, broad sweep 1785/0-fail, ruff/format clean. Pickle YES; Codex re-confirm pending (its exact sign-off condition). * #74 Codex re-confirm fold: satisfy the project formatter gate (the only re-confirm blocker -- semantically the static-For fix was already verified) + non-blocking step==0. Canonical chain is ruff-check --fix -> black 25.9.0 -> blackdoc (NO ruff format; ruff-format was a spurious self-introduced conflict). Collapsed two black-joined split regexes into single literals (behaviour-identical: r"a" r"b" == r"ab") to kill the black<->ISC001 conflict at the source; converged ruff/black (COM812 auto-fixed, black-stable). ruff check + black --check now clean, matching pre-commit. Non-blocking #1 (Codex): For(step=0) now raises a clear QIR NotImplementedError instead of a bare Python ValueError. Behaviour-preserving vs 177ab70c (formatting no-op; regex equiv; step==0 only changes an infinite-loop error message no test exercises). Pickle YES; Codex re-confirm pending (formatting was its sole remaining condition). * #73 pre-PR hygiene: resolve branch-wide ruff/black debt -> repo fully green. 6 files (4 branch-touched + 2 pre-existing examples/surface, user-chose whole-repo-green). No logic change. migrate_implicit_return.py (libcst CSTTransformer): N802 x2 -> scoped `# noqa: N802` with rationale (libcst dispatches by the exact `leave_<CSTNodeClassName>` name -- API contract, renaming breaks dispatch); ARG002 x2 -> rename unused `original` -> `_original` (libcst calls positionally; no suppression needed). COM812 x7 -> ruff --fix (black-stable). 2x ISC001 from black-joined split strings (_block_flatten error msg, test_ast_visitor assert msg) -> collapsed to single literals at source (behaviour-identical). All 6 black-formatted (line-length 120). Canonical chain ruff-check --fix -> black 25.9.0. Verified: repo-wide `ruff check` All passed + `black --check` clean (760 unchanged); broad sweep 1785 passed / 15 skipped / 0 fail (behaviour-preserving: formatting / equivalent-string / unused-param-rename / comment-only). * #78: fix the silent AST->QIR-sibling miscompiles in the Stim + QuantumCircuit AST codegens (same class as #74, applied per the explicit-over-implicit / fail-fast rule). _process_for: the isinstance(node.start, int) guard was always false (converter wraps For range bounds in LiteralExpr) -> Stim silently dropped every For body; QuantumCircuit's else *rejected every* static For (even valid For(i,0,3)). Now both resolve LiteralExpr(int) start/stop/step via _static_int_bound and unroll range(start,stop,step) exclusive (matches canonical gen_quantum_circuit); symbolic/non-static bound or step==0 -> clear NotImplementedError, never silent-drop. stim _process_while: was silent one-pass + stray TICK -> NotImplementedError (Stim has no runtime loop; same decision as #74's While; QC While already loud). Print -> fail loud in both (was a deliberately-pinned silent skip): Stim "does not support Print" (fundamental -- no classical-output stream); QuantumCircuit "does not yet support Print" (PECOS owns this format, may be added later). _process_repeat untouched (RepeatStmt.count SLR-enforced int -- never the bug; bounded to _process_for). Flipped test_print.py::TestCrossCodegenPrintEmission test_{stim,quantum_circuit}_byte_identical -> _raises_loud_on_print + docstring (same enshrined-bug-test correction #74 did for test_qir_byte_identical). New regression: TestStim/QuantumCircuitStaticForAndWhile (For unrolls 3x not dropped; While raises). Out of scope (surfaced, not changed): Stim silently skips unsupported gates / has no classical-Assign model -- long-standing, fundamental, broad reliance. Gates: ruff+black canonical clean; slr_tests 372/0-fail; broad 1923/0-fail. * #77 DONE: real executable QIR->QIS->selene Tier-2 differential (A+B+C -> A+B+C+D). selene_sim (already a PECOS dep) natively executes qir_qis.qir_to_qis's LLVM-21 opaque-pointer QIS via the bundled selene_helios_qis_plugin (+ Helios QIR runtime) -- the long-claimed "blocked on an LLVM 14<->21 bridge" / "bounded post-PR infra (LLVM-21 JIT + runtime shim)" were BOTH wrong; no PECOS LLVM bump, PECOS-Rust stays LLVM-14. tier2_semantic.py Layer D: _qis_exec_records (qir_bc -> qir_to_qis -> selene_sim.build(BitcodeString) -> run_shots(Stim)) + test_tier2_executable_differential. Deterministic representatives execute to EXACT known classical records (empirically pinned; B2 _generate_results records ALL declared CRegs, declaration order, packed LSB-first): set_int [11], zero_init_safety [0,0], multi_creg [2,1], conditional_correction [0]. bell preserves entanglement correlation (every shot record in {0b00,0b11}, verified across seeds 1/2/7/42/123 -- only 0/3, never 1/2; H/CX present). Clean cross-path differential vs the dual-reviewed AST->Guppy->Selene oracle for conditional_correction (set_int/multi_creg excluded -- Guppy wrapper can't return a set(int) CReg, the documented Layer-C limit; zero_init declared!=returned). Slow-marked; slr_tests 372/0-fail; ruff/black canonical clean. Pending #77 dual review. * #77 dual-review fold (Codex blocker + 2 non-blocking; Pickle YES). BLOCKER: the Bell executable check `all shots in {0,3}` was necessary-not-sufficient -- a dropped-H/dropped-CX/no-op Bell yields all-0 (subset of {0,3}) and would have passed, so the test could not catch the miscompile it guards. Fix: aggregate over fixed seeds (1,2,7,42) and require observed set == {0b00,0b11} (BOTH 00 and 11 must occur -> entanglement actually executed; 1/2 still rejected -> correlation) + single-record well-formedness. Non-blocking: (a) flipped the now-self-contradictory module docstring ("qir_to_qis->Selene blocked by LLVM 14<->21") to the truth -- Layer D selene_helios_qis_plugin runs LLVM-21 QIS, no PECOS LLVM work; framed as a *representative* (not exhaustive) differential; (b) registered the `slow` marker in slr_tests/pytest.ini (was commented under --strict-markers -> PytestUnknownMarkWarning; now clean). Verified: test 1 passed (4 deterministic exact + bell both-00/11-over-4-seeds + cond_corr cross-path), no marker warning, slr_tests 372/0-fail, ruff/black canonical clean. Pickle already YES; Codex re-confirm pending (blocker fix + docstring were its exact sign-off conditions). * #80: fail loud on the 2 QIR silent-miscompile bugs #79 pre-review surfaced (inline/Return-only CReg lost its record; non-Z Prep basis dropped), re-pin #71 gate from actual _qir_state (3 programs QIS_OK->BUILD_FAILED) qir.py: _require_creg() helper raises NotImplementedError at the 4 unknown-CReg silent-skip sites (_process_measure, _process_assign x2, _eval_expression BitExpr -- the last a #74-class silent Constant(0)). converter.py::_convert_prep: non-Z Prep basis raises at the shared root so QIR/Stim/QC/QASM all reject it (mirrors the Guppy Z/+Z preflight; Guppy/HUGR route unchanged -- its SLR-preflight runs pre-conversion). test_qir_spec_compliance.py: pins + docstring counts (build-fail 3->6, QIS_OK n->23) + resolved-#77 narrative. tier2_semantic.py: 2 regression tests. Verified: focused 3/0, broad 258/0, ruff/black clean. * #80 dual-review fold: fix Codex blocker (Return-only inline CReg silently emitted no record -- _process_return now validates returned classical regs, qubit returns not false-rejected) + make the _eval_expression unknown-type default fail loud (same #74/#80 class, Codex non-blocking) + regression test; fix Pickle's surfaced doc-test by rewriting the 3 slr-qeclib.md Prep(q,"X") sites to the v1 Prep;H idiom (generated/ is gitignored). Verified: focused 5/0, doc-tests 22/0, ast_guppy 258/0, ruff/black/blackdoc clean * #80 Codex re-confirm-1 fold: fix the Return-only inline-CReg name-collision bypass at the root (provenance, not name-guessing). _process_return skipped qubit returns by qubit_map name-membership, but _convert_return had erased QReg/CReg provenance (var.sym flatten), so an inline CReg colliding with a declared QReg name was silently dropped (same silent-output-loss class, public-SLR-reachable). Fix: additive ReturnOp.value_kinds populated from the real QReg/CReg object in _convert_return, preserved through _block_substitution, consumed by qir._process_return; unsound qubit_map name-skip removed. Guppy unaffected (reads .values). Collision regression test added. Verified: focused 5/0 (incl NAME_COLLISION raises, qreg_return builds), broad 258/0, doc-tests 22/0, ruff/black clean * #81 Stage A: PrepareOp.basis discriminant + 6 dedicated prep classes (PZ/PNZ/PX/PNX/PY/PNY) + converter _PREP_BASIS routing + recast stray-string guard (basis is the gate identity -- any string qarg on any prep gate fails loud, generalizes/supersedes #80 non-Z guard) + block-sub & pretty-print basis preserve + qb exports. Prep retained as PZ alias so the ~223 sites stay green (no churn until Stage C). #71 pins fragment -> 'stray string argument'. Verified: focused 4/0 (#71 gate green -- recast broke no QIS_OK program), broad ast_guppy 258/0, ast_tests 664/0, ruff/black clean * #81 Stage B: single shared canonical basis->tail map (_prep_tail.py) wired into all 5 codegens (QIR/Stim/QASM/QC/Guppy) -- reset + Clifford tail per pinned 6-row table (PZ id/PNZ X/PX H/PNX H;Z/PY H;S/PNY H;Sdg); non-PZ prepare_all fails loud where the backend no-ops it; PZ byte-identical (zero corpus churn); QC sequences reset/tail as separate ticks; Guppy functional tail. Verified: Stim peek_bloch all-6 PASS, broad ast_guppy 258/0, ast_tests 664/0, ruff/black clean * #81 Stage C: hard-replace Prep->PZ repo-wide (63 files, all 3 subsystems: SLR gate+call sites + legacy gen_codes op_class + circuits gate-vocab); removed class Prep + _PREP_BASIS Prep entry + qeclib __init__ Prep + dead Guppy non-Z preflight reject + dead _string_args; PhysicalQubit.pz->preps.PZ; converter scratch-lifecycle messages Prep->PZ. Public SLR API break (accepted). Verified: full slr_tests 387 passed + behavioral all-6 PASS + ruff/black clean. The 27 *_qir permutation/measurement failures are PRE-EXISTING (legacy gen_codes QIR comment assertions the AST codegen never emitted; git -S Permutation empty on ast qir.py; #81 introduced 0 new failures) -- tracked task #87, out of #81 scope * #81 Stage D: docs->dedicated PZ/PX + doc-tests regen (22/0); audit factories _docs_prep_basis_x/_docs_surface_syndrome_block18->dedicated gates, GuppyCodegenError expected_failure deleted; #71 re-pinned from ACTUAL _qir_state (BUILD_FAILED 6->5/QIS_OK 23->24: prep_basis_x->QIS_OK; surface_syndrome_block18 stays BUILD_FAILED on its inline Measure(data)>CReg(final) #80 'was not declared at Main scope' reason NOT prep -- pinned honestly; counts+docstring prose); sim hard-rename Pn{X,Y,Z}->PN{X,Y,Z} 8 files + gate_type.rs comments + removed dead .typos.toml Pn (rslib rebuilt; SIM DISPATCH 13/13 + PZ/PNZ/PX state-correct); fixed 5 genuinely-#81 test-fallout (test_ast_hugr->test_hugr_rejects_stray_prep_basis_string asserting converter NotImplementedError stray-string; 4 converter scratch-lifecycle messages Prep->PZ, re-Prepped verb kept to match deterministic sed-renamed test spec). Verified FULL slr_tests: 391 passed, only #87(14 perm/meas legacy-QIR-comment) + #88(9 #74/#80 scalar-var/oversize fail-loud + SX-rx/parallel/array-unpack) failing -- BOTH proven pre-existing-rel-to-#81 (exception-identity + no-prep; pecos/regression+guppy never subset-verified), #81 net-new=0; #71 green; behavioral all-6 PASS; doc-tests 22/0; ruff/black/cargo-fmt clean * #81 Stage E: behavioral suite test_prep_gates.py -- 6 dedicated prep gates Stim peek_bloch (+-Z/+-X/+-Y) + 6 AST->Guppy->Selene prep-then-rotate(SZdg/H)-then-measure deterministic + emitted-tail QIR/QASM spot-check per _prep_tail + B1 Codex soundness regression (PX in BlockDecl via BlockCall keeps X-basis through flatten_block_calls in QASM&Guppy + |+> via Selene) + sim PN/alias native-basis behavioral (MZ/MX/MY). Verified: fast 410 passed (only #87/#88 pre-existing, 0 outside, #81 net-new=0) + slow prep_gates 7/7 PASS + ruff/black clean * #81 post-review fold (round-1): Codex blocker -- direct canonical PNX/PNY keys added to statevec/bindings.py (was asymmetric vs PZ/PX/PY/PNZ; Rust dispatchers had all 6) + test_sim_pn_entry_points now 12 cases exercising all 6 direct (incl PNX-MX-1/PNY-MY-1) + 6 aliases. Pickle-surfaced 6th #81-fallout: git mv regression gold preps.Prep.qasm->preps.PZ.qasm (test_Prep builds qubit.PZ; reset-only content unchanged; tests/pecos/regression 105/0, no other gold fallout). 4 non-blocking folded: autosummary rst Prep->6 gates; emitted-tail asserts reset all 6; stray-string regression covers all 6 prep classes (was PZ-dup+PX); stale Prep prose in converter + pz() docstring. Verified: sim 6-direct+aliases 12/12 state-correct; test_Prep PASS; tests/pecos/regression 105/0; full slr_tests only #87/#88 pre-existing (#81 net-new=0); slow prep_gates 7/7; behavioral all-6 PASS; ruff/black clean * #87/#88 partial: AST-QIR optional_dependency-lane fixes (honest, pre-existing gen_codes->AST-cutover gaps, not #81) #87: qir._process_permute now emits the legacy-format `; Permutation:` comment (whole-reg `a <-> b` / per-element `a[0] -> b[1], ...`), PermuteOp carries whole_register, _block_substitution preserves it. Element-granularity was already correct via sources/targets. Net: test_mixed_permutation_qir + test_multiple_permutations_qir now pass. test_permutation_with_bell_circuit_qir: measurement regex was stale vs the committed #76-B2 M-B2-static model (it removed bespoke @mz_to_creg_bit + legacy 1-arg %Result*-returning mz__body; mz now lowers to standard 2-arg `call void @__quantum__qis__mz__body(%Qubit*, %Result*)` + read_result + store) -- updated the regex to the current correct form (QIR is correct; this is the same stale-format test-update class as #88, user-authorized). #88: qir.py PARAMETERIZED 1q+2q gate params resolve LiteralExpr before float() (was a hard TypeError for parallel/literal params); 6 test_slr_phys tests updated to the #74/#76/#80 fail-loud reality (>64-bit CReg -> NotImplementedError 'has 75 bits'; whole-CReg scalar arith/conditions -> NotImplementedError 'classical variable'). NOT fixed (surfaced to user, not auto-guessed): 7 #87 tests hard-assert #76-B2-REMOVED bespoke APIs (@set_creg_bit/@get_creg_bit/@create_creg/ @set_creg_to_int/creg-xor) -- un-passable without reverting the dual-reviewed M-B2-static model; test_comprehensive_qir_verification is a real element-wise-Permute silent miscompile (comment emitted but qubit remap is a no-op; allocator_offsets keyed by reg name, AST never ported legacy per-element permutation_map); 2 #88 deferred-class (test_sx_sxdg = #78-deferred unsupported-gate silent-drop; test_unique_unpacked_names = real Guppy size-1-unpack bug). Verified: default lane 403/0 (no regression); behavioral prep all-6 24/0; optional_dependency lane 12->8 failed (3 #87 + 5 #88-side fixed); ruff/black-25.9.0 clean. * #87 resolved: real Permute realization in AST QIR (static logical relabel) Root cause (disproven the 'just a missing comment' premise via the actual emitted QIR + instrumentation): the AST QIR codegen never realized Permute at all. The old allocator_offsets swap was dead code -- offsets are {a:0,b:0} (physical base comes from a parent allocator the swap never touched), and gate lowering never consulted it. Both element-wise AND whole-register Permute were silent #74-class no-op miscompiles (a `; Permutation` comment with gates still on the original qubits). Fix (mirrors the WORKING Guppy reference -- _emit_permute via the linearity tracker's `.permute()` atomic relabel; QIR and the Selene runtime have no permute intrinsic, confirmed by inspection, so a compile-time relabel is the only mechanism, same as legacy gen_qir's permutation_map): QirCodeGenContext.permutation_map maps a logical (reg,index) -> the (reg,index) whose storage it resolves to. _process_permute expands whole-register/element refs (qreg_sizes + creg_map), requires the mapping bijective over the same ref set, emits the legacy-format `; Permutation:` comment, then composes ATOMICALLY (snapshot old, map[s]=old.get(t,t)) so a whole-register sources=(a,b)/targets=(b,a) applies once instead of cancelling. Two single-point chokepoints consult it: get_qubit_index (every qubit ref) and _creg_bit_ptr (every classical-bit ref) -- decl-time pre-population sees the empty map so real qubits still allocate 1:1. Works uniformly for whole-register + element-wise, QReg + CReg. All 14 permutation/measurement *_qir tests rewritten as POSITIVE realized-behavior tests, expected QIR pinned from the actual emitted output (qubit indices deterministic in declaration order; the #76-B2-removed bespoke @set_creg_bit/@mz_to_creg_bit/@create_creg helpers replaced by alloca-buffer stores + the standard 2-arg @__quantum__qis__mz__body(%Qubit*,%Result*)) -- not weakened, not xfailed. Added test_whole_register_qreg_permutation_realized_qir guarding the realizable path. Verified: optional_dependency lane 41 passed (only the 2 #88-deferred fail: test_sx_sxdg, test_unique_unpacked_names); default lane 403/0 (no regression); behavioral prep all-6 24/0; ruff/black-25.9.0 clean. * #88: broad unsupported-gate fail-loud (88A, the class #78 deferred) + Guppy unpack xfail (88B) #88A: qir._process_gate `if qir_name is None: return` was a silent #74-class drop (18 gates absent from GATE_TO_QIR: CH/CRX/CRY/CRZ/CY/F/F4/F4dg/Fdg/SX/SXX/SXXdg/SXdg/SY/SYY/SYYdg/SYdg/ SZZdg) -> valid QIR, wrong semantics, qir-qis-uncatchable. Now raises NotImplementedError. The "broad reliance" #78 feared did NOT materialise -- blast radius is tiny: test_sx_sxdg -> raises (SX/SXdg never had a QIR lowering; legacy gen_qir didn't either) + an honest #71 re-pin (qeclib.generic_check_xyz / generic_check_1flag_ch use the "XYZ" Check whose Y branch emits CY (no QIR lowering); they were dishonestly QIS_OK on the silent drop -> moved _EXPECTED_QIS_OK 24->22 into _EXPECTED_BUILD_FAILED "has no QIR lowering"; docstring "(7)" / n=22, re-pinned from the actual _qir_state(), never guessed). #88B: test_unique_unpacked_names investigated -> real Guppy-emitter bug, NOT shallow: slot-local naming f"{allocator}_{index}" (guppy.py _local_name + guppy_linearity binding init + entry-unpack LHS -- 3 sites that must agree) collides when an unpack name (`q_0`) equals another declared register's name; `q_0,q_1 = q` rebinds the `q_0` param, then `q_0_0, = q_0` raises UnpackableError. A correct fix needs a single namespace-wide uniquification authority feeding all 3 sites (linearity state has no register/block-decl names) with corpus-wide local-name churn -- cross-cutting, out of #87/#88 pre-PR scope. Per the investigate->fix-if-shallow-else-xfail rule: @pytest.mark.xfail(strict=True) with full findings, tracked. Verified: default lane 403/0; optional_dependency lane 42 passed / 3 xfailed / 0 failed; behavioral prep all-6 24/0; ruff/black-25.9.0 clean. * #79: corpus-wide executable QIR validation (generalises #77 Layer D) For every audit-corpus program qir_to_qis accepts (the live #71 _qir_state() QIS_OK set, now 22), EXECUTE the lowered QIS (qir_bc -> qir_qis.qir_to_qis -> selene_sim.build -> run_shots(Stim) via the #77-proven _qis_exec_records) and assert the EXECUTED classical records -- the end-to-end proof of the B2 M-B2-static lowering + #74/#78/#87(Permute)/#88A fail-loud line at corpus scale. The oracle (the hard part): a _MANIFEST classifies all 22 QIS_OK programs D/P/X, each derived from first principles by reading the circuit and CONFIRMED-not-reverse-fitted by execution. D (3) = exact record; P (8) = a HARD invariant (correlation / fixed bits / small exact value set) asserted over a fixed seed set, must hold AND be exercised -- NEVER a statistical/tolerance compare; X (11) = no classical record or no sound first-principles invariant, documented per program. Folds the #79 dual PLAN pre-review: blocker 3 -> n_qubits from the QIR required_num_qubits entry attr (not max-operand-ref, which is 0 for no-op QReg programs and panics selene); blocker 1 -> a record-shape contract (an X-with-record program that executes to NO record fails = output-loss miscompile, not silent X). Blockers 1+2 root causes were resolved upstream by #80 (inline/Return-only CReg fails loud) + #81 (correct PX); prep cases re-derived under the correct #81 semantics -- docs.prep_basis_x is PX=|+>, one uniform Z-measure bit -> X, NOT the pre-review's deterministic [0] (which assumed the old broken Prep('X')=Z-reset). One first-principles guess corrected BY execution (the design-mandated confirm step): legacy.multiple_qregs is NOT c1==c2-correlated (all 4 combos occur); sound invariant is bit-1==0 per CReg, correlation NOT claimed. Drift guard: candidate set taken live from _qir_state() QIS_OK; class histogram pinned D=3/P=8/X=11 so a reclassification is deliberate. Slow + optional_dependency lane. Verified: 23 passed (22 executable + drift guard); default lane 403/0 unaffected (deselected); ruff/black-25.9.0 clean. Pending dual post-review (reviews/dual-79-corpus-executable-postreview-prompt.md). * #87 post-review fold (Codex blocker): validate expanded Permute refs BEFORE dict construction Codex post-review NO -- 1 blocker: _process_permute built `mapping` via `dict(...).update(zip(src_refs,tgt_refs))` and only THEN checked `set(mapping) != set(mapping.values())`. A duplicate expanded source collapses in the dict before the check, so a genuinely non-bijective public Permute compiled instead of failing loud (silent miscompile, the exact class this branch fights): `Permute([a[0],a[0]],[b[0],a[0]])` COMPILED. Fix (Codex's prescription): accumulate the expanded source/target refs as LISTS, then validate BEFORE building the dict -- reject (a) duplicate expanded sources, (b) duplicate expanded targets, (c) src set != tgt set -- then build the map atomically. Regression test_non_bijective_permute_fails_loud covers both Codex repro shapes (distinct non-bijective -> "bijective over the same ref set"; duplicate-source -> "duplicate source ref"). Pickle already YES; this was Codex's sole blocker. Verified: both repros now raise NotImplementedented loud; default lane 403/0; optional_dependency 43 passed / 3 xfailed / 0 failed (+1 new regression); behavioral prep all-6 24/0; ruff/black-25.9.0 clean. * Strip reviewer/persona names from code comments; pin duplicate-target Permute guard Per user direction: code comments/docstrings must not name reviewers/agents/personas or leak the review-process workflow. Branch-wide sweep of changed .py files -- replaced "Codex"/ "Pickle"/"pre-review blocker N"/"re-confirm note" etc. with the durable issue ref (#80/#79/#71/#88A) + the technical reason. No behavior change (comments/docstrings only). Also folds the #87 non-blocking symmetry note: test_non_bijective_permute_fails_loud now also pins the duplicate-TARGET guard (was only duplicate- source + distinct-non-bijective; the target guard was live but unpinned). Verified: default 403/0; optional_dependency 43 passed / 3 xfailed / 0 failed; slow #79 lane 23/23; ruff/black-25.9.0 clean; branch-wide grep for persona/review-process terms in *.py is empty. * #93 (partial): verified QIR lowering for SX/SXdg/SY/SYdg (executable-Clifford) The user-chosen verified-safe subset of the 18 #88A fail-loud gates. Methodology (no guessing): extract each gate's authoritative unitary from the PECOS StateVec simulator, search the EXECUTABLE Clifford primitive set for a sequence equal up to a global phase, then verify end-to-end through qir_to_qis -> selene. Key correctness finding: decomposing to rx/ry/rz is WRONG -- `__quantum__qis__rx__body` is a pinned build/exec failure (`docs.rotation_rx`) and selene's Stim backend silently no-ops an rx, so a rotation lowering is itself a silent miscompile (the behavioral check caught this -- SX;SX gave 0 not 1). Correct executable-Clifford lowering (verified up-to-phase vs PECOS sim AND end-to-end): SX=H;S;H, SXdg=H;Sdg;H, SY=H;X, SYdg=H;Z. `_GATE_DECOMP` + a pre-fail-loud branch in `_process_gate` (re-emits the primitive sequence; F-family/CY/CH/CR*/sqrt-2q stay fail-loud pending the scoped workstream). test_sx_sxdg flipped raises->positive. test_sqrt_clifford_gates_executable (slow+optional_dependency) pins the end-to-end identities SX;SX==X / SXdg;SX==I / SY;SY==Y / SYdg;SY==I + Z-randomness of the single gates. #71/#79 corpus buckets UNCHANGED (no curated case uses SX/SY; the CY-bearing generic_check programs stay BUILD_FAILED). Verified: default 403/0; optional_dependency 43 passed / 3 xfailed / 0 failed; slow 24 passed (22 #79 + manifest-cover + sqrt_clifford); ruff/black-25.9.0 clean. * Fix dead test module: rename tier2_semantic.py -> test_tier2_semantic.py so pytest collects it Integrity finding: tier2_semantic.py did not match pytest's test_*.py pattern and there is no python_files override, so it was only ever imported as a helper -- its tests NEVER ran in any lane. That silently killed #77's test_tier2_executable_differential (the QIR->qir_to_qis->selene executable differential) AND a whole module of #74/#80/#81 fail-loud regression guards (test_varexpr/while/print/oversize/inline-creg/prep-stray-string _raises_loud). A "verified" gate that never runs is faked-green. Proper fix (no python_files hack): git mv to test_tier2_semantic.py + update the one real importer (test_qir_corpus_executable.py) and the stale docstring prose in test_qir_spec_compliance.py. The revived tests were NOT bit-rotted -- they pass once actually collected. Verified: default lane 403 -> 413 (+10 revived fast guards, all green); optional_dependency 43 passed / 3 xfailed / 0 failed; slow 33 passed / 0 failed (incl. the revived test_tier2_semantic_b2 + test_tier2_executable_differential + #93 sqrt_clifford); ruff/black-25.9.0 clean. * A2: fix the Permute silent-miscompile in the Stim + QuantumCircuit codegens (same class as #87 QIR) Both AST codegens had the identical pre-#87 broken pattern: a `allocator_offsets` swap that NEVER reached gate qubit-index resolution (element-wise refs are keyed by element string, not reg name -> no-op) and self-cancelled for a whole-register (a,b)/(b,a) pair. Confirmed: `Permute([a[0],b[0]],[b[0],a[0]])` then `Y(a[0]); Z(b[0])` emitted `Y 0; Z 2` (Stim) / `Y:{0},Z:{2}` (QC) instead of the correct `Y 2; Z 0` -- a silent miscompile, the exact class #87 fixed in QIR (QASM was already correct). Fix: port the proven #87 `permutation_map` model to both -- a static logical relabel consulted in `get_qubit` (the single qubit-ref chokepoint), built by `_process_permute` from expanded refs with the post-#87-fold validation (dup-source / dup-target / src-set!=tgt-set all fail loud) and atomic compose. Whole-register CReg Permute fails loud (Stim/QC have no classical-register model). Mirrors the Guppy linearity tracker / QIR #87. Regression: test_permute_realized_quantum_circuit (default lane) + test_permute_realized_stim (optional_dependency) pin element + whole-register realized targeting and the non-bijective fail-loud. Verified: default 413->414 (+1 QC regression); optional_dependency 44 passed / 3 xfailed / 0 failed (+1 Stim regression); slow 33/0 (no regression); ruff/black-25.9.0 clean. No pre-existing test had pinned the broken behavior (unlike QIR's 14 #87 tests). * A: Stim unsupported-gate silent-skip -> fail loud (#78 surfaced-not-changed analogue of #88A) stim._process_gate `if stim_gate is None: return` silently dropped 12 gates (CH/CRX/CRY/CRZ/F/F4/F4dg/Fdg/RX/RY/RZ/RZZ) -> the emitted Stim circuit ran with WRONG semantics (a #74-class silent miscompile, uncatchable downstream). Now raises NotImplementedError ("has no Stim lowering"), same doctrine as #74/#78/#88A. Stim is Clifford-only so non-Clifford rotations are fundamentally unrepresentable; the bug was emitting the circuit without them. Regression test_unsupported_gate_fails_loud added. QC investigated, NOT changed: it does not silent-skip -- it name-passes-through (`.get(gate, gate.name)`); the 7 non-param fallbacks are valid PECOS-sim gates (correct), and parameterized gates already FAIL LOUD via the QuantumCircuit container's "requires N angle(s), got 0" ValueError (not a silent miscompile). No scope creep / no capability regression. Blast radius ZERO (the #78 "broad reliance" fear did not materialise in the corpus, same as #88A for QIR): default 414->415 (+1 regression), optional_dependency 44 passed / 3 xfailed / 0 failed, ruff/black-25.9.0 clean. * Post-review fold (Codex blocker): complete the branch-wide persona/process-name strip from .py comments Codex post-review NO -- 1 blocker: the 663c0f77 claim (and my session claim) of a *branch-wide* clean was false; ~30 added .py comment/docstring lines outside that 5-commit range still named reviewers/process ("Codex S2 review", "post-review blocker", "re-confirm bug", "Codex #81 ...", etc.) in 10 files (guppy.py/converter.py/nodes.py/check_1flag.py/migrate_implicit_ return.py + 5 test modules). Pickle YES (read the claim narrowly as changed-files-only); Codex's branch-wide reading matches the durable rule -- fold it. Stripped every occurrence, replacing with the durable issue ref + technical reason (e.g. "#80 re-confirm bug" -> "#80 name-collision bug"; "(Codex S2 review)" -> dropped, surrounding text already states the rationale; "confirmed Codex 2026-05-16" -> "confirmed 2026-05-16"). Comments/docstrings ONLY -- zero behavior change. Verified: `git diff dev...HEAD -- '*.py'` added lines now match none of Codex/Pickle/grug/Banana/pre-review/post-review/ re-confirm; default 415/0, optional_dependency 44 passed / 3 xfailed / 0 failed, slow 33/0, ruff/black-25.9.0 clean. * #93 (cont.): verified QIR lowering for the F/Fdg/F4/F4dg face Cliffords Same verification-first method as the SX/SY subset: extracted each unitary from the PECOS StateVec sim, searched the EXECUTABLE Clifford set for a sequence equal up to a global phase, verified end-to-end through qir_to_qis -> selene. Results (circuit order): F=SZdg;H, Fdg=H;SZ, F4=H;SZdg, F4dg=SZ;H. _GATE_DECOMP entries (incl. the existing SX/SXdg) now use GateKind.SZ/SZdg rather than the redundant S/Sdg aliases, per the PECOS convention that S==SZ / Sdg==SZdg (both already map to "s"/"s__adj" in every codegen -- zero functional change; re-verified). test_face_clifford_gates_executable pins the end-to-end identities: F;Fdg / Fdg;F / F4;F4dg / F4dg;F4 == I, the F|0>=|+> non-no-op discriminator, and F;F;F == I (PECOS F is the order-3 face Clifford). #71/#79 corpus buckets unchanged. Verified: default 415/0, optional_dependency 44 passed / 3 xfailed / 0 failed, slow face+sqrt 2/2 (full slow unaffected), ruff/black-25.9.0 clean. Remaining #93: 2q Clifford (CY/SXX/SYY/SXXdg/SYYdg/SZZdg) need a 2q decomposition path; CH (non-Clifford) + CRX/CRY/CRZ (rotations, no executable primitive) stay fail-loud. * #95: canonicalize GateKind on SZ/SZdg; remove redundant S/Sdg (user-directed) PECOS convention: the phase gate S == SZ and Sdg == SZdg. GateKind carried both as redundant members. Per user directive, removed GateKind.S/Sdg; the SLR `S`/`Sdg` gate classes now map (converter GATE_KIND_MAP) to the canonical GateKind.SZ/SZdg. Dropped the now redundant S/Sdg rows from every codegen map (GATE_TO_QIR/STIM/QC/ QASM/guppy -- SZ/SZdg rows already present), type_checker arity, gate_properties INVERSE_PAIRS (SZ<->SZdg pair remains), and _prep_tail PY/PNY tails. _GATE_DECOMP already on SZ/SZdg. Behavior delta (user-decided -- "keep SZ->rz(pi/2)"): the SLR `S` gate's QASM lowering changes `s q;` -> `rz(pi/2) q;` (and `sdg` -> `rz(-pi/2)`), since the canonical kind SZ maps to rz(pi/2) in qasm.py (the only codegen where S != SZ; physically identical, s == rz(pi/2) up to global phase). All other codegens unchanged (SZ == S there). #81 PY/PNY prep-tail QASM accordingly flips h;s -> h;rz(pi/2); test_prep_gate_emitted_tail[PY,PNY] re-pinned honestly from the actual emitted output (QIR needles unchanged -- SZ->"s" in QIR). 18 GateKind.S/Sdg refs across 10 source files; 0 residual; no test referenced GateKind.S/Sdg directly. Verified: default 415/0, optional_dependency 44 passed / 3 xfailed / 0 failed, slow 34/0, ruff/black-25.9.0 clean. Pending dual review (repo-wide rename + deliberate QASM delta). * test hygiene: dedupe the repeated QC expected-string into a local (behavior-identical; clears the dirty worktree the #93/#95 reviewers flagged) * #93 cont.: verified QIR lowering for SZZ/SZZdg/SXX/SXXdg/SYY/SYYdg/CY (7 2q Cliffords) Per ~/Repos/qir-qis/src/lib.rs:59 (ALLOWED_QIS_FNS, 23 functions): the qir-qis native 2q gate is rzz (parameterized) -- there is NO zz/szz in the allowlist (the prior `GATE_TO_QIR[SZZ]="zz"` emitted `__quantum__qis__zz__body` which qir-qis rejects with "Unsupported QIR QIS function"). Quantinuum supports ZZ-flavor entanglement natively, just as `rzz`. Fixed: - Removed the misleading `GateKind.SZZ: "zz"` from `GATE_TO_QIR` (and `SZZ` from `TWO_QUBIT_GATES`) -- no more misleading codegen surface. - Extended `_GATE_DECOMP` to a 2q-capable shape: each step is (prim_kind, qubit_idx_tuple, params_tuple); `_process_gate` emits each step with the input gate's targets routed and constant params threaded. - Added 7 verified decompositions: SZZ = RZZ(pi/2)(q0,q1) SZZdg = RZZ(-pi/2)(q0,q1) SXX = (H@H); RZZ(pi/2); (H@H) SXXdg = (H@H); RZZ(-pi/2); (H@H) SYY = (Sdg@Sdg);(H@H); RZZ(pi/2); (H@H);(S@S) SYYdg = (Sdg@Sdg);(H@H); RZZ(-pi/2); (H@H);(S@S) CY = Sdg(t); CX(c,t); S(t) Each VERIFIED up-to-global-phase against the PECOS StateVec unitary AND end-to-end via qir_to_qis -> selene (inverse pairs, CY|10> -> i|11> non-vacuity, SZZ^2 = Z discriminator). - `test_sqrt_pauli_2q_gates_executable` (slow+optional_dependency) pins the end-to-end identities. Methodology correction recorded earlier in memory: my prior "rx is a silent no-op on the Stim backend" claim was wrong (probe-harness bug). RX/RY/RZ/RZZ all execute correctly. The truly misleading surface was only SZZ -> "zz" (now fixed). #71 honest re-pin: BUILD_FAILED 7 -> 5, QIS_OK 22 -> 24 (generic_check_xyz / generic_check_1flag_ch use CY which now lowers, so they re-enter QIS_OK). #79 manifest updated: both classified X (XYZ stabiliser on |0..0> is not an eigenstate -> single uniformly random syndrome bit; no hard invariant); histogram pin D=3/P=8/X=11 -> X=13. Verified: default 415/0, optional_dependency 44 passed / 3 xfailed / 0 failed, slow 28/0 (24 QIS_OK + manifest guard + sqrt/face/sqrt_pauli_2q behavioral), ruff/black-25.9.0 clean. Remaining #93 (still fail-loud, no PECOS sim oracle): CH (sim doesn't support), CRX/CRY/CRZ (parameterized, sim doesn't support) -- decomposition would require a convention pin without a verification oracle. Surfaced for separate scope. * #93 post-review fold (Codex blocker): add phase-sensitive CY interference assertion Codex post-review NO -- 1 blocker: the committed CY executable test (X q0; CY; M q1 -> 1; CY|00>; M q1 -> 0) does not discriminate the CY target-phase decomposition. Mutation probe (swap CY's `Sdg(t); CX; S(t)` -> `S(t); CX; S(t)`) SURVIVED the test, violating the prompt's explicit non-vacuity requirement. Pickle YES (the unitary verification covers it; the e2e is a secondary guard) -- but the test should still fail under that mutation per the prompt + branch discipline. Folded: added the phase-sensitive interference assertion Codex prescribed -- `H q1; CY(q0, q1); H q1; MZ q1` with q0=|0> must give 0. Verified: committed PASSes; the mutated S;CX;S CY decomposition now FAILS the test (the assertion message names the exact mutation it catches). Mechanism: with q0=|0> the correct CY is identity, so H;I;H = I -> 0; the mutated S;CX;S applies S^2 = Z on q1 even when control=0 (since CX is identity then), giving H;Z;H = X -> 1. Verified: default 415/0, optional_dependency 44 passed / 3 xfailed / 0 failed, slow sqrt_pauli_2q PASSes, ruff/black clean. * #93 re-confirm fold (Codex blocker): COM812 trailing comma on the new CY assertion Codex re-confirm NO -- 1 trivial blocker: the folded CY interference assertion's multi-line set literal `{\n (0,)\n}` (black-reformatted) triggers ruff COM812. The assertion was semantically correct (Codex verified it catches both the prescribed `S;CX;S` mutation AND the symmetric `Sdg;CX;Sdg`), but `uv run ruff` (the project version, the authoritative gate -- distinct from `uvx ruff@latest` which didn't flag it) was red. Added the trailing comma via `ruff --fix`. Lanes unchanged: default 415/0, optional_dependency 44/3xf/0, slow sqrt_pauli_2q 1/0; ruff + black-25.9.0 clean. * #93 cont.: verified 1-CX QIR lowering for CH (non-Clifford -- Quest backend) * #93 cont.: verified 1-RZZ QIR lowering for CRX/CRY/CRZ + PECOS oracles * #88B: Guppy slot-local name disambiguation against declared register names * Cross-codegen: verified Stim decomposition for F/Fdg/F4/F4dg face Cliffords * Cross-codegen: native CRX/CRY/CRZ in ArbitraryRotationGateable + QC param-threading * Cross-codegen: CRX/CRY/CRZ in CuStateVec/CudaStateVec/MPS simulator backends * Cross-codegen post-review fold (Codex blocker): CR* control-superposition phase test * #94 B1: classical-variable (whole-CReg scalar) QIR lowering via shared i64 pack * Cross-codegen Guppy Phase A: native SX/SXdg (v/vdg) + RX/RY/RZ/CRZ rotations * Cross-codegen Guppy Phase B: decompose SY/SYdg/F-family/sqrt-Paulis/CRX/CRY (native zz_phase) * Post-review folds (Codex non-blockers): Guppy decomposed-rotation missing-angle fails loud + B1 LoopVar comment accuracy * #97: SLR API -- rotational gates use angle-first RX(theta, q), remove bracket form * #97 followup: fix stale test_control_flow_qir comment (rx now lowers; build-only is the non-Clifford-on-Stim caveat) * #97 post-review fold (Codex blocker): fail loud on mis-ordered angle-first calls (SLR __call__ type guard + QIR/QASM arity guards) * #97 post-review fold: narrow qarg guard to quantum qubit types (Codex reconfirm blocker) * Add SLR v2 typed-angle API: rotation gates take rad()/turns() over the exposed angle64 dtype * Make just lint pass: typos false-positive exemptions, black 26.x reformat, awk-portable GHA pinning check * Harden GHA pinning check: bind the 40-hex SHA to the uses: ref so a comment SHA can't mask an unpinned action (Codex blocker) * Strip internal planning/issue references and persona names from code comments, keeping the technical reasons * Strip remaining internal stage labels and string-literal planning references from comments and messages, keeping public-tracker refs * Strip remaining internal milestone labels from comment and message strings (R4/O1-2/S2-S3/Phase 3a-b/M-B2/iter) * Strip milestone Phase/iter references from comments and messages, keeping the two-phase BlockCall algorithm labels
1 parent fa15304 commit f559e60

227 files changed

Lines changed: 18044 additions & 21943 deletions

File tree

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

.typos.toml

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ IY = "IY"
88
IZ = "IZ"
99
iz = "iz"
1010
anc = "anc"
11-
Pn = "Pn"
1211
emiss = "emiss"
1312
fo = "fo"
1413
# HUGR JSON format uses "typ" as a field name for type information
@@ -42,3 +41,13 @@ agger = "agger"
4241
CPY = "CPY"
4342
# Abbreviation for "undetectable" in fault enumeration debug output
4443
UNDET = "UNDET"
44+
# Legitimate "mis-" prefix used in comments/messages (mis-ordered,
45+
# mis-declared, mis-emitted, mis-shape, mis-count, mis-mapping, ...)
46+
mis = "mis"
47+
# Valid spelling of "unparseable" (used in comments + a test name)
48+
unparseable = "unparseable"
49+
Unparseable = "Unparseable"
50+
# Prep-gate naming: the "Pn -> PN" entry-point rename (PNX/PNY/PNZ)
51+
Pn = "Pn"
52+
PN = "PN"
53+
pn = "pn"

crates/pecos-core/src/angle.rs

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,46 @@ where
148148
}
149149
}
150150

151+
/// Converts the angle to turns in `[0, 1)` (the inverse of `from_turns`).
152+
///
153+
/// # Panics
154+
/// This function will panic if the conversion of `fraction` or `max_value` to `f64` fails.
155+
pub fn to_turns(&self) -> f64 {
156+
let max_value = T::max_value()
157+
.to_f64()
158+
.expect("Failed to convert max_value to f64");
159+
self.fraction
160+
.to_f64()
161+
.expect("Failed to convert fraction to f64")
162+
/ max_value
163+
}
164+
165+
/// Converts the angle to signed turns in `(-0.5, 0.5]`.
166+
pub fn to_turns_signed(&self) -> f64 {
167+
let t = self.to_turns();
168+
if t > 0.5 { t - 1.0 } else { t }
169+
}
170+
171+
/// Converts the angle to half-turns in `[0, 2)` (π radians = 1 half-turn).
172+
///
173+
/// Half-turns are the unit used by some backends (e.g. Guppy's `angle`),
174+
/// where a full turn is `2.0`.
175+
///
176+
/// # Panics
177+
/// This function will panic if the conversion of `fraction` or `max_value` to `f64` fails.
178+
pub fn to_half_turns(&self) -> f64 {
179+
self.to_turns() * 2.0
180+
}
181+
182+
/// Converts the angle to signed half-turns in `(-1, 1]`.
183+
///
184+
/// Like [`Self::to_radians_signed`], this principal-value form avoids the
185+
/// spurious global phase that the unsigned `[0, 2)` form introduces when a
186+
/// half-angle (`θ/2`) computation crosses the 2π wrap point.
187+
pub fn to_half_turns_signed(&self) -> f64 {
188+
self.to_turns_signed() * 2.0
189+
}
190+
151191
/// Creates an angle from a value in radians.
152192
///
153193
/// # Panics
@@ -668,6 +708,35 @@ mod tests {
668708
use rand::RngExt;
669709
use std::f64::consts::{FRAC_PI_2, FRAC_PI_4, PI, TAU};
670710

711+
#[test]
712+
fn test_to_turns_and_half_turns() {
713+
// Quarter turn = pi/2 rad = 0.25 turns = 0.5 half-turns.
714+
let q = Angle64::QUARTER_TURN;
715+
assert!((q.to_turns() - 0.25).abs() < 1e-12);
716+
assert!((q.to_half_turns() - 0.5).abs() < 1e-12);
717+
assert!((q.to_turns_signed() - 0.25).abs() < 1e-12);
718+
assert!((q.to_half_turns_signed() - 0.5).abs() < 1e-12);
719+
720+
// Half-turn boundary: to_turns stays unsigned (0.5), signed maps to 0.5.
721+
let h = Angle64::HALF_TURN;
722+
assert!((h.to_turns() - 0.5).abs() < 1e-12);
723+
assert!((h.to_half_turns() - 1.0).abs() < 1e-12);
724+
725+
// Three-quarter turn: unsigned 0.75 turns; signed wraps to -0.25 turns
726+
// (-0.5 half-turns), mirroring to_radians_signed.
727+
let tq = Angle64::THREE_QUARTERS_TURN;
728+
assert!((tq.to_turns() - 0.75).abs() < 1e-12);
729+
assert!((tq.to_turns_signed() - (-0.25)).abs() < 1e-12);
730+
assert!((tq.to_half_turns_signed() - (-0.5)).abs() < 1e-12);
731+
732+
// half-turns == radians / pi for the unsigned form.
733+
let a = Angle64::from_radians(0.7);
734+
assert!((a.to_half_turns() - a.to_radians() / PI).abs() < 1e-9);
735+
// round-trip turns.
736+
let b = Angle64::from_turns(0.3);
737+
assert!((b.to_turns() - 0.3).abs() < 1e-9);
738+
}
739+
671740
// Basic Construction and Properties
672741
#[test]
673742
fn test_constructors() {

crates/pecos-core/src/gate_type.rs

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -97,12 +97,12 @@ pub enum GateType {
9797
// TODO: MPauli instead of the other variants?
9898

9999
// PX = 130
100-
// PnX = 131
100+
// PNX = 131
101101
// PY = 132
102-
// PnY = 133
102+
// PNY = 133
103103
// PZ = 134
104104
PZ = 134,
105-
// PnZ
105+
// PNZ
106106
/// Allocate a qubit in the |0⟩ state
107107
QAlloc = 135,
108108
/// Free/deallocate a qubit

crates/pecos-llvm/src/llvm_compat.rs

Lines changed: 147 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -226,7 +226,7 @@ impl<'ctx> LLFunctionType<'ctx> {
226226
}
227227

228228
/// Wrapper for LLVM types that mirrors llvmlite's type hierarchy
229-
#[derive(Clone, Copy)]
229+
#[derive(Clone, Copy, PartialEq, Eq)]
230230
pub enum LLType<'ctx> {
231231
Void,
232232
Int(IntType<'ctx>),
@@ -236,6 +236,38 @@ pub enum LLType<'ctx> {
236236
Array(ArrayType<'ctx>),
237237
}
238238

239+
// inkwell 0.8.0 only derives `Hash` for `IntType`; the other type wrappers
240+
// are `Eq` (LLVM type-ref pointer equality) but not `Hash`. Hash the same
241+
// `LLVMTypeRef` pointer so `Hash` stays consistent with that `Eq`.
242+
impl std::hash::Hash for LLType<'_> {
243+
fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
244+
use inkwell::types::AsTypeRef;
245+
match self {
246+
LLType::Void => 0u8.hash(state),
247+
LLType::Int(t) => {
248+
1u8.hash(state);
249+
(t.as_type_ref() as usize).hash(state);
250+
}
251+
LLType::Float(t) => {
252+
2u8.hash(state);
253+
(t.as_type_ref() as usize).hash(state);
254+
}
255+
LLType::Pointer(t) => {
256+
3u8.hash(state);
257+
(t.as_type_ref() as usize).hash(state);
258+
}
259+
LLType::Struct(t) => {
260+
4u8.hash(state);
261+
(t.as_type_ref() as usize).hash(state);
262+
}
263+
LLType::Array(t) => {
264+
5u8.hash(state);
265+
(t.as_type_ref() as usize).hash(state);
266+
}
267+
}
268+
}
269+
}
270+
239271
impl<'ctx> LLType<'ctx> {
240272
/// Create void type
241273
#[must_use]
@@ -717,6 +749,106 @@ impl<'ctx> LLIRBuilder<'ctx> {
717749
Ok(LLValue::Pointer(result))
718750
}
719751
}
752+
753+
// ========================================================================
754+
// Memory ops + casts (unblocks the standard CReg model)
755+
// ========================================================================
756+
757+
/// `alloca <ty>` -- stack slot. Caller positions the builder (B2
758+
/// places `CReg` buffers in the entry block via `position_at_end`).
759+
pub fn alloca(&self, ll_type: LLType<'ctx>, name: &str) -> LLResult<LLValue<'ctx>> {
760+
let basic_ty = ll_type
761+
.to_basic_metadata_type()
762+
.ok_or_else(|| PecosError::Generic("Cannot alloca a void type".into()))?;
763+
let result = self
764+
.builder
765+
.build_alloca(basic_ty, name)
766+
.map_err(|e| PecosError::Generic(format!("Failed to build alloca: {e}")))?;
767+
Ok(LLValue::Pointer(result))
768+
}
769+
770+
/// `load` (LLVM-14 typed pointer: pointee inferred from `ptr`).
771+
pub fn load(&self, ptr: LLValue<'ctx>, name: &str) -> LLResult<LLValue<'ctx>> {
772+
let result = self
773+
.builder
774+
.build_load(ptr.as_pointer_value(), name)
775+
.map_err(|e| PecosError::Generic(format!("Failed to build load: {e}")))?;
776+
Ok(match result {
777+
BasicValueEnum::IntValue(v) => LLValue::Int(v),
778+
BasicValueEnum::FloatValue(v) => LLValue::Float(v),
779+
BasicValueEnum::PointerValue(v) => LLValue::Pointer(v),
780+
BasicValueEnum::ArrayValue(v) => LLValue::Array(v),
781+
other => {
782+
return Err(PecosError::Generic(format!(
783+
"load: unsupported loaded value type: {other:?}"
784+
)));
785+
}
786+
})
787+
}
788+
789+
/// `store` -- discards inkwell's returned pointer (Python `-> None`).
790+
pub fn store(&self, ptr: LLValue<'ctx>, value: LLValue<'ctx>) -> LLResult<()> {
791+
self.builder
792+
.build_store(ptr.as_pointer_value(), value.to_basic_value())
793+
.map_err(|e| PecosError::Generic(format!("Failed to build store: {e}")))?;
794+
Ok(())
795+
}
796+
797+
/// `zext` int value to a wider int type.
798+
pub fn zext(
799+
&self,
800+
value: LLValue<'ctx>,
801+
dest_type: LLType<'ctx>,
802+
name: &str,
803+
) -> LLResult<LLValue<'ctx>> {
804+
let result = self
805+
.builder
806+
.build_int_z_extend(value.as_int_value(), dest_type.as_int_type(), name)
807+
.map_err(|e| PecosError::Generic(format!("Failed to build zext: {e}")))?;
808+
Ok(LLValue::Int(result))
809+
}
810+
811+
/// `trunc` int value to a narrower int type.
812+
pub fn trunc(
813+
&self,
814+
value: LLValue<'ctx>,
815+
dest_type: LLType<'ctx>,
816+
name: &str,
817+
) -> LLResult<LLValue<'ctx>> {
818+
let result = self
819+
.builder
820+
.build_int_truncate(value.as_int_value(), dest_type.as_int_type(), name)
821+
.map_err(|e| PecosError::Generic(format!("Failed to build trunc: {e}")))?;
822+
Ok(LLValue::Int(result))
823+
}
824+
825+
/// Unsigned integer comparison (mirrors `icmp_signed` with U-predicates).
826+
pub fn icmp_unsigned(
827+
&self,
828+
op: &str,
829+
lhs: LLValue<'ctx>,
830+
rhs: LLValue<'ctx>,
831+
name: &str,
832+
) -> LLResult<LLValue<'ctx>> {
833+
let predicate = match op {
834+
"==" => IntPredicate::EQ,
835+
"!=" => IntPredicate::NE,
836+
"<" => IntPredicate::ULT,
837+
">" => IntPredicate::UGT,
838+
"<=" => IntPredicate::ULE,
839+
">=" => IntPredicate::UGE,
840+
_ => {
841+
return Err(PecosError::Generic(format!(
842+
"Unknown comparison operator: {op}"
843+
)));
844+
}
845+
};
846+
let result = self
847+
.builder
848+
.build_int_compare(predicate, lhs.as_int_value(), rhs.as_int_value(), name)
849+
.map_err(|e| PecosError::Generic(format!("Failed to build icmp: {e}")))?;
850+
Ok(LLValue::Int(result))
851+
}
720852
}
721853

722854
// ============================================================================
@@ -766,4 +898,18 @@ impl LLConstant {
766898
)),
767899
}
768900
}
901+
902+
/// Zero/`zeroinitializer` constant of `ll_type` (backs
903+
/// `Constant(ty, None)`; Array -> `zeroinitializer`, Int -> `iN 0`).
904+
pub fn zero(ll_type: LLType<'_>) -> LLResult<LLValue<'_>> {
905+
match ll_type {
906+
LLType::Int(t) => Ok(LLValue::Int(t.const_zero())),
907+
LLType::Float(t) => Ok(LLValue::Float(t.const_zero())),
908+
LLType::Pointer(t) => Ok(LLValue::Pointer(t.const_zero())),
909+
LLType::Array(t) => Ok(LLValue::Array(t.const_zero())),
910+
LLType::Void | LLType::Struct(_) => Err(PecosError::Generic(
911+
"Cannot create a zero constant for void/struct type".to_string(),
912+
)),
913+
}
914+
}
769915
}

crates/pecos-simulators/src/arbitrary_rotation_gateable.rs

Lines changed: 70 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -254,6 +254,76 @@ pub trait ArbitraryRotationGateable: CliffordGateable {
254254
self.rxx(theta, pairs).ryy(phi, pairs).rzz(lambda, pairs)
255255
}
256256

257+
/// Applies a controlled-RZ rotation: target qubit gets RZ(theta) when control = |1>.
258+
///
259+
/// `CRZ(theta) = block-diag(I, RZ(theta)) = diag(1, 1, exp(-i*theta/2), exp(i*theta/2))`.
260+
///
261+
/// Default 2q-minimal decomposition (1 RZZ + 1 single-qubit RZ on the
262+
/// target): `CRZ(theta) = (I o RZ(theta/2)) . RZZ(-theta/2)`.
263+
/// Verified: with the trait's `RZ = exp(-i*theta/2*Z)` and `RZZ =
264+
/// exp(-i*theta/2*Z*Z)` conventions, the product on the c=0 sector
265+
/// gives `RZ(theta/2) . exp(i*theta/4*I) = I` up to global phase, and
266+
/// on c=1 (where ZZ acts as -Z on target) gives `RZ(theta/2) . X .
267+
/// RZ(theta/2) . X = RZ(theta)` -- i.e. the convention-1 controlled
268+
/// rotation. The non-PECOS-prefactor convention requires no extra
269+
/// RZ on the control.
270+
///
271+
/// # Parameters
272+
/// - `theta`: The rotation angle on the target.
273+
/// - `pairs`: Pairs of qubit indices `[(control, target), ...]`.
274+
///
275+
/// # Returns
276+
/// A mutable reference to `Self` for method chaining.
277+
#[inline]
278+
fn crz(&mut self, theta: Angle64, pairs: &[(QubitId, QubitId)]) -> &mut Self {
279+
// Half-angle first, THEN negate -- `Angle<T>` is a wrapping fraction
280+
// of a full turn (modulo 2pi), so `-theta / 2` would halve the wrapped
281+
// 2*pi - theta and produce pi - theta/2, not -theta/2.
282+
let half = theta / 2u64;
283+
let targets: QubitBuf = pairs.iter().map(|&(_, t)| t).collect();
284+
self.rzz(-half, pairs).rz(half, &targets)
285+
}
286+
287+
/// Applies a controlled-RX rotation: target qubit gets RX(theta) when control = |1>.
288+
///
289+
/// Default decomposition: `CRX(theta) = (I o H) . CRZ(theta) . (I o H)`,
290+
/// using `H.Z.H = X` so the c=1 sector applies `H.RZ(theta).H = RX(theta)`.
291+
/// Same 2q cost as `crz` (1 RZZ).
292+
///
293+
/// # Parameters
294+
/// - `theta`: The rotation angle on the target.
295+
/// - `pairs`: Pairs of qubit indices `[(control, target), ...]`.
296+
///
297+
/// # Returns
298+
/// A mutable reference to `Self` for method chaining.
299+
#[inline]
300+
fn crx(&mut self, theta: Angle64, pairs: &[(QubitId, QubitId)]) -> &mut Self {
301+
let targets: QubitBuf = pairs.iter().map(|&(_, t)| t).collect();
302+
self.h(&targets).crz(theta, pairs).h(&targets)
303+
}
304+
305+
/// Applies a controlled-RY rotation: target qubit gets RY(theta) when control = |1>.
306+
///
307+
/// Default decomposition: `CRY(theta) = (I o S.H) . CRZ(theta) . (I o H.Sdg)`,
308+
/// using `S.X.Sdg = Y` (so `S.Rx.Sdg = Ry`) and `H.Rz.H = Rx`, giving
309+
/// `S.H.RZ.H.Sdg = RY`. Same 2q cost as `crz` (1 RZZ).
310+
///
311+
/// # Parameters
312+
/// - `theta`: The rotation angle on the target.
313+
/// - `pairs`: Pairs of qubit indices `[(control, target), ...]`.
314+
///
315+
/// # Returns
316+
/// A mutable reference to `Self` for method chaining.
317+
#[inline]
318+
fn cry(&mut self, theta: Angle64, pairs: &[(QubitId, QubitId)]) -> &mut Self {
319+
let targets: QubitBuf = pairs.iter().map(|&(_, t)| t).collect();
320+
self.szdg(&targets)
321+
.h(&targets)
322+
.crz(theta, pairs)
323+
.h(&targets)
324+
.sz(&targets)
325+
}
326+
257327
/// Applies a general 2-qubit unitary via KAK decomposition:
258328
/// U = (U3(before[0]) x U3(before[1])) * RXXRYYRZZ(interaction) * (U3(after[0]) x U3(after[1]))
259329
///

0 commit comments

Comments
 (0)