Skip to main content

qir_qis/
lib.rs

1#[cfg(feature = "python")]
2use pyo3::prelude::*;
3#[cfg(feature = "python")]
4use pyo3_stub_gen::define_stub_info_gatherer;
5
6pub mod convert;
7mod decompose;
8mod llvm_verify;
9pub mod opt;
10mod utils;
11
12#[cfg(windows)]
13pub const DEFAULT_OPT_LEVEL: u32 = 0;
14#[cfg(not(windows))]
15pub const DEFAULT_OPT_LEVEL: u32 = 2;
16
17#[cfg(windows)]
18pub const DEFAULT_TARGET: &str = "native";
19#[cfg(not(windows))]
20pub const DEFAULT_TARGET: &str = "aarch64";
21
22/// Maximum number of statically declared qubits accepted by validation and translation.
23///
24/// This safety ceiling prevents untrusted entry-point metadata from driving
25/// effectively unbounded compiler allocations and LLVM instruction generation.
26pub const MAX_STATIC_QUBITS: u32 = 65_535;
27
28/// Maximum number of statically declared results accepted by validation and translation.
29///
30/// This safety ceiling prevents untrusted entry-point metadata from driving
31/// effectively unbounded compiler allocations.
32pub const MAX_STATIC_RESULTS: u32 = 65_535;
33
34mod aux {
35    #![allow(
36        clippy::expect_used,
37        reason = "LLVM builder calls here encode internal lowering invariants; user-facing validation errors are propagated separately"
38    )]
39    #![allow(
40        clippy::indexing_slicing,
41        reason = "LLVM operand and metadata layouts are validated before fixed-position access"
42    )]
43
44    use std::collections::{BTreeMap, BTreeSet, HashMap};
45
46    use crate::{
47        convert::{
48            INIT_QARRAY_FN, add_print_call, build_result_global, checked_qubit_index,
49            convert_globals, create_reset_call, get_index, get_or_create_function,
50            get_required_num_qubits, get_required_num_qubits_strict, get_required_num_results,
51            get_result_vars, get_string_label, handle_tuple_or_array_output,
52            is_reserved_passthrough_name, parse_gep, record_classical_output, replace_rxy_call,
53            replace_rz_call, replace_rzz_call,
54        },
55        decode_llvm_bytes,
56        utils::extract_operands,
57    };
58
59    use inkwell::{
60        AddressSpace,
61        attributes::AttributeLoc,
62        basic_block::BasicBlock,
63        context::Context,
64        module::{Linkage, Module},
65        types::{ArrayType, BasicMetadataTypeEnum, BasicTypeEnum, FunctionType},
66        values::{
67            AnyValue, AsValueRef, BasicMetadataValueEnum, BasicValue, BasicValueEnum,
68            CallSiteValue, FunctionValue, InstructionOpcode, PointerValue,
69        },
70    };
71
72    static ALLOWED_QIS_FNS: [&str; 23] = [
73        // Native gates
74        "__quantum__qis__rxy__body",
75        "__quantum__qis__rz__body",
76        "__quantum__qis__rzz__body",
77        "__quantum__qis__mz__body",
78        "__quantum__qis__mz_leaked__body",
79        "__quantum__qis__reset__body",
80        // mz + reset
81        "__quantum__qis__mresetz__body",
82        // Synonyms for native gates
83        "__quantum__qis__u1q__body", // rxy
84        "__quantum__qis__m__body",   // mz
85        // Decomposed to native gates
86        "__quantum__qis__h__body",
87        "__quantum__qis__x__body",
88        "__quantum__qis__y__body",
89        "__quantum__qis__z__body",
90        "__quantum__qis__s__body",
91        "__quantum__qis__s__adj",
92        "__quantum__qis__t__body",
93        "__quantum__qis__t__adj",
94        "__quantum__qis__rx__body",
95        "__quantum__qis__ry__body",
96        "__quantum__qis__cz__body",
97        "__quantum__qis__cx__body",
98        "__quantum__qis__cnot__body",
99        "__quantum__qis__ccx__body",
100        // Note: barrier instructions with arbitrary arity are validated separately
101    ];
102
103    static BASE_ALLOWED_RT_FNS: [&str; 8] = [
104        "__quantum__rt__read_result",
105        "__quantum__rt__initialize",
106        "__quantum__rt__result_record_output",
107        "__quantum__rt__array_record_output",
108        "__quantum__rt__tuple_record_output",
109        "__quantum__rt__bool_record_output",
110        "__quantum__rt__double_record_output",
111        "__quantum__rt__int_record_output",
112    ];
113
114    #[derive(Clone, Copy, Debug, Default)]
115    pub struct CapabilityFlags {
116        pub dynamic_qubit_management: bool,
117        pub dynamic_result_management: bool,
118        pub arrays: bool,
119    }
120
121    fn is_capability_gated_rt_function(fn_name: &str) -> bool {
122        matches!(
123            fn_name,
124            "__quantum__rt__qubit_allocate"
125                | "__quantum__rt__qubit_release"
126                | "__quantum__rt__result_allocate"
127                | "__quantum__rt__result_release"
128                | "__quantum__rt__qubit_array_allocate"
129                | "__quantum__rt__qubit_array_release"
130                | "__quantum__rt__result_array_allocate"
131                | "__quantum__rt__result_array_release"
132                | "__quantum__rt__result_array_record_output"
133        )
134    }
135
136    fn is_i64_type(type_: BasicMetadataTypeEnum<'_>) -> bool {
137        type_.is_int_type() && type_.into_int_type().get_bit_width() == 64
138    }
139
140    fn is_ptr_type(type_: BasicMetadataTypeEnum<'_>) -> bool {
141        type_.is_pointer_type()
142    }
143
144    fn is_void_return(fn_type: FunctionType<'_>) -> bool {
145        fn_type.get_return_type().is_none()
146    }
147
148    fn is_ptr_return(fn_type: FunctionType<'_>) -> bool {
149        fn_type
150            .get_return_type()
151            .is_some_and(BasicTypeEnum::is_pointer_type)
152    }
153
154    /// Extract label from a full tag string without allocating.
155    /// Handles format "USER:RESULT:tag" by returning the last component if present,
156    /// otherwise returns the full tag.
157    fn extract_label_from_tag(full_tag: &str) -> &str {
158        full_tag
159            .rsplit_once(':')
160            .map_or(full_tag, |(_, label)| label)
161    }
162
163    fn validate_dynamic_rt_signature(
164        fn_name: &str,
165        fn_type: FunctionType<'_>,
166    ) -> Result<(), String> {
167        let params = fn_type.get_param_types();
168        let valid = match fn_name {
169            "__quantum__rt__qubit_allocate" | "__quantum__rt__result_allocate" => {
170                is_ptr_return(fn_type)
171                    && matches!(params.as_slice(), [param] if is_ptr_type(*param))
172            }
173            "__quantum__rt__qubit_release" | "__quantum__rt__result_release" => {
174                is_void_return(fn_type)
175                    && matches!(params.as_slice(), [param] if is_ptr_type(*param))
176            }
177            "__quantum__rt__qubit_array_allocate"
178            | "__quantum__rt__result_array_allocate"
179            | "__quantum__rt__result_array_record_output" => {
180                is_void_return(fn_type)
181                    && matches!(
182                        params.as_slice(),
183                        [len, first_ptr, second_ptr]
184                            if is_i64_type(*len)
185                                && is_ptr_type(*first_ptr)
186                                && is_ptr_type(*second_ptr)
187                    )
188            }
189            "__quantum__rt__qubit_array_release" | "__quantum__rt__result_array_release" => {
190                is_void_return(fn_type)
191                    && matches!(params.as_slice(), [len, ptr] if is_i64_type(*len) && is_ptr_type(*ptr))
192            }
193            _ => true,
194        };
195
196        if valid {
197            Ok(())
198        } else {
199            Err(format!("Malformed QIR RT function declaration: {fn_name}"))
200        }
201    }
202
203    pub fn get_capability_flags(module: &Module) -> CapabilityFlags {
204        let module_flags = collect_module_flags(module);
205        CapabilityFlags {
206            dynamic_qubit_management: module_flag_is_enabled(
207                &module_flags,
208                "dynamic_qubit_management",
209            ),
210            dynamic_result_management: module_flag_is_enabled(
211                &module_flags,
212                "dynamic_result_management",
213            ),
214            arrays: module_flag_is_enabled(&module_flags, "arrays"),
215        }
216    }
217
218    #[cfg(feature = "wasm")]
219    static ALLOWED_QTM_FNS: [&str; 7] = [
220        "___get_current_shot",
221        "___random_seed",
222        "___random_int",
223        "___random_float",
224        "___random_int_bounded",
225        "___random_advance",
226        "___get_wasm_context",
227    ];
228
229    #[cfg(not(feature = "wasm"))]
230    static ALLOWED_QTM_FNS: [&str; 6] = [
231        "___get_current_shot",
232        "___random_seed",
233        "___random_int",
234        "___random_float",
235        "___random_int_bounded",
236        "___random_advance",
237    ];
238
239    #[cfg(not(windows))]
240    pub fn validate_module_layout_and_triple(module: &Module) {
241        let datalayout = module.get_data_layout();
242        let triple = module.get_triple();
243
244        if !datalayout.as_str().is_empty() {
245            log::warn!("QIR module has a data layout: {:?}", datalayout.as_str());
246        }
247        if !triple.as_str().is_empty() {
248            log::warn!("QIR module has a target triple: {:?}", triple.as_str());
249        }
250    }
251
252    #[cfg(windows)]
253    pub const fn validate_module_layout_and_triple(_module: &Module) {
254        // Best-effort warning path only. Avoid unstable getter APIs on Windows,
255        // where these calls have been unreliable in CI; re-checking locally on
256        // Windows Arm64 on March 23, 2026 reproduced STATUS_ACCESS_VIOLATION.
257    }
258
259    fn direct_qubit_operand_positions(fn_name: &str, arg_count: usize) -> Vec<usize> {
260        match fn_name {
261            "__quantum__qis__rxy__body" | "__quantum__qis__u1q__body" => vec![2],
262            "__quantum__qis__rz__body"
263            | "__quantum__qis__rx__body"
264            | "__quantum__qis__ry__body" => {
265                vec![1]
266            }
267            "__quantum__qis__rzz__body" => vec![1, 2],
268            "__quantum__qis__cz__body"
269            | "__quantum__qis__cx__body"
270            | "__quantum__qis__cnot__body" => vec![0, 1],
271            "__quantum__qis__ccx__body" => vec![0, 1, 2],
272            "__quantum__qis__h__body"
273            | "__quantum__qis__x__body"
274            | "__quantum__qis__y__body"
275            | "__quantum__qis__z__body"
276            | "__quantum__qis__s__body"
277            | "__quantum__qis__s__adj"
278            | "__quantum__qis__t__body"
279            | "__quantum__qis__t__adj"
280            | "__quantum__qis__mz__body"
281            | "__quantum__qis__m__body"
282            | "__quantum__qis__mz_leaked__body"
283            | "__quantum__qis__reset__body"
284            | "__quantum__qis__mresetz__body" => vec![0],
285            name if name.starts_with("__quantum__qis__barrier") && name.ends_with("__body") => {
286                (0..arg_count).collect()
287            }
288            _ => Vec::new(),
289        }
290    }
291
292    fn is_user_ir_defined_helper_name(name: &str) -> bool {
293        !name.starts_with("__quantum__qis__")
294            && !name.starts_with("__quantum__rt__")
295            && !name.starts_with("qir_qis.")
296            && !name.starts_with("___")
297            && !name.starts_with("print_")
298            && name != "panic"
299    }
300
301    fn find_pointer_param_index(
302        function: FunctionValue<'_>,
303        ptr: PointerValue<'_>,
304    ) -> Option<usize> {
305        function
306            .get_param_iter()
307            .enumerate()
308            .find_map(|(idx, param)| {
309                let BasicValueEnum::PointerValue(param_ptr) = param else {
310                    return None;
311                };
312                (param_ptr.as_value_ref() == ptr.as_value_ref()).then_some(idx)
313            })
314    }
315
316    fn infer_ir_defined_helper_qubit_params(
317        module: &Module,
318        errors: &mut Vec<String>,
319    ) -> HashMap<String, BTreeSet<usize>> {
320        let mut helper_qubit_params: HashMap<String, BTreeSet<usize>> = module
321            .get_functions()
322            .filter(|function| function.count_basic_blocks() > 0)
323            .filter_map(|function| {
324                function
325                    .get_name()
326                    .to_str()
327                    .ok()
328                    .filter(|name| is_user_ir_defined_helper_name(name))
329                    .map(|name| (name.to_string(), BTreeSet::new()))
330            })
331            .collect();
332
333        loop {
334            let mut changed = false;
335            for function in module
336                .get_functions()
337                .filter(|function| function.count_basic_blocks() > 0)
338            {
339                let Ok(function_name) = function.get_name().to_str() else {
340                    continue;
341                };
342                if !is_user_ir_defined_helper_name(function_name) {
343                    continue;
344                }
345                let mut discovered = helper_qubit_params
346                    .get(function_name)
347                    .cloned()
348                    .unwrap_or_default();
349
350                for bb in function.get_basic_blocks() {
351                    for instr in bb.get_instructions() {
352                        let Ok(call) = CallSiteValue::try_from(instr) else {
353                            continue;
354                        };
355                        let Some(callee_name) = call.get_called_fn_value().and_then(|f| {
356                            f.as_global_value()
357                                .get_name()
358                                .to_str()
359                                .ok()
360                                .map(ToOwned::to_owned)
361                        }) else {
362                            continue;
363                        };
364
365                        let call_args = match extract_operands(&instr) {
366                            Ok(args) => args,
367                            Err(err) => {
368                                errors.push(format!(
369                                    "Failed to inspect `{callee_name}` call in `{function_name}`: {err}"
370                                ));
371                                continue;
372                            }
373                        };
374
375                        let qubit_positions = {
376                            let direct_positions =
377                                direct_qubit_operand_positions(&callee_name, call_args.len());
378                            if direct_positions.is_empty() {
379                                helper_qubit_params
380                                    .get(&callee_name)
381                                    .map(|positions| positions.iter().copied().collect())
382                                    .unwrap_or_default()
383                            } else {
384                                direct_positions
385                            }
386                        };
387
388                        for pos in qubit_positions {
389                            let Some(BasicValueEnum::PointerValue(ptr)) =
390                                call_args.get(pos).copied()
391                            else {
392                                continue;
393                            };
394                            if let Some(param_idx) = find_pointer_param_index(function, ptr) {
395                                discovered.insert(param_idx);
396                            }
397                        }
398                    }
399                }
400
401                if helper_qubit_params.get(function_name) != Some(&discovered) {
402                    helper_qubit_params.insert(function_name.to_string(), discovered);
403                    changed = true;
404                }
405            }
406
407            if !changed {
408                return helper_qubit_params;
409            }
410        }
411    }
412
413    fn validate_static_qubit_call_operands(
414        function: FunctionValue<'_>,
415        callee_name: &str,
416        qubit_positions: impl IntoIterator<Item = usize>,
417        call_args: &[BasicValueEnum<'_>],
418        required_num_qubits: u32,
419        report_shape_errors: bool,
420        errors: &mut Vec<String>,
421    ) {
422        for pos in qubit_positions {
423            let Some(arg) = call_args.get(pos).copied() else {
424                if report_shape_errors {
425                    errors.push(format!(
426                        "Call to `{callee_name}` is missing qubit operand {pos}"
427                    ));
428                }
429                continue;
430            };
431            let BasicValueEnum::PointerValue(qubit_ptr) = arg else {
432                if report_shape_errors {
433                    errors.push(format!(
434                        "Call to `{callee_name}` has a non-pointer qubit operand"
435                    ));
436                }
437                continue;
438            };
439            if find_pointer_param_index(function, qubit_ptr).is_some() {
440                continue;
441            }
442
443            let Ok(qubit_idx) = get_index(qubit_ptr) else {
444                continue;
445            };
446            if let Err(err) = checked_qubit_index(qubit_idx, required_num_qubits) {
447                errors.push(format!(
448                    "Invalid static qubit handle passed to `{callee_name}`: {err}"
449                ));
450            }
451        }
452    }
453
454    pub fn validate_static_qubit_helper_usage(
455        module: &Module,
456        entry_fn: FunctionValue,
457        errors: &mut Vec<String>,
458    ) {
459        if get_capability_flags(module).dynamic_qubit_management {
460            return;
461        }
462        let required_num_qubits = match get_required_num_qubits_strict(entry_fn) {
463            Ok(required_num_qubits) => required_num_qubits,
464            Err(err) => {
465                errors.push(err);
466                return;
467            }
468        };
469
470        let previous_error_count = errors.len();
471        let helper_qubit_params = infer_ir_defined_helper_qubit_params(module, errors);
472        if errors.len() > previous_error_count {
473            return;
474        }
475
476        for function in module.get_functions() {
477            for bb in function.get_basic_blocks() {
478                for instr in bb.get_instructions() {
479                    let Ok(call) = CallSiteValue::try_from(instr) else {
480                        continue;
481                    };
482                    let Some(callee_name) = call.get_called_fn_value().and_then(|f| {
483                        f.as_global_value()
484                            .get_name()
485                            .to_str()
486                            .ok()
487                            .map(ToOwned::to_owned)
488                    }) else {
489                        continue;
490                    };
491                    let call_args = match extract_operands(&instr) {
492                        Ok(args) => args,
493                        Err(err) => {
494                            errors.push(format!("Failed to inspect `{callee_name}` call: {err}"));
495                            continue;
496                        }
497                    };
498
499                    let direct_positions =
500                        direct_qubit_operand_positions(&callee_name, call_args.len());
501                    if !direct_positions.is_empty() {
502                        validate_static_qubit_call_operands(
503                            function,
504                            &callee_name,
505                            direct_positions,
506                            &call_args,
507                            required_num_qubits,
508                            false,
509                            errors,
510                        );
511                    }
512
513                    if let Some(qubit_positions) = helper_qubit_params.get(&callee_name)
514                        && !qubit_positions.is_empty()
515                    {
516                        validate_static_qubit_call_operands(
517                            function,
518                            &callee_name,
519                            qubit_positions.iter().copied(),
520                            &call_args,
521                            required_num_qubits,
522                            true,
523                            errors,
524                        );
525                    }
526                }
527            }
528        }
529    }
530
531    #[derive(Default)]
532    struct ModuleFlagErrors {
533        entries: Vec<ModuleFlagErrorEntry>,
534    }
535
536    enum ModuleFlagErrorEntry {
537        MissingRequired(String),
538        MissingOrUnsupported(String),
539        Message(String),
540    }
541
542    impl ModuleFlagErrors {
543        fn push_message(&mut self, message: String) {
544            self.entries.push(ModuleFlagErrorEntry::Message(message));
545        }
546
547        fn push_missing_required(&mut self, flag_name: &str) {
548            self.entries
549                .push(ModuleFlagErrorEntry::MissingRequired(flag_name.to_string()));
550        }
551
552        fn push_missing_or_unsupported(&mut self, flag_name: &str) {
553            self.entries
554                .push(ModuleFlagErrorEntry::MissingOrUnsupported(
555                    flag_name.to_string(),
556                ));
557        }
558
559        fn into_messages(self) -> Vec<String> {
560            let mut messages = Vec::new();
561            let mut entries = self.entries.into_iter().peekable();
562            while let Some(entry) = entries.next() {
563                match entry {
564                    ModuleFlagErrorEntry::Message(message) => messages.push(message),
565                    ModuleFlagErrorEntry::MissingRequired(flag_name) => {
566                        let mut flag_names = vec![flag_name];
567                        while let Some(ModuleFlagErrorEntry::MissingRequired(next_flag_name)) =
568                            entries.peek()
569                        {
570                            push_unique_flag_name(&mut flag_names, next_flag_name);
571                            let _ = entries.next();
572                        }
573                        if let Some(message) = format_grouped_module_flag_error(
574                            "Missing required module flag",
575                            "Missing required module flags",
576                            &flag_names,
577                        ) {
578                            messages.push(message);
579                        }
580                    }
581                    ModuleFlagErrorEntry::MissingOrUnsupported(flag_name) => {
582                        let mut flag_names = vec![flag_name];
583                        while let Some(ModuleFlagErrorEntry::MissingOrUnsupported(next_flag_name)) =
584                            entries.peek()
585                        {
586                            push_unique_flag_name(&mut flag_names, next_flag_name);
587                            let _ = entries.next();
588                        }
589                        if let Some(message) = format_grouped_module_flag_error(
590                            "Missing or unsupported module flag",
591                            "Missing or unsupported module flags",
592                            &flag_names,
593                        ) {
594                            messages.push(message);
595                        }
596                    }
597                }
598            }
599
600            messages
601        }
602    }
603
604    fn push_unique_flag_name(flag_names: &mut Vec<String>, flag_name: &str) {
605        if flag_names.iter().all(|existing| existing != flag_name) {
606            flag_names.push(flag_name.to_string());
607        }
608    }
609
610    fn format_grouped_module_flag_error(
611        singular_prefix: &str,
612        plural_prefix: &str,
613        flag_names: &[String],
614    ) -> Option<String> {
615        match flag_names {
616            [] => None,
617            [flag_name] => Some(format!("{singular_prefix}: {flag_name}")),
618            _ => Some(format!("{plural_prefix}: {}", flag_names.join(", "))),
619        }
620    }
621
622    pub fn validate_module_flags(module: &Module, errors: &mut Vec<String>) {
623        let module_flags = collect_module_flags(module);
624        let mut module_flag_errors = ModuleFlagErrors::default();
625        if module_flags.has_malformed_name() {
626            module_flag_errors.push_message(
627                "Malformed llvm.module.flags entry: expected metadata string name".to_string(),
628            );
629        }
630        validate_qir_version_flags(&module_flags, &mut module_flag_errors);
631        validate_exact_module_flag(
632            &module_flags,
633            "dynamic_qubit_management",
634            &["i1 false", "i1 true"],
635            &mut module_flag_errors,
636        );
637        validate_exact_module_flag(
638            &module_flags,
639            "dynamic_result_management",
640            &["i1 false", "i1 true"],
641            &mut module_flag_errors,
642        );
643        validate_optional_module_flag(
644            &module_flags,
645            "arrays",
646            &["i1 false", "i1 true"],
647            &mut module_flag_errors,
648        );
649        errors.extend(module_flag_errors.into_messages());
650    }
651
652    fn validate_qir_version_flags(module_flags: &ModuleFlags, errors: &mut ModuleFlagErrors) {
653        let major_values = required_module_flag_values(module_flags, "qir_major_version", errors);
654        let minor_values = required_module_flag_values(module_flags, "qir_minor_version", errors);
655        let (Some(major_values), Some(minor_values)) = (major_values, minor_values) else {
656            return;
657        };
658
659        for major_value in major_values {
660            for minor_value in minor_values {
661                if matches!(
662                    (major_value.as_str(), minor_value.as_str()),
663                    ("i32 1", "i32 0") | ("i32 2", "i32 0" | "i32 1")
664                ) {
665                    return;
666                }
667            }
668        }
669
670        if !major_values
671            .iter()
672            .any(|major| matches!(major.as_str(), "i32 1" | "i32 2"))
673        {
674            errors.push_message(
675                "Unsupported qir_major_version: expected one of i32 1, i32 2".to_string(),
676            );
677            return;
678        }
679
680        errors.push_message(
681            "Unsupported qir_minor_version: expected i32 0 for QIR 1 or one of i32 0, i32 1 for QIR 2"
682                .to_string(),
683        );
684    }
685
686    fn required_module_flag_values<'a>(
687        module_flags: &'a ModuleFlags,
688        flag_name: &str,
689        errors: &mut ModuleFlagErrors,
690    ) -> Option<&'a [String]> {
691        match module_flags.get(flag_name) {
692            Some(values) => Some(values),
693            None if module_flags.is_malformed(flag_name) => {
694                errors.push_missing_or_unsupported(flag_name);
695                None
696            }
697            None => {
698                errors.push_missing_required(flag_name);
699                None
700            }
701        }
702    }
703
704    pub struct ModuleFlags {
705        values: BTreeMap<String, Vec<String>>,
706        has_malformed_name: bool,
707        malformed: BTreeSet<String>,
708    }
709
710    impl ModuleFlags {
711        pub fn get(&self, flag_name: &str) -> Option<&[String]> {
712            self.values.get(flag_name).map(Vec::as_slice)
713        }
714
715        const fn has_malformed_name(&self) -> bool {
716            self.has_malformed_name
717        }
718
719        fn is_malformed(&self, flag_name: &str) -> bool {
720            self.malformed.contains(flag_name)
721        }
722    }
723
724    pub fn collect_module_flags(module: &Module) -> ModuleFlags {
725        let mut values = BTreeMap::<String, Vec<String>>::new();
726        let mut has_malformed_name = false;
727        let mut malformed = BTreeSet::new();
728
729        for entry in module.get_global_metadata("llvm.module.flags") {
730            let Some(node_values) = entry.get_node_values() else {
731                continue;
732            };
733            let flag_name = extract_module_flag_name(&node_values);
734            if flag_name.is_none() {
735                has_malformed_name = true;
736            }
737
738            if node_values.len() != 3 {
739                if let Some(flag_name) = flag_name {
740                    malformed.insert(flag_name);
741                }
742                continue;
743            }
744
745            let Some(flag_name) = flag_name else {
746                continue;
747            };
748
749            let Some(flag_value) = node_values
750                .get(2)
751                .copied()
752                .and_then(format_module_flag_value)
753            else {
754                malformed.insert(flag_name);
755                continue;
756            };
757
758            values.entry(flag_name).or_default().push(flag_value);
759        }
760
761        ModuleFlags {
762            values,
763            has_malformed_name,
764            malformed,
765        }
766    }
767
768    fn extract_module_flag_name(values: &[BasicMetadataValueEnum]) -> Option<String> {
769        match values.get(1).copied()? {
770            BasicMetadataValueEnum::MetadataValue(value) => value
771                .get_string_value()
772                .and_then(decode_llvm_bytes)
773                .map(str::to_owned),
774            BasicMetadataValueEnum::ArrayValue(_)
775            | BasicMetadataValueEnum::IntValue(_)
776            | BasicMetadataValueEnum::FloatValue(_)
777            | BasicMetadataValueEnum::PointerValue(_)
778            | BasicMetadataValueEnum::StructValue(_)
779            | BasicMetadataValueEnum::VectorValue(_)
780            | BasicMetadataValueEnum::ScalableVectorValue(_) => None,
781        }
782    }
783
784    fn format_module_flag_value(value: BasicMetadataValueEnum) -> Option<String> {
785        match value {
786            BasicMetadataValueEnum::IntValue(value) => {
787                let bit_width = value.get_type().get_bit_width();
788                let raw_value = value.get_zero_extended_constant()?;
789                if bit_width == 1 {
790                    Some(format!(
791                        "i1 {}",
792                        if raw_value == 0 { "false" } else { "true" }
793                    ))
794                } else {
795                    Some(format!("i{bit_width} {raw_value}"))
796                }
797            }
798            BasicMetadataValueEnum::MetadataValue(value) => value
799                .get_string_value()
800                .and_then(decode_llvm_bytes)
801                .map(|string| format!("!\"{string}\"")),
802            BasicMetadataValueEnum::ArrayValue(_)
803            | BasicMetadataValueEnum::FloatValue(_)
804            | BasicMetadataValueEnum::PointerValue(_)
805            | BasicMetadataValueEnum::StructValue(_)
806            | BasicMetadataValueEnum::VectorValue(_)
807            | BasicMetadataValueEnum::ScalableVectorValue(_) => None,
808        }
809    }
810
811    fn module_flag_is_enabled(module_flags: &ModuleFlags, flag_name: &str) -> bool {
812        module_flags
813            .get(flag_name)
814            .is_some_and(|values| values.iter().any(|value| value == "i1 true"))
815    }
816
817    fn validate_optional_module_flag(
818        module_flags: &ModuleFlags,
819        flag_name: &str,
820        expected_values: &[&str],
821        errors: &mut ModuleFlagErrors,
822    ) {
823        let Some(actual_values) = module_flags.get(flag_name) else {
824            if module_flags.is_malformed(flag_name) {
825                errors.push_missing_or_unsupported(flag_name);
826            }
827            return;
828        };
829
830        if actual_values
831            .iter()
832            .any(|actual| expected_values.contains(&actual.as_str()))
833        {
834            return;
835        }
836
837        let expected = if expected_values.len() == 1 {
838            expected_values.first().unwrap_or(&"").to_string()
839        } else {
840            format!("one of {}", expected_values.join(", "))
841        };
842        errors.push_message(format!("Unsupported {flag_name}: expected {expected}"));
843    }
844    fn validate_exact_module_flag(
845        module_flags: &ModuleFlags,
846        flag_name: &str,
847        expected_values: &[&str],
848        errors: &mut ModuleFlagErrors,
849    ) {
850        let Some(actual_values) = module_flags.get(flag_name) else {
851            if module_flags.is_malformed(flag_name) {
852                errors.push_missing_or_unsupported(flag_name);
853                return;
854            }
855            errors.push_missing_required(flag_name);
856            return;
857        };
858
859        if actual_values
860            .iter()
861            .any(|actual| expected_values.contains(&actual.as_str()))
862        {
863            return;
864        }
865
866        let expected = format!("one of {}", expected_values.join(", "));
867        errors.push_message(format!("Unsupported {flag_name}: expected {expected}"));
868    }
869
870    fn get_fixed_pointer_array_len(
871        array_ptr: PointerValue<'_>,
872        opname: &str,
873    ) -> Result<u64, String> {
874        let array_backing_error =
875            || format!("{opname} requires a fixed-size backing array allocated as [N x ptr]");
876        let Some(instr) = array_ptr.as_instruction_value() else {
877            return Err(array_backing_error());
878        };
879        let opcode = instr.get_opcode();
880
881        if opcode == InstructionOpcode::Alloca {
882            let allocated_type = instr
883                .get_allocated_type()
884                .map_err(|_err| array_backing_error())?;
885            let BasicTypeEnum::ArrayType(array_type) = allocated_type else {
886                return Err(array_backing_error());
887            };
888            if !matches!(array_type.get_element_type(), BasicTypeEnum::PointerType(_)) {
889                return Err(array_backing_error());
890            }
891            return Ok(u64::from(array_type.len()));
892        }
893
894        if opcode == InstructionOpcode::BitCast || opcode == InstructionOpcode::AddrSpaceCast {
895            return instr
896                .get_operand(0)
897                .and_then(inkwell::values::Operand::value)
898                .map(BasicValueEnum::into_pointer_value)
899                .ok_or_else(array_backing_error)
900                .and_then(|backing_ptr| get_fixed_pointer_array_len(backing_ptr, opname));
901        }
902
903        if opcode == InstructionOpcode::GetElementPtr {
904            for operand_idx in 1..instr.get_num_operands() {
905                let Some(operand) = instr.get_operand(operand_idx) else {
906                    return Err(array_backing_error());
907                };
908                let inkwell::values::Operand::Value(value) = operand else {
909                    return Err(array_backing_error());
910                };
911                let idx = value
912                    .into_int_value()
913                    .get_zero_extended_constant()
914                    .ok_or_else(array_backing_error)?;
915                if idx != 0 {
916                    return Err(array_backing_error());
917                }
918            }
919
920            return instr
921                .get_operand(0)
922                .and_then(inkwell::values::Operand::value)
923                .map(BasicValueEnum::into_pointer_value)
924                .ok_or_else(array_backing_error)
925                .and_then(|backing_ptr| get_fixed_pointer_array_len(backing_ptr, opname));
926        }
927
928        Err(array_backing_error())
929    }
930
931    /// Standalone dynamic-array-allocation-backing check, retained for narrow
932    /// targeted unit tests. Production validation runs this same logic as part
933    /// of the single consolidated traversal in [`validate_qir_call_sites`].
934    #[cfg(test)]
935    pub fn validate_dynamic_array_allocation_backing(module: &Module, errors: &mut Vec<String>) {
936        for fun in module.get_functions() {
937            for bb in fun.get_basic_blocks() {
938                for instr in bb.get_instructions() {
939                    let Ok(call) = CallSiteValue::try_from(instr) else {
940                        continue;
941                    };
942                    let Some(callee) = call.get_called_fn_value() else {
943                        continue;
944                    };
945                    let callee_global = callee.as_global_value();
946                    let callee_name = callee_global.get_name();
947                    let Some(fn_name) = callee_name.to_str().ok() else {
948                        continue;
949                    };
950                    if !matches!(
951                        fn_name,
952                        "__quantum__rt__qubit_array_allocate"
953                            | "__quantum__rt__qubit_array_release"
954                            | "__quantum__rt__result_array_allocate"
955                            | "__quantum__rt__result_array_release"
956                            | "__quantum__rt__result_array_record_output"
957                    ) {
958                        continue;
959                    }
960
961                    let mut call_args: Vec<BasicValueEnum> = match extract_operands(&instr) {
962                        Ok(args) => args,
963                        Err(err) => {
964                            errors.push(format!("Failed to inspect {fn_name} operands: {err}"));
965                            continue;
966                        }
967                    };
968                    call_args.truncate(call.count_arguments() as usize);
969                    if call_args.len() < 2 {
970                        errors.push(format!(
971                            "{fn_name} requires a constant array length and backing array pointer"
972                        ));
973                        continue;
974                    }
975                    let BasicValueEnum::IntValue(_) = call_args[0] else {
976                        errors.push(format!(
977                            "{fn_name} requires a constant array length and backing array pointer"
978                        ));
979                        continue;
980                    };
981                    let requested_len = match extract_const_len(call_args[0], fn_name) {
982                        Ok(len) => len,
983                        Err(err) => {
984                            errors.push(err);
985                            continue;
986                        }
987                    };
988                    let BasicValueEnum::PointerValue(backing_ptr) = call_args[1] else {
989                        errors.push(format!(
990                            "{fn_name} requires a fixed-size backing array allocated as [N x ptr]"
991                        ));
992                        continue;
993                    };
994                    let backing_len = match get_fixed_pointer_array_len(backing_ptr, fn_name) {
995                        Ok(len) => len,
996                        Err(err) => {
997                            errors.push(err);
998                            continue;
999                        }
1000                    };
1001                    if requested_len != backing_len {
1002                        errors.push(format!(
1003                            "{fn_name} requires a fixed-size backing array whose requested length {requested_len} does not match backing array length {backing_len}"
1004                        ));
1005                    }
1006                    if fn_name == "__quantum__rt__result_array_record_output"
1007                        && requested_len > i32::MAX as u64
1008                    {
1009                        errors.push(format!(
1010                            "{fn_name} requires an array length that fits in i32 for RESULT_ARRAY output"
1011                        ));
1012                    }
1013                }
1014            }
1015        }
1016    }
1017
1018    /// Single-pass validation used by [`crate::validate_qir`], replacing what
1019    /// were previously five separate full-module traversals: function-level
1020    /// checks (allow-listed QIS/RT/Qtm functions, barrier arity, IR-defined
1021    /// function shape), result-slot usage, the per-call-site half of static
1022    /// qubit-helper usage, dynamic result-allocation placement, dynamic
1023    /// array-allocation backing, and capability-gated runtime function usage.
1024    ///
1025    /// Each of those checks used to independently walk every function, basic
1026    /// block, and instruction in the module, re-deriving the same call-site
1027    /// data (callee name and call arguments) each time. This function performs
1028    /// a single walk and shares that per-call-site data across all of the
1029    /// checks, while producing the same errors (grouped in the same relative
1030    /// order per check) that calling the individual passes in sequence used to
1031    /// produce.
1032    #[allow(
1033        clippy::too_many_lines,
1034        reason = "single consolidated validation pass mirrors five previously-separate functions"
1035    )]
1036    pub fn validate_qir_call_sites(
1037        module: &Module,
1038        entry_fn: FunctionValue,
1039        _wasm_fns: &BTreeMap<String, u64>,
1040        capability_flags: CapabilityFlags,
1041        errors: &mut Vec<String>,
1042    ) -> Vec<String> {
1043        // Errors are collected per-check so that, once merged back into
1044        // `errors`, they preserve the same relative grouping/order that the
1045        // previously-separate sequential passes produced.
1046        let mut functions_errors = Vec::new();
1047        let mut result_slot_errors = Vec::new();
1048        let mut static_qubit_errors = Vec::new();
1049        let mut dynamic_result_placement_errors = Vec::new();
1050        let mut dynamic_array_backing_errors = Vec::new();
1051        let mut capability_errors = Vec::new();
1052
1053        // --- Setup shared by the per-call-site checks below (mirrors what each
1054        // individual pass computed before its own traversal). ---
1055        let required_num_qubits_for_barrier = get_required_num_qubits(entry_fn);
1056
1057        let required_num_results = if entry_fn
1058            .get_string_attribute(AttributeLoc::Function, "required_num_results")
1059            .is_some()
1060        {
1061            match get_required_num_results(entry_fn) {
1062                Ok(required) => Some(required),
1063                Err(err) => {
1064                    result_slot_errors.push(err);
1065                    None
1066                }
1067            }
1068        } else {
1069            None
1070        };
1071
1072        let static_qubit_ctx = if capability_flags.dynamic_qubit_management {
1073            None
1074        } else {
1075            match get_required_num_qubits_strict(entry_fn) {
1076                Ok(required_num_qubits) => {
1077                    let mut infer_errors = Vec::new();
1078                    let helper_qubit_params =
1079                        infer_ir_defined_helper_qubit_params(module, &mut infer_errors);
1080                    if infer_errors.is_empty() {
1081                        Some((required_num_qubits, helper_qubit_params))
1082                    } else {
1083                        static_qubit_errors.extend(infer_errors);
1084                        None
1085                    }
1086                }
1087                Err(err) => {
1088                    static_qubit_errors.push(err);
1089                    None
1090                }
1091            }
1092        };
1093
1094        for fun in module.get_functions() {
1095            // --- validate_functions (function-level only; no instruction walk) ---
1096            if fun != entry_fn {
1097                let fn_name = fun.get_name().to_str().unwrap_or("");
1098                if fn_name.starts_with("qir_qis.") {
1099                    functions_errors.push(format!(
1100                        "Input QIR must not define internal helper function: {fn_name}"
1101                    ));
1102                } else if fn_name.starts_with("__quantum__qis__") {
1103                    let is_barrier = parse_barrier_arity(fn_name).is_ok_and(|arity| {
1104                        if let Some(max_qubits) = required_num_qubits_for_barrier
1105                            && let Ok(arity_u32) = u32::try_from(arity)
1106                            && arity_u32 > max_qubits
1107                        {
1108                            functions_errors.push(format!(
1109                    "Barrier arity {arity} exceeds module's required_num_qubits ({max_qubits})"
1110                ));
1111                        }
1112                        true
1113                    });
1114
1115                    if !is_barrier && !ALLOWED_QIS_FNS.contains(&fn_name) {
1116                        functions_errors.push(format!("Unsupported QIR QIS function: {fn_name}"));
1117                    }
1118                } else if fn_name.starts_with("__quantum__rt__") {
1119                    if !BASE_ALLOWED_RT_FNS.contains(&fn_name)
1120                        && !is_capability_gated_rt_function(fn_name)
1121                    {
1122                        functions_errors.push(format!("Unsupported QIR RT function: {fn_name}"));
1123                    } else if is_capability_gated_rt_function(fn_name)
1124                        && let Err(err) = validate_dynamic_rt_signature(fn_name, fun.get_type())
1125                    {
1126                        functions_errors.push(err);
1127                    }
1128                } else if fn_name.starts_with("___") {
1129                    if !ALLOWED_QTM_FNS.contains(&fn_name) {
1130                        functions_errors.push(format!("Unsupported Qtm QIS function: {fn_name}"));
1131                    }
1132                } else if fun.count_basic_blocks() > 0 {
1133                    // IR defined functions
1134                    if fn_name == "main" {
1135                        functions_errors
1136                            .push("IR defined function cannot be called `main`".to_string());
1137                    }
1138                    if fun
1139                        .get_type()
1140                        .get_return_type()
1141                        .is_some_and(BasicTypeEnum::is_pointer_type)
1142                    {
1143                        functions_errors
1144                            .push(format!("Function `{fn_name}` cannot return a pointer type"));
1145                    }
1146                } else {
1147                    log::debug!(
1148                        "External function `{fn_name}` found, leaving as-is for downstream processing"
1149                    );
1150                }
1151            }
1152
1153            let allowed_result_alloc_block = if fun == entry_fn {
1154                fun.get_first_basic_block()
1155            } else {
1156                None
1157            };
1158
1159            for bb in fun.get_basic_blocks() {
1160                for instr in bb.get_instructions() {
1161                    let Ok(call) = CallSiteValue::try_from(instr) else {
1162                        continue;
1163                    };
1164                    let Some(callee_name) = call.get_called_fn_value().and_then(|f| {
1165                        f.as_global_value()
1166                            .get_name()
1167                            .to_str()
1168                            .ok()
1169                            .map(ToOwned::to_owned)
1170                    }) else {
1171                        continue;
1172                    };
1173
1174                    // --- validate_dynamic_result_allocation_placement ---
1175                    if matches!(
1176                        callee_name.as_str(),
1177                        "__quantum__rt__result_allocate" | "__quantum__rt__result_array_allocate"
1178                    ) && Some(bb) != allowed_result_alloc_block
1179                    {
1180                        dynamic_result_placement_errors.push(format!(
1181                            "{callee_name} is only supported in the entry block because dynamic result slots are lowered to stack storage"
1182                        ));
1183                    }
1184
1185                    // --- validate_capability_usage ---
1186                    match callee_name.as_str() {
1187                        "__quantum__rt__qubit_array_allocate"
1188                        | "__quantum__rt__qubit_array_release"
1189                            if !capability_flags.arrays
1190                                || !capability_flags.dynamic_qubit_management =>
1191                        {
1192                            capability_errors.push(format!(
1193                                "{callee_name} requires both `arrays=true` and `dynamic_qubit_management=true`"
1194                            ));
1195                        }
1196                        "__quantum__rt__result_array_allocate"
1197                        | "__quantum__rt__result_array_release"
1198                        | "__quantum__rt__result_array_record_output"
1199                            if !capability_flags.arrays
1200                                || !capability_flags.dynamic_result_management =>
1201                        {
1202                            capability_errors.push(format!(
1203                                "{callee_name} requires both `arrays=true` and `dynamic_result_management=true`"
1204                            ));
1205                        }
1206                        "__quantum__rt__qubit_allocate" | "__quantum__rt__qubit_release"
1207                            if !capability_flags.dynamic_qubit_management =>
1208                        {
1209                            capability_errors.push(format!(
1210                                "{callee_name} requires `dynamic_qubit_management=true`"
1211                            ));
1212                        }
1213                        "__quantum__rt__result_allocate" | "__quantum__rt__result_release"
1214                            if !capability_flags.dynamic_result_management =>
1215                        {
1216                            capability_errors.push(format!(
1217                                "{callee_name} requires `dynamic_result_management=true`"
1218                            ));
1219                        }
1220                        _ => {}
1221                    }
1222
1223                    // Determine, without extracting operands, whether any check
1224                    // below needs this call site's arguments.
1225                    let result_operand_index = match callee_name.as_str() {
1226                        "__quantum__qis__mz__body"
1227                        | "__quantum__qis__m__body"
1228                        | "__quantum__qis__mresetz__body" => Some(1),
1229                        "__quantum__rt__read_result" | "__quantum__rt__result_record_output" => {
1230                            Some(0)
1231                        }
1232                        _ => None,
1233                    };
1234                    let result_slot_relevant = matches!(
1235                        (required_num_results, result_operand_index),
1236                        (Some(_), Some(_))
1237                    );
1238                    let direct_qubit_positions = static_qubit_ctx.as_ref().map(|_| {
1239                        direct_qubit_operand_positions(
1240                            &callee_name,
1241                            call.count_arguments() as usize,
1242                        )
1243                    });
1244                    let helper_qubit_positions = static_qubit_ctx
1245                        .as_ref()
1246                        .and_then(|(_, helper_qubit_params)| helper_qubit_params.get(&callee_name));
1247                    let static_qubit_operand_inspection_relevant = static_qubit_ctx.is_some();
1248                    let array_backing_relevant = matches!(
1249                        callee_name.as_str(),
1250                        "__quantum__rt__qubit_array_allocate"
1251                            | "__quantum__rt__qubit_array_release"
1252                            | "__quantum__rt__result_array_allocate"
1253                            | "__quantum__rt__result_array_release"
1254                            | "__quantum__rt__result_array_record_output"
1255                    );
1256
1257                    if matches!(
1258                        (
1259                            result_slot_relevant,
1260                            static_qubit_ctx.as_ref(),
1261                            array_backing_relevant
1262                        ),
1263                        (false, None, false)
1264                    ) {
1265                        continue;
1266                    }
1267
1268                    let mut call_args = match extract_operands(&instr) {
1269                        Ok(args) => args,
1270                        Err(err) => {
1271                            if result_slot_relevant {
1272                                result_slot_errors
1273                                    .push(format!("Failed to inspect `{callee_name}` call: {err}"));
1274                            }
1275                            if static_qubit_operand_inspection_relevant {
1276                                static_qubit_errors
1277                                    .push(format!("Failed to inspect `{callee_name}` call: {err}"));
1278                            }
1279                            if array_backing_relevant {
1280                                dynamic_array_backing_errors.push(format!(
1281                                    "Failed to inspect {callee_name} operands: {err}"
1282                                ));
1283                            }
1284                            continue;
1285                        }
1286                    };
1287                    call_args.truncate(call.count_arguments() as usize);
1288
1289                    // --- validate_result_slot_usage ---
1290                    if let (Some(required_num_results), Some(result_operand_index)) =
1291                        (required_num_results, result_operand_index)
1292                    {
1293                        match call_args.get(result_operand_index).copied() {
1294                            None => result_slot_errors.push(format!(
1295                                "Call to `{callee_name}` is missing a result operand"
1296                            )),
1297                            Some(BasicValueEnum::PointerValue(result_ptr)) => {
1298                                match get_index(result_ptr) {
1299                                    Ok(result_idx) => {
1300                                        if let Err(err) =
1301                                            checked_result_index(result_idx, required_num_results)
1302                                        {
1303                                            result_slot_errors.push(err);
1304                                        }
1305                                    }
1306                                    Err(err) => result_slot_errors.push(format!(
1307                                        "Failed to inspect result operand for `{callee_name}`: {err}"
1308                                    )),
1309                                }
1310                            }
1311                            Some(_) => result_slot_errors.push(format!(
1312                                "Call to `{callee_name}` has a non-pointer result operand"
1313                            )),
1314                        }
1315                    }
1316
1317                    // --- validate_static_qubit_helper_usage (per-call-site pass) ---
1318                    if let Some((required_num_qubits, _)) = static_qubit_ctx.as_ref() {
1319                        if let Some(direct_positions) = direct_qubit_positions
1320                            && !direct_positions.is_empty()
1321                        {
1322                            validate_static_qubit_call_operands(
1323                                fun,
1324                                &callee_name,
1325                                direct_positions,
1326                                &call_args,
1327                                *required_num_qubits,
1328                                false,
1329                                &mut static_qubit_errors,
1330                            );
1331                        }
1332                        if let Some(qubit_positions) = helper_qubit_positions
1333                            && !qubit_positions.is_empty()
1334                        {
1335                            validate_static_qubit_call_operands(
1336                                fun,
1337                                &callee_name,
1338                                qubit_positions.iter().copied(),
1339                                &call_args,
1340                                *required_num_qubits,
1341                                true,
1342                                &mut static_qubit_errors,
1343                            );
1344                        }
1345                    }
1346
1347                    // --- validate_dynamic_array_allocation_backing ---
1348                    if array_backing_relevant {
1349                        let Some((length_operand, backing_operand)) =
1350                            call_args.first().copied().zip(call_args.get(1).copied())
1351                        else {
1352                            dynamic_array_backing_errors.push(format!(
1353                                "{callee_name} requires a constant array length and backing array pointer"
1354                            ));
1355                            continue;
1356                        };
1357                        if matches!(length_operand, BasicValueEnum::IntValue(_)) {
1358                            match extract_const_len(length_operand, &callee_name) {
1359                                Ok(requested_len) => {
1360                                    let BasicValueEnum::PointerValue(backing_ptr) = backing_operand
1361                                    else {
1362                                        dynamic_array_backing_errors.push(format!(
1363                                            "{callee_name} requires a fixed-size backing array allocated as [N x ptr]"
1364                                        ));
1365                                        continue;
1366                                    };
1367                                    match get_fixed_pointer_array_len(backing_ptr, &callee_name) {
1368                                        Ok(backing_len) => {
1369                                            if requested_len != backing_len {
1370                                                dynamic_array_backing_errors.push(format!(
1371                                                    "{callee_name} requires a fixed-size backing array whose requested length {requested_len} does not match backing array length {backing_len}"
1372                                                ));
1373                                            }
1374                                            if callee_name
1375                                                == "__quantum__rt__result_array_record_output"
1376                                                && requested_len > i32::MAX as u64
1377                                            {
1378                                                dynamic_array_backing_errors.push(format!(
1379                                                    "{callee_name} requires an array length that fits in i32 for RESULT_ARRAY output"
1380                                                ));
1381                                            }
1382                                        }
1383                                        Err(err) => dynamic_array_backing_errors.push(err),
1384                                    }
1385                                }
1386                                Err(err) => dynamic_array_backing_errors.push(err),
1387                            }
1388                        } else {
1389                            dynamic_array_backing_errors.push(format!(
1390                                "{callee_name} requires a constant array length and backing array pointer"
1391                            ));
1392                        }
1393                    }
1394                }
1395            }
1396        }
1397
1398        errors.extend(functions_errors);
1399        errors.extend(result_slot_errors);
1400        errors.extend(static_qubit_errors);
1401        errors.extend(dynamic_result_placement_errors);
1402        errors.extend(dynamic_array_backing_errors);
1403        capability_errors
1404    }
1405
1406    // SAFETY: `ProcessCallArgs` is created and consumed synchronously within a single
1407    // `process_call_instruction` invocation. The raw pointers below point at
1408    // stack-owned state from `process_entry_function` that outlives the handler
1409    // call, and handlers never persist those pointers beyond the call.
1410    struct ProcessCallArgs<'ctx> {
1411        ctx: &'ctx Context,
1412        module: *const Module<'ctx>,
1413        instr: inkwell::values::InstructionValue<'ctx>,
1414        fn_name: String,
1415        #[allow(dead_code, reason = "reserved field keeps WASM API shape stable")]
1416        wasm_fns: *const BTreeMap<String, u64>,
1417        passthrough_calls: *const BTreeSet<String>,
1418        qubit_array: Option<PointerValue<'ctx>>,
1419        qubit_array_type: Option<ArrayType<'ctx>>,
1420        capability_flags: CapabilityFlags,
1421        global_mapping: *mut HashMap<String, inkwell::values::GlobalValue<'ctx>>,
1422        result_ssa: *mut Vec<Option<(BasicValueEnum<'ctx>, Option<BasicValueEnum<'ctx>>)>>,
1423    }
1424
1425    /// Primary translation loop over the entry function for translation to QIS.
1426    pub fn process_entry_function<'ctx>(
1427        ctx: &'ctx Context,
1428        module: &Module<'ctx>,
1429        entry_fn: FunctionValue<'ctx>,
1430        wasm_fns: &BTreeMap<String, u64>,
1431        passthrough_calls: &BTreeSet<String>,
1432        qubit_array: Option<PointerValue<'ctx>>,
1433        capability_flags: CapabilityFlags,
1434    ) -> Result<(), String> {
1435        let mut global_mapping = convert_globals(ctx, module)?;
1436
1437        if global_mapping.is_empty() {
1438            log::warn!("No globals found in QIR module");
1439        }
1440        let mut result_ssa = if capability_flags.dynamic_result_management {
1441            Vec::new()
1442        } else {
1443            get_result_vars(entry_fn)?
1444        };
1445        let qubit_array_type = if capability_flags.dynamic_qubit_management {
1446            None
1447        } else {
1448            Some(
1449                ctx.i64_type()
1450                    .array_type(get_required_num_qubits_strict(entry_fn)?),
1451            )
1452        };
1453
1454        for bb in entry_fn.get_basic_blocks() {
1455            // Snapshot instructions before rewriting calls. Some rewrite paths
1456            // erase/replace instructions, which can invalidate in-place iterators.
1457            let instructions: Vec<_> = bb.get_instructions().collect();
1458            for instr in instructions {
1459                let Ok(call) = CallSiteValue::try_from(instr) else {
1460                    continue;
1461                };
1462                if let Some(fn_name) = call.get_called_fn_value().and_then(|f| {
1463                    f.as_global_value()
1464                        .get_name()
1465                        .to_str()
1466                        .ok()
1467                        .map(ToOwned::to_owned)
1468                }) {
1469                    let args = ProcessCallArgs {
1470                        ctx,
1471                        module,
1472                        instr,
1473                        fn_name,
1474                        wasm_fns: std::ptr::from_ref(wasm_fns),
1475                        passthrough_calls: std::ptr::from_ref(passthrough_calls),
1476                        qubit_array,
1477                        qubit_array_type,
1478                        capability_flags,
1479                        global_mapping: &raw mut global_mapping,
1480                        result_ssa: &raw mut result_ssa,
1481                    };
1482                    process_call_instruction(args)?;
1483                }
1484            }
1485        }
1486
1487        Ok(())
1488    }
1489
1490    fn process_call_instruction(mut args: ProcessCallArgs<'_>) -> Result<(), String> {
1491        let call = CallSiteValue::try_from(args.instr)
1492            .map_err(|()| "Instruction is not a call site".to_string())?;
1493        if let Some(f) = call.get_called_fn_value() {
1494            let passthrough_calls = passthrough_calls_ref(&args);
1495            let is_passthrough = passthrough_calls.contains(args.fn_name.as_str());
1496            let is_ir_defined = f.count_basic_blocks() > 0;
1497            match args.fn_name.as_str() {
1498                name if name.starts_with("__quantum__qis__") => {
1499                    if matches!(try_handle_qis_call(&args)?, BuiltinCallHandling::Handled) {
1500                        return Ok(());
1501                    }
1502                    if is_ir_defined {
1503                        // Under LLVM 21, decomposition functions may remain as IR-defined calls
1504                        // rather than being fully inlined at this stage. Allow these calls to
1505                        // pass through; their bodies are lowered by process_ir_defined_q_fns.
1506                        return Ok(());
1507                    }
1508                    if is_passthrough {
1509                        return passthrough_external_call(&args);
1510                    }
1511                    if check_downstream_attribute(f, args.fn_name.as_str()) {
1512                        return Ok(());
1513                    }
1514                    return Err(format!("Unsupported QIR QIS function: {}", args.fn_name));
1515                }
1516                name if name.starts_with("__quantum__rt__") => {
1517                    if matches!(try_handle_rt_call(&mut args)?, BuiltinCallHandling::Handled) {
1518                        return Ok(());
1519                    }
1520                    if is_passthrough && !is_ir_defined {
1521                        return passthrough_external_call(&args);
1522                    }
1523                    if check_downstream_attribute(f, args.fn_name.as_str()) {
1524                        return Ok(());
1525                    }
1526                    return Err(format!("Unsupported QIR RT function: {}", args.fn_name));
1527                }
1528                name if name.starts_with("___") => return handle_qtm_call(&args),
1529                _ => {}
1530            }
1531
1532            // IR defined function calls
1533            if is_ir_defined {
1534                // INIT_QARRAY_FN is a frequently invoked helper for array initialization;
1535                // skipping debug logs for it avoids excessive log noise while preserving
1536                // useful debug information for other IR-defined functions.
1537                if args.fn_name != INIT_QARRAY_FN {
1538                    log::debug!("IR defined function `{}`: {}", args.fn_name, f.get_type());
1539                }
1540                if args.fn_name == "main" {
1541                    return Err("IR defined function cannot be called `main`".to_string());
1542                }
1543                return Ok(());
1544            }
1545
1546            if is_passthrough {
1547                return passthrough_external_call(&args);
1548            }
1549
1550            if check_downstream_attribute(f, args.fn_name.as_str()) {
1551                return Ok(());
1552            }
1553
1554            log::error!("Unknown external function: {}", args.fn_name);
1555            return Err(format!("Unsupported function: {}", args.fn_name));
1556        }
1557
1558        match args.fn_name.as_str() {
1559            name if name.starts_with("__quantum__qis__") => {
1560                if matches!(try_handle_qis_call(&args)?, BuiltinCallHandling::Handled) {
1561                    return Ok(());
1562                }
1563                Err(format!("Unsupported QIR QIS function: {}", args.fn_name))
1564            }
1565            name if name.starts_with("__quantum__rt__") => {
1566                if matches!(try_handle_rt_call(&mut args)?, BuiltinCallHandling::Handled) {
1567                    return Ok(());
1568                }
1569                Err(format!("Unsupported QIR RT function: {}", args.fn_name))
1570            }
1571            name if name.starts_with("___") => handle_qtm_call(&args),
1572            _ => {
1573                log::error!("Unsupported function: {}", args.fn_name);
1574                Err(format!("Unsupported function: {}", args.fn_name))
1575            }
1576        }
1577    }
1578
1579    /// Returns `true` if `f` carries a `cudaq-fnid` or `wasm` attribute,
1580    /// indicating it should be left as-is for downstream processing.
1581    fn check_downstream_attribute(f: FunctionValue<'_>, fn_name: &str) -> bool {
1582        if f.get_string_attribute(AttributeLoc::Function, "cudaq-fnid")
1583            .is_some()
1584        {
1585            log::debug!("GPU function `{fn_name}` found, leaving as-is for downstream processing");
1586            return true;
1587        }
1588        if f.get_string_attribute(AttributeLoc::Function, "wasm")
1589            .is_some()
1590        {
1591            log::debug!("WASM function `{fn_name}` found, leaving as-is for downstream processing");
1592            return true;
1593        }
1594        false
1595    }
1596
1597    fn passthrough_external_call(args: &ProcessCallArgs<'_>) -> Result<(), String> {
1598        if is_reserved_passthrough_name(args.fn_name.as_str()) {
1599            return Err(format!(
1600                "Pass-through function `{}` uses a reserved converter output name",
1601                args.fn_name
1602            ));
1603        }
1604        log::debug!(
1605            "Pass-through function `{}` found, leaving as-is for downstream processing",
1606            args.fn_name
1607        );
1608        Ok(())
1609    }
1610
1611    enum BuiltinCallHandling {
1612        Handled,
1613        Unsupported,
1614    }
1615
1616    fn try_handle_qis_call(args: &ProcessCallArgs<'_>) -> Result<BuiltinCallHandling, String> {
1617        let required_num_qubits = args
1618            .qubit_array_type
1619            .map_or(0, inkwell::types::ArrayType::len);
1620        match args.fn_name.as_str() {
1621            "__quantum__qis__rxy__body" => {
1622                replace_rxy_call(
1623                    args.ctx,
1624                    module_ref(args),
1625                    args.instr,
1626                    args.capability_flags.dynamic_qubit_management,
1627                    required_num_qubits,
1628                )?;
1629                Ok(BuiltinCallHandling::Handled)
1630            }
1631            "__quantum__qis__rz__body" => {
1632                replace_rz_call(
1633                    args.ctx,
1634                    module_ref(args),
1635                    args.instr,
1636                    args.capability_flags.dynamic_qubit_management,
1637                    required_num_qubits,
1638                )?;
1639                Ok(BuiltinCallHandling::Handled)
1640            }
1641            "__quantum__qis__rzz__body" => {
1642                replace_rzz_call(
1643                    args.ctx,
1644                    module_ref(args),
1645                    args.instr,
1646                    args.capability_flags.dynamic_qubit_management,
1647                    required_num_qubits,
1648                )?;
1649                Ok(BuiltinCallHandling::Handled)
1650            }
1651            "__quantum__qis__u1q__body" => {
1652                log::info!(
1653                    "`__quantum__qis__u1q__body` used, synonym for `__quantum__qis__rxy__body`"
1654                );
1655                replace_rxy_call(
1656                    args.ctx,
1657                    module_ref(args),
1658                    args.instr,
1659                    args.capability_flags.dynamic_qubit_management,
1660                    required_num_qubits,
1661                )?;
1662                Ok(BuiltinCallHandling::Handled)
1663            }
1664            "__quantum__qis__mz__body"
1665            | "__quantum__qis__m__body"
1666            | "__quantum__qis__mresetz__body" => {
1667                handle_mz_call(
1668                    args.ctx,
1669                    args.module.cast::<()>(),
1670                    &args.instr,
1671                    args.fn_name.as_str(),
1672                    args.capability_flags,
1673                    args.qubit_array,
1674                    args.qubit_array_type,
1675                    args.result_ssa.cast::<()>(),
1676                )?;
1677                Ok(BuiltinCallHandling::Handled)
1678            }
1679            "__quantum__qis__mz_leaked__body" => {
1680                handle_mz_leaked_call(args)?;
1681                Ok(BuiltinCallHandling::Handled)
1682            }
1683            "__quantum__qis__reset__body" => {
1684                handle_reset_call(args)?;
1685                Ok(BuiltinCallHandling::Handled)
1686            }
1687            name if name.starts_with("__quantum__qis__barrier") && name.ends_with("__body") => {
1688                handle_barrier_call(args)?;
1689                Ok(BuiltinCallHandling::Handled)
1690            }
1691            _ => Ok(BuiltinCallHandling::Unsupported),
1692        }
1693    }
1694
1695    fn try_handle_rt_call(args: &mut ProcessCallArgs<'_>) -> Result<BuiltinCallHandling, String> {
1696        match args.fn_name.as_str() {
1697            "__quantum__rt__initialize" => {
1698                args.instr.erase_from_basic_block();
1699                Ok(BuiltinCallHandling::Handled)
1700            }
1701            "__quantum__rt__qubit_allocate" => {
1702                lower_dynamic_qubit_allocate(args)?;
1703                Ok(BuiltinCallHandling::Handled)
1704            }
1705            "__quantum__rt__qubit_release" => {
1706                lower_dynamic_qubit_release(args)?;
1707                Ok(BuiltinCallHandling::Handled)
1708            }
1709            "__quantum__rt__qubit_array_allocate" => {
1710                lower_dynamic_qubit_array_allocate(args)?;
1711                Ok(BuiltinCallHandling::Handled)
1712            }
1713            "__quantum__rt__qubit_array_release" => {
1714                lower_dynamic_qubit_array_release(args)?;
1715                Ok(BuiltinCallHandling::Handled)
1716            }
1717            "__quantum__rt__result_allocate" => {
1718                lower_dynamic_result_allocate(args)?;
1719                Ok(BuiltinCallHandling::Handled)
1720            }
1721            "__quantum__rt__result_release" => {
1722                lower_dynamic_result_release(args)?;
1723                Ok(BuiltinCallHandling::Handled)
1724            }
1725            "__quantum__rt__result_array_allocate" => {
1726                lower_dynamic_result_array_allocate(args)?;
1727                Ok(BuiltinCallHandling::Handled)
1728            }
1729            "__quantum__rt__result_array_release" => {
1730                lower_dynamic_result_array_release(args)?;
1731                Ok(BuiltinCallHandling::Handled)
1732            }
1733            "__quantum__rt__result_array_record_output" => {
1734                lower_dynamic_result_array_record_output(args)?;
1735                Ok(BuiltinCallHandling::Handled)
1736            }
1737            "__quantum__rt__read_result" | "__quantum__rt__result_record_output" => {
1738                handle_read_result_call(
1739                    args.ctx,
1740                    args.module.cast::<()>(),
1741                    &args.instr,
1742                    args.fn_name.as_str(),
1743                    args.capability_flags,
1744                    args.global_mapping.cast::<()>(),
1745                    args.result_ssa.cast::<()>(),
1746                )?;
1747                Ok(BuiltinCallHandling::Handled)
1748            }
1749            "__quantum__rt__tuple_record_output" | "__quantum__rt__array_record_output" => {
1750                let fn_name = args.fn_name.clone();
1751                handle_tuple_or_array_output(
1752                    args.ctx,
1753                    module_ref(args),
1754                    args.instr,
1755                    unsafe { &mut *args.global_mapping },
1756                    fn_name.as_str(),
1757                )?;
1758                Ok(BuiltinCallHandling::Handled)
1759            }
1760            "__quantum__rt__bool_record_output"
1761            | "__quantum__rt__int_record_output"
1762            | "__quantum__rt__double_record_output" => {
1763                handle_classical_record_output(args)?;
1764                Ok(BuiltinCallHandling::Handled)
1765            }
1766            _ => Ok(BuiltinCallHandling::Unsupported),
1767        }
1768    }
1769
1770    fn handle_qtm_call(args: &ProcessCallArgs<'_>) -> Result<(), String> {
1771        match args.fn_name.as_str() {
1772            "___get_current_shot" => {
1773                handle_get_current_shot(args)?;
1774            }
1775            "___random_seed" => {
1776                handle_random_seed(args)?;
1777            }
1778            "___random_int" => {
1779                handle_random_int(args)?;
1780            }
1781            "___random_float" => {
1782                handle_random_float(args)?;
1783            }
1784            "___random_int_bounded" => {
1785                handle_random_int_bounded(args)?;
1786            }
1787            "___random_advance" => {
1788                handle_random_advance(args)?;
1789            }
1790            "___get_wasm_context" => {
1791                // External context calls are left as-is for downstream processing
1792                log::debug!("___get_wasm_context found, leaving as-is for downstream processing");
1793            }
1794            "___barrier" => {
1795                return Err("Unsupported Qtm QIS function: ___barrier".to_string());
1796            }
1797            _ => {
1798                // Ignore already converted Qtm QIS functions
1799                log::trace!("Ignoring Qtm QIS function: {}", args.fn_name);
1800            }
1801        }
1802        Ok(())
1803    }
1804
1805    fn get_qubit_handle<'ctx>(
1806        ctx: &'ctx Context,
1807        capability_flags: CapabilityFlags,
1808        qubit_array: Option<PointerValue<'ctx>>,
1809        qubit_array_type: Option<ArrayType<'ctx>>,
1810        builder: &inkwell::builder::Builder<'ctx>,
1811        qubit_ptr: PointerValue<'ctx>,
1812    ) -> Result<BasicValueEnum<'ctx>, String> {
1813        if capability_flags.dynamic_qubit_management {
1814            return builder
1815                .build_ptr_to_int(qubit_ptr, ctx.i64_type(), "qbit")
1816                .map(BasicValueEnum::from)
1817                .map_err(|e| format!("Failed to convert qubit pointer to handle: {e}"));
1818        }
1819
1820        let qubit_array = qubit_array.ok_or("Missing static qubit array for qubit lookup")?;
1821        let qubit_array_type =
1822            qubit_array_type.ok_or("Missing static qubit array type for qubit lookup")?;
1823        let i64_type = ctx.i64_type();
1824        let index = get_index(qubit_ptr)?;
1825        let index = checked_qubit_index(index, qubit_array_type.len())?;
1826        let index_val = i64_type.const_int(index, false);
1827        let elem_ptr = unsafe {
1828            builder.build_gep(
1829                qubit_array_type,
1830                qubit_array,
1831                &[i64_type.const_zero(), index_val],
1832                "",
1833            )
1834        }
1835        .map_err(|e| format!("Failed to build GEP for qubit handle: {e}"))?;
1836        builder
1837            .build_load(i64_type, elem_ptr, "qbit")
1838            .map_err(|e| format!("Failed to build load for qubit handle: {e}"))
1839    }
1840
1841    const fn module_ref<'ctx>(args: &ProcessCallArgs<'ctx>) -> &'ctx Module<'ctx> {
1842        // SAFETY: `args.module` points to the borrowed module passed into
1843        // `process_entry_function`, which outlives all handler calls.
1844        unsafe { &*args.module }
1845    }
1846
1847    const fn passthrough_calls_ref<'a>(args: &'a ProcessCallArgs<'_>) -> &'a BTreeSet<String> {
1848        // SAFETY: `args.passthrough_calls` points to the borrowed set passed into
1849        // `process_entry_function`, which outlives all handler calls.
1850        unsafe { &*args.passthrough_calls }
1851    }
1852
1853    fn get_or_create_qalloc_fail_global<'ctx>(
1854        ctx: &'ctx Context,
1855        module: &Module<'ctx>,
1856    ) -> inkwell::values::GlobalValue<'ctx> {
1857        let panic_msg = ctx.const_string(b".EXIT:INT:No more qubits available to allocate.", false);
1858        let panic_arr_ty = panic_msg.get_type();
1859        module.get_global("e_qalloc_fail").unwrap_or_else(|| {
1860            let global = module.add_global(panic_arr_ty, None, "e_qalloc_fail");
1861            global.set_initializer(&panic_msg);
1862            global.set_linkage(Linkage::Private);
1863            global.set_constant(true);
1864            global
1865        })
1866    }
1867
1868    struct DynamicQubitAllocatePaths<'ctx> {
1869        fail: BasicBlock<'ctx>,
1870        fail_store: BasicBlock<'ctx>,
1871        fail_panic: BasicBlock<'ctx>,
1872        success: BasicBlock<'ctx>,
1873        store_ok: BasicBlock<'ctx>,
1874        ret_ok: BasicBlock<'ctx>,
1875    }
1876
1877    fn build_dynamic_qubit_allocate_fail_path<'ctx>(
1878        ctx: &'ctx Context,
1879        module: &Module<'ctx>,
1880        builder: &inkwell::builder::Builder<'ctx>,
1881        ptr_type: inkwell::types::PointerType<'ctx>,
1882        out_err: PointerValue<'ctx>,
1883        paths: &DynamicQubitAllocatePaths<'ctx>,
1884    ) {
1885        builder.position_at_end(paths.fail);
1886        let out_err_is_null =
1887            build_ptr_is_null(ctx, builder, out_err, "out_err_int").expect("out_err null check");
1888        builder
1889            .build_conditional_branch(out_err_is_null, paths.fail_panic, paths.fail_store)
1890            .expect("branch fail handler");
1891
1892        builder.position_at_end(paths.fail_store);
1893        let _ = builder.build_store(out_err, ctx.bool_type().const_all_ones());
1894        builder
1895            .build_return(Some(&ptr_type.const_zero()))
1896            .expect("return null qubit");
1897
1898        builder.position_at_end(paths.fail_panic);
1899        let err_global = get_or_create_qalloc_fail_global(ctx, module);
1900        let err_ty = err_global
1901            .get_initializer()
1902            .expect("panic global initializer")
1903            .into_array_value()
1904            .get_type();
1905        let err_gep = unsafe {
1906            builder.build_gep(
1907                err_ty,
1908                err_global.as_pointer_value(),
1909                &[ctx.i64_type().const_zero(), ctx.i64_type().const_zero()],
1910                "err_gep",
1911            )
1912        }
1913        .expect("panic msg gep");
1914        let panic_fn = get_or_create_function(
1915            module,
1916            "panic",
1917            ctx.void_type()
1918                .fn_type(&[ctx.i32_type().into(), ptr_type.into()], false),
1919        );
1920        let _ = builder.build_call(
1921            panic_fn,
1922            &[ctx.i32_type().const_int(1001, false).into(), err_gep.into()],
1923            "",
1924        );
1925        builder.build_unreachable().expect("unreachable");
1926    }
1927
1928    fn build_dynamic_qubit_allocate_success_path<'ctx>(
1929        ctx: &'ctx Context,
1930        builder: &inkwell::builder::Builder<'ctx>,
1931        ptr_type: inkwell::types::PointerType<'ctx>,
1932        out_err: PointerValue<'ctx>,
1933        qid: inkwell::values::IntValue<'ctx>,
1934        paths: &DynamicQubitAllocatePaths<'ctx>,
1935    ) {
1936        builder.position_at_end(paths.success);
1937        let out_err_is_null =
1938            build_ptr_is_null(ctx, builder, out_err, "out_err_int_ok").expect("out_err null check");
1939        builder
1940            .build_conditional_branch(out_err_is_null, paths.ret_ok, paths.store_ok)
1941            .expect("branch success handler");
1942
1943        builder.position_at_end(paths.store_ok);
1944        let _ = builder.build_store(out_err, ctx.bool_type().const_zero());
1945        builder
1946            .build_unconditional_branch(paths.ret_ok)
1947            .expect("jump ret");
1948
1949        builder.position_at_end(paths.ret_ok);
1950        let ptr_val = builder
1951            .build_int_to_ptr(qid, ptr_type, "qubit_ptr")
1952            .expect("int to ptr");
1953        builder
1954            .build_return(Some(&ptr_val))
1955            .expect("return qubit ptr");
1956    }
1957
1958    struct DynamicQubitArrayAllocateBlocks<'ctx> {
1959        loop_header: BasicBlock<'ctx>,
1960        loop_body: BasicBlock<'ctx>,
1961        loop_exit: BasicBlock<'ctx>,
1962        continue_alloc: BasicBlock<'ctx>,
1963        maybe_rollback: BasicBlock<'ctx>,
1964        rollback: BasicBlock<'ctx>,
1965        rollback_done: BasicBlock<'ctx>,
1966    }
1967
1968    struct PointerArrayReleaseHelperArgs<'ctx> {
1969        function: FunctionValue<'ctx>,
1970        len: inkwell::values::IntValue<'ctx>,
1971        array_ptr: PointerValue<'ctx>,
1972        release_fn: FunctionValue<'ctx>,
1973        ptr_elem_type: inkwell::types::PointerType<'ctx>,
1974        gep_error: &'static str,
1975        load_error: &'static str,
1976        release_error: &'static str,
1977    }
1978
1979    fn create_dynamic_qubit_array_allocate_blocks<'ctx>(
1980        ctx: &'ctx Context,
1981        function: FunctionValue<'ctx>,
1982    ) -> DynamicQubitArrayAllocateBlocks<'ctx> {
1983        DynamicQubitArrayAllocateBlocks {
1984            loop_header: ctx.append_basic_block(function, "loop_header"),
1985            loop_body: ctx.append_basic_block(function, "loop_body"),
1986            loop_exit: ctx.append_basic_block(function, "loop_exit"),
1987            continue_alloc: ctx.append_basic_block(function, "continue_alloc"),
1988            maybe_rollback: ctx.append_basic_block(function, "maybe_rollback"),
1989            rollback: ctx.append_basic_block(function, "rollback"),
1990            rollback_done: ctx.append_basic_block(function, "rollback_done"),
1991        }
1992    }
1993
1994    fn build_dynamic_qubit_array_allocate_rollback<'ctx>(
1995        ctx: &'ctx Context,
1996        builder: &inkwell::builder::Builder<'ctx>,
1997        idx: inkwell::values::IntValue<'ctx>,
1998        array_ptr: PointerValue<'ctx>,
1999        release_array_fn: FunctionValue<'ctx>,
2000        rollback: BasicBlock<'ctx>,
2001        rollback_done: BasicBlock<'ctx>,
2002    ) {
2003        builder.position_at_end(rollback);
2004        let rollback_len = builder
2005            .build_int_add(idx, ctx.i64_type().const_int(1, false), "rollback_len")
2006            .expect("rollback len");
2007        let _ = builder
2008            .build_call(
2009                release_array_fn,
2010                &[rollback_len.into(), array_ptr.into()],
2011                "",
2012            )
2013            .expect("rollback release");
2014        builder
2015            .build_unconditional_branch(rollback_done)
2016            .expect("jump rollback_done");
2017    }
2018
2019    fn get_bool_cl_array_type(ctx: &Context) -> inkwell::types::StructType<'_> {
2020        ctx.struct_type(
2021            &[
2022                ctx.i32_type().into(),
2023                ctx.i32_type().into(),
2024                ctx.ptr_type(AddressSpace::default()).into(),
2025                ctx.ptr_type(AddressSpace::default()).into(),
2026            ],
2027            true,
2028        )
2029    }
2030
2031    fn build_dynamic_result_array_values<'ctx>(
2032        ctx: &'ctx Context,
2033        function: FunctionValue<'ctx>,
2034        builder: &inkwell::builder::Builder<'ctx>,
2035        len: inkwell::values::IntValue<'ctx>,
2036        array_ptr: PointerValue<'ctx>,
2037        read_fn: FunctionValue<'ctx>,
2038    ) -> Result<PointerValue<'ctx>, String> {
2039        let ptr_type = ctx.ptr_type(AddressSpace::default());
2040        let bool_arr = builder
2041            .build_array_alloca(ctx.bool_type(), len, "result_arr_data")
2042            .map_err(|e| format!("Failed to allocate result array data: {e}"))?;
2043        build_dynamic_array_loop(ctx, function, builder, len, |builder, idx| {
2044            let elem_ptr = unsafe { builder.build_gep(ptr_type, array_ptr, &[idx], "elem_ptr") }
2045                .map_err(|e| format!("Failed to build result record array GEP: {e}"))?;
2046            let result_ptr = builder
2047                .build_load(ptr_type, elem_ptr, "result_ptr")
2048                .map_err(|e| format!("Failed to load result pointer: {e}"))?;
2049            let bool_call = builder
2050                .build_call(read_fn, &[result_ptr.into()], "result_bool")
2051                .map_err(|e| format!("Failed to read result array element: {e}"))?;
2052            let bool_val = match bool_call.try_as_basic_value() {
2053                inkwell::values::ValueKind::Basic(bv) => bv,
2054                inkwell::values::ValueKind::Instruction(_) => {
2055                    return Err("Dynamic result read helper did not return a bool".to_string());
2056                }
2057            };
2058            let out_ptr =
2059                unsafe { builder.build_gep(ctx.bool_type(), bool_arr, &[idx], "result_bool_ptr") }
2060                    .map_err(|e| format!("Failed to index result bool array: {e}"))?;
2061            let _ = builder
2062                .build_store(out_ptr, bool_val)
2063                .map_err(|e| format!("Failed to store result bool array element: {e}"))?;
2064            Ok(())
2065        })?;
2066        Ok(bool_arr)
2067    }
2068
2069    fn build_dynamic_result_array_descriptor<'ctx>(
2070        ctx: &'ctx Context,
2071        builder: &inkwell::builder::Builder<'ctx>,
2072        len: inkwell::values::IntValue<'ctx>,
2073        bool_arr: PointerValue<'ctx>,
2074    ) -> Result<PointerValue<'ctx>, String> {
2075        let ptr_type = ctx.ptr_type(AddressSpace::default());
2076        let array_desc_type = get_bool_cl_array_type(ctx);
2077        let array_desc = builder
2078            .build_alloca(array_desc_type, "result_arr_desc")
2079            .map_err(|e| format!("Failed to allocate result array descriptor: {e}"))?;
2080        let x_ptr = builder
2081            .build_struct_gep(array_desc_type, array_desc, 0, "result_arr_x")
2082            .map_err(|e| format!("Failed to build result array length GEP: {e}"))?;
2083        let y_ptr = builder
2084            .build_struct_gep(array_desc_type, array_desc, 1, "result_arr_y")
2085            .map_err(|e| format!("Failed to build result array rank GEP: {e}"))?;
2086        let data_ptr = builder
2087            .build_struct_gep(array_desc_type, array_desc, 2, "result_arr_data_ptr")
2088            .map_err(|e| format!("Failed to build result array data GEP: {e}"))?;
2089        let mask_ptr = builder
2090            .build_struct_gep(array_desc_type, array_desc, 3, "result_arr_mask_ptr")
2091            .map_err(|e| format!("Failed to build result array mask GEP: {e}"))?;
2092        let mask_zero = builder
2093            .build_alloca(ctx.i32_type(), "result_arr_mask")
2094            .map_err(|e| format!("Failed to allocate result array mask: {e}"))?;
2095        let _ = builder
2096            .build_store(mask_zero, ctx.i32_type().const_zero())
2097            .map_err(|e| format!("Failed to initialize result array mask: {e}"))?;
2098        let len_i32 = builder
2099            .build_int_truncate(len, ctx.i32_type(), "result_arr_len")
2100            .map_err(|e| format!("Failed to truncate result array length: {e}"))?;
2101        let _ = builder
2102            .build_store(x_ptr, len_i32)
2103            .map_err(|e| format!("Failed to store result array length: {e}"))?;
2104        let _ = builder
2105            .build_store(y_ptr, ctx.i32_type().const_int(1, false))
2106            .map_err(|e| format!("Failed to store result array rank: {e}"))?;
2107        let data_as_ptr = builder
2108            .build_bit_cast(bool_arr, ptr_type, "result_arr_data_cast")
2109            .map_err(|e| format!("Failed to cast result array data pointer: {e}"))?;
2110        let _ = builder
2111            .build_store(data_ptr, data_as_ptr)
2112            .map_err(|e| format!("Failed to store result array data pointer: {e}"))?;
2113        let mask_as_ptr = builder
2114            .build_bit_cast(mask_zero, ptr_type, "result_arr_mask_cast")
2115            .map_err(|e| format!("Failed to cast result array mask pointer: {e}"))?;
2116        let _ = builder
2117            .build_store(mask_ptr, mask_as_ptr)
2118            .map_err(|e| format!("Failed to store result array mask pointer: {e}"))?;
2119        Ok(array_desc)
2120    }
2121
2122    struct DynamicResultSlotPtrs<'ctx> {
2123        state: PointerValue<'ctx>,
2124        cached: PointerValue<'ctx>,
2125        future: PointerValue<'ctx>,
2126    }
2127
2128    fn get_dynamic_result_slot_ptrs<'ctx>(
2129        builder: &inkwell::builder::Builder<'ctx>,
2130        slot_type: inkwell::types::StructType<'ctx>,
2131        result_ptr: PointerValue<'ctx>,
2132    ) -> DynamicResultSlotPtrs<'ctx> {
2133        DynamicResultSlotPtrs {
2134            state: builder
2135                .build_struct_gep(slot_type, result_ptr, 0, "state")
2136                .expect("state gep"),
2137            cached: builder
2138                .build_struct_gep(slot_type, result_ptr, 1, "cached")
2139                .expect("cached gep"),
2140            future: builder
2141                .build_struct_gep(slot_type, result_ptr, 2, "future")
2142                .expect("future gep"),
2143        }
2144    }
2145
2146    #[allow(
2147        clippy::unreachable,
2148        reason = "a runtime helper returning no basic value indicates a broken lowering invariant"
2149    )]
2150    fn build_dynamic_result_pending_return<'ctx>(
2151        ctx: &'ctx Context,
2152        module: &Module<'ctx>,
2153        builder: &inkwell::builder::Builder<'ctx>,
2154        future_ptr: PointerValue<'ctx>,
2155        cached_ptr: PointerValue<'ctx>,
2156        state_ptr: PointerValue<'ctx>,
2157    ) {
2158        let future = builder
2159            .build_load(ctx.i64_type(), future_ptr, "future")
2160            .expect("load future");
2161        let read_fn = get_or_create_function(
2162            module,
2163            "___read_future_bool",
2164            ctx.bool_type().fn_type(&[ctx.i64_type().into()], false),
2165        );
2166        let dec_fn = get_or_create_function(
2167            module,
2168            "___dec_future_refcount",
2169            ctx.void_type().fn_type(&[ctx.i64_type().into()], false),
2170        );
2171        let bool_call = builder
2172            .build_call(read_fn, &[future.into()], "bool")
2173            .expect("read future");
2174        let bool_val = match bool_call.try_as_basic_value() {
2175            inkwell::values::ValueKind::Basic(bv) => bv,
2176            inkwell::values::ValueKind::Instruction(_) => unreachable!(),
2177        };
2178        let _ = builder.build_call(dec_fn, &[future.into()], "");
2179        let _ = builder.build_store(cached_ptr, bool_val);
2180        let _ = builder.build_store(state_ptr, ctx.i8_type().const_int(2, false));
2181        builder
2182            .build_return(Some(&bool_val.into_int_value()))
2183            .expect("ret pending");
2184    }
2185
2186    fn call_basic_value<'ctx>(
2187        builder: &inkwell::builder::Builder<'ctx>,
2188        callee: FunctionValue<'ctx>,
2189        args: &[BasicMetadataValueEnum<'ctx>],
2190        name: &str,
2191        build_error: &str,
2192        value_error: &str,
2193    ) -> Result<BasicValueEnum<'ctx>, String> {
2194        let call = match builder.build_call(callee, args, name) {
2195            Ok(call) => call,
2196            Err(err) => return Err(format!("{build_error}: {err}")),
2197        };
2198        match call.try_as_basic_value() {
2199            inkwell::values::ValueKind::Basic(bv) => Ok(bv),
2200            inkwell::values::ValueKind::Instruction(_) => Err(value_error.to_string()),
2201        }
2202    }
2203
2204    fn clear_dynamic_result_slot<'ctx>(
2205        ctx: &'ctx Context,
2206        builder: &inkwell::builder::Builder<'ctx>,
2207        slot_ptrs: &DynamicResultSlotPtrs<'ctx>,
2208    ) {
2209        let _ = builder.build_store(slot_ptrs.state, ctx.i8_type().const_zero());
2210        let _ = builder.build_store(slot_ptrs.cached, ctx.bool_type().const_zero());
2211        let _ = builder.build_store(slot_ptrs.future, ctx.i64_type().const_zero());
2212    }
2213
2214    fn set_dynamic_result_pending<'ctx>(
2215        ctx: &'ctx Context,
2216        builder: &inkwell::builder::Builder<'ctx>,
2217        slot_ptrs: &DynamicResultSlotPtrs<'ctx>,
2218        future: inkwell::values::IntValue<'ctx>,
2219    ) {
2220        let _ = builder.build_store(slot_ptrs.state, ctx.i8_type().const_int(1, false));
2221        let _ = builder.build_store(slot_ptrs.future, future);
2222    }
2223
2224    fn read_result_bool<'ctx>(
2225        ctx: &'ctx Context,
2226        module: &Module<'ctx>,
2227        builder: &inkwell::builder::Builder<'ctx>,
2228        result_ptr: PointerValue<'ctx>,
2229        capability_flags: CapabilityFlags,
2230        result_ssa: &mut [Option<(BasicValueEnum<'ctx>, Option<BasicValueEnum<'ctx>>)>],
2231    ) -> Result<BasicValueEnum<'ctx>, String> {
2232        if capability_flags.dynamic_result_management {
2233            let read_func = ensure_dynamic_result_read(ctx, module);
2234            return call_basic_value(
2235                builder,
2236                read_func,
2237                &[result_ptr.into()],
2238                "bool",
2239                "Failed to build call for dynamic result read",
2240                "Failed to get basic value from dynamic result read call",
2241            );
2242        }
2243
2244        let result_idx = get_index(result_ptr)?;
2245        let result_idx_usize = usize::try_from(result_idx)
2246            .map_err(|e| format!("Failed to convert result index to usize: {e}"))?;
2247        let meas_handle = result_ssa[result_idx_usize]
2248            .ok_or_else(|| "Expected measurement handle".to_string())?;
2249        result_ssa[result_idx_usize]
2250            .and_then(|v| v.1)
2251            .and_then(|val: BasicValueEnum<'_>| val.as_instruction_value())
2252            .map_or_else(
2253                || {
2254                    let read_func = get_or_create_function(
2255                        module,
2256                        "___read_future_bool",
2257                        ctx.bool_type().fn_type(&[ctx.i64_type().into()], false),
2258                    );
2259                    let bool_val = call_basic_value(
2260                        builder,
2261                        read_func,
2262                        &[meas_handle.0.into()],
2263                        "bool",
2264                        "Failed to build call for read_future_bool",
2265                        "Failed to get basic value from read_future_bool call",
2266                    )?;
2267                    let dec_func = get_or_create_function(
2268                        module,
2269                        "___dec_future_refcount",
2270                        ctx.void_type().fn_type(&[ctx.i64_type().into()], false),
2271                    );
2272                    let _ = builder
2273                        .build_call(dec_func, &[meas_handle.0.into()], "")
2274                        .map_err(|e| {
2275                            format!("Failed to build call for dec_future_refcount: {e}")
2276                        })?;
2277                    result_ssa[result_idx_usize] = Some((meas_handle.0, Some(bool_val)));
2278                    Ok(bool_val)
2279                },
2280                |val: inkwell::values::InstructionValue<'_>| {
2281                    val.as_any_value_enum()
2282                        .try_into()
2283                        .map_err(|()| "Expected BasicValueEnum".to_string())
2284                },
2285            )
2286    }
2287
2288    fn get_or_create_result_output_global<'ctx>(
2289        ctx: &'ctx Context,
2290        module: &Module<'ctx>,
2291        global_mapping: &mut HashMap<String, inkwell::values::GlobalValue<'ctx>>,
2292        capability_flags: CapabilityFlags,
2293        result_ptr: PointerValue<'ctx>,
2294        gep: BasicValueEnum<'ctx>,
2295    ) -> Result<inkwell::values::GlobalValue<'ctx>, String> {
2296        if let Ok(old_global) = parse_gep(gep) {
2297            return global_mapping
2298                .get(old_global.as_str())
2299                .copied()
2300                .ok_or_else(|| format!("Output global `{old_global}` not found in mapping"));
2301        }
2302
2303        let fallback_label = if capability_flags.dynamic_result_management {
2304            "result_dynamic".to_string()
2305        } else {
2306            format!("result_{}", get_index(result_ptr)?)
2307        };
2308        let (new_const, new_name) =
2309            build_result_global(ctx, &fallback_label, &fallback_label, "RESULT", None)?;
2310        let new_global = module.add_global(new_const.get_type(), None, &new_name);
2311        new_global.set_initializer(&new_const);
2312        new_global.set_linkage(inkwell::module::Linkage::Private);
2313        new_global.set_constant(true);
2314        global_mapping.insert(fallback_label, new_global);
2315        Ok(new_global)
2316    }
2317
2318    fn get_dynamic_result_slot_type(ctx: &Context) -> inkwell::types::StructType<'_> {
2319        ctx.struct_type(
2320            &[
2321                ctx.i8_type().into(),   // state: 0=false, 1=pending future, 2=cached bool
2322                ctx.bool_type().into(), // cached bool
2323                ctx.i64_type().into(),  // future handle
2324            ],
2325            false,
2326        )
2327    }
2328
2329    fn build_ptr_is_null<'ctx>(
2330        ctx: &'ctx Context,
2331        builder: &inkwell::builder::Builder<'ctx>,
2332        ptr: PointerValue<'ctx>,
2333        name: &str,
2334    ) -> Result<inkwell::values::IntValue<'ctx>, String> {
2335        let ptr_as_int = builder
2336            .build_ptr_to_int(ptr, ctx.i64_type(), name)
2337            .map_err(|e| format!("Failed to convert pointer to int: {e}"))?;
2338        builder
2339            .build_int_compare(
2340                inkwell::IntPredicate::EQ,
2341                ptr_as_int,
2342                ctx.i64_type().const_zero(),
2343                "is_null",
2344            )
2345            .map_err(|e| format!("Failed to compare pointer with null: {e}"))
2346    }
2347
2348    #[allow(
2349        clippy::unreachable,
2350        reason = "the internal qalloc helper must produce a basic value or lowering is inconsistent"
2351    )]
2352    fn ensure_dynamic_qubit_allocate<'ctx>(
2353        ctx: &'ctx Context,
2354        module: &Module<'ctx>,
2355    ) -> FunctionValue<'ctx> {
2356        if let Some(existing) = module.get_function("qir_qis.qubit_allocate") {
2357            return existing;
2358        }
2359
2360        let ptr_type = ctx.ptr_type(AddressSpace::default());
2361        let fn_type = ptr_type.fn_type(&[ptr_type.into()], false);
2362        let function =
2363            module.add_function("qir_qis.qubit_allocate", fn_type, Some(Linkage::Private));
2364        let builder = ctx.create_builder();
2365        let entry = ctx.append_basic_block(function, "entry");
2366        let paths = DynamicQubitAllocatePaths {
2367            fail: ctx.append_basic_block(function, "fail"),
2368            fail_store: ctx.append_basic_block(function, "fail_store"),
2369            fail_panic: ctx.append_basic_block(function, "fail_panic"),
2370            success: ctx.append_basic_block(function, "success"),
2371            store_ok: ctx.append_basic_block(function, "store_ok"),
2372            ret_ok: ctx.append_basic_block(function, "ret_ok"),
2373        };
2374        builder.position_at_end(entry);
2375
2376        let out_err = function
2377            .get_first_param()
2378            .expect("qubit allocate helper has out_err")
2379            .into_pointer_value();
2380        let qalloc_fn =
2381            get_or_create_function(module, "___qalloc", ctx.i64_type().fn_type(&[], false));
2382        let call_result = builder
2383            .build_call(qalloc_fn, &[], "qalloc")
2384            .expect("qalloc call");
2385        let qid = match call_result.try_as_basic_value() {
2386            inkwell::values::ValueKind::Basic(bv) => bv.into_int_value(),
2387            inkwell::values::ValueKind::Instruction(_) => unreachable!(),
2388        };
2389        let is_fail = builder
2390            .build_int_compare(
2391                inkwell::IntPredicate::EQ,
2392                qid,
2393                ctx.i64_type().const_int(u64::MAX, false),
2394                "is_fail",
2395            )
2396            .expect("compare fail");
2397        builder
2398            .build_conditional_branch(is_fail, paths.fail, paths.success)
2399            .expect("branch fail");
2400        build_dynamic_qubit_allocate_fail_path(ctx, module, &builder, ptr_type, out_err, &paths);
2401        build_dynamic_qubit_allocate_success_path(ctx, &builder, ptr_type, out_err, qid, &paths);
2402        function
2403    }
2404
2405    fn ensure_dynamic_qubit_release<'ctx>(
2406        ctx: &'ctx Context,
2407        module: &Module<'ctx>,
2408    ) -> FunctionValue<'ctx> {
2409        if let Some(existing) = module.get_function("qir_qis.qubit_release") {
2410            return existing;
2411        }
2412        let ptr_type = ctx.ptr_type(AddressSpace::default());
2413        let fn_type = ctx.void_type().fn_type(&[ptr_type.into()], false);
2414        let function =
2415            module.add_function("qir_qis.qubit_release", fn_type, Some(Linkage::Private));
2416        let builder = ctx.create_builder();
2417        let entry = ctx.append_basic_block(function, "entry");
2418        let ret = ctx.append_basic_block(function, "ret");
2419        let body = ctx.append_basic_block(function, "body");
2420        builder.position_at_end(entry);
2421        let qubit_ptr = function
2422            .get_first_param()
2423            .expect("qubit release param")
2424            .into_pointer_value();
2425        let is_null = build_ptr_is_null(ctx, &builder, qubit_ptr, "qubit_int").expect("null check");
2426        builder
2427            .build_conditional_branch(is_null, ret, body)
2428            .expect("branch");
2429        builder.position_at_end(body);
2430        let q_handle = builder
2431            .build_ptr_to_int(qubit_ptr, ctx.i64_type(), "qbit")
2432            .expect("ptr to int");
2433        let qfree_fn = get_or_create_function(
2434            module,
2435            "___qfree",
2436            ctx.void_type().fn_type(&[ctx.i64_type().into()], false),
2437        );
2438        let _ = builder.build_call(qfree_fn, &[q_handle.into()], "");
2439        builder.build_unconditional_branch(ret).expect("jump ret");
2440        builder.position_at_end(ret);
2441        builder.build_return(None).expect("return");
2442        function
2443    }
2444
2445    fn build_dynamic_array_loop<'ctx, F>(
2446        ctx: &'ctx Context,
2447        function: FunctionValue<'ctx>,
2448        builder: &inkwell::builder::Builder<'ctx>,
2449        trip_count: inkwell::values::IntValue<'ctx>,
2450        mut body_builder: F,
2451    ) -> Result<(), String>
2452    where
2453        F: FnMut(
2454            &inkwell::builder::Builder<'ctx>,
2455            inkwell::values::IntValue<'ctx>,
2456        ) -> Result<(), String>,
2457    {
2458        let entry_block = builder
2459            .get_insert_block()
2460            .ok_or("Missing loop entry block")?;
2461        let loop_header = ctx.append_basic_block(function, "loop_header");
2462        let loop_body = ctx.append_basic_block(function, "loop_body");
2463        let loop_exit = ctx.append_basic_block(function, "loop_exit");
2464        builder
2465            .build_unconditional_branch(loop_header)
2466            .map_err(|e| format!("Failed to branch to loop header: {e}"))?;
2467        builder.position_at_end(loop_header);
2468        let idx_phi = builder
2469            .build_phi(ctx.i64_type(), "idx")
2470            .map_err(|e| format!("Failed to create loop phi: {e}"))?;
2471        idx_phi.add_incoming(&[(&ctx.i64_type().const_zero(), entry_block)]);
2472        let idx = idx_phi.as_basic_value().into_int_value();
2473        let cond = builder
2474            .build_int_compare(inkwell::IntPredicate::ULT, idx, trip_count, "loop_cond")
2475            .map_err(|e| format!("Failed to build loop condition: {e}"))?;
2476        builder
2477            .build_conditional_branch(cond, loop_body, loop_exit)
2478            .map_err(|e| format!("Failed to build loop branch: {e}"))?;
2479        builder.position_at_end(loop_body);
2480        body_builder(builder, idx)?;
2481        let next = builder
2482            .build_int_add(idx, ctx.i64_type().const_int(1, false), "next_idx")
2483            .map_err(|e| format!("Failed to increment loop index: {e}"))?;
2484        builder
2485            .build_unconditional_branch(loop_header)
2486            .map_err(|e| format!("Failed to jump to loop header: {e}"))?;
2487        idx_phi.add_incoming(&[(&next, loop_body)]);
2488        builder.position_at_end(loop_exit);
2489        Ok(())
2490    }
2491
2492    fn build_pointer_array_release_helper<'ctx>(
2493        ctx: &'ctx Context,
2494        builder: &inkwell::builder::Builder<'ctx>,
2495        args: &PointerArrayReleaseHelperArgs<'ctx>,
2496    ) -> Result<(), String> {
2497        build_dynamic_array_loop(ctx, args.function, builder, args.len, |builder, idx| {
2498            let elem_ptr = unsafe {
2499                builder.build_gep(args.ptr_elem_type, args.array_ptr, &[idx], "elem_ptr")
2500            }
2501            .map_err(|e| format!("{}: {e}", args.gep_error))?;
2502            let value_ptr = builder
2503                .build_load(args.ptr_elem_type, elem_ptr, "value_ptr")
2504                .map_err(|e| format!("{}: {e}", args.load_error))?;
2505            let _ = builder
2506                .build_call(args.release_fn, &[value_ptr.into()], "")
2507                .map_err(|e| format!("{}: {e}", args.release_error))?;
2508            Ok(())
2509        })
2510    }
2511
2512    #[allow(
2513        clippy::unreachable,
2514        reason = "the dynamic qubit allocation helper must return a pointer or lowering is inconsistent"
2515    )]
2516    fn ensure_dynamic_qubit_array_allocate<'ctx>(
2517        ctx: &'ctx Context,
2518        module: &Module<'ctx>,
2519    ) -> FunctionValue<'ctx> {
2520        if let Some(existing) = module.get_function("qir_qis.qubit_array_allocate") {
2521            return existing;
2522        }
2523        let ptr_type = ctx.ptr_type(AddressSpace::default());
2524        let function = module.add_function(
2525            "qir_qis.qubit_array_allocate",
2526            ctx.void_type().fn_type(
2527                &[ctx.i64_type().into(), ptr_type.into(), ptr_type.into()],
2528                false,
2529            ),
2530            Some(Linkage::Private),
2531        );
2532        let builder = ctx.create_builder();
2533        let entry = ctx.append_basic_block(function, "entry");
2534        builder.position_at_end(entry);
2535        let len = function.get_nth_param(0).expect("len").into_int_value();
2536        let array_ptr = function
2537            .get_nth_param(1)
2538            .expect("array")
2539            .into_pointer_value();
2540        let out_err = function
2541            .get_nth_param(2)
2542            .expect("out_err")
2543            .into_pointer_value();
2544        let alloc_fn = ensure_dynamic_qubit_allocate(ctx, module);
2545        let out_err_success_fn = ensure_out_err_success(ctx, module);
2546        let _ = builder
2547            .build_call(out_err_success_fn, &[out_err.into()], "")
2548            .expect("initialize out_err");
2549        let release_array_fn = ensure_dynamic_qubit_array_release(ctx, module);
2550        let entry_block = entry;
2551        let blocks = create_dynamic_qubit_array_allocate_blocks(ctx, function);
2552
2553        builder
2554            .build_unconditional_branch(blocks.loop_header)
2555            .expect("jump loop_header");
2556        builder.position_at_end(blocks.loop_header);
2557        let idx_phi = builder.build_phi(ctx.i64_type(), "idx").expect("idx phi");
2558        idx_phi.add_incoming(&[(&ctx.i64_type().const_zero(), entry_block)]);
2559        let idx = idx_phi.as_basic_value().into_int_value();
2560        let cond = builder
2561            .build_int_compare(inkwell::IntPredicate::ULT, idx, len, "loop_cond")
2562            .expect("loop cond");
2563        builder
2564            .build_conditional_branch(cond, blocks.loop_body, blocks.loop_exit)
2565            .expect("loop branch");
2566
2567        builder.position_at_end(blocks.loop_body);
2568        let elem_ptr = unsafe { builder.build_gep(ptr_type, array_ptr, &[idx], "elem_ptr") }
2569            .expect("elem gep");
2570        let slot = builder
2571            .build_call(alloc_fn, &[out_err.into()], "qubit_slot")
2572            .expect("alloc qubit");
2573        let slot = match slot.try_as_basic_value() {
2574            inkwell::values::ValueKind::Basic(bv) => bv,
2575            inkwell::values::ValueKind::Instruction(_) => unreachable!(),
2576        };
2577        let _ = builder.build_store(elem_ptr, slot).expect("store qubit");
2578        let out_err_is_null =
2579            build_ptr_is_null(ctx, &builder, out_err, "out_err_int").expect("out_err null check");
2580        builder
2581            .build_conditional_branch(
2582                out_err_is_null,
2583                blocks.continue_alloc,
2584                blocks.maybe_rollback,
2585            )
2586            .expect("branch maybe_rollback");
2587
2588        builder.position_at_end(blocks.maybe_rollback);
2589        let failed = builder
2590            .build_load(ctx.bool_type(), out_err, "alloc_failed")
2591            .expect("load out_err")
2592            .into_int_value();
2593        builder
2594            .build_conditional_branch(failed, blocks.rollback, blocks.continue_alloc)
2595            .expect("branch rollback");
2596        build_dynamic_qubit_array_allocate_rollback(
2597            ctx,
2598            &builder,
2599            idx,
2600            array_ptr,
2601            release_array_fn,
2602            blocks.rollback,
2603            blocks.rollback_done,
2604        );
2605
2606        builder.position_at_end(blocks.continue_alloc);
2607        let next = builder
2608            .build_int_add(idx, ctx.i64_type().const_int(1, false), "next_idx")
2609            .expect("next idx");
2610        builder
2611            .build_unconditional_branch(blocks.loop_header)
2612            .expect("jump loop_header");
2613        idx_phi.add_incoming(&[(&next, blocks.continue_alloc)]);
2614
2615        builder.position_at_end(blocks.loop_exit);
2616        builder.build_return(None).expect("return");
2617
2618        builder.position_at_end(blocks.rollback_done);
2619        builder.build_return(None).expect("return");
2620        function
2621    }
2622
2623    fn ensure_dynamic_qubit_array_release<'ctx>(
2624        ctx: &'ctx Context,
2625        module: &Module<'ctx>,
2626    ) -> FunctionValue<'ctx> {
2627        if let Some(existing) = module.get_function("qir_qis.qubit_array_release") {
2628            return existing;
2629        }
2630        let ptr_type = ctx.ptr_type(AddressSpace::default());
2631        let function = module.add_function(
2632            "qir_qis.qubit_array_release",
2633            ctx.void_type()
2634                .fn_type(&[ctx.i64_type().into(), ptr_type.into()], false),
2635            Some(Linkage::Private),
2636        );
2637        let builder = ctx.create_builder();
2638        let entry = ctx.append_basic_block(function, "entry");
2639        builder.position_at_end(entry);
2640        let len = function.get_nth_param(0).expect("len").into_int_value();
2641        let array_ptr = function
2642            .get_nth_param(1)
2643            .expect("array")
2644            .into_pointer_value();
2645        let release_fn = ensure_dynamic_qubit_release(ctx, module);
2646        build_pointer_array_release_helper(
2647            ctx,
2648            &builder,
2649            &PointerArrayReleaseHelperArgs {
2650                function,
2651                len,
2652                array_ptr,
2653                release_fn,
2654                ptr_elem_type: ptr_type,
2655                gep_error: "Failed to build qubit array GEP",
2656                load_error: "Failed to load dynamic qubit pointer",
2657                release_error: "Failed to release dynamic qubit",
2658            },
2659        )
2660        .expect("build dynamic qubit release loop");
2661        builder.build_return(None).expect("return");
2662        function
2663    }
2664
2665    fn replace_call_with_value<'ctx>(
2666        instr: inkwell::values::InstructionValue<'ctx>,
2667        value: BasicValueEnum<'ctx>,
2668    ) -> Result<(), String> {
2669        let instruction_val = value
2670            .as_instruction_value()
2671            .ok_or("Expected replacement value to be instruction-backed")?;
2672        instr.replace_all_uses_with(&instruction_val);
2673        instr.erase_from_basic_block();
2674        Ok(())
2675    }
2676
2677    fn initialize_dynamic_result_slot<'ctx>(
2678        ctx: &'ctx Context,
2679        builder: &inkwell::builder::Builder<'ctx>,
2680        slot_ptr: PointerValue<'ctx>,
2681    ) {
2682        let slot_type = get_dynamic_result_slot_type(ctx);
2683        let slot_ptrs = get_dynamic_result_slot_ptrs(builder, slot_type, slot_ptr);
2684        clear_dynamic_result_slot(ctx, builder, &slot_ptrs);
2685    }
2686
2687    fn ensure_out_err_success<'ctx>(
2688        ctx: &'ctx Context,
2689        module: &Module<'ctx>,
2690    ) -> FunctionValue<'ctx> {
2691        if let Some(existing) = module.get_function("qir_qis.out_err_success") {
2692            return existing;
2693        }
2694        let ptr_type = ctx.ptr_type(AddressSpace::default());
2695        let function = module.add_function(
2696            "qir_qis.out_err_success",
2697            ctx.void_type().fn_type(&[ptr_type.into()], false),
2698            Some(Linkage::Private),
2699        );
2700        let builder = ctx.create_builder();
2701        let entry = ctx.append_basic_block(function, "entry");
2702        let ret = ctx.append_basic_block(function, "ret");
2703        let set_ok = ctx.append_basic_block(function, "set_ok");
2704        builder.position_at_end(entry);
2705        let out_err = function
2706            .get_first_param()
2707            .expect("out_err")
2708            .into_pointer_value();
2709        let is_null = build_ptr_is_null(ctx, &builder, out_err, "out_err_int").expect("null");
2710        builder
2711            .build_conditional_branch(is_null, ret, set_ok)
2712            .expect("branch");
2713        builder.position_at_end(set_ok);
2714        let _ = builder.build_store(out_err, ctx.bool_type().const_zero());
2715        builder.build_unconditional_branch(ret).expect("jump");
2716        builder.position_at_end(ret);
2717        builder.build_return(None).expect("ret");
2718        function
2719    }
2720
2721    fn extract_const_len(value: BasicValueEnum<'_>, opname: &str) -> Result<u64, String> {
2722        let Some(len) = value.into_int_value().get_zero_extended_constant() else {
2723            return Err(format!(
2724                "{opname} currently requires a constant array length"
2725            ));
2726        };
2727        Ok(len)
2728    }
2729
2730    fn lower_void_helper_call<'ctx>(
2731        ctx: &'ctx Context,
2732        instr: inkwell::values::InstructionValue<'ctx>,
2733        helper: FunctionValue<'ctx>,
2734        call_args: &[BasicValueEnum<'ctx>],
2735        error_context: &str,
2736    ) -> Result<(), String> {
2737        let builder = ctx.create_builder();
2738        builder.position_before(&instr);
2739        let metadata_args: Vec<BasicMetadataValueEnum<'ctx>> =
2740            call_args.iter().copied().map(Into::into).collect();
2741        if let Err(err) = builder.build_call(helper, &metadata_args, "") {
2742            return Err(format!("{error_context}: {err}"));
2743        }
2744        instr.erase_from_basic_block();
2745        Ok(())
2746    }
2747
2748    fn lower_dynamic_qubit_allocate(args: &ProcessCallArgs<'_>) -> Result<(), String> {
2749        let builder = args.ctx.create_builder();
2750        builder.position_before(&args.instr);
2751        let call_args: Vec<BasicValueEnum> = extract_operands(&args.instr)?;
2752        let helper = ensure_dynamic_qubit_allocate(args.ctx, module_ref(args));
2753        let value = call_basic_value(
2754            &builder,
2755            helper,
2756            &[call_args[0].into()],
2757            "dyn_q",
2758            "Failed to lower dynamic qubit allocation",
2759            "Dynamic qubit allocate helper did not return a pointer",
2760        )?;
2761        replace_call_with_value(args.instr, value)
2762    }
2763
2764    fn lower_dynamic_qubit_release(args: &ProcessCallArgs<'_>) -> Result<(), String> {
2765        let call_args: Vec<BasicValueEnum> = extract_operands(&args.instr)?;
2766        let helper = ensure_dynamic_qubit_release(args.ctx, module_ref(args));
2767        lower_void_helper_call(
2768            args.ctx,
2769            args.instr,
2770            helper,
2771            &call_args[..1],
2772            "Failed to lower dynamic qubit release",
2773        )
2774    }
2775
2776    fn lower_dynamic_qubit_array_allocate(args: &ProcessCallArgs<'_>) -> Result<(), String> {
2777        let call_args: Vec<BasicValueEnum> = extract_operands(&args.instr)?;
2778        let helper = ensure_dynamic_qubit_array_allocate(args.ctx, module_ref(args));
2779        lower_void_helper_call(
2780            args.ctx,
2781            args.instr,
2782            helper,
2783            &call_args[..3],
2784            "Failed to lower dynamic qubit array allocation",
2785        )
2786    }
2787
2788    fn lower_dynamic_qubit_array_release(args: &ProcessCallArgs<'_>) -> Result<(), String> {
2789        let call_args: Vec<BasicValueEnum> = extract_operands(&args.instr)?;
2790        let helper = ensure_dynamic_qubit_array_release(args.ctx, module_ref(args));
2791        lower_void_helper_call(
2792            args.ctx,
2793            args.instr,
2794            helper,
2795            &call_args[..2],
2796            "Failed to lower dynamic qubit array release",
2797        )
2798    }
2799
2800    fn ensure_dynamic_result_setter<'ctx>(
2801        ctx: &'ctx Context,
2802        module: &Module<'ctx>,
2803    ) -> FunctionValue<'ctx> {
2804        if let Some(existing) = module.get_function("qir_qis.result_set_pending") {
2805            return existing;
2806        }
2807        let ptr_type = ctx.ptr_type(AddressSpace::default());
2808        let slot_type = get_dynamic_result_slot_type(ctx);
2809        let function = module.add_function(
2810            "qir_qis.result_set_pending",
2811            ctx.void_type()
2812                .fn_type(&[ptr_type.into(), ctx.i64_type().into()], false),
2813            Some(Linkage::Private),
2814        );
2815        let builder = ctx.create_builder();
2816        let entry = ctx.append_basic_block(function, "entry");
2817        builder.position_at_end(entry);
2818        let result_ptr = function
2819            .get_nth_param(0)
2820            .expect("result")
2821            .into_pointer_value();
2822        let future = function.get_nth_param(1).expect("future").into_int_value();
2823        let slot_ptrs = get_dynamic_result_slot_ptrs(&builder, slot_type, result_ptr);
2824        set_dynamic_result_pending(ctx, &builder, &slot_ptrs, future);
2825        builder.build_return(None).expect("ret");
2826        function
2827    }
2828
2829    fn ensure_dynamic_result_read<'ctx>(
2830        ctx: &'ctx Context,
2831        module: &Module<'ctx>,
2832    ) -> FunctionValue<'ctx> {
2833        if let Some(existing) = module.get_function("qir_qis.result_read") {
2834            return existing;
2835        }
2836        let ptr_type = ctx.ptr_type(AddressSpace::default());
2837        let slot_type = get_dynamic_result_slot_type(ctx);
2838        let function = module.add_function(
2839            "qir_qis.result_read",
2840            ctx.bool_type().fn_type(&[ptr_type.into()], false),
2841            Some(Linkage::Private),
2842        );
2843        let builder = ctx.create_builder();
2844        let entry = ctx.append_basic_block(function, "entry");
2845        let ret_false = ctx.append_basic_block(function, "ret_false");
2846        let pending = ctx.append_basic_block(function, "pending");
2847        let cached = ctx.append_basic_block(function, "cached");
2848        builder.position_at_end(entry);
2849        let result_ptr = function
2850            .get_first_param()
2851            .expect("result")
2852            .into_pointer_value();
2853        let is_null =
2854            build_ptr_is_null(ctx, &builder, result_ptr, "result_int").expect("null check");
2855        let read_state = ctx.append_basic_block(function, "read_state");
2856        builder
2857            .build_conditional_branch(is_null, ret_false, read_state)
2858            .expect("branch");
2859
2860        builder.position_at_end(read_state);
2861        let slot_ptrs = get_dynamic_result_slot_ptrs(&builder, slot_type, result_ptr);
2862        let state = builder
2863            .build_load(ctx.i8_type(), slot_ptrs.state, "state")
2864            .expect("load state")
2865            .into_int_value();
2866        let is_pending = builder
2867            .build_int_compare(
2868                inkwell::IntPredicate::EQ,
2869                state,
2870                ctx.i8_type().const_int(1, false),
2871                "is_pending",
2872            )
2873            .expect("cmp");
2874        let is_cached = builder
2875            .build_int_compare(
2876                inkwell::IntPredicate::EQ,
2877                state,
2878                ctx.i8_type().const_int(2, false),
2879                "is_cached",
2880            )
2881            .expect("cmp");
2882        let check_cached = ctx.append_basic_block(function, "check_cached");
2883        builder
2884            .build_conditional_branch(is_pending, pending, check_cached)
2885            .expect("branch");
2886
2887        builder.position_at_end(check_cached);
2888        builder
2889            .build_conditional_branch(is_cached, cached, ret_false)
2890            .expect("branch");
2891
2892        builder.position_at_end(pending);
2893        build_dynamic_result_pending_return(
2894            ctx,
2895            module,
2896            &builder,
2897            slot_ptrs.future,
2898            slot_ptrs.cached,
2899            slot_ptrs.state,
2900        );
2901
2902        builder.position_at_end(cached);
2903        let cached_val = builder
2904            .build_load(ctx.bool_type(), slot_ptrs.cached, "cached")
2905            .expect("cached load");
2906        builder
2907            .build_return(Some(&cached_val.into_int_value()))
2908            .expect("ret cached");
2909
2910        builder.position_at_end(ret_false);
2911        builder
2912            .build_return(Some(&ctx.bool_type().const_zero()))
2913            .expect("ret false");
2914        function
2915    }
2916
2917    fn ensure_dynamic_result_release<'ctx>(
2918        ctx: &'ctx Context,
2919        module: &Module<'ctx>,
2920    ) -> FunctionValue<'ctx> {
2921        if let Some(existing) = module.get_function("qir_qis.result_release") {
2922            return existing;
2923        }
2924        let ptr_type = ctx.ptr_type(AddressSpace::default());
2925        let slot_type = get_dynamic_result_slot_type(ctx);
2926        let function = module.add_function(
2927            "qir_qis.result_release",
2928            ctx.void_type().fn_type(&[ptr_type.into()], false),
2929            Some(Linkage::Private),
2930        );
2931        let builder = ctx.create_builder();
2932        let entry = ctx.append_basic_block(function, "entry");
2933        let ret = ctx.append_basic_block(function, "ret");
2934        let body = ctx.append_basic_block(function, "body");
2935        builder.position_at_end(entry);
2936        let result_ptr = function
2937            .get_first_param()
2938            .expect("result")
2939            .into_pointer_value();
2940        let is_null =
2941            build_ptr_is_null(ctx, &builder, result_ptr, "result_int").expect("null check");
2942        builder
2943            .build_conditional_branch(is_null, ret, body)
2944            .expect("branch");
2945        builder.position_at_end(body);
2946        let slot_ptrs = get_dynamic_result_slot_ptrs(&builder, slot_type, result_ptr);
2947        let state = builder
2948            .build_load(ctx.i8_type(), slot_ptrs.state, "state")
2949            .expect("load state")
2950            .into_int_value();
2951        let is_pending = builder
2952            .build_int_compare(
2953                inkwell::IntPredicate::EQ,
2954                state,
2955                ctx.i8_type().const_int(1, false),
2956                "is_pending",
2957            )
2958            .expect("cmp");
2959        let dec_block = ctx.append_basic_block(function, "dec_future");
2960        let clear_block = ctx.append_basic_block(function, "clear");
2961        builder
2962            .build_conditional_branch(is_pending, dec_block, clear_block)
2963            .expect("branch");
2964        builder.position_at_end(dec_block);
2965        let future = builder
2966            .build_load(ctx.i64_type(), slot_ptrs.future, "future")
2967            .expect("load future");
2968        let dec_fn = get_or_create_function(
2969            module,
2970            "___dec_future_refcount",
2971            ctx.void_type().fn_type(&[ctx.i64_type().into()], false),
2972        );
2973        let _ = builder.build_call(dec_fn, &[future.into()], "");
2974        builder
2975            .build_unconditional_branch(clear_block)
2976            .expect("jump");
2977        builder.position_at_end(clear_block);
2978        clear_dynamic_result_slot(ctx, &builder, &slot_ptrs);
2979        builder.build_unconditional_branch(ret).expect("jump");
2980        builder.position_at_end(ret);
2981        builder.build_return(None).expect("ret");
2982        function
2983    }
2984
2985    fn ensure_dynamic_result_array_release<'ctx>(
2986        ctx: &'ctx Context,
2987        module: &Module<'ctx>,
2988    ) -> FunctionValue<'ctx> {
2989        if let Some(existing) = module.get_function("qir_qis.result_array_release") {
2990            return existing;
2991        }
2992        let ptr_type = ctx.ptr_type(AddressSpace::default());
2993        let function = module.add_function(
2994            "qir_qis.result_array_release",
2995            ctx.void_type()
2996                .fn_type(&[ctx.i64_type().into(), ptr_type.into()], false),
2997            Some(Linkage::Private),
2998        );
2999        let builder = ctx.create_builder();
3000        let entry = ctx.append_basic_block(function, "entry");
3001        builder.position_at_end(entry);
3002        let len = function.get_nth_param(0).expect("len").into_int_value();
3003        let array_ptr = function
3004            .get_nth_param(1)
3005            .expect("array")
3006            .into_pointer_value();
3007        let release_fn = ensure_dynamic_result_release(ctx, module);
3008        let ptr_elem_type = ctx.ptr_type(AddressSpace::default());
3009        build_pointer_array_release_helper(
3010            ctx,
3011            &builder,
3012            &PointerArrayReleaseHelperArgs {
3013                function,
3014                len,
3015                array_ptr,
3016                release_fn,
3017                ptr_elem_type,
3018                gep_error: "Failed to build result array GEP",
3019                load_error: "Failed to load dynamic result pointer",
3020                release_error: "Failed to release dynamic result",
3021            },
3022        )
3023        .expect("build dynamic result release loop");
3024        builder.build_return(None).expect("ret");
3025        function
3026    }
3027
3028    fn ensure_dynamic_result_array_record_output<'ctx>(
3029        ctx: &'ctx Context,
3030        module: &Module<'ctx>,
3031    ) -> Result<FunctionValue<'ctx>, String> {
3032        if let Some(existing) = module.get_function("qir_qis.result_array_record_output") {
3033            return Ok(existing);
3034        }
3035        let ptr_type = ctx.ptr_type(AddressSpace::default());
3036        let function = module.add_function(
3037            "qir_qis.result_array_record_output",
3038            ctx.void_type().fn_type(
3039                &[
3040                    ctx.i64_type().into(),
3041                    ptr_type.into(),
3042                    ptr_type.into(),
3043                    ctx.i64_type().into(),
3044                ],
3045                false,
3046            ),
3047            Some(Linkage::Private),
3048        );
3049        let builder = ctx.create_builder();
3050        let entry = ctx.append_basic_block(function, "entry");
3051        builder.position_at_end(entry);
3052        let len = function.get_nth_param(0).expect("len").into_int_value();
3053        let array_ptr = function
3054            .get_nth_param(1)
3055            .expect("array")
3056            .into_pointer_value();
3057        let tag_ptr = function.get_nth_param(2).expect("tag").into_pointer_value();
3058        let tag_len = function.get_nth_param(3).expect("tag_len").into_int_value();
3059        let print_bool_arr = get_or_create_function(
3060            module,
3061            "print_bool_arr",
3062            ctx.void_type().fn_type(
3063                &[ptr_type.into(), ctx.i64_type().into(), ptr_type.into()],
3064                false,
3065            ),
3066        );
3067        let read_fn = ensure_dynamic_result_read(ctx, module);
3068        let bool_arr =
3069            build_dynamic_result_array_values(ctx, function, &builder, len, array_ptr, read_fn)?;
3070        let array_desc = build_dynamic_result_array_descriptor(ctx, &builder, len, bool_arr)?;
3071        let _ = builder
3072            .build_call(
3073                print_bool_arr,
3074                &[tag_ptr.into(), tag_len.into(), array_desc.into()],
3075                "",
3076            )
3077            .map_err(|e| format!("Failed to print result array: {e}"))?;
3078        builder.build_return(None).expect("ret");
3079        Ok(function)
3080    }
3081
3082    fn lower_dynamic_result_allocate(args: &ProcessCallArgs<'_>) -> Result<(), String> {
3083        let builder = args.ctx.create_builder();
3084        builder.position_before(&args.instr);
3085        let call_args: Vec<BasicValueEnum> = extract_operands(&args.instr)?;
3086        let slot_ptr = builder
3087            .build_alloca(get_dynamic_result_slot_type(args.ctx), "dyn_result")
3088            .map_err(|e| format!("Failed to allocate dynamic result slot: {e}"))?;
3089        initialize_dynamic_result_slot(args.ctx, &builder, slot_ptr);
3090        let helper = ensure_out_err_success(args.ctx, module_ref(args));
3091        let _ = builder
3092            .build_call(helper, &[call_args[0].into()], "")
3093            .map_err(|e| format!("Failed to store dynamic result out_err success flag: {e}"))?;
3094        replace_call_with_value(args.instr, slot_ptr.as_basic_value_enum())
3095    }
3096
3097    fn lower_dynamic_result_release(args: &ProcessCallArgs<'_>) -> Result<(), String> {
3098        let call_args: Vec<BasicValueEnum> = extract_operands(&args.instr)?;
3099        let helper = ensure_dynamic_result_release(args.ctx, module_ref(args));
3100        lower_void_helper_call(
3101            args.ctx,
3102            args.instr,
3103            helper,
3104            &call_args[..1],
3105            "Failed to lower dynamic result release",
3106        )
3107    }
3108
3109    fn lower_dynamic_result_array_allocate(args: &mut ProcessCallArgs<'_>) -> Result<(), String> {
3110        let builder = args.ctx.create_builder();
3111        builder.position_before(&args.instr);
3112        let call_args: Vec<BasicValueEnum> = extract_operands(&args.instr)?;
3113        let len = extract_const_len(call_args[0], "__quantum__rt__result_array_allocate")?;
3114        let array_ptr = call_args[1].into_pointer_value();
3115        let out_err = call_args[2].into_pointer_value();
3116        let ptr_type = args.ctx.ptr_type(AddressSpace::default());
3117        for idx in 0..len {
3118            let elem_ptr = unsafe {
3119                builder.build_gep(
3120                    ptr_type,
3121                    array_ptr,
3122                    &[args.ctx.i64_type().const_int(idx, false)],
3123                    "result_elem_ptr",
3124                )
3125            }
3126            .map_err(|e| format!("Failed to build result array element GEP: {e}"))?;
3127            let slot_ptr = builder
3128                .build_alloca(get_dynamic_result_slot_type(args.ctx), "dyn_result")
3129                .map_err(|e| format!("Failed to allocate dynamic result slot: {e}"))?;
3130            initialize_dynamic_result_slot(args.ctx, &builder, slot_ptr);
3131            let _ = builder
3132                .build_store(elem_ptr, slot_ptr)
3133                .map_err(|e| format!("Failed to store dynamic result slot in array: {e}"))?;
3134        }
3135        let helper = ensure_out_err_success(args.ctx, module_ref(args));
3136        let _ = builder
3137            .build_call(helper, &[out_err.into()], "")
3138            .map_err(|e| {
3139                format!("Failed to store dynamic result array out_err success flag: {e}")
3140            })?;
3141        args.instr.erase_from_basic_block();
3142        Ok(())
3143    }
3144
3145    fn lower_dynamic_result_array_release(args: &ProcessCallArgs<'_>) -> Result<(), String> {
3146        let call_args: Vec<BasicValueEnum> = extract_operands(&args.instr)?;
3147        let helper = ensure_dynamic_result_array_release(args.ctx, module_ref(args));
3148        lower_void_helper_call(
3149            args.ctx,
3150            args.instr,
3151            helper,
3152            &call_args[..2],
3153            "Failed to lower dynamic result array release",
3154        )
3155    }
3156
3157    fn lower_dynamic_result_array_record_output(
3158        args: &mut ProcessCallArgs<'_>,
3159    ) -> Result<(), String> {
3160        let builder = args.ctx.create_builder();
3161        builder.position_before(&args.instr);
3162        let call_args: Vec<BasicValueEnum> = extract_operands(&args.instr)?;
3163        let old_name = parse_gep(call_args[2])?;
3164        let full_tag =
3165            if let Some(global) = unsafe { &mut *args.global_mapping }.get(old_name.as_str()) {
3166                get_string_label(*global)?
3167            } else {
3168                return Err(format!("Output global `{old_name}` not found in mapping"));
3169            };
3170        let old_label = extract_label_from_tag(&full_tag);
3171        let (new_const, new_name) =
3172            build_result_global(args.ctx, old_label, &old_name, "RESULT_ARRAY", None)?;
3173        let new_global = module_ref(args).add_global(new_const.get_type(), None, &new_name);
3174        new_global.set_initializer(&new_const);
3175        new_global.set_linkage(Linkage::Private);
3176        new_global.set_constant(true);
3177        unsafe { &mut *args.global_mapping }.insert(old_name, new_global);
3178        let helper = ensure_dynamic_result_array_record_output(args.ctx, module_ref(args))?;
3179        let tag_len = args.ctx.i64_type().const_int(
3180            u64::from(new_const.get_type().len()).saturating_sub(1),
3181            false,
3182        );
3183        let tag_ptr = unsafe {
3184            builder.build_gep(
3185                new_const.get_type(),
3186                new_global.as_pointer_value(),
3187                &[
3188                    args.ctx.i64_type().const_zero(),
3189                    args.ctx.i64_type().const_zero(),
3190                ],
3191                "tag_gep",
3192            )
3193        }
3194        .map_err(|e| format!("Failed to build result array tag GEP: {e}"))?;
3195        let _ = builder
3196            .build_call(
3197                helper,
3198                &[
3199                    call_args[0].into(),
3200                    call_args[1].into(),
3201                    tag_ptr.into(),
3202                    tag_len.into(),
3203                ],
3204                "",
3205            )
3206            .map_err(|e| format!("Failed to lower result array record output: {e}"))?;
3207        args.instr.erase_from_basic_block();
3208        Ok(())
3209    }
3210
3211    #[allow(
3212        clippy::too_many_arguments,
3213        reason = "measurement lowering needs a fixed set of builder inputs"
3214    )]
3215    fn handle_mz_call<'ctx>(
3216        ctx: &'ctx Context,
3217        module: *const (),
3218        instr: &'ctx inkwell::values::InstructionValue<'ctx>,
3219        fn_name: &str,
3220        capability_flags: CapabilityFlags,
3221        qubit_array: Option<PointerValue<'ctx>>,
3222        qubit_array_type: Option<ArrayType<'ctx>>,
3223        result_ssa: *mut (),
3224    ) -> Result<(), String> {
3225        let module = unsafe { &*module.cast::<Module<'ctx>>() };
3226        if fn_name == "__quantum__qis__m__body" {
3227            log::warn!(
3228                "`__quantum__qis__m__body` is from Q# QDK, synonym for `__quantum__qis__mz__body`"
3229            );
3230        }
3231        let builder = ctx.create_builder();
3232        builder.position_before(instr);
3233
3234        // Extract qubit and result indices
3235        let call_args: Vec<BasicValueEnum> = extract_operands(instr)?;
3236        let qubit_ptr = call_args[0].into_pointer_value();
3237        let result_ptr = call_args[1].into_pointer_value();
3238
3239        let q_handle = get_qubit_handle(
3240            ctx,
3241            capability_flags,
3242            qubit_array,
3243            qubit_array_type,
3244            &builder,
3245            qubit_ptr,
3246        )?;
3247
3248        // Create ___lazy_measure call
3249        let meas = {
3250            let meas_func = get_or_create_function(
3251                module,
3252                "___lazy_measure",
3253                ctx.i64_type().fn_type(&[ctx.i64_type().into()], false),
3254            );
3255
3256            let call = builder.build_call(meas_func, &[q_handle.into()], "meas");
3257            let call_result =
3258                call.map_err(|e| format!("Failed to build call for lazy measure function: {e}"))?;
3259            match call_result.try_as_basic_value() {
3260                inkwell::values::ValueKind::Basic(bv) => bv,
3261                inkwell::values::ValueKind::Instruction(_) => {
3262                    return Err("Failed to get basic value from lazy measure call".into());
3263                }
3264            }
3265        };
3266
3267        // Store measurement result
3268        if capability_flags.dynamic_result_management {
3269            let set_result_fn = ensure_dynamic_result_setter(ctx, module);
3270            let _ = builder
3271                .build_call(set_result_fn, &[result_ptr.into(), meas.into()], "")
3272                .map_err(|e| format!("Failed to update dynamic result state: {e}"))?;
3273        } else {
3274            let result_ssa = unsafe {
3275                &mut *result_ssa
3276                    .cast::<Vec<Option<(BasicValueEnum<'ctx>, Option<BasicValueEnum<'ctx>>)>>>()
3277            };
3278            let result_idx = get_index(result_ptr)?;
3279            let result_idx_usize = checked_result_index(result_idx, result_ssa.len())?;
3280            result_ssa[result_idx_usize] = Some((meas, None));
3281        }
3282
3283        if fn_name == "__quantum__qis__mresetz__body" {
3284            log::warn!("`__quantum__qis__mresetz__body` is from Q# QDK");
3285            // Create ___reset call
3286            create_reset_call(ctx, module, &builder, q_handle);
3287        }
3288
3289        // Remove original call
3290        instr.erase_from_basic_block();
3291        Ok(())
3292    }
3293
3294    fn handle_mz_leaked_call(args: &ProcessCallArgs) -> Result<(), String> {
3295        let ProcessCallArgs { ctx, instr, .. } = args;
3296        let module = module_ref(args);
3297        let builder = ctx.create_builder();
3298        builder.position_before(instr);
3299        let call = CallSiteValue::try_from(args.instr)
3300            .map_err(|()| "Malformed mz_leaked call: instruction is not a call site".to_string())?;
3301        let called_fn = call
3302            .get_called_fn_value()
3303            .ok_or_else(|| "Malformed mz_leaked call: missing callee".to_string())?;
3304        let fn_type = called_fn.get_type();
3305        let param_types = fn_type.get_param_types();
3306        let has_expected_signature = fn_type
3307            .get_return_type()
3308            .is_some_and(|ty| ty.is_int_type() && ty.into_int_type().get_bit_width() == 64)
3309            && param_types.len() == 1
3310            && param_types[0].is_pointer_type();
3311        if !has_expected_signature {
3312            return Err("Malformed mz_leaked call: expected signature i64 (ptr)".to_string());
3313        }
3314
3315        let call_args: Vec<BasicValueEnum> = extract_operands(instr)?;
3316        let qubit_ptr = mz_leaked_qubit_operand(&call_args)?;
3317
3318        let q_handle = get_qubit_handle(
3319            ctx,
3320            args.capability_flags,
3321            args.qubit_array,
3322            args.qubit_array_type,
3323            &builder,
3324            qubit_ptr,
3325        )?;
3326
3327        let meas_handle = {
3328            let meas_func = get_or_create_function(
3329                module,
3330                "___lazy_measure_leaked",
3331                ctx.i64_type().fn_type(&[ctx.i64_type().into()], false),
3332            );
3333
3334            let call = builder.build_call(meas_func, &[q_handle.into()], "meas_leaked");
3335            let call_result = call.map_err(|e| {
3336                format!("Failed to build call for lazy leaked measure function: {e}")
3337            })?;
3338            match call_result.try_as_basic_value() {
3339                inkwell::values::ValueKind::Basic(bv) => bv,
3340                inkwell::values::ValueKind::Instruction(_) => {
3341                    return Err("Failed to get basic value from lazy leaked measure call".into());
3342                }
3343            }
3344        };
3345
3346        let meas_value = {
3347            let read_func = get_or_create_function(
3348                module,
3349                "___read_future_uint",
3350                ctx.i64_type().fn_type(&[ctx.i64_type().into()], false),
3351            );
3352            let call = builder.build_call(read_func, &[meas_handle.into()], "meas_leaked_value");
3353            let call_result =
3354                call.map_err(|e| format!("Failed to build call for read_future_uint: {e}"))?;
3355            match call_result.try_as_basic_value() {
3356                inkwell::values::ValueKind::Basic(bv) => bv,
3357                inkwell::values::ValueKind::Instruction(_) => {
3358                    return Err("Failed to get basic value from read_future_uint call".into());
3359                }
3360            }
3361        };
3362
3363        let dec_func = get_or_create_function(
3364            module,
3365            "___dec_future_refcount",
3366            ctx.void_type().fn_type(&[ctx.i64_type().into()], false),
3367        );
3368        let _ = builder
3369            .build_call(dec_func, &[meas_handle.into()], "")
3370            .map_err(|e| format!("Failed to build call for dec_future_refcount: {e}"))?;
3371
3372        let instruction_val = meas_value
3373            .as_instruction_value()
3374            .ok_or("Failed to convert leaked measurement value to instruction value")?;
3375        instr.replace_all_uses_with(&instruction_val);
3376        instr.erase_from_basic_block();
3377        Ok(())
3378    }
3379
3380    pub fn mz_leaked_qubit_operand<'ctx>(
3381        call_args: &[BasicValueEnum<'ctx>],
3382    ) -> Result<PointerValue<'ctx>, String> {
3383        match call_args {
3384            [BasicValueEnum::PointerValue(ptr), _] => Ok(*ptr),
3385            [_, _] => {
3386                Err("Malformed mz_leaked call: expected first argument to be a pointer".into())
3387            }
3388            _ => Err(format!(
3389                "Malformed mz_leaked call: expected 1 argument plus callee, got {} operands",
3390                call_args.len()
3391            )),
3392        }
3393    }
3394
3395    fn handle_reset_call(args: &ProcessCallArgs<'_>) -> Result<(), String> {
3396        let ProcessCallArgs { ctx, instr, .. } = args;
3397        let module = module_ref(args);
3398        let builder = ctx.create_builder();
3399        builder.position_before(instr);
3400
3401        // Extract qubit index
3402        let call_args: Vec<BasicValueEnum> = extract_operands(instr)?;
3403        let qubit_ptr = call_args[0].into_pointer_value();
3404
3405        let q_handle = get_qubit_handle(
3406            ctx,
3407            args.capability_flags,
3408            args.qubit_array,
3409            args.qubit_array_type,
3410            &builder,
3411            qubit_ptr,
3412        )?;
3413
3414        // Create ___reset call
3415        create_reset_call(ctx, module, &builder, q_handle);
3416
3417        instr.erase_from_basic_block();
3418        Ok(())
3419    }
3420
3421    fn parse_barrier_arity(fn_name: &str) -> Result<usize, String> {
3422        fn_name
3423            .strip_prefix("__quantum__qis__barrier")
3424            .and_then(|s| s.strip_suffix("__body"))
3425            .and_then(|s| s.parse::<usize>().ok())
3426            .filter(|&n| n > 0)
3427            .ok_or_else(|| format!("Invalid barrier function name: {fn_name}"))
3428    }
3429
3430    #[allow(
3431        clippy::too_many_lines,
3432        reason = "barrier lowering stays linear to keep validation and rewrite steps aligned"
3433    )]
3434    fn handle_barrier_call(args: &ProcessCallArgs<'_>) -> Result<(), String> {
3435        let ProcessCallArgs {
3436            ctx,
3437            instr,
3438            fn_name,
3439            ..
3440        } = args;
3441        let module = module_ref(args);
3442        let builder = ctx.create_builder();
3443        builder.position_before(instr);
3444
3445        let num_qubits = parse_barrier_arity(fn_name)?;
3446
3447        // Extract qubit arguments (excluding the last operand which is the function pointer)
3448        let all_operands: Vec<BasicValueEnum> = extract_operands(instr)?;
3449        let num_operands = all_operands
3450            .len()
3451            .checked_sub(1)
3452            .ok_or("Expected at least one operand")?;
3453
3454        if num_operands != num_qubits {
3455            return Err(format!(
3456                "Barrier function {fn_name} expects {num_qubits} arguments, got {num_operands}"
3457            ));
3458        }
3459
3460        let call_args = &all_operands[..num_operands];
3461
3462        // Load qubit handles into an array
3463        let i64_type = ctx.i64_type();
3464        let array_type = i64_type.array_type(
3465            u32::try_from(num_qubits).map_err(|e| format!("Failed to convert num_qubits: {e}"))?,
3466        );
3467        let array_alloca = builder
3468            .build_alloca(array_type, "barrier_qubits")
3469            .map_err(|e| format!("Failed to allocate array for barrier qubits: {e}"))?;
3470
3471        for (i, arg) in call_args.iter().enumerate() {
3472            let qubit_ptr = arg.into_pointer_value();
3473            let q_handle = get_qubit_handle(
3474                ctx,
3475                args.capability_flags,
3476                args.qubit_array,
3477                args.qubit_array_type,
3478                &builder,
3479                qubit_ptr,
3480            )?;
3481
3482            let elem_ptr = unsafe {
3483                builder.build_gep(
3484                    array_type,
3485                    array_alloca,
3486                    &[
3487                        i64_type.const_zero(),
3488                        i64_type.const_int(
3489                            u64::try_from(i)
3490                                .map_err(|e| format!("Failed to convert index: {e}"))?,
3491                            false,
3492                        ),
3493                    ],
3494                    "",
3495                )
3496            }
3497            .map_err(|e| format!("Failed to build GEP for barrier array: {e}"))?;
3498            builder
3499                .build_store(elem_ptr, q_handle)
3500                .map_err(|e| format!("Failed to store qubit handle in array: {e}"))?;
3501        }
3502
3503        let array_ptr = unsafe {
3504            builder.build_gep(
3505                array_type,
3506                array_alloca,
3507                &[i64_type.const_zero(), i64_type.const_zero()],
3508                "barrier_array_ptr",
3509            )
3510        }
3511        .map_err(|e| format!("Failed to build GEP for barrier array pointer: {e}"))?;
3512
3513        // void ___barrier(i64* %qbs, i64 %qbs_len)
3514        let barrier_func = get_or_create_function(
3515            module,
3516            "___barrier",
3517            ctx.void_type().fn_type(
3518                &[
3519                    ctx.ptr_type(AddressSpace::default()).into(),
3520                    i64_type.into(),
3521                ],
3522                false,
3523            ),
3524        );
3525
3526        builder
3527            .build_call(
3528                barrier_func,
3529                &[
3530                    array_ptr.into(),
3531                    i64_type
3532                        .const_int(
3533                            u64::try_from(num_qubits)
3534                                .map_err(|e| format!("Failed to convert num_qubits: {e}"))?,
3535                            false,
3536                        )
3537                        .into(),
3538                ],
3539                "",
3540            )
3541            .map_err(|e| format!("Failed to build call to ___barrier: {e}"))?;
3542
3543        instr.erase_from_basic_block();
3544        Ok(())
3545    }
3546
3547    fn handle_read_result_call<'ctx>(
3548        ctx: &'ctx Context,
3549        module: *const (),
3550        instr: &'ctx inkwell::values::InstructionValue<'ctx>,
3551        fn_name: &str,
3552        capability_flags: CapabilityFlags,
3553        global_mapping: *mut (),
3554        result_ssa: *mut (),
3555    ) -> Result<(), String> {
3556        let module = unsafe { &*module.cast::<Module<'ctx>>() };
3557        let global_mapping = unsafe {
3558            &mut *global_mapping.cast::<HashMap<String, inkwell::values::GlobalValue<'ctx>>>()
3559        };
3560        let result_ssa = unsafe {
3561            &mut *result_ssa
3562                .cast::<Vec<Option<(BasicValueEnum<'ctx>, Option<BasicValueEnum<'ctx>>)>>>()
3563        };
3564        let call_args: Vec<BasicValueEnum> = extract_operands(instr)?;
3565        let result_ptr = call_args[0].into_pointer_value();
3566
3567        let builder = ctx.create_builder();
3568        builder.position_before(instr);
3569
3570        let bool_val = read_result_bool(
3571            ctx,
3572            module,
3573            &builder,
3574            result_ptr,
3575            capability_flags,
3576            result_ssa,
3577        )?;
3578
3579        if fn_name == "__quantum__rt__read_result" {
3580            let instruction_val = bool_val
3581                .as_instruction_value()
3582                .ok_or("Failed to convert bool_val to instruction value")?;
3583            instr.replace_all_uses_with(&instruction_val);
3584        } else {
3585            let new_global = get_or_create_result_output_global(
3586                ctx,
3587                module,
3588                global_mapping,
3589                capability_flags,
3590                result_ptr,
3591                call_args[1],
3592            )?;
3593
3594            let print_func = get_or_create_function(
3595                module,
3596                "print_bool",
3597                ctx.void_type().fn_type(
3598                    &[
3599                        ctx.ptr_type(AddressSpace::default()).into(), // ptr
3600                        ctx.i64_type().into(),                        // i64
3601                        ctx.bool_type().into(),                       // i1
3602                    ],
3603                    false,
3604                ),
3605            );
3606
3607            add_print_call(ctx, &builder, new_global, print_func, bool_val)?;
3608        }
3609        instr.erase_from_basic_block();
3610        Ok(())
3611    }
3612
3613    pub fn checked_result_index(result_idx: u64, result_ssa_len: usize) -> Result<usize, String> {
3614        let result_idx_usize = usize::try_from(result_idx)
3615            .map_err(|e| format!("Failed to convert result index to usize: {e}"))?;
3616        if result_idx_usize >= result_ssa_len {
3617            return Err(format!(
3618                "Result index {result_idx} exceeds required_num_results ({result_ssa_len})"
3619            ));
3620        }
3621        Ok(result_idx_usize)
3622    }
3623
3624    fn handle_classical_record_output(args: &mut ProcessCallArgs<'_>) -> Result<(), String> {
3625        let ProcessCallArgs {
3626            ctx,
3627            instr,
3628            fn_name,
3629            ..
3630        } = args;
3631        let module = unsafe { &*args.module };
3632        let global_mapping = unsafe { &mut *args.global_mapping };
3633        let call_args: Vec<BasicValueEnum> = extract_operands(instr)?;
3634        let (print_func_name, value, type_tag) = match fn_name.as_str() {
3635            "__quantum__rt__bool_record_output" => (
3636                "print_bool",
3637                call_args[0].into_int_value().as_basic_value_enum(),
3638                "BOOL",
3639            ),
3640            "__quantum__rt__int_record_output" => (
3641                "print_int",
3642                call_args[0].into_int_value().as_basic_value_enum(),
3643                "INT",
3644            ),
3645            "__quantum__rt__double_record_output" => (
3646                "print_float",
3647                call_args[0].into_float_value().as_basic_value_enum(),
3648                "FLOAT",
3649            ),
3650            _ => return Err(format!("Unsupported print helper for `{fn_name}`")),
3651        };
3652
3653        // Get the print function type based on the value type
3654        let ret_type = ctx.void_type();
3655        let param_types = &[
3656            ctx.ptr_type(AddressSpace::default()).into(), // ptr
3657            ctx.i64_type().into(),                        // i64
3658            match type_tag {
3659                "BOOL" => ctx.bool_type().into(),
3660                "INT" => ctx.i64_type().into(),
3661                "FLOAT" => ctx.f64_type().into(),
3662                _ => return Err(format!("Unsupported type tag `{type_tag}` for `{fn_name}`")),
3663            },
3664        ];
3665        let fn_type = ret_type.fn_type(param_types, false);
3666
3667        let print_func = get_or_create_function(module, print_func_name, fn_type);
3668
3669        let parsed_name = parse_gep(call_args[1]);
3670        let old_name = parsed_name.clone().unwrap_or_else(|_| {
3671            format!(
3672                "anon_classical_{}",
3673                match type_tag {
3674                    "BOOL" => "bool",
3675                    "INT" => "int",
3676                    "FLOAT" => "float",
3677                    _ => "value",
3678                }
3679            )
3680        });
3681
3682        let full_tag = if let Some(existing) = global_mapping.get(old_name.as_str()) {
3683            get_string_label(*existing)?
3684        } else if parsed_name.is_ok() {
3685            return Err(format!("Output global `{old_name}` not found in mapping"));
3686        } else {
3687            old_name.clone()
3688        };
3689        // Parse the label from the global string (format: USER:RESULT:tag)
3690        let old_label = extract_label_from_tag(&full_tag);
3691
3692        let (new_const, new_name) = build_result_global(ctx, old_label, &old_name, type_tag, None)?;
3693
3694        let new_global = module.add_global(new_const.get_type(), None, &new_name);
3695        new_global.set_initializer(&new_const);
3696        new_global.set_linkage(inkwell::module::Linkage::Private);
3697        new_global.set_constant(true);
3698        global_mapping.insert(old_name, new_global);
3699        record_classical_output(ctx, *instr, new_global, print_func, value)?;
3700        Ok(())
3701    }
3702
3703    fn handle_get_current_shot(args: &ProcessCallArgs<'_>) -> Result<(), String> {
3704        let ProcessCallArgs { ctx, instr, .. } = args;
3705        let module = module_ref(args);
3706        let get_shot_func = get_or_create_function(
3707            module,
3708            "get_current_shot",
3709            ctx.i64_type().fn_type(&[], false),
3710        );
3711        handle_runtime_value_call(
3712            ctx,
3713            *instr,
3714            get_shot_func,
3715            &[],
3716            "current_shot",
3717            "Failed to build call to get_current_shot",
3718            "Failed to get basic value from get_current_shot call",
3719        )
3720    }
3721
3722    fn handle_runtime_value_call<'ctx>(
3723        ctx: &'ctx Context,
3724        instr: inkwell::values::InstructionValue<'ctx>,
3725        callee: FunctionValue<'ctx>,
3726        call_args: &[BasicMetadataValueEnum<'ctx>],
3727        name: &str,
3728        build_error: &str,
3729        value_error: &str,
3730    ) -> Result<(), String> {
3731        let builder = ctx.create_builder();
3732        builder.position_before(&instr);
3733        let value = call_basic_value(&builder, callee, call_args, name, build_error, value_error)?;
3734        if let Some(instr_val) = value.as_instruction_value() {
3735            instr.replace_all_uses_with(&instr_val);
3736        }
3737        instr.erase_from_basic_block();
3738        Ok(())
3739    }
3740
3741    fn handle_runtime_void_call<'ctx>(
3742        ctx: &'ctx Context,
3743        instr: inkwell::values::InstructionValue<'ctx>,
3744        callee: FunctionValue<'ctx>,
3745        call_args: &[BasicMetadataValueEnum<'ctx>],
3746        build_error: &str,
3747    ) -> Result<(), String> {
3748        let builder = ctx.create_builder();
3749        builder.position_before(&instr);
3750        if let Err(err) = builder.build_call(callee, call_args, "") {
3751            return Err(format!("{build_error}: {err}"));
3752        }
3753        instr.erase_from_basic_block();
3754        Ok(())
3755    }
3756
3757    fn handle_random_seed(args: &ProcessCallArgs<'_>) -> Result<(), String> {
3758        let ProcessCallArgs { ctx, instr, .. } = args;
3759        let module = module_ref(args);
3760        let call_args: Vec<BasicValueEnum> = extract_operands(instr)?;
3761        let random_seed_func = get_or_create_function(
3762            module,
3763            "random_seed",
3764            ctx.void_type().fn_type(&[ctx.i64_type().into()], false),
3765        );
3766        handle_runtime_void_call(
3767            ctx,
3768            *instr,
3769            random_seed_func,
3770            &[call_args[0].into()],
3771            "Failed to build call to random_seed",
3772        )
3773    }
3774
3775    fn handle_random_int(args: &ProcessCallArgs<'_>) -> Result<(), String> {
3776        let ProcessCallArgs { ctx, instr, .. } = args;
3777        let module = module_ref(args);
3778        let random_int_func =
3779            get_or_create_function(module, "random_int", ctx.i32_type().fn_type(&[], false));
3780        handle_runtime_value_call(
3781            ctx,
3782            *instr,
3783            random_int_func,
3784            &[],
3785            "rint",
3786            "Failed to build call to random_int",
3787            "Failed to get basic value from random_int call",
3788        )
3789    }
3790
3791    fn handle_random_float(args: &ProcessCallArgs<'_>) -> Result<(), String> {
3792        let ProcessCallArgs { ctx, instr, .. } = args;
3793        let module = module_ref(args);
3794        let random_float_func =
3795            get_or_create_function(module, "random_float", ctx.f64_type().fn_type(&[], false));
3796        handle_runtime_value_call(
3797            ctx,
3798            *instr,
3799            random_float_func,
3800            &[],
3801            "rfloat",
3802            "Failed to build call to random_float",
3803            "Failed to get basic value from random_float call",
3804        )
3805    }
3806
3807    fn handle_random_int_bounded(args: &ProcessCallArgs<'_>) -> Result<(), String> {
3808        let ProcessCallArgs { ctx, instr, .. } = args;
3809        let module = module_ref(args);
3810        let call_args: Vec<BasicValueEnum> = extract_operands(instr)?;
3811        let random_rng_func = get_or_create_function(
3812            module,
3813            "random_rng",
3814            ctx.i32_type().fn_type(&[ctx.i32_type().into()], false),
3815        );
3816        handle_runtime_value_call(
3817            ctx,
3818            *instr,
3819            random_rng_func,
3820            &[call_args[0].into()],
3821            "rintb",
3822            "Failed to build call to random_rng",
3823            "Failed to get basic value from random_rng call",
3824        )
3825    }
3826
3827    fn handle_random_advance(args: &ProcessCallArgs<'_>) -> Result<(), String> {
3828        let ProcessCallArgs { ctx, instr, .. } = args;
3829        let module = module_ref(args);
3830        let call_args: Vec<BasicValueEnum> = extract_operands(instr)?;
3831        let random_advance_func = get_or_create_function(
3832            module,
3833            "random_advance",
3834            ctx.void_type().fn_type(&[ctx.i64_type().into()], false),
3835        );
3836        handle_runtime_void_call(
3837            ctx,
3838            *instr,
3839            random_advance_func,
3840            &[call_args[0].into()],
3841            "Failed to build call to random_advance",
3842        )
3843    }
3844
3845    #[cfg(test)]
3846    mod tests {
3847        use super::format_grouped_module_flag_error;
3848
3849        #[test]
3850        fn test_format_grouped_module_flag_error_returns_none_for_empty_flag_names() {
3851            let empty: [String; 0] = [];
3852            assert_eq!(
3853                format_grouped_module_flag_error(
3854                    "Missing required module flag",
3855                    "Missing required module flags",
3856                    &empty,
3857                ),
3858                None,
3859            );
3860        }
3861    }
3862}
3863
3864pub(crate) fn decode_llvm_bytes(value: &[u8]) -> Option<&str> {
3865    std::str::from_utf8(value).ok()
3866}
3867
3868pub(crate) fn decode_llvm_c_string(value: &std::ffi::CStr) -> Option<&str> {
3869    value.to_str().ok()
3870}
3871
3872/// Create an LLVM memory buffer by copying the provided bytes.
3873///
3874/// This is the supported low-level construction path for public bitcode bytes
3875/// that need to be handed to `inkwell`/LLVM parser APIs.
3876/// It deliberately does not classify raw versus wrapped LLVM bitcode
3877/// containers; callers that need precise malformed-input diagnostics should
3878/// run those checks before constructing the buffer.
3879///
3880/// # Errors
3881/// Returns an error if the buffer name contains an interior NUL byte or LLVM
3882/// fails to allocate the memory buffer.
3883pub fn create_memory_buffer_from_bytes(
3884    bytes: &[u8],
3885    name: &str,
3886) -> Result<inkwell::memory_buffer::MemoryBuffer<'static>, String> {
3887    use llvm_sys::core::LLVMCreateMemoryBufferWithMemoryRangeCopy;
3888
3889    let name = std::ffi::CString::new(name)
3890        .map_err(|_err| "Memory buffer name contains interior NUL byte".to_string())?;
3891    let memory_buffer = unsafe {
3892        LLVMCreateMemoryBufferWithMemoryRangeCopy(bytes.as_ptr().cast(), bytes.len(), name.as_ptr())
3893    };
3894    if memory_buffer.is_null() {
3895        return Err(
3896            "LLVM failed to create memory buffer from bytes: received null memory buffer pointer"
3897                .to_string(),
3898        );
3899    }
3900
3901    unsafe { Ok(inkwell::memory_buffer::MemoryBuffer::new(memory_buffer)) }
3902}
3903
3904pub(crate) fn create_module_from_ir_text<'ctx>(
3905    ctx: &'ctx inkwell::context::Context,
3906    ll_text: &str,
3907    name: &str,
3908) -> Result<inkwell::module::Module<'ctx>, String> {
3909    let memory_buffer = create_memory_buffer_from_bytes(ll_text.as_bytes(), name)?;
3910    ctx.create_module_from_ir(memory_buffer)
3911        .map_err(|e| format!("Failed to create module from LLVM IR: {e}"))
3912}
3913
3914/// Copy an LLVM memory buffer into public bitcode bytes.
3915///
3916/// `MemoryBuffer::as_slice` reports exactly the buffer payload LLVM allocated,
3917/// so the returned bytes can be written to files or passed to file-oriented
3918/// bitcode consumers as-is. No terminator is stripped: bitcode payloads are
3919/// four-byte aligned and legitimately end in a zero byte, so trimming a
3920/// trailing NUL would truncate real bitcode.
3921#[must_use]
3922pub fn memory_buffer_to_owned_bytes(
3923    memory_buffer: &inkwell::memory_buffer::MemoryBuffer<'_>,
3924) -> Vec<u8> {
3925    memory_buffer.as_slice().to_vec()
3926}
3927
3928/// Parse public bitcode bytes into an `inkwell` module.
3929///
3930/// This helper uses [`create_memory_buffer_from_bytes`] so downstream Rust
3931/// callers do not need to duplicate the LLVM memory-buffer compatibility path.
3932/// It is a copied-buffer parser path, not a bitcode container classifier or
3933/// malformed-input validator. Callers that need stable diagnostics for
3934/// truncated headers, wrapped-bitcode payload ranges, or other pre-parse
3935/// conditions should keep those checks before calling this helper.
3936///
3937/// # Errors
3938/// Returns an error if the memory buffer cannot be created or LLVM rejects the
3939/// bitcode payload.
3940pub fn parse_bitcode_module<'ctx>(
3941    ctx: &'ctx inkwell::context::Context,
3942    bitcode: &[u8],
3943    name: &str,
3944) -> Result<inkwell::module::Module<'ctx>, String> {
3945    let memory_buffer = create_memory_buffer_from_bytes(bitcode, name)?;
3946    inkwell::module::Module::parse_bitcode_from_buffer(&memory_buffer, ctx)
3947        .map_err(|e| format!("Failed to parse bitcode: {e}"))
3948}
3949
3950/// Core QIR to QIS translation logic.
3951///
3952/// # Arguments
3953/// - `bc_bytes` - The QIR bytes to translate.
3954/// - `opt_level` - The optimization level to use (0-3). Platform defaults are
3955///   exposed via [`DEFAULT_OPT_LEVEL`].
3956/// - `target` - Target architecture ("aarch64", "x86-64", "native"). Platform
3957///   defaults are exposed via [`DEFAULT_TARGET`].
3958/// - `wasm_bytes` - Optional WASM bytes for Wasm codegen. When the `wasm` feature is enabled,
3959///   invalid WASM bytes are rejected during translation.
3960///
3961/// # Errors
3962/// Returns an error string if the translation fails.
3963pub fn qir_to_qis(
3964    bc_bytes: &[u8],
3965    opt_level: u32,
3966    target: &str,
3967    wasm_bytes: Option<&[u8]>,
3968) -> Result<Vec<u8>, String> {
3969    qir_to_qis_with_passthrough_calls(bc_bytes, opt_level, target, wasm_bytes, &[])
3970}
3971
3972/// QIR to QIS translation logic with an explicit downstream external-call pass-through allow-list.
3973///
3974/// Preserves unknown externals that are either explicitly allow-listed or tagged for downstream
3975/// handling via `cudaq-fnid` / `wasm` attributes.
3976/// IR-defined functions and converter/runtime-reserved names are not pass-through eligible.
3977/// Also supports allow-listed unknown `__quantum__qis__*` / `__quantum__rt__*` externals;
3978/// [`validate_qir`] still rejects those names (and `___*`).
3979///
3980/// # Arguments
3981/// - `bc_bytes` - The QIR bytes to translate.
3982/// - `opt_level` - The optimization level to use (0-3). Platform defaults are
3983///   exposed via [`DEFAULT_OPT_LEVEL`].
3984/// - `target` - Target architecture ("aarch64", "x86-64", "native"). Platform
3985///   defaults are exposed via [`DEFAULT_TARGET`].
3986/// - `wasm_bytes` - Optional WASM bytes for Wasm codegen. When the `wasm` feature is enabled,
3987///   invalid WASM bytes are rejected during translation.
3988/// - `passthrough_calls` - External call names to preserve for downstream processing.
3989///
3990/// # Errors
3991/// Returns an error string if the translation fails.
3992pub fn qir_to_qis_with_passthrough_calls(
3993    bc_bytes: &[u8],
3994    opt_level: u32,
3995    target: &str,
3996    wasm_bytes: Option<&[u8]>,
3997    passthrough_calls: &[&str],
3998) -> Result<Vec<u8>, String> {
3999    use crate::{
4000        aux::{get_capability_flags, process_entry_function, validate_static_qubit_helper_usage},
4001        convert::{
4002            add_qmain_wrapper, create_qubit_array, find_entry_function, free_all_qubits,
4003            get_string_attrs, process_ir_defined_q_fns, prune_unused_ir_qis_helpers,
4004            validate_declared_resource_limits,
4005        },
4006        decompose::add_decompositions,
4007        opt::optimize,
4008        utils::add_generator_metadata,
4009    };
4010    use inkwell::{attributes::AttributeLoc, context::Context};
4011    use std::{collections::BTreeSet, env};
4012
4013    let ctx = Context::create();
4014    let module = parse_bitcode_module(&ctx, bc_bytes, "bitcode")?;
4015    crate::llvm_verify::verify_module(&module, "LLVM module verification failed after parse")?;
4016    let passthrough_calls: BTreeSet<String> = passthrough_calls
4017        .iter()
4018        .copied()
4019        .map(ToOwned::to_owned)
4020        .collect();
4021    for name in &passthrough_calls {
4022        if module
4023            .get_function(name)
4024            .is_some_and(|function| function.count_basic_blocks() > 0)
4025        {
4026            return Err(format!(
4027                "Pass-through function `{name}` must be an external declaration"
4028            ));
4029        }
4030    }
4031
4032    add_decompositions(&ctx, &module)
4033        .map_err(|e| format!("Failed to add QIR decompositions: {e}"))?;
4034    let entry_fn = find_entry_function(&module)
4035        .map_err(|e| format!("Failed to find entry function in QIR module: {e}"))?;
4036    validate_declared_resource_limits(entry_fn)?;
4037
4038    let entry_fn_name = entry_fn
4039        .get_name()
4040        .to_str()
4041        .map_err(|e| format!("Invalid UTF-8 in entry function name: {e}"))?;
4042    let capability_flags = get_capability_flags(&module);
4043    let mut validation_errors = Vec::new();
4044    validate_static_qubit_helper_usage(&module, entry_fn, &mut validation_errors);
4045    if !validation_errors.is_empty() {
4046        return Err(validation_errors.join("; "));
4047    }
4048
4049    log::trace!("Entry function: {entry_fn_name}");
4050    let new_name = format!("___user_qir_{entry_fn_name}");
4051    entry_fn.as_global_value().set_name(&new_name);
4052    log::debug!("Renamed entry function to: {new_name}");
4053    let qubit_array = if capability_flags.dynamic_qubit_management {
4054        None
4055    } else {
4056        Some(create_qubit_array(&ctx, &module, entry_fn)?)
4057    };
4058
4059    let wasm_fns = get_wasm_functions(wasm_bytes)?;
4060    process_entry_function(
4061        &ctx,
4062        &module,
4063        entry_fn,
4064        &wasm_fns,
4065        &passthrough_calls,
4066        qubit_array,
4067        capability_flags,
4068    )?;
4069
4070    // Handle IR defined functions that take qubits
4071    process_ir_defined_q_fns(
4072        &ctx,
4073        &module,
4074        entry_fn,
4075        capability_flags.dynamic_qubit_management,
4076        &passthrough_calls,
4077    )?;
4078
4079    if let Some(qubit_array) = qubit_array {
4080        free_all_qubits(&ctx, &module, entry_fn, qubit_array)?;
4081    }
4082
4083    // Add qmain wrapper that calls setup, entry function, and teardown
4084    let _ = add_qmain_wrapper(&ctx, &module, entry_fn);
4085
4086    crate::llvm_verify::verify_module(&module, "LLVM module verification failed")?;
4087
4088    // Clean up the translated module
4089    for attr in get_string_attrs(entry_fn) {
4090        let kind = decode_string_attribute_kind(attr)?;
4091        entry_fn.remove_string_attribute(AttributeLoc::Function, &kind);
4092    }
4093
4094    // TODO: remove global module metadata
4095    // seems inkwell doesn't support this yet
4096
4097    // Add metadata to the module
4098    let md_string = ctx.metadata_string("mainlib");
4099    let md_node = ctx.metadata_node(&[md_string.into()]);
4100    module
4101        .add_global_metadata("name", &md_node)
4102        .map_err(|e| format!("Failed to add global metadata: {e}"))?;
4103    add_generator_metadata(&ctx, &module, "gen_name", env!("CARGO_PKG_NAME"))?;
4104    add_generator_metadata(&ctx, &module, "gen_version", env!("CARGO_PKG_VERSION"))?;
4105
4106    optimize(&module, opt_level, target)?;
4107    prune_unused_ir_qis_helpers(&module);
4108
4109    Ok(memory_buffer_to_owned_bytes(
4110        &module.write_bitcode_to_memory(),
4111    ))
4112}
4113
4114/// Extract WASM function mapping from the given WASM bytes.
4115///
4116/// # Errors
4117/// Returns an error string if parsing fails.
4118#[cfg(feature = "wasm")]
4119pub fn get_wasm_functions(
4120    wasm_bytes: Option<&[u8]>,
4121) -> Result<std::collections::BTreeMap<String, u64>, String> {
4122    use crate::utils::parse_wasm_functions;
4123    use std::collections::BTreeMap;
4124
4125    let mut wasm_fns: BTreeMap<String, u64> = BTreeMap::new();
4126    if let Some(bytes) = wasm_bytes {
4127        wasm_fns = parse_wasm_functions(bytes)?;
4128        log::debug!("WASM function map: {wasm_fns:?}");
4129    }
4130    Ok(wasm_fns)
4131}
4132
4133#[cfg(not(feature = "wasm"))]
4134fn get_wasm_functions(
4135    _wasm_bytes: Option<&[u8]>,
4136) -> Result<std::collections::BTreeMap<String, u64>, String> {
4137    Ok(std::collections::BTreeMap::new())
4138}
4139
4140/// Validate the given QIR bitcode.
4141///
4142/// # Arguments
4143/// - `bc_bytes` - The QIR bytes to validate.
4144/// - `wasm_bytes` - Optional WASM bytes to validate against.
4145///
4146/// # Errors
4147/// Returns an error string if validation fails.
4148pub fn validate_qir(bc_bytes: &[u8], wasm_bytes: Option<&[u8]>) -> Result<(), String> {
4149    use crate::{
4150        aux::{
4151            get_capability_flags, validate_module_flags, validate_module_layout_and_triple,
4152            validate_qir_call_sites,
4153        },
4154        convert::{
4155            ENTRY_ATTRIBUTE_KEYS, find_entry_function, validate_declared_qubit_limit,
4156            validate_declared_result_limit,
4157        },
4158    };
4159    use inkwell::{attributes::AttributeLoc, context::Context};
4160
4161    let ctx = Context::create();
4162    let module = parse_bitcode_module(&ctx, bc_bytes, "bitcode")?;
4163    let mut errors = Vec::new();
4164
4165    let capability_flags = get_capability_flags(&module);
4166    validate_module_layout_and_triple(&module);
4167    let entry_fn = if let Ok(entry_fn) = find_entry_function(&module) {
4168        if entry_fn.get_basic_blocks().is_empty() {
4169            errors.push("Entry function has no basic blocks".to_string());
4170        }
4171
4172        // Enforce required attributes
4173        for attr in ENTRY_ATTRIBUTE_KEYS.iter().copied().filter(|attr| {
4174            *attr != "entry_point"
4175                && !(*attr == "required_num_qubits" && capability_flags.dynamic_qubit_management)
4176                && !(*attr == "required_num_results" && capability_flags.dynamic_result_management)
4177        }) {
4178            let val = entry_fn.get_string_attribute(AttributeLoc::Function, attr);
4179            if val.is_none() {
4180                errors.push(format!("Missing required attribute: `{attr}`"));
4181            }
4182        }
4183
4184        // `required_num_qubits` must stay positive. `required_num_results`
4185        // may be zero for programs that only use classical-returning operations
4186        // such as `mz_leaked`.
4187        for (attr, type_) in [("required_num_qubits", "qubit")] {
4188            if capability_flags.dynamic_qubit_management {
4189                continue;
4190            }
4191            if entry_fn
4192                .get_string_attribute(AttributeLoc::Function, attr)
4193                .and_then(|a| {
4194                    decode_llvm_c_string(a.get_string_value())?
4195                        .parse::<u32>()
4196                        .ok()
4197                })
4198                == Some(0)
4199            {
4200                errors.push(format!("Entry function must have at least one {type_}"));
4201            }
4202        }
4203        if capability_flags.dynamic_qubit_management
4204            && let Err(err) = validate_declared_qubit_limit(entry_fn)
4205        {
4206            errors.push(err);
4207        }
4208        if capability_flags.dynamic_result_management
4209            && let Err(err) = validate_declared_result_limit(entry_fn)
4210        {
4211            errors.push(err);
4212        }
4213        entry_fn
4214    } else {
4215        errors.push("No entry function found in QIR module".to_string());
4216        return Err(errors.join("; "));
4217    };
4218
4219    let wasm_fns = get_wasm_functions(wasm_bytes)?;
4220
4221    let capability_errors =
4222        validate_qir_call_sites(&module, entry_fn, &wasm_fns, capability_flags, &mut errors);
4223
4224    validate_module_flags(&module, &mut errors);
4225    errors.extend(capability_errors);
4226
4227    if !errors.is_empty() {
4228        return Err(errors.join("; "));
4229    }
4230    log::info!("QIR validation passed");
4231    Ok(())
4232}
4233
4234/// Convert QIR LLVM IR text to QIR bitcode bytes.
4235///
4236/// # Errors
4237/// Returns an error string if the LLVM IR is invalid.
4238pub fn qir_ll_to_bc(ll_text: &str) -> Result<Vec<u8>, String> {
4239    use inkwell::context::Context;
4240
4241    let ctx = Context::create();
4242    let module = create_module_from_ir_text(&ctx, ll_text, "qir")?;
4243
4244    Ok(memory_buffer_to_owned_bytes(
4245        &module.write_bitcode_to_memory(),
4246    ))
4247}
4248
4249fn decode_string_attribute_kind(attr: inkwell::attributes::Attribute) -> Result<String, String> {
4250    use llvm_sys::core::LLVMGetStringAttributeKind;
4251    use std::slice;
4252
4253    let mut kind_len = 0_u32;
4254    let kind_ptr = unsafe { LLVMGetStringAttributeKind(attr.as_mut_ptr(), &raw mut kind_len) };
4255    if kind_ptr.is_null() {
4256        return Err("LLVM returned a null attribute kind pointer".to_string());
4257    }
4258    let kind_len = usize::try_from(kind_len)
4259        .map_err(|_err| "Attribute kind length does not fit into usize".to_string())?;
4260    let kind_bytes = unsafe { slice::from_raw_parts(kind_ptr.cast::<u8>(), kind_len) };
4261    std::str::from_utf8(kind_bytes)
4262        .map_err(|e| format!("Invalid UTF-8 in attribute kind: {e}"))
4263        .map(str::to_owned)
4264}
4265
4266fn decode_string_attribute_value(
4267    attr: inkwell::attributes::Attribute,
4268    kind: &str,
4269) -> Result<Option<String>, String> {
4270    use llvm_sys::core::LLVMGetStringAttributeValue;
4271    use std::slice;
4272
4273    let mut value_len = 0_u32;
4274    let value_ptr = unsafe { LLVMGetStringAttributeValue(attr.as_mut_ptr(), &raw mut value_len) };
4275    if value_len == 0 {
4276        return Ok(None);
4277    }
4278    if value_ptr.is_null() {
4279        return Err(format!(
4280            "LLVM returned a null attribute value pointer for `{kind}`"
4281        ));
4282    }
4283    let value_len = usize::try_from(value_len)
4284        .map_err(|_err| format!("Attribute `{kind}` value length does not fit into usize"))?;
4285    let value_bytes = unsafe { slice::from_raw_parts(value_ptr.cast::<u8>(), value_len) };
4286    let value = std::str::from_utf8(value_bytes)
4287        .map_err(|e| format!("Invalid UTF-8 in attribute `{kind}` value: {e}"))?
4288        .to_owned();
4289    Ok(Some(value))
4290}
4291
4292/// Get QIR entry point function attributes.
4293///
4294/// These attributes are used to generate METADATA records in QIR output schemas.
4295/// This function assumes that QIR has been validated using `validate_qir`.
4296///
4297/// # Errors
4298/// Returns an error string if the input bitcode is invalid.
4299pub fn get_entry_attributes(
4300    bc_bytes: &[u8],
4301) -> Result<std::collections::BTreeMap<String, Option<String>>, String> {
4302    use crate::convert::{find_entry_function, get_string_attrs};
4303    use inkwell::context::Context;
4304    use std::collections::BTreeMap;
4305
4306    let ctx = Context::create();
4307    let module = parse_bitcode_module(&ctx, bc_bytes, "bitcode")?;
4308
4309    let mut metadata = BTreeMap::new();
4310    if let Ok(entry_fn) = find_entry_function(&module) {
4311        for attr in get_string_attrs(entry_fn) {
4312            let kind_id = match decode_string_attribute_kind(attr) {
4313                Ok(kind_id) => kind_id,
4314                Err(err) => {
4315                    log::warn!("Skipping attribute with invalid kind: {err}");
4316                    continue;
4317                }
4318            };
4319            match decode_string_attribute_value(attr, &kind_id) {
4320                Ok(value) => {
4321                    metadata.insert(kind_id, value);
4322                }
4323                Err(err) => {
4324                    log::warn!("{err}");
4325                    metadata.insert(kind_id, None);
4326                }
4327            }
4328        }
4329    }
4330    Ok(metadata)
4331}
4332
4333#[cfg(feature = "python")]
4334mod exceptions {
4335    use pyo3::exceptions::PyException;
4336    use pyo3_stub_gen::create_exception;
4337
4338    create_exception!(
4339        qir_qis,
4340        ValidationError,
4341        PyException,
4342        "QIR ValidationError.\n\nRaised when the QIR is invalid."
4343    );
4344    create_exception!(
4345        qir_qis,
4346        CompilerError,
4347        PyException,
4348        "QIR CompilerError.\n\nRaised when QIR to QIS compilation fails."
4349    );
4350}
4351
4352#[cfg(feature = "python")]
4353#[pymodule]
4354mod qir_qis {
4355    use std::borrow::Cow;
4356    use std::collections::BTreeMap;
4357
4358    use super::{PyErr, PyResult, pyfunction};
4359
4360    use pyo3_stub_gen::derive::gen_stub_pyfunction;
4361
4362    #[pymodule_export]
4363    use super::exceptions::CompilerError;
4364    #[pymodule_export]
4365    use super::exceptions::ValidationError;
4366
4367    /// Validate the given QIR.
4368    ///
4369    /// # Arguments
4370    /// - `bc_bytes` - The QIR bytes to validate.
4371    /// - `wasm_bytes` - Optional WASM bytes to validate against.
4372    ///
4373    /// # Errors
4374    /// Returns a `ValidationError`:
4375    /// - If the QIR is invalid.
4376    /// - If the WASM module is invalid.
4377    /// - If a QIR-referenced WASM function is missing from the WASM module.
4378    #[gen_stub_pyfunction]
4379    #[pyfunction]
4380    #[allow(
4381        clippy::needless_pass_by_value,
4382        reason = "PyO3 entrypoint accepts owned bytes from Python callers"
4383    )]
4384    #[pyo3(signature = (bc_bytes, *, wasm_bytes = None))]
4385    pub fn validate_qir(bc_bytes: Cow<[u8]>, wasm_bytes: Option<Cow<[u8]>>) -> PyResult<()> {
4386        crate::validate_qir(&bc_bytes, wasm_bytes.as_deref())
4387            .map_err(PyErr::new::<ValidationError, _>)
4388    }
4389
4390    /// Translate QIR bitcode to Quantinuum QIS.
4391    ///
4392    /// # Arguments
4393    /// - `bc_bytes` - The QIR bytes to translate.
4394    /// - `opt_level` - The optimization level to use (0-3). Default is 2 on
4395    ///   Linux/macOS and 0 on Windows.
4396    /// - `target` - Target architecture (default: "aarch64" on Linux/macOS and
4397    ///   "native" on Windows; options: "x86-64", "native").
4398    /// - `wasm_bytes` - Optional WASM bytes for Wasm codegen.
4399    ///
4400    /// # Errors
4401    /// Returns a `CompilerError` if the translation fails.
4402    #[gen_stub_pyfunction]
4403    #[pyfunction]
4404    #[allow(
4405        clippy::needless_pass_by_value,
4406        reason = "PyO3 entrypoint accepts owned bytes from Python callers"
4407    )]
4408    #[allow(
4409        clippy::missing_errors_doc,
4410        reason = "PyO3 signature and Python exception type already document failures"
4411    )]
4412    #[cfg_attr(
4413        windows,
4414        pyo3(signature = (bc_bytes, *, opt_level = 0, target = "native", wasm_bytes = None))
4415    )]
4416    #[cfg_attr(
4417        not(windows),
4418        pyo3(signature = (bc_bytes, *, opt_level = 2, target = "aarch64", wasm_bytes = None))
4419    )]
4420    pub fn qir_to_qis<'a>(
4421        bc_bytes: Cow<[u8]>,
4422        opt_level: u32,
4423        target: &'a str,
4424        wasm_bytes: Option<Cow<'a, [u8]>>,
4425    ) -> PyResult<Cow<'a, [u8]>> {
4426        let result = crate::qir_to_qis(&bc_bytes, opt_level, target, wasm_bytes.as_deref())
4427            .map_err(PyErr::new::<CompilerError, _>)?;
4428
4429        Ok(result.into())
4430    }
4431
4432    /// Convert QIR LLVM IR to QIR bitcode.
4433    ///
4434    /// # Errors
4435    /// Returns a `ValidationError` if the LLVM IR is invalid.
4436    #[gen_stub_pyfunction]
4437    #[pyfunction]
4438    pub fn qir_ll_to_bc(ll_text: &str) -> PyResult<Cow<'_, [u8]>> {
4439        let result = crate::qir_ll_to_bc(ll_text).map_err(PyErr::new::<ValidationError, _>)?;
4440        Ok(result.into())
4441    }
4442
4443    /// Get QIR entry point function attributes.
4444    ///
4445    /// These attributes are used to generate METADATA records in QIR output schemas.
4446    /// This function assumes that QIR has been validated using `validate_qir`.
4447    ///
4448    /// # Errors
4449    /// Returns a `ValidationError` if the input bitcode is invalid.
4450    #[gen_stub_pyfunction]
4451    #[pyfunction]
4452    #[allow(
4453        clippy::needless_pass_by_value,
4454        reason = "PyO3 entrypoint accepts owned bytes from Python callers"
4455    )]
4456    fn get_entry_attributes(bc_bytes: Cow<[u8]>) -> PyResult<BTreeMap<String, Option<String>>> {
4457        crate::get_entry_attributes(&bc_bytes).map_err(PyErr::new::<ValidationError, _>)
4458    }
4459}
4460
4461#[cfg(feature = "python")]
4462define_stub_info_gatherer!(stub_info);
4463
4464#[cfg(test)]
4465mod test {
4466    #![allow(
4467        clippy::expect_used,
4468        reason = "tests use expect for direct fixture failure messages"
4469    )]
4470    #![allow(
4471        clippy::unwrap_used,
4472        reason = "tests unwrap fixed fixture and parser outputs"
4473    )]
4474    #![allow(
4475        clippy::indexing_slicing,
4476        reason = "tests inspect fixed positions in small generated arrays"
4477    )]
4478    use crate::{
4479        convert::get_string_label, create_memory_buffer_from_bytes, create_module_from_ir_text,
4480        get_entry_attributes, memory_buffer_to_owned_bytes, parse_bitcode_module, qir_ll_to_bc,
4481        qir_to_qis, qir_to_qis_with_passthrough_calls, validate_qir,
4482    };
4483    use inkwell::{
4484        context::Context,
4485        memory_buffer::MemoryBuffer,
4486        module::Module,
4487        values::{CallSiteValue, FunctionValue},
4488    };
4489    use proptest::prelude::*;
4490    use rstest::rstest;
4491    use std::{collections::BTreeMap, path::Path, sync::LazyLock};
4492    #[cfg(feature = "wasm")]
4493    use wasm_encoder::{ExportKind, ExportSection, Module as WasmModule};
4494
4495    const PROPERTY_FIXTURES: &[&str] = &[
4496        "tests/data/base.ll",
4497        "tests/data/base_array.ll",
4498        "tests/data/adaptive.ll",
4499        "tests/data/qir2_base.ll",
4500        "tests/data/qir2_adaptive.ll",
4501        "tests/data/mz_leaked.ll",
4502    ];
4503    const DYNAMIC_FEATURE_FIXTURES: &[&str] = &[
4504        "tests/data/dynamic_qubit_alloc.ll",
4505        "tests/data/dynamic_qubit_alloc_checked.ll",
4506        "tests/data/dynamic_qubit_array_checked.ll",
4507        "tests/data/dynamic_qubit_array_ssa.ll",
4508        "tests/data/dynamic_result_alloc.ll",
4509        "tests/data/dynamic_result_mixed_array_output.ll",
4510    ];
4511    static PROPERTY_FIXTURE_BITCODE: LazyLock<BTreeMap<&'static str, Vec<u8>>> =
4512        LazyLock::new(|| {
4513            PROPERTY_FIXTURES
4514                .iter()
4515                .map(|path| {
4516                    let ll_text =
4517                        std::fs::read_to_string(path).expect("Failed to read LLVM IR fixture");
4518                    let bitcode = qir_ll_to_bc(&ll_text)
4519                        .expect("Failed to convert LLVM IR fixture to bitcode");
4520                    (*path, bitcode)
4521                })
4522                .collect()
4523        });
4524
4525    fn conservative_translation_settings() -> (u32, &'static str) {
4526        (0, "native")
4527    }
4528
4529    fn load_fixture_bitcode(path: &str) -> Vec<u8> {
4530        PROPERTY_FIXTURE_BITCODE
4531            .get(path)
4532            .cloned()
4533            .expect("Fixture bitcode should be precompiled")
4534    }
4535
4536    fn verify_bitcode_module(bitcode: &[u8], name: &str) -> Result<(), String> {
4537        let ctx = Context::create();
4538        let module = parse_bitcode_module(&ctx, bitcode, name)?;
4539        crate::llvm_verify::verify_module(&module, "LLVM verifier rejected translated module")
4540    }
4541
4542    fn parse_bitcode_as_file(bitcode: &[u8], name: &str) -> Result<(), String> {
4543        let mut temp_file = tempfile::Builder::new()
4544            .prefix(name)
4545            .suffix(".bc")
4546            .tempfile()
4547            .map_err(|e| format!("Failed to create temp bitcode file: {e}"))?;
4548        std::io::Write::write_all(&mut temp_file, bitcode)
4549            .map_err(|e| format!("Failed to write temp bitcode: {e}"))?;
4550
4551        let ctx = Context::create();
4552        let memory_buffer = MemoryBuffer::create_from_file(temp_file.path())
4553            .map_err(|e| format!("Failed to read temp bitcode: {e}"))?;
4554        Module::parse_bitcode_from_buffer(&memory_buffer, &ctx)
4555            .map(|_| ())
4556            .map_err(|e| format!("Failed to parse bitcode: {e}"))
4557    }
4558
4559    fn validate_dynamic_array_backing_errors(ll_text: &str) -> Vec<String> {
4560        let ctx = Context::create();
4561        let module = create_module_from_ir_text(&ctx, ll_text, "qir")
4562            .expect("inline IR should parse for dynamic array backing validation");
4563        let mut errors = Vec::new();
4564        crate::aux::validate_dynamic_array_allocation_backing(&module, &mut errors);
4565        errors
4566    }
4567
4568    fn assert_public_bitcode_round_trips_from_file(bitcode: &[u8], name: &str) {
4569        let ctx = Context::create();
4570        let module = parse_bitcode_module(&ctx, bitcode, name)
4571            .expect("Bitcode should reparse through qir-qis helpers");
4572        let raw_buffer = module.write_bitcode_to_memory();
4573        assert_eq!(
4574            raw_buffer.as_slice().len(),
4575            bitcode.len(),
4576            "Public bitcode bytes should match LLVM's in-memory buffer length exactly"
4577        );
4578        parse_bitcode_as_file(bitcode, name)
4579            .expect("Public bitcode should parse when consumed from a file");
4580    }
4581
4582    #[cfg(feature = "wasm")]
4583    fn build_wasm_exports(exports: &[(String, u32)]) -> Vec<u8> {
4584        let mut module = WasmModule::new();
4585        let mut export_section = ExportSection::new();
4586        for (name, index) in exports {
4587            export_section.export(name, ExportKind::Func, *index);
4588        }
4589        module.section(&export_section);
4590        module.finish()
4591    }
4592
4593    fn minimal_qir_with_body(
4594        required_num_qubits: &str,
4595        required_num_results: &str,
4596        qir_major_flag: &str,
4597        extra_decl: &str,
4598        body: &str,
4599    ) -> String {
4600        format!(
4601            r#"%Qubit = type opaque
4602%Result = type opaque
4603
4604{extra_decl}
4605
4606define i64 @Entry_Point_Name() #0 {{
4607entry:
4608{body}
4609  ret i64 0
4610}}
4611
4612attributes #0 = {{ "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="{required_num_qubits}" "required_num_results"="{required_num_results}" }}
4613
4614!llvm.module.flags = !{{!0, !1, !2, !3}}
4615!0 = !{{i32 1, !"qir_major_version", i32 {qir_major_flag}}}
4616!1 = !{{i32 7, !"qir_minor_version", i32 0}}
4617!2 = !{{i32 1, !"dynamic_qubit_management", i1 false}}
4618!3 = !{{i32 1, !"dynamic_result_management", i1 false}}
4619"#
4620        )
4621    }
4622
4623    fn minimal_qir_missing_attr(missing_attr: &str) -> String {
4624        let attrs = [
4625            ("entry_point", None),
4626            ("qir_profiles", Some("base_profile")),
4627            ("output_labeling_schema", Some("schema_id")),
4628            ("required_num_qubits", Some("1")),
4629            ("required_num_results", Some("1")),
4630        ];
4631        let rendered_attrs = attrs
4632            .into_iter()
4633            .filter(|(name, _)| *name != missing_attr)
4634            .map(|(name, value)| {
4635                value.map_or_else(
4636                    || format!(r#""{name}""#),
4637                    |value| format!(r#""{name}"="{value}""#),
4638                )
4639            })
4640            .collect::<Vec<_>>()
4641            .join(" ");
4642
4643        format!(
4644            r#"
4645define i64 @Entry_Point_Name() #0 {{
4646entry:
4647  ret i64 0
4648}}
4649
4650attributes #0 = {{ {rendered_attrs} }}
4651
4652!llvm.module.flags = !{{!0, !1, !2, !3}}
4653!0 = !{{i32 1, !"qir_major_version", i32 1}}
4654!1 = !{{i32 7, !"qir_minor_version", i32 0}}
4655!2 = !{{i32 1, !"dynamic_qubit_management", i1 false}}
4656!3 = !{{i32 1, !"dynamic_result_management", i1 false}}
4657"#
4658        )
4659    }
4660
4661    fn minimal_dynamic_rt_declaration_qir(declaration: &str) -> String {
4662        format!(
4663            r#"
4664define i64 @Entry_Point_Name() #0 {{
4665entry:
4666  ret i64 0
4667}}
4668
4669{declaration}
4670
4671attributes #0 = {{ "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" }}
4672
4673!llvm.module.flags = !{{!0, !1, !2, !3, !4}}
4674!0 = !{{i32 1, !"qir_major_version", i32 2}}
4675!1 = !{{i32 7, !"qir_minor_version", i32 0}}
4676!2 = !{{i32 1, !"dynamic_qubit_management", i1 true}}
4677!3 = !{{i32 1, !"dynamic_result_management", i1 true}}
4678!4 = !{{i32 1, !"arrays", i1 true}}
4679"#
4680        )
4681    }
4682
4683    fn assert_malformed_dynamic_rt_declaration(case_name: &str, declaration: &str, fn_name: &str) {
4684        let ll_text = minimal_dynamic_rt_declaration_qir(declaration);
4685        let bc_bytes = qir_ll_to_bc(&ll_text).unwrap();
4686        let err = validate_qir(&bc_bytes, None)
4687            .expect_err(&format!("{case_name} should fail validation"));
4688        assert!(
4689            err.contains(&format!("Malformed QIR RT function declaration: {fn_name}")),
4690            "{case_name} reported unexpected error: {err}"
4691        );
4692    }
4693
4694    fn minimal_qir_with_duplicate_major_flags(first_major: &str, second_major: &str) -> String {
4695        format!(
4696            r#"
4697define i64 @Entry_Point_Name() #0 {{
4698entry:
4699  ret i64 0
4700}}
4701
4702attributes #0 = {{ "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" }}
4703
4704!llvm.module.flags = !{{!0, !1, !2, !3, !4}}
4705!0 = !{{i32 1, !"qir_major_version", i32 {first_major}}}
4706!1 = !{{i32 1, !"qir_major_version", i32 {second_major}}}
4707!2 = !{{i32 7, !"qir_minor_version", i32 0}}
4708!3 = !{{i32 1, !"dynamic_qubit_management", i1 false}}
4709!4 = !{{i32 1, !"dynamic_result_management", i1 false}}
4710"#
4711        )
4712    }
4713
4714    fn minimal_qir_with_duplicate_dynamic_flags(first_flag: &str, second_flag: &str) -> String {
4715        format!(
4716            r#"
4717define i64 @Entry_Point_Name() #0 {{
4718entry:
4719  ret i64 0
4720}}
4721
4722attributes #0 = {{ "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" }}
4723
4724!llvm.module.flags = !{{!0, !1, !2, !3, !4}}
4725!0 = !{{i32 1, !"qir_major_version", i32 1}}
4726!1 = !{{i32 7, !"qir_minor_version", i32 0}}
4727!2 = !{{i32 1, !"dynamic_qubit_management", i1 {first_flag}}}
4728!3 = !{{i32 1, !"dynamic_qubit_management", i1 {second_flag}}}
4729!4 = !{{i32 1, !"dynamic_result_management", i1 false}}
4730"#
4731        )
4732    }
4733
4734    fn collect_called_function_names(helper: FunctionValue<'_>) -> Vec<String> {
4735        let mut calls = Vec::new();
4736        for bb in helper.get_basic_blocks() {
4737            for instr in bb.get_instructions() {
4738                let Ok(call) = CallSiteValue::try_from(instr) else {
4739                    continue;
4740                };
4741                let Some(callee) = call.get_called_fn_value() else {
4742                    continue;
4743                };
4744                let Ok(name) = callee.get_name().to_str() else {
4745                    continue;
4746                };
4747                calls.push(name.to_string());
4748            }
4749        }
4750        calls
4751    }
4752
4753    fn get_qir_bytes(ll_path: &Path) -> Vec<u8> {
4754        let ll_text = std::fs::read_to_string(ll_path).expect("Failed to read test fixture");
4755        qir_ll_to_bc(&ll_text).expect("Failed to convert test fixture to bitcode")
4756    }
4757
4758    #[test]
4759    fn test_get_entry_attributes() {
4760        let ll_text = std::fs::read_to_string("tests/data/base-attrs.ll")
4761            .expect("Failed to read base-attrs.ll");
4762        let bc_bytes = qir_ll_to_bc(&ll_text).unwrap();
4763        let attrs = get_entry_attributes(&bc_bytes).unwrap();
4764        assert!(matches!(attrs.get("entry_point"), Some(None)));
4765        assert_eq!(
4766            attrs.get("qir_profiles"),
4767            Some(&Some("base_profile".to_string()))
4768        );
4769        assert_eq!(
4770            attrs.get("output_labeling_schema"),
4771            Some(&Some("labeled".to_string()))
4772        );
4773        assert_eq!(
4774            attrs.get("required_num_qubits"),
4775            Some(&Some("2".to_string()))
4776        );
4777        assert_eq!(
4778            attrs.get("required_num_results"),
4779            Some(&Some("2".to_string()))
4780        );
4781    }
4782
4783    #[test]
4784    fn test_entry_attributes_includes_optional_custom_attr() {
4785        let ll_text = r#"
4786define i64 @Entry_Point_Name() #0 {
4787entry:
4788  ret i64 0
4789}
4790
4791attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="labeled" "required_num_qubits"="2" "required_num_results"="2" "custom_attr"="custom_value" }
4792
4793!llvm.module.flags = !{!0, !1, !2, !3}
4794
4795!0 = !{i32 1, !"qir_major_version", i32 1}
4796!1 = !{i32 7, !"qir_minor_version", i32 0}
4797!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
4798!3 = !{i32 1, !"dynamic_result_management", i1 false}
4799"#;
4800        let bc_bytes = qir_ll_to_bc(ll_text).unwrap();
4801        let attrs = get_entry_attributes(&bc_bytes).unwrap();
4802        assert_eq!(
4803            attrs.get("custom_attr"),
4804            Some(&Some("custom_value".to_string()))
4805        );
4806    }
4807
4808    #[test]
4809    fn test_get_entry_attributes_is_order_independent() {
4810        let ll_text = r#"
4811define i64 @Entry_Point_Name() #0 {
4812entry:
4813  ret i64 0
4814}
4815
4816attributes #0 = { "custom_attr"="custom_value" "required_num_results"="2" "output_labeling_schema"="labeled" "entry_point" "required_num_qubits"="2" "qir_profiles"="base_profile" }
4817
4818!llvm.module.flags = !{!0, !1, !2, !3}
4819!0 = !{i32 1, !"qir_major_version", i32 1}
4820!1 = !{i32 7, !"qir_minor_version", i32 0}
4821!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
4822!3 = !{i32 1, !"dynamic_result_management", i1 false}
4823"#;
4824        let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode");
4825        let attrs = get_entry_attributes(&bc_bytes).expect("entry attributes should parse");
4826        assert!(matches!(attrs.get("entry_point"), Some(None)));
4827        assert_eq!(
4828            attrs.get("qir_profiles"),
4829            Some(&Some("base_profile".to_string()))
4830        );
4831        assert_eq!(
4832            attrs.get("custom_attr"),
4833            Some(&Some("custom_value".to_string()))
4834        );
4835    }
4836
4837    #[test]
4838    fn test_qir_to_qis_strips_custom_entry_attrs() {
4839        let ll_text = r#"
4840define i64 @Entry_Point_Name() #0 {
4841entry:
4842  ret i64 0
4843}
4844
4845attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="labeled" "required_num_qubits"="2" "required_num_results"="2" "custom_attr"="custom_value" }
4846
4847!llvm.module.flags = !{!0, !1, !2, !3}
4848
4849!0 = !{i32 1, !"qir_major_version", i32 1}
4850!1 = !{i32 7, !"qir_minor_version", i32 0}
4851!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
4852!3 = !{i32 1, !"dynamic_result_management", i1 false}
4853"#;
4854        let bc_bytes = qir_ll_to_bc(ll_text).unwrap();
4855        let (opt_level, target) = if cfg!(windows) {
4856            (0, "native")
4857        } else {
4858            (2, "aarch64")
4859        };
4860        let qis_bytes = qir_to_qis(&bc_bytes, opt_level, target, None).unwrap();
4861
4862        let ctx = Context::create();
4863        let module = parse_bitcode_module(&ctx, &qis_bytes, "qis").unwrap();
4864        let entry_fn = module.get_function("___user_qir_Entry_Point_Name").unwrap();
4865
4866        assert!(
4867            entry_fn
4868                .get_string_attribute(inkwell::attributes::AttributeLoc::Function, "custom_attr")
4869                .is_none()
4870        );
4871    }
4872
4873    fn passthrough_qir_fixture(
4874        declarations: &str,
4875        entry_body: &str,
4876        required_num_qubits: u32,
4877        required_num_results: u32,
4878    ) -> Vec<u8> {
4879        let ll_text = format!(
4880            r#"
4881{declarations}
4882
4883define i64 @Entry_Point_Name() #0 {{
4884entry:
4885{entry_body}
4886}}
4887
4888attributes #0 = {{ "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="labeled" "required_num_qubits"="{required_num_qubits}" "required_num_results"="{required_num_results}" }}
4889
4890!llvm.module.flags = !{{!0, !1, !2, !3}}
4891!0 = !{{i32 1, !"qir_major_version", i32 2}}
4892!1 = !{{i32 7, !"qir_minor_version", i32 0}}
4893!2 = !{{i32 1, !"dynamic_qubit_management", i1 false}}
4894!3 = !{{i32 1, !"dynamic_result_management", i1 false}}
4895"#
4896        );
4897        qir_ll_to_bc(ll_text.as_str()).expect("Failed to convert pass-through fixture to bitcode")
4898    }
4899
4900    fn passthrough_fixture() -> Vec<u8> {
4901        passthrough_qir_fixture(
4902            "declare i64 @external_counter()",
4903            "  %value = call i64 @external_counter()\n  ret i64 %value",
4904            1,
4905            0,
4906        )
4907    }
4908
4909    #[test]
4910    fn test_qir_to_qis_rejects_unknown_external_call_by_default() {
4911        let bc_bytes = passthrough_fixture();
4912
4913        let err = qir_to_qis(&bc_bytes, 0, "native", None)
4914            .expect_err("default API should reject unknown external call");
4915
4916        assert!(err.contains("Unsupported function: external_counter"));
4917    }
4918
4919    #[test]
4920    fn test_qir_to_qis_with_passthrough_calls_preserves_listed_external_call() {
4921        let bc_bytes = passthrough_fixture();
4922
4923        let qis_bytes =
4924            qir_to_qis_with_passthrough_calls(&bc_bytes, 0, "native", None, &["external_counter"])
4925                .expect("listed external call should pass through");
4926
4927        let ctx = Context::create();
4928        let module = parse_bitcode_module(&ctx, &qis_bytes, "qis").unwrap();
4929        let external_counter = module.get_function("external_counter").unwrap();
4930        assert!(module_contains_direct_call(&module, external_counter));
4931    }
4932
4933    #[test]
4934    fn test_qir_to_qis_with_passthrough_calls_rejects_unlisted_external_call() {
4935        let bc_bytes = passthrough_fixture();
4936
4937        let err =
4938            qir_to_qis_with_passthrough_calls(&bc_bytes, 0, "native", None, &["other_external"])
4939                .expect_err("unlisted external call should fail");
4940
4941        assert!(err.contains("Unsupported function: external_counter"));
4942    }
4943
4944    #[test]
4945    fn test_qir_to_qis_with_passthrough_calls_rejects_ir_defined_passthrough_name() {
4946        let bc_bytes = passthrough_qir_fixture(
4947            r"define i64 @external_counter() {
4948entry:
4949  ret i64 7
4950}",
4951            "  %value = call i64 @external_counter()\n  ret i64 %value",
4952            1,
4953            0,
4954        );
4955
4956        let err =
4957            qir_to_qis_with_passthrough_calls(&bc_bytes, 0, "native", None, &["external_counter"])
4958                .expect_err("IR-defined pass-through names should fail");
4959
4960        assert!(
4961            err.contains(
4962                "Pass-through function `external_counter` must be an external declaration"
4963            )
4964        );
4965    }
4966
4967    #[test]
4968    fn test_qir_to_qis_with_passthrough_calls_preserves_listed_prefixed_external_call() {
4969        let bc_bytes = passthrough_qir_fixture(
4970            "declare void @__quantum__qis__vendor__body()",
4971            "  call void @__quantum__qis__vendor__body()\n  ret i64 0",
4972            1,
4973            0,
4974        );
4975
4976        let qis_bytes = qir_to_qis_with_passthrough_calls(
4977            &bc_bytes,
4978            0,
4979            "native",
4980            None,
4981            &["__quantum__qis__vendor__body"],
4982        )
4983        .expect("listed prefixed external call should pass through");
4984
4985        let ctx = Context::create();
4986        let module = parse_bitcode_module(&ctx, &qis_bytes, "qis").unwrap();
4987        let vendor = module.get_function("__quantum__qis__vendor__body").unwrap();
4988        assert!(module_contains_direct_call(&module, vendor));
4989    }
4990
4991    #[test]
4992    fn test_qir_to_qis_with_passthrough_calls_rejects_ir_defined_prefixed_call() {
4993        let bc_bytes = passthrough_qir_fixture(
4994            r"define void @__quantum__qis__vendor__body() {
4995entry:
4996  ret void
4997}",
4998            "  call void @__quantum__qis__vendor__body()\n  ret i64 0",
4999            1,
5000            0,
5001        );
5002
5003        let err = qir_to_qis_with_passthrough_calls(
5004            &bc_bytes,
5005            0,
5006            "native",
5007            None,
5008            &["__quantum__qis__vendor__body"],
5009        )
5010        .expect_err("IR-defined prefixed pass-through name should be rejected");
5011
5012        assert!(err.contains(
5013            "Pass-through function `__quantum__qis__vendor__body` must be an external declaration"
5014        ));
5015    }
5016
5017    #[test]
5018    fn test_qir_to_qis_rejects_unlisted_prefixed_external_rt_call() {
5019        let bc_bytes = passthrough_qir_fixture(
5020            "declare void @__quantum__rt__vendor__body()",
5021            "  call void @__quantum__rt__vendor__body()\n  ret i64 0",
5022            1,
5023            0,
5024        );
5025
5026        let err = qir_to_qis(&bc_bytes, 0, "native", None)
5027            .expect_err("unlisted prefixed runtime call should fail");
5028
5029        assert!(err.contains("Unsupported QIR RT function: __quantum__rt__vendor__body"));
5030    }
5031
5032    #[test]
5033    fn test_qir_to_qis_with_passthrough_calls_preserves_listed_prefixed_external_rt_call() {
5034        let bc_bytes = passthrough_qir_fixture(
5035            "declare void @__quantum__rt__vendor__body()",
5036            "  call void @__quantum__rt__vendor__body()\n  ret i64 0",
5037            1,
5038            0,
5039        );
5040
5041        let qis_bytes = qir_to_qis_with_passthrough_calls(
5042            &bc_bytes,
5043            0,
5044            "native",
5045            None,
5046            &["__quantum__rt__vendor__body"],
5047        )
5048        .expect("listed prefixed runtime call should pass through");
5049
5050        let ctx = Context::create();
5051        let module = parse_bitcode_module(&ctx, &qis_bytes, "qis").unwrap();
5052        let vendor = module.get_function("__quantum__rt__vendor__body").unwrap();
5053        assert!(module_contains_direct_call(&module, vendor));
5054    }
5055
5056    #[test]
5057    fn test_qir_to_qis_with_passthrough_calls_lowers_listed_builtin_qis_call() {
5058        let bc_bytes = passthrough_qir_fixture(
5059            r"%Qubit = type opaque
5060
5061declare void @__quantum__qis__h__body(%Qubit*)",
5062            "  %q0 = inttoptr i64 0 to %Qubit*\n  call void @__quantum__qis__h__body(%Qubit* %q0)\n  ret i64 0",
5063            1,
5064            0,
5065        );
5066
5067        let default_qis_bytes =
5068            qir_to_qis(&bc_bytes, 0, "native", None).expect("built-in QIS call should lower");
5069        let passthrough_qis_bytes = qir_to_qis_with_passthrough_calls(
5070            &bc_bytes,
5071            0,
5072            "native",
5073            None,
5074            &["__quantum__qis__h__body"],
5075        )
5076        .expect("listed built-in QIS call should still lower");
5077
5078        assert_eq!(passthrough_qis_bytes, default_qis_bytes);
5079    }
5080
5081    #[test]
5082    fn test_qir_to_qis_with_passthrough_calls_rejects_reserved_output_names() {
5083        for reserved_name in crate::convert::RESERVED_PASSTHROUGH_EXACT_NAMES
5084            .iter()
5085            .copied()
5086            .chain(["qir_qis.helper", "res_result_slot"])
5087        {
5088            let bc_bytes = passthrough_qir_fixture(
5089                format!("declare void @{reserved_name}()").as_str(),
5090                format!("  call void @{reserved_name}()\n  ret i64 0").as_str(),
5091                1,
5092                0,
5093            );
5094
5095            let err =
5096                qir_to_qis_with_passthrough_calls(&bc_bytes, 0, "native", None, &[reserved_name])
5097                    .expect_err("reserved pass-through names should fail");
5098
5099            assert!(err.contains(&format!(
5100                "Pass-through function `{reserved_name}` uses a reserved converter output name"
5101            )));
5102        }
5103    }
5104
5105    #[cfg(feature = "wasm")]
5106    #[test]
5107    fn test_qir_to_qis_with_passthrough_calls_rejects_invalid_wasm_bytes() {
5108        let bc_bytes = passthrough_qir_fixture("", "  ret i64 0", 0, 0);
5109        let err = qir_to_qis_with_passthrough_calls(&bc_bytes, 0, "native", Some(&[0x00]), &[])
5110            .expect_err("invalid wasm bytes should be rejected");
5111
5112        assert!(!err.is_empty());
5113    }
5114
5115    fn module_contains_direct_call(module: &Module<'_>, callee: FunctionValue<'_>) -> bool {
5116        module.get_functions().any(|function| {
5117            function.get_basic_blocks().iter().any(|bb| {
5118                bb.get_instructions()
5119                    .any(|instr| match CallSiteValue::try_from(instr) {
5120                        Ok(call) => call.get_called_fn_value() == Some(callee),
5121                        Err(()) => false,
5122                    })
5123            })
5124        })
5125    }
5126
5127    #[test]
5128    fn test_platform_default_conversion_settings_match_expectations() {
5129        if cfg!(windows) {
5130            assert_eq!(crate::DEFAULT_OPT_LEVEL, 0);
5131            assert_eq!(crate::DEFAULT_TARGET, "native");
5132        } else {
5133            assert_eq!(crate::DEFAULT_OPT_LEVEL, 2);
5134            assert_eq!(crate::DEFAULT_TARGET, "aarch64");
5135        }
5136    }
5137
5138    #[cfg(windows)]
5139    #[test]
5140    fn test_windows_optimized_conversion_returns_actionable_error() {
5141        let ll_text = r#"
5142define i64 @Entry_Point_Name() #0 {
5143entry:
5144  ret i64 0
5145}
5146
5147attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="labeled" "required_num_qubits"="1" "required_num_results"="1" }
5148
5149!llvm.module.flags = !{!0, !1, !2, !3}
5150!0 = !{i32 1, !"qir_major_version", i32 1}
5151!1 = !{i32 7, !"qir_minor_version", i32 0}
5152!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
5153!3 = !{i32 1, !"dynamic_result_management", i1 false}
5154"#;
5155        let bc_bytes = qir_ll_to_bc(ll_text).unwrap();
5156        let err = qir_to_qis(&bc_bytes, 1, "native", None)
5157            .expect_err("optimized native conversion should fail fast on Windows");
5158        assert!(err.contains("supported only for `target=\"x86-64\"`"));
5159        assert!(err.contains("opt_level=0"));
5160    }
5161
5162    #[cfg(windows)]
5163    #[test]
5164    fn test_windows_optimized_x86_64_conversion_smoke() {
5165        let bc_bytes = load_fixture_bitcode("tests/data/base.ll");
5166        let output_bc = qir_to_qis(&bc_bytes, 1, "x86-64", None)
5167            .expect("optimized x86-64 conversion should succeed on Windows");
5168        let ctx = Context::create();
5169        let module = parse_bitcode_module(&ctx, &output_bc, "optimized_windows_qis")
5170            .expect("optimized Windows output should parse");
5171        assert!(module.get_function("qmain").is_some());
5172    }
5173
5174    #[test]
5175    fn test_invalid_target_reports_invalid_architecture_even_when_optimized_requested() {
5176        let bc_bytes = load_fixture_bitcode("tests/data/base.ll");
5177        let err = qir_to_qis(&bc_bytes, 1, "invalid-target", None)
5178            .expect_err("invalid targets should fail before platform-specific optimized guards");
5179        assert!(err.contains("Invalid target architecture: invalid-target"));
5180    }
5181
5182    #[test]
5183    fn test_qir_ll_to_bc_accepts_legacy_typed_pointers() {
5184        let ll_text =
5185            std::fs::read_to_string("tests/data/base.ll").expect("Failed to read base.ll");
5186        let bc_bytes = qir_ll_to_bc(&ll_text).unwrap();
5187        assert!(!bc_bytes.is_empty());
5188    }
5189
5190    #[test]
5191    fn test_qir_ll_to_bc_output_parses_when_read_from_file() {
5192        let ll_text =
5193            std::fs::read_to_string("tests/data/base.ll").expect("Failed to read base.ll");
5194        let bc_bytes = qir_ll_to_bc(&ll_text).expect("Failed to convert base.ll to bitcode");
5195
5196        assert_public_bitcode_round_trips_from_file(&bc_bytes, "public_qir_output");
5197    }
5198
5199    #[test]
5200    fn test_public_bitcode_helpers_parse_and_extract_public_bytes() {
5201        let ll_text =
5202            std::fs::read_to_string("tests/data/base.ll").expect("Failed to read base.ll");
5203        let public_bc = qir_ll_to_bc(&ll_text).expect("Failed to convert base.ll to bitcode");
5204
5205        let ctx = Context::create();
5206        let module = parse_bitcode_module(&ctx, &public_bc, "public_bitcode")
5207            .expect("public parser helper should parse generated bitcode");
5208        let raw_buffer = module.write_bitcode_to_memory();
5209
5210        let extracted = memory_buffer_to_owned_bytes(&raw_buffer);
5211        assert_eq!(
5212            extracted.len(),
5213            raw_buffer.as_slice().len(),
5214            "extracted bytes should preserve the full LLVM buffer payload"
5215        );
5216        assert_eq!(extracted, raw_buffer.as_slice());
5217
5218        let copied_buffer = create_memory_buffer_from_bytes(&extracted, "copied_public_bitcode")
5219            .expect("public memory-buffer helper should copy public bitcode");
5220        Module::parse_bitcode_from_buffer(&copied_buffer, &ctx)
5221            .expect("copied public bitcode should parse through inkwell");
5222    }
5223
5224    /// Regression guard for the `inkwell` 0.10 memory-buffer change.
5225    ///
5226    /// Earlier `inkwell` releases reported an extra phantom trailing NUL from
5227    /// `MemoryBuffer::get_size`, so this crate trimmed the last byte. `inkwell`
5228    /// 0.10 reports the exact payload length, and trimming truncated real
5229    /// bitcode: the bytes stopped being four-byte aligned and LLVM rejected
5230    /// them with "Invalid bitcode signature".
5231    #[test]
5232    fn test_memory_buffer_to_owned_bytes_preserves_full_bitcode_payload() {
5233        let ll_text =
5234            std::fs::read_to_string("tests/data/base.ll").expect("Failed to read base.ll");
5235        let bitcode = qir_ll_to_bc(&ll_text).expect("Failed to convert base.ll to bitcode");
5236
5237        assert!(
5238            !bitcode.is_empty(),
5239            "bitcode writer should produce a non-empty payload"
5240        );
5241        assert_eq!(
5242            bitcode.len() % 4,
5243            0,
5244            "LLVM bitcode payloads are four-byte aligned; a truncated trailing byte breaks parsing"
5245        );
5246        assert_eq!(
5247            bitcode.first_chunk::<4>(),
5248            Some(&[0x42, 0x43, 0xc0, 0xde]),
5249            "bitcode should retain the LLVM 'BC\\u{{c0}}\\u{{de}}' magic"
5250        );
5251
5252        let ctx = Context::create();
5253        let module = parse_bitcode_module(&ctx, &bitcode, "round_trip")
5254            .expect("bitcode should parse without any terminator trimming");
5255        assert_eq!(
5256            memory_buffer_to_owned_bytes(&module.write_bitcode_to_memory()),
5257            bitcode,
5258            "round-tripping through the memory-buffer helper should be byte-identical"
5259        );
5260    }
5261
5262    /// Unwrap a [`PyResult`] without letting `PyErr` reach a `Debug` formatter.
5263    ///
5264    /// `Result::unwrap`/`expect` format the error with `Debug`, and
5265    /// `PyErr`'s `Debug` impl acquires the GIL. Under `cargo test` there is no
5266    /// initialized interpreter, so a failing `PyO3` call aborts the whole test
5267    /// binary with `SIGABRT` inside `PyO3` instead of reporting the real error.
5268    /// Converting to [`Option`] first keeps failures readable.
5269    #[cfg(feature = "python")]
5270    #[track_caller]
5271    #[expect(
5272        clippy::ok_expect,
5273        reason = "expecting the Result directly formats PyErr with Debug, which needs an \
5274                  initialized interpreter and aborts the test binary"
5275    )]
5276    fn expect_py_ok<T>(result: crate::PyResult<T>, msg: &str) -> T {
5277        result.ok().expect(msg)
5278    }
5279
5280    /// Cover the `PyO3` wrappers' success paths.
5281    ///
5282    /// The conversion tests call the plain Rust entry points so that failures
5283    /// print an error instead of aborting, which leaves the wrappers' owned
5284    /// `Cow` round-trip uncovered on the Rust side. Pin it here by asserting
5285    /// the wrappers return exactly what the Rust API returns.
5286    #[cfg(feature = "python")]
5287    #[test]
5288    fn test_python_wrappers_match_rust_api_on_success() {
5289        let ll_text =
5290            std::fs::read_to_string("tests/data/base.ll").expect("Failed to read base.ll");
5291
5292        let rust_bc = qir_ll_to_bc(&ll_text).expect("Rust qir_ll_to_bc should succeed");
5293        let py_bc = expect_py_ok(
5294            crate::qir_qis::qir_ll_to_bc(&ll_text),
5295            "PyO3 qir_ll_to_bc should succeed for a valid fixture",
5296        );
5297        assert_eq!(
5298            py_bc.as_ref(),
5299            rust_bc.as_slice(),
5300            "PyO3 qir_ll_to_bc should return the same bytes as the Rust API"
5301        );
5302
5303        expect_py_ok(
5304            crate::qir_qis::validate_qir(rust_bc.clone().into(), None),
5305            "PyO3 validate_qir should accept a valid fixture",
5306        );
5307
5308        // `opt_level=0` with `target="native"` is the one combination accepted
5309        // on every supported platform, including Windows with LLVM 21.
5310        let rust_qis =
5311            qir_to_qis(&rust_bc, 0, "native", None).expect("Rust qir_to_qis should succeed");
5312        let py_qis = expect_py_ok(
5313            crate::qir_qis::qir_to_qis(rust_bc.into(), 0, "native", None),
5314            "PyO3 qir_to_qis should succeed for a valid fixture",
5315        );
5316        assert_eq!(
5317            py_qis.as_ref(),
5318            rust_qis.as_slice(),
5319            "PyO3 qir_to_qis should return the same bytes as the Rust API"
5320        );
5321    }
5322
5323    #[test]
5324    fn test_parse_bitcode_module_reports_llvm_parse_errors_for_malformed_bytes() {
5325        let ctx = Context::create();
5326        let err = parse_bitcode_module(&ctx, b"not llvm bitcode", "malformed")
5327            .expect_err("malformed bitcode should fail in LLVM parsing");
5328
5329        assert!(err.starts_with("Failed to parse bitcode:"));
5330    }
5331
5332    #[test]
5333    fn test_qir2_base_fixture_validate_and_compile() {
5334        let ll_text = std::fs::read_to_string("tests/data/qir2_base.ll")
5335            .expect("Failed to read qir2_base.ll");
5336        let input_bc = qir_ll_to_bc(&ll_text).expect("Failed to convert qir2_base.ll to bitcode");
5337
5338        validate_qir(&input_bc, None).expect("QIR 2.1 base fixture should validate");
5339        let output_bc =
5340            qir_to_qis(&input_bc, 0, "native", None).expect("QIR 2.1 base fixture should compile");
5341
5342        let ctx = Context::create();
5343        let module = parse_bitcode_module(&ctx, &output_bc, "qis_module")
5344            .expect("Compiled QIS bitcode should parse");
5345        assert!(module.get_function("qmain").is_some());
5346        assert!(module.get_function("qir_qis.load_qubit").is_some());
5347    }
5348
5349    #[test]
5350    fn test_qir2_adaptive_fixture_validate_and_compile() {
5351        let ll_text = std::fs::read_to_string("tests/data/qir2_adaptive.ll")
5352            .expect("Failed to read qir2_adaptive.ll");
5353        let input_bc =
5354            qir_ll_to_bc(&ll_text).expect("Failed to convert qir2_adaptive.ll to bitcode");
5355
5356        validate_qir(&input_bc, None).expect("QIR 2.1 adaptive fixture should validate");
5357        let output_bc = qir_to_qis(&input_bc, 0, "native", None)
5358            .expect("QIR 2.1 adaptive fixture should compile");
5359
5360        let ctx = Context::create();
5361        let module = parse_bitcode_module(&ctx, &output_bc, "qis_module")
5362            .expect("Compiled QIS bitcode should parse");
5363        assert!(module.get_function("qmain").is_some());
5364        assert!(module.get_function("___lazy_measure").is_some());
5365    }
5366
5367    #[test]
5368    fn test_qir_to_qis_output_parses_with_raw_llvm_buffer() {
5369        let ll_text =
5370            std::fs::read_to_string("tests/data/base.ll").expect("Failed to read base.ll");
5371        let input_bc = qir_ll_to_bc(&ll_text).expect("Failed to convert base.ll to bitcode");
5372        let output_bc =
5373            qir_to_qis(&input_bc, 0, "native", None).expect("base fixture should compile");
5374
5375        assert_public_bitcode_round_trips_from_file(&output_bc, "selene_qis_output");
5376    }
5377
5378    #[test]
5379    fn test_validate_module_flags_are_checked_cross_platform() {
5380        let ll_text = r#"
5381define i64 @Entry_Point_Name() #0 {
5382entry:
5383  ret i64 0
5384}
5385
5386attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" }
5387
5388!llvm.module.flags = !{!0, !1, !2, !3}
5389!0 = !{i32 1, !"qir_major_version", i32 2}
5390!1 = !{i32 7, !"qir_minor_version", i32 0}
5391!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
5392!3 = !{i32 1, !"dynamic_result_management", i1 false}
5393"#;
5394        let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode");
5395        validate_qir(&bc_bytes, None).expect("Module flags should validate on every platform");
5396    }
5397
5398    #[test]
5399    fn test_module_flag_parser_reads_existing_flags() {
5400        use crate::aux::collect_module_flags;
5401        use inkwell::context::Context;
5402
5403        let ll_text = r#"
5404define i64 @Entry_Point_Name() #0 {
5405entry:
5406  ret i64 0
5407}
5408
5409attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" }
5410
5411!llvm.module.flags = !{!0, !1, !2, !3}
5412!0 = !{i32 1, !"qir_major_version", i32 2}
5413!1 = !{i32 7, !"qir_minor_version", i32 0}
5414!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
5415!3 = !{i32 1, !"dynamic_result_management", i1 false}
5416"#;
5417
5418        let ctx = Context::create();
5419        let module = create_module_from_ir_text(&ctx, ll_text, "qir")
5420            .expect("Failed to create module from inline IR");
5421
5422        let flags = collect_module_flags(&module);
5423        assert_eq!(
5424            flags.get("qir_major_version").map(<[String]>::to_vec),
5425            Some(vec!["i32 2".to_string()])
5426        );
5427        assert_eq!(
5428            flags.get("qir_minor_version").map(<[String]>::to_vec),
5429            Some(vec!["i32 0".to_string()])
5430        );
5431        assert_eq!(
5432            flags
5433                .get("dynamic_qubit_management")
5434                .map(<[String]>::to_vec),
5435            Some(vec!["i1 false".to_string()])
5436        );
5437        assert_eq!(
5438            flags
5439                .get("dynamic_result_management")
5440                .map(<[String]>::to_vec),
5441            Some(vec!["i1 false".to_string()])
5442        );
5443    }
5444
5445    #[test]
5446    fn test_validate_module_flags_accept_duplicate_entries_if_one_matches() {
5447        let ll_text = r#"
5448define i64 @Entry_Point_Name() #0 {
5449entry:
5450  ret i64 0
5451}
5452
5453attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" }
5454
5455!llvm.module.flags = !{!0, !1, !2, !3, !4}
5456!0 = !{i32 1, !"qir_major_version", i32 99}
5457!1 = !{i32 1, !"qir_major_version", i32 2}
5458!2 = !{i32 7, !"qir_minor_version", i32 0}
5459!3 = !{i32 1, !"dynamic_qubit_management", i1 false}
5460!4 = !{i32 1, !"dynamic_result_management", i1 false}
5461"#;
5462
5463        let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode");
5464        validate_qir(&bc_bytes, None)
5465            .expect("Module flags should validate when any duplicate entry matches");
5466    }
5467
5468    #[test]
5469    fn test_validate_module_flags_reports_malformed_required_flag() {
5470        let ll_text = r#"
5471define i64 @Entry_Point_Name() #0 {
5472entry:
5473  ret i64 0
5474}
5475
5476attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" }
5477
5478!llvm.module.flags = !{!0, !1, !2, !3}
5479!0 = !{i32 1, !"qir_major_version", !4}
5480!1 = !{i32 7, !"qir_minor_version", i32 0}
5481!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
5482!3 = !{i32 1, !"dynamic_result_management", i1 false}
5483!4 = !{i32 99}
5484"#;
5485
5486        let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode");
5487        let err = validate_qir(&bc_bytes, None).expect_err("Malformed module flag should fail");
5488        assert!(err.contains("Missing or unsupported module flag: qir_major_version"));
5489    }
5490
5491    #[test]
5492    fn test_validate_module_flags_with_non_metadata_name_operand_does_not_panic() {
5493        let ll_text = r#"
5494define i64 @Entry_Point_Name() #0 {
5495entry:
5496  ret i64 0
5497}
5498
5499attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" }
5500
5501!llvm.module.flags = !{!0, !1, !2, !3}
5502!0 = !{i32 1, i32 123, i32 2}
5503!1 = !{i32 7, !"qir_minor_version", i32 0}
5504!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
5505!3 = !{i32 1, !"dynamic_result_management", i1 false}
5506"#;
5507
5508        let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode");
5509        let err = validate_qir(&bc_bytes, None)
5510            .expect_err("Non-metadata module flag name operand should fail validation");
5511        assert!(err.contains("Missing required module flag: qir_major_version"));
5512    }
5513
5514    #[test]
5515    fn test_validate_module_flags_rejects_malformed_name_even_when_required_flags_exist() {
5516        let ll_text = r#"
5517define i64 @Entry_Point_Name() #0 {
5518entry:
5519  ret i64 0
5520}
5521
5522attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" }
5523
5524!llvm.module.flags = !{!0, !1, !2, !3, !4}
5525!0 = !{i32 1, i32 123, i32 2}
5526!1 = !{i32 1, !"qir_major_version", i32 1}
5527!2 = !{i32 7, !"qir_minor_version", i32 0}
5528!3 = !{i32 1, !"dynamic_qubit_management", i1 false}
5529!4 = !{i32 1, !"dynamic_result_management", i1 false}
5530"#;
5531
5532        let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode");
5533        let err = validate_qir(&bc_bytes, None).expect_err(
5534            "Malformed module flag names should fail even with complete required flags",
5535        );
5536        assert!(err.contains("Malformed llvm.module.flags entry: expected metadata string name"));
5537    }
5538
5539    #[test]
5540    fn test_validate_qir_reports_exact_single_expected_module_flag_value() {
5541        let ll_text = r#"
5542define i64 @Entry_Point_Name() #0 {
5543entry:
5544  ret i64 0
5545}
5546
5547attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" }
5548
5549!llvm.module.flags = !{!0, !1, !2, !3}
5550!0 = !{i32 1, !"qir_major_version", i32 1}
5551!1 = !{i32 7, !"qir_minor_version", i32 99}
5552!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
5553!3 = !{i32 1, !"dynamic_result_management", i1 false}
5554"#;
5555
5556        let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode");
5557        let err = validate_qir(&bc_bytes, None)
5558            .expect_err("unsupported single-valued module flag should fail");
5559        assert!(err.contains("Unsupported qir_minor_version"));
5560    }
5561
5562    #[test]
5563    fn test_validate_qir_rejects_qir_1_1_version_pair() {
5564        let ll_text = r#"
5565define i64 @Entry_Point_Name() #0 {
5566entry:
5567  ret i64 0
5568}
5569
5570attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" }
5571
5572!llvm.module.flags = !{!0, !1, !2, !3}
5573!0 = !{i32 1, !"qir_major_version", i32 1}
5574!1 = !{i32 7, !"qir_minor_version", i32 1}
5575!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
5576!3 = !{i32 1, !"dynamic_result_management", i1 false}
5577"#;
5578
5579        let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode");
5580        let err = validate_qir(&bc_bytes, None).expect_err("QIR 1.1 should not validate");
5581        assert!(err.contains("Unsupported qir_minor_version"));
5582    }
5583
5584    #[test]
5585    fn test_validate_qir_accepts_qir_2_1_version_pair() {
5586        let ll_text = r#"
5587define i64 @Entry_Point_Name() #0 {
5588entry:
5589  ret i64 0
5590}
5591
5592attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" }
5593
5594!llvm.module.flags = !{!0, !1, !2, !3}
5595!0 = !{i32 1, !"qir_major_version", i32 2}
5596!1 = !{i32 7, !"qir_minor_version", i32 1}
5597!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
5598!3 = !{i32 1, !"dynamic_result_management", i1 false}
5599"#;
5600
5601        let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode");
5602        validate_qir(&bc_bytes, None).expect("QIR 2.1 should validate");
5603    }
5604
5605    #[test]
5606    fn test_validate_qir_reports_unsupported_optional_arrays_module_flag_value() {
5607        let ll_text = r#"
5608define i64 @Entry_Point_Name() #0 {
5609entry:
5610  ret i64 0
5611}
5612
5613attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" }
5614
5615!llvm.module.flags = !{!0, !1, !2, !3, !4}
5616!0 = !{i32 1, !"qir_major_version", i32 2}
5617!1 = !{i32 7, !"qir_minor_version", i32 0}
5618!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
5619!3 = !{i32 1, !"dynamic_result_management", i1 false}
5620!4 = !{i32 1, !"arrays", i32 7}
5621"#;
5622
5623        let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode");
5624        let err = validate_qir(&bc_bytes, None)
5625            .expect_err("unsupported optional arrays flag should fail validation");
5626        assert!(err.contains("Unsupported arrays: expected one of i1 false, i1 true"));
5627    }
5628
5629    #[test]
5630    fn test_validate_qir_reports_exact_expected_dynamic_module_flag_value() {
5631        let ll_text = r#"
5632define i64 @Entry_Point_Name() #0 {
5633entry:
5634  ret i64 0
5635}
5636
5637attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" }
5638
5639!llvm.module.flags = !{!0, !1, !2, !3}
5640!0 = !{i32 1, !"qir_major_version", i32 1}
5641!1 = !{i32 7, !"qir_minor_version", i32 0}
5642!2 = !{i32 1, !"dynamic_qubit_management", i32 7}
5643!3 = !{i32 1, !"dynamic_result_management", i1 false}
5644"#;
5645
5646        let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode");
5647        let err = validate_qir(&bc_bytes, None).expect_err("unsupported dynamic flag should fail");
5648        assert!(
5649            err.contains("Unsupported dynamic_qubit_management: expected one of i1 false, i1 true")
5650        );
5651    }
5652
5653    #[test]
5654    fn test_validate_qir_reports_malformed_optional_arrays_module_flag() {
5655        let ll_text = r#"
5656define i64 @Entry_Point_Name() #0 {
5657entry:
5658  ret i64 0
5659}
5660
5661attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" }
5662
5663!llvm.module.flags = !{!0, !1, !2, !3, !4}
5664!0 = !{i32 1, !"qir_major_version", i32 2}
5665!1 = !{i32 7, !"qir_minor_version", i32 0}
5666!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
5667!3 = !{i32 1, !"dynamic_result_management", i1 false}
5668!4 = !{i32 1, !"arrays", !5}
5669!5 = !{i32 99}
5670"#;
5671
5672        let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode");
5673        let err =
5674            validate_qir(&bc_bytes, None).expect_err("malformed optional arrays flag should fail");
5675        assert!(err.contains("Missing or unsupported module flag: arrays"));
5676    }
5677
5678    #[test]
5679    fn test_validate_qir_missing_required_module_flag_reports_exact_message() {
5680        let ll_text = r#"
5681define i64 @Entry_Point_Name() #0 {
5682entry:
5683  ret i64 0
5684}
5685
5686attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" }
5687
5688!llvm.module.flags = !{!0, !1, !2}
5689!0 = !{i32 7, !"qir_minor_version", i32 0}
5690!1 = !{i32 1, !"dynamic_qubit_management", i1 false}
5691!2 = !{i32 1, !"dynamic_result_management", i1 false}
5692"#;
5693
5694        let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode");
5695        let err = validate_qir(&bc_bytes, None).expect_err("Missing flag should fail");
5696        assert!(err.contains("Missing required module flag: qir_major_version"));
5697    }
5698
5699    #[test]
5700    fn test_validate_qir_consolidates_missing_required_module_flags() {
5701        let ll_text = r#"
5702define i64 @Entry_Point_Name() #0 {
5703entry:
5704  ret i64 0
5705}
5706
5707attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" }
5708"#;
5709
5710        let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode");
5711        let err = validate_qir(&bc_bytes, None).expect_err("Missing flags should fail");
5712        assert_eq!(
5713            err,
5714            "Missing required module flags: qir_major_version, qir_minor_version, dynamic_qubit_management, dynamic_result_management"
5715        );
5716    }
5717
5718    #[test]
5719    fn test_validate_qir_consolidates_missing_or_unsupported_module_flags() {
5720        let ll_text = r#"
5721define i64 @Entry_Point_Name() #0 {
5722entry:
5723  ret i64 0
5724}
5725
5726attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" }
5727
5728!llvm.module.flags = !{!0, !1, !2, !3, !4}
5729!0 = !{i32 1, !"qir_major_version", !5}
5730!1 = !{i32 7, !"qir_minor_version", i32 0}
5731!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
5732!3 = !{i32 1, !"dynamic_result_management", i1 false}
5733!4 = !{i32 1, !"arrays", !5}
5734!5 = !{i32 99}
5735"#;
5736
5737        let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode");
5738        let err = validate_qir(&bc_bytes, None)
5739            .expect_err("Malformed module flags should report unsupported flags");
5740        assert_eq!(
5741            err,
5742            "Missing or unsupported module flags: qir_major_version, arrays"
5743        );
5744    }
5745
5746    #[test]
5747    fn test_validate_qir_preserves_error_ordering_across_grouped_module_flags() {
5748        let ll_text = r#"
5749define i64 @Entry_Point_Name() #0 {
5750entry:
5751  ret i64 0
5752}
5753
5754attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" }
5755
5756!llvm.module.flags = !{!0, !1, !2, !3, !4}
5757!0 = !{i32 1, !"qir_major_version", !5}
5758!1 = !{i32 7, !"qir_minor_version", i32 0}
5759!2 = !{i32 1, !"dynamic_qubit_management", i32 7}
5760!3 = !{i32 1, !"dynamic_result_management", i1 false}
5761!4 = !{i32 1, !"arrays", !5}
5762!5 = !{i32 99}
5763"#;
5764
5765        let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode");
5766        let err =
5767            validate_qir(&bc_bytes, None).expect_err("Malformed and unsupported flags should fail");
5768        assert_eq!(
5769            err,
5770            "Missing or unsupported module flag: qir_major_version; Unsupported dynamic_qubit_management: expected one of i1 false, i1 true; Missing or unsupported module flag: arrays"
5771        );
5772    }
5773
5774    #[test]
5775    fn test_validate_qir_preserves_function_before_invalid_required_num_results_error_ordering() {
5776        let ll_text = minimal_qir_with_body(
5777            "1",
5778            "abc",
5779            "1",
5780            "declare void @__quantum__qis__bogus__body(%Qubit*)",
5781            r"  %q0 = inttoptr i64 0 to %Qubit*
5782  call void @__quantum__qis__bogus__body(%Qubit* %q0)",
5783        );
5784
5785        let bc_bytes = qir_ll_to_bc(&ll_text).expect("Failed to convert inline QIR to bitcode");
5786        let err = validate_qir(&bc_bytes, None)
5787            .expect_err("invalid required_num_results and unsupported QIS call should fail");
5788
5789        let unsupported_fn_idx = err
5790            .find("Unsupported QIR QIS function: __quantum__qis__bogus__body")
5791            .expect("unsupported function error should be present");
5792        let invalid_required_num_results_idx = err
5793            .find("Invalid required_num_results attribute value: abc")
5794            .expect("invalid required_num_results error should be present");
5795
5796        assert!(
5797            unsupported_fn_idx < invalid_required_num_results_idx,
5798            "unsupported function error should be reported before required_num_results parse error: {err}"
5799        );
5800    }
5801
5802    #[test]
5803    fn test_validate_qir_preserves_module_flag_before_capability_error_ordering() {
5804        let ll_text = r#"
5805%Result = type opaque
5806
5807declare %Result* @__quantum__rt__result_allocate(ptr)
5808
5809define i64 @Entry_Point_Name() #0 {
5810entry:
5811  %tmp = alloca i64
5812  %res = call %Result* @__quantum__rt__result_allocate(ptr %tmp)
5813  ret i64 0
5814}
5815
5816attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" }
5817
5818!llvm.module.flags = !{!0, !1, !2, !3}
5819!0 = !{i32 1, !"qir_major_version", !4}
5820!1 = !{i32 7, !"qir_minor_version", i32 0}
5821!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
5822!3 = !{i32 1, !"dynamic_result_management", i1 false}
5823!4 = !{i32 99}
5824"#;
5825
5826        let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode");
5827        let err = validate_qir(&bc_bytes, None)
5828            .expect_err("malformed module flags and capability misuse should fail");
5829
5830        let module_flag_idx = err
5831            .find("Missing or unsupported module flag: qir_major_version")
5832            .expect("module flag error should be present");
5833        let capability_idx = err
5834            .find("__quantum__rt__result_allocate requires `dynamic_result_management=true`")
5835            .expect("capability error should be present");
5836
5837        assert!(
5838            module_flag_idx < capability_idx,
5839            "module flag errors should be reported before capability errors: {err}"
5840        );
5841    }
5842
5843    #[test]
5844    fn test_qir_to_qis_bool_output_uses_bool_tag_and_print_bool() {
5845        let ll_text = r#"
5846%Result = type opaque
5847
5848@bool_out = private constant [2 x i8] c"b\00"
5849
5850declare void @__quantum__rt__bool_record_output(i1, ptr)
5851
5852define i64 @Entry_Point_Name() #0 {
5853entry:
5854  call void @__quantum__rt__bool_record_output(i1 true, ptr getelementptr inbounds ([2 x i8], ptr @bool_out, i64 0, i64 0))
5855  ret i64 0
5856}
5857
5858attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" }
5859
5860!llvm.module.flags = !{!0, !1, !2, !3}
5861!0 = !{i32 1, !"qir_major_version", i32 1}
5862!1 = !{i32 7, !"qir_minor_version", i32 0}
5863!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
5864!3 = !{i32 1, !"dynamic_result_management", i1 false}
5865"#;
5866
5867        let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode");
5868        let output_bc =
5869            qir_to_qis(&bc_bytes, 0, "native", None).expect("bool output should compile");
5870
5871        let ctx = Context::create();
5872        let module = parse_bitcode_module(&ctx, &output_bc, "qis_module")
5873            .expect("Compiled QIS bitcode should parse");
5874        assert!(module.get_function("print_bool").is_some());
5875
5876        #[cfg(not(windows))]
5877        {
5878            let text = module.to_string();
5879            assert!(text.contains("USER:BOOL:b"));
5880        }
5881
5882        #[cfg(windows)]
5883        {
5884            let labels = module
5885                .get_globals()
5886                .filter_map(|global| crate::convert::get_string_label(global).ok())
5887                .collect::<Vec<_>>();
5888            assert!(labels.iter().any(|label| label.contains("USER:BOOL:b")));
5889        }
5890    }
5891
5892    #[test]
5893    fn test_validate_qir_rejects_malformed_barrier_suffix() {
5894        let ll_text = r#"
5895%Qubit = type opaque
5896
5897declare void @__quantum__qis__barrier2__adj(%Qubit*, %Qubit*)
5898
5899define i64 @Entry_Point_Name() #0 {
5900entry:
5901  %q0 = inttoptr i64 0 to %Qubit*
5902  %q1 = inttoptr i64 1 to %Qubit*
5903  call void @__quantum__qis__barrier2__adj(%Qubit* %q0, %Qubit* %q1)
5904  ret i64 0
5905}
5906
5907attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="2" "required_num_results"="1" }
5908
5909!llvm.module.flags = !{!0, !1, !2, !3}
5910!0 = !{i32 1, !"qir_major_version", i32 1}
5911!1 = !{i32 7, !"qir_minor_version", i32 0}
5912!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
5913!3 = !{i32 1, !"dynamic_result_management", i1 false}
5914"#;
5915
5916        let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode");
5917        let err = validate_qir(&bc_bytes, None).expect_err("malformed barrier suffix should fail");
5918        assert!(err.contains("Unsupported QIR QIS function: __quantum__qis__barrier2__adj"));
5919    }
5920
5921    #[test]
5922    fn test_validate_qir_accepts_barrier_matching_required_qubits() {
5923        let ll_text = r#"
5924%Qubit = type opaque
5925
5926declare void @__quantum__qis__barrier2__body(%Qubit*, %Qubit*)
5927
5928define i64 @Entry_Point_Name() #0 {
5929entry:
5930  %q0 = inttoptr i64 0 to %Qubit*
5931  %q1 = inttoptr i64 1 to %Qubit*
5932  call void @__quantum__qis__barrier2__body(%Qubit* %q0, %Qubit* %q1)
5933  ret i64 0
5934}
5935
5936attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="2" "required_num_results"="1" }
5937
5938!llvm.module.flags = !{!0, !1, !2, !3}
5939!0 = !{i32 1, !"qir_major_version", i32 1}
5940!1 = !{i32 7, !"qir_minor_version", i32 0}
5941!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
5942!3 = !{i32 1, !"dynamic_result_management", i1 false}
5943"#;
5944
5945        let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode");
5946        validate_qir(&bc_bytes, None)
5947            .expect("barrier arity matching required_num_qubits should validate");
5948    }
5949
5950    #[test]
5951    fn test_validate_qir_rejects_zero_arity_barrier() {
5952        let ll_text = r#"
5953%Qubit = type opaque
5954
5955declare void @__quantum__qis__barrier0__body()
5956
5957define i64 @Entry_Point_Name() #0 {
5958entry:
5959  call void @__quantum__qis__barrier0__body()
5960  ret i64 0
5961}
5962
5963attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" }
5964
5965!llvm.module.flags = !{!0, !1, !2, !3}
5966!0 = !{i32 1, !"qir_major_version", i32 1}
5967!1 = !{i32 7, !"qir_minor_version", i32 0}
5968!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
5969!3 = !{i32 1, !"dynamic_result_management", i1 false}
5970"#;
5971
5972        let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode");
5973        let err = validate_qir(&bc_bytes, None).expect_err("barrier0 should be rejected");
5974        assert!(err.contains("Unsupported QIR QIS function: __quantum__qis__barrier0__body"));
5975    }
5976
5977    #[test]
5978    fn test_validate_qir_rejects_unsupported_qtm_function() {
5979        let ll_text = r#"
5980declare void @___unknown_qtm()
5981
5982define i64 @Entry_Point_Name() #0 {
5983entry:
5984  call void @___unknown_qtm()
5985  ret i64 0
5986}
5987
5988attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" }
5989
5990!llvm.module.flags = !{!0, !1, !2, !3}
5991!0 = !{i32 1, !"qir_major_version", i32 1}
5992!1 = !{i32 7, !"qir_minor_version", i32 0}
5993!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
5994!3 = !{i32 1, !"dynamic_result_management", i1 false}
5995"#;
5996
5997        let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode");
5998        let err = validate_qir(&bc_bytes, None)
5999            .expect_err("unsupported QTM declarations should fail validation");
6000        assert!(err.contains("Unsupported Qtm QIS function: ___unknown_qtm"));
6001    }
6002
6003    #[test]
6004    fn test_validate_qir_rejects_raw_barrier_runtime_function() {
6005        let ll_text = r#"
6006declare void @___barrier(ptr, i64)
6007
6008define i64 @Entry_Point_Name() #0 {
6009entry:
6010  call void @___barrier(ptr null, i64 1)
6011  ret i64 0
6012}
6013
6014attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" }
6015
6016!llvm.module.flags = !{!0, !1, !2, !3}
6017!0 = !{i32 1, !"qir_major_version", i32 1}
6018!1 = !{i32 7, !"qir_minor_version", i32 0}
6019!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
6020!3 = !{i32 1, !"dynamic_result_management", i1 false}
6021"#;
6022
6023        let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode");
6024        let err = validate_qir(&bc_bytes, None)
6025            .expect_err("raw ___barrier declarations should fail validation");
6026        assert!(err.contains("Unsupported Qtm QIS function: ___barrier"));
6027    }
6028
6029    #[test]
6030    fn test_qir_to_qis_rejects_raw_barrier_runtime_function() {
6031        let ll_text = r#"
6032declare void @___barrier(ptr, i64)
6033
6034define i64 @Entry_Point_Name() #0 {
6035entry:
6036  call void @___barrier(ptr null, i64 1)
6037  ret i64 0
6038}
6039
6040attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" }
6041
6042!llvm.module.flags = !{!0, !1, !2, !3}
6043!0 = !{i32 1, !"qir_major_version", i32 1}
6044!1 = !{i32 7, !"qir_minor_version", i32 0}
6045!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
6046!3 = !{i32 1, !"dynamic_result_management", i1 false}
6047"#;
6048
6049        let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode");
6050        let err = qir_to_qis(&bc_bytes, 0, "native", None)
6051            .expect_err("raw ___barrier declarations should fail translation");
6052        assert!(err.contains("Unsupported Qtm QIS function: ___barrier"));
6053    }
6054
6055    #[test]
6056    fn test_validate_qir_allows_ir_defined_non_main_helper() {
6057        let ll_text = r#"
6058%Qubit = type opaque
6059
6060define void @helper(%Qubit* %qubit) {
6061entry:
6062  call void @__quantum__qis__h__body(%Qubit* %qubit)
6063  ret void
6064}
6065
6066define i64 @Entry_Point_Name() #0 {
6067entry:
6068  %q0 = inttoptr i64 0 to %Qubit*
6069  call void @helper(%Qubit* %q0)
6070  ret i64 0
6071}
6072
6073declare void @__quantum__qis__h__body(%Qubit*)
6074
6075attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" }
6076
6077!llvm.module.flags = !{!0, !1, !2, !3}
6078!0 = !{i32 1, !"qir_major_version", i32 1}
6079!1 = !{i32 7, !"qir_minor_version", i32 0}
6080!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
6081!3 = !{i32 1, !"dynamic_result_management", i1 false}
6082"#;
6083
6084        let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode");
6085        validate_qir(&bc_bytes, None)
6086            .expect("IR-defined helper functions with non-main names should be allowed");
6087    }
6088
6089    #[test]
6090    fn test_validate_qir_allows_external_pointer_returning_declarations() {
6091        let ll_text = r#"
6092%Qubit = type opaque
6093
6094declare ptr @external_helper()
6095
6096define i64 @Entry_Point_Name() #0 {
6097entry:
6098  ret i64 0
6099}
6100
6101attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" }
6102
6103!llvm.module.flags = !{!0, !1, !2, !3}
6104!0 = !{i32 1, !"qir_major_version", i32 1}
6105!1 = !{i32 7, !"qir_minor_version", i32 0}
6106!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
6107!3 = !{i32 1, !"dynamic_result_management", i1 false}
6108"#;
6109
6110        let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode");
6111        validate_qir(&bc_bytes, None)
6112            .expect("external declarations without bodies should not be treated as IR-defined");
6113    }
6114
6115    #[test]
6116    fn test_validate_qir_rejects_ir_defined_pointer_returning_function() {
6117        let ll_text = r#"
6118define ptr @helper() {
6119entry:
6120  ret ptr null
6121}
6122
6123define i64 @Entry_Point_Name() #0 {
6124entry:
6125  ret i64 0
6126}
6127
6128attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" }
6129
6130!llvm.module.flags = !{!0, !1, !2, !3}
6131!0 = !{i32 1, !"qir_major_version", i32 1}
6132!1 = !{i32 7, !"qir_minor_version", i32 0}
6133!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
6134!3 = !{i32 1, !"dynamic_result_management", i1 false}
6135"#;
6136
6137        let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode");
6138        let err = validate_qir(&bc_bytes, None)
6139            .expect_err("IR-defined pointer-returning helper should fail validation");
6140        assert!(err.contains("Function `helper` cannot return a pointer type"));
6141    }
6142
6143    #[test]
6144    fn test_qir_to_qis_rejects_unknown_declared_qis_function() {
6145        let ll_text = r#"
6146%Qubit = type opaque
6147
6148declare void @__quantum__qis__mystery__body(%Qubit*)
6149
6150define i64 @Entry_Point_Name() #0 {
6151entry:
6152  %q0 = inttoptr i64 0 to %Qubit*
6153  call void @__quantum__qis__mystery__body(%Qubit* %q0)
6154  ret i64 0
6155}
6156
6157attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" }
6158
6159!llvm.module.flags = !{!0, !1, !2, !3}
6160!0 = !{i32 1, !"qir_major_version", i32 1}
6161!1 = !{i32 7, !"qir_minor_version", i32 0}
6162!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
6163!3 = !{i32 1, !"dynamic_result_management", i1 false}
6164"#;
6165
6166        let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode");
6167        let err = qir_to_qis(&bc_bytes, 0, "native", None)
6168            .expect_err("unknown declared QIS function should fail");
6169        assert!(err.contains("Unsupported QIR QIS function: __quantum__qis__mystery__body"));
6170    }
6171
6172    #[test]
6173    fn test_qir_to_qis_u1q_synonym_lowers_to_rxy() {
6174        let ll_text = r#"
6175%Qubit = type opaque
6176
6177declare void @__quantum__qis__u1q__body(double, double, %Qubit*)
6178
6179define i64 @Entry_Point_Name() #0 {
6180entry:
6181  %q0 = inttoptr i64 0 to %Qubit*
6182  call void @__quantum__qis__u1q__body(double 1.0, double 0.5, %Qubit* %q0)
6183  ret i64 0
6184}
6185
6186attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" }
6187
6188!llvm.module.flags = !{!0, !1, !2, !3}
6189!0 = !{i32 1, !"qir_major_version", i32 1}
6190!1 = !{i32 7, !"qir_minor_version", i32 0}
6191!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
6192!3 = !{i32 1, !"dynamic_result_management", i1 false}
6193"#;
6194
6195        let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode");
6196        let output_bc =
6197            qir_to_qis(&bc_bytes, 0, "native", None).expect("u1q synonym should compile");
6198
6199        let ctx = Context::create();
6200        let module = parse_bitcode_module(&ctx, &output_bc, "qis_module")
6201            .expect("Compiled QIS bitcode should parse");
6202        assert!(module.get_function("___rxy").is_some());
6203
6204        #[cfg(not(windows))]
6205        {
6206            let text = module.to_string();
6207            assert!(text.contains("___rxy"));
6208        }
6209    }
6210
6211    #[test]
6212    fn test_checked_result_index_rejects_out_of_bounds_values() {
6213        let err = crate::aux::checked_result_index(5, 1)
6214            .expect_err("out-of-bounds result indices should fail cleanly");
6215        assert_eq!(err, "Result index 5 exceeds required_num_results (1)");
6216    }
6217
6218    #[test]
6219    fn test_checked_qubit_index_accepts_zero_based_static_ids() {
6220        assert_eq!(
6221            crate::convert::checked_qubit_index(0, 2)
6222                .expect("zero-based first qubit id should map to slot 0"),
6223            0
6224        );
6225        assert_eq!(
6226            crate::convert::checked_qubit_index(1, 2)
6227                .expect("zero-based second qubit id should map to slot 1"),
6228            1
6229        );
6230    }
6231
6232    #[test]
6233    fn test_checked_qubit_index_rejects_out_of_bounds_values() {
6234        let err = crate::convert::checked_qubit_index(2, 2)
6235            .expect_err("out-of-bounds zero-based qubit ids should fail cleanly");
6236        assert_eq!(err, "Qubit index 2 exceeds required_num_qubits (2)");
6237    }
6238
6239    #[test]
6240    fn test_qir_to_qis_rejects_nonzero_single_qubit_rz_handle() {
6241        let ll_text = r#"
6242%Qubit = type opaque
6243
6244declare void @__quantum__qis__rz__body(double, %Qubit*)
6245
6246define i64 @Entry_Point_Name() #0 {
6247entry:
6248  %q0 = inttoptr i64 1 to %Qubit*
6249  call void @__quantum__qis__rz__body(double 5.000000e-1, %Qubit* %q0)
6250  ret i64 0
6251}
6252
6253attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" }
6254
6255!llvm.module.flags = !{!0, !1, !2, !3}
6256!0 = !{i32 1, !"qir_major_version", i32 1}
6257!1 = !{i32 7, !"qir_minor_version", i32 0}
6258!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
6259!3 = !{i32 1, !"dynamic_result_management", i1 false}
6260"#;
6261
6262        let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode");
6263        let err = qir_to_qis(&bc_bytes, 0, "native", None).expect_err(
6264            "static qubit id 1 should be rejected in a single-qubit zero-based program",
6265        );
6266        assert!(err.contains("Invalid static qubit handle passed to `__quantum__qis__rz__body`"));
6267        assert!(err.contains("Qubit index 1 exceeds required_num_qubits (1)"));
6268    }
6269
6270    #[test]
6271    fn test_validate_qir_rejects_out_of_range_handles_for_direct_qubit_gate_shapes() {
6272        let ll_text = r#"
6273%Qubit = type opaque
6274
6275declare void @__quantum__qis__rxy__body(double, double, %Qubit*)
6276declare void @__quantum__qis__u1q__body(double, double, %Qubit*)
6277declare void @__quantum__qis__rzz__body(double, %Qubit*, %Qubit*)
6278declare void @__quantum__qis__cx__body(%Qubit*, %Qubit*)
6279declare void @__quantum__qis__ccx__body(%Qubit*, %Qubit*, %Qubit*)
6280
6281define i64 @Entry_Point_Name() #0 {
6282entry:
6283  %q0 = inttoptr i64 0 to %Qubit*
6284  %q1 = inttoptr i64 1 to %Qubit*
6285  %q2 = inttoptr i64 2 to %Qubit*
6286  call void @__quantum__qis__rxy__body(double 5.000000e-1, double 2.500000e-1, %Qubit* %q1)
6287  call void @__quantum__qis__u1q__body(double 5.000000e-1, double 2.500000e-1, %Qubit* %q1)
6288  call void @__quantum__qis__rzz__body(double 5.000000e-1, %Qubit* %q0, %Qubit* %q1)
6289  call void @__quantum__qis__cx__body(%Qubit* %q0, %Qubit* %q1)
6290  call void @__quantum__qis__ccx__body(%Qubit* %q0, %Qubit* %q1, %Qubit* %q2)
6291  ret i64 0
6292}
6293
6294attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" }
6295
6296!llvm.module.flags = !{!0, !1, !2, !3}
6297!0 = !{i32 1, !"qir_major_version", i32 1}
6298!1 = !{i32 7, !"qir_minor_version", i32 0}
6299!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
6300!3 = !{i32 1, !"dynamic_result_management", i1 false}
6301"#;
6302
6303        let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode");
6304        let err = validate_qir(&bc_bytes, None)
6305            .expect_err("direct gate qubit positions should reject out-of-range static handles");
6306        for callee in [
6307            "__quantum__qis__rxy__body",
6308            "__quantum__qis__u1q__body",
6309            "__quantum__qis__rzz__body",
6310            "__quantum__qis__cx__body",
6311            "__quantum__qis__ccx__body",
6312        ] {
6313            assert!(
6314                err.contains(&format!("Invalid static qubit handle passed to `{callee}`")),
6315                "missing static handle validation for {callee}: {err}"
6316            );
6317        }
6318        assert!(err.contains("Qubit index 1 exceeds required_num_qubits (1)"));
6319        assert!(err.contains("Qubit index 2 exceeds required_num_qubits (1)"));
6320    }
6321
6322    #[test]
6323    fn test_validate_qir_rejects_nonzero_single_qubit_decomposed_handle() {
6324        let ll_text = r#"
6325%Qubit = type opaque
6326
6327declare void @__quantum__qis__h__body(%Qubit*)
6328
6329define i64 @Entry_Point_Name() #0 {
6330entry:
6331  %q0 = inttoptr i64 1 to %Qubit*
6332  call void @__quantum__qis__h__body(%Qubit* %q0)
6333  ret i64 0
6334}
6335
6336attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" }
6337
6338!llvm.module.flags = !{!0, !1, !2, !3}
6339!0 = !{i32 1, !"qir_major_version", i32 1}
6340!1 = !{i32 7, !"qir_minor_version", i32 0}
6341!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
6342!3 = !{i32 1, !"dynamic_result_management", i1 false}
6343"#;
6344
6345        let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode");
6346        let err = validate_qir(&bc_bytes, None)
6347            .expect_err("decomposed QIS gates should reject out-of-range static qubit handles");
6348        assert!(err.contains("Invalid static qubit handle passed to `__quantum__qis__h__body`"));
6349        assert!(err.contains("Qubit index 1 exceeds required_num_qubits (1)"));
6350    }
6351
6352    #[test]
6353    fn test_validate_qir_rejects_out_of_range_barrier_static_handles() {
6354        let ll_text = r#"
6355%Qubit = type opaque
6356
6357declare void @__quantum__qis__barrier2__body(%Qubit*, %Qubit*)
6358
6359define i64 @Entry_Point_Name() #0 {
6360entry:
6361  %q0 = inttoptr i64 0 to %Qubit*
6362  %q1 = inttoptr i64 2 to %Qubit*
6363  call void @__quantum__qis__barrier2__body(%Qubit* %q0, %Qubit* %q1)
6364  ret i64 0
6365}
6366
6367attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="2" "required_num_results"="1" }
6368
6369!llvm.module.flags = !{!0, !1, !2, !3}
6370!0 = !{i32 1, !"qir_major_version", i32 1}
6371!1 = !{i32 7, !"qir_minor_version", i32 0}
6372!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
6373!3 = !{i32 1, !"dynamic_result_management", i1 false}
6374"#;
6375
6376        let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode");
6377        let err = validate_qir(&bc_bytes, None)
6378            .expect_err("barrier calls should reject out-of-range static qubit handles");
6379        assert!(
6380            err.contains("Invalid static qubit handle passed to `__quantum__qis__barrier2__body`")
6381        );
6382        assert!(err.contains("Qubit index 2 exceeds required_num_qubits (2)"));
6383    }
6384
6385    #[test]
6386    fn test_validate_qir_reports_static_handle_errors_with_preexisting_errors() {
6387        let ll_text = r#"
6388%Qubit = type opaque
6389
6390declare void @__quantum__qis__h__body(%Qubit*)
6391
6392define i64 @Entry_Point_Name() #0 {
6393entry:
6394  %q0 = inttoptr i64 1 to %Qubit*
6395  call void @__quantum__qis__h__body(%Qubit* %q0)
6396  ret i64 0
6397}
6398
6399attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" }
6400
6401!llvm.module.flags = !{!0, !1, !2, !3}
6402!0 = !{i32 1, !"qir_major_version", i32 1}
6403!1 = !{i32 7, !"qir_minor_version", i32 0}
6404!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
6405!3 = !{i32 1, !"dynamic_result_management", i1 false}
6406"#;
6407
6408        let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode");
6409        let err = validate_qir(&bc_bytes, None)
6410            .expect_err("validation should collect unrelated and static qubit errors");
6411        assert!(err.contains("Missing required attribute: `required_num_results`"));
6412        assert!(err.contains("Invalid static qubit handle passed to `__quantum__qis__h__body`"));
6413        assert!(err.contains("Qubit index 1 exceeds required_num_qubits (1)"));
6414    }
6415
6416    #[test]
6417    fn test_validate_qir_ignores_reserved_helper_name_prefixes_for_qubit_inference() {
6418        for helper_name in [
6419            "__quantum__qis__custom_helper",
6420            "__quantum__rt__custom_helper",
6421            "qir_qis.custom_helper",
6422            "___custom_helper",
6423            "print_custom_helper",
6424        ] {
6425            let ll_text = format!(
6426                r#"
6427%Qubit = type opaque
6428
6429define internal void @{helper_name}(%Qubit* %qubit) {{
6430entry:
6431  call void @__quantum__qis__h__body(%Qubit* %qubit)
6432  ret void
6433}}
6434
6435define i64 @Entry_Point_Name() #0 {{
6436entry:
6437  %q1 = inttoptr i64 1 to %Qubit*
6438  call void @{helper_name}(%Qubit* %q1)
6439  ret i64 0
6440}}
6441
6442declare void @__quantum__qis__h__body(%Qubit*)
6443
6444attributes #0 = {{ "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" }}
6445
6446!llvm.module.flags = !{{!0, !1, !2, !3}}
6447!0 = !{{i32 1, !"qir_major_version", i32 1}}
6448!1 = !{{i32 7, !"qir_minor_version", i32 0}}
6449!2 = !{{i32 1, !"dynamic_qubit_management", i1 false}}
6450!3 = !{{i32 1, !"dynamic_result_management", i1 false}}
6451"#
6452            );
6453
6454            let ctx = Context::create();
6455            let module = create_module_from_ir_text(&ctx, &ll_text, "qir")
6456                .expect("inline IR should parse for reserved helper-name coverage");
6457            let entry_fn = crate::convert::find_entry_function(&module)
6458                .expect("inline IR should include an entry function");
6459            let mut errors = Vec::new();
6460
6461            crate::aux::validate_static_qubit_helper_usage(&module, entry_fn, &mut errors);
6462
6463            assert!(
6464                errors.is_empty(),
6465                "reserved helper name `{helper_name}` should not trigger helper qubit inference: {errors:?}"
6466            );
6467        }
6468    }
6469
6470    #[test]
6471    fn test_validate_qir_does_not_treat_non_barrier_helpers_ending_in_body_as_barriers() {
6472        let ll_text = r#"
6473%Qubit = type opaque
6474
6475define internal void @helper__body(%Qubit* %qubit) {
6476entry:
6477  ret void
6478}
6479
6480define i64 @Entry_Point_Name() #0 {
6481entry:
6482  %q1 = inttoptr i64 1 to %Qubit*
6483  call void @helper__body(%Qubit* %q1)
6484  ret i64 0
6485}
6486
6487attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" }
6488
6489!llvm.module.flags = !{!0, !1, !2, !3}
6490!0 = !{i32 1, !"qir_major_version", i32 1}
6491!1 = !{i32 7, !"qir_minor_version", i32 0}
6492!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
6493!3 = !{i32 1, !"dynamic_result_management", i1 false}
6494"#;
6495
6496        let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode");
6497        let result = validate_qir(&bc_bytes, None);
6498        assert!(
6499            result.is_ok(),
6500            "non-barrier helpers ending in `__body` should not be treated as barriers: {result:?}"
6501        );
6502    }
6503
6504    #[test]
6505    fn test_qir_to_qis_loads_runtime_handles_for_static_native_calls() {
6506        let ll_path = Path::new("tests/data/base_native_only.ll");
6507        let qir_bytes = get_qir_bytes(ll_path);
6508        let (opt_level, target) = if cfg!(windows) {
6509            (0, "native")
6510        } else {
6511            (2, "aarch64")
6512        };
6513        let output_bc = qir_to_qis(&qir_bytes, opt_level, target, None)
6514            .expect("base_native_only should translate successfully");
6515
6516        let context = Context::create();
6517        let qis_text = crate::parse_bitcode_module(&context, &output_bc, "qis_module")
6518            .expect("translated QIS bitcode should parse")
6519            .to_string();
6520
6521        assert!(
6522            qis_text.contains("@qir_qis.load_qubit"),
6523            "expected translated output to keep the static-handle loader: {qis_text}"
6524        );
6525        assert!(
6526            !qis_text.contains("@___rzz(i64 0, i64 1,"),
6527            "native gates should use loaded runtime handles rather than slot indices: {qis_text}"
6528        );
6529        assert!(
6530            !qis_text.contains("@___rxy(i64 0,"),
6531            "single-qubit native gates should use loaded runtime handles rather than slot indices: {qis_text}"
6532        );
6533        assert!(
6534            !qis_text.contains("@___rz(i64 1,"),
6535            "single-qubit native gates should use loaded runtime handles rather than slot indices: {qis_text}"
6536        );
6537    }
6538
6539    #[test]
6540    fn test_validate_qir_allows_zero_required_num_results_for_mz_leaked() {
6541        let ll_text = minimal_qir_with_body(
6542            "1",
6543            "0",
6544            "1",
6545            r#"
6546declare i64 @__quantum__qis__mz_leaked__body(%Qubit*)
6547declare void @__quantum__rt__int_record_output(i64, i8*)
6548
6549@0 = private constant [7 x i8] c"leaked\00"
6550"#,
6551            r"  %q0 = inttoptr i64 0 to %Qubit*
6552  %0 = call i64 @__quantum__qis__mz_leaked__body(%Qubit* %q0)
6553  call void @__quantum__rt__int_record_output(i64 %0, i8* getelementptr inbounds ([7 x i8], [7 x i8]* @0, i64 0, i64 0))",
6554        );
6555
6556        let bc_bytes = qir_ll_to_bc(&ll_text).expect("Failed to convert inline QIR to bitcode");
6557        validate_qir(&bc_bytes, None)
6558            .expect("mz_leaked-only programs should validate with zero result slots");
6559    }
6560
6561    #[test]
6562    fn test_validate_qir_reports_invalid_required_num_results_value() {
6563        let ll_text = minimal_qir_with_body("1", "abc", "1", "", "");
6564
6565        let bc_bytes = qir_ll_to_bc(&ll_text).expect("Failed to convert inline QIR to bitcode");
6566        let err = validate_qir(&bc_bytes, None)
6567            .expect_err("invalid required_num_results should fail validation");
6568        assert!(err.contains("Invalid required_num_results attribute value: abc"));
6569    }
6570
6571    #[test]
6572    fn test_validate_qir_reports_missing_required_num_results_once() {
6573        let ll_text = r#"
6574%Qubit = type opaque
6575
6576define i64 @Entry_Point_Name() #0 {
6577entry:
6578  ret i64 0
6579}
6580
6581attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" }
6582
6583!llvm.module.flags = !{!0, !1, !2, !3}
6584!0 = !{i32 1, !"qir_major_version", i32 1}
6585!1 = !{i32 7, !"qir_minor_version", i32 0}
6586!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
6587!3 = !{i32 1, !"dynamic_result_management", i1 false}
6588"#;
6589
6590        let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode");
6591        let err =
6592            validate_qir(&bc_bytes, None).expect_err("missing required_num_results should fail");
6593        assert_eq!(err, "Missing required attribute: `required_num_results`");
6594    }
6595
6596    #[test]
6597    fn test_validate_qir_rejects_result_usage_in_ir_defined_helper_with_zero_slots() {
6598        let ll_text = r#"
6599%Qubit = type opaque
6600%Result = type opaque
6601
6602declare i1 @__quantum__rt__read_result(%Result*)
6603
6604define internal void @helper() {
6605entry:
6606  %0 = call i1 @__quantum__rt__read_result(%Result* null)
6607  ret void
6608}
6609
6610define i64 @Entry_Point_Name() #0 {
6611entry:
6612  call void @helper()
6613  ret i64 0
6614}
6615
6616attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="0" }
6617
6618!llvm.module.flags = !{!0, !1, !2, !3}
6619!0 = !{i32 1, !"qir_major_version", i32 1}
6620!1 = !{i32 7, !"qir_minor_version", i32 0}
6621!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
6622!3 = !{i32 1, !"dynamic_result_management", i1 false}
6623"#;
6624
6625        let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode");
6626        let err = validate_qir(&bc_bytes, None).expect_err(
6627            "result usage in IR-defined helpers should still respect required_num_results",
6628        );
6629        assert!(err.contains("Result index 0 exceeds required_num_results (0)"));
6630    }
6631
6632    #[test]
6633    fn test_validate_qir_rejects_out_of_range_static_handle_passed_to_ir_defined_helper() {
6634        let ll_text = r#"
6635%Qubit = type opaque
6636
6637define internal void @helper(%Qubit* %qubit) {
6638entry:
6639  call void @__quantum__qis__h__body(%Qubit* %qubit)
6640  ret void
6641}
6642
6643define i64 @Entry_Point_Name() #0 {
6644entry:
6645  %q1 = inttoptr i64 1 to %Qubit*
6646  call void @helper(%Qubit* %q1)
6647  ret i64 0
6648}
6649
6650declare void @__quantum__qis__h__body(%Qubit*)
6651
6652attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" }
6653
6654!llvm.module.flags = !{!0, !1, !2, !3}
6655!0 = !{i32 1, !"qir_major_version", i32 1}
6656!1 = !{i32 7, !"qir_minor_version", i32 0}
6657!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
6658!3 = !{i32 1, !"dynamic_result_management", i1 false}
6659"#;
6660
6661        let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode");
6662        let err = validate_qir(&bc_bytes, None)
6663            .expect_err("helper calls with out-of-range static handles should fail validation");
6664        assert!(err.contains("Invalid static qubit handle passed to `helper`"));
6665        assert!(err.contains("Qubit index 1 exceeds required_num_qubits (1)"));
6666    }
6667
6668    #[test]
6669    fn test_validate_qir_rejects_zero_required_num_results_for_result_measurement() {
6670        let ll_text = minimal_qir_with_body(
6671            "1",
6672            "0",
6673            "1",
6674            "declare void @__quantum__qis__mz__body(%Qubit*, %Result* writeonly)",
6675            r"  call void @__quantum__qis__mz__body(%Qubit* null, %Result* writeonly null)",
6676        );
6677
6678        let bc_bytes = qir_ll_to_bc(&ll_text).expect("Failed to convert inline QIR to bitcode");
6679        let err = validate_qir(&bc_bytes, None)
6680            .expect_err("result-backed measurements should fail validation without result slots");
6681        assert!(err.contains("Result index 0 exceeds required_num_results (0)"));
6682    }
6683
6684    #[test]
6685    fn test_validate_qir_rejects_zero_required_num_results_for_read_result() {
6686        let ll_text = minimal_qir_with_body(
6687            "1",
6688            "0",
6689            "1",
6690            "declare i1 @__quantum__rt__read_result(%Result*)",
6691            r"  %0 = call i1 @__quantum__rt__read_result(%Result* null)",
6692        );
6693
6694        let bc_bytes = qir_ll_to_bc(&ll_text).expect("Failed to convert inline QIR to bitcode");
6695        let err = validate_qir(&bc_bytes, None)
6696            .expect_err("result reads should fail validation without result slots");
6697        assert!(err.contains("Result index 0 exceeds required_num_results (0)"));
6698    }
6699
6700    #[test]
6701    fn test_validate_qir_rejects_zero_required_num_results_for_result_record_output() {
6702        let ll_text = minimal_qir_with_body(
6703            "1",
6704            "0",
6705            "1",
6706            r#"
6707declare void @__quantum__rt__result_record_output(%Result*, i8*)
6708
6709@0 = private constant [4 x i8] c"res\00"
6710"#,
6711            r"  call void @__quantum__rt__result_record_output(%Result* null, i8* getelementptr inbounds ([4 x i8], [4 x i8]* @0, i64 0, i64 0))",
6712        );
6713
6714        let bc_bytes = qir_ll_to_bc(&ll_text).expect("Failed to convert inline QIR to bitcode");
6715        let err = validate_qir(&bc_bytes, None)
6716            .expect_err("result output should fail validation without result slots");
6717        assert!(err.contains("Result index 0 exceeds required_num_results (0)"));
6718    }
6719
6720    #[test]
6721    fn test_validate_qir_rejects_out_of_bounds_result_measurement_index() {
6722        let ll_text = minimal_qir_with_body(
6723            "1",
6724            "1",
6725            "1",
6726            "declare void @__quantum__qis__mz__body(%Qubit*, %Result* writeonly)",
6727            r"  call void @__quantum__qis__mz__body(%Qubit* null, %Result* writeonly inttoptr (i64 5 to %Result*))",
6728        );
6729
6730        let bc_bytes = qir_ll_to_bc(&ll_text).expect("Failed to convert inline QIR to bitcode");
6731        let err = validate_qir(&bc_bytes, None)
6732            .expect_err("out-of-bounds result indices should fail during validation");
6733        assert!(err.contains("Result index 5 exceeds required_num_results (1)"));
6734    }
6735
6736    #[test]
6737    fn test_qir_to_qis_rejects_malformed_mz_leaked_call() {
6738        let ll_text = minimal_qir_with_body(
6739            "1",
6740            "0",
6741            "1",
6742            "declare i64 @__quantum__qis__mz_leaked__body()",
6743            r"  %0 = call i64 @__quantum__qis__mz_leaked__body()",
6744        );
6745
6746        let bc_bytes = qir_ll_to_bc(&ll_text).expect("Failed to convert inline QIR to bitcode");
6747        let err = qir_to_qis(&bc_bytes, 0, "native", None)
6748            .expect_err("malformed mz_leaked calls should fail cleanly");
6749        assert!(err.contains("Malformed mz_leaked call"));
6750    }
6751
6752    #[test]
6753    fn test_qir_to_qis_rejects_mz_leaked_with_wrong_return_type() {
6754        let ll_text = minimal_qir_with_body(
6755            "1",
6756            "0",
6757            "1",
6758            "declare void @__quantum__qis__mz_leaked__body(%Qubit*)",
6759            r"  call void @__quantum__qis__mz_leaked__body(%Qubit* null)",
6760        );
6761
6762        let bc_bytes = qir_ll_to_bc(&ll_text).expect("Failed to convert inline QIR to bitcode");
6763        let err = qir_to_qis(&bc_bytes, 0, "native", None)
6764            .expect_err("mz_leaked with the wrong signature should fail cleanly");
6765        assert!(err.contains("Malformed mz_leaked call: expected signature i64 (ptr)"));
6766    }
6767
6768    #[test]
6769    fn test_qir_to_qis_rejects_mz_leaked_with_wrong_return_width() {
6770        let ll_text = minimal_qir_with_body(
6771            "1",
6772            "0",
6773            "1",
6774            "declare i1 @__quantum__qis__mz_leaked__body(%Qubit*)",
6775            r"  %0 = call i1 @__quantum__qis__mz_leaked__body(%Qubit* null)",
6776        );
6777
6778        let bc_bytes = qir_ll_to_bc(&ll_text).expect("Failed to convert inline QIR to bitcode");
6779        let err = qir_to_qis(&bc_bytes, 0, "native", None)
6780            .expect_err("mz_leaked with the wrong return width should fail cleanly");
6781        assert_eq!(
6782            err,
6783            "Malformed mz_leaked call: expected signature i64 (ptr)"
6784        );
6785    }
6786
6787    #[test]
6788    fn test_qir_to_qis_rejects_mz_leaked_with_non_pointer_parameter() {
6789        let ll_text = minimal_qir_with_body(
6790            "1",
6791            "0",
6792            "1",
6793            "declare i64 @__quantum__qis__mz_leaked__body(i64)",
6794            r"  %0 = call i64 @__quantum__qis__mz_leaked__body(i64 0)",
6795        );
6796
6797        let bc_bytes = qir_ll_to_bc(&ll_text).expect("Failed to convert inline QIR to bitcode");
6798        let err = qir_to_qis(&bc_bytes, 0, "native", None)
6799            .expect_err("mz_leaked with a non-pointer parameter should fail cleanly");
6800        assert_eq!(
6801            err,
6802            "Malformed mz_leaked call: expected signature i64 (ptr)"
6803        );
6804    }
6805
6806    #[test]
6807    fn test_qir_to_qis_rejects_nonzero_single_qubit_mz_leaked_handle() {
6808        let ll_text = minimal_qir_with_body(
6809            "1",
6810            "0",
6811            "1",
6812            "declare i64 @__quantum__qis__mz_leaked__body(%Qubit*)",
6813            r"  %q0 = inttoptr i64 1 to %Qubit*
6814  %0 = call i64 @__quantum__qis__mz_leaked__body(%Qubit* %q0)",
6815        );
6816
6817        let bc_bytes = qir_ll_to_bc(&ll_text).expect("Failed to convert inline QIR to bitcode");
6818        let err = qir_to_qis(&bc_bytes, 0, "native", None).expect_err(
6819            "static mz_leaked qubit id 1 should be rejected in a single-qubit zero-based program",
6820        );
6821        assert!(
6822            err.contains("Invalid static qubit handle passed to `__quantum__qis__mz_leaked__body`")
6823        );
6824        assert!(err.contains("Qubit index 1 exceeds required_num_qubits (1)"));
6825    }
6826
6827    #[test]
6828    fn test_qir_to_qis_rejects_out_of_range_static_handle_passed_to_ir_defined_helper() {
6829        let ll_text = r#"
6830%Qubit = type opaque
6831
6832define internal void @helper(%Qubit* %qubit) {
6833entry:
6834  call void @__quantum__qis__h__body(%Qubit* %qubit)
6835  ret void
6836}
6837
6838define i64 @Entry_Point_Name() #0 {
6839entry:
6840  %q1 = inttoptr i64 1 to %Qubit*
6841  call void @helper(%Qubit* %q1)
6842  ret i64 0
6843}
6844
6845declare void @__quantum__qis__h__body(%Qubit*)
6846
6847attributes #0 = { "entry_point" "qir_profiles"="base_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" }
6848
6849!llvm.module.flags = !{!0, !1, !2, !3}
6850!0 = !{i32 1, !"qir_major_version", i32 1}
6851!1 = !{i32 7, !"qir_minor_version", i32 0}
6852!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
6853!3 = !{i32 1, !"dynamic_result_management", i1 false}
6854"#;
6855
6856        let bc_bytes = qir_ll_to_bc(ll_text).expect("Failed to convert inline QIR to bitcode");
6857        let err = qir_to_qis(&bc_bytes, 0, "native", None).expect_err(
6858            "helper calls with out-of-range static handles should fail before runtime helper generation",
6859        );
6860        assert!(err.contains("Invalid static qubit handle passed to `helper`"));
6861        assert!(err.contains("Qubit index 1 exceeds required_num_qubits (1)"));
6862    }
6863
6864    #[test]
6865    fn test_mz_leaked_operand_check_rejects_non_pointer_first_operand() {
6866        let ctx = Context::create();
6867        let value = ctx.i64_type().const_zero().into();
6868        let err = crate::aux::mz_leaked_qubit_operand(&[value, value])
6869            .expect_err("non-pointer mz_leaked operands should fail cleanly");
6870        assert_eq!(
6871            err,
6872            "Malformed mz_leaked call: expected first argument to be a pointer"
6873        );
6874    }
6875
6876    #[cfg(not(windows))]
6877    #[test]
6878    fn test_qir_to_qis_mz_leaked_lowers_via_uint_future_runtime() {
6879        let bc_bytes = load_fixture_bitcode("tests/data/mz_leaked.ll");
6880        let output_bc = qir_to_qis(&bc_bytes, 0, "native", None)
6881            .expect("mz_leaked fixture should compile successfully");
6882
6883        verify_bitcode_module(&output_bc, "mz_leaked_qis")
6884            .expect("translated leaked-measure module should remain LLVM-verifiable");
6885
6886        let ctx = Context::create();
6887        let module = parse_bitcode_module(&ctx, &output_bc, "mz_leaked_qis")
6888            .expect("Compiled QIS bitcode should parse");
6889        let text = module.to_string();
6890        assert!(text.contains("___lazy_measure_leaked"));
6891        assert!(text.contains("___read_future_uint"));
6892        assert!(text.contains("___dec_future_refcount"));
6893        assert!(!text.contains("___read_future_bool"));
6894    }
6895
6896    #[cfg(windows)]
6897    #[test]
6898    fn test_qir_to_qis_mz_leaked_windows_smoke() {
6899        let bc_bytes = load_fixture_bitcode("tests/data/mz_leaked.ll");
6900        let output_bc = qir_to_qis(&bc_bytes, 0, "native", None)
6901            .expect("mz_leaked fixture should compile successfully on Windows");
6902
6903        let ctx = Context::create();
6904        let module = parse_bitcode_module(&ctx, &output_bc, "mz_leaked_qis")
6905            .expect("Compiled QIS bitcode should parse on Windows");
6906        assert!(module.get_function("qmain").is_some());
6907    }
6908
6909    #[cfg(feature = "wasm")]
6910    proptest! {
6911        #[test]
6912        fn prop_get_wasm_functions_round_trips_exact_exports(
6913            exports in proptest::collection::btree_map("[A-Za-z_][A-Za-z0-9_]{0,8}", 0u32..32u32, 0..8)
6914        ) {
6915            let exports_vec = exports
6916                .iter()
6917                .map(|(name, index)| (name.clone(), *index))
6918                .collect::<Vec<_>>();
6919            let wasm = build_wasm_exports(&exports_vec);
6920            let parsed = crate::get_wasm_functions(Some(&wasm))
6921                .map_err(|err| TestCaseError::fail(format!("get_wasm_functions failed unexpectedly: {err}")))?;
6922
6923            prop_assert_eq!(parsed.len(), exports.len());
6924            for (name, index) in exports {
6925                prop_assert_eq!(parsed.get(&name), Some(&u64::from(index)));
6926            }
6927        }
6928    }
6929
6930    proptest! {
6931        #[cfg(not(windows))]
6932        #[test]
6933        fn prop_qir_ll_to_bc_rejects_malformed_ir(suffix in "\\PC{0,128}") {
6934            let ll_text = format!("this is not valid llvm ir\n{suffix}");
6935            prop_assert!(qir_ll_to_bc(&ll_text).is_err());
6936        }
6937
6938        #[cfg(not(windows))]
6939        #[test]
6940        fn prop_validate_qir_rejects_malformed_bitcode(tail in proptest::collection::vec(any::<u8>(), 0..256)) {
6941            let mut bytes = b"NOTQIR".to_vec();
6942            bytes.extend(tail);
6943            prop_assert!(validate_qir(&bytes, None).is_err());
6944        }
6945
6946        #[cfg(not(windows))]
6947        #[test]
6948        fn prop_qir_to_qis_rejects_malformed_bitcode(tail in proptest::collection::vec(any::<u8>(), 0..256)) {
6949            let mut bytes = b"NOTQIR".to_vec();
6950            bytes.extend(tail);
6951            let (opt_level, target) = conservative_translation_settings();
6952            prop_assert!(qir_to_qis(&bytes, opt_level, target, None).is_err());
6953        }
6954
6955        #[test]
6956        fn prop_valid_fixtures_translate_to_verifiable_qis(fixture in proptest::sample::select(PROPERTY_FIXTURES)) {
6957            let input_bc = load_fixture_bitcode(fixture);
6958            prop_assert!(validate_qir(&input_bc, None).is_ok());
6959
6960            let (opt_level, target) = conservative_translation_settings();
6961            let output_bc = qir_to_qis(&input_bc, opt_level, target, None)
6962                .map_err(|err| TestCaseError::fail(format!("translation failed for {fixture}: {err}")))?;
6963
6964            verify_bitcode_module(&output_bc, "property_qis_module")
6965                .map_err(|err| TestCaseError::fail(format!("verification failed for {fixture}: {err}")))?;
6966        }
6967
6968        #[test]
6969        fn prop_invalid_targets_fail_fast(target in "[a-z0-9_-]{1,12}") {
6970            prop_assume!(target != "native");
6971            prop_assume!(target != "aarch64");
6972            prop_assume!(target != "x86-64");
6973
6974            let input_bc = load_fixture_bitcode("tests/data/base.ll");
6975            prop_assert!(qir_to_qis(&input_bc, 0, &target, None).is_err());
6976        }
6977
6978        #[test]
6979        fn prop_missing_required_attrs_fail_validation(
6980            missing_idx in 0usize..4usize
6981        ) {
6982            let missing_attr = *[
6983                "qir_profiles",
6984                "output_labeling_schema",
6985                "required_num_qubits",
6986                "required_num_results",
6987            ]
6988            .get(missing_idx)
6989            .expect("missing_idx is generated within bounds");
6990            let ll_text = minimal_qir_missing_attr(missing_attr);
6991            let bc = qir_ll_to_bc(&ll_text)
6992                .map_err(|err| TestCaseError::fail(format!("inline IR should parse: {err}")))?;
6993            let err = validate_qir(&bc, None)
6994                .expect_err("validation should reject missing required attributes");
6995            let expected = format!("Missing required attribute: `{missing_attr}`");
6996            prop_assert!(err.contains(&expected));
6997        }
6998
6999        #[test]
7000        fn prop_qir_major_versions_accept_only_one_or_two(major in 0u32..5u32) {
7001            let ll_text = minimal_qir_with_body("1", "1", &major.to_string(), "", "");
7002            let bc = qir_ll_to_bc(&ll_text)
7003                .map_err(|err| TestCaseError::fail(format!("inline IR should parse: {err}")))?;
7004            let result = validate_qir(&bc, None);
7005            if matches!(major, 1 | 2) {
7006                prop_assert!(result.is_ok());
7007            } else {
7008                let err = result.expect_err("invalid major versions must fail");
7009                prop_assert!(err.contains("Unsupported qir_major_version"));
7010            }
7011        }
7012
7013        #[test]
7014        fn prop_duplicate_qir_major_flags_pass_if_any_match(
7015            valid_first in any::<bool>(),
7016            valid_second in any::<bool>(),
7017        ) {
7018            let first_major = if valid_first { "1" } else { "99" };
7019            let second_major = if valid_second { "2" } else { "100" };
7020            let ll_text = minimal_qir_with_duplicate_major_flags(first_major, second_major);
7021            let bc = qir_ll_to_bc(&ll_text)
7022                .map_err(|err| TestCaseError::fail(format!("inline IR should parse: {err}")))?;
7023            let result = validate_qir(&bc, None);
7024            if valid_first || valid_second {
7025                prop_assert!(result.is_ok());
7026            } else {
7027                let err = result.expect_err("all-invalid duplicate major flags must fail");
7028                prop_assert!(err.contains("Unsupported qir_major_version"));
7029            }
7030        }
7031
7032        #[test]
7033        fn prop_duplicate_dynamic_qubit_flags_accept_true_and_false_values(
7034            first_is_true in any::<bool>(),
7035            second_is_true in any::<bool>(),
7036        ) {
7037            let first_flag = if first_is_true { "true" } else { "false" };
7038            let second_flag = if second_is_true { "true" } else { "false" };
7039            let ll_text = minimal_qir_with_duplicate_dynamic_flags(first_flag, second_flag);
7040            let bc = qir_ll_to_bc(&ll_text)
7041                .map_err(|err| TestCaseError::fail(format!("inline IR should parse: {err}")))?;
7042            prop_assert!(validate_qir(&bc, None).is_ok());
7043        }
7044
7045        #[test]
7046        fn prop_barrier_validation_tracks_required_qubits(
7047            required_num_qubits in 1u32..5u32,
7048            barrier_arity in 1u32..5u32,
7049        ) {
7050            let barrier_name = format!("__quantum__qis__barrier{barrier_arity}__body");
7051            let barrier_args = (0..barrier_arity)
7052                .map(|idx| format!("%Qubit* %q{idx}"))
7053                .collect::<Vec<_>>()
7054                .join(", ");
7055            let extra_decl = format!(
7056                "declare void @{barrier_name}({})",
7057                std::iter::repeat_n("%Qubit*", usize::try_from(barrier_arity).unwrap_or(0))
7058                    .collect::<Vec<_>>()
7059                    .join(", ")
7060            );
7061            let body = (0..barrier_arity)
7062                .map(|idx| format!("  %q{idx} = inttoptr i64 {idx} to %Qubit*"))
7063                .chain(std::iter::once(format!("  call void @{barrier_name}({barrier_args})")))
7064                .collect::<Vec<_>>()
7065                .join("\n");
7066            let ll_text = minimal_qir_with_body(
7067                &required_num_qubits.to_string(),
7068                "1",
7069                "1",
7070                &extra_decl,
7071                &body,
7072            );
7073            let bc = qir_ll_to_bc(&ll_text)
7074                .map_err(|err| TestCaseError::fail(format!("inline IR should parse: {err}")))?;
7075            let result = validate_qir(&bc, None);
7076            if barrier_arity <= required_num_qubits {
7077                prop_assert!(result.is_ok());
7078            } else {
7079                let err = result.expect_err("oversized barrier arity must fail");
7080                prop_assert!(err.contains("Barrier arity"));
7081            }
7082        }
7083
7084        #[cfg(not(windows))]
7085        #[test]
7086        fn prop_get_entry_attributes_rejects_malformed_bitcode(
7087            tail in proptest::collection::vec(any::<u8>(), 0..256)
7088        ) {
7089            let mut bytes = b"NOTQIR".to_vec();
7090            bytes.extend(tail);
7091            prop_assert!(get_entry_attributes(&bytes).is_err());
7092        }
7093    }
7094
7095    #[test]
7096    fn test_zero_qubits_fail_validation() {
7097        let ll_text = minimal_qir_with_body("0", "1", "1", "", "");
7098        let bc = qir_ll_to_bc(&ll_text).expect("inline IR should parse");
7099        let err = validate_qir(&bc, None).expect_err("validation should reject zero qubits");
7100        assert!(err.contains("Entry function must have at least one qubit"));
7101    }
7102
7103    #[rstest]
7104    #[case("65535", "1")]
7105    #[case("1", "65535")]
7106    fn test_validate_static_resource_limit_accepts_maximum(
7107        #[case] required_num_qubits: &str,
7108        #[case] required_num_results: &str,
7109    ) {
7110        let ll_text = minimal_qir_with_body(required_num_qubits, required_num_results, "1", "", "");
7111        let bc = qir_ll_to_bc(&ll_text).expect("inline IR should parse");
7112        validate_qir(&bc, None).expect("maximum static resource count should validate");
7113        qir_to_qis(&bc, 0, "native", None).expect("maximum static resource count should translate");
7114    }
7115
7116    #[rstest]
7117    #[case("65536", "1", "required_num_qubits")]
7118    #[case("1", "65536", "required_num_results")]
7119    fn test_static_resource_limit_rejects_first_oversized_value(
7120        #[case] required_num_qubits: &str,
7121        #[case] required_num_results: &str,
7122        #[case] resource_name: &str,
7123    ) {
7124        let ll_text = minimal_qir_with_body(required_num_qubits, required_num_results, "1", "", "");
7125        let bc = qir_ll_to_bc(&ll_text).expect("inline IR should parse");
7126        let expected_error = format!("{resource_name} value 65536 exceeds compiler limit 65535");
7127
7128        let validation_error =
7129            validate_qir(&bc, None).expect_err("oversized static resources should not validate");
7130        assert!(validation_error.contains(&expected_error));
7131
7132        let translation_error = qir_to_qis(&bc, 0, "native", None)
7133            .expect_err("oversized static resources should not be allocated during translation");
7134        assert!(translation_error.contains(&expected_error));
7135    }
7136
7137    #[rstest]
7138    #[case("65536", "1", "required_num_qubits")]
7139    #[case("1", "65536", "required_num_results")]
7140    fn test_dynamic_management_rejects_oversized_optional_static_resource_count(
7141        #[case] required_num_qubits: &str,
7142        #[case] required_num_results: &str,
7143        #[case] resource_name: &str,
7144    ) {
7145        let ll_text = minimal_qir_with_body(required_num_qubits, required_num_results, "2", "", "")
7146            .replace(
7147                "dynamic_qubit_management\", i1 false",
7148                "dynamic_qubit_management\", i1 true",
7149            )
7150            .replace(
7151                "dynamic_result_management\", i1 false",
7152                "dynamic_result_management\", i1 true",
7153            );
7154        let bc = qir_ll_to_bc(&ll_text).expect("inline adaptive IR should parse");
7155        let expected_error = format!("{resource_name} value 65536 exceeds compiler limit 65535");
7156
7157        let validation_error = validate_qir(&bc, None)
7158            .expect_err("oversized optional static resources should not validate");
7159        assert!(validation_error.contains(&expected_error));
7160
7161        let translation_error = qir_to_qis(&bc, 0, "native", None)
7162            .expect_err("oversized optional static resources should not translate");
7163        assert!(translation_error.contains(&expected_error));
7164    }
7165
7166    #[test]
7167    fn test_validate_dynamic_qubits_without_required_num_qubits() {
7168        let ll_text = r#"
7169define i64 @Entry_Point_Name() #0 {
7170entry:
7171  %err = alloca i1, align 1
7172  %q = call ptr @__quantum__rt__qubit_allocate(ptr %err)
7173  call void @__quantum__qis__h__body(ptr %q)
7174  call void @__quantum__rt__qubit_release(ptr %q)
7175  ret i64 0
7176}
7177
7178declare ptr @__quantum__rt__qubit_allocate(ptr)
7179declare void @__quantum__rt__qubit_release(ptr)
7180declare void @__quantum__qis__h__body(ptr)
7181
7182attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_results"="1" }
7183
7184!llvm.module.flags = !{!0, !1, !2, !3, !4}
7185!0 = !{i32 1, !"qir_major_version", i32 2}
7186!1 = !{i32 7, !"qir_minor_version", i32 0}
7187!2 = !{i32 1, !"dynamic_qubit_management", i1 true}
7188!3 = !{i32 1, !"dynamic_result_management", i1 false}
7189!4 = !{i32 1, !"arrays", i1 false}
7190"#;
7191        let bc_bytes = qir_ll_to_bc(ll_text).unwrap();
7192        validate_qir(&bc_bytes, None).expect("dynamic qubit fixture should validate");
7193        let qis_bytes =
7194            qir_to_qis(&bc_bytes, 0, "native", None).expect("dynamic qubit fixture should compile");
7195        let ctx = Context::create();
7196        let module = parse_bitcode_module(&ctx, &qis_bytes, "qis_module").unwrap();
7197        assert!(module.get_function("qir_qis.qubit_allocate").is_some());
7198        assert!(module.get_function("qir_qis.qubit_release").is_some());
7199    }
7200
7201    #[test]
7202    fn test_validate_capability_usage_ignores_unused_rt_declarations() {
7203        let ll_text = r#"
7204define i64 @Entry_Point_Name() #0 {
7205entry:
7206  ret i64 0
7207}
7208
7209declare ptr @__quantum__rt__result_allocate(ptr)
7210declare void @__quantum__rt__result_release(ptr)
7211
7212attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" }
7213
7214!llvm.module.flags = !{!0, !1, !2, !3, !4}
7215!0 = !{i32 1, !"qir_major_version", i32 2}
7216!1 = !{i32 7, !"qir_minor_version", i32 0}
7217!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
7218!3 = !{i32 1, !"dynamic_result_management", i1 false}
7219!4 = !{i32 1, !"arrays", i1 false}
7220"#;
7221        let bc_bytes = qir_ll_to_bc(ll_text).unwrap();
7222        validate_qir(&bc_bytes, None)
7223            .expect("unused dynamic result declarations should not fail validation");
7224    }
7225
7226    #[test]
7227    fn test_validate_capability_usage_reports_called_rt_function_without_flag() {
7228        let ll_text = r#"
7229define i64 @Entry_Point_Name() #0 {
7230entry:
7231  %err = alloca i1, align 1
7232  %r = call ptr @__quantum__rt__result_allocate(ptr %err)
7233  call void @__quantum__rt__result_release(ptr %r)
7234  ret i64 0
7235}
7236
7237declare ptr @__quantum__rt__result_allocate(ptr)
7238declare void @__quantum__rt__result_release(ptr)
7239
7240attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" }
7241
7242!llvm.module.flags = !{!0, !1, !2, !3, !4}
7243!0 = !{i32 1, !"qir_major_version", i32 2}
7244!1 = !{i32 7, !"qir_minor_version", i32 0}
7245!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
7246!3 = !{i32 1, !"dynamic_result_management", i1 false}
7247!4 = !{i32 1, !"arrays", i1 false}
7248"#;
7249        let bc_bytes = qir_ll_to_bc(ll_text).unwrap();
7250        let err = validate_qir(&bc_bytes, None)
7251            .expect_err("called dynamic result functions should fail validation");
7252        assert!(
7253            err.contains(
7254                "__quantum__rt__result_allocate requires `dynamic_result_management=true`"
7255            )
7256        );
7257    }
7258
7259    #[test]
7260    fn test_validate_capability_usage_reports_called_dynamic_qubit_rt_function_without_flag() {
7261        let ll_text = r#"
7262define i64 @Entry_Point_Name() #0 {
7263entry:
7264  %err = alloca i1, align 1
7265  %q = call ptr @__quantum__rt__qubit_allocate(ptr %err)
7266  call void @__quantum__rt__qubit_release(ptr %q)
7267  ret i64 0
7268}
7269
7270declare ptr @__quantum__rt__qubit_allocate(ptr)
7271declare void @__quantum__rt__qubit_release(ptr)
7272
7273attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_results"="1" }
7274
7275!llvm.module.flags = !{!0, !1, !2, !3, !4}
7276!0 = !{i32 1, !"qir_major_version", i32 2}
7277!1 = !{i32 7, !"qir_minor_version", i32 0}
7278!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
7279!3 = !{i32 1, !"dynamic_result_management", i1 false}
7280!4 = !{i32 1, !"arrays", i1 false}
7281"#;
7282
7283        let bc_bytes = qir_ll_to_bc(ll_text).unwrap();
7284        let err = validate_qir(&bc_bytes, None)
7285            .expect_err("called dynamic qubit functions should fail validation");
7286        assert!(
7287            err.contains("__quantum__rt__qubit_allocate requires `dynamic_qubit_management=true`")
7288        );
7289    }
7290
7291    #[test]
7292    fn test_validate_qir_rejects_malformed_dynamic_qubit_allocate_signature() {
7293        let ll_text = r#"
7294define i64 @Entry_Point_Name() #0 {
7295entry:
7296  %q = call ptr @__quantum__rt__qubit_allocate()
7297  call void @__quantum__rt__qubit_release(ptr %q)
7298  ret i64 0
7299}
7300
7301declare ptr @__quantum__rt__qubit_allocate()
7302declare void @__quantum__rt__qubit_release(ptr)
7303
7304attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_results"="1" }
7305
7306!llvm.module.flags = !{!0, !1, !2, !3, !4}
7307!0 = !{i32 1, !"qir_major_version", i32 2}
7308!1 = !{i32 7, !"qir_minor_version", i32 0}
7309!2 = !{i32 1, !"dynamic_qubit_management", i1 true}
7310!3 = !{i32 1, !"dynamic_result_management", i1 false}
7311!4 = !{i32 1, !"arrays", i1 false}
7312"#;
7313
7314        let bc_bytes = qir_ll_to_bc(ll_text).unwrap();
7315        let err = validate_qir(&bc_bytes, None)
7316            .expect_err("malformed dynamic runtime declaration should fail validation");
7317        assert!(
7318            err.contains("Malformed QIR RT function declaration: __quantum__rt__qubit_allocate")
7319        );
7320    }
7321
7322    #[test]
7323    fn test_validate_qir_rejects_malformed_dynamic_allocate_return_signature() {
7324        let cases = [
7325            (
7326                "declare void @__quantum__rt__qubit_allocate(ptr)",
7327                "__quantum__rt__qubit_allocate",
7328            ),
7329            (
7330                "declare void @__quantum__rt__result_allocate(ptr)",
7331                "__quantum__rt__result_allocate",
7332            ),
7333        ];
7334
7335        for (declaration, fn_name) in cases {
7336            assert_malformed_dynamic_rt_declaration(fn_name, declaration, fn_name);
7337        }
7338    }
7339
7340    #[test]
7341    fn test_validate_qir_rejects_malformed_dynamic_release_signatures() {
7342        let cases = [
7343            (
7344                "wrong return",
7345                "declare ptr @__quantum__rt__qubit_release(ptr)",
7346                "__quantum__rt__qubit_release",
7347            ),
7348            (
7349                "wrong arity",
7350                "declare void @__quantum__rt__qubit_release()",
7351                "__quantum__rt__qubit_release",
7352            ),
7353            (
7354                "wrong param type",
7355                "declare void @__quantum__rt__result_release(i64)",
7356                "__quantum__rt__result_release",
7357            ),
7358        ];
7359
7360        for (case_name, declaration, fn_name) in cases {
7361            assert_malformed_dynamic_rt_declaration(case_name, declaration, fn_name);
7362        }
7363    }
7364
7365    #[test]
7366    fn test_validate_qir_rejects_malformed_dynamic_array_three_arg_signatures() {
7367        let cases = [
7368            (
7369                "wrong return",
7370                "declare ptr @__quantum__rt__qubit_array_allocate(i64, ptr, ptr)",
7371                "__quantum__rt__qubit_array_allocate",
7372            ),
7373            (
7374                "wrong arity",
7375                "declare void @__quantum__rt__qubit_array_allocate(i64, ptr)",
7376                "__quantum__rt__qubit_array_allocate",
7377            ),
7378            (
7379                "wrong length type",
7380                "declare void @__quantum__rt__result_array_allocate(i32, ptr, ptr)",
7381                "__quantum__rt__result_array_allocate",
7382            ),
7383            (
7384                "wrong backing param type",
7385                "declare void @__quantum__rt__result_array_allocate(i64, i64, ptr)",
7386                "__quantum__rt__result_array_allocate",
7387            ),
7388            (
7389                "wrong output param type",
7390                "declare void @__quantum__rt__result_array_record_output(i64, ptr, i64)",
7391                "__quantum__rt__result_array_record_output",
7392            ),
7393        ];
7394
7395        for (case_name, declaration, fn_name) in cases {
7396            assert_malformed_dynamic_rt_declaration(case_name, declaration, fn_name);
7397        }
7398    }
7399
7400    #[test]
7401    fn test_validate_qir_rejects_malformed_dynamic_array_release_signatures() {
7402        let cases = [
7403            (
7404                "wrong return",
7405                "declare ptr @__quantum__rt__qubit_array_release(i64, ptr)",
7406                "__quantum__rt__qubit_array_release",
7407            ),
7408            (
7409                "wrong arity",
7410                "declare void @__quantum__rt__qubit_array_release(i64)",
7411                "__quantum__rt__qubit_array_release",
7412            ),
7413            (
7414                "wrong length type",
7415                "declare void @__quantum__rt__result_array_release(i32, ptr)",
7416                "__quantum__rt__result_array_release",
7417            ),
7418            (
7419                "wrong backing param type",
7420                "declare void @__quantum__rt__result_array_release(i64, i64)",
7421                "__quantum__rt__result_array_release",
7422            ),
7423        ];
7424
7425        for (case_name, declaration, fn_name) in cases {
7426            assert_malformed_dynamic_rt_declaration(case_name, declaration, fn_name);
7427        }
7428    }
7429
7430    #[test]
7431    fn test_validate_qir_rejects_unsupported_rt_function_declaration() {
7432        let ll_text = r#"
7433declare void @__quantum__rt__mystery()
7434
7435define i64 @Entry_Point_Name() #0 {
7436entry:
7437  ret i64 0
7438}
7439
7440attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" }
7441
7442!llvm.module.flags = !{!0, !1, !2, !3, !4}
7443!0 = !{i32 1, !"qir_major_version", i32 2}
7444!1 = !{i32 7, !"qir_minor_version", i32 0}
7445!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
7446!3 = !{i32 1, !"dynamic_result_management", i1 false}
7447!4 = !{i32 1, !"arrays", i1 false}
7448"#;
7449
7450        let bc_bytes = qir_ll_to_bc(ll_text).unwrap();
7451        let err =
7452            validate_qir(&bc_bytes, None).expect_err("unsupported RT declarations should fail");
7453        assert!(err.contains("Unsupported QIR RT function: __quantum__rt__mystery"));
7454    }
7455
7456    #[test]
7457    fn test_validate_dynamic_results_without_required_num_results() {
7458        let ll_text = r#"
7459@0 = internal constant [3 x i8] c"r0\00"
7460
7461define i64 @Entry_Point_Name() #0 {
7462entry:
7463  %r = call ptr @__quantum__rt__result_allocate(ptr null)
7464  call void @__quantum__qis__mz__body(ptr null, ptr %r)
7465  call void @__quantum__rt__result_record_output(ptr %r, ptr @0)
7466  call void @__quantum__rt__result_release(ptr %r)
7467  ret i64 0
7468}
7469
7470declare ptr @__quantum__rt__result_allocate(ptr)
7471declare void @__quantum__rt__result_release(ptr)
7472declare void @__quantum__qis__mz__body(ptr, ptr writeonly) #1
7473declare void @__quantum__rt__result_record_output(ptr, ptr)
7474
7475attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" }
7476attributes #1 = { "irreversible" }
7477
7478!llvm.module.flags = !{!0, !1, !2, !3, !4}
7479!0 = !{i32 1, !"qir_major_version", i32 2}
7480!1 = !{i32 7, !"qir_minor_version", i32 0}
7481!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
7482!3 = !{i32 1, !"dynamic_result_management", i1 true}
7483!4 = !{i32 1, !"arrays", i1 false}
7484"#;
7485        let bc_bytes = qir_ll_to_bc(ll_text).unwrap();
7486        validate_qir(&bc_bytes, None).expect("dynamic result fixture should validate");
7487        let qis_bytes = qir_to_qis(&bc_bytes, 0, "native", None)
7488            .expect("dynamic result fixture should compile");
7489        let ctx = Context::create();
7490        let module = parse_bitcode_module(&ctx, &qis_bytes, "qis_module").unwrap();
7491        assert!(module.get_function("qir_qis.result_read").is_some());
7492        assert!(module.get_function("qir_qis.out_err_success").is_some());
7493    }
7494
7495    #[test]
7496    fn test_validate_dynamic_result_array_record_output() {
7497        let ll_text = r#"
7498@0 = internal constant [3 x i8] c"a0\00"
7499
7500define i64 @Entry_Point_Name() #0 {
7501entry:
7502  %results = alloca [2 x ptr], align 8
7503  call void @__quantum__rt__result_array_allocate(i64 2, ptr %results, ptr null)
7504  %r0_ptr = getelementptr inbounds [2 x ptr], ptr %results, i64 0, i64 0
7505  %r0 = load ptr, ptr %r0_ptr, align 8
7506  %r1_ptr = getelementptr inbounds [2 x ptr], ptr %results, i64 0, i64 1
7507  %r1 = load ptr, ptr %r1_ptr, align 8
7508  call void @__quantum__qis__mz__body(ptr null, ptr %r0)
7509  call void @__quantum__qis__mz__body(ptr inttoptr (i64 1 to ptr), ptr %r1)
7510  call void @__quantum__rt__result_array_record_output(i64 2, ptr %results, ptr @0)
7511  call void @__quantum__rt__result_array_release(i64 2, ptr %results)
7512  ret i64 0
7513}
7514
7515declare void @__quantum__rt__result_array_allocate(i64, ptr, ptr)
7516declare void @__quantum__rt__result_array_release(i64, ptr)
7517declare void @__quantum__rt__result_array_record_output(i64, ptr, ptr)
7518declare void @__quantum__qis__mz__body(ptr, ptr writeonly) #1
7519
7520attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="2" }
7521attributes #1 = { "irreversible" }
7522
7523!llvm.module.flags = !{!0, !1, !2, !3, !4}
7524!0 = !{i32 1, !"qir_major_version", i32 2}
7525!1 = !{i32 7, !"qir_minor_version", i32 0}
7526!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
7527!3 = !{i32 1, !"dynamic_result_management", i1 true}
7528!4 = !{i32 1, !"arrays", i1 true}
7529"#;
7530        let bc_bytes = qir_ll_to_bc(ll_text).unwrap();
7531        validate_qir(&bc_bytes, None).expect("dynamic result array fixture should validate");
7532        let qis_bytes = qir_to_qis(&bc_bytes, 0, "native", None)
7533            .expect("dynamic result array fixture should compile");
7534        let ctx = Context::create();
7535        let module = parse_bitcode_module(&ctx, &qis_bytes, "qis_module").unwrap();
7536        assert!(
7537            module
7538                .get_function("qir_qis.result_array_record_output")
7539                .is_some()
7540        );
7541        assert!(module.get_function("qir_qis.out_err_success").is_some());
7542
7543        #[cfg(not(windows))]
7544        {
7545            let module_text = module.to_string();
7546            assert!(
7547                module_text.contains("USER:RESULT_ARRAY:a0"),
7548                "expected RESULT_ARRAY output tag, got module:\n{module_text}"
7549            );
7550            let print_bool_arr_calls = module_text.matches("@print_bool_arr").count();
7551            assert!(
7552                print_bool_arr_calls >= 2,
7553                "expected print_bool_arr declaration and use, got module:\n{module_text}"
7554            );
7555            assert!(
7556                !module_text.contains("@print_bool("),
7557                "expected array output lowering without scalar print_bool fallback, got module:\n{module_text}"
7558            );
7559        }
7560
7561        #[cfg(windows)]
7562        {
7563            let labels = module
7564                .get_globals()
7565                .filter_map(|global| crate::convert::get_string_label(global).ok())
7566                .collect::<Vec<_>>();
7567            assert!(
7568                labels
7569                    .iter()
7570                    .any(|label| label.contains("USER:RESULT_ARRAY:a0")),
7571                "expected RESULT_ARRAY output label in globals, got labels: {labels:?}"
7572            );
7573            assert!(module.get_function("print_bool_arr").is_some());
7574            assert!(module.get_function("print_bool").is_none());
7575        }
7576    }
7577
7578    #[test]
7579    fn test_validate_dynamic_result_array_record_output_length_mismatch_fails() {
7580        let ll_text = r#"
7581@0 = internal constant [3 x i8] c"a0\00"
7582
7583define i64 @Entry_Point_Name() #0 {
7584entry:
7585  %results = alloca [1 x ptr], align 8
7586  call void @__quantum__rt__result_array_record_output(i64 2, ptr %results, ptr @0)
7587  ret i64 0
7588}
7589
7590declare void @__quantum__rt__result_array_record_output(i64, ptr, ptr)
7591
7592attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" }
7593
7594!llvm.module.flags = !{!0, !1, !2, !3, !4}
7595!0 = !{i32 1, !"qir_major_version", i32 2}
7596!1 = !{i32 7, !"qir_minor_version", i32 0}
7597!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
7598!3 = !{i32 1, !"dynamic_result_management", i1 true}
7599!4 = !{i32 1, !"arrays", i1 true}
7600"#;
7601        let bc_bytes = qir_ll_to_bc(ll_text).unwrap();
7602        let err = validate_qir(&bc_bytes, None)
7603            .expect_err("mismatched result array output backing should fail validation");
7604        assert!(err.contains(
7605            "__quantum__rt__result_array_record_output requires a fixed-size backing array"
7606        ));
7607        assert!(err.contains("requested length 2 does not match backing array length 1"));
7608    }
7609
7610    #[test]
7611    fn test_validate_dynamic_result_array_record_output_large_length_fails() {
7612        let ll_text = r#"
7613@0 = internal constant [3 x i8] c"a0\00"
7614
7615define i64 @Entry_Point_Name() #0 {
7616entry:
7617  %results = alloca [2147483648 x ptr], align 8
7618  call void @__quantum__rt__result_array_record_output(i64 2147483648, ptr %results, ptr @0)
7619  ret i64 0
7620}
7621
7622declare void @__quantum__rt__result_array_record_output(i64, ptr, ptr)
7623
7624attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" }
7625
7626!llvm.module.flags = !{!0, !1, !2, !3, !4}
7627!0 = !{i32 1, !"qir_major_version", i32 2}
7628!1 = !{i32 7, !"qir_minor_version", i32 0}
7629!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
7630!3 = !{i32 1, !"dynamic_result_management", i1 true}
7631!4 = !{i32 1, !"arrays", i1 true}
7632"#;
7633        let bc_bytes = qir_ll_to_bc(ll_text).unwrap();
7634        let err = validate_qir(&bc_bytes, None)
7635            .expect_err("oversized result array output length should fail validation");
7636        assert!(err.contains(
7637            "__quantum__rt__result_array_record_output requires an array length that fits in i32 for RESULT_ARRAY output"
7638        ));
7639    }
7640
7641    #[test]
7642    fn test_validate_dynamic_result_array_record_output_i32_max_length_succeeds() {
7643        let ll_text = r#"
7644@0 = internal constant [3 x i8] c"a0\00"
7645
7646define i64 @Entry_Point_Name() #0 {
7647entry:
7648  %results = alloca [2147483647 x ptr], align 8
7649  call void @__quantum__rt__result_array_record_output(i64 2147483647, ptr %results, ptr @0)
7650  ret i64 0
7651}
7652
7653declare void @__quantum__rt__result_array_record_output(i64, ptr, ptr)
7654
7655attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" }
7656
7657!llvm.module.flags = !{!0, !1, !2, !3, !4}
7658!0 = !{i32 1, !"qir_major_version", i32 2}
7659!1 = !{i32 7, !"qir_minor_version", i32 0}
7660!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
7661!3 = !{i32 1, !"dynamic_result_management", i1 true}
7662!4 = !{i32 1, !"arrays", i1 true}
7663"#;
7664        let bc_bytes = qir_ll_to_bc(ll_text).unwrap();
7665        validate_qir(&bc_bytes, None).expect("i32::MAX result array output should validate");
7666    }
7667
7668    #[test]
7669    fn test_validate_dynamic_result_allocate_outside_entry_block_fails() {
7670        let ll_text = r#"
7671define i64 @Entry_Point_Name() #0 {
7672entry:
7673  br label %body
7674
7675body:
7676  %r = call ptr @__quantum__rt__result_allocate(ptr null)
7677  call void @__quantum__rt__result_release(ptr %r)
7678  ret i64 0
7679}
7680
7681declare ptr @__quantum__rt__result_allocate(ptr)
7682declare void @__quantum__rt__result_release(ptr)
7683
7684attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" }
7685
7686!llvm.module.flags = !{!0, !1, !2, !3, !4}
7687!0 = !{i32 1, !"qir_major_version", i32 2}
7688!1 = !{i32 7, !"qir_minor_version", i32 0}
7689!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
7690!3 = !{i32 1, !"dynamic_result_management", i1 true}
7691!4 = !{i32 1, !"arrays", i1 false}
7692"#;
7693        let bc_bytes = qir_ll_to_bc(ll_text).unwrap();
7694        let err = validate_qir(&bc_bytes, None).expect_err("fixture should fail validation");
7695        assert!(
7696            err.contains("__quantum__rt__result_allocate is only supported in the entry block")
7697        );
7698    }
7699
7700    #[test]
7701    fn test_validate_dynamic_result_allocate_in_helper_fails() {
7702        let ll_text = r#"
7703define void @helper() {
7704entry:
7705  %r = call ptr @__quantum__rt__result_allocate(ptr null)
7706  call void @__quantum__rt__result_release(ptr %r)
7707  ret void
7708}
7709
7710define i64 @Entry_Point_Name() #0 {
7711entry:
7712  call void @helper()
7713  ret i64 0
7714}
7715
7716declare ptr @__quantum__rt__result_allocate(ptr)
7717declare void @__quantum__rt__result_release(ptr)
7718
7719attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" }
7720
7721!llvm.module.flags = !{!0, !1, !2, !3, !4}
7722!0 = !{i32 1, !"qir_major_version", i32 2}
7723!1 = !{i32 7, !"qir_minor_version", i32 0}
7724!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
7725!3 = !{i32 1, !"dynamic_result_management", i1 true}
7726!4 = !{i32 1, !"arrays", i1 false}
7727"#;
7728        let bc_bytes = qir_ll_to_bc(ll_text).unwrap();
7729        let err = validate_qir(&bc_bytes, None).expect_err("fixture should fail validation");
7730        assert!(
7731            err.contains("__quantum__rt__result_allocate is only supported in the entry block")
7732        );
7733    }
7734
7735    #[test]
7736    fn test_validate_input_defined_qir_qis_helper_fails() {
7737        let ll_text = r#"
7738define ptr @qir_qis.qubit_allocate(ptr %out_err) {
7739entry:
7740  ret ptr null
7741}
7742
7743define i64 @Entry_Point_Name() #0 {
7744entry:
7745  ret i64 0
7746}
7747
7748attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" "required_num_results"="1" }
7749
7750!llvm.module.flags = !{!0, !1, !2, !3, !4}
7751!0 = !{i32 1, !"qir_major_version", i32 2}
7752!1 = !{i32 7, !"qir_minor_version", i32 0}
7753!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
7754!3 = !{i32 1, !"dynamic_result_management", i1 false}
7755!4 = !{i32 1, !"arrays", i1 false}
7756"#;
7757        let bc_bytes = qir_ll_to_bc(ll_text).unwrap();
7758        let err = validate_qir(&bc_bytes, None).expect_err("fixture should fail validation");
7759        assert!(err.contains("Input QIR must not define internal helper function"));
7760        assert!(err.contains("qir_qis.qubit_allocate"));
7761    }
7762
7763    #[test]
7764    fn test_validate_dynamic_qubit_array_allocate_length_mismatch_fails() {
7765        let ll_text = r#"
7766define i64 @Entry_Point_Name() #0 {
7767entry:
7768  %qubits = alloca [1 x ptr], align 8
7769  call void @__quantum__rt__qubit_array_allocate(i64 2, ptr %qubits, ptr null)
7770  ret i64 0
7771}
7772
7773declare void @__quantum__rt__qubit_array_allocate(i64, ptr, ptr)
7774
7775attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_results"="1" }
7776
7777!llvm.module.flags = !{!0, !1, !2, !3, !4}
7778!0 = !{i32 1, !"qir_major_version", i32 2}
7779!1 = !{i32 7, !"qir_minor_version", i32 0}
7780!2 = !{i32 1, !"dynamic_qubit_management", i1 true}
7781!3 = !{i32 1, !"dynamic_result_management", i1 false}
7782!4 = !{i32 1, !"arrays", i1 true}
7783"#;
7784        let bc_bytes = qir_ll_to_bc(ll_text).unwrap();
7785        let err = validate_qir(&bc_bytes, None).expect_err("fixture should fail validation");
7786        assert!(
7787            err.contains("__quantum__rt__qubit_array_allocate requires a fixed-size backing array")
7788        );
7789        assert!(err.contains("requested length 2 does not match backing array length 1"));
7790    }
7791
7792    #[test]
7793    fn test_validate_dynamic_qubit_array_allocate_malformed_signature_fails_without_panic() {
7794        let ll_text = r#"
7795define i64 @Entry_Point_Name() #0 {
7796entry:
7797  call void @__quantum__rt__qubit_array_allocate(i64 2)
7798  ret i64 0
7799}
7800
7801declare void @__quantum__rt__qubit_array_allocate(i64)
7802
7803attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_results"="1" }
7804
7805!llvm.module.flags = !{!0, !1, !2, !3, !4}
7806!0 = !{i32 1, !"qir_major_version", i32 2}
7807!1 = !{i32 7, !"qir_minor_version", i32 0}
7808!2 = !{i32 1, !"dynamic_qubit_management", i1 true}
7809!3 = !{i32 1, !"dynamic_result_management", i1 false}
7810!4 = !{i32 1, !"arrays", i1 true}
7811"#;
7812        let bc_bytes = qir_ll_to_bc(ll_text).unwrap();
7813        let err = validate_qir(&bc_bytes, None).expect_err("fixture should fail validation");
7814        assert!(err.contains(
7815            "Malformed QIR RT function declaration: __quantum__rt__qubit_array_allocate"
7816        ));
7817        assert!(err.contains(
7818            "__quantum__rt__qubit_array_allocate requires a constant array length and backing array pointer"
7819        ));
7820    }
7821
7822    #[test]
7823    fn test_validate_dynamic_result_array_allocate_pointer_length_fails_without_panic() {
7824        let ll_text = r#"
7825define i64 @Entry_Point_Name() #0 {
7826entry:
7827  %len = alloca ptr, align 8
7828  %results = alloca [2 x ptr], align 8
7829  call void @__quantum__rt__result_array_allocate(ptr %len, ptr %results, ptr null)
7830  ret i64 0
7831}
7832
7833declare void @__quantum__rt__result_array_allocate(ptr, ptr, ptr)
7834
7835attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" }
7836
7837!llvm.module.flags = !{!0, !1, !2, !3, !4}
7838!0 = !{i32 1, !"qir_major_version", i32 2}
7839!1 = !{i32 7, !"qir_minor_version", i32 0}
7840!2 = !{i32 1, !"dynamic_qubit_management", i1 true}
7841!3 = !{i32 1, !"dynamic_result_management", i1 true}
7842!4 = !{i32 1, !"arrays", i1 true}
7843"#;
7844        let bc_bytes = qir_ll_to_bc(ll_text).unwrap();
7845        let err = validate_qir(&bc_bytes, None).expect_err("fixture should fail validation");
7846        assert!(err.contains(
7847            "__quantum__rt__result_array_allocate requires a constant array length and backing array pointer"
7848        ));
7849    }
7850
7851    #[test]
7852    fn test_validate_dynamic_result_array_allocate_length_mismatch_fails() {
7853        let ll_text = r#"
7854define i64 @Entry_Point_Name() #0 {
7855entry:
7856  %results = alloca [1 x ptr], align 8
7857  call void @__quantum__rt__result_array_allocate(i64 2, ptr %results, ptr null)
7858  ret i64 0
7859}
7860
7861declare void @__quantum__rt__result_array_allocate(i64, ptr, ptr)
7862
7863attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" }
7864
7865!llvm.module.flags = !{!0, !1, !2, !3, !4}
7866!0 = !{i32 1, !"qir_major_version", i32 2}
7867!1 = !{i32 7, !"qir_minor_version", i32 0}
7868!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
7869!3 = !{i32 1, !"dynamic_result_management", i1 true}
7870!4 = !{i32 1, !"arrays", i1 true}
7871"#;
7872        let bc_bytes = qir_ll_to_bc(ll_text).unwrap();
7873        let err = validate_qir(&bc_bytes, None).expect_err("fixture should fail validation");
7874        assert!(
7875            err.contains(
7876                "__quantum__rt__result_array_allocate requires a fixed-size backing array"
7877            )
7878        );
7879        assert!(err.contains("requested length 2 does not match backing array length 1"));
7880    }
7881
7882    #[test]
7883    fn test_validate_dynamic_qubit_array_release_length_mismatch_fails() {
7884        let ll_text = r#"
7885define i64 @Entry_Point_Name() #0 {
7886entry:
7887  %qubits = alloca [1 x ptr], align 8
7888  call void @__quantum__rt__qubit_array_release(i64 2, ptr %qubits)
7889  ret i64 0
7890}
7891
7892declare void @__quantum__rt__qubit_array_release(i64, ptr)
7893
7894attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_results"="1" }
7895
7896!llvm.module.flags = !{!0, !1, !2, !3, !4}
7897!0 = !{i32 1, !"qir_major_version", i32 2}
7898!1 = !{i32 7, !"qir_minor_version", i32 0}
7899!2 = !{i32 1, !"dynamic_qubit_management", i1 true}
7900!3 = !{i32 1, !"dynamic_result_management", i1 false}
7901!4 = !{i32 1, !"arrays", i1 true}
7902"#;
7903        let bc_bytes = qir_ll_to_bc(ll_text).unwrap();
7904        let err = validate_qir(&bc_bytes, None).expect_err("fixture should fail validation");
7905        assert!(
7906            err.contains("__quantum__rt__qubit_array_release requires a fixed-size backing array")
7907        );
7908        assert!(err.contains("requested length 2 does not match backing array length 1"));
7909    }
7910
7911    #[test]
7912    fn test_validate_dynamic_result_array_release_length_mismatch_fails() {
7913        let ll_text = r#"
7914define i64 @Entry_Point_Name() #0 {
7915entry:
7916  %results = alloca [1 x ptr], align 8
7917  call void @__quantum__rt__result_array_release(i64 2, ptr %results)
7918  ret i64 0
7919}
7920
7921declare void @__quantum__rt__result_array_release(i64, ptr)
7922
7923attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1" }
7924
7925!llvm.module.flags = !{!0, !1, !2, !3, !4}
7926!0 = !{i32 1, !"qir_major_version", i32 2}
7927!1 = !{i32 7, !"qir_minor_version", i32 0}
7928!2 = !{i32 1, !"dynamic_qubit_management", i1 false}
7929!3 = !{i32 1, !"dynamic_result_management", i1 true}
7930!4 = !{i32 1, !"arrays", i1 true}
7931"#;
7932        let bc_bytes = qir_ll_to_bc(ll_text).unwrap();
7933        let err = validate_qir(&bc_bytes, None).expect_err("fixture should fail validation");
7934        assert!(
7935            err.contains("__quantum__rt__result_array_release requires a fixed-size backing array")
7936        );
7937        assert!(err.contains("requested length 2 does not match backing array length 1"));
7938    }
7939
7940    #[test]
7941    fn test_validate_dynamic_qubit_array_release_matching_backing_succeeds() {
7942        let ll_text = r"
7943define i64 @Entry_Point_Name() {
7944entry:
7945  %qubits = alloca [2 x ptr], align 8
7946  call void @__quantum__rt__qubit_array_release(i64 2, ptr %qubits)
7947  ret i64 0
7948}
7949
7950declare void @__quantum__rt__qubit_array_release(i64, ptr)
7951";
7952
7953        let errors = validate_dynamic_array_backing_errors(ll_text);
7954        assert!(
7955            errors.is_empty(),
7956            "matching two-argument array release should not report backing errors: {errors:?}"
7957        );
7958    }
7959
7960    #[test]
7961    fn test_validate_dynamic_qubit_array_release_missing_backing_fails_without_panic() {
7962        let ll_text = r"
7963define i64 @Entry_Point_Name() {
7964entry:
7965  call void @__quantum__rt__qubit_array_release(i64 2)
7966  ret i64 0
7967}
7968
7969declare void @__quantum__rt__qubit_array_release(i64)
7970";
7971
7972        let errors = validate_dynamic_array_backing_errors(ll_text);
7973        assert_eq!(
7974            errors,
7975            vec![
7976                "__quantum__rt__qubit_array_release requires a constant array length and backing array pointer"
7977                    .to_string()
7978            ]
7979        );
7980    }
7981
7982    #[test]
7983    fn test_validate_dynamic_qubit_array_allocate_bitcast_backing_succeeds() {
7984        let ll_text = r#"
7985define i64 @Entry_Point_Name() #0 {
7986entry:
7987  %qubits = alloca [2 x ptr], align 8
7988  %backing = bitcast ptr %qubits to ptr
7989  call void @__quantum__rt__qubit_array_allocate(i64 2, ptr %backing, ptr null)
7990  ret i64 0
7991}
7992
7993declare void @__quantum__rt__qubit_array_allocate(i64, ptr, ptr)
7994
7995attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_results"="1" }
7996
7997!llvm.module.flags = !{!0, !1, !2, !3, !4}
7998!0 = !{i32 1, !"qir_major_version", i32 2}
7999!1 = !{i32 7, !"qir_minor_version", i32 0}
8000!2 = !{i32 1, !"dynamic_qubit_management", i1 true}
8001!3 = !{i32 1, !"dynamic_result_management", i1 false}
8002!4 = !{i32 1, !"arrays", i1 true}
8003"#;
8004        let bc_bytes = qir_ll_to_bc(ll_text).unwrap();
8005        validate_qir(&bc_bytes, None).expect("bitcast-backed qubit array should validate");
8006    }
8007
8008    #[test]
8009    fn test_validate_dynamic_qubit_array_allocate_zero_gep_backing_succeeds() {
8010        let ll_text = r#"
8011define i64 @Entry_Point_Name() #0 {
8012entry:
8013  %qubits = alloca [2 x ptr], align 8
8014  %backing = getelementptr inbounds [2 x ptr], ptr %qubits, i64 0, i64 0
8015  call void @__quantum__rt__qubit_array_allocate(i64 2, ptr %backing, ptr null)
8016  ret i64 0
8017}
8018
8019declare void @__quantum__rt__qubit_array_allocate(i64, ptr, ptr)
8020
8021attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_results"="1" }
8022
8023!llvm.module.flags = !{!0, !1, !2, !3, !4}
8024!0 = !{i32 1, !"qir_major_version", i32 2}
8025!1 = !{i32 7, !"qir_minor_version", i32 0}
8026!2 = !{i32 1, !"dynamic_qubit_management", i1 true}
8027!3 = !{i32 1, !"dynamic_result_management", i1 false}
8028!4 = !{i32 1, !"arrays", i1 true}
8029"#;
8030        let bc_bytes = qir_ll_to_bc(ll_text).unwrap();
8031        validate_qir(&bc_bytes, None).expect("zero-index GEP backing should validate");
8032    }
8033
8034    #[test]
8035    fn test_validate_dynamic_qubit_array_allocate_nonzero_gep_backing_fails() {
8036        let ll_text = r#"
8037define i64 @Entry_Point_Name() #0 {
8038entry:
8039  %qubits = alloca [2 x ptr], align 8
8040  %backing = getelementptr inbounds [2 x ptr], ptr %qubits, i64 0, i64 1
8041  call void @__quantum__rt__qubit_array_allocate(i64 2, ptr %backing, ptr null)
8042  ret i64 0
8043}
8044
8045declare void @__quantum__rt__qubit_array_allocate(i64, ptr, ptr)
8046
8047attributes #0 = { "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_results"="1" }
8048
8049!llvm.module.flags = !{!0, !1, !2, !3, !4}
8050!0 = !{i32 1, !"qir_major_version", i32 2}
8051!1 = !{i32 7, !"qir_minor_version", i32 0}
8052!2 = !{i32 1, !"dynamic_qubit_management", i1 true}
8053!3 = !{i32 1, !"dynamic_result_management", i1 false}
8054!4 = !{i32 1, !"arrays", i1 true}
8055"#;
8056        let bc_bytes = qir_ll_to_bc(ll_text).unwrap();
8057        let err = validate_qir(&bc_bytes, None).expect_err(
8058            "non-zero GEP-backed pointer should not count as a fixed-size array backing",
8059        );
8060        assert!(err.contains(
8061            "__quantum__rt__qubit_array_allocate requires a fixed-size backing array allocated as [N x ptr]"
8062        ));
8063    }
8064
8065    #[test]
8066    fn test_dynamic_qubit_array_allocate_rolls_back_on_failure() {
8067        let ll_text = std::fs::read_to_string("tests/data/dynamic_qubit_array_checked.ll")
8068            .expect("Failed to read dynamic_qubit_array_checked.ll");
8069        let input_bc =
8070            qir_ll_to_bc(&ll_text).expect("Failed to convert dynamic_qubit_array_checked.ll");
8071        let output_bc = qir_to_qis(&input_bc, 0, "native", None)
8072            .expect("dynamic qubit array checked fixture should compile");
8073
8074        let ctx = Context::create();
8075        let module = parse_bitcode_module(&ctx, &output_bc, "qis_module")
8076            .expect("Compiled QIS bitcode should parse");
8077        let helper = module
8078            .get_function("qir_qis.qubit_array_allocate")
8079            .expect("qubit array allocate helper should exist");
8080        let called_functions = collect_called_function_names(helper);
8081        assert!(
8082            called_functions
8083                .iter()
8084                .any(|name| name == "qir_qis.qubit_array_release"),
8085            "expected rollback release path in helper, got calls: {called_functions:?}"
8086        );
8087    }
8088
8089    #[test]
8090    fn test_dynamic_qubit_array_allocate_initializes_out_err() {
8091        let ll_text = std::fs::read_to_string("tests/data/dynamic_qubit_array_checked.ll")
8092            .expect("Failed to read dynamic_qubit_array_checked.ll");
8093        let input_bc =
8094            qir_ll_to_bc(&ll_text).expect("Failed to convert dynamic_qubit_array_checked.ll");
8095        let output_bc = qir_to_qis(&input_bc, 0, "native", None)
8096            .expect("dynamic qubit array checked fixture should compile");
8097
8098        let ctx = Context::create();
8099        let module = parse_bitcode_module(&ctx, &output_bc, "qis_module")
8100            .expect("Compiled QIS bitcode should parse");
8101        let helper = module
8102            .get_function("qir_qis.qubit_array_allocate")
8103            .expect("qubit array allocate helper should exist");
8104        let called_functions = collect_called_function_names(helper);
8105        assert!(
8106            called_functions
8107                .iter()
8108                .any(|name| name == "qir_qis.out_err_success"),
8109            "expected helper to initialize out_err, got calls: {called_functions:?}"
8110        );
8111    }
8112
8113    #[test]
8114    fn test_dynamic_feature_fixtures_translate_to_verifiable_qis() {
8115        let (opt_level, target) = conservative_translation_settings();
8116        for fixture in DYNAMIC_FEATURE_FIXTURES {
8117            let ll_text = std::fs::read_to_string(fixture).expect("Failed to read fixture");
8118            let input_bc = qir_ll_to_bc(&ll_text).expect("Failed to convert fixture to bitcode");
8119            validate_qir(&input_bc, None).expect("Dynamic fixture should validate");
8120            let output_bc = qir_to_qis(&input_bc, opt_level, target, None)
8121                .expect("Dynamic fixture should translate");
8122            verify_bitcode_module(&output_bc, fixture)
8123                .expect("Dynamic fixture should remain LLVM-verifiable");
8124        }
8125    }
8126
8127    proptest! {
8128        #[test]
8129        fn prop_dynamic_result_array_record_output_preserves_label(
8130            label in "[A-Za-z0-9_]{1,8}",
8131        ) {
8132            let label_len = label
8133                .len()
8134                .checked_add(1)
8135                .expect("label length bound should leave room for null terminator");
8136            let ll_text = format!(
8137                r#"
8138@0 = internal constant [{label_len} x i8] c"{label}\00"
8139
8140define i64 @Entry_Point_Name() #0 {{
8141entry:
8142  %results = alloca [2 x ptr], align 8
8143  call void @__quantum__rt__result_array_allocate(i64 2, ptr %results, ptr null)
8144  %r0_ptr = getelementptr inbounds [2 x ptr], ptr %results, i64 0, i64 0
8145  %r0 = load ptr, ptr %r0_ptr, align 8
8146  %r1_ptr = getelementptr inbounds [2 x ptr], ptr %results, i64 0, i64 1
8147  %r1 = load ptr, ptr %r1_ptr, align 8
8148  call void @__quantum__qis__mz__body(ptr null, ptr %r0)
8149  call void @__quantum__qis__mz__body(ptr inttoptr (i64 1 to ptr), ptr %r1)
8150  call void @__quantum__rt__result_array_record_output(i64 2, ptr %results, ptr @0)
8151  call void @__quantum__rt__result_array_release(i64 2, ptr %results)
8152  ret i64 0
8153}}
8154
8155declare void @__quantum__rt__result_array_allocate(i64, ptr, ptr)
8156declare void @__quantum__rt__result_array_release(i64, ptr)
8157declare void @__quantum__rt__result_array_record_output(i64, ptr, ptr)
8158declare void @__quantum__qis__mz__body(ptr, ptr writeonly) #1
8159
8160attributes #0 = {{ "entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="2" }}
8161attributes #1 = {{ "irreversible" }}
8162
8163!llvm.module.flags = !{{!0, !1, !2, !3, !4}}
8164!0 = !{{i32 1, !"qir_major_version", i32 2}}
8165!1 = !{{i32 7, !"qir_minor_version", i32 0}}
8166!2 = !{{i32 1, !"dynamic_qubit_management", i1 false}}
8167!3 = !{{i32 1, !"dynamic_result_management", i1 true}}
8168!4 = !{{i32 1, !"arrays", i1 true}}
8169"#
8170            );
8171
8172            let bc_bytes = qir_ll_to_bc(&ll_text)
8173                .map_err(|err| TestCaseError::fail(format!("Failed to lower inline IR: {err}")))?;
8174            validate_qir(&bc_bytes, None)
8175                .map_err(|err| TestCaseError::fail(format!("Fixture should validate: {err}")))?;
8176            let qis_bytes = qir_to_qis(&bc_bytes, 0, "native", None)
8177                .map_err(|err| TestCaseError::fail(format!("Fixture should compile: {err}")))?;
8178            let ctx = Context::create();
8179            let module = parse_bitcode_module(&ctx, &qis_bytes, "qis_module")
8180                .map_err(|err| TestCaseError::fail(format!("Compiled module should parse: {err}")))?;
8181            let expected_label = format!("USER:RESULT_ARRAY:{label}");
8182            let labels = module
8183                .get_globals()
8184                .filter_map(|global| get_string_label(global).ok())
8185                .collect::<Vec<_>>();
8186
8187            prop_assert!(
8188                labels.iter().any(|item| item.contains(&expected_label)),
8189                "expected label payload {expected_label}, got globals: {labels:?}"
8190            );
8191        }
8192
8193        #[test]
8194        fn prop_dynamic_array_allocate_requires_matching_fixed_backing(
8195            backing_len in 1u32..4u32,
8196            requested_len in 1u32..4u32,
8197            is_result_array in any::<bool>(),
8198        ) {
8199            let (rt_decl, call, attrs, flags) = if is_result_array {
8200                (
8201                    "declare void @__quantum__rt__result_array_allocate(i64, ptr, ptr)",
8202                    format!(
8203                        "  %results = alloca [{backing_len} x ptr], align 8\n  call void @__quantum__rt__result_array_allocate(i64 {requested_len}, ptr %results, ptr null)"
8204                    ),
8205                    r#""entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1""#,
8206                    "!2 = !{i32 1, !\"dynamic_qubit_management\", i1 false}\n!3 = !{i32 1, !\"dynamic_result_management\", i1 true}\n!4 = !{i32 1, !\"arrays\", i1 true}",
8207                )
8208            } else {
8209                (
8210                    "declare void @__quantum__rt__qubit_array_allocate(i64, ptr, ptr)",
8211                    format!(
8212                        "  %qubits = alloca [{backing_len} x ptr], align 8\n  call void @__quantum__rt__qubit_array_allocate(i64 {requested_len}, ptr %qubits, ptr null)"
8213                    ),
8214                    r#""entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_results"="1""#,
8215                    "!2 = !{i32 1, !\"dynamic_qubit_management\", i1 true}\n!3 = !{i32 1, !\"dynamic_result_management\", i1 false}\n!4 = !{i32 1, !\"arrays\", i1 true}",
8216                )
8217            };
8218
8219            let ll_text = format!(
8220                r#"
8221define i64 @Entry_Point_Name() #0 {{
8222entry:
8223{call}
8224  ret i64 0
8225}}
8226
8227{rt_decl}
8228
8229attributes #0 = {{ {attrs} }}
8230
8231!llvm.module.flags = !{{!0, !1, !2, !3, !4}}
8232!0 = !{{i32 1, !"qir_major_version", i32 2}}
8233!1 = !{{i32 7, !"qir_minor_version", i32 0}}
8234{flags}
8235"#
8236            );
8237            let bc = qir_ll_to_bc(&ll_text)
8238                .map_err(|err| TestCaseError::fail(format!("inline IR should parse: {err}")))?;
8239            let result = validate_qir(&bc, None);
8240            if backing_len == requested_len {
8241                prop_assert!(result.is_ok());
8242            } else {
8243                let err = result.expect_err("mismatched fixed-size backing should fail");
8244                prop_assert!(err.contains("requires a fixed-size backing array"));
8245                prop_assert!(err.contains("requested length"));
8246            }
8247        }
8248
8249        #[test]
8250        fn prop_dynamic_array_runtime_calls_require_both_arrays_and_matching_dynamic_flag(
8251            arrays_enabled in any::<bool>(),
8252            dynamic_enabled in any::<bool>(),
8253            is_result_array in any::<bool>(),
8254        ) {
8255            let (call, rt_decl, attrs, flags, expected_error) = if is_result_array {
8256                (
8257                    "  %results = alloca [2 x ptr], align 8\n  call void @__quantum__rt__result_array_allocate(i64 2, ptr %results, ptr null)",
8258                    "declare void @__quantum__rt__result_array_allocate(i64, ptr, ptr)",
8259                    r#""entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_qubits"="1""#.to_string(),
8260                    format!(
8261                        "!2 = !{{i32 1, !\"dynamic_qubit_management\", i1 false}}\n!3 = !{{i32 1, !\"dynamic_result_management\", i1 {dynamic_enabled}}}\n!4 = !{{i32 1, !\"arrays\", i1 {arrays_enabled}}}"
8262                    ),
8263                    "__quantum__rt__result_array_allocate requires both `arrays=true` and `dynamic_result_management=true`",
8264                )
8265            } else {
8266                (
8267                    "  %qubits = alloca [2 x ptr], align 8\n  call void @__quantum__rt__qubit_array_allocate(i64 2, ptr %qubits, ptr null)",
8268                    "declare void @__quantum__rt__qubit_array_allocate(i64, ptr, ptr)",
8269                    r#""entry_point" "qir_profiles"="adaptive_profile" "output_labeling_schema"="schema_id" "required_num_results"="1""#.to_string(),
8270                    format!(
8271                        "!2 = !{{i32 1, !\"dynamic_qubit_management\", i1 {dynamic_enabled}}}\n!3 = !{{i32 1, !\"dynamic_result_management\", i1 false}}\n!4 = !{{i32 1, !\"arrays\", i1 {arrays_enabled}}}"
8272                    ),
8273                    "__quantum__rt__qubit_array_allocate requires both `arrays=true` and `dynamic_qubit_management=true`",
8274                )
8275            };
8276
8277            let ll_text = format!(
8278                r#"
8279define i64 @Entry_Point_Name() #0 {{
8280entry:
8281{call}
8282  ret i64 0
8283}}
8284
8285{rt_decl}
8286
8287attributes #0 = {{ {attrs} }}
8288
8289!llvm.module.flags = !{{!0, !1, !2, !3, !4}}
8290!0 = !{{i32 1, !"qir_major_version", i32 2}}
8291!1 = !{{i32 7, !"qir_minor_version", i32 0}}
8292{flags}
8293"#
8294            );
8295            let bc = qir_ll_to_bc(&ll_text)
8296                .map_err(|err| TestCaseError::fail(format!("inline IR should parse: {err}")))?;
8297            let result = validate_qir(&bc, None);
8298            if arrays_enabled && dynamic_enabled {
8299                prop_assert!(result.is_ok());
8300            } else {
8301                let err = result.expect_err("missing capability flag combination should fail");
8302                prop_assert!(err.contains(expected_error));
8303            }
8304        }
8305    }
8306}