Pass callables as arguments end-to-end through defunctionalization#3410
Merged
Conversation
2835530 to
61250cf
Compare
c1c80fc to
716456e
Compare
5810017 to
0c88c57
Compare
0c88c57 to
c20d8f4
Compare
|
Change in memory usage detected by benchmark. Memory Report for 9639a19
|
swernli
reviewed
Jul 6, 2026
|
Change in memory usage detected by benchmark. Memory Report for 7fc2aa9
|
swernli
reviewed
Jul 6, 2026
|
Change in memory usage detected by benchmark. Memory Report for 14d0921
|
|
Change in memory usage detected by benchmark. Memory Report for 94bbd36
|
|
Change in memory usage detected by benchmark. Memory Report for 222cf0d
|
…e values and show errors instead of crashing.
…of a nested if/else could be dispatched incorrectly at runtime.
|
Change in memory usage detected by benchmark. Memory Report for f41a985
|
|
Change in memory usage detected by benchmark. Memory Report for 0514c73
|
…t inputs compile correctly, with tests covering the tricky cases.
|
Change in memory usage detected by benchmark. Memory Report for d76160b
|
…/else branches could be dispatched incorrectly at runtime.
|
Change in memory usage detected by benchmark. Memory Report for 62fd642
|
|
Change in memory usage detected by benchmark. Memory Report for bf8f568
|
swernli
reviewed
Jul 8, 2026
swernli
reviewed
Jul 8, 2026
|
Change in memory usage detected by benchmark. Memory Report for 298f27a
|
swernli
reviewed
Jul 8, 2026
swernli
approved these changes
Jul 8, 2026
|
Change in memory usage detected by benchmark. Memory Report for 230b510
|
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
In Q#, you can hand one operation or function to another as a value - for example passing
AllHintoInvokeWithQubits, or building a little function on the fly likeRz(theta, _)and passing that.Hardware and the QIR output format can't deal with "a function as a value at runtime" - every call has to point at a specific, known target. So the compiler has a stage called defunctionalization whose job is to look at each place a function-value is used and rewrite it into a plain, direct call to the real target. This branch makes that stage dramatically more capable and fixes a pile of cases where it previously produced wrong code, crashed, or gave up.
The bugs fixed
1. Only one function-argument at a time.
When an operation took several function arguments (e.g.
InvokeThree(first, second, third)), the compiler couldn't resolve them all together if there was captured state. This branch handles multiple function-arguments to the same call in a single pass.2. Closures and "partially applied" functions didn't survive.
Things like
Rz(theta, _)orx -> x + inccapture a value (theta,inc) and carry it around. Passing these, especially a closure that wraps another closure, often failed or lost the captured value. Now the captured values are correctly threaded through as extra arguments, including nested cases.3. Passing a function from Python as a top-level argument produced broken QIR/circuits.
When you called
qsharp.compile(InvokeWithQubits, 3, AllH)from Python, the function-value created by the interpreter carried stale internal type information. Once that value became part of the program to compile, it violated the compiler's own consistency checks. The fix normalizes those type signatures before compiling, so QIR generation and circuit drawing now work from Python-supplied callables.4. Generic functions behind a closure weren't specialized.
A closure like
Rz(theta, _)points at a generic library operation. Monomorphization only fixed up direct calls, not closures. It now also rewrites closure targets, so the concrete version actually gets created.5. Arrays of functions and index-based dispatch.
Selecting a function out of an array (
ops[i]) - including arrays of closures, tuples, and functions stored in record fields, now correctly turns into anif/elsethat dispatches to the right target, evaluating the index only once.6. Wrong answers from reassignment and control-flow tracking.
The stage tracks "which function does this variable actually hold at this point." Several cases were wrong:
f = Bar;) buried inside a sub-expression wasn't noticed in the right order.and/or/and=short-circuits were wrongly applied.w/update expressions were evaluated in the wrong order.if cond { X } else { Y }) now becomes proper branch-by-branch dispatch instead of dropping a branch.7. Side effects running twice.
When a function was selected by a condition that itself did something (e.g. a measurement), that side effect could be emitted more than once. Coordinating with the condition-normalization stage ensures each side effect runs exactly once.
8. Dynamic
Anglereturn in std lib.The angle calculations for custom gates in OpenQASM hit an issue where partial evaluation cannot lower a dynamic branch that returns an Angle struct. The calculation is now done inline eliminating the dynamic
Anglereturn during construction.The Fixes
Rather than the textbook approach using dispatch tables and dynamic dispatch, we must use specialization: for each call site where it can figure out the concrete function being passed, it makes a dedicated copy of the higher-order operation with the function-argument baked in as a direct call.
Apply(q => Y(q), target)becomes a call to a generatedApply_specialized_Y. This is required for QIR profiles as they don't support runtime resloved calls.To do that reliably, the branch:
The large test additions (Rust snapshot tests plus a new
test_callable_passing.py) lock in every scenario above: Python<=>Q# callables, closures with distinct captures, nested closures, and end-to-end QIR/circuit output.