From 4a60c42cd0608541d6a05072f26834c20b76d3a0 Mon Sep 17 00:00:00 2001 From: varun-r-mallya Date: Fri, 3 Oct 2025 21:25:58 +0530 Subject: [PATCH 01/60] add global failing test Signed-off-by: varun-r-mallya --- tests/c-form/globals.bpf.c | 0 tests/failing_tests/globals.py | 0 2 files changed, 0 insertions(+), 0 deletions(-) create mode 100644 tests/c-form/globals.bpf.c create mode 100644 tests/failing_tests/globals.py diff --git a/tests/c-form/globals.bpf.c b/tests/c-form/globals.bpf.c new file mode 100644 index 0000000..e69de29 diff --git a/tests/failing_tests/globals.py b/tests/failing_tests/globals.py new file mode 100644 index 0000000..e69de29 From c3a512d5cf737346e0003ba2283c59433b266d5a Mon Sep 17 00:00:00 2001 From: varun-r-mallya Date: Fri, 3 Oct 2025 22:20:04 +0530 Subject: [PATCH 02/60] add global support with broken generation function --- pythonbpf/codegen.py | 3 +- pythonbpf/globals_pass.py | 86 +++++++++++++++++++++++++++++----- tests/c-form/globals.bpf.c | 27 +++++++++++ tests/failing_tests/globals.py | 23 +++++++++ 4 files changed, 127 insertions(+), 12 deletions(-) diff --git a/pythonbpf/codegen.py b/pythonbpf/codegen.py index 5de23a5..e0cb7b6 100644 --- a/pythonbpf/codegen.py +++ b/pythonbpf/codegen.py @@ -4,7 +4,7 @@ from .license_pass import license_processing from .functions_pass import func_proc from .maps import maps_proc from .structs import structs_proc -from .globals_pass import globals_processing +from .globals_pass import globals_list_creation, globals_processing from .debuginfo import DW_LANG_C11, DwarfBehaviorEnum, DebugInfoGenerator import os import subprocess @@ -46,6 +46,7 @@ def processor(source_code, filename, module): license_processing(tree, module) globals_processing(tree, module) + globals_list_creation(tree, module) def compile_to_ir(filename: str, output: str, loglevel=logging.WARNING): diff --git a/pythonbpf/globals_pass.py b/pythonbpf/globals_pass.py index 1228809..86ba19f 100644 --- a/pythonbpf/globals_pass.py +++ b/pythonbpf/globals_pass.py @@ -1,8 +1,71 @@ from llvmlite import ir import ast +from llvmlite import ir +import ast +from logging import Logger +import logging +from .type_deducer import ctypes_to_ir -def emit_globals(module: ir.Module, names: list[str]): +logger: Logger = logging.getLogger(__name__) + + +def emit_global(module: ir.Module, node, name): + print("global", node.returns.id) + ty = ctypes_to_ir(node.returns.id) + + gvar = ir.GlobalVariable(module, ty, name=name) + gvar.initializer = ir.Constant(ty, initial_value) + gvar.align = 8 + gvar.linkage = "dso_local" + gvar.global_constant = False + return gvar + + +def globals_processing(tree, module): + """Process stuff decorated with @bpf and @bpfglobal except license and return the section name""" + global_sym_tab = [] + + for node in tree.body: + # Skip non-assignment and non-function nodes + if not (isinstance(node, (ast.FunctionDef, ast.AnnAssign, ast.Assign))): + continue + + # Get the name based on node type + if isinstance(node, ast.FunctionDef): + name = node.name + else: + continue + + # Check for duplicate names + if name in global_sym_tab: + raise SyntaxError(f"ERROR: Global name '{name}' previously defined") + else: + global_sym_tab.append(name) + + # Process decorated functions + if isinstance(node, ast.FunctionDef) and node.name != "LICENSE": + # Check decorators + decorators = [ + dec.id for dec in node.decorator_list if isinstance(dec, ast.Name) + ] + + if "bpf" in decorators and "bpfglobal" in decorators: + if ( + len(node.body) == 1 + and isinstance(node.body[0], ast.Return) + and node.body[0].value is not None + and isinstance(node.body[0].value, (ast.Constant, ast.Name)) + ): + emit_global(module, node, name) + return node.name + else: + logger.info(f"Invalid global expression for '{node.name}'") + return None + + return None + +def emit_llvm_compiler_used(module: ir.Module, names: list[str]): """ Emit the @llvm.compiler.used global given a list of function/global names. """ @@ -24,26 +87,27 @@ def emit_globals(module: ir.Module, names: list[str]): gv.section = "llvm.metadata" -def globals_processing(tree, module: ir.Module): +def globals_list_creation(tree, module: ir.Module): collected = ["LICENSE"] for node in tree.body: if isinstance(node, ast.FunctionDef): for dec in node.decorator_list: if ( - isinstance(dec, ast.Call) - and isinstance(dec.func, ast.Name) - and dec.func.id == "section" - and len(dec.args) == 1 - and isinstance(dec.args[0], ast.Constant) - and isinstance(dec.args[0].value, str) + isinstance(dec, ast.Call) + and isinstance(dec.func, ast.Name) + and dec.func.id == "section" + and len(dec.args) == 1 + and isinstance(dec.args[0], ast.Constant) + and isinstance(dec.args[0].value, str) ): collected.append(node.name) - elif isinstance(dec, ast.Name) and dec.id == "bpfglobal": - collected.append(node.name) + # NOTE: all globals other than + # elif isinstance(dec, ast.Name) and dec.id == "bpfglobal": + # collected.append(node.name) elif isinstance(dec, ast.Name) and dec.id == "map": collected.append(node.name) - emit_globals(module, collected) + emit_llvm_compiler_used(module, collected) diff --git a/tests/c-form/globals.bpf.c b/tests/c-form/globals.bpf.c index e69de29..588cac5 100644 --- a/tests/c-form/globals.bpf.c +++ b/tests/c-form/globals.bpf.c @@ -0,0 +1,27 @@ +// SPDX-License-Identifier: GPL-2.0 OR BSD-3-Clause +#include +#include +#include +#include + +struct test_struct { + __u64 a; + __u64 b; +}; + +struct test_struct w = {}; +volatile __u64 prev_time = 0; + +SEC("tracepoint/syscalls/sys_enter_execve") +int trace_execve(void *ctx) +{ + bpf_printk("previous %ul now %ul", w.b, w.a); + __u64 ts = bpf_ktime_get_ns(); + bpf_printk("prev %ul now %ul", prev_time, ts); + w.a = ts; + w.b = prev_time; + prev_time = ts; + return 0; +} + +char LICENSE[] SEC("license") = "GPL"; diff --git a/tests/failing_tests/globals.py b/tests/failing_tests/globals.py index e69de29..e1b9222 100644 --- a/tests/failing_tests/globals.py +++ b/tests/failing_tests/globals.py @@ -0,0 +1,23 @@ +import logging + +from pythonbpf import compile, bpf, section, bpfglobal, compile_to_ir +from ctypes import c_void_p, c_int64 + +@bpf +@bpfglobal +def somevalue() -> c_int64: + return c_int64(0) + +@bpf +@section("sometag1") +def sometag(ctx: c_void_p) -> c_int64: + return c_int64(0) + +@bpf +@bpfglobal +def LICENSE() -> str: + return "GPL" + + +compile_to_ir("globals.py", "globals.ll", loglevel=logging.INFO) +compile() From ab1c4223d5514bdabf323ebaa828d7990446dbea Mon Sep 17 00:00:00 2001 From: varun-r-mallya Date: Fri, 3 Oct 2025 22:55:40 +0530 Subject: [PATCH 03/60] fix broken IR generation logic for globals --- pythonbpf/globals_pass.py | 74 +++++++++++++++++++++++----------- tests/failing_tests/globals.py | 19 +++++++-- 2 files changed, 65 insertions(+), 28 deletions(-) diff --git a/pythonbpf/globals_pass.py b/pythonbpf/globals_pass.py index 86ba19f..4178f0f 100644 --- a/pythonbpf/globals_pass.py +++ b/pythonbpf/globals_pass.py @@ -1,8 +1,6 @@ from llvmlite import ir import ast -from llvmlite import ir -import ast from logging import Logger import logging from .type_deducer import ctypes_to_ir @@ -11,11 +9,41 @@ logger: Logger = logging.getLogger(__name__) def emit_global(module: ir.Module, node, name): - print("global", node.returns.id) + logger.info(f"global identifier {name} processing") + # TODO: below part is LLM generated check logic. + # deduce LLVM type from the annotated return + if not isinstance(node.returns, ast.Name): + raise ValueError(f"Unsupported return annotation {ast.dump(node.returns)}") ty = ctypes_to_ir(node.returns.id) + # extract the return expression + ret_stmt = node.body[0] + if not isinstance(ret_stmt, ast.Return) or ret_stmt.value is None: + raise ValueError(f"Global '{name}' has no valid return") + + init_val = ret_stmt.value + + # simple constant like "return 0" + if isinstance(init_val, ast.Constant): + llvm_init = ir.Constant(ty, init_val.value) + + # variable reference like "return SOME_CONST" + elif isinstance(init_val, ast.Name): + # you may need symbol resolution here, stub as 0 for now + raise ValueError(f"Name reference {init_val.id} not yet supported") + + # constructor call like "return c_int64(0)" or dataclass(...) + elif isinstance(init_val, ast.Call): + if len(init_val.args) == 1 and isinstance(init_val.args[0], ast.Constant): + llvm_init = ir.Constant(ty, init_val.args[0].value) + else: + raise ValueError(f"Complex constructor not supported: {ast.dump(init_val)}") + + else: + raise ValueError(f"Unsupported return expr {ast.dump(init_val)}") + gvar = ir.GlobalVariable(module, ty, name=name) - gvar.initializer = ir.Constant(ty, initial_value) + gvar.initializer = llvm_init gvar.align = 8 gvar.linkage = "dso_local" gvar.global_constant = False @@ -24,11 +52,11 @@ def emit_global(module: ir.Module, node, name): def globals_processing(tree, module): """Process stuff decorated with @bpf and @bpfglobal except license and return the section name""" - global_sym_tab = [] + globals_sym_tab = [] for node in tree.body: # Skip non-assignment and non-function nodes - if not (isinstance(node, (ast.FunctionDef, ast.AnnAssign, ast.Assign))): + if not (isinstance(node, ast.FunctionDef)): continue # Get the name based on node type @@ -38,33 +66,31 @@ def globals_processing(tree, module): continue # Check for duplicate names - if name in global_sym_tab: + if name in globals_sym_tab: raise SyntaxError(f"ERROR: Global name '{name}' previously defined") else: - global_sym_tab.append(name) + globals_sym_tab.append(name) - # Process decorated functions if isinstance(node, ast.FunctionDef) and node.name != "LICENSE": - # Check decorators decorators = [ dec.id for dec in node.decorator_list if isinstance(dec, ast.Name) ] - if "bpf" in decorators and "bpfglobal" in decorators: if ( - len(node.body) == 1 - and isinstance(node.body[0], ast.Return) - and node.body[0].value is not None - and isinstance(node.body[0].value, (ast.Constant, ast.Name)) + len(node.body) == 1 + and isinstance(node.body[0], ast.Return) + and node.body[0].value is not None + and isinstance( + node.body[0].value, (ast.Constant, ast.Name, ast.Call) + ) ): emit_global(module, node, name) - return node.name else: - logger.info(f"Invalid global expression for '{node.name}'") - return None + raise SyntaxError(f"ERROR: Invalid syntax for {name} global") return None + def emit_llvm_compiler_used(module: ir.Module, names: list[str]): """ Emit the @llvm.compiler.used global given a list of function/global names. @@ -94,12 +120,12 @@ def globals_list_creation(tree, module: ir.Module): if isinstance(node, ast.FunctionDef): for dec in node.decorator_list: if ( - isinstance(dec, ast.Call) - and isinstance(dec.func, ast.Name) - and dec.func.id == "section" - and len(dec.args) == 1 - and isinstance(dec.args[0], ast.Constant) - and isinstance(dec.args[0].value, str) + isinstance(dec, ast.Call) + and isinstance(dec.func, ast.Name) + and dec.func.id == "section" + and len(dec.args) == 1 + and isinstance(dec.args[0], ast.Constant) + and isinstance(dec.args[0].value, str) ): collected.append(node.name) diff --git a/tests/failing_tests/globals.py b/tests/failing_tests/globals.py index e1b9222..05ac5fd 100644 --- a/tests/failing_tests/globals.py +++ b/tests/failing_tests/globals.py @@ -1,17 +1,28 @@ import logging from pythonbpf import compile, bpf, section, bpfglobal, compile_to_ir -from ctypes import c_void_p, c_int64 +from ctypes import c_void_p, c_int64, c_int32 @bpf @bpfglobal -def somevalue() -> c_int64: +def somevalue() -> c_int32: + return c_int32(0) + +@bpf +@bpfglobal +def somevalue2() -> c_int64: return c_int64(0) @bpf -@section("sometag1") +@bpfglobal +def somevalue1() -> c_int32: + return c_int32(0) + +@bpf +@section("tracepoint/syscalls/sys_enter_execve") def sometag(ctx: c_void_p) -> c_int64: - return c_int64(0) + print("test") + return c_int64(1) @bpf @bpfglobal From 7aeac86bd34c2171b9b55100663928f1308a7d57 Mon Sep 17 00:00:00 2001 From: varun-r-mallya Date: Sat, 4 Oct 2025 06:32:25 +0530 Subject: [PATCH 04/60] fix broken IR generation logic for globals --- pythonbpf/globals_pass.py | 7 ++-- tests/failing_tests/globals.py | 67 ++++++++++++++++++++++++++++++++++ 2 files changed, 70 insertions(+), 4 deletions(-) diff --git a/pythonbpf/globals_pass.py b/pythonbpf/globals_pass.py index 4178f0f..198d813 100644 --- a/pythonbpf/globals_pass.py +++ b/pythonbpf/globals_pass.py @@ -34,11 +34,11 @@ def emit_global(module: ir.Module, node, name): # constructor call like "return c_int64(0)" or dataclass(...) elif isinstance(init_val, ast.Call): - if len(init_val.args) == 1 and isinstance(init_val.args[0], ast.Constant): + if len(init_val.args) >= 1 and isinstance(init_val.args[0], ast.Constant): llvm_init = ir.Constant(ty, init_val.args[0].value) else: - raise ValueError(f"Complex constructor not supported: {ast.dump(init_val)}") - + logger.info("Defaulting to zero as no constant argument found") + llvm_init = ir.Constant(ty, 0) else: raise ValueError(f"Unsupported return expr {ast.dump(init_val)}") @@ -49,7 +49,6 @@ def emit_global(module: ir.Module, node, name): gvar.global_constant = False return gvar - def globals_processing(tree, module): """Process stuff decorated with @bpf and @bpfglobal except license and return the section name""" globals_sym_tab = [] diff --git a/tests/failing_tests/globals.py b/tests/failing_tests/globals.py index 05ac5fd..6b914fd 100644 --- a/tests/failing_tests/globals.py +++ b/tests/failing_tests/globals.py @@ -18,6 +18,73 @@ def somevalue2() -> c_int64: def somevalue1() -> c_int32: return c_int32(0) +import ast +from dataclasses import dataclass +from typing import List + +# --- Passing examples --- + +# Simple constant return +@bpf +@bpfglobal +def g1() -> c_int64: + return 42 + +# Constructor with one constant argument +@bpf +@bpfglobal +def g2() -> c_int64: + return c_int64(0) + + +# --- Failing examples --- + +# No return annotation +# @bpf +# @bpfglobal +# def g3(): +# return 42 + +# Return annotation is complex +# @bpf +# @bpfglobal +# def g4() -> List[int]: +# return [] + +# # Return is missing +# @bpf +# @bpfglobal +# def g5() -> c_int64: +# pass + +# # Return is a variable reference +# #TODO: maybe fix this sometime later. It defaults to 0 +CONST = 5 +@bpf +@bpfglobal +def g6() -> c_int64: + return c_int64(CONST) + +# Constructor with multiple args +#TODO: this is not working. should it work ? +@bpf +@bpfglobal +def g7() -> c_int64: + return c_int64(1, 2) + +# Dataclass call +#TODO: fails with dataclass +# @dataclass +# class Point: +# x: c_int64 +# y: c_int64 + +# @bpf +# @bpfglobal +# def g8() -> Point: +# return Point(1, 2) + + @bpf @section("tracepoint/syscalls/sys_enter_execve") def sometag(ctx: c_void_p) -> c_int64: From 7720fe9f9fb75d22d4c4f9f204cfa53bf8159ee3 Mon Sep 17 00:00:00 2001 From: varun-r-mallya Date: Sat, 4 Oct 2025 06:33:09 +0530 Subject: [PATCH 05/60] format chore --- pythonbpf/globals_pass.py | 1 + tests/failing_tests/globals.py | 3 --- 2 files changed, 1 insertion(+), 3 deletions(-) diff --git a/pythonbpf/globals_pass.py b/pythonbpf/globals_pass.py index 198d813..4c6ed97 100644 --- a/pythonbpf/globals_pass.py +++ b/pythonbpf/globals_pass.py @@ -49,6 +49,7 @@ def emit_global(module: ir.Module, node, name): gvar.global_constant = False return gvar + def globals_processing(tree, module): """Process stuff decorated with @bpf and @bpfglobal except license and return the section name""" globals_sym_tab = [] diff --git a/tests/failing_tests/globals.py b/tests/failing_tests/globals.py index 6b914fd..ae526a5 100644 --- a/tests/failing_tests/globals.py +++ b/tests/failing_tests/globals.py @@ -18,9 +18,6 @@ def somevalue2() -> c_int64: def somevalue1() -> c_int32: return c_int32(0) -import ast -from dataclasses import dataclass -from typing import List # --- Passing examples --- From ab610147a53c68a1d7f6db671dcf9e2b4bdb0567 Mon Sep 17 00:00:00 2001 From: varun-r-mallya Date: Sat, 4 Oct 2025 06:36:26 +0530 Subject: [PATCH 06/60] update globals test and todos. --- pythonbpf/globals_pass.py | 4 ++-- tests/failing_tests/globals.py | 8 ++++---- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/pythonbpf/globals_pass.py b/pythonbpf/globals_pass.py index 4c6ed97..b73a072 100644 --- a/pythonbpf/globals_pass.py +++ b/pythonbpf/globals_pass.py @@ -10,13 +10,13 @@ logger: Logger = logging.getLogger(__name__) def emit_global(module: ir.Module, node, name): logger.info(f"global identifier {name} processing") - # TODO: below part is LLM generated check logic. # deduce LLVM type from the annotated return if not isinstance(node.returns, ast.Name): raise ValueError(f"Unsupported return annotation {ast.dump(node.returns)}") ty = ctypes_to_ir(node.returns.id) # extract the return expression + # TODO: turn this return extractor into a generic function I can use everywhere. ret_stmt = node.body[0] if not isinstance(ret_stmt, ast.Return) or ret_stmt.value is None: raise ValueError(f"Global '{name}' has no valid return") @@ -29,7 +29,7 @@ def emit_global(module: ir.Module, node, name): # variable reference like "return SOME_CONST" elif isinstance(init_val, ast.Name): - # you may need symbol resolution here, stub as 0 for now + # need symbol resolution here, stub as 0 for now raise ValueError(f"Name reference {init_val.id} not yet supported") # constructor call like "return c_int64(0)" or dataclass(...) diff --git a/tests/failing_tests/globals.py b/tests/failing_tests/globals.py index ae526a5..02c604e 100644 --- a/tests/failing_tests/globals.py +++ b/tests/failing_tests/globals.py @@ -6,17 +6,17 @@ from ctypes import c_void_p, c_int64, c_int32 @bpf @bpfglobal def somevalue() -> c_int32: - return c_int32(0) + return c_int32(42) @bpf @bpfglobal def somevalue2() -> c_int64: - return c_int64(0) + return c_int64(69) @bpf @bpfglobal def somevalue1() -> c_int32: - return c_int32(0) + return c_int32(42) # --- Passing examples --- @@ -31,7 +31,7 @@ def g1() -> c_int64: @bpf @bpfglobal def g2() -> c_int64: - return c_int64(0) + return c_int64(69) # --- Failing examples --- From df3f00261abf8f586b2645151f0881d30f3cd93a Mon Sep 17 00:00:00 2001 From: varun-r-mallya Date: Sat, 4 Oct 2025 08:17:16 +0530 Subject: [PATCH 07/60] changer order of passes --- pythonbpf/codegen.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/pythonbpf/codegen.py b/pythonbpf/codegen.py index e0cb7b6..3b8f76a 100644 --- a/pythonbpf/codegen.py +++ b/pythonbpf/codegen.py @@ -40,12 +40,13 @@ def processor(source_code, filename, module): for func_node in bpf_chunks: logger.info(f"Found BPF function/struct: {func_node.name}") + license_processing(tree, module) + globals_processing(tree, module) + structs_sym_tab = structs_proc(tree, module, bpf_chunks) map_sym_tab = maps_proc(tree, module, bpf_chunks) func_proc(tree, module, bpf_chunks, map_sym_tab, structs_sym_tab) - license_processing(tree, module) - globals_processing(tree, module) globals_list_creation(tree, module) From 7ae84a0d5ad3240078a2e544c5e9de2f05184f0b Mon Sep 17 00:00:00 2001 From: varun-r-mallya Date: Sun, 5 Oct 2025 00:55:22 +0530 Subject: [PATCH 08/60] add failing test --- tests/failing_tests/undeclared_values.py | 21 +++++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 tests/failing_tests/undeclared_values.py diff --git a/tests/failing_tests/undeclared_values.py b/tests/failing_tests/undeclared_values.py new file mode 100644 index 0000000..1cd0d59 --- /dev/null +++ b/tests/failing_tests/undeclared_values.py @@ -0,0 +1,21 @@ +import logging + +from pythonbpf import compile, bpf, section, bpfglobal, compile_to_ir +from ctypes import c_void_p, c_int64 + +# This should not pass as somevalue is not declared at all. +@bpf +@section("tracepoint/syscalls/sys_enter_execve") +def sometag(ctx: c_void_p) -> c_int64: + print("test") + print(f"{somevalue}") + return c_int64(1) + +@bpf +@bpfglobal +def LICENSE() -> str: + return "GPL" + + +compile_to_ir("globals.py", "globals.ll", loglevel=logging.INFO) +compile() From 01bd7604edac1f35765db1141c21ccfb0b98b0af Mon Sep 17 00:00:00 2001 From: varun-r-mallya Date: Sun, 5 Oct 2025 14:02:46 +0530 Subject: [PATCH 09/60] add global symbol table populate function --- pythonbpf/codegen.py | 7 ++++++- pythonbpf/globals_pass.py | 24 ++++++++++++++++++++++++ tests/failing_tests/globals.py | 17 ++++++++++------- 3 files changed, 40 insertions(+), 8 deletions(-) diff --git a/pythonbpf/codegen.py b/pythonbpf/codegen.py index 3b8f76a..772c2b2 100644 --- a/pythonbpf/codegen.py +++ b/pythonbpf/codegen.py @@ -4,7 +4,11 @@ from .license_pass import license_processing from .functions_pass import func_proc from .maps import maps_proc from .structs import structs_proc -from .globals_pass import globals_list_creation, globals_processing +from .globals_pass import ( + globals_list_creation, + globals_processing, + populate_global_symbol_table, +) from .debuginfo import DW_LANG_C11, DwarfBehaviorEnum, DebugInfoGenerator import os import subprocess @@ -40,6 +44,7 @@ def processor(source_code, filename, module): for func_node in bpf_chunks: logger.info(f"Found BPF function/struct: {func_node.name}") + populate_global_symbol_table(tree, module) license_processing(tree, module) globals_processing(tree, module) diff --git a/pythonbpf/globals_pass.py b/pythonbpf/globals_pass.py index b73a072..1e97763 100644 --- a/pythonbpf/globals_pass.py +++ b/pythonbpf/globals_pass.py @@ -7,6 +7,30 @@ from .type_deducer import ctypes_to_ir logger: Logger = logging.getLogger(__name__) +# TODO: this is going to be a huge fuck of a headache in the future. +global_sym_tab = [] + + +def populate_global_symbol_table(tree, module: ir.Module): + for node in tree.body: + if isinstance(node, ast.FunctionDef): + for dec in node.decorator_list: + if ( + isinstance(dec, ast.Call) + and isinstance(dec.func, ast.Name) + and dec.func.id == "section" + and len(dec.args) == 1 + and isinstance(dec.args[0], ast.Constant) + and isinstance(dec.args[0].value, str) + ): + global_sym_tab.append(node) + elif isinstance(dec, ast.Name) and dec.id == "bpfglobal": + global_sym_tab.append(node) + + elif isinstance(dec, ast.Name) and dec.id == "map": + global_sym_tab.append(node) + return False + def emit_global(module: ir.Module, node, name): logger.info(f"global identifier {name} processing") diff --git a/tests/failing_tests/globals.py b/tests/failing_tests/globals.py index 02c604e..55d9740 100644 --- a/tests/failing_tests/globals.py +++ b/tests/failing_tests/globals.py @@ -25,7 +25,7 @@ def somevalue1() -> c_int32: @bpf @bpfglobal def g1() -> c_int64: - return 42 + return c_int64(42) # Constructor with one constant argument @bpf @@ -56,18 +56,18 @@ def g2() -> c_int64: # # Return is a variable reference # #TODO: maybe fix this sometime later. It defaults to 0 -CONST = 5 -@bpf -@bpfglobal -def g6() -> c_int64: - return c_int64(CONST) +# CONST = 5 +# @bpf +# @bpfglobal +# def g6() -> c_int64: +# return c_int64(CONST) # Constructor with multiple args #TODO: this is not working. should it work ? @bpf @bpfglobal def g7() -> c_int64: - return c_int64(1, 2) + return c_int64(1) # Dataclass call #TODO: fails with dataclass @@ -86,6 +86,9 @@ def g7() -> c_int64: @section("tracepoint/syscalls/sys_enter_execve") def sometag(ctx: c_void_p) -> c_int64: print("test") + global somevalue + somevalue = 2 + print(f"{somevalue}") return c_int64(1) @bpf From 3abe07c5b27bb689b6a0fadfa8d81ca16dd3c32c Mon Sep 17 00:00:00 2001 From: varun-r-mallya Date: Sun, 5 Oct 2025 14:02:46 +0530 Subject: [PATCH 10/60] add global symbol table populate function --- tests/failing_tests/undeclared_values.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/failing_tests/undeclared_values.py b/tests/failing_tests/undeclared_values.py index 1cd0d59..02f5184 100644 --- a/tests/failing_tests/undeclared_values.py +++ b/tests/failing_tests/undeclared_values.py @@ -8,7 +8,7 @@ from ctypes import c_void_p, c_int64 @section("tracepoint/syscalls/sys_enter_execve") def sometag(ctx: c_void_p) -> c_int64: print("test") - print(f"{somevalue}") + print(f"{somevalue}") #type: ignore return c_int64(1) @bpf From d1055e4d411ed8279239c81692fb034f990f9018 Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Mon, 6 Oct 2025 22:20:54 +0530 Subject: [PATCH 11/60] Reduce a condition from handle_cond --- pythonbpf/functions/functions_pass.py | 4 +--- 1 file changed, 1 insertion(+), 3 deletions(-) diff --git a/pythonbpf/functions/functions_pass.py b/pythonbpf/functions/functions_pass.py index 18904ec..f31d48e 100644 --- a/pythonbpf/functions/functions_pass.py +++ b/pythonbpf/functions/functions_pass.py @@ -242,9 +242,7 @@ def handle_assign( def handle_cond(func, module, builder, cond, local_sym_tab, map_sym_tab): if isinstance(cond, ast.Constant): - if isinstance(cond.value, bool): - return ir.Constant(ir.IntType(1), int(cond.value)) - elif isinstance(cond.value, int): + if isinstance(cond.value, bool) or isinstance(cond.value, int): return ir.Constant(ir.IntType(1), int(bool(cond.value))) else: logger.info("Unsupported constant type in condition") From f11a43010df363e4e3a4f4cb91dd831e0647de51 Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Mon, 6 Oct 2025 22:33:03 +0530 Subject: [PATCH 12/60] Add _handle_cond to expr_pass --- pythonbpf/expr_pass.py | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/pythonbpf/expr_pass.py b/pythonbpf/expr_pass.py index 56d047e..78987cd 100644 --- a/pythonbpf/expr_pass.py +++ b/pythonbpf/expr_pass.py @@ -132,6 +132,12 @@ def _handle_ctypes_call( return val +def _handle_compare( + func, module, builder, expr, local_sym_tab, map_sym_tab, structs_sym_tab=None +): + pass + + def eval_expr( func, module, @@ -212,6 +218,10 @@ 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 + ) logger.info("Unsupported expression evaluation") return None From 6cf5115ea95efe8741e84f78e4c0cfc7d0a9054f Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Mon, 6 Oct 2025 22:38:43 +0530 Subject: [PATCH 13/60] Eval LHS and RHS in _handle_compare --- pythonbpf/expr_pass.py | 32 ++++++++++++++++++++++++++++++-- 1 file changed, 30 insertions(+), 2 deletions(-) diff --git a/pythonbpf/expr_pass.py b/pythonbpf/expr_pass.py index 78987cd..fd673df 100644 --- a/pythonbpf/expr_pass.py +++ b/pythonbpf/expr_pass.py @@ -133,9 +133,37 @@ def _handle_ctypes_call( def _handle_compare( - func, module, builder, expr, local_sym_tab, map_sym_tab, structs_sym_tab=None + func, module, builder, cond, local_sym_tab, map_sym_tab, structs_sym_tab=None ): - pass + 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 None def eval_expr( From 4f433d00cc849db94be530f3c991cf07b8849f9c Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Mon, 6 Oct 2025 23:04:45 +0530 Subject: [PATCH 14/60] Add Boolean return support --- pythonbpf/expr_pass.py | 9 ++++----- tests/passing_tests/return/bool.py | 18 ++++++++++++++++++ 2 files changed, 22 insertions(+), 5 deletions(-) create mode 100644 tests/passing_tests/return/bool.py diff --git a/pythonbpf/expr_pass.py b/pythonbpf/expr_pass.py index fd673df..b037cc3 100644 --- a/pythonbpf/expr_pass.py +++ b/pythonbpf/expr_pass.py @@ -22,12 +22,11 @@ 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) + logger.info("We the best") + 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 diff --git a/tests/passing_tests/return/bool.py b/tests/passing_tests/return/bool.py new file mode 100644 index 0000000..b5627a6 --- /dev/null +++ b/tests/passing_tests/return/bool.py @@ -0,0 +1,18 @@ +from pythonbpf import bpf, section, bpfglobal, compile +from ctypes import c_void_p, c_int64 + + +@bpf +@section("tracepoint/syscalls/sys_enter_execve") +def hello_world(ctx: c_void_p) -> c_int64: + print("Hello, World!") + return True + + +@bpf +@bpfglobal +def LICENSE() -> str: + return "GPL" + + +compile() From fb63dbd698d2d5b464887250c15bd73f3263f826 Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Tue, 7 Oct 2025 03:11:23 +0530 Subject: [PATCH 15/60] Move conditional logic to eval_expr, add _conver_to_bool, add passing bool test --- pythonbpf/expr_pass.py | 28 +++++++++++++++++++++++- pythonbpf/functions/functions_pass.py | 17 ++++++++++++-- tests/passing_tests/conditionals/bool.py | 21 ++++++++++++++++++ 3 files changed, 63 insertions(+), 3 deletions(-) create mode 100644 tests/passing_tests/conditionals/bool.py diff --git a/pythonbpf/expr_pass.py b/pythonbpf/expr_pass.py index b037cc3..79ff4bb 100644 --- a/pythonbpf/expr_pass.py +++ b/pythonbpf/expr_pass.py @@ -131,9 +131,35 @@ def _handle_ctypes_call( return val +def _handle_comparator(builder, op, lhs, rhs): + """Handle comparison operations.""" + + # NOTE: For now assume same types + + comparison_ops = { + ast.Eq: "==", + ast.NotEq: "!=", + ast.Lt: "<", + ast.LtE: "<=", + ast.Gt: ">", + ast.GtE: ">=", + } + + 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) + + 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 @@ -162,7 +188,7 @@ def _handle_compare( lhs, _ = lhs rhs, _ = rhs - return None + return _handle_comparator(builder, cond.ops[0], lhs, rhs) def eval_expr( diff --git a/pythonbpf/functions/functions_pass.py b/pythonbpf/functions/functions_pass.py index f31d48e..7463f54 100644 --- a/pythonbpf/functions/functions_pass.py +++ b/pythonbpf/functions/functions_pass.py @@ -240,12 +240,25 @@ def handle_assign( logger.info("Unsupported assignment value type") +def _convert_to_bool(builder, val): + 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_cond(func, module, builder, cond, local_sym_tab, map_sym_tab): + if True: + val = eval_expr(func, module, builder, cond, local_sym_tab, map_sym_tab)[0] + return _convert_to_bool(builder, val) if isinstance(cond, ast.Constant): if isinstance(cond.value, bool) or isinstance(cond.value, int): - return ir.Constant(ir.IntType(1), int(bool(cond.value))) + return ir.Constant(ir.IntType(1), int(cond.value)) else: - logger.info("Unsupported constant type in condition") + raise ValueError("Unsupported constant type in condition") return None elif isinstance(cond, ast.Name): if cond.id in local_sym_tab: diff --git a/tests/passing_tests/conditionals/bool.py b/tests/passing_tests/conditionals/bool.py new file mode 100644 index 0000000..341fa46 --- /dev/null +++ b/tests/passing_tests/conditionals/bool.py @@ -0,0 +1,21 @@ +from pythonbpf import bpf, section, bpfglobal, compile +from ctypes import c_void_p, c_int64 + + +@bpf +@section("tracepoint/syscalls/sys_enter_execve") +def hello_world(ctx: c_void_p) -> c_int64: + if True: + print("Hello, World!") + else: + print("Goodbye, World!") + return + + +@bpf +@bpfglobal +def LICENSE() -> str: + return "GPL" + + +compile() From 682a7e6566066d930ba095850d808447c374cdf4 Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Tue, 7 Oct 2025 03:15:34 +0530 Subject: [PATCH 16/60] Add const_int test for conditionals --- tests/passing_tests/conditionals/const_int.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 tests/passing_tests/conditionals/const_int.py diff --git a/tests/passing_tests/conditionals/const_int.py b/tests/passing_tests/conditionals/const_int.py new file mode 100644 index 0000000..47589c8 --- /dev/null +++ b/tests/passing_tests/conditionals/const_int.py @@ -0,0 +1,21 @@ +from pythonbpf import bpf, section, bpfglobal, compile +from ctypes import c_void_p, c_int64 + + +@bpf +@section("tracepoint/syscalls/sys_enter_execve") +def hello_world(ctx: c_void_p) -> c_int64: + if 0: + print("Hello, World!") + else: + print("Goodbye, World!") + return + + +@bpf +@bpfglobal +def LICENSE() -> str: + return "GPL" + + +compile() From 1cce49f5e01db66aca9c59913db71dc4dbabc001 Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Tue, 7 Oct 2025 03:24:11 +0530 Subject: [PATCH 17/60] Add const_binop test for conditionals --- .../passing_tests/conditionals/const_binop.py | 21 +++++++++++++++++++ 1 file changed, 21 insertions(+) create mode 100644 tests/passing_tests/conditionals/const_binop.py diff --git a/tests/passing_tests/conditionals/const_binop.py b/tests/passing_tests/conditionals/const_binop.py new file mode 100644 index 0000000..9dffd30 --- /dev/null +++ b/tests/passing_tests/conditionals/const_binop.py @@ -0,0 +1,21 @@ +from pythonbpf import bpf, section, bpfglobal, compile +from ctypes import c_void_p, c_int64 + + +@bpf +@section("tracepoint/syscalls/sys_enter_execve") +def hello_world(ctx: c_void_p) -> c_int64: + if (0 + 1) * 0: + print("Hello, World!") + else: + print("Goodbye, World!") + return + + +@bpf +@bpfglobal +def LICENSE() -> str: + return "GPL" + + +compile() From 2de280915a59e5ade39a3ccfa89d802c5a747e1e Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Tue, 7 Oct 2025 03:37:13 +0530 Subject: [PATCH 18/60] Add var test for conditionals --- tests/passing_tests/conditionals/var.py | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 tests/passing_tests/conditionals/var.py diff --git a/tests/passing_tests/conditionals/var.py b/tests/passing_tests/conditionals/var.py new file mode 100644 index 0000000..449501e --- /dev/null +++ b/tests/passing_tests/conditionals/var.py @@ -0,0 +1,22 @@ +from pythonbpf import bpf, section, bpfglobal, compile +from ctypes import c_void_p, c_int64 + + +@bpf +@section("tracepoint/syscalls/sys_enter_execve") +def hello_world(ctx: c_void_p) -> c_int64: + x = 0 + if x: + print("Hello, World!") + else: + print("Goodbye, World!") + return + + +@bpf +@bpfglobal +def LICENSE() -> str: + return "GPL" + + +compile() From 12b712c21783eb9d2a8f7188c2045cab7f2beb77 Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Tue, 7 Oct 2025 03:43:36 +0530 Subject: [PATCH 19/60] Add var_binop test for conditionals --- tests/passing_tests/conditionals/var_binop.py | 22 +++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 tests/passing_tests/conditionals/var_binop.py diff --git a/tests/passing_tests/conditionals/var_binop.py b/tests/passing_tests/conditionals/var_binop.py new file mode 100644 index 0000000..75a2262 --- /dev/null +++ b/tests/passing_tests/conditionals/var_binop.py @@ -0,0 +1,22 @@ +from pythonbpf import bpf, section, bpfglobal, compile +from ctypes import c_void_p, c_int64 + + +@bpf +@section("tracepoint/syscalls/sys_enter_execve") +def hello_world(ctx: c_void_p) -> c_int64: + x = 0 + if x * 1: + print("Hello, World!") + else: + print("Goodbye, World!") + return + + +@bpf +@bpfglobal +def LICENSE() -> str: + return "GPL" + + +compile() From 1d6226d8293000b5ea907358a74b9adcbf380027 Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Tue, 7 Oct 2025 04:06:16 +0530 Subject: [PATCH 20/60] Add map test to conditionals --- tests/passing_tests/conditionals/map.py | 30 +++++++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 tests/passing_tests/conditionals/map.py diff --git a/tests/passing_tests/conditionals/map.py b/tests/passing_tests/conditionals/map.py new file mode 100644 index 0000000..fa490a7 --- /dev/null +++ b/tests/passing_tests/conditionals/map.py @@ -0,0 +1,30 @@ +from pythonbpf import bpf, map, section, bpfglobal, compile +from ctypes import c_void_p, c_int64, c_uint64 +from pythonbpf.maps import HashMap + + +@bpf +@map +def last() -> HashMap: + return HashMap(key=c_uint64, value=c_uint64, max_entries=3) + + +@bpf +@section("tracepoint/syscalls/sys_enter_execve") +def hello_world(ctx: c_void_p) -> c_int64: + # last.update(0, 1) + tsp = last.lookup(0) + if tsp: + print("Hello, World!") + else: + print("Goodbye, World!") + return + + +@bpf +@bpfglobal +def LICENSE() -> str: + return "GPL" + + +compile() From 176673017cc75159a31caf1a2a246d72616929f7 Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Tue, 7 Oct 2025 04:17:26 +0530 Subject: [PATCH 21/60] Add failing tests struct and not for conditionals --- tests/failing_tests/conditionals/not.py | 30 ++++++++++++++++++++++ tests/failing_tests/conditionals/struct.py | 29 +++++++++++++++++++++ 2 files changed, 59 insertions(+) create mode 100644 tests/failing_tests/conditionals/not.py create mode 100644 tests/failing_tests/conditionals/struct.py diff --git a/tests/failing_tests/conditionals/not.py b/tests/failing_tests/conditionals/not.py new file mode 100644 index 0000000..773291e --- /dev/null +++ b/tests/failing_tests/conditionals/not.py @@ -0,0 +1,30 @@ +from pythonbpf import bpf, map, section, bpfglobal, compile +from ctypes import c_void_p, c_int64, c_uint64 +from pythonbpf.maps import HashMap + + +@bpf +@map +def last() -> HashMap: + return HashMap(key=c_uint64, value=c_uint64, max_entries=3) + + +@bpf +@section("tracepoint/syscalls/sys_enter_execve") +def hello_world(ctx: c_void_p) -> c_int64: + # last.update(0, 1) + tsp = last.lookup(0) + if not tsp: + print("Hello, World!") + else: + print("Goodbye, World!") + return + + +@bpf +@bpfglobal +def LICENSE() -> str: + return "GPL" + + +compile() diff --git a/tests/failing_tests/conditionals/struct.py b/tests/failing_tests/conditionals/struct.py new file mode 100644 index 0000000..bbfbba4 --- /dev/null +++ b/tests/failing_tests/conditionals/struct.py @@ -0,0 +1,29 @@ +from pythonbpf import bpf, struct, section, bpfglobal, compile +from ctypes import c_void_p, c_int64, c_uint64 + + +@bpf +@struct +class data_t: + pid: c_uint64 + ts: c_uint64 + + +@bpf +@section("tracepoint/syscalls/sys_enter_execve") +def hello_world(ctx: c_void_p) -> c_int64: + dat = data_t() + if dat.pid: + print("Hello, World!") + else: + print("Goodbye, World!") + return + + +@bpf +@bpfglobal +def LICENSE() -> str: + return "GPL" + + +compile() From a574527891bc08bb14a42bf55b29b9e699e2cfc3 Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Tue, 7 Oct 2025 04:49:45 +0530 Subject: [PATCH 22/60] Add support for unary op 'not' in eval_expr, move not test to passing --- pythonbpf/expr_pass.py | 42 ++++++++++++++++++- pythonbpf/functions/functions_pass.py | 14 +------ .../{struct.py => struct_access.py} | 0 .../conditionals/not.py | 0 4 files changed, 43 insertions(+), 13 deletions(-) rename tests/failing_tests/conditionals/{struct.py => struct_access.py} (100%) rename tests/{failing_tests => passing_tests}/conditionals/not.py (100%) diff --git a/pythonbpf/expr_pass.py b/pythonbpf/expr_pass.py index 79ff4bb..ca0fbf7 100644 --- a/pythonbpf/expr_pass.py +++ b/pythonbpf/expr_pass.py @@ -22,7 +22,6 @@ 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.""" - logger.info("We the best") if isinstance(expr.value, int) or isinstance(expr.value, bool): return ir.Constant(ir.IntType(64), int(expr.value)), ir.IntType(64) else: @@ -191,6 +190,43 @@ def _handle_compare( return _handle_comparator(builder, cond.ops[0], lhs, rhs) +def convert_to_bool(builder, val): + 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_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 eval_expr( func, module, @@ -275,6 +311,10 @@ def eval_expr( 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 + ) logger.info("Unsupported expression evaluation") return None diff --git a/pythonbpf/functions/functions_pass.py b/pythonbpf/functions/functions_pass.py index 7463f54..00cbe75 100644 --- a/pythonbpf/functions/functions_pass.py +++ b/pythonbpf/functions/functions_pass.py @@ -7,7 +7,7 @@ from dataclasses import dataclass from pythonbpf.helper import HelperHandlerRegistry, handle_helper_call from pythonbpf.type_deducer import ctypes_to_ir from pythonbpf.binary_ops import handle_binary_op -from pythonbpf.expr_pass import eval_expr, handle_expr +from pythonbpf.expr_pass import eval_expr, handle_expr, convert_to_bool from .return_utils import _handle_none_return, _handle_xdp_return, _is_xdp_name @@ -240,20 +240,10 @@ def handle_assign( logger.info("Unsupported assignment value type") -def _convert_to_bool(builder, val): - 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_cond(func, module, builder, cond, local_sym_tab, map_sym_tab): if True: val = eval_expr(func, module, builder, cond, local_sym_tab, map_sym_tab)[0] - return _convert_to_bool(builder, val) + return convert_to_bool(builder, val) if isinstance(cond, ast.Constant): if isinstance(cond.value, bool) or isinstance(cond.value, int): return ir.Constant(ir.IntType(1), int(cond.value)) diff --git a/tests/failing_tests/conditionals/struct.py b/tests/failing_tests/conditionals/struct_access.py similarity index 100% rename from tests/failing_tests/conditionals/struct.py rename to tests/failing_tests/conditionals/struct_access.py diff --git a/tests/failing_tests/conditionals/not.py b/tests/passing_tests/conditionals/not.py similarity index 100% rename from tests/failing_tests/conditionals/not.py rename to tests/passing_tests/conditionals/not.py From 0e7dcafbab6443aa63bdba7009781a9fb26c6a97 Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Tue, 7 Oct 2025 05:02:26 +0530 Subject: [PATCH 23/60] Add var_comp test for conditionals --- tests/passing_tests/conditionals/var_comp.py | 22 ++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 tests/passing_tests/conditionals/var_comp.py diff --git a/tests/passing_tests/conditionals/var_comp.py b/tests/passing_tests/conditionals/var_comp.py new file mode 100644 index 0000000..4f12f15 --- /dev/null +++ b/tests/passing_tests/conditionals/var_comp.py @@ -0,0 +1,22 @@ +from pythonbpf import bpf, section, bpfglobal, compile +from ctypes import c_void_p, c_int64 + + +@bpf +@section("tracepoint/syscalls/sys_enter_execve") +def hello_world(ctx: c_void_p) -> c_int64: + x = 2 + if x > 3: + print("Hello, World!") + else: + print("Goodbye, World!") + return + + +@bpf +@bpfglobal +def LICENSE() -> str: + return "GPL" + + +compile() From b7092fa362ee66d1546c03967b34cc73a3059c68 Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Tue, 7 Oct 2025 05:20:43 +0530 Subject: [PATCH 24/60] Add failing test map_comp for conditionals --- tests/failing_tests/conditionals/map_comp.py | 30 ++++++++++++++++++++ 1 file changed, 30 insertions(+) create mode 100644 tests/failing_tests/conditionals/map_comp.py diff --git a/tests/failing_tests/conditionals/map_comp.py b/tests/failing_tests/conditionals/map_comp.py new file mode 100644 index 0000000..f5df5bc --- /dev/null +++ b/tests/failing_tests/conditionals/map_comp.py @@ -0,0 +1,30 @@ +from pythonbpf import bpf, map, section, bpfglobal, compile +from ctypes import c_void_p, c_int64, c_uint64 +from pythonbpf.maps import HashMap + + +@bpf +@map +def last() -> HashMap: + return HashMap(key=c_uint64, value=c_uint64, max_entries=3) + + +@bpf +@section("tracepoint/syscalls/sys_enter_execve") +def hello_world(ctx: c_void_p) -> c_int64: + last.update(0, 1) + tsp = last.lookup(0) + if not (tsp > 0): + print("Hello, World!") + else: + print("Goodbye, World!") + return + + +@bpf +@bpfglobal +def LICENSE() -> str: + return "GPL" + + +compile() From f41693bc6d0eef8300925c25a5636ce523db6c79 Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Tue, 7 Oct 2025 05:27:31 +0530 Subject: [PATCH 25/60] Add 'and' and 'or' BoolOps as future deliverables --- TODO.md | 1 + 1 file changed, 1 insertion(+) diff --git a/TODO.md b/TODO.md index 6be4112..7019d91 100644 --- a/TODO.md +++ b/TODO.md @@ -5,6 +5,7 @@ - XDP support in pylibbpf - ringbuf support - recursive expression resolution +- Add supoprt for BoolOp and short circuiting in conditions ## Long term From caa5d92c32de634f6dd57190842aa6a7195267f9 Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Tue, 7 Oct 2025 13:35:31 +0530 Subject: [PATCH 26/60] Fix struct_access in eval_expr, move struct_access conditional test to passing --- pythonbpf/expr_pass.py | 1 - pythonbpf/functions/functions_pass.py | 12 +++++++++--- .../conditionals/struct_access.py | 2 +- 3 files changed, 10 insertions(+), 5 deletions(-) rename tests/{failing_tests => passing_tests}/conditionals/struct_access.py (96%) diff --git a/pythonbpf/expr_pass.py b/pythonbpf/expr_pass.py index ca0fbf7..0bccc5b 100644 --- a/pythonbpf/expr_pass.py +++ b/pythonbpf/expr_pass.py @@ -43,7 +43,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) diff --git a/pythonbpf/functions/functions_pass.py b/pythonbpf/functions/functions_pass.py index 00cbe75..80fa520 100644 --- a/pythonbpf/functions/functions_pass.py +++ b/pythonbpf/functions/functions_pass.py @@ -240,9 +240,13 @@ def handle_assign( logger.info("Unsupported assignment value type") -def handle_cond(func, module, builder, cond, local_sym_tab, map_sym_tab): +def handle_cond( + func, module, builder, cond, local_sym_tab, map_sym_tab, structs_sym_tab=None +): if True: - val = eval_expr(func, module, builder, cond, local_sym_tab, map_sym_tab)[0] + val = eval_expr( + func, module, builder, cond, local_sym_tab, map_sym_tab, structs_sym_tab + )[0] return convert_to_bool(builder, val) if isinstance(cond, ast.Constant): if isinstance(cond.value, bool) or isinstance(cond.value, int): @@ -321,7 +325,9 @@ def handle_if( else: else_block = None - cond = handle_cond(func, module, builder, stmt.test, local_sym_tab, map_sym_tab) + cond = handle_cond( + func, module, builder, stmt.test, local_sym_tab, map_sym_tab, structs_sym_tab + ) if else_block: builder.cbranch(cond, then_block, else_block) else: diff --git a/tests/failing_tests/conditionals/struct_access.py b/tests/passing_tests/conditionals/struct_access.py similarity index 96% rename from tests/failing_tests/conditionals/struct_access.py rename to tests/passing_tests/conditionals/struct_access.py index bbfbba4..5267290 100644 --- a/tests/failing_tests/conditionals/struct_access.py +++ b/tests/passing_tests/conditionals/struct_access.py @@ -13,7 +13,7 @@ class data_t: @section("tracepoint/syscalls/sys_enter_execve") def hello_world(ctx: c_void_p) -> c_int64: dat = data_t() - if dat.pid: + if dat.ts: print("Hello, World!") else: print("Goodbye, World!") From 1843ca6c53bb5c45828cc7ec4c88727edca5d7d8 Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Tue, 7 Oct 2025 13:42:58 +0530 Subject: [PATCH 27/60] Add failing struct_ptr test for conditionals --- .../failing_tests/conditionals/struct_ptr.py | 29 +++++++++++++++++++ 1 file changed, 29 insertions(+) create mode 100644 tests/failing_tests/conditionals/struct_ptr.py diff --git a/tests/failing_tests/conditionals/struct_ptr.py b/tests/failing_tests/conditionals/struct_ptr.py new file mode 100644 index 0000000..baee3a8 --- /dev/null +++ b/tests/failing_tests/conditionals/struct_ptr.py @@ -0,0 +1,29 @@ +from pythonbpf import bpf, struct, section, bpfglobal, compile +from ctypes import c_void_p, c_int64, c_uint64 + + +@bpf +@struct +class data_t: + pid: c_uint64 + ts: c_uint64 + + +@bpf +@section("tracepoint/syscalls/sys_enter_execve") +def hello_world(ctx: c_void_p) -> c_int64: + dat = data_t() + if dat: + print("Hello, World!") + else: + print("Goodbye, World!") + return + + +@bpf +@bpfglobal +def LICENSE() -> str: + return "GPL" + + +compile() From 9e1142bf053629738c7d0c10e191f138b0c7a246 Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Tue, 7 Oct 2025 14:02:09 +0530 Subject: [PATCH 28/60] Add type_mismatch failing test for conditionals --- .../conditionals/type_mismatch.py | 23 +++++++++++++++++++ 1 file changed, 23 insertions(+) create mode 100644 tests/failing_tests/conditionals/type_mismatch.py diff --git a/tests/failing_tests/conditionals/type_mismatch.py b/tests/failing_tests/conditionals/type_mismatch.py new file mode 100644 index 0000000..1efc5e2 --- /dev/null +++ b/tests/failing_tests/conditionals/type_mismatch.py @@ -0,0 +1,23 @@ +from pythonbpf import bpf, section, bpfglobal, compile +from ctypes import c_void_p, c_int64, c_int32 + + +@bpf +@section("tracepoint/syscalls/sys_enter_execve") +def hello_world(ctx: c_void_p) -> c_int64: + x = 0 + y = c_int32(0) + if x == y: + print("Hello, World!") + else: + print("Goodbye, World!") + return + + +@bpf +@bpfglobal +def LICENSE() -> str: + return "GPL" + + +compile() From 2d850f457f37b4861043d73299dfaaa60e3a2f1f Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Wed, 8 Oct 2025 02:22:41 +0530 Subject: [PATCH 29/60] Add _normalize_types to handle mismatched ints, move type_mismatch test to passing --- pythonbpf/expr_pass.py | 19 +++++++++++++++++++ .../conditionals/type_mismatch.py | 0 2 files changed, 19 insertions(+) rename tests/{failing_tests => passing_tests}/conditionals/type_mismatch.py (100%) diff --git a/pythonbpf/expr_pass.py b/pythonbpf/expr_pass.py index 0bccc5b..fa22c2e 100644 --- a/pythonbpf/expr_pass.py +++ b/pythonbpf/expr_pass.py @@ -129,10 +129,29 @@ def _handle_ctypes_call( return val +def _normalize_types(builder, lhs, rhs): + """Normalize types for comparison.""" + + 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 + + logger.error(f"Type mismatch: {lhs.type} vs {rhs.type}") + return None, None + + def _handle_comparator(builder, op, lhs, rhs): """Handle comparison operations.""" # NOTE: For now assume same types + if lhs.type != rhs.type: + lhs, rhs = _normalize_types(builder, lhs, rhs) + + if lhs is None or rhs is None: + return None comparison_ops = { ast.Eq: "==", diff --git a/tests/failing_tests/conditionals/type_mismatch.py b/tests/passing_tests/conditionals/type_mismatch.py similarity index 100% rename from tests/failing_tests/conditionals/type_mismatch.py rename to tests/passing_tests/conditionals/type_mismatch.py From ab7127556605c59a31d3ea3faea9811adff57ce8 Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Wed, 8 Oct 2025 03:00:52 +0530 Subject: [PATCH 30/60] Add _get_base_type to expr_pass --- pythonbpf/expr_pass.py | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/pythonbpf/expr_pass.py b/pythonbpf/expr_pass.py index fa22c2e..3ba2125 100644 --- a/pythonbpf/expr_pass.py +++ b/pythonbpf/expr_pass.py @@ -129,6 +129,14 @@ def _handle_ctypes_call( return val +def _get_base_type(ir_type): + """Get the base type for pointer types.""" + cur_type = ir_type + while isinstance(cur_type, ir.PointerType): + cur_type = cur_type.pointee + return cur_type + + def _normalize_types(builder, lhs, rhs): """Normalize types for comparison.""" From 480afd1341eb6024bdbd3963e6565974e8ae2c82 Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Wed, 8 Oct 2025 03:02:31 +0530 Subject: [PATCH 31/60] Move _get_base_type to _get_base_type_and_depth --- pythonbpf/expr_pass.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/pythonbpf/expr_pass.py b/pythonbpf/expr_pass.py index 3ba2125..fe04b68 100644 --- a/pythonbpf/expr_pass.py +++ b/pythonbpf/expr_pass.py @@ -129,12 +129,14 @@ def _handle_ctypes_call( return val -def _get_base_type(ir_type): +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 + return cur_type, depth def _normalize_types(builder, lhs, rhs): From 3f9604a370710aff18c7bce20c1f1d053ea73491 Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Wed, 8 Oct 2025 03:12:17 +0530 Subject: [PATCH 32/60] Add _deref_to_depth in expr_pass --- pythonbpf/expr_pass.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/pythonbpf/expr_pass.py b/pythonbpf/expr_pass.py index fe04b68..834d924 100644 --- a/pythonbpf/expr_pass.py +++ b/pythonbpf/expr_pass.py @@ -139,6 +139,18 @@ def _get_base_type_and_depth(ir_type): return cur_type, depth +def _deref_to_depth(builder, val, target_depth): + """Dereference a pointer to a certain depth.""" + + cur_val = val + for _ in range(target_depth): + if not isinstance(val.type, ir.PointerType): + logger.error("Cannot dereference further, non-pointer type") + return None + cur_val = builder.load(cur_val) + return cur_val + + def _normalize_types(builder, lhs, rhs): """Normalize types for comparison.""" From 0c977514af85096a45096c18a66a61773846f2a9 Mon Sep 17 00:00:00 2001 From: varunrmallya <100590632+varun-r-mallya@users.noreply.github.com> Date: Wed, 8 Oct 2025 05:34:25 +0530 Subject: [PATCH 33/60] Add TODO for fixing struct_kioctx issue --- TODO.md | 1 + 1 file changed, 1 insertion(+) diff --git a/TODO.md b/TODO.md index 6be4112..78f7967 100644 --- a/TODO.md +++ b/TODO.md @@ -10,3 +10,4 @@ - Refactor the codebase to be better than a hackathon project - Port to C++ and use actual LLVM? +- Fix struct_kioctx issue in the vmlinux transpiler From 6b599808740d5fe774041eff42ad1f8d2afc0a2a Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Wed, 8 Oct 2025 05:53:12 +0530 Subject: [PATCH 34/60] Add null checks for pointer derefs to avoid map_value_or_null verifier errors --- pythonbpf/expr_pass.py | 66 +++++++++++++++++++++++++++++++++++------- 1 file changed, 56 insertions(+), 10 deletions(-) diff --git a/pythonbpf/expr_pass.py b/pythonbpf/expr_pass.py index 834d924..aee2ab1 100644 --- a/pythonbpf/expr_pass.py +++ b/pythonbpf/expr_pass.py @@ -139,38 +139,84 @@ def _get_base_type_and_depth(ir_type): return cur_type, depth -def _deref_to_depth(builder, val, target_depth): +def _deref_to_depth(func, builder, val, target_depth): """Dereference a pointer to a certain depth.""" cur_val = val - for _ in range(target_depth): + 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 - cur_val = builder.load(cur_val) + + # 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(builder, lhs, rhs): +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 - - logger.error(f"Type mismatch: {lhs.type} vs {rhs.type}") - return None, None + 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 _handle_comparator(builder, op, lhs, rhs): +def _handle_comparator(func, builder, op, lhs, rhs): """Handle comparison operations.""" # NOTE: For now assume same types if lhs.type != rhs.type: - lhs, rhs = _normalize_types(builder, lhs, rhs) + lhs, rhs = _normalize_types(func, builder, lhs, rhs) if lhs is None or rhs is None: return None @@ -227,7 +273,7 @@ def _handle_compare( lhs, _ = lhs rhs, _ = rhs - return _handle_comparator(builder, cond.ops[0], lhs, rhs) + return _handle_comparator(func, builder, cond.ops[0], lhs, rhs) def convert_to_bool(builder, val): From 95a196a91fdfb1591262e9babd5ddad5a003384a Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Wed, 8 Oct 2025 05:53:52 +0530 Subject: [PATCH 35/60] Move map_comp test to passing --- tests/{failing_tests => passing_tests}/conditionals/map_comp.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) rename tests/{failing_tests => passing_tests}/conditionals/map_comp.py (96%) diff --git a/tests/failing_tests/conditionals/map_comp.py b/tests/passing_tests/conditionals/map_comp.py similarity index 96% rename from tests/failing_tests/conditionals/map_comp.py rename to tests/passing_tests/conditionals/map_comp.py index f5df5bc..2350f06 100644 --- a/tests/failing_tests/conditionals/map_comp.py +++ b/tests/passing_tests/conditionals/map_comp.py @@ -14,7 +14,7 @@ def last() -> HashMap: def hello_world(ctx: c_void_p) -> c_int64: last.update(0, 1) tsp = last.lookup(0) - if not (tsp > 0): + if tsp > 0: print("Hello, World!") else: print("Goodbye, World!") From a764b095f8607d7550e602c972ccd00cfabc1661 Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Wed, 8 Oct 2025 05:54:49 +0530 Subject: [PATCH 36/60] Add helper_cond failing test for conditionals --- .../failing_tests/conditionals/helper_cond.py | 26 +++++++++++++++++++ 1 file changed, 26 insertions(+) create mode 100644 tests/failing_tests/conditionals/helper_cond.py diff --git a/tests/failing_tests/conditionals/helper_cond.py b/tests/failing_tests/conditionals/helper_cond.py new file mode 100644 index 0000000..fb13e3a --- /dev/null +++ b/tests/failing_tests/conditionals/helper_cond.py @@ -0,0 +1,26 @@ +from pythonbpf import bpf, map, section, bpfglobal +from ctypes import c_void_p, c_int64, c_uint64 +from pythonbpf.maps import HashMap + + +@bpf +@map +def last() -> HashMap: + return HashMap(key=c_uint64, value=c_uint64, max_entries=3) + + +@bpf +@section("tracepoint/syscalls/sys_enter_execve") +def hello_world(ctx: c_void_p) -> c_int64: + last.update(0, 1) + if last.lookup(0) > 0: + print("Hello, World!") + else: + print("Goodbye, World!") + return + + +@bpf +@bpfglobal +def LICENSE() -> str: + return "GPL" From ecac24c1d2d00fb4caace445ec6cbc7554140eb4 Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Wed, 8 Oct 2025 05:57:17 +0530 Subject: [PATCH 37/60] Add explanation notes to failing conditionals tests --- tests/failing_tests/conditionals/helper_cond.py | 5 +++++ tests/failing_tests/conditionals/struct_ptr.py | 5 +++++ 2 files changed, 10 insertions(+) diff --git a/tests/failing_tests/conditionals/helper_cond.py b/tests/failing_tests/conditionals/helper_cond.py index fb13e3a..32473c3 100644 --- a/tests/failing_tests/conditionals/helper_cond.py +++ b/tests/failing_tests/conditionals/helper_cond.py @@ -2,6 +2,11 @@ from pythonbpf import bpf, map, section, bpfglobal from ctypes import c_void_p, c_int64, c_uint64 from pythonbpf.maps import HashMap +# NOTE: Decided against fixing this +# as a workaround is assigning the result of lookup to a variable +# and then using that variable in the if statement. +# Might fix in future. + @bpf @map diff --git a/tests/failing_tests/conditionals/struct_ptr.py b/tests/failing_tests/conditionals/struct_ptr.py index baee3a8..7085f81 100644 --- a/tests/failing_tests/conditionals/struct_ptr.py +++ b/tests/failing_tests/conditionals/struct_ptr.py @@ -1,6 +1,11 @@ from pythonbpf import bpf, struct, section, bpfglobal, compile from ctypes import c_void_p, c_int64, c_uint64 +# NOTE: Decided against fixing this +# as one workaround is to just check any field of the struct +# in the if statement. Ugly but works. +# Might fix in future. + @bpf @struct From d2ff53052caecded4666aacc1de6eb1cf9cc9cd0 Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Wed, 8 Oct 2025 06:04:29 +0530 Subject: [PATCH 38/60] Add support for is and is not keywords --- pythonbpf/expr_pass.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/pythonbpf/expr_pass.py b/pythonbpf/expr_pass.py index aee2ab1..2c734ea 100644 --- a/pythonbpf/expr_pass.py +++ b/pythonbpf/expr_pass.py @@ -228,6 +228,8 @@ def _handle_comparator(func, builder, op, lhs, rhs): ast.LtE: "<=", ast.Gt: ">", ast.GtE: ">=", + ast.Is: "==", + ast.IsNot: "!=", } if type(op) not in comparison_ops: From 98f262ae226b40a538a82ff4553303ac6e2b3d53 Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Wed, 8 Oct 2025 06:11:59 +0530 Subject: [PATCH 39/60] Add BoolOp handling stub in eval_expr --- pythonbpf/expr_pass.py | 16 ++++++++++++++++ 1 file changed, 16 insertions(+) diff --git a/pythonbpf/expr_pass.py b/pythonbpf/expr_pass.py index 2c734ea..98e208a 100644 --- a/pythonbpf/expr_pass.py +++ b/pythonbpf/expr_pass.py @@ -315,6 +315,18 @@ def _handle_unary_op( return result, ir.IntType(1) +def _handle_boolean_op( + func, + module, + builder, + expr: ast.BoolOp, + local_sym_tab, + map_sym_tab, + structs_sym_tab=None, +): + pass + + def eval_expr( func, module, @@ -403,6 +415,10 @@ def eval_expr( 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 From f98491f3bd5b3068d258ef90bcc98bd7a67d30fd Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Wed, 8 Oct 2025 06:14:32 +0530 Subject: [PATCH 40/60] Add handle_and and handle_or handling stub in eval_expr --- pythonbpf/expr_pass.py | 22 +++++++++++++++++++++- 1 file changed, 21 insertions(+), 1 deletion(-) diff --git a/pythonbpf/expr_pass.py b/pythonbpf/expr_pass.py index 98e208a..17d9500 100644 --- a/pythonbpf/expr_pass.py +++ b/pythonbpf/expr_pass.py @@ -315,6 +315,14 @@ def _handle_unary_op( return result, ir.IntType(1) +def _handle_and_op(func, builder, expr, local_sym_tab, map_sym_tab, structs_sym_tab): + pass + + +def _handle_or_op(func, builder, expr, local_sym_tab, map_sym_tab, structs_sym_tab): + pass + + def _handle_boolean_op( func, module, @@ -324,7 +332,19 @@ def _handle_boolean_op( map_sym_tab, structs_sym_tab=None, ): - pass + """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( From 1f96bab94404bc86634f2eed3e5706afbb81b67a Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Wed, 8 Oct 2025 06:24:13 +0530 Subject: [PATCH 41/60] Add _handle_and_op in expr_pass --- pythonbpf/expr_pass.py | 51 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/pythonbpf/expr_pass.py b/pythonbpf/expr_pass.py index 17d9500..7accf6e 100644 --- a/pythonbpf/expr_pass.py +++ b/pythonbpf/expr_pass.py @@ -316,7 +316,56 @@ def _handle_unary_op( def _handle_and_op(func, builder, expr, local_sym_tab, map_sym_tab, structs_sym_tab): - pass + """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): From 95d63d969e5f743ecd229ddc2d6dfce4887232ed Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Wed, 8 Oct 2025 06:24:57 +0530 Subject: [PATCH 42/60] Add _handle_or_or in expr_pass --- pythonbpf/expr_pass.py | 51 +++++++++++++++++++++++++++++++++++++++++- 1 file changed, 50 insertions(+), 1 deletion(-) diff --git a/pythonbpf/expr_pass.py b/pythonbpf/expr_pass.py index 7accf6e..fb4cfe6 100644 --- a/pythonbpf/expr_pass.py +++ b/pythonbpf/expr_pass.py @@ -369,7 +369,56 @@ def _handle_and_op(func, builder, expr, local_sym_tab, map_sym_tab, structs_sym_ def _handle_or_op(func, builder, expr, local_sym_tab, map_sym_tab, structs_sym_tab): - pass + """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( From e7912a088fc2cfc1d47cee2d677545b94719984f Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Wed, 8 Oct 2025 06:27:18 +0530 Subject: [PATCH 43/60] Add passing or.py test for conditionals --- tests/passing_tests/conditionals/or.py | 32 ++++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 tests/passing_tests/conditionals/or.py diff --git a/tests/passing_tests/conditionals/or.py b/tests/passing_tests/conditionals/or.py new file mode 100644 index 0000000..5626179 --- /dev/null +++ b/tests/passing_tests/conditionals/or.py @@ -0,0 +1,32 @@ +from pythonbpf import bpf, map, section, bpfglobal, compile +from ctypes import c_void_p, c_int64, c_uint64 +from pythonbpf.maps import HashMap + + +@bpf +@map +def last() -> HashMap: + return HashMap(key=c_uint64, value=c_uint64, max_entries=3) + + +@bpf +@section("tracepoint/syscalls/sys_enter_execve") +def hello_world(ctx: c_void_p) -> c_int64: + last.update(0, 1) + # last.update(1, 2) + x = last.lookup(0) + y = last.lookup(1) + if x or y: + print("Hello, World!") + else: + print("Goodbye, World!") + return + + +@bpf +@bpfglobal +def LICENSE() -> str: + return "GPL" + + +compile() From 3bb4b099c188ee6b02948245f3b58b6015403255 Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Wed, 8 Oct 2025 06:28:03 +0530 Subject: [PATCH 44/60] Add passing and.py test for conditionals --- tests/passing_tests/conditionals/and.py | 32 +++++++++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 tests/passing_tests/conditionals/and.py diff --git a/tests/passing_tests/conditionals/and.py b/tests/passing_tests/conditionals/and.py new file mode 100644 index 0000000..5cb4824 --- /dev/null +++ b/tests/passing_tests/conditionals/and.py @@ -0,0 +1,32 @@ +from pythonbpf import bpf, map, section, bpfglobal, compile +from ctypes import c_void_p, c_int64, c_uint64 +from pythonbpf.maps import HashMap + + +@bpf +@map +def last() -> HashMap: + return HashMap(key=c_uint64, value=c_uint64, max_entries=3) + + +@bpf +@section("tracepoint/syscalls/sys_enter_execve") +def hello_world(ctx: c_void_p) -> c_int64: + last.update(0, 1) + last.update(1, 2) + x = last.lookup(0) + y = last.lookup(1) + if x and y: + print("Hello, World!") + else: + print("Goodbye, World!") + return + + +@bpf +@bpfglobal +def LICENSE() -> str: + return "GPL" + + +compile() From 4857739eece640d7e9bc98c952f12e276b17f6ba Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Wed, 8 Oct 2025 06:42:34 +0530 Subject: [PATCH 45/60] cleanup handle_cond in functions_pass --- pythonbpf/functions/functions_pass.py | 71 ++------------------------- 1 file changed, 4 insertions(+), 67 deletions(-) diff --git a/pythonbpf/functions/functions_pass.py b/pythonbpf/functions/functions_pass.py index 80fa520..5b04311 100644 --- a/pythonbpf/functions/functions_pass.py +++ b/pythonbpf/functions/functions_pass.py @@ -243,73 +243,10 @@ def handle_assign( def handle_cond( func, module, builder, cond, local_sym_tab, map_sym_tab, structs_sym_tab=None ): - if True: - val = eval_expr( - func, module, builder, cond, local_sym_tab, map_sym_tab, structs_sym_tab - )[0] - return convert_to_bool(builder, val) - if isinstance(cond, ast.Constant): - if isinstance(cond.value, bool) or isinstance(cond.value, int): - return ir.Constant(ir.IntType(1), int(cond.value)) - else: - raise ValueError("Unsupported constant type in condition") - return None - elif isinstance(cond, ast.Name): - if cond.id in local_sym_tab: - var = local_sym_tab[cond.id].var - val = builder.load(var) - if val.type != ir.IntType(1): - # Convert nonzero values to true, zero to false - if isinstance(val.type, ir.PointerType): - # For pointer types, compare with null pointer - zero = ir.Constant(val.type, None) - else: - # For integer types, compare with zero - zero = ir.Constant(val.type, 0) - val = builder.icmp_signed("!=", val, zero) - return val - else: - logger.info(f"Undefined variable {cond.id} in condition") - return None - elif isinstance(cond, ast.Compare): - lhs = eval_expr(func, module, builder, cond.left, local_sym_tab, map_sym_tab)[0] - if len(cond.ops) != 1 or len(cond.comparators) != 1: - logger.info("Unsupported complex comparison") - return None - rhs = eval_expr( - func, module, builder, cond.comparators[0], local_sym_tab, map_sym_tab - )[0] - op = cond.ops[0] - - if lhs.type != rhs.type: - if isinstance(lhs.type, ir.IntType) and isinstance(rhs.type, ir.IntType): - # Extend the smaller type to the larger type - if lhs.type.width < rhs.type.width: - lhs = builder.sext(lhs, rhs.type) - elif lhs.type.width > rhs.type.width: - rhs = builder.sext(rhs, lhs.type) - else: - logger.info("Type mismatch in comparison") - return None - - if isinstance(op, ast.Eq): - return builder.icmp_signed("==", lhs, rhs) - elif isinstance(op, ast.NotEq): - return builder.icmp_signed("!=", lhs, rhs) - elif isinstance(op, ast.Lt): - return builder.icmp_signed("<", lhs, rhs) - elif isinstance(op, ast.LtE): - return builder.icmp_signed("<=", lhs, rhs) - elif isinstance(op, ast.Gt): - return builder.icmp_signed(">", lhs, rhs) - elif isinstance(op, ast.GtE): - return builder.icmp_signed(">=", lhs, rhs) - else: - logger.info("Unsupported comparison operator") - return None - else: - logger.info("Unsupported condition expression") - return None + val = eval_expr( + func, module, builder, cond, local_sym_tab, map_sym_tab, structs_sym_tab + )[0] + return convert_to_bool(builder, val) def handle_if( From b86341ce7a91dc766cd86fe5b7e91718147e1252 Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Wed, 8 Oct 2025 06:45:52 +0530 Subject: [PATCH 46/60] Rework dir structure for expr --- pythonbpf/expr/__init__.py | 3 +++ pythonbpf/{ => expr}/expr_pass.py | 0 2 files changed, 3 insertions(+) create mode 100644 pythonbpf/expr/__init__.py rename pythonbpf/{ => expr}/expr_pass.py (100%) diff --git a/pythonbpf/expr/__init__.py b/pythonbpf/expr/__init__.py new file mode 100644 index 0000000..55eae5e --- /dev/null +++ b/pythonbpf/expr/__init__.py @@ -0,0 +1,3 @@ +from .expr_pass import eval_expr, handle_expr, convert_to_bool + +__all__ = ["eval_expr", "handle_expr", "convert_to_bool"] diff --git a/pythonbpf/expr_pass.py b/pythonbpf/expr/expr_pass.py similarity index 100% rename from pythonbpf/expr_pass.py rename to pythonbpf/expr/expr_pass.py From 5f9eaff59c2c079839ff1aede7d256310e6ddb68 Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Wed, 8 Oct 2025 06:49:34 +0530 Subject: [PATCH 47/60] Fix expr imports --- pythonbpf/functions/functions_pass.py | 2 +- pythonbpf/helper/helper_utils.py | 2 +- pythonbpf/maps/maps_pass.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/pythonbpf/functions/functions_pass.py b/pythonbpf/functions/functions_pass.py index 5b04311..7fc3feb 100644 --- a/pythonbpf/functions/functions_pass.py +++ b/pythonbpf/functions/functions_pass.py @@ -7,7 +7,7 @@ from dataclasses import dataclass from pythonbpf.helper import HelperHandlerRegistry, handle_helper_call from pythonbpf.type_deducer import ctypes_to_ir from pythonbpf.binary_ops import handle_binary_op -from pythonbpf.expr_pass import eval_expr, handle_expr, convert_to_bool +from pythonbpf.expr import eval_expr, handle_expr, convert_to_bool from .return_utils import _handle_none_return, _handle_xdp_return, _is_xdp_name diff --git a/pythonbpf/helper/helper_utils.py b/pythonbpf/helper/helper_utils.py index 0da1e5e..68ab52c 100644 --- a/pythonbpf/helper/helper_utils.py +++ b/pythonbpf/helper/helper_utils.py @@ -3,7 +3,7 @@ import logging from collections.abc import Callable from llvmlite import ir -from pythonbpf.expr_pass import eval_expr +from pythonbpf.expr import eval_expr logger = logging.getLogger(__name__) diff --git a/pythonbpf/maps/maps_pass.py b/pythonbpf/maps/maps_pass.py index cc8dfa6..95748a8 100644 --- a/pythonbpf/maps/maps_pass.py +++ b/pythonbpf/maps/maps_pass.py @@ -3,7 +3,7 @@ from logging import Logger from llvmlite import ir from enum import Enum from .maps_utils import MapProcessorRegistry -from ..debuginfo import DebugInfoGenerator +from pythonbpf.debuginfo import DebugInfoGenerator import logging logger: Logger = logging.getLogger(__name__) From ee90ee93925add1f54973d8b0f5a1a551a6c8ef7 Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Wed, 8 Oct 2025 06:50:53 +0530 Subject: [PATCH 48/60] Fix type_deducer import in expr --- pythonbpf/expr/expr_pass.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pythonbpf/expr/expr_pass.py b/pythonbpf/expr/expr_pass.py index fb4cfe6..b1cf39e 100644 --- a/pythonbpf/expr/expr_pass.py +++ b/pythonbpf/expr/expr_pass.py @@ -4,7 +4,7 @@ from logging import Logger 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 logger: Logger = logging.getLogger(__name__) From e62557bd1da0ac92e79a79b97ea0225724a5cfe1 Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Wed, 8 Oct 2025 06:59:32 +0530 Subject: [PATCH 49/60] Seperate type_normalization from expr_pass --- pythonbpf/expr/expr_pass.py | 85 +-------------------------- pythonbpf/expr/type_normalization.py | 86 ++++++++++++++++++++++++++++ 2 files changed, 88 insertions(+), 83 deletions(-) create mode 100644 pythonbpf/expr/type_normalization.py diff --git a/pythonbpf/expr/expr_pass.py b/pythonbpf/expr/expr_pass.py index b1cf39e..f0c58a5 100644 --- a/pythonbpf/expr/expr_pass.py +++ b/pythonbpf/expr/expr_pass.py @@ -5,6 +5,7 @@ import logging from typing import Dict from pythonbpf.type_deducer import ctypes_to_ir, is_ctypes +from .type_normalization import normalize_types logger: Logger = logging.getLogger(__name__) @@ -129,94 +130,12 @@ def _handle_ctypes_call( return val -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 _handle_comparator(func, builder, op, lhs, rhs): """Handle comparison operations.""" # NOTE: For now assume same types if lhs.type != rhs.type: - lhs, rhs = _normalize_types(func, builder, lhs, rhs) + lhs, rhs = normalize_types(func, builder, lhs, rhs) if lhs is None or rhs is None: return None diff --git a/pythonbpf/expr/type_normalization.py b/pythonbpf/expr/type_normalization.py new file mode 100644 index 0000000..a1b3fad --- /dev/null +++ b/pythonbpf/expr/type_normalization.py @@ -0,0 +1,86 @@ +from llvmlite import ir +import logging + +logger = logging.getLogger(__name__) + + +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) From 0a6571726a459567eff87ad00ba7af282904dadb Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Wed, 8 Oct 2025 07:14:42 +0530 Subject: [PATCH 50/60] Move convert_to_bool to type_normalization --- pythonbpf/expr/expr_pass.py | 12 +----------- pythonbpf/expr/type_normalization.py | 11 +++++++++++ 2 files changed, 12 insertions(+), 11 deletions(-) diff --git a/pythonbpf/expr/expr_pass.py b/pythonbpf/expr/expr_pass.py index f0c58a5..a72a4bb 100644 --- a/pythonbpf/expr/expr_pass.py +++ b/pythonbpf/expr/expr_pass.py @@ -5,7 +5,7 @@ import logging from typing import Dict from pythonbpf.type_deducer import ctypes_to_ir, is_ctypes -from .type_normalization import normalize_types +from .type_normalization import normalize_types, convert_to_bool logger: Logger = logging.getLogger(__name__) @@ -197,16 +197,6 @@ def _handle_compare( return _handle_comparator(func, builder, cond.ops[0], lhs, rhs) -def convert_to_bool(builder, val): - 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_unary_op( func, module, diff --git a/pythonbpf/expr/type_normalization.py b/pythonbpf/expr/type_normalization.py index a1b3fad..2715e1f 100644 --- a/pythonbpf/expr/type_normalization.py +++ b/pythonbpf/expr/type_normalization.py @@ -84,3 +84,14 @@ def normalize_types(func, builder, lhs, rhs): 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) From d38d73d5c66c1ff2cf5c96eea242ff1360b51b3d Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Wed, 8 Oct 2025 07:20:04 +0530 Subject: [PATCH 51/60] Move handle_comparator to type_normalization --- pythonbpf/expr/expr_pass.py | 35 ++-------------------------- pythonbpf/expr/type_normalization.py | 35 ++++++++++++++++++++++++++-- 2 files changed, 35 insertions(+), 35 deletions(-) diff --git a/pythonbpf/expr/expr_pass.py b/pythonbpf/expr/expr_pass.py index a72a4bb..21be196 100644 --- a/pythonbpf/expr/expr_pass.py +++ b/pythonbpf/expr/expr_pass.py @@ -5,7 +5,7 @@ import logging from typing import Dict from pythonbpf.type_deducer import ctypes_to_ir, is_ctypes -from .type_normalization import normalize_types, convert_to_bool +from .type_normalization import convert_to_bool, handle_comparator logger: Logger = logging.getLogger(__name__) @@ -130,37 +130,6 @@ def _handle_ctypes_call( return val -def _handle_comparator(func, builder, op, lhs, rhs): - """Handle comparison operations.""" - - # NOTE: For now assume same types - if lhs.type != rhs.type: - lhs, rhs = normalize_types(func, builder, lhs, rhs) - - if lhs is None or rhs is None: - return None - - comparison_ops = { - ast.Eq: "==", - ast.NotEq: "!=", - ast.Lt: "<", - ast.LtE: "<=", - ast.Gt: ">", - ast.GtE: ">=", - ast.Is: "==", - ast.IsNot: "!=", - } - - 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) - - def _handle_compare( func, module, builder, cond, local_sym_tab, map_sym_tab, structs_sym_tab=None ): @@ -194,7 +163,7 @@ def _handle_compare( lhs, _ = lhs rhs, _ = rhs - return _handle_comparator(func, builder, cond.ops[0], lhs, rhs) + return handle_comparator(func, builder, cond.ops[0], lhs, rhs) def _handle_unary_op( diff --git a/pythonbpf/expr/type_normalization.py b/pythonbpf/expr/type_normalization.py index 2715e1f..7a2fb57 100644 --- a/pythonbpf/expr/type_normalization.py +++ b/pythonbpf/expr/type_normalization.py @@ -1,8 +1,20 @@ 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.""" @@ -60,7 +72,7 @@ def _deref_to_depth(func, builder, val, target_depth): return cur_val -def normalize_types(func, builder, lhs, rhs): +def _normalize_types(func, builder, lhs, rhs): """Normalize types for comparison.""" logger.info(f"Normalizing types: {lhs.type} vs {rhs.type}") @@ -83,7 +95,7 @@ def normalize_types(func, builder, lhs, rhs): 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) + return _normalize_types(func, builder, lhs, rhs) def convert_to_bool(builder, val): @@ -95,3 +107,22 @@ def convert_to_bool(builder, val): 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) From 6362a5e665026b0380efa4be548b3ce7b2959b49 Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Wed, 8 Oct 2025 07:24:14 +0530 Subject: [PATCH 52/60] Fix expr imports --- pythonbpf/expr/__init__.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/pythonbpf/expr/__init__.py b/pythonbpf/expr/__init__.py index 55eae5e..d58c543 100644 --- a/pythonbpf/expr/__init__.py +++ b/pythonbpf/expr/__init__.py @@ -1,3 +1,4 @@ -from .expr_pass import eval_expr, handle_expr, convert_to_bool +from .expr_pass import eval_expr, handle_expr +from .type_normalization import convert_to_bool __all__ = ["eval_expr", "handle_expr", "convert_to_bool"] From 17004d58df777b0c3c925ab9daade1559e37376a Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Wed, 8 Oct 2025 07:25:14 +0530 Subject: [PATCH 53/60] Remove completed short term goal from TODO.md --- TODO.md | 1 - 1 file changed, 1 deletion(-) diff --git a/TODO.md b/TODO.md index 7019d91..6be4112 100644 --- a/TODO.md +++ b/TODO.md @@ -5,7 +5,6 @@ - XDP support in pylibbpf - ringbuf support - recursive expression resolution -- Add supoprt for BoolOp and short circuiting in conditions ## Long term From 9fdc6fa3ed28a386dfb1f18c3be319ead2623ba4 Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Wed, 8 Oct 2025 07:26:41 +0530 Subject: [PATCH 54/60] Add compile to tests/failing_tests/conditionals/helper_cond.py --- tests/failing_tests/conditionals/helper_cond.py | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/tests/failing_tests/conditionals/helper_cond.py b/tests/failing_tests/conditionals/helper_cond.py index 32473c3..8cf5bdb 100644 --- a/tests/failing_tests/conditionals/helper_cond.py +++ b/tests/failing_tests/conditionals/helper_cond.py @@ -1,4 +1,4 @@ -from pythonbpf import bpf, map, section, bpfglobal +from pythonbpf import bpf, map, section, bpfglobal, compile from ctypes import c_void_p, c_int64, c_uint64 from pythonbpf.maps import HashMap @@ -29,3 +29,6 @@ def hello_world(ctx: c_void_p) -> c_int64: @bpfglobal def LICENSE() -> str: return "GPL" + + +compile() From 8e3942d38c341ea03c47e9d5a3ebb378fc3f9a81 Mon Sep 17 00:00:00 2001 From: varun-r-mallya Date: Wed, 8 Oct 2025 14:31:37 +0530 Subject: [PATCH 55/60] format chore --- tests/failing_tests/undeclared_values.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/tests/failing_tests/undeclared_values.py b/tests/failing_tests/undeclared_values.py index 02f5184..1267f55 100644 --- a/tests/failing_tests/undeclared_values.py +++ b/tests/failing_tests/undeclared_values.py @@ -8,7 +8,7 @@ from ctypes import c_void_p, c_int64 @section("tracepoint/syscalls/sys_enter_execve") def sometag(ctx: c_void_p) -> c_int64: print("test") - print(f"{somevalue}") #type: ignore + print(f"{somevalue}") # noqa: F821 return c_int64(1) @bpf From e66ae7cc8926953c15d90f2106afbde86c92441f Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Wed, 8 Oct 2025 21:36:08 +0530 Subject: [PATCH 56/60] Add failing oneline IfExpr conditional test --- tests/failing_tests/conditionals/oneline.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) create mode 100644 tests/failing_tests/conditionals/oneline.py diff --git a/tests/failing_tests/conditionals/oneline.py b/tests/failing_tests/conditionals/oneline.py new file mode 100644 index 0000000..55cfa42 --- /dev/null +++ b/tests/failing_tests/conditionals/oneline.py @@ -0,0 +1,18 @@ +from pythonbpf import bpf, section, bpfglobal, compile +from ctypes import c_void_p, c_int64 + + +@bpf +@section("tracepoint/syscalls/sys_enter_execve") +def hello_world(ctx: c_void_p) -> c_int64: + print("Hello, World!") if True else print("Goodbye, World!") + return + + +@bpf +@bpfglobal +def LICENSE() -> str: + return "GPL" + + +compile() From 120aec08dad29bf690b67ec07e3fa7eab02a6758 Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi Date: Wed, 8 Oct 2025 21:40:14 +0530 Subject: [PATCH 57/60] Update TODO.md --- TODO.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/TODO.md b/TODO.md index 78f7967..a1daba2 100644 --- a/TODO.md +++ b/TODO.md @@ -4,7 +4,7 @@ - Add all maps - XDP support in pylibbpf - ringbuf support -- recursive expression resolution +- Add oneline IfExpr conditionals (wishlist) ## Long term From 989134f4be33e827640c1096dbad1b778c8fb26f Mon Sep 17 00:00:00 2001 From: varun-r-mallya Date: Wed, 8 Oct 2025 21:47:02 +0530 Subject: [PATCH 58/60] add patch for Kernel 6.14 BTF Signed-off-by: varun-r-mallya --- tools/vmlinux-gen.py | 11 +++++++++++ 1 file changed, 11 insertions(+) diff --git a/tools/vmlinux-gen.py b/tools/vmlinux-gen.py index 5bd913a..a1a580b 100755 --- a/tools/vmlinux-gen.py +++ b/tools/vmlinux-gen.py @@ -243,6 +243,17 @@ class BTFConverter: data ) + # below to replace those c_bool with bitfield greater than 8 + def repl(m): + name, bits = m.groups() + return f"('{name}', ctypes.c_uint32, {bits})" if int(bits) > 8 else m.group(0) + + data = re.sub( + r"\('([^']+)',\s*ctypes\.c_bool,\s*(\d+)\)", + repl, + data + ) + # Remove ctypes. prefix from invalid entries invalid_ctypes = ["bpf_iter_state", "_cache_type", "fs_context_purpose"] for name in invalid_ctypes: From 6402cf7be5c0abfd4469485a70e46701e12a7c9e Mon Sep 17 00:00:00 2001 From: varun-r-mallya Date: Wed, 8 Oct 2025 22:27:51 +0530 Subject: [PATCH 59/60] remove todos and move to projects on github. --- TODO.md | 13 ------------- 1 file changed, 13 deletions(-) delete mode 100644 TODO.md diff --git a/TODO.md b/TODO.md deleted file mode 100644 index a1daba2..0000000 --- a/TODO.md +++ /dev/null @@ -1,13 +0,0 @@ -## Short term - -- Implement enough functionality to port the BCC tutorial examples in PythonBPF -- Add all maps -- XDP support in pylibbpf -- ringbuf support -- Add oneline IfExpr conditionals (wishlist) - -## Long term - -- Refactor the codebase to be better than a hackathon project -- Port to C++ and use actual LLVM? -- Fix struct_kioctx issue in the vmlinux transpiler From 21ce041353c1d046265267b2fbd13c82ced0879e Mon Sep 17 00:00:00 2001 From: Pragyansh Chaturvedi <76248539+r41k0u@users.noreply.github.com> Date: Fri, 10 Oct 2025 20:45:07 +0530 Subject: [PATCH 60/60] Refactor hist() calls to use dot notation --- README.md | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/README.md b/README.md index 17e4de4..1b4c3f8 100644 --- a/README.md +++ b/README.md @@ -83,14 +83,14 @@ def hist() -> HashMap: def hello(ctx: c_void_p) -> c_int64: process_id = pid() one = 1 - prev = hist().lookup(process_id) + prev = hist.lookup(process_id) if prev: previous_value = prev + 1 print(f"count: {previous_value} with {process_id}") - hist().update(process_id, previous_value) + hist.update(process_id, previous_value) return c_int64(0) else: - hist().update(process_id, one) + hist.update(process_id, one) return c_int64(0)