From b0f0c8bbdf668f4b4e5b35cb43308a9caf44377e Mon Sep 17 00:00:00 2001 From: ciaranra Date: Tue, 7 Jul 2026 14:49:12 -0600 Subject: [PATCH 1/3] Run generated docs examples in docs CI --- .github/workflows/test-docs-examples.yml | 17 +++-------------- 1 file changed, 3 insertions(+), 14 deletions(-) diff --git a/.github/workflows/test-docs-examples.yml b/.github/workflows/test-docs-examples.yml index fc1b33b20..70fa998be 100644 --- a/.github/workflows/test-docs-examples.yml +++ b/.github/workflows/test-docs-examples.yml @@ -98,24 +98,13 @@ jobs: path: ~/.pecos/deps/llvm-21.1 key: ${{ steps.cache-llvm.outputs.cache-primary-key }} - - name: Install dependencies and build (post-merge) - if: github.event_name != 'pull_request' + - name: Install dependencies and build run: | uv lock --check --project . just python-ci-build-docs - - name: Install docs dependencies (PR fast path) - if: github.event_name == 'pull_request' - run: | - uv lock --check --project . - uv sync --locked \ - --group dev \ - --group test \ - --no-install-workspace - - - name: Test working documentation examples - if: github.event_name != 'pull_request' - run: uv run --frozen python scripts/docs/test_working_examples.py + - name: Test documentation examples + run: just docs-test - name: Build documentation if: success() From 9a8bb043119080ee64ca7b96b384d72c247a7111 Mon Sep 17 00:00:00 2001 From: ciaranra Date: Tue, 7 Jul 2026 15:30:18 -0600 Subject: [PATCH 2/3] Fix generated docs example failures --- Justfile | 3 ++- docs/user-guide/qec-guppy.md | 5 +---- docs/user-guide/simulators.md | 34 +++++++++++++++++++--------------- 3 files changed, 22 insertions(+), 20 deletions(-) diff --git a/Justfile b/Justfile index cae5c997e..242bb089a 100644 --- a/Justfile +++ b/Justfile @@ -222,6 +222,7 @@ python-ci-build-docs profile="debug": _msvc-bootstrap (validate-profile "python- set -euo pipefail PROFILE="{{profile}}" PECOS_BUILD_MWPF=0 {{pecos}} python build --profile "$PROFILE" --no-cuda + uv run --frozen --package pecos-rslib-exp maturin develop --uv --locked --manifest-path python/pecos-rslib-exp/Cargo.toml # Build the extra experimental bindings exercised by the fast Python core test lane. [group('build')] @@ -998,7 +999,7 @@ python-ci-sync-test: python-ci-sync-docs: #!/usr/bin/env bash set -euo pipefail - just python-ci-sync + just python-ci-sync-test [group('setup')] python-ci-sync-lint: diff --git a/docs/user-guide/qec-guppy.md b/docs/user-guide/qec-guppy.md index 468346a65..def765ca7 100644 --- a/docs/user-guide/qec-guppy.md +++ b/docs/user-guide/qec-guppy.md @@ -95,7 +95,6 @@ The 4.8.8 triangular color code supports transversal Clifford gates. ### Quick Start ```python -from pecos import sim, state_vector from pecos.guppy import make_color_code, get_num_qubits_color # Create a distance-3 color code memory experiment @@ -103,9 +102,7 @@ prog = make_color_code(distance=3, num_rounds=2, basis="Z") # Get qubit count num_qubits = get_num_qubits_color(3) - -# Run simulation -results = sim(prog).qubits(num_qubits).quantum(state_vector()).seed(42).run(100) +print(f"Color code d=3 uses {num_qubits} qubits") ``` ### Comparing Surface and Color Codes diff --git a/docs/user-guide/simulators.md b/docs/user-guide/simulators.md index cb7d0c386..9a0c6741b 100644 --- a/docs/user-guide/simulators.md +++ b/docs/user-guide/simulators.md @@ -131,9 +131,9 @@ The default simulator, optimized for QEC workloads with sparse stabilizer tablea results = sim(Qasm(circuit)).run(1000) # Or explicitly select it - from pecos.simulators import SparseStab + from pecos.simulators import sparse_stab - results = sim(Qasm(circuit)).quantum(SparseStab).run(1000) + results = sim(Qasm(circuit)).quantum(sparse_stab()).run(1000) ``` === ":fontawesome-brands-rust: Rust" @@ -165,7 +165,10 @@ Pure Python reference implementation—useful for learning and debugging but slo ```python from pecos.simulators import SparseStabPy -results = sim(Qasm(circuit)).quantum(SparseStabPy).run(100) +state = SparseStabPy(num_qubits=2) +state.run_gate("H", {0}) +state.run_gate("CNOT", {(0, 1)}) +measurement = state.run_gate("Measure", {0}) ``` ### Stabilizer @@ -173,9 +176,9 @@ results = sim(Qasm(circuit)).quantum(SparseStabPy).run(100) Dense Rust stabilizer backend for Clifford circuits. ```python -from pecos.simulators import Stabilizer +from pecos.simulators import stabilizer -results = sim(Qasm(circuit)).quantum(Stabilizer).run(100) +results = sim(Qasm(circuit)).quantum(stabilizer()).run(100) ``` **Strengths:** @@ -199,9 +202,9 @@ Pure Rust state vector implementation. === ":fontawesome-brands-python: Python" ```python - from pecos.simulators import StateVec + from pecos.simulators import state_vector - results = sim(Qasm(circuit)).quantum(StateVec).run(100) + results = sim(Qasm(circuit)).quantum(state_vector()).run(100) ``` === ":fontawesome-brands-rust: Rust" @@ -222,9 +225,9 @@ Pure Rust state vector implementation. Rust backend specialized for Clifford circuits plus Z-axis rotations. ```python -from pecos.simulators import StabVec +from pecos.simulators import stab_vec -results = sim(Qasm(circuit)).quantum(StabVec).run(100) +results = sim(Qasm(circuit)).quantum(stab_vec()).run(100) ``` **Strengths:** @@ -287,7 +290,7 @@ Density matrix simulators represent mixed quantum states, enabling simulation of ```python from pecos.simulators import density_matrix -results = sim(Qasm(circuit)).quantum(density_matrix).run(100) +results = sim(Qasm(circuit)).quantum(density_matrix()).run(100) ``` **Use cases:** @@ -347,10 +350,10 @@ Returns random measurement results, ignoring all gates. Useful for testing. === ":fontawesome-brands-python: Python" ```python - from pecos.simulators import CoinToss + from pecos.simulators import coin_toss # Test classical logic with random quantum outcomes - results = sim(Qasm(circuit)).quantum(CoinToss).run(1000) + results = sim(Qasm(circuit)).quantum(coin_toss()).run(1000) ``` === ":fontawesome-brands-rust: Rust" @@ -395,7 +398,7 @@ The `sim()` API lets you switch simulators easily: ```python from pecos import sim, Qasm - from pecos.simulators import SparseStab, StateVec, Stabilizer + from pecos.simulators import sparse_stab, state_vector, stabilizer circuit = Qasm( """ @@ -413,8 +416,9 @@ The `sim()` API lets you switch simulators easily: results = sim(circuit).run(1000) # Explicit simulator selection - results = sim(circuit).quantum(StateVec).run(1000) - results = sim(circuit).quantum(Stabilizer).run(1000) + results = sim(circuit).quantum(sparse_stab()).run(1000) + results = sim(circuit).quantum(state_vector()).run(1000) + results = sim(circuit).quantum(stabilizer()).run(1000) ``` === ":fontawesome-brands-rust: Rust" From a6e90592ed073965e66c18dc31e66eb40ceadad6 Mon Sep 17 00:00:00 2001 From: ciaranra Date: Tue, 7 Jul 2026 17:21:50 -0600 Subject: [PATCH 3/3] Harden LLVM setup for lint workflow --- Justfile | 16 +++- crates/pecos-build/src/cargo_config.rs | 40 ++++++++ crates/pecos-build/src/llvm/config.rs | 121 ++++++++++++++++++++++++- crates/pecos-cli/src/cli/llvm_cmd.rs | 12 ++- crates/pecos-cli/src/main.rs | 13 +++ 5 files changed, 192 insertions(+), 10 deletions(-) diff --git a/Justfile b/Justfile index 242bb089a..855f0abbf 100644 --- a/Justfile +++ b/Justfile @@ -301,9 +301,10 @@ test mode="release": (validate-test-mode "test" mode) (rstest mode) pytest # Fix formatting and linting issues (or: just lint check) [group('lint')] -lint mode="fix": _msvc-bootstrap (validate-lint-mode mode) python-workspace-check +lint mode="fix": _msvc-bootstrap (validate-lint-mode mode) ensure-local-build-env python-workspace-check #!/usr/bin/env bash set -euo pipefail + eval "$({{pecos}} env)" MODE="{{mode}}" # Detect CUDA: only use --all-features when CUDA toolkit is available if command -v nvcc >/dev/null 2>&1 || [ -n "${CUDA_PATH:-}" ] || [ -d /usr/local/cuda ]; then @@ -350,9 +351,10 @@ lint mode="fix": _msvc-bootstrap (validate-lint-mode mode) python-workspace-chec # Fast lint lane for Python PR CI. Keep this scoped to Rust + Python checks so # the Python critical path does not opportunistically pick up Julia/Go tools. [group('lint')] -python-ci-lint: _msvc-bootstrap python-workspace-check +python-ci-lint: _msvc-bootstrap ensure-local-build-env python-workspace-check #!/usr/bin/env bash set -euo pipefail + eval "$({{pecos}} env)" if command -v nvcc >/dev/null 2>&1 || [ -n "${CUDA_PATH:-}" ] || [ -d /usr/local/cuda ]; then CLIPPY_FEATURES="--all-features" echo "(CUDA detected -- linting with all features)" @@ -372,7 +374,10 @@ python-ci-lint: _msvc-bootstrap python-workspace-check # Run cargo check [group('lint')] -check: _msvc-bootstrap +check: _msvc-bootstrap ensure-local-build-env + #!/usr/bin/env bash + set -euo pipefail + eval "$({{pecos}} env)" cargo check --locked --workspace --all-targets # Check Python workspace metadata @@ -382,9 +387,10 @@ python-workspace-check: # Run cargo clippy (CUDA-aware: uses --all-features only when CUDA is available) [group('lint')] -clippy: _msvc-bootstrap +clippy: _msvc-bootstrap ensure-local-build-env #!/usr/bin/env bash set -euo pipefail + eval "$({{pecos}} env)" if command -v nvcc >/dev/null 2>&1 || [ -n "${CUDA_PATH:-}" ] || [ -d /usr/local/cuda ]; then echo "(CUDA detected -- clippy with all features)" cargo clippy --locked --workspace --all-targets --all-features -- -D warnings @@ -396,6 +402,8 @@ clippy: _msvc-bootstrap # Check Rust formatting [group('lint')] fmt: + #!/usr/bin/env bash + set -euo pipefail cargo fmt --all -- --check # Run benchmarks (profile: release/native; features: optional; pattern: filter) diff --git a/crates/pecos-build/src/cargo_config.rs b/crates/pecos-build/src/cargo_config.rs index 5a1328b65..f21160499 100644 --- a/crates/pecos-build/src/cargo_config.rs +++ b/crates/pecos-build/src/cargo_config.rs @@ -82,6 +82,26 @@ impl CargoConfig { Ok(self) } + /// Remove `[env]` entries matching the predicate. Returns the removed keys. + /// + /// # Errors + /// Returns an error if `[env]` exists but is not a table. + pub fn remove_env_matching(&mut self, mut should_remove: F) -> Result> + where + F: FnMut(&str) -> bool, + { + let env = Self::table_mut(&mut self.doc, "env")?; + let keys: Vec = env + .iter() + .filter(|(key, _)| should_remove(key)) + .map(|(key, _)| key.to_string()) + .collect(); + for key in &keys { + env.remove(key); + } + Ok(keys) + } + /// Set `[target.].linker`. /// /// # Errors @@ -241,4 +261,24 @@ mod tests { "re-applying the same value must not rewrite the file" ); } + + #[test] + fn removes_matching_env_entries() { + let tmp = tempfile::tempdir().unwrap(); + let mut cfg = CargoConfig::open(tmp.path()).unwrap(); + cfg.set_env("LLVM_SYS_140_PREFIX", "/old", true).unwrap(); + cfg.set_env("LLVM_SYS_211_PREFIX", "/llvm", true).unwrap(); + cfg.set_env("FOO", "bar", false).unwrap(); + + let removed = cfg + .remove_env_matching(|key| key.starts_with("LLVM_SYS_") && key != "LLVM_SYS_211_PREFIX") + .unwrap(); + assert_eq!(removed, vec!["LLVM_SYS_140_PREFIX"]); + cfg.save().unwrap(); + + let parsed: toml::Value = toml::from_str(&read(tmp.path())).unwrap(); + assert!(parsed["env"].get("LLVM_SYS_140_PREFIX").is_none()); + assert!(parsed["env"].get("LLVM_SYS_211_PREFIX").is_some()); + assert_eq!(parsed["env"]["FOO"].as_str().unwrap(), "bar"); + } } diff --git a/crates/pecos-build/src/llvm/config.rs b/crates/pecos-build/src/llvm/config.rs index 1756b57f5..69b1b159e 100644 --- a/crates/pecos-build/src/llvm/config.rs +++ b/crates/pecos-build/src/llvm/config.rs @@ -13,6 +13,8 @@ use std::path::{Path, PathBuf}; pub struct ConfigValidation { /// Path configured in .cargo/config.toml (if any) pub configured_path: Option, + /// Stale versioned LLVM env keys that should not be present. + pub stale_llvm_prefix_envs: Vec, /// Whether the configured path exists pub path_exists: bool, /// Whether the configured path is the required LLVM version @@ -27,7 +29,10 @@ impl ConfigValidation { /// Check if the configuration is healthy #[must_use] pub fn is_healthy(&self) -> bool { - self.configured_path.is_some() && self.path_exists && self.path_is_valid_llvm + self.configured_path.is_some() + && self.stale_llvm_prefix_envs.is_empty() + && self.path_exists + && self.path_is_valid_llvm } /// Print validation warnings if there are issues @@ -36,6 +41,17 @@ impl ConfigValidation { let cmd = get_pecos_command(); if let Some(ref configured) = self.configured_path { + if !self.stale_llvm_prefix_envs.is_empty() { + eprintln!(); + eprintln!( + "Warning: .cargo/config.toml contains stale LLVM config: {}", + self.stale_llvm_prefix_envs.join(", ") + ); + eprintln!(); + eprintln!("To fix this:"); + eprintln!(" {cmd} llvm configure"); + } + if !self.path_exists { eprintln!(); eprintln!( @@ -121,12 +137,58 @@ pub fn read_configured_llvm_path() -> Option { None } +fn is_versioned_llvm_prefix_env(key: &str) -> bool { + let Some(version) = key + .strip_prefix("LLVM_SYS_") + .and_then(|rest| rest.strip_suffix("_PREFIX")) + else { + return false; + }; + !version.is_empty() && version.bytes().all(|b| b.is_ascii_digit()) +} + +fn is_stale_llvm_prefix_env(key: &str) -> bool { + is_versioned_llvm_prefix_env(key) && key != LLVM_SYS_PREFIX_ENV +} + +/// Read stale versioned LLVM prefix keys from `.cargo/config.toml`. +/// +/// PECOS uses `LLVM_SYS_211_PREFIX`. Older entries such as +/// `LLVM_SYS_140_PREFIX` can still affect transitive build scripts when Cargo +/// exports forced `[env]` values, so they should be removed. +#[must_use] +pub fn read_stale_llvm_prefix_envs(project_root: &Path) -> Vec { + let config_path = project_root.join(".cargo").join("config.toml"); + let Ok(content) = fs::read_to_string(&config_path) else { + return Vec::new(); + }; + let Ok(table) = content.parse::() else { + return Vec::new(); + }; + let Some(env) = table.get("env").and_then(|env| env.as_table()) else { + return Vec::new(); + }; + + let mut stale: Vec = env + .keys() + .filter(|key| is_stale_llvm_prefix_env(key)) + .cloned() + .collect(); + stale.sort(); + stale +} + /// Validate the current LLVM configuration #[must_use] pub fn validate_llvm_config() -> ConfigValidation { + let project_root = find_cargo_project_root(); let configured_path = read_configured_llvm_path(); let repo_root = get_repo_root_from_manifest(); let detected_path = find_llvm(repo_root); + let stale_llvm_prefix_envs = project_root + .as_deref() + .map(read_stale_llvm_prefix_envs) + .unwrap_or_default(); let (path_exists, path_is_valid_llvm) = if let Some(ref path) = configured_path { (path.exists(), is_valid_llvm(path)) @@ -142,6 +204,7 @@ pub fn validate_llvm_config() -> ConfigValidation { ConfigValidation { configured_path, + stale_llvm_prefix_envs, path_exists, path_is_valid_llvm, detected_path, @@ -239,7 +302,63 @@ pub fn write_cargo_config(project_root: &Path, llvm_path: &Path, force: bool) -> // Forward slashes keep the value backslash-escape-free in TOML. let llvm_path_str = path_to_env_string(llvm_path); let mut cfg = crate::cargo_config::CargoConfig::open(project_root)?; + cfg.remove_env_matching(is_stale_llvm_prefix_env)?; cfg.set_env(LLVM_SYS_PREFIX_ENV, &llvm_path_str, force)?; cfg.save()?; Ok(()) } + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn detects_stale_llvm_prefix_envs() { + let tmp = tempfile::tempdir().unwrap(); + let cargo_dir = tmp.path().join(".cargo"); + fs::create_dir_all(&cargo_dir).unwrap(); + fs::write( + cargo_dir.join("config.toml"), + r#" +[env] +LLVM_SYS_140_PREFIX = { value = "/old", force = true } +LLVM_SYS_211_PREFIX = { value = "/current", force = true } +LLVM_SYS_PREFIX = "/not-versioned" +FOO = "bar" +"#, + ) + .unwrap(); + + assert_eq!( + read_stale_llvm_prefix_envs(tmp.path()), + vec!["LLVM_SYS_140_PREFIX"] + ); + } + + #[test] + fn write_cargo_config_removes_stale_llvm_prefix_envs() { + let tmp = tempfile::tempdir().unwrap(); + let cargo_dir = tmp.path().join(".cargo"); + fs::create_dir_all(&cargo_dir).unwrap(); + fs::write( + cargo_dir.join("config.toml"), + r#" +[env] +LLVM_SYS_140_PREFIX = { value = "/old", force = true } +FOO = "bar" +"#, + ) + .unwrap(); + + write_cargo_config(tmp.path(), Path::new("/llvm"), true).unwrap(); + + let text = fs::read_to_string(cargo_dir.join("config.toml")).unwrap(); + let parsed: toml::Value = toml::from_str(&text).unwrap(); + assert!(parsed["env"].get("LLVM_SYS_140_PREFIX").is_none()); + assert_eq!( + parsed["env"]["LLVM_SYS_211_PREFIX"]["value"], + "/llvm".into() + ); + assert_eq!(parsed["env"]["FOO"].as_str().unwrap(), "bar"); + } +} diff --git a/crates/pecos-cli/src/cli/llvm_cmd.rs b/crates/pecos-cli/src/cli/llvm_cmd.rs index b46bf4b49..310f555e1 100644 --- a/crates/pecos-cli/src/cli/llvm_cmd.rs +++ b/crates/pecos-cli/src/cli/llvm_cmd.rs @@ -39,6 +39,7 @@ pub fn run(command: LlvmCommands) -> Result<()> { fn run_check(quiet: bool) { let repo_root = get_repo_root_from_manifest(); if let Some(llvm_path) = find_configured_or_detected_llvm(repo_root) { + let validation = validate_llvm_config(); if !quiet { println!("LLVM 21.1 found at: {}", llvm_path.display()); if let Ok(version) = get_llvm_version(&llvm_path) { @@ -47,13 +48,14 @@ fn run_check(quiet: bool) { print_link_info(&llvm_path); // Validate configuration - let validation = validate_llvm_config(); validation.print_warnings(); + } - // Exit with error if config is unhealthy (would cause build failures) - if !validation.is_healthy() && validation.configured_path.is_some() { - std::process::exit(1); - } + // Exit with error if config is unhealthy (would cause build failures). + // Keep this outside the quiet branch so `pecos llvm check >/dev/null` + // still works as a preflight gate. + if !validation.is_healthy() && validation.configured_path.is_some() { + std::process::exit(1); } } else { if !quiet { diff --git a/crates/pecos-cli/src/main.rs b/crates/pecos-cli/src/main.rs index ac5f7dd94..d2c252643 100644 --- a/crates/pecos-cli/src/main.rs +++ b/crates/pecos-cli/src/main.rs @@ -978,6 +978,19 @@ fn run_doctor() { } } + if llvm_config.stale_llvm_prefix_envs.is_empty() { + print_check(".cargo/config.toml stale LLVM keys", true, "none"); + } else { + print_check( + ".cargo/config.toml stale LLVM keys", + false, + &llvm_config.stale_llvm_prefix_envs.join(", "), + ); + problems.push( + "Stale LLVM settings found in .cargo/config.toml. Run: pecos llvm configure".into(), + ); + } + if let Ok(env_val) = std::env::var(LLVM_SYS_PREFIX_ENV) { let env_path = std::path::Path::new(&env_val); if env_path.exists() {