Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
9 changes: 9 additions & 0 deletions docs/SPEC.md
Original file line number Diff line number Diff line change
Expand Up @@ -748,6 +748,15 @@ All in `PM/commonTest`, so they run wherever the interpreter loads.
caller without fastmath): **this is the one path in TypedPython that is explicitly NOT
CPython-identical** (a sum or fused multiply-add in a `@compiled(fastmath=True)` function may round
differently from the interpreter so float reductions and FMA loops can vectorise).
No compiler goes beyond reassociation and FMA contraction (#368). Clang uses
`#pragma clang fp reassociate(on) contract(fast)`, GCC
`optimize("associative-math,no-signed-zeros,no-trapping-math,fp-contract=fast")` (the two extra
options are what associative-math needs to take effect), MSVC `fp_contract(on)` only: cl.exe has no
reassociation-only switch, and `float_control(precise, off)` (`/fp:fast`) also folds `x != x`
away, so on MSVC a fastmath function contracts but does not reassociate. None of them assumes finite
values, so a NaN check (`x != x`, `isnan`) in a fastmath function still works; GCC's `fast-math` is
not used (`test_pipeline.py`: the macro test, the NaN-check test on the host compiler of every CI OS
including MSVC on Windows, and the same under a real GCC on Linux CI).
`Status: implemented` for desktop host builds
(`test_pipeline.py`, `test_cgen*.py`, `test_recursion.py`, `test_eval_breaker.py`, `test_fast_entry.py`,
`test_free_threaded.py`; free-threaded 3.15t, #159). Not yet: shipping compiled modules in an app through
Expand Down
12 changes: 9 additions & 3 deletions python-multiplatform-ksp/src/main/python/typedpython/cgen.py
Original file line number Diff line number Diff line change
Expand Up @@ -758,11 +758,17 @@ def generate(module: ir.Module, source_path: Path, display_path: str | None = No
w("#define TP_FASTMATH_BEGIN _Pragma(\"clang fp reassociate(on) contract(fast)\")")
w("#define TP_FASTMATH_END _Pragma(\"clang fp reassociate(off) contract(off)\")")
w("#elif defined(__GNUC__)")
w("#define TP_FASTMATH_BEGIN _Pragma(\"GCC push_options\") _Pragma(\"GCC optimize(\\\"fast-math\\\")\")")
# The same scope as clang (#368): reassociation and FMA contraction only.
# associative-math takes effect only with no-signed-zeros and no-trapping-math; never
# fast-math, whose finite-math-only folds `x != x` and isnan away.
w("#define TP_FASTMATH_BEGIN _Pragma(\"GCC push_options\") _Pragma(\"GCC optimize("
"\\\"associative-math,no-signed-zeros,no-trapping-math,fp-contract=fast\\\")\")")
w("#define TP_FASTMATH_END _Pragma(\"GCC pop_options\")")
w("#elif defined(_MSC_VER)")
w("#define TP_FASTMATH_BEGIN __pragma(float_control(precise, off, push)) __pragma(fp_contract(on))")
w("#define TP_FASTMATH_END __pragma(fp_contract(off)) __pragma(float_control(pop))")
# cl.exe has no reassociation-only switch: float_control(precise, off) is /fp:fast, which also
# folds `x != x` away (Windows CI, #368). So MSVC gets FMA contraction only.
w("#define TP_FASTMATH_BEGIN __pragma(fp_contract(on))")
w("#define TP_FASTMATH_END __pragma(fp_contract(off))")
w("#else")
w("#define TP_FASTMATH_BEGIN")
w("#define TP_FASTMATH_END")
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -498,7 +498,12 @@ def worker():
except Exception as e: # noqa: BLE001
seen[name][1].append(repr(e))
else:
seen[name][1].append("succeeded")
# The call can return between the `done` check above and this probe;
# the buffer is then legitimately free. A probe that succeeds while the
# buffer is really unheld does so ~0.05 s into a ~0.6 s call, so `done`
# stays unset for far longer than this grace period.
if not done.wait(0.05):
seen[name][1].append("succeeded")
return
time.sleep(0.001)

Expand Down
97 changes: 97 additions & 0 deletions python-multiplatform-ksp/src/test/python/test_pipeline.py
Original file line number Diff line number Diff line change
Expand Up @@ -238,3 +238,100 @@ def calls_fast_and_adds(a: float, b: float, c: float) -> float:
got_fast = mod.fma_fast(a, a, c)
assert isinstance(got_fast, float)



_NAN_SRC = """
@compiled(fastmath=True)
def nan_count(xs: list[float]) -> int:
n = 0
for i in range(len(xs)):
x = xs[i]
if x != x:
n += 1
return n

@compiled(fastmath=True)
def is_nan(x: float) -> bool:
return x != x
"""


def _real_gcc():
"""A GCC driver that is really GCC (macOS `gcc` is clang), or None."""
import shutil
import subprocess
for name in ("gcc", "gcc-15", "gcc-14", "gcc-13"):
exe = shutil.which(name)
if exe:
out = subprocess.run([exe, "--version"], capture_output=True, text=True).stdout
if "clang" not in out.lower():
return exe
return None


def test_gcc_fastmath_scope_is_reassociation_and_contract_only(write):
"""#368: GCC's fastmath macro allows the same scope as clang and MSVC, reassociation and FMA
contraction (plus the no-signed-zeros / no-trapping-math that associative-math needs); never
`fast-math`, whose finite-math-only folds `x != x` to false."""
from typedpython import cgen, frontend, verify

path = write("fm_scope.py", _NAN_SRC)
proved, _ = verify.verify(frontend.lower(path))
c_text = cgen.generate(proved, path)
gcc_line = next(l for l in c_text.splitlines() if "define TP_FASTMATH_BEGIN" in l and "GCC" in l)
assert "fast-math" not in gcc_line
for opt in ("associative-math", "no-signed-zeros", "no-trapping-math", "fp-contract=fast"):
assert opt in gcc_line, (opt, gcc_line)
assert "finite-math" not in gcc_line


def test_fastmath_keeps_nan_checks(write, tmp_path):
"""#368: `x != x` inside a fastmath function still detects NaN (host compiler)."""
path = write("fm_nan.py", _NAN_SRC)
res = pipeline.compile_module(path, tmp_path / "out", cost_rule=False)
assert {f.name for f in res.module.functions} == {"nan_count", "is_nan"}, res.skipped
mod = pipeline.load(res)
nan = float("nan")
assert mod.is_nan(nan) is True and mod.is_nan(1.0) is False
assert mod.nan_count([1.0, nan, 2.0, nan]) == 2


def test_fastmath_keeps_nan_checks_under_gcc(write, tmp_path, monkeypatch):
"""#368: the same under a real GCC (Linux CI); skipped where no GCC is installed."""
import sys
from typedpython import cbuild
gcc = _real_gcc()
if gcc is None or sys.platform == "win32":
pytest.skip("no real GCC on this host")
monkeypatch.setattr(cbuild, "compiler", lambda: [gcc])
path = write("fm_nan_gcc.py", _NAN_SRC)
# GCC only warns on an optimize option it does not know; make that an error so a dropped
# option cannot pass silently.
from typedpython import cgen, frontend, verify
proved, _ = verify.verify(frontend.lower(path))
c_file = tmp_path / "fm_nan_gcc.c"
c_file.write_text(cgen.generate(proved, path))
chk = subprocess.run([gcc, "-fsyntax-only", "-Werror=pragmas", "-Werror=attributes",
f"-I{cbuild._python_include(None)}", f"-I{cbuild.RUNTIME_DIR}", str(c_file)],
capture_output=True, text=True)
assert chk.returncode == 0, chk.stderr
res = pipeline.compile_module(path, tmp_path / "out", cost_rule=False)
assert {f.name for f in res.module.functions} == {"nan_count", "is_nan"}, res.skipped
mod = pipeline.load(res)
nan = float("nan")
assert mod.is_nan(nan) is True
assert mod.nan_count([1.0, nan, 2.0, nan]) == 2


def test_msvc_fastmath_scope_is_contract_only(write):
"""#368: cl.exe has no reassociation-only switch; `float_control(precise, off)` is /fp:fast and
folds `x != x` (seen on Windows CI). MSVC's fastmath macro enables FMA contraction only."""
from typedpython import cgen, frontend, verify

path = write("fm_msvc.py", _NAN_SRC)
proved, _ = verify.verify(frontend.lower(path))
lines = cgen.generate(proved, path).splitlines()
i = lines.index("#elif defined(_MSC_VER)", next(k for k, l in enumerate(lines) if "TP_FASTMATH_BEGIN" in l))
begin = next(l for l in lines[i:] if "define TP_FASTMATH_BEGIN" in l)
assert "fp_contract(on)" in begin, begin
assert "float_control" not in begin and "precise" not in begin, begin
Loading