From 0e54f8eeaee9c940491518b15551a3cc25bc5afa Mon Sep 17 00:00:00 2001 From: BREW <81044362+b-re-w@users.noreply.github.com> Date: Sat, 31 Oct 2026 23:49:18 +0900 Subject: [PATCH] Feat: replace # typedpython: parallel comment with @compiled(parallel=True) (#358) --- docs/SPEC.md | 4 +- docs/design/typedpython-parallel-loops.md | 15 +- .../src/main/python/typedpython/__main__.py | 2 +- .../builtin_stubs/__builtins__.pyi | 6 + .../src/main/python/typedpython/frontend.py | 85 ++++--- .../src/main/python/typedpython/ir.py | 4 +- .../src/main/python/typedpython_builtins.pth | 2 +- .../src/test/python/conftest.py | 4 +- .../src/test/python/test_builtin_compiled.py | 13 +- .../src/test/python/test_frontend.py | 23 ++ .../src/test/python/test_parallel_loops.py | 216 ++++++------------ .../python/multiplatform/ffi/Python3.kt | 4 +- .../multiplatform/ffi/BuiltinCompiledTest.kt | 4 +- 13 files changed, 192 insertions(+), 190 deletions(-) diff --git a/docs/SPEC.md b/docs/SPEC.md index 61c454c2f..a41dd5b2e 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -80,7 +80,7 @@ fail, then implement. extracts it. `Status: partial`, exercised by the shared suite on the simulator; no iOS-specific lifecycle test beyond `PM/iosSimulatorArm64Test/.../AsyncioAvailabilityProbeTest.kt`. An installed app carries its own stdlib: L-11. -- **L-8** Initialisation sets `builtins.compiled` to an identity decorator (an existing one is kept), so `@compiled` needs no import on any platform (N-7, issue #42). `Status: implemented`, `PM/commonTest/.../ffi/BuiltinCompiledTest.kt`. +- **L-8** Initialisation sets `builtins.compiled` to an identity decorator accepting both `@compiled` (`compiled(f)`) and `@compiled(**opts)` (`compiled(**opts)(f)`), leaving an existing `builtins.compiled` alone, so `@compiled` needs no import on any platform (N-7, issue #42, #358). `Status: implemented`, `PM/commonTest/.../ffi/BuiltinCompiledTest.kt`. - **L-9** Desktop: a packaged application starts with no `PYTHONHOME` in its environment (issue #60, ROADMAP §15.4). The Gradle plugin copies the prefix `stagePythonHome` stages into Compose Desktop's application resources (every `prepare*AppResources` task, so `createDistributable` and the @@ -985,7 +985,7 @@ All in `PM/commonTest`, so they run wherever the interpreter loads. - **N-15** TypedPython parallel execution of order-independent loops on free-threaded CPython (#151; `docs/design/typedpython-parallel-loops.md`). A compiled `for ... in range(...)` loop may run its iterations on several threads only if its result cannot depend on iteration order, in a function the - user marked with a `# typedpython: parallel` comment line directly above the `def` (above its decorators if any); module-level functions only, and then the + user marked `@compiled(parallel=True)` (#358; the earlier `# typedpython: parallel` comment line was removed); module-level functions only, and then the result, the exception, the partial writes and the deopt behaviour are bit-for-bit those of the sequential loop (INTENT §1.4; N-12: every execution is indistinguishable from some valid CPython execution). diff --git a/docs/design/typedpython-parallel-loops.md b/docs/design/typedpython-parallel-loops.md index e86c3e06b..2834d69a6 100644 --- a/docs/design/typedpython-parallel-loops.md +++ b/docs/design/typedpython-parallel-loops.md @@ -30,8 +30,7 @@ checkable on the existing IR (`typedpython/ir.py`), and says where that rule giv (§7, decision 3). Thread creation cost is measured together with the threshold. 6. A cost rule decides, at run time, whether a region is worth its fixed cost; below the threshold the sequential code runs (§8). The threshold is measured, not guessed here. -7. v1 parallelises only functions the user marks (decision 5). The spelling of the marking is a design - detail settled at implementation. Widening to unmarked functions later is easy; narrowing is hard. +7. v1 parallelises only functions the user marks `@compiled(parallel=True)` (decision 5, #358). Widening to unmarked functions later is easy; narrowing is hard. ## 2. What exists today (the facts this design stands on) @@ -78,7 +77,7 @@ condition that failed (like `verify/array-open`), so users can see why. - The function holds a copy-in list array, a local array, or only scalars (a pure scalar loop, e.g. a reduction over `range`). A function whose loop touches a buffer array (`array.array`) is not parallelised (decision 1). -- The user marked the function (decision 5). +- The user marked the function `@compiled(parallel=True)` (decision 5, #358). - Not under `entry_globals` mutation: entry globals are read once at entry and are locals in the body, so they are read-only and fine. @@ -503,9 +502,11 @@ Recorded 2026-10-25 from the maintainer. The body and SPEC N-15 follow them. measured together with the threshold (`TP_PAR_MIN_WORK`). 4. **Float `+` and `*` reductions are excluded in v1.** Not permanently: a fastmath opt-in (#153) awaits the owner's decision, and N-15 follows that decision. Until then they stay sequential. -5. **Only functions the user marks are parallelised in v1.** How the marking is spelled (decorator - argument, comment, other) is a design detail to settle at implementation. Widening later (implicit - parallelisation) is easy; narrowing later is hard, so v1 starts narrow. +5. **Only functions the user marks are parallelised in v1.** Marked with `@compiled(parallel=True)` + (maintainer decision 2026-10-30, #358: `@compiled` is the only compilation mark, and options are + keyword arguments on `@compiled`, matching `#332` `@compiled(fastmath=True)`; the earlier + `# typedpython: parallel` comment marker was removed). Widening later (implicit parallelisation) is + easy; narrowing later is hard, so v1 starts narrow. 6. **`min`/`max` reductions are in v1 only if exactly equal to Python's** (§6). If an implementation cannot match it, they move to a later step and v1 keeps the integer and bitwise reductions only. 7. **Thresholds are measured, not chosen here.** `TP_PAR_MIN_WORK` and the thread cap (default 8) are @@ -516,4 +517,4 @@ Recorded 2026-10-25 from the maintainer. The body and SPEC N-15 follow them. - (Settled: free-threaded 3.15t is the only build, #158, so there is no GIL-build variant.) - Which devices the threshold is measured on (the Mac mini only, or also an Android device). The measurement itself is decided (item 7); the device set is not. -- The spelling of the marking (item 5), settled at implementation. +- (Settled 2026-10-30, #358: the marking is `@compiled(parallel=True)`.) diff --git a/python-multiplatform-ksp/src/main/python/typedpython/__main__.py b/python-multiplatform-ksp/src/main/python/typedpython/__main__.py index a9cda63e2..4db824787 100644 --- a/python-multiplatform-ksp/src/main/python/typedpython/__main__.py +++ b/python-multiplatform-ksp/src/main/python/typedpython/__main__.py @@ -113,7 +113,7 @@ def _demo(module: Path, module_argv: list[str], out: Path) -> int: 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) + builtins.compiled = getattr(builtins, "compiled", lambda f=None, /, **_k: (lambda g: g) if f is None else 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/builtin_stubs/__builtins__.pyi b/python-multiplatform-ksp/src/main/python/typedpython/builtin_stubs/__builtins__.pyi index f773368b9..122e4cdf5 100644 --- a/python-multiplatform-ksp/src/main/python/typedpython/builtin_stubs/__builtins__.pyi +++ b/python-multiplatform-ksp/src/main/python/typedpython/builtin_stubs/__builtins__.pyi @@ -1,3 +1,9 @@ # TypedPython's builtin names (docs/design/typedpython.md §2.3). A `__builtins__.pyi` on the type # checker's search path declares names the interpreter provides without an import. +from collections.abc import Callable +from typing import overload + +@overload def compiled[T](f: T, /) -> T: ... +@overload +def compiled[T](*, parallel: bool = ..., fastmath: bool = ...) -> Callable[[T], T]: ... diff --git a/python-multiplatform-ksp/src/main/python/typedpython/frontend.py b/python-multiplatform-ksp/src/main/python/typedpython/frontend.py index 6e0b75aa6..a65ea1b9f 100644 --- a/python-multiplatform-ksp/src/main/python/typedpython/frontend.py +++ b/python-multiplatform-ksp/src/main/python/typedpython/frontend.py @@ -190,7 +190,8 @@ def lower(path: Path) -> ir.Module: info.classes = class_decls skipped.update(class_skips) signatures: dict[str, _Signature] = {} - for node, reason in _targets(tree, source, info): + marked: set[str] = set() + for node, reason, parallel in _targets(tree, source, info): if reason is not None: skipped[node.name] = reason continue @@ -199,6 +200,9 @@ def lower(path: Path) -> ir.Module: signatures[node.name] = _signature(node, info.classes, info) except Skip as e: skipped[node.name] = e.reason + else: + if parallel: + marked.add(node.name) for owner, node, reason in _method_targets(tree, info): q = f"{owner}.{node.name}" if reason is not None: @@ -236,28 +240,10 @@ def lower(path: Path) -> ir.Module: else: skipped[name] = result functions.sort(key=lambda f: f.source_line) - marked = _parallel_marked(tree, source) functions = [dataclasses.replace(f, parallel=True) if f.name in marked else f for f in functions] return ir.Module(path.stem, tuple(functions), skipped, tuple(class_decls.values())) -PARALLEL_MARKER = "# typedpython: parallel" - - -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, - so the interpreted module is unchanged and needs no runtime name.""" - lines = source.splitlines() - out: set[str] = set() - for node in tree.body: - if isinstance(node, ast.FunctionDef): - first = min([node.lineno] + [d.lineno for d in node.decorator_list]) - if first >= 2 and lines[first - 2].strip() == PARALLEL_MARKER: - out.add(node.name) - return out - - def _module_info(tree: ast.Module) -> _ModuleInfo: bindings: dict[str, list[tuple[int, str]]] = {} @@ -431,13 +417,45 @@ def _binding_targets(stmt: ast.stmt) -> list[ast.expr]: SLOT_ACCESS_DUNDERS = ("__getattribute__", "__getattr__", "__setattr__", "__delattr__", "__new__") +def _is_compiled_decorator(d: ast.expr) -> bool: + return (isinstance(d, ast.Name) and d.id == "compiled") or ( + isinstance(d, ast.Call) and isinstance(d.func, ast.Name) and d.func.id == "compiled" + ) + + +def _compiled_decorator_options(d: ast.expr) -> tuple[bool, str | None]: + """Parse `@compiled` or `@compiled(parallel=...)` -> (parallel, error_reason_or_None).""" + if isinstance(d, ast.Name): + return False, None + assert isinstance(d, ast.Call) + if d.args: + return False, (f"line {d.lineno}: `@compiled` takes keyword arguments only " + f"(got positional `{_src(d.args[0])}`)") + parallel = False + seen: set[str] = set() + for kw in d.keywords: + if kw.arg is None: + return False, f"line {d.lineno}: `**{_src(kw.value)}` in `@compiled(...)` is not supported" + if kw.arg != "parallel": + return False, (f"line {d.lineno}: `@compiled` keyword argument `{kw.arg}` is not " + "supported (only `parallel=True` or `parallel=False`)") + if kw.arg in seen: + return False, f"line {d.lineno}: `@compiled` keyword argument `{kw.arg}` is repeated" + seen.add(kw.arg) + if not (isinstance(kw.value, ast.Constant) and isinstance(kw.value.value, bool)): + return False, (f"line {d.lineno}: `@compiled(parallel=...)` requires a literal `True` " + f"or `False`, got `{_src(kw.value)}`") + parallel = kw.value.value + return parallel, None + + def is_opted_in(path: Path) -> bool: """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() 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) + and any(_is_compiled_decorator(d) for d in node.decorator_list) for node in ast.parse(source, filename=str(path)).body) @@ -596,15 +614,16 @@ 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]]: - found: list[tuple[ast.FunctionDef | ast.AsyncFunctionDef, str | None]] = [] +) -> list[tuple[ast.FunctionDef | ast.AsyncFunctionDef, str | None, bool]]: + found: list[tuple[ast.FunctionDef | ast.AsyncFunctionDef, str | None, bool]] = [] 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"] + marked = [d for d in node.decorator_list if _is_compiled_decorator(d)] if not marked: continue reason = None + parallel = False if marked and info.bound("compiled"): line = info.bindings["compiled"][0][0] reason = (f"line {node.lineno}: `compiled` is bound in this module (line {line}), " @@ -612,12 +631,20 @@ def _targets( elif other := [d for d in node.decorator_list if d not in marked]: reason = (f"line {other[0].lineno}: decorator `@{_src(other[0])}` is not supported " "(only `@compiled`)") - elif isinstance(node, ast.AsyncFunctionDef): - reason = f"line {node.lineno}: async function is not supported" - elif len(info.bindings.get(node.name, [])) > 1: - lines = ", ".join(str(line) for line, _ in info.bindings[node.name]) - reason = f"line {node.lineno}: `{node.name}` is bound more than once (lines {lines})" - found.append((node, reason)) + else: + for d in marked: + par, r = _compiled_decorator_options(d) + if r is not None: + reason = r + break + parallel = parallel or par + if reason is None: + if isinstance(node, ast.AsyncFunctionDef): + reason = f"line {node.lineno}: async function is not supported" + elif len(info.bindings.get(node.name, [])) > 1: + lines = ", ".join(str(line) for line, _ in info.bindings[node.name]) + reason = f"line {node.lineno}: `{node.name}` is bound more than once (lines {lines})" + found.append((node, reason, parallel)) return found diff --git a/python-multiplatform-ksp/src/main/python/typedpython/ir.py b/python-multiplatform-ksp/src/main/python/typedpython/ir.py index 368c1c3ba..1176d2182 100644 --- a/python-multiplatform-ksp/src/main/python/typedpython/ir.py +++ b/python-multiplatform-ksp/src/main/python/typedpython/ir.py @@ -817,8 +817,8 @@ class Function: # rejects a function whose declared entries differ (`verify/virtual-escape`). The front end # leaves it empty. virtual: tuple[str, ...] = () - # SPEC N-15: the user marked this function with the comment line `# typedpython: parallel` - # directly above its `def`. Only a marked function can hold a `ForRange.par`. + # SPEC N-15: the user marked this function `@compiled(parallel=True)` (#358). + # Only a marked function can hold a `ForRange.par`. parallel: bool = False # SPEC N-12b (#268): the producers whose result this pure function keeps inside one statement # (`check(make(d))`), so each such call builds its objects in a region. Set only by `verify`, diff --git a/python-multiplatform-ksp/src/main/python/typedpython_builtins.pth b/python-multiplatform-ksp/src/main/python/typedpython_builtins.pth index 98776970a..1b8b0a0ad 100644 --- a/python-multiplatform-ksp/src/main/python/typedpython_builtins.pth +++ b/python-multiplatform-ksp/src/main/python/typedpython_builtins.pth @@ -1 +1 @@ -import builtins; builtins.compiled = getattr(builtins, "compiled", lambda f, /: f) +import builtins; builtins.compiled = getattr(builtins, "compiled", lambda f=None, /, **_k: (lambda g: g) if f is None else f) diff --git a/python-multiplatform-ksp/src/test/python/conftest.py b/python-multiplatform-ksp/src/test/python/conftest.py index 85b038234..975fdc08b 100644 --- a/python-multiplatform-ksp/src/test/python/conftest.py +++ b/python-multiplatform-ksp/src/test/python/conftest.py @@ -7,11 +7,11 @@ 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) +builtins.compiled = getattr(builtins, "compiled", lambda f=None, /, **_k: (lambda g: g) if f is None else 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" +COMPILED_SHIM = "import builtins\nif not hasattr(builtins, 'compiled'):\n builtins.compiled = lambda f=None, /, **_k: (lambda g: g) if f is None else f\n" FIXTURES = Path(__file__).parent / "fixtures" diff --git a/python-multiplatform-ksp/src/test/python/test_builtin_compiled.py b/python-multiplatform-ksp/src/test/python/test_builtin_compiled.py index 4f184812b..1357c7ed7 100644 --- a/python-multiplatform-ksp/src/test/python/test_builtin_compiled.py +++ b/python-multiplatform-ksp/src/test/python/test_builtin_compiled.py @@ -21,7 +21,16 @@ def test_gate_accepts_compiled_without_an_import(write): def f(x: int) -> int: return x + 1 - y: int = f(1) + @compiled(parallel=True) + def g(x: int) -> int: + return x + 2 + + @compiled(fastmath=True) + def h(x: float) -> float: + return x * 2.0 + + y: int = f(1) + g(2) + z: float = h(1.5) """) assert errors(gate.check([path])) == [] @@ -82,7 +91,7 @@ def test_decorator_works_with_no_import(installed): def test_compiled_is_the_identity(installed): python, _ = installed - r = py(python, "-c", "def f(): pass\nprint(compiled(f) is f)") + r = py(python, "-c", "def f(): pass\nprint(compiled(f) is f and compiled(parallel=True)(f) is f and compiled()(f) is f)") assert r.stdout.strip() == "True", r.stderr diff --git a/python-multiplatform-ksp/src/test/python/test_frontend.py b/python-multiplatform-ksp/src/test/python/test_frontend.py index fe8f4d94c..ddcf3fcc9 100644 --- a/python-multiplatform-ksp/src/test/python/test_frontend.py +++ b/python-multiplatform-ksp/src/test/python/test_frontend.py @@ -654,6 +654,29 @@ def c(x: float) -> float: assert set(m.skipped) == {"c"} and "decorator" in m.skipped["c"] +def test_compiled_parallel_option_and_former_comment_marker(tmp_path): + m = lower(tmp_path, """ + @compiled(parallel=True) + def p(x: float) -> float: + return x + + @compiled(parallel=False) + def s(x: float) -> float: + return -x + + # typedpython: parallel + @compiled + def c(x: float) -> float: + return x * 2.0 + + @compiled(unknown=True) + def bad(x: float) -> float: + return x + """) + assert [(f.name, f.parallel) for f in m.functions] == [("p", True), ("s", False), ("c", False)] + assert set(m.skipped) == {"bad"} and "parallel" in m.skipped["bad"] + + def test_the_former_module_pragma_comment_compiles_nothing(tmp_path): m = lower(tmp_path, """ # typedpython: compiled 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 ecf4b8c7a..8f215ecd2 100644 --- a/python-multiplatform-ksp/src/test/python/test_parallel_loops.py +++ b/python-multiplatform-ksp/src/test/python/test_parallel_loops.py @@ -1,14 +1,14 @@ -"""TypedPython parallel order-independent loops, step 1 (SPEC N-15, #151, #277). +"""TypedPython parallel order-independent loops, step 1 (SPEC N-15, #151, #277, #358). -Step 1 scope: in a function marked by the comment line `# typedpython: parallel` directly above its -`def`, a `for i in range(len(a))` loop whose body is only stores `a[i] = ` (the same +Step 1 scope: in a function decorated with `@compiled(parallel=True)`, a `for i in range(len(a))` +loop whose body is only stores `a[i] = ` (the same index i, each proven in bounds), over list copy-in or local arrays, with values built from constants, loop-invariant scalars, `float(i)`, reads `b[i]` at the same i (proven) and F64 + - * and negation, runs on several C threads above TP_PAR_MIN_WORK. The verifier recomputes the set (`ForRange.par`) and rejects a declared one that differs (`verify/par-loop`). Everything else compiles as before. Red record: written before the implementation. Before it, `ir.ForRange` had no `par`, `ir.Function` -had no `parallel`, the marker comment was ignored and the generated module had no +had no `parallel`, the parallel opt-in was ignored and the generated module had no `__typedpython_parallel__`; every test below that touches those failed (see the report in #277). """ from __future__ import annotations @@ -35,8 +35,7 @@ SOURCE = '''\ -# typedpython: parallel -@compiled +@compiled(parallel=True) def scale(xs: list[float], a: float, b: float) -> None: for i in range(len(xs)): xs[i] = xs[i] * a + b @@ -48,30 +47,26 @@ def scale_unmarked(xs: list[float], a: float, b: float) -> None: xs[i] = xs[i] * a + b -# typedpython: parallel -@compiled +@compiled(parallel=True) def ramp(xs: list[float], a: float) -> None: for i in range(len(xs)): xs[i] = -(float(i) * a) + xs[i] -# typedpython: parallel -@compiled +@compiled(parallel=True) def two_stores(xs: list[float], ys: list[float], a: float) -> None: for i in range(len(xs)): xs[i] = xs[i] + a xs[i] = xs[i] * xs[i] -# typedpython: parallel -@compiled +@compiled(parallel=True) def neighbour(xs: list[float]) -> None: for i in range(len(xs) - 1): xs[i] = xs[i + 1] -# typedpython: parallel -@compiled +@compiled(parallel=True) def total(xs: list[float]) -> float: s: float = 0.0 for i in range(len(xs)): @@ -84,22 +79,19 @@ def helper(x: float) -> float: return x * 2.0 -# typedpython: parallel -@compiled +@compiled(parallel=True) def with_call(xs: list[float]) -> None: for i in range(len(xs)): xs[i] = helper(xs[i]) -# typedpython: parallel -@compiled +@compiled(parallel=True) def divides(xs: list[float], d: float) -> None: for i in range(len(xs)): xs[i] = xs[i] / d -# typedpython: parallel -@compiled +@compiled(parallel=True) def local_fill(a: float) -> float: t = [0.0] * 300000 for i in range(len(t)): @@ -107,8 +99,7 @@ def local_fill(a: float) -> float: return t[7] + t[299999] -# typedpython: parallel -@compiled +@compiled(parallel=True) def local_dyn(n: int, a: float) -> float: t = [1.0] * n for i in range(len(t)): @@ -250,7 +241,7 @@ def test_c_has_a_region_only_for_eligible_loops(built): def test_no_par_code_without_a_marker(tmp_path): - src = SOURCE.replace("# typedpython: parallel\n", "") + src = SOURCE.replace("@compiled(parallel=True)", "@compiled") _, c_text, _, _ = build(tmp_path, src, name="nomark") assert "tp_par_run(" not in c_text and "__typedpython_parallel__" not in c_text @@ -450,8 +441,7 @@ def sh(): RSOURCE = '''\ -# typedpython: parallel -@compiled +@compiled(parallel=True) def r_sum(xs: list[int]) -> int: s: int = 0 for i in range(len(xs)): @@ -459,8 +449,7 @@ def r_sum(xs: list[int]) -> int: return s -# typedpython: parallel -@compiled +@compiled(parallel=True) def r_sum_init(xs: list[int], s0: int) -> int: s: int = s0 for i in range(len(xs)): @@ -468,8 +457,7 @@ def r_sum_init(xs: list[int], s0: int) -> int: return s -# typedpython: parallel -@compiled +@compiled(parallel=True) def r_prod(xs: list[int], c: int) -> int: s: int = 1 for i in range(len(xs)): @@ -477,8 +465,7 @@ def r_prod(xs: list[int], c: int) -> int: return s -# typedpython: parallel -@compiled +@compiled(parallel=True) def r_sum_mul(xs: list[int], c: int) -> int: s: int = 0 for i in range(len(xs)): @@ -486,8 +473,7 @@ def r_sum_mul(xs: list[int], c: int) -> int: return s -# typedpython: parallel -@compiled +@compiled(parallel=True) def r_two(xs: list[int]) -> int: a: int = 0 b: int = 7 @@ -497,8 +483,7 @@ def r_two(xs: list[int]) -> int: return a * 3 + b -# typedpython: parallel -@compiled +@compiled(parallel=True) def r_range_sum(xs: list[int]) -> int: s: int = 5 for i in range(len(xs)): @@ -506,8 +491,7 @@ def r_range_sum(xs: list[int]) -> int: return s -# typedpython: parallel -@compiled +@compiled(parallel=True) def r_local(n: int) -> int: t = [0] * n for i in range(len(t)): @@ -526,8 +510,7 @@ def r_unmarked(xs: list[int]) -> int: return s -# typedpython: parallel -@compiled +@compiled(parallel=True) def n_acc_read(xs: list[int]) -> int: s: int = 0 for i in range(len(xs)): @@ -535,8 +518,7 @@ def n_acc_read(xs: list[int]) -> int: return s -# typedpython: parallel -@compiled +@compiled(parallel=True) def n_two_ops(xs: list[int]) -> int: s: int = 0 for i in range(len(xs)): @@ -545,8 +527,7 @@ def n_two_ops(xs: list[int]) -> int: return s -# typedpython: parallel -@compiled +@compiled(parallel=True) def n_float(xs: list[float]) -> float: s: float = 0.0 for i in range(len(xs)): @@ -554,8 +535,7 @@ def n_float(xs: list[float]) -> float: return s -# typedpython: parallel -@compiled +@compiled(parallel=True) def n_copy(xs: list[int]) -> int: s: int = 0 w: int = 0 @@ -565,8 +545,7 @@ def n_copy(xs: list[int]) -> int: return s + w -# typedpython: parallel -@compiled +@compiled(parallel=True) def n_mixed(xs: list[int]) -> int: t = [0.0] * len(xs) s: int = 0 @@ -576,8 +555,7 @@ def n_mixed(xs: list[int]) -> int: return s -# typedpython: parallel -@compiled +@compiled(parallel=True) def n_div(xs: list[int], d: int) -> int: s: int = 0 for i in range(len(xs)): @@ -585,8 +563,7 @@ def n_div(xs: list[int], d: int) -> int: return s -# typedpython: parallel -@compiled +@compiled(parallel=True) def n_shift_step(xs: list[int]) -> int: s: int = 0 for i in range(0, len(xs), 2): @@ -864,7 +841,7 @@ def test_reduction_results_identical_for_every_thread_count(rbuilt): def test_no_reduction_region_without_a_marker(tmp_path): - src = RSOURCE.replace("# typedpython: parallel\n", "") + src = RSOURCE.replace("@compiled(parallel=True)", "@compiled") _, c_text, _, _ = build(tmp_path, src, name="rnomark") assert "tp_par_run(" not in c_text and "__typedpython_parallel__" not in c_text @@ -885,8 +862,7 @@ def test_no_reduction_region_without_a_marker(tmp_path): MSOURCE = '''\ -# typedpython: parallel -@compiled +@compiled(parallel=True) def mn_i(xs: list[int]) -> int: m = xs[0] for i in range(len(xs)): @@ -895,8 +871,7 @@ def mn_i(xs: list[int]) -> int: return m -# typedpython: parallel -@compiled +@compiled(parallel=True) def mx_i(xs: list[int]) -> int: m = xs[0] for i in range(len(xs)): @@ -905,8 +880,7 @@ def mx_i(xs: list[int]) -> int: return m -# typedpython: parallel -@compiled +@compiled(parallel=True) def mn_f(xs: list[float]) -> float: m = xs[0] for i in range(len(xs)): @@ -915,8 +889,7 @@ def mn_f(xs: list[float]) -> float: return m -# typedpython: parallel -@compiled +@compiled(parallel=True) def mx_f(xs: list[float]) -> float: m = xs[0] for i in range(len(xs)): @@ -925,8 +898,7 @@ def mx_f(xs: list[float]) -> float: return m -# typedpython: parallel -@compiled +@compiled(parallel=True) def mn_f_flipped(xs: list[float], init: float) -> float: m = init for i in range(len(xs)): @@ -935,8 +907,7 @@ def mn_f_flipped(xs: list[float], init: float) -> float: return m -# typedpython: parallel -@compiled +@compiled(parallel=True) def mx_f_flipped(xs: list[float], init: float) -> float: m = init for i in range(len(xs)): @@ -945,8 +916,7 @@ def mx_f_flipped(xs: list[float], init: float) -> float: return m -# typedpython: parallel -@compiled +@compiled(parallel=True) def mn_i_expr(xs: list[int], c: int) -> int: m = c for i in range(len(xs)): @@ -955,8 +925,7 @@ def mn_i_expr(xs: list[int], c: int) -> int: return m -# typedpython: parallel -@compiled +@compiled(parallel=True) def mn_f_expr(xs: list[float], a: float) -> float: m = a for i in range(len(xs)): @@ -965,8 +934,7 @@ def mn_f_expr(xs: list[float], a: float) -> float: return m -# typedpython: parallel -@compiled +@compiled(parallel=True) def mn_and_sum(xs: list[int]) -> int: m = xs[0] s = 0 @@ -977,8 +945,7 @@ def mn_and_sum(xs: list[int]) -> int: return m * 1000003 + s -# typedpython: parallel -@compiled +@compiled(parallel=True) def two_ext(xs: list[float]) -> float: lo = xs[0] hi = xs[0] @@ -990,8 +957,7 @@ def two_ext(xs: list[float]) -> float: return hi - lo -# typedpython: parallel -@compiled +@compiled(parallel=True) def mn_i_init(xs: list[int], init: int) -> int: m = init for i in range(len(xs)): @@ -1000,8 +966,7 @@ def mn_i_init(xs: list[int], init: int) -> int: return m -# typedpython: parallel -@compiled +@compiled(parallel=True) def mn_i_big_e(xs: list[int], c: int) -> int: m = 0 for i in range(len(xs)): @@ -1019,8 +984,7 @@ def mn_unmarked(xs: list[int]) -> int: return m -# typedpython: parallel -@compiled +@compiled(parallel=True) def n_le(xs: list[float]) -> float: m = xs[0] for i in range(len(xs)): @@ -1029,8 +993,7 @@ def n_le(xs: list[float]) -> float: return m -# typedpython: parallel -@compiled +@compiled(parallel=True) def n_other_value(xs: list[int]) -> int: m = xs[0] for i in range(len(xs)): @@ -1039,8 +1002,7 @@ def n_other_value(xs: list[int]) -> int: return m -# typedpython: parallel -@compiled +@compiled(parallel=True) def n_else(xs: list[int]) -> int: m = xs[0] s = 0 @@ -1052,8 +1014,7 @@ def n_else(xs: list[int]) -> int: return m + s -# typedpython: parallel -@compiled +@compiled(parallel=True) def n_two_in_then(xs: list[int]) -> int: m = xs[0] s = 0 @@ -1064,8 +1025,7 @@ def n_two_in_then(xs: list[int]) -> int: return m + s -# typedpython: parallel -@compiled +@compiled(parallel=True) def n_acc_in_value(xs: list[int]) -> int: m = xs[0] for i in range(len(xs)): @@ -1075,8 +1035,7 @@ def n_acc_in_value(xs: list[int]) -> int: return m -# typedpython: parallel -@compiled +@compiled(parallel=True) def n_cond_other(xs: list[int], t: int) -> int: m = xs[0] for i in range(len(xs)): @@ -1085,8 +1044,7 @@ def n_cond_other(xs: list[int], t: int) -> int: return m -# typedpython: parallel -@compiled +@compiled(parallel=True) def n_mixed_types(xs: list[int], init: float) -> float: m = init for i in range(len(xs)): @@ -1095,8 +1053,7 @@ def n_mixed_types(xs: list[int], init: float) -> float: return m -# typedpython: parallel -@compiled +@compiled(parallel=True) def n_acc_reads_acc(xs: list[int]) -> int: m = xs[0] for i in range(len(xs)): @@ -1107,8 +1064,7 @@ def n_acc_reads_acc(xs: list[int]) -> int: return m -# typedpython: parallel -@compiled +@compiled(parallel=True) def n_div(xs: list[float], d: float) -> float: m = xs[0] for i in range(len(xs)): @@ -1537,8 +1493,7 @@ def spin(): BSOURCE = '''\ -# typedpython: parallel -@compiled +@compiled(parallel=True) def b_and(xs: list[int]) -> int: s: int = -1 for i in range(len(xs)): @@ -1546,8 +1501,7 @@ def b_and(xs: list[int]) -> int: return s -# typedpython: parallel -@compiled +@compiled(parallel=True) def b_or(xs: list[int]) -> int: s: int = 0 for i in range(len(xs)): @@ -1555,8 +1509,7 @@ def b_or(xs: list[int]) -> int: return s -# typedpython: parallel -@compiled +@compiled(parallel=True) def b_xor(xs: list[int]) -> int: s: int = 0 for i in range(len(xs)): @@ -1564,8 +1517,7 @@ def b_xor(xs: list[int]) -> int: return s -# typedpython: parallel -@compiled +@compiled(parallel=True) def b_and_swapped(xs: list[int], s0: int) -> int: s: int = s0 for i in range(len(xs)): @@ -1573,8 +1525,7 @@ def b_and_swapped(xs: list[int], s0: int) -> int: return s -# typedpython: parallel -@compiled +@compiled(parallel=True) def b_or_swapped(xs: list[int]) -> int: s: int = 0 for i in range(len(xs)): @@ -1582,8 +1533,7 @@ def b_or_swapped(xs: list[int]) -> int: return s -# typedpython: parallel -@compiled +@compiled(parallel=True) def b_xor_expr(xs: list[int], c: int) -> int: s: int = 0 for i in range(len(xs)): @@ -1591,8 +1541,7 @@ def b_xor_expr(xs: list[int], c: int) -> int: return s -# typedpython: parallel -@compiled +@compiled(parallel=True) def b_or_not(xs: list[int]) -> int: s: int = 0 for i in range(len(xs)): @@ -1600,8 +1549,7 @@ def b_or_not(xs: list[int]) -> int: return s -# typedpython: parallel -@compiled +@compiled(parallel=True) def b_mix(xs: list[int]) -> int: a: int = 0 x: int = 5 @@ -1616,8 +1564,7 @@ def b_mix(xs: list[int]) -> int: return a + x * 3 + o * 5 + m * 7 -# typedpython: parallel -@compiled +@compiled(parallel=True) def b_local(n: int) -> int: t = [0] * n for i in range(len(t)): @@ -1636,8 +1583,7 @@ def b_unmarked(xs: list[int]) -> int: return s -# typedpython: parallel -@compiled +@compiled(parallel=True) def nb_acc_read(xs: list[int]) -> int: s: int = 0 for i in range(len(xs)): @@ -1645,8 +1591,7 @@ def nb_acc_read(xs: list[int]) -> int: return s -# typedpython: parallel -@compiled +@compiled(parallel=True) def nb_two_ops(xs: list[int]) -> int: s: int = 0 for i in range(len(xs)): @@ -1655,8 +1600,7 @@ def nb_two_ops(xs: list[int]) -> int: return s -# typedpython: parallel -@compiled +@compiled(parallel=True) def nb_and_then_add(xs: list[int]) -> int: s: int = 0 for i in range(len(xs)): @@ -1665,8 +1609,7 @@ def nb_and_then_add(xs: list[int]) -> int: return s -# typedpython: parallel -@compiled +@compiled(parallel=True) def nb_shift(xs: list[int]) -> int: s: int = 0 for i in range(len(xs)): @@ -1674,8 +1617,7 @@ def nb_shift(xs: list[int]) -> int: return s -# typedpython: parallel -@compiled +@compiled(parallel=True) def nb_rshift(xs: list[int], k: int) -> int: s: int = 0 for i in range(len(xs)): @@ -1683,8 +1625,7 @@ def nb_rshift(xs: list[int], k: int) -> int: return s -# typedpython: parallel -@compiled +@compiled(parallel=True) def nb_copy(xs: list[int]) -> int: s: int = 0 w: int = 0 @@ -1694,8 +1635,7 @@ def nb_copy(xs: list[int]) -> int: return s + w -# typedpython: parallel -@compiled +@compiled(parallel=True) def nb_div(xs: list[int], d: int) -> int: s: int = 0 for i in range(len(xs)): @@ -1857,15 +1797,13 @@ def test_bitwise_equals_interpreter_for_every_thread_count(bbuilt, threads): TSOURCE = '''\ -# typedpython: parallel -@compiled +@compiled(parallel=True) 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 +@compiled(parallel=True) def t_sum(xs: list[int]) -> int: s: int = 0 for i in range(len(xs)): @@ -1873,8 +1811,7 @@ def t_sum(xs: list[int]) -> int: return s -# typedpython: parallel -@compiled +@compiled(parallel=True) def t_prod(xs: list[int], c: int) -> int: s: int = 1 for i in range(len(xs)): @@ -1882,8 +1819,7 @@ def t_prod(xs: list[int], c: int) -> int: return s -# typedpython: parallel -@compiled +@compiled(parallel=True) def t_min(xs: list[int]) -> int: m = xs[0] for i in range(len(xs)): @@ -1892,8 +1828,7 @@ def t_min(xs: list[int]) -> int: return m -# typedpython: parallel -@compiled +@compiled(parallel=True) def t_max(xs: list[float]) -> float: m = xs[0] for i in range(len(xs)): @@ -1902,8 +1837,7 @@ def t_max(xs: list[float]) -> float: return m -# typedpython: parallel -@compiled +@compiled(parallel=True) def t_and(xs: list[int]) -> int: s: int = -1 for i in range(len(xs)): @@ -1911,8 +1845,7 @@ def t_and(xs: list[int]) -> int: return s -# typedpython: parallel -@compiled +@compiled(parallel=True) def t_or(xs: list[int]) -> int: s: int = 0 for i in range(len(xs)): @@ -1920,8 +1853,7 @@ def t_or(xs: list[int]) -> int: return s -# typedpython: parallel -@compiled +@compiled(parallel=True) def t_xor(xs: list[int]) -> int: s: int = 0 for i in range(len(xs)): diff --git a/python-multiplatform/src/commonMain/kotlin/python/multiplatform/ffi/Python3.kt b/python-multiplatform/src/commonMain/kotlin/python/multiplatform/ffi/Python3.kt index 14d153326..5060b9469 100644 --- a/python-multiplatform/src/commonMain/kotlin/python/multiplatform/ffi/Python3.kt +++ b/python-multiplatform/src/commonMain/kotlin/python/multiplatform/ffi/Python3.kt @@ -124,7 +124,9 @@ object Python3 { def _pm_install_compiled(): import builtins if not hasattr(builtins, "compiled"): - def compiled(f): + def compiled(f=None, /, **_opts): + if f is None: + return lambda g: g return f builtins.compiled = compiled _pm_install_compiled() diff --git a/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/BuiltinCompiledTest.kt b/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/BuiltinCompiledTest.kt index 5d3ef407f..e7765283f 100644 --- a/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/BuiltinCompiledTest.kt +++ b/python-multiplatform/src/commonTest/kotlin/python/multiplatform/ffi/BuiltinCompiledTest.kt @@ -11,7 +11,9 @@ class BuiltinCompiledTest { @Test fun compiledResolvesWithoutImportAndIsIdentity() = PythonTestFixture.withInterpreter { assertEquals("True", evalStr("compiled(len) is len")) - Python3.exec("def _bc_f(): return 1\n_bc_r = compiled(_bc_f) is _bc_f") + assertEquals("True", evalStr("compiled(parallel=True)(len) is len")) + assertEquals("True", evalStr("compiled()(len) is len")) + Python3.exec("def _bc_f(): return 1\n_bc_r = compiled(_bc_f) is _bc_f and compiled(parallel=True)(_bc_f) is _bc_f") assertEquals("True", evalStr("_bc_r")) // It must live in builtins, not leak into __main__. assertEquals("False", evalStr("'compiled' in globals()"))