[Lang] Make qd.static not bypass the pure-kernel purity check#733
Draft
hughperkins wants to merge 6 commits into
Draft
[Lang] Make qd.static not bypass the pure-kernel purity check#733hughperkins wants to merge 6 commits into
hughperkins wants to merge 6 commits into
GitHub Actions / Coverage Report
succeeded
Jun 29, 2026 in 0s
Diff Coverage Report
See details below for per-line coverage annotations.
Details
Coverage Report (9275a5ca9)
| Metric | Value |
|---|---|
| Diff coverage (changed lines only) | 91% |
| Overall project coverage | 68% |
Total: 57 lines, 5 missing, 91% covered
🟢 python/quadrants/lang/ast/ast_transformer.py (100%)
100 # ``qd.static`` is intentionally NOT a purity escape hatch: a captured module global is still flagged inside
101 # a static scope, since its value never enters the fastcache key regardless of static wrapping.
🟢 102 if ctx.is_pure and node.violates_pure:
103 # ``str`` is included alongside the numeric/``Field`` types: a captured string only affects a kernel through
104 # compile-time ``qd.static`` branches, and its value never enters the fastcache key, so it is cache-unsafe
105 # in exactly the same way as a captured int/float.
🟢 106 if isinstance(node.ptr, (float, int, str, Field)):
109 # Transition period: violations inside a ``qd.static`` scope only warn instead of raising, giving
110 # downstream code time to migrate such constants to kernel params. ``UPPERCASE`` names also warn.
🟢 111 if node.id.upper() == node.id or ctx.is_in_static_scope():
792 # ``qd.static`` is intentionally NOT a purity escape hatch (see ``build_Name``).
🟢 793 if ctx.is_pure and node.violates_pure:
794 # ``str`` included for the same reason as in ``build_Name``: a captured string is cache-unsafe.
🟢 795 if isinstance(node.ptr, (int, float, str, Field)):
805 # Transition period (see ``build_Name``): ``qd.static`` scope downgrades this to a warning.
🟢 806 if node.attr.upper() == node.attr or ctx.is_in_static_scope():
🟢 tests/python/quadrants/lang/fast_caching/test_pure_validation.py (97%)
🟢 45 @test_utils.test()
🟢 46 def test_pure_validation_str():
47 # A captured ``str`` global is cache-unsafe in the same way as a captured int/float, so it must trigger a purity
48 # violation. Direct access (not wrapped in ``qd.static``) of a lowercase-named global raises.
🟢 49 s = "hello"
50
🟢 51 @qd.kernel(pure=True)
🟢 52 def k1():
🔴 53 print(s)
54
🟢 55 with pytest.raises(qd.QuadrantsCompilationError):
🟢 56 k1()
57
58
299
300
301 # Restricted to a single (CPU) arch on purpose: the purity check is a Python-side AST analysis and is entirely
302 # arch-independent, and running it across multiple archs in one worker lets a fastcache hit from one arch suppress the
303 # warning on the next, which makes ``pytest.warns`` flaky.
🟢 304 @test_utils.test(arch=qd.cpu)
🟢 305 def test_pure_validation_static_scope_warns():
306 # Transition period: a captured global accessed inside a ``qd.static`` scope of a pure kernel only warns instead of
307 # raising, to give downstream code time to migrate such constants to kernel parameters.
🟢 308 assert qd.lang is not None
🟢 309 arch = qd.lang.impl.current_cfg().arch
🟢 310 qd.init(arch=arch, offline_cache=False)
311
🟢 312 use_alias = True
313
🟢 314 @qd.kernel(pure=True)
🟢 315 def k1() -> qd.i32:
🟢 316 ret = 0
🟢 317 if qd.static(use_alias):
🟢 318 ret = 1
🟢 319 return ret
320
🟢 321 with pytest.warns(UserWarning, match=r"\[PURE\.VIOLATION\]"):
🟢 322 assert k1() == 1
323
🟢 324 class Cfg:
🟢 325 def __init__(self) -> None:
🟢 326 self.flag = True
327
🟢 328 cfg = Cfg()
329
🟢 330 @qd.kernel(pure=True)
🟢 331 def k2() -> qd.i32:
🟢 332 ret = 0
🟢 333 if qd.static(cfg.flag):
🟢 334 ret = 1
🟢 335 return ret
336
🟢 337 with pytest.warns(UserWarning, match=r"\[PURE\.VIOLATION\]"):
🟢 338 assert k2() == 1
🔴 tests/python/quadrants/lang/fast_caching/test_src_ll_cache.py (25%)
🔴 297 def k1(a: qd.i32, output: qd.types.NDArray[qd.i32, 1], return_something: qd.Template) -> bool:
🟢 299 if qd.static(return_something):
🔴 304 assert k1(3, output, args_obj.return_something)
🔴 317 k1(3, output, args_obj.return_something)
🟢 tests/python/test_tile.py (92%)
🟢 91 def k1(dst_arr: Ann, N: qd.Template, use_alias: qd.Template):
🔴 95 if qd.static(use_alias):
🟢 102 k1(dst, tdim, use_zeros_alias)
🟢 120 def k1(src_arr: Ann, dst_arr: Ann, N: qd.Template, inplace: qd.Template):
🟢 135 k1(src, dst, tdim, inplace)
🟢 910 def k1(s: qd.types.NDArray[qd.f32, 2], d: qd.types.NDArray[qd.f32, 2], N: qd.Template, bad_slice: qd.Template):
🟢 926 k1(src, dst, tdim, bad_slice)
🟢 945 def k1(s: qd.types.NDArray[qd.f32, 2], d: qd.types.NDArray[qd.f32, 2], N: qd.Template, bad_slice: qd.Template):
🟢 961 k1(src, dst, tdim, bad_slice)
🟢 1107 def k1(s: qd.types.NDArray[qd.f32, 2], d: qd.types.NDArray[qd.f32, 2], N: qd.Template, bad_slice: qd.Template):
🟢 1120 k1(src, dst, tdim, bad_slice)
🟢 1331 def k1(
1332 src_f: qd.Template,
1333 dst_f: qd.Template,
1334 NCOLS: qd.i32,
1335 N: qd.Template,
1336 partial_store: qd.Template,
1337 partial_load: qd.Template,
1338 ):
🟢 1366 k1(src, dst, NCOLS, tdim, partial_store, partial_load)
Loading