From b03e55bb7e9da5229d413ee441b445d21e7283ef Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Thu, 8 Oct 2026 16:38:35 +0800 Subject: [PATCH] =?UTF-8?q?fix(gen):=20=E7=B1=BB=E5=BD=92=E5=B1=9E?= =?UTF-8?q?=E4=B8=8D=E9=87=87=E4=BF=A1=E6=98=9F=E5=8F=B7=E5=AF=BC=E5=85=A5?= =?UTF-8?q?=E5=80=99=E9=80=89=EF=BC=8C=E6=94=B6=E5=8F=A3=20fatal=5Ferror?= =?UTF-8?q?=20=E8=BF=94=E5=9B=9E=E7=B1=BB=E5=9E=8B=E4=B8=8E=E9=BB=98?= =?UTF-8?q?=E8=AE=A4=E5=80=BC=E8=A1=A5=E5=85=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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)。 --- App/lib/Stub/StubGen/VpiReader.vp | 7 +- App/lib/core/Gen/AssignGenMixin.vp | 6 +- App/lib/core/Gen/Call/CallGenBase.vp | 71 ++++++++++++++- App/lib/core/Gen/Call/CallGenCall.vp | 6 +- App/lib/core/Gen/Call/CallGenMixin.vp | 6 ++ App/lib/core/Gen/Call/CallGenProp.vp | 2 +- App/lib/core/Gen/StructGenMixin.vp | 111 +++++++++++++++++------- App/lib/core/Gen/Type/TypeGenErr.vp | 16 +++- App/lib/core/Gen/Type/TypeGenResolve.vp | 7 +- 9 files changed, 183 insertions(+), 49 deletions(-) diff --git a/App/lib/Stub/StubGen/VpiReader.vp b/App/lib/Stub/StubGen/VpiReader.vp index bafa847..e15a786 100644 --- a/App/lib/Stub/StubGen/VpiReader.vp +++ b/App/lib/Stub/StubGen/VpiReader.vp @@ -1920,8 +1920,11 @@ def _ParseVpiIntoModuleImpl(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, v # Import/ImportFrom -> skip (nothing to process) - # free the SHA1 buffer (allocated by _extract_sha1_from_path via stdlib.malloc) - if src_sha1 is not None: stdlib.free(src_sha1) + # 不要 free src_sha1:它会被后续阶段继续引用(模块/类型注册按 `.Name` 命名, + # 之后 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) stdlib.free(vpi_buf) return 1 diff --git a/App/lib/core/Gen/AssignGenMixin.vp b/App/lib/core/Gen/AssignGenMixin.vp index 56f7f07..f06eab9 100644 --- a/App/lib/core/Gen/AssignGenMixin.vp +++ b/App/lib/core/Gen/AssignGenMixin.vp @@ -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") else: VLogger.error(f"assignment rhs is None: sha1={sha1}:{val_line} call '{func_nm}(...)' returned None", "ASGN") else: - if rel_path is not None: VLogger.error(f"assignment rhs is None: {rel_path}:{val_line} [kind={val_kind}]") - else: VLogger.error(f"assignment rhs is None: sha1={sha1}:{val_line} [kind={val_kind}]") + # 必须与相邻分支一致显式传 category:跨模块模块级函数调用目前不会补默认值, + # 漏传会生成操作数不足的 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") return 0 diff --git a/App/lib/core/Gen/Call/CallGenBase.vp b/App/lib/core/Gen/Call/CallGenBase.vp index 896460d..e340ff8 100644 --- a/App/lib/core/Gen/Call/CallGenBase.vp +++ b/App/lib/core/Gen/Call/CallGenBase.vp @@ -294,8 +294,12 @@ novirtual class CallGenBase: return None - def _lookup_reexport_from_vpi(self, pool: memhub.MemBuddy*, mod_sha1: str, func_name: str) -> 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).""" + 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). + 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 # accumulate star-import candidates (space-separated); caller validates each star_cands: str = None @@ -362,6 +366,8 @@ novirtual class CallGenBase: nm_start: sizet = imp_pos + 8 # star import (from X import *): collect all candidates; caller verifies 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 if buf[mod_start] == '.': star_dot = 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 # ============================================================ - 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.""" - 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") return None diff --git a/App/lib/core/Gen/Call/CallGenCall.vp b/App/lib/core/Gen/Call/CallGenCall.vp index b7603e1..5a4fcfb 100644 --- a/App/lib/core/Gen/Call/CallGenCall.vp +++ b/App/lib/core/Gen/Call/CallGenCall.vp @@ -489,7 +489,7 @@ novirtual class CallGenCall(CallGenBase.CallGenBase): if bits == 0: cd_i0 = 1 case _: 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. if found_func is None: # 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) # returns None and breaks expression translation). 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 # 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 # 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) - 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. # 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) diff --git a/App/lib/core/Gen/Call/CallGenMixin.vp b/App/lib/core/Gen/Call/CallGenMixin.vp index 83179a4..7e86229 100644 --- a/App/lib/core/Gen/Call/CallGenMixin.vp +++ b/App/lib/core/Gen/Call/CallGenMixin.vp @@ -940,6 +940,12 @@ novirtual class CallGenMixin(CallGenBase.CallGenBase): # 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). 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: # 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 diff --git a/App/lib/core/Gen/Call/CallGenProp.vp b/App/lib/core/Gen/Call/CallGenProp.vp index a83f2dc..fc9d7ca 100644 --- a/App/lib/core/Gen/Call/CallGenProp.vp +++ b/App/lib/core/Gen/Call/CallGenProp.vp @@ -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): # 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) - 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 # coercion (self/obj or the classmethod cls occupies index 0, skipped via start_idx). diff --git a/App/lib/core/Gen/StructGenMixin.vp b/App/lib/core/Gen/StructGenMixin.vp index 223130a..df2a065 100644 --- a/App/lib/core/Gen/StructGenMixin.vp +++ b/App/lib/core/Gen/StructGenMixin.vp @@ -114,7 +114,10 @@ novirtual class StructGenMixin: # VarEntry/FieldEntry 的 "Appended at the end so existing field offsets are unchanged")。 # 池数组 + 线性查找;每项 3 槽:name(池上 str 地址) | mask(第 k 位=第 k 形参逃逸) | ret_borrow 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: """Initialize struct-mixin fields; called inline from subclass constructors.""" @@ -132,6 +135,7 @@ novirtual class StructGenMixin: self.FileFuncNames = None self.EscTable = None self.EscCount = 0 + self.EscScratch = None self.StructNameIdx = None self.StructTypeIdx = None self.StructIdxCap = 0 @@ -253,16 +257,20 @@ novirtual class StructGenMixin: if ch is None: return if self.FileFuncNames is None: self.FileFuncNames = hashtable.HashTable(pool) if self.FileFuncNames is None: return - # 生命期摘要表:每次进入本文件时重置(v1b 只覆盖同文件;跨文件属 v3) - # 两个要点(对照 init_struct_table): - # 1) 必须从 **PermPool** 分配——临时池在 Phase1 会被 reset_to(mark) 回收,同块内存 - # 被复用后我们会握着已变脏的旧指针(ASan 抓不到,表现为槽里读出垃圾 → strcmp 崩)。 - # 2) 必须显式清零:未写入的槽若不是 0,_EscLookup 的 strcmp 会对脏指针解引用。 + # 生命期摘要表:**只分配一次**(PermPool,对照 init_struct_table 的长生命周期表口径), + # 此后每文件仅重置 EscCount。表内只存 u64 名哈希,且读只发生在 [0, EscCount) 区间; + # 重置后该区间必然"先写后读",故无需每文件 memset —— 既避免每文件 24KB 泄漏, + # 又保留"槽内不含指针、不可能崩在 strcmp"的保证。 esc_cap: sizet = 1024 esc_pool: memhub.MemBuddy* = CompilerFactory.factory.PermPool if esc_pool is None: esc_pool = pool - self.EscTable = sizet*(esc_pool.alloc(esc_cap * 24)) - if self.EscTable is not None: string.memset(self.EscTable, 0, esc_cap * 24) + if self.EscTable is None: + 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 cn: sizet = len(ch) ci: sizet @@ -283,9 +291,16 @@ novirtual class StructGenMixin: # 从根上消除"槽里是坏指针 → strcmp 崩在 0xffff…"这一整类问题。 self._RegisterFuncEscapeSummary(esc_pool, fd, fd.name) - # v2 传递闭包:再迭代两次,让 `f` 经调用点继承 `g` 的逃逸(单调 OR-in,稳定即收敛) - for pi2 in range(2): + # v2 传递闭包:反复重扫直到掩码不再变化(单调 OR-in 必收敛),上限 8 轮兜底。 + # 原来固定 2 轮是近似,>2 层的链会漏;这里改为扫到不动点。 + prev_sum: sizet = 0 + pass_i: int = 0 + while pass_i < 8: 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: """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: """线性查找摘要项下标;未找到返回 -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 i: int = 0 while i < self.EscCount and i < cap: - # 槽 0 是名哈希(u64);未写入的槽为 0,与任何非零哈希都不相等,无需判空 - if u64(self.EscTable[i * 3 + 0]) == h: return i + # 槽 0 是名哈希(u64/sizet);未写入的槽为 0,与任何非零哈希都不相等,无需判空 + if self.EscTable[i * 3 + 0] == h: return i i += 1 return -1 - def _NameHash(self, name: str) -> u64: - """djb2-64(小立即数,溢出按 u64 回绕):把函数名压成表键""" - h: u64 = 5381 + def _NameHash(self, name: str) -> sizet: + """djb2-64(小立即数,溢出按无符号回绕):把函数名压成表键。 + 注:返回类型用 sizet(本文件通用、内建),不用 stdint 的 u64 —— 后者未 import, + TPC 会因"方法返回类型解析不出"而 fail-fast。""" + h: sizet = 5381 if name is None: return h n: sizet = string.strlen(name) i: sizet = 0 while i < n: - h = h * 33 + u64(uint8(name[i])) + h = h * 33 + sizet(uint8(name[i])) i += 1 return h @@ -336,13 +353,15 @@ novirtual class StructGenMixin: def _RegisterFuncEscapeSummary(self, pool: memhub.MemBuddy*, fd: ast.FunctionDef*, key: str) -> int: """扫函数体登记摘要;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.EscCount >= 1024: return 1 - gset: hashtable.HashTable* = hashtable.HashTable(pool) - if gset is not None: self._CollectGlobalNames(fd.children, gset, 0) - box: sizet* = sizet*(pool.alloc(16)) - if box is None: return 1 + # gset 现在是**定长数组**(复用区;[0]=计数,[1..]=名字指针),不再是 HashTable; + # box 也在同一块复用区里(偏移 65 槽 = 520 字节)。 + gset: sizet* = self.EscScratch + gset[0] = 0 + self._CollectGlobalNames(fd.children, gset, 0) + box: sizet* = sizet*(uint64(self.EscScratch) + 520) box[0] = 0 box[1] = 0 self._ScanEscapes(fd, fd.children, gset, box, 0) @@ -353,8 +372,8 @@ novirtual class StructGenMixin: self.EscCount = slot + 1 return 0 - def _CollectGlobalNames(self, stmts: list[ast.AST*]*, gset: hashtable.HashTable*, depth: int) -> int: - """收集本函数(含嵌套块)里 `global` 声明的名字""" + def _CollectGlobalNames(self, stmts: list[ast.AST*]*, gset: sizet*, depth: int) -> int: + """收集本函数(含嵌套块)`global` 声明的名字到定长数组:[0]=计数,[1..]=名字指针""" if stmts is None or gset is None or depth > 64: return 0 n: sizet = len(stmts) for i in range(n): @@ -369,7 +388,13 @@ novirtual class StructGenMixin: nnd: ast.AST* = gn.names.get(j) if nnd is not None and nnd.kind() == ast.ASTKind.Name: 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: ifn: ast.If* = ast.If*(st) 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) 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 是对象字段或已声明的全局,则该形参逃逸""" pi: int = self._ParamIndex(fd, value) 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) elif tk == ast.ASTKind.Name: 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) 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""" if fd is None or stmts is None or box is None or depth > 64: return 0 n: sizet = len(stmts) @@ -497,10 +533,11 @@ novirtual class StructGenMixin: if fd is None or fd.name is None: continue idx: int = self._EscLookup(fd.name) if idx < 0: continue - gset: hashtable.HashTable* = hashtable.HashTable(pool) - if gset is not None: self._CollectGlobalNames(fd.children, gset, 0) - box: sizet* = sizet*(pool.alloc(16)) - if box is None: continue + if self.EscScratch is None: continue + gset: sizet* = self.EscScratch + gset[0] = 0 + self._CollectGlobalNames(fd.children, gset, 0) + box: sizet* = sizet*(uint64(self.EscScratch) + 520) box[0] = self.EscTable[idx * 3 + 1] box[1] = self.EscTable[idx * 3 + 2] self._ScanEscapes(fd, fd.children, gset, box, 0) @@ -508,6 +545,16 @@ novirtual class StructGenMixin: self.EscTable[idx * 3 + 2] = box[1] 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: """Return the module SHA1 of the file being translated (None if unregistered).""" return self.FileModuleSha1 diff --git a/App/lib/core/Gen/Type/TypeGenErr.vp b/App/lib/core/Gen/Type/TypeGenErr.vp index f7a22be..3d9eaec 100644 --- a/App/lib/core/Gen/Type/TypeGenErr.vp +++ b/App/lib/core/Gen/Type/TypeGenErr.vp @@ -91,7 +91,11 @@ novirtual class TypeGenErr(TypeGenBase.TypeGenBase): else: stdio.printf(' File "%s"\n', file_path) if err_msg is None or err_msg[0] == 0: err_msg = "syntax error" 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).""" stdio.printf("Traceback (most recent call last, compile-time error):\n") # Print the call stack frames (bottom to top) @@ -151,11 +155,15 @@ novirtual class TypeGenErr(TypeGenBase.TypeGenBase): stdio.fflush(None) # 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. - 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)""" - 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*: """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 diff --git a/App/lib/core/Gen/Type/TypeGenResolve.vp b/App/lib/core/Gen/Type/TypeGenResolve.vp index 2017f69..0c69ac7 100644 --- a/App/lib/core/Gen/Type/TypeGenResolve.vp +++ b/App/lib/core/Gen/Type/TypeGenResolve.vp @@ -187,7 +187,12 @@ novirtual class TypeGenResolve(TypeGenBase.TypeGenBase): # [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. 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 ".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 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