Skip to content

Commit 603de4b

Browse files
r41k0uclaude
andcommitted
Core: Type each binary operation per node with C's arithmetic conversions
Arithmetic is now typed bottom-up through the expression tree the way C does it. get_typed_operand evaluates an operand to (value, IntTy); a binary node computes its type with usual_arithmetic_conversions, converts each operand to that type per the operand's own sign (source-driven, folding literals), runs the operation in the i64 working register, and narrows the result back to the node's type. That last step is what makes a u32 * u32 wrap at 32 bits before it is widened into a u64, exactly as clang emits (mul i32, zext) -- verified against tests/c-form/signedness.bpf.c. There is no expression-wide signed or unsigned mode; each node decides from its operands, and the assignment target only converts the finished result. Augmented assignment is typed identically (x op= v is x = x op v), unary minus negates in the operand's promoted type (2^N - x for unsigned), and return statements now convert the value to the declared return type instead of emitting whatever width the expression happened to have. Corpus: 13 programs change; with SSA numbering normalised every changed line is a trunc/sext/zext inserted by canonicalisation where an operation's type is narrower than 64 bits (int-typed literals and c_int32/c_uint32 operands). No program changes compile status. Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01BSDVsZH5NtoASyxB8FCtGU
1 parent 4db253d commit 603de4b

4 files changed

Lines changed: 117 additions & 60 deletions

File tree

‎pythonbpf/expr/__init__.py‎

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,12 @@
1-
from .expr_pass import eval_expr, handle_expr, get_operand_value
1+
from .expr_pass import eval_expr, handle_expr, get_operand_value, get_typed_operand
22
from .type_normalization import (
33
convert_to_bool,
44
get_base_type_and_depth,
55
convert,
66
canonicalise,
7+
to_promoted,
78
)
9+
from .operators import usual_arithmetic_conversions
810
from .ir_ops import deref_to_depth, access_struct_field
911
from .operators import apply_binop
1012
from .call_registry import CallHandlerRegistry
@@ -16,6 +18,9 @@
1618
"convert_to_bool",
1719
"convert",
1820
"canonicalise",
21+
"to_promoted",
22+
"get_typed_operand",
23+
"usual_arithmetic_conversions",
1924
"get_base_type_and_depth",
2025
"deref_to_depth",
2126
"apply_binop",

‎pythonbpf/expr/expr_pass.py‎

Lines changed: 70 additions & 49 deletions
Original file line numberDiff line numberDiff line change
@@ -4,12 +4,14 @@
44
import logging
55
from typing import Dict
66

7-
from pythonbpf.type_deducer import ctypes_to_ir, is_ctypes, IntTy
7+
from pythonbpf.type_deducer import ctypes_to_ir, is_ctypes, IntTy, signedness
88
from .call_registry import CallHandlerRegistry
99
from .ir_ops import deref_to_depth, access_struct_field
10-
from .operators import apply_binop, UNARY_OPS, BOOL_OPS
10+
from .operators import apply_binop, usual_arithmetic_conversions, UNARY_OPS, BOOL_OPS
1111
from .type_normalization import (
1212
convert,
13+
to_promoted,
14+
canonicalise,
1315
convert_to_bool,
1416
handle_comparator,
1517
get_base_type_and_depth,
@@ -180,71 +182,89 @@ def _handle_deref_call(expr: ast.Call, local_sym_tab: Dict, builder: ir.IRBuilde
180182
# ============================================================================
181183

182184

183-
def get_operand_value(func, compilation_context, operand, builder, local_sym_tab):
184-
"""Extract the value from an operand, handling variables and constants."""
185-
logger.info(f"Getting operand value for: {ast.dump(operand)}")
185+
def _descriptor(val, ty):
186+
"""IntTy descriptor for an evaluated integer value: width from the physical
187+
value unless the descriptor is itself an integer type, sign from the
188+
descriptor (an IntTy, a vmlinux Field, or plain -> signed)."""
189+
width = ty.width if isinstance(ty, ir.IntType) else val.type.width
190+
return IntTy(width, signedness(ty))
191+
192+
193+
def get_typed_operand(func, compilation_context, operand, builder, local_sym_tab):
194+
"""Evaluate an operand to (value, IntTy). Pointers (map-lookup results) are
195+
dereferenced to the scalar they point at."""
196+
logger.info(f"Getting typed operand for: {ast.dump(operand)}")
186197
if isinstance(operand, ast.Name):
187198
if operand.id in local_sym_tab:
188-
var = local_sym_tab[operand.id].var
189-
var_type = var.type
190-
base_type, depth = get_base_type_and_depth(var_type)
191-
logger.info(f"var is {var}, base_type is {base_type}, depth is {depth}")
192-
if depth == 1:
193-
val = builder.load(var)
194-
return val
195-
else:
196-
val = deref_to_depth(func, builder, var, depth)
197-
return val
199+
sym = local_sym_tab[operand.id]
200+
var = sym.var
201+
base_type, depth = get_base_type_and_depth(var.type)
202+
val = (
203+
builder.load(var)
204+
if depth == 1
205+
else deref_to_depth(func, builder, var, depth)
206+
)
207+
return val, _descriptor(val, sym.ir_type if depth == 1 else base_type)
198208
elif operand.id in compilation_context.bpf_globals:
199-
# A @bpfglobal: plain load off the global symbol.
200-
return builder.load(compilation_context.bpf_globals[operand.id].var)
209+
sym = compilation_context.bpf_globals[operand.id]
210+
return builder.load(sym.var), _descriptor(None, sym.ir_type)
201211
else:
202-
# Check if it's a vmlinux enum/constant
203212
vmlinux_result = VmlinuxHandlerRegistry.handle_name(operand.id)
204213
if vmlinux_result is not None:
205214
val, _ = vmlinux_result
206-
return val
215+
return val, IntTy(64, True)
207216
elif isinstance(operand, ast.Constant):
208-
if isinstance(operand.value, int):
209-
cst = ir.Constant(ir.IntType(64), int(operand.value))
210-
return cst
217+
if isinstance(operand.value, (int, bool)):
218+
v = int(operand.value)
219+
lit_ty = IntTy(32, True) if -(1 << 31) <= v < (1 << 31) else IntTy(64, True)
220+
return ir.Constant(ir.IntType(64), v), lit_ty
211221
raise TypeError(f"Unsupported constant type: {type(operand.value)}")
212222
elif isinstance(operand, ast.BinOp):
213-
res = _handle_binary_op_impl(
223+
return _handle_binary_op_impl(
214224
func, compilation_context, operand, builder, local_sym_tab
215225
)
216-
return res
217226
else:
218227
res = eval_expr(func, compilation_context, builder, operand, local_sym_tab)
219228
if res is None:
220229
raise ValueError(f"Failed to evaluate call expression: {operand}")
221-
val, _ = res
230+
val, ty = res
222231
logger.info(f"Evaluated expr to {val} of type {val.type}")
223232
base_type, depth = get_base_type_and_depth(val.type)
224233
if depth > 0:
225234
val = deref_to_depth(func, builder, val, depth)
226-
return val
235+
return val, _descriptor(val, ty)
227236
raise TypeError(f"Unsupported operand type: {type(operand)}")
228237

229238

239+
def get_operand_value(func, compilation_context, operand, builder, local_sym_tab):
240+
"""Extract the value from an operand, handling variables and constants."""
241+
return get_typed_operand(
242+
func, compilation_context, operand, builder, local_sym_tab
243+
)[0]
244+
245+
230246
def _handle_binary_op_impl(func, compilation_context, rval, builder, local_sym_tab):
247+
"""A binary operation, typed per node the way C types it: the operation is
248+
performed in the type given by the usual arithmetic conversions of its two
249+
operands, each operand converted to that type first, and the result
250+
narrowed to it -- so u32 * u32 wraps at 32 bits even though the arithmetic
251+
itself runs in an i64 register. Returns (value, IntTy)."""
231252
op = rval.op
232-
left = get_operand_value(
253+
left, left_ty = get_typed_operand(
233254
func, compilation_context, rval.left, builder, local_sym_tab
234255
)
235-
right = get_operand_value(
256+
right, right_ty = get_typed_operand(
236257
func, compilation_context, rval.right, builder, local_sym_tab
237258
)
238-
logger.info(f"left is {left}, right is {right}, op is {op}")
239-
240-
# NOTE: Before doing the operation, if the operands are integers
241-
# we always extend them to i64. The assignment to LHS will take
242-
# care of truncation if needed.
243-
left = convert(builder, left, left.type, ir.IntType(64))
244-
right = convert(builder, right, right.type, ir.IntType(64))
245-
246-
# Map AST operation nodes to LLVM IR builder methods
247-
return apply_binop(builder, op, left, right)
259+
result_ty = usual_arithmetic_conversions(left_ty, right_ty)
260+
logger.info(
261+
f"binop {type(op).__name__}: {left_ty.describe()} x {right_ty.describe()} "
262+
f"-> {result_ty.describe()}"
263+
)
264+
left = to_promoted(builder, left, left_ty, result_ty)
265+
right = to_promoted(builder, right, right_ty, result_ty)
266+
result = apply_binop(builder, op, left, right)
267+
return canonicalise(builder, result, result_ty), result_ty
248268

249269

250270
def _handle_binary_op(
@@ -255,15 +275,14 @@ def _handle_binary_op(
255275
var_name,
256276
local_sym_tab,
257277
):
258-
result = _handle_binary_op_impl(
278+
result, result_ty = _handle_binary_op_impl(
259279
func, compilation_context, rval, builder, local_sym_tab
260280
)
261281
if var_name and var_name in local_sym_tab:
262-
logger.info(
263-
f"Storing result {result} into variable {local_sym_tab[var_name].var}"
264-
)
265-
builder.store(result, local_sym_tab[var_name].var)
266-
return result, result.type
282+
slot = local_sym_tab[var_name]
283+
logger.info(f"Storing result {result} into variable {slot.var}")
284+
builder.store(convert(builder, result, result_ty, slot.ir_type), slot.var)
285+
return result, result_ty
267286

268287

269288
# ============================================================================
@@ -371,7 +390,7 @@ def _handle_unary_op(
371390
logger.error("Only 'not' and '-' unary operators are supported")
372391
return None
373392

374-
operand = get_operand_value(
393+
operand, operand_ty = get_typed_operand(
375394
func, compilation_context, expr.operand, builder, local_sym_tab
376395
)
377396
if operand is None:
@@ -383,10 +402,12 @@ def _handle_unary_op(
383402
result = builder.xor(convert_to_bool(builder, operand), true_const)
384403
return result, ir.IntType(1)
385404
elif isinstance(expr.op, ast.USub):
386-
# Multiply by -1
387-
neg_one = ir.Constant(ir.IntType(64), -1)
388-
result = builder.mul(operand, neg_one)
389-
return result, ir.IntType(64)
405+
# Negation happens in the operand's promoted type; for an unsigned
406+
# operand that is C's 2^N - x, which the narrowing produces.
407+
result_ty = usual_arithmetic_conversions(operand_ty, operand_ty)
408+
operand = to_promoted(builder, operand, operand_ty, result_ty)
409+
result = builder.mul(operand, ir.Constant(ir.IntType(64), -1))
410+
return canonicalise(builder, result, result_ty), result_ty
390411
return None
391412

392413

‎pythonbpf/expr/type_normalization.py‎

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,12 +62,36 @@ def convert(builder, val, from_ty, to_ty):
6262
return val
6363

6464

65+
def _fold_int_constant(val, ty, width):
66+
"""A literal re-expressed at the working width holding type ty's value:
67+
wrap to ty's width, take the representative ty's sign implies."""
68+
v = val.constant % (1 << ty.width)
69+
if signedness(ty) and v >= 1 << (ty.width - 1):
70+
v -= 1 << ty.width
71+
return ir.Constant(ir.IntType(width), v)
72+
73+
74+
def to_promoted(builder, val, from_ty, to_ty, width=64):
75+
"""Bring an operand to the promoted type of its operation, C-style.
76+
77+
First convert it to to_ty per its *own* sign (that is C's conversion of an
78+
operand to the common type), then widen to the working width per to_ty's
79+
sign so the i64 register holds exactly a to_ty value. Literals are folded.
80+
"""
81+
if isinstance(val, ir.Constant) and isinstance(val.constant, int):
82+
return _fold_int_constant(val, to_ty, width)
83+
val = convert(builder, val, from_ty, ir.IntType(to_ty.width))
84+
return canonicalise(builder, val, to_ty, width)
85+
86+
6587
def canonicalise(builder, val, ty, width=64):
6688
"""Bring `val` to the working width holding exactly the value of type `ty`:
6789
truncate to ty's width if the register is wider (so the operation wraps at
6890
ty's width, as C does), then extend per ty's sign."""
6991
if not isinstance(val.type, ir.IntType):
7092
return val
93+
if isinstance(val, ir.Constant) and isinstance(val.constant, int):
94+
return _fold_int_constant(val, ty, width)
7195
if val.type.width > ty.width:
7296
val = builder.trunc(val, ir.IntType(ty.width))
7397
if val.type.width < width:

‎pythonbpf/functions/functions_pass.py‎

Lines changed: 17 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,12 @@
1111
eval_expr,
1212
handle_expr,
1313
convert_to_bool,
14-
get_operand_value,
14+
get_typed_operand,
1515
apply_binop,
1616
convert,
17+
to_promoted,
18+
canonicalise,
19+
usual_arithmetic_conversions,
1720
VmlinuxHandlerRegistry,
1821
)
1922
from pythonbpf.assign_pass import (
@@ -273,20 +276,22 @@ def handle_aug_assign(func, compilation_context, builder, stmt, local_sym_tab):
273276

274277
# Python evaluates the target's current value before the right-hand side.
275278
current = builder.load(slot)
276-
rhs = get_operand_value(
279+
rhs, rhs_ty = get_typed_operand(
277280
func, compilation_context, stmt.value, builder, local_sym_tab
278281
)
279282
if rhs is None:
280283
raise SyntaxError(
281284
f"Failed to evaluate augmented-assignment value: {ast.dump(stmt.value)}"
282285
)
283-
# Same width discipline as binary-op evaluation: compute in i64, narrow
284-
# back to the slot's width on the way out.
285-
current = convert(builder, current, slot_type, ir.IntType(64))
286-
rhs = convert(builder, rhs, rhs.type, ir.IntType(64))
287-
result = apply_binop(builder, stmt.op, current, rhs)
288-
result = convert(builder, result, result.type, slot_type)
289-
builder.store(result, slot)
286+
# x op= v is typed exactly like x = x op v: operate in the promoted type,
287+
# then convert the result to the target's type on the way back in.
288+
result_ty = usual_arithmetic_conversions(slot_type, rhs_ty)
289+
current = to_promoted(builder, current, slot_type, result_ty)
290+
rhs = to_promoted(builder, rhs, rhs_ty, result_ty)
291+
result = canonicalise(
292+
builder, apply_binop(builder, stmt.op, current, rhs), result_ty
293+
)
294+
builder.store(convert(builder, result, result_ty, slot_type), slot)
290295

291296

292297
def handle_cond(func, compilation_context, builder, cond, local_sym_tab):
@@ -367,7 +372,9 @@ def handle_return(builder, stmt, local_sym_tab, ret_type, compilation_context=No
367372
local_sym_tab=local_sym_tab,
368373
)
369374
logger.info(f"Evaluated return expression to {val}")
370-
builder.ret(val[0])
375+
# The declared return type is the LHS of an implicit assignment:
376+
# widen per the value's sign, truncate if narrower.
377+
builder.ret(convert(builder, val[0], val[1], ret_type))
371378
return True
372379

373380

0 commit comments

Comments
 (0)