Skip to content

Commit b1ef413

Browse files
authored
Merge pull request #64 from thisisthepy/work/typedpython-m2
TypedPython stage 2: typed IR → C back end, end to end on desktop
2 parents fa2558e + 9a16536 commit b1ef413

44 files changed

Lines changed: 15162 additions & 17 deletions

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

‎.github/workflows/typedpython.yml‎

Lines changed: 77 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,77 @@
1+
name: TypedPython CI
2+
3+
# The TypedPython gate and compiler are Python (python-multiplatform-ksp/src/{main,test}/python).
4+
# Gradle does not run them; this workflow does.
5+
on:
6+
push:
7+
branches: [ "develop", "main" ]
8+
paths: [ "python-multiplatform-ksp/**", ".github/workflows/typedpython.yml" ]
9+
pull_request:
10+
branches: [ "develop", "main" ]
11+
paths: [ "python-multiplatform-ksp/**", ".github/workflows/typedpython.yml" ]
12+
13+
jobs:
14+
test:
15+
name: TypedPython tests (${{ matrix.os }})
16+
runs-on: ${{ matrix.os }}
17+
strategy:
18+
fail-fast: false
19+
matrix:
20+
os: [ubuntu-latest, macos-latest]
21+
defaults:
22+
run:
23+
working-directory: python-multiplatform-ksp
24+
steps:
25+
- uses: actions/checkout@v4
26+
27+
- uses: astral-sh/setup-uv@v5
28+
29+
- name: Create the environment
30+
# pyrefly is pinned exactly: the gate reads its versioned type-report format.
31+
run: |
32+
uv venv .venv --python 3.13
33+
uv pip install --python .venv/bin/python "pyrefly==1.3.2" pytest
34+
35+
- name: Behaviour tests
36+
run: .venv/bin/python -m pytest -q -m "not measurement"
37+
38+
- name: Measurements (recorded, sanity bounds only)
39+
# A shared runner is not an idle machine (AGENTS.md §8): read ratios, not absolute times.
40+
run: .venv/bin/python -m pytest -q -s -m measurement
41+
42+
sanitizers:
43+
# SPEC N-8: ASan/UBSan runs are the safety net outside the proof. Every extension TypedPython
44+
# builds in these tests is compiled with -fsanitize=address,undefined (TP_SANITIZE=1, see
45+
# conftest.py) and the interpreter runs with the ASan runtime preloaded; any report fails the run.
46+
# Linux only: macOS refuses to load the ASan runtime into the framework Python (the cgen
47+
# sanitizer test has a launcher-based macOS path of its own).
48+
name: TypedPython tests under ASan + UBSan (ubuntu-latest)
49+
runs-on: ubuntu-latest
50+
defaults:
51+
run:
52+
working-directory: python-multiplatform-ksp
53+
steps:
54+
- uses: actions/checkout@v4
55+
56+
- uses: astral-sh/setup-uv@v5
57+
58+
- name: Create the environment
59+
run: |
60+
uv venv .venv --python 3.13
61+
uv pip install --python .venv/bin/python "pyrefly==1.3.2" pytest
62+
63+
- name: Relax ASLR entropy (ASan shadow mapping on newer kernels)
64+
run: sudo sysctl -w vm.mmap_rnd_bits=28 || true
65+
66+
- name: Self-check, the sanitizers must catch planted faults
67+
# Builds a deliberately faulty extension (heap out-of-bounds read, signed overflow) and
68+
# REQUIRES both reports. If this step passes, the setup below can detect faults.
69+
run: .venv/bin/python tools/sanitizers.py selfcheck
70+
71+
- name: cgen, runtime and pipeline tests, sanitized
72+
# detect_leaks=0: CPython itself leaks at exit. LD_PRELOAD is applied to this one command
73+
# only (tools/sanitizers.py env prints it); the gate's pyrefly runs without it.
74+
run: |
75+
env $(.venv/bin/python tools/sanitizers.py env) \
76+
.venv/bin/python -m pytest -q -m "not measurement" \
77+
src/test/python/test_cgen.py src/test/python/test_runtime.py src/test/python/test_pipeline.py

‎docs/INTENT.md‎

Lines changed: 18 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,23 @@ CPython interpreter inside a Kotlin Multiplatform application so that:
2626
written (`Greeter('Kotlin').greet(2)` in `sample/`).
2727
3. **The same code runs on every target** a Kotlin Multiplatform app ships to: desktop JVM, Android,
2828
iOS, Android native, and the web (wasm, experimental).
29+
4. **The project's own Python is statically typed by default** (TypedPython). The maintainer's goal:
30+
31+
> "파이썬을 컴파일 언어 및 타입 안정화, 정적 타입 검사, 컴파일 타임 타입검사를 지원할 수 있도록"
32+
> — *Support Python as a compiled language with type stability, static type checking and
33+
> compile-time type checking*, so that Python stays usable as AI-friendly languages such as Rust
34+
> and TypeScript gain ground.
35+
36+
> "TypedPython을 별도 레포가 아니라 PythonMultiplatform의 기본 동작이 될거야"
37+
> — *TypedPython is not a separate repository; it will be PythonMultiplatform's default behaviour.*
38+
39+
The maintainer chose the scope "검사만 기본, 컴파일은 opt-in" (2026-10-02): every user Python source
40+
is **checked** by default; **compiling** it to native code is opt-in. The compiled code still runs on
41+
the embedded CPython, as an extension module (`docs/design/typedpython.md`).
42+
43+
> The maintainer also asked to *explore* GPU kernels written in Python without a separate language
44+
> ("GPU 관련 최적화까지도 별도 언어 없이 지원할 방안을 모색해보자"). That is an exploration, not yet
45+
> a commitment; it is recorded in the design note only.
2946
3047
The CPython version moved on from 3.13 (the build pins 3.14.x today and offers opt-in free-threaded 3.14t builds
3148
on desktop only); the intent — embed **CPython itself**, not a re-implementation — is unchanged.
@@ -88,7 +105,7 @@ no per-platform Python → Kotlin binding code to maintain.
88105
| A replacement for the `pythonx` packages | §2.3 — they are real packages in their own repository |
89106
| Runtime-reflection or name-lookup based binding | §2.1 — impossible under Kotlin/Native and native image |
90107
| A hand-written wrapper per Kotlin function or class | Generic adaptation plus generated tables; per-declaration wrappers were tried in 2024 and rotted (`docs/archive/pythonx-adapter-design.md`) |
91-
| A Python re-implementation, transpiler, or JVM-hosted Python | It embeds CPython itself, so C extensions keep working |
108+
| A Python re-implementation, transpiler, or JVM-hosted Python | It embeds CPython itself, so C extensions keep working. TypedPython (§1.4) does not change this: checked code is ordinary Python, and compiled code is a CPython extension module, not a translation to another runtime |
92109
| A build tool, dependency resolver or packager | That is pypackpack (the work) and toolchain (the Gradle vocabulary) |
93110
| A sub-interpreter parallelism model | Rejected: extensions that do not declare multi-interpreter support fail to import; parallelism comes from free-threaded CPython |
94111
| A vendored interpreter | The build downloads and verifies CPython per platform |

‎docs/SPEC.md‎

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -317,6 +317,50 @@ All in `PM/commonTest`, so they run wherever the interpreter loads.
317317
`androidx.compose.runtime.mutableStateOf(x)` callable, and `.value` of the returned `MutableState`
318318
readable and writable on its proxy. Implemented by #38 (B-1, U-10); B-8's test now writes its root
319319
through `state.value = root` and the KSP-bound stand-in is gone. `Status: implemented`.
320+
- **N-7** TypedPython soundness gate (INTENT §1.4; `docs/design/typedpython.md` §4.2): checking a set of
321+
user Python files reports every Pyrefly type error, the Any-leak kinds (`implicit-any-parameter`,
322+
`unannotated-return`, `unknown-variable-type`, `no-any-return-implicit`, `no-any-return-explicit`,
323+
`explicit-any`) and any user-code expression of type `Any` outside `cast()` (`any-flow`) as errors;
324+
the forbidden list (`eval`/`exec`, computed-name `getattr`/`setattr`/`delattr`, patching an imported
325+
module or a module-level class, rebinding an unannotated name to a different type, and a list/set/dict
326+
display whose inferred element type is a union) as warnings in `checked` mode and errors in
327+
`compiled` mode. Nothing
328+
inside dependencies is reported. `python -m typedpython check` exits 0 / 1 / 2 for clean / errors /
329+
tool failure. `Status: partial` — the gate is asserted by
330+
`python-multiplatform-ksp/src/test/python/` (run with that module's `.venv`); it is **not yet run by the
331+
build** (design §6 step 1b). Rebinding and mixed containers are found by a second Pyrefly pass over a
332+
probed copy (design §6.1), which runs only when a file has a name assigned twice or a container
333+
display; `for` targets and comprehension variables are not probed.
334+
- **N-8** TypedPython compilation to C (INTENT §1.4; design §4.3; #41). `@compiled` (a builtin name, no
335+
import — #42) and a first-line `# typedpython: compiled` mark functions; the compiler lowers them
336+
through the typed IR (`typedpython/ir.py`) to C against the CPython C API and builds an extension with
337+
the platform C compiler. Compiled functions return what CPython returns (IR contract: checked i64
338+
with deopt to the interpreted function, Python floor division/modulo, exact int/float comparison,
339+
CPython's exception types and messages; tracebacks are the one accepted difference). `Status: planned`.
340+
- **N-9** TypedPython memory safety is proved on the IR before C is generated (#41; maintainer decision
341+
2026-10-03). Each property, and what happens when it cannot be proved:
342+
343+
| Property proved on the IR | If not proved |
344+
|---|---|
345+
| No local read before assignment on every path | function not compiled; diagnostic names the local |
346+
| Every node's operand types match; no implicit conversion | not compiled; diagnostic |
347+
| No deopting node in an impure function | not compiled; diagnostic |
348+
| Array access in bounds, or a runtime bounds check kept | the check stays (never an unchecked access) |
349+
| Array parameters only indexed, stored, `len`-ed; no aliasing (entry guard) | not compiled; diagnostic |
350+
| OBJ references follow one ownership rule; no IR form for free, borrowed alias or NULL read | not compiled; diagnostic |
351+
| Integer overflow checked and promoted (deopt); an op marked `proven` must re-prove its interval fits i64 | not compiled; diagnostic `verify/proven-overflow` |
352+
| `Call.redo` only from an impure caller to a pure, may-deopt callee returning F64/BOOL/NONE | not compiled; diagnostic `verify/redo` |
353+
| `entry_globals` only in a closed function (no user code runs); a call of a function with entry globals is a deopt point in its caller unless it is a legal redo or the caller snapshots the same globals | not compiled; diagnostic `verify/entry-global-open` / `verify/deopt-impure` |
354+
355+
Generated C reaches memory and `PyObject`s only through the runtime helpers. ASan/UBSan CI,
356+
differential tests against the interpreter and fuzzing back the proof. `Status: planned`.
357+
- **N-10** TypedPython incremental compilation (#41; maintainer decision 2026-10-03). The unit is a module.
358+
Its cache key is the source hash, the interface hashes (typed signatures) of the modules it depends
359+
on, the compiler version, the target platform and the build flags; a body-only change leaves the
360+
interface hash unchanged, so dependent modules are not recompiled. C objects are cached per module.
361+
The Gradle task declares exact inputs and outputs so up-to-date checks and the build cache apply.
362+
Tests: changing one function body recompiles that module only; an unchanged rebuild spends ~0 s in
363+
the compile step. `Status: planned`.
320364

321365
---
322366

0 commit comments

Comments
 (0)