Skip to content

Commit 3c3ad8d

Browse files
r41k0uclaude
andcommitted
Tests: Shadowing is a passing case now, unbound reads are the error
globals_shadowing.py asserted the old rule, so it moves to passing_tests/globals/shadowing.py and asserts the new one: the write without `global` binds a local, and a second program still reads the global's initializer. copy_to_local.py covers `x = a_global`, the read shape that used to fail in the allocation pass. globals_read_before_shadow.py takes over as the failing case, since a read above the binding is what Python rejects, and the augassign test keeps its xfail with the reason reworded to the UnboundLocalError it is. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
1 parent 3a3b4b2 commit 3c3ad8d

5 files changed

Lines changed: 103 additions & 5 deletions

File tree

‎tests/failing_tests/globals_augassign_shadowing.py‎

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
1-
# `counter += 1` without `global counter` must be the same loud error as plain
2-
# assignment. Before direct lowering this slipped past the allocation pass
3-
# (which only walks Assign) and failed later with a misleading message.
1+
# `counter += 1` without `global counter` binds `counter` as a local and reads
2+
# it in the same statement, which is UnboundLocalError in Python. Plain
3+
# assignment shadows the global instead (see passing_tests/globals/shadowing.py);
4+
# this shape cannot, because it reads before it binds.
45
from pythonbpf import bpf, section, bpfglobal, compile
56
from ctypes import c_void_p, c_int64, c_uint64
67

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,29 @@
1+
# The assignment below makes `counter` a local for the whole function, so the
2+
# read above it is an UnboundLocalError in Python -- not a read of the global.
3+
# There is no runtime here in which to raise that, so the compiler rejects the
4+
# program instead of silently loading an uninitialised slot.
5+
from pythonbpf import bpf, section, bpfglobal, compile
6+
from ctypes import c_void_p, c_int64, c_uint64
7+
8+
9+
@bpf
10+
@bpfglobal
11+
def counter() -> c_uint64:
12+
return c_uint64(7)
13+
14+
15+
@bpf
16+
@section("tracepoint/raw_syscalls/sys_enter")
17+
def prog(ctx: c_void_p) -> c_int64:
18+
print(f"counter {counter}") # noqa: F821, F823 -- reads a local bound below
19+
counter = 1 # noqa: F841 -- ...here, which is what makes the read unbound
20+
return c_int64(0)
21+
22+
23+
@bpf
24+
@bpfglobal
25+
def LICENSE() -> str:
26+
return "GPL"
27+
28+
29+
compile()
Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,30 @@
1+
# Reading a global needs no declaration, in every shape a read can take --
2+
# including `x = a_global`, which resolves through the allocation pass rather
3+
# than the expression evaluator and so needs the globals table there too.
4+
from pythonbpf import bpf, section, bpfglobal, compile
5+
from ctypes import c_void_p, c_int64, c_uint64
6+
7+
8+
@bpf
9+
@bpfglobal
10+
def threshold() -> c_uint64:
11+
return c_uint64(42)
12+
13+
14+
@bpf
15+
@section("tracepoint/raw_syscalls/sys_enter")
16+
def prog(ctx: c_void_p) -> c_int64:
17+
limit = threshold
18+
doubled = threshold * 2
19+
if threshold > 10:
20+
print(f"limit {limit} doubled {doubled}")
21+
return c_int64(0)
22+
23+
24+
@bpf
25+
@bpfglobal
26+
def LICENSE() -> str:
27+
return "GPL"
28+
29+
30+
compile()
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
# Python's scoping rule, applied to BPF globals: assigning a name without a
2+
# `global` statement creates a local that shadows the global for the whole
3+
# function body, and leaves the global's storage alone. `tick` therefore writes
4+
# a stack slot, and `read_back` still sees the initializer.
5+
from pythonbpf import bpf, section, bpfglobal, compile
6+
from ctypes import c_void_p, c_int64, c_uint64
7+
8+
9+
@bpf
10+
@bpfglobal
11+
def counter() -> c_uint64:
12+
return c_uint64(7)
13+
14+
15+
@bpf
16+
@section("tracepoint/raw_syscalls/sys_enter")
17+
def tick(ctx: c_void_p) -> c_int64:
18+
counter = 1 # noqa: F841 -- no `global`, so this is a local; that is the test
19+
counter = counter + 1
20+
print(f"local counter {counter}")
21+
return c_int64(0)
22+
23+
24+
@bpf
25+
@section("tracepoint/syscalls/sys_enter_execve")
26+
def read_back(ctx: c_void_p) -> c_int64:
27+
# No assignment here, so the bare name is the global: still 7.
28+
print(f"global counter {counter}")
29+
return c_int64(0)
30+
31+
32+
@bpf
33+
@bpfglobal
34+
def LICENSE() -> str:
35+
return "GPL"
36+
37+
38+
compile()

‎tests/test_config.toml‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -33,12 +33,12 @@
3333

3434
"failing_tests/xdp_pass.py" = {reason = "XDP program using vmlinux structs (struct_xdp_md) and complex map/struct interaction not yet supported", level = "ir"}
3535

36-
"failing_tests/globals_shadowing.py" = {reason = "Assignment to a global name without a `global` statement is a deliberate compile error (would shadow the BPF global)", level = "ir"}
36+
"failing_tests/globals_read_before_shadow.py" = {reason = "Reading a name above the assignment that makes it a local shadowing a global is UnboundLocalError in Python, and a compile error here", level = "ir"}
3737

3838
"failing_tests/globals_undeclared_name.py" = {reason = "`global x` naming something that is not a @bpfglobal is a compile error", level = "ir"}
3939

4040
"failing_tests/globals_bad_type.py" = {reason = "Non-integer-scalar globals are not supported in milestone 1 (NotImplementedError by design)", level = "ir"}
4141

42-
"failing_tests/globals_augassign_shadowing.py" = {reason = "Augmented assignment to a global name without a `global` statement is a deliberate compile error (would shadow the BPF global)", level = "ir"}
42+
"failing_tests/globals_augassign_shadowing.py" = {reason = "`x += 1` on a global name without `global` binds a local and reads it unbound: UnboundLocalError in Python, a compile error here", level = "ir"}
4343

4444
"failing_tests/globals_parameter_and_global.py" = {reason = "A parameter may not be declared global (Python: name is parameter and global)", level = "ir"}

0 commit comments

Comments
 (0)