Uh oh!
There was an error while loading.Please reload this page.
- Notifications
You must be signed in to change notification settings - Fork3.1k
Mypyc Development Workflows
This page explains some common workflows for developing mypyc.
Most mypyc test cases are defined in the same format (.test) as usedfor test cases for mypy. Look at mypy developer documentation for ageneral overview of how things work. Test cases live undermypyc/test-data/, and you can run all mypyc tests viapytest -q mypyc. If you don't make changes to code undermypy/, it's notimportant to regularly run mypy tests during development.
When you create a PR, we have Continuous Integration jobs set up thatcompile mypy using mypyc and run the mypy test suite using thecompiled mypy. This will sometimes catch additional issues not caughtby the mypyc test suite. It's okay to not do this in your localdevelopment environment.
We discuss writing tests in more detail later in this document.
It's often useful to look at the generated IR when debugging issues orwhen trying to understand how mypyc compiles some code. When youcompile some module by runningmypyc, mypyc will write thepretty-printed IR intobuild/ops.txt. This is the final IR thatincludes the output from exception and reference count handlinginsertion passes.
We also have tests that verify the generate IR(mypyc/test-data/irbuild-*.text).
./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 usingpip (which is compiled)and usingdmypy (mypy daemon) is a much, much faster way to typecheck mypyc during development.
It's often useful to inspect the C code genenerate by mypyc to debugissues. Mypyc stores the generated C code asbuild/__native.c.Compiled native functions have the prefixCPyDef_, while wrapperfunctions used for calling functions from interpreted Python code havetheCPyPy_ prefix.
This section gives an overview of where to look for andwhat to do to implement specific kinds of mypyc features.
Our bread-and-butter testing strategy is compiling code with mypyc andrunning it. There are downsides to this (kind of slow, tests a hugenumber of components at once, insensitive to the particular details ofthe IR), but there really is no substitute for running code. You canalso write tests that test the generated IR, however.
Test cases that compile and run code are located inmypyc/test-data/run*.test and the test runner is inmypyc.test.test_run. The code to compile comes after[case test<name>]. The code gets saved into the filenative.py, and itgets compiled into the modulenative.
Each test case uses a non-compiled Python driver that imports thenative module and typically calls some compiled functions. Sometests also perform assertions and print messages in the driver.
If you don't provide a driver, a default driver is used. The defaultdriver just calls each module-level function that is prefixed withtest_ and reports any uncaught exceptions as failures. (Failure tobuild or a segfault also count as failures.)testStringOps inmypyc/test-data/run-strings.test is an example of a test that usesthe default driver.
You should usually use the default driver (don't includedriver.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 andtest_concat_empty_lists. Note that you canuse the pytest -k argument to only runtestConcetanateLists, but youcan't filter tests at the sub-case level.
It's recommended to have multiple sub-cases per test case, since eachtest case has significant fixed overhead. Each test case is run in afresh Python subprocess.
Many of the existing test cases provide a custom driver by having[file driver.py], followed by the driver implementation. Here thedriver is not compiled, which is useful if you want to testinteractions between compiled and non-compiled code. However, many ofthe tests don't have a good reason to use a custom driver -- when theywere written, the default driver wasn't available.
Test cases can also have a[out] section, which specifies theexpected contents of stdout the test case should produce. New testcases should prefer assert statements to[out] sections.
If the specifics of the generated IR of a change is important(because, for example, you want to make sure a particular optimizationis triggering), you should add amypyc.irbuild test as well. Testcases are located inmypyc/test-data/irbuild-*.test and the testdriver is inmypyc.test.test_irbuild. IR build tests do a directcomparison of the IR output, so try to make the test as targeted aspossible so as to capture only the important details. (Many of ourexisting IR build tests do not follow this advice, unfortunately!)
If you pass the--update-data flag to pytest, it will automaticallyupdate the expected output of any tests to match the actualoutput. 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 forbuiltins during tests (mypyc/test-data/fixtures/ir.py). We don't usefull typeshed stubs to run tests since they would seriously slow downtests.
Many mypyc improvements attempt to make some operations faster. Forany such change, you should run some measurements to verify thatthere actually is a measurable performance impact.
A typical benchmark would initialize some data to be operated on, andthen measure time spent in some function. In particular, you shouldnot measure time needed to run the entire benchmark program, as thiswould include Python startup overhead and other things that aren'trelevant. In general, for microbenchmarks, you want to do as little aspossible in the timed portion. So ideally you'll just have some loopsand the code under test. Be ready to provide your benchmark in codereview 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 andchanged versions, ignore outliers, and report the averageruntime. Actual performance of a typical desktop or laptop computer isquite variable, due to dynamic CPU clock frequency changes, backgroundprocesses, etc. If you observe a high variance in timings, you'll needto 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 Afollowed by 10 runs with variant B, but run an A run, a B run, an Arun, etc. Otherwise you risk that the CPU frequency will be differentbetween variants. You can also try adding a delay of 5 to 20s betweenruns to avoid CPU frequency changes.
Instead of averaging over many measurements, you can try to adjustyour environment to provide more stable measurements. However, thiscan be hard to do with some hardware, including many laptops. VictorStinner has written a series of blog posts about making measurementsstable:
- https://vstinner.github.io/journey-to-stable-benchmark-system.html
- https://vstinner.github.io/journey-to-stable-benchmark-average.html
If you add an operation that compiles into a lot of C code, you mayalso want to add a C helper function for the operation to make thegenerated code smaller. Here is how to do this:
Declare the operation in
mypyc/lib-rt/CPy.h. We avoid macros, andwe generally avoid inline functions to make it easier to targetadditional backends in the future.Consider adding a unit test for your C helper in
mypyc/lib-rt/test_capi.cc.We useGoogle Test for writingtests in C++. The framework is included in the repository under thedirectorygoogletest/. The C unit tests are run as part of thepytest test suite (test_c_unit_test).
Mypyc speeds up operations on primitive types such aslist andintby having primitive operations specialized for specific types. Theseoperations are declared inmypyc.primitives (andmypyc/lib-rt/CPy.h). For example,mypyc.primitives.list_opscontains primitives that target list objects.
The operation definitions are data driven: you specify the kind ofoperation (such as a call tobuiltins.len or a binary addition) andthe operand types (such aslist_primitive), and what code should begenerated for the operation. Mypyc does AST matching to find the mostsuitable primitive operation automatically.
Look at the existing primitive definitions and the docstrings inmypyc.primitives.registry for examples and more information.
Some types (typically Python Python built-in types), such asint andlist, are special cased in mypyc to generate optimized operationsspecific to these types. We'll occasionally want to add additionalprimitive 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 (arepresentation that is not just
PyObject *). For most types we'lluse a boxed representation, as it's easier to implement and moreclosely matches Python semantics.Create a new instance of
RPrimitiveto support the primitive typeand add it tomypyc.ir.rtypes. Make sure all the attributes areset correctly and also define<foo>_rprimitiveandis_<foo>_rprimitive.Update
mypyc.irbuild.mapper.Mapper.type_to_rtype().If the type is not unboxed, update
emit_castinmypyc.codegen.emit.
If the type is unboxed, there are some additional steps:
Update
emit_boxinmypyc.codegen.emit.Update
emit_unboxinmypyc.codegen.emit.Update
emit_inc_refandemit_dec_refinmypypc.codegen.emit.If the unboxed representation does not need reference counting,these can be no-ops.Update
emit_error_checkinmypyc.codegen.emit.Update
emit_gc_visitandemit_gc_clearinmypyc.codegen.emitif the type has an unboxed representation with pointers.
The above may be enough to allow you to declare variables with thetype, pass values around, perform runtime type checks, and use genericfallback primitive operations to perform method calls, binaryoperations, and so on. You likely also want to add some faster,specialized primitive operations for the type (see Adding aSpecialized Primitive Operation above for how to do this).
Add a test case tomypyc/test-data/run*.test to test compilation andrunning 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 withincompiled code and from regular Python code. Also test thisfor return values.
Test using the type as list item type. Test both getting a list itemand setting a list item.
Mypyc supports most Python syntax, but there are still some gaps.
Support for syntactic sugar that doesn't need additional IR operationstypically only requires changes tomypyc.irbuild.
Some new syntax also needs new IR primitives to be added tomypyc.primitives. Seemypyc.primitives.registry for documentationabout how to do this.
This developer documentation is not aimed to be very complete. Muchof our documentation is in comments and docstring in the code. Ifsomething is unclear, study the code.
It can be useful to look through some recent PRs to get an idea ofwhat typical code changes, test cases, etc. look like.
Feel free to open GitHub issues with questions if you need help whencontributing, or ask questions in existing issues. Note that we onlysupport contributors. Mypyc is not (yet) an end-user product. Youcan also ask questions in our Gitter chat(https://gitter.im/mypyc-dev/community).
These workflows would be useful for mypyc contributors. We should addthem to mypyc developer documentation:
- How to inspect the generated IR before some transform passes.