Files
ViperC/App/lib/core/Gen/Call/CallGenProp.vp
T
TermiNexus 19f9c53a5f fix(codegen): zero-extend unsigned 16-bit widening in every coercion path
A uint16 value that is widened to int/uint32/uint64 must be zero-extended
(movzwl), and an int16 value must be sign-extended (movswl). coerce_to_type
picks the direction from the source AST node's declared signedness, which
requires the translator; several call sites omitted it and therefore fell
back to sext, so a big-endian uint16 such as 0xDCBC became 0xFFFF...DCBC
(numGlyphs 56508 read as -9028 and every glyph range collapsed to zero).

- pass trans at the assignment, annotated-assignment, augmented-assignment,
  global-array initializer, index, element-store, constructor-argument,
  conditional-expression and return coercion sites
- printf argument promotion now receives the argument expression and trans,
  so uint16/uint8 promote with zext like any other widening
- print selects the format token and extension from the source signedness
  (%u/%llu + zext for unsigned, %d/%lld + sext for signed) instead of a
  hardcoded %d + sext for 8/16-bit values
2026-10-28 23:06:17 +08:00

332 lines
22 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 lib.core.Gen.GenApi as GenApi
from stdint import *
import ast
import mlirlite
import memhub
import string
import stdio
import viperlib
import lib.core.Gen.Translator as HT
from . import CallGenBase
# CallGenProp - split from CallGenMixin (stage B): property access (getter/setter/deleter / static class calls / list builtins)
# ============================================================
novirtual class CallGenProp(CallGenBase.CallGenBase):
"""Property access (getter/setter/deleter / static class calls / list builtins)"""
# Property methods (@property/@x.setter/@x.deleter) and static/class call helpers.
# Translate obj.prop / obj.prop=v / del obj.prop / Class.method() into the matching {sha1}.ClassName.prop call.
# _translate_property_call - translate a property/static method call.
# Resolve the call name like _translate_method_call; call_head/call_count is the prebuilt arg list.
def _translate_property_call(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, class_name: str, method_name: str, call_head: mlirlite.Value*, call_count: int, trans: HT.Translator*, arg_nodes: list[ast.AST*]* = None, start_idx: int = 0) -> mlirlite.Value*:
"""Translate a property/static method call, return the call result Value"""
if pool is None or builder is None or class_name is None or method_name is None: return None
# Build the lookup name: ClassName.method_name
lookup_name: str = pool.alloc(128)
if lookup_name is None: return None
viperlib.snprintf(lookup_name, 128, "%s.%s", class_name, method_name)
# 重载:同名静态/类方法 >=2 时按实参决议。静态方法没有 self/cls 形参(param_base=0),
# 类方法第 0 个形参是 cls(param_base=1);arg_nodes 前 start_idx 个是预置接收者,不参与匹配。
found_func: mlirlite.Function* = None
m_ovl_done: int = 0
if trans is not None and trans._funcs is not None:
if self.Gen.CallGenLeafKit.count_owned_overloads(trans._funcs, trans._func_count, lookup_name, trans.ModuleSha1, 1) >= 2:
a_n_p: sizet = 0
if arg_nodes is not None: a_n_p = len(arg_nodes)
user_n_p: sizet = 0
if int(a_n_p) > start_idx: user_n_p = a_n_p - sizet(start_idx)
pbase_p: int = 0
if start_idx > 0: pbase_p = 1
ovl_p: mlirlite.Function* = self.Gen.CallGenLeafKit.resolve_overload_call(pool, trans, trans._funcs, trans._func_count, lookup_name, trans.ModuleSha1, arg_nodes, user_n_p, None, 1, pbase_p, start_idx)
if ovl_p is None: return None
found_func = ovl_p
m_ovl_done = 1
# Look up the function (exact/suffix match)
if m_ovl_done == 0: found_func = self.find_func_in_module(mod, lookup_name)
# Determine the call name
call_name: str = lookup_name
if found_func is not None:
found_name: str = mlirlite.function_get_name(found_func)
if found_name is not None: call_name = found_name
else:
# Not found: build the call name from the class's owning module SHA1
cls_sha1_pc: str = self.Gen.StructGen.get_struct_sha1(class_name)
if cls_sha1_pc is not None:
mlen_pc: sizet = string.strlen(cls_sha1_pc) + string.strlen(lookup_name) + 2
mangled_pc: str = pool.alloc(mlen_pc)
if mangled_pc is not None:
viperlib.snprintf(mangled_pc, mlen_pc, "%s.%s", cls_sha1_pc, lookup_name)
call_name = mangled_pc
# Determine the return type
i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool)
call_ret_ty: mlirlite.LLVMType* = i32_ty
if found_func is not None:
frt: mlirlite.LLVMType* = mlirlite.function_get_ret_ty(found_func)
if frt is not None: call_ret_ty = frt
else:
sp_ty1: mlirlite.LLVMType* = self._special_method_ret_ty(pool, class_name, method_name)
if sp_ty1 is not None: call_ret_ty = sp_ty1
else:
cls_sha1_rt: str = self.Gen.StructGen.get_struct_sha1(class_name)
# Authoritative source: vpi stub return-annotation cache (in place, no hardcoded whitelist)
vpi_ret_ty: mlirlite.LLVMType* = self._lookup_method_ret_ty_via_vpi(pool, cls_sha1_rt, class_name, method_name, trans)
if vpi_ret_ty is not None: call_ret_ty = vpi_ret_ty
else:
# Fallback: parse the module text.ll define line (legacy form not covered by vpi)
looked_up_ty: mlirlite.LLVMType* = self._LookupMethodRetTyFromTextLL(pool, cls_sha1_rt, class_name, method_name)
if looked_up_ty is not None: call_ret_ty = looked_up_ty
else:
# Phase A deps not ready (cls_sha1_rt None / the defining module's text.ll missing):
# keep the i32 default (build_call(Void) returning None would break expression translation).
if cls_sha1_rt is None or self._dep_text_ll_exists(pool, cls_sha1_rt) == 0: call_ret_ty = mlirlite.Int32(pool)
else: call_ret_ty = self._fail_method_ret_ty(method_name, None, class_name, None, cls_sha1_rt)
# Value-reference (&T / &temp T) params: auto-address the user args before param-type
# coercion (self/obj or the classmethod cls occupies index 0, skipped via start_idx).
call_head = self._apply_auto_addr_to_call_args(builder, pool, mod, trans, call_name, arg_nodes, call_head, call_count, start_idx)
# Coerce args to the declared param types (same as _translate_method_call); target func: found_func,
# else the mangled module declare. Otherwise call(i32) vs declare(i64) mismatches and llc errors.
tgt_func_pc: mlirlite.Function* = found_func
if tgt_func_pc is None: tgt_func_pc = self.find_func_in_module(mod, call_name)
if tgt_func_pc is not None:
prev_pc: mlirlite.Value* = None
cur_arg_pc: mlirlite.Value* = call_head
cur_param_pc: mlirlite.Param* = mlirlite.function_get_param_head(tgt_func_pc)
while cur_arg_pc is not None and cur_param_pc is not None:
param_ty_pc: mlirlite.LLVMType* = mlirlite.param_get_ty(cur_param_pc)
if param_ty_pc is not None and cur_arg_pc.Ty is not None:
coerced_pc: mlirlite.Value* = self.Gen.ExprGenBase.coerce_to_type(builder, cur_arg_pc, param_ty_pc, None, trans)
if coerced_pc is not None and coerced_pc != cur_arg_pc:
# Keep the list intact: the new Value inherits the node's Next; predecessor points to it
mlirlite.value_set_next(coerced_pc, cur_arg_pc.Next)
if prev_pc is None: call_head = coerced_pc
else: mlirlite.value_set_next(prev_pc, coerced_pc)
cur_arg_pc = coerced_pc
prev_pc = cur_arg_pc
cur_arg_pc = cur_arg_pc.Next
cur_param_pc = mlirlite.param_get_next(cur_param_pc)
# Ensure the declare exists, then call
self._ensure_method_declare(pool, mod, call_name, call_ret_ty, call_head, call_count)
return mlirlite.build_call(builder, call_name, call_head, call_count, call_ret_ty, 0)
# _resolve_obj_class_name - resolve an object expression's class short name.
# Prefer the variable annotation (AnnotClassName); fall back to the LLVM type (Ptr(Struct)).
def _resolve_obj_class_name(self, pool: memhub.MemBuddy*, trans: HT.Translator*, obj_node: ast.AST*) -> str:
"""Resolve an object expression's class short name, None on failure"""
if pool is None or trans is None or obj_node is None: return None
if obj_node.kind() == ast.ASTKind.Name:
obj_nm: ast.Name* = ast.Name*(obj_node)
if obj_nm.id is not None:
var_entry: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, obj_nm.id)
annot_val: str = self.Gen.VarGen.var_annot_class_name(var_entry)
if annot_val is not None:
# [D32] Split the qualified name only on '.' outside brackets: the old strrchr over the
# whole string picked the '.' inside "list[sha1.AST*]", yielding a bogus short name.
n_av: sizet = string.strlen(annot_val)
last_dot_av: sizet = n_av
depth_av: int = 0
for ai_av in range(int(n_av)):
ch_av: int = annot_val[ai_av]
if ch_av == '[': depth_av += 1
elif ch_av == ']': depth_av -= 1
elif ch_av == '.' and depth_av == 0: last_dot_av = ai_av
if last_dot_av < n_av: return annot_val + last_dot_av + 1
return annot_val
return None
# _try_property_getter_attr - try to translate obj.attr into a property getter call.
# If the class has an @property/@x.getter for attr, emit call Class.attr(obj); else return None.
def _try_property_getter_attr(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, at_node: ast.Attribute*, obj_ptr: mlirlite.Value*, trans: HT.Translator*, fallback_class_name: str = None) -> mlirlite.Value*:
"""Try a property getter access, return the call result Value; None if not a getter."""
if at_node is None or at_node.attr is None or obj_ptr is None or trans is None: return None
class_name: str = self._resolve_obj_class_name(pool, trans, at_node.value)
if class_name is None: class_name = fallback_class_name
if class_name is None: return None
meta: int = self.Gen.ClassGenBase.lookup_method_meta(class_name, at_node.attr)
if meta & GenTypes.META_PROP_GETTER == 0: return None
# Single-arg call: obj -> getter (no user args to auto-address; obj is slot 0)
mlirlite.value_set_next(obj_ptr, None)
return self._translate_property_call(pool, builder, mod, class_name, at_node.attr, obj_ptr, 1, trans, None, 1)
# _try_property_setter - try to translate obj.attr = v into a property setter call.
# If the class has an @x.setter for attr, emit call Class.attr$set(obj, v) and return 1; else 0.
def _try_property_setter(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, target_attr: ast.Attribute*, rhs_val: mlirlite.Value*, trans: HT.Translator*, rhs_node: ast.AST* = None) -> int:
"""Try a property setter assignment, return 1=handled (call emitted)"""
if pool is None or builder is None or mod is None or trans is None: return 0
if target_attr is None or target_attr.attr is None or rhs_val is None: return 0
class_name: str = self._resolve_obj_class_name(pool, trans, target_attr.value)
if class_name is None: return 0
meta: int = self.Gen.ClassGenBase.lookup_method_meta(class_name, target_attr.attr)
if meta & GenTypes.META_PROP_SETTER == 0: return 0
# Translate the object value (self argument)
obj_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, target_attr.value, None, 0, trans)
if obj_val is None: return 0
# Build the argument list: obj -> rhs
mlirlite.value_set_next(obj_val, None)
mlirlite.value_set_next(rhs_val, None)
mlirlite.value_set_next(obj_val, rhs_val)
# The setter function name carries the $set suffix
setter_name_buf: str = pool.alloc(128)
if setter_name_buf is None: return 0
viperlib.snprintf(setter_name_buf, 128, "%s$set", target_attr.attr)
# Value-reference (&T / &temp T) setter value param: expose the rhs AST as user arg 0
# (obj is slot 0) so the helper can take the lvalue address / build a temp copy.
set_arg_nodes: list[ast.AST*]* = None
if rhs_node is not None:
set_arg_nodes = list[ast.AST*](pool, 1)
if set_arg_nodes is not None: set_arg_nodes.append(rhs_node)
self._translate_property_call(pool, builder, mod, class_name, setter_name_buf, obj_val, 2, trans, set_arg_nodes, 1)
return 1
# _try_property_deleter - try to translate del obj.attr into a property deleter call.
# If the class has an @x.deleter for attr, emit call Class.attr$del(obj) and return 1; else 0.
def _try_property_deleter(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, target_attr: ast.Attribute*, trans: HT.Translator*) -> int:
"""Try a property deleter, return 1=handled (call emitted)"""
if pool is None or builder is None or mod is None or trans is None: return 0
if target_attr is None or target_attr.attr is None: return 0
class_name: str = self._resolve_obj_class_name(pool, trans, target_attr.value)
if class_name is None: return 0
meta: int = self.Gen.ClassGenBase.lookup_method_meta(class_name, target_attr.attr)
if meta & GenTypes.META_PROP_DELETER == 0: return 0
# Translate the object value (self argument)
obj_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, target_attr.value, None, 0, trans)
if obj_val is None: return 0
mlirlite.value_set_next(obj_val, None)
# The deleter function name carries the $del suffix
del_name_buf: str = pool.alloc(128)
if del_name_buf is None: return 0
viperlib.snprintf(del_name_buf, 128, "%s$del", target_attr.attr)
self._translate_property_call(pool, builder, mod, class_name, del_name_buf, obj_val, 1, trans, None, 1)
return 1
# _translate_static_class_call - translate ClassName.method(args) for static/class methods.
# @staticmethod: call Class.method(args...). @classmethod: prepend a stack-allocated class pointer.
def _translate_static_class_call(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, class_name: str, method_name: str, cl: ast.Call*, trans: HT.Translator*) -> mlirlite.Value*:
"""Translate a ClassName static/class method call, return the call result Value; None otherwise"""
if pool is None or builder is None or mod is None or trans is None: return None
if class_name is None or method_name is None or cl is None: return None
meta: int = self.Gen.ClassGenBase.lookup_method_meta(class_name, method_name)
is_static: int = 0
is_class: int = 0
if meta & GenTypes.META_STATIC != 0: is_static = 1
if meta & GenTypes.META_CLASS != 0: is_class = 1
if is_static == 0 and is_class == 0: return None
# Build the argument list
head: mlirlite.Value* = None
total_count: int = 0
# @classmethod: prepend a stack-allocated class pointer as the cls argument
if is_class != 0:
struct_ty_cc: mlirlite.LLVMType* = self.Gen.StructGen.get_struct_type(class_name)
if struct_ty_cc is None: return None
cls_ptr: mlirlite.Value* = self.Gen.VarGen._alloca_at_entry(builder, struct_ty_cc)
if cls_ptr is None: return None
mlirlite.value_set_next(cls_ptr, None)
head = cls_ptr
total_count = 1
# Translate the call arguments
cargs: list[ast.AST*]* = cl.args
can: sizet = 0
if cargs is not None: can = len(cargs)
tail: mlirlite.Value* = head
for ai in range(int(can)):
arg_node: ast.AST* = cargs.get(ai)
if arg_node is None: continue
arg_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, arg_node, None, 0, trans)
if arg_val is None: continue
mlirlite.value_set_next(arg_val, None)
if head is None:
head = arg_val
tail = arg_val
else:
mlirlite.value_set_next(tail, arg_val)
tail = arg_val
total_count += 1
# Zero-arg @staticmethod (e.g. PyListObj.new()): head=None is a valid no-arg call, so still route
# through _translate_property_call; returning None would fall back to bare-name lookup and fail.
# Value-reference (&T / &temp T) user args: static methods have no self slot (start_idx=0),
# classmethods carry the cls pointer at slot 0 (start_idx=1).
sc_start_idx: int = 0
if is_class != 0: sc_start_idx = 1
return self._translate_property_call(pool, builder, mod, class_name, method_name, head, total_count, trans, cargs, sc_start_idx)
# _is_list_builtin_method - test whether (class_name, method_name) is a list generic builtin.
# list[T] methods are inlined by translate_call's list branch, not registered as functions.
def _is_list_builtin_method(self, class_name: str, method_name: str) -> int:
"""Test whether (class_name, method_name) is a list generic builtin method"""
if class_name is None or method_name is None: return 0
# Class name must start with "list" (list / list[AST*] specializations)
if string.strncmp(class_name, "list", 4) != 0: return 0
# Method name must be one of the list builtin methods
if string.strcmp(method_name, "append") == 0: return 1
if string.strcmp(method_name, "get") == 0: return 1
if string.strcmp(method_name, "set") == 0: return 1
if string.strcmp(method_name, "pop") == 0: return 1
if string.strcmp(method_name, "clear") == 0: return 1
if string.strcmp(method_name, "__iter__") == 0: return 1
if string.strcmp(method_name, "__len__") == 0: return 1
if string.strcmp(method_name, "__getitem__") == 0: return 1
if string.strcmp(method_name, "__setitem__") == 0: return 1
return 0
# _strip_module_sha1_prefix - strip the cross-module specialization sha1 prefix.
# Receiver struct names often carry a "{16 hex}." prefix; use GenApi.sha1_prefix_len (C10).
def _strip_module_sha1_prefix(self, name: str) -> str:
"""Strip the "{16hex}." prefix (return the input unchanged if absent; returns a substring of the input)"""
if name is None: return None
pfx: int = GenApi.sha1_prefix_len(name)
return name + pfx if pfx != 0 else name
# _resolve_generic_receiver_name - resolve the receiver's actual generic specialization name (IR truth).
# Strip the sha1 prefix, take the base before '['; bare generic base => fail-fast (D25), else None.
def _resolve_generic_receiver_name(self, pool: memhub.MemBuddy*, struct_ty: mlirlite.LLVMType*, method_name: str, cl: ast.Call*) -> str:
"""Return the actual generic specialization name (sha1 stripped) or None; a bare generic base fails fast"""
if struct_ty is None: return None
ty_nm: str = mlirlite.get_struct_name(struct_ty)
if ty_nm is None or ty_nm[0] == '\0': return None
short_nm: str = self._strip_module_sha1_prefix(ty_nm)
lb: str = string.strchr(short_nm, '[')
bare_len: sizet = 0
if lb is not None: bare_len = sizet(uint64(lb) - uint64(short_nm))
else: bare_len = string.strlen(short_nm)
if bare_len == 0: return None
base_nm: str = pool.alloc(bare_len + 1)
if base_nm is None: return None
string.strncpy(base_nm, short_nm, bare_len)
base_nm[bare_len] = '\0'
# Base is not a registered generic template (plain class receiver) => not generic; return None
if self.Gen.ClassGenGeneric._find_generic_template(base_nm) is None: return None
if lb is not None: return short_nm
# Bare generic base (no type args): element type unknown -> fail-fast (D25)
self.Gen.TypeGenErr.fatal_error(cl, f"the actual struct type of the receiver of method '{method_name}' is a bare generic base " f"'{short_nm}*' (type arguments lost), so the element type is unknown; " f"silently using the default element type is forbidden (treating elements as i32 would truncate pointers); " f"make the variable annotation/type carry type arguments (e.g. list[ast.AST*])", "TypeError")
return None
# _is_list_builtin_receiver - test list builtin methods from the receiver's actual type.
# Prefer the actual LLVM struct name (sha1 stripped); fall back to the annotation/registered name.
def _is_list_builtin_receiver(self, pool: memhub.MemBuddy*, recv_val: mlirlite.Value*, fallback_name: str, method_name: str, cl: ast.Call*) -> int:
"""Test list builtin methods from the receiver's actual type (fall back to the annotation name if unavailable)"""
sty: mlirlite.LLVMType* = self.Gen.StructGen.get_struct_type_from_value(recv_val)
if sty is not None:
ty_nm: str = mlirlite.get_struct_name(sty)
if ty_nm is not None and ty_nm[0] != '\0':
# Generic template receiver: bare base fail-fast; with type args use the actual specialization
act_nm: str = self._resolve_generic_receiver_name(pool, sty, method_name, cl)
if act_nm is not None: return self._is_list_builtin_method(act_nm, method_name)
# Non-generic template receiver: judge by the actual short name (sha1 stripped)
return self._is_list_builtin_method(self._strip_module_sha1_prefix(ty_nm), method_name)
# Actual type unavailable -> fall back to the annotation/registered name
return self._is_list_builtin_method(fallback_name, method_name)