From 986c736a5003c78024a5e8013b9c817b612b740b Mon Sep 17 00:00:00 2001 From: BREW <81044362+b-re-w@users.noreply.github.com> Date: Thu, 5 Nov 2026 11:38:31 +0900 Subject: [PATCH 1/3] TypedPython: narrow GCC fastmath to reassociation and FMA contract (#368) --- docs/SPEC.md | 6 ++ .../src/main/python/typedpython/cgen.py | 6 +- .../src/test/python/test_pipeline.py | 83 +++++++++++++++++++ 3 files changed, 94 insertions(+), 1 deletion(-) diff --git a/docs/SPEC.md b/docs/SPEC.md index 2e65b6f4..14c5ece6 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -748,6 +748,12 @@ 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). + The three compilers allow the same scope (#368): reassociation and FMA contraction only. Clang uses + `#pragma clang fp reassociate(on) contract(fast)`, MSVC `float_control(precise, off)` with + `fp_contract(on)`, 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). 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, and the NaN-check test run 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 diff --git a/python-multiplatform-ksp/src/main/python/typedpython/cgen.py b/python-multiplatform-ksp/src/main/python/typedpython/cgen.py index 940837ed..29548418 100644 --- a/python-multiplatform-ksp/src/main/python/typedpython/cgen.py +++ b/python-multiplatform-ksp/src/main/python/typedpython/cgen.py @@ -758,7 +758,11 @@ 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 and MSVC (#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))") diff --git a/python-multiplatform-ksp/src/test/python/test_pipeline.py b/python-multiplatform-ksp/src/test/python/test_pipeline.py index 18355aec..dcfb7d6d 100644 --- a/python-multiplatform-ksp/src/test/python/test_pipeline.py +++ b/python-multiplatform-ksp/src/test/python/test_pipeline.py @@ -238,3 +238,86 @@ 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 From 5d93fe927e21ce3648a5ecc3954542d0e83f4890 Mon Sep 17 00:00:00 2001 From: BREW <81044362+b-re-w@users.noreply.github.com> Date: Thu, 5 Nov 2026 21:21:07 +0900 Subject: [PATCH 2/3] TypedPython test: ignore a probe that succeeds only after the call returned The macOS run of develop c50b53d failed in test_other_thread_release_and_append_get_bufferror_during_a_memoryview_call with 113 BufferErrors followed by one success: the compiled call returned between the worker's done check and its next probe. A success now counts only when the call keeps running for 0.05 s after it. --- .../src/test/python/test_buffer_memoryview.py | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) 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 8a616abc..e622536b 100644 --- a/python-multiplatform-ksp/src/test/python/test_buffer_memoryview.py +++ b/python-multiplatform-ksp/src/test/python/test_buffer_memoryview.py @@ -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) From 6f7b669ea28e902ccd0156fa9db3c46650de3663 Mon Sep 17 00:00:00 2001 From: BREW <81044362+b-re-w@users.noreply.github.com> Date: Thu, 5 Nov 2026 21:44:19 +0900 Subject: [PATCH 3/3] TypedPython: MSVC fastmath enables FMA contraction only (#368) Windows CI failed test_fastmath_keeps_nan_checks: float_control(precise, off) is /fp:fast, which folds x != x. cl.exe has no reassociation-only switch, so on MSVC a fastmath function now contracts but does not reassociate. SPEC N-8 says so; a macro test pins it. --- docs/SPEC.md | 13 ++++++++----- .../src/main/python/typedpython/cgen.py | 8 +++++--- .../src/test/python/test_pipeline.py | 14 ++++++++++++++ 3 files changed, 27 insertions(+), 8 deletions(-) diff --git a/docs/SPEC.md b/docs/SPEC.md index 14c5ece6..e8ebb203 100644 --- a/docs/SPEC.md +++ b/docs/SPEC.md @@ -748,12 +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). - The three compilers allow the same scope (#368): reassociation and FMA contraction only. Clang uses - `#pragma clang fp reassociate(on) contract(fast)`, MSVC `float_control(precise, off)` with - `fp_contract(on)`, 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). None of them assumes finite + 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, and the NaN-check test run under a real GCC on Linux CI). + 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 diff --git a/python-multiplatform-ksp/src/main/python/typedpython/cgen.py b/python-multiplatform-ksp/src/main/python/typedpython/cgen.py index 29548418..fe1635e7 100644 --- a/python-multiplatform-ksp/src/main/python/typedpython/cgen.py +++ b/python-multiplatform-ksp/src/main/python/typedpython/cgen.py @@ -758,15 +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__)") - # The same scope as clang and MSVC (#368): reassociation and FMA contraction only. + # 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") diff --git a/python-multiplatform-ksp/src/test/python/test_pipeline.py b/python-multiplatform-ksp/src/test/python/test_pipeline.py index dcfb7d6d..a50a4bfe 100644 --- a/python-multiplatform-ksp/src/test/python/test_pipeline.py +++ b/python-multiplatform-ksp/src/test/python/test_pipeline.py @@ -321,3 +321,17 @@ def test_fastmath_keeps_nan_checks_under_gcc(write, tmp_path, monkeypatch): 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