fix(gen): 类归属不采信星号导入候选,收口 fatal_error 返回类型与默认值补全

TypeGenResolve._class_true_def_sha1 / CallGenBase._lookup_reexport_from_vpi 新增 allow_star 参数:类归属只采信具名 re-export。此前 `from X import *` 候选被当成类的真实定义模块,而几乎每个文件都 star-import stdint,导致 TypeGenErr 前向声明被建成 "stdint.TypeGenErr";该名字在 StructTable 里只剩一个错误 sha1 的条目,fatal_error 的跨模块返回类型解析全部落空并 fail-fast。

TypeGenErr.fatal_error/fatal_type_error 显式 -> int(留空会解析出退化类型 i0,命中 CallGenCall 的 i0 守卫);_fail_method_ret_ty 增加失败上下文转储并并入同一条 VLogger.error(批处理下 error 即 sys.exit,其后代码全是死代码)。

CallGenMixin 模块级调用:默认值已知但重建不出时 fail-fast,不再静默产出操作数不足的 llvm.call(只会被 MLIR 校验拦下);AssignGenMixin 两处 VLogger.error 补上 category 实参。

M2.3 v1b 收尾:逃逸摘要表与扫描临时区改为只分配一次、传递闭包扫到不动点、u64 改 sizet;VpiReader 不再 free 跨阶段引用的 src_sha1(heap-use-after-free)。
This commit is contained in:
TermiNexus committed 2026-10-08 16:38:35 +08:00
1 parent 1fb0f4a4ea
commit b03e55bb7e
9 files changed
+183 -49

No files matched your search

+5 -2
View File
@@ -1920,8 +1920,11 @@ def _ParseVpiIntoModuleImpl(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, v
# Import/ImportFrom -> skip (nothing to process) # Import/ImportFrom -> skip (nothing to process)
# free the SHA1 buffer (allocated by _extract_sha1_from_path via stdlib.malloc) # 不要 free src_sha1:它会被后续阶段继续引用(模块/类型注册按 `<sha1>.Name` 命名,
if src_sha1 is not None: stdlib.free(src_sha1) # 之后 resolve_annotation_type 仍会对它 strcmp)——在此 free 会造成 heap-use-after-free
# (现场:alloc @ _extract_sha1_from_path → free @ 本函数 → 读 @ resolve_annotation_type:2065)。
# 跨阶段字符串按本仓惯例常驻;若将来确需回收,应改为在**存入注册表处**拷贝一份到 PermPool,
# 而不是在解析结束时释放。
# free vpi_buf only after all tree processing (see the parsing comment) # free vpi_buf only after all tree processing (see the parsing comment)
stdlib.free(vpi_buf) stdlib.free(vpi_buf)
return 1 return 1
+4 -2
View File
@@ -2011,8 +2011,10 @@ novirtual class AssignGenMixin:
if rel_path is not None: VLogger.error(f"assignment rhs is None: {rel_path}:{val_line} call '{func_nm}(...)' returned None", "ASGN") if rel_path is not None: VLogger.error(f"assignment rhs is None: {rel_path}:{val_line} call '{func_nm}(...)' returned None", "ASGN")
else: VLogger.error(f"assignment rhs is None: sha1={sha1}:{val_line} call '{func_nm}(...)' returned None", "ASGN") else: VLogger.error(f"assignment rhs is None: sha1={sha1}:{val_line} call '{func_nm}(...)' returned None", "ASGN")
else: else:
if rel_path is not None: VLogger.error(f"assignment rhs is None: {rel_path}:{val_line} [kind={val_kind}]") # 必须与相邻分支一致显式传 category:跨模块模块级函数调用目前不会补默认值,
else: VLogger.error(f"assignment rhs is None: sha1={sha1}:{val_line} [kind={val_kind}]") # 漏传会生成操作数不足的 llvm.call(MLIR 校验直接失败)。
if rel_path is not None: VLogger.error(f"assignment rhs is None: {rel_path}:{val_line} [kind={val_kind}]", "ASGN")
else: VLogger.error(f"assignment rhs is None: sha1={sha1}:{val_line} [kind={val_kind}]", "ASGN")
else: VLogger.error("assignment rhs is None (asgn.value is empty)", "ASGN") else: VLogger.error("assignment rhs is None (asgn.value is empty)", "ASGN")
return 0 return 0
+67 -4
View File
@@ -294,8 +294,12 @@ novirtual class CallGenBase:
return None return None
def _lookup_reexport_from_vpi(self, pool: memhub.MemBuddy*, mod_sha1: str, func_name: str) -> str: def _lookup_reexport_from_vpi(self, pool: memhub.MemBuddy*, mod_sha1: str, func_name: str, allow_star: int = 1) -> str:
"""On a re-export table miss, parse the module's .vpi from-import statements to locate the defining module (the table may be empty since it is only populated during full translation).""" """On a re-export table miss, parse the module's .vpi from-import statements to locate the defining module (the table may be empty since it is only populated during full translation).
allow_star=0 skips `from X import *` candidates: they are unverified hints (the caller must
check them), and a CLASS owner must never be taken from one -- nearly every file star-imports
stdint, so accepting one would attribute arbitrary class names to stdint (TypeGenErr was
forward-declared as "stdint.TypeGenErr" this way, then every vpi/text.ll lookup missed)."""
if pool is None or mod_sha1 is None or func_name is None: return None if pool is None or mod_sha1 is None or func_name is None: return None
# accumulate star-import candidates (space-separated); caller validates each # accumulate star-import candidates (space-separated); caller validates each
star_cands: str = None star_cands: str = None
@@ -362,6 +366,8 @@ novirtual class CallGenBase:
nm_start: sizet = imp_pos + 8 nm_start: sizet = imp_pos + 8
# star import (from X import *): collect all candidates; caller verifies # star import (from X import *): collect all candidates; caller verifies
if nm_start < ls + ll and buf[nm_start] == '*': if nm_start < ls + ll and buf[nm_start] == '*':
# Class-owner queries opt out: a star target is not evidence that it owns the name.
if allow_star == 0: continue
star_dot: int = 0 star_dot: int = 0
if buf[mod_start] == '.': star_dot = 1 if buf[mod_start] == '.': star_dot = 1
star_buf: str = pool.alloc(mod_end - mod_start + 1) star_buf: str = pool.alloc(mod_end - mod_start + 1)
@@ -821,9 +827,66 @@ novirtual class CallGenBase:
# _fail_method_ret_ty: ERROR + abort when the method return type cannot be resolved # _fail_method_ret_ty: ERROR + abort when the method return type cannot be resolved
# ============================================================ # ============================================================
def _fail_method_ret_ty(self, method_name: str, node: ast.AST* = None) -> mlirlite.LLVMType*: def _fail_method_ret_ty(self, method_name: str, node: ast.AST* = None, cls: str = None, anc: str = None, sha1: str = None) -> mlirlite.LLVMType*:
"""Fail fast when a method return type cannot be resolved precisely (module function, text.ll define, then vpi cache); never default to i32.""" """Fail fast when a method return type cannot be resolved precisely (module function, text.ll define, then vpi cache); never default to i32."""
VLogger.error(f"method return type cannot be obtained precisely (function/text.ll/vpi stub all missed): {method_name}", "HandlesExprCall") # NOTE: VLogger.error terminates the process in batch mode (sys.exit(1)), so the whole
# report must be a SINGLE call -- anything after it is dead code (this is why a separate
# diagnostic line never showed up, and why the following fatal_error traceback is never
# printed either). The context dump is therefore merged into this one message.
# Reports: receiver class, its registered struct sha1, the class/sha1 actually used for
# the lookup (MRO fallback candidate), whether that module's text.ll exists, and the raw
# vpi return-annotation string if the cache holds one.
c_cls: str = cls
if c_cls is None: c_cls = "?"
c_anc: str = anc
if c_anc is None: c_anc = "?"
# Probe with the sha1 actually used for the lookup, falling back to the receiver class's
# own registered sha1 when the caller passed none (so the dump is informative everywhere).
probe_sha: str = sha1
if probe_sha is None and anc is not None: probe_sha = self.Gen.StructGen.get_struct_sha1(anc)
if probe_sha is None and cls is not None: probe_sha = self.Gen.StructGen.get_struct_sha1(cls)
c_sha: str = probe_sha
if c_sha is None: c_sha = "?"
c_st_sha: str = "?"
if cls is not None:
st_sha_d: str = self.Gen.StructGen.get_struct_sha1(cls)
if st_sha_d is not None: c_st_sha = st_sha_d
c_tll: int = 0
c_vpi: str = "-"
if probe_sha is not None:
c_tll = self._dep_text_ll_exists(None, probe_sha)
if self.Gen.CallGenBase.TempDir is not None and self.Gen.CallGenBase.TempDirLen > 0:
VpiReader._ensure_vpi_ret_annot_loaded(probe_sha, self.Gen.CallGenBase.TempDir)
annot_d: str = VpiReader._lookup_vpi_ret_annot(probe_sha, cls, method_name)
if annot_d is not None: c_vpi = annot_d
# Same-named struct entries: find_struct returns the FIRST exact match, so an earlier bogus
# registration (alias/forward/stale, different ModuleSha1) silently shadows the real one.
dup_n_d: int = 0
dup_last_d: str = "-"
# The entry's LLVM type name embeds "{sha1}.{Class}", i.e. the sha1 used when the entry
# was first created (registration / forward-declare / stub injection) -- it reveals
# provenance even if a later path overwrote ModuleSha1.
dup_ty_nm_d: str = "-"
sc_n_d: int = self.Gen.StructGen.get_struct_count()
sc_i_d: int = 0
while sc_i_d < sc_n_d:
e_d: GenTypes.StructEntry* = self.Gen.StructGen._get_struct_entry(sc_i_d)
if e_d is not None and e_d.Name is not None:
if string.strcmp(e_d.Name, c_cls) == 0:
dup_n_d += 1
if e_d.ModuleSha1 is not None: dup_last_d = e_d.ModuleSha1
if e_d.Ty is not None:
nm_d: str = mlirlite.get_struct_name(e_d.Ty)
if nm_d is not None: dup_ty_nm_d = nm_d
sc_i_d += 1
# Current-file / defining-module sha1 at the moment of failure: distinguishes
# "wrong sha1 captured during (vpi injection) forward-declare" from "correct sha1
# overwritten later".
cur_file_d: str = self.Gen.StructGen.get_file_module_sha1()
if cur_file_d is None: cur_file_d = "-"
cur_def_d: str = self.Gen.StructGen.CurDefModuleSha1
if cur_def_d is None: cur_def_d = "-"
VLogger.error(f"method return type cannot be obtained precisely (function/text.ll/vpi stub all missed): {method_name} | [diag] cls={c_cls} struct_sha1={c_st_sha} lookup_anc={c_anc} lookup_sha1={c_sha} text_ll={c_tll} vpi_annot={c_vpi} same_name_entries={dup_n_d} last_same_sha1={dup_last_d} entry_ty={dup_ty_nm_d} cur_file_sha1={cur_file_d} cur_def_sha1={cur_def_d}", "HandlesExprCall")
self.Gen.TypeGenErr.fatal_error(node, f"method return type cannot be obtained precisely; defaulting to i32 would truncate pointers/i64: {method_name}", "TypeError") self.Gen.TypeGenErr.fatal_error(node, f"method return type cannot be obtained precisely; defaulting to i32 would truncate pointers/i64: {method_name}", "TypeError")
return None return None
+3 -3
View File
@@ -489,7 +489,7 @@ novirtual class CallGenCall(CallGenBase.CallGenBase):
if bits == 0: cd_i0 = 1 if bits == 0: cd_i0 = 1
case _: case _:
pass pass
if cd_i0 != 0: call_ret_ty = self._fail_method_ret_ty(method_name, node) if cd_i0 != 0: call_ret_ty = self._fail_method_ret_ty(method_name, node, class_name)
# Cross-module call (found_func is None): create a declare to avoid undefined value. # Cross-module call (found_func is None): create a declare to avoid undefined value.
if found_func is None: if found_func is None:
# Coerce args to the definition-side params before building the declare; # Coerce args to the definition-side params before building the declare;
@@ -836,7 +836,7 @@ novirtual class CallGenCall(CallGenBase.CallGenBase):
# Dependency not ready: keep default i32 (build_call(Void) # Dependency not ready: keep default i32 (build_call(Void)
# returns None and breaks expression translation). # returns None and breaks expression translation).
call_ret_ty = mlirlite.Int32(pool) call_ret_ty = mlirlite.Int32(pool)
else: call_ret_ty = self._fail_method_ret_ty(method_name, cl) else: call_ret_ty = self._fail_method_ret_ty(method_name, cl, class_name, ret_anc_name, ret_anc_sha1)
# found_func missed but the return type resolved: build the cross-module # found_func missed but the return type resolved: build the cross-module
# call name from the defining class to avoid undefined references. # call name from the defining class to avoid undefined references.
@@ -1559,7 +1559,7 @@ novirtual class CallGenCall(CallGenBase.CallGenBase):
# Phase A not ready (anc_sha1_sp None / defining module text.ll # Phase A not ready (anc_sha1_sp None / defining module text.ll
# missing): keep default i32 (build_call(Void) breaks expressions). # missing): keep default i32 (build_call(Void) breaks expressions).
if anc_sha1_sp is None or self._dep_text_ll_exists(pool, anc_sha1_sp) == 0: ret_ty_sp = mlirlite.Int32(pool) if anc_sha1_sp is None or self._dep_text_ll_exists(pool, anc_sha1_sp) == 0: ret_ty_sp = mlirlite.Int32(pool)
else: self._fail_method_ret_ty(method_name, cl) else: self._fail_method_ret_ty(method_name, cl, cur_class, anc, anc_sha1_sp)
# 7. Cross-module: create the declare; emit the call. # 7. Cross-module: create the declare; emit the call.
# Value-reference (&T / &temp T) params: auto-address the parent-method args (self is slot 0). # Value-reference (&T / &temp T) params: auto-address the parent-method args (self is slot 0).
head = self._apply_auto_addr_to_call_args(builder, pool, mod, trans, call_name, cargs_sp, head, arg_count_sp, 1) head = self._apply_auto_addr_to_call_args(builder, pool, mod, trans, call_name, cargs_sp, head, arg_count_sp, 1)
+6
View File
@@ -940,6 +940,12 @@ novirtual class CallGenMixin(CallGenBase.CallGenBase):
# Appended default args must increment can: the downstream coerce/declare steps rebuild # Appended default args must increment can: the downstream coerce/declare steps rebuild
# the chain bounded by can, so not counting them truncates the trailing defaults (json.write OOM root cause). # the chain bounded by can, so not counting them truncates the trailing defaults (json.write OOM root cause).
can += 1 can += 1
# fail-fast: this callee is known to have defaults, but they could not be rebuilt into
# values (snapshot missing / not a simple constant). Emitting fewer operands than the
# callee declares yields invalid IR that only the MLIR verifier catches
# ("'llvm.call' op incorrect number of operands (N) for callee"); stop here instead.
if can < df_param_count:
self.Gen.TypeGenErr.fatal_error(cl, f"Cross-module default argument cannot be rebuilt into a value (snapshot missing, or the default is not a simple constant): {func_name}; parameter {can + 1} would be missing, which emits an llvm.call with fewer operands than the callee declares", "TypeError")
elif found_func is not None: elif found_func is not None:
# Zero-value fallback when defaults are missing: use found_func's real param count (excluding EH out-params %__eh_msg_out__/%__eh_code_out__) and fill missing args with zeros. # Zero-value fallback when defaults are missing: use found_func's real param count (excluding EH out-params %__eh_msg_out__/%__eh_code_out__) and fill missing args with zeros.
fc_cnt: int = 0 fc_cnt: int = 0
+1 -1
View File
@@ -72,7 +72,7 @@ novirtual class CallGenProp(CallGenBase.CallGenBase):
# Phase A deps not ready (cls_sha1_rt None / the defining module's text.ll missing): # 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). # 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) 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) 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 # 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). # coercion (self/obj or the classmethod cls occupies index 0, skipped via start_idx).
+79 -32
View File
@@ -114,7 +114,10 @@ novirtual class StructGenMixin:
# VarEntry/FieldEntry 的 "Appended at the end so existing field offsets are unchanged")。 # VarEntry/FieldEntry 的 "Appended at the end so existing field offsets are unchanged")。
# 池数组 + 线性查找;每项 3 槽:name(池上 str 地址) | mask(第 k 位=第 k 形参逃逸) | ret_borrow # 池数组 + 线性查找;每项 3 槽:name(池上 str 地址) | mask(第 k 位=第 k 形参逃逸) | ret_borrow
EscTable: sizet* EscTable: sizet*
EscCount: int EscCount: int
# 扫描临时区(也只分配一次、每函数复用),消除每函数每轮的 gset/box 分配:
# [0]=global 名计数 | [1..64]=global 名指针 | [65],[66]=box(mask, ret_borrow)
EscScratch: sizet*
def InitStructGen(self) -> int: def InitStructGen(self) -> int:
"""Initialize struct-mixin fields; called inline from subclass constructors.""" """Initialize struct-mixin fields; called inline from subclass constructors."""
@@ -132,6 +135,7 @@ novirtual class StructGenMixin:
self.FileFuncNames = None self.FileFuncNames = None
self.EscTable = None self.EscTable = None
self.EscCount = 0 self.EscCount = 0
self.EscScratch = None
self.StructNameIdx = None self.StructNameIdx = None
self.StructTypeIdx = None self.StructTypeIdx = None
self.StructIdxCap = 0 self.StructIdxCap = 0
@@ -253,16 +257,20 @@ novirtual class StructGenMixin:
if ch is None: return if ch is None: return
if self.FileFuncNames is None: self.FileFuncNames = hashtable.HashTable(pool) if self.FileFuncNames is None: self.FileFuncNames = hashtable.HashTable(pool)
if self.FileFuncNames is None: return if self.FileFuncNames is None: return
# 生命期摘要表:每次进入本文件时重置(v1b 只覆盖同文件;跨文件属 v3) # 生命期摘要表:**只分配一次**(PermPool,对照 init_struct_table 的长生命周期表口径),
# 两个要点(对照 init_struct_table): # 此后每文件仅重置 EscCount。表内只存 u64 名哈希,且读只发生在 [0, EscCount) 区间;
# 1) 必须从 **PermPool** 分配——临时池在 Phase1 会被 reset_to(mark) 回收,同块内存 # 重置后该区间必然"先写后读",故无需每文件 memset —— 既避免每文件 24KB 泄漏,
# 被复用后我们会握着已变脏的旧指针(ASan 抓不到,表现为槽里读出垃圾 → strcmp 崩)。 # 又保留"槽内不含指针、不可能崩在 strcmp"的保证。
# 2) 必须显式清零:未写入的槽若不是 0,_EscLookup 的 strcmp 会对脏指针解引用。
esc_cap: sizet = 1024 esc_cap: sizet = 1024
esc_pool: memhub.MemBuddy* = CompilerFactory.factory.PermPool esc_pool: memhub.MemBuddy* = CompilerFactory.factory.PermPool
if esc_pool is None: esc_pool = pool if esc_pool is None: esc_pool = pool
self.EscTable = sizet*(esc_pool.alloc(esc_cap * 24)) if self.EscTable is None:
if self.EscTable is not None: string.memset(self.EscTable, 0, esc_cap * 24) self.EscTable = sizet*(esc_pool.alloc(esc_cap * 24))
if self.EscTable is not None: string.memset(self.EscTable, 0, esc_cap * 24)
# 扫描临时区同样只分配一次:每函数复用前只把 global 计数清 0(读只到 [0, count))
if self.EscScratch is None:
self.EscScratch = sizet*(esc_pool.alloc(72 * 8))
if self.EscScratch is not None: string.memset(self.EscScratch, 0, 72 * 8)
self.EscCount = 0 self.EscCount = 0
cn: sizet = len(ch) cn: sizet = len(ch)
ci: sizet ci: sizet
@@ -283,9 +291,16 @@ novirtual class StructGenMixin:
# 从根上消除"槽里是坏指针 → strcmp 崩在 0xffff…"这一整类问题。 # 从根上消除"槽里是坏指针 → strcmp 崩在 0xffff…"这一整类问题。
self._RegisterFuncEscapeSummary(esc_pool, fd, fd.name) self._RegisterFuncEscapeSummary(esc_pool, fd, fd.name)
# v2 传递闭包:再迭代两次,让 `f` 经调用点继承 `g` 的逃逸(单调 OR-in,稳定即收敛) # v2 传递闭包:反复重扫直到掩码不再变化(单调 OR-in 必收敛),上限 8 轮兜底。
for pi2 in range(2): # 原来固定 2 轮是近似,>2 层的链会漏;这里改为扫到不动点。
prev_sum: sizet = 0
pass_i: int = 0
while pass_i < 8:
self._RescanEscapes(esc_pool, tree) self._RescanEscapes(esc_pool, tree)
cur_sum: sizet = self._EscMaskSum()
if cur_sum == prev_sum: break
prev_sum = cur_sum
pass_i += 1
def is_file_func(self, name: str) -> int: def is_file_func(self, name: str) -> int:
"""Return 1 when the bare name is a top-level function declared in this file.""" """Return 1 when the bare name is a top-level function declared in this file."""
@@ -302,23 +317,25 @@ novirtual class StructGenMixin:
def _EscLookup(self, name: str) -> int: def _EscLookup(self, name: str) -> int:
"""线性查找摘要项下标;未找到返回 -1""" """线性查找摘要项下标;未找到返回 -1"""
if name is None or self.EscTable is None: return -1 if name is None or self.EscTable is None: return -1
h: u64 = self._NameHash(name) h: sizet = self._NameHash(name)
cap: int = 1024 cap: int = 1024
i: int = 0 i: int = 0
while i < self.EscCount and i < cap: while i < self.EscCount and i < cap:
# 槽 0 是名哈希(u64);未写入的槽为 0,与任何非零哈希都不相等,无需判空 # 槽 0 是名哈希(u64/sizet);未写入的槽为 0,与任何非零哈希都不相等,无需判空
if u64(self.EscTable[i * 3 + 0]) == h: return i if self.EscTable[i * 3 + 0] == h: return i
i += 1 i += 1
return -1 return -1
def _NameHash(self, name: str) -> u64: def _NameHash(self, name: str) -> sizet:
"""djb2-64(小立即数,溢出按 u64 回绕):把函数名压成表键""" """djb2-64(小立即数,溢出按无符号回绕):把函数名压成表键。
h: u64 = 5381 注:返回类型用 sizet(本文件通用、内建),不用 stdint 的 u64 —— 后者未 import,
TPC 会因"方法返回类型解析不出"而 fail-fast。"""
h: sizet = 5381
if name is None: return h if name is None: return h
n: sizet = string.strlen(name) n: sizet = string.strlen(name)
i: sizet = 0 i: sizet = 0
while i < n: while i < n:
h = h * 33 + u64(uint8(name[i])) h = h * 33 + sizet(uint8(name[i]))
i += 1 i += 1
return h return h
@@ -336,13 +353,15 @@ novirtual class StructGenMixin:
def _RegisterFuncEscapeSummary(self, pool: memhub.MemBuddy*, fd: ast.FunctionDef*, key: str) -> int: def _RegisterFuncEscapeSummary(self, pool: memhub.MemBuddy*, fd: ast.FunctionDef*, key: str) -> int:
"""扫函数体登记摘要;key 必须是与 EscTable 同池的字符串""" """扫函数体登记摘要;key 必须是与 EscTable 同池的字符串"""
if pool is None or fd is None or key is None or self.EscTable is None: return 1 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._EscLookup(key) >= 0: return 0
if self.EscCount >= 1024: return 1 if self.EscCount >= 1024: return 1
gset: hashtable.HashTable* = hashtable.HashTable(pool) # gset 现在是**定长数组**(复用区;[0]=计数,[1..]=名字指针),不再是 HashTable;
if gset is not None: self._CollectGlobalNames(fd.children, gset, 0) # box 也在同一块复用区里(偏移 65 槽 = 520 字节)。
box: sizet* = sizet*(pool.alloc(16)) gset: sizet* = self.EscScratch
if box is None: return 1 gset[0] = 0
self._CollectGlobalNames(fd.children, gset, 0)
box: sizet* = sizet*(uint64(self.EscScratch) + 520)
box[0] = 0 box[0] = 0
box[1] = 0 box[1] = 0
self._ScanEscapes(fd, fd.children, gset, box, 0) self._ScanEscapes(fd, fd.children, gset, box, 0)
@@ -353,8 +372,8 @@ novirtual class StructGenMixin:
self.EscCount = slot + 1 self.EscCount = slot + 1
return 0 return 0
def _CollectGlobalNames(self, stmts: list[ast.AST*]*, gset: hashtable.HashTable*, depth: int) -> int: def _CollectGlobalNames(self, stmts: list[ast.AST*]*, gset: sizet*, depth: int) -> int:
"""收集本函数(含嵌套块)里 `global` 声明的名字""" """收集本函数(含嵌套块)`global` 声明的名字到定长数组:[0]=计数,[1..]=名字指针"""
if stmts is None or gset is None or depth > 64: return 0 if stmts is None or gset is None or depth > 64: return 0
n: sizet = len(stmts) n: sizet = len(stmts)
for i in range(n): for i in range(n):
@@ -369,7 +388,13 @@ novirtual class StructGenMixin:
nnd: ast.AST* = gn.names.get(j) nnd: ast.AST* = gn.names.get(j)
if nnd is not None and nnd.kind() == ast.ASTKind.Name: if nnd is not None and nnd.kind() == ast.ASTKind.Name:
nnm: ast.Name* = ast.Name*(nnd) nnm: ast.Name* = ast.Name*(nnd)
if nnm.id is not None: gset.set_int(nnm.id, 1) if nnm.id is not None:
gc: sizet = gset[0]
if gc < 64:
# 存**名哈希**而非字符串指针:避免"str 当 str* 解"的用法错误
# (str*(x)[0] 只在 x 是 str 变量地址时成立;存 str 本身会读到其字节)
gset[1 + gc] = sizet(self._NameHash(nnm.id))
gset[0] = gc + 1
elif k == ast.ASTKind.If: elif k == ast.ASTKind.If:
ifn: ast.If* = ast.If*(st) ifn: ast.If* = ast.If*(st)
self._CollectGlobalNames(ifn.children, gset, depth + 1) self._CollectGlobalNames(ifn.children, gset, depth + 1)
@@ -405,7 +430,7 @@ novirtual class StructGenMixin:
if pa.arg is not None and string.strcmp(pa.arg, vnm.id) == 0: return int(i) if pa.arg is not None and string.strcmp(pa.arg, vnm.id) == 0: return int(i)
return -1 return -1
def _MarkEscapingStore(self, fd: ast.FunctionDef*, target: ast.AST*, value: ast.AST*, gset: hashtable.HashTable*, box: sizet*) -> int: def _MarkEscapingStore(self, fd: ast.FunctionDef*, target: ast.AST*, value: ast.AST*, gset: sizet*, box: sizet*) -> int:
"""`target = 形参`:若 target 是对象字段或已声明的全局,则该形参逃逸""" """`target = 形参`:若 target 是对象字段或已声明的全局,则该形参逃逸"""
pi: int = self._ParamIndex(fd, value) pi: int = self._ParamIndex(fd, value)
if pi < 0 or target is None or box is None: return 0 if pi < 0 or target is None or box is None: return 0
@@ -414,11 +439,22 @@ novirtual class StructGenMixin:
box[0] = box[0] | (sizet(1) << pi) box[0] = box[0] | (sizet(1) << pi)
elif tk == ast.ASTKind.Name: elif tk == ast.ASTKind.Name:
tnm: ast.Name* = ast.Name*(target) tnm: ast.Name* = ast.Name*(target)
if tnm.id is not None and gset is not None and gset.__contains__(tnm.id) != 0: if tnm.id is not None and gset is not None and self._IsGlobalNm(gset, tnm.id) != 0:
box[0] = box[0] | (sizet(1) << pi) box[0] = box[0] | (sizet(1) << pi)
return 0 return 0
def _ScanEscapes(self, fd: ast.FunctionDef*, stmts: list[ast.AST*]*, gset: hashtable.HashTable*, box: sizet*, depth: int) -> int: def _IsGlobalNm(self, garr: sizet*, name: str) -> int:
"""name 是否在定长 global 名单里([0]=计数,[1..]=名哈希);1=是 / 0=否"""
if garr is None or name is None: return 0
h: sizet = self._NameHash(name)
gc: sizet = garr[0]
i: sizet = 0
while i < gc:
if garr[1 + i] == h: return 1
i += 1
return 0
def _ScanEscapes(self, fd: ast.FunctionDef*, stmts: list[ast.AST*]*, gset: sizet*, box: sizet*, depth: int) -> int:
"""递归扫描:return p / G = p / obj.f = p;只进 if/while/for,不进嵌套 def/class""" """递归扫描:return p / G = p / obj.f = p;只进 if/while/for,不进嵌套 def/class"""
if fd is None or stmts is None or box is None or depth > 64: return 0 if fd is None or stmts is None or box is None or depth > 64: return 0
n: sizet = len(stmts) n: sizet = len(stmts)
@@ -497,10 +533,11 @@ novirtual class StructGenMixin:
if fd is None or fd.name is None: continue if fd is None or fd.name is None: continue
idx: int = self._EscLookup(fd.name) idx: int = self._EscLookup(fd.name)
if idx < 0: continue if idx < 0: continue
gset: hashtable.HashTable* = hashtable.HashTable(pool) if self.EscScratch is None: continue
if gset is not None: self._CollectGlobalNames(fd.children, gset, 0) gset: sizet* = self.EscScratch
box: sizet* = sizet*(pool.alloc(16)) gset[0] = 0
if box is None: continue self._CollectGlobalNames(fd.children, gset, 0)
box: sizet* = sizet*(uint64(self.EscScratch) + 520)
box[0] = self.EscTable[idx * 3 + 1] box[0] = self.EscTable[idx * 3 + 1]
box[1] = self.EscTable[idx * 3 + 2] box[1] = self.EscTable[idx * 3 + 2]
self._ScanEscapes(fd, fd.children, gset, box, 0) self._ScanEscapes(fd, fd.children, gset, box, 0)
@@ -508,6 +545,16 @@ novirtual class StructGenMixin:
self.EscTable[idx * 3 + 2] = box[1] self.EscTable[idx * 3 + 2] = box[1]
return 0 return 0
def _EscMaskSum(self) -> sizet:
"""所有条目 (mask, ret_borrow) 的滚动指纹,用于判定传递闭包是否已收敛"""
if self.EscTable is None: return 0
s: sizet = 0
i: int = 0
while i < self.EscCount:
s = s * 31 + self.EscTable[i * 3 + 1] * 1099511628211 + self.EscTable[i * 3 + 2]
i += 1
return s
def get_file_module_sha1(self) -> str: def get_file_module_sha1(self) -> str:
"""Return the module SHA1 of the file being translated (None if unregistered).""" """Return the module SHA1 of the file being translated (None if unregistered)."""
return self.FileModuleSha1 return self.FileModuleSha1
+12 -4
View File
@@ -91,7 +91,11 @@ novirtual class TypeGenErr(TypeGenBase.TypeGenBase):
else: stdio.printf(' File "%s"\n', file_path) else: stdio.printf(' File "%s"\n', file_path)
if err_msg is None or err_msg[0] == 0: err_msg = "syntax error" if err_msg is None or err_msg[0] == 0: err_msg = "syntax error"
stdio.printf("SyntaxError: %s\n", err_msg) stdio.printf("SyntaxError: %s\n", err_msg)
def fatal_error(self, node: ast.AST*, msg: str, err_type: str = "SyntaxError", col_start: int = 0, col_end: int = 0): # 注:返回类型必须显式给一个**真实类型**(int),不能留空、也不能用 None。
# 本方法是"必定终止"的 fail-fast,从不返回;但留空/None 都会让跨模块 call site 解析出
# 退化类型 i0,命中 CallGenCall.vp 的 i0 守卫 → _fail_method_ret_ty("fatal_error") 中止编译。
# 给 int 后解析为 i32,守卫不再命中;因从不返回,该类型不会被真正使用。
def fatal_error(self, node: ast.AST*, msg: str, err_type: str = "SyntaxError", col_start: int = 0, col_end: int = 0) -> int:
"""Report an error in Python style and exit (supports multiple frames, error type, column range).""" """Report an error in Python style and exit (supports multiple frames, error type, column range)."""
stdio.printf("Traceback (most recent call last, compile-time error):\n") stdio.printf("Traceback (most recent call last, compile-time error):\n")
# Print the call stack frames (bottom to top) # Print the call stack frames (bottom to top)
@@ -151,11 +155,15 @@ novirtual class TypeGenErr(TypeGenBase.TypeGenBase):
stdio.fflush(None) stdio.fflush(None)
# Termination convergence: in REPL, longjmp back to the loop via VLogger.repl_abort (the # Termination convergence: in REPL, longjmp back to the loop via VLogger.repl_abort (the
# traceback is printed and the process survives); in batch this exit is sys.exit(1) unchanged. # traceback is printed and the process survives); in batch this exit is sys.exit(1) unchanged.
VLogger.repl_abort() VLogger.repl_abort()
# 不可达:repl_abort 要么 longjmp 回 REPL 循环、要么 sys.exit(1)。
# 显式 return 只为满足上面声明的 -> int 返回类型(避免"缺返回路径")。
return 0
def fatal_type_error(self, node: ast.AST*, msg: str, col_start: int = 0, col_end: int = 0): # 同 fatal_error:跨模块调用需要**真实**返回类型,否则解析出 i0 会命中守卫中止编译。
def fatal_type_error(self, node: ast.AST*, msg: str, col_start: int = 0, col_end: int = 0) -> int:
"""Report an error in Python style and exit (TypeError variant)""" """Report an error in Python style and exit (TypeError variant)"""
self.fatal_error(node, msg, "TypeError", col_start, col_end) return self.fatal_error(node, msg, "TypeError", col_start, col_end)
def try_extract_callable_ret(self, pool: memhub.MemBuddy*, annot: ast.AST*, imported_modules: str, from_imports: str, trans: HT.Translator*) -> mlirlite.LLVMType*: def try_extract_callable_ret(self, pool: memhub.MemBuddy*, annot: ast.AST*, imported_modules: str, from_imports: str, trans: HT.Translator*) -> mlirlite.LLVMType*:
"""Extract the return type of a function-pointer annotation (t.Callable[[A,B], R]); None for non-Callable or signature-less forms.""" """Extract the return type of a function-pointer annotation (t.Callable[[A,B], R]); None for non-Callable or signature-less forms."""
if annot is None or annot.kind() != ast.ASTKind.Subscript: return None if annot is None or annot.kind() != ast.ASTKind.Subscript: return None
+6 -1
View File
@@ -187,7 +187,12 @@ novirtual class TypeGenResolve(TypeGenBase.TypeGenBase):
# [reexport class ownership] cls not found under cand: it may be re-exported # [reexport class ownership] cls not found under cand: it may be re-exported
# via from-import; look up the global reexport map, else parse the module .vpi. # via from-import; look up the global reexport map, else parse the module .vpi.
rr_mod: str = self.Gen.ImportsGen.lookup_reexport(cand_sha1, cls_name) rr_mod: str = self.Gen.ImportsGen.lookup_reexport(cand_sha1, cls_name)
if rr_mod is None: rr_mod = self.Gen.CallGenBase._lookup_reexport_from_vpi(pool, cand_sha1, cls_name) # allow_star=0: only a NAMED re-export (`from X import Cls`) is evidence of ownership.
# A `from X import *` candidate must not be used here -- nearly every file star-imports
# stdint, and taking one as the class's true defining module mis-owns the forward
# declaration (entry type "<candidate_sha1>.ClassName"), after which every vpi/text.ll
# return-type lookup for that class misses and fail-fasts.
if rr_mod is None: rr_mod = self.Gen.CallGenBase._lookup_reexport_from_vpi(pool, cand_sha1, cls_name, 0)
if rr_mod is None: return cand_sha1 if rr_mod is None: return cand_sha1
rr_sha: str = self.Gen.StructGen._resolve_module_sha1(rr_mod) rr_sha: str = self.Gen.StructGen._resolve_module_sha1(rr_mod)
if rr_sha is None or rr_sha[0] == '\0' or string.strcmp(rr_sha, cand_sha1) == 0: return cand_sha1 if rr_sha is None or rr_sha[0] == '\0' or string.strcmp(rr_sha, cand_sha1) == 0: return cand_sha1