Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
6008d98
Change loglevel of multi-assignment warning in handle_assign
r41k0u Oct 8, 2025
84ed27f
Add handle_variable_assignment stub and boilerplate in handle_assign
r41k0u Oct 8, 2025
d7bfe86
Add handle_variable_assignment to assign_pass
r41k0u Oct 8, 2025
054a834
Add failing assign test retype.py, with explanation
r41k0u Oct 8, 2025
c596213
Add cst_var_binop.py as passing assign test
r41k0u Oct 8, 2025
23afb0b
Add deref_to_val to deref into final value and return the chain as we…
r41k0u Oct 9, 2025
1253f51
Use deref_to_val instead of recursive_dereferencer in get_operand value
r41k0u Oct 9, 2025
8bab07e
Remove recursive_dereferencer
r41k0u Oct 9, 2025
489244a
Add store_through_chain
r41k0u Oct 9, 2025
047f361
Allocate twice for map lookups
r41k0u Oct 10, 2025
1d517d4
Add double_alloc in alloc_mem
r41k0u Oct 10, 2025
99aacca
WIP: allow pointer assignments to var
r41k0u Oct 10, 2025
9febadf
Add pointer handling to helper_utils, finish pointer assignment
r41k0u Oct 10, 2025
7529820
Allow int** pointers to store binops of type int** op int
r41k0u Oct 10, 2025
a756f5e
Add passing helper test for assignment
r41k0u Oct 10, 2025
3175756
Interpret bools as ints in binops
r41k0u Oct 10, 2025
cac88d1
Allow different int widths in binops
r41k0u Oct 10, 2025
c2c1774
Remove store_through_chain
r41k0u Oct 10, 2025
91a3fe1
Remove unnecessary return artifacts from get_operand_value
r41k0u Oct 10, 2025
c9bbe1f
Call eval_expr properly within get_operand_value
r41k0u Oct 10, 2025
8b7b1c0
Add struct_and_helper_binops passing test for assignments
r41k0u Oct 11, 2025
8776d76
Add count_temps_in_call to call scratch space needed in a helper call
r41k0u Oct 11, 2025
321415f
Add update_max_temps_for_stmt in allocate_mem
r41k0u Oct 11, 2025
6bce29b
Allocate scratch space for temp vars at the end of allocate_mem
r41k0u Oct 11, 2025
5dcf670
Add ScratchPoolManager and it's singleton
r41k0u Oct 11, 2025
207f714
Use scratch space to store consts passed to helpers
r41k0u Oct 11, 2025
cd74e89
Allow binops as args to helpers accepting int*
r41k0u Oct 11, 2025
d66e6a6
Allow struct members as helper args
r41k0u Oct 12, 2025
2cf68f6
Allow map-based helpers to be used as helper args / within binops whi…
r41k0u Oct 12, 2025
4e33fd4
Add negation UnaryOp
r41k0u Oct 12, 2025
a3b4d09
Fix errorstring in _handle_unary_op
r41k0u Oct 12, 2025
e8026a1
Allow helpers to be called within themselves
r41k0u Oct 12, 2025
fa82dc7
Add comprehensive passing test for assignment
r41k0u Oct 12, 2025
b93f704
Tweak the comprehensive assignment test
r41k0u Oct 12, 2025
933d2a5
Fix comprehensive assignment test
r41k0u Oct 12, 2025
105c5a7
Cleanup handle_assign
r41k0u Oct 12, 2025
3ad1b73
Add handle_struct_field_assignment to assign_pass
r41k0u Oct 12, 2025
64e44d0
Use handle_struct_field_assignment in handle_assign
r41k0u Oct 12, 2025
08c0ccf
Pass map_sym_tab to handle_struct_field_assign
r41k0u Oct 12, 2025
0f6971b
Refactor allocate_mem
r41k0u Oct 12, 2025
2f1aaa4
Fix typos
r41k0u Oct 12, 2025
69bee5f
Seperate LocalSymbol from functions
r41k0u Oct 12, 2025
e0ad1bf
Move bulk of allocation logic to allocation_pass
r41k0u Oct 12, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
191 changes: 191 additions & 0 deletions pythonbpf/allocation_pass.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,191 @@
import ast
import logging

from llvmlite import ir
from dataclasses import dataclass
from typing import Any
from pythonbpf.helper import HelperHandlerRegistry
from pythonbpf.type_deducer import ctypes_to_ir

logger = logging.getLogger(__name__)


@dataclass
class LocalSymbol:
var: ir.AllocaInstr
ir_type: ir.Type
metadata: Any = None

def __iter__(self):
yield self.var
yield self.ir_type
yield self.metadata


def _is_helper_call(call_node):
"""Check if a call node is a BPF helper function call."""
if isinstance(call_node.func, ast.Name):
# Exclude print from requiring temps (handles f-strings differently)
func_name = call_node.func.id
return HelperHandlerRegistry.has_handler(func_name) and func_name != "print"

elif isinstance(call_node.func, ast.Attribute):
return HelperHandlerRegistry.has_handler(call_node.func.attr)

return False


def handle_assign_allocation(builder, stmt, local_sym_tab, structs_sym_tab):
"""Handle memory allocation for assignment statements."""

# Validate assignment
if len(stmt.targets) != 1:
logger.warning("Multi-target assignment not supported, skipping allocation")
return

target = stmt.targets[0]

# Skip non-name targets (e.g., struct field assignments)
if isinstance(target, ast.Attribute):
logger.debug(f"Struct field assignment to {target.attr}, no allocation needed")
return

if not isinstance(target, ast.Name):
logger.warning(f"Unsupported assignment target type: {type(target).__name__}")
return

var_name = target.id
rval = stmt.value

# Skip if already allocated
if var_name in local_sym_tab:
logger.debug(f"Variable {var_name} already allocated, skipping")
return

# Determine type and allocate based on rval
if isinstance(rval, ast.Call):
_allocate_for_call(builder, var_name, rval, local_sym_tab, structs_sym_tab)
elif isinstance(rval, ast.Constant):
_allocate_for_constant(builder, var_name, rval, local_sym_tab)
elif isinstance(rval, ast.BinOp):
_allocate_for_binop(builder, var_name, local_sym_tab)
else:
logger.warning(
f"Unsupported assignment value type for {var_name}: {type(rval).__name__}"
)


def _allocate_for_call(builder, var_name, rval, local_sym_tab, structs_sym_tab):
"""Allocate memory for variable assigned from a call."""

if isinstance(rval.func, ast.Name):
call_type = rval.func.id

# C type constructors
if call_type in ("c_int32", "c_int64", "c_uint32", "c_uint64"):
ir_type = ctypes_to_ir(call_type)
var = builder.alloca(ir_type, name=var_name)
var.align = ir_type.width // 8
local_sym_tab[var_name] = LocalSymbol(var, ir_type)
logger.info(f"Pre-allocated {var_name} as {call_type}")

# Helper functions
elif HelperHandlerRegistry.has_handler(call_type):
ir_type = ir.IntType(64) # Assume i64 return type
var = builder.alloca(ir_type, name=var_name)
var.align = 8
local_sym_tab[var_name] = LocalSymbol(var, ir_type)
logger.info(f"Pre-allocated {var_name} for helper {call_type}")

# Deref function
elif call_type == "deref":
ir_type = ir.IntType(64) # Assume i64 return type
var = builder.alloca(ir_type, name=var_name)
var.align = 8
local_sym_tab[var_name] = LocalSymbol(var, ir_type)
logger.info(f"Pre-allocated {var_name} for deref")

# Struct constructors
elif call_type in structs_sym_tab:
struct_info = structs_sym_tab[call_type]
var = builder.alloca(struct_info.ir_type, name=var_name)
local_sym_tab[var_name] = LocalSymbol(var, struct_info.ir_type, call_type)
logger.info(f"Pre-allocated {var_name} for struct {call_type}")

else:
logger.warning(f"Unknown call type for allocation: {call_type}")

elif isinstance(rval.func, ast.Attribute):
# Map method calls - need double allocation for ptr handling
_allocate_for_map_method(builder, var_name, local_sym_tab)

else:
logger.warning(f"Unsupported call function type for {var_name}")


def _allocate_for_map_method(builder, var_name, local_sym_tab):
"""Allocate memory for variable assigned from map method (double alloc)."""

# Main variable (pointer to pointer)
ir_type = ir.PointerType(ir.IntType(64))
var = builder.alloca(ir_type, name=var_name)
local_sym_tab[var_name] = LocalSymbol(var, ir_type)

# Temporary variable for computed values
tmp_ir_type = ir.IntType(64)
var_tmp = builder.alloca(tmp_ir_type, name=f"{var_name}_tmp")
local_sym_tab[f"{var_name}_tmp"] = LocalSymbol(var_tmp, tmp_ir_type)

logger.info(f"Pre-allocated {var_name} and {var_name}_tmp for map method")


def _allocate_for_constant(builder, var_name, rval, local_sym_tab):
"""Allocate memory for variable assigned from a constant."""

if isinstance(rval.value, bool):
ir_type = ir.IntType(1)
var = builder.alloca(ir_type, name=var_name)
var.align = 1
local_sym_tab[var_name] = LocalSymbol(var, ir_type)
logger.info(f"Pre-allocated {var_name} as bool")

elif isinstance(rval.value, int):
ir_type = ir.IntType(64)
var = builder.alloca(ir_type, name=var_name)
var.align = 8
local_sym_tab[var_name] = LocalSymbol(var, ir_type)
logger.info(f"Pre-allocated {var_name} as i64")

elif isinstance(rval.value, str):
ir_type = ir.PointerType(ir.IntType(8))
var = builder.alloca(ir_type, name=var_name)
var.align = 8
local_sym_tab[var_name] = LocalSymbol(var, ir_type)
logger.info(f"Pre-allocated {var_name} as string")

else:
logger.warning(
f"Unsupported constant type for {var_name}: {type(rval.value).__name__}"
)


def _allocate_for_binop(builder, var_name, local_sym_tab):
"""Allocate memory for variable assigned from a binary operation."""
ir_type = ir.IntType(64) # Assume i64 result
var = builder.alloca(ir_type, name=var_name)
var.align = 8
local_sym_tab[var_name] = LocalSymbol(var, ir_type)
logger.info(f"Pre-allocated {var_name} for binop result")


def allocate_temp_pool(builder, max_temps, local_sym_tab):
"""Allocate the temporary scratch space pool for helper arguments."""
if max_temps == 0:
return

logger.info(f"Allocating temp pool of {max_temps} variables")
for i in range(max_temps):
temp_name = f"__helper_temp_{i}"
temp_var = builder.alloca(ir.IntType(64), name=temp_name)
temp_var.align = 8
local_sym_tab[temp_name] = LocalSymbol(temp_var, ir.IntType(64))
108 changes: 108 additions & 0 deletions pythonbpf/assign_pass.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,108 @@
import ast
import logging
from llvmlite import ir
from pythonbpf.expr import eval_expr

logger = logging.getLogger(__name__)


def handle_struct_field_assignment(
func, module, builder, target, rval, local_sym_tab, map_sym_tab, structs_sym_tab
):
"""Handle struct field assignment (obj.field = value)."""

var_name = target.value.id
field_name = target.attr

if var_name not in local_sym_tab:
logger.error(f"Variable '{var_name}' not found in symbol table")
return

struct_type = local_sym_tab[var_name].metadata
struct_info = structs_sym_tab[struct_type]

if field_name not in struct_info.fields:
logger.error(f"Field '{field_name}' not found in struct '{struct_type}'")
return

# Get field pointer and evaluate value
field_ptr = struct_info.gep(builder, local_sym_tab[var_name].var, field_name)
val = eval_expr(
func, module, builder, rval, local_sym_tab, map_sym_tab, structs_sym_tab
)

if val is None:
logger.error(f"Failed to evaluate value for {var_name}.{field_name}")
return

# TODO: Handle string assignment to char array (not a priority)
field_type = struct_info.field_type(field_name)
if isinstance(field_type, ir.ArrayType) and val[1] == ir.PointerType(ir.IntType(8)):
logger.warning(
f"String to char array assignment not implemented for {var_name}.{field_name}"
)
return

# Store the value
builder.store(val[0], field_ptr)
logger.info(f"Assigned to struct field {var_name}.{field_name}")


def handle_variable_assignment(
func, module, builder, var_name, rval, local_sym_tab, map_sym_tab, structs_sym_tab
):
"""Handle single named variable assignment."""

if var_name not in local_sym_tab:
logger.error(f"Variable {var_name} not declared.")
return False

var_ptr = local_sym_tab[var_name].var
var_type = local_sym_tab[var_name].ir_type

# NOTE: Special case for struct initialization
if isinstance(rval, ast.Call) and isinstance(rval.func, ast.Name):
struct_name = rval.func.id
if struct_name in structs_sym_tab and len(rval.args) == 0:
struct_info = structs_sym_tab[struct_name]
ir_struct = struct_info.ir_type

builder.store(ir.Constant(ir_struct, None), var_ptr)
logger.info(f"Initialized struct {struct_name} for variable {var_name}")
return True

val_result = eval_expr(
func, module, builder, rval, local_sym_tab, map_sym_tab, structs_sym_tab
)
if val_result is None:
logger.error(f"Failed to evaluate value for {var_name}")
return False

val, val_type = val_result
logger.info(f"Evaluated value for {var_name}: {val} of type {val_type}, {var_type}")
if val_type != var_type:
if isinstance(val_type, ir.IntType) and isinstance(var_type, ir.IntType):
# Allow implicit int widening
if val_type.width < var_type.width:
val = builder.sext(val, var_type)
logger.info(f"Implicitly widened int for variable {var_name}")
elif val_type.width > var_type.width:
val = builder.trunc(val, var_type)
logger.info(f"Implicitly truncated int for variable {var_name}")
elif isinstance(val_type, ir.IntType) and isinstance(var_type, ir.PointerType):
# NOTE: This is assignment to a PTR_TO_MAP_VALUE_OR_NULL
logger.info(
f"Creating temporary variable for pointer assignment to {var_name}"
)
var_ptr_tmp = local_sym_tab[f"{var_name}_tmp"].var
builder.store(val, var_ptr_tmp)
val = var_ptr_tmp
else:
logger.error(
f"Type mismatch for variable {var_name}: {val_type} vs {var_type}"
)
return False

builder.store(val, var_ptr)
logger.info(f"Assigned value to variable {var_name}")
return True
Loading