ppy-lang 0.1.0a1__tar.gz

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.
Files changed (148) hide show
  1. ppy_lang-0.1.0a1/.gitignore +23 -0
  2. ppy_lang-0.1.0a1/CHANGELOG.md +49 -0
  3. ppy_lang-0.1.0a1/CONTRIBUTING.md +135 -0
  4. ppy_lang-0.1.0a1/LICENSE +21 -0
  5. ppy_lang-0.1.0a1/PKG-INFO +325 -0
  6. ppy_lang-0.1.0a1/README.md +282 -0
  7. ppy_lang-0.1.0a1/docs/architecture.md +132 -0
  8. ppy_lang-0.1.0a1/docs/cli.md +353 -0
  9. ppy_lang-0.1.0a1/docs/compatibility.md +70 -0
  10. ppy_lang-0.1.0a1/docs/config.md +75 -0
  11. ppy_lang-0.1.0a1/docs/conversion.md +184 -0
  12. ppy_lang-0.1.0a1/docs/diagnostics.md +111 -0
  13. ppy_lang-0.1.0a1/docs/guide.md +108 -0
  14. ppy_lang-0.1.0a1/docs/language.md +204 -0
  15. ppy_lang-0.1.0a1/docs/plugins.md +89 -0
  16. ppy_lang-0.1.0a1/pyproject.toml +232 -0
  17. ppy_lang-0.1.0a1/scripts/check.sh +17 -0
  18. ppy_lang-0.1.0a1/src/ppy/__init__.py +154 -0
  19. ppy_lang-0.1.0a1/src/ppy/_alloc.py +62 -0
  20. ppy_lang-0.1.0a1/src/ppy/_directives.py +151 -0
  21. ppy_lang-0.1.0a1/src/ppy/_importer.py +134 -0
  22. ppy_lang-0.1.0a1/src/ppy/_io.py +381 -0
  23. ppy_lang-0.1.0a1/src/ppy/_markers.py +290 -0
  24. ppy_lang-0.1.0a1/src/ppy/py.typed +0 -0
  25. ppy_lang-0.1.0a1/src/ppy_compiler/__init__.py +0 -0
  26. ppy_lang-0.1.0a1/src/ppy_compiler/__main__.py +4 -0
  27. ppy_lang-0.1.0a1/src/ppy_compiler/analysis/__init__.py +0 -0
  28. ppy_lang-0.1.0a1/src/ppy_compiler/analysis/aliasing.py +410 -0
  29. ppy_lang-0.1.0a1/src/ppy_compiler/analysis/annotations.py +442 -0
  30. ppy_lang-0.1.0a1/src/ppy_compiler/analysis/binding.py +99 -0
  31. ppy_lang-0.1.0a1/src/ppy_compiler/analysis/builtins.py +508 -0
  32. ppy_lang-0.1.0a1/src/ppy_compiler/analysis/checker.py +3270 -0
  33. ppy_lang-0.1.0a1/src/ppy_compiler/analysis/codec.py +172 -0
  34. ppy_lang-0.1.0a1/src/ppy_compiler/analysis/contracts.py +410 -0
  35. ppy_lang-0.1.0a1/src/ppy_compiler/analysis/decorators.py +266 -0
  36. ppy_lang-0.1.0a1/src/ppy_compiler/analysis/effects.py +118 -0
  37. ppy_lang-0.1.0a1/src/ppy_compiler/analysis/env.py +87 -0
  38. ppy_lang-0.1.0a1/src/ppy_compiler/analysis/global_writes.py +118 -0
  39. ppy_lang-0.1.0a1/src/ppy_compiler/analysis/inference.py +497 -0
  40. ppy_lang-0.1.0a1/src/ppy_compiler/analysis/lexical.py +326 -0
  41. ppy_lang-0.1.0a1/src/ppy_compiler/analysis/refinements.py +156 -0
  42. ppy_lang-0.1.0a1/src/ppy_compiler/analysis/reflection.py +202 -0
  43. ppy_lang-0.1.0a1/src/ppy_compiler/analysis/render.py +204 -0
  44. ppy_lang-0.1.0a1/src/ppy_compiler/analysis/representation.py +112 -0
  45. ppy_lang-0.1.0a1/src/ppy_compiler/analysis/results.py +104 -0
  46. ppy_lang-0.1.0a1/src/ppy_compiler/analysis/stdlib.py +232 -0
  47. ppy_lang-0.1.0a1/src/ppy_compiler/analysis/symbols.py +949 -0
  48. ppy_lang-0.1.0a1/src/ppy_compiler/analysis/types.py +619 -0
  49. ppy_lang-0.1.0a1/src/ppy_compiler/backend/__init__.py +0 -0
  50. ppy_lang-0.1.0a1/src/ppy_compiler/backend/binder.py +5 -0
  51. ppy_lang-0.1.0a1/src/ppy_compiler/backend/exported_runtime.py +5 -0
  52. ppy_lang-0.1.0a1/src/ppy_compiler/backend/llvm/__init__.py +745 -0
  53. ppy_lang-0.1.0a1/src/ppy_compiler/backend/llvm/fused_runtime.py +211 -0
  54. ppy_lang-0.1.0a1/src/ppy_compiler/backend/llvm/fusion.py +439 -0
  55. ppy_lang-0.1.0a1/src/ppy_compiler/backend/llvm/jit.py +199 -0
  56. ppy_lang-0.1.0a1/src/ppy_compiler/backend/llvm/link.py +348 -0
  57. ppy_lang-0.1.0a1/src/ppy_compiler/backend/llvm/lowering.py +1929 -0
  58. ppy_lang-0.1.0a1/src/ppy_compiler/backend/llvm/lowering_cache.py +145 -0
  59. ppy_lang-0.1.0a1/src/ppy_compiler/backend/llvm/parallel.py +89 -0
  60. ppy_lang-0.1.0a1/src/ppy_compiler/backend/llvm/runtime.py +5 -0
  61. ppy_lang-0.1.0a1/src/ppy_compiler/backend/llvm/specialize.py +262 -0
  62. ppy_lang-0.1.0a1/src/ppy_compiler/backend/llvm/standalone.py +300 -0
  63. ppy_lang-0.1.0a1/src/ppy_compiler/backend/llvm/wrapper.py +536 -0
  64. ppy_lang-0.1.0a1/src/ppy_compiler/backend/llvm/wrapper_build.py +177 -0
  65. ppy_lang-0.1.0a1/src/ppy_compiler/backend/python/__init__.py +4 -0
  66. ppy_lang-0.1.0a1/src/ppy_compiler/backend/python/emit.py +82 -0
  67. ppy_lang-0.1.0a1/src/ppy_compiler/backend/python/runner.py +11 -0
  68. ppy_lang-0.1.0a1/src/ppy_compiler/backend/region_runtime.py +5 -0
  69. ppy_lang-0.1.0a1/src/ppy_compiler/cache/__init__.py +11 -0
  70. ppy_lang-0.1.0a1/src/ppy_compiler/cache/keys.py +86 -0
  71. ppy_lang-0.1.0a1/src/ppy_compiler/cache/store.py +509 -0
  72. ppy_lang-0.1.0a1/src/ppy_compiler/diagnostics/__init__.py +20 -0
  73. ppy_lang-0.1.0a1/src/ppy_compiler/diagnostics/codes.py +69 -0
  74. ppy_lang-0.1.0a1/src/ppy_compiler/diagnostics/model.py +176 -0
  75. ppy_lang-0.1.0a1/src/ppy_compiler/driver/__init__.py +0 -0
  76. ppy_lang-0.1.0a1/src/ppy_compiler/driver/cli.py +336 -0
  77. ppy_lang-0.1.0a1/src/ppy_compiler/driver/commands.py +465 -0
  78. ppy_lang-0.1.0a1/src/ppy_compiler/driver/config.py +225 -0
  79. ppy_lang-0.1.0a1/src/ppy_compiler/driver/convert.py +1300 -0
  80. ppy_lang-0.1.0a1/src/ppy_compiler/driver/explain.py +308 -0
  81. ppy_lang-0.1.0a1/src/ppy_compiler/driver/formatting.py +403 -0
  82. ppy_lang-0.1.0a1/src/ppy_compiler/driver/linting.py +305 -0
  83. ppy_lang-0.1.0a1/src/ppy_compiler/driver/pipeline.py +355 -0
  84. ppy_lang-0.1.0a1/src/ppy_compiler/driver/plan.py +73 -0
  85. ppy_lang-0.1.0a1/src/ppy_compiler/driver/reporting.py +57 -0
  86. ppy_lang-0.1.0a1/src/ppy_compiler/driver/rewrite.py +601 -0
  87. ppy_lang-0.1.0a1/src/ppy_compiler/driver/staging.py +241 -0
  88. ppy_lang-0.1.0a1/src/ppy_compiler/frontend/__init__.py +0 -0
  89. ppy_lang-0.1.0a1/src/ppy_compiler/frontend/modules.py +266 -0
  90. ppy_lang-0.1.0a1/src/ppy_compiler/frontend/parser.py +63 -0
  91. ppy_lang-0.1.0a1/src/ppy_compiler/frontend/source.py +68 -0
  92. ppy_lang-0.1.0a1/src/ppy_compiler/lsp/__init__.py +19 -0
  93. ppy_lang-0.1.0a1/src/ppy_compiler/lsp/protocol.py +75 -0
  94. ppy_lang-0.1.0a1/src/ppy_compiler/lsp/server.py +294 -0
  95. ppy_lang-0.1.0a1/src/ppy_compiler/lsp/service.py +523 -0
  96. ppy_lang-0.1.0a1/src/ppy_compiler/migration/__init__.py +13 -0
  97. ppy_lang-0.1.0a1/src/ppy_compiler/migration/dynamic.py +325 -0
  98. ppy_lang-0.1.0a1/src/ppy_compiler/migration/globals.py +92 -0
  99. ppy_lang-0.1.0a1/src/ppy_compiler/migration/pipeline.py +79 -0
  100. ppy_lang-0.1.0a1/src/ppy_compiler/migration/report.py +169 -0
  101. ppy_lang-0.1.0a1/src/ppy_compiler/opt/__init__.py +3 -0
  102. ppy_lang-0.1.0a1/src/ppy_compiler/opt/annotate.py +64 -0
  103. ppy_lang-0.1.0a1/src/ppy_compiler/opt/manager.py +272 -0
  104. ppy_lang-0.1.0a1/src/ppy_compiler/opt/passes.py +833 -0
  105. ppy_lang-0.1.0a1/src/ppy_compiler/opt/rewrites.py +53 -0
  106. ppy_lang-0.1.0a1/src/ppy_compiler/plugins/__init__.py +0 -0
  107. ppy_lang-0.1.0a1/src/ppy_compiler/plugins/base.py +150 -0
  108. ppy_lang-0.1.0a1/src/ppy_compiler/plugins/jax_export.py +235 -0
  109. ppy_lang-0.1.0a1/src/ppy_compiler/plugins/jax_plugin.py +629 -0
  110. ppy_lang-0.1.0a1/src/ppy_compiler/plugins/numpy_plugin.py +463 -0
  111. ppy_lang-0.1.0a1/src/ppy_compiler/plugins/pydantic_plugin.py +90 -0
  112. ppy_lang-0.1.0a1/src/ppy_compiler/plugins/registry.py +47 -0
  113. ppy_lang-0.1.0a1/src/ppy_compiler/plugins/torch_build.py +128 -0
  114. ppy_lang-0.1.0a1/src/ppy_compiler/plugins/torch_plugin.py +434 -0
  115. ppy_lang-0.1.0a1/src/ppy_compiler/plugins/torch_region.py +286 -0
  116. ppy_lang-0.1.0a1/src/ppy_compiler/plugins/uvicorn_plugin.py +265 -0
  117. ppy_lang-0.1.0a1/src/ppy_compiler/py.typed +0 -0
  118. ppy_lang-0.1.0a1/src/ppy_compiler/testing/__init__.py +0 -0
  119. ppy_lang-0.1.0a1/src/ppy_compiler/testing/differential.py +109 -0
  120. ppy_lang-0.1.0a1/src/ppy_compiler/testing/generate.py +177 -0
  121. ppy_lang-0.1.0a1/src/ppy_compiler/version.py +48 -0
  122. ppy_lang-0.1.0a1/src/ppy_runtime/__init__.py +9 -0
  123. ppy_lang-0.1.0a1/src/ppy_runtime/abi.py +110 -0
  124. ppy_lang-0.1.0a1/src/ppy_runtime/binding.py +518 -0
  125. ppy_lang-0.1.0a1/src/ppy_runtime/dispatch.py +60 -0
  126. ppy_lang-0.1.0a1/src/ppy_runtime/execute.py +197 -0
  127. ppy_lang-0.1.0a1/src/ppy_runtime/exported.py +74 -0
  128. ppy_lang-0.1.0a1/src/ppy_runtime/generated.py +120 -0
  129. ppy_lang-0.1.0a1/src/ppy_runtime/launch.py +142 -0
  130. ppy_lang-0.1.0a1/src/ppy_runtime/manifest.py +142 -0
  131. ppy_lang-0.1.0a1/src/ppy_runtime/regions.py +65 -0
  132. ppy_lang-0.1.0a1/tests/conftest.py +53 -0
  133. ppy_lang-0.1.0a1/tests/test_analysis.py +2107 -0
  134. ppy_lang-0.1.0a1/tests/test_backends.py +655 -0
  135. ppy_lang-0.1.0a1/tests/test_bench.py +253 -0
  136. ppy_lang-0.1.0a1/tests/test_build.py +782 -0
  137. ppy_lang-0.1.0a1/tests/test_cache.py +775 -0
  138. ppy_lang-0.1.0a1/tests/test_cli.py +2290 -0
  139. ppy_lang-0.1.0a1/tests/test_codec.py +178 -0
  140. ppy_lang-0.1.0a1/tests/test_conformance.py +994 -0
  141. ppy_lang-0.1.0a1/tests/test_differential_fuzz.py +56 -0
  142. ppy_lang-0.1.0a1/tests/test_io.py +195 -0
  143. ppy_lang-0.1.0a1/tests/test_library_integration.py +777 -0
  144. ppy_lang-0.1.0a1/tests/test_lsp.py +387 -0
  145. ppy_lang-0.1.0a1/tests/test_migration.py +282 -0
  146. ppy_lang-0.1.0a1/tests/test_native.py +3031 -0
  147. ppy_lang-0.1.0a1/tests/test_plugins.py +576 -0
  148. ppy_lang-0.1.0a1/tests/test_runtime.py +324 -0
@@ -0,0 +1,23 @@
1
+ __pycache__/
2
+ *.py[cod]
3
+ .venv/
4
+ testenv/
5
+ dist/
6
+ build/
7
+ *.egg-info/
8
+ .pytest_cache/
9
+ .coverage
10
+ htmlcov/
11
+ .ppy-cache/
12
+ *.md
13
+ !README.md
14
+ !CONTRIBUTING.md
15
+ !CHANGELOG.md
16
+ !docs/*.md
17
+ !examples/**/*.md
18
+ examples/**/uv.lock
19
+ examples/**/.venv/
20
+
21
+ # Compiled C reference binaries built beside an example.
22
+ *_c
23
+ examples/**/native/
@@ -0,0 +1,49 @@
1
+ # Changelog
2
+
3
+ ## 0.1.0a1
4
+
5
+ The first release, and an alpha in the ordinary sense: the language and the
6
+ diagnostics are in use and tested, and neither is promised to stay put. Pin
7
+ an exact version.
8
+
9
+ On PyPI as **`ppy-lang`**; the packages it installs are `ppy`,
10
+ `ppy_compiler`, and `ppy_runtime`, so a program still writes `import ppy`.
11
+
12
+ ### The language
13
+
14
+ - A `.ppy` file is valid Python 3.12+. Everything PPY adds is carried by
15
+ decorators and annotations from the `ppy` package, all inert under plain
16
+ CPython.
17
+ - Strict analysis by default: an implicit `Any` is an error, dynamic features
18
+ need a `ppy.dynamic` boundary, and a decorator must have vouched semantics.
19
+ - `ppy convert` staticizes Python into strict PPY; `ppy migrate` is the
20
+ permissive form. Both are deterministic, both refuse to write anything when
21
+ the settled analysis holds an error.
22
+
23
+ ### Running it
24
+
25
+ Three paths, held to one answer — plain CPython, an optimized Python backend,
26
+ and LLVM-lowered native code. Any observable difference between them is a
27
+ compiler bug; the suite and `examples/run_all.py` compare all three on every
28
+ example.
29
+
30
+ - `ppy build` produces an artifact that runs through `ppy_runtime` with
31
+ machine code from the library beside it, and keeps working with the
32
+ compiler uninstalled.
33
+ - `ppy build --standalone` links a native executable with no CPython inside,
34
+ for a program whose reachable graph is entirely native.
35
+ - `ppy.input[T]`, `ppy.buffer[T]`, and `Buffer[T]` (including one-byte
36
+ `ppy.i8`/`ppy.u8` elements) read and hold data without a Python object per
37
+ value.
38
+
39
+ ### Plugins
40
+
41
+ NumPy, PyTorch, JAX/Flax, pydantic, and FastAPI/Uvicorn are modeled; each is
42
+ an optional extra, and a missing runtime disables only its plugin.
43
+
44
+ ### Known limits
45
+
46
+ - `ppy.read_token` has no standalone lowering yet, which is the one thing
47
+ keeping the substring-search example off that path.
48
+ - Floats do not print from a standalone binary, pending native formatting
49
+ that reproduces CPython's shortest round-trip repr exactly.
@@ -0,0 +1,135 @@
1
+ # Contributing
2
+
3
+ ## Setting up
4
+
5
+ ```bash
6
+ uv sync # the compiler core, LLVM, NumPy, pydantic, and the linters
7
+ ./scripts/check.sh # the one gate; CI runs exactly this
8
+ ```
9
+
10
+ `uv sync` installs the `dev` group. That is what a contributor to the
11
+ compiler needs and more than a user needs: a user installs `ppy` and gets the
12
+ compiler, the runtime, and nothing else.
13
+
14
+ The plugin runtimes are separate groups, so you install only what you intend
15
+ to test:
16
+
17
+ ```bash
18
+ uv sync --group torch # PyTorch, CPU wheels
19
+ uv sync --group jax # JAX and Flax, CPU wheels
20
+ uv sync --group uvicorn # FastAPI and Uvicorn
21
+ uv sync --group all # everything
22
+ ```
23
+
24
+ Torch and JAX resolve from the CPU index, which works on every platform.
25
+ On a CUDA machine, override it for your checkout rather than for the
26
+ repository:
27
+
28
+ ```bash
29
+ uv sync --group torch --index pytorch-cuda=https://download.pytorch.org/whl/cu128
30
+ ```
31
+
32
+ ## The gate
33
+
34
+ `./scripts/check.sh` is the single source of truth for "clean": ruff, ruff
35
+ format, pylint, the test suite, the conversion check, the three-path example
36
+ run, and the example lint. CI runs that script and nothing else, so a local
37
+ pass and a CI pass are the same claim. Run it before every commit.
38
+
39
+ A plugin's own claim is `./scripts/plugin_check.sh <torch|jax|uvicorn>`,
40
+ which fails if the plugin's tests all skip — a skipped test proves nothing.
41
+
42
+ ## What a change has to keep true
43
+
44
+ - **The three paths agree.** Plain CPython, the optimized Python backend, and
45
+ the native backend produce the same answer for the same program.
46
+ `examples/run_all.py` checks every example on all three.
47
+ - **A guard that fails falls back.** Native code that cannot keep a promise
48
+ runs the Python body; it never answers differently.
49
+ - **The cache is disposable.** Corrupting or deleting any part of it may cost
50
+ a rebuild and must never cost the answer. See
51
+ [docs/compatibility.md](docs/compatibility.md).
52
+ - **`ppy_runtime` never imports `ppy_compiler`.** A built artifact keeps
53
+ working with the compiler uninstalled, and a test enforces it.
54
+ - **A generated `.ppy` is exactly what `ppy convert` writes.** Never hand-edit
55
+ one; `examples/verify_conversions.py` regenerates and diffs them.
56
+
57
+ ## Examples
58
+
59
+ An example is either hand-written or generated, and its README says which.
60
+ A generated one keeps its `.py` source beside it and is regenerated with
61
+ `ppy convert <name>.py` (some folders add `--promote-buffers`; see
62
+ `examples/verify_conversions.py`).
63
+
64
+ `python scripts/refresh.py` reports anything that has drifted — an example
65
+ that no longer checks, a conversion that no longer matches, a measurement
66
+ that has moved, a README table that is behind the record — and `--write`
67
+ brings all of them back in line. `--quick` skips the benchmark; `--record
68
+ FILE` also writes the raw numbers of that run, drift or not.
69
+
70
+ ## Measurements
71
+
72
+ Numbers in the documentation come from `examples/15_algorithms/bench.py`,
73
+ are recorded in `measurements.json` with the machine they were taken on, and
74
+ the README tables are rendered from that file rather than typed. They are
75
+ not a per-change gate: a scheduled workflow re-measures and reports drift
76
+ beyond a tolerance, because absolute wall times differ between runners.
77
+
78
+ What counts as drift needs both signals: the milliseconds moved beyond the
79
+ tolerance *and* the ratio to the C reference moved with them. A busy machine
80
+ slows every path at once, so the ratio holds where the times do not; and the
81
+ reference is a few milliseconds on the smaller problems, so a wobble there
82
+ moves every ratio at once. Either on its own reports the machine. `ppy run`
83
+ is exempt from the ratio entirely — it is mostly the compiler, and there is
84
+ no ratio to take against a C program that compiled beforehand — so its
85
+ movement is reported and never fatal.
86
+
87
+ A machine that cannot build every path fails the run and records nothing. A
88
+ record with a column missing would replace a whole one, and the gap would
89
+ read as a result rather than as a machine without `gcc` or without a shared
90
+ libpython; `bench.py` says up front which paths it had to skip and why, and
91
+ `--record` still writes what it measured so a failed scheduled run keeps its
92
+ evidence. That file may not be `measurements.json` itself: the baseline is
93
+ written only once the run is judged worth keeping.
94
+
95
+ ## Releasing
96
+
97
+ The distribution is **`ppy-lang`**; the packages it installs are `ppy`,
98
+ `ppy_compiler`, and `ppy_runtime`.
99
+
100
+ `COMPILER_VERSION` in `src/ppy_compiler/version.py` is the version. The
101
+ packaging metadata reads it (`[tool.hatch.version]`), so `pyproject.toml`
102
+ does not repeat it. `ppy.__version__` is a second literal, deliberately: the
103
+ runtime package does not import the compiler, and giving it one just to
104
+ learn a string would be a dependency in the wrong direction. A test holds
105
+ the two together, along with the installed distribution's metadata, because
106
+ the compiler keys its caches on that string and a stale copy would serve
107
+ artifacts from a version that is not running.
108
+
109
+ To cut a release:
110
+
111
+ 1. Move `COMPILER_VERSION` and `ppy.__version__` together, and write the
112
+ release into `CHANGELOG.md`.
113
+ 2. `./scripts/check.sh`, then `uv build` and
114
+ `uv run --with twine twine check dist/*`.
115
+ 3. Tag it `vX.Y.Z` — matching the declared version, which the workflow
116
+ verifies — and push the tag. `.github/workflows/release.yml` runs the
117
+ gate, builds, installs the built wheel into a clean environment and runs
118
+ it, and publishes.
119
+
120
+ A workflow pins actions by **ref**, and a ref is not a release. Some
121
+ publishers cut releases past the last moving major tag they maintain, so
122
+ `gh api repos/OWNER/REPO/releases/latest` can name a version that
123
+ `uses:` cannot resolve. Check the ref itself before changing one:
124
+
125
+ ```bash
126
+ gh api repos/astral-sh/setup-uv/git/ref/tags/v10.0.1 --jq .ref
127
+ ```
128
+
129
+ The workflow publishes through PyPI's trusted publishing, so there is no API
130
+ token in the repository. It needs, once: a pending publisher on PyPI for
131
+ `ppy-lang` naming this repository, the workflow `release.yml`, and the
132
+ environment `pypi`; the same on TestPyPI with the environment `testpypi`;
133
+ and both environments created under the repository's settings.
134
+ `workflow_dispatch` on the workflow publishes to TestPyPI by default, which
135
+ is the way to rehearse one.
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 franknoh
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.
@@ -0,0 +1,325 @@
1
+ Metadata-Version: 2.5
2
+ Name: ppy-lang
3
+ Version: 0.1.0a1
4
+ Summary: Pretty Python: a source-compatible, statically analyzable subset of Python
5
+ Project-URL: Homepage, https://github.com/franknoh/PPy
6
+ Project-URL: Repository, https://github.com/franknoh/PPy
7
+ Project-URL: Documentation, https://github.com/franknoh/PPy/blob/main/docs/guide.md
8
+ Project-URL: Issues, https://github.com/franknoh/PPy/issues
9
+ Project-URL: Changelog, https://github.com/franknoh/PPy/blob/main/CHANGELOG.md
10
+ Author: franknoh
11
+ License-Expression: MIT
12
+ License-File: LICENSE
13
+ Keywords: compiler,jit,llvm,native,python,static-analysis
14
+ Classifier: Development Status :: 3 - Alpha
15
+ Classifier: Intended Audience :: Developers
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3.12
18
+ Classifier: Programming Language :: Python :: 3.13
19
+ Classifier: Programming Language :: Python :: 3.14
20
+ Classifier: Programming Language :: Python :: Implementation :: CPython
21
+ Classifier: Topic :: Software Development :: Compilers
22
+ Classifier: Topic :: Software Development :: Interpreters
23
+ Classifier: Typing :: Typed
24
+ Requires-Python: >=3.12
25
+ Requires-Dist: libcst>=1.4
26
+ Requires-Dist: typing-extensions>=4.12
27
+ Provides-Extra: jax
28
+ Requires-Dist: flatbuffers>=24.0; extra == 'jax'
29
+ Requires-Dist: jax>=0.4; extra == 'jax'
30
+ Requires-Dist: jaxlib>=0.4; extra == 'jax'
31
+ Provides-Extra: llvm
32
+ Requires-Dist: llvmlite>=0.44; extra == 'llvm'
33
+ Provides-Extra: numpy
34
+ Requires-Dist: numpy>=1.26; extra == 'numpy'
35
+ Provides-Extra: pydantic
36
+ Requires-Dist: pydantic>=2.0; extra == 'pydantic'
37
+ Provides-Extra: torch
38
+ Requires-Dist: ninja>=1.11; extra == 'torch'
39
+ Requires-Dist: torch>=2.4; extra == 'torch'
40
+ Provides-Extra: uvicorn
41
+ Requires-Dist: uvicorn>=0.30; extra == 'uvicorn'
42
+ Description-Content-Type: text/markdown
43
+
44
+ # PPY — Pretty Python
45
+
46
+ [![PyPI](https://img.shields.io/pypi/v/ppy-lang?logo=pypi&logoColor=white)](https://pypi.org/project/ppy-lang/)
47
+ [![Python](https://img.shields.io/pypi/pyversions/ppy-lang?logo=python&logoColor=white)](https://pypi.org/project/ppy-lang/)
48
+ [![CI](https://github.com/franknoh/PPy/actions/workflows/ci.yml/badge.svg?branch=main)](https://github.com/franknoh/PPy/actions/workflows/ci.yml)
49
+ [![License](https://img.shields.io/badge/license-MIT-blue)](LICENSE)
50
+
51
+ A statically analyzable language in Python's syntax. A `.ppy` file *is* valid
52
+ Python: it runs under plain CPython with no compiler involved. The compiler
53
+ adds static checking, an optimized Python backend, and an LLVM native
54
+ backend — and all three must produce the same answer.
55
+
56
+ PPY is source-compatible with Python's syntax and ecosystem, not with every
57
+ dynamic Python behavior: `exec`/`eval`, monkey-patching, dynamic namespace
58
+ mutation, and unrestricted runtime reflection are deliberately restricted —
59
+ or isolated behind an explicit `ppy.dynamic` boundary — in exchange for
60
+ analysis, optimization, and native compilation that can be trusted. Running
61
+ existing Python is a migration feature (`ppy migrate`), not the definition of
62
+ the language.
63
+
64
+ ## Install
65
+
66
+ Add PPY to your project with [uv](https://docs.astral.sh/uv/) (Python 3.12+):
67
+
68
+ ```bash
69
+ uv add "ppy-lang[llvm]"
70
+ ```
71
+
72
+ or with pip: `pip install "ppy-lang[llvm]"`. The distribution is `ppy-lang`
73
+ and what it installs is `ppy`, so your code writes `import ppy`.
74
+
75
+ Releases are alphas — the language and the diagnostics are in use and
76
+ tested, and neither is promised to stay put — so pin an exact version:
77
+ `uv add "ppy-lang[llvm]==0.1.0a1"`. For the development tip instead:
78
+
79
+ ```bash
80
+ uv add "ppy-lang[llvm] @ git+https://github.com/franknoh/PPy.git"
81
+ ```
82
+
83
+ The base package is the compiler and the runtime; extras enable the rest, so
84
+ you install only what you use:
85
+
86
+ | extra | enables |
87
+ |---|---|
88
+ | `llvm` | the native backend (llvmlite) |
89
+ | `numpy` / `pydantic` / `uvicorn` | the matching plugin |
90
+ | `torch` / `jax` | the matching plugin, CPU builds by default — point at a CUDA index in your own project if you want one |
91
+
92
+ Everything degrades cleanly: a missing library only disables its plugin, and
93
+ `uv run ppy doctor` reports what was found. For the fastest native call
94
+ boundary, have the CPython headers installed (`python3-dev`); without them
95
+ PPY says so once and uses the slower boundary.
96
+
97
+ ## One file, four ways
98
+
99
+ ```python
100
+ # collatz.ppy
101
+ import ppy
102
+
103
+
104
+ @ppy.pure
105
+ @ppy.opt(3)
106
+ def longest(limit: int) -> int:
107
+ best: int = 0
108
+ for start in range(1, limit):
109
+ n: int = start
110
+ steps: int = 0
111
+ while n != 1:
112
+ n = n // 2 if n % 2 == 0 else 3 * n + 1
113
+ steps += 1
114
+ best = max(best, steps)
115
+ return best
116
+
117
+
118
+ print(longest(ppy.input[int]()))
119
+ ```
120
+
121
+ `ppy.input[T]()` reads the next value the way `T` says to read it, straight
122
+ into memory rather than through a Python object per field. Piping the limit
123
+ in, `echo 300000 |` before each of these:
124
+
125
+ ```bash
126
+ uv run python collatz.ppy # 1 plain CPython, no compiler 1170.2 ± 9.1 ms
127
+ uv run ppy collatz.ppy # 2 optimized Python backend 1167.9 ± 13.1 ms
128
+ uv run ppy run collatz.ppy # 3 LLVM, JIT every run 45.4 ± 1.3 ms (+ ~1200 ms JIT)
129
+ uv run ppy build collatz.ppy -o dist # 4 LLVM, built once (~800 ms)...
130
+ ./dist/collatz # ...then the native binary 33.6 ± 1.1 ms
131
+ # ...--host-cpu, not portable 31.8 ± 0.9 ms
132
+
133
+ gcc -O3 collatz.c && ./a.out # reference: the same loop in C 45.0 ± 0.6 ms (+ ~110 ms gcc)
134
+ ```
135
+
136
+ Numbers are the kernel's wall time on one machine — mean ± standard
137
+ deviation over ten runs, each a fresh process; the parenthesized figure is
138
+ what that row spends turning source into machine code, and how often.
139
+ Ways 3 and 4 are one compilation path, ahead of time or not — the binary is
140
+ `ppy run` in a compiled coat, machine code taken from the library built next
141
+ to it. They differ in one default: `ppy run` keeps Python-integer semantics
142
+ (overflow is guarded and falls back to arbitrary precision — that is the
143
+ 45.4 ms, level with C with the guards in), while `ppy build` produces a
144
+ wrap-semantics artifact like every native compiler — that is the 33.6 ms,
145
+ past C. `run --unsafe` and `build --safe` flip either one; bounds
146
+ checks stay in both. The built binary is compiled software: it starts an
147
+ embedded interpreter and imports `ppy_runtime` — about 35 ms before the
148
+ program begins — and keeps working with the compiler uninstalled.
149
+ `ppy build --standalone` removes even that, for a program whose reachable
150
+ graph is entirely native. (Python's floor semantics help too:
151
+ `n // 2` lowers to one arithmetic shift exactly, where C's truncating
152
+ division needs a sign fixup.)
153
+
154
+ The same kernel through the neighbors, same machine and methodology:
155
+
156
+ | compiler | kernel | integer semantics |
157
+ |---|---:|---|
158
+ | **PPY** `ppy build --host-cpu` | **31.8 ± 0.9 ms** | 64-bit, wraps on overflow (this machine's instruction set) |
159
+ | **PPY** `ppy build` binary | **33.6 ± 1.1 ms** | 64-bit, wraps on overflow (`--safe` to keep Python ints) |
160
+ | Codon `-release` | 35.9 ± 1.9 ms | 64-bit, wraps on overflow |
161
+ | Numba `@njit` | 36.5 ± 1.0 ms | 64-bit, wraps on overflow |
162
+ | C (`gcc -O3`) | 45.0 ± 0.6 ms | 64-bit, wraps on overflow |
163
+ | **PPY** `ppy run` | **45.4 ± 1.3 ms** | **Python ints: guarded, falls back to arbitrary precision** |
164
+ | PyPy 3.11 | 56.9 ± 2.5 ms | Python ints |
165
+ | mypyc | 78.4 ± 1.4 ms | Python ints |
166
+ | Cython (`cdef long long`) | 81.4 ± 3.0 ms | 64-bit, wraps on overflow |
167
+ | Nuitka | 776.9 ± 11.6 ms | Python ints, no type specialization |
168
+ | CPython 3.14 | 1170.2 ± 9.1 ms | Python ints |
169
+
170
+ `--host-cpu` is the opt-in that compiles for the machine doing the build
171
+ instead of the portable baseline — 33.6 ms to 31.8 ms here, and about 20% on
172
+ a matmul kernel where the vectorizer has something to work with. It is off
173
+ by default because an artifact is meant to be shipped and host code faults
174
+ on an older CPU; JIT code under `ppy run` always targets the host, which is
175
+ free because it never leaves the machine. Giving C the same option changes
176
+ nothing on this kernel (`gcc -O3 -march=native`: 45.5 ± 1.2 ms), so the row
177
+ above it is not winning on a flag C was denied.
178
+
179
+ Same compiler, `run` and `build`: the 12 ms between them is the price of
180
+ Python's integers, and it is a per-command default rather than a language decision —
181
+ wrap semantics where a native artifact is expected, full Python semantics
182
+ where a Python program is. Ports are the straightforward one for each tool:
183
+ `@njit`, a `cdef long long` `.pyx`, an annotated module for mypyc, `codon
184
+ build -release`, `nuitka --module`; Numba 0.67, Cython 3.3, mypy 2.3.1,
185
+ Nuitka on CPython 3.13, Codon 0.19.6, PyPy 3.11.15 (warm).
186
+
187
+ ## Turning ordinary Python into it
188
+
189
+ ```bash
190
+ uv run ppy convert src/ --in-place # strict: the output must be valid strict PPY
191
+ uv run ppy migrate src/ --in-place # permissive: rewrite toward it, report the rest
192
+ ```
193
+
194
+ `convert` is the compiler-facing command: it refuses to produce anything
195
+ `ppy check` would reject, and says why. `migrate` is for normal existing
196
+ Python — it first rewrites dynamic-but-static patterns (`setattr` with a
197
+ constant name, `globals()["X"] = ...`, constant `importlib.import_module`),
198
+ then staticizes, and classifies whatever remains (`--report migration.json`,
199
+ `--diff`).
200
+
201
+ Given untyped Python:
202
+
203
+ ```python
204
+ import math
205
+
206
+ LIMIT = 3.0
207
+
208
+
209
+ def clamp(value):
210
+ return min(value, LIMIT)
211
+
212
+
213
+ def spread(samples):
214
+ total = 0.0
215
+ for sample in samples:
216
+ total += clamp(sample)
217
+ return math.sqrt(total / len(samples))
218
+
219
+
220
+ print(spread([1.0, 2.0, 9.0]), spread((4.0, 5.0)))
221
+ ```
222
+
223
+ `ppy convert` writes:
224
+
225
+ ```python
226
+ import math
227
+ from collections.abc import Sequence
228
+ from typing import Final
229
+
230
+ import ppy
231
+
232
+ LIMIT: Final[float] = 3.0
233
+
234
+
235
+ @ppy.pure
236
+ def clamp(value: float) -> float:
237
+ return min(value, LIMIT)
238
+
239
+
240
+ @ppy.pure
241
+ def spread(samples: Sequence[float]) -> float:
242
+ total: float = 0.0
243
+ for sample in samples:
244
+ total += clamp(sample)
245
+ return math.sqrt(total / len(samples))
246
+
247
+
248
+ print(spread([1.0, 2.0, 9.0]), spread((4.0, 5.0)))
249
+ ```
250
+
251
+ Types come from the whole call graph, not one file. `samples` is `Sequence`
252
+ rather than `list` because the body only reads it and one call site passes a
253
+ tuple; `LIMIT` is `Final` because nothing rebinds it; `@ppy.pure` is attached
254
+ only where the checker proved it. Nothing is renamed and no function is split —
255
+ those are design decisions, not mechanical ones.
256
+
257
+ ## Works with
258
+
259
+ Each library is a plugin: the compiler learns that library's types and effects,
260
+ takes a faster path where it can prove one is equivalent, and falls back to the
261
+ ordinary Python call everywhere else. A guard that fails is a fallback, never a
262
+ different answer.
263
+
264
+ **NumPy** — elementwise expressions fuse into a single loop with no
265
+ temporaries; `dot`, `matmul`, `inner`, `vdot`, and `tensordot` route to the
266
+ linear-algebra path. Contiguity and shape are guarded at runtime, not assumed.
267
+ Reduction order is preserved unless `@ppy.fastmath` permits reassociation, so a
268
+ sum stays bit-identical to NumPy's.
269
+
270
+ **PyTorch** — a function whose body is entirely curated tensor operations (55 of
271
+ them) compiles into one C++ region calling ATen directly, removing a Python
272
+ round trip per operator. Every call still goes through the dispatcher, so
273
+ autograd, device selection, and backend keys are unchanged; a tensor subclass or
274
+ a `__torch_function__` override trips the guard and the Python body runs. CUDA
275
+ is used when it is there.
276
+
277
+ **JAX / Flax** — a `@jax.jit` function whose inputs carry `ppy.Shape` and
278
+ `ppy.DType` can be exported to StableHLO at build time, so the trace is not
279
+ repeated at startup; shapes may be symbolic, so one artifact serves every
280
+ batch size (export runs project code and is off until the project opts in).
281
+ The same plugin models Flax and optax — layers, activations, `Module.init`/
282
+ `apply` through the external MRO, optimizers — so a Flax training loop
283
+ checks under strict mode as-is.
284
+
285
+ **Pydantic** — models are typed, constructor and output shapes are kept
286
+ distinct, and field constraints become refinements the checker can use.
287
+
288
+ **Uvicorn / FastAPI** — the ASGI application is resolved statically instead of
289
+ re-imported by module string per worker, and the reloader is told to watch
290
+ `.ppy`. FastAPI rides the same plugin: its surface is modeled so strict mode
291
+ checks a FastAPI service as-is, while route handlers keep the exact
292
+ signatures FastAPI reads at import.
293
+
294
+ The plugin's exact version is part of every cache key, so an artifact built
295
+ against one build of a library is never reused against another. `ppy doctor`
296
+ prints what it found.
297
+
298
+ ## Docs
299
+
300
+ - [docs/guide.md](docs/guide.md) — overview, measurements, the import hook
301
+ - [docs/language.md](docs/language.md) — the subset, directives, markers
302
+ - [docs/conversion.md](docs/conversion.md) — how `ppy convert` and `ppy migrate` infer what they write
303
+ - [docs/architecture.md](docs/architecture.md) — pipeline, cache, threads
304
+ - [docs/plugins.md](docs/plugins.md) — how each library integration works
305
+ - [docs/config.md](docs/config.md) — every `[tool.ppy]` key
306
+ - [docs/diagnostics.md](docs/diagnostics.md) — every diagnostic code
307
+ - [docs/cli.md](docs/cli.md) — every command and option
308
+ - [docs/compatibility.md](docs/compatibility.md) — what is stable, what moves, and what the cache and artifact ABI promise
309
+ - [examples/README.md](examples/README.md) — 30 folders, 39 runnable programs
310
+ - [CONTRIBUTING.md](CONTRIBUTING.md) — setting up, the gate, and what a change has to keep true
311
+ - [the implementation spec](ppy-compiler-implementation-spec-v1.md) — the normative baseline the source cites by section number (`spec 11.2`, `spec 16.4`, …)
312
+
313
+ ## Development
314
+
315
+ ```bash
316
+ git clone https://github.com/franknoh/PPy.git
317
+ cd PPy
318
+ uv sync # the compiler core, LLVM, NumPy, pydantic, and the linters
319
+ ./scripts/check.sh # the one gate; CI runs exactly this
320
+ ```
321
+
322
+ Plugin runtimes are separate groups (`uv sync --group torch|jax|uvicorn|all`),
323
+ and everything else a contributor needs — the gate, the invariants a change
324
+ has to keep, how examples and measurements are kept current — is in
325
+ [CONTRIBUTING.md](CONTRIBUTING.md).