fix(call): 泛型特化类跨模块 declare 按模板 vpi 注解对齐形参口径

特化名({sha}.list[str].__init__)在 vpi 参数注解缓存中无精确键——键段取自 class 行文本,只存模板 list[T]——定义侧形参注解取不到,调用点 declare 停在调用点实参口径(elem_size 字面量 i32 vs 定义侧 t.CSizeT=i64;池实参 MemBuddy*/i8* vs 定义侧 MemManager*),并经 .stub.ll 传播到未调用该函数的消费方模块

新增 VpiReader._resolve_vpi_param_annot_seg_template:按 {sha1}.{base}[T].{method} 定位模板条目、用特化实参替换段内类型形参 token 后再解析,未命中返回 None 不兜底;_coerce_args_to_def_params 在精确键未命中且类名含 [ 时改走模板退查并置 tpl_src,使 _coerce_arg_value_to_ty 对同为具体结构体指针但基类型不同补 bitcast;_call_method_on_ptr 的跨模块分支补建 declare 前的 coerce(先例 a83d183 的三处调用点)

同时把 {sha}.{cls}.{method} 的切分由首个 . 改为末个 .(CallGenLib/CallGenBase 两处):method 不含 .,首个 . 会把含模块限定名的类型实参截断(list[3571eabe5a83d798.AST|t.CPtr] 截成无 ] 的类名 ⇒ 模板退查必 miss)

另修 VpiReader 一处 double-free:_subst_template_arg 早退分支返回入参本身,替换前释放原缓冲需先判同一性(三条路径各释放一次)

效果: 危险级签名不一致 TransPyV 8->4->0、Test/AstTest 6->2->0,ZlibTest/WebTest/FakeDuckTest/ListTest/GenericTest2/VectorGenericTest 保持 0;4 个目标符号 define/declare 共 148 处全部统一为 MemManager*+i64(0 处旧口径);自举达全树逐字节不动点(exe sha256 相同、temp/** 635 文件零差异);AstTest 及既有测试全 PASS 且无 ASan 报告
This commit is contained in:
TermiNexus committed 2026-09-16 09:31:57 +08:00
1 parent a83d183abb
commit 429de97083
4 files changed
+163 -11

No files matched your search

+110
View File
@@ -709,6 +709,116 @@ def _lookup_vpi_param_annots(src_sha1: str, cls_name: str, method_name: str) ->
return r
# ============================================================
# _resolve_vpi_param_annot_seg_template - 泛型特化类的参数注解段解析
#
# 真源:_cache_vpi_param_annots_text 的类名键段取自 vpi 的 class 行文本
# (取到 ':' / 空格 / '(' 为止),`class list[T]:` ⇒ 键段 "list[T]";因此特化
# 类名(list[str]/list[int]/list[rowptr[AST]])的精确键必然遗漏,定义侧形参
# 注解取不到 → 调用点 declare 停在调用点实参宽度(elem_size 字面量 i32 把
# 定义侧 t.CSizeT(i64) 固化窄)。
# 与 _lookup_vpi_ret_annot 的"泛型特化归一化匹配"同源:按 {sha1}.{base}[T].
# {method} 形态在缓存中定位模板条目,把段内独立的类型形参 token 用特化实参
# (cls_name 的 [...] 内容)替换后再解析(先例 _subst_template_arg)。
# 未命中/多参模板(键段含空格被截断,如 "Pair[A,")返回 None,不做兜底推断。
# ============================================================
def _resolve_vpi_param_annot_seg_template(pool: rowptr[memhub.MemBuddy],
src_sha1: str,
cls_name: str,
method_name: str,
seg_idx: int,
i32_ty: llvmlite.LLVMType | t.CPtr,
i8_ptr_ty: llvmlite.LLVMType | t.CPtr) -> llvmlite.LLVMType | t.CPtr:
"""特化类 X[arg] 的参数注解段解析(模板 X[T] 注解 + T→arg 替换);未命中 None"""
if pool is None or src_sha1 is None or cls_name is None or method_name is None \
or _g_vpi_param_keys is None:
return None
lbkt_t: str = string.strchr(cls_name, 91) # '[' = 91
if lbkt_t is None:
return None
rbkt_t: str = string.strrchr(cls_name, 93) # ']' = 93
if rbkt_t is None or rbkt_t <= lbkt_t:
return None
base_len_t: sizet = uint64(lbkt_t) - uint64(cls_name)
arg_len_t: sizet = uint64(rbkt_t) - uint64(lbkt_t) - 1
if base_len_t <= 0 or arg_len_t <= 0:
return None
base_t: bytes = stdlib.malloc(base_len_t + 1)
if base_t is None:
return None
arg_t: bytes = stdlib.malloc(arg_len_t + 1)
if arg_t is None:
stdlib.free(base_t)
return None
string.strncpy(base_t, cls_name, base_len_t)
base_t[base_len_t] = '\0'
string.strncpy(arg_t, lbkt_t + 1, arg_len_t)
arg_t[arg_len_t] = '\0'
sha1_len_t: sizet = string.strlen(src_sha1)
ty_t: llvmlite.LLVMType | t.CPtr = None
ki_t: int
for ki_t in range(_g_vpi_param_count):
gk_t: bytes = _g_vpi_param_keys + ki_t * VPI_PARAM_KEY_LEN
if string.strncmp(gk_t, src_sha1, sha1_len_t) != 0 or gk_t[sha1_len_t] != '.':
continue
if string.strncmp(gk_t + sha1_len_t + 1, base_t, base_len_t) != 0:
continue
if gk_t[sha1_len_t + 1 + base_len_t] != 91:
continue
gklb_t: str = string.strchr(gk_t + sha1_len_t + 1 + base_len_t, 93)
if gklb_t is None:
continue
tplen_t: sizet = uint64(gklb_t) - uint64(gk_t + sha1_len_t + 1 + base_len_t) - 1
# 方法名须整体匹配(gklb_t 后即 ']' 与 '.';mangled 形态保证无同名歧义)
if tplen_t <= 0 or string.strcmp(gklb_t + 2, method_name) != 0:
continue
annots_t: str = _g_vpi_param_strs + ki_t * VPI_PARAM_LEN
if annots_t is None or annots_t[0] == '\0':
break
# 段定位(与 _resolve_vpi_param_annot_seg 同口径:'@' 分隔,段 0 = self 空段)
cur_t: sizet = 0
cur_seg_t: int = 0
while cur_seg_t < seg_idx and annots_t[cur_t] != '\0':
while annots_t[cur_t] != '\0' and annots_t[cur_t] != '@':
cur_t += 1
if annots_t[cur_t] == '@':
cur_t += 1
cur_seg_t += 1
seg_start_t: sizet = cur_t
seg_len_t: sizet = 0
while annots_t[seg_start_t + seg_len_t] != '\0' \
and annots_t[seg_start_t + seg_len_t] != '@':
seg_len_t += 1
if seg_len_t == 0:
break
seg_buf_t: bytes = stdlib.malloc(seg_len_t + 1)
if seg_buf_t is None:
break
string.strncpy(seg_buf_t, annots_t + seg_start_t, seg_len_t)
seg_buf_t[seg_len_t] = '\0'
# 类型形参 token(如 T)→ 特化实参(如 str / rowptr[AST] / AST|t.CPtr):
# 模板注解按源码原样缓存,不替换则 `item: T` 解析不出类型(None 跳过)
tparam_t: bytes = stdlib.malloc(tplen_t + 1)
if tparam_t is not None:
string.strncpy(tparam_t, gk_t + sha1_len_t + 1 + base_len_t + 1, tplen_t)
tparam_t[tplen_t] = '\0'
subst_t: str = _subst_template_arg(seg_buf_t, tparam_t, arg_t)
if subst_t is not None:
# _subst_template_arg 早退分支(tplen==0 或 tplen>alen)返回入参
# 本身 ⇒ subst_t 与 seg_buf_t 同指针时并非新分配,此处不得 free
# (否则与末尾 free(seg_buf_t) 重复释放)
if uint64(subst_t) != uint64(seg_buf_t):
stdlib.free(seg_buf_t)
seg_buf_t = subst_t
stdlib.free(tparam_t)
ty_t = _resolve_vpi_ret_annot_str(pool, seg_buf_t, i32_ty, i8_ptr_ty)
stdlib.free(seg_buf_t)
break
stdlib.free(base_t)
stdlib.free(arg_t)
return ty_t
# ============================================================
# _repair_declare_params_from_text - 用文本缓存补全 declare 参数
#
+2 -1
View File
@@ -745,7 +745,8 @@ class CallGenBase:
sha1_buf_d[pfx_len_d - 1] = '\0'
sha1_d = sha1_buf_d
rest_d: str = call_name + pfx_len_d
dot2_d: str = string.strchr(rest_d, '.')
# 取末个 '.':method 不含 '.',首个 '.' 会把含模块限定名的类型实参截断
dot2_d: str = string.strrchr(rest_d, '.')
if dot2_d is not None:
cls_len_d: sizet = uint64(dot2_d) - uint64(rest_d)
if cls_len_d > 0:
+10
View File
@@ -652,6 +652,16 @@ class CallGenCall(CallGenBase.CallGenBase):
call_ret_ty = self._fail_method_ret_ty(method_name, node)
# 跨模块调用(found_func 为 None):创建 declare 声明,避免 llc 报 undefined value
if found_func is None:
# 先按定义侧形参 coerce 实参(宽度/指针口径对齐),再建 declare(先例
# a83d183 的三处调用点:_translate_method_call / _translate_call_with_kwargs
# / CallGenDispatch 特化分支)。本路径此前直接用调用点实参类型建
# declare:泛型特化构造 `list[T](pool, 8)` 的 elem_size 字面量 i32 会把
# 定义侧 t.CSizeT(i64) 固化窄,且池实参(MemBuddy*/i8*)与定义侧
# MemManager* 口径不一致 → 跨模块 declare/define 不一致并经 .stub.ll
# 传播到未调用该函数的消费方模块。泛型特化名({sha}.list[str].__new__)
# 的定义侧注解在模板类上,由 _coerce_args_to_def_params 的模板退查解析。
head = self.Gen.CallGenLib._coerce_args_to_def_params(
pool, builder, call_name, head, total_count, None, 1, trans)
self._ensure_method_declare(pool, mod, call_name, call_ret_ty, head, total_count)
return llvmlite.build_call(builder, call_name, head, total_count, call_ret_ty, 0)
+41 -10
View File
@@ -233,7 +233,8 @@ class CallGenLib(CallGenBase.CallGenBase):
rest: str = call_name + 17
cls_nm: str = None
meth_nm: str = rest
dot_r: str = string.strchr(rest, 46)
# 取末个 '.':method 不含 '.',首个 '.' 会把含模块限定名的类型实参截断
dot_r: str = string.strrchr(rest, 46)
if dot_r is not None:
cls_len: sizet = uint64(dot_r) - uint64(rest)
if cls_len <= 0:
@@ -263,11 +264,21 @@ class CallGenLib(CallGenBase.CallGenBase):
if self.Gen.CallGenBase.TempDir is not None and self.Gen.CallGenBase.TempDirLen > 0:
VpiReader._ensure_vpi_param_annot_loaded(sha1_buf, self.Gen.CallGenBase.TempDir)
prm_annots: str = VpiReader._lookup_vpi_param_annots(sha1_buf, cls_nm, meth_nm)
# 泛型特化类({sha}.list[str].__new__):定义侧注解写在**模板类**上
# (vpi 键段为 "list[T]",见 VpiReader._cache_vpi_param_annots_text 的
# 类名取值),特化名的精确键必然遗漏 ⇒ 逐段退查模板注解并把类型形参
# 替换为特化实参(VpiReader._resolve_vpi_param_annot_seg_template,
# 与 _lookup_vpi_ret_annot 的泛型特化归一化匹配同源)。tpl_src=1 使
# 指针口径也对齐定义侧(MemBuddy* → MemManager*)。
tpl_src: int = 0
if prm_annots is None:
stdlib.free(sha1_buf)
if cls_nm is not None:
stdlib.free(cls_nm)
return head
if cls_nm is not None and string.strchr(cls_nm, 91) is not None:
tpl_src = 1
else:
stdlib.free(sha1_buf)
if cls_nm is not None:
stdlib.free(cls_nm)
return head
i32_ty: llvmlite.LLVMType | t.CPtr = llvmlite.Int32(pool)
i8_ptr_ty: llvmlite.LLVMType | t.CPtr = llvmlite.Ptr(pool, llvmlite.Int8(pool))
new_head: llvmlite.Value | t.CPtr = None
@@ -276,16 +287,21 @@ class CallGenLib(CallGenBase.CallGenBase):
idx: int = 0
while cur is not None and idx < count:
node_v: llvmlite.Value | t.CPtr = cur
tgt_ty: llvmlite.LLVMType | t.CPtr = VpiReader._resolve_vpi_param_annot_seg(
pool, sha1_buf, cls_nm, meth_nm, idx, i32_ty, i8_ptr_ty)
if tgt_ty is not None and cur.Ty is not None:
tgt_ty: llvmlite.LLVMType | t.CPtr = None
if tpl_src != 0:
tgt_ty = VpiReader._resolve_vpi_param_annot_seg_template(
pool, sha1_buf, cls_nm, meth_nm, idx, i32_ty, i8_ptr_ty)
else:
tgt_ty = VpiReader._resolve_vpi_param_annot_seg(
pool, sha1_buf, cls_nm, meth_nm, idx, i32_ty, i8_ptr_ty)
if tgt_ty is not None and cur.Ty is not None:
# 实参 AST(源符号性判定用):链下标 idx 减 self 偏移即 AST 下标
arg_idx: int = idx - self_off
arg_node: ast.AST | t.CPtr = None
if cargs is not None and arg_idx >= 0 and arg_idx < len(cargs):
arg_node = cargs.get(arg_idx)
node_v = self._coerce_arg_value_to_ty(
pool, builder, cur, tgt_ty, arg_node, trans)
pool, builder, cur, tgt_ty, arg_node, trans, tpl_src)
if new_head is None:
new_head = node_v
else:
@@ -320,11 +336,15 @@ class CallGenLib(CallGenBase.CallGenBase):
val: llvmlite.Value | t.CPtr,
tgt_ty: llvmlite.LLVMType | t.CPtr,
node: ast.AST | t.CPtr = None,
trans: HT.Translator | t.CPtr = None) -> llvmlite.Value | t.CPtr:
trans: HT.Translator | t.CPtr = None,
tpl_src: int = 0) -> llvmlite.Value | t.CPtr:
"""把单个实参 Value 转换到目标形参类型,无法安全转换时原样返回
node/trans:实参 AST 与其翻译器(仅 SSA 整型扩宽的源符号性判定用,
委托 ExprGenBase._zext_or_sext_val;缺省退化为有符号 sext)。
tpl_src:目标类型来自**泛型模板类**注解(特化名精确键遗漏 → 退查模板 +
类型形参替换,见 VpiReader._resolve_vpi_param_annot_seg_template)⇒
指针口径亦可对齐定义侧(同为具体结构体指针但基类型不同时 bitcast)。
"""
if pool is None or builder is None or val is None or tgt_ty is None or val.Ty is None:
return val
@@ -407,6 +427,17 @@ class CallGenLib(CallGenBase.CallGenBase):
builder, val, tgt_ty)
if nv_p is not None:
return nv_p
elif tpl_src != 0:
# 模板注解来源:两侧同为具体结构体指针但基类型不同
# (实参 MemBuddy* vs 定义侧形参 MemManager*,注解
# `memhub.MemManager | t.CPtr`)→ bitcast 使 declare
# 指针口径与 define 一致(位不变、ABI 无影响)。
# 非模板来源保持既有"具体指针原样保留"策略,
# 避免把 list*/Struct* 无谓降级为 i8* 的 IR 噪声。
nv_p2: llvmlite.Value | t.CPtr = llvmlite.build_bitcast(
builder, val, tgt_ty)
if nv_p2 is not None:
return nv_p2
case _:
pass
case _: