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."""
+13 -2
View File
@@ -1506,7 +1506,12 @@ novirtual class AssignGenMixin:
# Duplicate-declaration check (fail fast): re-annotating a module-level name inside a function.
# Legal alternatives: a global declaration plus unannotated assignment, or a differently named local.
if nm.id is not None and trans is not None and self.Gen.VarGen.is_module_scope(trans.SymTab) == 0 and self.Gen.VarGen.lookup_module_var(trans.SymTab, nm.id) is not None:
has_mod_decl_add: int = 0
if nm.id is not None and trans is not None and self.Gen.VarGen.is_module_scope(trans.SymTab) == 0:
if self.Gen.VarGen.lookup_module_var(trans.SymTab, nm.id) is not None: has_mod_decl_add = 1
# 双保险:本文件顶层变量名集合(SymTab.Root.Vars 可能在部分时序看不到)
if has_mod_decl_add == 0 and GenApi.is_file_var(nm.id) != 0: has_mod_decl_add = 1
if has_mod_decl_add != 0:
dup_decl: int = 0
if GenApi.is_global_name(trans, nm.id) != 0 or GenApi.is_nonlocal_name(trans, nm.id) != 0: dup_decl = 1
if dup_decl != 0:
@@ -2320,7 +2325,13 @@ novirtual class AssignGenMixin:
# Block assignment to a module-level global inside a function without a global declaration
if self.Gen.VarGen.is_module_scope(trans.SymTab) == 0:
mod_gv: mlirlite.Value* = self.Gen.VarGen.lookup_module_var(trans.SymTab, nm.id)
if mod_gv is not None: self.Gen.TypeGenErr.fatal_error(target, f"variable '{nm.id}' is a module-level global; assignment inside a function requires a global declaration (otherwise a local variable shadows the global, leaving it null and crashing)", "SyntaxError")
mod_decl_g: int = 0
if mod_gv is not None: mod_decl_g = 1
# 双保险:SymTab.Root.Vars 在部分时序/阶段可能看不到本文件已声明的模块全局
# (现场:某函数对模块全局赋值漏写 global → 静默建局部影子 → 全局仍 NULL →
# 下游 ASan 写 0x208 崩)。按规则此处必须 fail-fast,不得静默建局部。
if mod_decl_g == 0 and GenApi.is_file_var(nm.id) != 0: mod_decl_g = 1
if mod_decl_g != 0: self.Gen.TypeGenErr.fatal_error(target, f"variable '{nm.id}' is a module-level global; assignment inside a function requires a global declaration (otherwise a local variable shadows the global, leaving it null and crashing)", "SyntaxError")
# Ordinary local variable
alloca: mlirlite.Value* = self.Gen.VarGen.get_or_create_sym(trans.SymTab, pool, builder, nm.id, rhs_val.Ty)
+34
View File
@@ -794,6 +794,40 @@ novirtual class CallGenBase:
if annot is None: return None
return VpiReader._resolve_vpi_ret_annot_str(pool, annot, i32_ty, i8_ptr_ty)
def _vpi_escape_packed(self, sha1: str, cls_name: str, method_name: str) -> int:
"""跨模块逃逸摘要查询(v3):按 (定义模块 sha1, Class?, method) 查 .vpi 缓存;0=无摘要。
用"本模块已加载"备忘包住 _ensure_vpi_ret_annot_loaded(其内部是 O(N) 前缀判定):
调用点每次都要查摘要,不做备忘会把编译拖慢一个量级。"""
if sha1 is None or method_name is None: return 0
if VpiReader._vpi_escape_module_loaded(sha1) == 0:
if self.Gen.CallGenBase.TempDir is None or self.Gen.CallGenBase.TempDirLen == 0: return 0
VpiReader._ensure_vpi_ret_annot_loaded(sha1, self.Gen.CallGenBase.TempDir)
VpiReader._vpi_escape_mark_module(sha1)
return VpiReader._lookup_vpi_escape_packed(sha1, cls_name, method_name)
def lookup_escape_via_vpi(self, pool: memhub.MemBuddy*, cls_sha1: str, cls_name: str, method_name: str) -> int:
"""跨模块逃逸摘要(v3):本类 + MRO 祖先各自的模块 sha1 依次查 .vpi,返回打包值(0=无摘要)。
本文件的摘要表按文件重置,跨模块调用点上必为空,只能靠 .vpi 传递;
键与方法路径一致:"Class.method"(方法)/ 裸名(模块级函数,此时 cls_name=None)。"""
if method_name is None: return 0
if cls_sha1 is not None:
p0: int = self._vpi_escape_packed(cls_sha1, cls_name, method_name)
if p0 != 0: return p0
if cls_name is not None:
mro: sizet* = self.Gen.StructGen.get_mro_chain(pool, cls_name)
mi: int = 1
while mro is not None and mro[mi] != 0:
anc: str = rawptr(mro[mi])
if anc is not None and string.strcmp(anc, cls_name) != 0:
sha_a: str = self.Gen.StructGen.get_struct_sha1(anc)
if sha_a is not None:
pa: int = self._vpi_escape_packed(sha_a, anc, method_name)
if pa != 0: return pa
mi += 1
return 0
# _vpi_method_param_annot: on-demand lookup of the vpi param annotation string
def _vpi_method_param_annot(self, pool: memhub.MemBuddy*, cls_sha1: str, cls_name: str, method_name: str) -> str:
"""Load and query the vpi parameter annotation string on demand; None on miss"""
+10
View File
@@ -152,6 +152,11 @@ novirtual class CallGenCall(CallGenBase.CallGenBase):
# `__new__` 不查:其形参与 __init__ 共享且可能被裁剪,位置对应不可靠。
if arg_nodes is not None and string.strcmp(method_name, "__init__") == 0:
esc_mask_ci: int = GenApi.get_param_escapes_method(pool, class_name, method_name)
if esc_mask_ci == 0:
# 跨模块回退(v3):构造器常定义在别的模块(self.f = p 写在那边)
sha_ci: str = self.Gen.StructGen.get_struct_sha1(class_name)
pk_ci: int = self.Gen.CallGenBase.lookup_escape_via_vpi(pool, sha_ci, class_name, method_name)
if pk_ci != 0: esc_mask_ci = int(pk_ci & 4294967295)
if esc_mask_ci != 0:
dummy_ptr_ci: mlirlite.LLVMType* = mlirlite.Ptr(pool, mlirlite.Int8(pool))
can_ci: sizet = len(arg_nodes)
@@ -536,6 +541,11 @@ novirtual class CallGenCall(CallGenBase.CallGenBase):
cargs_mc: list[ast.AST*]* = cl.args
if cargs_mc is not None:
esc_mask_mc: int = GenApi.get_param_escapes_method(pool, class_name, method_name)
if esc_mask_mc == 0:
# 跨模块回退(v3):本文件摘要表按文件重置,跨模块只能查 .vpi 落盘的摘要
sha_mc: str = self.Gen.StructGen.get_struct_sha1(class_name)
pk_mc: int = self.Gen.CallGenBase.lookup_escape_via_vpi(pool, sha_mc, class_name, method_name)
if pk_mc != 0: esc_mask_mc = int(pk_mc & 4294967295)
if esc_mask_mc != 0:
dummy_ptr_mc: mlirlite.LLVMType* = mlirlite.Ptr(pool, mlirlite.Int8(pool))
can_mc: sizet = len(cargs_mc)
+21
View File
@@ -80,6 +80,27 @@ novirtual class CallGenMixin(CallGenBase.CallGenBase):
# 类型——只用于让 life_of_expr 判定"裸局部数组名处于指针位置 = 数组退让 = 栈地址"。
if cargs is not None and func_name is not None:
esc_mask: int = GenApi.get_param_escapes(func_name)
if esc_mask == 0:
# 跨模块回退(v3):本文件摘要表按文件重置,跨模块的被调者只能靠 .vpi 传递。
# 解析被调者所属模块 sha1:带模块前缀(mod.f)用限定名,否则用 from-import 别名。
mod_sha_ec: str = None
if cl.func is not None and cl.func.kind() == ast.ASTKind.Attribute:
at_ec: ast.Attribute* = ast.Attribute*(cl.func)
mod_p_ec: str = None
if at_ec.value is not None and at_ec.value.kind() == ast.ASTKind.Name:
mn_ec: ast.Name* = ast.Name*(at_ec.value)
if mn_ec.id is not None: mod_p_ec = mn_ec.id
elif at_ec.value is not None and at_ec.value.kind() == ast.ASTKind.Attribute:
mod_p_ec = self.Gen.ExprGenAttr._attr_module_path_str(pool, at_ec.value)
if mod_p_ec is not None:
fi_ec: str = None
if trans is not None: fi_ec = trans._from_imports
mod_sha_ec = self.Gen.StructGen._resolve_module_sha1(mod_p_ec, fi_ec)
if mod_sha_ec is None and trans is not None and trans._from_imports is not None:
mod_sha_ec = self._lookup_mod_sha1(pool, trans._from_imports, func_name, 0)
if mod_sha_ec is not None:
pk_ec: int = self.Gen.CallGenBase.lookup_escape_via_vpi(pool, mod_sha_ec, None, func_name)
if pk_ec != 0: esc_mask = int(pk_ec & 4294967295)
if esc_mask != 0:
_dummy_ptr: mlirlite.LLVMType* = mlirlite.Ptr(pool, mlirlite.Int8(pool))
for ai in range(can):
+13 -1
View File
@@ -285,6 +285,12 @@ def is_file_func(name: str) -> int:
return 0
def is_file_var(name: str) -> int:
"""本文件是否顶层声明过该模块级变量名(AnnAssign/Assign 目标);1=是 / 0=否。"""
if _g_gen is not None: return _g_gen.StructGen.is_file_var(name)
return 0
# ---------------- 生命期摘要查询(M2.3 v1b, delegates to StructGenMixin) ----------------
def get_param_escapes(name: str) -> int:
"""形参逃逸掩码(第 k 位=第 k 个形参逃逸);无记录返回 0"""
@@ -299,11 +305,17 @@ def get_ret_borrow(name: str) -> int:
def get_param_escapes_method(pool: memhub.MemBuddy*, class_name: str, method_name: str) -> int:
"""方法形参逃逸掩码:查 "Class.method" 并沿 MRO 回退,最后退裸名键;无记录返回 0"""
"""方法形参逃逸掩码:查 "Class.method" 并沿 MRO 回退;无记录返回 0"""
if _g_gen is not None: return _g_gen.StructGen.get_param_escapes_method(pool, class_name, method_name)
return 0
def compute_escape_summary(pool: memhub.MemBuddy*, fd: ast.AST*, gset: sizet*, box: sizet*) -> int:
"""计算函数逃逸摘要(box[0]=形参逃逸掩码,box[1]=返回借用位);.vpi 生成时落盘用"""
if _g_gen is not None: return _g_gen.StructGen.compute_escape_summary(pool, ast.FunctionDef*(fd), gset, box)
return 1
# ---------------- find_struct_by_type (delegates to StructGenMixin.find_struct_by_type) ----------------
def find_struct_by_type(struct_ty: mlirlite.LLVMType*) -> GenTypes.StructEntry*:
"""Delegate to gen.find_struct_by_type"""
+66 -8
View File
@@ -102,6 +102,10 @@ novirtual class StructGenMixin:
# Top-level FunctionDef name table for this file (see register_file_funcs).
# A stub-injected bare-name declaration (extern/state/export, no SHA1 prefix) that matches a local declaration would take the name first and make the local declaration silently dropped in forward_declare_functions; the injection side (VpiReader._inject_function_declare) skips these names.
FileFuncNames: hashtable.HashTable*
# 本文件顶层**变量名**集合(AnnAssign/Assign 目标)。用于"函数内无 global 写同名模块变量"
# 判据的兜底:该判据原本只查 SymTab.Root.Vars,在部分时序/阶段会看不到已声明的模块全局,
# 于是赋值被静默当成本地影子(现场:漏写 global → 全局仍 NULL → 下游 ASan 写 0x208 崩)。
FileVarNames: hashtable.HashTable*
# --- Name | type index (fast lookup path; falls back to a linear scan when the index is untrusted) ---
StructNameIdx: sizet* # name -> first registration index (u64 slot: 0 = empty, else index+1)
@@ -133,6 +137,7 @@ novirtual class StructGenMixin:
self.FileClassNames = None
self.FileModuleSha1 = None
self.FileFuncNames = None
self.FileVarNames = None
self.EscTable = None
self.EscCount = 0
self.EscScratch = None
@@ -162,6 +167,7 @@ novirtual class StructGenMixin:
self.FileClassNames = None
self.FileModuleSha1 = None
self.FileFuncNames = None
self.FileVarNames = None
self.ModuleClasses = hashtable.HashTable(pool)
if strict != 0:
# Strict mode: create a new HashTable recording visible structs.
@@ -301,6 +307,36 @@ novirtual class StructGenMixin:
if child2.kind() != ast.ASTKind.ClassDef: continue
self._RegisterClassMethods(esc_pool, ast.ClassDef*(child2))
# 本文件顶层变量名集合(供"函数内无 global 写同名模块变量"判据兜底)。
for ci3 in range(cn):
child3: ast.AST* = ch.get(ci3)
if child3 is None: continue
kc3: int = child3.kind()
tlist3: list[ast.AST*]* = None
if kc3 == ast.ASTKind.AnnAssign:
aa3: ast.AnnAssign* = ast.AnnAssign*(child3)
if aa3 is not None and aa3.target is not None:
tlist3 = list[ast.AST*](pool, 1)
if tlist3 is not None: tlist3.append(aa3.target)
elif kc3 == ast.ASTKind.Assign:
asg3: ast.Assign* = ast.Assign*(child3)
if asg3 is not None: tlist3 = asg3.targets
if tlist3 is None: continue
if self.FileVarNames is None: self.FileVarNames = hashtable.HashTable(pool)
if self.FileVarNames is None: continue
nt3: sizet = len(tlist3)
for ti3 in range(nt3):
tn3: ast.AST* = tlist3.get(ti3)
if tn3 is None or tn3.kind() != ast.ASTKind.Name: continue
vnm3: ast.Name* = ast.Name*(tn3)
if vnm3.id is None: continue
if self.FileVarNames.__contains__(vnm3.id) != 0: continue
nb3_len: sizet = string.strlen(vnm3.id)
nb3: str = pool.alloc(nb3_len + 1)
if nb3 is None: continue
string.strcpy(nb3, vnm3.id)
self.FileVarNames.set_int(nb3, 1)
# v2 传递闭包:反复重扫直到掩码不再变化(单调 OR-in 必收敛),上限 8 轮兜底。
# 原来固定 2 轮是近似,>2 层的链会漏;这里改为扫到不动点。
prev_sum: sizet = 0
@@ -317,6 +353,13 @@ novirtual class StructGenMixin:
if name is None or self.FileFuncNames is None: return 0
return self.FileFuncNames.__contains__(name)
def is_file_var(self, name: str) -> int:
"""本文件是否顶层声明过该模块级变量名(AnnAssign/Assign 目标);1=是 / 0=否。
与 SymTab.Root.Vars 双保险:后者在部分时序/阶段会看不到已声明的模块全局。"""
if name is None or self.FileVarNames is None: return 0
return self.FileVarNames.__contains__(name)
# ============================================================
# 生命期摘要(M2.3 v1b):本文件函数 → 形参逃逸掩码 / 返回借用标记
# 覆盖:`return p`、`global G` 下的 `G = p`、`obj.f = p`(含 AnnAssign);
@@ -361,20 +404,35 @@ novirtual class StructGenMixin:
if idx < 0: return 0
return int(self.EscTable[idx * 3 + 2])
def compute_escape_summary(self, pool: memhub.MemBuddy*, fd: ast.FunctionDef*, gset: sizet*, box: sizet*) -> int:
"""计算 fd 的摘要:box[0]=形参逃逸掩码、box[1]=返回借用位(写前清零)。
gset = global 名定长数组([0]=计数,[1..]=名哈希),box 必须在 gset 之后同一块内
(调用方给 gset+65 槽)。gset 传 None 时用本文件的 EscScratch(复用区),两者都没有
才从 pool 现分配。两条路径共用同一实现:① 本文件登记(register_file_funcs);
② .vpi 生成时把摘要落盘(v3 跨模块)。"""
if fd is None or box is None: return 1
box[0] = 0
box[1] = 0
g: sizet* = gset
if g is None: g = self.EscScratch
if g is None:
if pool is None: return 1
g = sizet*(pool.alloc(74 * 8))
if g is None: return 1
g[0] = 0
self._CollectGlobalNames(fd.children, g, 0)
self._ScanEscapes(fd, fd.children, g, box, 0)
return 0
def _RegisterFuncEscapeSummary(self, pool: memhub.MemBuddy*, fd: ast.FunctionDef*, key: str) -> int:
"""扫函数体登记摘要;key 必须是与 EscTable 同池的字符串"""
if fd is None or key is None or self.EscTable is None or self.EscScratch is None: return 1
if self._EscLookup(key) >= 0: return 0
if self.EscCount >= 1024: return 1
# gset 现在是**定长数组**(复用区;[0]=计数,[1..]=名字指针),不再是 HashTable;
# box 也在同一块复用区里(偏移 65 槽 = 520 字节)。
gset: sizet* = self.EscScratch
gset[0] = 0
self._CollectGlobalNames(fd.children, gset, 0)
# gset/box 复用 EscScratch([0]=global 计数,[1..]=名哈希;box 在偏移 65 槽 = 520 字节)
box: sizet* = sizet*(uint64(self.EscScratch) + 520)
box[0] = 0
box[1] = 0
self._ScanEscapes(fd, fd.children, gset, box, 0)
if self.compute_escape_summary(pool, fd, None, box) != 0: return 1
slot: int = self.EscCount
self.EscTable[slot * 3 + 0] = sizet(self._NameHash(key))
self.EscTable[slot * 3 + 1] = box[0]