openqasm/source/language/scope.rst

354 lines
13 KiB
ReStructuredText

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 <scope-global>`, which is the current scope only when no
other scopes are active.
* :ref:`Gate and function scope <scope-subroutine>`, which is entered in the
body of ``gate`` and ``def`` definitions.
* :ref:`Local block scope <scope-block>`, which becomes active on entry to a
non-gate and non-subroutine block, such as those associated with ``if`` or
``for`` statements or introduced via anonymous scope syntax.
* 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 <openpulse>` 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
<pulse-gates>`, 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
-----------
In addition to the scopes explicitly defined above, local scope blocks may be
introduced with curly brackets ``{`` and ``}`` wherever a statement is expected.
These are sometimes referred to as "anonymous" scope blocks, because they are not
directly associated with a named entity such as a gate or subroutine, or the global
scope.
Certain control-flow operations rely on the introduction of local scope blocks.
These operations are
* ``for`` loops,
* ``while`` loops,
* ``if`` and ``else`` blocks,
* ``box`` statements.
Local scope blocks 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.
// This is an anonymous scope block
{
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 above block, and
// the previous 'ii' defined on line 3 is accessible again.
ii *= 2; // global 'ii' is now 400.
if (true) {
// As before, a local variable 'ii' is declared and shadows the global definition.
// This is also distinct from the 'ii' introduced in the anonymous scope block above.
int ii = 1;
}
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.
}