Skip to content

Commit 49d43f7

Browse files
r41k0uclaude
andcommitted
Tests: Pin which storage each global-variable access resolves to
Levels 1 and 2 only prove the globals cases compile; what they are about is whether a name resolves to the @bpfglobal symbol or to a stack slot that shadows it. test_globals_ir.py reads the IR of each BPF function and counts loads and stores per operand, exactly: `global counter` is load/add/store on @counter with no slot, a read needs no declaration and hits the symbol, and a write without `global` binds a slot for the whole body of that function while a second function still reads the symbol. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
1 parent 3c3ad8d commit 49d43f7

1 file changed

Lines changed: 80 additions & 0 deletions

File tree

‎tests/test_globals_ir.py‎

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,80 @@
1+
"""
2+
Global variables: where the loads and stores go.
3+
4+
Levels 1 and 2 only prove that the passing_tests/globals cases compile. What
5+
those cases are *about* is which storage a name resolves to: the @bpfglobal's
6+
symbol, or a stack slot that shadows it. This test reads the IR of each BPF
7+
function and checks exactly that, so the scoping rules are pinned rather than
8+
inferred from "it compiled".
9+
10+
Storage is identified by the operand: `@"name"` is the global's symbol, and
11+
`%"name"` is the alloca of a local of the same name.
12+
"""
13+
14+
import re
15+
from pathlib import Path
16+
17+
import pytest
18+
19+
from tests.framework.compiler import run_ir_generation
20+
21+
GLOBALS_DIR = Path(__file__).parent / "passing_tests" / "globals"
22+
23+
24+
def _function_bodies(ir_text: str) -> dict[str, str]:
25+
"""Map each defined function's name to the text of its body."""
26+
return {
27+
m.group(1): m.group(2)
28+
for m in re.finditer(r'define [^\n]*@"(\w+)"[^\n]*\n\{(.*?)\n\}', ir_text, re.S)
29+
}
30+
31+
32+
def _stores_to(body: str, operand: str) -> int:
33+
return len(re.findall(rf"store i\d+ [^,]+, i\d+\* {re.escape(operand)}", body))
34+
35+
36+
def _loads_from(body: str, operand: str) -> int:
37+
return len(re.findall(rf"load i\d+, i\d+\* {re.escape(operand)}", body))
38+
39+
40+
# file -> {function: [(operand, expected stores, expected loads)]}
41+
# Expectations are exact counts, so an accidental extra access is caught too.
42+
CASES = {
43+
# `global counter` writes through: load, add, store on the symbol, no slot.
44+
"augassign_counter.py": {
45+
"tick": [('@"counter"', 1, 1), ('%"counter"', 0, 0)],
46+
},
47+
# `global cg_id` inside an if: the store still targets the symbol.
48+
"write_scalar.py": {
49+
"trace": [('@"cg_id"', 1, 0), ('%"cg_id"', 0, 0)],
50+
},
51+
# Reads need no declaration and resolve to the symbol.
52+
"copy_to_local.py": {
53+
"prog": [('@"threshold"', 0, 3), ('%"threshold"', 0, 0)],
54+
},
55+
# Python's scoping rule: without `global`, `counter = 1` binds a local for
56+
# the whole body of `tick`, so every access there is the stack slot and the
57+
# symbol is untouched; `read_back`, which assigns nothing, reads the symbol.
58+
"shadowing.py": {
59+
"tick": [('%"counter"', 2, 2), ('@"counter"', 0, 0)],
60+
"read_back": [('@"counter"', 0, 1), ('%"counter"', 0, 0)],
61+
},
62+
}
63+
64+
65+
@pytest.mark.parametrize("name", list(CASES))
66+
def test_globals_ir_storage(name, tmp_path):
67+
ll_path = tmp_path / name.replace(".py", ".ll")
68+
run_ir_generation(GLOBALS_DIR / name, ll_path)
69+
bodies = _function_bodies(ll_path.read_text())
70+
71+
for func, expectations in CASES[name].items():
72+
assert func in bodies, f"{name}: no function '{func}' in the IR"
73+
body = bodies[func]
74+
for operand, stores, loads in expectations:
75+
assert _stores_to(body, operand) == stores, (
76+
f"{name}::{func}: expected {stores} store(s) to {operand}"
77+
)
78+
assert _loads_from(body, operand) == loads, (
79+
f"{name}::{func}: expected {loads} load(s) from {operand}"
80+
)

0 commit comments

Comments
 (0)