#!/usr/bin/env python3 """pass-span-lint — one `pass_span` per render pass at a time (decision 2258, bug B390). bevy's `DiagnosticsRecorder::pass_span` is a timestamp pair PLUS a pipeline-statistics query, and wgpu allows exactly one statistics query active at a time: a second `pass_span` opened on a pass whose own span is still open is a validation error — on the backends that expose `PIPELINE_STATISTICS_QUERY`, which in wgpu 27 is Vulkan alone (Metal never had it; wgpu-hal's DX12 arm has the line commented out). So the gates on this machine (Metal) and the lab laptop (pinned to DX12) run the nesting as a silent no-op, and the 09-15 sync shipped one: every Linux player, and every Windows player wgpu had put on Vulkan, aborted on the first world frame (B390). No compile, no test and no macOS run can see that shape. This check can, in a tenth of a second. The shape it refuses, per function: `let g = .pass_span(&mut P, …)` followed by another `.pass_span(&mut P, …)` before `g.end(&mut P)`. Timestamp-only spans (`time_span`) nest fine and are not looked at. Lexical, per file, reset at every `fn` — a span never outlives its function. Usage: scripts/pass-span-lint.py [path …] (default: every crates/**/*.rs) Exit: 0 clean, 1 findings. """ from __future__ import annotations import pathlib import re import sys OPEN = re.compile(r"\blet\s+(?:mut\s+)?(\w+)\s*=\s*[\w.]+\.pass_span\(\s*&mut\s+(\w+)\s*,") ANY_OPEN = re.compile(r"\.pass_span\(\s*&mut\s+(\w+)\s*,") END = re.compile(r"\b(\w+)\.end\(\s*&mut\s+(\w+)\s*\)") FN = re.compile(r"^\s*(?:pub(?:\([^)]*\))?\s+)?(?:async\s+)?(?:unsafe\s+)?fn\s+\w+") def lint(path: pathlib.Path) -> list[str]: findings: list[str] = [] open_spans: list[tuple[str, str, int]] = [] # (guard, pass, line) for lineno, raw in enumerate(path.read_text(encoding="utf-8").splitlines(), 1): line = raw.split("//", 1)[0] if FN.match(line): open_spans.clear() m = OPEN.search(line) if m: guard, pass_name = m.group(1), m.group(2) else: m = ANY_OPEN.search(line) guard, pass_name = (None, m.group(1)) if m else (None, None) if pass_name is not None: for outer_guard, outer_pass, outer_line in open_spans: if outer_pass == pass_name: findings.append( f"{path}:{lineno}: a second pass_span on `{pass_name}` while " f"`{outer_guard}` (line {outer_line}) is still open — one " f"pipeline-statistics query at a time (2258, B390)" ) if guard is not None: open_spans.append((guard, pass_name, lineno)) e = END.search(line) if e: open_spans = [s for s in open_spans if s[0] != e.group(1)] return findings def main() -> int: args = [pathlib.Path(a) for a in sys.argv[1:]] or [pathlib.Path("crates")] files: list[pathlib.Path] = [] for a in args: files.extend(sorted(a.rglob("*.rs")) if a.is_dir() else [a]) findings = [f for p in files for f in lint(p)] for f in findings: print(f) print(f"pass-span-lint: {'clean' if not findings else f'{len(findings)} finding(s)'} — " f"{len(files)} file(s)") return 1 if findings else 0 if __name__ == "__main__": sys.exit(main())