From a59096f40bf5cdd7f89d6889b353001518a1db03 Mon Sep 17 00:00:00 2001 From: BREW <81044362+b-re-w@users.noreply.github.com> Date: Fri, 30 Oct 2026 04:54:13 +0900 Subject: [PATCH 1/2] TypedPython: @compiled is the only mark; drop the '# typedpython: compiled' module pragma The maintainer decided (2026-10-30) to keep only the builtin @compiled. The pragma is now an ordinary comment; tests, the Kotlin-bridge fixture, benchmarks and the release example decorate the functions the pragma used to cover. --- .../2026-10-17-buffer-atomic-kernels.py | 4 +- benchmarks/typedpython/run.py | 16 +++- docs/SPEC.md | 2 +- docs/design/typedpython.md | 6 +- docs/release/desktop/typedpython/kernels.py | 3 +- .../typedpython/tp_kotlin_bridge.py | 3 +- .../src/main/python/typedpython/__main__.py | 4 +- .../src/main/python/typedpython/frontend.py | 26 ++----- .../src/test/python/conftest.py | 20 +++++ .../src/test/python/test_bitwise.py | 27 ++++++- .../src/test/python/test_buffer_arrays.py | 24 +++++- .../src/test/python/test_buffer_memoryview.py | 11 ++- .../src/test/python/test_buffer_numpy.py | 76 +++++++++++++++++- .../src/test/python/test_class_methods.py | 20 +++-- .../src/test/python/test_cost_rule.py | 19 +++-- .../src/test/python/test_eval_breaker.py | 19 ++++- .../src/test/python/test_fast_entry.py | 13 +++- .../src/test/python/test_free_threaded.py | 20 ++++- .../src/test/python/test_frontend.py | 29 +++++-- .../src/test/python/test_frontend_classes.py | 9 +-- .../src/test/python/test_interval_proofs.py | 17 +++- .../src/test/python/test_module_report.py | 29 +++---- .../src/test/python/test_native_int_impure.py | 35 +++++++-- .../src/test/python/test_osr_exit.py | 4 +- .../src/test/python/test_parallel_loops.py | 78 +++++++++++++++++-- .../src/test/python/test_pipeline.py | 16 ++-- .../test/python/test_pipeline_incremental.py | 19 +++-- .../src/test/python/test_recursion.py | 8 +- .../src/test/python/test_regions.py | 13 +++- .../src/test/python/test_resume.py | 4 +- .../src/test/python/test_str_dict.py | 38 ++++++++- .../src/test/python/test_tracing.py | 5 +- 32 files changed, 498 insertions(+), 119 deletions(-) diff --git a/benchmarks/typedpython/results/2026-10-17-buffer-atomic-kernels.py b/benchmarks/typedpython/results/2026-10-17-buffer-atomic-kernels.py index a0cd59c56..53a917186 100644 --- a/benchmarks/typedpython/results/2026-10-17-buffer-atomic-kernels.py +++ b/benchmarks/typedpython/results/2026-10-17-buffer-atomic-kernels.py @@ -1,7 +1,7 @@ -# typedpython: compiled import array +@compiled def dot_arr(a: array.array[float], b: array.array[float]) -> float: s: float = 0.0 for i in range(len(a)): @@ -9,11 +9,13 @@ def dot_arr(a: array.array[float], b: array.array[float]) -> float: return s +@compiled def scale_arr(a: array.array[float], k: float) -> None: for i in range(len(a)): a[i] = a[i] * k +@compiled def dot_list(a: list[float], b: list[float]) -> float: s: float = 0.0 for i in range(len(a)): diff --git a/benchmarks/typedpython/run.py b/benchmarks/typedpython/run.py index 104ad572d..c6c3aaa47 100644 --- a/benchmarks/typedpython/run.py +++ b/benchmarks/typedpython/run.py @@ -10,7 +10,7 @@ The "TypedPython @compiled" column runs only with `--typedpython ` (the directory that holds the `typedpython` package, python-multiplatform-ksp/src/main/python) and `--typedpython-python` (an interpreter with the gate's dependencies, e.g. that module's .venv): each benchmark is copied with -the `# typedpython: compiled` marker, compiled through `typedpython.pipeline`, and its `main()` is +`@compiled` above every top-level def and class, compiled through `typedpython.pipeline`, and its `main()` is run from the built extension. Without the option the column reports n/a. binary-trees declares `__slots__` (TypedPython compiles fixed-layout classes only, SPEC N-11). The @@ -18,6 +18,7 @@ with the `__slots__` line removed, so what adding the slots changes in CPython is recorded too. """ import argparse +import ast import datetime import json import os @@ -87,6 +88,17 @@ def build() -> None: """ +def with_compiled(source: str) -> str: + """`source` with `@compiled` above every top-level def and class (above their other decorators).""" + tree = ast.parse(source) + lines = source.split("\n") + firsts = [min([n.lineno] + [d.lineno for d in n.decorator_list]) for n in tree.body + if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef))] + for first in reversed(firsts): + lines.insert(first - 1, "@compiled") + return "\n".join(lines) + + def build_compiled(tp_dir: Path, tp_python: str) -> dict[str, tuple[str, list[str]] | str]: """Compile each benchmark with TypedPython; name -> (extension, compiled functions) or a reason.""" 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 work = TP_BUILD / name work.mkdir(parents=True, exist_ok=True) source = work / py - source.write_text("# typedpython: compiled\n" + (ROOT / "py" / py).read_text()) + source.write_text(with_compiled((ROOT / "py" / py).read_text())) result = subprocess.run([tp_python, "-c", COMPILE_SNIPPET, str(source), str(work / "out")], capture_output=True, text=True, env=env) if result.returncode != 0: diff --git a/docs/SPEC.md b/docs/SPEC.md index 1ba12bbc1..61c454c2f 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -724,7 +724,7 @@ All in `PM/commonTest`, so they run wherever the interpreter loads. with `.diagnostics`) before the module runs. Results are cached in memory by file content hash, failures included. `Status: implemented`, `python-multiplatform-ksp/src/test/python/test_importgate.py`. - **N-8** TypedPython compilation to C (INTENT §1.4; design §4.3; #41). `@compiled` (a builtin name, no - import, #42) and a first-line `# typedpython: compiled` mark functions; the compiler lowers them + import, #42) is the only mark for functions (the first-line module pragma was removed, #353); the compiler lowers them through the typed IR (`typedpython/ir.py`) to C against the CPython C API and builds an extension with the platform C compiler. Compiled functions return what CPython returns (IR contract: checked i64 with deopt to the interpreted function, Python floor division/modulo, exact int/float comparison, diff --git a/docs/design/typedpython.md b/docs/design/typedpython.md index 1ae91c1c2..991207854 100644 --- a/docs/design/typedpython.md +++ b/docs/design/typedpython.md @@ -62,7 +62,7 @@ TypedPython 의 목표는 **새 언어를 만들지 않고, 파이썬 문법 그 |---|---|---|---| | 0 | 서드파티 패키지 (의존성) | 없음, 검사하지 않는다 | 바이트코드 (pypackpack 이 받은 그대로) | | 1 `checked` | **사용자 프로젝트의 파이썬 코드 전부, 기본값** | 빌드 타임 타입 오류, `Any` 유출 0 (§4.2) | 바이트코드 | -| 1 `compiled` | `@compiled` (내장 이름, import 없음) 또는 모듈 선언 `# typedpython: compiled` | `checked` + §3.1 금지 목록 | C 확장 모듈 (NEON · AVX 벡터화, §5.3) | +| 1 `compiled` | `@compiled` (내장 이름, import 없음) | `checked` + §3.1 금지 목록 | C 확장 모듈 (NEON · AVX 벡터화, §5.3) | | 2 `kernel` | `@kernel` | 객체·힙 할당 없음, 고정 dtype | SPIR-V / MSL (직접) / WGSL / PTX, 빌드 타임 (§5) | - **사용자 코드는 표시하지 않아도 검사된다.** 타입 오류가 있으면 Kotlin 컴파일 에러처럼 빌드가 @@ -685,9 +685,7 @@ upstream 대비 수치 오차와 커널 시간을 기준선으로 박아 둔다. 인터프리터 시작 시 `builtins.compiled` 를 설치하고(#42), 일반 CPython 에서는 `typedpython` wheel 의 `typedpython_builtins.pth` 가 `builtins` 에 없을 때만 항등 함수를 설치한다(런타임이 넣은 것을 덮어쓰지 않는다). 타입 검사기에는 `__builtins__.pyi` 로 알린다, Pyrefly 1.3.2 는 검색 경로의 그 파일을 추가 - 내장으로 읽는다(실측). 모듈 단위 표기는 `# typedpython: compiled` 주석을 유지한다: `# pyright: strict` 와 같은 - 도구 pragma 형태이고, 실행 시 이름 조회가 필요 없어 일반 CPython 에서 부작용이 없으며, 데코레이터 없이 - 모듈 전체를 지정할 수 있다. + 내장으로 읽는다(실측). 모듈 단위 pragma(`# typedpython: compiled`)는 2026-10-30 메인테이너 결정으로 제거했다(#353). `@compiled` 가 컴파일 대상을 표시하는 유일한 수단이다. 2. ~~고정폭 정수의 오버플로 의미~~, **결정됨:** `int` 는 i64 로 컴파일하고 오버플로 시 인터프리터 재실행으로 큰 정수로 승격(의미 보존). 3. ~~pypackpack Cython 슬롯 인터페이스 소유~~, **결정됨:** pypackpack 이 정한다. TypedPython 은 생성한 C 소스, 빌드 플래그, C 를 낸 모듈 목록을 넘긴다. 확장이 원본 소스를 품으므로 별도 `.py` 폴백은 없다(Cython 폐기 후 갱신). 4. ~~GPU 착지를 torchnative 커스텀 op 로 할 것인가~~, **결정됨 (2026-10-04, 메인테이너):** 커널은 diff --git a/docs/release/desktop/typedpython/kernels.py b/docs/release/desktop/typedpython/kernels.py index 81cc9f952..3fb98914e 100644 --- a/docs/release/desktop/typedpython/kernels.py +++ b/docs/release/desktop/typedpython/kernels.py @@ -1,7 +1,7 @@ -# typedpython: compiled import sys +@compiled def dot(n: int) -> float: s = 0.0 for i in range(n): @@ -10,6 +10,7 @@ def dot(n: int) -> float: return s +@compiled def collatz_steps(limit: int) -> int: total = 0 for start in range(1, limit): diff --git a/python-multiplatform-ksp/fixtures/app/src/desktopTest/typedpython/tp_kotlin_bridge.py b/python-multiplatform-ksp/fixtures/app/src/desktopTest/typedpython/tp_kotlin_bridge.py index 7677eca9c..311eb2f5b 100644 --- a/python-multiplatform-ksp/fixtures/app/src/desktopTest/typedpython/tp_kotlin_bridge.py +++ b/python-multiplatform-ksp/fixtures/app/src/desktopTest/typedpython/tp_kotlin_bridge.py @@ -1,4 +1,3 @@ -# typedpython: compiled """Issue #45: compiled TypedPython code calling Kotlin through the binder. Every name imported below is a binder proxy (`fixture/app/TypedPythonBridge.kt`, published by @@ -21,6 +20,7 @@ from fixture.app import TpSample, tpAccumulate, tpCheck, tpTouch +@compiled def drive(n: int, scale: float, token: object) -> float: """Per step: a Kotlin constructor call that returns a typed proxy, a method call on that proxy, 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: return total +@compiled def checked(n: int, limit: float) -> float: """`tpCheck` throws in Kotlin once `acc` passes `limit`; the caller sees what Python sees.""" acc: float = 0.0 diff --git a/python-multiplatform-ksp/src/main/python/typedpython/__main__.py b/python-multiplatform-ksp/src/main/python/typedpython/__main__.py index bd220e713..a9cda63e2 100644 --- a/python-multiplatform-ksp/src/main/python/typedpython/__main__.py +++ b/python-multiplatform-ksp/src/main/python/typedpython/__main__.py @@ -84,7 +84,7 @@ def _build(paths: list[Path], cache: Path, out: Path | None) -> int: print(f"{m.status:7} {m.name}: {what}{why}") rel = m.path.relative_to(root).as_posix() if m.kind == "not_marked": - print(f" {rel}: not marked for compilation (no `# typedpython: compiled`, no @compiled)") + print(f" {rel}: not marked for compilation (no @compiled)") for item in m.skipped_items: where = f"{item.file}:{item.line}" if item.line is not None else item.file print(f" {where}: {item.name}: {item.message}") @@ -112,6 +112,8 @@ def _demo(module: Path, module_argv: list[str], out: Path) -> int: return EXIT_FAILURE def interpreted() -> object: + import builtins # the module's `@compiled` is a builtin name; typedpython_builtins.pth installs it + builtins.compiled = getattr(builtins, "compiled", lambda f, /: f) spec = importlib.util.spec_from_file_location(module.stem + "_interpreted", module) assert spec is not None and spec.loader is not None mod = importlib.util.module_from_spec(spec) diff --git a/python-multiplatform-ksp/src/main/python/typedpython/frontend.py b/python-multiplatform-ksp/src/main/python/typedpython/frontend.py index 0da894f10..6e0b75aa6 100644 --- a/python-multiplatform-ksp/src/main/python/typedpython/frontend.py +++ b/python-multiplatform-ksp/src/main/python/typedpython/frontend.py @@ -3,8 +3,7 @@ `lower(path)` reads one module and returns an `ir.Module`: - Which functions: module-level `def`s decorated with the builtin `@compiled` (a bare name, no - import, #42), or every module-level `def` when the first non-empty line is - `# typedpython: compiled`. + import, #42). That decorator is the only mark; there is no module-level pragma (#353). - A function is lowered only when **all** of it is expressible in the IR; otherwise it is left out and `Module.skipped[name]` says why, with the line. There is no partial lowering. - Types: parameters and returns from their annotations (`int`/`float`/`bool`, `list[float]` and @@ -80,8 +79,6 @@ from typedpython import gate, ir, pyrefly, rebinding, resume, taint from typedpython.ir import Type -MARKER = "# typedpython: compiled" - I64_MIN, I64_MAX = -2**63, 2**63 - 1 TOP = (I64_MIN, I64_MAX) 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: def _parallel_marked(tree: ast.Module, source: str) -> set[str]: """Module-level functions the user marked for SPEC N-15: the comment line `# typedpython: parallel` - directly above the `def` (above its decorators if it has any). A comment, like the module marker, + directly above the `def` (above its decorators if it has any). A comment, so the interpreted module is unchanged and needs no runtime name.""" lines = source.splitlines() out: set[str] = set() @@ -434,17 +431,10 @@ def _binding_targets(stmt: ast.stmt) -> list[ast.expr]: SLOT_ACCESS_DUNDERS = ("__getattribute__", "__getattr__", "__setattr__", "__delattr__", "__new__") -def _whole_module(source: str) -> bool: - return next((line.strip() for line in source.splitlines() if line.strip()), "") == MARKER - - def is_opted_in(path: Path) -> bool: - """Whether the front end considers the module for compilation at all: the `# typedpython: compiled` - marker as its first non-empty line, or a module-level `def` / `class` decorated `@compiled` - (the same tests `_targets` and `_classes` apply).""" + """Whether the front end considers the module for compilation at all: a module-level `def` / `class` + decorated `@compiled` (the same test `_targets` and `_classes` apply).""" source = Path(path).read_text() - if _whole_module(source): - return True return any( isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef)) 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: def _classes( tree: ast.Module, source: str, info: _ModuleInfo, ) -> tuple[dict[str, ir.ClassDecl], dict[str, str]]: - whole = _whole_module(source) decls: dict[str, ir.ClassDecl] = {} skipped: dict[str, str] = {} for node in tree.body: if not isinstance(node, ast.ClassDef): continue marked = any(isinstance(d, ast.Name) and d.id == "compiled" for d in node.decorator_list) - if not (whole or marked): + if not marked: continue result = _class_decl(node, tree, info) if isinstance(result, str): @@ -608,13 +597,12 @@ def _class_of(kind: str, classes: dict[str, ir.ClassDecl]) -> tuple[str, bool] | def _targets( tree: ast.Module, source: str, info: _ModuleInfo, ) -> list[tuple[ast.FunctionDef | ast.AsyncFunctionDef, str | None]]: - whole_module = _whole_module(source) found: list[tuple[ast.FunctionDef | ast.AsyncFunctionDef, str | None]] = [] for node in tree.body: if not isinstance(node, (ast.FunctionDef, ast.AsyncFunctionDef)): continue marked = [d for d in node.decorator_list if isinstance(d, ast.Name) and d.id == "compiled"] - if not (whole_module or marked): + if not marked: continue reason = None if marked and info.bound("compiled"): @@ -2499,4 +2487,4 @@ def _breaks(body: tuple[ir.Stmt, ...]) -> bool: return False -__all__ = ["lower", "MARKER"] +__all__ = ["lower"] diff --git a/python-multiplatform-ksp/src/test/python/conftest.py b/python-multiplatform-ksp/src/test/python/conftest.py index 452a4e613..669816bf4 100644 --- a/python-multiplatform-ksp/src/test/python/conftest.py +++ b/python-multiplatform-ksp/src/test/python/conftest.py @@ -1,12 +1,32 @@ +import ast +import builtins import sys from pathlib import Path import textwrap import pytest +# The `typedpython_builtins.pth` of an installed wheel; the test interpreter has no such install. +builtins.compiled = getattr(builtins, "compiled", lambda f, /: f) + FIXTURES = Path(__file__).parent / "fixtures" +def decorate_compiled(source: str) -> str: + """`source` with `@compiled` put above every module-level def / class (above its other decorators), + the set a whole-module mark used to cover. A source that does not parse is returned unchanged.""" + try: + tree = ast.parse(source) + except SyntaxError: + return source + lines = source.split("\n") + firsts = [min([n.lineno] + [d.lineno for d in n.decorator_list]) for n in tree.body + if isinstance(n, (ast.FunctionDef, ast.AsyncFunctionDef, ast.ClassDef))] + for first in reversed(firsts): + lines.insert(first - 1, "@compiled") + return "\n".join(lines) + + @pytest.fixture def write(tmp_path): """Write a dedented source file under tmp_path and return its path.""" diff --git a/python-multiplatform-ksp/src/test/python/test_bitwise.py b/python-multiplatform-ksp/src/test/python/test_bitwise.py index 0bd844034..c0d48b3d4 100644 --- a/python-multiplatform-ksp/src/test/python/test_bitwise.py +++ b/python-multiplatform-ksp/src/test/python/test_bitwise.py @@ -20,33 +20,39 @@ from test_native_int_impure import build_source SOURCE = textwrap.dedent(''' - # typedpython: compiled + @compiled def p_and(a: int, b: int) -> int: return a & b + @compiled def p_or(a: int, b: int) -> int: return a | b + @compiled def p_xor(a: int, b: int) -> int: return a ^ b + @compiled def p_inv(a: int) -> int: return ~a + @compiled def p_shl(a: int, b: int) -> int: return a << b + @compiled def p_shr(a: int, b: int) -> int: return a >> b + @compiled def p_aug(a: int, b: int) -> int: x: int = a x ^= b @@ -55,22 +61,26 @@ def p_aug(a: int, b: int) -> int: return x + @compiled def p_augshl(a: int, b: int) -> int: x: int = a x <<= b return x + @compiled def p_augshr(a: int, b: int) -> int: x: int = a x >>= b return x + @compiled def p_mix(a: int, b: int) -> int: return (((a & 255) << 3) | (b >> 2)) ^ ~a + @compiled def p_mask_loop(n: int) -> int: h: int = 0 for i in range(n): @@ -78,6 +88,7 @@ def p_mask_loop(n: int) -> int: return h + @compiled def p_array(xs: list[int]) -> None: for i in range(len(xs)): xs[i] &= 0xFF @@ -85,10 +96,12 @@ def p_array(xs: list[int]) -> None: xs[i] ^= i + @compiled def p_iv(a: int, b: int) -> int: return (a & 0xFFFF) * (b & 0xFFFF) + ~(a & 7) + @compiled def i_and(log: list[object], a: int, b: int) -> object: log.append("start") r: int = a & b @@ -96,6 +109,7 @@ def i_and(log: list[object], a: int, b: int) -> object: return r + @compiled def i_or(log: list[object], a: int, b: int) -> object: log.append("start") r: int = a | b @@ -103,6 +117,7 @@ def i_or(log: list[object], a: int, b: int) -> object: return r + @compiled def i_xor(log: list[object], a: int, b: int) -> object: log.append("start") r: int = a ^ b @@ -110,6 +125,7 @@ def i_xor(log: list[object], a: int, b: int) -> object: return r + @compiled def i_inv(log: list[object], a: int) -> object: log.append("start") r: int = ~a @@ -117,6 +133,7 @@ def i_inv(log: list[object], a: int) -> object: return r + @compiled def i_shl(log: list[object], a: int, b: int) -> object: log.append("start") r: int = a << b @@ -124,6 +141,7 @@ def i_shl(log: list[object], a: int, b: int) -> object: return r + @compiled def i_shr(log: list[object], a: int, b: int) -> object: log.append("start") r: int = a >> b @@ -131,6 +149,7 @@ def i_shr(log: list[object], a: int, b: int) -> object: return r + @compiled def i_aug(log: list[object], a: int, b: int) -> object: log.append("start") x: int = a @@ -143,6 +162,7 @@ def i_aug(log: list[object], a: int, b: int) -> object: return x + @compiled def i_big(log: list[object], a: int, b: int, c: int) -> object: log.append("start") r: int = (a * b) & c @@ -156,6 +176,7 @@ def i_big(log: list[object], a: int, b: int, c: int) -> object: return r + @compiled def o_ops(log: list[object], x: object, y: object) -> object: log.append("start") r: object = x | y @@ -164,21 +185,25 @@ def o_ops(log: list[object], x: object, y: object) -> object: return r + @compiled def o_inplace(log: list[object], x: object, y: object) -> object: log.append("start") x |= y return x + @compiled def f_and(log: list[object], a: float, b: int) -> object: log.append("start") return a & b + @compiled def b_and(a: bool, b: bool) -> bool: return a & b + @compiled def b_inv(a: bool) -> int: return ~a ''') diff --git a/python-multiplatform-ksp/src/test/python/test_buffer_arrays.py b/python-multiplatform-ksp/src/test/python/test_buffer_arrays.py index 71d95f0c0..724f6759e 100644 --- a/python-multiplatform-ksp/src/test/python/test_buffer_arrays.py +++ b/python-multiplatform-ksp/src/test/python/test_buffer_arrays.py @@ -29,10 +29,10 @@ from typedpython import cbuild, cgen, frontend, ir, verify SOURCE = textwrap.dedent(''' - # typedpython: compiled import array + @compiled def sum_ad(a: array.array[float]) -> float: s = 0.0 for i in range(len(a)): @@ -40,6 +40,7 @@ def sum_ad(a: array.array[float]) -> float: return s + @compiled def sum_aq(a: array.array[int]) -> int: s = 0 for i in range(len(a)): @@ -47,70 +48,85 @@ def sum_aq(a: array.array[int]) -> int: return s + @compiled def get_ad(a: array.array[float], i: int) -> float: return a[i] + @compiled def get_aq(a: array.array[int], i: int) -> int: return a[i] + @compiled def set_ad(a: array.array[float], i: int, v: float) -> None: a[i] = v + @compiled def set_aq(a: array.array[int], i: int, v: int) -> None: a[i] = v + @compiled def length_ad(a: array.array[float]) -> int: return len(a) + @compiled def scale_ad(a: array.array[float], k: float) -> None: for i in range(len(a)): a[i] = a[i] * k + @compiled def iota_aq(a: array.array[int]) -> None: for i in range(len(a)): a[i] = i + @compiled def copy_ad(dst: array.array[float], src: array.array[float]) -> None: for i in range(len(dst)): dst[i] = src[i] + @compiled def cascade_ad(a: array.array[float], b: array.array[float]) -> None: for i in range(len(a)): a[i] = b[i] + 1.0 + @compiled def fill_n(a: array.array[float], n: int) -> None: for i in range(n): a[i] = 1.0 + @compiled def mixed(l: list[float], a: array.array[float]) -> None: for i in range(len(a)): a[i] = l[i] + @compiled def four(a: array.array[float], b: array.array[float], c: array.array[float], d: array.array[float]) -> None: for i in range(len(a)): a[i] = b[i] + c[i] + d[i] + @compiled def div_ad(a: array.array[float]) -> float: return a[0] / a[1] + @compiled def open_ad(a: array.array[float], o: object) -> float: return a[0] + float(o.v) + @compiled def rebind_view(a: array.array[float]) -> None: y = a[:] y[0] = 5.0 @@ -505,30 +521,34 @@ def many(n, buffer): # --- vectorised element-wise store loops (#226) --------------------------------------------------- VEC_SOURCE = textwrap.dedent(''' - # typedpython: compiled import array + @compiled def vscale(a: array.array[float], k: float) -> None: for i in range(len(a)): a[i] = a[i] * k + @compiled def vmix(a: array.array[float], k: float) -> None: for i in range(len(a)): a[i] = -(a[i] - k) * 2.5 + k + @compiled def vtwo(a: array.array[float], b: array.array[float]) -> None: for i in range(len(a)): a[i] = a[i] + b[i] + @compiled def vfill(a: array.array[float], k: float) -> None: for i in range(len(a)): a[i] = k + @compiled def vdiv(a: array.array[float], k: float) -> None: for i in range(len(a)): a[i] = a[i] / k diff --git a/python-multiplatform-ksp/src/test/python/test_buffer_memoryview.py b/python-multiplatform-ksp/src/test/python/test_buffer_memoryview.py index 1766b3300..8a616abc8 100644 --- a/python-multiplatform-ksp/src/test/python/test_buffer_memoryview.py +++ b/python-multiplatform-ksp/src/test/python/test_buffer_memoryview.py @@ -23,10 +23,10 @@ same_value) SOURCE = textwrap.dedent(''' - # typedpython: compiled import array + @compiled def sum_mf(m: memoryview[float]) -> float: s = 0.0 for i in range(len(m)): @@ -34,6 +34,7 @@ def sum_mf(m: memoryview[float]) -> float: return s + @compiled def sum_mi(m: memoryview[int]) -> int: s = 0 for i in range(len(m)): @@ -41,31 +42,38 @@ def sum_mi(m: memoryview[int]) -> int: return s + @compiled def get_mf(m: memoryview[float], i: int) -> float: return m[i] + @compiled def get_mi(m: memoryview[int], i: int) -> int: return m[i] + @compiled def set_mf(m: memoryview[float], i: int, v: float) -> None: m[i] = v + @compiled def set_mi(m: memoryview[int], i: int, v: int) -> None: m[i] = v + @compiled def length_mf(m: memoryview[float]) -> int: return len(m) + @compiled def scale_mf(m: memoryview[float], k: float) -> None: for i in range(len(m)): m[i] = m[i] * k + @compiled def spin_mf(m: memoryview[float], n: int) -> float: s = 0.0 L = len(m) @@ -74,6 +82,7 @@ def spin_mf(m: memoryview[float], n: int) -> float: return s + @compiled def spin_af(a: array.array[float], n: int) -> float: s = 0.0 L = len(a) diff --git a/python-multiplatform-ksp/src/test/python/test_buffer_numpy.py b/python-multiplatform-ksp/src/test/python/test_buffer_numpy.py index 582e4b094..f14a32c00 100644 --- a/python-multiplatform-ksp/src/test/python/test_buffer_numpy.py +++ b/python-multiplatform-ksp/src/test/python/test_buffer_numpy.py @@ -21,13 +21,13 @@ from test_buffer_arrays import build_source, load_so # noqa: E402 SOURCE = textwrap.dedent(''' - # typedpython: compiled import array import math import numpy as np import numpy.typing as npt + @compiled def sum_ad(a: array.array[float]) -> float: s = 0.0 for i in range(len(a)): @@ -35,18 +35,22 @@ def sum_ad(a: array.array[float]) -> float: return s + @compiled def nd_first(x: npt.NDArray[np.float64]) -> object: return x[0] + @compiled def nd_first_f(x: npt.NDArray[np.float64]) -> float: return x[0] + @compiled def nd_first_i(x: npt.NDArray[np.int64]) -> int: return x[0] + @compiled def nd_sum(x: npt.NDArray[np.float64]) -> float: s = 0.0 for i in range(len(x)): @@ -54,6 +58,7 @@ def nd_sum(x: npt.NDArray[np.float64]) -> float: return s + @compiled def nd_sum_from_first(x: npt.NDArray[np.float64]) -> float: s = x[0] for i in range(1, len(x)): @@ -61,6 +66,7 @@ def nd_sum_from_first(x: npt.NDArray[np.float64]) -> float: return s + @compiled def nd_isum(x: npt.NDArray[np.int64]) -> int: s = 0 for i in range(len(x)): @@ -68,64 +74,79 @@ def nd_isum(x: npt.NDArray[np.int64]) -> int: return s + @compiled def nd_gt(x: npt.NDArray[np.float64]) -> object: return x[0] > x[1] + @compiled def nd_flag(x: npt.NDArray[np.float64]) -> object: b = x[0] > 0.0 return b + @compiled def nd_plain(x: npt.NDArray[np.float64]) -> object: return float(x[0]) + @compiled def nd_mixed(x: npt.NDArray[np.float64], k: float) -> object: return k + 1.0 + @compiled def nd_sqrt(x: npt.NDArray[np.float64]) -> object: return math.sqrt(x[0] * x[0]) + @compiled def nd_neg(x: npt.NDArray[np.float64]) -> object: return -x[1] + @compiled def nd_len(x: npt.NDArray[np.float64]) -> int: return len(x) + @compiled def nd_set(x: npt.NDArray[np.float64], i: int, v: float) -> None: x[i] = v + @compiled def nd_copy(dst: npt.NDArray[np.float64], src: npt.NDArray[np.float64]) -> None: for i in range(len(dst)): dst[i] = src[i] + @compiled def nd_iset(x: npt.NDArray[np.int64], i: int, v: int) -> None: x[i] = v + @compiled def nd_get(x: npt.NDArray[np.float64], i: int) -> object: return x[i] + @compiled def nd_overflow(x: npt.NDArray[np.float64]) -> object: return x[0] * x[1] + @compiled def nd_invalid(x: npt.NDArray[np.float64]) -> object: return x[0] - x[1] + @compiled def nd_iover(x: npt.NDArray[np.int64]) -> object: return x[0] + x[1] + @compiled def nd_energy(x: npt.NDArray[np.float64], y: npt.NDArray[np.float64], z: npt.NDArray[np.float64], vx: npt.NDArray[np.float64], vy: npt.NDArray[np.float64], vz: npt.NDArray[np.float64], m: npt.NDArray[np.float64]) -> float: @@ -135,6 +156,7 @@ def nd_energy(x: npt.NDArray[np.float64], y: npt.NDArray[np.float64], z: npt.NDA return e + @compiled def nd_copy7(x: npt.NDArray[np.float64], y: npt.NDArray[np.float64], z: npt.NDArray[np.float64], vx: npt.NDArray[np.float64], vy: npt.NDArray[np.float64], vz: npt.NDArray[np.float64], m: npt.NDArray[np.float64]) -> None: @@ -145,20 +167,24 @@ def nd_copy7(x: npt.NDArray[np.float64], y: npt.NDArray[np.float64], z: npt.NDAr m[i] = x[i] + @compiled def nd_div(x: npt.NDArray[np.float64]) -> object: return x[0] / x[1] + @compiled def nd_idiv(x: npt.NDArray[np.int64]) -> object: return x[0] // x[1] + @compiled def nd_param(x: npt.NDArray[np.float64], k: float, use: bool) -> object: if use: k = x[0] return k + @compiled def nd_branch(x: npt.NDArray[np.float64], use: bool) -> object: if use: v = x[0] @@ -167,36 +193,44 @@ def nd_branch(x: npt.NDArray[np.float64], use: bool) -> object: return v + @compiled def nd_tuple(x: npt.NDArray[np.int64]) -> tuple[int, int, bool]: return (x[0], 3, x[1] > 0) + @compiled def nd_abs(x: npt.NDArray[np.float64]) -> object: return abs(x[1]) + @compiled def nd_to_list(x: npt.NDArray[np.float64], dst: list[float]) -> None: dst[0] = x[0] + @compiled def nd_mixed_cmp(x: npt.NDArray[np.int64], f: float) -> object: if x[0] < f: return 1 return 0 + @compiled def whole_sum(x: npt.NDArray[np.float64]) -> object: return np.sum(x) + @compiled def whole_attr(x: npt.NDArray[np.float64]) -> object: return x.size + @compiled def whole_pass(x: npt.NDArray[np.float64]) -> object: return np.add(x, 1.0).sum() + @compiled def nd_int_impure(x: npt.NDArray[np.int64]) -> object: s = 0 for i in range(len(x)): @@ -205,83 +239,103 @@ def nd_int_impure(x: npt.NDArray[np.int64]) -> object: return s + @compiled def nd_scale(x: npt.NDArray[np.float64], k: float) -> None: for i in range(len(x)): x[i] = x[i] * k + @compiled def p_div(x: npt.NDArray[np.float64]) -> object: return x[0] / x[1] + @compiled def p_fdiv(x: npt.NDArray[np.float64]) -> object: return x[0] // x[1] + @compiled def p_mod(x: npt.NDArray[np.float64]) -> object: return x[0] % x[1] + @compiled def p_add(x: npt.NDArray[np.float64]) -> object: return x[0] + x[1] + @compiled def p_sub(x: npt.NDArray[np.float64]) -> object: return x[0] - x[1] + @compiled def p_mul(x: npt.NDArray[np.float64]) -> object: return x[0] * x[1] + @compiled def p_pdiv(x: npt.NDArray[np.float64], k: float) -> object: return k / x[0] + @compiled def p_idiv(x: npt.NDArray[np.int64]) -> object: return x[0] / x[1] + @compiled def p_ifdiv(x: npt.NDArray[np.int64]) -> object: return x[0] // x[1] + @compiled def p_imod(x: npt.NDArray[np.int64]) -> object: return x[0] % x[1] + @compiled def p_imixed(x: npt.NDArray[np.int64]) -> object: return x[0] // 3 + x[1] % 4 + @compiled def i_div(x: npt.NDArray[np.float64]) -> None: x[2] = x[0] / x[1] + @compiled def i_fdiv(x: npt.NDArray[np.float64]) -> None: x[2] = x[0] // x[1] + @compiled def i_mod(x: npt.NDArray[np.float64]) -> None: x[2] = x[0] % x[1] + @compiled def i_add(x: npt.NDArray[np.float64]) -> None: x[2] = x[0] + x[1] + @compiled def i_sub(x: npt.NDArray[np.float64]) -> None: x[2] = x[0] - x[1] + @compiled def i_mul(x: npt.NDArray[np.float64]) -> None: x[2] = x[0] * x[1] + @compiled def i_plain(x: npt.NDArray[np.float64], k: float) -> None: x[2] = k / x[1] x[3] = k * 2.0 + @compiled def i_multiline(x: npt.NDArray[np.float64]) -> None: x[2] = (x[0] * x[1]) @@ -290,12 +344,14 @@ def i_multiline(x: npt.NDArray[np.float64]) -> None: + x[0] * x[1]) + @compiled def i_aug(x: npt.NDArray[np.float64]) -> None: x[0] *= x[1] x[1] /= x[2] x[2] += x[3] + @compiled def i_loop(x: npt.NDArray[np.float64], y: npt.NDArray[np.float64]) -> float: s = 0.0 for i in range(len(x)): @@ -304,21 +360,25 @@ def i_loop(x: npt.NDArray[np.float64], y: npt.NDArray[np.float64]) -> float: return s + @compiled def i_mixed(x: npt.NDArray[np.float64], n: int) -> None: x[0] = n / x[1] x[2] = x[3] * n + @compiled def i_ret(x: npt.NDArray[np.float64]) -> object: x[2] = x[0] * x[1] return x[0] / x[1] + @compiled def i_ifdiv(x: npt.NDArray[np.int64]) -> object: x[2] = x[0] // x[1] return x[2] + @compiled def nd_advance(dt: float, steps: int, x: npt.NDArray[np.float64], y: npt.NDArray[np.float64], z: npt.NDArray[np.float64], vx: npt.NDArray[np.float64], vy: npt.NDArray[np.float64], vz: npt.NDArray[np.float64], @@ -343,6 +403,7 @@ def nd_advance(dt: float, steps: int, x: npt.NDArray[np.float64], y: npt.NDArray z[i] += dt * vz[i] + @compiled def nd_energy_full(x: npt.NDArray[np.float64], y: npt.NDArray[np.float64], z: npt.NDArray[np.float64], vx: npt.NDArray[np.float64], vy: npt.NDArray[np.float64], vz: npt.NDArray[np.float64], @@ -357,10 +418,12 @@ def nd_energy_full(x: npt.NDArray[np.float64], y: npt.NDArray[np.float64], e -= mass[i] * mass[j] / math.sqrt(dx * dx + dy * dy + dz * dz) return e # #261: np.bool_ arrays (1-byte items, format "?") + @compiled def nd_b_get(x: npt.NDArray[np.bool_], i: int) -> object: return x[i] + @compiled def nd_b_any(x: npt.NDArray[np.bool_]) -> bool: for i in range(len(x)): if x[i]: @@ -368,6 +431,7 @@ def nd_b_any(x: npt.NDArray[np.bool_]) -> bool: return False + @compiled def nd_b_count(x: npt.NDArray[np.bool_]) -> int: c = 0 for i in range(len(x)): @@ -376,45 +440,55 @@ def nd_b_count(x: npt.NDArray[np.bool_]) -> int: return c + @compiled def nd_b_set(x: npt.NDArray[np.bool_], i: int, v: bool) -> None: x[i] = v + @compiled def nd_b_flip(x: npt.NDArray[np.bool_]) -> None: for i in range(len(x)): x[i] = not x[i] + @compiled def nd_b_copy(a: npt.NDArray[np.bool_], b: npt.NDArray[np.bool_]) -> None: for i in range(len(a)): b[i] = a[i] + @compiled def nd_b_eq(x: npt.NDArray[np.bool_], y: npt.NDArray[np.bool_], i: int) -> object: return x[i] == y[i] + @compiled def nd_b_gt(x: npt.NDArray[np.float64], m: npt.NDArray[np.bool_]) -> None: for i in range(len(x)): m[i] = x[i] > 2.0 + @compiled def nd_b_len(x: npt.NDArray[np.bool_]) -> int: return len(x) + @compiled def nd_b_store_int(x: npt.NDArray[np.bool_], i: int, v: int) -> None: x[i] = v + @compiled def nd_b_store_float(x: npt.NDArray[np.bool_], i: int, v: float) -> None: x[i] = v + @compiled def nd_b_arith(x: npt.NDArray[np.bool_], i: int) -> object: return x[i] + 1 + @compiled def nd_b_whole(x: npt.NDArray[np.bool_]) -> object: return np.sum(x) ''') diff --git a/python-multiplatform-ksp/src/test/python/test_class_methods.py b/python-multiplatform-ksp/src/test/python/test_class_methods.py index d6693dcad..cd08794e3 100644 --- a/python-multiplatform-ksp/src/test/python/test_class_methods.py +++ b/python-multiplatform-ksp/src/test/python/test_class_methods.py @@ -40,6 +40,7 @@ import pytest +from conftest import decorate_compiled from typedpython import cbuild, cgen, cost, frontend, pipeline, verify HERE = Path(__file__).resolve().parent @@ -51,10 +52,10 @@ BINARY_TREES = REPO / "benchmarks" / "typedpython" / "py" / "binary_trees.py" SOURCE = textwrap.dedent('''\ - # typedpython: compiled """typedpython class-method test module.""" + @compiled class Node: __slots__ = ("left", "right") left: "Node | None" @@ -135,21 +136,25 @@ def no_annotation(self, x) -> int: return 1 + @compiled class Plain: def check(self) -> int: return 5 + @compiled def make(depth: int) -> Node: if depth > 0: return Node(make(depth - 1), make(depth - 1)) return Node(None, None) + @compiled def check_of(n: Node) -> int: return n.check() + @compiled def leaf_of(n: Node) -> bool: return n.leaf() ''') @@ -231,6 +236,7 @@ def test_the_methods_of_a_class_that_is_not_fixed_layout_are_not_considered(tmp_ def test_a_method_named_like_a_module_function_does_not_collide(tmp_path): m = lower(tmp_path, SOURCE + textwrap.dedent('''\ + @compiled def height(n: Node) -> int: return n.height() ''')) @@ -240,7 +246,7 @@ def height(n: Node) -> int: def test_a_method_with_a_foreign_self_annotation_stays_interpreted(tmp_path): src = textwrap.dedent('''\ - # typedpython: compiled + @compiled class A: __slots__ = ("x",) x: int @@ -258,7 +264,7 @@ def get(self: int) -> int: def test_a_method_defined_twice_in_the_class_body_stays_interpreted(tmp_path): src = textwrap.dedent('''\ - # typedpython: compiled + @compiled class A: __slots__ = ("x",) x: int @@ -277,7 +283,7 @@ def get(self) -> int: assert "more than once" in m.skipped["A.get"] -def test_an_at_compiled_class_compiles_its_methods_without_the_module_marker(tmp_path): +def test_an_at_compiled_class_compiles_its_methods(tmp_path): src = textwrap.dedent('''\ @compiled class A: @@ -887,7 +893,7 @@ def run_main(path_or_module, n): def test_binary_trees_with_check_as_a_method_compiles_and_matches(tmp_path): - text = "# typedpython: compiled\n" + method_form(BINARY_TREES.read_text()) + text = decorate_compiled(method_form(BINARY_TREES.read_text())) path = tmp_path / "bt_method.py" path.write_text(text) result = pipeline.compile_module(path, tmp_path / "out") @@ -921,8 +927,8 @@ def test_binary_trees_with_check_as_a_method_compiles_and_matches(tmp_path): @pytest.mark.parametrize("name", sorted(EXPECTED)) def test_the_benchmarks_compile_the_same_functions_as_before(tmp_path, name): path = tmp_path / f"{name}.py" - path.write_text("# typedpython: compiled\n" + (REPO / "benchmarks" / "typedpython" / "py" - / f"{name}.py").read_text()) + path.write_text(decorate_compiled((REPO / "benchmarks" / "typedpython" / "py" + / f"{name}.py").read_text())) proved, diags = verify.verify(frontend.lower(path)) proved, diags = cost.apply(proved, diags) assert {f.name for f in proved.functions} == EXPECTED[name] diff --git a/python-multiplatform-ksp/src/test/python/test_cost_rule.py b/python-multiplatform-ksp/src/test/python/test_cost_rule.py index db6c4999a..c540bb260 100644 --- a/python-multiplatform-ksp/src/test/python/test_cost_rule.py +++ b/python-multiplatform-ksp/src/test/python/test_cost_rule.py @@ -12,20 +12,19 @@ import pytest +from conftest import decorate_compiled from typedpython import cost, pipeline -HEADER = "# typedpython: compiled\n" - -OBJECT_ONLY = HEADER + textwrap.dedent(''' +OBJECT_ONLY = decorate_compiled(textwrap.dedent(''' def copy_words(src: list[str], dst: list[str]) -> str: w: str = "" for x in src: w = x + w dst.append(w) return w -''') +''')) -INT_LOOP = HEADER + textwrap.dedent(''' +INT_LOOP = decorate_compiled(textwrap.dedent(''' def word_of(word_id: int) -> str: x: int = word_id + 17576 w: str = "" @@ -33,7 +32,7 @@ def word_of(word_id: int) -> str: w = chr(97 + x % 26) + w x //= 26 return w -''') +''')) def build(tmp_path, source, **kw): @@ -67,7 +66,7 @@ def test_the_opt_out_compiles_the_same_function(tmp_path): assert names(r) == {"copy_words"} and r.extension is not None -MIXED = HEADER + textwrap.dedent(''' +MIXED = decorate_compiled(textwrap.dedent(''' def mixed(n: int, xs: list[str]) -> float: total: float = 0.0 for i in range(n): @@ -106,7 +105,7 @@ def native_loop_inside_object_loop(rows: list[list[str]]) -> float: for k in range(3): total += 0.5 * 2.0 return total -''') +''')) def test_one_native_loop_with_object_operations_compiles(tmp_path): @@ -148,10 +147,10 @@ def test_the_rule_is_what_keeps_it_interpreted(tmp_path, monkeypatch): test_an_object_only_loop_stays_interpreted_with_the_reason(tmp_path) -STRAIGHT_LINE = HEADER + textwrap.dedent(''' +STRAIGHT_LINE = decorate_compiled(textwrap.dedent(''' def label(n: int, name: str) -> str: return f"{name}:{n + 1}" -''') +''')) def test_a_straight_line_impure_function_still_compiles(tmp_path): diff --git a/python-multiplatform-ksp/src/test/python/test_eval_breaker.py b/python-multiplatform-ksp/src/test/python/test_eval_breaker.py index 51f1bca43..0d6bb643b 100644 --- a/python-multiplatform-ksp/src/test/python/test_eval_breaker.py +++ b/python-multiplatform-ksp/src/test/python/test_eval_breaker.py @@ -23,9 +23,9 @@ posix_signals = pytest.mark.skipif(sys.platform == "win32", reason="needs POSIX signals (SIGALRM/SIGUSR1/setitimer) or fork; Windows has none") SOURCE = """\ -# typedpython: compiled +@compiled def spin(n: int) -> float: acc: float = 0.0 for k in range(n): @@ -33,12 +33,14 @@ def spin(n: int) -> float: return acc +@compiled def fib(n: int) -> int: if n < 2: return n return fib(n - 1) + fib(n - 2) +@compiled def total(xs: list[float], rounds: int) -> float: s: float = 0.0 for r in range(rounds): @@ -165,9 +167,9 @@ def handler(signum, frame): # ---- chunked polling for range loops (#206, SPEC N-8) --------------------------------------------- CHUNK_SOURCE = """\ -# typedpython: compiled +@compiled def brk(n: int, k: int) -> int: s: int = 0 last: int = -1 @@ -179,6 +181,7 @@ def brk(n: int, k: int) -> int: return s * 3 + last +@compiled def cont(n: int, m: int) -> int: s: int = 0 for i in range(n): @@ -188,6 +191,7 @@ def cont(n: int, m: int) -> int: return s +@compiled def ret(n: int, k: int) -> int: for i in range(n): if i == k: @@ -195,6 +199,7 @@ def ret(n: int, k: int) -> int: return -1 +@compiled def down(n: int) -> int: s: int = 0 for i in range(n, 0, -3): @@ -202,6 +207,7 @@ def down(n: int) -> int: return s +@compiled def down1(n: int) -> int: s: int = 0 for i in range(n, -1, -1): @@ -209,6 +215,7 @@ def down1(n: int) -> int: return s +@compiled def up3(a: int, b: int) -> int: s: int = 0 for i in range(a, b, 3): @@ -216,6 +223,7 @@ def up3(a: int, b: int) -> int: return s +@compiled def nested(n: int, m: int) -> int: s: int = 0 for i in range(n): @@ -226,6 +234,7 @@ def nested(n: int, m: int) -> int: return s +@compiled def brk_in_while(n: int) -> int: s: int = 0 for i in range(n): @@ -238,6 +247,7 @@ def brk_in_while(n: int) -> int: return s +@compiled def huge(a: int, b: int, cap: int) -> int: c: int = 0 for i in range(a, b): @@ -247,6 +257,7 @@ def huge(a: int, b: int, cap: int) -> int: return c +@compiled def huge_last(a: int, b: int, cap: int) -> int: c: int = 0 last: int = 0 @@ -346,7 +357,7 @@ def test_range_loop_polls_once_per_chunk(chunk_module): def test_while_loop_keeps_its_per_iteration_countdown(tmp_path): path = tmp_path / "w.py" - path.write_text("# typedpython: compiled\n\n\ndef w(n: int) -> int:\n i: int = 0\n while i < n:\n" + path.write_text("\n\n@compiled\ndef w(n: int) -> int:\n i: int = 0\n while i < n:\n" " i += 1\n return i\n") body = _function_c(_generated_c(path), "w") lines = body.splitlines() @@ -379,9 +390,9 @@ def test_keyboard_interrupt_reaches_a_long_range_loop_in_chunks(extension): MID_SOURCE = """\ -# typedpython: compiled +@compiled def mid(n: int) -> float: x: float = 1.0 for k in range(n): diff --git a/python-multiplatform-ksp/src/test/python/test_fast_entry.py b/python-multiplatform-ksp/src/test/python/test_fast_entry.py index f1e43a1ac..d448e3b41 100644 --- a/python-multiplatform-ksp/src/test/python/test_fast_entry.py +++ b/python-multiplatform-ksp/src/test/python/test_fast_entry.py @@ -25,45 +25,53 @@ posix_signals = pytest.mark.skipif(sys.platform == "win32", reason="needs POSIX signals (SIGALRM/SIGUSR1/setitimer) or fork; Windows has none") SOURCE = """\ -# typedpython: compiled +@compiled def helper(x: int) -> float: d: dict[str, int] = {} d['a'] = x return d['a'] * 2.5 +@compiled def leaf(i: int) -> float: return 1.0 / (i + 1) +@compiled def mid(i: int) -> float: return leaf(i) + leaf(i + 1) +@compiled def top(i: int) -> float: return mid(i) * 2.0 +@compiled def objy(i: int) -> float: return float(helper(i)) + leaf(i) +@compiled def viaobj(i: int) -> float: return objy(i) + 1.0 +@compiled def rec(n: int) -> int: if n == 0: return 0 return 1 + rec(n - 1) +@compiled def callrec(n: int) -> int: return rec(n) + 1 +@compiled def loopy(n: int) -> float: s: float = 0.0 for i in range(n): @@ -71,14 +79,17 @@ def loopy(n: int) -> float: return s +@compiled def outer(n: int) -> float: return loopy(n) + 1.0 +@compiled def cube(x: int) -> float: return float(x * x * x) + leaf(x) +@compiled def fill(x: int, out: list[float]) -> None: out[0] = cube(x) """ diff --git a/python-multiplatform-ksp/src/test/python/test_free_threaded.py b/python-multiplatform-ksp/src/test/python/test_free_threaded.py index 378395e78..e954c03ba 100644 --- a/python-multiplatform-ksp/src/test/python/test_free_threaded.py +++ b/python-multiplatform-ksp/src/test/python/test_free_threaded.py @@ -33,9 +33,9 @@ FREE_THREADED = bool(sysconfig.get_config_var("Py_GIL_DISABLED")) SOURCE = '''\ -# typedpython: compiled +@compiled class Cell: __slots__ = ("a", "b") a: object @@ -46,6 +46,7 @@ def __init__(self, a: object, b: object) -> None: self.b = b +@compiled class Twin: __slots__ = ("a", "b") a: object @@ -56,18 +57,22 @@ def __init__(self, a: object, b: object) -> None: self.b = b +@compiled def geta(c: Cell) -> object: return c.a +@compiled def seta(c: Cell, v: object) -> None: c.a = v +@compiled def mk(a: object, b: object) -> Cell: return Cell(a, b) +@compiled def fresh(a: object, n: int) -> object: c = Cell(a, None) keep: object = c # read as a value: c is a real object, not virtual (N-12a) @@ -77,11 +82,13 @@ def fresh(a: object, n: int) -> object: return s +@compiled def vpick(a: object, b: object) -> object: c = Cell(a, b) return c.b +@compiled def total(xs: list[float], rounds: int) -> float: s: float = 0.0 for r in range(rounds): @@ -90,24 +97,29 @@ def total(xs: list[float], rounds: int) -> float: return s +@compiled def fill(xs: list[float], v: float) -> None: for i in range(len(xs)): xs[i] = v +@compiled def edge(xs: list[float]) -> float: return xs[0] - xs[len(xs) - 1] +@compiled def fill_int(xs: list[int], v: int) -> None: for i in range(len(xs)): xs[i] = v +@compiled def edge_int(xs: list[int]) -> int: return xs[0] - xs[len(xs) - 1] +@compiled def rotate(xs: list[float]) -> None: if len(xs) > 1: first: float = xs[0] @@ -116,6 +128,7 @@ def rotate(xs: list[float]) -> None: xs[len(xs) - 1] = first +@compiled def zero2(xs: list[int], ys: list[int]) -> int: for i in range(len(xs)): xs[i] = 0 @@ -124,10 +137,12 @@ def zero2(xs: list[int], ys: list[int]) -> int: return 0 +@compiled def pair_edge(xs: list[int], ys: list[int]) -> int: return xs[0] - ys[len(ys) - 1] +@compiled def three(a: list[int], b: list[int], c: list[int]) -> int: return len(a) + len(b) + len(c) @@ -135,6 +150,7 @@ def three(a: list[int], b: list[int], c: list[int]) -> int: K: int = 3 +@compiled def usek(x: int) -> int: return x + K ''' @@ -244,7 +260,7 @@ def test_three_list_parameters_stay_interpreted(built): def test_front_end_gives_a_clear_reason(tmp_path): p = tmp_path / "m.py" - p.write_text("# typedpython: compiled\n\n\ndef f(a: list[int], b: list[float], c: list[int]) -> int:\n" + p.write_text("\n\n@compiled\ndef f(a: list[int], b: list[float], c: list[int]) -> int:\n" " return len(a)\n") m = frontend.lower(p) assert not m.functions diff --git a/python-multiplatform-ksp/src/test/python/test_frontend.py b/python-multiplatform-ksp/src/test/python/test_frontend.py index 8b2a55fd9..fe8f4d94c 100644 --- a/python-multiplatform-ksp/src/test/python/test_frontend.py +++ b/python-multiplatform-ksp/src/test/python/test_frontend.py @@ -12,6 +12,7 @@ import pytest +from conftest import decorate_compiled from typedpython import frontend from typedpython.ir import ( And, ArrayParam, Assign, BinOp, BinOpKind, Box, Break, Call, CallObject, Compare, CompareKind, @@ -54,7 +55,7 @@ def nodes(root, cls): def test_scalar_loop_lowers_to_for_range(tmp_path): m = lower(tmp_path, """ - # typedpython: compiled + @compiled def total(n: int, x: float) -> float: acc: float = 0.0 for k in range(n): @@ -633,16 +634,18 @@ def f(x: float) -> object: assert Global(OBJ, "K") in nodes(f, Global) -def test_module_marker_compiles_every_module_level_def(tmp_path): +def test_compiled_marks_every_decorated_module_level_def(tmp_path): m = lower(tmp_path, """ - # typedpython: compiled + @compiled def a(x: float) -> float: return x + @compiled def b(x: float) -> float: return -x + @compiled @staticmethod def c(x: float) -> float: return x @@ -651,7 +654,19 @@ def c(x: float) -> float: assert set(m.skipped) == {"c"} and "decorator" in m.skipped["c"] -def test_without_marker_only_compiled_functions_are_lowered(tmp_path): +def test_the_former_module_pragma_comment_compiles_nothing(tmp_path): + m = lower(tmp_path, """ + # typedpython: compiled + def a(x: float) -> float: + return x + + def b(x: float) -> float: + return -x + """) + assert m.functions == () and m.skipped == {} + + +def test_without_a_decorator_only_compiled_functions_are_lowered(tmp_path): m = lower(tmp_path, """ x: int = 1 # typedpython: compiled @@ -828,7 +843,7 @@ def f(n: int) -> int: def test_nbody_kernels_lower(tmp_path): path = tmp_path / "nbody.py" shutil.copy(BENCH / "nbody.py", path) - path.write_text("# typedpython: compiled\n" + path.read_text()) + path.write_text(decorate_compiled(path.read_text())) m = frontend.lower(path) for name in ("advance", "energy", "offset_momentum"): function(m, name) @@ -847,7 +862,7 @@ def test_nbody_kernels_lower(tmp_path): @pytest.mark.skipif(not BENCH.exists(), reason="benchmark worktree not present") def test_spectral_norm_kernels_lower(tmp_path): path = tmp_path / "spectral_norm.py" - path.write_text("# typedpython: compiled\n" + (BENCH / "spectral_norm.py").read_text()) + path.write_text(decorate_compiled((BENCH / "spectral_norm.py").read_text())) m = frontend.lower(path) eval_a = function(m, "eval_a") assert eval_a.pure and eval_a.may_deopt @@ -1048,7 +1063,7 @@ def f(a: int, b: int) -> {annotation}: @pytest.mark.skipif(not BENCH.exists(), reason="benchmark worktree not present") def test_fannkuch_lowers(tmp_path): path = tmp_path / "fannkuch.py" - path.write_text("# typedpython: compiled\n" + (BENCH / "fannkuch.py").read_text()) + path.write_text(decorate_compiled((BENCH / "fannkuch.py").read_text())) m = frontend.lower(path) assert "fannkuch" in [f.name for f in m.functions], m.skipped.get("fannkuch") f = function(m, "fannkuch") diff --git a/python-multiplatform-ksp/src/test/python/test_frontend_classes.py b/python-multiplatform-ksp/src/test/python/test_frontend_classes.py index 33c0473cc..a2daaeed9 100644 --- a/python-multiplatform-ksp/src/test/python/test_frontend_classes.py +++ b/python-multiplatform-ksp/src/test/python/test_frontend_classes.py @@ -18,6 +18,7 @@ import pytest +from conftest import decorate_compiled from typedpython import frontend from typedpython.ir import ( And, Assign, Box, Call, CallObject, CheckExact, ClassDecl, Const, ExprStmt, FieldGet, FieldSet, @@ -28,8 +29,6 @@ BINARY_TREES = Path(__file__).resolve().parents[4] / "benchmarks/typedpython/py/binary_trees.py" SLOTS_LINE = ' __slots__ = ("left", "right")\n' -MARK = "# typedpython: compiled\n" - POINT = """ class P: __slots__ = ("x", "y") @@ -44,7 +43,7 @@ def __init__(self, x: float, y: float) -> None: def lower(tmp_path, source, name="mod.py"): path = tmp_path / name - path.write_text(MARK + textwrap.dedent(source).lstrip("\n")) + path.write_text(decorate_compiled(textwrap.dedent(source).lstrip("\n"))) return frontend.lower(path) @@ -573,7 +572,7 @@ def test_binary_trees_with_slots_lowers_make_and_check(tmp_path): source = BINARY_TREES.read_text() assert "class Node:\n" + SLOTS_LINE in source # the benchmark itself declares the slots path = tmp_path / "binary_trees.py" - path.write_text(MARK + source) + path.write_text(decorate_compiled(source)) m = frontend.lower(path) assert cls(m, "Node") == ClassDecl("Node", ("left", "right"), True) @@ -598,7 +597,7 @@ def test_binary_trees_without_slots_stays_object_code(tmp_path): path = tmp_path / "binary_trees.py" source = BINARY_TREES.read_text() assert SLOTS_LINE in source - path.write_text(MARK + source.replace(SLOTS_LINE, "", 1)) # the run.py no-slots baseline + path.write_text(decorate_compiled(source.replace(SLOTS_LINE, "", 1))) # the run.py no-slots baseline m = frontend.lower(path) assert m.classes == () assert "`__slots__`" in m.skipped["Node"] diff --git a/python-multiplatform-ksp/src/test/python/test_interval_proofs.py b/python-multiplatform-ksp/src/test/python/test_interval_proofs.py index 045a593e3..64260ddf7 100644 --- a/python-multiplatform-ksp/src/test/python/test_interval_proofs.py +++ b/python-multiplatform-ksp/src/test/python/test_interval_proofs.py @@ -330,39 +330,44 @@ def test_a_mod_result_can_feed_a_proof(): # --- 2. end to end --------------------------------------------------------------------------------- SOURCE = '''\ -# typedpython: compiled +@compiled def dec_if_positive(n: int) -> int: if n > 0: return n - 1 return -1 +@compiled def dec_after_guard(n: int) -> int: if n <= 0: return 0 return n - 1 +@compiled def inc_below_max(n: int) -> int: if n < 9223372036854775807: return n + 1 return 0 +@compiled def inc_below_100(n: int) -> int: if n < 100: return n + 1 return 0 +@compiled def inc_if_positive(n: int) -> int: if n > 0: return n + 1 return 0 +@compiled def reverse_gap(a: int, b: int) -> int: lo: int = a hi: int = b @@ -374,6 +379,7 @@ def reverse_gap(a: int, b: int) -> int: return hi - lo + steps +@compiled def killed_guard(n: int, a: list[int]) -> int: if n < 100: n = a[0] @@ -381,6 +387,7 @@ def killed_guard(n: int, a: list[int]) -> int: return 0 +@compiled def killed_in_loop(c: int, a: list[int]) -> int: n: int = a[0] t: int = 0 @@ -391,6 +398,7 @@ def killed_in_loop(c: int, a: list[int]) -> int: return t +@compiled def parity(n: int) -> int: k: int = 0 s: int = 0 @@ -401,11 +409,13 @@ def parity(n: int) -> int: return s +@compiled def const_local() -> int: x: int = 41 return x + 1 +@compiled def range_shifts(a: int, b: int) -> int: if a < 0: return 0 @@ -415,26 +425,31 @@ def range_shifts(a: int, b: int) -> int: return t +@compiled def guarded_shift(n: int) -> int: if n >= 0: return n // 16 + n % 16 return 0 +@compiled def negative_shift(n: int) -> int: return n // 4 + n % 8 +@compiled def odd_divisor(n: int) -> int: if n >= 0: return n // 3 + n % 6 return 0 +@compiled def mod_then_add(n: int) -> int: return n % 10 + 1 +@compiled def pair_sums(a: list[int]) -> int: best: int = 0 for i in range(len(a)): diff --git a/python-multiplatform-ksp/src/test/python/test_module_report.py b/python-multiplatform-ksp/src/test/python/test_module_report.py index 8625cf18d..86ead4b97 100644 --- a/python-multiplatform-ksp/src/test/python/test_module_report.py +++ b/python-multiplatform-ksp/src/test/python/test_module_report.py @@ -8,16 +8,17 @@ import textwrap from pathlib import Path +from conftest import decorate_compiled from typedpython import cbuild, cgen, frontend, pipeline, verify -SRC = "# typedpython: compiled\n" + textwrap.dedent("""\ +SRC = decorate_compiled(textwrap.dedent("""\ def twice(n: int) -> int: return n * 2 def boom(s: str) -> str: raise ValueError("boom " + s) - """) + """)) def make_project(root: Path) -> Path: @@ -95,14 +96,14 @@ def test_a_moved_file_with_the_same_content_does_not_reuse_the_extension(tmp_pat # --- #128 ---------------------------------------------------------------------------------------- -MIXED = "# typedpython: compiled\n" + textwrap.dedent("""\ +MIXED = decorate_compiled(textwrap.dedent("""\ def ok(n: int) -> int: return n + 1 def greet(name: str) -> str: return name[1:] - """) + """)) def build_one(tmp_path, rel, source, cache="cache"): @@ -119,20 +120,20 @@ def test_kind_compiled(tmp_path): assert m.kind == "compiled" and m.extension is not None [item] = m.skipped_items assert item.name == "greet" and item.file == "pkg/mixed.py" - assert item.line == 7 and not item.message.startswith("line ") - assert m.skipped["greet"].startswith("line 7: ") # ModuleResult.skipped unchanged + assert item.line == 8 and not item.message.startswith("line ") + assert m.skipped["greet"].startswith("line 8: ") # ModuleResult.skipped unchanged def test_kind_nothing_compiled_with_structured_items(tmp_path): _, _, m = build_one(tmp_path, "pkg/plain.py", - "# typedpython: compiled\n\ndef f(s: str) -> str:\n return s[1:]\n") + "@compiled\ndef f(s: str) -> str:\n return s[1:]\n") assert m.kind == "nothing_compiled" and m.extension is None [item] = m.skipped_items - assert (item.name, item.file, item.line) == ("f", "pkg/plain.py", 4) + assert (item.name, item.file, item.line) == ("f", "pkg/plain.py", 3) assert item.message and not re.match(r"line \d+", item.message) -def test_kind_not_marked_when_there_is_neither_marker_nor_decorator(tmp_path): +def test_kind_not_marked_when_there_is_no_decorator(tmp_path): _, _, m = build_one(tmp_path, "pkg/unmarked.py", "def f(x: int) -> int:\n return x\n") assert m.kind == "not_marked" and m.extension is None and m.skipped_items == [] @@ -145,15 +146,15 @@ def test_a_compiled_decorator_alone_opts_in(tmp_path): def test_class_items_carry_name_and_line(tmp_path): - src = ("# typedpython: compiled\n\n\nclass Node:\n __slots__ = ('a',)\n" + src = ("@compiled\nclass Node:\n __slots__ = ('a',)\n" " def __init__(self, a: int) -> None:\n self.a = a\n") _, _, m = build_one(tmp_path, "pkg/cls.py", src) cls = [i for i in m.skipped_items if i.name == "Node"] - assert cls and cls[0].line == 4 and cls[0].file == "pkg/cls.py" + assert cls and cls[0].line == 2 and cls[0].file == "pkg/cls.py" def test_verifier_skips_carry_a_line_and_no_prefix(tmp_path): - src = "# typedpython: compiled\n\n\ndef f(x: int) -> int:\n return g(x)\n\n\ndef g(x: int) -> int:\n return x\n" + src = "@compiled\ndef f(x: int) -> int:\n return g(x)\n\n\n@compiled\ndef g(x: int) -> int:\n return x\n" _, _, m = build_one(tmp_path, "pkg/v.py", src) for i in m.skipped_items: assert isinstance(i.line, int) and not i.message.startswith("line ") @@ -170,7 +171,7 @@ def test_structure_round_trips_through_the_cache_for_reused_modules(tmp_path): def test_not_marked_and_nothing_compiled_survive_reuse(tmp_path): root, path, _ = build_one(tmp_path, "pkg/plain.py", - "# typedpython: compiled\n\ndef f(s: str) -> str:\n return s[1:]\n") + "@compiled\ndef f(s: str) -> str:\n return s[1:]\n") again = pipeline.compile_project([path], root, tmp_path / "cache").modules[0] assert again.status == "reused" and again.kind == "nothing_compiled" and again.skipped_items root2, path2, _ = build_one(tmp_path, "pkg/un.py", "def f(x: int) -> int:\n return x\n") @@ -191,5 +192,5 @@ def test_cli_build_prints_file_line_name_message_and_not_marked(tmp_path): [sys.executable, "-m", "typedpython", "build", str(root), "--cache", str(tmp_path / "c")], capture_output=True, text=True, env=dict(os.environ, PYTHONPATH=str(src))) assert out.returncode == 0, out.stdout + out.stderr - assert re.search(r"^\s*pkg/mixed\.py:7: greet: \S", out.stdout, re.M), out.stdout + assert re.search(r"^\s*pkg/mixed\.py:8: greet: \S", out.stdout, re.M), out.stdout assert re.search(r"pkg/un\.py.*not marked for compilation", out.stdout), out.stdout diff --git a/python-multiplatform-ksp/src/test/python/test_native_int_impure.py b/python-multiplatform-ksp/src/test/python/test_native_int_impure.py index f2ceeb875..2da246f90 100644 --- a/python-multiplatform-ksp/src/test/python/test_native_int_impure.py +++ b/python-multiplatform-ksp/src/test/python/test_native_int_impure.py @@ -28,13 +28,13 @@ import pytest +from conftest import decorate_compiled from typedpython import cbuild, cgen, cost, frontend, ir, verify REPO = Path(__file__).resolve().parents[4] WORDFREQ = REPO / "benchmarks" / "typedpython" / "py" / "wordfreq.py" SOURCE = textwrap.dedent(''' - # typedpython: compiled import math as GMOD @@ -42,14 +42,17 @@ MOD: int = 2147483647 + @compiled def _one() -> int: return 1 + @compiled def _two() -> int: return 2 + @compiled class GTYPE: pass @@ -57,6 +60,7 @@ class GTYPE: GFN = _one + @compiled def add2(log: list[object], a: int, b: int) -> object: log.append("start") r: int = a + b @@ -66,6 +70,7 @@ def add2(log: list[object], a: int, b: int) -> object: return r + @compiled def sub2(log: list[object], a: int, b: int) -> object: log.append("start") r: int = a - b @@ -75,6 +80,7 @@ def sub2(log: list[object], a: int, b: int) -> object: return r + @compiled def mul2(log: list[object], a: int, b: int) -> object: log.append("start") r: int = a * b @@ -84,6 +90,7 @@ def mul2(log: list[object], a: int, b: int) -> object: return r + @compiled def neg2(log: list[object], a: int) -> object: log.append("start") r: int = -a @@ -93,6 +100,7 @@ def neg2(log: list[object], a: int) -> object: return r + @compiled def floordiv2(log: list[object], a: int, b: int) -> object: log.append("start") r: int = a + 1 @@ -104,6 +112,7 @@ def floordiv2(log: list[object], a: int, b: int) -> object: return r + @compiled def mod2(log: list[object], a: int, b: int) -> object: log.append("start") r: int = a - 1 @@ -115,6 +124,7 @@ def mod2(log: list[object], a: int, b: int) -> object: return r + @compiled def lsh(log: list[object], a: int, b: int) -> object: log.append("start") r: int = a << b @@ -122,6 +132,7 @@ def lsh(log: list[object], a: int, b: int) -> object: return r + @compiled def rsh(log: list[object], a: int, b: int) -> object: log.append("start") r: int = a + 1 @@ -131,6 +142,7 @@ def rsh(log: list[object], a: int, b: int) -> object: return r + @compiled def roundtrip(log: list[object], a: int) -> object: log.append(a) x: int = a @@ -145,6 +157,7 @@ def roundtrip(log: list[object], a: int) -> object: return x + @compiled def compare(log: list[object], a: int, b: int) -> None: log.append("start") x: int = a + 1 @@ -167,6 +180,7 @@ def compare(log: list[object], a: int, b: int) -> None: log.append("y-zero") + @compiled def fact(log: list[object], n: int) -> object: log.append(n) f: int = 1 @@ -176,6 +190,7 @@ def fact(log: list[object], n: int) -> object: return f + @compiled def grow(log: list[object], a: int, k: int) -> object: x: int = a i: int = 0 @@ -186,16 +201,19 @@ def grow(log: list[object], a: int, k: int) -> object: return x + @compiled def consts(log: list[object]) -> None: log.append(7000000000) log.append(5) log.append(-7000000000) + @compiled def next_state(s: int) -> int: return (s * 48271) % MOD + @compiled def drive(log: list[object], seed: int, n: int) -> object: s: int = seed + 1 for _ in range(n): @@ -204,37 +222,44 @@ def drive(log: list[object], seed: int, n: int) -> object: return s + @compiled def read_limit(log: list[object]) -> object: log.append(1) return LIMIT + @compiled def read_twice(log: list[object], cb: object) -> None: log.append(LIMIT) cb() log.append(LIMIT) + @compiled def use_builtin(log: list[object], s: str) -> object: log.append(1) return len(s) + @compiled def read_fn(log: list[object]) -> object: log.append(1) return GFN + @compiled def read_type(log: list[object]) -> object: log.append(1) return GTYPE + @compiled def read_mod(log: list[object]) -> object: log.append(1) return GMOD + @compiled def call_g(x: object) -> object: return GFN() ''') @@ -314,7 +339,7 @@ def test_int_locals_of_impure_functions_are_native(built): def test_an_int_fed_from_an_object_stays_an_object(tmp_path): src = textwrap.dedent(''' - # typedpython: compiled + @compiled def f(log: list[object], g: object) -> object: log.append(1) n: int = g() @@ -656,7 +681,7 @@ def test_a_cache_that_is_never_invalidated_is_caught(tmp_path): def test_an_int_loop_is_native_work_for_the_cost_rule(tmp_path): src = textwrap.dedent(''' - # typedpython: compiled + @compiled def digits(log: list[object], n: int) -> None: x: int = n + 1 while x > 0: @@ -672,7 +697,7 @@ def digits(log: list[object], n: int) -> None: def test_an_object_only_loop_is_still_rejected(tmp_path): src = textwrap.dedent(''' - # typedpython: compiled + @compiled def words(xs: list[object], ys: list[object]) -> None: for x in xs: ys.append(x) @@ -688,7 +713,7 @@ def words(xs: list[object], ys: list[object]) -> None: @pytest.fixture(scope="module") def wordfreq(tmp_path_factory): d = tmp_path_factory.mktemp("wordfreq") - src = "# typedpython: compiled\n" + WORDFREQ.read_text() + src = decorate_compiled(WORDFREQ.read_text()) path = d / "wordfreq.py" path.write_text(src) proved, diags = verify.verify(frontend.lower(path)) diff --git a/python-multiplatform-ksp/src/test/python/test_osr_exit.py b/python-multiplatform-ksp/src/test/python/test_osr_exit.py index fbfbdd3cf..227d50ecd 100644 --- a/python-multiplatform-ksp/src/test/python/test_osr_exit.py +++ b/python-multiplatform-ksp/src/test/python/test_osr_exit.py @@ -17,11 +17,11 @@ from typedpython import cbuild, cgen, frontend, verify SOURCE = textwrap.dedent(''' - # typedpython: compiled SCALE = 2.0 + @compiled def scale_all(x: list[float]) -> float: s = 0.0 for i in range(len(x)): @@ -30,6 +30,7 @@ def scale_all(x: list[float]) -> float: return s + @compiled def nested(x: list[float], n: int) -> int: hits = 0 for r in range(n): @@ -43,6 +44,7 @@ def nested(x: list[float], n: int) -> int: return hits + @compiled def countdown(x: list[int]) -> int: k = 0 total = 0 diff --git a/python-multiplatform-ksp/src/test/python/test_parallel_loops.py b/python-multiplatform-ksp/src/test/python/test_parallel_loops.py index 55fc184c6..e86605b69 100644 --- a/python-multiplatform-ksp/src/test/python/test_parallel_loops.py +++ b/python-multiplatform-ksp/src/test/python/test_parallel_loops.py @@ -33,26 +33,29 @@ PAR_RULE = "verify/par-loop" SOURCE = '''\ -# typedpython: compiled # typedpython: parallel +@compiled def scale(xs: list[float], a: float, b: float) -> None: for i in range(len(xs)): xs[i] = xs[i] * a + b +@compiled def scale_unmarked(xs: list[float], a: float, b: float) -> None: for i in range(len(xs)): xs[i] = xs[i] * a + b # typedpython: parallel +@compiled def ramp(xs: list[float], a: float) -> None: for i in range(len(xs)): xs[i] = -(float(i) * a) + xs[i] # typedpython: parallel +@compiled def two_stores(xs: list[float], ys: list[float], a: float) -> None: for i in range(len(xs)): xs[i] = xs[i] + a @@ -60,12 +63,14 @@ def two_stores(xs: list[float], ys: list[float], a: float) -> None: # typedpython: parallel +@compiled def neighbour(xs: list[float]) -> None: for i in range(len(xs) - 1): xs[i] = xs[i + 1] # typedpython: parallel +@compiled def total(xs: list[float]) -> float: s: float = 0.0 for i in range(len(xs)): @@ -73,23 +78,27 @@ def total(xs: list[float]) -> float: return s +@compiled def helper(x: float) -> float: return x * 2.0 # typedpython: parallel +@compiled def with_call(xs: list[float]) -> None: for i in range(len(xs)): xs[i] = helper(xs[i]) # typedpython: parallel +@compiled def divides(xs: list[float], d: float) -> None: for i in range(len(xs)): xs[i] = xs[i] / d # typedpython: parallel +@compiled def local_fill(a: float) -> float: t = [0.0] * 300000 for i in range(len(t)): @@ -98,6 +107,7 @@ def local_fill(a: float) -> float: # typedpython: parallel +@compiled def local_dyn(n: int, a: float) -> float: t = [1.0] * n for i in range(len(t)): @@ -438,9 +448,9 @@ def sh(): # ===================================================================================================== RSOURCE = '''\ -# typedpython: compiled # typedpython: parallel +@compiled def r_sum(xs: list[int]) -> int: s: int = 0 for i in range(len(xs)): @@ -449,6 +459,7 @@ def r_sum(xs: list[int]) -> int: # typedpython: parallel +@compiled def r_sum_init(xs: list[int], s0: int) -> int: s: int = s0 for i in range(len(xs)): @@ -457,6 +468,7 @@ def r_sum_init(xs: list[int], s0: int) -> int: # typedpython: parallel +@compiled def r_prod(xs: list[int], c: int) -> int: s: int = 1 for i in range(len(xs)): @@ -465,6 +477,7 @@ def r_prod(xs: list[int], c: int) -> int: # typedpython: parallel +@compiled def r_sum_mul(xs: list[int], c: int) -> int: s: int = 0 for i in range(len(xs)): @@ -473,6 +486,7 @@ def r_sum_mul(xs: list[int], c: int) -> int: # typedpython: parallel +@compiled def r_two(xs: list[int]) -> int: a: int = 0 b: int = 7 @@ -483,6 +497,7 @@ def r_two(xs: list[int]) -> int: # typedpython: parallel +@compiled def r_range_sum(xs: list[int]) -> int: s: int = 5 for i in range(len(xs)): @@ -491,6 +506,7 @@ def r_range_sum(xs: list[int]) -> int: # typedpython: parallel +@compiled def r_local(n: int) -> int: t = [0] * n for i in range(len(t)): @@ -501,6 +517,7 @@ def r_local(n: int) -> int: return s +@compiled def r_unmarked(xs: list[int]) -> int: s: int = 0 for i in range(len(xs)): @@ -509,6 +526,7 @@ def r_unmarked(xs: list[int]) -> int: # typedpython: parallel +@compiled def n_acc_read(xs: list[int]) -> int: s: int = 0 for i in range(len(xs)): @@ -517,6 +535,7 @@ def n_acc_read(xs: list[int]) -> int: # typedpython: parallel +@compiled def n_two_ops(xs: list[int]) -> int: s: int = 0 for i in range(len(xs)): @@ -526,6 +545,7 @@ def n_two_ops(xs: list[int]) -> int: # typedpython: parallel +@compiled def n_float(xs: list[float]) -> float: s: float = 0.0 for i in range(len(xs)): @@ -534,6 +554,7 @@ def n_float(xs: list[float]) -> float: # typedpython: parallel +@compiled def n_copy(xs: list[int]) -> int: s: int = 0 w: int = 0 @@ -544,6 +565,7 @@ def n_copy(xs: list[int]) -> int: # typedpython: parallel +@compiled def n_mixed(xs: list[int]) -> int: t = [0.0] * len(xs) s: int = 0 @@ -554,6 +576,7 @@ def n_mixed(xs: list[int]) -> int: # typedpython: parallel +@compiled def n_div(xs: list[int], d: int) -> int: s: int = 0 for i in range(len(xs)): @@ -562,6 +585,7 @@ def n_div(xs: list[int], d: int) -> int: # typedpython: parallel +@compiled def n_shift_step(xs: list[int]) -> int: s: int = 0 for i in range(0, len(xs), 2): @@ -859,9 +883,9 @@ def test_no_reduction_region_without_a_marker(tmp_path): # ===================================================================================================== MSOURCE = '''\ -# typedpython: compiled # typedpython: parallel +@compiled def mn_i(xs: list[int]) -> int: m = xs[0] for i in range(len(xs)): @@ -871,6 +895,7 @@ def mn_i(xs: list[int]) -> int: # typedpython: parallel +@compiled def mx_i(xs: list[int]) -> int: m = xs[0] for i in range(len(xs)): @@ -880,6 +905,7 @@ def mx_i(xs: list[int]) -> int: # typedpython: parallel +@compiled def mn_f(xs: list[float]) -> float: m = xs[0] for i in range(len(xs)): @@ -889,6 +915,7 @@ def mn_f(xs: list[float]) -> float: # typedpython: parallel +@compiled def mx_f(xs: list[float]) -> float: m = xs[0] for i in range(len(xs)): @@ -898,6 +925,7 @@ def mx_f(xs: list[float]) -> float: # typedpython: parallel +@compiled def mn_f_flipped(xs: list[float], init: float) -> float: m = init for i in range(len(xs)): @@ -907,6 +935,7 @@ def mn_f_flipped(xs: list[float], init: float) -> float: # typedpython: parallel +@compiled def mx_f_flipped(xs: list[float], init: float) -> float: m = init for i in range(len(xs)): @@ -916,6 +945,7 @@ def mx_f_flipped(xs: list[float], init: float) -> float: # typedpython: parallel +@compiled def mn_i_expr(xs: list[int], c: int) -> int: m = c for i in range(len(xs)): @@ -925,6 +955,7 @@ def mn_i_expr(xs: list[int], c: int) -> int: # typedpython: parallel +@compiled def mn_f_expr(xs: list[float], a: float) -> float: m = a for i in range(len(xs)): @@ -934,6 +965,7 @@ def mn_f_expr(xs: list[float], a: float) -> float: # typedpython: parallel +@compiled def mn_and_sum(xs: list[int]) -> int: m = xs[0] s = 0 @@ -945,6 +977,7 @@ def mn_and_sum(xs: list[int]) -> int: # typedpython: parallel +@compiled def two_ext(xs: list[float]) -> float: lo = xs[0] hi = xs[0] @@ -957,6 +990,7 @@ def two_ext(xs: list[float]) -> float: # typedpython: parallel +@compiled def mn_i_init(xs: list[int], init: int) -> int: m = init for i in range(len(xs)): @@ -966,6 +1000,7 @@ def mn_i_init(xs: list[int], init: int) -> int: # typedpython: parallel +@compiled def mn_i_big_e(xs: list[int], c: int) -> int: m = 0 for i in range(len(xs)): @@ -974,6 +1009,7 @@ def mn_i_big_e(xs: list[int], c: int) -> int: return m +@compiled def mn_unmarked(xs: list[int]) -> int: m = xs[0] for i in range(len(xs)): @@ -983,6 +1019,7 @@ def mn_unmarked(xs: list[int]) -> int: # typedpython: parallel +@compiled def n_le(xs: list[float]) -> float: m = xs[0] for i in range(len(xs)): @@ -992,6 +1029,7 @@ def n_le(xs: list[float]) -> float: # typedpython: parallel +@compiled def n_other_value(xs: list[int]) -> int: m = xs[0] for i in range(len(xs)): @@ -1001,6 +1039,7 @@ def n_other_value(xs: list[int]) -> int: # typedpython: parallel +@compiled def n_else(xs: list[int]) -> int: m = xs[0] s = 0 @@ -1013,6 +1052,7 @@ def n_else(xs: list[int]) -> int: # typedpython: parallel +@compiled def n_two_in_then(xs: list[int]) -> int: m = xs[0] s = 0 @@ -1024,6 +1064,7 @@ def n_two_in_then(xs: list[int]) -> int: # typedpython: parallel +@compiled def n_acc_in_value(xs: list[int]) -> int: m = xs[0] for i in range(len(xs)): @@ -1034,6 +1075,7 @@ def n_acc_in_value(xs: list[int]) -> int: # typedpython: parallel +@compiled def n_cond_other(xs: list[int], t: int) -> int: m = xs[0] for i in range(len(xs)): @@ -1043,6 +1085,7 @@ def n_cond_other(xs: list[int], t: int) -> int: # typedpython: parallel +@compiled def n_mixed_types(xs: list[int], init: float) -> float: m = init for i in range(len(xs)): @@ -1052,6 +1095,7 @@ def n_mixed_types(xs: list[int], init: float) -> float: # typedpython: parallel +@compiled def n_acc_reads_acc(xs: list[int]) -> int: m = xs[0] for i in range(len(xs)): @@ -1063,6 +1107,7 @@ def n_acc_reads_acc(xs: list[int]) -> int: # typedpython: parallel +@compiled def n_div(xs: list[float], d: float) -> float: m = xs[0] for i in range(len(xs)): @@ -1490,9 +1535,9 @@ def spin(): # ===================================================================================================== BSOURCE = '''\ -# typedpython: compiled # typedpython: parallel +@compiled def b_and(xs: list[int]) -> int: s: int = -1 for i in range(len(xs)): @@ -1501,6 +1546,7 @@ def b_and(xs: list[int]) -> int: # typedpython: parallel +@compiled def b_or(xs: list[int]) -> int: s: int = 0 for i in range(len(xs)): @@ -1509,6 +1555,7 @@ def b_or(xs: list[int]) -> int: # typedpython: parallel +@compiled def b_xor(xs: list[int]) -> int: s: int = 0 for i in range(len(xs)): @@ -1517,6 +1564,7 @@ def b_xor(xs: list[int]) -> int: # typedpython: parallel +@compiled def b_and_swapped(xs: list[int], s0: int) -> int: s: int = s0 for i in range(len(xs)): @@ -1525,6 +1573,7 @@ def b_and_swapped(xs: list[int], s0: int) -> int: # typedpython: parallel +@compiled def b_or_swapped(xs: list[int]) -> int: s: int = 0 for i in range(len(xs)): @@ -1533,6 +1582,7 @@ def b_or_swapped(xs: list[int]) -> int: # typedpython: parallel +@compiled def b_xor_expr(xs: list[int], c: int) -> int: s: int = 0 for i in range(len(xs)): @@ -1541,6 +1591,7 @@ def b_xor_expr(xs: list[int], c: int) -> int: # typedpython: parallel +@compiled def b_or_not(xs: list[int]) -> int: s: int = 0 for i in range(len(xs)): @@ -1549,6 +1600,7 @@ def b_or_not(xs: list[int]) -> int: # typedpython: parallel +@compiled def b_mix(xs: list[int]) -> int: a: int = 0 x: int = 5 @@ -1564,6 +1616,7 @@ def b_mix(xs: list[int]) -> int: # typedpython: parallel +@compiled def b_local(n: int) -> int: t = [0] * n for i in range(len(t)): @@ -1574,6 +1627,7 @@ def b_local(n: int) -> int: return s +@compiled def b_unmarked(xs: list[int]) -> int: s: int = 0 for i in range(len(xs)): @@ -1582,6 +1636,7 @@ def b_unmarked(xs: list[int]) -> int: # typedpython: parallel +@compiled def nb_acc_read(xs: list[int]) -> int: s: int = 0 for i in range(len(xs)): @@ -1590,6 +1645,7 @@ def nb_acc_read(xs: list[int]) -> int: # typedpython: parallel +@compiled def nb_two_ops(xs: list[int]) -> int: s: int = 0 for i in range(len(xs)): @@ -1599,6 +1655,7 @@ def nb_two_ops(xs: list[int]) -> int: # typedpython: parallel +@compiled def nb_and_then_add(xs: list[int]) -> int: s: int = 0 for i in range(len(xs)): @@ -1608,6 +1665,7 @@ def nb_and_then_add(xs: list[int]) -> int: # typedpython: parallel +@compiled def nb_shift(xs: list[int]) -> int: s: int = 0 for i in range(len(xs)): @@ -1616,6 +1674,7 @@ def nb_shift(xs: list[int]) -> int: # typedpython: parallel +@compiled def nb_rshift(xs: list[int], k: int) -> int: s: int = 0 for i in range(len(xs)): @@ -1624,6 +1683,7 @@ def nb_rshift(xs: list[int], k: int) -> int: # typedpython: parallel +@compiled def nb_copy(xs: list[int]) -> int: s: int = 0 w: int = 0 @@ -1634,6 +1694,7 @@ def nb_copy(xs: list[int]) -> int: # typedpython: parallel +@compiled def nb_div(xs: list[int], d: int) -> int: s: int = 0 for i in range(len(xs)): @@ -1794,15 +1855,16 @@ def test_bitwise_equals_interpreter_for_every_thread_count(bbuilt, threads): # ===================================================================================================== TSOURCE = '''\ -# typedpython: compiled # typedpython: parallel +@compiled def t_scale(xs: list[float], a: float, b: float) -> None: for i in range(len(xs)): xs[i] = xs[i] * a + b # typedpython: parallel +@compiled def t_sum(xs: list[int]) -> int: s: int = 0 for i in range(len(xs)): @@ -1811,6 +1873,7 @@ def t_sum(xs: list[int]) -> int: # typedpython: parallel +@compiled def t_prod(xs: list[int], c: int) -> int: s: int = 1 for i in range(len(xs)): @@ -1819,6 +1882,7 @@ def t_prod(xs: list[int], c: int) -> int: # typedpython: parallel +@compiled def t_min(xs: list[int]) -> int: m = xs[0] for i in range(len(xs)): @@ -1828,6 +1892,7 @@ def t_min(xs: list[int]) -> int: # typedpython: parallel +@compiled def t_max(xs: list[float]) -> float: m = xs[0] for i in range(len(xs)): @@ -1837,6 +1902,7 @@ def t_max(xs: list[float]) -> float: # typedpython: parallel +@compiled def t_and(xs: list[int]) -> int: s: int = -1 for i in range(len(xs)): @@ -1845,6 +1911,7 @@ def t_and(xs: list[int]) -> int: # typedpython: parallel +@compiled def t_or(xs: list[int]) -> int: s: int = 0 for i in range(len(xs)): @@ -1853,6 +1920,7 @@ def t_or(xs: list[int]) -> int: # typedpython: parallel +@compiled def t_xor(xs: list[int]) -> int: s: int = 0 for i in range(len(xs)): diff --git a/python-multiplatform-ksp/src/test/python/test_pipeline.py b/python-multiplatform-ksp/src/test/python/test_pipeline.py index 6efbeb14d..ed326eb4c 100644 --- a/python-multiplatform-ksp/src/test/python/test_pipeline.py +++ b/python-multiplatform-ksp/src/test/python/test_pipeline.py @@ -20,9 +20,9 @@ def interpreted(path: Path): SCALAR = """\ -# typedpython: compiled +@compiled def total(n: int) -> float: acc: float = 0.0 for k in range(n): @@ -30,6 +30,7 @@ def total(n: int) -> float: return acc +@compiled def fact(n: int) -> int: r: int = 1 for k in range(2, n + 1): @@ -53,6 +54,7 @@ def test_scalar_module_compiles_and_matches_cpython(write, tmp_path): def test_unsupported_functions_stay_interpreted_with_a_reason(write, tmp_path): path = write("mixed.py", SCALAR + """ +@compiled def greet(name: str) -> str: return name[1:] """) @@ -68,16 +70,16 @@ def test_a_module_with_nothing_to_compile_produces_no_extension(write, tmp_path) def test_gate_errors_block_compilation(write, tmp_path): - path = write("bad.py", "# typedpython: compiled\n\ndef f(x):\n return x\n") + path = write("bad.py", "@compiled\ndef f(x):\n return x\n") with pytest.raises(pipeline.CompileError, match="implicit-any-parameter"): pipeline.compile_module(path, tmp_path / "out") DEMO = """\ -# typedpython: compiled import sys +@compiled def total(n: int) -> float: acc: float = 0.0 for k in range(n): @@ -85,6 +87,7 @@ def total(n: int) -> float: return acc +@compiled def main() -> None: print(f"{total(int(sys.argv[1])):.3f}") """ @@ -103,15 +106,16 @@ def test_demo_reports_identical_output(write, tmp_path): ARRAY_CALLBACK = """\ -# typedpython: compiled from typing import Callable +@compiled def first_after(xs: list[float], cb: Callable[[], None]) -> float: cb() return xs[0] +@compiled def fill_then(xs: list[float], cb: Callable[[], None]) -> None: xs[1] = 5.0 cb() @@ -138,9 +142,9 @@ def cb2(): GC_REACHABLE = """\ -# typedpython: compiled +@compiled class Cell: a: object b: object @@ -151,10 +155,12 @@ def __init__(self, a: object, b: object) -> None: self.b = b +@compiled def seta(c: Cell, v: object) -> None: c.a = v +@compiled def fresh(a: object, n: int) -> object: c = Cell(None, None) seta(c, a) diff --git a/python-multiplatform-ksp/src/test/python/test_pipeline_incremental.py b/python-multiplatform-ksp/src/test/python/test_pipeline_incremental.py index dd7cfad86..3735df9bc 100644 --- a/python-multiplatform-ksp/src/test/python/test_pipeline_incremental.py +++ b/python-multiplatform-ksp/src/test/python/test_pipeline_incremental.py @@ -9,25 +9,24 @@ import pytest +from conftest import decorate_compiled from typedpython import cbuild, frontend, pipeline -HEADER = "# typedpython: compiled\n" - -B = HEADER + textwrap.dedent("""\ +B = decorate_compiled(textwrap.dedent("""\ def twice(n: int) -> int: return n * 2 - """) -A = HEADER + textwrap.dedent("""\ + """)) +A = decorate_compiled(textwrap.dedent("""\ import b def inc(n: int) -> int: return n + 1 - """) -C = HEADER + textwrap.dedent("""\ + """)) +C = decorate_compiled(textwrap.dedent("""\ def half(x: float) -> float: return x * 0.5 - """) + """)) def interpreted(path: Path): @@ -133,7 +132,7 @@ def test_rebuilt_and_reused_extensions_compute_the_interpreted_results(project, def test_module_with_nothing_compiled_has_no_extension_and_is_reused(tmp_path): root = tmp_path / "proj" root.mkdir() - (root / "plain.py").write_text(HEADER + "def f(s: str) -> str:\n return s[1:]\n") + (root / "plain.py").write_text("@compiled\ndef f(s: str) -> str:\n return s[1:]\n") cache = tmp_path / "cache" first = pipeline.compile_project([root / "plain.py"], root, cache) m = first.modules[0] @@ -151,7 +150,7 @@ def test_interface_describes_the_verified_module_not_the_lowered_one(tmp_path, m root = tmp_path / "proj" root.mkdir() - (root / "dep.py").write_text(HEADER + "def ok(n: int) -> int:\n return n + 1\n") + (root / "dep.py").write_text("@compiled\ndef ok(n: int) -> int:\n return n + 1\n") real_lower = frontend.lower seen = {} diff --git a/python-multiplatform-ksp/src/test/python/test_recursion.py b/python-multiplatform-ksp/src/test/python/test_recursion.py index 289e1de66..49901b0e6 100644 --- a/python-multiplatform-ksp/src/test/python/test_recursion.py +++ b/python-multiplatform-ksp/src/test/python/test_recursion.py @@ -20,15 +20,16 @@ PACKAGE = Path(__file__).resolve().parents[2] / "main" / "python" SOURCE = """\ -# typedpython: compiled +@compiled def fib(n: int) -> int: if n < 2: return n return fib(n - 1) + fib(n - 2) +@compiled def ack(m: int, n: int) -> int: if m == 0: return n + 1 @@ -37,30 +38,35 @@ def ack(m: int, n: int) -> int: return ack(m - 1, ack(m, n - 1)) +@compiled def is_even(n: int) -> bool: if n == 0: return True return is_odd(n - 1) +@compiled def is_odd(n: int) -> bool: if n == 0: return False return is_even(n - 1) +@compiled def down(n: int) -> int: if n == 0: return 0 return 1 + down(n - 1) +@compiled def pow2(n: int) -> int: if n == 0: return 1 return 2 * pow2(n - 1) +@compiled def fact(n: int) -> int: if n < 2: return 1 diff --git a/python-multiplatform-ksp/src/test/python/test_regions.py b/python-multiplatform-ksp/src/test/python/test_regions.py index 0246fd207..ac7aad6c8 100644 --- a/python-multiplatform-ksp/src/test/python/test_regions.py +++ b/python-multiplatform-ksp/src/test/python/test_regions.py @@ -37,12 +37,12 @@ FAIL_AFTER = "__typedpython_region_fail_after__" # (n) -> the nth region object allocation fails; -1 off SOURCE = textwrap.dedent('''\ - # typedpython: compiled """typedpython region test module.""" KEPT = None + @compiled class Node: __slots__ = ("left", "right") left: "Node | None" @@ -53,12 +53,14 @@ def __init__(self, left: "Node | None", right: "Node | None") -> None: self.right = right + @compiled def make(depth: int) -> Node: if depth > 0: return Node(make(depth - 1), make(depth - 1)) return Node(None, None) + @compiled def check(node: Node) -> int: left: Node | None = node.left right: Node | None = node.right @@ -67,10 +69,12 @@ def check(node: Node) -> int: return 1 + check(left) + check(right) + @compiled def tree_sum(d: int) -> int: return check(make(d)) + @compiled def repeat(d: int, n: int) -> int: t: int = 0 for _ in range(n): @@ -78,35 +82,42 @@ def repeat(d: int, n: int) -> int: return t + @compiled def escapes(d: int) -> Node: return make(d) + @compiled def keeps(d: int) -> int: global KEPT KEPT = make(d) return check(KEPT) + @compiled def prints(d: int) -> int: n = make(d) print("unknown code while the tree is alive") return check(n) + @compiled def ident(d: int) -> int: return id(make(d)) * 0 + @compiled def same(d: int) -> bool: return make(d) is make(d) + @compiled def mixed(d: int) -> int: t = make(d) return check(make(d)) + check(t) + @compiled def noisy(d: int) -> object: print("unknown code") return check(make(d)) diff --git a/python-multiplatform-ksp/src/test/python/test_resume.py b/python-multiplatform-ksp/src/test/python/test_resume.py index 14115cc79..984c7b4b3 100644 --- a/python-multiplatform-ksp/src/test/python/test_resume.py +++ b/python-multiplatform-ksp/src/test/python/test_resume.py @@ -519,7 +519,7 @@ def lower(tmp_path, source): def test_frontend_gives_a_snapshot_holder_one_resume_per_loop(tmp_path): module = lower(tmp_path, """ - # typedpython: compiled + @compiled def f(xs: list[float], n: int) -> float: t = 0.0 for i in range(n): @@ -572,7 +572,7 @@ def f(xs: list[float], n: int) -> float: def test_frontend_function_without_a_snapshot_has_no_resumes(tmp_path): module = lower(tmp_path, """ - # typedpython: compiled + @compiled def g(n: int) -> int: t = 0 for i in range(n): diff --git a/python-multiplatform-ksp/src/test/python/test_str_dict.py b/python-multiplatform-ksp/src/test/python/test_str_dict.py index 2362033c4..e6bc52c7b 100644 --- a/python-multiplatform-ksp/src/test/python/test_str_dict.py +++ b/python-multiplatform-ksp/src/test/python/test_str_dict.py @@ -26,70 +26,84 @@ import pytest +from conftest import decorate_compiled from typedpython import cbuild, cgen, frontend, verify REPO = Path(__file__).resolve().parents[4] WORDFREQ = REPO / "benchmarks" / "typedpython" / "py" / "wordfreq.py" SOURCE = textwrap.dedent(''' - # typedpython: compiled from typing import SupportsIndex + @compiled def get(d: dict[object, object], k: object) -> object: return d[k] + @compiled def get_list(xs: list[object], i: int) -> object: return xs[i] + @compiled def get_index(xs: list[object], i: SupportsIndex) -> object: return xs[i] + @compiled def get_str(s: str, i: int) -> str: return s[i] + @compiled def get_nested(d: dict[object, dict[object, object]], a: object, b: object) -> object: return d[a][b] + @compiled def put(d: dict[object, object], k: object, v: object) -> None: d[k] = v + @compiled def put_list(xs: list[object], i: int, v: object) -> None: xs[i] = v + @compiled def has(c: list[object], x: object) -> bool: return x in c + @compiled def has_not(c: list[object], x: object) -> bool: return x not in c + @compiled def has_dict(c: dict[object, object], x: object) -> bool: return x in c + @compiled def has_str(c: str, x: str) -> bool: return x in c + @compiled def has_branch(c: list[object], x: object) -> str: if x in c: return "yes" return "no" + @compiled def has_chain(c: list[object], x: object, y: object) -> bool: return x in c in y + @compiled def pairs(xs: list[tuple[object, ...]]) -> list[object]: out: list[object] = [] for a, b in xs: @@ -98,6 +112,7 @@ def pairs(xs: list[tuple[object, ...]]) -> list[object]: return out + @compiled def triples(xs: list[tuple[object, ...]]) -> list[object]: out: list[object] = [] for a, b, c in xs: @@ -106,6 +121,7 @@ def triples(xs: list[tuple[object, ...]]) -> list[object]: return out + @compiled def each(xs: list[object]) -> list[object]: out: list[object] = [] for x in xs: @@ -115,6 +131,7 @@ def each(xs: list[object]) -> list[object]: return out + @compiled def first_after(xs: list[object], stop: object) -> object: found: object = None for x in xs: @@ -124,12 +141,14 @@ def first_after(xs: list[object], stop: object) -> object: return found + @compiled def return_inside(xs: list[object]) -> object: for x in xs: return x return None + @compiled def nested(xs: list[list[object]]) -> list[object]: out: list[object] = [] for row in xs: @@ -140,6 +159,7 @@ def nested(xs: list[list[object]]) -> list[object]: return out + @compiled def items_of(d: dict[str, object]) -> list[object]: out: list[object] = [] for k, v in d.items(): @@ -148,6 +168,7 @@ def items_of(d: dict[str, object]) -> list[object]: return out + @compiled def mutate_while_iterating(d: dict[str, object]) -> object: n: int = 0 for k in d: @@ -156,30 +177,37 @@ def mutate_while_iterating(d: dict[str, object]) -> object: return n + @compiled def fmt(x: object, y: float, n: int) -> str: return f"{x}|{x!r}|{x!s}|{x!a}|{y:.3f}|{n:05d}|{x:>5}|{n}" + @compiled def fmt_one(x: object) -> str: return f"{x}" + @compiled def fmt_text(x: object) -> str: return f"a{x}b{x!r}c" + @compiled def fmt_empty() -> str: return f"" + @compiled def fmt_debug(x: object) -> str: return f"{x=}" + @compiled def cat(a: str, b: str) -> str: return a + b + @compiled def cat_in_place(a: str, b: str) -> str: s: str = "" s += a @@ -187,6 +215,7 @@ def cat_in_place(a: str, b: str) -> str: return s + @compiled def grow(xs: list[object], ys: list[object]) -> list[object]: out: list[object] = [] out += xs @@ -194,6 +223,7 @@ def grow(xs: list[object], ys: list[object]) -> list[object]: return out + @compiled def empties() -> object: d: dict[object, object] = {} xs: list[object] = [] @@ -201,6 +231,7 @@ def empties() -> object: return xs + @compiled class Acc: def __init__(self, log: list[object]) -> None: self.log = log @@ -230,6 +261,7 @@ def __imod__(self, other: int) -> Acc: return self + @compiled def aug_ops(a: Acc, b: int) -> Acc: a += b a -= b @@ -240,12 +272,14 @@ def aug_ops(a: Acc, b: int) -> Acc: return a + @compiled def aug_list(xs: list[str], n: int) -> list[str]: xs *= n xs += xs return xs + @compiled def lookup_default(d: dict[str, int], k: str) -> object: counts: dict[str, int] = {} counts[k] = counts.get(k, 0) + 1 @@ -905,7 +939,7 @@ def run_main(main, n): @pytest.fixture(scope="module") def wordfreq(tmp_path_factory): d = tmp_path_factory.mktemp("wordfreq") - src = "# typedpython: compiled\n" + WORDFREQ.read_text() + src = decorate_compiled(WORDFREQ.read_text()) proved, diags, m = build_source(d, src, name="wordfreq") spec = importlib.util.spec_from_file_location("wordfreq_plain", WORDFREQ) plain = importlib.util.module_from_spec(spec) diff --git a/python-multiplatform-ksp/src/test/python/test_tracing.py b/python-multiplatform-ksp/src/test/python/test_tracing.py index 2c6939249..39f3c78b3 100644 --- a/python-multiplatform-ksp/src/test/python/test_tracing.py +++ b/python-multiplatform-ksp/src/test/python/test_tracing.py @@ -19,14 +19,15 @@ from typedpython import pipeline SOURCE = """\ -# typedpython: compiled from typing import Callable +@compiled def sq(x: float) -> float: return x * x +@compiled def loop(n: int) -> float: acc: float = 0.0 for k in range(n): @@ -34,11 +35,13 @@ def loop(n: int) -> float: return acc +@compiled def leaf(cb: Callable[[], None], n: float) -> float: cb() return n + 1.0 +@compiled def outer(cb: Callable[[], None], n: float) -> float: cb() return leaf(cb, n) + leaf(cb, n) From cca2ef5753f544a53c2c19eae5715e3a6b3a6616 Mon Sep 17 00:00:00 2001 From: BREW <81044362+b-re-w@users.noreply.github.com> Date: Fri, 30 Oct 2026 05:01:16 +0900 Subject: [PATCH 2/2] TypedPython tests: child processes install the compiled builtin themselves Plain CPython without typedpython_builtins.pth has no builtins.compiled; the child scripts now prepend conftest's COMPILED_SHIM. Also fixes test_regions' split that left a dangling @compiled. --- python-multiplatform-ksp/src/test/python/conftest.py | 4 ++++ .../src/test/python/test_class_methods.py | 8 ++++---- .../src/test/python/test_eval_breaker.py | 3 ++- .../src/test/python/test_fast_entry.py | 3 ++- .../src/test/python/test_free_threaded.py | 3 ++- .../src/test/python/test_parallel_loops.py | 3 ++- .../src/test/python/test_recursion.py | 3 ++- python-multiplatform-ksp/src/test/python/test_regions.py | 4 +++- python-multiplatform-ksp/src/test/python/test_tracing.py | 3 ++- 9 files changed, 23 insertions(+), 11 deletions(-) diff --git a/python-multiplatform-ksp/src/test/python/conftest.py b/python-multiplatform-ksp/src/test/python/conftest.py index 669816bf4..85b038234 100644 --- a/python-multiplatform-ksp/src/test/python/conftest.py +++ b/python-multiplatform-ksp/src/test/python/conftest.py @@ -9,6 +9,10 @@ # The `typedpython_builtins.pth` of an installed wheel; the test interpreter has no such install. builtins.compiled = getattr(builtins, "compiled", lambda f, /: f) +# Prepended to every child-process script that loads a module whose source uses `@compiled`: a plain +# CPython has the builtin only when `typedpython_builtins.pth` is installed, so the child installs it. +COMPILED_SHIM = "import builtins\nif not hasattr(builtins, 'compiled'):\n builtins.compiled = lambda f: f\n" + FIXTURES = Path(__file__).parent / "fixtures" diff --git a/python-multiplatform-ksp/src/test/python/test_class_methods.py b/python-multiplatform-ksp/src/test/python/test_class_methods.py index cd08794e3..b35b5db6e 100644 --- a/python-multiplatform-ksp/src/test/python/test_class_methods.py +++ b/python-multiplatform-ksp/src/test/python/test_class_methods.py @@ -40,7 +40,7 @@ import pytest -from conftest import decorate_compiled +from conftest import COMPILED_SHIM, decorate_compiled from typedpython import cbuild, cgen, cost, frontend, pipeline, verify HERE = Path(__file__).resolve().parent @@ -553,7 +553,7 @@ def test_class_info_reports_the_methods(built): # --- class state changed after import: child processes -------------------------------------------- -CHILD = ''' +CHILD = COMPILED_SHIM + ''' import json, sys from importlib.util import module_from_spec, spec_from_file_location from pathlib import Path @@ -736,7 +736,7 @@ def test_reference_counts_are_flat_over_many_calls(built): # --- free-threaded concurrency -------------------------------------------------------------------- -THREADS = ''' +THREADS = COMPILED_SHIM + ''' import sys, threading from pathlib import Path sys.path.insert(0, {main!r}) @@ -800,7 +800,7 @@ def test_concurrent_calls_on_shared_and_private_instances(built): # --- mutation: the exact-type guard of `self` ------------------------------------------------------- -GUARD_PROBE = ''' +GUARD_PROBE = COMPILED_SHIM + ''' import sys from pathlib import Path sys.path.insert(0, {main!r}) diff --git a/python-multiplatform-ksp/src/test/python/test_eval_breaker.py b/python-multiplatform-ksp/src/test/python/test_eval_breaker.py index 0d6bb643b..dece8d3b8 100644 --- a/python-multiplatform-ksp/src/test/python/test_eval_breaker.py +++ b/python-multiplatform-ksp/src/test/python/test_eval_breaker.py @@ -18,6 +18,7 @@ import pytest +from conftest import COMPILED_SHIM from typedpython import pipeline posix_signals = pytest.mark.skipif(sys.platform == "win32", reason="needs POSIX signals (SIGALRM/SIGUSR1/setitimer) or fork; Windows has none") @@ -49,7 +50,7 @@ def total(xs: list[float], rounds: int) -> float: return s """ -LOADER = """\ +LOADER = COMPILED_SHIM + """\ import importlib.machinery, importlib.util, json, signal, sys, threading, time path = sys.argv[1] loader = importlib.machinery.ExtensionFileLoader("loops", path) diff --git a/python-multiplatform-ksp/src/test/python/test_fast_entry.py b/python-multiplatform-ksp/src/test/python/test_fast_entry.py index d448e3b41..633a14487 100644 --- a/python-multiplatform-ksp/src/test/python/test_fast_entry.py +++ b/python-multiplatform-ksp/src/test/python/test_fast_entry.py @@ -20,6 +20,7 @@ import pytest +from conftest import COMPILED_SHIM from typedpython import cbuild, cgen, frontend, verify posix_signals = pytest.mark.skipif(sys.platform == "win32", reason="needs POSIX signals (SIGALRM/SIGUSR1/setitimer) or fork; Windows has none") @@ -170,7 +171,7 @@ def test_the_fast_impl_is_never_addressed(traced): assert "&tp_fimpl_" not in c and "(tp_fimpl_" not in c -LOAD = """\ +LOAD = COMPILED_SHIM + """\ import importlib.machinery, importlib.util, json, sys def load(name, path, ext): if ext: diff --git a/python-multiplatform-ksp/src/test/python/test_free_threaded.py b/python-multiplatform-ksp/src/test/python/test_free_threaded.py index e954c03ba..e857d3950 100644 --- a/python-multiplatform-ksp/src/test/python/test_free_threaded.py +++ b/python-multiplatform-ksp/src/test/python/test_free_threaded.py @@ -27,6 +27,7 @@ import pytest +from conftest import COMPILED_SHIM from typedpython import cbuild, cgen, frontend, pipeline, verify STRESS = max(1, int(os.environ.get("TP_FT_STRESS", "1"))) @@ -155,7 +156,7 @@ def usek(x: int) -> int: return x + K ''' -LOADER = """\ +LOADER = COMPILED_SHIM + """\ import faulthandler, importlib.machinery, importlib.util, json, sys, threading, time faulthandler.enable() path = sys.argv[1] diff --git a/python-multiplatform-ksp/src/test/python/test_parallel_loops.py b/python-multiplatform-ksp/src/test/python/test_parallel_loops.py index e86605b69..ecf4b8c7a 100644 --- a/python-multiplatform-ksp/src/test/python/test_parallel_loops.py +++ b/python-multiplatform-ksp/src/test/python/test_parallel_loops.py @@ -25,6 +25,7 @@ import pytest +from conftest import COMPILED_SHIM from typedpython import cbuild, cgen, frontend, ir, verify posix_signals = pytest.mark.skipif(sys.platform == "win32", reason="needs POSIX signals (SIGALRM/SIGUSR1/setitimer) or fork; Windows has none") @@ -335,7 +336,7 @@ def test_list_untouched_when_the_call_raises_before_the_loop(mod): # --- child processes: thread counts, creation failure, concurrency ----------------------------------- -CHILD = """\ +CHILD = COMPILED_SHIM + """\ import importlib.machinery, importlib.util, json, struct, sys, threading path = sys.argv[1] name = path.replace(chr(92), "/").rsplit("/", 1)[-1].split(".")[0] diff --git a/python-multiplatform-ksp/src/test/python/test_recursion.py b/python-multiplatform-ksp/src/test/python/test_recursion.py index 49901b0e6..84c36f1e6 100644 --- a/python-multiplatform-ksp/src/test/python/test_recursion.py +++ b/python-multiplatform-ksp/src/test/python/test_recursion.py @@ -15,6 +15,7 @@ import pytest +from conftest import COMPILED_SHIM from typedpython import pipeline PACKAGE = Path(__file__).resolve().parents[2] / "main" / "python" @@ -239,7 +240,7 @@ def inner(j): # --- sanitizers: the RecursionError path --------------------------------------------------------- -ASAN_SCRIPT = textwrap.dedent('''\ +ASAN_SCRIPT = COMPILED_SHIM + textwrap.dedent('''\ import sys sys.path.insert(0, {pkg!r}) from typedpython import cbuild diff --git a/python-multiplatform-ksp/src/test/python/test_regions.py b/python-multiplatform-ksp/src/test/python/test_regions.py index ac7aad6c8..7d7080ee9 100644 --- a/python-multiplatform-ksp/src/test/python/test_regions.py +++ b/python-multiplatform-ksp/src/test/python/test_regions.py @@ -25,6 +25,7 @@ import pytest from test_cgen import runtime_dir, need_runtime +from conftest import COMPILED_SHIM from typedpython import cbuild, cgen, cost, frontend, verify posix_signals = pytest.mark.skipif(sys.platform == "win32", reason="needs POSIX signals (SIGALRM/SIGUSR1/setitimer) or fork; Windows has none") @@ -392,10 +393,11 @@ def test_a_forged_module_level_variant_set_is_reported(built): def test_a_finalizer_or_a_field_store_keeps_the_class_out_of_a_region(tmp_path): - base = SOURCE.split("def escapes")[0] + base = SOURCE.split("@compiled\ndef escapes")[0] with_del = base.replace(" self.right = right\n", " self.right = right\n\n def __del__(self) -> None:\n pass\n", 1) with_store = base + textwrap.dedent('''\ + @compiled def poke(node: Node) -> int: node.left = None return 0 diff --git a/python-multiplatform-ksp/src/test/python/test_tracing.py b/python-multiplatform-ksp/src/test/python/test_tracing.py index 39f3c78b3..823c1dd1d 100644 --- a/python-multiplatform-ksp/src/test/python/test_tracing.py +++ b/python-multiplatform-ksp/src/test/python/test_tracing.py @@ -16,6 +16,7 @@ import pytest +from conftest import COMPILED_SHIM from typedpython import pipeline SOURCE = """\ @@ -47,7 +48,7 @@ def outer(cb: Callable[[], None], n: float) -> float: return leaf(cb, n) + leaf(cb, n) """ -LOADER = """\ +LOADER = COMPILED_SHIM + """\ import importlib.machinery, importlib.util, json, sys, threading path = sys.argv[1] loader = importlib.machinery.ExtensionFileLoader("tmod", path)