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
45 changes: 9 additions & 36 deletions crates/pecos-phir-json/src/v0_1/classical_interpreter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -69,13 +69,6 @@ pub enum YieldedOp {
MOp(YieldedMOp),
}

/// Metadata about a quantum variable (for resolving qubit args to IDs).
#[derive(Debug, Clone)]
struct QVarMeta {
/// Starting global qubit ID for this variable
start_id: usize,
}

/// Classical interpreter for PHIR programs.
///
/// Walks the PHIR AST, executes classical operations (assignment, Result mapping,
Expand All @@ -84,12 +77,8 @@ struct QVarMeta {
pub struct PhirClassicalInterpreter {
/// The parsed PHIR program
program: Option<PHIRProgram>,
/// Classical variable environment
/// Variable environment and declaration-order qubit IDs
environment: Environment,
/// Quantum variable metadata (name -> `QVarMeta`)
qvar_meta: BTreeMap<String, QVarMeta>,
/// Total number of qubits
num_qubits: usize,
/// Foreign object for `FFCalls`
foreign_object: Option<Box<dyn ForeignObject>>,
}
Expand All @@ -101,8 +90,6 @@ impl PhirClassicalInterpreter {
Self {
program: None,
environment: Environment::new(),
qvar_meta: BTreeMap::new(),
num_qubits: 0,
foreign_object: None,
}
}
Expand Down Expand Up @@ -147,8 +134,6 @@ impl PhirClassicalInterpreter {

self.foreign_object = foreign_object;
self.environment = Environment::new();
self.qvar_meta.clear();
self.num_qubits = 0;

// Process variable definitions from the ops
for op in &program.ops {
Expand All @@ -162,13 +147,8 @@ impl PhirClassicalInterpreter {
let resolved_size = infer_size(data_type, *size);
match data.as_str() {
"qvar_define" if data_type == "qubits" => {
let start_id = self.num_qubits;
self.qvar_meta
.insert(variable.clone(), QVarMeta { start_id });
self.num_qubits += resolved_size;
// Don't add quantum vars to the classical environment --
// they live in qvar_meta only, matching Python behavior
// where qvar_meta and csym2id are separate namespaces.
self.environment
.add_quantum_register(variable, resolved_size)?;
}
"cvar_define" => {
let dt = data_type.parse::<DataType>()?;
Expand All @@ -182,7 +162,7 @@ impl PhirClassicalInterpreter {
}

self.program = Some(program);
Ok(self.num_qubits)
Ok(self.num_qubits())
}

/// Reset variable values for a new shot (keeps definitions).
Expand Down Expand Up @@ -375,26 +355,19 @@ impl PhirClassicalInterpreter {
/// Get the number of qubits.
#[must_use]
pub fn num_qubits(&self) -> usize {
self.num_qubits
self.environment.count_qubits()
}

/// Resolve a `QubitArg` to integer qubit IDs.
fn resolve_qubit_arg(&self, arg: &QubitArg) -> Result<Vec<usize>, PecosError> {
match arg {
QubitArg::SingleQubit((var, idx)) => {
let meta = self
.qvar_meta
.get(var)
.ok_or_else(|| PecosError::Input(format!("Unknown quantum variable: {var}")))?;
Ok(vec![meta.start_id + idx])
Ok(vec![self.environment.resolve_qubit(var, *idx)?])
}
QubitArg::MultipleQubits(qubits) => {
let mut ids = Vec::new();
for (var, idx) in qubits {
let meta = self.qvar_meta.get(var).ok_or_else(|| {
PecosError::Input(format!("Unknown quantum variable: {var}"))
})?;
ids.push(meta.start_id + idx);
ids.push(self.environment.resolve_qubit(var, *idx)?);
}
Ok(ids)
}
Expand All @@ -405,7 +378,7 @@ impl PhirClassicalInterpreter {
///
/// # Errors
///
/// Returns `PecosError` if qubit args reference unknown quantum variables.
/// Returns `PecosError` for unknown quantum variables or out-of-bounds qubit indices.
pub fn make_qop(
&self,
qop_name: &str,
Expand Down Expand Up @@ -490,7 +463,7 @@ impl PhirClassicalInterpreter {
///
/// # Errors
///
/// Returns `PecosError` if qubit args reference unknown quantum variables.
/// Returns `PecosError` for unknown quantum variables or out-of-bounds qubit indices.
pub fn make_mop(
&self,
mop_name: &str,
Expand Down
56 changes: 52 additions & 4 deletions crates/pecos-phir-json/src/v0_1/environment.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use std::collections::BTreeMap;
use std::fmt;
use std::ops::Range;
use std::str::FromStr;

use pecos_core::BitUInt;
Expand Down Expand Up @@ -469,6 +470,9 @@ pub struct Environment {
metadata: Vec<VariableInfo>,
/// Maps source variable names to destination names for output
mappings: Vec<(String, String)>,
/// Declaration-order global qubit ranges, separate from classical storage.
quantum_registers: BTreeMap<String, Range<usize>>,
num_qubits: usize,
}

impl Environment {
Expand All @@ -480,6 +484,8 @@ impl Environment {
name_to_index: BTreeMap::new(),
metadata: Vec::new(),
mappings: Vec::new(),
quantum_registers: BTreeMap::new(),
num_qubits: 0,
}
}

Expand Down Expand Up @@ -546,6 +552,10 @@ impl Environment {
}
}

if data_type == DataType::Qubits {
self.add_quantum_register(name, size)?;
}

let index = self.values.len();
self.name_to_index.insert(name.to_string(), index);

Expand All @@ -562,6 +572,47 @@ impl Environment {
Ok(())
}

/// Define a quantum register without adding classical value storage.
/// The interpreter keeps quantum and classical names in separate namespaces.
pub(crate) fn add_quantum_register(
&mut self,
name: &str,
size: usize,
) -> Result<(), PecosError> {
if self.quantum_registers.contains_key(name) {
// Python allocates another block and overwrites the name mapping.
// PECOS deliberately rejects duplicates instead of remapping qubits.
return Err(PecosError::Input(format!(
"Quantum register '{name}' already exists"
)));
}
let end = self.num_qubits.checked_add(size).ok_or_else(|| {
PecosError::Input(format!("Qubit count overflow defining register '{name}'"))
})?;
self.quantum_registers
.insert(name.to_string(), self.num_qubits..end);
self.num_qubits = end;
Ok(())
}

/// Resolve a register-local qubit index to its declaration-order global ID.
///
/// # Errors
/// Returns an error for an unknown quantum register or an out-of-bounds index.
pub fn resolve_qubit(&self, name: &str, index: usize) -> Result<usize, PecosError> {
let range = self
.quantum_registers
.get(name)
.ok_or_else(|| PecosError::Input(format!("Unknown quantum register '{name}'")))?;
let size = range.end - range.start;
if index >= size {
return Err(PecosError::Input(format!(
"Index {index} out of bounds for quantum register '{name}' of size {size}"
)));
}
Ok(range.start + index)
}

/// Checks if a variable exists in the environment
#[must_use]
pub fn has_variable(&self, name: &str) -> bool {
Expand Down Expand Up @@ -717,10 +768,7 @@ impl Environment {
/// Gets the total number of qubits in the environment
#[must_use]
pub fn count_qubits(&self) -> usize {
self.get_variables_of_type(&DataType::Qubits)
.iter()
.map(|info| info.size)
.sum()
self.num_qubits
}

/// Returns the total number of variables in the environment
Expand Down
39 changes: 27 additions & 12 deletions crates/pecos-phir-json/src/v0_1/operations.rs
Original file line number Diff line number Diff line change
Expand Up @@ -512,11 +512,9 @@ impl OperationProcessor {
// Validate all qubits in the barrier
for (var, idx) in args {
self.validate_variable_access(var, *idx)?;
self.environment.resolve_qubit(var, *idx)?;
}

// Extract qubit indices for the barrier (just for validation)
let _qubit_indices: Vec<usize> = args.iter().map(|(_, idx)| *idx).collect();

// Return barrier result
Ok(MetaInstructionResult::Barrier {
qubits: args.to_vec(),
Expand All @@ -531,7 +529,7 @@ impl OperationProcessor {
/// Add a meta instruction to the byte message builder
///
/// # Errors
/// Currently never returns an error, but may in future implementations.
/// Returns an error if a qubit register is unknown or an index is out of bounds.
pub fn add_meta_instruction_to_builder(
&self,
_builder: &mut ByteMessageBuilder,
Expand All @@ -540,7 +538,10 @@ impl OperationProcessor {
match meta_result {
MetaInstructionResult::Barrier { qubits } => {
// Extract qubit indices for the barrier for debug output
let qubit_indices: Vec<usize> = qubits.iter().map(|(_, idx)| *idx).collect();
let qubit_indices: Vec<usize> = qubits
.iter()
.map(|(var, idx)| self.environment.resolve_qubit(var, *idx))
.collect::<Result<_, _>>()?;

// Add barrier operation to the builder (if supported by the ByteMessageBuilder)
// For now, we handle it as a "no-op" since barriers are primarily compiler hints
Expand Down Expand Up @@ -742,12 +743,14 @@ impl OperationProcessor {
QubitArg::SingleQubit((var, idx)) => {
// Validate the qubit exists
self.validate_variable_access(var, *idx)?;
self.environment.resolve_qubit(var, *idx)?;
qubits.push((var.clone(), *idx));
}
QubitArg::MultipleQubits(qubit_list) => {
for (var, idx) in qubit_list {
// Validate each qubit exists
self.validate_variable_access(var, *idx)?;
self.environment.resolve_qubit(var, *idx)?;
qubits.push((var.clone(), *idx));
}
}
Expand All @@ -774,7 +777,7 @@ impl OperationProcessor {
/// * `Err(PecosError)` - If the operation could not be added
///
/// # Errors
/// Currently never returns an error, but may in future implementations.
/// Returns an error if a qubit register is unknown or an index is out of bounds.
///
/// # Notes
///
Expand All @@ -794,7 +797,10 @@ impl OperationProcessor {
..
} => {
// Extract qubit indices for the idle operation
let qubit_indices: Vec<usize> = qubits.iter().map(|(_, idx)| *idx).collect();
let qubit_indices: Vec<usize> = qubits
.iter()
.map(|(var, idx)| self.environment.resolve_qubit(var, *idx))
.collect::<Result<_, _>>()?;

// Add idle operation to the builder
if !qubit_indices.is_empty() {
Expand All @@ -811,7 +817,10 @@ impl OperationProcessor {
..
} => {
// Extract qubit indices for the transport operation
let qubit_indices: Vec<usize> = qubits.iter().map(|(_, idx)| *idx).collect();
let qubit_indices: Vec<usize> = qubits
.iter()
.map(|(var, idx)| self.environment.resolve_qubit(var, *idx))
.collect::<Result<_, _>>()?;

// Add transport operation to the builder if supported
// For now, we'll treat it as an idle operation
Expand All @@ -829,7 +838,10 @@ impl OperationProcessor {
..
} => {
// Extract qubit indices for the delay operation
let qubit_indices: Vec<usize> = qubits.iter().map(|(_, idx)| *idx).collect();
let qubit_indices: Vec<usize> = qubits
.iter()
.map(|(var, idx)| self.environment.resolve_qubit(var, *idx))
.collect::<Result<_, _>>()?;

// Add delay operation to the builder if supported
// For now, we'll treat it as an idle operation
Expand All @@ -848,7 +860,10 @@ impl OperationProcessor {
..
} => {
// Extract qubit indices for the timing operation
let qubit_indices: Vec<usize> = qubits.iter().map(|(_, idx)| *idx).collect();
let qubit_indices: Vec<usize> = qubits
.iter()
.map(|(var, idx)| self.environment.resolve_qubit(var, *idx))
.collect::<Result<_, _>>()?;

// Add timing operation to the builder if supported
debug!(
Expand Down Expand Up @@ -1518,13 +1533,13 @@ impl OperationProcessor {
QubitArg::SingleQubit((var, idx)) => {
// Validate the qubit
self.validate_variable_access(var, *idx)?;
qubit_args.push(*idx);
qubit_args.push(self.environment.resolve_qubit(var, *idx)?);
}
QubitArg::MultipleQubits(qubits) => {
for (var, idx) in qubits {
// Validate each qubit
self.validate_variable_access(var, *idx)?;
qubit_args.push(*idx);
qubit_args.push(self.environment.resolve_qubit(var, *idx)?);
}
}
}
Expand Down
Loading
Loading