circuitpython-build-tools
circuitpython-build-tools copied to clipboard
Add advanced source transformations to reduce type checking overhead
Add advanced source transformations to reduce type checking overhead
The new 'munge' module performs transformations on the source code. It uses the AST (abstract syntax tree) representation of Python code to recognize some idioms such as if STATIC_TYPING: and transforms them into alternatives that have zero overhead in mpy-compiled files (e.g., if STATIC_TYPING: is transformed into if 0:, which is eliminated at compile time due to mpy-cross constant-propagation and dead branch elimination)
The code assumes the input file is black-formatted. In particular, it would malfunction if an if-statement and its body are on the same line: if STATIC_TYPING: print("boo") would be incorrectly munged.
This fails on the community bundle due to invalid syntax that is accepted by circuitpython but not python3. Before now, library code was not actually required to be valid python3.
I filed a PR with the one affected lib but it's probably not likely to see timely action: https://github.com/dastels/circuitPython_dotstar_featherwing/pull/1/files
Neat!
Writing zero-overhead type checking for CircuitPython
We love type hints! They improve documentation as well as developer experience in Python-aware IDEs. Unfortunately, these additions can sometimes increase the size of the "mpy" files as well as runtime RAM usage. However, together with recent improvements in circuitpython-build-tools, almost all of the overhead can be eliminated during the bundle build process, providing that the type checks are written in the correct style.
Now, circuitpython-build-tools rewriting (or "munging") process does some specified transformations at the top level of the file (not inside functions or nested inside if/try/etc blocks):
from __future__ import ...is removed anywhere it appears- A
try/exceptblock that tries to import from typing (import typing,import typing as ...,from typing import ...) as its first statement is removed, but its firstexcept:(orexcept ImportErrororexcept Exception) block is executed in its place as anif 1:block - An
iftestingsys.implementation.namefor equality/inequality with"circuitpython"is transformed intoif 1:orif 0: - Testing
if TYPE_CHECKINGis transformed toif 0:
The mpy-cross process, or the byte-compiling process on a circuitpython device, can intelligently avoid doing most work within if <constant>: blocks, including not permanently storing string identifiers used only within the blocks or including them in the mpy file.
OK, given those transformations, what's the proper way to write type hints:
- If desired, use
from __future__ import annotationsat the top of your file. Anyfrom __future__ importstatement is eliminated. - Place all typing-related imports in a single try/except block that begins
import typing,import typing as ..., orfrom typing import ... - Have an
except ImportError:block that either just sayspassor, if you must refer toTYPE_CHECKINGelsewhere, it should sayTYPE_CHECKING=const(0). (no need tofrom micropython import const) - use
if TYPE_CHECKING:freely
Almost all type annotations can be modified to follow the above rules.
The only important thing that the author knows of that is not zero-overhead is providing a do-nothing implementation of typing.cast which returns its val argument, which costs just a few bytes of bytecode.
This looks very nice. I have a worry: will this change the line numbers reported when there is an exception in the .mpy file, etc?
No, the code takes care to preserve line numbers.
the test case (note the lines are cut off):
2 |
3 try: |
4 from typing import TYPE_CHECK |
5 except ImportError: | if 1:
6 pass | pass
7 |
8 try: |
9 from typing import TYPE_CHECK |
10 except ImportError: | if 1:
11 pass | pass
12 |
13 |
14 try: |
15 import typing |
16 except: | if 1:
17 pass | pass
18 |
19 try: |
20 import typing as T |
21 except: | if 1:
22 pass | pass
23 |
24 __version__ = "0.0.0-auto" | __version__ = "1.2.3"
25 |
26 if sys.implementation.name == "ci | if 1:
27 print("is circuitpython") | print("is circuitpython")
28 |
29 if sys.implementation.name != "ci | if 0:
30 print("not circuitpython (1)" | print("not circuitpython (1)"
31 |
32 if not sys.implementation.name == | if 0:
33 print("not circuitpython (2)" | print("not circuitpython (2)"
It's lengthy, but here are the diffs for the adafruit bundle: https://gist.githubusercontent.com/jepler/8e9e477e84d65a81da36aa0db2eb4864/raw/db8354aea74f313e6e97b7543a06986cc59980ac/munge_changes.patch
It's nigh impossible to review that much repetitive junk but I can say I didn't spot anything problematic at least
Usage: circuitpython-munge [OPTIONS] INPUT [OUTPUT]
Options:
--diff / --no-diff
--munged-version TEXT
--help Show this message and exit.
the core will need to learn how to "munge" files when it builds in frozen modules. the circuitpython-munge script would be helpful for that.
I forgot to follow up on this. Do we need a bunch of fix-ups on libraries, especially community ones, before we merge this?
I did my best to spot check the results, and didn't find any erroneous transformations. However, it's entirely possible there are some and I just didn't see them.
It's possible some libraries (both adafruit bundle & community bundle) can be improved to follow one of these specific formats but that can happen second.
If this waits until I return that's fine as well.
@jepler I never re-reviewed this, sorry. It needs some merge-conflict fixing. But it's still viable, right?
Updated with main branch!
I'd love to see this happen someday, but it's anything I'm going to be promoting right now. It's good to know there are some things we can potentially do to slightly lower mpy file size when it's needed. but this needs extensive testing that's not going to occur right now.