interval-example.rst 11.2 KB
Newer Older
dugupeiwen's avatar
dugupeiwen committed
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
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289

Example: An Interval Type
=========================

In this example, we will extend the Numba frontend to add support for a user-defined
class that it does not internally support. This will allow:

* Passing an instance of the class to a Numba function
* Accessing attributes of the class in a Numba function
* Constructing and returning a new instance of the class from a Numba function

(all the above in :term:`nopython mode`)

We will mix APIs from the :ref:`high-level extension API <high-level-extending>`
and the :ref:`low-level extension API <low-level-extending>`, depending on what is
available for a given task.

The starting point for our example is the following pure Python class:

.. literalinclude:: ../../../numba/tests/doc_examples/test_interval_example.py
   :language: python
   :start-after: magictoken.interval_py_class.begin
   :end-before: magictoken.interval_py_class.end
   :dedent: 8

Extending the typing layer
""""""""""""""""""""""""""

Creating a new Numba type
-------------------------

As the ``Interval`` class is not known to Numba, we must create a new Numba
type to represent instances of it.  Numba does not deal with Python types
directly: it has its own type system that allows a different level of
granularity as well as various meta-information not available with regular
Python types.

We first create a type class ``IntervalType`` and, since we don't need the
type to be parametric, we instantiate a single type instance ``interval_type``:

.. literalinclude:: ../../../numba/tests/doc_examples/test_interval_example.py
   :language: python
   :start-after: magictoken.interval_type_class.begin
   :end-before: magictoken.interval_type_class.end
   :dedent: 8


Type inference for Python values
--------------------------------

In itself, creating a Numba type doesn't do anything.  We must teach Numba
how to infer some Python values as instances of that type.  In this example,
it is trivial: any instance of the ``Interval`` class should be treated as
belonging to the type ``interval_type``:

.. literalinclude:: ../../../numba/tests/doc_examples/test_interval_example.py
   :language: python
   :start-after: magictoken.interval_typeof_register.begin
   :end-before: magictoken.interval_typeof_register.end
   :dedent: 8

Function arguments and global values will thusly be recognized as belonging
to ``interval_type`` whenever they are instances of ``Interval``.


Type inference for Python annotations
-------------------------------------

While ``typeof`` is used to infer the Numba type of Python objects,
``as_numba_type`` is used to infer the Numba type of Python types.  For simple
cases, we can simply register that the Python type ``Interval`` corresponds with
the Numba type ``interval_type``:

.. literalinclude:: ../../../numba/tests/doc_examples/test_interval_example.py
   :language: python
   :start-after: magictoken.numba_type_register.begin
   :end-before: magictoken.numba_type_register.end
   :dedent: 8

Note that ``as_numba_type`` is only used to infer types from type annotations at
compile time.  The ``typeof`` registry above is used to infer the type of
objects at runtime.


Type inference for operations
-----------------------------

We want to be able to construct interval objects from Numba functions, so
we must teach Numba to recognize the two-argument ``Interval(lo, hi)``
constructor.  The arguments should be floating-point numbers:

.. literalinclude:: ../../../numba/tests/doc_examples/test_interval_example.py
   :language: python
   :start-after: magictoken.numba_type_callable.begin
   :end-before: magictoken.numba_type_callable.end
   :dedent: 8


The :func:`type_callable` decorator specifies that the decorated function
should be invoked when running type inference for the given callable object
(here the ``Interval`` class itself).  The decorated function must simply
return a typer function that will be called with the argument types.  The
reason for this seemingly convoluted setup is for the typer function to have
*exactly* the same signature as the typed callable.  This allows handling
keyword arguments correctly.

The *context* argument received by the decorated function is useful in
more sophisticated cases where computing the callable's return type
requires resolving other types.


Extending the lowering layer
""""""""""""""""""""""""""""

We have finished teaching Numba about our type inference additions.
We must now teach Numba how to actually generate code and data for
the new operations.


Defining the data model for native intervals
--------------------------------------------

As a general rule, :term:`nopython mode` does not work on Python objects
as they are generated by the CPython interpreter.  The representations
used by the interpreter are far too inefficient for fast native code.
Each type supported in :term:`nopython mode` therefore has to define
a tailored native representation, also called a *data model*.

A common case of data model is an immutable struct-like data model, that
is akin to a C ``struct``.  Our interval datatype conveniently falls in
that category, and here is a possible data model for it:

.. literalinclude:: ../../../numba/tests/doc_examples/test_interval_example.py
   :language: python
   :start-after: magictoken.interval_model.begin
   :end-before: magictoken.interval_model.end
   :dedent: 8

This instructs Numba that values of type ``IntervalType`` (or any instance
thereof) are represented as a structure of two fields ``lo`` and ``hi``,
each of them a double-precision floating-point number (``types.float64``).

.. note::
   Mutable types need more sophisticated data models to be able to
   persist their values after modification.  They typically cannot be
   stored and passed on the stack or in registers like immutable types do.


Exposing data model attributes
------------------------------

We want the data model attributes ``lo`` and ``hi`` to be exposed under
the same names for use in Numba functions.  Numba provides a convenience
function to do exactly that:

.. literalinclude:: ../../../numba/tests/doc_examples/test_interval_example.py
   :language: python
   :start-after: magictoken.interval_attribute_wrapper.begin
   :end-before: magictoken.interval_attribute_wrapper.end
   :dedent: 8

This will expose the attributes in read-only mode.  As mentioned above,
writable attributes don't fit in this model.


Exposing a property
-------------------

As the ``width`` property is computed rather than stored in the structure,
we cannot simply expose it like we did for ``lo`` and ``hi``.  We have to
re-implement it explicitly:

.. literalinclude:: ../../../numba/tests/doc_examples/test_interval_example.py
   :language: python
   :start-after: magictoken.interval_overload_attribute.begin
   :end-before: magictoken.interval_overload_attribute.end
   :dedent: 8

You might ask why we didn't need to expose a type inference hook for this
attribute? The answer is that ``@overload_attribute`` is part of the
high-level API: it combines type inference and code generation in a
single API.


Implementing the constructor
----------------------------

Now we want to implement the two-argument ``Interval`` constructor:

.. literalinclude:: ../../../numba/tests/doc_examples/test_interval_example.py
   :language: python
   :start-after: magictoken.interval_lower_builtin.begin
   :end-before: magictoken.interval_lower_builtin.end
   :dedent: 8


There is a bit more going on here.  ``@lower_builtin`` decorates the
implementation of the given callable or operation (here the ``Interval``
constructor) for some specific argument types.  This allows defining
type-specific implementations of a given operation, which is important
for heavily overloaded functions such as :func:`len`.

``types.Float`` is the class of all floating-point types (``types.float64``
is an instance of ``types.Float``).  It is generally more future-proof
to match argument types on their class rather than on specific instances
(however, when *returning* a type -- chiefly during the type inference
phase --, you must usually return a type instance).

``cgutils.create_struct_proxy()`` and ``interval._getvalue()`` are a bit
of boilerplate due to how Numba passes values around.  Values are passed
as instances of :class:`llvmlite.ir.Value`, which can be too limited:
LLVM structure values especially are quite low-level.  A struct proxy
is a temporary wrapper around a LLVM structure value allowing to easily
get or set members of the structure. The ``_getvalue()`` call simply
gets the LLVM value out of the wrapper.


Boxing and unboxing
-------------------

If you try to use an ``Interval`` instance at this point, you'll certainly
get the error *"cannot convert Interval to native value"*.  This is because
Numba doesn't yet know how to make a native interval value from a Python
``Interval`` instance.  Let's teach it how to do it:

.. literalinclude:: ../../../numba/tests/doc_examples/test_interval_example.py
   :language: python
   :start-after: magictoken.interval_unbox.begin
   :end-before: magictoken.interval_unbox.end
   :dedent: 8

*Unbox* is the other name for "convert a Python object to a native value"
(it fits the idea of a Python object as a sophisticated box containing
a simple native value).  The function returns a ``NativeValue`` object
which gives its caller access to the computed native value, the error bit
and possibly other information.

The snippet above makes abundant use of the ``c.pyapi`` object, which
gives access to a subset of the
`Python interpreter's C API <https://docs.python.org/3/c-api/index.html>`_.
Note the use of ``early_exit_if_null`` to detect and handle any errors that
may have happened when unboxing the object (try passing ``Interval('a', 'b')``
for example).

We also want to do the reverse operation, called *boxing*, so as to return
interval values from Numba functions:

.. literalinclude:: ../../../numba/tests/doc_examples/test_interval_example.py
   :language: python
   :start-after: magictoken.interval_box.begin
   :end-before: magictoken.interval_box.end
   :dedent: 8


Using it
""""""""

:term:`nopython mode` functions are now able to make use of Interval objects
and the various operations you have defined on them.  You can try for
example the following functions:

.. literalinclude:: ../../../numba/tests/doc_examples/test_interval_example.py
   :language: python
   :start-after: magictoken.interval_usage.begin
   :end-before: magictoken.interval_usage.end
   :dedent: 8


Conclusion
""""""""""

We have shown how to do the following tasks:

* Define a new Numba type class by subclassing the ``Type`` class
* Define a singleton Numba type instance for a non-parametric type
* Teach Numba how to infer the Numba type of Python values of a certain class,
  using ``typeof_impl.register``
* Teach Numba how to infer the Numba type of the Python type itself, using
  ``as_numba_type.register``
* Define the data model for a Numba type using ``StructModel``
  and ``register_model``
* Implementing a boxing function for a Numba type using the ``@box`` decorator
* Implementing an unboxing function for a Numba type using the ``@unbox`` decorator
  and the ``NativeValue`` class
* Type and implement a callable using the ``@type_callable`` and
  ``@lower_builtin`` decorators
* Expose a read-only structure attribute using the ``make_attribute_wrapper``
  convenience function
* Implement a read-only property using the ``@overload_attribute`` decorator