Skip to content

Commit cc14724

Browse files
authored
Merge pull request #357 from thisisthepy/feat/typedpython-drop-module-pragma
TypedPython: @compiled is the only mark; drop the module pragma
2 parents 66a1410 + cca2ef5 commit cc14724

32 files changed

Lines changed: 520 additions & 129 deletions

‎benchmarks/typedpython/results/2026-10-17-buffer-atomic-kernels.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,21 @@
1-
# typedpython: compiled
21
import array
32

43

4+
@compiled
55
def dot_arr(a: array.array[float], b: array.array[float]) -> float:
66
s: float = 0.0
77
for i in range(len(a)):
88
s += a[i] * b[i]
99
return s
1010

1111

12+
@compiled
1213
def scale_arr(a: array.array[float], k: float) -> None:
1314
for i in range(len(a)):
1415
a[i] = a[i] * k
1516

1617

18+
@compiled
1719
def dot_list(a: list[float], b: list[float]) -> float:
1820
s: float = 0.0
1921
for i in range(len(a)):

‎benchmarks/typedpython/run.py‎

Lines changed: 14 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,14 +10,15 @@
1010
The "TypedPython @compiled" column runs only with `--typedpython <dir>` (the directory that holds
1111
the `typedpython` package, python-multiplatform-ksp/src/main/python) and `--typedpython-python` (an
1212
interpreter with the gate's dependencies, e.g. that module's .venv): each benchmark is copied with
13-
the `# typedpython: compiled` marker, compiled through `typedpython.pipeline`, and its `main()` is
13+
`@compiled` above every top-level def and class, compiled through `typedpython.pipeline`, and its `main()` is
1414
run from the built extension. Without the option the column reports n/a.
1515
1616
binary-trees declares `__slots__` (TypedPython compiles fixed-layout classes only, SPEC N-11). The
1717
speedup is reported against that CPython version; `CPython (no __slots__)` runs the same program
1818
with the `__slots__` line removed, so what adding the slots changes in CPython is recorded too.
1919
"""
2020
import argparse
21+
import ast
2122
import datetime
2223
import json
2324
import os
@@ -87,6 +88,17 @@ def build() -> None:
8788
"""
8889

8990

91+
def with_compiled(source: str) -> str:
92+
"""`source` with `@compiled` above every top-level def and class (above their other decorators)."""
93+
tree = ast.parse(source)
94+
lines = source.split("\n")
95+
firsts = [min([n.lineno] + [d.lineno for d in n.decorator_list]) for n in tree.body
96+
if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef))]
97+
for first in reversed(firsts):
98+
lines.insert(first - 1, "@compiled")
99+
return "\n".join(lines)
100+
101+
90102
def build_compiled(tp_dir: Path, tp_python: str) -> dict[str, tuple[str, list[str]] | str]:
91103
"""Compile each benchmark with TypedPython; name -> (extension, compiled functions) or a reason."""
92104
built: dict[str, tuple[str, list[str]] | str] = {}
@@ -95,7 +107,7 @@ def build_compiled(tp_dir: Path, tp_python: str) -> dict[str, tuple[str, list[st
95107
work = TP_BUILD / name
96108
work.mkdir(parents=True, exist_ok=True)
97109
source = work / py
98-
source.write_text("# typedpython: compiled\n" + (ROOT / "py" / py).read_text())
110+
source.write_text(with_compiled((ROOT / "py" / py).read_text()))
99111
result = subprocess.run([tp_python, "-c", COMPILE_SNIPPET, str(source), str(work / "out")],
100112
capture_output=True, text=True, env=env)
101113
if result.returncode != 0:

‎docs/SPEC.md‎

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -724,7 +724,7 @@ All in `PM/commonTest`, so they run wherever the interpreter loads.
724724
with `.diagnostics`) before the module runs. Results are cached in memory by file content hash, failures
725725
included. `Status: implemented`, `python-multiplatform-ksp/src/test/python/test_importgate.py`.
726726
- **N-8** TypedPython compilation to C (INTENT §1.4; design §4.3; #41). `@compiled` (a builtin name, no
727-
import, #42) and a first-line `# typedpython: compiled` mark functions; the compiler lowers them
727+
import, #42) is the only mark for functions (the first-line module pragma was removed, #353); the compiler lowers them
728728
through the typed IR (`typedpython/ir.py`) to C against the CPython C API and builds an extension with
729729
the platform C compiler. Compiled functions return what CPython returns (IR contract: checked i64
730730
with deopt to the interpreted function, Python floor division/modulo, exact int/float comparison,

‎docs/design/typedpython.md‎

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -62,7 +62,7 @@ TypedPython 의 목표는 **새 언어를 만들지 않고, 파이썬 문법 그
6262
|---|---|---|---|
6363
| 0 | 서드파티 패키지 (의존성) | 없음, 검사하지 않는다 | 바이트코드 (pypackpack 이 받은 그대로) |
6464
| 1 `checked` | **사용자 프로젝트의 파이썬 코드 전부, 기본값** | 빌드 타임 타입 오류, `Any` 유출 0 (§4.2) | 바이트코드 |
65-
| 1 `compiled` | `@compiled` (내장 이름, import 없음) 또는 모듈 선언 `# typedpython: compiled` | `checked` + §3.1 금지 목록 | C 확장 모듈 (NEON · AVX 벡터화, §5.3) |
65+
| 1 `compiled` | `@compiled` (내장 이름, import 없음) | `checked` + §3.1 금지 목록 | C 확장 모듈 (NEON · AVX 벡터화, §5.3) |
6666
| 2 `kernel` | `@kernel` | 객체·힙 할당 없음, 고정 dtype | SPIR-V / MSL (직접) / WGSL / PTX, 빌드 타임 (§5) |
6767

6868
- **사용자 코드는 표시하지 않아도 검사된다.** 타입 오류가 있으면 Kotlin 컴파일 에러처럼 빌드가
@@ -685,9 +685,7 @@ upstream 대비 수치 오차와 커널 시간을 기준선으로 박아 둔다.
685685
인터프리터 시작 시 `builtins.compiled` 를 설치하고(#42), 일반 CPython 에서는 `typedpython` wheel 의
686686
`typedpython_builtins.pth` 가 `builtins` 에 없을 때만 항등 함수를 설치한다(런타임이 넣은 것을 덮어쓰지
687687
않는다). 타입 검사기에는 `__builtins__.pyi` 로 알린다, Pyrefly 1.3.2 는 검색 경로의 그 파일을 추가
688-
내장으로 읽는다(실측). 모듈 단위 표기는 `# typedpython: compiled` 주석을 유지한다: `# pyright: strict` 와 같은
689-
도구 pragma 형태이고, 실행 시 이름 조회가 필요 없어 일반 CPython 에서 부작용이 없으며, 데코레이터 없이
690-
모듈 전체를 지정할 수 있다.
688+
내장으로 읽는다(실측). 모듈 단위 pragma(`# typedpython: compiled`)는 2026-10-30 메인테이너 결정으로 제거했다(#353). `@compiled` 가 컴파일 대상을 표시하는 유일한 수단이다.
691689
2. ~~고정폭 정수의 오버플로 의미~~, **결정됨:** `int` 는 i64 로 컴파일하고 오버플로 시 인터프리터 재실행으로 큰 정수로 승격(의미 보존).
692690
3. ~~pypackpack Cython 슬롯 인터페이스 소유~~, **결정됨:** pypackpack 이 정한다. TypedPython 은 생성한 C 소스, 빌드 플래그, C 를 낸 모듈 목록을 넘긴다. 확장이 원본 소스를 품으므로 별도 `.py` 폴백은 없다(Cython 폐기 후 갱신).
693691
4. ~~GPU 착지를 torchnative 커스텀 op 로 할 것인가~~, **결정됨 (2026-10-04, 메인테이너):** 커널은

‎docs/release/desktop/typedpython/kernels.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
1-
# typedpython: compiled
21
import sys
32

43

4+
@compiled
55
def dot(n: int) -> float:
66
s = 0.0
77
for i in range(n):
@@ -10,6 +10,7 @@ def dot(n: int) -> float:
1010
return s
1111

1212

13+
@compiled
1314
def collatz_steps(limit: int) -> int:
1415
total = 0
1516
for start in range(1, limit):

‎python-multiplatform-ksp/fixtures/app/src/desktopTest/typedpython/tp_kotlin_bridge.py‎

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,3 @@
1-
# typedpython: compiled
21
"""Issue #45: compiled TypedPython code calling Kotlin through the binder.
32
43
Every name imported below is a binder proxy (`fixture/app/TypedPythonBridge.kt`, published by
@@ -21,6 +20,7 @@
2120
from fixture.app import TpSample, tpAccumulate, tpCheck, tpTouch
2221

2322

23+
@compiled
2424
def drive(n: int, scale: float, token: object) -> float:
2525
"""Per step: a Kotlin constructor call that returns a typed proxy, a method call on that proxy,
2626
the proxy handed back to a Kotlin function, and a PyObject handed to Kotlin -- inside native
@@ -35,6 +35,7 @@ def drive(n: int, scale: float, token: object) -> float:
3535
return total
3636

3737

38+
@compiled
3839
def checked(n: int, limit: float) -> float:
3940
"""`tpCheck` throws in Kotlin once `acc` passes `limit`; the caller sees what Python sees."""
4041
acc: float = 0.0

‎python-multiplatform-ksp/src/main/python/typedpython/__main__.py‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ def _build(paths: list[Path], cache: Path, out: Path | None) -> int:
8484
print(f"{m.status:7} {m.name}: {what}{why}")
8585
rel = m.path.relative_to(root).as_posix()
8686
if m.kind == "not_marked":
87-
print(f" {rel}: not marked for compilation (no `# typedpython: compiled`, no @compiled)")
87+
print(f" {rel}: not marked for compilation (no @compiled)")
8888
for item in m.skipped_items:
8989
where = f"{item.file}:{item.line}" if item.line is not None else item.file
9090
print(f" {where}: {item.name}: {item.message}")
@@ -112,6 +112,8 @@ def _demo(module: Path, module_argv: list[str], out: Path) -> int:
112112
return EXIT_FAILURE
113113

114114
def interpreted() -> object:
115+
import builtins # the module's `@compiled` is a builtin name; typedpython_builtins.pth installs it
116+
builtins.compiled = getattr(builtins, "compiled", lambda f, /: f)
115117
spec = importlib.util.spec_from_file_location(module.stem + "_interpreted", module)
116118
assert spec is not None and spec.loader is not None
117119
mod = importlib.util.module_from_spec(spec)

‎python-multiplatform-ksp/src/main/python/typedpython/frontend.py‎

Lines changed: 7 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,7 @@
33
`lower(path)` reads one module and returns an `ir.Module`:
44
55
- Which functions: module-level `def`s decorated with the builtin `@compiled` (a bare name, no
6-
import, #42), or every module-level `def` when the first non-empty line is
7-
`# typedpython: compiled`.
6+
import, #42). That decorator is the only mark; there is no module-level pragma (#353).
87
- A function is lowered only when **all** of it is expressible in the IR; otherwise it is left out
98
and `Module.skipped[name]` says why, with the line. There is no partial lowering.
109
- Types: parameters and returns from their annotations (`int`/`float`/`bool`, `list[float]` and
@@ -80,8 +79,6 @@
8079
from typedpython import gate, ir, pyrefly, rebinding, resume, taint
8180
from typedpython.ir import Type
8281

83-
MARKER = "# typedpython: compiled"
84-
8582
I64_MIN, I64_MAX = -2**63, 2**63 - 1
8683
TOP = (I64_MIN, I64_MAX)
8784
EXACT_INT_FLOAT = 2**53 # |n| <= this: int/int true division is exact-rounded in C (ir.BinOp)
@@ -249,7 +246,7 @@ def lower(path: Path) -> ir.Module:
249246

250247
def _parallel_marked(tree: ast.Module, source: str) -> set[str]:
251248
"""Module-level functions the user marked for SPEC N-15: the comment line `# typedpython: parallel`
252-
directly above the `def` (above its decorators if it has any). A comment, like the module marker,
249+
directly above the `def` (above its decorators if it has any). A comment,
253250
so the interpreted module is unchanged and needs no runtime name."""
254251
lines = source.splitlines()
255252
out: set[str] = set()
@@ -434,17 +431,10 @@ def _binding_targets(stmt: ast.stmt) -> list[ast.expr]:
434431
SLOT_ACCESS_DUNDERS = ("__getattribute__", "__getattr__", "__setattr__", "__delattr__", "__new__")
435432

436433

437-
def _whole_module(source: str) -> bool:
438-
return next((line.strip() for line in source.splitlines() if line.strip()), "") == MARKER
439-
440-
441434
def is_opted_in(path: Path) -> bool:
442-
"""Whether the front end considers the module for compilation at all: the `# typedpython: compiled`
443-
marker as its first non-empty line, or a module-level `def` / `class` decorated `@compiled`
444-
(the same tests `_targets` and `_classes` apply)."""
435+
"""Whether the front end considers the module for compilation at all: a module-level `def` / `class`
436+
decorated `@compiled` (the same test `_targets` and `_classes` apply)."""
445437
source = Path(path).read_text()
446-
if _whole_module(source):
447-
return True
448438
return any(
449439
isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef))
450440
and any(isinstance(d, ast.Name) and d.id == "compiled" for d in node.decorator_list)
@@ -454,14 +444,13 @@ def is_opted_in(path: Path) -> bool:
454444
def _classes(
455445
tree: ast.Module, source: str, info: _ModuleInfo,
456446
) -> tuple[dict[str, ir.ClassDecl], dict[str, str]]:
457-
whole = _whole_module(source)
458447
decls: dict[str, ir.ClassDecl] = {}
459448
skipped: dict[str, str] = {}
460449
for node in tree.body:
461450
if not isinstance(node, ast.ClassDef):
462451
continue
463452
marked = any(isinstance(d, ast.Name) and d.id == "compiled" for d in node.decorator_list)
464-
if not (whole or marked):
453+
if not marked:
465454
continue
466455
result = _class_decl(node, tree, info)
467456
if isinstance(result, str):
@@ -608,13 +597,12 @@ def _class_of(kind: str, classes: dict[str, ir.ClassDecl]) -> tuple[str, bool] |
608597
def _targets(
609598
tree: ast.Module, source: str, info: _ModuleInfo,
610599
) -> list[tuple[ast.FunctionDef | ast.AsyncFunctionDef, str | None]]:
611-
whole_module = _whole_module(source)
612600
found: list[tuple[ast.FunctionDef | ast.AsyncFunctionDef, str | None]] = []
613601
for node in tree.body:
614602
if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)):
615603
continue
616604
marked = [d for d in node.decorator_list if isinstance(d, ast.Name) and d.id == "compiled"]
617-
if not (whole_module or marked):
605+
if not marked:
618606
continue
619607
reason = None
620608
if marked and info.bound("compiled"):
@@ -2499,4 +2487,4 @@ def _breaks(body: tuple[ir.Stmt, ...]) -> bool:
24992487
return False
25002488

25012489

2502-
__all__ = ["lower", "MARKER"]
2490+
__all__ = ["lower"]

‎python-multiplatform-ksp/src/test/python/conftest.py‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,36 @@
1+
import ast
2+
import builtins
13
import sys
24
from pathlib import Path
35
import textwrap
46

57
import pytest
68

9+
# The `typedpython_builtins.pth` of an installed wheel; the test interpreter has no such install.
10+
builtins.compiled = getattr(builtins, "compiled", lambda f, /: f)
11+
12+
# Prepended to every child-process script that loads a module whose source uses `@compiled`: a plain
13+
# CPython has the builtin only when `typedpython_builtins.pth` is installed, so the child installs it.
14+
COMPILED_SHIM = "import builtins\nif not hasattr(builtins, 'compiled'):\n builtins.compiled = lambda f: f\n"
15+
716
FIXTURES = Path(__file__).parent / "fixtures"
817

918

19+
def decorate_compiled(source: str) -> str:
20+
"""`source` with `@compiled` put above every module-level def / class (above its other decorators),
21+
the set a whole-module mark used to cover. A source that does not parse is returned unchanged."""
22+
try:
23+
tree = ast.parse(source)
24+
except SyntaxError:
25+
return source
26+
lines = source.split("\n")
27+
firsts = [min([n.lineno] + [d.lineno for d in n.decorator_list]) for n in tree.body
28+
if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef))]
29+
for first in reversed(firsts):
30+
lines.insert(first - 1, "@compiled")
31+
return "\n".join(lines)
32+
33+
1034
@pytest.fixture
1135
def write(tmp_path):
1236
"""Write a dedented source file under tmp_path and return its path."""

0 commit comments

Comments
 (0)