Skip to content

Commit 29d7aaa

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 3c3a35d commit 29d7aaa

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,
@@ -177,71 +179,89 @@ def _handle_deref_call(expr: ast.Call, local_sym_tab: Dict, builder: ir.IRBuilde
177179
# ============================================================================
178180

179181

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

226235

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

246266

247267
def _handle_binary_op(
@@ -252,15 +272,14 @@ def _handle_binary_op(
252272
var_name,
253273
local_sym_tab,
254274
):
255-
result = _handle_binary_op_impl(
275+
result, result_ty = _handle_binary_op_impl(
256276
func, compilation_context, rval, builder, local_sym_tab
257277
)
258278
if var_name and var_name in local_sym_tab:
259-
logger.info(
260-
f"Storing result {result} into variable {local_sym_tab[var_name].var}"
261-
)
262-
builder.store(result, local_sym_tab[var_name].var)
263-
return result, result.type
279+
slot = local_sym_tab[var_name]
280+
logger.info(f"Storing result {result} into variable {slot.var}")
281+
builder.store(convert(builder, result, result_ty, slot.ir_type), slot.var)
282+
return result, result_ty
264283

265284

266285
# ============================================================================
@@ -368,7 +387,7 @@ def _handle_unary_op(
368387
logger.error("Only 'not' and '-' unary operators are supported")
369388
return None
370389

371-
operand = get_operand_value(
390+
operand, operand_ty = get_typed_operand(
372391
func, compilation_context, expr.operand, builder, local_sym_tab
373392
)
374393
if operand is None:
@@ -380,10 +399,12 @@ def _handle_unary_op(
380399
result = builder.xor(convert_to_bool(builder, operand), true_const)
381400
return result, ir.IntType(1)
382401
elif isinstance(expr.op, ast.USub):
383-
# Multiply by -1
384-
neg_one = ir.Constant(ir.IntType(64), -1)
385-
result = builder.mul(operand, neg_one)
386-
return result, ir.IntType(64)
402+
# Negation happens in the operand's promoted type; for an unsigned
403+
# operand that is C's 2^N - x, which the narrowing produces.
404+
result_ty = usual_arithmetic_conversions(operand_ty, operand_ty)
405+
operand = to_promoted(builder, operand, operand_ty, result_ty)
406+
result = builder.mul(operand, ir.Constant(ir.IntType(64), -1))
407+
return canonicalise(builder, result, result_ty), result_ty
387408
return None
388409

389410

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 (
@@ -254,20 +257,22 @@ def handle_aug_assign(func, compilation_context, builder, stmt, local_sym_tab):
254257

255258
# Python evaluates the target's current value before the right-hand side.
256259
current = builder.load(slot)
257-
rhs = get_operand_value(
260+
rhs, rhs_ty = get_typed_operand(
258261
func, compilation_context, stmt.value, builder, local_sym_tab
259262
)
260263
if rhs is None:
261264
raise SyntaxError(
262265
f"Failed to evaluate augmented-assignment value: {ast.dump(stmt.value)}"
263266
)
264-
# Same width discipline as binary-op evaluation: compute in i64, narrow
265-
# back to the slot's width on the way out.
266-
current = convert(builder, current, slot_type, ir.IntType(64))
267-
rhs = convert(builder, rhs, rhs.type, ir.IntType(64))
268-
result = apply_binop(builder, stmt.op, current, rhs)
269-
result = convert(builder, result, result.type, slot_type)
270-
builder.store(result, slot)
267+
# x op= v is typed exactly like x = x op v: operate in the promoted type,
268+
# then convert the result to the target's type on the way back in.
269+
result_ty = usual_arithmetic_conversions(slot_type, rhs_ty)
270+
current = to_promoted(builder, current, slot_type, result_ty)
271+
rhs = to_promoted(builder, rhs, rhs_ty, result_ty)
272+
result = canonicalise(
273+
builder, apply_binop(builder, stmt.op, current, rhs), result_ty
274+
)
275+
builder.store(convert(builder, result, result_ty, slot_type), slot)
271276

272277

273278
def handle_cond(func, compilation_context, builder, cond, local_sym_tab):
@@ -348,7 +353,9 @@ def handle_return(builder, stmt, local_sym_tab, ret_type, compilation_context=No
348353
local_sym_tab=local_sym_tab,
349354
)
350355
logger.info(f"Evaluated return expression to {val}")
351-
builder.ret(val[0])
356+
# The declared return type is the LHS of an implicit assignment:
357+
# widen per the value's sign, truncate if narrower.
358+
builder.ret(convert(builder, val[0], val[1], ret_type))
352359
return True
353360

354361

0 commit comments

Comments
 (0)