Files
TransPyC/lib/core/LLVMCG/ExprGen.py
T
TermiNexus 55a6856c4e fix(tpc): >> 符号性只按左操作数判定,去掉“残留变量名”误判(负数右移发成 lshr)
- 现场:`b: int64 = -196608; b >> 1` 发成 lshr ⇒ 负数变大正数;而同样注解的 `c: int64 = 0 - 196608; c >> 1` 却正确 ⇒ 与 RHS/位置相关(IntWidthTest Test 5/6)

- 根因:emit_binary_op 在 i32/i64 分支又用 self._last_var_name 重判符号性,而它只是“最近见过的变量名”,可能是上一条表达式残留的别的(无符号)变量 ⇒ 误判无符号

- 改动:① ExprGen.emit_binary_op 删除 i32/i64 的 _last_var_name 分支(符号性只信调用方;i8/i16 仍按宽度定无符号);② HandlesExprOps 的 `>>` 改用 _check_node_unsigned(Node.left),覆盖 Name 之外的形态

- 验收:Test/IntWidthTest(Test 5/6),目标 TPC 侧 9/9 PASS
2026-10-14 20:18:26 +08:00

450 lines
27 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
from __future__ import annotations
from typing import Any
import logging
import llvmlite.ir as ir
from lib.constants.config import mode as _config_mode
class ExprGenMixin:
def emit_constant(self, value: Any, type_name: str = 'int') -> ir.Value:
if type_name == 'int':
return ir.Constant(ir.IntType(32), int(value))
elif type_name == 'uint64_t' or type_name == 'unsigned long long':
return ir.Constant(ir.IntType(64), int(value))
elif type_name == 'uint32_t' or type_name == 'unsigned int':
return ir.Constant(ir.IntType(32), int(value))
elif type_name == 'uint16_t' or type_name == 'unsigned short':
return ir.Constant(ir.IntType(16), int(value))
elif type_name == 'uint8_t' or type_name == 'unsigned char':
return ir.Constant(ir.IntType(8), int(value))
elif type_name == 'int64_t' or type_name == 'long long':
return ir.Constant(ir.IntType(64), int(value))
elif type_name == 'int32_t' or type_name == 'int':
return ir.Constant(ir.IntType(32), int(value))
elif type_name == 'int16_t' or type_name == 'short':
return ir.Constant(ir.IntType(16), int(value))
elif type_name == 'int8_t' or type_name == 'char':
return ir.Constant(ir.IntType(8), int(value))
elif type_name == 'float' or type_name == 'float32_t' or type_name == 'FLOAT32':
return ir.Constant(ir.FloatType(), float(value))
elif type_name == 'double' or type_name == 'float64_t' or type_name == 'FLOAT64':
return ir.Constant(ir.DoubleType(), float(value))
elif type_name == 'float16_t' or type_name == 'FLOAT16':
return ir.Constant(ir.IntType(16), int(value))
elif type_name == 'float8_t' or type_name == 'FLOAT8':
return ir.Constant(ir.IntType(8), int(value))
elif type_name == 'float128_t' or type_name == 'FLOAT128':
return ir.Constant(ir.IntType(128), int(value))
elif type_name == 'string':
encoded: bytes = value.encode('utf-8') + b'\x00'
str_type: ir.ArrayType = ir.ArrayType(ir.IntType(8), len(encoded))
str_const: ir.Constant = ir.Constant(str_type, bytearray(encoded))
global_var: ir.GlobalVariable = ir.GlobalVariable(self.module, str_type, name=f"str_const_{self.string_const_counter}")
self.string_const_counter += 1
global_var.initializer = str_const
global_var.linkage = 'internal'
return self.builder.bitcast(global_var, ir.PointerType(ir.IntType(8)), name="str")
return ir.Constant(ir.IntType(32), int(value))
def emit_binary_op(self, op: str, left: ir.Value, right: ir.Value, is_unsigned: bool = False, is_aug: bool = False) -> ir.Value:
if isinstance(left.type, ir.PointerType) and isinstance(right.type, ir.IntType):
# 数组指针需要先 decay 为元素指针,否则 GEP 会以整个数组为步长
# 例如 [64 x i8]* + 1 会前进 64 字节而非 1 字节
if isinstance(left.type.pointee, ir.ArrayType):
zero: ir.Constant = ir.Constant(ir.IntType(32), 0)
left = self.builder.gep(left, [zero, zero], name="array_decay")
right = self.builder.zext(right, ir.IntType(64), name="int2ptrint")
if op == '+' or op == 'Add':
return self.builder.gep(left, [right], name="ptr_add")
elif op == '-' or op == 'Sub':
return self.builder.gep(left, [self.builder.neg(right, name="neg")], name="ptr_sub")
if isinstance(left.type, ir.IntType) and isinstance(right.type, ir.PointerType):
if isinstance(right.type.pointee, ir.ArrayType):
zero: ir.Constant = ir.Constant(ir.IntType(32), 0)
right = self.builder.gep(right, [zero, zero], name="array_decay_rev")
left = self.builder.zext(left, ir.IntType(64), name="int2ptrint2")
if op == '+' or op == 'Add':
return self.builder.gep(right, [left], name="ptr_add_rev")
# ============================================================
# 双指针算术全域 fail-fast(**单一收敛点**)
#
# BinOp(HandlesExprOps._HandleBinOpLlvm)与 AugAssign
# (HandlesAugAssign._HandleAugAssignLlvm,+=、-=、*= …)都经本函数,
# 故在此一次收口,避免多点各写一份判据/报文。
# 判据:运算符属二元算术/位运算/移位集合,且**两操作数都是 LLVM 指针**。
# 本语言的 str 是 i8*,没有 `+` 拼接语义;指针 ± 指针也不是 GEP 语义
# (GEP 只接受「一指针一整数」,已在上方两个 ptr/int 分支处理并返回)。
# 落到下方 ops/shift_ops/bit_ops 会直出非法 IR(`add i8*, i8*` ⇒ opt 报
# invalid operand type for instruction),故在 IR 生成前 fail-fast。
# 判据与报文逐句对齐 VPC ExprGenOps.translate_binop / AssignGenMixin._apply_aug_op。
# ============================================================
if op in ('+', 'Add', '-', 'Sub', '*', 'Mult', '/', 'Div', '//', 'FloorDiv',
'%', 'Mod', '**', 'Pow', '&', 'BitAnd', '|', 'BitOr', '^', 'BitXor',
'<<', 'LShift', '>>', 'RShift') \
and isinstance(left.type, ir.PointerType) \
and isinstance(right.type, ir.PointerType):
LeftTy: str = str(left.type)
RightTy: str = str(right.type)
if LeftTy == 'i8*':
LeftTy = 'str(i8*)'
if RightTy == 'i8*':
RightTy = 'str(i8*)'
OpCh: str = op
AugNote: str = ''
if is_aug:
# 增量赋值(+= 等)要能自报家门,便于定位(不写成裸 '+')
OpCh = f'{op}='
AugNote = '(增量赋值)'
raise TypeError(
f"[TransPyC] 双指针运算类型非法: op = '{OpCh}'{AugNote},"
f'左操作数类型 = {LeftTy},右操作数类型 = {RightTy}。'
f'本语言的 str 是 i8*,没有 `+` 拼接语义(`str + str` 与指针 ± 指针'
f'都会退化成裸指针 add/sub ⇒ 非法 IR: opt 报 '
f'invalid operand type for instruction)。'
f'禁止静默降级,已终止编译。'
f'修法: 用 f-string 插值、string.strcat 或 viperlib.snprintf 构建字符串。'
+ self._get_node_info()
)
is_float: bool = isinstance(left.type, (ir.FloatType, ir.DoubleType)) or \
isinstance(right.type, (ir.FloatType, ir.DoubleType))
if is_float:
ftype: ir.Type
if isinstance(left.type, (ir.FloatType, ir.DoubleType)):
ftype = left.type
elif isinstance(right.type, (ir.FloatType, ir.DoubleType)):
ftype = right.type
else:
ftype = ir.DoubleType()
if not isinstance(left.type, (ir.FloatType, ir.DoubleType)):
left = self._to_float(left, ftype)
elif left.type != ftype:
if isinstance(left.type, ir.FloatType) and isinstance(ftype, ir.DoubleType):
left = self.builder.fpext(left, ftype, name="fpext_l")
else:
left = self.builder.fptrunc(left, ftype, name="fptrunc_l")
if not isinstance(right.type, (ir.FloatType, ir.DoubleType)):
right = self._to_float(right, ftype)
elif right.type != ftype:
if isinstance(right.type, ir.FloatType) and isinstance(ftype, ir.DoubleType):
right = self.builder.fpext(right, ftype, name="fpext_r")
else:
right = self.builder.fptrunc(right, ftype, name="fptrunc_r")
float_ops: dict[str, Any] = {
'+': self.builder.fadd, 'Add': self.builder.fadd,
'-': self.builder.fsub, 'Sub': self.builder.fsub,
'*': self.builder.fmul, 'Mult': self.builder.fmul,
'/': self.builder.fdiv, 'Div': self.builder.fdiv,
'%': self.builder.frem, 'Mod': self.builder.frem,
}
float_cmp_ops: dict[str, str] = {
'==': '==', 'Eq': '==', '!=': '!=', 'NotEq': '!=',
'<': '<', 'Lt': '<', '<=': '<=', 'LtE': '<=',
'>': '>', 'Gt': '>', '>=': '>=', 'GtE': '>=',
}
if op in float_ops:
return float_ops[op](left, right, name=op)
if op in float_cmp_ops:
return self.builder.fcmp_ordered(float_cmp_ops[op], left, right, name=op)
ops: dict[str, Any]
if is_unsigned:
ops = {
'+': self.builder.add, 'Add': self.builder.add,
'-': self.builder.sub, 'Sub': self.builder.sub,
'*': self.builder.mul, 'Mult': self.builder.mul,
'/': self.builder.udiv, 'Div': self.builder.udiv,
'%': self.builder.urem, 'Mod': self.builder.urem,
'//': self.builder.udiv, 'FloorDiv': self.builder.udiv,
}
else:
ops = {
'+': self.builder.add, 'Add': self.builder.add,
'-': self.builder.sub, 'Sub': self.builder.sub,
'*': self.builder.mul, 'Mult': self.builder.mul,
'/': self.builder.sdiv, 'Div': self.builder.sdiv,
'%': self.builder.srem, 'Mod': self.builder.srem,
'//': self.builder.sdiv, 'FloorDiv': self.builder.sdiv,
}
cmp_ops: dict[str, str] = {
'==': '==', 'Eq': '==', '!=': '!=', 'NotEq': '!=',
'<': '<', 'Lt': '<', '<=': '<=', 'LtE': '<=',
'>': '>', 'Gt': '>', '>=': '>=', 'GtE': '>=',
}
logic_ops: dict[str, Any] = {
'&&': self.builder.and_, 'And': self.builder.and_,
'||': self.builder.or_, 'Or': self.builder.or_,
}
if op in ('>>', 'RShift') and not is_unsigned:
if isinstance(left.type, ir.IntType):
if left.type.width == 8:
is_unsigned = True
elif left.type.width == 16:
is_unsigned = True
# i32/i64 的符号性**只由调用方按左操作数判定**(调用方拿得到 Node.left,
# 见 HandlesExprOps / HandlesAugAssign)。此前这里又用 self._last_var_name
# 判一次,而它只是“最近见过的变量名”,可能是**上一条表达式残留的别的变量**:
# b: int64 = -196608
# b >> 1 # 若残留名恰好是某个无符号变量(t.CSizeT 等) ⇒ 误判无符号 ⇒ lshr
# 结果:负数右移变大正数(IntWidthTest Test 5/6 的 FAIL 即此)。
shift_ops: dict[str, Any] = {
'>>': self.builder.lshr if is_unsigned else self.builder.ashr,
'RShift': self.builder.lshr if is_unsigned else self.builder.ashr,
'<<': self.builder.shl, 'LShift': self.builder.shl,
}
bit_ops: dict[str, Any] = {
'&': self.builder.and_,
'|': self.builder.or_,
'^': self.builder.xor,
}
# 整数操作数宽度对齐:两个操作数都是整数但位宽不同时,
# 统一扩展到较大位宽(低位宽操作数 zext/sext)。
# 触发场景:`_g_alloc_count: t.CSizeT = 0`(i64 全局)在函数内
# `_g_alloc_count += 1`,RHS 整数字面量是 i32,不扩展会报
# "Operands must be the same type, got (i64, i32)"。
if (op in ops or op in shift_ops or op in bit_ops or op in cmp_ops):
if (isinstance(left.type, ir.IntType) and isinstance(right.type, ir.IntType)
and left.type != right.type and left.type.width != 1 and right.type.width != 1):
if left.type.width > right.type.width:
if is_unsigned:
right = self.builder.zext(right, left.type, name="zext_rhs_op")
else:
right = self.builder.sext(right, left.type, name="sext_rhs_op")
elif right.type.width > left.type.width:
if is_unsigned:
left = self.builder.zext(left, right.type, name="zext_lhs_op")
else:
left = self.builder.sext(left, right.type, name="sext_lhs_op")
if op == '**' or op == 'Pow':
if isinstance(left, ir.Constant) and isinstance(right, ir.Constant):
try:
lv: Any = left.constant
rv: Any = right.constant
if isinstance(lv, int) and isinstance(rv, int) and rv >= 0:
result: int = lv ** rv
return ir.Constant(left.type, result)
except Exception as _e:
if _config_mode == "strict":
logging.warning(f"异常被忽略: {_e}")
pass
ftype: ir.Type
if isinstance(left.type, (ir.FloatType, ir.DoubleType)):
ftype = left.type
elif isinstance(right.type, (ir.FloatType, ir.DoubleType)):
ftype = right.type
else:
ftype = ir.DoubleType()
lf: ir.Value = self._to_float(left, ftype)
rf: ir.Value = self._to_float(right, ftype)
powf: ir.Function | None = self.module.globals.get('llvm.pow.f64')
if not powf:
fnty: ir.FunctionType = ir.FunctionType(ir.DoubleType(), [ir.DoubleType(), ir.DoubleType()])
powf = self._CreateOrReuseFuncDecl('llvm.pow.f64', fnty)
if lf.type != ir.DoubleType():
lf = self.builder.fpext(lf, ir.DoubleType(), name="ext_f64")
if rf.type != ir.DoubleType():
rf = self.builder.fpext(rf, ir.DoubleType(), name="ext_f64")
result: ir.Value = self.builder.call(powf, [lf, rf], name="pow")
both_int: bool = isinstance(left.type, ir.IntType) and isinstance(right.type, ir.IntType)
right_is_pos_int_const: bool = isinstance(right, ir.Constant) and isinstance(right.constant, int) and right.constant >= 0
if both_int and right_is_pos_int_const:
return self.builder.fptosi(result, left.type, name="pow2int")
if isinstance(left.type, ir.FloatType):
return self.builder.fptrunc(result, ir.FloatType(), name="pow2f32")
return result
if op in ops:
try:
return ops[op](left, right, name=op)
except Exception as e:
raise ValueError(f"{e}{self._get_node_info()}")
if op in shift_ops:
try:
return shift_ops[op](left, right, name=op)
except Exception as e:
raise ValueError(f"{e}{self._get_node_info()}")
if op in bit_ops:
try:
return bit_ops[op](left, right, name=op)
except Exception as e:
raise ValueError(f"{e}{self._get_node_info()}")
if op in cmp_ops:
try:
if is_unsigned:
return self.builder.icmp_unsigned(cmp_ops[op], left, right, name=op)
return self.builder.icmp_signed(cmp_ops[op], left, right, name=op)
except Exception as e:
raise ValueError(f"{e}{self._get_node_info()}")
if op in logic_ops:
try:
return logic_ops[op](left, right, name=op)
except Exception as e:
raise ValueError(f"{e}{self._get_node_info()}")
return left
def _to_float(self, val: ir.Value, ftype: ir.Type | None = None) -> ir.Value:
if ftype is None:
ftype = ir.DoubleType()
if isinstance(val.type, (ir.FloatType, ir.DoubleType)):
if val.type == ftype:
return val
if isinstance(val.type, ir.FloatType) and isinstance(ftype, ir.DoubleType):
return self.builder.fpext(val, ftype, name="fpext")
return self.builder.fptrunc(val, ftype, name="fptrunc")
if isinstance(val.type, ir.IntType):
if val.type.width == 1:
val = self.builder.zext(val, ir.IntType(32), name="bool2i32")
return self.builder.sitofp(val, ftype, name="int2float")
return val
def emit_return(self, value: ir.Value | None = None, Node: Any = None) -> None:
if not self.builder or self.builder.block.is_terminated:
return
if value is not None:
self._unregister_local_heap_ptr(value)
self._emit_local_heap_frees()
if self._variadic_info and self._variadic_info.get('va_start_called'):
va_list_ptr: ir.Value = self._variadic_info['va_list_ptr']
self.emit_va_end(va_list_ptr)
if value is None:
if self.func and hasattr(self.func, 'ftype'):
ret_type: ir.Type = self.func.ftype.return_type
if isinstance(ret_type, ir.IntType):
self.builder.ret(ir.Constant(ret_type, 0))
elif isinstance(ret_type, ir.PointerType):
self.builder.ret(ir.Constant(ret_type, None))
elif isinstance(ret_type, (ir.FloatType, ir.DoubleType)):
self.builder.ret(ir.Constant(ret_type, 0.0))
elif isinstance(ret_type, ir.BaseStructType):
if ret_type.elements:
zero_val: ir.Constant = ir.Constant(ret_type, [ir.Constant(et, None) if isinstance(et, (ir.PointerType, ir.IdentifiedStructType, ir.LiteralStructType, ir.ArrayType)) else ir.Constant(et, 0) if isinstance(et, (ir.IntType, ir.FloatType, ir.DoubleType)) else ir.Constant(et, ir.Undefined) for et in ret_type.elements])
else:
zero_val = ir.Constant(ret_type, None)
self.builder.ret(zero_val)
elif isinstance(ret_type, ir.ArrayType):
self.builder.ret(ir.Constant(ret_type, None))
else:
self.builder.ret_void()
else:
self.builder.ret_void()
else:
if self.func and hasattr(self.func, 'ftype'):
ret_type: ir.Type = self.func.ftype.return_type
# None(空值常量)允许隐式转换为任意返回类型:指针目标→空指针,
# 其余目标→目标类型零值(整数 0 / 浮点 0.0 / 聚合 zeroinitializer)。
if not isinstance(ret_type, ir.VoidType) and self._IsNoneValue(value, Node):
value = self._ZeroConst(ret_type)
if ret_type != value.type:
if isinstance(ret_type, ir.PointerType) and isinstance(value.type, ir.PointerType):
Allowed: bool
Reason: str
Allowed, Reason = self._ClassifyPtrImplicitConvert(value.type, ret_type)
if not Allowed:
from lib.core.LLVMCG.FuncGen import FailFastError
raise FailFastError(
f"指针隐式转换已禁止({Reason}):函数 {self.func.name} 声明返回 {ret_type},"
f"但 return 的是指针类型 {value.type};如需转换请显式 cast(如 t.CPtr(...))"
f"{self._DescribeValueNode(Node)}"
)
value = self.builder.bitcast(value, ret_type, name="ret_cast")
elif isinstance(value.type, ir.PointerType) and not isinstance(ret_type, ir.PointerType):
if isinstance(ret_type, ir.IntType):
# 指针 → 整数:隐式转换一律禁止(与 ViperC coerce_to_type 同口径)。
# 原先"同宽 load 所指物 / 其余 ptrtoint"都是静默转换。
from lib.core.LLVMCG.FuncGen import FailFastError
raise FailFastError(
f"指针隐式转整数已禁止:函数 {self.func.name} 声明返回 {ret_type},"
f"但 return 的是指针类型 {value.type};如需转换请显式 cast(如 t.CUInt64T(p) / t.CInt(p))"
f"{self._DescribeValueNode(Node)}"
)
elif isinstance(value.type.pointee, ret_type.__class__) and not isinstance(value.type.pointee, ir.IntType):
value = self._load(value, name="ret_Load")
elif isinstance(value.type.pointee, ir.PointerType):
Loaded: ir.Value = self._load(value, name="ret_deref")
if Loaded.type == ret_type:
value = Loaded
elif isinstance(ret_type, ir.IntType):
from lib.core.LLVMCG.FuncGen import FailFastError
raise FailFastError(
f"指针隐式转整数已禁止:函数 {self.func.name} 声明返回 {ret_type},"
f"但 return 的是指针类型 {Loaded.type};如需转换请显式 cast(如 t.CUInt64T(p) / t.CInt(p))"
f"{self._DescribeValueNode(Node)}"
)
elif isinstance(ret_type, ir.IntType) and isinstance(value.type, ir.IntType):
if value.type.width < ret_type.width:
if value.type.width == 1:
# i1 是布尔值(来自 icmp/fcmp/__contains__),无符号语义,必须 zext
value = self.builder.zext(value, ret_type, name="ret_zext_bool")
else:
is_unsigned: bool = id(value) in self._unsigned_results
if is_unsigned:
value = self.builder.zext(value, ret_type, name="ret_zext")
else:
value = self.builder.sext(value, ret_type, name="ret_sext")
elif value.type.width > ret_type.width:
value = self.builder.trunc(value, ret_type, name="ret_trunc")
elif isinstance(ret_type, ir.PointerType) and isinstance(value.type, ir.IntType):
value = self.builder.inttoptr(value, ret_type, name="ret_inttoptr")
elif isinstance(ret_type, (ir.FloatType, ir.DoubleType)) and isinstance(value.type, ir.IntType):
value = self.builder.sitofp(value, ret_type, name="ret_int2float")
elif isinstance(ret_type, ir.IntType) and isinstance(value.type, (ir.FloatType, ir.DoubleType)):
value = self.builder.fptosi(value, ret_type, name="ret_float2int")
elif isinstance(ret_type, (ir.FloatType, ir.DoubleType)) and isinstance(value.type, (ir.FloatType, ir.DoubleType)):
if ret_type != value.type:
if isinstance(value.type, ir.FloatType) and isinstance(ret_type, ir.DoubleType):
value = self.builder.fpext(value, ret_type, name="ret_fpext")
elif isinstance(value.type, ir.DoubleType) and isinstance(ret_type, ir.FloatType):
value = self.builder.fptrunc(value, ret_type, name="ret_fptrunc")
self.builder.ret(value)
def _emit_printf(self, args: list[ir.Value], unsigned_flags: list[bool] | None = None, sep: str = " ", end: str = "\n") -> ir.Value | None:
if not args:
return None
if unsigned_flags is None:
unsigned_flags = []
format_str: str = ""
cast_args: list[ir.Value] = []
for i, arg in enumerate(args):
is_u: bool = i < len(unsigned_flags) and unsigned_flags[i]
if isinstance(arg.type, ir.IntType):
if arg.type.width == 1:
# i1 是布尔值(来自 icmp/fcmp/__contains__),zext 到 i32 后用 %d
format_str += "%d"
ext: ir.Value = self.builder.zext(arg, ir.IntType(32), name="bool2int")
cast_args.append(ext)
elif arg.type.width == 8:
format_str += "%c"
ext: ir.Value = self.builder.zext(arg, ir.IntType(32), name="char2int")
cast_args.append(ext)
elif arg.type.width == 64:
format_str += "%llu" if is_u else "%lld"
cast_args.append(arg)
else:
format_str += "%u" if is_u else "%d"
cast_args.append(arg)
elif isinstance(arg.type, (ir.FloatType, ir.DoubleType)):
format_str += "%f"
cast_args.append(arg)
elif isinstance(arg.type, ir.PointerType):
if isinstance(arg.type.pointee, ir.IntType) and arg.type.pointee.width == 8:
format_str += "%s"
cast_args.append(arg)
else:
format_str += "%d"
int_val: ir.Value = self.builder.ptrtoint(arg, ir.IntType(64), name="ptr2int")
trunc_val: ir.Value = self.builder.trunc(int_val, ir.IntType(32), name="trunc")
cast_args.append(trunc_val)
else:
format_str += "%d"
cast_args.append(arg)
if sep is not None and i < len(args) - 1:
format_str += sep
if end is not None:
format_str += end
string_type: ir.ArrayType = ir.ArrayType(ir.IntType(8), len(format_str.encode('utf-8')) + 1)
string_const: ir.Constant = ir.Constant(string_type, bytearray(format_str + '\x00', 'utf-8'))
global_var: ir.GlobalVariable = ir.GlobalVariable(self.module, string_type, name=f"str_const_{self.string_const_counter}")
self.string_const_counter += 1
global_var.initializer = string_const
global_var.linkage = 'internal'
format_ptr: ir.Value = self.builder.bitcast(global_var, ir.PointerType(ir.IntType(8)))
call_args: list[ir.Value] = [format_ptr] + cast_args
printf_func: ir.Function | None = self.get_or_declare_c_func('printf', ir.FunctionType(ir.IntType(32), [ir.PointerType(ir.IntType(8))], var_arg=True))
return self.builder.call(printf_func, call_args, name="call_printf")