diff --git a/source/qdk_package/qdk/_adaptive_pass.py b/source/qdk_package/qdk/_adaptive_pass.py index f56ba8721cd..bd8bec71a27 100644 --- a/source/qdk_package/qdk/_adaptive_pass.py +++ b/source/qdk_package/qdk/_adaptive_pass.py @@ -612,6 +612,21 @@ def _is_output_recording_label(value: pyqir.Constant): and ty.element.width == 8 ) + @staticmethod + def _is_zero_array(value: pyqir.Value) -> bool: + """Whether `value` is an all-zero array constant (`zeroinitializer`). + + LLVM folds arrays whose elements are all zero (including + `[N x ptr] [ptr inttoptr (i64 0 to ptr), ...]`, since that is `null`) + to `zeroinitializer`, which pyqir exposes as a `Constant` rather + than an `ArrayConstant`. + """ + return ( + isinstance(value, pyqir.Constant) + and isinstance(value.type, pyqir.ArrayType) + and value.is_null + ) + @staticmethod def _is_global_array(global_variable: pyqir.GlobalVariable): init = global_variable.initializer @@ -620,7 +635,9 @@ def _is_global_array(global_variable: pyqir.GlobalVariable): # If the global variable has an initializer, but the initializer value # is not an array, return False. E.g.: This can happens when initializing # a global integer constant. - if not isinstance(init, pyqir.ArrayConstant): + if not isinstance( + init, pyqir.ArrayConstant + ) and not AdaptiveProfilePass._is_zero_array(init): return False # ``[N x i8]`` globals are excluded on purpose: in Adaptive Profile QIR @@ -658,12 +675,12 @@ def _scan_global_arrays(self, mod: pyqir.Module) -> None: # encoding elements. This ensures that forward references between # globals (e.g. @matrix declared before @row0/@row1) are resolved # correctly in Pass 2. - supported_globals: list[tuple[pyqir.GlobalVariable, pyqir.ArrayConstant]] = [] + supported_globals: list[tuple[pyqir.GlobalVariable, pyqir.Constant]] = [] base = len(self.constant_data) for gv in mod.global_variables: if not self._is_global_array(gv): continue - init = cast(pyqir.ArrayConstant, gv.initializer) + init = cast(pyqir.Constant, gv.initializer) self._global_to_address[gv.name] = base base += self._size_in_words(init.type) supported_globals.append((gv, init)) @@ -675,21 +692,25 @@ def _scan_global_arrays(self, mod: pyqir.Module) -> None: self._alloca_ptr = len(self.constant_data) self._memory_size = self._alloca_ptr - def _encode_array_elements(self, arr: pyqir.ArrayConstant, gv_name: str) -> None: + def _encode_array_elements(self, arr: pyqir.Constant, gv_name: str) -> None: """Recursively encode ArrayConstant elements into constant_data. Nested ``ArrayConstant`` elements (e.g. ``[2 x [2 x i32]]``) are - flattened in row-major order. + flattened in row-major order. All-zero arrays (``zeroinitializer``) + have no elements and are encoded as zero words. """ + if self._is_zero_array(arr): + self.constant_data.extend([0] * self._size_in_words(arr.type)) + return mask = (1 << self._int_bits) - 1 - for elem in arr.elements: + for elem in cast(pyqir.ArrayConstant, arr).elements: if isinstance(elem, pyqir.IntConstant): self.constant_data.append(elem.value & mask) elif isinstance(elem, pyqir.FloatConstant): self.constant_data.append( encode_float_as_bits(elem.value, self._bytecode_kind) ) - elif isinstance(elem, pyqir.ArrayConstant): + elif isinstance(elem, pyqir.ArrayConstant) or self._is_zero_array(elem): # Nested array — flatten recursively. self._encode_array_elements(elem, gv_name) elif isinstance(elem, pyqir.GlobalVariable): diff --git a/source/qdk_package/tests/test_adaptive_pass.py b/source/qdk_package/tests/test_adaptive_pass.py index b306d823d19..dffc9f574be 100644 --- a/source/qdk_package/tests/test_adaptive_pass.py +++ b/source/qdk_package/tests/test_adaptive_pass.py @@ -1471,6 +1471,98 @@ def test_byte_string_global_used_as_data_raises(): assert "record_output" in msg +# --------------------------------------------------------------------------- +# Test: all-zero global arrays (folded by LLVM to `zeroinitializer`) +# --------------------------------------------------------------------------- + +ZERO_INITIALIZER_ARRAY_QIR = """\ +%Result = type opaque +%Qubit = type opaque + +@zeros = internal constant [3 x i64] [i64 0, i64 0, i64 0] +@after = internal constant [2 x i64] [i64 7, i64 8] + +define void @ENTRYPOINT__main() #0 { +entry: + %ptr_z = getelementptr inbounds [3 x i64], [3 x i64]* @zeros, i64 0, i64 1 + %ptr_a = getelementptr inbounds [2 x i64], [2 x i64]* @after, i64 0, i64 1 + %vz = load i64, i64* %ptr_z, align 8 + %va = load i64, i64* %ptr_a, align 8 + call void @__quantum__rt__tuple_record_output(i64 0, i8* null) + ret void +} + +declare void @__quantum__rt__tuple_record_output(i64, i8*) + +attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "required_num_qubits"="1" "required_num_results"="0" } +""" + + +def test_zero_initializer_array_constant_data(): + """All-zero arrays (parsed as `zeroinitializer`) are encoded as zeros. + + Subsequent globals must still be placed after the zero-filled region. + """ + r = _run_pass(ZERO_INITIALIZER_ARRAY_QIR) + assert r.constant_data == [0, 0, 0, 7, 8] + + +NESTED_ZERO_INITIALIZER_ARRAY_QIR = """\ +%Result = type opaque +%Qubit = type opaque + +@matrix = internal constant [2 x [2 x i64]] [ + [2 x i64] [i64 0, i64 0], + [2 x i64] [i64 3, i64 4] +] + +define void @ENTRYPOINT__main() #0 { +entry: + %ptr = getelementptr inbounds [2 x [2 x i64]], [2 x [2 x i64]]* @matrix, i64 0, i64 1, i64 0 + %val = load i64, i64* %ptr, align 8 + call void @__quantum__rt__tuple_record_output(i64 0, i8* null) + ret void +} + +declare void @__quantum__rt__tuple_record_output(i64, i8*) + +attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "required_num_qubits"="1" "required_num_results"="0" } +""" + + +def test_nested_zero_initializer_array_constant_data(): + """A zero sub-array inside a nested array is flattened as zeros.""" + r = _run_pass(NESTED_ZERO_INITIALIZER_ARRAY_QIR) + assert r.constant_data == [0, 0, 3, 4] + + +EMPTY_LABEL_QIR = """\ +%Result = type opaque +%Qubit = type opaque + +@empty = internal constant [1 x i8] c"\\00" +@data = internal constant [2 x i64] [i64 42, i64 99] + +define void @ENTRYPOINT__main() #0 { +entry: + %ptr = getelementptr inbounds [2 x i64], [2 x i64]* @data, i64 0, i64 0 + %val = load i64, i64* %ptr, align 8 + call void @__quantum__rt__tuple_record_output(i64 0, i8* null) + ret void +} + +declare void @__quantum__rt__tuple_record_output(i64, i8*) + +attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "required_num_qubits"="1" "required_num_results"="0" } +""" + + +def test_empty_label_zero_initializer_skipped(): + """An empty `[1 x i8]` label (also `zeroinitializer`) is still skipped.""" + r = _run_pass(EMPTY_LABEL_QIR) + assert r.constant_data == [42, 99] + + # --------------------------------------------------------------------------- # Test: mutable global array still works # --------------------------------------------------------------------------- diff --git a/source/qdk_package/tests/test_clifford_simulator.py b/source/qdk_package/tests/test_clifford_simulator.py index 2f5ed3d6879..0d15c004654 100644 --- a/source/qdk_package/tests/test_clifford_simulator.py +++ b/source/qdk_package/tests/test_clifford_simulator.py @@ -150,6 +150,25 @@ def test_program_with_branching_succeeds(): assert len(results) == 1 +def test_program_indexing_single_zero_result_array_succeeds(): + # Regression test for https://github.com/microsoft/qdk/issues/3777. + qsharp.init(target_profile=TargetProfile.Adaptive) + qsharp.eval(""" + operation Main() : Result { + use q = Qubit(); + for result in [M(q)] { + if result == One { + X(q); + } + } + MResetZ(q) + } + """) + ir = qsharp.compile("Main()") + results = run_qir_clifford(str(ir), 1, NoiseConfig()) + assert results == [Result.Zero] + + def test_program_with_unconditional_branching_succeeds(): qir = """ %Result = type opaque