Skip to content

Commit cc55f45

Browse files
authored
rust phir classical interp (#284)
* Add Rust PhirClassicalInterpreter as drop-in replacement for Python version * Add FFCall bridge, Angle64 name resolver, typed results, cvar_export support * Fix signed integer binary string formatting to match Python behavior * Fix multi-assign eval order, optional size, multi-qubit arg format, signed binary strings * Add unsigned size masking and optional size inference from data type * Add cinterp='rust' option to HybridEngine, fix dtype name mapping * Add passthrough iterator for inner interpreter, cache Python class lookups * Add run_phir_sim for full-Rust execution path (3.6x speedup), auto-detect in HybridEngine * Remove Result cop requirement, fix bit string widths, filter internal vars from results * Add WASM and depolarizing noise support to run_phir_sim full Rust path * Fix cross-engine test to compare values not Data types * Support Rust and Python noise models in run_phir_sim via build_noise_model * Add ArgItem::UInteger for u64 max values, fix unsigned ResultValue * Replace TypedValue storage with BitUInt-based BitValue in Environment Uses BitUInt from pecos-core for proper arbitrary-width integer storage. Values are automatically masked to declared bit width. Signed values interpreted via two's complement on read. Removes manual mask_to_size workaround. * Fix BitValue sign interpretation to use type width, disable auto fast path * Make Rust classical interpreter the default for HybridEngine * Add pickle support for multiprocessing compatibility * Add foreign_obj getter for protocol parity * Fix signed type storage to use type width (matching Python), improve fuzz to 970/971 * Fix expression evaluation to constrain results to operand type width * Untrack known-issues docs * Track suspected Python PhirClassicalInterpreter bugs found during Rust reimplementation * Remove stale Rust known-issues gitignore entry (issues resolved) * Refine Python suspected bugs: remove fixed/non-bugs, add confidence levels * Note relationship between suspected Python bugs and PECOS#213 * Fix PECOS dtype overflow (related to #213): truncate instead of reject out-of-range values * Mark Python dtype overflow bugs as fixed in suspected bugs doc * Fix ScalarI64 arithmetic overflow panic: use wrapping_add/sub/mul * Add Rust/Python parity test suite with fuzz testing (60 tests) * Fix u64 dtype constructor to accept negative values (wrap to unsigned) * Refactor ExpressionEvaluator to use BitUInt at 64-bit evaluation width All expression evaluation now happens at MIN_EVAL_WIDTH (64 bits), matching the hardware model where everything is i64 under the hood. Variables wider than 64 bits will naturally evaluate at their width. Removes constrain_to_width hack. Fixes comparison between Boolean and integer types. * Use BitUInt arithmetic directly in expression evaluator for arbitrary-width support * Store all types at type width, mask to size on assignment; fix Qubits zero-width panic * Fix Python eval_expr to evaluate at Python int width, mask all types to size on assignment * Add gitignore entry for unstaged QASM-to-PHIR test plan * Add QASM-to-PHIR-JSON converter, validation tests, and classical edge case tests * Default to Python classical interpreter, remove disabled full-Rust path * Fix signed arithmetic, remove dead code, tighten tests * Guard against negative shift amounts, add i64::MIN / -1 regression test * Remove TypedValue, qasm-to-phir-test-plan.md, and gitignore entry
1 parent 65d0c7e commit cc55f45

30 files changed

Lines changed: 5763 additions & 1238 deletions

crates/pecos-phir-json/src/lib.rs

Lines changed: 17 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -604,14 +604,26 @@ mod tests {
604604
"shot counts should match"
605605
);
606606

607-
// Compare shared register values between engines
607+
// Compare shared register values between engines using binary string representation
608+
// (different engines may store values as BitVec vs U32, but the logical values should match)
608609
let mut compared = 0;
609610
for (i, (s1, s2)) in shots1.shots.iter().zip(shots2.shots.iter()).enumerate() {
610-
for (name, val1) in &s1.data {
611-
if let Some(val2) = s2.data.get(name) {
611+
for name in s1.data.keys() {
612+
if name.starts_with('_') {
613+
continue; // Skip metadata keys like _width_*
614+
}
615+
if let (Some(str1), Some(str2)) = (
616+
s1.register_to_binary_string(name),
617+
s2.register_to_binary_string(name),
618+
) {
619+
// Compare values by stripping leading zeros (engines may use different widths)
620+
let v1 = str1.trim_start_matches('0');
621+
let v2 = str2.trim_start_matches('0');
622+
let v1 = if v1.is_empty() { "0" } else { v1 };
623+
let v2 = if v2.is_empty() { "0" } else { v2 };
612624
assert_eq!(
613-
val1, val2,
614-
"shot {i}: register '{name}' differs between engines"
625+
v1, v2,
626+
"shot {i}: register '{name}' differs between engines (str1={str1}, str2={str2})"
615627
);
616628
compared += 1;
617629
}

crates/pecos-phir-json/src/v0_1.rs

Lines changed: 2 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,8 @@
11
pub mod ast;
2+
pub mod classical_interpreter;
23
pub mod engine;
34
pub mod foreign_objects;
5+
pub mod name_resolver;
46
pub mod operations;
57
pub mod phir_converter;
68
pub mod wasm_foreign_object;
@@ -48,22 +50,6 @@ impl PhirImplementation for V0_1 {
4850
)));
4951
}
5052

51-
// Validate that at least one Result command exists
52-
let has_result_command = program.ops.iter().any(|op| {
53-
if let ast::Operation::ClassicalOp { cop, .. } = op {
54-
cop == "Result"
55-
} else {
56-
false
57-
}
58-
});
59-
60-
if !has_result_command {
61-
return Err(PecosError::Input(
62-
"Invalid PHIR-JSON program structure: Program must contain at least one Result command to specify outputs"
63-
.to_string(),
64-
));
65-
}
66-
6753
Ok(program)
6854
}
6955

crates/pecos-phir-json/src/v0_1/ast.rs

Lines changed: 26 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ use std::f64::consts::PI;
77
pub struct PHIRProgram {
88
pub format: String,
99
pub version: String,
10+
#[serde(default)]
1011
pub metadata: BTreeMap<String, serde_json::Value>,
1112
pub ops: Vec<Operation>,
1213
}
@@ -20,7 +21,9 @@ pub enum Operation {
2021
data: String,
2122
data_type: String,
2223
variable: String,
23-
size: usize,
24+
/// Size in bits. Optional -- if omitted, inferred from `data_type`.
25+
#[serde(default)]
26+
size: Option<usize>,
2427
},
2528
/// Quantum operation (gates, measurements)
2629
QuantumOp {
@@ -78,6 +81,11 @@ pub enum Operation {
7881
#[serde(default)]
7982
metadata: Option<BTreeMap<String, serde_json::Value>>,
8083
},
84+
/// Data export (`cvar_export`) -- specifies which variables to export
85+
DataExport {
86+
data: String,
87+
variables: Vec<String>,
88+
},
8189
/// Comment
8290
Comment {
8391
#[serde(rename = "//")]
@@ -103,8 +111,10 @@ pub enum ArgItem {
103111
Indexed((String, usize)),
104112
/// Simple argument (entire register)
105113
Simple(String),
106-
/// Integer literal
114+
/// Integer literal (signed, covers most cases)
107115
Integer(i64),
116+
/// Unsigned integer literal (for values > `i64::MAX`, e.g. `u64::MAX`)
117+
UInteger(u64),
108118
/// Expression (for nested expressions)
109119
Expression(Box<Expression>),
110120
}
@@ -124,6 +134,20 @@ pub enum Expression {
124134
// Constants for internal register naming
125135
pub const MEASUREMENT_PREFIX: &str = "measurement_";
126136

137+
/// Infer variable size from data type when not explicitly provided.
138+
///
139+
/// For types like "i32", "u64", extracts the bit width from the name.
140+
/// For "qubits", returns 0 (size must be explicit).
141+
#[must_use]
142+
pub fn infer_size(data_type: &str, explicit_size: Option<usize>) -> usize {
143+
if let Some(s) = explicit_size {
144+
return s;
145+
}
146+
// Try to extract bit width from type name (e.g., "i32" -> 32, "u64" -> 64)
147+
let digits: String = data_type.chars().filter(char::is_ascii_digit).collect();
148+
digits.parse().unwrap_or(0)
149+
}
150+
127151
/// Custom deserializer to convert angles to radians
128152
fn deserialize_angles_to_radians<'de, D>(deserializer: D) -> Result<Option<Vec<f64>>, D::Error>
129153
where

crates/pecos-phir-json/src/v0_1/block_executor.rs

Lines changed: 10 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -144,8 +144,12 @@ impl BlockExecutor {
144144
size,
145145
} => {
146146
debug!("Processing variable definition: {data_type} {variable}");
147-
self.processor
148-
.handle_variable_definition(data, data_type, variable, *size)?;
147+
self.processor.handle_variable_definition(
148+
data,
149+
data_type,
150+
variable,
151+
crate::v0_1::ast::infer_size(data_type, *size),
152+
)?;
149153
}
150154
Operation::QuantumOp {
151155
qop, angles, args, ..
@@ -219,6 +223,9 @@ impl BlockExecutor {
219223
debug!("Skipping comment: {comment}");
220224
// Comments are no-ops
221225
}
226+
Operation::DataExport { .. } => {
227+
// Data exports are no-ops during execution
228+
}
222229
}
223230

224231
Ok(())
@@ -831,7 +838,7 @@ mod tests {
831838
data: "cvar_define".to_string(),
832839
data_type: "i32".to_string(),
833840
variable: "x".to_string(),
834-
size: 32,
841+
size: Some(32),
835842
},
836843
Operation::ClassicalOp {
837844
cop: "=".to_string(),

0 commit comments

Comments
 (0)