Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
14 commits
Select commit Hold shift + click to select a range
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
4 changes: 4 additions & 0 deletions crates/pecos-core/src/errors.rs
Original file line number Diff line number Diff line change
Expand Up @@ -100,6 +100,10 @@ pub enum PecosError {
#[error("Division by zero")]
RuntimeDivisionByZero,

/// Undefined variable referenced at runtime
#[error("Variable '{name}' not found")]
RuntimeUndefinedVariable { name: String },

/// Stack overflow
#[error("Stack overflow")]
RuntimeStackOverflow,
Expand Down
25 changes: 25 additions & 0 deletions crates/pecos-phir-json/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -150,6 +150,31 @@ mod tests {
use std::io::Write;
use tempfile::tempdir;

#[cfg(feature = "v0_1")]
#[test]
fn test_oversized_signed_register_fails_fast_in_engine() {
// A signed size==64 register is i(S+1) = i65, which does not fit the
// 64-bit backing integer. The engine must reject it at construction
// (fail fast) rather than swallow the definition error and silently
// recreate the register as i32 on first assignment. Regression test for
// issue #345 (engine-path variable-definition error swallowing).
let program = r#"{
"format": "PHIR/JSON",
"version": "0.1.0",
"ops": [
{"data": "cvar_define", "data_type": "i64", "variable": "x", "size": 64},
{"cop": "=", "returns": ["x"], "args": [9223372036854775807]},
{"cop": "Result", "args": ["x"], "returns": ["out"]}
]
}"#;
let err = PhirJsonEngine::from_json(program)
.expect_err("oversized signed register must fail fast at construction");
assert!(
err.to_string().contains("does not fit"),
"unexpected error: {err}"
);
}

#[cfg(feature = "v0_1")]
#[test]
#[allow(clippy::too_many_lines)]
Expand Down
71 changes: 70 additions & 1 deletion crates/pecos-phir-json/src/v0_1/ast.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
use serde::Deserialize;
use std::collections::BTreeMap;
use std::collections::{BTreeMap, BTreeSet};
use std::f64::consts::PI;

/// Program structure for PHIR (PECOS High-level Intermediate Representation)
Expand All @@ -12,6 +12,49 @@ pub struct PHIRProgram {
pub ops: Vec<Operation>,
}

impl PHIRProgram {
/// Collect the names of all foreign functions invoked by `ffcall` classical
/// operations anywhere in the program, including inside `if`/`sequence`
/// blocks. Mirrors `PyPHIR.foreign_func_calls` and is used to fail fast when
/// a program calls a foreign function the supplied foreign object lacks.
#[must_use]
pub fn foreign_func_calls(&self) -> BTreeSet<String> {
let mut names = BTreeSet::new();
collect_foreign_func_calls(&self.ops, &mut names);
names
}
}

/// Recursively gather `ffcall` function names from a slice of operations.
fn collect_foreign_func_calls(ops: &[Operation], names: &mut BTreeSet<String>) {
for op in ops {
match op {
Operation::ClassicalOp {
cop,
function: Some(name),
..
} if cop == "ffcall" => {
names.insert(name.clone());
}
Operation::Block {
ops,
true_branch,
false_branch,
..
} => {
collect_foreign_func_calls(ops, names);
if let Some(branch) = true_branch {
collect_foreign_func_calls(branch, names);
}
if let Some(branch) = false_branch {
collect_foreign_func_calls(branch, names);
}
}
_ => {}
}
}
}

/// Represents an operation in the PHIR program.
///
/// Deserialized via a manual `Deserialize` impl that inspects which discriminating
Expand Down Expand Up @@ -510,4 +553,30 @@ mod tests {
panic!("Expected DataExport");
}
}

#[test]
fn test_foreign_func_calls_collects_nested_and_dedups() {
let json = r#"{
"format": "PHIR/JSON", "version": "0.1.0", "metadata": {},
"ops": [
{"cop": "ffcall", "function": "foo", "args": [1], "returns": ["a"]},
{"block": "if", "condition": {"cop": "==", "args": ["a", 1]},
"true_branch": [
{"cop": "ffcall", "function": "bar", "args": [], "returns": ["b"]}
],
"false_branch": [
{"cop": "ffcall", "function": "foo", "args": [2], "returns": ["c"]}
]},
{"block": "sequence", "ops": [
{"cop": "ffcall", "function": "baz", "args": [], "returns": ["d"]}
]}
]
}"#;
let program: PHIRProgram = serde_json::from_str(json).expect("should parse");
let expected: BTreeSet<String> = ["bar", "baz", "foo"]
.iter()
.map(|s| (*s).to_string())
.collect();
assert_eq!(program.foreign_func_calls(), expected);
}
}
30 changes: 10 additions & 20 deletions crates/pecos-phir-json/src/v0_1/classical_interpreter.rs
Original file line number Diff line number Diff line change
Expand Up @@ -207,6 +207,12 @@ impl PhirClassicalInterpreter {
Ok(())
}

/// Detach the foreign object (e.g. so the interpreter can be pickled
/// without it; workers re-attach their own copy via `init`).
pub fn clear_foreign_object(&mut self) {
self.foreign_object = None;
}

/// Get a reference to the program ops.
#[must_use]
pub fn program_ops(&self) -> &[Operation] {
Expand Down Expand Up @@ -532,18 +538,14 @@ impl PhirClassicalInterpreter {
/// The Environment's `BitValue` storage automatically masks to the declared
/// bit width, so no manual masking is needed.
fn assign_int_var(&mut self, name: &str, val: u64) -> Result<(), PecosError> {
if self.environment.has_variable(name) {
self.environment.set_raw(name, val)?;
}
Ok(())
// Assigning a measurement result to an undeclared variable is an error
// (matching Python's `csym2id[cvar]` lookup), not a silent no-op.
self.environment.set_raw(name, val)
}

/// Assign a bit value to a specific bit of a classical variable.
fn assign_int_bit(&mut self, name: &str, idx: usize, val: i64) -> Result<(), PecosError> {
if self.environment.has_variable(name) {
self.environment.set_bit(name, idx, (val & 1) != 0)?;
}
Ok(())
self.environment.set_bit(name, idx, (val & 1) != 0)
}

/// Evaluate an expression using the current environment.
Expand Down Expand Up @@ -591,16 +593,10 @@ impl PhirClassicalInterpreter {
for (val, ret) in values.into_iter().zip(returns.iter()) {
match ret {
ArgItem::Simple(var) => {
if !self.environment.has_variable(var) {
self.environment.add_variable(var, DataType::I32, 31)?;
}
#[allow(clippy::cast_sign_loss)]
self.environment.set_raw(var, val as u64)?;
}
ArgItem::Indexed((var, idx)) => {
if !self.environment.has_variable(var) {
self.environment.add_variable(var, DataType::I32, 31)?;
}
self.environment.set_bit(var, *idx, (val & 1) != 0)?;
}
_ => {
Expand Down Expand Up @@ -660,16 +656,10 @@ impl PhirClassicalInterpreter {
if i < result.len() {
match ret {
ArgItem::Simple(var) => {
if !self.environment.has_variable(var) {
self.environment.add_variable(var, DataType::I32, 31)?;
}
#[allow(clippy::cast_sign_loss)]
self.environment.set_raw(var, result[i] as u64)?;
}
ArgItem::Indexed((var, idx)) => {
if !self.environment.has_variable(var) {
self.environment.add_variable(var, DataType::I32, 31)?;
}
self.environment.set_bit(var, *idx, (result[i] & 1) != 0)?;
}
_ => {
Expand Down
8 changes: 4 additions & 4 deletions crates/pecos-phir-json/src/v0_1/engine.rs
Original file line number Diff line number Diff line change
Expand Up @@ -154,12 +154,12 @@ impl PhirJsonEngine {
size,
} = op
{
let _ = processor.handle_variable_definition(
processor.handle_variable_definition(
data,
data_type,
variable,
infer_size(data_type, *size),
);
)?;
}
}

Expand Down Expand Up @@ -281,12 +281,12 @@ impl PhirJsonEngine {
variable,
size,
} => {
let _ = self.processor.handle_variable_definition(
self.processor.handle_variable_definition(
data,
data_type,
variable,
infer_size(data_type, *size),
);
)?;
self.advance_cursor();
}
Operation::QuantumOp {
Expand Down
20 changes: 12 additions & 8 deletions crates/pecos-phir-json/src/v0_1/environment.rs
Original file line number Diff line number Diff line change
Expand Up @@ -604,7 +604,9 @@ impl Environment {
self.values[idx] = BitValue::from_u64(&info.data_type, info.size, value);
Ok(())
} else {
Err(PecosError::Input(format!("Variable '{name}' not found")))
Err(PecosError::RuntimeUndefinedVariable {
name: name.to_string(),
})
}
}

Expand All @@ -616,7 +618,9 @@ impl Environment {
if let Some(&idx) = self.name_to_index.get(name) {
Ok(&self.metadata[idx])
} else {
Err(PecosError::Input(format!("Variable '{name}' not found")))
Err(PecosError::RuntimeUndefinedVariable {
name: name.to_string(),
})
}
}

Expand All @@ -634,9 +638,9 @@ impl Environment {
if let Some(&idx) = self.name_to_index.get(var_name) {
self.values[idx].get_bit(bit_index).map(BoolBit)
} else {
Err(PecosError::Input(format!(
"Variable '{var_name}' not found"
)))
Err(PecosError::RuntimeUndefinedVariable {
name: var_name.to_string(),
})
}
}

Expand All @@ -661,9 +665,9 @@ impl Environment {
self.values[idx] = BitValue::from_u64(&info.data_type, info.size, updated.as_u64());
Ok(())
} else {
Err(PecosError::Input(format!(
"Variable '{var_name}' not found"
)))
Err(PecosError::RuntimeUndefinedVariable {
name: var_name.to_string(),
})
}
}

Expand Down
26 changes: 13 additions & 13 deletions crates/pecos-phir-json/src/v0_1/expression.rs
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,7 @@ impl<'a> ExpressionEvaluator<'a> {
ExprValue::from_bit_value(value)
});
}
return Err(PecosError::Input(format!("Variable '{name}' not found")));
return Err(PecosError::RuntimeUndefinedVariable { name: name.clone() });
}
Expression::Operation { .. } => {}
}
Expand Down Expand Up @@ -221,17 +221,15 @@ impl<'a> ExpressionEvaluator<'a> {
ExprValue::from_bit_value(value)
})
} else {
Err(PecosError::Input(format!("Variable '{name}' not found")))
Err(PecosError::RuntimeUndefinedVariable { name: name.clone() })
}
}
ArgItem::Indexed((name, idx)) => {
if let Ok(bit) = self.environment.get_bit(name, *idx) {
Ok(ExprValue::Boolean(bit.0))
} else {
Err(PecosError::Input(format!(
"Failed to access bit {name}[{idx}]"
)))
}
// Propagate the underlying error so an undefined variable surfaces
// as `RuntimeUndefinedVariable` (-> KeyError) rather than being
// flattened into a generic `Input` error.
let bit = self.environment.get_bit(name, *idx)?;
Ok(ExprValue::Boolean(bit.0))
}
ArgItem::Integer(val) => Ok(ExprValue::signed(*val)),
ArgItem::UInteger(val) => Ok(ExprValue::unsigned(*val)),
Expand Down Expand Up @@ -414,10 +412,12 @@ impl<'a> ExpressionEvaluator<'a> {
/// # Errors
/// Returns an error if any bit access fails.
pub fn get_bits(&self, name: &str, indices: &[usize]) -> Result<Vec<bool>, PecosError> {
let value = self
.environment
.get(name)
.ok_or_else(|| PecosError::Input(format!("Variable '{name}' not found")))?;
let value =
self.environment
.get(name)
.ok_or_else(|| PecosError::RuntimeUndefinedVariable {
name: name.to_string(),
})?;
let value_u64 = value.as_u64();
indices
.iter()
Expand Down
Loading
Loading