Skip to content

Support mutable fixed size arrays in Adaptive Profile QIR emission - #3706

Open
Stefan J. Wernli (swernli) wants to merge 1 commit into
mainfrom
swernli/mutable-arrays
Open

Stefan J. Wernli (swernli) wants to merge 1 commit into
mainfrom
swernli/mutable-arrays

Conversation

@swernli

Copy link
Copy Markdown
Contributor

This change adds support for mutable fixed size arrays in Adaptive Profile. It adds new RIR instructions to correspond to array copy, slice, and store index instructions and updates Partial Evaluation to emit these instructions when needed. To minimize extraneous propagation of variables and only emit the new instructions when required by the program, the implementation carefully tracks the usage of mutable fixed size arrays throughout the program, the change also updates Runtime Capabilities Analysis to mark those arrays who are dynamically updated and propagate those results as part of analysis. This allows Partial Eval to identify these arrays when they are initialized and emit the expected store instructions, tracking the resulting variable rather than the array contents (since emitting at first array update is too late). The new instructions generate multiple lines of QIR each, rather than emitting to a loop, to preserve block linearity and avoid introducing new control flow where it is unexpected. Changes are validated in Partial Eval and Codegen unit tests, as well as Python integration testing.

This change adds support for mutable fixed size arrays in Adaptive Profile. It adds new RIR instructions to correspond to array copy, slice, and store index instructions and updates Partial Evaluation to emit these instructions when needed. To minimize extraneous propagation of variables and only emit the new instructions when required by the program, the implementation carefully tracks the usage of mutable fixed size arrays throughout the program, the change also updates Runtime Capabilities Analysis to mark those arrays who are dynamically updated and propagate those results as part of analysis. This allows Partial Eval to identify these arrays when they are initialized and emit the expected store instructions, tracking the resulting variable rather than the array contents (since emitting at first array update is too late). The new instructions generate multiple lines of QIR each, rather than emitting to a loop, to preserve block linearity and avoid introducing new control flow where it is unexpected. Changes are validated in Partial Eval and Codegen unit tests, as well as Python integration testing.
@swernli

Copy link
Copy Markdown
Contributor Author

As an example of code that this change now supports, consider:

@EntryPoint(Adaptive)
operation Main() : Bool[] {
    use q = Qubit[2];
    mutable redo = true;
    mutable results = [false, false];
    mutable loop_count = 0;
    repeat {
        // Psuedo "X with noise" operation
        Rx(2.5, q[0]);
        Rx(2.5, q[1]);
        let r0 = MResetZ(q[0]);
        let r1 = MResetZ(q[1]);
        results[0] = not IsLossResult(r0) and r0 == One;
        results[1] = not IsLossResult(r1) and r1 == One;
        loop_count += 1;
    } until (results[0] and results[1]) or loop_count > 100;
    results
}

This used to fail because it involved mutating the array of Bool values:

image

It now produces valid Adaptive QIR:

image

@amcasey

Andrew Casey (amcasey) commented Sep 14, 2026

Copy link
Copy Markdown
Member
@EntryPoint(Adaptive)
operation Main() : Int {
    mutable values = [0];

    if 2 < 3 {
        values[0] = 136;
    }

    use qubit = Qubit();
    H(qubit);
    if MResetZ(qubit) == One {
        values[0] += 1;
    }

    return values[0];
}

I would expect this to evaluate to either 136 or 137, but it actually evaluates to 0 or 137 - looks like the static if is getting dropped.

Quoth Copilot:

Mutable fixed-size array updates nested inside statically evaluated control flow are lost during Adaptive QIR emission.

In static_if_array_repro.qs, the static branch sets values[0] = 136. The compiler remembers this value when folding the later increment, emitting 137 for the measured One branch. However, static_if_array_repro.ll:10-20 initializes the backing array to 0 and never emits the 136 store.

Expected output: 136 or 137
Actual QIR output: 0 or 137

The partial evaluator’s classical and hybrid states become desynchronized. eval_static_expr synchronizes assignments only when the root expression is an assignment; here the root is a static if, so its nested array mutation never reaches emitted QIR.

The synchronization logic predates #3706, but the PR exposes it through newly supported mutable fixed-size arrays and lacks coverage for static control flow followed by dynamic use.

@amcasey

Andrew Casey (amcasey) commented Sep 14, 2026

Copy link
Copy Markdown
Member

I can't generate QIR for this:

@EntryPoint(Adaptive)
operation Main() : Int {
    mutable matrix = [
        [9, 1, 7],
        [3, 5, 2],
        [8, 4, 6]
    ];
    use q = Qubit();
    H(q);
    if (MResetZ(q) == One) {
        matrix[0][1] = 2;
    }
    return matrix[1][2];
}
[error] [wasm] Wasm panic occurred: panicked at source/compiler/qsc_partial_eval/src/lib.rs:4948:18:
[9, 1, 7] cannot be mapped to a RIR operand

@swernli

Copy link
Copy Markdown
Contributor Author

Ooo, both good finds, Andrew Casey (@amcasey)! For the first one, I need to ensure that expressions that include an update of a dynamic, mutable, fixed size array are themselves considered dynamic. Then for the second, at the very least this should be a graceful failure at QIR generation time, but I'd like to look into whether or not this can be detected at analysis time so the user gets feedback sooner that it isn't supported.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants