feat(gen): M2.3 v3 跨模块逃逸摘要 + 全局赋值判据双保险

【v3 跨模块摘要】Converter._PyiFunction 在每个 def 签名行尾写 `# esc=<mask> ret=<ret>`(仅非零时写);VpiReader 新增逃逸摘要缓存(键 {sha1}.{Class}.{method} / {sha1}.{func},值打包 mask|ret<<32),与返回注解缓存同一次加载解析,并加"模块已加载"备忘 + 键前缀过滤避免拖慢编译;StructGenMixin 抽出 compute_escape_summary 供登记与落盘共用;CallGenBase.lookup_escape_via_vpi 按本类 + MRO 祖先模块 sha1 查询,方法/构造器/模块级函数三处调用点在本地表未命中时回退查 vpi。

【修复】Converter._PyiEnsureEscScratch 补 global(漏写导致全局仍 NULL,box 算成 0+520 → ASan WRITE on 0x208);函数内写"本文件已声明的模块全局"且无 global/nonlocal 时必须 fail-fast——原判据只看 SymTab.Root.Vars,部分时序看不到模块全局而静默建局部影子;新增 FileVarNames(register_file_funcs 预扫收集顶层变量名)+ is_file_var() 兜底,普通赋值与注解式重声明两处都接上。
This commit is contained in:
TermiNexus committed 2026-10-08 17:28:06 +08:00
1 parent 9f98e683a9
commit cd68bb62ac
8 files changed
+347 -12

No files matched your search

+31 -1
View File
@@ -418,6 +418,23 @@ def _PyiRejectCReturnDecorator(node: ast.AST*, decorators: list[ast.AST*]*) -> i
# ============================================================
# 逃逸摘要计算用的复用临时区(74 槽:gset[0]=global 计数、gset[1..65)=名哈希、box=gset+65)。
# 只分配一次,避免每个 def 都分配;布局与 StructGen 的 EscScratch 完全一致。
_g_pyi_esc_scratch: sizet* = None
def _PyiEnsureEscScratch() -> int:
"""惰性分配摘要临时区;1=可用 / 0=失败"""
# 必须声明 global:否则赋值会落到**局部**,调用方读到的仍是 NULL 全局,
# box 指针算成 0+520 → ASan: WRITE on 0x208(本文件它处同款写法见 _g_pyi_cdefine_found)。
global _g_pyi_esc_scratch
if _g_pyi_esc_scratch is not None: return 1
_g_pyi_esc_scratch = sizet*(stdlib.malloc(74 * 8))
if _g_pyi_esc_scratch is None: return 0
string.memset(_g_pyi_esc_scratch, 0, 74 * 8)
return 1
# _PyiFunction - emit a function signature (def name(...) -> Ret: pass)
def _PyiFunction(buf: bytes, buf_size: sizet, pos: sizet, fn: ast.FunctionDef*, indent_level: int, class_name: str) -> sizet:
"""Emit a function signature into buf and return the new pos"""
@@ -496,7 +513,20 @@ def _PyiFunction(buf: bytes, buf_size: sizet, pos: sizet, fn: ast.FunctionDef*,
# Without a `->` annotation, @c.CReturn is removed: fail fast to regenerate vpi
_PyiRejectCReturnDecorator(fn, fn.decorator_list)
pos = _PyiEmit(buf, buf_size, pos, ": pass\n")
pos = _PyiEmit(buf, buf_size, pos, ": pass")
# v3 跨模块逃逸摘要:把形参逃逸掩码与返回借用位写成**同一行**的行尾注释,供 VpiReader
# 解析后在别的模块的调用点查询(跨模块摘要表按文件重置,只能靠 vpi 传递)。
# 只在真有逃逸/返回借用时写,保持 vpi 干净:无标记 = 无摘要 = 调用点保守放行。
if _PyiEnsureEscScratch() != 0:
esc_box_c: sizet* = sizet*(uint64(_g_pyi_esc_scratch) + 520)
if GenApi.compute_escape_summary(None, fn, _g_pyi_esc_scratch, esc_box_c) == 0:
if esc_box_c[0] != 0 or esc_box_c[1] != 0:
tmp_c: bytes = stdlib.malloc(64)
if tmp_c is not None:
viperlib.snprintf(tmp_c, 64, " # esc=%d ret=%d", int(esc_box_c[0]), int(esc_box_c[1]))
pos = _PyiEmit(buf, buf_size, pos, tmp_c)
stdlib.free(tmp_c)
pos = _PyiEmit(buf, buf_size, pos, "\n")
return pos
+159
View File
@@ -109,6 +109,48 @@ define VPI_IDX_FNV_OFFSET = 2166136261
define VPI_IDX_FNV_PRIME = 16777619
define VPI_IDX_FNV_MASK = 4294967295
# v3 跨模块逃逸摘要缓存(.vpi 行尾 `# esc=<mask> ret=<ret>`,由 StubGen.Converter._PyiFunction 写出)。
# 键 = "{sha1}.{Class}.{method}"(方法)或 "{sha1}.{func}"(模块级函数);
# 值打包:低 32 位 = 形参逃逸掩码,高 32 位 = 返回借用位。无逃逸/无返回借用的 def 不写条目,
# 因此"查不到"即"无摘要"(调用点保守放行),与本地表口径一致。
define VPI_ESC_MAX = 8192
define VPI_ESC_KEY_LEN = 224
_g_vpi_esc_keys: bytes = None
_g_vpi_esc_vals: sizet* = None
_g_vpi_esc_count: int = 0
# "本模块的 vpi 已加载过"备忘(每项 17 字节 sha1)。调用点每处都要查摘要,
# 若每次都走 _ensure_vpi_ret_annot_loaded 的 O(N) 前缀判定会显著拖慢编译。
define VPI_ESC_MOD_MAX = 512
_g_vpi_esc_mods: bytes = None
_g_vpi_esc_mod_count: int = 0
def _vpi_escape_module_loaded(src_sha1: str) -> int:
"""该模块的 vpi 是否已由逃逸查询路径加载过;1=是 / 0=否"""
if _g_vpi_esc_mods is None or src_sha1 is None: return 0
s0: int = src_sha1[0]
s1: int = src_sha1[1]
i: int = 0
while i < _g_vpi_esc_mod_count:
ent_m: bytes = _g_vpi_esc_mods + sizet(i) * 17
if ent_m[0] == s0 and ent_m[1] == s1 and string.strcmp(ent_m, src_sha1) == 0: return 1
i += 1
return 0
def _vpi_escape_mark_module(src_sha1: str) -> int:
"""记录"该模块的 vpi 已加载";1=成功 / 0=失败"""
global _g_vpi_esc_mods, _g_vpi_esc_mod_count
if src_sha1 is None: return 0
if _g_vpi_esc_mods is None:
_g_vpi_esc_mods = stdlib.malloc(VPI_ESC_MOD_MAX * 17)
if _g_vpi_esc_mods is None: return 0
string.memset(_g_vpi_esc_mods, 0, VPI_ESC_MOD_MAX * 17)
if _vpi_escape_module_loaded(src_sha1) != 0: return 1
if _g_vpi_esc_mod_count >= VPI_ESC_MOD_MAX: return 0
string.strcpy(_g_vpi_esc_mods + sizet(_g_vpi_esc_mod_count) * 17, src_sha1)
_g_vpi_esc_mod_count += 1
return 1
_g_vpi_param_keys: bytes = None
_g_vpi_param_strs: bytes = None
_g_vpi_param_count: int = 0
@@ -823,11 +865,128 @@ def _ensure_vpi_ret_annot_loaded(src_sha1: str, temp_dir: str) -> int:
if bytes_read > 0:
if bytes_read < VPI_READ_BUF_SIZE: vpi_buf[bytes_read] = 0
_cache_vpi_return_annots_text(vpi_buf, src_sha1)
# v3:同一次加载里把行尾逃逸摘要标记也收进缓存(两者共享"本模块是否已加载"的判定)
_cache_vpi_escape_annots_text(vpi_buf, src_sha1)
stdlib.free(vpi_buf)
stdlib.free(vpi_path)
return 1
# _cache_vpi_escape_annots_text - extract the trailing `# esc=<mask> ret=<ret>` markers.
# v3 跨模块逃逸摘要:不依赖 AST,按行扫描(与 _cache_vpi_return_annots_text 同构)。
def _cache_vpi_escape_annots_text(vpi_buf: bytes, src_sha1: str) -> int:
"""抽取 vpi 行尾逃逸摘要标记进缓存;1=处理完成 / 0=缓冲区或分配失败"""
global _g_vpi_esc_keys, _g_vpi_esc_vals, _g_vpi_esc_count
if vpi_buf is None or src_sha1 is None: return 0
if _g_vpi_esc_keys is None:
_g_vpi_esc_keys = stdlib.malloc(VPI_ESC_MAX * VPI_ESC_KEY_LEN)
_g_vpi_esc_vals = sizet*(stdlib.malloc(VPI_ESC_MAX * 8))
if _g_vpi_esc_keys is None or _g_vpi_esc_vals is None: return 0
string.memset(_g_vpi_esc_keys, 0, VPI_ESC_MAX * VPI_ESC_KEY_LEN)
string.memset(_g_vpi_esc_vals, 0, VPI_ESC_MAX * 8)
sha1_len: sizet = string.strlen(src_sha1)
buf_len: sizet = string.strlen(vpi_buf)
cur_cls: bytes = None
pos: sizet = 0
while pos < buf_len:
ls: sizet = pos
le: sizet = pos
while le < buf_len and vpi_buf[le] != '\n': le += 1
i: sizet = ls
while i < le and vpi_buf[i] == ' ': i += 1
if i < le:
# class X: -> 记录当前类上下文(与方法键一致)
if le - i >= 6 and string.strncmp(vpi_buf + i, "class ", 6) == 0:
cstart: sizet = i + 6
cend: sizet = cstart
while cend < le and vpi_buf[cend] != ':' and vpi_buf[cend] != ' ' and vpi_buf[cend] != '(': cend += 1
if cend > cstart:
if cur_cls is not None: stdlib.free(cur_cls)
cur_cls = stdlib.malloc(cend - cstart + 1)
if cur_cls is not None:
string.strncpy(cur_cls, vpi_buf + cstart, cend - cstart)
cur_cls[cend - cstart] = '\0'
# def name(...) ... # esc=<mask> ret=<ret>
elif le - i >= 4 and string.strncmp(vpi_buf + i, "def ", 4) == 0:
# 模块级函数(无缩进)清掉类上下文,避免上一类泄漏
if i == ls and cur_cls is not None:
stdlib.free(cur_cls)
cur_cls = None
# 找同行 "# esc=" 标记
mark: sizet = le
j: sizet = i
while j + 6 <= le:
if vpi_buf[j] == '#' and vpi_buf[j + 1] == ' ' and string.strncmp(vpi_buf + j + 2, "esc=", 4) == 0:
mark = j
break
j += 1
if mark < le:
kp: sizet = mark + 6
esc_v: int = 0
while kp < le and vpi_buf[kp] >= '0' and vpi_buf[kp] <= '9':
esc_v = esc_v * 10 + (int(vpi_buf[kp]) - 48)
kp += 1
ret_v: int = 0
while kp < le and vpi_buf[kp] != 'e': kp += 1
if kp + 4 <= le:
kp += 4
while kp < le and vpi_buf[kp] >= '0' and vpi_buf[kp] <= '9':
ret_v = ret_v * 10 + (int(vpi_buf[kp]) - 48)
kp += 1
if (esc_v != 0 or ret_v != 0) and _g_vpi_esc_count < VPI_ESC_MAX:
dstart: sizet = i + 4
dend: sizet = dstart
while dend < le and vpi_buf[dend] != '(': dend += 1
if dend > dstart:
mname_len: sizet = dend - dstart
cls_len_e: sizet = 0
if cur_cls is not None: cls_len_e = string.strlen(cur_cls)
key_len_e: sizet = sha1_len + 1 + cls_len_e + 1 + mname_len
if key_len_e < VPI_ESC_KEY_LEN:
mname_buf_e: bytes = stdlib.malloc(mname_len + 1)
key_buf_e: bytes = stdlib.malloc(key_len_e + 1)
if mname_buf_e is not None and key_buf_e is not None:
string.strncpy(mname_buf_e, vpi_buf + dstart, mname_len)
mname_buf_e[mname_len] = '\0'
if cur_cls is not None: viperlib.snprintf(key_buf_e, key_len_e + 1, "%s.%s.%s", src_sha1, cur_cls, mname_buf_e)
else: viperlib.snprintf(key_buf_e, key_len_e + 1, "%s.%s", src_sha1, mname_buf_e)
string.strcpy(_g_vpi_esc_keys + sizet(_g_vpi_esc_count) * VPI_ESC_KEY_LEN, key_buf_e)
_g_vpi_esc_vals[_g_vpi_esc_count] = sizet(esc_v) | (sizet(ret_v) << 32)
_g_vpi_esc_count += 1
if mname_buf_e is not None: stdlib.free(mname_buf_e)
if key_buf_e is not None: stdlib.free(key_buf_e)
pos = le + 1
if cur_cls is not None: stdlib.free(cur_cls)
return 1
# _lookup_vpi_escape_packed - query the cross-module escape summary cache (v3).
def _lookup_vpi_escape_packed(src_sha1: str, cls_name: str, method_name: str) -> int:
"""查跨模块逃逸摘要;返回打包值(低32位 mask | ret<<32),0 = 无摘要"""
if _g_vpi_esc_keys is None or _g_vpi_esc_count == 0: return 0
if src_sha1 is None or method_name is None: return 0
sha1_len: sizet = string.strlen(src_sha1)
cl_len: sizet = 0
if cls_name is not None: cl_len = string.strlen(cls_name)
me_len: sizet = string.strlen(method_name)
key_len: sizet = sha1_len + 1 + cl_len + 1 + me_len
kb: bytes = stdlib.malloc(key_len + 1)
if kb is None: return 0
if cls_name is not None: viperlib.snprintf(kb, key_len + 1, "%s.%s.%s", src_sha1, cls_name, method_name)
else: viperlib.snprintf(kb, key_len + 1, "%s.%s", src_sha1, method_name)
res: int = 0
idx: int = 0
while idx < _g_vpi_esc_count:
ent: bytes = _g_vpi_esc_keys + sizet(idx) * VPI_ESC_KEY_LEN
# 键以模块 sha1 打头(前 2 个十六进制字符足以筛掉绝大多数候选,避免逐项 strcmp)
if ent[0] == kb[0] and ent[1] == kb[1] and string.strcmp(ent, kb) == 0:
res = int(_g_vpi_esc_vals[idx])
break
idx += 1
stdlib.free(kb)
return res
# _vpi_param_annot_seg_is - equality check for a param-annotation segment (for the "str/bytes is not a generic pointer" rule).
def _vpi_param_annot_seg_is(src_sha1: str, cls_name: str, method_name: str, seg_idx: int, want: str) -> int:
"""Check whether the definition-side param-annotation segment equals want exactly; 1=yes / 0=no."""