Skip to content
Merged
Show file tree
Hide file tree
Changes from 40 commits
Commits
Show all changes
43 commits
Select commit Hold shift + click to select a range
d1055e4
Reduce a condition from handle_cond
r41k0u Oct 6, 2025
f11a430
Add _handle_cond to expr_pass
r41k0u Oct 6, 2025
6cf5115
Eval LHS and RHS in _handle_compare
r41k0u Oct 6, 2025
4f433d0
Add Boolean return support
r41k0u Oct 6, 2025
fb63dbd
Move conditional logic to eval_expr, add _conver_to_bool, add passing…
r41k0u Oct 6, 2025
682a7e6
Add const_int test for conditionals
r41k0u Oct 6, 2025
1cce49f
Add const_binop test for conditionals
r41k0u Oct 6, 2025
2de2809
Add var test for conditionals
r41k0u Oct 6, 2025
12b712c
Add var_binop test for conditionals
r41k0u Oct 6, 2025
1d6226d
Add map test to conditionals
r41k0u Oct 6, 2025
1766730
Add failing tests struct and not for conditionals
r41k0u Oct 6, 2025
a574527
Add support for unary op 'not' in eval_expr, move not test to passing
r41k0u Oct 6, 2025
0e7dcaf
Add var_comp test for conditionals
r41k0u Oct 6, 2025
b7092fa
Add failing test map_comp for conditionals
r41k0u Oct 6, 2025
f41693b
Add 'and' and 'or' BoolOps as future deliverables
r41k0u Oct 6, 2025
caa5d92
Fix struct_access in eval_expr, move struct_access conditional test t…
r41k0u Oct 7, 2025
1843ca6
Add failing struct_ptr test for conditionals
r41k0u Oct 7, 2025
9e1142b
Add type_mismatch failing test for conditionals
r41k0u Oct 7, 2025
2d850f4
Add _normalize_types to handle mismatched ints, move type_mismatch te…
r41k0u Oct 7, 2025
ab71275
Add _get_base_type to expr_pass
r41k0u Oct 7, 2025
480afd1
Move _get_base_type to _get_base_type_and_depth
r41k0u Oct 7, 2025
3f9604a
Add _deref_to_depth in expr_pass
r41k0u Oct 7, 2025
6b59980
Add null checks for pointer derefs to avoid map_value_or_null verifie…
r41k0u Oct 8, 2025
95a196a
Move map_comp test to passing
r41k0u Oct 8, 2025
a764b09
Add helper_cond failing test for conditionals
r41k0u Oct 8, 2025
ecac24c
Add explanation notes to failing conditionals tests
r41k0u Oct 8, 2025
d2ff530
Add support for is and is not keywords
r41k0u Oct 8, 2025
98f262a
Add BoolOp handling stub in eval_expr
r41k0u Oct 8, 2025
f98491f
Add handle_and and handle_or handling stub in eval_expr
r41k0u Oct 8, 2025
1f96bab
Add _handle_and_op in expr_pass
r41k0u Oct 8, 2025
95d63d9
Add _handle_or_or in expr_pass
r41k0u Oct 8, 2025
e7912a0
Add passing or.py test for conditionals
r41k0u Oct 8, 2025
3bb4b09
Add passing and.py test for conditionals
r41k0u Oct 8, 2025
4857739
cleanup handle_cond in functions_pass
r41k0u Oct 8, 2025
b86341c
Rework dir structure for expr
r41k0u Oct 8, 2025
5f9eaff
Fix expr imports
r41k0u Oct 8, 2025
ee90ee9
Fix type_deducer import in expr
r41k0u Oct 8, 2025
e62557b
Seperate type_normalization from expr_pass
r41k0u Oct 8, 2025
0a65717
Move convert_to_bool to type_normalization
r41k0u Oct 8, 2025
d38d73d
Move handle_comparator to type_normalization
r41k0u Oct 8, 2025
6362a5e
Fix expr imports
r41k0u Oct 8, 2025
17004d5
Remove completed short term goal from TODO.md
r41k0u Oct 8, 2025
9fdc6fa
Add compile to tests/failing_tests/conditionals/helper_cond.py
r41k0u Oct 8, 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
1 change: 1 addition & 0 deletions TODO.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@
- XDP support in pylibbpf
- ringbuf support
- recursive expression resolution
- Add supoprt for BoolOp and short circuiting in conditions

## Long term

Expand Down
3 changes: 3 additions & 0 deletions pythonbpf/expr/__init__.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
from .expr_pass import eval_expr, handle_expr, convert_to_bool

__all__ = ["eval_expr", "handle_expr", "convert_to_bool"]
217 changes: 210 additions & 7 deletions pythonbpf/expr_pass.py → pythonbpf/expr/expr_pass.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,8 @@
import logging
from typing import Dict

from .type_deducer import ctypes_to_ir, is_ctypes
from pythonbpf.type_deducer import ctypes_to_ir, is_ctypes
from .type_normalization import convert_to_bool, handle_comparator

logger: Logger = logging.getLogger(__name__)

Expand All @@ -22,12 +23,10 @@ def _handle_name_expr(expr: ast.Name, local_sym_tab: Dict, builder: ir.IRBuilder

def _handle_constant_expr(expr: ast.Constant):
"""Handle ast.Constant expressions."""
if isinstance(expr.value, int):
return ir.Constant(ir.IntType(64), expr.value), ir.IntType(64)
elif isinstance(expr.value, bool):
return ir.Constant(ir.IntType(1), int(expr.value)), ir.IntType(1)
if isinstance(expr.value, int) or isinstance(expr.value, bool):
return ir.Constant(ir.IntType(64), int(expr.value)), ir.IntType(64)
else:
logger.info("Unsupported constant type")
logger.error("Unsupported constant type")
return None


Expand All @@ -45,7 +44,6 @@ def _handle_attribute_expr(
var_ptr, var_type, var_metadata = local_sym_tab[var_name]
logger.info(f"Loading attribute {attr_name} from variable {var_name}")
logger.info(f"Variable type: {var_type}, Variable ptr: {var_ptr}")

metadata = structs_sym_tab[var_metadata]
if attr_name in metadata.fields:
gep = metadata.gep(builder, var_ptr, attr_name)
Expand Down Expand Up @@ -132,6 +130,199 @@ def _handle_ctypes_call(
return val


def _handle_compare(
func, module, builder, cond, local_sym_tab, map_sym_tab, structs_sym_tab=None
):
"""Handle ast.Compare expressions."""

if len(cond.ops) != 1 or len(cond.comparators) != 1:
logger.error("Only single comparisons are supported")
return None
lhs = eval_expr(
func,
module,
builder,
cond.left,
local_sym_tab,
map_sym_tab,
structs_sym_tab,
)
rhs = eval_expr(
func,
module,
builder,
cond.comparators[0],
local_sym_tab,
map_sym_tab,
structs_sym_tab,
)

if lhs is None or rhs is None:
logger.error("Failed to evaluate comparison operands")
return None

lhs, _ = lhs
rhs, _ = rhs
return handle_comparator(func, builder, cond.ops[0], lhs, rhs)


def _handle_unary_op(
func,
module,
builder,
expr: ast.UnaryOp,
local_sym_tab,
map_sym_tab,
structs_sym_tab=None,
):
"""Handle ast.UnaryOp expressions."""
if not isinstance(expr.op, ast.Not):
logger.error("Only 'not' unary operator is supported")
return None

operand = eval_expr(
func, module, builder, expr.operand, local_sym_tab, map_sym_tab, structs_sym_tab
)
if operand is None:
logger.error("Failed to evaluate operand for unary operation")
return None

operand_val, operand_type = operand
true_const = ir.Constant(ir.IntType(1), 1)
result = builder.xor(convert_to_bool(builder, operand_val), true_const)
return result, ir.IntType(1)


def _handle_and_op(func, builder, expr, local_sym_tab, map_sym_tab, structs_sym_tab):
"""Handle `and` boolean operations."""

logger.debug(f"Handling 'and' operator with {len(expr.values)} operands")

merge_block = func.append_basic_block(name="and.merge")
false_block = func.append_basic_block(name="and.false")

incoming_values = []

for i, value in enumerate(expr.values):
is_last = i == len(expr.values) - 1

# Evaluate current operand
operand_result = eval_expr(
func, None, builder, value, local_sym_tab, map_sym_tab, structs_sym_tab
)
if operand_result is None:
logger.error(f"Failed to evaluate operand {i} in 'and' expression")
return None

operand_val, operand_type = operand_result

# Convert to boolean if needed
operand_bool = convert_to_bool(builder, operand_val)
current_block = builder.block

if is_last:
# Last operand: result is this value
builder.branch(merge_block)
incoming_values.append((operand_bool, current_block))
else:
# Not last: check if true, continue or short-circuit
next_check = func.append_basic_block(name=f"and.check_{i + 1}")
builder.cbranch(operand_bool, next_check, false_block)
builder.position_at_end(next_check)

# False block: short-circuit with false
builder.position_at_end(false_block)
builder.branch(merge_block)
false_value = ir.Constant(ir.IntType(1), 0)
incoming_values.append((false_value, false_block))

# Merge block: phi node
builder.position_at_end(merge_block)
phi = builder.phi(ir.IntType(1), name="and.result")
for val, block in incoming_values:
phi.add_incoming(val, block)

logger.debug(f"Generated 'and' with {len(incoming_values)} incoming values")
return phi, ir.IntType(1)


def _handle_or_op(func, builder, expr, local_sym_tab, map_sym_tab, structs_sym_tab):
"""Handle `or` boolean operations."""

logger.debug(f"Handling 'or' operator with {len(expr.values)} operands")

merge_block = func.append_basic_block(name="or.merge")
true_block = func.append_basic_block(name="or.true")

incoming_values = []

for i, value in enumerate(expr.values):
is_last = i == len(expr.values) - 1

# Evaluate current operand
operand_result = eval_expr(
func, None, builder, value, local_sym_tab, map_sym_tab, structs_sym_tab
)
if operand_result is None:
logger.error(f"Failed to evaluate operand {i} in 'or' expression")
return None

operand_val, operand_type = operand_result

# Convert to boolean if needed
operand_bool = convert_to_bool(builder, operand_val)
current_block = builder.block

if is_last:
# Last operand: result is this value
builder.branch(merge_block)
incoming_values.append((operand_bool, current_block))
else:
# Not last: check if false, continue or short-circuit
next_check = func.append_basic_block(name=f"or.check_{i + 1}")
builder.cbranch(operand_bool, true_block, next_check)
builder.position_at_end(next_check)

# True block: short-circuit with true
builder.position_at_end(true_block)
builder.branch(merge_block)
true_value = ir.Constant(ir.IntType(1), 1)
incoming_values.append((true_value, true_block))

# Merge block: phi node
builder.position_at_end(merge_block)
phi = builder.phi(ir.IntType(1), name="or.result")
for val, block in incoming_values:
phi.add_incoming(val, block)

logger.debug(f"Generated 'or' with {len(incoming_values)} incoming values")
return phi, ir.IntType(1)


def _handle_boolean_op(
func,
module,
builder,
expr: ast.BoolOp,
local_sym_tab,
map_sym_tab,
structs_sym_tab=None,
):
"""Handle `and` and `or` boolean operations."""

if isinstance(expr.op, ast.And):
return _handle_and_op(
func, builder, expr, local_sym_tab, map_sym_tab, structs_sym_tab
)
elif isinstance(expr.op, ast.Or):
return _handle_or_op(
func, builder, expr, local_sym_tab, map_sym_tab, structs_sym_tab
)
else:
logger.error(f"Unsupported boolean operator: {type(expr.op).__name__}")
return None


def eval_expr(
func,
module,
Expand Down Expand Up @@ -212,6 +403,18 @@ def eval_expr(
from pythonbpf.binary_ops import handle_binary_op

return handle_binary_op(expr, builder, None, local_sym_tab)
elif isinstance(expr, ast.Compare):
return _handle_compare(
func, module, builder, expr, local_sym_tab, map_sym_tab, structs_sym_tab
)
elif isinstance(expr, ast.UnaryOp):
return _handle_unary_op(
func, module, builder, expr, local_sym_tab, map_sym_tab, structs_sym_tab
)
elif isinstance(expr, ast.BoolOp):
return _handle_boolean_op(
func, module, builder, expr, local_sym_tab, map_sym_tab, structs_sym_tab
)
logger.info("Unsupported expression evaluation")
return None

Expand Down
128 changes: 128 additions & 0 deletions pythonbpf/expr/type_normalization.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,128 @@
from llvmlite import ir
import logging
import ast

logger = logging.getLogger(__name__)

COMPARISON_OPS = {
ast.Eq: "==",
ast.NotEq: "!=",
ast.Lt: "<",
ast.LtE: "<=",
ast.Gt: ">",
ast.GtE: ">=",
ast.Is: "==",
ast.IsNot: "!=",
}


def _get_base_type_and_depth(ir_type):
"""Get the base type for pointer types."""
cur_type = ir_type
depth = 0
while isinstance(cur_type, ir.PointerType):
depth += 1
cur_type = cur_type.pointee
return cur_type, depth


def _deref_to_depth(func, builder, val, target_depth):
"""Dereference a pointer to a certain depth."""

cur_val = val
cur_type = val.type

for depth in range(target_depth):
if not isinstance(val.type, ir.PointerType):
logger.error("Cannot dereference further, non-pointer type")
return None

# dereference with null check
pointee_type = cur_type.pointee
null_check_block = builder.block
not_null_block = func.append_basic_block(name=f"deref_not_null_{depth}")
merge_block = func.append_basic_block(name=f"deref_merge_{depth}")

null_ptr = ir.Constant(cur_type, None)
is_not_null = builder.icmp_signed("!=", cur_val, null_ptr)
logger.debug(f"Inserted null check for pointer at depth {depth}")

builder.cbranch(is_not_null, not_null_block, merge_block)

builder.position_at_end(not_null_block)
dereferenced_val = builder.load(cur_val)
logger.debug(f"Dereferenced to depth {depth - 1}, type: {pointee_type}")
builder.branch(merge_block)

builder.position_at_end(merge_block)
phi = builder.phi(pointee_type, name=f"deref_result_{depth}")

zero_value = (
ir.Constant(pointee_type, 0)
if isinstance(pointee_type, ir.IntType)
else ir.Constant(pointee_type, None)
)
phi.add_incoming(zero_value, null_check_block)

phi.add_incoming(dereferenced_val, not_null_block)

# Continue with phi result
cur_val = phi
cur_type = pointee_type
return cur_val


def _normalize_types(func, builder, lhs, rhs):
"""Normalize types for comparison."""

logger.info(f"Normalizing types: {lhs.type} vs {rhs.type}")
if isinstance(lhs.type, ir.IntType) and isinstance(rhs.type, ir.IntType):
if lhs.type.width < rhs.type.width:
lhs = builder.sext(lhs, rhs.type)
else:
rhs = builder.sext(rhs, lhs.type)
return lhs, rhs
elif not isinstance(lhs.type, ir.PointerType) and not isinstance(
rhs.type, ir.PointerType
):
logger.error(f"Type mismatch: {lhs.type} vs {rhs.type}")
return None, None
else:
lhs_base, lhs_depth = _get_base_type_and_depth(lhs.type)
rhs_base, rhs_depth = _get_base_type_and_depth(rhs.type)
if lhs_base == rhs_base:
if lhs_depth < rhs_depth:
rhs = _deref_to_depth(func, builder, rhs, rhs_depth - lhs_depth)
elif rhs_depth < lhs_depth:
lhs = _deref_to_depth(func, builder, lhs, lhs_depth - rhs_depth)
return _normalize_types(func, builder, lhs, rhs)


def convert_to_bool(builder, val):
"""Convert a value to boolean."""
if val.type == ir.IntType(1):
return val
if isinstance(val.type, ir.PointerType):
zero = ir.Constant(val.type, None)
else:
zero = ir.Constant(val.type, 0)
return builder.icmp_signed("!=", val, zero)


def handle_comparator(func, builder, op, lhs, rhs):
"""Handle comparison operations."""

if lhs.type != rhs.type:
lhs, rhs = _normalize_types(func, builder, lhs, rhs)

if lhs is None or rhs is None:
return None

if type(op) not in COMPARISON_OPS:
logger.error(f"Unsupported comparison operator: {type(op)}")
return None

predicate = COMPARISON_OPS[type(op)]
result = builder.icmp_signed(predicate, lhs, rhs)
logger.debug(f"Comparison result: {result}")
return result, ir.IntType(1)
Loading