Skip to content
Open
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
2 changes: 1 addition & 1 deletion source/compiler/qsc_eval/src/val.rs
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@ use std::{

use crate::{AsIndex, Error, Range as EvalRange, error::PackageSpan};

pub(super) const DEFAULT_RANGE_STEP: i64 = 1;
pub const DEFAULT_RANGE_STEP: i64 = 1;

#[derive(Clone, Debug, PartialEq)]
pub enum Value {
Expand Down
118 changes: 97 additions & 21 deletions source/compiler/qsc_partial_eval/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1287,10 +1287,9 @@ impl<'a> PartialEvaluator<'a> {
"literal should have been classically evaluated".to_string(),
expr_package_span,
)),
ExprKind::Range(_, _, _) => Err(Error::Unexpected(
"dynamic ranges are invalid".to_string(),
expr_package_span,
)),
ExprKind::Range(start, step, end) => {
self.eval_expr_range(*start, *step, *end, expr_package_span)
}
ExprKind::Return(expr_id) => self.eval_expr_return(*expr_id),
ExprKind::Struct(..) => Err(Error::Unexpected(
"instruction generation for struct constructor expressions is invalid".to_string(),
Expand Down Expand Up @@ -1813,23 +1812,49 @@ impl<'a> PartialEvaluator<'a> {
)),
// The following intrinsic functions and operations should never make it past conditional compilation and
// the capabilities check pass.
"DrawRandomInt" | "DrawRandomDouble" | "DrawRandomBool" | "Length" => {
Err(Error::Unexpected(
format!(
"`{}` is not a supported by partial evaluation",
callable_decl.name.name
),
callee_expr_span,
))
}
"IntAsDouble" => {
let variable_id = self.resource_manager.next_var();
self.convert_value(&args_value, rir::Variable::new_double(variable_id))
}
"Truncate" => {
let variable_id = self.resource_manager.next_var();
self.convert_value(&args_value, rir::Variable::new_integer(variable_id))
"DrawRandomInt" | "DrawRandomDouble" | "DrawRandomBool" => Err(Error::Unexpected(
format!(
"`{}` is not a supported by partial evaluation",
callable_decl.name.name
),
callee_expr_span,
)),
"Length" => {
let Value::Array(arr) = args_value else {
return Err(Error::Unexpected(
"length call on dynamically sized array".to_string(),
callee_expr_span,
));
};
match arr.len().try_into() {
Ok(len) => Ok(Value::Int(len)),
Err(_) => Err(EvalError::ArrayTooLarge(args_span).into()),
}
}
"IntAsDouble" => match args_value {
#[allow(clippy::cast_precision_loss)]
Value::Int(i) => Ok(Value::Double(i as f64)),
Value::Var(_) => {
let variable_id = self.resource_manager.next_var();
self.convert_value(&args_value, rir::Variable::new_double(variable_id))
}
_ => panic!(
"Unexpected value type for IntAsDouble: {}",
args_value.type_name()
),
},
"Truncate" => match args_value {
#[allow(clippy::cast_possible_truncation)]
Value::Double(d) => Ok(Value::Int(d as i64)),
Value::Var(_) => {
let variable_id = self.resource_manager.next_var();
self.convert_value(&args_value, rir::Variable::new_integer(variable_id))
}
_ => panic!(
"Unexpected value type for Truncate: {}",
args_value.type_name()
),
},
_ => self.eval_expr_call_to_intrinsic_qis(
store_item_id,
callable_decl,
Expand Down Expand Up @@ -2392,7 +2417,7 @@ impl<'a> PartialEvaluator<'a> {
.config
.capabilities
.contains(TargetCapabilityFlags::BackwardsBranching)
&& !self.is_static_expr(condition_expr_id)
&& self.is_variable_expr(condition_expr_id)
{
// If backwards branching is supported and the loop condition is not static,
// we can generate a while loop structure in RIR without unrolling the loop.
Expand Down Expand Up @@ -3027,6 +3052,11 @@ impl<'a> PartialEvaluator<'a> {
matches!(compute_kind, ComputeKind::Static)
}

fn is_variable_expr(&self, expr_id: ExprId) -> bool {
let compute_kind = self.get_expr_compute_kind(expr_id);
compute_kind.is_variable_value_kind()
}

fn allocate_qubit(&mut self) -> Value {
let qubit = self.resource_manager.allocate_qubit();
Value::Qubit(qubit)
Expand Down Expand Up @@ -4043,6 +4073,52 @@ impl<'a> PartialEvaluator<'a> {

Ok(Value::Var(eval_variable))
}

fn eval_expr_range(
&mut self,
start: Option<ExprId>,
step: Option<ExprId>,
end: Option<ExprId>,
span: PackageSpan,
) -> Result<EvalControlFlow, Error> {
let mut exprs = Vec::new();
for expr in [start, step, end] {
// Try to evaluate the sub-expression.
let expr_control_flow = expr.map(|id| self.try_eval_expr(id)).transpose()?;
// From there, get the value, assuming that any embedded returns are invalid and produce an error.
let expr_value = expr_control_flow
.map(|cf| match cf {
EvalControlFlow::Continue(val) => Ok(val),
EvalControlFlow::Return(_) => Err(Error::Unexpected(
"embedded return in Range expression".to_string(),
span,
)),
})
.transpose()?;
// Convert the value to an integer, if possible. Non-integer values should never happen,
// variable values should be caught by RCA but may sneak through so fail gracefully.
let expr_int = expr_value
.map(|v| match v {
Value::Int(i) => Ok(i),
Value::Var(_) => Err(Error::Unexpected(
"dynamic variable in Range expression".to_string(),
span,
)),
_ => panic!("invalid type for Range expression: {}", v.type_name()),
})
.transpose()?;
exprs.push(expr_int);
}

// Create a new range value from the processed sub-expressions, using the default step if not specified.
Ok(EvalControlFlow::Continue(Value::Range(Box::new(
val::Range {
start: exprs[0],
step: exprs[1].unwrap_or(val::DEFAULT_RANGE_STEP),
end: exprs[2],
},
))))
}
}

#[derive(Default)]
Expand Down
25 changes: 25 additions & 0 deletions source/compiler/qsc_partial_eval/src/tests/misc.rs
Original file line number Diff line number Diff line change
Expand Up @@ -979,3 +979,28 @@ fn custom_two_qubit_measurement_in_loop_of_variable_qubits_supported() {
Jump(6)"#]],
);
}

#[test]
fn test_length_with_embedded_qubit_operations() {
let program = get_rir_program_with_capabilities(
indoc! {
r#"
operation Main() : Int {
Length({use q = Qubit(); M(q); [1]})
}
"#,
},
Profile::AdaptiveRIFLA.into(),
);

assert_blocks(
&program,
&expect![[r#"
Blocks:
Block 0:Block:
Call id(1), args( Pointer, )
Call id(2), args( Qubit(0), Result(0), )
Call id(3), args( Integer(1), Tag(0, 3), )
Return"#]],
);
}
68 changes: 11 additions & 57 deletions source/compiler/qsc_rca/src/core.rs
Original file line number Diff line number Diff line change
Expand Up @@ -341,14 +341,7 @@ impl<'a> Analyzer<'a> {
value_kind,
}
} else {
let call_compute_kind =
self.analyze_expr_call_with_static_callee(callee_expr_id, args_expr_id, expr_type);
match call_compute_kind {
CallComputeKind::Regular(compute_kind) => compute_kind,
CallComputeKind::Override(compute_kind) => {
return compute_kind;
}
}
self.analyze_expr_call_with_static_callee(callee_expr_id, args_expr_id, expr_type)
};

// If this call happens within a dynamic scope, there might be additional runtime features being used.
Expand Down Expand Up @@ -394,40 +387,13 @@ impl<'a> Analyzer<'a> {
compute_kind
}

fn analyze_expr_call_for_length_intrinsic(&self, args_expr_id: ExprId) -> ComputeKind {
let application_instance = self.get_current_application_instance();
let args_compute_kind = *application_instance.get_expr_compute_kind(args_expr_id);
match args_compute_kind {
ComputeKind::Static => ComputeKind::Static,
ComputeKind::Dynamic {
runtime_features, ..
} => {
if runtime_features.contains(RuntimeFeatureFlags::UseOfDynamicallySizedArray) {
ComputeKind::Dynamic {
runtime_features,
value_kind: ValueKind::Variable,
}
} else {
ComputeKind::Static
}
}
}
}

fn analyze_expr_call_with_spec_callee(
&mut self,
callee: &Callee,
callable_decl: &'a CallableDecl,
args_expr_id: ExprId,
fixed_args: Option<Vec<LocalVarId>>,
) -> CallComputeKind {
// The `Length` intrinsic function has a specialized override.
if is_length_intrinsic(callable_decl) {
return CallComputeKind::Override(
self.analyze_expr_call_for_length_intrinsic(args_expr_id),
);
}

) -> ComputeKind {
// Analyze the specialization to determine its application generator set.
let callee_id = GlobalSpecId::from((callee.item, callee.functor_app.functor_set_value()));
self.analyze_spec(callee_id, callable_decl);
Expand Down Expand Up @@ -503,15 +469,15 @@ impl<'a> Analyzer<'a> {
runtime_features.insert(RuntimeFeatureFlags::CallToCyclicOperation);
}

CallComputeKind::Regular(compute_kind)
compute_kind
}

fn analyze_expr_call_with_static_callee(
&mut self,
callee_expr_id: ExprId,
args_expr_id: ExprId,
expr_type: &Ty,
) -> CallComputeKind {
) -> ComputeKind {
// Try to resolve the callee.
let package_id = self.get_current_package_id();
let package = self.package_store.get(package_id);
Expand All @@ -535,7 +501,7 @@ impl<'a> Analyzer<'a> {
self.get_current_application_instance_mut()
.unresolved_callee_exprs
.push(callee_expr_id);
return CallComputeKind::Regular(compute_kind);
return compute_kind;
};

if self.callee_in_active_contexts(&callee) {
Expand All @@ -554,18 +520,18 @@ impl<'a> Analyzer<'a> {
self.get_current_application_instance_mut()
.unresolved_callee_exprs
.push(callee_expr_id);
return CallComputeKind::Regular(ComputeKind::Dynamic {
return ComputeKind::Dynamic {
runtime_features: RuntimeFeatureFlags::CallToUnresolvedCallee,
value_kind: ValueKind::Constant,
});
};
}

// We try to resolve the callee and determine the compute kind of the call depending on the callee kind.
let Some(global_callee) = self.package_store.get_global(callee.item) else {
// If the callee is not found, that is an indication that it is an item that was removed during
// incremental compilation but remains in the name resolution data structures. Assume it is static
// so that it generates an "unbound name" error at runtime.
return CallComputeKind::Regular(ComputeKind::Static);
return ComputeKind::Static;
};
match global_callee {
Global::Callable(callable_decl) => self.analyze_expr_call_with_spec_callee(
Expand All @@ -574,9 +540,7 @@ impl<'a> Analyzer<'a> {
args_expr_id,
fixed_args,
),
Global::Udt => {
CallComputeKind::Regular(self.analyze_expr_call_with_udt_callee(args_expr_id))
}
Global::Udt => self.analyze_expr_call_with_udt_callee(args_expr_id),
}
}

Expand Down Expand Up @@ -1174,8 +1138,8 @@ impl<'a> Analyzer<'a> {
}
};

// If the condition is dynamic, we require an additional runtime feature.
if !matches!(condition_expr_compute_kind, ComputeKind::Static) {
// If the condition is a dynamic variable, we require an additional runtime feature.
if condition_expr_compute_kind.is_variable_value_kind() {
let ComputeKind::Dynamic {
runtime_features, ..
} = &mut compute_kind
Expand Down Expand Up @@ -2245,11 +2209,6 @@ impl SpecContext {
}
}

enum CallComputeKind {
Regular(ComputeKind),
Override(ComputeKind),
}

fn derive_intrinsic_function_application_generator_set(
callable_context: &CallableContext,
) -> ApplicationGeneratorSet {
Expand Down Expand Up @@ -2368,11 +2327,6 @@ fn derive_instrinsic_operation_application_generator_set(
}
}

fn is_length_intrinsic(callable_decl: &CallableDecl) -> bool {
matches!(callable_decl.implementation, CallableImpl::Intrinsic)
&& callable_decl.name.name.as_ref() == "Length"
}

fn ty_to_runtime_runtime_output_flags(ty: &Ty) -> RuntimeFeatureFlags {
match ty {
Ty::Array(content_type) => ty_to_runtime_runtime_output_flags(content_type),
Expand Down
4 changes: 3 additions & 1 deletion source/compiler/qsc_rca/src/tests/overrides.rs
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,9 @@ fn check_rca_for_length_of_statically_sized_array_with_dynamic_content() {
package_store_compute_properties,
&expect![[r#"
ApplicationsGeneratorSet:
inherent: Static
inherent: Dynamic:
runtime_features: RuntimeFeatureFlags(0x0)
value_kind: Constant
dynamic_param_applications: <empty>"#]],
);
}
Expand Down
Loading