Scoping of variables ==================== This section describes the rules surrounding scoping of variables in OpenQASM. OpenQASM 3.0 has four types of scoping constructs, and the visibility of some symbols can differ slightly between these. The types of scope are: * :ref:`Global scope `, which is the current scope only when no other scopes are active. * :ref:`Gate and function scope `, which is entered in the body of ``gate`` and ``def`` definitions. * :ref:`Local block scope `, which becomes active on entry to a non-gate and non-subroutine control-flow block, such as ``if`` or ``for``. * Calibration scopes, which are the shared calibration state contained in ``cal`` and ``defcal`` blocks. The precise rules for these may vary depending on the particular companion calibration grammar that has been loaded for this program. See :ref:`the OpenPulse specification ` for details on how this is handled in the OpenPulse calibration language. This document will not cover this topic further. In general, the lifetime of each identifier begins when it is declared, and ends at the completion of the scope it was declared in. The storage space for each variable must be allocated for at least the lifetime of the identifier, but it is up to individual implementations to define their allocation strategies within nested scopes. In order to be a valid OpenQASM 3.0 program, symbols must be defined before they are used; in particular, there is no forward declaration of functions and gates, and there can be no mutual recursion. Most regular identifiers of variables can be shadowed in inner scopes, so that the identifier temporarily refers to a different variable, but cannot be re-declared without entering an inner scope. The type of the shadowing variable does not need to be the same as the type of the variable it is shadowing. Not all identifiers declared in outer scopes are visible within inner scopes. The visibility depends on the type of the variable, and the type of the inner scope. Approximately, ``const`` variables, gates and subroutines are visible within *all* inner scopes, while other variables in outer scopes are visible within inner control-flow scopes but not gate and function scopes. The ``include`` statement should be seen as extending the global scope of the file it is contained within; all variables that are in scope at the time of the ``include`` statement are also in scope while the included file is being parsed, and variables defined in that file will be available in the containing file's scope once the inclusion has been parsed. There is no separate namespacing defined in OpenQASM 3.0. .. _scope-global: Global scope ------------ The global scope is active when no other scopes are active. Several OpenQASM 3.0 statements are only valid in the global scope, such as (non-exhaustive): * ``gate``, ``def`` and ``defcal`` declarations, * ``qubit`` declarations, * ``array`` declarations. A simple example of scoping between a main program file and an included file: .. code-block:: c :caption: Main program OPENQASM 3.0; gate h q { U(pi/2, 0, pi) q; // `U` is the single-qubit gate that is implicitly defined in the global // scope of all OpenQASM 3.0 programs, and so is available here. // Similarly `pi` is one of the implicitly defined constants. } int i = 100; include "my_definitions.qasm"; // Identifiers 'h', 'my_gate', 'i' and 'j' are defined and in scope. .. code-block:: c :caption: File ``my_definitions.qasm`` gate my_gate q { U(pi, 0, pi) q; } int j = i + 5; // This usage of `i` is valid because the scope inside the `include` // inherits all definitions from the scope that did the including. Within the global scope, identifiers declared by ``gate``, ``def`` and ``defcal`` statements may not be shadowed or otherwise redeclared. As described in the :ref:`section on pulse-level descriptions of quantum operations `, multiple ``defcal`` statements may affect the same operation; this is not an instance of shadowing, but a form of overloading, extending the calibration definitions for different qubits. .. warning:: The following example is *not* valid OpenQASM 3.0 due to invalid scoping. .. code-block:: c OPENQASM 3.0; gate h q { U(pi/2, 0, pi) q; } int h = 1; // ERROR: 'h' is already defined and cannot be re-declared. uint a = 1; uint a = 2; // ERROR: 'a' is already defined and cannot be re-declared. defcal h $0 { // ... } // No error: this is valid OpenQASM 3.0 because `defcal` statements do not // redefine, they overload quantum operations with specific pulse-level // control statements. defcal a $0 { // ... } // ERROR: 'a' is already defined and cannot be re-declared. Unlike the // previous example, this is an error because 'a' is already declared as a // non-quantum-operation type ('uint'). This `defcal` would be defining a // new gate, which is invalid with 'a' already defined. .. _scope-subroutine: Subroutine and gate scope ------------------------- The definitions of subroutines (``def``) and gates (``gate``) introduce a new scope. The ``def`` and ``gate`` statements are only valid directly within the global scope of the program. Inside the definition of the subroutine or gate, symbols that were already defined in the global scope with the ``const`` modifier, or previously defined gates and subroutines are visible. Globally scoped variables without the ``const`` modifier are not visible inside the definition. In other words, subroutines and gates cannot close over variables that may be modified at run-time. Variables defined in subroutine scopes are local to the subroutine body. Variables defined in the parameter specifications of subroutines and gates behave for scoping purposes as if they were defined in the scope of the definition. The lifetime of these local variables ends at the end of the function body, and they are not accessible after the subroutine or gate body. Similarly, the qubit identifiers in a gate definition are valid only within the definition of the gate. The identifier of a subroutine or gate is available in the scope of its own body, allowing direct recursion. For gates, the direct recursion is unlikely to ever be useful, since this would generally be non-terminating. Local subroutine or gate variables, including parameters and qubit definitions, may shadow variables defined in the outer scope. Inside the body, the identifier will refer to the local variable instead. After the definition of the body has completed (and we are back in the global scope), the identifier will refer to the same variable it did before the subroutine or gate. Subroutines cannot contain ``qubit`` declarations in their bodies, but can accept variables of type ``qubit`` in their parameter lists. Aliases can be declared within subroutine and gate scopes, and have the same lifetime and visibility as other local variables. For example: .. code-block:: c :linenos: OPENQASM 3.0; qubit[5] all_qubits; int a = 1; int b = 2; const int c = 3; const int d = 4; def my_routine(uint a, uint c) { // In this body, 'a' refers to the subroutine parameter, not the external // variable, which wouldn't be visible even without the shadowing. int in_body = 5; // Identifiers in scope are: // - 'my_routine': the subroutine itself // - 'a': type 'uint', from the parameter list // - 'c': type 'uint', from the parameter list (shadows the outer 'const // int' 'c'). // - 'd': type 'const int', value 4, visible from the global scope // because it is a 'const' type. // - 'in_body': type 'int', value 5, from regular definition in the // current scope. // - other built-in identifiers (such as 'U' and 'pi') that are // implicitly defined in the global scope. // - all available hardware qubits (such as '$0') // // The variable 'b' is not in scope, because its visibility as a // non-'const' type does not make it available within subroutines. The // hardware qubit identifiers are in scope, but not the virtual qubit // identifier 'all_qubits'. } // After the subroutine block, 'a' and 'c' once again refer to the variables // of type 'int' and 'const int' defined on lines 3 and 5 respectively. // 'in_body' (from the subroutine body) is not in scope, while 'my_routine' // (the subroutine) is. const float[64] new_variable = 1.5; def second_subroutine(qubit[4] q) { int in_body = 8; let some_qubits = q[0:2]; // Identifiers in scope are: // - 'second_subroutine' // - 'my_subroutine' // - 'in_body': type 'int', value 8 // - 'c': type 'const int', value 3 // - 'd': type 'const int', value 4 // - 'q': type 'qubit[4]', a virtual, run-time-known qubit register. // - 'some_qubits': alias for the first three qubits of 'q'. // - 'new_variable': type 'const float[64]', value 1.5 // - the other built-in identifiers like 'U' and 'pi' // - the available hardware qubits like '$0'. } .. _scope-block: Block scope ----------- Certain control-flow operations introduce their own local scope. These operations are: * ``for`` loops, * ``while`` loops, * ``if`` and ``else`` blocks, * ``box`` statements. These scopes inherit *all* variables that are in scope in the immediately containing scope. Unlike subroutines and gate scopes, this includes variables that are not ``const``. This is broadly similar how these constructs behave in other procedural languages, such as C. The iteration variable of a ``for`` loop has lifetime and visibility as if it were declared as the first statement in the body of that loop. It is not accessible after the body of the loop. The blocks associated with ``if`` and its corresponding ``else`` define two different scopes; the variables and definitions are not shared between them. As with subroutine scopes, variables defined locally in these scopes (including the for-loop iteration variable) may shadow variables with the same name in outer scopes. When the defining scope of a shadowing variable ends, the previous variable (which was shadowed) becomes accessible again. Qubits and arrays cannot be declared within local block scopes, but aliases can. Some further examples: .. code-block:: c :linenos: OPENQASM 3.0 int ii = 100; // 'ii' is declared in the global scope. qubit[5] q; // 'q' is declared in the global scope. let some_q = q[0:2]; // alias 'some_q' is declared in the global scope. if (true) { ii *= 2; // This is the global 'ii', which now has the value 200. // A local variable 'ii' is declared, which shadows the global definition. // The global 'ii' is no longer accessible until this scope ends. int ii = 1; // The local variable 'ii' is modified, and now has the value 2. ii *= 2; } // The local 'ii' went out of scope at the conclusion of the 'if' block, and // the previous 'ii' defined on line 3 is accessible again. ii *= 2; // global 'ii' is now 400. uint sum = 0; for uint ii in [1:4] { // The global 'ii' is shadowed by the iteration variable 'ii', which also // has a different type. The outer 'sum' is still accessible. // Values at this point in various iterations: // Iteration ii sum // 0 1 0 // 1 2 2 // 2 3 6 // 3 4 12 sum += ii; // Iteration variable 'ii' is added to global 'sum' // Iteration ii sum // 0 1 1 // 1 2 4 // 2 3 9 // 3 4 16 if (sum > 10) { float ii = 10.0; // For-loop iteration variable shadowed. sum += uint(ii * 2.0); } else { sum += ii; // 'ii' is the for-loop iteration variable. } // Iteration ii sum // 0 1 2 // 1 2 6 // 2 3 12 // 3 4 36 U(0, 0, (sum / 55) * pi) q; // Global-scope qubit 'q' is in scope here. } // The lifetime of the local for-loop iteration variable 'ii' ended at the // conclusion of the for-loop body, and the global 'ii' is back in scope. while (ii > 0) { let some_q = q[3:4]; // local alias 'some_q' shadows the global alias. }