Skip to content

Commit f5c4dec

Browse files
r41k0uclaude
andcommitted
Core: Unify the Python-operator tables in expr/operators.py
Operator knowledge was spread across three files: the binary-op table (an op_map dict rebuilt inline on every _handle_binary_op_impl call, then extracted to ir_ops.apply_binop for augmented assignment), COMPARISON_OPS in type_normalization.py, and the unary/boolean operators as isinstance chains in expr_pass.py. Anyone asking "which operators does PythonBPF support" had to read all three. They now live in one module. BINOP_METHODS and COMPARISON_OPS are the tables themselves; UNARY_OPS and BOOL_OPS list the operators whose lowering is structural (convert_to_bool, short-circuit control flow) so the registry is complete even where the emission stays in expr_pass. apply_binop moves here from ir_ops, which goes back to being purely about IR operations. No behaviour change: 133 passed, 19 xfailed before and after. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BSDVsZH5NtoASyxB8FCtGU
1 parent e8ab118 commit f5c4dec

6 files changed

Lines changed: 68 additions & 43 deletions

File tree

.claude/skills/ir-first-feature/SKILL.md

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ both use `apply_binop` for the operator table and `get_operand_value` for operan
8181
and let each handler resolve its own target and emit its own store. Two handlers calling
8282
one helper is the idiom; one handler manufacturing input for another is not.
8383

84+
The operator tables themselves — binary operators, comparisons, and the supported
85+
unary/boolean operators — live in exactly one place, `expr/operators.py`. A new operator
86+
is added there first; if it is not in that file, the compiler does not support it.
87+
8488
## 6. Test at the right tier
8589

8690
- Works now → `tests/passing_tests/<category>/`.

pythonbpf/expr/__init__.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
from .expr_pass import eval_expr, handle_expr, get_operand_value
22
from .type_normalization import convert_to_bool, get_base_type_and_depth
3-
from .ir_ops import deref_to_depth, access_struct_field, apply_binop
3+
from .ir_ops import deref_to_depth, access_struct_field
4+
from .operators import apply_binop
45
from .call_registry import CallHandlerRegistry
56
from .vmlinux_registry import VmlinuxHandlerRegistry
67

pythonbpf/expr/expr_pass.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66

77
from pythonbpf.type_deducer import ctypes_to_ir, is_ctypes
88
from .call_registry import CallHandlerRegistry
9-
from .ir_ops import deref_to_depth, access_struct_field, apply_binop
9+
from .ir_ops import deref_to_depth, access_struct_field
10+
from .operators import apply_binop, UNARY_OPS, BOOL_OPS
1011
from .type_normalization import (
1112
convert_to_bool,
1213
handle_comparator,
@@ -375,7 +376,7 @@ def _handle_unary_op(
375376
local_sym_tab,
376377
):
377378
"""Handle ast.UnaryOp expressions."""
378-
if not isinstance(expr.op, ast.Not) and not isinstance(expr.op, ast.USub):
379+
if not isinstance(expr.op, UNARY_OPS):
379380
logger.error("Only 'not' and '-' unary operators are supported")
380381
return None
381382

@@ -518,6 +519,9 @@ def _handle_boolean_op(
518519
):
519520
"""Handle `and` and `or` boolean operations."""
520521

522+
if not isinstance(expr.op, BOOL_OPS):
523+
logger.error(f"Unsupported boolean operator: {type(expr.op).__name__}")
524+
return None
521525
if isinstance(expr.op, ast.And):
522526
return _handle_and_op(func, builder, expr, local_sym_tab, compilation_context)
523527
elif isinstance(expr.op, ast.Or):

pythonbpf/expr/ir_ops.py

Lines changed: 0 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -1,37 +1,9 @@
1-
import ast
21
import logging
32
from llvmlite import ir
43

54
logger = logging.getLogger(__name__)
65

76

8-
BINOP_METHODS = {
9-
ast.Add: "add",
10-
ast.Sub: "sub",
11-
ast.Mult: "mul",
12-
ast.Div: "sdiv",
13-
ast.Mod: "srem",
14-
ast.LShift: "shl",
15-
ast.RShift: "lshr",
16-
ast.BitOr: "or_",
17-
ast.BitXor: "xor",
18-
ast.BitAnd: "and_",
19-
ast.FloorDiv: "udiv",
20-
}
21-
22-
23-
def apply_binop(builder, op, left, right):
24-
"""Emit the LLVM instruction for a Python binary operator.
25-
26-
Shared by binary-op evaluation and augmented assignment so the operator
27-
table exists exactly once.
28-
"""
29-
method = BINOP_METHODS.get(type(op))
30-
if method is None:
31-
raise SyntaxError("Unsupported binary operation")
32-
return getattr(builder, method)(left, right)
33-
34-
357
def deref_to_depth(func, builder, val, target_depth):
368
"""Dereference a pointer to a certain depth."""
379

pythonbpf/expr/operators.py

Lines changed: 55 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
"""Every Python operator PythonBPF understands, and the IR it maps to.
2+
3+
This is the single registry. Binary operators map to IRBuilder method names,
4+
comparisons map to icmp predicates, and the unary/boolean operators are listed
5+
here even though their lowering is structural (they need convert_to_bool or
6+
short-circuit control flow, so they live in expr_pass): if an operator is not
7+
in this file, the compiler does not support it. Add new operators here first.
8+
"""
9+
10+
import ast
11+
12+
# ast.BinOp.op class -> llvmlite IRBuilder method name.
13+
# Shared by binary-op evaluation and augmented assignment.
14+
BINOP_METHODS = {
15+
ast.Add: "add",
16+
ast.Sub: "sub",
17+
ast.Mult: "mul",
18+
ast.Div: "sdiv",
19+
ast.Mod: "srem",
20+
ast.LShift: "shl",
21+
ast.RShift: "lshr",
22+
ast.BitOr: "or_",
23+
ast.BitXor: "xor",
24+
ast.BitAnd: "and_",
25+
ast.FloorDiv: "udiv",
26+
}
27+
28+
# ast.Compare op class -> icmp predicate string.
29+
COMPARISON_OPS = {
30+
ast.Eq: "==",
31+
ast.NotEq: "!=",
32+
ast.Lt: "<",
33+
ast.LtE: "<=",
34+
ast.Gt: ">",
35+
ast.GtE: ">=",
36+
ast.Is: "==",
37+
ast.IsNot: "!=",
38+
}
39+
40+
# Lowered structurally in expr_pass (need convert_to_bool / control flow).
41+
UNARY_OPS = (ast.Not, ast.USub)
42+
BOOL_OPS = (ast.And, ast.Or)
43+
44+
45+
def apply_binop(builder, op, left, right):
46+
"""Emit the LLVM instruction for a Python binary operator."""
47+
method = BINOP_METHODS.get(type(op))
48+
if method is None:
49+
raise SyntaxError(f"Unsupported binary operation: {type(op).__name__}")
50+
return getattr(builder, method)(left, right)
51+
52+
53+
def comparison_predicate(op):
54+
"""icmp predicate for a Python comparison operator, or None if unsupported."""
55+
return COMPARISON_OPS.get(type(op))

pythonbpf/expr/type_normalization.py

Lines changed: 1 addition & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,10 @@
11
import logging
2-
import ast
32
from llvmlite import ir
43
from .ir_ops import deref_to_depth
4+
from .operators import COMPARISON_OPS
55

66
logger = logging.getLogger(__name__)
77

8-
COMPARISON_OPS = {
9-
ast.Eq: "==",
10-
ast.NotEq: "!=",
11-
ast.Lt: "<",
12-
ast.LtE: "<=",
13-
ast.Gt: ">",
14-
ast.GtE: ">=",
15-
ast.Is: "==",
16-
ast.IsNot: "!=",
17-
}
18-
198

209
def get_base_type_and_depth(ir_type):
2110
"""Get the base type for pointer types."""

0 commit comments

Comments
 (0)