|
| 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