Skip to content

preserve runtime callable target functors - #3915

Draft
Ian Davis (idavis) wants to merge 1 commit into
iadavis/pass-split/return-condition-semanticsfrom
iadavis/pass-split/runtime-callable-functors
Draft

Ian Davis (idavis) wants to merge 1 commit into
iadavis/pass-split/return-condition-semanticsfrom
iadavis/pass-split/runtime-callable-functors

Conversation

@idavis

@idavis Ian Davis (idavis) commented Oct 7, 2026 •

Copy link
Copy Markdown
Collaborator

Summary

Fixes QIR and static-circuit generation when the requested entry is a runtime callable value with applied functors, such as:

Adjoint Target
Controlled Target
Controlled Adjoint Target
Controlled Controlled Target

Previously, runtime codegen could discard the callable value’s Adjoint or Controlled state and invoke the original body specialization. The branch carries functor information through both codegen backends, partial evaluation, specialization selection, argument resolution, validation, and diagnostics.

The branch fixes runtime callable codegen bugs that could:

  • discard Adjoint or Controlled from a runtime callable value;
  • invoke the body instead of an explicit specialization;
  • lose functors on either codegen backend;
  • mis-handle nested control argument shapes;
  • flatten the wrong controls;
  • alter controlled-specialization gate order;
  • combine target and argument functors incorrectly;
  • accept unsupported runtime functors;
  • miss duplicate control/target qubits; or
  • report failures at the wrong source location.

1. Runtime adjoints invoking the body specialization

Compiling a runtime value representing Adjoint Target could silently invoke Target’s body.

operation Target() : Unit is Adj {
    body (...) {}

    adjoint (...) {
        fail "adjoint selected";
    }
}

// Runtime value passed to QIR or circuit generation:
Adjoint Target

The generated program must fail with adjoint selected. Successful execution would prove that codegen incorrectly invoked the empty body.

The branch passes the runtime FunctorApp into partial evaluation and selects the adjoint specialization.

2. Applied functors being lost on the direct reinvocation path

For ordinary concrete arguments, codegen may retain the original callable and invoke it directly during partial evaluation.

operation Rotate(theta : Double) : Unit is Adj {
    use q = Qubit();
    Rz(theta, q);
}

// Runtime target and argument:
Adjoint Rotate
0.25

The expected rotation is Rz(-0.25, q), not Rz(0.25, q).

The branch records the target functor alongside the callable ID and arguments when using the reinvocation backend.

3. Applied functors being lost on synthetic entries

When arguments contain callable values, codegen creates a synthetic FIR entry so defunctionalization can process them.

function Identity(value : Double) : Double {
    value
}

operation Higher(
    transform : Double -> Double,
    theta : Double
) : Unit is Adj {
    use q = Qubit();
    Rz(transform(theta), q);
}

// Runtime target and arguments:
Adjoint Higher
(Identity, 0.25)

The synthetic entry must invoke Adjoint Higher, not Higher.

The branch wraps the synthetic callee with the runtime target’s functor applications.

4. Explicit adjoint and controlled specializations being ignored

Operations can provide behaviorally distinct specialization bodies.

operation Target(theta : Double) : Unit is Adj + Ctl {
    body (...) {
        use q = Qubit();
        Rz(theta, q);
    }

    adjoint (...) {
        use q = Qubit();
        Rz(-theta, q);
    }

    controlled (controls, ...) {
        use q = Qubit();
        Controlled X(controls, q);
        Rz(theta, q);
    }

    controlled adjoint (controls, ...) {
        use q = Qubit();
        Controlled X(controls, q);
        Rz(-theta, q);
    }
}

Runtime requests for Adjoint Target, Controlled Target, and Controlled Adjoint Target must select their corresponding explicit specializations.

The branch uses the runtime functor when resolving the specialization declaration.

5. Nested controlled arguments using the wrong input shape

Each controlled application adds another (Qubit[], input) layer.

operation Target() : Unit is Ctl {
    body (...) {}

    controlled (controls, ...) {
        use q = Qubit();
        Rz(IntAsDouble(Length(controls)), q);
    }
}

use qs = Qubit[3];

// Runtime target:
Controlled Controlled Target

// Runtime argument:
([qs[2]], ([qs[0], qs[1]], ()))

The controlled specialization must receive all three controls and preserve the underlying Unit input.

The branch tracks control depth, peels the correct number of tuple layers, and flattens the control arrays for invocation.

6. Nested controls changing explicit specialization gate order

A controlled-adjoint specialization can have an observable gate sequence.

operation Target(theta : Double) : Unit is Adj + Ctl {
    body (...) {}

    adjoint (...) {}

    controlled adjoint (controls, ...) {
        use (extra, target) = (Qubit(), Qubit());
        Controlled X(controls + [extra], target);
        Rz(-theta, target);
        Controlled X(controls + [extra], target);
    }
}

// Runtime target:
Controlled Controlled Adjoint Target

Codegen must preserve:

  1. the first controlled X;
  2. the negative rotation; and
  3. the second controlled X.

The branch preserves both control depth and adjoint selection in QIR and static circuits.

7. Target and callable-argument functors interfering

A higher-order target and its callable argument can each have an independent adjoint application.

operation Rotate(theta : Double) : Unit is Adj {
    use q = Qubit();
    Rz(theta, q);
}

operation Higher(
    operationValue : Double => Unit is Adj,
    theta : Double
) : Unit is Adj {
    operationValue(theta);
}

These runtime combinations have different results:

Higher(Adjoint Rotate, 0.25)          // -0.25
(Adjoint Higher)(Rotate, 0.25)        // -0.25
(Adjoint Higher)(Adjoint Rotate, 0.25) // +0.25

The branch preserves the target’s functor separately from functors already attached to callable-valued arguments.

8. Unsupported runtime functors reaching codegen

Runtime API values can request a functor that the callable does not support.

operation Plain(value : Int) : Unit {}
operation AdjointOnly(value : Int) : Unit is Adj {}
operation ControlledOnly(value : Int) : Unit is Ctl {}

Invalid runtime requests include the equivalents of:

Adjoint Plain
Controlled AdjointOnly
Adjoint Controlled ControlledOnly

Valid Q# source rejects these expressions, but runtime clients can hold or construct callable values independently.

The branch validates the requested functor before codegen and reports InvalidRuntimeCallableFunctor instead of selecting a nonexistent specialization or panicking.

9. Duplicate controls not being checked at the target invocation

Nested runtime controls can reuse a qubit across layers or as the target input.

operation Target(q : Qubit) : Unit is Ctl {
    body (...) {}
    controlled (controls, ...) {}
}

use qs = Qubit[3];

// Invalid nested controlled argument:
([qs[0]], ([qs[0]], qs[2]))

The same qubit appears in both control layers.

The branch passes control depth and the callable’s source span into argument resolution, preserving the “qubits in invocation are not unique” diagnostic at Target.

10. Runtime-adjoint failures losing their source location

A failure in the selected specialization must remain attributed to the original Q# source.

operation Target(value : Int) : Unit is Adj {
    body (...) {}

    adjoint (...) {
        fail "adjoint selected";
    }
}

// Runtime target:
Adjoint Target

QIR and static-circuit generation must report the failure at:

fail "adjoint selected"

The branch invokes the actual adjoint specialization during partial evaluation and preserves its source span.

@idavis
Ian Davis (idavis) added this pull request to stack #3892 October 7, 2026 20:51

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.

1 participant