From c9180d0c210f155cc40403241fc50d77f5fed095 Mon Sep 17 00:00:00 2001 From: "Stefan J. Wernli" Date: Tue, 2 Jun 2026 16:02:58 -0700 Subject: [PATCH 1/2] Remove special override in RCA for `Length`, handle `Length` calls and `Range` types in Partial Eval This change removes the special override for `Length` that was used in RCA, processing it using the regular inferred characteristics. This means that it may show up as "Dynamic Constant" expression, so Partial Eval now needs to handle those calls to `Length` directly rather than assuming they will be handed off to the underlying full evaluator. Likewise, this now allows for "Dynamic Constant" `Range` values, which Partial Eval can handle appropriately. To fix the handling of loops based on these constant values, we now correctly emit the loop only if the loop condition is a "Dynamic Variable" value, rather than on any non-"Static" value. Fixes #3281 Fixes #3282 --- source/compiler/qsc_eval/src/val.rs | 2 +- source/compiler/qsc_partial_eval/src/lib.rs | 86 ++++++++++++++++--- .../qsc_partial_eval/src/tests/misc.rs | 25 ++++++ source/compiler/qsc_rca/src/core.rs | 68 +++------------ .../compiler/qsc_rca/src/tests/overrides.rs | 4 +- 5 files changed, 113 insertions(+), 72 deletions(-) diff --git a/source/compiler/qsc_eval/src/val.rs b/source/compiler/qsc_eval/src/val.rs index b1c392d2a8..24c8b1176c 100644 --- a/source/compiler/qsc_eval/src/val.rs +++ b/source/compiler/qsc_eval/src/val.rs @@ -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 { diff --git a/source/compiler/qsc_partial_eval/src/lib.rs b/source/compiler/qsc_partial_eval/src/lib.rs index b71ce650c3..8bb25b21a7 100644 --- a/source/compiler/qsc_partial_eval/src/lib.rs +++ b/source/compiler/qsc_partial_eval/src/lib.rs @@ -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(), @@ -1813,14 +1812,24 @@ 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, - )) + "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" => { let variable_id = self.resource_manager.next_var(); @@ -2392,7 +2401,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. @@ -3027,6 +3036,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) @@ -4043,6 +4057,52 @@ impl<'a> PartialEvaluator<'a> { Ok(Value::Var(eval_variable)) } + + fn eval_expr_range( + &mut self, + start: Option, + step: Option, + end: Option, + span: PackageSpan, + ) -> Result { + 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)] diff --git a/source/compiler/qsc_partial_eval/src/tests/misc.rs b/source/compiler/qsc_partial_eval/src/tests/misc.rs index 3958dfacf0..7c376eff87 100644 --- a/source/compiler/qsc_partial_eval/src/tests/misc.rs +++ b/source/compiler/qsc_partial_eval/src/tests/misc.rs @@ -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"#]], + ); +} diff --git a/source/compiler/qsc_rca/src/core.rs b/source/compiler/qsc_rca/src/core.rs index 30f0b7da32..6e5b031632 100644 --- a/source/compiler/qsc_rca/src/core.rs +++ b/source/compiler/qsc_rca/src/core.rs @@ -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. @@ -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>, - ) -> 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); @@ -503,7 +469,7 @@ impl<'a> Analyzer<'a> { runtime_features.insert(RuntimeFeatureFlags::CallToCyclicOperation); } - CallComputeKind::Regular(compute_kind) + compute_kind } fn analyze_expr_call_with_static_callee( @@ -511,7 +477,7 @@ impl<'a> Analyzer<'a> { 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); @@ -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) { @@ -554,10 +520,10 @@ 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. @@ -565,7 +531,7 @@ impl<'a> Analyzer<'a> { // 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( @@ -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), } } @@ -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 @@ -2245,11 +2209,6 @@ impl SpecContext { } } -enum CallComputeKind { - Regular(ComputeKind), - Override(ComputeKind), -} - fn derive_intrinsic_function_application_generator_set( callable_context: &CallableContext, ) -> ApplicationGeneratorSet { @@ -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), diff --git a/source/compiler/qsc_rca/src/tests/overrides.rs b/source/compiler/qsc_rca/src/tests/overrides.rs index 9f0fb18527..ba25e571fc 100644 --- a/source/compiler/qsc_rca/src/tests/overrides.rs +++ b/source/compiler/qsc_rca/src/tests/overrides.rs @@ -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: "#]], ); } From 00a4030a501a1e55998e7b1e1d21c17131411f05 Mon Sep 17 00:00:00 2001 From: "Stefan J. Wernli" Date: Tue, 2 Jun 2026 22:39:52 -0700 Subject: [PATCH 2/2] support `IntAsDouble` and `Truncate` on dynamic constants --- source/compiler/qsc_partial_eval/src/lib.rs | 32 +++++++++++++++------ 1 file changed, 24 insertions(+), 8 deletions(-) diff --git a/source/compiler/qsc_partial_eval/src/lib.rs b/source/compiler/qsc_partial_eval/src/lib.rs index 8bb25b21a7..a9cbd3d60b 100644 --- a/source/compiler/qsc_partial_eval/src/lib.rs +++ b/source/compiler/qsc_partial_eval/src/lib.rs @@ -1831,14 +1831,30 @@ impl<'a> PartialEvaluator<'a> { Err(_) => Err(EvalError::ArrayTooLarge(args_span).into()), } } - "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)) - } + "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,