From 6fea580693c3b0d1e7bf2e5e1d206d2db1a2871f Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Fri, 3 Oct 2025 17:56:21 +0530 Subject: [PATCH 01/13] Fix t/f/return.py, tweak handle_binary_ops --- pythonbpf/binary_ops.py | 4 ++- pythonbpf/codegen.py | 6 ++-- pythonbpf/functions_pass.py | 32 +++++++++++++------ tests/failing_tests/var_rval.py | 0 .../return.py | 0 5 files changed, 29 insertions(+), 13 deletions(-) create mode 100644 tests/failing_tests/var_rval.py rename tests/{failing_tests => passing_tests}/return.py (100%) diff --git a/pythonbpf/binary_ops.py b/pythonbpf/binary_ops.py index 5ab393f..1b25c9c 100644 --- a/pythonbpf/binary_ops.py +++ b/pythonbpf/binary_ops.py @@ -63,4 +63,6 @@ def handle_binary_op_impl(rval, module, builder, local_sym_tab): def handle_binary_op(rval, module, builder, var_name, local_sym_tab): result = handle_binary_op_impl(rval, module, builder, local_sym_tab) - builder.store(result, local_sym_tab[var_name].var) + if var_name in local_sym_tab: + builder.store(result, local_sym_tab[var_name].var) + return result, result.type diff --git a/pythonbpf/codegen.py b/pythonbpf/codegen.py index 5de23a5..cf20f06 100644 --- a/pythonbpf/codegen.py +++ b/pythonbpf/codegen.py @@ -48,7 +48,7 @@ def processor(source_code, filename, module): globals_processing(tree, module) -def compile_to_ir(filename: str, output: str, loglevel=logging.WARNING): +def compile_to_ir(filename: str, output: str, loglevel=logging.INFO): logging.basicConfig( level=loglevel, format="%(asctime)s [%(levelname)s] %(name)s: %(message)s" ) @@ -121,7 +121,7 @@ def compile_to_ir(filename: str, output: str, loglevel=logging.WARNING): return output -def compile(loglevel=logging.WARNING) -> bool: +def compile(loglevel=logging.INFO) -> bool: # Look one level up the stack to the caller of this function caller_frame = inspect.stack()[1] caller_file = Path(caller_frame.filename).resolve() @@ -154,7 +154,7 @@ def compile(loglevel=logging.WARNING) -> bool: return success -def BPF(loglevel=logging.WARNING) -> BpfProgram: +def BPF(loglevel=logging.INFO) -> BpfProgram: caller_frame = inspect.stack()[1] src = inspect.getsource(caller_frame.frame) with tempfile.NamedTemporaryFile( diff --git a/pythonbpf/functions_pass.py b/pythonbpf/functions_pass.py index 430b55f..e343aa3 100644 --- a/pythonbpf/functions_pass.py +++ b/pythonbpf/functions_pass.py @@ -391,17 +391,31 @@ def process_stmt( isinstance(stmt.value, ast.Call) and isinstance(stmt.value.func, ast.Name) and len(stmt.value.args) == 1 - and isinstance(stmt.value.args[0], ast.Constant) - and isinstance(stmt.value.args[0].value, int) ): - call_type = stmt.value.func.id - if ctypes_to_ir(call_type) != ret_type: - raise ValueError( - "Return type mismatch: expected" - f"{ctypes_to_ir(call_type)}, got {call_type}" + if isinstance(stmt.value.args[0], ast.Constant) and isinstance( + stmt.value.args[0].value, int + ): + call_type = stmt.value.func.id + if ctypes_to_ir(call_type) != ret_type: + raise ValueError( + "Return type mismatch: expected" + f"{ctypes_to_ir(call_type)}, got {call_type}" + ) + else: + builder.ret(ir.Constant(ret_type, stmt.value.args[0].value)) + did_return = True + elif isinstance(stmt.value.args[0], ast.BinOp): + # TODO: Should be routed through eval_expr + val = handle_binary_op( + stmt.value.args[0], module, builder, None, local_sym_tab ) - else: - builder.ret(ir.Constant(ret_type, stmt.value.args[0].value)) + if val is None: + raise ValueError("Failed to evaluate return expression") + if val[1] != ret_type: + raise ValueError( + "Return type mismatch: expected" f"{ret_type}, got {val[1]}" + ) + builder.ret(val[0]) did_return = True elif isinstance(stmt.value, ast.Name): if stmt.value.id == "XDP_PASS": diff --git a/tests/failing_tests/var_rval.py b/tests/failing_tests/var_rval.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/failing_tests/return.py b/tests/passing_tests/return.py similarity index 100% rename from tests/failing_tests/return.py rename to tests/passing_tests/return.py From 6d5d6345e22ed8141ed7b886d44b87798fc28184 Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Fri, 3 Oct 2025 18:01:15 +0530 Subject: [PATCH 02/13] Add var_rval failing test --- tests/failing_tests/var_rval.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/tests/failing_tests/var_rval.py b/tests/failing_tests/var_rval.py index e69de29..38d993e 100644 --- a/tests/failing_tests/var_rval.py +++ b/tests/failing_tests/var_rval.py @@ -0,0 +1,18 @@ +from pythonbpf import compile, bpf, section, bpfglobal +from ctypes import c_void_p, c_int64 + + +@bpf +@section("sometag1") +def sometag(ctx: c_void_p) -> c_int64: + a = 1 - 1 + return c_int64(a) + + +@bpf +@bpfglobal +def LICENSE() -> str: + return "GPL" + + +compile() From 3f061750cf433787856085439e9da77ab3c55526 Mon Sep 17 00:00:00 2001 From: varun-r-mallya Date: Fri, 3 Oct 2025 19:11:11 +0530 Subject: [PATCH 03/13] fix return value error --- pythonbpf/functions_pass.py | 4 ++-- tests/{failing_tests => passing_tests}/var_rval.py | 8 +++++--- 2 files changed, 7 insertions(+), 5 deletions(-) rename tests/{failing_tests => passing_tests}/var_rval.py (55%) diff --git a/pythonbpf/functions_pass.py b/pythonbpf/functions_pass.py index e343aa3..a8f106a 100644 --- a/pythonbpf/functions_pass.py +++ b/pythonbpf/functions_pass.py @@ -385,7 +385,7 @@ def process_stmt( ) elif isinstance(stmt, ast.Return): if stmt.value is None: - builder.ret(ir.Constant(ir.IntType(32), 0)) + builder.ret(ir.Constant(ir.IntType(64), 0)) did_return = True elif ( isinstance(stmt.value, ast.Call) @@ -582,7 +582,7 @@ def process_func_body( ) if not did_return: - builder.ret(ir.Constant(ir.IntType(32), 0)) + builder.ret(ir.Constant(ir.IntType(64), 0)) def process_bpf_chunk(func_node, module, return_type, map_sym_tab, structs_sym_tab): diff --git a/tests/failing_tests/var_rval.py b/tests/passing_tests/var_rval.py similarity index 55% rename from tests/failing_tests/var_rval.py rename to tests/passing_tests/var_rval.py index 38d993e..3fd2c10 100644 --- a/tests/failing_tests/var_rval.py +++ b/tests/passing_tests/var_rval.py @@ -1,4 +1,6 @@ -from pythonbpf import compile, bpf, section, bpfglobal +import logging + +from pythonbpf import compile, bpf, section, bpfglobal, compile_to_ir from ctypes import c_void_p, c_int64 @@ -14,5 +16,5 @@ def sometag(ctx: c_void_p) -> c_int64: def LICENSE() -> str: return "GPL" - -compile() +compile_to_ir("var_rval.py", "var_rval.ll") +compile(loglevel=logging.INFO) From be05b5d1027aa29788123755c427667529a38f86 Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Fri, 3 Oct 2025 19:50:56 +0530 Subject: [PATCH 04/13] Allow local symbols to be used within return --- pythonbpf/functions_pass.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/pythonbpf/functions_pass.py b/pythonbpf/functions_pass.py index a8f106a..38355ef 100644 --- a/pythonbpf/functions_pass.py +++ b/pythonbpf/functions_pass.py @@ -417,6 +417,19 @@ def process_stmt( ) builder.ret(val[0]) did_return = True + elif isinstance(stmt.value.args[0], ast.Name): + if stmt.value.args[0].id in local_sym_tab: + var = local_sym_tab[stmt.value.args[0].id].var + val = builder.load(var) + if val.type != ret_type: + raise ValueError( + "Return type mismatch: expected" + f"{ret_type}, got {val.type}" + ) + builder.ret(val) + did_return = True + else: + raise ValueError("Failed to evaluate return expression") elif isinstance(stmt.value, ast.Name): if stmt.value.id == "XDP_PASS": builder.ret(ir.Constant(ret_type, 2)) From f41a9ccf266cfea7ddb0135e4c50b79e4aa42251 Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Sat, 4 Oct 2025 02:07:31 +0530 Subject: [PATCH 05/13] Remove unnecessary args from binary_ops --- pythonbpf/binary_ops.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/pythonbpf/binary_ops.py b/pythonbpf/binary_ops.py index 1b25c9c..dbc7a58 100644 --- a/pythonbpf/binary_ops.py +++ b/pythonbpf/binary_ops.py @@ -9,6 +9,7 @@ def recursive_dereferencer(var, builder): """dereference until primitive type comes out""" # TODO: Not worrying about stack overflow for now + logger.info(f"Dereferencing {var}, type is {var.type}") if isinstance(var.type, ir.PointerType): a = builder.load(var) return recursive_dereferencer(a, builder) @@ -18,7 +19,7 @@ def recursive_dereferencer(var, builder): raise TypeError(f"Unsupported type for dereferencing: {var.type}") -def get_operand_value(operand, module, builder, local_sym_tab): +def get_operand_value(operand, builder, local_sym_tab): """Extract the value from an operand, handling variables and constants.""" if isinstance(operand, ast.Name): if operand.id in local_sym_tab: @@ -29,14 +30,14 @@ def get_operand_value(operand, module, builder, local_sym_tab): return ir.Constant(ir.IntType(64), operand.value) raise TypeError(f"Unsupported constant type: {type(operand.value)}") elif isinstance(operand, ast.BinOp): - return handle_binary_op_impl(operand, module, builder, local_sym_tab) + return handle_binary_op_impl(operand, builder, local_sym_tab) raise TypeError(f"Unsupported operand type: {type(operand)}") -def handle_binary_op_impl(rval, module, builder, local_sym_tab): +def handle_binary_op_impl(rval, builder, local_sym_tab): op = rval.op - left = get_operand_value(rval.left, module, builder, local_sym_tab) - right = get_operand_value(rval.right, module, builder, local_sym_tab) + left = get_operand_value(rval.left, builder, local_sym_tab) + right = get_operand_value(rval.right, builder, local_sym_tab) logger.info(f"left is {left}, right is {right}, op is {op}") # Map AST operation nodes to LLVM IR builder methods @@ -61,8 +62,8 @@ def handle_binary_op_impl(rval, module, builder, local_sym_tab): raise SyntaxError("Unsupported binary operation") -def handle_binary_op(rval, module, builder, var_name, local_sym_tab): - result = handle_binary_op_impl(rval, module, builder, local_sym_tab) +def handle_binary_op(rval, builder, var_name, local_sym_tab): + result = handle_binary_op_impl(rval, builder, local_sym_tab) if var_name in local_sym_tab: builder.store(result, local_sym_tab[var_name].var) return result, result.type From 1239d1c35fb2c32aa858bb8140de0fde17c57f63 Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Sat, 4 Oct 2025 02:09:11 +0530 Subject: [PATCH 06/13] Fix handle_binary_ops calls in functions_pass --- pythonbpf/functions_pass.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/pythonbpf/functions_pass.py b/pythonbpf/functions_pass.py index 38355ef..019344c 100644 --- a/pythonbpf/functions_pass.py +++ b/pythonbpf/functions_pass.py @@ -233,7 +233,7 @@ def handle_assign( else: logger.info("Unsupported assignment call function type") elif isinstance(rval, ast.BinOp): - handle_binary_op(rval, module, builder, var_name, local_sym_tab) + handle_binary_op(rval, builder, var_name, local_sym_tab) else: logger.info("Unsupported assignment value type") @@ -406,9 +406,7 @@ def process_stmt( did_return = True elif isinstance(stmt.value.args[0], ast.BinOp): # TODO: Should be routed through eval_expr - val = handle_binary_op( - stmt.value.args[0], module, builder, None, local_sym_tab - ) + val = handle_binary_op(stmt.value.args[0], builder, None, local_sym_tab) if val is None: raise ValueError("Failed to evaluate return expression") if val[1] != ret_type: From af44bd063cb90adf2d80f6d42fd8514616d33a7e Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Sat, 4 Oct 2025 02:13:46 +0530 Subject: [PATCH 07/13] Add explanation for direct_assign.py failing test --- tests/failing_tests/direct_assign.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/tests/failing_tests/direct_assign.py b/tests/failing_tests/direct_assign.py index 18ff266..5d2454e 100644 --- a/tests/failing_tests/direct_assign.py +++ b/tests/failing_tests/direct_assign.py @@ -4,6 +4,18 @@ from ctypes import c_void_p, c_int64 +# NOTE: I have decided to not fix this example for now. +# The issue is in line 31, where we are passing an expression. +# The update helper expects a pointer type. But the problem is +# that we must allocate the space for said pointer in the first +# basic block. As that usage is in a different basic block, we +# are unable to cast the expression to a pointer type. (as we never +# allocated space for it). +# Shall we change our space allocation logic? That allows users to +# spam the same helper with the same args, and still run out of +# stack space. So we consider this usage invalid for now. +# Might fix it later. + @bpf @map From ac49cd8b1ca7976e530e6e2845814d72a25c02d8 Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Sat, 4 Oct 2025 02:14:33 +0530 Subject: [PATCH 08/13] Fix hashmap access in direct_assign.py --- tests/failing_tests/direct_assign.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/failing_tests/direct_assign.py b/tests/failing_tests/direct_assign.py index 5d2454e..a784313 100644 --- a/tests/failing_tests/direct_assign.py +++ b/tests/failing_tests/direct_assign.py @@ -26,12 +26,12 @@ def count() -> HashMap: @bpf @section("xdp") def hello_world(ctx: c_void_p) -> c_int64: - prev = count().lookup(0) + prev = count.lookup(0) if prev: - count().update(0, prev + 1) + count.update(0, prev + 1) return XDP_PASS else: - count().update(0, 1) + count.update(0, 1) return XDP_PASS From 283b947fc5ac2ecf2bdda99e39ea4c91c5eb72fe Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Sat, 4 Oct 2025 19:50:33 +0530 Subject: [PATCH 09/13] Add named_arg failing test --- tests/failing_tests/named_arg.py | 34 ++++++++++++++++++++++++++++++++ tests/passing_tests/var_rval.py | 4 ++-- 2 files changed, 36 insertions(+), 2 deletions(-) create mode 100644 tests/failing_tests/named_arg.py diff --git a/tests/failing_tests/named_arg.py b/tests/failing_tests/named_arg.py new file mode 100644 index 0000000..efa1c69 --- /dev/null +++ b/tests/failing_tests/named_arg.py @@ -0,0 +1,34 @@ +from pythonbpf import bpf, map, section, bpfglobal, compile +from pythonbpf.helper import XDP_PASS +from pythonbpf.maps import HashMap + +from ctypes import c_void_p, c_int64 + + +@bpf +@map +def count() -> HashMap: + return HashMap(key=c_int64, value=c_int64, max_entries=1) + + +@bpf +@section("xdp") +def hello_world(ctx: c_void_p) -> c_int64: + prev = count.lookup(0) + if prev: + prev = prev + 1 + count.update(0, prev) + return XDP_PASS + else: + count.update(0, 1) + + return XDP_PASS + + +@bpf +@bpfglobal +def LICENSE() -> str: + return "GPL" + + +compile() diff --git a/tests/passing_tests/var_rval.py b/tests/passing_tests/var_rval.py index 3fd2c10..ee1735e 100644 --- a/tests/passing_tests/var_rval.py +++ b/tests/passing_tests/var_rval.py @@ -1,6 +1,6 @@ import logging -from pythonbpf import compile, bpf, section, bpfglobal, compile_to_ir +from pythonbpf import compile, bpf, section, bpfglobal from ctypes import c_void_p, c_int64 @@ -16,5 +16,5 @@ def sometag(ctx: c_void_p) -> c_int64: def LICENSE() -> str: return "GPL" -compile_to_ir("var_rval.py", "var_rval.ll") + compile(loglevel=logging.INFO) From 2fabb67942f8e75f80376fbbe945921568cc87d4 Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Sun, 5 Oct 2025 03:15:17 +0530 Subject: [PATCH 10/13] Add note for faling test named_arg --- tests/failing_tests/named_arg.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/tests/failing_tests/named_arg.py b/tests/failing_tests/named_arg.py index efa1c69..edfe324 100644 --- a/tests/failing_tests/named_arg.py +++ b/tests/failing_tests/named_arg.py @@ -4,6 +4,14 @@ from ctypes import c_void_p, c_int64 +# NOTE: This example exposes the problems with our typing system. +# We assign every variable the type i64* by default. +# lookup() return type is ptr, which can't be loaded. +# So we can't do steps on line 25 and 27. +# To counter this, we should allocate vars by speculating their type. +# And in the assign pass, we should have something like a +# recursive_dereferencer() that dereferences a ptr until it hits a non-ptr type. +# And a recursive_wrapper() that does the opposite. @bpf @map From d341cb24c025c59f7f55103d8babe754fa6d68e8 Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Sun, 5 Oct 2025 04:27:37 +0530 Subject: [PATCH 11/13] Update explanation for named_arg --- tests/failing_tests/named_arg.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/tests/failing_tests/named_arg.py b/tests/failing_tests/named_arg.py index edfe324..79ac830 100644 --- a/tests/failing_tests/named_arg.py +++ b/tests/failing_tests/named_arg.py @@ -5,13 +5,11 @@ from ctypes import c_void_p, c_int64 # NOTE: This example exposes the problems with our typing system. -# We assign every variable the type i64* by default. -# lookup() return type is ptr, which can't be loaded. -# So we can't do steps on line 25 and 27. -# To counter this, we should allocate vars by speculating their type. -# And in the assign pass, we should have something like a -# recursive_dereferencer() that dereferences a ptr until it hits a non-ptr type. -# And a recursive_wrapper() that does the opposite. +# We can't do steps on line 25 and 27. +# prev is of type i64**. For prev + 1, we deref it down to i64 +# To assign it back to prev, we need to go back to i64**. +# We cannot allocate space for the intermediate type now. +# We probably need to track the ref/deref chain for each variable. @bpf @map From ef36ea1e034a1e5fac80b5d83de1d93c4b49300f Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Sun, 5 Oct 2025 14:02:08 +0530 Subject: [PATCH 12/13] Add nullcheck for var_name in handle_binary_ops --- pythonbpf/binary_ops.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pythonbpf/binary_ops.py b/pythonbpf/binary_ops.py index dbc7a58..c0ca0ec 100644 --- a/pythonbpf/binary_ops.py +++ b/pythonbpf/binary_ops.py @@ -64,6 +64,9 @@ def handle_binary_op_impl(rval, builder, local_sym_tab): def handle_binary_op(rval, builder, var_name, local_sym_tab): result = handle_binary_op_impl(rval, builder, local_sym_tab) - if var_name in local_sym_tab: + if var_name and var_name in local_sym_tab: + logger.info( + f"Storing result {result} into variable {local_sym_tab[var_name].var}" + ) builder.store(result, local_sym_tab[var_name].var) return result, result.type From c7f2955ee97ec318a757df17f63e2769cf34456a Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Sun, 5 Oct 2025 14:03:19 +0530 Subject: [PATCH 13/13] Fix typo in process_stmt --- pythonbpf/functions_pass.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/pythonbpf/functions_pass.py b/pythonbpf/functions_pass.py index 019344c..d5537da 100644 --- a/pythonbpf/functions_pass.py +++ b/pythonbpf/functions_pass.py @@ -411,7 +411,7 @@ def process_stmt( raise ValueError("Failed to evaluate return expression") if val[1] != ret_type: raise ValueError( - "Return type mismatch: expected" f"{ret_type}, got {val[1]}" + "Return type mismatch: expected " f"{ret_type}, got {val[1]}" ) builder.ret(val[0]) did_return = True @@ -480,6 +480,9 @@ def allocate_mem( continue var_name = target.id rval = stmt.value + if var_name in local_sym_tab: + logger.info(f"Variable {var_name} already allocated") + continue if isinstance(rval, ast.Call): if isinstance(rval.func, ast.Name): call_type = rval.func.id