Skip to content

Commit 3c3a35d

Browse files
r41k0uclaude
andcommitted
Core: Carry signedness from every declaration and inference site
Sign now originates wherever a type is declared or inferred, and emitted IR changes only where a value is widened out of a type now known to be unsigned: - Helper return types are typed per the kernel signature: ktime_get_ns, get_current_pid_tgid, get_current_uid_gid, get_current_cgroup_id, get_prandom_u32 and get_smp_processor_id are unsigned; the probe_read family, perf_event_output and get_stack return long. - Undeclared locals are 64-bit with the sign of their initializer -- from the helper registry for helper results, and for binary operations from a small static inference (expr/type_inference.py) applying the same usual arithmetic conversions the code generator will. - Integer literals are typed as C types them (int if it fits, else long long) while remaining 64-bit constants; this rank is what makes u32 / -2 promote to an unsigned 32-bit division. - signedness() understands vmlinux Field descriptors, and load_ctx_field sign-extends signed sub-64-bit context fields instead of always zero-extending them. - printk widening goes through convert. Two sites decided whether to emit a conversion from descriptor *equality*. With literal descriptors now narrower than the constants that carry them, that skipped required truncations (storing an i64 into an i32 slot). Assignment and the ctypes cast handler now always run convert, which is a no-op when the physical widths already agree. Corpus: the following programs change, every changed line a sext->zext swap on a value of known unsigned type: failing_tests/xdp/xdp_test_1.py.ll passing_tests/assign/augassign_struct_field.py.ll passing_tests/helpers/smp_processor_id.py.ll Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BSDVsZH5NtoASyxB8FCtGU
1 parent f8db6f9 commit 3c3a35d

8 files changed

Lines changed: 181 additions & 62 deletions

File tree

pythonbpf/allocation_pass.py

Lines changed: 21 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,8 @@
66
from pythonbpf.helper import HelperHandlerRegistry
77
from pythonbpf.vmlinux_parser.dependency_node import Field
88
from .expr import VmlinuxHandlerRegistry
9-
from pythonbpf.type_deducer import ctypes_to_ir
9+
from pythonbpf.type_deducer import ctypes_to_ir, IntTy, signedness
10+
from pythonbpf.expr.type_inference import infer_int_type
1011
from pythonbpf.maps import BPFMapType
1112

1213
logger = logging.getLogger(__name__)
@@ -72,7 +73,9 @@ def handle_assign_allocation(compilation_context, builder, stmt, local_sym_tab):
7273
elif isinstance(rval, ast.Constant):
7374
_allocate_for_constant(builder, var_name, rval, local_sym_tab)
7475
elif isinstance(rval, ast.BinOp):
75-
_allocate_for_binop(builder, var_name, local_sym_tab)
76+
_allocate_for_binop(
77+
builder, var_name, rval, local_sym_tab, compilation_context
78+
)
7679
elif isinstance(rval, ast.Name):
7780
# Variable-to-variable assignment (b = a)
7881
_allocate_for_name(builder, var_name, rval, local_sym_tab)
@@ -104,7 +107,11 @@ def _allocate_for_call(builder, var_name, rval, local_sym_tab, compilation_conte
104107

105108
# Helper functions
106109
elif HelperHandlerRegistry.has_handler(call_type):
107-
ir_type = ir.IntType(64) # Assume i64 return type
110+
# Undeclared locals are 64-bit; the sign comes from the helper.
111+
ret = HelperHandlerRegistry.get_return_type(call_type)
112+
ir_type = IntTy(
113+
64, signedness(ret) if isinstance(ret, ir.IntType) else True
114+
)
108115
var = builder.alloca(ir_type, name=var_name)
109116
var.align = 8
110117
local_sym_tab[var_name] = LocalSymbol(var, ir_type)
@@ -270,9 +277,17 @@ def _allocate_for_constant(builder, var_name, rval, local_sym_tab):
270277
)
271278

272279

273-
def _allocate_for_binop(builder, var_name, local_sym_tab):
274-
"""Allocate memory for variable assigned from a binary operation."""
275-
ir_type = ir.IntType(64) # Assume i64 result
280+
def _allocate_for_binop(builder, var_name, rval, local_sym_tab, compilation_context):
281+
"""Allocate memory for variable assigned from a binary operation.
282+
283+
Undeclared locals are 64-bit; the sign is that of the expression's C type,
284+
inferred statically. Falls back to signed when the expression involves
285+
something the inference does not know.
286+
"""
287+
inferred = infer_int_type(rval, local_sym_tab, compilation_context)
288+
if inferred is None:
289+
logger.debug(f"Could not infer a type for {var_name}, assuming signed i64")
290+
ir_type = IntTy(64, signedness(inferred) if inferred is not None else True)
276291
var = builder.alloca(ir_type, name=var_name)
277292
var.align = 8
278293
local_sym_tab[var_name] = LocalSymbol(var, ir_type)

pythonbpf/assign_pass.py

Lines changed: 6 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -151,7 +151,12 @@ def handle_variable_assignment(
151151
f"Evaluated value for {var_name}: {val} of type {val_type}, expected {var_type}"
152152
)
153153

154-
if val_type != var_type:
154+
if isinstance(val_type, ir.IntType) and isinstance(var_type, ir.IntType):
155+
# The descriptor may be narrower than the constant carrying the value
156+
# (a literal is a 64-bit constant typed as C int), so never decide
157+
# from descriptor equality: convert is a no-op when widths match.
158+
val = convert(builder, val, val_type, var_type)
159+
elif val_type != var_type:
155160
# Handle vmlinux struct pointers - they're represented as Python classes but are i64 pointers
156161
if isclass(val_type) and (val_type.__module__ == "vmlinux"):
157162
logger.info("Handling vmlinux struct pointer assignment")
@@ -216,8 +221,6 @@ def handle_variable_assignment(
216221
f"Failed to assign ctype struct field to {var_name}: {val_type} != {var_type}"
217222
)
218223
return False
219-
elif isinstance(val_type, ir.IntType) and isinstance(var_type, ir.IntType):
220-
val = convert(builder, val, val_type, var_type)
221224
elif isinstance(val_type, ir.IntType) and isinstance(var_type, ir.PointerType):
222225
# NOTE: This is assignment to a PTR_TO_MAP_VALUE_OR_NULL
223226
logger.info(

pythonbpf/expr/expr_pass.py

Lines changed: 17 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,7 @@
44
import logging
55
from typing import Dict
66

7-
from pythonbpf.type_deducer import ctypes_to_ir, is_ctypes
7+
from pythonbpf.type_deducer import ctypes_to_ir, is_ctypes, IntTy
88
from .call_registry import CallHandlerRegistry
99
from .ir_ops import deref_to_depth, access_struct_field
1010
from .operators import apply_binop, UNARY_OPS, BOOL_OPS
@@ -49,7 +49,11 @@ def _handle_name_expr(
4949
def _handle_constant_expr(compilation_context, builder, expr: ast.Constant):
5050
"""Handle ast.Constant expressions."""
5151
if isinstance(expr.value, int) or isinstance(expr.value, bool):
52-
return ir.Constant(ir.IntType(64), int(expr.value)), ir.IntType(64)
52+
# C gives a literal the type int if it fits, otherwise long long. That
53+
# rank is what makes `u32 / -2` an unsigned 32-bit division as in C.
54+
v = int(expr.value)
55+
lit_ty = IntTy(32, True) if -(1 << 31) <= v < (1 << 31) else IntTy(64, True)
56+
return ir.Constant(ir.IntType(64), v), lit_ty
5357
elif isinstance(expr.value, str):
5458
str_name = f".str.{id(expr)}"
5559
str_bytes = expr.value.encode("utf-8") + b"\x00"
@@ -307,32 +311,17 @@ def _handle_ctypes_call(
307311
else:
308312
actual_ir_type = val_type
309313

310-
if actual_ir_type != expected_type:
311-
# NOTE: We are only considering casting to and from int types for now
312-
if isinstance(actual_ir_type, ir.IntType) and isinstance(
313-
expected_type, ir.IntType
314-
):
315-
if actual_ir_type.width < expected_type.width:
316-
value = builder.sext(value, expected_type)
317-
logger.info(
318-
f"Sign-extended from i{actual_ir_type.width} to i{
319-
expected_type.width
320-
}"
321-
)
322-
elif actual_ir_type.width > expected_type.width:
323-
value = builder.trunc(value, expected_type)
324-
logger.info(
325-
f"Truncated from i{actual_ir_type.width} to i{expected_type.width}"
326-
)
327-
else:
328-
# Same width, just use as-is (e.g., both i64)
329-
pass
330-
else:
331-
raise ValueError(
332-
f"Type mismatch: expected {expected_type}, got {
333-
actual_ir_type
334-
} (original type: {val_type})"
335-
)
314+
if isinstance(actual_ir_type, ir.IntType) and isinstance(expected_type, ir.IntType):
315+
# A cast is truncate-or-extend per the source's sign; the result then
316+
# takes the cast's type. Decide from the value's physical width (as
317+
# convert does), never from descriptor equality: a literal is a 64-bit
318+
# constant whose descriptor may already read as C int.
319+
value = convert(builder, value, actual_ir_type, expected_type)
320+
elif actual_ir_type != expected_type:
321+
raise ValueError(
322+
f"Type mismatch: expected {expected_type}, got {actual_ir_type} "
323+
f"(original type: {val_type})"
324+
)
336325

337326
return value, expected_type
338327

pythonbpf/expr/type_inference.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,100 @@
1+
"""Static integer typing of an expression, for the allocation pass.
2+
3+
The allocation pass runs before code generation and must size and type a slot
4+
for `x = <expr>` without evaluating <expr>. Undeclared locals are always 64-bit
5+
(declare with a ctypes constructor for a narrower type); what this decides is
6+
their sign, by walking the expression with the same usual-arithmetic-conversion
7+
rule the code generator applies.
8+
"""
9+
10+
import ast
11+
import ctypes
12+
13+
from llvmlite import ir
14+
15+
from pythonbpf.type_deducer import (
16+
IntTy,
17+
ctypes_to_ir,
18+
is_ctypes,
19+
is_signed_ctype,
20+
signedness,
21+
)
22+
from .operators import usual_arithmetic_conversions
23+
from .vmlinux_registry import VmlinuxHandlerRegistry
24+
25+
26+
def _as_intty(ty):
27+
if isinstance(ty, ir.IntType):
28+
return IntTy(ty.width, signedness(ty))
29+
return None
30+
31+
32+
def infer_int_type(expr, local_sym_tab, compilation_context):
33+
"""Best static integer type of `expr`, or None when it cannot be determined."""
34+
if isinstance(expr, ast.Constant) and isinstance(expr.value, (int, bool)):
35+
v = int(expr.value)
36+
return IntTy(32, True) if -(1 << 31) <= v < (1 << 31) else IntTy(64, True)
37+
38+
if isinstance(expr, ast.Name):
39+
if expr.id in local_sym_tab:
40+
return _as_intty(local_sym_tab[expr.id].ir_type)
41+
if expr.id in compilation_context.bpf_globals:
42+
return _as_intty(compilation_context.bpf_globals[expr.id].ir_type)
43+
if VmlinuxHandlerRegistry.handle_name(expr.id) is not None:
44+
return IntTy(64, True) # enum constants are emitted as i64
45+
return None
46+
47+
if isinstance(expr, ast.BinOp):
48+
left = infer_int_type(expr.left, local_sym_tab, compilation_context)
49+
right = infer_int_type(expr.right, local_sym_tab, compilation_context)
50+
if left is None or right is None:
51+
return None
52+
return usual_arithmetic_conversions(left, right)
53+
54+
if isinstance(expr, ast.UnaryOp):
55+
inner = infer_int_type(expr.operand, local_sym_tab, compilation_context)
56+
return None if inner is None else usual_arithmetic_conversions(inner, inner)
57+
58+
if isinstance(expr, ast.Call) and isinstance(expr.func, ast.Name):
59+
from pythonbpf.helper import HelperHandlerRegistry # avoid an import cycle
60+
61+
name = expr.func.id
62+
if is_ctypes(name):
63+
return _as_intty(ctypes_to_ir(name))
64+
if HelperHandlerRegistry.has_handler(name):
65+
return _as_intty(HelperHandlerRegistry.get_return_type(name))
66+
return None
67+
68+
if isinstance(expr, ast.Call) and isinstance(expr.func, ast.Attribute):
69+
# map.lookup(k) used as a value: the map's declared value type
70+
map_name = getattr(expr.func.value, "id", None)
71+
sym = compilation_context.map_sym_tab.get(map_name)
72+
value = (sym.params or {}).get("value") if sym else None
73+
return (
74+
_as_intty(ctypes_to_ir(value))
75+
if isinstance(value, str) and is_ctypes(value)
76+
else None
77+
)
78+
79+
if isinstance(expr, ast.Attribute) and isinstance(expr.value, ast.Name):
80+
base = local_sym_tab.get(expr.value.id)
81+
if base is None:
82+
return None
83+
meta = base.metadata
84+
if meta in compilation_context.structs_sym_tab:
85+
return _as_intty(
86+
compilation_context.structs_sym_tab[meta].field_type(expr.attr)
87+
)
88+
if getattr(meta, "__module__", None) == "vmlinux":
89+
try:
90+
_, field = VmlinuxHandlerRegistry.get_field_type(
91+
meta.__name__, expr.attr
92+
)
93+
cname = field.type.__name__
94+
if is_ctypes(cname):
95+
return IntTy(ctypes.sizeof(field.type) * 8, is_signed_ctype(cname))
96+
except Exception:
97+
return None
98+
return None
99+
100+
return None

pythonbpf/helper/bpf_helper_handler.py

Lines changed: 14 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
from enum import Enum
44

55
from .helper_registry import HelperHandlerRegistry
6+
from pythonbpf.type_deducer import IntTy
67
from .helper_utils import (
78
get_or_create_ptr_from_arg,
89
get_flags_val,
@@ -45,7 +46,7 @@ class BPFHelperID(Enum):
4546
@HelperHandlerRegistry.register(
4647
"ktime",
4748
param_types=[],
48-
return_type=ir.IntType(64),
49+
return_type=IntTy(64, False),
4950
)
5051
def bpf_ktime_get_ns_emitter(
5152
call,
@@ -70,7 +71,7 @@ def bpf_ktime_get_ns_emitter(
7071
@HelperHandlerRegistry.register(
7172
"get_current_cgroup_id",
7273
param_types=[],
73-
return_type=ir.IntType(64),
74+
return_type=IntTy(64, False),
7475
)
7576
def bpf_get_current_cgroup_id(
7677
call,
@@ -310,7 +311,7 @@ def bpf_map_delete_elem_emitter(
310311
@HelperHandlerRegistry.register(
311312
"comm",
312313
param_types=[ir.PointerType(ir.IntType(8))],
313-
return_type=ir.IntType(64),
314+
return_type=IntTy(64, True),
314315
)
315316
def bpf_get_current_comm_emitter(
316317
call,
@@ -369,7 +370,7 @@ def bpf_get_current_comm_emitter(
369370
@HelperHandlerRegistry.register(
370371
"pid",
371372
param_types=[],
372-
return_type=ir.IntType(64),
373+
return_type=IntTy(64, False),
373374
)
374375
def bpf_get_current_pid_tgid_emitter(
375376
call,
@@ -496,7 +497,7 @@ def bpf_ringbuf_output_emitter(
496497
@HelperHandlerRegistry.register(
497498
"output",
498499
param_types=[ir.PointerType(ir.IntType(8))],
499-
return_type=ir.IntType(64),
500+
return_type=IntTy(64, True),
500501
)
501502
def handle_output_helper(
502503
call,
@@ -566,7 +567,7 @@ def emit_probe_read_kernel_str_call(builder, dst_ptr, dst_size, src_ptr):
566567
ir.PointerType(ir.IntType(8)),
567568
ir.PointerType(ir.IntType(8)),
568569
],
569-
return_type=ir.IntType(64),
570+
return_type=IntTy(64, True),
570571
)
571572
def bpf_probe_read_kernel_str_emitter(
572573
call,
@@ -633,7 +634,7 @@ def emit_probe_read_kernel_call(builder, dst_ptr, dst_size, src_ptr):
633634
ir.PointerType(ir.IntType(8)),
634635
ir.PointerType(ir.IntType(8)),
635636
],
636-
return_type=ir.IntType(64),
637+
return_type=IntTy(64, True),
637638
)
638639
def bpf_probe_read_kernel_emitter(
639640
call,
@@ -670,7 +671,7 @@ def bpf_probe_read_kernel_emitter(
670671
@HelperHandlerRegistry.register(
671672
"random",
672673
param_types=[],
673-
return_type=ir.IntType(32),
674+
return_type=IntTy(32, False),
674675
)
675676
def bpf_get_prandom_u32_emitter(
676677
call,
@@ -698,7 +699,7 @@ def bpf_get_prandom_u32_emitter(
698699
ir.IntType(32),
699700
ir.PointerType(ir.IntType(8)),
700701
],
701-
return_type=ir.IntType(64),
702+
return_type=IntTy(64, True),
702703
)
703704
def bpf_probe_read_emitter(
704705
call,
@@ -763,7 +764,7 @@ def bpf_probe_read_emitter(
763764
@HelperHandlerRegistry.register(
764765
"smp_processor_id",
765766
param_types=[],
766-
return_type=ir.IntType(32),
767+
return_type=IntTy(32, False),
767768
)
768769
def bpf_get_smp_processor_id_emitter(
769770
call,
@@ -788,7 +789,7 @@ def bpf_get_smp_processor_id_emitter(
788789
@HelperHandlerRegistry.register(
789790
"uid",
790791
param_types=[],
791-
return_type=ir.IntType(64),
792+
return_type=IntTy(64, False),
792793
)
793794
def bpf_get_current_uid_gid_emitter(
794795
call,
@@ -822,7 +823,7 @@ def bpf_get_current_uid_gid_emitter(
822823
ir.IntType(32),
823824
ir.IntType(64),
824825
],
825-
return_type=ir.IntType(64),
826+
return_type=IntTy(64, True),
826827
)
827828
def bpf_skb_store_bytes_emitter(
828829
call,
@@ -1010,7 +1011,7 @@ def bpf_ringbuf_submit_emitter(
10101011
@HelperHandlerRegistry.register(
10111012
"get_stack",
10121013
param_types=[ir.PointerType(ir.IntType(8)), ir.IntType(64)],
1013-
return_type=ir.IntType(64),
1014+
return_type=IntTy(64, True),
10141015
)
10151016
def bpf_get_stack_emitter(
10161017
call,

pythonbpf/helper/printk_formatter.py

Lines changed: 4 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22
import logging
33

44
from llvmlite import ir
5-
from pythonbpf.expr import eval_expr, get_base_type_and_depth, deref_to_depth
5+
from pythonbpf.expr import eval_expr, get_base_type_and_depth, deref_to_depth, convert
66
from pythonbpf.expr.vmlinux_registry import VmlinuxHandlerRegistry
77
from pythonbpf.helper.helper_utils import get_char_array_ptr_and_size
88

@@ -267,8 +267,6 @@ def _handle_pointer_arg(val, func, builder):
267267
return ir.Constant(ir.IntType(64), 0)
268268

269269

270-
def _handle_int_arg(val, builder):
271-
"""Convert integer type for bpf_printk (sign-extend to i64)."""
272-
if val.type.width < 64:
273-
return builder.sext(val, ir.IntType(64))
274-
return val
270+
def _handle_int_arg(val, builder, ty=None):
271+
"""Widen an integer for bpf_printk to i64, per the value's sign."""
272+
return convert(builder, val, ty if ty is not None else val.type, ir.IntType(64))

0 commit comments

Comments
 (0)