Skip to content

[Lang] Make qd.static not bypass the pure-kernel purity check#733

Draft
hughperkins wants to merge 6 commits into
mainfrom
hp/pure-static-no-escape
Draft

[Lang] Make qd.static not bypass the pure-kernel purity check#733
hughperkins wants to merge 6 commits into
mainfrom
hp/pure-static-no-escape

Merge branch 'main' into hp/pure-static-no-escape

9275a5c
Select commit
Loading
Failed to load commit list.
Sign in for the full log view
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)