Skip to content

Commit 930594d

Browse files
r41k0uclaude
andcommitted
Core: Support writes to @bpfglobal variables via the global statement
Python's own scoping marks the write: 'global cg_id' declares that assignments to cg_id in this function mean the BPF global, and the assignment then emits the plain 'store i64 %v, ptr @cg_id' of the C reference, with the same implicit widening/truncation rules as local assignments. Without the declaration, an assignment to a global's name is refused: SyntaxError: assignment to 'cg_id' shadows the BPF global of the same name -- add 'global cg_id' to write to it That is deliberate. In real Python such an assignment creates a shadowing local; silently compiling it as either a local or a global store would be wrong in one direction or the other, so it is a loud error instead. 'global' naming something that is not a @bpfglobal is also a compile error. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01L1PX8EuP9C3o3veWGA84RF
1 parent e95ee71 commit 930594d

3 files changed

Lines changed: 59 additions & 0 deletions

File tree

pythonbpf/allocation_pass.py

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,20 @@ def handle_assign_allocation(compilation_context, builder, stmt, local_sym_tab):
4949
continue
5050

5151
var_name = target.id
52+
53+
# Writes to @bpfglobal variables use the global symbol, not a stack
54+
# slot. Requires Python's own `global` declaration; without it an
55+
# assignment to a global's name would silently create a local that
56+
# shadows it, which is exactly the bug class we refuse to compile.
57+
if var_name in compilation_context.current_func_globals:
58+
logger.debug(f"'{var_name}' is a declared global, no allocation needed")
59+
continue
60+
if var_name in compilation_context.bpf_globals:
61+
raise SyntaxError(
62+
f"assignment to '{var_name}' shadows the BPF global of the same "
63+
f"name — add 'global {var_name}' to write to it"
64+
)
65+
5266
# Skip if already allocated
5367
if var_name in local_sym_tab:
5468
logger.debug(f"Variable {var_name} already allocated, skipping")

pythonbpf/assign_pass.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,30 @@ def handle_variable_assignment(
105105
):
106106
"""Handle single named variable assignment."""
107107

108+
# A name declared with `global` writes the @bpfglobal symbol directly:
109+
# the plain `store i64 %v, ptr @counter` form of the C reference.
110+
if var_name in compilation_context.current_func_globals:
111+
sym = compilation_context.bpf_globals[var_name]
112+
val_result = eval_expr(func, compilation_context, builder, rval, local_sym_tab)
113+
if val_result is None:
114+
logger.error(f"Failed to evaluate value for global {var_name}")
115+
return False
116+
val, val_type = val_result
117+
if isinstance(val_type, ir.IntType) and isinstance(sym.ir_type, ir.IntType):
118+
# Same implicit widening/truncation rules as local assignments
119+
if val_type.width < sym.ir_type.width:
120+
val = builder.sext(val, sym.ir_type)
121+
elif val_type.width > sym.ir_type.width:
122+
val = builder.trunc(val, sym.ir_type)
123+
elif val_type != sym.ir_type:
124+
logger.error(
125+
f"Type mismatch for global {var_name}: {val_type} vs {sym.ir_type}"
126+
)
127+
return False
128+
builder.store(val, sym.var)
129+
logger.info(f"Stored to BPF global {var_name}")
130+
return True
131+
108132
if var_name not in local_sym_tab:
109133
logger.error(f"Variable {var_name} not declared.")
110134
return False

pythonbpf/functions/functions_pass.py

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -284,6 +284,9 @@ def process_stmt(
284284
handle_assign(func, compilation_context, builder, stmt, local_sym_tab)
285285
elif isinstance(stmt, ast.AugAssign):
286286
raise SyntaxError("Augmented assignment not supported")
287+
elif isinstance(stmt, ast.Global):
288+
# Declarations were collected by process_func_body; nothing to emit.
289+
pass
287290
elif isinstance(stmt, ast.If):
288291
handle_if(func, compilation_context, builder, stmt, local_sym_tab)
289292
elif isinstance(stmt, ast.Return):
@@ -311,6 +314,22 @@ def process_func_body(
311314

312315
local_sym_tab = {}
313316

317+
# Collect `global x` declarations. Python scoping rules apply: a declared
318+
# name may be written anywhere in this function and always means the
319+
# @bpfglobal, never a local. Undeclared writes to a global name are
320+
# rejected in the allocation pass rather than silently shadowing.
321+
declared_globals: set[str] = set()
322+
for node in ast.walk(func_node):
323+
if isinstance(node, ast.Global):
324+
for gname in node.names:
325+
if gname not in compilation_context.bpf_globals:
326+
raise SyntaxError(
327+
f"'global {gname}' in '{func_node.name}': no @bpfglobal "
328+
f"named '{gname}' is declared"
329+
)
330+
declared_globals.add(gname)
331+
compilation_context.current_func_globals = declared_globals
332+
314333
# Add the context parameter (first function argument) to the local symbol table
315334
if func_node.args.args and len(func_node.args.args) > 0:
316335
context_arg = func_node.args.args[0]
@@ -375,6 +394,8 @@ def process_func_body(
375394
if not did_return:
376395
builder.ret(ir.Constant(ir.IntType(64), 0))
377396

397+
compilation_context.current_func_globals = set()
398+
378399

379400
def process_bpf_chunk(func_node, compilation_context, return_type):
380401
"""Process a single BPF chunk (function) and emit corresponding LLVM IR."""

0 commit comments

Comments
 (0)