Skip to content

Commit b16009d

Browse files
r41k0uclaude
andcommitted
Core: Add the signedness machinery (no behaviour change)
LLVM integer types are sign-agnostic; the sign lives in the operations. The frontend therefore has to carry it, and until now it did not: c_uint32 became a bare i32 at declaration and every later choice (sext, sdiv, icmp_signed) assumed signed. This commit adds the pieces without changing any emitted IR: - type_deducer.IntTy, an ir.IntType subclass carrying 'signed'. It renders, compares and hashes as the plain type, so the 40 isinstance checks and every == ir.IntType(64) comparison keep working; signedness() reads it and treats a plain IntType as signed (today's behaviour). Constructed outside IntType's per-width instance cache, which is shared with subclasses and would have merged the two signs of a width into one object. - ctypes_to_ir now returns IntTy, which types five declaration kinds at once: locals from ctypes constructors, struct fields, globals, context annotations, and map key/value types. is_signed_ctype replaces the private copy in globals_pass. - operators.usual_arithmetic_conversions: C's rule for the type a binary operation is performed in. - type_normalization.convert (source-driven zext/sext when widening, trunc when narrowing) and canonicalise (bring a value to the working width holding exactly a given type's value). Nothing consults the sign yet. The corpus of 67 compilable test programs produces byte-identical .ll before and after. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BSDVsZH5NtoASyxB8FCtGU
1 parent 2455bf1 commit b16009d

4 files changed

Lines changed: 159 additions & 36 deletions

File tree

pythonbpf/expr/operators.py

Lines changed: 26 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
import ast
1111

12+
from pythonbpf.type_deducer import IntTy, signedness
13+
1214
# ast.BinOp.op class -> llvmlite IRBuilder method name.
1315
# Shared by binary-op evaluation and augmented assignment.
1416
BINOP_METHODS = {
@@ -53,3 +55,27 @@ def apply_binop(builder, op, left, right):
5355
def comparison_predicate(op):
5456
"""icmp predicate for a Python comparison operator, or None if unsupported."""
5557
return COMPARISON_OPS.get(type(op))
58+
59+
60+
def usual_arithmetic_conversions(left, right) -> IntTy:
61+
"""The type a C binary operation on `left` and `right` is performed in.
62+
63+
Integer promotion first: anything narrower than int becomes a signed 32-bit
64+
int (int can represent every value of the narrower type, signed or not).
65+
Then, if the signs agree, the wider type wins; if they differ, the unsigned
66+
operand wins at equal or greater width, otherwise the signed one -- because
67+
it can then represent every value of the unsigned one.
68+
"""
69+
70+
def promote(ty):
71+
if ty.width < 32:
72+
return IntTy(32, True)
73+
return IntTy(ty.width, signedness(ty))
74+
75+
left, right = promote(left), promote(right)
76+
if left.signed == right.signed:
77+
return IntTy(max(left.width, right.width), left.signed)
78+
unsigned, signed = (left, right) if not left.signed else (right, left)
79+
if unsigned.width >= signed.width:
80+
return IntTy(unsigned.width, False)
81+
return IntTy(signed.width, True)

pythonbpf/expr/type_normalization.py

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
import logging
22
from llvmlite import ir
33
from .ir_ops import deref_to_depth
4+
from pythonbpf.type_deducer import signedness
45
from .operators import COMPARISON_OPS
56

67
logger = logging.getLogger(__name__)
@@ -42,6 +43,39 @@ def _normalize_types(func, builder, lhs, rhs):
4243
return _normalize_types(func, builder, lhs, rhs)
4344

4445

46+
def convert(builder, val, from_ty, to_ty):
47+
"""Convert an integer value between types the way C does.
48+
49+
Widening is driven by the *source* sign (zext for unsigned, sext for
50+
signed) so the mathematical value is preserved; narrowing truncates; equal
51+
width is a reinterpretation and emits nothing. `from_ty` and `to_ty` are
52+
descriptors (see type_deducer.IntTy); the physical width comes from the
53+
value itself, which may already be wider than its descriptor says.
54+
"""
55+
if not (isinstance(to_ty, ir.IntType) and isinstance(val.type, ir.IntType)):
56+
return val
57+
if val.type.width > to_ty.width:
58+
return builder.trunc(val, to_ty)
59+
if val.type.width < to_ty.width:
60+
ext = builder.zext if not signedness(from_ty) else builder.sext
61+
return ext(val, to_ty)
62+
return val
63+
64+
65+
def canonicalise(builder, val, ty, width=64):
66+
"""Bring `val` to the working width holding exactly the value of type `ty`:
67+
truncate to ty's width if the register is wider (so the operation wraps at
68+
ty's width, as C does), then extend per ty's sign."""
69+
if not isinstance(val.type, ir.IntType):
70+
return val
71+
if val.type.width > ty.width:
72+
val = builder.trunc(val, ir.IntType(ty.width))
73+
if val.type.width < width:
74+
ext = builder.zext if not signedness(ty) else builder.sext
75+
val = ext(val, ir.IntType(width))
76+
return val
77+
78+
4579
def convert_to_bool(builder, val):
4680
"""Convert a value to boolean."""
4781
if val.type == ir.IntType(1):

pythonbpf/globals_pass.py

Lines changed: 2 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -3,26 +3,14 @@
33

44
from logging import Logger
55
import logging
6-
from .type_deducer import ctypes_to_ir
6+
from .type_deducer import ctypes_to_ir, is_signed_ctype
77
from .symbols import BpfGlobalSymbol
88
from .debuginfo import DebugInfoGenerator
99
from .expr import VmlinuxHandlerRegistry
1010
from .debuginfo import dwarf_constants as dc
1111

1212
logger: Logger = logging.getLogger(__name__)
1313

14-
_SIGNED_CTYPES = {
15-
"c_int8",
16-
"c_int16",
17-
"c_int32",
18-
"c_int64",
19-
"c_int",
20-
"c_short",
21-
"c_long",
22-
"c_longlong",
23-
"c_byte",
24-
}
25-
2614
_C_NAME_BY_WIDTH = {8: "char", 16: "short", 32: "int", 64: "long long"}
2715

2816

@@ -106,7 +94,7 @@ def _emit_global_debug_info(compilation_context, gvar, name, ctype_name):
10694
"""
10795
generator = DebugInfoGenerator(compilation_context.module)
10896
width = gvar.value_type.width
109-
signed = ctype_name in _SIGNED_CTYPES
97+
signed = is_signed_ctype(ctype_name)
11098
base = _C_NAME_BY_WIDTH[width]
11199
if width == 8:
112100
encoding = dc.DW_ATE_signed_char if signed else dc.DW_ATE_unsigned_char

pythonbpf/type_deducer.py

Lines changed: 97 additions & 22 deletions
Original file line numberDiff line numberDiff line change
@@ -1,30 +1,105 @@
11
from llvmlite import ir
22

3+
4+
class IntTy(ir.IntType):
5+
"""An LLVM integer type that also remembers its signedness.
6+
7+
LLVM integer types are sign-agnostic by design: `i32` is just 32 bits, and
8+
the sign lives in the operations (sdiv/udiv, sext/zext, icmp s*/u*). The
9+
frontend therefore has to carry it. IntTy is a plain ir.IntType for every
10+
purpose LLVM cares about -- it renders as `i32`, compares and hashes equal
11+
to ir.IntType(32), and passes every isinstance check -- with one extra
12+
attribute the compiler reads when choosing between signed and unsigned
13+
forms of an operation.
14+
15+
Invariant: the sign is read only from a *descriptor* -- a Symbol.ir_type
16+
or the type half of an eval_expr result -- never from `value.type`. Values
17+
produced by the IRBuilder (loads, arithmetic, extensions) come back with a
18+
plain ir.IntType, so a sign on a value's own type is lost at the first
19+
operation. Descriptors are constructed by the compiler; that is where the
20+
sign lives.
21+
"""
22+
23+
def __new__(cls, bits: int, signed: bool = True):
24+
# ir.IntType.__new__ memoises one instance per width in a cache shared
25+
# with subclasses. Going through it would (a) merge the signed and
26+
# unsigned flavours of a width into one object and (b) plant an IntTy
27+
# in the cache so that ir.IntType(32) itself started returning one.
28+
# Construct directly instead; equality and hashing are inherited and
29+
# depend only on the width, so an IntTy still compares equal to i32.
30+
self = object.__new__(cls)
31+
self.width = bits
32+
return self
33+
34+
def __init__(self, bits: int, signed: bool = True):
35+
self.signed = signed
36+
37+
def __getnewargs__(self):
38+
return self.width, self.signed
39+
40+
def describe(self) -> str:
41+
return f"{'i' if self.signed else 'u'}{self.width}"
42+
43+
44+
def signedness(ty) -> bool:
45+
"""Sign of an integer type descriptor. Plain ir.IntType (a site that has not
46+
been taught to carry a sign yet) reads as signed, which is the compiler's
47+
historical behaviour."""
48+
return getattr(ty, "signed", True)
49+
50+
51+
_SIGNED_CTYPES = {
52+
"c_int8",
53+
"c_int16",
54+
"c_int32",
55+
"c_int64",
56+
"c_int",
57+
"c_short",
58+
"c_long",
59+
"c_longlong",
60+
"c_byte",
61+
}
62+
63+
_INT_CTYPE_WIDTHS = {
64+
"c_int8": 8,
65+
"c_uint8": 8,
66+
"c_byte": 8,
67+
"c_ubyte": 8,
68+
"c_int16": 16,
69+
"c_uint16": 16,
70+
"c_short": 16,
71+
"c_ushort": 16,
72+
"c_int32": 32,
73+
"c_uint32": 32,
74+
"c_int": 32,
75+
"c_uint": 32,
76+
"c_int64": 64,
77+
"c_uint64": 64,
78+
"c_long": 64,
79+
"c_ulong": 64,
80+
"c_longlong": 64,
81+
# A pointer-sized integer; treated as unsigned like uintptr_t.
82+
"c_void_p": 64,
83+
}
84+
85+
86+
def is_signed_ctype(ctype: str) -> bool:
87+
return ctype in _SIGNED_CTYPES
88+
89+
390
# TODO: THIS IS NOT SUPPOSED TO MATCH STRINGS :skull:
491
mapping = {
5-
"c_int8": ir.IntType(8),
6-
"c_uint8": ir.IntType(8),
7-
"c_int16": ir.IntType(16),
8-
"c_uint16": ir.IntType(16),
9-
"c_int32": ir.IntType(32),
10-
"c_uint32": ir.IntType(32),
11-
"c_int64": ir.IntType(64),
12-
"c_uint64": ir.IntType(64),
13-
"c_float": ir.FloatType(),
14-
"c_double": ir.DoubleType(),
15-
"c_void_p": ir.IntType(64),
16-
"c_long": ir.IntType(64),
17-
"c_ulong": ir.IntType(64),
18-
"c_longlong": ir.IntType(64),
19-
"c_uint": ir.IntType(32),
20-
"c_int": ir.IntType(32),
21-
"c_ushort": ir.IntType(16),
22-
"c_short": ir.IntType(16),
23-
"c_ubyte": ir.IntType(8),
24-
"c_byte": ir.IntType(8),
25-
# Not so sure about this one
26-
"str": ir.PointerType(ir.IntType(8)),
92+
name: IntTy(width, is_signed_ctype(name))
93+
for name, width in _INT_CTYPE_WIDTHS.items()
2794
}
95+
mapping.update(
96+
{
97+
"c_float": ir.FloatType(),
98+
"c_double": ir.DoubleType(),
99+
# Not so sure about this one
100+
"str": ir.PointerType(ir.IntType(8)),
101+
}
102+
)
28103

29104

30105
def ctypes_to_ir(ctype: str):

0 commit comments

Comments
 (0)