From 3effae93b847a4b94002efec05488b4d27bbb8bf Mon Sep 17 00:00:00 2001 From: Varun R Mallya Date: Wed, 23 Sep 2026 17:50:06 +0530 Subject: [PATCH] tests: Add loop and AnnAssign xfail tests - process_stmt now raises NotImplementedError on an unsupported statement instead of warning: a dropped statement silently changes what the program means (a for/while loop compiled to nothing). - failing_tests/loops/: 8 xfail tests for for/while/break/continue/nested loops and map iteration - failing_tests/assign/annotated_assign.py: xfail for `x: c_int64 = 0` (ast.AnnAssign is not handled) --- pythonbpf/functions/functions_pass.py | 10 +++---- .../failing_tests/assign/annotated_assign.py | 18 ++++++++++++ tests/failing_tests/loops/for_break.py | 22 ++++++++++++++ tests/failing_tests/loops/for_continue.py | 22 ++++++++++++++ tests/failing_tests/loops/for_map_items.py | 29 +++++++++++++++++++ .../loops/for_range_start_stop_step.py | 20 +++++++++++++ tests/failing_tests/loops/for_range_sum.py | 20 +++++++++++++ tests/failing_tests/loops/nested_for.py | 21 ++++++++++++++ tests/failing_tests/loops/while_basic.py | 20 +++++++++++++ tests/failing_tests/loops/while_true_break.py | 22 ++++++++++++++ tests/test_config.toml | 25 ++++++++++++++++ 11 files changed, 224 insertions(+), 5 deletions(-) create mode 100644 tests/failing_tests/assign/annotated_assign.py create mode 100644 tests/failing_tests/loops/for_break.py create mode 100644 tests/failing_tests/loops/for_continue.py create mode 100644 tests/failing_tests/loops/for_map_items.py create mode 100644 tests/failing_tests/loops/for_range_start_stop_step.py create mode 100644 tests/failing_tests/loops/for_range_sum.py create mode 100644 tests/failing_tests/loops/nested_for.py create mode 100644 tests/failing_tests/loops/while_basic.py create mode 100644 tests/failing_tests/loops/while_true_break.py diff --git a/pythonbpf/functions/functions_pass.py b/pythonbpf/functions/functions_pass.py index b1d17b06..8aee77a6 100644 --- a/pythonbpf/functions/functions_pass.py +++ b/pythonbpf/functions/functions_pass.py @@ -401,11 +401,11 @@ def process_stmt( builder, stmt, local_sym_tab, ret_type, compilation_context ) else: - # Silently dropping a statement makes the program mean something other - # than what it says, so say so. - logger.warning( - f"Unsupported statement on line {getattr(stmt, 'lineno', '?')}, " - f"ignored: {type(stmt).__name__}" + # Dropping a statement makes the program mean something other than what + # it says (a loop compiles to nothing), so refuse rather than warn. + raise NotImplementedError( + f"Unsupported statement on line {getattr(stmt, 'lineno', '?')}: " + f"{type(stmt).__name__}" ) return did_return diff --git a/tests/failing_tests/assign/annotated_assign.py b/tests/failing_tests/assign/annotated_assign.py new file mode 100644 index 00000000..432dc327 --- /dev/null +++ b/tests/failing_tests/assign/annotated_assign.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(ctx: c_void_p) -> c_int64: + total: c_int64 = 0 + return total + + +@bpf +@bpfglobal +def LICENSE() -> str: + return "GPL" + + +compile() diff --git a/tests/failing_tests/loops/for_break.py b/tests/failing_tests/loops/for_break.py new file mode 100644 index 00000000..4a8e107d --- /dev/null +++ b/tests/failing_tests/loops/for_break.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(ctx: c_void_p) -> c_int64: + total: c_int64 = 0 + for i in range(10): + if i == 5: + break + total = total + 1 + return total + + +@bpf +@bpfglobal +def LICENSE() -> str: + return "GPL" + + +compile() diff --git a/tests/failing_tests/loops/for_continue.py b/tests/failing_tests/loops/for_continue.py new file mode 100644 index 00000000..b8b14f7d --- /dev/null +++ b/tests/failing_tests/loops/for_continue.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(ctx: c_void_p) -> c_int64: + total: c_int64 = 0 + for i in range(10): + if i % 2 == 0: + continue + total = total + i + return total + + +@bpf +@bpfglobal +def LICENSE() -> str: + return "GPL" + + +compile() diff --git a/tests/failing_tests/loops/for_map_items.py b/tests/failing_tests/loops/for_map_items.py new file mode 100644 index 00000000..4deaca04 --- /dev/null +++ b/tests/failing_tests/loops/for_map_items.py @@ -0,0 +1,29 @@ +from pythonbpf import bpf, map, bpfglobal, section, compile +from pythonbpf.maps import HashMap +from ctypes import c_void_p, c_int64, c_int32, c_uint64 + + +@bpf +@map +def mymap() -> HashMap: + return HashMap(key=c_int32, value=c_uint64, max_entries=16) + + +# Imagined sugar over the kernel's bpf_for_each_map_elem() callback helper: +# iterating a map's entries directly from a `for` statement. +@bpf +@section("tracepoint/syscalls/sys_enter_execve") +def hello(ctx: c_void_p) -> c_int64: + total: c_int64 = 0 + for k, v in mymap.items(): + total = total + 1 + return total + + +@bpf +@bpfglobal +def LICENSE() -> str: + return "GPL" + + +compile() diff --git a/tests/failing_tests/loops/for_range_start_stop_step.py b/tests/failing_tests/loops/for_range_start_stop_step.py new file mode 100644 index 00000000..bc82c1f0 --- /dev/null +++ b/tests/failing_tests/loops/for_range_start_stop_step.py @@ -0,0 +1,20 @@ +from pythonbpf import bpf, section, bpfglobal, compile +from ctypes import c_void_p, c_int64 + + +@bpf +@section("tracepoint/syscalls/sys_enter_execve") +def hello(ctx: c_void_p) -> c_int64: + total: c_int64 = 0 + for i in range(0, 10, 2): + total = total + i + return total + + +@bpf +@bpfglobal +def LICENSE() -> str: + return "GPL" + + +compile() diff --git a/tests/failing_tests/loops/for_range_sum.py b/tests/failing_tests/loops/for_range_sum.py new file mode 100644 index 00000000..425e36e6 --- /dev/null +++ b/tests/failing_tests/loops/for_range_sum.py @@ -0,0 +1,20 @@ +from pythonbpf import bpf, section, bpfglobal, compile +from ctypes import c_void_p, c_int64 + + +@bpf +@section("tracepoint/syscalls/sys_enter_execve") +def hello(ctx: c_void_p) -> c_int64: + total: c_int64 = 0 + for i in range(10): + total = total + 1 + return total + + +@bpf +@bpfglobal +def LICENSE() -> str: + return "GPL" + + +compile() diff --git a/tests/failing_tests/loops/nested_for.py b/tests/failing_tests/loops/nested_for.py new file mode 100644 index 00000000..f20d11d4 --- /dev/null +++ b/tests/failing_tests/loops/nested_for.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(ctx: c_void_p) -> c_int64: + total: c_int64 = 0 + for i in range(3): + for j in range(3): + total = total + 1 + return total + + +@bpf +@bpfglobal +def LICENSE() -> str: + return "GPL" + + +compile() diff --git a/tests/failing_tests/loops/while_basic.py b/tests/failing_tests/loops/while_basic.py new file mode 100644 index 00000000..c1564b66 --- /dev/null +++ b/tests/failing_tests/loops/while_basic.py @@ -0,0 +1,20 @@ +from pythonbpf import bpf, section, bpfglobal, compile +from ctypes import c_void_p, c_int64 + + +@bpf +@section("tracepoint/syscalls/sys_enter_execve") +def hello(ctx: c_void_p) -> c_int64: + i: c_int64 = 0 + while i < 10: + i = i + 1 + return i + + +@bpf +@bpfglobal +def LICENSE() -> str: + return "GPL" + + +compile() diff --git a/tests/failing_tests/loops/while_true_break.py b/tests/failing_tests/loops/while_true_break.py new file mode 100644 index 00000000..8c37070b --- /dev/null +++ b/tests/failing_tests/loops/while_true_break.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(ctx: c_void_p) -> c_int64: + total: c_int64 = 0 + while True: + total = total + 1 + if total == 10: + break + return total + + +@bpf +@bpfglobal +def LICENSE() -> str: + return "GPL" + + +compile() diff --git a/tests/test_config.toml b/tests/test_config.toml index cab8db6a..24d0ade1 100644 --- a/tests/test_config.toml +++ b/tests/test_config.toml @@ -41,3 +41,28 @@ "failing_tests/globals_augassign_shadowing.py" = {reason = "`x += 1` on a global name without `global` binds a local and reads it unbound: UnboundLocalError in Python, a compile error here", level = "ir"} "failing_tests/globals_parameter_and_global.py" = {reason = "A parameter may not be declared global (Python: name is parameter and global)", level = "ir"} + +"failing_tests/assign/annotated_assign.py" = {reason = "Annotated assignment (ast.AnnAssign, e.g. `x: c_int64 = 0`) is not handled by process_stmt", level = "ir"} + +# Loop constructs: ast.For/ast.While are not handled by the statement dispatcher +# (pythonbpf/functions/functions_pass.py process_stmt only matches Expr, Assign, +# AugAssign, If, Return) and the pre-pass variable scan does not descend into +# loop bodies. A loop is silently skipped rather than erroring, so anything +# that reads a variable assigned in/around the loop afterward blows up later +# with "Undefined variable ..." at the `return` statement. These document the +# intended surface for when loops are implemented. +"failing_tests/loops/for_range_sum.py" = {reason = "for/while loops not implemented: ast.For is silently skipped by the statement dispatcher", level = "ir"} + +"failing_tests/loops/for_range_start_stop_step.py" = {reason = "for/while loops not implemented: range() with start/stop/step is not supported", level = "ir"} + +"failing_tests/loops/while_basic.py" = {reason = "for/while loops not implemented: ast.While is silently skipped by the statement dispatcher", level = "ir"} + +"failing_tests/loops/while_true_break.py" = {reason = "for/while loops not implemented: while True with a break-based exit is not supported", level = "ir"} + +"failing_tests/loops/nested_for.py" = {reason = "for/while loops not implemented: nested for loops are not supported", level = "ir"} + +"failing_tests/loops/for_break.py" = {reason = "for/while loops not implemented: break is not supported", level = "ir"} + +"failing_tests/loops/for_continue.py" = {reason = "for/while loops not implemented: continue is not supported", level = "ir"} + +"failing_tests/loops/for_map_items.py" = {reason = "for/while loops not implemented: no sugar over bpf_for_each_map_elem()-style map iteration exists yet", level = "ir"}