Skip to content

Treat binding immutable to mutable as copy even when static value present - #3901

Open
Stefan J. Wernli (swernli) wants to merge 2 commits into
swernli/issue3802from
swernli/issue3811
Open

Stefan J. Wernli (swernli) wants to merge 2 commits into
swernli/issue3802from
swernli/issue3811

Conversation

@swernli

@swernli Stefan J. Wernli (swernli) commented Oct 6, 2026 •

Copy link
Copy Markdown
Contributor

Previously we used an optimization to avoid extra store instructions by having a special case around setting an immutable variable to a mutable variable where the latter had a known static mapping. This could incorrectly propagate references to a mutable variable instead of creating a copy (via store). Instead, this avoids trying to optimize in favor of correctness. For Adaptive_RI/RIF this ended up having no effect, as those extra store instructions are not produced due to how loop unrolling worked, but for Adaptive the extra store instructions create some noise in existing QIR via "needless" load, store, load pattern that ends up being correct for creating copies when mutation occurs in a loop.

Note for reviewers: the only code change is a couple lines in source/compiler/qsc_partial_eval/src/lib.rs, the rest are new tests and existing snapshot updates.

Fixes #3811
Fixes #3809


Stack created with GitHub Stacks CLI • Give Feedback 💬

@amcasey

Copy link
Copy Markdown
Member

Remaining scenarios related to #3811

Review of 7428d06c (Treat binding immutable to mutable as copy even when static value present, PR #3901, fixes #3811), compared against its parent b2f49b20.

The commit changes bind_value_to_ident (source/compiler/qsc_partial_eval/src/lib.rs:353) so that let t = <mutable> always makes a point-in-time copy. Previously, when the mutable had a known value, t was bound to the mutable's own RIR variable.

Now correct:

The three scenarios below still produce wrong output. Each was checked by comparing the Q# simulator against qir-runner output for the QIR that each profile produces, at b2f49b20 ("Base") and at 7428d06c ("HEAD"). The one exception is noted under issue 3's Status.

# Scenario Base HEAD Existing issue
1 [x, size = n] holds a reference to the mutable x, not its value Wrong output Wrong output (unfixed) None found
2 Copy-and-update (w/) and update-in-place (w/=) hold a reference to the mutable, not its value Wrong output Wrong output (unfixed) None found
3 Adaptive_RI/RIF: a copy of a mutable, read inside a later runtime branch after the original changes, gives the wrong value Wrong output Wrong output (unfixed) Array-literal form is #3800's "Other shapes" (open); same family as #3769 and #3807 (closed)

Issues 1 and 2 have the same root cause as #3811. The copy added by 7428d06c applies only when the bound value is itself a Value::Var. An array built by repeat or index update keeps the mutable's RIR variable as an element, and binding that array to let does not copy nested elements. Array literals (eval_expr_array, lib.rs:2787) and tuple literals (eval_expr_tuple, lib.rs:2819) already copy each Var element into a fresh variable with a Store; the two paths below do not.

Issue 3 is the same "copy follows the original" symptom, but it comes from the Adaptive_RI/RIF SSA pipeline in qsc_rir, not from partial evaluation. The commit message notes that the change "ended up having no effect" for those profiles.

#3810 (values assigned in a dynamic loop body leak past the loop) is cited in #3811 as the underlying cause and is still open; see issue 4 in issue3802.md. 7428d06c avoids depending on those leaked values for let, but doesn't fix the leak.

Existing issues: I checked open and closed microsoft/qdk issues as of 2026-10-07. #3820 involves w/ but is a panic for a dynamic index, which is unrelated.

All Python repros require pip install qdk qirrunner; use a qdk built from the commit under test. Each repro exits 0 while the bug reproduces.


1. [x, size = n] holds a reference to the mutable x, not its value

Kind: Miscompile (wrong output)
Affects: Adaptive_RI, Adaptive_RIF and Adaptive in straight-line code; only Adaptive inside runtime branches and emitted loops
Status: Pre-existing; not fixed by 7428d06c.
Existing issue: None found. #3800 covers array literals ([n]), which now copy correctly.

Q#

operation Main() : Int {
    use q = Qubit();
    X(q);
    mutable n = 0;
    if MResetZ(q) == One { set n = 2; }
    let arr = [n, size = 1];
    set n = 7;
    arr[0]
}

arr is built when n is 2, so arr[0] is 2.

Outputs (HEAD; Base is identical)

Variant (see the Python repro) Simulator (correct) Adaptive_RI Adaptive_RIF Adaptive
Straight-line (above) 2 7 7 7
Built by function Wrap(x : Int) : Int[] { [x, size = 1] } 2 7 7 7
Inside a runtime branch, where n has a known value (the #3809 shape) 1 1 1 7
Inside an emitted loop (the #3811 shape) 1 1 1 5

Probable cause

eval_expr_array_repeat (lib.rs:1431-1458) builds the array with vec![value; size]. When value is a Value::Var, every element is the mutable's own RIR variable, and nothing copies it later. eval_expr_array copies each such element into a fresh variable; eval_expr_array_repeat should do the same.

In Adaptive_RI/RIF, loops are unrolled, and reading a mutable inside a branch yields its known literal value. eval_expr_var returns the variable itself only when backwards branching is supported (lib.rs:2984). That's why the branch and loop variants fail only on Adaptive.

Python repro

"""An array built with `[x, size = n]` holds a reference to the mutable `x`, not its value.

Compiles each Q# program below to QIR for each Adaptive profile, runs that QIR with
qir-runner, and runs the same program in the Q# simulator for comparison.
Exits 0 while the bug reproduces.

Requires: pip install qdk qirrunner
"""

import os
import re
import sys
import tempfile

import qirrunner
from qdk import Context, TargetProfile

# name -> (Q# source, correct output). In every case the array is built before the mutable changes.
CASES = {
    "straight-line": ("""
operation Main() : Int {
    use q = Qubit();
    X(q);
    mutable n = 0;
    if MResetZ(q) == One { set n = 2; }
    let arr = [n, size = 1];
    set n = 7;
    arr[0]
}
""", "2"),
    "built by a helper function": ("""
function Wrap(x : Int) : Int[] { [x, size = 1] }
operation Main() : Int {
    use q = Qubit();
    X(q);
    mutable n = 0;
    if MResetZ(q) == One { set n = 2; }
    let arr = Wrap(n);
    set n = 7;
    arr[0]
}
""", "2"),
    "inside a runtime branch": ("""
operation Main() : Int {
    use q = Qubit();
    X(q);
    let d = MResetZ(q) == One;
    mutable n = 1;
    if d { set n = 1; }
    mutable r = 0;
    if d {
        let h = [n, size = 1];
        set n = 7;
        set r = h[0];
    }
    r
}
""", "1"),
    "inside an emitted loop": ("""
operation Main() : Int {
    use q = Qubit();
    mutable a = 1;
    mutable r = 0;
    for i in 0..0 {
        X(q);
        let arr = [a, size = 1];
        if i == 7 { X(q); }
        set a = 5;
        set r = arr[0];
    }
    Reset(q);
    r
}
""", "1"),
}

ENTRY = "Main()"
SHOTS = 10
PROFILES = ["Adaptive_RI", "Adaptive_RIF", "Adaptive"]


def fmt(value):
    """Formats a simulator value the same way as qir-runner output below."""
    if value is None:
        return "()"
    if type(value).__name__ == "Result":
        return "1" if str(value).endswith("One") else "0"
    if isinstance(value, bool):
        return str(value).lower()
    if isinstance(value, float):
        return repr(value)
    if isinstance(value, (tuple, list)):
        inner = ", ".join(fmt(v) for v in value)
        return f"({inner})" if isinstance(value, tuple) else f"[{inner}]"
    return str(value)


def parse_shots(text):
    """Turns qir-runner's output records into one formatted value per shot."""
    shots, records = [], None
    for line in text.splitlines():
        fields = line.split("\t")
        if fields[0] == "START":
            records = []
        elif fields[0] == "OUTPUT" and records is not None:
            records.append((fields[1], fields[2]))
        elif fields[0] == "END" and records is not None:
            shots.append(to_text(iter(records)) if records else "()")
            if fields[1] != "0":
                shots[-1] = f"exit code {fields[1]}"
            records = None
    return shots


def to_text(records):
    kind, value = next(records)
    if kind in ("TUPLE", "ARRAY"):
        inner = ", ".join(to_text(records) for _ in range(int(value)))
        return f"({inner})" if kind == "TUPLE" else f"[{inner}]"
    if kind == "DOUBLE":
        return repr(float(value))
    if kind == "BOOL":
        return value.lower()
    return value


def run_qir(qir):
    with tempfile.NamedTemporaryFile("w", suffix=".ll", delete=False) as file:
        file.write(qir)
    handler = qirrunner.OutputHandler()
    try:
        qirrunner.run(file.name, shots=SHOTS, rng_seed=1, output_fn=handler.handle)
    except Exception as error:  # qir-runner rejects the module
        message = str(error).strip().splitlines()[0]
        return {"invalid QIR: " + re.sub(r"^\S+\.ll:\d+:\d+: (error: )?", "", message)}
    finally:
        os.unlink(file.name)
    return set(parse_shots(handler.get_output()))


def compile_qir(source, profile):
    """Returns (qir, None) on success or (None, description) when compilation fails."""
    ctx = Context(target_profile=getattr(TargetProfile, profile))
    try:
        ctx.eval(source)
        return str(ctx.compile(ENTRY)), None
    except (KeyboardInterrupt, SystemExit):
        raise
    except BaseException as error:  # pyo3 surfaces Rust panics as PanicException (a BaseException)
        kind = "COMPILER PANIC" if type(error).__name__ == "PanicException" else "rejected"
        return None, f"{kind}: {str(error).strip().splitlines()[0]}"


def outputs(source):
    rows = {}
    ctx = Context(target_profile=TargetProfile.Unrestricted)
    ctx.eval(source)
    rows["Simulator"] = {fmt(v) for v in ctx.run(ENTRY, SHOTS, seed=1)}
    for profile in PROFILES:
        qir, failure = compile_qir(source, profile)
        rows[profile] = {failure} if failure else run_qir(qir)
    return rows


def show(name, rows, expected):
    print(f"--- {name} (expected {expected})")
    width = max(map(len, rows)) + 2
    for row, values in rows.items():
        print(f"  {row:<{width}}{' | '.join(sorted(values))}")



def main():
    reproduced = []
    for name, (source, expected) in CASES.items():
        rows = outputs(source)
        show(name, rows, expected)
        if any(rows[p] != {expected} for p in PROFILES):
            reproduced.append(name)
    if reproduced:
        print(f"REPRODUCED: {', '.join(reproduced)}")
        return 0
    print("NOT REPRODUCED")
    return 1


if __name__ == "__main__":
    sys.exit(main())

2. Copy-and-update (w/) and update-in-place (w/=) hold a reference to the mutable, not its value

Kind: Miscompile (wrong output)
Affects: Adaptive_RI, Adaptive_RIF and Adaptive in straight-line code; only Adaptive inside runtime branches and emitted loops
Status: Pre-existing; not fixed by 7428d06c.
Existing issue: None found. #3820 is a panic for a dynamic index in w/, which is a different problem.

Q#

operation Main() : Int {
    use q = Qubit();
    X(q);
    mutable n = 0;
    if MResetZ(q) == One { set n = 2; }
    let arr = [0] w/ 0 <- n;
    set n = 7;
    arr[0]
}

arr is built when n is 2, so arr[0] is 2.

Outputs (HEAD; Base is identical)

Variant (see the Python repro) Simulator (correct) Adaptive_RI Adaptive_RIF Adaptive
let arr = [0] w/ 0 <- n; (above) 2 7 7 7
mutable arr = [0]; set arr w/= 0 <- n; 2 7 7 7
w/ in function Wrap(x : Int) : Int[] { [0] w/ 0 <- x } 2 7 7 7
w/ inside a runtime branch, where n has a known value 1 1 1 7
w/ inside an emitted loop 1 1 1 5

set arr w/= 0 <- a inside an emitted loop gave the correct result. Inside a runtime branch, every profile rejects it with UseOfDynamicArray.

Probable cause

eval_array_update_index (lib.rs:563-603) inserts the evaluated update value into the array as-is (update_index_single(array, index, update_value, ...), lib.rs:590). Both w/ (eval_expr_update_index, lib.rs:2944) and w/= (eval_expr_assign_index, lib.rs:1477) go through it. A Value::Var update value should be copied into a fresh variable first, as eval_expr_array does. The branch and loop variants fail only on Adaptive for the reason given in issue 1.

Python repro

"""Copy-and-update (`w/`) and update-in-place (`w/=`) store a reference to the mutable, not its value.

Compiles each Q# program below to QIR for each Adaptive profile, runs that QIR with
qir-runner, and runs the same program in the Q# simulator for comparison.
Exits 0 while the bug reproduces.

Requires: pip install qdk qirrunner
"""

import os
import re
import sys
import tempfile

import qirrunner
from qdk import Context, TargetProfile

STRAIGHT_LINE = """
{prefix}operation Main() : Int {{
    use q = Qubit();
    X(q);
    mutable n = 0;
    if MResetZ(q) == One {{ set n = 2; }}
    {build}
    set n = 7;
    arr[0]
}}
"""
# name -> (Q# source, correct output). In every case the array is updated before the mutable changes.
CASES = {
    "copy-and-update `w/`": (STRAIGHT_LINE.format(prefix="", build="let arr = [0] w/ 0 <- n;"), "2"),
    "update-in-place `w/=`": (STRAIGHT_LINE.format(prefix="", build="mutable arr = [0];\n    set arr w/= 0 <- n;"), "2"),
    "`w/` in a helper function": (STRAIGHT_LINE.format(
        prefix="function Wrap(x : Int) : Int[] { [0] w/ 0 <- x }\n", build="let arr = Wrap(n);"), "2"),
    "`w/` inside a runtime branch": ("""
operation Main() : Int {
    use q = Qubit();
    X(q);
    let d = MResetZ(q) == One;
    mutable n = 1;
    if d { set n = 1; }
    mutable r = 0;
    if d {
        let h = [0] w/ 0 <- n;
        set n = 7;
        set r = h[0];
    }
    r
}
""", "1"),
    "`w/` inside an emitted loop": ("""
operation Main() : Int {
    use q = Qubit();
    mutable a = 1;
    mutable r = 0;
    for i in 0..0 {
        X(q);
        let arr = [0] w/ 0 <- a;
        if i == 7 { X(q); }
        set a = 5;
        set r = arr[0];
    }
    Reset(q);
    r
}
""", "1"),
}

ENTRY = "Main()"
SHOTS = 10
PROFILES = ["Adaptive_RI", "Adaptive_RIF", "Adaptive"]


def fmt(value):
    """Formats a simulator value the same way as qir-runner output below."""
    if value is None:
        return "()"
    if type(value).__name__ == "Result":
        return "1" if str(value).endswith("One") else "0"
    if isinstance(value, bool):
        return str(value).lower()
    if isinstance(value, float):
        return repr(value)
    if isinstance(value, (tuple, list)):
        inner = ", ".join(fmt(v) for v in value)
        return f"({inner})" if isinstance(value, tuple) else f"[{inner}]"
    return str(value)


def parse_shots(text):
    """Turns qir-runner's output records into one formatted value per shot."""
    shots, records = [], None
    for line in text.splitlines():
        fields = line.split("\t")
        if fields[0] == "START":
            records = []
        elif fields[0] == "OUTPUT" and records is not None:
            records.append((fields[1], fields[2]))
        elif fields[0] == "END" and records is not None:
            shots.append(to_text(iter(records)) if records else "()")
            if fields[1] != "0":
                shots[-1] = f"exit code {fields[1]}"
            records = None
    return shots


def to_text(records):
    kind, value = next(records)
    if kind in ("TUPLE", "ARRAY"):
        inner = ", ".join(to_text(records) for _ in range(int(value)))
        return f"({inner})" if kind == "TUPLE" else f"[{inner}]"
    if kind == "DOUBLE":
        return repr(float(value))
    if kind == "BOOL":
        return value.lower()
    return value


def run_qir(qir):
    with tempfile.NamedTemporaryFile("w", suffix=".ll", delete=False) as file:
        file.write(qir)
    handler = qirrunner.OutputHandler()
    try:
        qirrunner.run(file.name, shots=SHOTS, rng_seed=1, output_fn=handler.handle)
    except Exception as error:  # qir-runner rejects the module
        message = str(error).strip().splitlines()[0]
        return {"invalid QIR: " + re.sub(r"^\S+\.ll:\d+:\d+: (error: )?", "", message)}
    finally:
        os.unlink(file.name)
    return set(parse_shots(handler.get_output()))


def compile_qir(source, profile):
    """Returns (qir, None) on success or (None, description) when compilation fails."""
    ctx = Context(target_profile=getattr(TargetProfile, profile))
    try:
        ctx.eval(source)
        return str(ctx.compile(ENTRY)), None
    except (KeyboardInterrupt, SystemExit):
        raise
    except BaseException as error:  # pyo3 surfaces Rust panics as PanicException (a BaseException)
        kind = "COMPILER PANIC" if type(error).__name__ == "PanicException" else "rejected"
        return None, f"{kind}: {str(error).strip().splitlines()[0]}"


def outputs(source):
    rows = {}
    ctx = Context(target_profile=TargetProfile.Unrestricted)
    ctx.eval(source)
    rows["Simulator"] = {fmt(v) for v in ctx.run(ENTRY, SHOTS, seed=1)}
    for profile in PROFILES:
        qir, failure = compile_qir(source, profile)
        rows[profile] = {failure} if failure else run_qir(qir)
    return rows


def show(name, rows, expected):
    print(f"--- {name} (expected {expected})")
    width = max(map(len, rows)) + 2
    for row, values in rows.items():
        print(f"  {row:<{width}}{' | '.join(sorted(values))}")



def main():
    reproduced = []
    for name, (source, expected) in CASES.items():
        rows = outputs(source)
        show(name, rows, expected)
        if any(rows[p] != {expected} for p in PROFILES):
            reproduced.append(name)
    if reproduced:
        print(f"REPRODUCED: {', '.join(reproduced)}")
        return 0
    print("NOT REPRODUCED")
    return 1


if __name__ == "__main__":
    sys.exit(main())

3. Adaptive_RI/RIF: a copy of a mutable, read inside a later runtime branch, loses its value

Kind: Miscompile (wrong output)
Affects: Adaptive_RI, Adaptive_RIF. Adaptive, which doesn't use the SSA pipeline, is correct.
Status: Pre-existing; not fixed by 7428d06c. I verified the array-literal form at Base. The let and mutable forms take code paths that this commit doesn't change, because n has no known value at the copy.
Existing issue: The array-literal form is #3800's "Other shapes" (open). #3800 reported 2 on every profile; now Adaptive is correct and Adaptive_RI/RIF return 0. The let and mutable forms are not filed. This is the same family as #3769 and #3807 (both closed), whose own repros now pass on every profile.

Q#

operation Main() : Int {
    use q = Qubit();
    X(q);
    let d = MResetZ(q) == One;
    mutable n = 0;
    if d { set n = 1; }
    let t = n;
    set n = 2;
    mutable r = 0;
    if d { set r = t; }
    r
}

d is true, so n is 1 when t copies it, and r is 1. With set r = t; moved outside the second if, every profile is correct.

Outputs (HEAD)

Copy Simulator (correct) Adaptive_RI Adaptive_RIF Adaptive
let t = n; 1 0 0 1
mutable t = n; 1 0 0 1
let a = [n];, read as a[0] (#3800) 1 0 0 1

Probable cause

Partial evaluation produces correct RIR. Block 4 stores the copy (Variable(4)) into r (Variable(5)):

Block 1: Variable(4, Integer) = Store Variable(3, Integer)   // let t = n
         Variable(3, Integer) = Store Integer(2)             // set n = 2
         Variable(5, Integer) = Store Integer(0)             // mutable r = 0
         Branch Variable(2, Boolean), 4, 3
Block 4: Variable(5, Integer) = Store Variable(4, Integer)   // set r = t
         Jump(3)
Block 3: Variable(6, Integer) = Store Variable(5, Integer)
         Call id(5), args( Variable(6, Integer), Tag(0, 3), )

After transform_to_ssa (source/compiler/qsc_rir/src/passes/ssa_transform.rs:18), the transform still creates the phi for n/t, but there is no phi for r at the join, and the constant 0 is recorded:

Block 2: Variable(7, Integer) = Phi ( [Integer(0), 0], [Integer(1), 1], )
         Branch Variable(0, Boolean), 3, 4
Block 3: Jump(4)
Block 4: Call id(5), args( Integer(0), Tag(0, 3), )

Only Adaptive_RI/RIF run this pass (source/compiler/qsc_rir/src/passes.rs:55-62). The fixes for #3769 and #3807 cover a copy that is read in straight-line code (their repros pass). A copy whose only read is inside a later branch is still mishandled.

Python repro

"""Adaptive_RI/RIF: a copy of a mutable, read inside a later runtime branch, loses its value.

The copy is taken after a join, the original is then reassigned, and the copy is read
inside a branch on the same measurement. Adaptive (no SSA) is correct. The same program
with the read outside the branch is correct on every profile.
Exits 0 while the bug reproduces.

Requires: pip install qdk qirrunner
"""

import os
import re
import sys
import tempfile

import qirrunner
from qdk import Context, TargetProfile

TEMPLATE = """
operation Main() : Int {{
    use q = Qubit();
    X(q);
    let d = MResetZ(q) == One;
    mutable n = 0;
    if d {{ set n = 1; }}
    {copy}
    set n = 2;
    mutable r = 0;
    if d {{ set r = {read}; }}
    r
}}
"""
# name -> (Q# source, correct output). `d` is true, so `n` is 1 when it's copied.
CASES = {
    "let t = n": (TEMPLATE.format(copy="let t = n;", read="t"), "1"),
    "mutable t = n": (TEMPLATE.format(copy="mutable t = n;", read="t"), "1"),
    "let a = [n] (#3800 other shape)": (TEMPLATE.format(copy="let a = [n];", read="a[0]"), "1"),
}

ENTRY = "Main()"
SHOTS = 10
PROFILES = ["Adaptive_RI", "Adaptive_RIF", "Adaptive"]


def fmt(value):
    """Formats a simulator value the same way as qir-runner output below."""
    if value is None:
        return "()"
    if type(value).__name__ == "Result":
        return "1" if str(value).endswith("One") else "0"
    if isinstance(value, bool):
        return str(value).lower()
    if isinstance(value, float):
        return repr(value)
    if isinstance(value, (tuple, list)):
        inner = ", ".join(fmt(v) for v in value)
        return f"({inner})" if isinstance(value, tuple) else f"[{inner}]"
    return str(value)


def parse_shots(text):
    """Turns qir-runner's output records into one formatted value per shot."""
    shots, records = [], None
    for line in text.splitlines():
        fields = line.split("\t")
        if fields[0] == "START":
            records = []
        elif fields[0] == "OUTPUT" and records is not None:
            records.append((fields[1], fields[2]))
        elif fields[0] == "END" and records is not None:
            shots.append(to_text(iter(records)) if records else "()")
            if fields[1] != "0":
                shots[-1] = f"exit code {fields[1]}"
            records = None
    return shots


def to_text(records):
    kind, value = next(records)
    if kind in ("TUPLE", "ARRAY"):
        inner = ", ".join(to_text(records) for _ in range(int(value)))
        return f"({inner})" if kind == "TUPLE" else f"[{inner}]"
    if kind == "DOUBLE":
        return repr(float(value))
    if kind == "BOOL":
        return value.lower()
    return value


def run_qir(qir):
    with tempfile.NamedTemporaryFile("w", suffix=".ll", delete=False) as file:
        file.write(qir)
    handler = qirrunner.OutputHandler()
    try:
        qirrunner.run(file.name, shots=SHOTS, rng_seed=1, output_fn=handler.handle)
    except Exception as error:  # qir-runner rejects the module
        message = str(error).strip().splitlines()[0]
        return {"invalid QIR: " + re.sub(r"^\S+\.ll:\d+:\d+: (error: )?", "", message)}
    finally:
        os.unlink(file.name)
    return set(parse_shots(handler.get_output()))


def compile_qir(source, profile):
    """Returns (qir, None) on success or (None, description) when compilation fails."""
    ctx = Context(target_profile=getattr(TargetProfile, profile))
    try:
        ctx.eval(source)
        return str(ctx.compile(ENTRY)), None
    except (KeyboardInterrupt, SystemExit):
        raise
    except BaseException as error:  # pyo3 surfaces Rust panics as PanicException (a BaseException)
        kind = "COMPILER PANIC" if type(error).__name__ == "PanicException" else "rejected"
        return None, f"{kind}: {str(error).strip().splitlines()[0]}"


def outputs(source):
    rows = {}
    ctx = Context(target_profile=TargetProfile.Unrestricted)
    ctx.eval(source)
    rows["Simulator"] = {fmt(v) for v in ctx.run(ENTRY, SHOTS, seed=1)}
    for profile in PROFILES:
        qir, failure = compile_qir(source, profile)
        rows[profile] = {failure} if failure else run_qir(qir)
    return rows


def show(name, rows, expected):
    print(f"--- {name} (expected {expected})")
    width = max(map(len, rows)) + 2
    for row, values in rows.items():
        print(f"  {row:<{width}}{' | '.join(sorted(values))}")



def main():
    reproduced = []
    for name, (source, expected) in CASES.items():
        rows = outputs(source)
        show(name, rows, expected)
        if any(rows[p] != {expected} for p in PROFILES):
            reproduced.append(name)
    if reproduced:
        print(f"REPRODUCED: {', '.join(reproduced)}")
        return 0
    print("NOT REPRODUCED")
    return 1


if __name__ == "__main__":
    sys.exit(main())

@swernli

Copy link
Copy Markdown
Contributor Author

1 and 2 from above are fixed in the stack (via #3869). Investigating 3...

@swernli

Copy link
Copy Markdown
Contributor Author

Andrew Casey (@amcasey) 3 from above is different enough, and more than a spot fix, that it warrants separating out into it's own issue and PR. Would you mind adding it as a sub-issue under #3781?

@amcasey

Copy link
Copy Markdown
Member

Andrew Casey (Andrew Casey (@amcasey)) 3 from above is different enough, and more than a spot fix, that it warrants separating out into it's own issue and PR. Would you mind adding it as a sub-issue under #3781?

Files #3908

…sent

Previously we used an optimization to avoid extra store instructions by having a special case around setting an immutable variable to a mutable variable where the latter had a known static mapping. This could incorrectly propagate references to a mutable variable instead of creating a copy (via store). Instead, this avoids trying to optimize in favor of correctness. For Adaptive_RI/RIF this ended up having no effect, as those extra store instructions are not produced due to how loop unrolling worked, but for Adaptive the extra store instructions create some noise in existing QIR via "needless" load, store, load pattern that ends up being correct for creating copies when mutation occurs in a loop.

Note for reviewers: the only code change is a couple lines in source/compiler/qsc_partial_eval/src/lib.rs, the rest are new tests and existing snapshot updates.

Fixes #3811
Fixes #3809

This branch has not been deployed

No deployments
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.

[Miscompile] Adaptive: let t = a inside a loop aliases a [Miscompile] Adaptive: let h = n inside a runtime branch aliases n when n has a known value

2 participants