Skip to content

Commit 13538a4

Browse files
Core: Augmented assignment to a map-lookup local rebinds it, as v = v + 1 does
`v = m.lookup(k); v += 1` was rejected with "augmented assignment needs an integer target, got i64*": the Name branch took the local's slot type, the pointer, as the target type. `v = v + 1` already compiles, and to a rebinding: the value is read through the pointer, null-checked, and the local is pointed at its spare `v_tmp` slot holding the result; the map is not written. `v += 1` now does the same. The read is get_typed_operand's, so the operation takes the map's declared value type and sign (u64 >>= is an lshr); the write is a rebind_map_value_local helper, now shared with plain assignment so the two cannot drift apart. A struct-valued lookup local is refused with a pointer to its fields.
1 parent dc628ee commit 13538a4

5 files changed

Lines changed: 91 additions & 11 deletions

File tree

‎docs/user-guide/maps.md‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -60,6 +60,10 @@ def trace_open(ctx: c_void_p) -> c_int64:
6060
return 0
6161
```
6262

63+
An integer value behaves like a Python `int`: `value += 1`, like
64+
`value = value + 1`, rebinds the local to the new number and leaves the map
65+
unchanged. To store it, call `update`.
66+
6367
##### update(key, value, flags=None)
6468

6569
Update or insert a key-value pair.

‎pythonbpf/assign_pass.py‎

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -87,6 +87,24 @@ def _copy_string_to_char_array(
8787
emit_probe_read_kernel_str_call(builder, dst_i8_ptr, array_size, src_ptr)
8888

8989

90+
def is_map_value_local(local_sym_tab, var_name):
91+
"""Whether `var_name` holds a map-lookup result: a pointer to the value,
92+
allocated with a spare `<name>_tmp` slot for values computed from it."""
93+
return (
94+
isinstance(local_sym_tab[var_name].ir_type, ir.PointerType)
95+
and f"{var_name}_tmp" in local_sym_tab
96+
)
97+
98+
99+
def rebind_map_value_local(builder, local_sym_tab, var_name, val):
100+
"""Bind a map-lookup local to a computed value, the way Python rebinds a
101+
name: the value goes into the local's `<name>_tmp` slot and the local is
102+
pointed at it. The map itself is not written."""
103+
tmp = local_sym_tab[f"{var_name}_tmp"].var
104+
builder.store(val, tmp)
105+
builder.store(tmp, local_sym_tab[var_name].var)
106+
107+
90108
def _is_char_array(ir_type):
91109
"""Check if type is [N x i8]."""
92110
return (
@@ -223,12 +241,9 @@ def handle_variable_assignment(
223241
return False
224242
elif isinstance(val_type, ir.IntType) and isinstance(var_type, ir.PointerType):
225243
# NOTE: This is assignment to a PTR_TO_MAP_VALUE_OR_NULL
226-
logger.info(
227-
f"Creating temporary variable for pointer assignment to {var_name}"
228-
)
229-
var_ptr_tmp = local_sym_tab[f"{var_name}_tmp"].var
230-
builder.store(val, var_ptr_tmp)
231-
val = var_ptr_tmp
244+
rebind_map_value_local(builder, local_sym_tab, var_name, val)
245+
logger.info(f"Rebound map-lookup local {var_name} to a computed value")
246+
return True
232247
else:
233248
logger.error(
234249
f"Type mismatch for variable {var_name}: {val_type} vs {var_type}"

‎pythonbpf/functions/functions_pass.py‎

Lines changed: 28 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -22,6 +22,8 @@
2222
from pythonbpf.assign_pass import (
2323
handle_variable_assignment,
2424
handle_struct_field_assignment,
25+
is_map_value_local,
26+
rebind_map_value_local,
2527
)
2628
from pythonbpf.allocation_pass import (
2729
handle_assign_allocation,
@@ -207,6 +209,9 @@ def handle_aug_assign(func, compilation_context, builder, stmt, local_sym_tab):
207209
read, and the operator table is apply_binop, the same one binary-op
208210
evaluation uses.
209211
"""
212+
# A map-lookup local reads through its pointer and is rebound, not
213+
# written through, exactly as `v = v + 1` does it.
214+
map_value_local = None
210215
if isinstance(stmt.target, ast.Name):
211216
name = stmt.target.id
212217
# One table: a declared global is a local_sym_tab entry whose slot is
@@ -220,6 +225,13 @@ def handle_aug_assign(func, compilation_context, builder, stmt, local_sym_tab):
220225
raise SyntaxError(
221226
f"cannot assign to '{name}': it is the context parameter"
222227
)
228+
if is_map_value_local(local_sym_tab, name):
229+
if local_sym_tab[name].metadata in compilation_context.structs_sym_tab:
230+
raise SyntaxError(
231+
f"augmented assignment to '{name}', a struct map value; "
232+
f"update one of its fields instead ({name}.field += ...)"
233+
)
234+
map_value_local = name
223235
elif name in compilation_context.bpf_globals:
224236
# `x += v` binds x as a local and reads it unbound: UnboundLocalError.
225237
raise SyntaxError(
@@ -263,13 +275,20 @@ def handle_aug_assign(func, compilation_context, builder, stmt, local_sym_tab):
263275
f"Unsupported augmented-assignment target: {ast.dump(stmt.target)}"
264276
)
265277

266-
if not isinstance(slot_type, ir.IntType):
278+
# Python evaluates the target's current value before the right-hand side.
279+
if map_value_local is not None:
280+
# The null-checked read every other use of the local gets; its
281+
# descriptor is the map's declared value type, which is also the
282+
# type the result is stored back as.
283+
current, slot_type = get_typed_operand(
284+
func, compilation_context, stmt.target, builder, local_sym_tab
285+
)
286+
elif isinstance(slot_type, ir.IntType):
287+
current = builder.load(slot)
288+
else:
267289
raise SyntaxError(
268290
f"augmented assignment needs an integer target, got {slot_type}"
269291
)
270-
271-
# Python evaluates the target's current value before the right-hand side.
272-
current = builder.load(slot)
273292
rhs, rhs_ty = get_typed_operand(
274293
func, compilation_context, stmt.value, builder, local_sym_tab
275294
)
@@ -287,7 +306,11 @@ def handle_aug_assign(func, compilation_context, builder, stmt, local_sym_tab):
287306
apply_binop(builder, stmt.op, current, rhs, signedness(result_ty)),
288307
result_ty,
289308
)
290-
builder.store(convert(builder, result, result_ty, slot_type), slot)
309+
result = convert(builder, result, result_ty, slot_type)
310+
if map_value_local is not None:
311+
rebind_map_value_local(builder, local_sym_tab, map_value_local, result)
312+
else:
313+
builder.store(result, slot)
291314

292315

293316
def handle_cond(func, compilation_context, builder, cond, local_sym_tab):
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
# Augmented assignment to a map-lookup local: `v += 1` is `v = v + 1`. It reads
2+
# the value through the pointer (null-checked) and rebinds v to the result; the
3+
# map itself is not written.
4+
from pythonbpf import bpf, map, section, bpfglobal, compile
5+
from pythonbpf.maps import HashMap
6+
from ctypes import c_void_p, c_int64, c_uint64
7+
8+
9+
@bpf
10+
@map
11+
def m() -> HashMap:
12+
return HashMap(key=c_int64, value=c_uint64, max_entries=4)
13+
14+
15+
@bpf
16+
@section("tracepoint/raw_syscalls/sys_enter")
17+
def prog(ctx: c_void_p) -> c_int64:
18+
v = m.lookup(0)
19+
if v:
20+
v += 1
21+
v >>= 1 # c_uint64 value: a logical shift
22+
return v
23+
return c_int64(0)
24+
25+
26+
@bpf
27+
@bpfglobal
28+
def LICENSE() -> str:
29+
return "GPL"
30+
31+
32+
compile()

‎tests/test_signedness_ir.py‎

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,12 @@
8181
[r"trunc i64 .* to i32", r"zext i32 .* to i64"],
8282
[r"sext i32 .* to i64"],
8383
),
84+
# `v += 1; v >>= 1` on a c_uint64 map value: read through the pointer,
85+
# operate unsigned, rebind v to the lookup local's spare slot.
86+
"assign/augassign_map_value.py": (
87+
[r"deref_1_not_null", r"\blshr i64", r"store i64\* %\"v_tmp\", i64\*\* %\"v\""],
88+
[r"\bashr i64"],
89+
),
8490
# `return p` on a map lookup dereferences through a null check and returns
8591
# the i64, never the pointer.
8692
"return/map_value.py": (

0 commit comments

Comments
 (0)