Skip to content

Commit 7ecc737

Browse files
Core: Lower annotated assignment (x: c_int32 = 0)
The annotation types the slot, so `x: c_int32 = 0` is an i32 where the literal alone would give an i64; the store goes through the same handle_variable_assignment plain assignment uses. A bare `x: T` makes the slot and binds nothing. The already-bound and global-shadowing bookkeeping that Assign allocation did inline moves into _bind_name, which both statements now call.
1 parent dc628ee commit 7ecc737

3 files changed

Lines changed: 135 additions & 48 deletions

File tree

‎pythonbpf/allocation_pass.py‎

Lines changed: 110 additions & 48 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@
66
from pythonbpf.helper import HelperHandlerRegistry
77
from pythonbpf.vmlinux_parser.dependency_node import Field
88
from .expr import VmlinuxHandlerRegistry
9-
from pythonbpf.type_deducer import ctypes_to_ir, IntTy, signedness
9+
from pythonbpf.type_deducer import ctypes_to_ir, is_ctypes, IntTy, signedness
1010
from pythonbpf.expr.type_inference import infer_int_type
1111
from pythonbpf.maps import BPFMapType
1212

@@ -49,57 +49,119 @@ def handle_assign_allocation(compilation_context, builder, stmt, local_sym_tab):
4949
)
5050
continue
5151

52-
var_name = target.id
52+
_bind_name(
53+
compilation_context,
54+
stmt,
55+
target,
56+
local_sym_tab,
57+
lambda var_name: _allocate_for_value(
58+
builder, var_name, rval, local_sym_tab, compilation_context
59+
),
60+
)
5361

54-
# Already bound in this scope: a parameter, an earlier assignment, or a
55-
# `global` declaration (whose slot is the GlobalVariable). No slot needed.
56-
if var_name in local_sym_tab:
57-
logger.debug(f"'{var_name}' already bound, no allocation needed")
58-
continue
5962

60-
# Not declared `global`, yet named like one: Python creates a local
61-
# that shadows the global for the whole function body, and leaves the
62-
# global untouched. Do the same.
63-
shadows_global = var_name in compilation_context.bpf_globals
64-
if shadows_global:
65-
logger.info(
66-
f"'{var_name}' is assigned without a 'global' declaration, so it "
67-
f"is a local shadowing the @bpfglobal of the same name"
68-
)
63+
def handle_ann_assign_allocation(compilation_context, builder, stmt, local_sym_tab):
64+
"""Handle memory allocation for annotated assignment (`x: c_int32 = 0`).
6965
70-
# Determine type and allocate based on rval
71-
if isinstance(rval, ast.Call):
72-
_allocate_for_call(
73-
builder, var_name, rval, local_sym_tab, compilation_context
74-
)
75-
elif isinstance(rval, ast.Constant):
76-
_allocate_for_constant(builder, var_name, rval, local_sym_tab)
77-
elif isinstance(rval, ast.BinOp):
78-
_allocate_for_binop(
79-
builder, var_name, rval, local_sym_tab, compilation_context
80-
)
81-
elif isinstance(rval, ast.Name):
82-
# Variable-to-variable assignment (b = a)
83-
_allocate_for_name(
84-
builder, var_name, rval, local_sym_tab, compilation_context
85-
)
86-
elif isinstance(rval, ast.Attribute):
87-
# Struct field-to-variable assignment (a = dat.fld)
88-
_allocate_for_attribute(
89-
builder, var_name, rval, local_sym_tab, compilation_context
90-
)
91-
else:
92-
logger.warning(
93-
f"Unsupported assignment value type for {var_name}: {type(rval).__name__}"
94-
)
66+
The annotation, not the value, types the slot: `x: c_int32 = 0` is an i32
67+
even though the literal alone would give an i64. With no value
68+
(`x: c_int64`) the slot is still made, and the name stays unbound until
69+
something assigns it, as in Python.
70+
"""
71+
logger.info(f"Handling annotated assignment for allocation: {ast.dump(stmt)}")
9572

96-
if shadows_global and var_name in local_sym_tab:
97-
# Where the binding ends, so that a read above it is reported the
98-
# way Python reports it. end_lineno, not lineno, so a read on a
99-
# continuation line of a multi-line binding counts as above it too.
100-
local_sym_tab[var_name].shadows_global_from = (
101-
getattr(stmt, "end_lineno", None) or target.lineno
102-
)
73+
if not isinstance(stmt.target, ast.Name):
74+
raise SyntaxError(
75+
f"annotated assignment on line {stmt.lineno} must target a plain "
76+
f"name, got {type(stmt.target).__name__}"
77+
)
78+
79+
ir_type = annotation_to_ir(stmt.annotation, stmt.lineno)
80+
_bind_name(
81+
compilation_context,
82+
stmt,
83+
stmt.target,
84+
local_sym_tab,
85+
lambda var_name: local_sym_tab.__setitem__(
86+
var_name,
87+
LocalSymbol(_allocate_with_type(builder, var_name, ir_type), ir_type),
88+
),
89+
)
90+
91+
92+
def annotation_to_ir(annotation, lineno):
93+
"""IR type for a ctypes annotation, written `c_int32` or `ctypes.c_int32`."""
94+
if isinstance(annotation, ast.Name):
95+
name = annotation.id
96+
elif isinstance(annotation, ast.Attribute):
97+
name = annotation.attr
98+
else:
99+
name = None
100+
if name is None or not is_ctypes(name):
101+
raise SyntaxError(
102+
f"unsupported annotation on line {lineno}: {ast.unparse(annotation)} "
103+
f"(annotate locals with a ctypes integer type such as c_int64)"
104+
)
105+
return ctypes_to_ir(name)
106+
107+
108+
def _bind_name(compilation_context, stmt, target, local_sym_tab, allocate):
109+
"""What every statement that binds a bare name shares, around the
110+
statement-specific `allocate(var_name)` that makes the slot.
111+
112+
A name already bound needs no new slot. A name that is also a @bpfglobal
113+
but was not declared `global` becomes a local shadowing it, and records
114+
where its binding ends so a read above it is reported as Python would.
115+
"""
116+
var_name = target.id
117+
118+
# Already bound in this scope: a parameter, an earlier assignment, or a
119+
# `global` declaration (whose slot is the GlobalVariable). No slot needed.
120+
if var_name in local_sym_tab:
121+
logger.debug(f"'{var_name}' already bound, no allocation needed")
122+
return
123+
124+
# Not declared `global`, yet named like one: Python creates a local
125+
# that shadows the global for the whole function body, and leaves the
126+
# global untouched. Do the same.
127+
shadows_global = var_name in compilation_context.bpf_globals
128+
if shadows_global:
129+
logger.info(
130+
f"'{var_name}' is assigned without a 'global' declaration, so it "
131+
f"is a local shadowing the @bpfglobal of the same name"
132+
)
133+
134+
allocate(var_name)
135+
136+
if shadows_global and var_name in local_sym_tab:
137+
# Where the binding ends, so that a read above it is reported the
138+
# way Python reports it. end_lineno, not lineno, so a read on a
139+
# continuation line of a multi-line binding counts as above it too.
140+
local_sym_tab[var_name].shadows_global_from = (
141+
getattr(stmt, "end_lineno", None) or target.lineno
142+
)
143+
144+
145+
def _allocate_for_value(builder, var_name, rval, local_sym_tab, compilation_context):
146+
"""Allocate a slot for `var_name = rval`, typed from the value."""
147+
if isinstance(rval, ast.Call):
148+
_allocate_for_call(builder, var_name, rval, local_sym_tab, compilation_context)
149+
elif isinstance(rval, ast.Constant):
150+
_allocate_for_constant(builder, var_name, rval, local_sym_tab)
151+
elif isinstance(rval, ast.BinOp):
152+
_allocate_for_binop(builder, var_name, rval, local_sym_tab, compilation_context)
153+
elif isinstance(rval, ast.Name):
154+
# Variable-to-variable assignment (b = a)
155+
_allocate_for_name(builder, var_name, rval, local_sym_tab, compilation_context)
156+
elif isinstance(rval, ast.Attribute):
157+
# Struct field-to-variable assignment (a = dat.fld)
158+
_allocate_for_attribute(
159+
builder, var_name, rval, local_sym_tab, compilation_context
160+
)
161+
else:
162+
logger.warning(
163+
f"Unsupported assignment value type for {var_name}: {type(rval).__name__}"
164+
)
103165

104166

105167
def _allocate_for_call(builder, var_name, rval, local_sym_tab, compilation_context):

‎pythonbpf/functions/functions_pass.py‎

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
)
2626
from pythonbpf.allocation_pass import (
2727
handle_assign_allocation,
28+
handle_ann_assign_allocation,
2829
allocate_temp_pool,
2930
create_targets_and_rvals,
3031
LocalSymbol,
@@ -146,6 +147,10 @@ def update_max_temps_for_stmt(stmt):
146147
)
147148
elif isinstance(stmt, ast.Assign):
148149
handle_assign_allocation(compilation_context, builder, stmt, local_sym_tab)
150+
elif isinstance(stmt, ast.AnnAssign):
151+
handle_ann_assign_allocation(
152+
compilation_context, builder, stmt, local_sym_tab
153+
)
149154

150155
allocate_temp_pool(builder, max_temps_needed, local_sym_tab)
151156

@@ -195,6 +200,24 @@ def handle_assign(func, compilation_context, builder, stmt, local_sym_tab):
195200
logger.error(f"Unsupported assignment target: {ast.dump(target)}")
196201

197202

203+
def handle_ann_assign(func, compilation_context, builder, stmt, local_sym_tab):
204+
"""Handle `x: T = v`. The allocation pass already made x's slot with the
205+
annotated type, so what is left is an ordinary store of v into it, through
206+
the same helper plain assignment uses (which converts v to the slot's type).
207+
A bare `x: T` binds nothing and emits nothing."""
208+
if stmt.value is None:
209+
return
210+
if not handle_variable_assignment(
211+
func,
212+
compilation_context,
213+
builder,
214+
stmt.target.id,
215+
stmt.value,
216+
local_sym_tab,
217+
):
218+
logger.error(f"Failed to handle annotated assignment to {stmt.target.id}")
219+
220+
198221
def handle_aug_assign(func, compilation_context, builder, stmt, local_sym_tab):
199222
"""Handle `x += v` and friends by direct lowering: resolve the target's
200223
slot, load it, apply the operator, store back.
@@ -397,6 +420,8 @@ def process_stmt(
397420
)
398421
elif isinstance(stmt, ast.Assign):
399422
handle_assign(func, compilation_context, builder, stmt, local_sym_tab)
423+
elif isinstance(stmt, ast.AnnAssign):
424+
handle_ann_assign(func, compilation_context, builder, stmt, local_sym_tab)
400425
elif isinstance(stmt, ast.AugAssign):
401426
handle_aug_assign(func, compilation_context, builder, stmt, local_sym_tab)
402427
elif isinstance(stmt, ast.Global):
File renamed without changes.

0 commit comments

Comments
 (0)