Files
ViperC/App/lib/core/Gen/Expr/ExprGenOps.vp
T

1278 lines
84 KiB
Plaintext
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.
import lib.core.Gen.GenTypes as GenTypes
import t, c
from stdint import *
import ast
import mlirlite
import memhub
import string
import viperlib
import lib.core.Gen.Translator as HT
import lib.core.Gen.Type.TypeSpec as TSpec
import lib.core.Gen.Expr.ExprGenBase as ExprGenBase
# ExprGenOps - ExprGenMixin split (stage D): compare / unary / bool / binop operations
# ============================================================
novirtual class ExprGenOps(ExprGenBase.ExprGenBase):
"""Comparison/unary/boolean/binop operations"""
# translate ternary IfExp(test, body, orelse): emit `cond ? body : orelse` with then/else blocks
# branching to a merge block whose phi is [body_val, then_blk], [orelse_val, else_blk].
def translate_ifexp(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, node: ast.AST*, trans: HT.Translator* = None) -> mlirlite.Value*:
"""Translate a ternary (body if test else orelse) into a phi Value."""
if node is None or builder is None or trans is None: return None
ie: ast.IfExp* = ast.IfExp*(node)
if ie is None: return None
func: mlirlite.Function* = trans._cur_func
if func is None: return None
# 1. Evaluate the condition
cond_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, ie.test, None, 0, trans)
if cond_val is None: return None
# Convert to i1 (prefer dispatching __bool__)
cond_i1: mlirlite.Value* = self.coerce_cond_to_bool(pool, builder, mod, cond_val, trans)
# 2. Create the three basic blocks
cnt: int = trans._label_counter
trans._label_counter = cnt + 1
name_buf: str = pool.alloc(32)
if name_buf is None: return None
viperlib.snprintf(name_buf, 32, "ifexp.then.%d", cnt)
then_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, name_buf)
viperlib.snprintf(name_buf, 32, "ifexp.else.%d", cnt)
else_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, name_buf)
viperlib.snprintf(name_buf, 32, "ifexp.merge.%d", cnt)
merge_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, name_buf)
if then_bb is None or else_bb is None or merge_bb is None: return None
# 3. Conditional branch
mlirlite.build_cond_br(builder, cond_i1, then_bb, else_bb)
# 4. then branch
mlirlite.position_at_end(builder, then_bb)
body_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, ie.body, None, 0, trans)
then_blk_for_phi: mlirlite.BasicBlock* = trans._cur_builder.CurBlock
if body_val is None:
# fail-fast: ternary then-branch translation failed; abort compilation (expose the upstream root cause)
self.Gen.TypeGenErr.fatal_error(ie.body, "ternary expression branch translation failed", "TypeError")
# Do not br yet; br after type alignment
# 5. else branch. When the then-branch is a pointer, pass its type as the else target-type context: a single-quoted
# 1-char literal (CONST_CHAR, e.g. '?') materializes as a 1-char string, otherwise as i32 -> Coerce CastError.
orelse_target_ty: mlirlite.LLVMType* = None
if self.is_ptr_type(body_val.Ty) != 0: orelse_target_ty = body_val.Ty
mlirlite.position_at_end(builder, else_bb)
orelse_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, ie.orelse, None, 0, trans, orelse_target_ty)
else_blk_for_phi: mlirlite.BasicBlock* = trans._cur_builder.CurBlock
if orelse_val is None:
# fail-fast: ternary else-branch translation failed; abort compilation (expose the upstream root cause)
self.Gen.TypeGenErr.fatal_error(ie.orelse, "ternary expression branch translation failed", "TypeError")
# Do not br yet; br after type alignment
# Type alignment (the two branches may differ): coerce_to_type must run inside each branch so the merge block's
# PHI is its first instruction (otherwise sext/zext/ptrtoint/inttoptr land before the PHI, violating LLVM rules).
body_bits: int = self.get_llvm_type_bits(body_val.Ty)
orelse_bits: int = self.get_llvm_type_bits(orelse_val.Ty)
body_is_ptr: int = self.is_ptr_type(body_val.Ty)
orelse_is_ptr: int = self.is_ptr_type(orelse_val.Ty)
# Case 0: one side is a pointer and the other an integer (e.g. i64 vs i8* null); must be handled first since
# Ptr.bits == 0 skips the next three branches. body_val.Ty becomes phi_ty and orelse_val is coerced to it.
if body_is_ptr != orelse_is_ptr:
mlirlite.position_at_end(builder, else_blk_for_phi)
orelse_val = self.coerce_to_type(builder, orelse_val, body_val.Ty, node)
elif body_bits > orelse_bits and orelse_bits > 0:
# Coerce orelse_val in the else block
mlirlite.position_at_end(builder, else_blk_for_phi)
orelse_val = self.coerce_to_type(builder, orelse_val, body_val.Ty, node)
elif orelse_bits > body_bits and body_bits > 0:
# Back in the then block, coerce body_val
mlirlite.position_at_end(builder, then_blk_for_phi)
body_val = self.coerce_to_type(builder, body_val, orelse_val.Ty, node)
elif body_bits == orelse_bits and body_bits > 0:
# Same bits but different types (e.g. i64 vs Ptr): null cannot be used in an i64 PHI.
# This branch should not trigger (case 0 covers it); kept as a defensive guard.
if self.is_ptr_type(body_val.Ty) != self.is_ptr_type(orelse_val.Ty):
mlirlite.position_at_end(builder, else_blk_for_phi)
orelse_val = self.coerce_to_type(builder, orelse_val, body_val.Ty, node)
# Now br to merge from the then/else blocks (after type coercion)
mlirlite.position_at_end(builder, then_blk_for_phi)
mlirlite.build_br(builder, merge_bb)
mlirlite.position_at_end(builder, else_blk_for_phi)
mlirlite.build_br(builder, merge_bb)
# 6. merge block: phi (the PHI is the merge block's first instruction)
mlirlite.position_at_end(builder, merge_bb)
phi_ty: mlirlite.LLVMType* = body_val.Ty
if phi_ty is None:
# fail-fast: ternary branch type missing; abort compilation (expose the upstream root cause)
self.Gen.TypeGenErr.fatal_error(node, "ternary expression branch type missing", "TypeError")
inc1: mlirlite.PhiIncoming* = mlirlite.new_phi_incoming(pool, body_val, then_blk_for_phi)
inc2: mlirlite.PhiIncoming* = mlirlite.new_phi_incoming(pool, orelse_val, else_blk_for_phi)
if inc1 is None or inc2 is None: return None
# List order: inc1 (head) -> inc2 (tail). Writing inc2.Next = inc1 by mistake left inc1.Next as None, emitting a
# single phi entry and triggering LLVM verification: "PHINode should have one entry for each predecessor".
inc1.Next = inc2
return mlirlite.build_phi(builder, phi_ty, inc1, 2)
# translate_compare - Compare(left, ops, comparators)
# ============================================================
def should_zext_widen(self, pool: memhub.MemBuddy*, narrow_val: mlirlite.Value*, wide_val: mlirlite.Value*, narrow_node: ast.AST* = None, trans: HT.Translator* = None) -> int:
"""Decide integer widening: 1 = zext (unsigned semantics), 0 = sext."""
if narrow_val is None or narrow_val.Ty is None: return 0
narrow_bits: int = self.get_llvm_type_bits(narrow_val.Ty)
if narrow_bits == 1: return 1
if narrow_bits == 8:
# i8 defaults to unsigned-byte handling (systems convention), but an explicitly signed source annotation
# must sext to preserve sign: `int32(v)` with v: int8 = -50 must stay -50, not become 206.
if self._node_type_signed(pool, narrow_node, trans) != 0: return 0
return 1
# Wide-side constant heuristic
if wide_val is not None and wide_val.IsConst == 1:
wide_bits: int = self.get_llvm_type_bits(wide_val.Ty)
if wide_bits > narrow_bits and narrow_bits < 64:
signed_max: int64 = (int64(1) << (narrow_bits - 1)) - 1
if wide_val.IntVal > signed_max: return 1
# Narrow side resolves to an unsigned type (e.g. t.CUIntXXT literal / cast). trans must be threaded down: for
# Name/Subscript, unsignedness comes from trans.SymTab; without trans it returns 0 and sext is wrongly used.
if narrow_node is not None:
if self._node_type_unsigned(pool, narrow_node, trans) != 0: return 1
# A CDefine constant with the high bit set (0x80000000..0xFFFFFFFF) -> zext. CDefine is a compile-time value,
# so _node_type_unsigned finds no annotation; the negative-signed range means an unsigned sentinel (e.g. 0xFFFFFFFF).
if narrow_node is not None and narrow_node.kind() == ast.ASTKind.Name:
cdef_nm_zw: ast.Name* = ast.Name*(narrow_node)
if cdef_nm_zw.id is not None:
cdv_zw: int64 = self.Gen.TypeGenCDef.lookup_cdefine_constant(cdef_nm_zw.id)
if self.Gen.TypeGenCDef.is_cdefine_found() != 0:
if cdv_zw > 0x7FFFFFFF: return 1
return 0
def _node_type_unsigned(self, pool: memhub.MemBuddy*, node: ast.AST*, trans: HT.Translator* = None) -> int:
"""Return 1 if the AST node resolves to an unsigned integer type, else 0."""
if node is None: return 0
k: int = node.kind()
if k == ast.ASTKind.Call:
cl: ast.Call* = ast.Call*(node)
if cl.func is not None and cl.func.kind() == ast.ASTKind.Attribute:
at: ast.Attribute* = ast.Attribute*(cl.func)
if at.value is not None and at.value.kind() == ast.ASTKind.Name:
mod_nm: ast.Name* = ast.Name*(at.value)
if mod_nm.id is not None and string.strcmp(mod_nm.id, "t") == 0 and at.attr is not None:
if self._type_name_unsigned(at.attr) != 0: return 1
return 0
if k == ast.ASTKind.Name:
nm: ast.Name* = ast.Name*(node)
if nm.id is not None:
# 1) The name itself is an unsigned type name (e.g. the UInt32T alias)
if self._type_name_unsigned(nm.id) != 0: return 1
# 2) The variable's annotation type is unsigned (query SymTab when trans is available)
if trans is not None:
ve: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, nm.id)
if ve is not None:
# [S4 read] An explicit unsigned Spec (TS_UINT) takes priority over the string check
if self._entry_spec_unsigned(ve.Spec) != 0: return 1
vcn_u: str = self.Gen.VarGen.var_annot_class_name(ve)
if vcn_u is not None:
if self._type_name_unsigned(vcn_u) != 0: return 1
return 0
if k == ast.ASTKind.BinOp:
bop: ast.BinOp* = ast.BinOp*(node)
if self._node_type_unsigned(pool, bop.left, trans) != 0: return 1
if self._node_type_unsigned(pool, bop.right, trans) != 0: return 1
return 0
if k == ast.ASTKind.UnaryOp:
uop: ast.UnaryOp* = ast.UnaryOp*(node)
if self._node_type_unsigned(pool, uop.operand, trans) != 0: return 1
return 0
if k == ast.ASTKind.Subscript:
# Array/pointer subscript: md5_buf[i] (t.CArray[uint8, N]) or buf[i] (param uint8*) has an unsigned element -> zext.
sub_su: ast.Subscript* = ast.Subscript*(node)
if sub_su.value is not None and sub_su.value.kind() == ast.ASTKind.Name:
snm_su: ast.Name* = ast.Name*(sub_su.value)
if snm_su.id is not None and trans is not None:
ve_su: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, snm_su.id)
if ve_su is not None:
# [S4 read] An explicit unsigned Spec wins; for union annotations the Spec is still Ptr/Array -> returns 0 and falls back to the string path.
if self._entry_spec_unsigned(ve_su.Spec) != 0: return 1
scn_u: str = self.Gen.VarGen.var_annot_class_name(ve_su)
if scn_u is not None:
if self._annot_str_unsigned(scn_u) != 0: return 1
return 0
if k == ast.ASTKind.Attribute:
# Struct field (e.g. viperio.Buf's self.capacity/length: sizet): get the class name from the receiver's AnnotClassName, then look up the field annotation to decide unsignedness. Read-only; returns 0 on failure, so find_struct + manual field walk is used instead of lookup_field_by_class.
at_u: ast.Attribute* = ast.Attribute*(node)
if at_u.value is not None and at_u.attr is not None and trans is not None:
cls_nm_u: str = None
if at_u.value.kind() == ast.ASTKind.Name:
vn_u: ast.Name* = ast.Name*(at_u.value)
if vn_u.id is not None:
ve_u: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, vn_u.id)
if ve_u is not None: cls_nm_u = self.Gen.VarGen.var_annot_class_name(ve_u)
if cls_nm_u is not None:
# Take the bare class name (before the dot): look up by bare name only, avoiding module-qualified resolution
bare_u: str = cls_nm_u
dot_u: str = self.Gen.StructGen._last_dot_outside_brackets(cls_nm_u)
if dot_u is not None: bare_u = dot_u + 1
if bare_u is not None:
st_u: GenTypes.StructEntry* = self.Gen.StructGen.find_struct(bare_u)
if st_u is not None:
for fi_u in range(int(st_u.FieldCount)):
fe_u: GenTypes.FieldEntry* = self.Gen.StructGen._get_field_entry(st_u, fi_u)
if fe_u is not None and fe_u.Name is not None and string.strcmp(fe_u.Name, at_u.attr) == 0:
# [S4 read] An explicit unsigned field Spec wins
if self._entry_spec_unsigned(fe_u.Spec) != 0: return 1
fcn_u: str = self.Gen.StructGen.get_field_annot_class_name(fe_u)
if fcn_u is not None:
if self._annot_str_unsigned(fcn_u) != 0: return 1
break
return 0
return 0
def _node_type_signed(self, pool: memhub.MemBuddy*, node: ast.AST*, trans: HT.Translator* = None) -> int:
"""Return 1 if the source node is explicitly a signed integer, else 0."""
if node is None: return 0
k: int = node.kind()
if k == ast.ASTKind.Call:
cl: ast.Call* = ast.Call*(node)
if cl.func is not None and cl.func.kind() == ast.ASTKind.Attribute:
at: ast.Attribute* = ast.Attribute*(cl.func)
if at.value is not None and at.value.kind() == ast.ASTKind.Name:
mod_nm: ast.Name* = ast.Name*(at.value)
if mod_nm.id is not None and string.strcmp(mod_nm.id, "t") == 0 and at.attr is not None:
if self._spec_sign_from_annot(at.attr) == TSpec.TS_SIGN_SIGNED: return 1
return 0
if k == ast.ASTKind.Name:
nm: ast.Name* = ast.Name*(node)
if nm.id is not None:
if self._spec_sign_from_annot(nm.id) == TSpec.TS_SIGN_SIGNED: return 1
if trans is not None:
ve: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, nm.id)
if ve is not None:
ncn_s: str = self.Gen.VarGen.var_annot_class_name(ve)
if ncn_s is not None:
if self._spec_sign_from_annot(ncn_s) == TSpec.TS_SIGN_SIGNED: return 1
return 0
if k == ast.ASTKind.Subscript:
sub_s: ast.Subscript* = ast.Subscript*(node)
if sub_s.value is not None and sub_s.value.kind() == ast.ASTKind.Name:
snm: ast.Name* = ast.Name*(sub_s.value)
if snm.id is not None and trans is not None:
ve_s: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, snm.id)
if ve_s is not None:
scn_s: str = self.Gen.VarGen.var_annot_class_name(ve_s)
if scn_s is not None:
if self._spec_sign_from_annot(scn_s) == TSpec.TS_SIGN_SIGNED: return 1
return 0
return 0
# S4 de-stringification (unsigned detection): the single source of truth is TypeGenCType.spec_sign_of_annot; the old
# strstr-substring fallbacks were deleted (they caused the A4/A5 false positives). UNKNOWN means no opinion -> return 0.
def _spec_sign_from_annot(self, annot: str) -> int:
"""Map an annotation to TS_SIGN_*; UNKNOWN takes no position."""
return self.Gen.TypeGenCType.spec_sign_of_annot(annot, 0)
# _entry_spec_unsigned - whether a variable/field entry's Spec is an explicitly unsigned integer. `Kind == TS_UINT`
# is a strong signal and takes priority; `Kind == TS_INT` is ambiguous, so only the unsigned direction is switched.
def _entry_spec_unsigned(self, sp: TSpec.TypeSpec*) -> int:
"""Return 1 if the entry Spec is explicitly unsigned (TS_UINT), else 0."""
if sp is None: return 0
if sp.Kind == TSpec.TS_UINT: return 1
return 0
def _type_name_unsigned(self, type_name: str) -> int:
"""Return 1 if the type name is unsigned; UNKNOWN means 0."""
if type_name is not None:
if self._spec_sign_from_annot(type_name) == TSpec.TS_SIGN_UNSIGNED: return 1
return 0
def _annot_str_unsigned(self, annot: str) -> int:
"""Return 1 if the annotation string is unsigned; UNKNOWN means 0."""
if annot is not None:
if self._spec_sign_from_annot(annot) == TSpec.TS_SIGN_UNSIGNED: return 1
return 0
def translate_compare(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, node: ast.AST*, trans: HT.Translator* = None) -> mlirlite.Value*:
"""Translate a comparison expression and return an i1 result."""
cmp: ast.Compare* = ast.Compare*(node)
if cmp is None: return None
ops_list: list[int]* = (list[int]*)(cmp.ops)
if ops_list is None or len(ops_list) == 0: return None
op: int = ops_list.get(0)
comparators: list[ast.AST*]* = cmp.comparators
if comparators is None or len(comparators) == 0: return None
rhs_node: ast.AST* = comparators.get(0)
rhs: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, rhs_node, None, 0, trans)
if rhs is None: return None
# === None comparison optimization: x == None / x != None use identity comparison (icmp eq/ne null) ===
# Avoids emitting invalid __eq__/__ne__ calls for struct pointers; Python treats == None as identity comparison.
lhs_node: ast.AST* = cmp.left
cmp_with_none: int = 0
if op == ast.OpKind.Eq or op == ast.OpKind.Ne or op == ast.OpKind.Is or op == ast.OpKind.IsNot:
if rhs_node is not None and rhs_node.kind() == ast.ASTKind.Constant:
rhs_cn: ast.Constant* = ast.Constant*(rhs_node)
if rhs_cn is not None and rhs_cn.const_kind == ast.CONST_NONE: cmp_with_none = 1
if cmp_with_none == 0 and lhs_node is not None and lhs_node.kind() == ast.ASTKind.Constant:
lhs_cn: ast.Constant* = ast.Constant*(lhs_node)
if lhs_cn is not None and lhs_cn.const_kind == ast.CONST_NONE: cmp_with_none = 1
# === Pointer null-check pattern: ptr != 0 / ptr == 0. Skip operator overload and use identity comparison
# (icmp null); otherwise cross-module classes without __eq__/__ne__ get ghost calls -> undefined reference. ===
cmp_zero: int = 0
if op == ast.OpKind.Eq or op == ast.OpKind.Ne:
if rhs_node is not None and self._is_zero_int_literal(rhs_node) != 0: cmp_zero = 1
if cmp_zero == 0 and lhs_node is not None and self._is_zero_int_literal(lhs_node) != 0: cmp_zero = 1
# === Compare operator overload path 1: lhs is a Name bound to a struct variable ===
# For value-type variables (e.g. cnt: Counter) try the overload with the alloca pointer; None comparison is skipped.
if cmp_with_none == 0 and lhs_node is not None and lhs_node.kind() == ast.ASTKind.Name and trans is not None:
nm: ast.Name* = ast.Name*(lhs_node)
if nm is not None and nm.id is not None:
lhs_alloca: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, nm.id)
if lhs_alloca is not None:
if cmp_zero == 0 or self.get_ptr_level(lhs_alloca.Ty) == 0:
ovl_self1: mlirlite.Value* = lhs_alloca
# Ptr(Ptr(Struct)) object storage slot: load the object pointer as self (same as translate_binop path 1) to avoid using the slot address as the object address.
if lhs_alloca.Ty is not None and lhs_alloca.Ty.Pointee != 0 and self.Gen.ExprGenBase.is_ptr_type(lhs_alloca.Ty.Pointee) != 0:
load_self1: mlirlite.Value* = mlirlite.build_load(builder, lhs_alloca.Ty.Pointee, lhs_alloca)
if load_self1 is not None: ovl_self1 = load_self1
ovl_result: mlirlite.Value* = self.try_operator_overload(pool, builder, mod, ovl_self1, rhs, op, trans, 1)
if ovl_result is not None: return ovl_result
# Translate lhs normally
lhs: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, cmp.left, None, 0, trans)
if lhs is None: return None
# === in / not in: the container rhs calls __contains__ (aligned with TPC HandlesExprOps.py L261-281).
# x in y -> y.__contains__(x); x not in y -> !y.__contains__(x). With no __contains__, return i1 0 (always False).
if op == ast.OpKind.In or op == ast.OpKind.NotIn:
cn_st: mlirlite.LLVMType* = self.Gen.StructGen.get_struct_type_from_value(rhs)
if cn_st is not None:
cn_cls: str = self.Gen.StructGen.get_class_name_by_type(pool, cn_st)
if cn_cls is not None:
cn_lookup: str = pool.alloc(128)
if cn_lookup is not None:
viperlib.snprintf(cn_lookup, 128, "%s.__contains__", cn_cls)
cn_fn: mlirlite.Function* = self.Gen.CallGenBase.find_func_in_module(mod, cn_lookup)
if cn_fn is not None:
cn_ret: mlirlite.LLVMType* = mlirlite.function_get_ret_ty(cn_fn)
cn_fn_name: str = mlirlite.function_get_name(cn_fn)
if cn_ret is not None and cn_fn_name is not None:
# Align the self argument (container) to __contains__'s first param type
cn_self: mlirlite.Value* = rhs
cn_first: mlirlite.Param* = mlirlite.function_get_param_head(cn_fn)
if cn_first is not None:
cn_self_ty: mlirlite.LLVMType* = mlirlite.param_get_ty(cn_first)
if cn_self_ty is not None: cn_self = mlirlite.build_bitcast(builder, rhs, cn_self_ty)
if cn_self is not None:
# Param list: self -> key (separately null-terminated)
cn_self.Next = lhs
lhs.Next = None
cn_res: mlirlite.Value* = mlirlite.build_call(builder, cn_fn_name, cn_self, 2, cn_ret, 0)
if cn_res is not None:
# Normalize to i1 (__contains__ may return int/i32 etc.)
cn_i1: mlirlite.Value* = cn_res
match cn_ret:
case mlirlite.LLVMType.Int(cn_bits):
if cn_bits > 1:
cn_zero: mlirlite.Value* = mlirlite.ConstInt(pool, cn_ret, 0, "0")
if cn_zero is not None: cn_i1 = mlirlite.build_icmp(builder, mlirlite.ICMP_NE, cn_res, cn_zero)
case _:
pass
if op == ast.OpKind.NotIn:
i1_ty_ni: mlirlite.LLVMType* = mlirlite.Int1(pool)
if i1_ty_ni is not None and cn_i1 is not None:
one_i1_ni: mlirlite.Value* = mlirlite.ConstInt(pool, i1_ty_ni, 1, "1")
if one_i1_ni is not None: cn_i1 = mlirlite.build_xor(builder, cn_i1, one_i1_ni)
if cn_i1 is not None: return cn_i1
# No __contains__: TPC semantics are always False (i1 0)
i1_ty_in: mlirlite.LLVMType* = mlirlite.Int1(pool)
if i1_ty_in is not None: return mlirlite.ConstInt(pool, i1_ty_in, 0, "0")
return None
# === Compare operator overload path 2: lhs is Ptr(Struct) ===
# Is/IsNot are unmapped so they return None and fall back to native comparison; None and pointer null-checks skip it.
if cmp_with_none == 0 and (cmp_zero == 0 or self.get_ptr_level(lhs.Ty) == 0):
ovl_result2: mlirlite.Value* = self.try_operator_overload(pool, builder, mod, lhs, rhs, op, trans, 1)
if ovl_result2 is not None: return ovl_result2
# === Unsigned comparison (A5): unsigned int operands (CUInt64T/CUInt32T etc.) use ULT/ULE/UGT/UGE.
# A fixed signed predicate made uint64 address comparisons wrong across 2^63 (A5 root cause); reuse _node_type_unsigned.
unsigned_l: int = 0
unsigned_r: int = 0
if lhs_node is not None: unsigned_l = self._node_type_unsigned(pool, lhs_node, trans)
if rhs_node is not None: unsigned_r = self._node_type_unsigned(pool, rhs_node, trans)
cmp_unsigned: int = 1 if (unsigned_l != 0 or unsigned_r != 0) else 0
predicate: int = mlirlite.ICMP_SLT
if op == ast.OpKind.Lt: predicate = mlirlite.ICMP_ULT if cmp_unsigned != 0 else mlirlite.ICMP_SLT
elif op == ast.OpKind.Le: predicate = mlirlite.ICMP_ULE if cmp_unsigned != 0 else mlirlite.ICMP_SLE
elif op == ast.OpKind.Gt: predicate = mlirlite.ICMP_UGT if cmp_unsigned != 0 else mlirlite.ICMP_SGT
elif op == ast.OpKind.Ge: predicate = mlirlite.ICMP_UGE if cmp_unsigned != 0 else mlirlite.ICMP_SGE
elif op == ast.OpKind.Eq: predicate = mlirlite.ICMP_EQ
elif op == ast.OpKind.Ne: predicate = mlirlite.ICMP_NE
elif op == ast.OpKind.Is:
# `x is y` -> pointer/value equality comparison
predicate = mlirlite.ICMP_EQ
elif op == ast.OpKind.IsNot:
# `x is not y` -> pointer/value inequality comparison
predicate = mlirlite.ICMP_NE
# Type safety: number vs number allowed (sext-aligned by bit width); one-level pointer vs one-level pointer allowed (address compare); number vs pointer(1+) forbidden (fail-fast, except None compare); different-level pointers forbidden. None compare skips this check.
if cmp_with_none == 0:
lhs_ptr_level: int = self.get_ptr_level(lhs.Ty)
rhs_ptr_level: int = self.get_ptr_level(rhs.Ty)
if lhs_ptr_level != rhs_ptr_level:
# Pointer vs integer literal 0 (null-check pattern): 0 is treated as NULL and converted to a same-type
# null pointer (icmp needs matching types), not a type mismatch.
zero_is_rhs: int = 0
zero_is_lhs: int = 0
if rhs_ptr_level == 0 and self._is_zero_int_literal(rhs_node) != 0: zero_is_rhs = 1
if lhs_ptr_level == 0 and self._is_zero_int_literal(lhs_node) != 0: zero_is_lhs = 1
if zero_is_rhs != 0 or zero_is_lhs != 0:
if zero_is_rhs != 0: rhs = mlirlite.ConstNull(pool, lhs.Ty, "null")
else: lhs = mlirlite.ConstNull(pool, rhs.Ty, "null")
else:
# Pointer vs non-zero integer (C sentinel pattern, e.g. INVALID_HANDLE_VALUE = -1): align with TPC
# HandlesExprOps.py L388-398; ptrtoint the pointer to compare as an integer address.
if lhs_ptr_level != 0 and rhs_ptr_level == 0: lhs = mlirlite.build_ptrtoint(builder, lhs, rhs.Ty)
elif rhs_ptr_level != 0 and lhs_ptr_level == 0: rhs = mlirlite.build_ptrtoint(builder, rhs, lhs.Ty)
else:
# Different-level pointers (1 vs 2 etc.) -> forbidden (fail-fast)
self.Gen.TypeGenErr.fatal_error(cmp, f"comparison type mismatch: pointers of different levels compared (lhs level={lhs_ptr_level}, rhs level={rhs_ptr_level})", "TypeError")
else:
# None comparison: the None side is implicitly 0/null, its form decided by the other side.
# pointer -> same-type null pointer (icmp eq/ne); integer -> 0 (x == 0); struct value -> never None (== false, != true).
lhs_is_none_lit: int = 0
if lhs_node is not None and lhs_node.kind() == ast.ASTKind.Constant:
lhs_none_cn: ast.Constant* = ast.Constant*(lhs_node)
if lhs_none_cn is not None and lhs_none_cn.const_kind == ast.CONST_NONE: lhs_is_none_lit = 1
lhs_ptr_level_none: int = self.get_ptr_level(lhs.Ty)
rhs_ptr_level_none: int = self.get_ptr_level(rhs.Ty)
if lhs_is_none_lit != 0:
# None on the left (`None is x` / `None == x`): the rhs type decides None's form
if rhs_ptr_level_none == 0:
rhs_is_struct_none: int = 0
match rhs.Ty:
case mlirlite.LLVMType.Struct(_, _, _):
rhs_is_struct_none = 1
case _:
rhs_is_struct_none = 0
if rhs_is_struct_none != 0:
if op == ast.OpKind.Eq or op == ast.OpKind.Is: return mlirlite.const_int(pool, 1, 0)
return mlirlite.const_int(pool, 1, 1)
# rhs is numeric -> convert the None side to integer 0 (rhs unchanged)
lhs = mlirlite.const_int32(pool, 0)
else:
# None on the right (`x is None` / `x == None`): the lhs type decides None's form
if lhs_ptr_level_none == 0:
# Check whether lhs is a struct value type (a non-pointer Struct)
lhs_is_struct_none: int = 0
match lhs.Ty:
case mlirlite.LLVMType.Struct(_, _, _):
lhs_is_struct_none = 1
case _:
lhs_is_struct_none = 0
if lhs_is_struct_none != 0:
# A struct value is never None: ==/is return false, !=/is not return true
if op == ast.OpKind.Eq or op == ast.OpKind.Is: return mlirlite.const_int(pool, 1, 0)
else: # Ne, IsNot
return mlirlite.const_int(pool, 1, 1)
# lhs is numeric and rhs is i8* null -> convert rhs to numeric 0 (lhs unchanged)
rhs = mlirlite.const_int32(pool, 0)
# Type alignment: icmp requires matching types on both sides. Only numeric types are aligned (same-level pointers icmp directly); i1 uses zext (1 stays 1).
lhs_bits: int = self.get_llvm_type_bits(lhs.Ty)
rhs_bits: int = self.get_llvm_type_bits(rhs.Ty)
# C 语义整数提升(与算术同一口径):两侧都窄于 32 位时先提升到 i32 再比较,避免 8/16 位
# 直接比较在符号/边界组合下语义模糊(如 signed i8 与 unsigned u8 直接比)。
# 提升后把 bits 记为 32,下面的等宽化不再重复处理。
if lhs_bits != 0 and rhs_bits != 0 and lhs_bits < 32 and rhs_bits < 32:
cmp_i32_pr: mlirlite.LLVMType* = mlirlite.Int32(pool)
cmp_probe: mlirlite.Value* = mlirlite.const_int32(pool, 0)
if self.should_zext_widen(pool, lhs, cmp_probe, cmp.left, trans) != 0: lhs = mlirlite.build_zext(builder, lhs, cmp_i32_pr)
else: lhs = mlirlite.build_sext(builder, lhs, cmp_i32_pr)
if self.should_zext_widen(pool, rhs, cmp_probe, rhs_node, trans) != 0: rhs = mlirlite.build_zext(builder, rhs, cmp_i32_pr)
else: rhs = mlirlite.build_sext(builder, rhs, cmp_i32_pr)
lhs_bits = 32
rhs_bits = 32
if lhs_bits != 0 and rhs_bits != 0 and lhs_bits != rhs_bits:
if lhs_bits < rhs_bits:
if self.should_zext_widen(pool, lhs, rhs, cmp.left, trans) != 0: lhs = mlirlite.build_zext(builder, lhs, rhs.Ty)
else: lhs = mlirlite.build_sext(builder, lhs, rhs.Ty)
else:
if self.should_zext_widen(pool, rhs, lhs, rhs_node, trans) != 0: rhs = mlirlite.build_zext(builder, rhs, lhs.Ty)
else: rhs = mlirlite.build_sext(builder, rhs, lhs.Ty)
# Pointer type alignment: same-level pointers with different pointee types are unified by bitcast (e.g. i8* vs Struct*).
if lhs_bits == 0 and rhs_bits == 0:
lhs_is_ptr_now: int = self.is_ptr_type(lhs.Ty)
rhs_is_ptr_now: int = self.is_ptr_type(rhs.Ty)
if lhs_is_ptr_now != 0 and rhs_is_ptr_now != 0:
# Both are pointers; compare type strings via TypePrint
lhs_ty_buf: str = pool.alloc_tagged(4096, memhub.DETSITE_EXPRGENOPS_500)
rhs_ty_buf: str = pool.alloc_tagged(4096, memhub.DETSITE_EXPRGENOPS_501)
if lhs_ty_buf is not None and rhs_ty_buf is not None:
mlirlite.TypePrint(lhs_ty_buf, 4096, lhs.Ty, pool)
mlirlite.TypePrint(rhs_ty_buf, 4096, rhs.Ty, pool)
if string.strcmp(lhs_ty_buf, rhs_ty_buf) != 0: rhs = mlirlite.build_bitcast(builder, rhs, lhs.Ty)
# Float comparison: use fcmp instead of icmp when either operand is floating point
lhs_fbits: int = self.get_llvm_float_bits(lhs.Ty)
rhs_fbits: int = self.get_llvm_float_bits(rhs.Ty)
if lhs_fbits != 0 or rhs_fbits != 0:
# Float type alignment (fpext/fptrunc)
if lhs_fbits != 0 and rhs_fbits != 0 and lhs_fbits != rhs_fbits:
if lhs_fbits < rhs_fbits: lhs = mlirlite.build_fpext(builder, lhs, rhs.Ty)
else: rhs = mlirlite.build_fptrunc(builder, rhs, lhs.Ty)
elif lhs_fbits != 0 and rhs_fbits == 0:
# int → float
rhs = mlirlite.build_si2fp(builder, rhs, lhs.Ty)
elif rhs_fbits != 0 and lhs_fbits == 0: lhs = mlirlite.build_si2fp(builder, lhs, rhs.Ty)
# Map the comparison predicate to fcmp
fpredicate: int = mlirlite.FCMP_OEQ
if op == ast.OpKind.Eq or op == ast.OpKind.Is: fpredicate = mlirlite.FCMP_OEQ
elif op == ast.OpKind.Ne or op == ast.OpKind.IsNot: fpredicate = mlirlite.FCMP_ONE
elif op == ast.OpKind.Lt: fpredicate = mlirlite.FCMP_OLT
elif op == ast.OpKind.Le: fpredicate = mlirlite.FCMP_OLE
elif op == ast.OpKind.Gt: fpredicate = mlirlite.FCMP_OGT
elif op == ast.OpKind.Ge: fpredicate = mlirlite.FCMP_OGE
return mlirlite.build_fcmp(builder, fpredicate, lhs, rhs)
return mlirlite.build_icmp(builder, predicate, lhs, rhs)
# translate_unaryop - UnaryOp(op, operand)
# ============================================================
def translate_unaryop(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, node: ast.AST*, trans: HT.Translator* = None) -> mlirlite.Value*:
"""Translate unary operations (-x, +x, not x, ~x)."""
uo: ast.UnaryOp* = ast.UnaryOp*(node)
if uo is None: return None
operand: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, uo.operand, None, 0, trans)
if operand is None: return None
# === Unary operator overload: -obj/+obj/~obj call __neg__/__pos__/__invert__ (only USub/UAdd/Invert have overloads).
if uo.op == ast.OpKind.USub or uo.op == ast.OpKind.UAdd or uo.op == ast.OpKind.Invert:
ovl_u: mlirlite.Value* = self.try_unary_operator_overload(pool, builder, mod, operand, uo.op, trans)
if ovl_u is not None: return ovl_u
# C 语义整数提升(与算术/比较同一口径):一元 +/-/~ 先把窄于 32 位的整数操作数提升到 i32,
# 这样 `0 - x` / `x ^ -1` 的常量与操作数同宽(否则是 i32 常量对 i8/i16 操作数)。
# `not x` 不需要:结果为 i1,操作数宽度不影响语义。
if uo.op == ast.OpKind.USub or uo.op == ast.OpKind.UAdd or uo.op == ast.OpKind.Invert:
u_bits: int = self.get_llvm_type_bits(operand.Ty)
if u_bits != 0 and u_bits < 32:
u_i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool)
u_probe: mlirlite.Value* = mlirlite.const_int32(pool, 0)
if self.should_zext_widen(pool, operand, u_probe, uo.operand, trans) != 0: operand = mlirlite.build_zext(builder, operand, u_i32_ty)
else: operand = mlirlite.build_sext(builder, operand, u_i32_ty)
if uo.op == ast.OpKind.USub:
# -x
# Float type uses fneg (emit fneg directly to avoid 0.0 - x type mismatch)
operand_fbits: int = self.get_llvm_float_bits(operand.Ty)
if operand_fbits > 0:
# 编译期常量折叠:-c 直接产出常量并保持 IsConst
if operand.IsConst == 1: return mlirlite.ConstFloat(pool, operand.Ty, -operand.FloatVal)
return mlirlite.build_fneg(builder, operand)
# 编译期常量折叠:-c 直接产出常量(IsConst=1)。否则 `v: int8 = -50` 会被当成运行时 i32 值,
# 触发“隐式窄化禁止”,尽管其数值完全落在 i8 内(隐式窄化的“编译期常量”豁免失效)。
operand_bits: int = self.get_llvm_type_bits(operand.Ty)
if operand_bits != 0 and operand.IsConst == 1:
return mlirlite.const_int(pool, operand_bits, -operand.IntVal)
# Integer: -x = 0 - x, choosing the zero constant by bit width (avoid i32/i64 mismatch)
zero: mlirlite.Value* = mlirlite.const_int32(pool, 0)
if operand_bits == 64: zero = mlirlite.const_int64(pool, 0)
elif operand_bits > 0 and operand_bits != 32:
# Other widths (i8/i16 etc.): use i32 zero; operand will be coerced
zero = mlirlite.const_int32(pool, 0)
return mlirlite.build_sub(builder, zero, operand)
elif uo.op == ast.OpKind.UAdd:
# +x = x
return operand
elif uo.op == ast.OpKind.Not:
# not x = (bool(x) == 0), returns i1; prefer dispatching __bool__
not_cond: mlirlite.Value* = self.coerce_cond_to_bool(pool, builder, mod, operand, trans)
if not_cond is None: return None
false_val: mlirlite.Value* = mlirlite.const_int(pool, 1, 0)
return mlirlite.build_icmp(builder, mlirlite.ICMP_EQ, not_cond, false_val)
elif uo.op == ast.OpKind.Invert:
# ~x = x ^ -1
operand_bits_inv: int = self.get_llvm_type_bits(operand.Ty)
# 编译期常量折叠:~c 直接产出常量并保持 IsConst(与 -c 相同的窄化豁免口径)
if operand_bits_inv != 0 and operand.IsConst == 1:
return mlirlite.const_int(pool, operand_bits_inv, operand.IntVal ^ -1)
neg1: mlirlite.Value* = mlirlite.const_int32(pool, -1)
if operand_bits_inv == 64: neg1 = mlirlite.const_int64(pool, -1)
return mlirlite.build_xor(builder, operand, neg1)
elif uo.op == ast.OpKind.Deref:
# *p -> load by the pointee type (aligned with translate_c_deref)
if self.Gen.ExprGenBase.is_ptr_type(operand.Ty) != 0:
d_pointee: mlirlite.LLVMType* = operand.Ty.Pointee
if d_pointee is not None: return mlirlite.build_load(builder, d_pointee, operand)
# Non-pointer means type inference degraded: fail-fast, never fall back to reading as i8
self.Gen.TypeGenErr.fatal_error(node, "the operand of unary '*' dereference must be a pointer type (type inference degraded)", "TypeError")
return None
elif uo.op == ast.OpKind.AddrOf:
# &x -> lvalue address (no load): Name alloca / module global / module function / Attribute field GEP / Subscript element address.
if trans is None: return None
if uo.operand.kind() == ast.ASTKind.Name:
nm_ao: ast.Name* = ast.Name*(uo.operand)
if nm_ao.id is not None:
# P2 willFree:对已失效变量取地址 → fail-fast(与读取路径同一口径)
if self.Gen.VarGen._FreedCheck(self.Gen.VarGen.lookup_var(trans.SymTab, nm_ao.id)) != 0:
self.Gen.TypeGenErr.fatal_error(node, f"cannot take the address of '{nm_ao.id}': already released by a @life(willFree(...)) call", "LifetimeError")
# self/cls object reference: return the object pointer itself (Ptr(Struct)), not the pointer variable's address (Ptr(Ptr(Struct))).
if string.strcmp(nm_ao.id, "self") == 0 or string.strcmp(nm_ao.id, "cls") == 0:
se_ao: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, nm_ao.id)
if se_ao is not None and se_ao.Alloca is not None and se_ao.Alloca.Ty is not None:
match se_ao.Alloca.Ty:
case mlirlite.LLVMType.Ptr(sa1):
match sa1:
case mlirlite.LLVMType.Ptr(sa2):
obj_ty_sa: mlirlite.LLVMType* = mlirlite.Ptr(pool, sa2)
return mlirlite.build_load(builder, obj_ty_sa, se_ao.Alloca)
case _:
pass
case _:
pass
v_ao: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, nm_ao.id)
if v_ao is None and mod is not None:
# Module global variable: convert via make_global_ref into a quoted Ptr(Ty) reference
gv_ao: mlirlite.GlobalVariable* = self.Gen.ExprGenAttr.find_global_in_module(mod, nm_ao.id)
if gv_ao is not None and gv_ao.Name is not None and gv_ao.Ty is not None: v_ao = self.Gen.ExprGenAttr.make_global_ref(pool, gv_ao.Name, gv_ao.Ty)
if v_ao is not None and v_ao.Ty is not None:
# Object-pointer variable (alloca is Ptr(Ptr(Struct))): the variable is the object, return the object pointer (load once), aligned with translate_c_addr.
match v_ao.Ty:
case mlirlite.LLVMType.Ptr(ao1):
match ao1:
case mlirlite.LLVMType.Ptr(ao2):
match ao2:
case mlirlite.LLVMType.Struct(_, _, _):
obj_ty_ao: mlirlite.LLVMType* = mlirlite.Ptr(pool, ao2)
return mlirlite.build_load(builder, obj_ty_ao, v_ao)
case _:
pass
case _:
pass
case _:
pass
return v_ao
# Module function address: the function symbol itself
if v_ao is None and mod is not None:
f_ao: mlirlite.Function* = self.Gen.CallGenBase.find_func_in_module(mod, nm_ao.id)
if f_ao is not None:
fname_ao: str = mlirlite.function_get_name(f_ao)
if fname_ao is not None:
# The param chain is a Param chain but mlirlite.Func's type builder uses ParamNode -> rebuild via _FuncTyFromFunction (same as CallGenCBuiltin).
fn_ty_ao: mlirlite.LLVMType* = self.Gen.CallGenBase._FuncTyFromFunction(pool, f_ao)
# Quote the function reference name when needed (sha1-prefixed names starting with a digit or containing '.')
fnlen_ao: sizet = string.strlen(fname_ao)
needs_q_ao: int = self.Gen.ClassGenVTable._vtable_name_needs_quote(fname_ao)
fbuf_ao: str = pool.alloc(fnlen_ao + 4)
if fbuf_ao is not None and fn_ty_ao is not None:
if needs_q_ao != 0: viperlib.snprintf(fbuf_ao, fnlen_ao + 4, "@\"%s\"", fname_ao)
else: viperlib.snprintf(fbuf_ao, fnlen_ao + 4, "@%s", fname_ao)
return mlirlite.SSAValue(pool, mlirlite.Ptr(pool, fn_ty_ao), fbuf_ao)
elif uo.operand.kind() == ast.ASTKind.Attribute:
# Field address (GEP, no load)
fptr_ao: mlirlite.Value* = self.Gen.ExprGenAttr.get_attribute_ptr(builder, pool, mod, uo.operand, trans)
if fptr_ao is not None: return fptr_ao
elif uo.operand.kind() == ast.ASTKind.Subscript:
# Element address (GEP, no load)
sptr_ao: mlirlite.Value* = self.Gen.ExprGenName.get_subscript_ptr(builder, pool, mod, uo.operand, trans)
if sptr_ao is not None: return sptr_ao
elif uo.operand.kind() == ast.ASTKind.Call:
# &<call>() -> take the address of an rvalue. Reuse the already-evaluated operand (never re-evaluate:
# a ctor call would re-run __before_init__/__init__). See _addrof_of_call.
ac_ao: mlirlite.Value* = self._addrof_of_call(builder, pool, operand)
if ac_ao is not None: return ac_ao
self.Gen.TypeGenErr.fatal_error(node, "the operand of unary '&' address-of must be an lvalue (Name/Attribute/Subscript) or a constructor/function call result", "TypeError")
return None
return operand
def _addrof_of_call(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, call_val: mlirlite.Value*) -> mlirlite.Value*:
"""Return the address of a call result (&<call>()), reusing the evaluated value."""
if call_val is None or call_val.Ty is None: return None
if self.Gen.ExprGenBase.is_ptr_type(call_val.Ty) != 0 and call_val.Ty.Pointee is not None:
pointee_is_struct: int = 0
match call_val.Ty.Pointee:
case mlirlite.LLVMType.Struct(_, _, _):
pointee_is_struct = 1
case _:
pointee_is_struct = 0
if pointee_is_struct != 0: return call_val
tmp_slot_ao: mlirlite.Value* = mlirlite.build_alloca_at_entry(builder, call_val.Ty)
if tmp_slot_ao is None: return None
mlirlite.build_store(builder, call_val, tmp_slot_ao)
return tmp_slot_ao
# translate_boolop - BoolOp(op, values): short-circuit evaluation returning i1.
# and: a false short-circuits to merge (returns a's i1); or: a true short-circuits; the last value is evaluated then branches to merge; merge phis all i1 edges.
def translate_boolop(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, node: ast.AST*, funcs_ptr: t.CPtr = None, func_count: int = 0, trans: HT.Translator* = None) -> mlirlite.Value*:
"""Translate boolean and/or with short-circuit evaluation, returning i1."""
bo: ast.BoolOp* = ast.BoolOp*(node)
if bo is None: return None
op: int = bo.op
values: list[ast.AST*]* = bo.values
if values is None: return None
count: sizet = len(values)
if count == 0: return None
if count == 1:
return self.Gen.ExprGen.translate_value(builder, pool, mod, values.get(0), funcs_ptr, func_count, trans)
func: mlirlite.Function* = builder.Func
if func is None: return None
# Create the merge BB (unique label from builder.Counter; position_at_end's move_to_end keeps BB text order matching control-flow order).
cnt: int = builder.Counter
builder.Counter = cnt + 1
name_buf: str = pool.alloc(32)
if name_buf is None: return None
viperlib.snprintf(name_buf, 32, "bool.merge.%d", cnt)
merge_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, name_buf)
# phi incoming list head/tail
phi_head: mlirlite.PhiIncoming* = None
phi_tail: mlirlite.PhiIncoming* = None
phi_count: int = int(count - 1) # loop iteration count
# Evaluate and short-circuit each value except the last
for i in range(int(count - 1)):
val_node: ast.AST* = values.get(i)
val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, val_node, funcs_ptr, func_count, trans)
if val is None: return None
# Convert to i1 (use directly if already i1; else coerce_cond_to_bool, preferring __bool__)
val_i1: mlirlite.Value* = self.coerce_cond_to_bool(pool, builder, mod, val, trans)
# Create the next BB (evaluate the next value)
cnt = builder.Counter
builder.Counter = cnt + 1
viperlib.snprintf(name_buf, 32, "bool.next.%d", cnt)
next_bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, name_buf)
# and: false -> merge, true -> next; or: true -> merge, false -> next
if op == ast.OpKind.And: mlirlite.build_cond_br(builder, val_i1, next_bb, merge_bb)
else: mlirlite.build_cond_br(builder, val_i1, merge_bb, next_bb)
# Add the phi incoming edge: (val_i1, the BB holding cond_br)
cur_bb: mlirlite.BasicBlock* = builder.CurBlock
inc: mlirlite.PhiIncoming* = mlirlite.new_phi_incoming(pool, val_i1, cur_bb)
if phi_head is None:
phi_head = inc
phi_tail = inc
else:
phi_tail.Next = inc
phi_tail = inc
# Move to the next BB and evaluate the next value
mlirlite.position_at_end(builder, next_bb)
# Last value: evaluate then branch straight to merge
last_node: ast.AST* = values.get(count - 1)
last_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, last_node, funcs_ptr, func_count, trans)
if last_val is None: return None
last_i1: mlirlite.Value* = self.coerce_cond_to_bool(pool, builder, mod, last_val, trans)
mlirlite.build_br(builder, merge_bb)
cur_bb = builder.CurBlock
inc = mlirlite.new_phi_incoming(pool, last_i1, cur_bb)
if phi_head is None: phi_head = inc
else: phi_tail.Next = inc
phi_count += 1
# Move to merge and create the phi merging all incoming edges
mlirlite.position_at_end(builder, merge_bb)
i1_ty: mlirlite.LLVMType* = mlirlite.Int1(pool)
return mlirlite.build_phi(builder, i1_ty, phi_head, phi_count)
# emit_pow - `**` exponentiation for basic types (aligned with TPC ExprGen.emit_binary_op's Pow branch).
# Float -> llvm.pow; int -> compute in double and fptosi back when the exponent is a non-negative constant. Custom types go through __pow__.
def emit_pow(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, lhs: mlirlite.Value*, rhs: mlirlite.Value*, node: ast.AST* = None, trans: HT.Translator* = None) -> mlirlite.Value*:
"""Emit `**` exponentiation (basic types go through llvm.pow)."""
if lhs is None or rhs is None: return None
lhs_fbits: int = self.get_llvm_float_bits(lhs.Ty)
rhs_fbits: int = self.get_llvm_float_bits(rhs.Ty)
f64_ty: mlirlite.LLVMType* = mlirlite.Double(pool)
# Float path: either operand floating -> float result
if lhs_fbits != 0 or rhs_fbits != 0:
target_f: mlirlite.LLVMType* = f64_ty
if lhs_fbits == 32 and rhs_fbits == 32: target_f = mlirlite.Float(pool)
lf: mlirlite.Value* = self.coerce_to_type(builder, lhs, target_f, node, trans)
rf: mlirlite.Value* = self.coerce_to_type(builder, rhs, target_f, node, trans)
return self._call_llvm_pow(pool, builder, mod, lf, rf)
# Integer path: promote to double and compute
li: mlirlite.Value* = self.coerce_to_type(builder, lhs, f64_ty, node, trans)
ri: mlirlite.Value* = self.coerce_to_type(builder, rhs, f64_ty, node, trans)
res: mlirlite.Value* = self._call_llvm_pow(pool, builder, mod, li, ri)
if res is None: return None
# Non-negative integer exponent constant -> fptosi back to lhs type (Python int ** int = int)
rhs_bits: int = self.get_llvm_type_bits(rhs.Ty)
if rhs_bits != 0 and rhs.IsConst == 1 and rhs.IntVal >= 0: return self.coerce_to_type(builder, res, lhs.Ty, node, trans)
return res
def _call_llvm_pow(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, lf: mlirlite.Value*, rf: mlirlite.Value*) -> mlirlite.Value*:
"""Call the llvm.pow.f64 intrinsic and truncate an f32 result back to float."""
if lf is None or rf is None: return None
f64_ty: mlirlite.LLVMType* = mlirlite.Double(pool)
pow_fn: mlirlite.Function* = self.Gen.CallGenBase.find_func_in_module(mod, "llvm.pow.f64")
if pow_fn is None:
pow_fn = mlirlite.create_declare(pool, mod, "llvm.pow.f64", f64_ty)
if pow_fn is not None:
mlirlite.add_param(pool, pow_fn, f64_ty, "x")
mlirlite.add_param(pool, pow_fn, f64_ty, "y")
lf_d: mlirlite.Value* = lf
if self.get_llvm_float_bits(lf.Ty) != 64: lf_d = mlirlite.build_fpext(builder, lf, f64_ty)
rf_d: mlirlite.Value* = rf
if self.get_llvm_float_bits(rf.Ty) != 64: rf_d = mlirlite.build_fpext(builder, rf, f64_ty)
if lf_d is None or rf_d is None: return None
lf_d.Next = rf_d
rf_d.Next = None
res: mlirlite.Value* = mlirlite.build_call(builder, "llvm.pow.f64", lf_d, 2, f64_ty, 0)
if res is None: return None
# f32 target: llvm.pow is always f64, truncate the result back to float
if self.get_llvm_float_bits(lf.Ty) == 32 and self.get_llvm_float_bits(rf.Ty) == 32: res = mlirlite.build_fptrunc(builder, res, mlirlite.Float(pool))
return res
# _binop_to_dunder - BinOp op -> dunder method name
# ============================================================
def _binop_to_dunder(self, op: int) -> str:
"""Map a BinOp operator to its dunder method name, or None if unmapped."""
if op == ast.OpKind.Add: return "__add__"
if op == ast.OpKind.Sub: return "__sub__"
if op == ast.OpKind.Mult: return "__mul__"
if op == ast.OpKind.Div: return "__truediv__"
if op == ast.OpKind.Mod: return "__mod__"
if op == ast.OpKind.Pow: return "__pow__"
if op == ast.OpKind.BitAnd: return "__and__"
if op == ast.OpKind.BitOr: return "__or__"
if op == ast.OpKind.BitXor: return "__xor__"
if op == ast.OpKind.LShift: return "__lshift__"
if op == ast.OpKind.RShift: return "__rshift__"
if op == ast.OpKind.FloorDiv: return "__floordiv__"
return None
# _rbinop_to_dunder - BinOp op -> reverse dunder name (__radd__ etc.): dispatch rhs's reverse dunder
# when lhs lacks the normal dunder (a + b -> b.__radd__(a) if a.__add__ is missing).
def _rbinop_to_dunder(self, op: int) -> str:
"""Map a BinOp operator to its reverse dunder method name, or None."""
if op == ast.OpKind.Add: return "__radd__"
if op == ast.OpKind.Sub: return "__rsub__"
if op == ast.OpKind.Mult: return "__rmul__"
if op == ast.OpKind.Div: return "__rtruediv__"
if op == ast.OpKind.FloorDiv: return "__rfloordiv__"
if op == ast.OpKind.Mod: return "__rmod__"
if op == ast.OpKind.Pow: return "__rpow__"
if op == ast.OpKind.BitAnd: return "__rand__"
if op == ast.OpKind.BitOr: return "__ror__"
if op == ast.OpKind.BitXor: return "__rxor__"
if op == ast.OpKind.LShift: return "__rlshift__"
if op == ast.OpKind.RShift: return "__rrshift__"
return None
# _cmpop_to_dunder - Compare op -> dunder method name
# ============================================================
def _cmpop_to_dunder(self, op: int) -> str:
"""Map a Compare operator to its dunder method name, or None if unmapped."""
if op == ast.OpKind.Eq: return "__eq__"
if op == ast.OpKind.Ne: return "__ne__"
if op == ast.OpKind.Lt: return "__lt__"
if op == ast.OpKind.Le: return "__le__"
if op == ast.OpKind.Gt: return "__gt__"
if op == ast.OpKind.Ge: return "__ge__"
return None
# _unaryop_to_dunder - UnaryOp op -> dunder method name
# ============================================================
def _unaryop_to_dunder(self, op: int) -> str:
"""Map a UnaryOp operator to its dunder method name, or None if unmapped."""
if op == ast.OpKind.USub: return "__neg__"
if op == ast.OpKind.UAdd: return "__pos__"
if op == ast.OpKind.Invert: return "__invert__"
return None
# try_operator_overload - attempt operator overloading. If lhs is a struct pointer, look up the dunder method
# in its class (including the inheritance chain) and emit lhs.__dunder__(rhs); otherwise return None to fall back.
def try_operator_overload(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, lhs: mlirlite.Value*, rhs: mlirlite.Value*, op: int, trans: HT.Translator*, is_compare: int) -> mlirlite.Value*:
"""Call lhs's dunder method when lhs is a struct pointer; else return None."""
if lhs is None or rhs is None: return None
# Map the operator to a dunder method name
dunder: str = None
if is_compare != 0: dunder = self._cmpop_to_dunder(op)
else: dunder = self._binop_to_dunder(op)
if dunder is None: return None
# Check whether lhs is a struct pointer
struct_ty: mlirlite.LLVMType* = self.Gen.StructGen.get_struct_type_from_value(lhs)
if struct_ty is None: return None
# Get the class name
class_name: str = self.Gen.StructGen.get_class_name_by_type(pool, struct_ty)
if class_name is None: return None
# Look up the dunder method in the inheritance chain
lookup_name: str = pool.alloc(128)
if lookup_name is None: return None
viperlib.snprintf(lookup_name, 128, "%s.%s", class_name, dunder)
found_func: mlirlite.Function* = self.Gen.CallGenBase.find_func_in_module(mod, lookup_name)
search_class: str = class_name
if found_func is None:
# Multiple inheritance: walk the MRO ancestor chain (earlier-declared parent first)
mro_ptr: sizet* = self.Gen.StructGen.get_mro_chain(pool, class_name)
mi: int = 1
while mro_ptr is not None and mro_ptr[mi] != 0:
anc: str = rawptr(mro_ptr[mi])
if anc is not None:
parent_lookup: str = pool.alloc(128)
if parent_lookup is not None:
viperlib.snprintf(parent_lookup, 128, "%s.%s", anc, dunder)
parent_func: mlirlite.Function* = self.Gen.CallGenBase.find_func_in_module(mod, parent_lookup)
if parent_func is not None:
found_func = parent_func
search_class = anc
break
mi += 1
# When the method is not in the current module, check whether the class (or a parent) has a SHA1 (stub may be
# uninjected). Prefer locating the entry by type pointer to get the SHA1.
cls_sha1: str = None
op_entry: GenTypes.StructEntry* = self.Gen.StructGen.find_struct_by_type(struct_ty)
if op_entry is not None: cls_sha1 = op_entry.ModuleSha1
if cls_sha1 is None: cls_sha1 = self.Gen.StructGen.get_struct_sha1(search_class)
if cls_sha1 is None:
mro_p: sizet* = self.Gen.StructGen.get_mro_chain(pool, search_class)
mj: int = 1
while mro_p is not None and mro_p[mj] != 0 and cls_sha1 is None:
anc_p: str = rawptr(mro_p[mj])
if anc_p is not None: cls_sha1 = self.Gen.StructGen.get_struct_sha1(anc_p)
mj += 1
# Neither a function definition nor a SHA1 -> the class has no such dunder; return None to fall back to native.
if found_func is None and cls_sha1 is None: return None
# The method really does not exist in its defining module (same-module class without the dunder): return None
# and never emit a ghost call, which would leave a side-effecting `call void @...__div__` and double-emit sdiv.
if found_func is None and trans is not None and trans.ModuleSha1 is not None:
if cls_sha1 is not None and string.strcmp(cls_sha1, trans.ModuleSha1) == 0: return None
# Cross-module class: the stub may be uninjected, so method existence relies on the dependency module's vpi.
# A vpi miss falls back to native comparison (icmp); a ghost call would link as undefined (SysLibTest root cause).
vpi_dup: mlirlite.LLVMType* = self.Gen.CallGenBase._lookup_method_ret_ty_via_vpi(pool, cls_sha1, search_class, dunder)
if vpi_dup is None: return None
# Build the extra_args array (just the one rhs argument)
extra_args: sizet* = pool.alloc(8)
if extra_args is None: return None
extra_args[0] = sizet(rhs)
# Call the method: lhs.__dunder__(rhs)
return self.Gen.CallGenCall._call_method_on_ptr(pool, builder, mod, search_class, dunder, lhs, extra_args, 1, trans)
# try_unary_operator_overload - attempt unary operator overloading. If operand is a struct pointer, look up
# __neg__/__pos__/__invert__ in its class (with inheritance) and emit operand.__dunder__(); else return None.
def try_unary_operator_overload(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, operand: mlirlite.Value*, op: int, trans: HT.Translator*) -> mlirlite.Value*:
"""Call operand's dunder method when operand is a struct pointer; else None."""
if operand is None: return None
dunder: str = self._unaryop_to_dunder(op)
if dunder is None: return None
return self._try_named_dunder(pool, builder, mod, operand, dunder, trans)
def _resolve_dunder_by_class(self, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, class_name: str, dunder: str, trans: HT.Translator*, struct_ty: mlirlite.LLVMType* = None) -> str:
"""Resolve the class (including MRO ancestors) that defines the dunder, or None."""
if class_name is None or dunder is None: return None
# Look up the dunder method in the inheritance chain
lookup_name: str = pool.alloc(128)
if lookup_name is None: return None
viperlib.snprintf(lookup_name, 128, "%s.%s", class_name, dunder)
found_func: mlirlite.Function* = self.Gen.CallGenBase.find_func_in_module(mod, lookup_name)
search_class: str = class_name
if found_func is None:
# Multiple inheritance: walk the MRO ancestor chain (earlier-declared parent first)
mro_ptr: sizet* = self.Gen.StructGen.get_mro_chain(pool, class_name)
mi: int = 1
while mro_ptr is not None and mro_ptr[mi] != 0:
anc: str = rawptr(mro_ptr[mi])
if anc is not None:
parent_lookup: str = pool.alloc(128)
if parent_lookup is not None:
viperlib.snprintf(parent_lookup, 128, "%s.%s", anc, dunder)
parent_func: mlirlite.Function* = self.Gen.CallGenBase.find_func_in_module(mod, parent_lookup)
if parent_func is not None:
found_func = parent_func
search_class = anc
break
mi += 1
# When the method is not in the current module, check whether the class (or a parent) has a SHA1 (stub may be uninjected)
cls_sha1: str = None
if struct_ty is not None:
uop_entry: GenTypes.StructEntry* = self.Gen.StructGen.find_struct_by_type(struct_ty)
if uop_entry is not None: cls_sha1 = uop_entry.ModuleSha1
if cls_sha1 is None: cls_sha1 = self.Gen.StructGen.get_struct_sha1(search_class)
if cls_sha1 is None:
mro_p: sizet* = self.Gen.StructGen.get_mro_chain(pool, search_class)
mj: int = 1
while mro_p is not None and mro_p[mj] != 0 and cls_sha1 is None:
anc_p: str = rawptr(mro_p[mj])
if anc_p is not None: cls_sha1 = self.Gen.StructGen.get_struct_sha1(anc_p)
mj += 1
# Neither a function definition nor a SHA1 -> the class has no such dunder method
if found_func is None and cls_sha1 is None: return None
# Same-module class without the dunder: return None and fall back (consistent with try_operator_overload) to avoid a ghost call.
if found_func is None and trans is not None and trans.ModuleSha1 is not None:
if cls_sha1 is not None and string.strcmp(cls_sha1, trans.ModuleSha1) == 0: return None
# Cross-module class: existence relies on the dependency module's vpi; a vpi miss falls back (no ghost call, avoiding undefined).
if cls_sha1 is not None:
vpi_dup: mlirlite.LLVMType* = self.Gen.CallGenBase._lookup_method_ret_ty_via_vpi(pool, cls_sha1, search_class, dunder)
if vpi_dup is None: return None
return search_class
def _try_named_dunder(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, operand: mlirlite.Value*, dunder: str, trans: HT.Translator*, extra_args: sizet* = None, extra_count: int = 0) -> mlirlite.Value*:
"""Look up and call a dunder method by name on operand's class (including MRO)."""
if operand is None or dunder is None: return None
# Check whether operand is a struct pointer
struct_ty: mlirlite.LLVMType* = self.Gen.StructGen.get_struct_type_from_value(operand)
if struct_ty is None: return None
# Get the class name
class_name: str = self.Gen.StructGen.get_class_name_by_type(pool, struct_ty)
if class_name is None: return None
search_class: str = self._resolve_dunder_by_class(pool, mod, class_name, dunder, trans, struct_ty)
if search_class is None: return None
# Call the method: operand.__dunder__(extra_args...)
return self.Gen.CallGenCall._call_method_on_ptr(pool, builder, mod, search_class, dunder, operand, extra_args, extra_count, trans)
# coerce_cond_to_bool - normalize a condition value to i1 (the single implementation for if/while/and/or/ternary).
# Already-i1 returns as-is; __bool__ is called if defined; pointer != null; otherwise != 0.
def coerce_cond_to_bool(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, val: mlirlite.Value*, trans: HT.Translator*) -> mlirlite.Value*:
"""Normalize a condition to i1, preferring __bool__ and else comparing with 0/null."""
if val is None: return None
bits: int = self.get_llvm_type_bits(val.Ty)
if bits == 1: return val
# Class instance: prefer calling __bool__ (pointer to a struct whose class defines it)
bres: mlirlite.Value* = self._try_named_dunder(pool, builder, mod, val, "__bool__", trans)
if bres is not None:
b_bits: int = self.get_llvm_type_bits(bres.Ty)
if b_bits == 1: return bres
if self.is_ptr_type(bres.Ty) != 0:
null_b: mlirlite.Value* = mlirlite.ConstNull(pool, bres.Ty, "null")
return mlirlite.build_icmp(builder, mlirlite.ICMP_NE, bres, null_b)
zero_b: mlirlite.Value* = mlirlite.const_int32(pool, 0)
return mlirlite.build_icmp(builder, mlirlite.ICMP_NE, bres, zero_b)
if self.is_ptr_type(val.Ty) != 0:
# Pointer type: compare with null (not integer 0, which llc rejects as a type mismatch)
null_val: mlirlite.Value* = mlirlite.ConstNull(pool, val.Ty, "null")
return mlirlite.build_icmp(builder, mlirlite.ICMP_NE, val, null_val)
zero: mlirlite.Value* = mlirlite.const_int32(pool, 0)
return mlirlite.build_icmp(builder, mlirlite.ICMP_NE, val, zero)
# fail_if_double_ptr_binop - double-pointer arithmetic fail-fast (shared by BinOp and AugAssign). Fires when the op
# is arithmetic/bitwise/shift AND both operands are pointers; such IR (`add i8*, i8*`) is invalid, so fail before emitting.
def fail_if_double_ptr_binop(self, pool: memhub.MemBuddy*, op: int, lhs: mlirlite.Value*, rhs: mlirlite.Value*, node: ast.AST* = None, is_aug: int = 0) -> int:
"""Fail fast on double-pointer arithmetic; returns 0 when not triggered."""
if lhs is None or rhs is None: return 0
if self.is_ptr_type(lhs.Ty) == 0 or self.is_ptr_type(rhs.Ty) == 0: return 0
op_ch: str = None
if op == ast.OpKind.Add: op_ch = "+"
elif op == ast.OpKind.Sub: op_ch = "-"
elif op == ast.OpKind.Mult: op_ch = "*"
elif op == ast.OpKind.Div: op_ch = "/"
elif op == ast.OpKind.FloorDiv: op_ch = "//"
elif op == ast.OpKind.Mod: op_ch = "%"
elif op == ast.OpKind.Pow: op_ch = "**"
elif op == ast.OpKind.BitAnd: op_ch = "&"
elif op == ast.OpKind.BitOr: op_ch = "|"
elif op == ast.OpKind.BitXor: op_ch = "^"
elif op == ast.OpKind.LShift: op_ch = "<<"
elif op == ast.OpKind.RShift: op_ch = ">>"
else: return 0
op_lbl: str = op_ch
aug_note: str = ""
if is_aug != 0:
# Augmented assignment: build the "+=" form with an f-string (this language's str has no `+` concatenation).
op_lbl = f"{op_ch}="
aug_note = "(augmented assignment)"
lty_buf: str = pool.alloc_tagged(4096, memhub.DETSITE_EXPRGENOPS_1059)
rty_buf: str = pool.alloc_tagged(4096, memhub.DETSITE_EXPRGENOPS_1060)
lty_lbl: str = "?"
rty_lbl: str = "?"
if lty_buf is not None:
mlirlite.TypePrint(lty_buf, 4096, lhs.Ty, pool)
lty_lbl = lty_buf
if string.strcmp(lty_buf, "i8*") == 0: lty_lbl = "str(i8*)"
if rty_buf is not None:
mlirlite.TypePrint(rty_buf, 4096, rhs.Ty, pool)
rty_lbl = rty_buf
if string.strcmp(rty_buf, "i8*") == 0: rty_lbl = "str(i8*)"
col_s: int = 0
if node is not None: col_s = node.col_offset + 1
self.Gen.TypeGenErr.fatal_error(node, f"[VPC] illegal double-pointer arithmetic type: op = '{op_lbl}'{aug_note}, " f"left operand type = {lty_lbl}, right operand type = {rty_lbl}. " f"in this language str is i8*, with no `+` concatenation semantics (`str + str` and pointer +/- pointer " f"both degrade to raw pointer add/sub => illegal IR: opt reports " f"invalid operand type for instruction). " f"silent degradation is forbidden, compilation aborted. " f"fix: build strings with f-string interpolation, string.strcat, or viperlib.snprintf.", "TypeError", col_s, col_s)
return 1
# binop_needs_int_promotion - whether this op takes C-style integer promotion (to >= 32 bits)
def binop_needs_int_promotion(self, op: int) -> int:
"""C 语义整数提升的适用范围:**算术与移位**为 1(窄于 32 位先提升再算,避免 8/16 位回绕),
**位运算(& | ^)为 0**——按既有约定"两侧同宽就不提升"(WORD | WORD 保持 u16)。"""
if op == ast.OpKind.Add or op == ast.OpKind.Sub or op == ast.OpKind.Mult: return 1
if op == ast.OpKind.Div or op == ast.OpKind.FloorDiv or op == ast.OpKind.Mod: return 1
if op == ast.OpKind.LShift or op == ast.OpKind.RShift: return 1
return 0
# translate_binop - binary operations with automatic type promotion
# ============================================================
def translate_binop(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, node: ast.AST*, trans: HT.Translator* = None) -> mlirlite.Value*:
"""Translate a binary operation with automatic type promotion."""
binop: ast.BinOp* = ast.BinOp*(node)
if binop is None: return None
lhs_node: ast.AST* = binop.left
rhs_node: ast.AST* = binop.right
op: int = binop.op
# Translate rhs first (only once, to avoid repeating side effects)
rhs: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, rhs_node, None, 0, trans)
if rhs is None: return None
# === Operator overload path 1: lhs is a Name bound to a struct variable ===
# For value-type variables (e.g. cnt: Counter) translate_value loads a Struct value, but dunder needs Ptr(Struct) as self; try the overload with the alloca pointer.
if lhs_node is not None and lhs_node.kind() == ast.ASTKind.Name and trans is not None:
nm: ast.Name* = ast.Name*(lhs_node)
if nm is not None and nm.id is not None:
lhs_alloca: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, nm.id)
if lhs_alloca is not None:
ovl_self1: mlirlite.Value* = lhs_alloca
# When the alloca is Ptr(Ptr(Struct)) (an object storage slot), load the object pointer as self; using the
# slot address as self reads the object pointer as a field -> garbage (MultiInheritTest root cause).
if lhs_alloca.Ty is not None and lhs_alloca.Ty.Pointee != 0 and self.Gen.ExprGenBase.is_ptr_type(lhs_alloca.Ty.Pointee) != 0:
load_self1: mlirlite.Value* = mlirlite.build_load(builder, lhs_alloca.Ty.Pointee, lhs_alloca)
if load_self1 is not None: ovl_self1 = load_self1
ovl_result: mlirlite.Value* = self.try_operator_overload(pool, builder, mod, ovl_self1, rhs, op, trans, 0)
if ovl_result is not None: return ovl_result
# Translate lhs normally
lhs: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, lhs_node, None, 0, trans)
if lhs is None: return None
# === Operator overload path 2: lhs is Ptr(Struct) (e.g. after loading a Counter* variable) ===
ovl_result2: mlirlite.Value* = self.try_operator_overload(pool, builder, mod, lhs, rhs, op, trans, 0)
if ovl_result2 is not None: return ovl_result2
# === Operator overload path 3: reverse operator - dispatch rhs.__rX__(lhs) when lhs has no dunder ===
# (Python semantics: a + b -> a.__add__(b), else b.__radd__(a)). Skip early if rhs is not a struct.
r_dunder: str = self._rbinop_to_dunder(op)
if r_dunder is not None:
if self.Gen.StructGen.get_struct_type_from_value(rhs) is not None:
rev_extra: sizet* = pool.alloc(8)
if rev_extra is not None:
rev_extra[0] = sizet(lhs)
r_ovl: mlirlite.Value* = self._try_named_dunder(pool, builder, mod, rhs, r_dunder, trans, rev_extra, 1)
if r_ovl is not None: return r_ovl
# `**` exponentiation: basic types -> llvm.pow (int result written back per the exponent constant). Handled before the float/int split and after overloads.
if op == ast.OpKind.Pow: return self.emit_pow(pool, builder, mod, lhs, rhs, node, trans)
# Fetch operand type information
lhs_bits: int = self.get_llvm_type_bits(lhs.Ty)
rhs_bits: int = self.get_llvm_type_bits(rhs.Ty)
lhs_fbits: int = self.get_llvm_float_bits(lhs.Ty)
rhs_fbits: int = self.get_llvm_float_bits(rhs.Ty)
# Float operation: use float instructions when either operand is float (must precede pointer arithmetic, since float has 0 int bits and would be mistaken for a pointer).
if lhs_fbits != 0 or rhs_fbits != 0:
# Determine the target float type (use the larger bit width)
target_fbits: int = lhs_fbits
if rhs_fbits > target_fbits: target_fbits = rhs_fbits
target_float_ty: mlirlite.LLVMType* = mlirlite.Double(pool)
if target_fbits == 32: target_float_ty = mlirlite.Float(pool)
# Convert both operands to the target float type (int->float via si2fp, float->float via fpext/fptrunc)
lhs = self.coerce_to_type(builder, lhs, target_float_ty, node)
rhs = self.coerce_to_type(builder, rhs, target_float_ty, node)
if op == ast.OpKind.Add: return mlirlite.build_fadd(builder, lhs, rhs)
if op == ast.OpKind.Sub: return mlirlite.build_fsub(builder, lhs, rhs)
if op == ast.OpKind.Mult: return mlirlite.build_fmul(builder, lhs, rhs)
if op == ast.OpKind.Div: return mlirlite.build_fdiv(builder, lhs, rhs)
if op == ast.OpKind.Mod: return mlirlite.build_frem(builder, lhs, rhs)
return None
# Pointer arithmetic: ptr + int / ptr - int / int + ptr -> getelementptr (scaled by element size). int - ptr has no GEP form (illegal in C).
if (lhs_bits == 0 and rhs_bits != 0) or (lhs_bits != 0 and rhs_bits == 0):
if op == ast.OpKind.Add:
if lhs_bits == 0: return self.translate_ptr_arith(pool, builder, lhs, rhs, op)
# int + ptr (swap operands)
return self.translate_ptr_arith(pool, builder, rhs, lhs, op)
if op == ast.OpKind.Sub and lhs_bits == 0: return self.translate_ptr_arith(pool, builder, lhs, rhs, op)
# Double-pointer arithmetic (including AugAssign) fail-fast: the single implementation is fail_if_double_ptr_binop.
# Pointer-int (GEP) is handled by translate_ptr_arith above; double-pointer Add/Sub/... is illegal here (str is i8*).
self.fail_if_double_ptr_binop(pool, op, lhs, rhs, node, 0)
# Widening direction (zext/sext) and the div/mod predicates both need "is the operand unsigned"; evaluate once here and thread trans down.
binop_unsigned: int = 0
if lhs_node is not None:
if self._node_type_unsigned(pool, lhs_node, trans) != 0: binop_unsigned = 1
if binop_unsigned == 0 and rhs_node is not None:
if self._node_type_unsigned(pool, rhs_node, trans) != 0: binop_unsigned = 1
# C 语义整数提升(仅算术/移位;位运算 & | ^ 保持"同宽不提升"):
# 两侧都窄于 32 位时先提升到 32 位再算,避免 8/16 位回绕(如两个 16 位 define 的
# `MAX_INCLUDES * MAX_REL_PATH_LEN` 会算成 0)。提升后把 lhs_bits/rhs_bits 记为 32,
# 让下面的等宽化逻辑不再重复处理(否则会对同宽 i32 再做一次无意义转换)。
if self.binop_needs_int_promotion(op) != 0:
if lhs_bits != 0 and rhs_bits != 0 and lhs_bits < 32 and rhs_bits < 32:
i32_ty_pr: mlirlite.LLVMType* = mlirlite.Int32(pool)
# 加宽方向与下面的等宽化同一判据(i1/i8 默认 zext、带符号注解 sext、无符号 typedef zext);
# probe 只是给 should_zext_widen 提供"宽侧类型",其常量值不参与判据。
i32_probe: mlirlite.Value* = mlirlite.const_int32(pool, 0)
if self.should_zext_widen(pool, lhs, i32_probe, lhs_node, trans) != 0: lhs = mlirlite.build_zext(builder, lhs, i32_ty_pr)
else: lhs = self.coerce_to_type(builder, lhs, i32_ty_pr, node, trans)
if self.should_zext_widen(pool, rhs, i32_probe, rhs_node, trans) != 0: rhs = mlirlite.build_zext(builder, rhs, i32_ty_pr)
else: rhs = self.coerce_to_type(builder, rhs, i32_ty_pr, node, trans)
lhs_bits = 32
rhs_bits = 32
if lhs_bits > rhs_bits:
# 宽侧是「值能无损落进窄侧」的编译期常量时,把**常量**降到窄侧类型(C 语义:常量优先适配另一侧),
# 否则 `crc(u32) ^ 0xFFFFFFFF(i64)` 会把 crc 抬成 64 位,结果再赋回 32 位时报隐式窄化。
if self._const_fits_int_type(lhs, rhs.Ty) != 0: lhs = self.coerce_to_type(builder, lhs, rhs.Ty, node, trans)
elif self.should_zext_widen(pool, rhs, lhs, rhs_node, trans) != 0: rhs = mlirlite.build_zext(builder, rhs, lhs.Ty)
else: rhs = self.coerce_to_type(builder, rhs, lhs.Ty, node, trans)
elif rhs_bits > lhs_bits:
if self._const_fits_int_type(rhs, lhs.Ty) != 0: rhs = self.coerce_to_type(builder, rhs, lhs.Ty, node, trans)
elif self.should_zext_widen(pool, lhs, rhs, lhs_node, trans) != 0: lhs = mlirlite.build_zext(builder, lhs, rhs.Ty)
else: lhs = self.coerce_to_type(builder, lhs, rhs.Ty, node, trans)
# Bitwise type safety: when both sides are pointers (lhs_bits==0 and rhs_bits==0), bitwise ops are illegal (LLVM
# requires integer operands). Common trigger: a failed CDefine lookup returns pointer null, then `|` with a pointer.
if lhs_bits == 0 and rhs_bits == 0:
if op == ast.OpKind.BitOr or op == ast.OpKind.BitAnd or op == ast.OpKind.BitXor or op == ast.OpKind.LShift or op == ast.OpKind.RShift:
# fail-fast: illegal bitwise operand types; abort compilation (expose the upstream root cause)
self.Gen.TypeGenErr.fatal_error(node, "illegal bitwise operand types: both lhs/rhs are non-integer types (pointer or struct), cannot perform bitwise operations", "TypeError")
# 移位:计数静态可确定时做范围检查(与 AugAssign 的 <<= / >>= 共用);
# 计数宽度本身豁免,按提升/等宽化后的左操作数有效位宽比较(C 语义下 u8 << 8 合法)。
if op == ast.OpKind.LShift or op == ast.OpKind.RShift:
self.Gen.AssignGen.require_shift_count_in_range(pool, trans, node, rhs_node, self.get_llvm_type_bits(lhs.Ty))
if op == ast.OpKind.Add: return mlirlite.build_add(builder, lhs, rhs)
elif op == ast.OpKind.Sub: return mlirlite.build_sub(builder, lhs, rhs)
elif op == ast.OpKind.Mult: return mlirlite.build_mul(builder, lhs, rhs)
elif op == ast.OpKind.Div:
# Unsigned operands -> udiv (binop_unsigned from _node_type_unsigned, covering sizet/uint64 and annotated
# variable/array elements). An unconditional sdiv gave wrong results for unsigned values >= 2^63.
if binop_unsigned != 0: return mlirlite.build_udiv(builder, lhs, rhs)
return mlirlite.build_sdiv(builder, lhs, rhs)
elif op == ast.OpKind.FloorDiv:
if binop_unsigned != 0: return mlirlite.build_udiv(builder, lhs, rhs)
return mlirlite.build_sdiv(builder, lhs, rhs)
elif op == ast.OpKind.Mod:
if binop_unsigned != 0: return mlirlite.build_urem(builder, lhs, rhs)
return mlirlite.build_srem(builder, lhs, rhs)
elif op == ast.OpKind.BitAnd: return mlirlite.build_and(builder, lhs, rhs)
elif op == ast.OpKind.BitOr: return mlirlite.build_or(builder, lhs, rhs)
elif op == ast.OpKind.BitXor: return mlirlite.build_xor(builder, lhs, rhs)
elif op == ast.OpKind.LShift: return mlirlite.build_shl(builder, lhs, rhs)
elif op == ast.OpKind.RShift:
# Unsigned >> -> lshr, signed >> -> ashr (aligned with TPC ExprGen.emit_binary_op: i8/i16 default unsigned, others check operand signedness).
if lhs_bits == 8 or lhs_bits == 16: return mlirlite.build_lshr(builder, lhs, rhs)
if self._node_type_unsigned(pool, lhs_node, trans) != 0: return mlirlite.build_lshr(builder, lhs, rhs)
return mlirlite.build_ashr(builder, lhs, rhs)
return None