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:
Global scope, which is the current scope only when no other scopes are active.
Gate and function scope, which is entered in the body of
gate
anddef
definitions.Local block scope, which becomes active on entry to a non-gate and non-subroutine control-flow block, such as
if
orfor
.Calibration scopes, which are the shared calibration state contained in
cal
anddefcal
blocks. The precise rules for these may vary depending on the particular companion calibration grammar that has been loaded for this program. See 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.
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
anddefcal
declarations,qubit
declarations,array
declarations.
A simple example of scoping between a main program file and an included file:
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.
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 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.
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.
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:
1OPENQASM 3.0;
2
3qubit[5] all_qubits;
4
5int a = 1;
6int b = 2;
7const int c = 3;
8const int d = 4;
9
10def my_routine(uint a, uint c) {
11 // In this body, 'a' refers to the subroutine parameter, not the external
12 // variable, which wouldn't be visible even without the shadowing.
13
14 int in_body = 5;
15
16 // Identifiers in scope are:
17 // - 'my_routine': the subroutine itself
18 // - 'a': type 'uint', from the parameter list
19 // - 'c': type 'uint', from the parameter list (shadows the outer 'const
20 // int' 'c').
21 // - 'd': type 'const int', value 4, visible from the global scope
22 // because it is a 'const' type.
23 // - 'in_body': type 'int', value 5, from regular definition in the
24 // current scope.
25 // - other built-in identifiers (such as 'U' and 'pi') that are
26 // implicitly defined in the global scope.
27 // - all available hardware qubits (such as '$0')
28 //
29 // The variable 'b' is not in scope, because its visibility as a
30 // non-'const' type does not make it available within subroutines. The
31 // hardware qubit identifiers are in scope, but not the virtual qubit
32 // identifier 'all_qubits'.
33}
34
35// After the subroutine block, 'a' and 'c' once again refer to the variables
36// of type 'int' and 'const int' defined on lines 3 and 5 respectively.
37// 'in_body' (from the subroutine body) is not in scope, while 'my_routine'
38// (the subroutine) is.
39
40const float[64] new_variable = 1.5;
41
42def second_subroutine(qubit[4] q) {
43 int in_body = 8;
44
45 let some_qubits = q[0:2];
46
47 // Identifiers in scope are:
48 // - 'second_subroutine'
49 // - 'my_subroutine'
50 // - 'in_body': type 'int', value 8
51 // - 'c': type 'const int', value 3
52 // - 'd': type 'const int', value 4
53 // - 'q': type 'qubit[4]', a virtual, run-time-known qubit register.
54 // - 'some_qubits': alias for the first three qubits of 'q'.
55 // - 'new_variable': type 'const float[64]', value 1.5
56 // - the other built-in identifiers like 'U' and 'pi'
57 // - the available hardware qubits like '$0'.
58}
Block scope¶
Certain control-flow operations introduce their own local scope. These operations are:
for
loops,while
loops,if
andelse
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:
1OPENQASM 3.0
2
3int ii = 100; // 'ii' is declared in the global scope.
4qubit[5] q; // 'q' is declared in the global scope.
5let some_q = q[0:2]; // alias 'some_q' is declared in the global scope.
6
7if (true) {
8 ii *= 2; // This is the global 'ii', which now has the value 200.
9
10 // A local variable 'ii' is declared, which shadows the global definition.
11 // The global 'ii' is no longer accessible until this scope ends.
12 int ii = 1;
13 // The local variable 'ii' is modified, and now has the value 2.
14 ii *= 2;
15}
16
17// The local 'ii' went out of scope at the conclusion of the 'if' block, and
18// the previous 'ii' defined on line 3 is accessible again.
19ii *= 2; // global 'ii' is now 400.
20
21uint sum = 0;
22for uint ii in [1:4] {
23 // The global 'ii' is shadowed by the iteration variable 'ii', which also
24 // has a different type. The outer 'sum' is still accessible.
25
26 // Values at this point in various iterations:
27 // Iteration ii sum
28 // 0 1 0
29 // 1 2 2
30 // 2 3 6
31 // 3 4 12
32
33 sum += ii; // Iteration variable 'ii' is added to global 'sum'
34
35 // Iteration ii sum
36 // 0 1 1
37 // 1 2 4
38 // 2 3 9
39 // 3 4 16
40
41 if (sum > 10) {
42 float ii = 10.0; // For-loop iteration variable shadowed.
43 sum += uint(ii * 2.0);
44 } else {
45 sum += ii; // 'ii' is the for-loop iteration variable.
46 }
47
48 // Iteration ii sum
49 // 0 1 2
50 // 1 2 6
51 // 2 3 12
52 // 3 4 36
53
54 U(0, 0, (sum / 55) * pi) q; // Global-scope qubit 'q' is in scope here.
55}
56
57// The lifetime of the local for-loop iteration variable 'ii' ended at the
58// conclusion of the for-loop body, and the global 'ii' is back in scope.
59
60while (ii > 0) {
61 let some_q = q[3:4]; // local alias 'some_q' shadows the global alias.
62}