fix(stub): 包 __init__ 判定改按模块名,修 ast.ClassDef 票名前缀分裂

存量缺口:VpiReader 判定"本 stub 是否包 __init__"依赖 sha1→rel_path 反查
(strstr(rel_path, "__init__.vp"))。该反查在部分运行形态下取不到条目 ⇒ 判定恒
false ⇒ 包重导出类的别名 entry 静默不登记 ⇒ `ast.ClassDef` 这类限定名在泛型实参
里保留包前缀(幽灵名 `list[1abcd5277fa442de.ClassDef*]` 全产物恒 opaque),与构造
函数侧的真定义模块前缀(`list[e70bd9b4487d921d.ClassDef*]`)分裂成两个类型,报
「指针隐式转指针已禁止」(现场 ClassGenGeneric.vp:92 的 CastError)。

改法(不再依赖 rel_path 字段,均为模块名表的既有事实):
- StubMergerStore 新增 LookupModNameBySha1:按 SHA1 反查模块名(只读指针)。
- VpiReader 的 ImportFrom 分支:取 level>0 的兄弟模块 S,用其模块名去掉末段得到包
  前缀 P;又因 Utils.PathToModuleName 把 pkg/__init__.vp 记成 "pkg",故
  「本 stub 的模块名 == P」即证明本 stub 就是包 P 的 __init__ ⇒ is_pkg_init_rt=1,
  同时直接用 P 拼出子模块全名(不再从 rel_path 反推目录名)。
- 模块名证据不足时仍回退原 rel_path 路线,行为不变。
This commit is contained in:
TermiNexus committed 2026-10-02 00:07:23 +08:00
1 parent b91dfef777
commit 9994d2e983
2 files changed
+111 -24

No files matched your search

+80 -24
View File
@@ -2155,23 +2155,69 @@ def _ParseVpiIntoModuleImpl(pool: memhub.MemBuddy*,
elif sk == ast.ASTKind.ImportFrom:
sifr: ast.ImportFrom* = ast.ImportFrom*(schild)
sifr_mod: str = sifr.module
# 相对导入补包前缀:vpi 里 `from .base import X` 只存 module="base"(前导点记在 level),若按字面 "base" 精确查找则查不到 "ast.base" ⇒ 裸类名(AST 等)解析失败 ⇒ rawptr[AST] 等注解 fail-fast。
# 故按 stub 自身相对路径还原包名(ast/parser.vp → "ast"),level==1 时拼成完整模块名 "ast.base" 再登记(level>1 保持原样,避免未确认路径形态时做过深上溯)。
# 相对导入补包前缀 + 包 __init__ 判定(两者**都不再依赖 rel_path 表**)。
#
# vpi 里 `from .base import X` 只存 module="base"(前导点记在 level),按字面
# "base" 精确查找查不到 "ast.base" ⇒ 裸类名(AST 等)解析失败。包前缀的
# 权威来源是**模块名表**:兄弟模块 S 的全名 = "<包前缀>.S"(如 "ast.stmts")
# ⇒ 包前缀 = S 的模块名去掉最后一段;而 Utils.PathToModuleName 把
# `pkg/__init__.vp` 记成 "pkg"(**不带** .__init__,见其 :150 文档)
# ⇒ "本 stub 的模块名 == 该包前缀"即证明**本 stub 就是该包的 __init__**,
# 这正是重导出别名 entry 的登记条件。
# 为什么要脱离 rel_path:rel_path 反查在部分运行形态下取不到条目 ⇒
# 原 `strstr(rel_path,"__init__.vp")` 判定恒 false ⇒ 别名静默不登记 ⇒
# `ast.ClassDef` 这类**限定名在泛型实参里保留包前缀**,与构造函数侧(真
# 定义模块前缀)分裂成两个类型(现场 ClassGenGeneric.vp:92 的 CastError;
# 幽灵名 `list[1abcd5277fa442de.ClassDef*]` 在全部产物里恒为 opaque)。
# 取不到模块名证据时回退旧的 rel_path 路线,行为不变。
pkg_prefix_rt: str = None
if sifr.level > 0 and sifr_mod is not None and sifr_mod[0] != '\0':
_sib_sha_rt: str = StubMergerStore.LookupSha1ByModNameSuffix(sifr_mod)
if _sib_sha_rt is not None:
_sub_mod_rt: str = StubMergerStore.LookupModNameBySha1(_sib_sha_rt)
if _sub_mod_rt is not None:
_dot_rt: str = string.strrchr(_sub_mod_rt, '.')
if _dot_rt is not None:
_pfx_len_rt: sizet = sizet(uint64(_dot_rt) - uint64(_sub_mod_rt))
if _pfx_len_rt > 0:
_my_mod_rt: str = StubMergerStore.LookupModNameBySha1(src_sha1)
# 确认:本 stub 的模块名必须**恰好等于**该前缀(= 本 stub
# 就是包 __init__)。后缀命中可能落到同后缀的别的模块,
# 故必须用本 stub 自己的模块名确认,不确认就让位给 rel_path。
_conf_rt: int = 0
if _my_mod_rt is not None and string.strlen(_my_mod_rt) == _pfx_len_rt:
if string.strncmp(_my_mod_rt, _sub_mod_rt, _pfx_len_rt) == 0:
_conf_rt = 1
if _conf_rt != 0:
pkg_prefix_rt = pool.alloc(_pfx_len_rt + 1)
if pkg_prefix_rt is not None:
string.strncpy(pkg_prefix_rt, _sub_mod_rt, _pfx_len_rt)
pkg_prefix_rt[_pfx_len_rt] = '\0'
sifr_mod_q: str = sifr_mod
if sifr_mod is not None and sifr_mod[0] != '\0' and sifr.level == 1:
rel_p_r: str = StubMergerStore.LookupSha1RelPath(src_sha1)
if rel_p_r is not None:
slash_r: str = string.strrchr(rel_p_r, '/')
if slash_r is not None:
pkg_len_r: sizet = string.strlen(rel_p_r) - string.strlen(slash_r)
if pkg_len_r > 0:
qtotal_r: sizet = pkg_len_r + 1 + string.strlen(sifr_mod) + 1
qbuf_r: str = pool.alloc(qtotal_r)
if qbuf_r is not None:
string.strncpy(qbuf_r, rel_p_r, pkg_len_r)
qbuf_r[pkg_len_r] = '.'
string.strcpy(qbuf_r + pkg_len_r + 1, sifr_mod)
sifr_mod_q = qbuf_r
if pkg_prefix_rt is not None:
qtotal_p: sizet = string.strlen(pkg_prefix_rt) + 1 + string.strlen(sifr_mod) + 1
qbuf_p: str = pool.alloc(qtotal_p)
if qbuf_p is not None:
qbuf_p[0] = '\0'
string.strcat(qbuf_p, pkg_prefix_rt)
string.strcat(qbuf_p, ".")
string.strcat(qbuf_p, sifr_mod)
sifr_mod_q = qbuf_p
else:
rel_p_r: str = StubMergerStore.LookupSha1RelPath(src_sha1)
if rel_p_r is not None:
slash_r: str = string.strrchr(rel_p_r, '/')
if slash_r is not None:
pkg_len_r: sizet = string.strlen(rel_p_r) - string.strlen(slash_r)
if pkg_len_r > 0:
qtotal_r: sizet = pkg_len_r + 1 + string.strlen(sifr_mod) + 1
qbuf_r: str = pool.alloc(qtotal_r)
if qbuf_r is not None:
string.strncpy(qbuf_r, rel_p_r, pkg_len_r)
qbuf_r[pkg_len_r] = '.'
string.strcpy(qbuf_r + pkg_len_r + 1, sifr_mod)
sifr_mod_q = qbuf_r
if sifr_mod is not None:
stub_imported_modules = _stub_add_imported(pool, stub_imported_modules, sifr_mod_q)
# 方案A:包 __init__.vpi 的重导出类以包 sha1 注册别名 entry
@@ -2179,16 +2225,26 @@ def _ParseVpiIntoModuleImpl(pool: memhub.MemBuddy*,
# sha1 匹配,注册端按定义文件 sha1 → name+sha1 错位 fail-fast。
# 仅 __init__ 触发,普通模块 vpi 的跨模块 from-import 不注册,
# 避免把类错误归属到当前模块 sha1)
# 包 __init__ 判定(**首选不依赖 rel_path 表**):pkg_prefix_rt 非空
# 即上面的模块名证据成立——"本 stub 的模块名 == 兄弟模块名去掉末段"
# ⇒ 本 stub 就是该包的 __init__(同一事实,不重复计算)。
# rel_path 反查取不到条目时原判定恒 false ⇒ 重导出别名静默不登记 ⇒
# `ast.ClassDef` 这类限定名在泛型实参里保留**包前缀**,与定义模块
# 前缀分裂(现场 ClassGenGeneric.vp:92 的 CastError)。
# 仅当模块名证据不足时才回退旧 rel_path 判定(行为不变)。
is_pkg_init_rt: int = 0
rel_path_rt: str = StubMergerStore.LookupSha1RelPath(src_sha1)
if rel_path_rt is not None:
# [FIX] 原判定 strcmp(rel_path + strlen(rel_path) - 9, "__init__.vp")
# 假设 "__init__.vp" 从 len-9 开始;实际包路径 "ast/__init__.vp"
# 长 15,该后缀从索引 4 起(11 字符),len-9=6 指向 "init__.vp"
# → 判定恒 false → 包重导出永不触发(ast.Attribute 未注册根因之一)。
# 改用 strstr 判定 rel_path 包含 "__init__.vp"(仅包 __init__ 文件命中)。
if string.strstr(rel_path_rt, "__init__.vp") is not None:
is_pkg_init_rt = 1
if pkg_prefix_rt is not None:
is_pkg_init_rt = 1
if is_pkg_init_rt == 0:
rel_path_rt: str = StubMergerStore.LookupSha1RelPath(src_sha1)
if rel_path_rt is not None:
# [FIX] 原判定 strcmp(rel_path + strlen(rel_path) - 9, "__init__.vp")
# 假设 "__init__.vp" 从 len-9 开始;实际包路径 "ast/__init__.vp"
# 长 15,该后缀从索引 4 起(11 字符),len-9=6 指向 "init__.vp"
# → 判定恒 false → 包重导出永不触发(ast.Attribute 未注册根因之一)。
# 改用 strstr 判定 rel_path 包含 "__init__.vp"(仅包 __init__ 文件命中)。
if string.strstr(rel_path_rt, "__init__.vp") is not None:
is_pkg_init_rt = 1
# 解析被 from-import 的模块 sha1:相对导入按 parser 表示判定——
# 前导点存于 level(from .exprs import → level=1, module="exprs",
# 见 includes/ast/stmts.vp ImportFrom.level / parser.vp _parse_from_import),
@@ -620,3 +620,34 @@ def LookupSha1RelPath(sha1: str) -> str:
ridx: sizet = sizet(i) * StubMerger.MAX_REL_PATH_LEN
return StubMerger._sha1_store_rel + ridx
return None
# ============================================================
# LookupModNameBySha1 - 通过 SHA1 反查模块名
#
# 用途:判定"本 stub 是否某包的 __init__"(模块名以 ".__init__" 结尾)——该事实
# **不依赖 sha1→rel_path 字段**:rel_path 反查在部分运行形态下拿不到条目 ⇒
# VpiReader 原来的 `strstr(rel_path, "__init__.vp")` 判定恒 false ⇒ 包重导出类的
# 别名 entry 静默不登记 ⇒ `ast.ClassDef` 这类**限定名在泛型实参里保留包前缀**,
# 与构造函数侧(真定义模块前缀)分裂成两个类型(现场 ClassGenGeneric.vp:92 的
# CastError;幽灵名 `list[1abcd5277fa442de.ClassDef*]` 全产物 opaque)。
# 模块名与 rel_path 是同一次登记写入的(PopulateSha1MapStore /
# AppendToSha1MapStore),但模块名可用即足以作出判定与取包前缀。
#
# 同一 SHA1 可能有多条(App 源文件按全路径模块名与短模块名各登记一次):返回
# **第一条命中**;各条的包前缀一致,调用方只取"去 .__init__ 后的前缀"。
# Args: sha1 SHA1 字符串(16 字符)
# Returns: 模块名字符串指针(只读,不要 free),None=未找到
# ============================================================
def LookupModNameBySha1(sha1: str) -> str:
"""通过 SHA1 查找模块名,返回只读指针,None=未找到"""
if sha1 is None or sha1[0] == '\0':
return None
if StubMerger._sha1_store_arr is None or StubMerger._sha1_store_mod is None:
return None
for i in range(StubMerger._sha1_store_count):
sidx_mb: sizet = sizet(i) * 17
if string.strcmp(StubMerger._sha1_store_arr + sidx_mb, sha1) == 0:
midx_mb: sizet = sizet(i) * 64
return StubMerger._sha1_store_mod + midx_mb
return None