astpp 0.1.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,258 @@
1
+ Metadata-Version: 2.4
2
+ Name: astpp
3
+ Version: 0.1.0
4
+ Summary: Self-Inlining Python Macro Preprocessor & Runtime Polyfill Library
5
+ Keywords: ast,preprocessor,micropython,pypy,macros,inlining,const
6
+ Author: f-fix
7
+ Requires-Python: >=3.8
8
+ Description-Content-Type: text/markdown
9
+ Classifier: Development Status :: 4 - Beta
10
+ Classifier: Intended Audience :: Developers
11
+ Classifier: License :: OSI Approved :: MIT License
12
+ Classifier: Programming Language :: Python :: 3
13
+ Classifier: Programming Language :: Python :: Implementation :: CPython
14
+ Classifier: Programming Language :: Python :: Implementation :: PyPy
15
+ Classifier: Programming Language :: Python :: Implementation :: MicroPython
16
+ Classifier: Topic :: Software Development :: Code Generators
17
+ Classifier: Topic :: Software Development :: Compilers
18
+ Classifier: Topic :: Software Development :: Pre-processors
19
+ License-File: LICENSE
20
+ Project-URL: Homepage, https://github.com/f-fix/astpp
21
+ Project-URL: Issues, https://github.com/f-fix/astpp/issues
22
+ Project-URL: Repository, https://github.com/f-fix/astpp
23
+
24
+ # astpp
25
+
26
+ **Self-Inlining Python Macro Preprocessor & Runtime Polyfill Library**
27
+
28
+ Project Homepage: [https://github.com/f-fix/astpp](https://github.com/f-fix/astpp)
29
+
30
+ ---
31
+
32
+ ## Overview
33
+
34
+ `astpp` is a dual-use Python library and AST-level macro preprocessor designed for cross-runtime codebases targeting **MicroPython**, **PyPy**, and **CPython**:
35
+
36
+ 1. **Runtime Polyfill (Zero-Build Development)**:
37
+ Importing `astpp` provides zero-overhead, runtime-compatible implementations of MicroPython compiler pragmas (`const`, `micropython.native`, `micropython.viper`) and conditional decorators (`keep`, `omit`, `keep_if`, `omit_if`). Scripts run directly under CPython, PyPy, or MicroPython without requiring an explicit compilation or preprocessing step.
38
+
39
+ 2. **Compile-Time Preprocessor (Zero-Dependency Production Builds)**:
40
+ When invoked as a script or via `astpp_main()`, `astpp` inspects the consumer's Abstract Syntax Tree (AST), removes `from astpp import ...` statements completely, and inlines minimal self-contained fallback definitions copied directly from its own source code. The emitted code has **zero external runtime dependencies** on `astpp.py`.
41
+
42
+ ## Installation
43
+
44
+ ### Via `pip` (CPython / PyPy / Host PC)
45
+
46
+ ```bash
47
+ # Install from PyPI (when published):
48
+ pip install astpp
49
+
50
+ # Or install directly from GitHub:
51
+ pip install git+https://github.com/f-fix/astpp.git
52
+
53
+ # Or install locally in editable development mode:
54
+ pip install -e .
55
+ ```
56
+
57
+ ### Via `mip` (MicroPython)
58
+
59
+ ```python
60
+ # In MicroPython REPL (on Wi-Fi / Ethernet enabled board):
61
+ import mip
62
+ mip.install("github:f-fix/astpp")
63
+ ```
64
+
65
+ ```bash
66
+ # Or from your host computer over USB/UART via mpremote:
67
+ mpremote mip install github:f-fix/astpp
68
+ ```
69
+
70
+ ---
71
+
72
+ ---
73
+
74
+ ## Key Features
75
+
76
+ - **Macro Definitions (`-DNAME=VALUE`)**:
77
+ Rewrites target assignments `NAME = ...` to `NAME = const(VALUE)`. Inlined constants are propagated directly into expressions throughout the module.
78
+ - **Dead-Branch Elimination**:
79
+ Resolves `if`, `elif`, and `else` branches whose conditions evaluate to compile-time constants. Dead branches are pruned from the AST, while active branches are promoted or spliced without leaving ghost `pass` statements.
80
+ - **Symmetric Conditional Decorators (`@keep`, `@omit`, `@keep_if`, `@omit_if`)**:
81
+ - `@keep`: Preserves the definition and strips the decorator (zero runtime overhead).
82
+ - `@omit`: Drops the decorated function or class from the parse tree completely.
83
+ - `@keep_if(cond)` / `@omit_if(cond)`: Evaluates compile-time conditions and resolves symmetrically to `@keep` or `@omit`.
84
+ - **Compiler Optimization Parity (`-O`, `-OO`, `-O0`–`-O3`)**:
85
+ - **Default / `-O0`**: Preserves `__debug__` and assertions for downstream runtime evaluation.
86
+ - **`-O` / `-O1`**: Sets `__debug__ = False` and purges all `assert` statements.
87
+ - **`-OO` / `-O2`**: Purges assertions and strips docstrings from modules, classes, and functions.
88
+ - **`-O3`**: Full constant propagation, dead branch pruning, and inlining.
89
+ - **Self-Contained Testing (`--test`)**:
90
+ Executes built-in regression unit tests that parse and validate the documented examples directly from the module docstring.
91
+
92
+ ---
93
+
94
+ ## Command-Line Usage
95
+
96
+ ```bash
97
+ # Transform via stdin / stdout filter:
98
+ python3 astpp.py [-O[n]] [-DNAME=VAL ...] < input.py > output.py
99
+
100
+ # Transform using named input and output files:
101
+ python3 astpp.py -O3 -D_IS_DESKTOP_PY=False input.py -o output.py
102
+
103
+ # Display rich formatted help:
104
+ python3 astpp.py --help
105
+
106
+ # Run self-contained test suite:
107
+ python3 astpp.py --test
108
+ ```
109
+
110
+ ### CLI Options
111
+
112
+ ```text
113
+ usage: astpp [-h] [-o OUTPUT] [-D NAME=VALUE] [-O [LEVEL]] [-OO] [--test]
114
+ [input]
115
+
116
+ Self-Inlining Macro Preprocessor & Runtime Polyfill Library.
117
+
118
+ positional arguments:
119
+ input Input Python source file or '-' for stdin (default: '-')
120
+
121
+ options:
122
+ -h, --help Show this help message and exit
123
+ -o OUTPUT, --output OUTPUT
124
+ Output destination file or '-' for stdout (default: '-')
125
+ -D NAME=VALUE Define macro replacement or override constant (e.g. -DNAME=VAL or -DNAME)
126
+ -O [LEVEL] Optimization level: -O (strip asserts, force __debug__=False),
127
+ -OO or -O2 (strip docstrings), -O3 (full)
128
+ -OO Optimization level 2: strip assertions, __debug__=False, and docstrings
129
+ --test Run self-contained unit test suite
130
+ ```
131
+
132
+ ---
133
+
134
+ ## Programmatic API
135
+
136
+ ```python
137
+ from astpp import astpp_main
138
+ import io
139
+
140
+ source = """
141
+ from astpp import const
142
+ BUFFER_SIZE = const(1024)
143
+ """
144
+
145
+ in_stream = io.StringIO(source)
146
+ out_stream = io.StringIO()
147
+
148
+ rc = astpp_main(["-O3", "-DBUFFER_SIZE=512"], in_stream, out_stream)
149
+ assert rc == 0
150
+ print(out_stream.getvalue())
151
+ ```
152
+
153
+ ---
154
+
155
+ ## Documented & Tested Transformation Examples
156
+
157
+ ### Example 1: Import Inlining & Self-Parsing
158
+ **Input**:
159
+ ```python
160
+ from astpp import const
161
+ X = const(10)
162
+ ```
163
+ **Flags**: `-O3`
164
+ **Output**:
165
+ ```python
166
+ try:
167
+ from micropython import const
168
+ except ImportError:
169
+
170
+ def const(x):
171
+ return x
172
+ X = const(10)
173
+ ```
174
+
175
+ ### Example 2: Constant Definition & In-Line Override
176
+ **Input**:
177
+ ```python
178
+ _X = const(True)
179
+ BUFFER_SIZE = const(1024)
180
+ ```
181
+ **Flags**: `-DBUFFER_SIZE=512 -O`
182
+ **Output**:
183
+ ```python
184
+ _X = const(True)
185
+ BUFFER_SIZE = const(512)
186
+ ```
187
+
188
+ ### Example 3: Conditional Decorator Pruning
189
+ **Input**:
190
+ ```python
191
+ @keep_if(_IS_DESKTOP_PY)
192
+ def desktop_helper():
193
+ return 42
194
+ ```
195
+ **Flags**: `-D_IS_DESKTOP_PY=False`
196
+ **Output**: *(Empty — function deleted from AST)*
197
+
198
+ ### Example 4: Assertions and Debug Block Stripping
199
+ **Input**:
200
+ ```python
201
+ assert 1 == 1, 'invariant'
202
+ if __debug__:
203
+ def debug_only():
204
+ pass
205
+ ```
206
+ **Flags**: `-O`
207
+ **Output**: *(Empty — assertions and debug blocks pruned)*
208
+
209
+ ### Example 5: Native Decorator Stripping via Macro Mapping
210
+ **Input**:
211
+ ```python
212
+ @micropython.native
213
+ def fast_calc(x):
214
+ return x * 2
215
+ ```
216
+ **Flags**: `-Dmicropython.native=keep`
217
+ **Output**:
218
+ ```python
219
+ def fast_calc(x):
220
+ return x * 2
221
+ ```
222
+
223
+ ---
224
+
225
+ ## Function & Runtime Reference
226
+
227
+ ### `const(x)`
228
+ Identity fallback for compile-time constants on non-MicroPython runtimes. On MicroPython, `const()` is intercepted natively at parse time to inline values. On CPython and PyPy, this function acts as an identity pass-through returning `x`.
229
+
230
+ ### `keep(obj)`
231
+ Identity decorator: keeps the decorated function or class as-is. When processed by `astpp`, the `@keep` decorator is stripped from the AST so that the compiled output incurs zero runtime decorator invocation overhead.
232
+
233
+ ### `omit(obj)`
234
+ Omission decorator: drops the decorated function or class completely. When processed by `astpp`, any node decorated with `@omit` is purged from the parse tree.
235
+
236
+ ### `keep_if(cond)`
237
+ Conditional decorator factory that preserves the target definition if `cond` is True. Returns the identity decorator `keep` if `cond` is True, or `omit` if False.
238
+
239
+ ### `omit_if(cond)`
240
+ Conditional decorator factory that drops the target definition if `cond` is True. Returns the omission decorator `omit` if `cond` is True, or `keep` if False.
241
+
242
+ ### `_MicroPythonShim`
243
+ Desktop compatibility shim providing no-op decorators (`native = staticmethod(keep)`, `viper = staticmethod(keep)`) for MicroPython compiler pragmas on non-MicroPython runtimes.
244
+
245
+ ---
246
+
247
+ ## License
248
+
249
+ MIT License. See `LICENSE` for details.
250
+
251
+ ## Note on the code and the tools used to write it
252
+
253
+ Parts of this code were written (including some initial ones that began in other, separate projects) with assistance from LLM-integrated coding tools. If you don't like it, feel free to use other software or rewrite parts you dislike. PRs are welcome!
254
+
255
+ ### How did I end up using those? Don't I dislike slop?
256
+
257
+ Yes, I hate slop. This project began because I wanted tape image conversion tools where the conversion steps were all clearly documented and readable code, but which also performed well enough in terms of accuracy to actually be the tool I use. I started out writing the tools myself, but my manual attempts hadn't yielded comparable accuracy to existing closed-source tools for some steps, so I started using the tools to help find the bugs and suggest improvements, and IMO the result is now good enough to actually be useful in some scenarios. In terms of slop, the tool-generated code doesn't closely resemble any existing solutions I have found. Rather it's a fairly passable translation of my requests into Python.
258
+
@@ -0,0 +1,6 @@
1
+ astpp.py,sha256=TAf-PH9N2m3zIK4JYw7S9780fxzbe-q3cgpf3n8PKJI,21781
2
+ astpp-0.1.0.dist-info/entry_points.txt,sha256=idXuWsk8gclhTlS-mYBeJm_jD5yin0XDduRGizVVdgw,36
3
+ astpp-0.1.0.dist-info/licenses/LICENSE,sha256=EPqk9kB5Zgygl1BXt-KJzSPRam4GcjyrEJPZw_mvMac,1062
4
+ astpp-0.1.0.dist-info/WHEEL,sha256=G2gURzTEtmeR8nrdXUJfNiB3VYVxigPQ-bEQujpNiNs,82
5
+ astpp-0.1.0.dist-info/METADATA,sha256=_4uXHcsQpkaW5d0uHDCY3fGmRyk71FnlihbxjxfgXw0,9400
6
+ astpp-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: flit 3.12.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any
@@ -0,0 +1,3 @@
1
+ [console_scripts]
2
+ astpp=astpp:main
3
+
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 -ffix
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
astpp.py ADDED
@@ -0,0 +1,637 @@
1
+ #!/usr/bin/env python3
2
+ """Self-Inlining Python Macro Preprocessor & Runtime Polyfill Library.
3
+
4
+ Homepage:
5
+ ---------
6
+ https://github.com/f-fix/astpp
7
+
8
+ Overview:
9
+ ---------
10
+ `astpp` is a dual-use Python library and AST-level macro preprocessor designed
11
+ for cross-runtime codebases that target MicroPython, PyPy, and CPython.
12
+
13
+ 1. Runtime Polyfill (Zero-Build Development):
14
+ Importing `astpp` provides zero-overhead, runtime-compatible implementations of
15
+ MicroPython compiler pragmas (`const`, `micropython.native`, `micropython.viper`)
16
+ and conditional decorators (`keep`, `omit`, `keep_if`, `omit_if`). Scripts can be
17
+ executed directly under CPython, PyPy, or MicroPython without requiring an explicit
18
+ build or preprocessing step.
19
+
20
+ 2. Compile-Time Preprocessor (Zero-Dependency Builds):
21
+ When invoked as a script or via `astpp_main()`, `astpp` inspects the consumer's
22
+ Abstract Syntax Tree (AST), removes the `from astpp import ...` statement entirely,
23
+ and inlines minimal self-contained fallback definitions copied directly from its own
24
+ source code. The emitted code has zero external dependencies on `astpp.py`.
25
+
26
+ Documentation & Tested Examples:
27
+ --------------------------------
28
+ The following verified examples illustrate source transformations across various CLI flags.
29
+ These exact inputs, flags, and outputs are parsed directly from this docstring and validated
30
+ by `astpp_main(["--test"])`:
31
+
32
+ ### Example 1: Import Inlining
33
+ Input:
34
+ ```python
35
+ from astpp import const
36
+ X = const(10)
37
+ ```
38
+ Flags: -O3
39
+ Output:
40
+ ```python
41
+ try:
42
+ from micropython import const
43
+ except ImportError:
44
+
45
+ def const(x):
46
+ return x
47
+ X = const(10)
48
+ ```
49
+
50
+ ### Example 2: Constant Definition & In-Line Override
51
+ Input:
52
+ ```python
53
+ _X = const(True)
54
+ BUFFER_SIZE = const(1024)
55
+ ```
56
+ Flags: -DBUFFER_SIZE=512 -O
57
+ Output:
58
+ ```python
59
+ _X = const(True)
60
+ BUFFER_SIZE = const(512)
61
+ ```
62
+
63
+ ### Example 3: Conditional Decorator Pruning
64
+ Input:
65
+ ```python
66
+ @keep_if(_IS_DESKTOP_PY)
67
+ def desktop_helper():
68
+ return 42
69
+ ```
70
+ Flags: -D_IS_DESKTOP_PY=False
71
+ Output:
72
+ ```python
73
+ ```
74
+
75
+ ### Example 4: Assertions and Debug Block Stripping
76
+ Input:
77
+ ```python
78
+ assert 1 == 1, 'invariant'
79
+ if __debug__:
80
+ def debug_only():
81
+ pass
82
+ ```
83
+ Flags: -O
84
+ Output:
85
+ ```python
86
+ ```
87
+
88
+ ### Example 5: Native Decorator Stripping via Macro Mapping
89
+ Input:
90
+ ```python
91
+ @micropython.native
92
+ def fast_calc(x):
93
+ return x * 2
94
+ ```
95
+ Flags: -Dmicropython.native=keep
96
+ Output:
97
+ ```python
98
+ def fast_calc(x):
99
+ return x * 2
100
+ ```
101
+
102
+ CLI Usage:
103
+ ----------
104
+ python3 astpp.py [-O[n]] [-DNAME=VAL ...] [input] [-o output]
105
+ python3 astpp.py --help
106
+ python3 astpp.py --test
107
+
108
+ API Usage:
109
+ ----------
110
+ from astpp import astpp_main
111
+ astpp_main(argv=['-O3', '-D_IS_DESKTOP_PY=False'], stdin=in_stream, stdout=out_stream)
112
+ """
113
+
114
+ # ==============================================================================
115
+ # 1. RUNTIME EXPORTS
116
+ # ==============================================================================
117
+ try:
118
+ from micropython import const
119
+ except ImportError:
120
+
121
+ def const(x):
122
+ """Identity fallback for compile-time constants on non-MicroPython runtimes.
123
+
124
+ On MicroPython, `const()` is intercepted natively at parse time to inline values.
125
+ On CPython and PyPy, this function acts as an identity pass-through returning `x`.
126
+ """
127
+ return x
128
+
129
+
130
+ def keep(obj):
131
+ """Identity decorator: keeps the decorated function or class as-is.
132
+
133
+ When processed by `astpp`, the `@keep` decorator is stripped from the AST so that the
134
+ compiled output incurs zero runtime decorator invocation overhead.
135
+ """
136
+ return obj
137
+
138
+
139
+ def omit(obj):
140
+ """Omission decorator: drops the decorated function or class completely.
141
+
142
+ When processed by `astpp`, any node decorated with `@omit` is purged from the parse tree.
143
+ """
144
+ return None
145
+
146
+
147
+ def keep_if(cond):
148
+ """Conditional decorator factory that preserves the target definition if cond is True.
149
+
150
+ Parameters:
151
+ cond: Expression evaluating to a truthy or falsy value.
152
+
153
+ Returns:
154
+ The identity decorator `keep` if cond is True, or `omit` if False.
155
+ When processed by `astpp`, the decorator is stripped if kept, or the entire node is
156
+ deleted from the AST if omitted.
157
+ """
158
+ return keep if cond else omit
159
+
160
+
161
+ def omit_if(cond):
162
+ """Conditional decorator factory that drops the target definition if cond is True.
163
+
164
+ Parameters:
165
+ cond: Expression evaluating to a truthy or falsy value.
166
+
167
+ Returns:
168
+ The omission decorator `omit` if cond is True, or `keep` if False.
169
+ When processed by `astpp`, the entire target node is pruned from the AST if omitted.
170
+ """
171
+ return omit if cond else keep
172
+
173
+
174
+ try:
175
+ import micropython as _real_mpy
176
+ except ImportError:
177
+ _real_mpy = None
178
+
179
+ if _real_mpy is not None:
180
+ micropython = _real_mpy
181
+ else:
182
+
183
+ class _MicroPythonShim:
184
+ """Desktop compatibility shim providing no-op decorators for MicroPython pragmas."""
185
+
186
+ native = staticmethod(keep)
187
+ viper = staticmethod(keep)
188
+
189
+ micropython = _MicroPythonShim()
190
+
191
+ __version__ = "0.1.0"
192
+
193
+
194
+ # ==============================================================================
195
+ # 2. PREPROCESSOR ENTRYPOINT & SELF-TEST SUITE
196
+ # ==============================================================================
197
+ def astpp_main(argv=None, stdin=None, stdout=None, stderr=None):
198
+ """Programmatic entrypoint for AST-level preprocessing, help display, and self-testing.
199
+
200
+ Parameters:
201
+ argv: List of command-line argument strings (e.g. ['-O3', '-DFOO=1']).
202
+ Defaults to sys.argv[1:] if None.
203
+ stdin: Readable text stream containing Python source code. Defaults to sys.stdin.
204
+ stdout: Writable text stream to receive transformed source code. Defaults to sys.stdout.
205
+ stderr: Writable text stream for diagnostic logging and error messages. Defaults to sys.stderr.
206
+
207
+ Returns:
208
+ 0 on successful transformation, help output, or passing test suite; non-zero on error.
209
+ """
210
+ import ast
211
+ import copy
212
+ import argparse
213
+
214
+ if argv is None or stdin is None or stdout is None or stderr is None:
215
+ import sys
216
+
217
+ if argv is None:
218
+ argv = sys.argv[1:]
219
+ if stdin is None:
220
+ stdin = sys.stdin
221
+ if stdout is None:
222
+ stdout = sys.stdout
223
+ if stderr is None:
224
+ stderr = sys.stderr
225
+
226
+ # --- Argument Parser & Normal Filter Mode ---
227
+ def _build_parser():
228
+ parser = argparse.ArgumentParser(
229
+ prog="astpp",
230
+ description="Self-Inlining Macro Preprocessor & Runtime Polyfill Library.",
231
+ add_help=False,
232
+ )
233
+ parser.add_argument(
234
+ "-h", "--help", action="store_true", help="Show this help message and exit"
235
+ )
236
+ parser.add_argument(
237
+ "input",
238
+ nargs="?",
239
+ default="-",
240
+ help="Input Python source file or '-' for stdin (default: '-')",
241
+ )
242
+ parser.add_argument(
243
+ "-o",
244
+ "--output",
245
+ default="-",
246
+ help="Output destination file or '-' for stdout (default: '-')",
247
+ )
248
+ parser.add_argument(
249
+ "-D",
250
+ dest="defines",
251
+ action="append",
252
+ default=[],
253
+ metavar="NAME=VALUE",
254
+ help="Define macro replacement or override constant (e.g. -DNAME=VAL or -DNAME)",
255
+ )
256
+ parser.add_argument(
257
+ "-O",
258
+ dest="opt",
259
+ nargs="?",
260
+ const="1",
261
+ default=None,
262
+ metavar="LEVEL",
263
+ help="Optimization level: -O (strip asserts, force __debug__=False), -OO or -O2 (strip docstrings), -O3 (full)",
264
+ )
265
+ parser.add_argument(
266
+ "-OO",
267
+ dest="opt_oo",
268
+ action="store_true",
269
+ help="Optimization level 2: strip assertions, __debug__=False, and docstrings",
270
+ )
271
+ parser.add_argument(
272
+ "--test",
273
+ action="store_true",
274
+ help="Run self-contained unit test suite",
275
+ )
276
+ return parser
277
+
278
+ parser = _build_parser()
279
+
280
+ if "-h" in argv or "--help" in argv:
281
+ parser.print_help(file=stdout)
282
+ if hasattr(stdout, "flush"):
283
+ stdout.flush()
284
+ return 0
285
+
286
+ try:
287
+ args = parser.parse_args(argv)
288
+ except SystemExit as e:
289
+ c = getattr(e, "code", None)
290
+ if c is None:
291
+ c = e.args[0] if e.args else 0
292
+ return c
293
+
294
+ # --- Self-Contained Test Mode ---
295
+ if args.test:
296
+ if not __debug__:
297
+ stderr.write("--test is not available in optimized builds\n")
298
+ if hasattr(stderr, "flush"):
299
+ stderr.flush()
300
+ return 1
301
+ try:
302
+ import unittest
303
+ except ImportError:
304
+ stderr.write(
305
+ "--test disabled: unittest is not available in this environment\n"
306
+ )
307
+ return 1
308
+
309
+ global TestAstpp
310
+
311
+ class TestAstpp(unittest.TestCase):
312
+ """Self-test suite for astpp runtime helpers, inlining, and docstring examples."""
313
+
314
+ def test_runtime_helpers(self):
315
+ """Validate behavior of const, keep, omit, keep_if, omit_if, and micropython shims."""
316
+ self.assertEqual(const(123), 123)
317
+ self.assertEqual(keep(42), 42)
318
+ self.assertIsNone(omit(42))
319
+ self.assertEqual(keep_if(True)(99), 99)
320
+ self.assertIsNone(keep_if(False)(99))
321
+ self.assertIsNone(omit_if(True)(99))
322
+ self.assertEqual(omit_if(False)(99), 99)
323
+ self.assertEqual(micropython.native(10), 10)
324
+ self.assertEqual(micropython.viper(20), 20)
325
+
326
+ def test_docstring_examples(self):
327
+ """Parse all documented examples directly from __doc__ and verify against astpp_main."""
328
+ import io, re, shlex, sys
329
+
330
+ doc = sys.modules[__name__].__doc__ or __doc__
331
+ pattern = re.compile(
332
+ r"Input:\s*```python\s*(.*?)\s*```\s*Flags:\s*(.*?)\s*Output:\s*```python\s*(.*?)\s*```",
333
+ re.DOTALL,
334
+ )
335
+ matches = pattern.findall(doc)
336
+ self.assertGreaterEqual(
337
+ len(matches), 5, "Must find at least 5 docstring examples"
338
+ )
339
+
340
+ for idx, (inp, flags_str, expected) in enumerate(matches):
341
+ flags = shlex.split(flags_str.strip())
342
+ fake_in = io.StringIO(inp.strip() + "\n")
343
+ fake_out = io.StringIO()
344
+ fake_err = io.StringIO()
345
+ rc = astpp_main(flags, fake_in, fake_out, fake_err)
346
+ self.assertEqual(
347
+ rc, 0, f"Example {idx+1} failed with rc={rc}: {flags}"
348
+ )
349
+
350
+ actual_ast = ast.parse(fake_out.getvalue().strip())
351
+ exp_ast = ast.parse(expected.strip())
352
+ self.assertEqual(
353
+ ast.dump(actual_ast),
354
+ ast.dump(exp_ast),
355
+ f"Example {idx+1} AST mismatch!\nActual:\n{fake_out.getvalue()}\nExpected:\n{expected}",
356
+ )
357
+
358
+ def test_cli_help_flag(self):
359
+ """Validate that --help writes formatted usage to stdout without process exit."""
360
+ import io, re
361
+
362
+ help_out = io.StringIO()
363
+ rc = astpp_main(["--help"], stdout=help_out, stderr=io.StringIO())
364
+ self.assertEqual(rc, 0)
365
+ val = help_out.getvalue()
366
+ clean_val = re.sub(r"\x1b\[[0-9;]*[a-zA-Z]", "", val)
367
+ self.assertIn("usage: astpp", clean_val)
368
+ self.assertIn("-D NAME=VALUE", clean_val)
369
+
370
+ loader = getattr(unittest, "TestLoader", None)
371
+ if loader is not None:
372
+ suite = loader().loadTestsFromTestCase(TestAstpp)
373
+ runner = unittest.TextTestRunner(stream=stderr, verbosity=2)
374
+ else:
375
+ suite = unittest.TestSuite()
376
+ suite.addTest(TestAstpp)
377
+ runner = unittest.TextTestRunner()
378
+ res = runner.run(suite)
379
+ return (
380
+ 0
381
+ if (
382
+ res.wasSuccessful()
383
+ if hasattr(res, "wasSuccessful")
384
+ else len(getattr(res, "errors", [])) == 0
385
+ )
386
+ else 1
387
+ )
388
+
389
+ def _parse_val(v_str):
390
+ if v_str == "True":
391
+ return True
392
+ if v_str == "False":
393
+ return False
394
+ if v_str == "None":
395
+ return None
396
+ try:
397
+ return int(v_str)
398
+ except ValueError:
399
+ return v_str
400
+
401
+ defines = {}
402
+ for item in args.defines:
403
+ if "=" in item:
404
+ k, v = item.split("=", 1)
405
+ defines[k] = _parse_val(v)
406
+ else:
407
+ defines[item] = True
408
+
409
+ opt_level = None
410
+ if args.opt_oo:
411
+ opt_level = 2
412
+ elif args.opt is not None:
413
+ opt_level = int(args.opt)
414
+
415
+ if opt_level and opt_level >= 1:
416
+ defines["__debug__"] = False
417
+
418
+ if args.input == "-":
419
+ in_source = stdin.read()
420
+ else:
421
+ with open(args.input, "r", encoding="utf-8") as f:
422
+ in_source = f.read()
423
+
424
+ # Self-parsing: extract live AST nodes preceding astpp_main directly from this source file
425
+ with open(__file__, "r", encoding="utf-8") as f:
426
+ self_tree = ast.parse(f.read(), filename=__file__)
427
+
428
+ self_helpers = []
429
+ for n in self_tree.body:
430
+ if isinstance(n, ast.FunctionDef) and n.name == "astpp_main":
431
+ break
432
+ self_helpers.append(n)
433
+
434
+ def _defined_names(node):
435
+ defs = set()
436
+ if isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)):
437
+ defs.add(node.name)
438
+ elif isinstance(node, ast.Assign):
439
+ for t in node.targets:
440
+ if isinstance(t, ast.Name):
441
+ defs.add(t.id)
442
+ elif isinstance(node, ast.Try):
443
+ for n in node.body:
444
+ defs.update(_defined_names(n))
445
+ for h in node.handlers:
446
+ for n in h.body:
447
+ defs.update(_defined_names(n))
448
+ elif isinstance(node, ast.If):
449
+ for n in node.body + node.orelse:
450
+ defs.update(_defined_names(n))
451
+ return defs
452
+
453
+ class Preprocessor(ast.NodeTransformer):
454
+ """AST transformer enforcing constant propagation, branch pruning, and inlining."""
455
+
456
+ def __init__(self):
457
+ self.constants = dict(defines)
458
+
459
+ def _eval_const(self, node):
460
+ """Evaluate constant expressions statically at compile time."""
461
+ if isinstance(node, ast.Constant):
462
+ return node.value
463
+ if isinstance(node, ast.Name):
464
+ return self.constants.get(node.id, None)
465
+ if isinstance(node, ast.UnaryOp) and isinstance(node.op, ast.Not):
466
+ v = self._eval_const(node.operand)
467
+ return (not v) if v is not None else None
468
+ return None
469
+
470
+ def _strip_docstrings(self, body):
471
+ """Strip docstring expression nodes when optimization level >= 2 (-OO / -O2)."""
472
+ if opt_level and opt_level >= 2 and body:
473
+ if (
474
+ isinstance(body[0], ast.Expr)
475
+ and isinstance(body[0].value, ast.Constant)
476
+ and isinstance(body[0].value.value, str)
477
+ ):
478
+ body.pop(0)
479
+ if not body:
480
+ body.append(ast.Pass())
481
+
482
+ def visit_Module(self, node):
483
+ """Process top-level module statements, stripping astpp imports and inlining helpers."""
484
+ self._strip_docstrings(node.body)
485
+ new_body = []
486
+ for stmt in node.body:
487
+ if (
488
+ isinstance(stmt, ast.ImportFrom)
489
+ and stmt.module in ("astpp", "pypp")
490
+ and not stmt.level
491
+ ):
492
+ names = {a.name for a in stmt.names}
493
+ needed = set(names)
494
+ if any(k in needed for k in ("keep_if", "omit_if")):
495
+ needed.update(["keep", "omit"])
496
+ if "micropython" in needed:
497
+ needed.update(["keep", "_real_mpy", "_MicroPythonShim"])
498
+
499
+ for helper in self_helpers:
500
+ h_defs = _defined_names(helper)
501
+ if needed.intersection(h_defs):
502
+ new_body.append(self.visit(copy.deepcopy(helper)))
503
+ continue
504
+
505
+ transformed = self.visit(stmt)
506
+ if transformed is not None:
507
+ if isinstance(transformed, list):
508
+ new_body.extend(transformed)
509
+ else:
510
+ new_body.append(transformed)
511
+ node.body = new_body
512
+ return node
513
+
514
+ def visit_Assert(self, node):
515
+ """Eliminate assert statements when optimization level >= 1 (-O / -O1)."""
516
+ return None if (opt_level and opt_level >= 1) else node
517
+
518
+ def visit_Assign(self, node):
519
+ """Harvest in-code const() definitions and apply -D constant overrides."""
520
+ for target in node.targets:
521
+ if isinstance(target, ast.Name):
522
+ name = target.id
523
+ if name in defines:
524
+ val = defines[name]
525
+ self.constants[name] = val
526
+ node.value = ast.Call(
527
+ func=ast.Name(id="const", ctx=ast.Load()),
528
+ args=[ast.Constant(value=val)],
529
+ keywords=[],
530
+ )
531
+ elif (
532
+ isinstance(node.value, ast.Call)
533
+ and getattr(node.value.func, "id", None) == "const"
534
+ ):
535
+ val = self._eval_const(node.value.args[0])
536
+ if val is not None:
537
+ self.constants[name] = val
538
+ return self.generic_visit(node)
539
+
540
+ def visit_Name(self, node):
541
+ """Inline known compile-time constants directly into load expressions."""
542
+ if (
543
+ opt_level
544
+ and opt_level >= 1
545
+ and isinstance(node.ctx, ast.Load)
546
+ and node.id in self.constants
547
+ ):
548
+ val = self.constants[node.id]
549
+ if isinstance(val, (bool, int, str, bytes, type(None))):
550
+ return ast.Constant(value=val)
551
+ return node
552
+
553
+ def visit_If(self, node):
554
+ """Eliminate dead branches and promote reachable bodies for compile-time if/elif/else."""
555
+ self.generic_visit(node)
556
+ cond = self._eval_const(node.test)
557
+ if cond is True:
558
+ node.orelse = []
559
+ return node.body
560
+ if cond is False:
561
+ if not node.orelse:
562
+ return None
563
+ if len(node.orelse) == 1 and isinstance(node.orelse[0], ast.If):
564
+ return node.orelse[0]
565
+ return node.orelse
566
+ if (
567
+ len(node.orelse) == 1
568
+ and isinstance(node.orelse[0], ast.If)
569
+ and self._eval_const(node.orelse[0].test) is False
570
+ ):
571
+ node.orelse = node.orelse[0].orelse
572
+ return node
573
+
574
+ def _handle_def(self, node):
575
+ """Handle conditional decorators (@keep, @omit, @keep_if, @omit_if, macro mappings)."""
576
+ self._strip_docstrings(node.body)
577
+ new_decs = []
578
+ for d in node.decorator_list:
579
+ dec_str = ast.unparse(d)
580
+ if isinstance(d, ast.Call) and getattr(d.func, "id", None) in (
581
+ "keep_if",
582
+ "omit_if",
583
+ ):
584
+ cond = self._eval_const(d.args[0])
585
+ if d.func.id == "omit_if":
586
+ cond = not cond
587
+ res = "keep" if cond else "omit"
588
+ elif dec_str in ("keep", "omit"):
589
+ res = dec_str
590
+ else:
591
+ res = defines.get(dec_str, dec_str)
592
+
593
+ if res == "omit":
594
+ return None
595
+ if res == "keep":
596
+ continue
597
+ new_decs.append(d)
598
+ node.decorator_list = new_decs
599
+ return self.generic_visit(node)
600
+
601
+ def visit_FunctionDef(self, node):
602
+ """Process function definitions, handling decorators and docstring stripping."""
603
+ return self._handle_def(node)
604
+
605
+ def visit_AsyncFunctionDef(self, node):
606
+ """Process async function definitions, handling decorators and docstring stripping."""
607
+ return self._handle_def(node)
608
+
609
+ def visit_ClassDef(self, node):
610
+ """Process class definitions, handling decorators and docstring stripping."""
611
+ return self._handle_def(node)
612
+
613
+ tree = ast.parse(in_source)
614
+ out_text = ast.unparse(Preprocessor().visit(tree))
615
+ if not out_text.endswith("\n"):
616
+ out_text += "\n"
617
+
618
+ if args.output == "-":
619
+ stdout.write(out_text)
620
+ if hasattr(stdout, "flush"):
621
+ stdout.flush()
622
+ else:
623
+ with open(args.output, "w", encoding="utf-8") as f:
624
+ f.write(out_text)
625
+
626
+ return 0
627
+
628
+
629
+ def main():
630
+ """Command-line entrypoint for pip-installed astpp console script."""
631
+ import sys
632
+
633
+ sys.exit(astpp_main(sys.argv[1:], sys.stdin, sys.stdout, sys.stderr))
634
+
635
+
636
+ if __name__ == "__main__":
637
+ main()