Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 3 additions & 14 deletions .github/workflows/test-docs-examples.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand Down
19 changes: 14 additions & 5 deletions Justfile
Original file line number Diff line number Diff line change
Expand Up @@ -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')]
Expand Down Expand Up @@ -300,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
Expand Down Expand Up @@ -349,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)"
Expand All @@ -371,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
Expand All @@ -381,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
Expand All @@ -395,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)
Expand Down Expand Up @@ -998,7 +1007,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:
Expand Down
40 changes: 40 additions & 0 deletions crates/pecos-build/src/cargo_config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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<F>(&mut self, mut should_remove: F) -> Result<Vec<String>>
where
F: FnMut(&str) -> bool,
{
let env = Self::table_mut(&mut self.doc, "env")?;
let keys: Vec<String> = env
.iter()
.filter(|(key, _)| should_remove(key))
.map(|(key, _)| key.to_string())
.collect();
for key in &keys {
env.remove(key);
}
Ok(keys)
}

/// Set `[target.<triple>].linker`.
///
/// # Errors
Expand Down Expand Up @@ -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");
}
}
121 changes: 120 additions & 1 deletion crates/pecos-build/src/llvm/config.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@ use std::path::{Path, PathBuf};
pub struct ConfigValidation {
/// Path configured in .cargo/config.toml (if any)
pub configured_path: Option<PathBuf>,
/// Stale versioned LLVM env keys that should not be present.
pub stale_llvm_prefix_envs: Vec<String>,
/// Whether the configured path exists
pub path_exists: bool,
/// Whether the configured path is the required LLVM version
Expand All @@ -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
Expand All @@ -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!(
Expand Down Expand Up @@ -121,12 +137,58 @@ pub fn read_configured_llvm_path() -> Option<PathBuf> {
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<String> {
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::<toml::Table>() else {
return Vec::new();
};
let Some(env) = table.get("env").and_then(|env| env.as_table()) else {
return Vec::new();
};

let mut stale: Vec<String> = 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))
Expand All @@ -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,
Expand Down Expand Up @@ -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");
}
}
12 changes: 7 additions & 5 deletions crates/pecos-cli/src/cli/llvm_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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) {
Expand All @@ -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 {
Expand Down
13 changes: 13 additions & 0 deletions crates/pecos-cli/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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() {
Expand Down
5 changes: 1 addition & 4 deletions docs/user-guide/qec-guppy.md
Original file line number Diff line number Diff line change
Expand Up @@ -95,17 +95,14 @@ 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
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
Expand Down
Loading
Loading