File: bytecode.rst

package info (click to toggle)
numba 0.61.2%2Bdfsg-2
  • links: PTS, VCS
  • area: main
  • in suites: sid
  • size: 17,316 kB
  • sloc: python: 211,580; ansic: 15,233; cpp: 6,544; javascript: 424; sh: 322; makefile: 173
file content (185 lines) | stat: -rw-r--r-- 7,044 bytes parent folder | download | duplicates (3)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
==========================
Notes on Bytecode Handling
==========================


``LOAD_FAST_AND_CLEAR`` opcode, ``Expr.undef`` IR Node, ``UndefVar`` type
=========================================================================

Python 3.12 introduced a new bytecode ``LOAD_FAST_AND_CLEAR`` which is solely  
used in comprehensions. The common pattern is:

.. code-block:: python

    In [1]: def foo(x):
    ...:      # 6 LOAD_FAST_AND_CLEAR      0 (x)  # push x and clear from scope
    ...:      y = [x for x in (1, 2)]             # comprehension
    ...:      # 30 STORE_FAST              0 (x)  # restore x
    ...:      return x
    ...:

    In [2]: import dis

    In [3]: dis.dis(foo)
    1           0 RESUME                   0

    3           2 LOAD_CONST               1 ((1, 2))
                4 GET_ITER
                6 LOAD_FAST_AND_CLEAR      0 (x)
                8 SWAP                     2
                10 BUILD_LIST               0
                12 SWAP                     2
            >>   14 FOR_ITER                 4 (to 26)
                18 STORE_FAST               0 (x)
                20 LOAD_FAST                0 (x)
                22 LIST_APPEND              2
                24 JUMP_BACKWARD            6 (to 14)
            >>   26 END_FOR
                28 STORE_FAST               1 (y)
                30 STORE_FAST               0 (x)

    5          32 LOAD_FAST_CHECK          0 (x)
                34 RETURN_VALUE
            >>   36 SWAP                     2
                38 POP_TOP

    3          40 SWAP                     2
                42 STORE_FAST               0 (x)
                44 RERAISE                  0
    ExceptionTable:
    10 to 26 -> 36 [2]


Numba handles the ``LOAD_FAST_AND_CLEAR`` bytecode differently to CPython 
because it relies on static instead of dynamic semantics. 

In Python, comprehensions can shadow variables from the enclosing function 
scope. To handle this, ``LOAD_FAST_AND_CLEAR`` snapshots the value of a 
potentially shadowed variable and clears it from the scope. This gives the 
illusion that comprehensions execute in a new scope, even though they are fully 
inlined in Python 3.12. The snapshotted value is later restored with 
``STORE_FAST`` after the comprehension.

Since Numba uses static semantics, it cannot precisely model the dynamic 
behavior of ``LOAD_FAST_AND_CLEAR``. Instead, Numba checks if a variable is 
used in previous opcodes to determine if it must be defined. If so, Numba 
treats it like a regular ``LOAD_FAST``. Otherwise, Numba emits an `Expr.undef`_ 
IR node to mark the stack value as undefined. Type inference assigns the 
`UndefVar`_ type to this node, allowing the value to be zero-initialized and 
implicitly cast to other types. 

In object mode, Numba uses the `\_UNDEFINED`_ sentinel object to indicate 
undefined values.

Numba does not raise ``UnboundLocalError`` if an undefined value is used.

Special case 1: ``LOAD_FAST_AND_CLEAR`` may load an undefined variable
----------------------------------------------------------------------

.. code-block:: python

    In [1]: def foo(a, v):               
    ...:      if a:
    ...:          x = v
    ...:      y = [x for x in (1, 2)]
    ...:      return x


In the above example, the variable ``x`` may or may not be defined before the 
list comprehension, depending on the truth value of ``a``. If ``a`` is ``True``, 
then ``x`` is defined and execution proceeds as described in the common case. 
However, if ``a`` is ``False``, then ``x`` is undefined. 
In this case, the Python interpreter would raise an ``UnboundLocalError`` at 
the ``return x`` line. Numba cannot determine whether ``x`` was previously 
defined, therefore it assumes ``x`` is defined to avoid the error. 
This deviates from Python's official semantics, since Numba will use a 
zero-initialized ``x`` even if it was not defined earlier.

.. code-block:: python

    In [1]: from numba import njit

    In [2]: def foo(a, v):
    ...:     if a:
    ...:         x = v
    ...:     y = [x for x in (1, 2)]
    ...:     return x
    ...:

    In [3]: foo(0, 123)
    ---------------------------------------------------------------------------
    UnboundLocalError                         Traceback (most recent call last)
    Cell In[3], line 1
    ----> 1 foo(0, 123)

    Cell In[2], line 5, in foo(a, v)
        3     x = v
        4 y = [x for x in (1, 2)]
    ----> 5 return x

    UnboundLocalError: cannot access local variable 'x' where it is not associated with a value

    In [4]: njit(foo)(0, 123)
    Out[4]: 0

As shown in the above example, Numba does not raise ``UnboundLocalError`` and
allows the function to return normally.

Special case 2: ``LOAD_FAST_AND_CLEAR`` loads undefined variable
----------------------------------------------------------------

If Numba can statically determine that a variable must be undefined,  
the type system will raise a ``TypingError`` instead of raising a ``NameError`` 
like the Python interpreter does.

.. code-block:: python
            
    In [1]: def foo():
    ...:     y = [x for x in (1, 2)]
    ...:     return x
    ...:

    In [2]: foo()
    ---------------------------------------------------------------------------
    NameError                                 Traceback (most recent call last)
    Cell In[2], line 1
    ----> 1 foo()

    Cell In[1], line 3, in foo()
        1 def foo():
        2     y = [x for x in (1, 2)]
    ----> 3     return x

    NameError: name 'x' is not defined

    In [3]: from numba import njit

    In [4]: njit(foo)()
    ---------------------------------------------------------------------------
    TypingError                               Traceback (most recent call last)
    Cell In[4], line 1
    ----> 1 njit(foo)()

    File /numba/numba/core/dispatcher.py:468, in _DispatcherBase._compile_for_args(self, *args, **kws)
        464         msg = (f"{str(e).rstrip()} \n\nThis error may have been caused "
        465                f"by the following argument(s):\n{args_str}\n")
        466         e.patch_message(msg)
    --> 468     error_rewrite(e, 'typing')
        469 except errors.UnsupportedError as e:
        470     # Something unsupported is present in the user code, add help info
        471     error_rewrite(e, 'unsupported_error')

    File /numba/numba/core/dispatcher.py:409, in _DispatcherBase._compile_for_args.<locals>.error_rewrite(e, issue_type)
        407     raise e
        408 else:
    --> 409     raise e.with_traceback(None)

    TypingError: Failed in nopython mode pipeline (step: nopython frontend)
    NameError: name 'x' is not defined


.. _UndefVar: https://github.com/numba/numba/blob/db5f0a45fcccb359cba248c4767cd1caf16c4a85/numba/core/types/misc.py#L36-L44

.. _\_UNDEFINED: https://github.com/numba/numba/blob/db5f0a45fcccb359cba248c4767cd1caf16c4a85/numba/core/pylowering.py#L32

.. _Expr.undef: https://github.com/numba/numba/blob/db5f0a45fcccb359cba248c4767cd1caf16c4a85/numba/core/ir.py#L565-L572