usse/funda-scraper/venv/lib/python3.10/site-packages/mypyc/doc/dev-intro.md

549 lines
22 KiB
Markdown
Raw Normal View History

2023-02-20 22:38:24 +00:00
# Introduction for Mypyc Contributors
This is a short introduction aimed at anybody who is interested in
contributing to mypyc, or anybody who is curious to understand how
mypyc works internally.
## Key Differences from Python
Code compiled using mypyc is often much faster than CPython since it
does these things differently:
* Mypyc generates C that is compiled to native code, instead of
compiling to interpreted byte code, which CPython uses. Interpreted
byte code always has some interpreter overhead, which slows things
down.
* Mypyc doesn't let you arbitrarily monkey patch classes and functions
in compiled modules. This allows *early binding* -- mypyc
statically binds calls to compiled functions, instead of going
through a namespace dictionary. Mypyc can also call methods of
compiled classes using vtables, which are more efficient than
dictionary lookups used by CPython.
* Mypyc compiles classes to C extension classes, which are generally
more efficient than normal Python classes. They use an efficient,
fixed memory representation (essentially a C struct). This lets us
use direct memory access instead of (typically) two hash table
lookups to access an attribute.
* As a result of early binding, compiled code can use C calls to call
compiled functions. Keyword arguments can be translated to
positional arguments during compilation. Thus most calls to native
functions and methods directly map to simple C calls. CPython calls
are quite expensive, since mapping of keyword arguments, `*args`,
and so on has to mostly happen at runtime.
* Compiled code has runtime type checks to ensure that runtimes types
match the declared static types. Compiled code can thus make
assumptions about the types of expressions, resulting in both faster
and smaller code, since many runtime type checks performed by the
CPython interpreter can be omitted.
* Compiled code can often use unboxed (not heap allocated)
representations for integers, booleans and tuples.
## Supported Python Features
Mypyc supports a large subset of Python. Note that if you try to
compile something that is not supported, you may not always get a very
good error message.
Here are some major things that aren't yet supported in compiled code:
* Many dunder methods (only some work, such as `__init__` and `__eq__`)
* Monkey patching compiled functions or classes
* General multiple inheritance (a limited form is supported)
* Named tuple defined using the class-based syntax
* Defining protocols
We are generally happy to accept contributions that implement new Python
features.
## Development Environment
First you should set up the mypy development environment as described in
the [mypy docs](https://github.com/python/mypy/blob/master/README.md).
macOS, Linux and Windows are supported.
## Compiling and Running Programs
When working on a mypyc feature or a fix, you'll often need to run
compiled code. For example, you may want to do interactive testing or
to run benchmarks. This is also handy if you want to inspect the
generated C code (see Inspecting Generated C).
Run `mypyc` to compile a module to a C extension using your
development version of mypyc:
```
$ mypyc program.py
```
This will generate a C extension for `program` in the current working
directory. For example, on a Linux system the generated file may be
called `program.cpython-37m-x86_64-linux-gnu.so`.
Since C extensions can't be run as programs, use `python3 -c` to run
the compiled module as a program:
```
$ python3 -c "import program"
```
Note that `__name__` in `program.py` will now be `program`, not
`__main__`!
You can manually delete the C extension to get back to an interpreted
version (this example works on Linux):
```
$ rm program.*.so
```
Another option is to invoke mypyc through tests (see Testing below).
## High-level Overview of Mypyc
Mypyc compiles a Python module (or a set of modules) to C, and
compiles the generated C to a Python C extension module (or
modules). You can compile only a subset of your program to C --
compiled and interpreted code can freely and transparently
interact. You can also freely use any Python libraries (including C
extensions) in compiled code.
Mypyc will only make compiled code faster. To see a significant
speedup, you must make sure that most of the time is spent in compiled
code -- and not in libraries, for example.
Mypyc has these passes:
* Type check the code using mypy and infer types for variables and
expressions. This produces a mypy AST (defined in `mypy.nodes`) and
a type map that describes the inferred types (`mypy.types.Type`) of
all expressions (as PEP 484 types).
* Translate the mypy AST into a mypyc-specific intermediate representation (IR).
* The IR is defined in `mypyc.ir` (see below for an explanation of the IR).
* Various primitive operations used in the IR are defined in `mypyc.primitives`.
* The translation to IR happens in `mypyc.irbuild`. The top-level logic is in
`mypyc.irbuild.main`.
* Insert checks for uses of potentially uninitialized variables
(`mypyc.transform.uninit`).
* Insert exception handling (`mypyc.transform.exceptions`).
* Insert explicit reference count inc/dec opcodes (`mypyc.transform.refcount`).
* Translate the IR into C (`mypyc.codegen`).
* Compile the generated C code using a C compiler (`mypyc.build`).
## Useful Background Information
Beyond the mypy documentation, here are some things that are helpful to
know for mypyc contributors:
* Experience with C
([The C Programming Language](https://en.wikipedia.org/wiki/The_C_Programming_Language)
is a classic book about C)
* Basic familiarity with the Python C API (see
[Python C API documentation](https://docs.python.org/3/c-api/intro.html)). [Extending and Embedding the Python Interpreter](https://docs.python.org/3/extending/index.html) is a good tutorial for beginners.
* Basics of compilers (see the
[mypy wiki](https://github.com/python/mypy/wiki/Learning-Resources)
for some ideas)
## Mypyc Intermediate Representation (IR)
The mypyc IR is defined in `mypyc.ir`. It covers several key concepts
that are essential to understand by all mypyc contributors:
* `mypyc.ir.ops.Op` is an Abstract Base Class for all IR
operations. These are low-level and generally map to simple
fragments of C each. Mypy expressions are translated to
linear sequences of these ops.
* `mypyc.ir.ops.BasicBlock` is a container of a sequence of ops with a
branch/goto/return at the end, and no branch/goto/return ops in the
middle. Each function is compiled to a bunch of basic blocks.
* `mypyc.ir.rtypes.RType` and its subclasses are the types used for
everything in the IR. These are lower-level and simpler than mypy or
PEP 484 types. For example, there are no general-purpose generic
types types here. Each `List[X]` type (for any `X`) is represented
by a single `list` type, for example.
* Primitive types are special RTypes of which mypyc has some special
understanding, and there are typically some specialized
ops. Examples include `int` (referred to as `int_rprimitive` in the
code) and `list` (`list_rprimitive`). Python types for which there
is no specific RType type will be represented by the catch-all
`object_rprimitive` type.
* Instances of compiled classes are generally represented using the
`RInstance` type. Classes are compiled to C extension classes and
contain vtables for fast method calls and fast attribute access.
* IR representations of functions and classes live in
`mypyc.ir.func_ir` and `mypyc.ir.class_ir`, respectively.
Look at the docstrings and comments in `mypyc.ir` for additional
information. See the test cases in
`mypyc/test-data/irbuild-basic.test` for examples of what the IR looks
like in a pretty-printed form.
## Testing overview
Most mypyc test cases are defined in the same format (`.test`) as used
for test cases for mypy. Look at mypy developer documentation for a
general overview of how things work. Test cases live under
`mypyc/test-data/`, and you can run all mypyc tests via `pytest
-q mypyc`. If you don't make changes to code under `mypy/`, it's not
important to regularly run mypy tests during development.
When you create a PR, we have Continuous Integration jobs set up that
compile mypy using mypyc and run the mypy test suite using the
compiled mypy. This will sometimes catch additional issues not caught
by the mypyc test suite. It's okay to not do this in your local
development environment.
We discuss writing tests in more detail later in this document.
## Inspecting Generated IR
It's often useful to look at the generated IR when debugging issues or
when trying to understand how mypyc compiles some code. When you
compile some module by running `mypyc`, mypyc will write the
pretty-printed IR into `build/ops.txt`. This is the final IR that
includes the output from exception and reference count handling
insertion passes.
We also have tests that verify the generate IR
(`mypyc/test-data/irbuild-*.text`).
## Type-checking Mypyc
`./runtests.py self` type checks mypy and mypyc. This is pretty slow,
however, since it's using an uncompiled mypy.
Installing a released version of mypy using `pip` (which is compiled)
and using `dmypy` (mypy daemon) is a much, much faster way to type
check mypyc during development.
## Value Representation
Mypyc uses a tagged pointer representation for values of type `int`
(`CPyTagged`), `char` for booleans, and C structs for tuples. For most
other objects mypyc uses the CPython `PyObject *`.
Python integers that fit in 31/63 bits (depending on whether we are on
a 32-bit or 64-bit platform) are represented as C integers
(`CPyTagged`) shifted left by 1. Integers that don't fit in this
representation are represented as pointers to a `PyObject *` (this is
always a Python `int` object) with the least significant bit
set. Tagged integer operations are defined in `mypyc/lib-rt/int_ops.c`
and `mypyc/lib-rt/CPy.h`.
There are also low-level integer types, such as `int32` (see
`mypyc.ir.rtypes`), that don't use the tagged representation. These
types are not exposed to users, but they are used in generated code.
## Overview of Generated C
Mypyc compiles a function into two functions, a native function and
a wrapper function:
* The native function takes a fixed number of C arguments with the
correct C types. It assumes that all argument have correct types.
* The wrapper function conforms to the Python C API calling convention
and takes an arbitrary set of arguments. It processes the arguments,
checks their types, unboxes values with special representations and
calls the native function. The return value from the native function
is translated back to a Python object ("boxing").
Calls to other compiled functions don't go through the Python module
namespace but directly call the target native C function. This makes
calls very fast compared to CPython.
The generated code does runtime checking so that it can assume that
values always have the declared types. Whenever accessing CPython
values which might have unexpected types we need to insert a runtime
type check operation. For example, when getting a list item we need to
insert a runtime type check (an unbox or a cast operation), since
Python lists can contain arbitrary objects.
The generated code uses various helpers defined in
`mypyc/lib-rt/CPy.h`. The implementations are in various `.c` files
under `mypyc/lib-rt`.
## Inspecting Generated C
It's often useful to inspect the C code genenerate by mypyc to debug
issues. Mypyc stores the generated C code as `build/__native.c`.
Compiled native functions have the prefix `CPyDef_`, while wrapper
functions used for calling functions from interpreted Python code have
the `CPyPy_` prefix.
## Other Important Limitations
All of these limitations will likely be fixed in the future:
* We don't detect stack overflows.
* We don't handle Ctrl-C in compiled code.
## Hints for Implementing Typical Mypyc Features
This section gives an overview of where to look for and
what to do to implement specific kinds of mypyc features.
### Testing
Our bread-and-butter testing strategy is compiling code with mypyc and
running it. There are downsides to this (kind of slow, tests a huge
number of components at once, insensitive to the particular details of
the IR), but there really is no substitute for running code. You can
also write tests that test the generated IR, however.
### Tests that compile and run code
Test cases that compile and run code are located in
`mypyc/test-data/run*.test` and the test runner is in
`mypyc.test.test_run`. The code to compile comes after `[case
test<name>]`. The code gets saved into the file `native.py`, and it
gets compiled into the module `native`.
Each test case uses a non-compiled Python driver that imports the
`native` module and typically calls some compiled functions. Some
tests also perform assertions and print messages in the driver.
If you don't provide a driver, a default driver is used. The default
driver just calls each module-level function that is prefixed with
`test_` and reports any uncaught exceptions as failures. (Failure to
build or a segfault also count as failures.) `testStringOps` in
`mypyc/test-data/run-strings.test` is an example of a test that uses
the default driver.
You should usually use the default driver (don't include
`driver.py`). It's the simplest way to write most tests.
Here's an example test case that uses the default driver:
```
[case testConcatenateLists]
def test_concat_lists() -> None:
assert [1, 2] + [5, 6] == [1, 2, 5, 6]
def test_concat_empty_lists() -> None:
assert [] + [] == []
```
There is one test case, `testConcatenateLists`. It has two sub-cases,
`test_concat_lists` and `test_concat_empty_lists`. Note that you can
use the pytest -k argument to only run `testConcetanateLists`, but you
can't filter tests at the sub-case level.
It's recommended to have multiple sub-cases per test case, since each
test case has significant fixed overhead. Each test case is run in a
fresh Python subprocess.
Many of the existing test cases provide a custom driver by having
`[file driver.py]`, followed by the driver implementation. Here the
driver is not compiled, which is useful if you want to test
interactions between compiled and non-compiled code. However, many of
the tests don't have a good reason to use a custom driver -- when they
were written, the default driver wasn't available.
Test cases can also have a `[out]` section, which specifies the
expected contents of stdout the test case should produce. New test
cases should prefer assert statements to `[out]` sections.
### IR tests
If the specifics of the generated IR of a change is important
(because, for example, you want to make sure a particular optimization
is triggering), you should add a `mypyc.irbuild` test as well. Test
cases are located in `mypyc/test-data/irbuild-*.test` and the test
driver is in `mypyc.test.test_irbuild`. IR build tests do a direct
comparison of the IR output, so try to make the test as targeted as
possible so as to capture only the important details. (Many of our
existing IR build tests do not follow this advice, unfortunately!)
If you pass the `--update-data` flag to pytest, it will automatically
update the expected output of any tests to match the actual
output. This is very useful for changing or creating IR build tests,
but make sure to carefully inspect the diff!
You may also need to add some definitions to the stubs used for
builtins during tests (`mypyc/test-data/fixtures/ir.py`). We don't use
full typeshed stubs to run tests since they would seriously slow down
tests.
### Benchmarking
Many mypyc improvements attempt to make some operations faster. For
any such change, you should run some measurements to verify that
there actually is a measurable performance impact.
A typical benchmark would initialize some data to be operated on, and
then measure time spent in some function. In particular, you should
not measure time needed to run the entire benchmark program, as this
would include Python startup overhead and other things that aren't
relevant. In general, for microbenchmarks, you want to do as little as
possible in the timed portion. So ideally you'll just have some loops
and the code under test. Be ready to provide your benchmark in code
review so that mypyc developers can check that the benchmark is fine
(writing a good benchmark is non-trivial).
You should run a benchmark at least five times, in both original and
changed versions, ignore outliers, and report the average
runtime. Actual performance of a typical desktop or laptop computer is
quite variable, due to dynamic CPU clock frequency changes, background
processes, etc. If you observe a high variance in timings, you'll need
to run the benchmark more times. Also try closing most applications,
including web browsers.
Interleave original and changed runs. Don't run 10 runs with variant A
followed by 10 runs with variant B, but run an A run, a B run, an A
run, etc. Otherwise you risk that the CPU frequency will be different
between variants. You can also try adding a delay of 5 to 20s between
runs to avoid CPU frequency changes.
Instead of averaging over many measurements, you can try to adjust
your environment to provide more stable measurements. However, this
can be hard to do with some hardware, including many laptops. Victor
Stinner has written a series of blog posts about making measurements
stable:
* https://vstinner.github.io/journey-to-stable-benchmark-system.html
* https://vstinner.github.io/journey-to-stable-benchmark-average.html
### Adding C Helpers
If you add an operation that compiles into a lot of C code, you may
also want to add a C helper function for the operation to make the
generated code smaller. Here is how to do this:
* Declare the operation in `mypyc/lib-rt/CPy.h`. We avoid macros, and
we generally avoid inline functions to make it easier to target
additional backends in the future.
* Consider adding a unit test for your C helper in `mypyc/lib-rt/test_capi.cc`.
We use
[Google Test](https://github.com/google/googletest) for writing
tests in C++. The framework is included in the repository under the
directory `googletest/`. The C unit tests are run as part of the
pytest test suite (`test_c_unit_test`).
### Adding a Specialized Primitive Operation
Mypyc speeds up operations on primitive types such as `list` and `int`
by having primitive operations specialized for specific types. These
operations are declared in `mypyc.primitives` (and
`mypyc/lib-rt/CPy.h`). For example, `mypyc.primitives.list_ops`
contains primitives that target list objects.
The operation definitions are data driven: you specify the kind of
operation (such as a call to `builtins.len` or a binary addition) and
the operand types (such as `list_primitive`), and what code should be
generated for the operation. Mypyc does AST matching to find the most
suitable primitive operation automatically.
Look at the existing primitive definitions and the docstrings in
`mypyc.primitives.registry` for examples and more information.
### Adding a New Primitive Type
Some types (typically Python Python built-in types), such as `int` and
`list`, are special cased in mypyc to generate optimized operations
specific to these types. We'll occasionally want to add additional
primitive types.
Here are some hints about how to add support for a new primitive type
(this may be incomplete):
* Decide whether the primitive type has an "unboxed" representation (a
representation that is not just `PyObject *`). For most types we'll
use a boxed representation, as it's easier to implement and more
closely matches Python semantics.
* Create a new instance of `RPrimitive` to support the primitive type
and add it to `mypyc.ir.rtypes`. Make sure all the attributes are
set correctly and also define `<foo>_rprimitive` and
`is_<foo>_rprimitive`.
* Update `mypyc.irbuild.mapper.Mapper.type_to_rtype()`.
* If the type is not unboxed, update `emit_cast` in `mypyc.codegen.emit`.
If the type is unboxed, there are some additional steps:
* Update `emit_box` in `mypyc.codegen.emit`.
* Update `emit_unbox` in `mypyc.codegen.emit`.
* Update `emit_inc_ref` and `emit_dec_ref` in `mypypc.codegen.emit`.
If the unboxed representation does not need reference counting,
these can be no-ops.
* Update `emit_error_check` in `mypyc.codegen.emit`.
* Update `emit_gc_visit` and `emit_gc_clear` in `mypyc.codegen.emit`
if the type has an unboxed representation with pointers.
The above may be enough to allow you to declare variables with the
type, pass values around, perform runtime type checks, and use generic
fallback primitive operations to perform method calls, binary
operations, and so on. You likely also want to add some faster,
specialized primitive operations for the type (see Adding a
Specialized Primitive Operation above for how to do this).
Add a test case to `mypyc/test-data/run*.test` to test compilation and
running compiled code. Ideas for things to test:
* Test using the type as an argument.
* Test using the type as a return value.
* Test passing a value of the type to a function both within
compiled code and from regular Python code. Also test this
for return values.
* Test using the type as list item type. Test both getting a list item
and setting a list item.
### Supporting More Python Syntax
Mypyc supports most Python syntax, but there are still some gaps.
Support for syntactic sugar that doesn't need additional IR operations
typically only requires changes to `mypyc.irbuild`.
Some new syntax also needs new IR primitives to be added to
`mypyc.primitives`. See `mypyc.primitives.registry` for documentation
about how to do this.
### Other Hints
* This developer documentation is not aimed to be very complete. Much
of our documentation is in comments and docstring in the code. If
something is unclear, study the code.
* It can be useful to look through some recent PRs to get an idea of
what typical code changes, test cases, etc. look like.
* Feel free to open GitHub issues with questions if you need help when
contributing, or ask questions in existing issues. Note that we only
support contributors. Mypyc is not (yet) an end-user product. You
can also ask questions in our Gitter chat
(https://gitter.im/mypyc-dev/community).
## Undocumented Workflows
These workflows would be useful for mypyc contributors. We should add
them to mypyc developer documentation:
* How to inspect the generated IR before some transform passes.