Skip to content

Pass callables as arguments end-to-end through defunctionalization#3410

Merged
idavis merged 15 commits into
mainfrom
iadavis/multi-callable-input-defunc
Jul 8, 2026
Merged

Pass callables as arguments end-to-end through defunctionalization#3410
idavis merged 15 commits into
mainfrom
iadavis/multi-callable-input-defunc

Conversation

@idavis

@idavis idavis commented Jun 29, 2026

Copy link
Copy Markdown
Collaborator

Summary

In Q#, you can hand one operation or function to another as a value - for example passing AllH into InvokeWithQubits, or building a little function on the fly like Rz(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, _) or x -> x + inc capture 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 an if/else that 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:

  • A reassignment (f = Bar;) buried inside a sub-expression wasn't noticed in the right order.
  • Reassignments on the never-executed side of and/or/and= short-circuits were wrongly applied.
  • w/ update expressions were evaluated in the wrong order.
  • Reassignment inside a loop is now correctly treated as "unknown," producing a clear error instead of silently mis-specializing.
  • A function chosen conditionally (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 Angle return 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 Angle return 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 generated Apply_specialized_Y. This is required for QIR profiles as they don't support runtime resloved calls.

To do that reliably, the branch:

  • Analyzes data flow more precisely - it tracks, at each program point and even inside sub-expressions, which concrete function a variable holds, correctly accounting for reassignments, branches, loops, and short-circuit evaluation.
  • Threads captured values through - a closure's captured data is passed as ordinary extra arguments to the specialized copy, and consumed closures are blanked out so the pass knows it's finished (this is what lets it converge).
  • Resolves indirect sources - functions coming from array indices, record fields, function return values, and partial applications are traced back to their concrete target.
  • Fixes the surrounding pipeline - monomorphization now specializes generic closure targets, argument-promotion keeps closure-call signatures stable, and the interpreter-to-QIR path normalizes callable type signatures so entry-point function arguments compile cleanly to QIR and circuits.

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.

@idavis idavis self-assigned this Jun 29, 2026
Comment thread source/qdk_package/tests/test_callable_passing.py Dismissed
Comment thread source/qdk_package/tests/test_callable_passing.py Dismissed
Comment thread source/qdk_package/tests/test_callable_passing.py Dismissed
@idavis idavis force-pushed the iadavis/multi-callable-input-defunc branch from 2835530 to 61250cf Compare June 30, 2026 17:41
Comment thread source/qdk_package/tests/test_callable_passing.py Dismissed
Comment thread source/qdk_package/tests/test_callable_passing.py Dismissed
@idavis idavis force-pushed the iadavis/multi-callable-input-defunc branch from c1c80fc to 716456e Compare June 30, 2026 21:07
Comment thread source/compiler/qsc_fir_transforms/src/defunctionalize/rewrite.rs Fixed
@idavis idavis force-pushed the iadavis/multi-callable-input-defunc branch from 5810017 to 0c88c57 Compare July 4, 2026 19:09
@idavis idavis force-pushed the iadavis/multi-callable-input-defunc branch from 0c88c57 to c20d8f4 Compare July 4, 2026 19:09
@idavis idavis marked this pull request as ready for review July 5, 2026 16:02
@github-actions

github-actions Bot commented Jul 5, 2026

Copy link
Copy Markdown

Change in memory usage detected by benchmark.

Memory Report for 9639a19

Test This Branch On Main Difference
compile core + standard lib 24612483 bytes 24588971 bytes 23512 bytes

@idavis idavis changed the title specialize multi-callable HOF arguments in one defunctionalization pass Pass callables as arguments end-to-end through defunctionalization Jul 5, 2026
@idavis idavis removed the request for review from minestarks July 5, 2026 20:15
Comment thread source/compiler/qsc_openqasm_compiler/src/tests/statement/gate_call.rs Outdated
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

Change in memory usage detected by benchmark.

Memory Report for 7fc2aa9

Test This Branch On Main Difference
compile core + standard lib 24612483 bytes 24588971 bytes 23512 bytes

Comment thread source/compiler/qsc_fir_transforms/src/fir_builder.rs Outdated
@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

Change in memory usage detected by benchmark.

Memory Report for 14d0921

Test This Branch On Main Difference
compile core + standard lib 24612483 bytes 24588971 bytes 23512 bytes

@github-actions

github-actions Bot commented Jul 6, 2026

Copy link
Copy Markdown

Change in memory usage detected by benchmark.

Memory Report for 94bbd36

Test This Branch On Main Difference
compile core + standard lib 24612483 bytes 24588971 bytes 23512 bytes

Comment thread source/qdk_package/tests/test_callable_passing.py Dismissed
Comment thread source/qdk_package/tests/test_callable_passing.py Dismissed
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

Change in memory usage detected by benchmark.

Memory Report for 222cf0d

Test This Branch On Main Difference
compile core + standard lib 24612483 bytes 24588971 bytes 23512 bytes

idavis added 2 commits July 7, 2026 09:38
…e values and show errors instead of crashing.
…of a nested if/else could be dispatched incorrectly at runtime.
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

Change in memory usage detected by benchmark.

Memory Report for f41a985

Test This Branch On Main Difference
compile core + standard lib 24612483 bytes 24588971 bytes 23512 bytes

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

Change in memory usage detected by benchmark.

Memory Report for 0514c73

Test This Branch On Main Difference
compile core + standard lib 24612483 bytes 24588971 bytes 23512 bytes

…t inputs compile correctly, with tests covering the tricky cases.
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

Change in memory usage detected by benchmark.

Memory Report for d76160b

Test This Branch On Main Difference
compile core + standard lib 24612483 bytes 24588971 bytes 23512 bytes

…/else branches could be dispatched incorrectly at runtime.
@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

Change in memory usage detected by benchmark.

Memory Report for 62fd642

Test This Branch On Main Difference
compile core + standard lib 24612483 bytes 24588971 bytes 23512 bytes

@github-actions

github-actions Bot commented Jul 7, 2026

Copy link
Copy Markdown

Change in memory usage detected by benchmark.

Memory Report for bf8f568

Test This Branch On Main Difference
compile core + standard lib 24612483 bytes 24588971 bytes 23512 bytes

Comment thread source/compiler/qsc_fir_transforms/src/defunctionalize/specialize.rs Outdated
Comment thread source/compiler/qsc_fir_transforms/src/defunctionalize/specialize.rs Outdated
@idavis idavis requested a review from amcasey as a code owner July 8, 2026 16:06
@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown

Change in memory usage detected by benchmark.

Memory Report for 298f27a

Test This Branch On Main Difference
compile core + standard lib 24612483 bytes 24588971 bytes 23512 bytes

@github-actions

github-actions Bot commented Jul 8, 2026

Copy link
Copy Markdown

Change in memory usage detected by benchmark.

Memory Report for 230b510

Test This Branch On Main Difference
compile core + standard lib 24612483 bytes 24588971 bytes 23512 bytes

@idavis idavis added this pull request to the merge queue Jul 8, 2026
Merged via the queue into main with commit 4cc0c0e Jul 8, 2026
12 checks passed
@idavis idavis deleted the iadavis/multi-callable-input-defunc branch July 8, 2026 18:43
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.

3 participants