Files
ViperC/App/lib/core/Gen/StructGenMixin.vp
T
TermiNexus 337be883d3 fix(nll): Attribute 节点显式只递归 .value,避免"拿不到子树=算提及"把 stdio.printf(...) 误判成不安全
现场:EscapeTest 的 h = new heap S() 永不降级(g(l,h) 照旧报逃逸)。根因是 _NllMentions 对 Attribute
落进保守分支(children 不含 value 时返回"提及")→ _NllExprUnsafe 把 stdio.printf("%s", h.q) 判为不安全
→ 整个 NLL 判据返回 0。属性名是静态字符串,变量只可能在基对象里,显式递归 .value 即可。
2026-10-09 20:57:31 +08:00

3315 lines
193 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import t, c
from stdint import *
import memhub
import string
import stdlib
import mlirlite
import stdio
import ast
import hashtable
import viperlib
import lib.core.CompilerFactory as CompilerFactory
import lib.core.VLogger as VLogger
import lib.Stub.StubMerger.StubMergerStore as StubMergerStore
import lib.core.Gen.GenTypes as GenTypes
import lib.core.Gen.Generator as Generator
import lib.core.Gen.Type.TypeSpec as TSpec
import lib.core.Gen.GenApi as GenApi
# StructGenMixin - struct registration / field lookup mixin.
# Registers and looks up structs (name/type indexes), finds fields, and matches cross-module ownership; @t.NoVTable uses static binding (MRO calls + this adjustment, no vtable cost).
# Constants (kept in sync with HandlesStruct).
define STRUCT_MAX = 512
# ── 跨过程常量特化(P3 收尾)的 scratch 槽位布局 ──
# 一块定长 sizet 数组,只分配一次;调用点用**常量实参**把被调者体重算一遍逃逸摘要。
# [0] global 名计数(gset,与 EscScratch 口径一致)
# [1..EC_GLOBAL_MAX] global 名哈希
# [EC_BOX_SLOT] / [EC_BOX_SLOT+1] box[0]=逃逸掩码、box[1]=返回借用位(compute_escape_summary 约定 box = gset+65 槽)
# [EC_EVAL_SLOT] 表达式常量求值的临时槽(递归求值每层算完立刻拷出,见 _EvalConstInt)
# [EC_ENV_SLOT..] 常量绑定环境:2 槽/形参([0]=是否已知、[1]=值),下标 = 形参位(self/cls 计入)
# [EC_CAND_SLOT..] 守卫候选:2 槽/候选(形参位 + 常量值),供 _CollectGuardCandidates 收
define EC_GLOBAL_MAX = 64
define EC_BOX_SLOT = 65
define EC_EVAL_SLOT = 246
define EC_ENV_SLOT = 68
define EC_ENV_MAX = 64
define EC_CAND_SLOT = 196
define EC_CAND_MAX = 8
# [EC_IND_SLOT..] 循环归纳变量区间:4 槽/条(名哈希, lo, hi, 保留),按 EscIndCount 当栈用
# [EC_IND_LO]/[EC_IND_HI] 区间查询的临时输出槽
define EC_IND_SLOT = 212
define EC_IND_MAX = 8
define EC_IND_LO = 244
define EC_IND_HI = 245
define EC_SCRATCH_SLOTS = 256
# 跨模块守卫记录的打包范围:p(8 位) | k(8 位,有符号) | m(其余位,被清掉的位掩码)。
# 超出范围(形参 > 255 个、常量超出 int8)就不记——只影响精度,不影响正确性。
define EC_PACK_K_MIN = -128
define EC_PACK_K_MAX = 127
# Name | type index slot count (power of two and > 2*STRUCT_MAX so load factor < 0.5 and probing terminates).
define STRUCT_IDX_CAP = 1024
# Field-slot upper limit (kept in sync with HandlesStruct): Generator flattens ~120 fields across 43 mixins, so a limit of 64 would drop 9th+ parent fields and break __init__ attribute assignment.
define FIELD_MAX = 256
define FIELD_NAME_MAX = 64
# Multiple-inheritance (MRO) support: max parents per class / max MRO chain length (Generator has 43 parents; PARENT_MAX=16 would drop the 17th+ parent fields and break __init__ writing self.DiagStmtCount).
define PARENT_MAX = 64
define MRO_MAX = 64
# Index hash constants (32-bit FNV-1a; all < 2^63 to avoid large decimal literal parsing risk).
define FNV32_OFFSET = 2166136261
define FNV32_PRIME = 16777619
define FNV32_MASK = 4294967295
# Index hash functions (name | Ty pointer value -> 64-bit hash).
# Used by StructTable's "name | Ty pointer value -> first registration index" index probing (see _struct_name_idx_insert / _struct_name_idx_find etc.).
def _struct_name_hash(name: str) -> uint64:
"""Hash a name with 32-bit FNV-1a over bytes up to NUL; empty name returns the offset basis."""
h: uint64 = uint64(FNV32_OFFSET)
if name is None: return h
i: int = 0
while name[i] != '\0':
h = (h ^ uint64(int(name[i]))) * uint64(FNV32_PRIME)
h = h & uint64(FNV32_MASK)
i += 1
return h
def _struct_ty_hash(tyval: uint64) -> uint64:
"""Hash a Ty pointer value with 32-bit FNV-1a over its 8 bytes."""
h: uint64 = uint64(FNV32_OFFSET)
v: uint64 = tyval
for j in range(8):
h = (h ^ (v & uint64(255))) * uint64(FNV32_PRIME)
h = h & uint64(FNV32_MASK)
v = v >> 8
return h
# StructGenMixin - struct registry / field lookup / visibility.
novirtual class StructGenMixin:
"""Mixin for struct registration and lookup."""
# --- Global registry (was _struct_table/_struct_count) ---
Gen: Generator.Generator*
StructTable: GenTypes.StructEntry* # STRUCT_MAX-slot array
StructCount: int
# --- OOM-reclaim allocation mode (was _g_stdlib_alloc) ---
StdlibAlloc: int
# --- Cross-module namespace isolation (was _visible_structs/_strict_visibility) ---
VisibleStructs: hashtable.HashTable*
StrictVisibility: int
# --- Per-module "declared class names" set (key "{module SHA1}.{class name}") ---
# Independent of StructTable registration state: recorded at the class-definition entry, so it can attribute a bare class name to its module even mid-definition; used by generic type-arg normalization (TypeGenResolve._norm_type_arg_name rule 5.5).
ModuleClasses: hashtable.HashTable*
# --- SHA1 of the module owning the class currently being defined (recorded by mark_module_class) ---
# Not reusing TypeGenBase.CurrentModuleSha1: A-pre/StubMerger paths only set Translator.ModuleSha1, and trans.ModuleSha1 is authoritative on every path, so it is the single source of truth for the "currently defining module".
CurDefModuleSha1: str
# --- Class name of the class currently being defined (recorded by mark_module_class; table-independent self-reference test) ---
# Needed because a self-referential field is parsed before its class is registered (and ModuleClasses may be unavailable), leaving a bare name that splits into two specialization names; the class name comes straight from the definition entry and never queries a table, so it is always decidable in that window.
CurDefClassName: str
# --- Top-level ClassDef name table for this file (rebuilt per file; registered by register_file_classes) ---
# A second "bare class name" window: the prologue translates module-level vars/forward declarations before translate_children, when the defining-class window is not yet established, so parameter/global bare class names would be registered as bare and split into two specialization names. This table records the positive fact "this name is declared in this file" before any annotation is parsed.
FileClassNames: hashtable.HashTable*
# Module SHA1 of this file (passed in by the Translator file entry with the pre-scan).
# Used only together with a FileClassNames hit, so even a stale value in the window cannot mis-attribute (a stale sha1 and "this file declares the class" cannot both hold).
FileModuleSha1: str
# 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)
StructTypeIdx: sizet* # Ty pointer value -> first registration index (same layout)
StructIdxCap: int # index slot count (power of two, > 2*STRUCT_MAX)
StructIdxBuilt: int # indexed entry count (-1 = untrusted; trusted only when == StructCount)
# 生命期摘要(M2.3 v1b):本文件函数 → 形参逃逸掩码 / 返回借用标记。
# **必须追加在类字段末尾**——新增字段插在中间会改变其后字段的偏移(本仓约定,见
# VarEntry/FieldEntry 的 "Appended at the end so existing field offsets are unchanged")。
# 池数组 + 线性查找;每项 3 槽:name(池上 str 地址) | mask(第 k 位=第 k 形参逃逸) | ret_borrow
EscTable: sizet*
EscCount: int
# 扫描临时区(也只分配一次、每函数复用),消除每函数每轮的 gset/box 分配:
# [0]=global 名计数 | [1..64]=global 名指针 | [65],[66]=box(mask, ret_borrow)
EscScratch: sizet*
# P2 willFree 标注表:本文件函数 → 形参失效掩码(第 k 位=第 k 个形参在本调用后失效)。
# 与 EscTable 同款:池数组 + 线性查找,每项 2 槽:name_hash | mask;只存 u64 哈希。
# **必须追加在类字段末尾**(见上方的字段偏移约定)。
WfTable: sizet*
WfCount: int
# ── 跨过程常量特化(P3 收尾)──
# 名哈希 → FunctionDef 指针,键口径与 EscTable 一致(普通函数用裸名键、方法用 "Class.method")。
# 只登记**本文件**的函数/方法,指针与文件 AST 同生命周期(EscFuncCount 每文件重置)。
EscFuncTable: sizet*
EscFuncCount: int
# 重算专用 scratch(布局见文件头 EC_*):与登记期的 EscScratch **分开**,避免调用点重算
# 把登记/.vpi 落盘过程中 EscScratch 的内容踩掉。
EscRefineScratch: sizet*
# 常量绑定环境(2 槽/形参,位于 EscRefineScratch 的 EC_ENV_SLOT 起)。None = 不启用常量特化。
EscConstEnv: sizet*
# 循环归纳变量区间的条数(在 EscRefineScratch 的 EC_IND_SLOT 起,当栈用:进循环体前 +1、出后 -1)
EscIndCount: int
# "硬逃逸位"表(键口径同 EscTable):该位由**全局写 / 返回形参 / 传递闭包 / base 未定的字段写**造成。
# 与"只写进形参对象字段"(调用点可判同命 → 放行)区分开:字段位 = 逃逸位 & ~硬位。每文件重置。
EscFieldTable: sizet*
EscFieldCount: int
# ── 逃逸点采集(**只走错误路径**)──
# escape_leak_line 重扫被调者函数体前,把"想要的形参位"放进 EscLeakWant;_MarkEscapingStore
# 命中该位时把**首个**造成逃逸的写入点行号写进 EscLeakLine。-1 = 不采集(登记/落盘路径恒为 -1)。
EscLeakWant: int
EscLeakLine: int
# ── 声明式指令表(`@life(escape(...))` / `@life(noescape(...))`)──
# 3 槽/项:[名哈希, 强制逃逸位, 强制不逃逸位]。调用点在**所有推断与裁剪之后**最后套用,
# 是不可被推翻的契约("unsafe 但强制")。每文件重置计数。
EscDirTable: sizet*
EscDirCount: int
# §13 NLL:当前正在翻译的函数定义(谓词 NllFrameConfined 要拿它的函数体做遍历)。
# 由三条函数体翻译入口设置/还原(顶层函数体 / 嵌套函数体 / 方法体)。
NllCurFunc: ast.AST*
def InitStructGen(self) -> int:
"""Initialize struct-mixin fields; called inline from subclass constructors."""
if self is None: return 1
self.StructTable = None
self.StructCount = 0
self.StdlibAlloc = 0
self.VisibleStructs = None
self.StrictVisibility = 0
self.ModuleClasses = None
self.CurDefModuleSha1 = None
self.CurDefClassName = None
self.FileClassNames = None
self.FileModuleSha1 = None
self.FileFuncNames = None
self.FileVarNames = None
self.EscTable = None
self.EscCount = 0
self.EscScratch = None
self.WfTable = None
self.WfCount = 0
self.EscFuncTable = None
self.EscFuncCount = 0
self.EscRefineScratch = None
self.EscConstEnv = None
self.EscIndCount = 0
self.EscFieldTable = None
self.EscFieldCount = 0
self.EscLeakWant = -1
self.EscLeakLine = 0
self.EscDirTable = None
self.EscDirCount = 0
self.NllCurFunc = None
self.StructNameIdx = None
self.StructTypeIdx = None
self.StructIdxCap = 0
self.StructIdxBuilt = 0
return 0
# P2-2 migrated methods (bodies from HandlesStruct.vp; globals become self fields, same-module calls become self methods; signatures identical to HandlesStruct).
def set_stdlib_alloc(self, flag: int) -> None:
"""Set the OOM-reclaim allocation mode: 1 = 1b phase (stdlib), 0 = registration phase (pool)."""
self.StdlibAlloc = flag
def get_stdlib_alloc(self) -> int:
"""Return the OOM-reclaim allocation mode (read via HandlesStruct delegation)."""
return self.StdlibAlloc
def reset_visible_structs(self, pool: memhub.MemBuddy*, strict: int) -> None:
"""Reset per-file visibility state and rebuild the per-file tables."""
self.StrictVisibility = strict
self.CurDefModuleSha1 = None
self.CurDefClassName = None
# The file-level class name table is rebuilt per file the same way (keys and table share a pool, so no dangling; re-registered by the Translator pre-scan, see register_file_classes).
# FileModuleSha1 points to trans.ModuleSha1 (same-pool string), so it must be cleared too to avoid reading reclaimed memory in the next file.
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.
self.VisibleStructs = hashtable.HashTable(pool)
else:
# Lenient mode: everything is visible, no HashTable needed.
self.VisibleStructs = None
def add_visible_struct(self, pool: memhub.MemBuddy*, name: str) -> None:
"""Record a visible struct name (class definition and from-import)."""
if name is None: return
# Lenient mode: everything is visible, no need to record.
if self.StrictVisibility == 0: return
if self.VisibleStructs is None: return
# Skip if already present (HashTable.__setitem__ would overwrite; check early to avoid a redundant allocation).
if self.VisibleStructs.__contains__(name) != 0: return
self.VisibleStructs.set_int(name, 1)
def is_struct_visible(self, name: str) -> int:
"""Return 1 when the struct name is visible, 0 otherwise."""
if self.StrictVisibility == 0: return 1
if name is None or self.VisibleStructs is None: return 0
return self.VisibleStructs.__contains__(name)
def _module_class_key(self, pool: memhub.MemBuddy*, sha1: str, name: str) -> str:
"""Build the "{sha1}.{class name}" lookup and registration key in the given pool."""
if pool is None or sha1 is None or name is None: return None
k_len: sizet = string.strlen(sha1) + 1 + string.strlen(name) + 1
k_buf: str = pool.alloc(k_len)
if k_buf is None: return None
k_buf[0] = '\0'
string.strcat(k_buf, sha1)
string.strcat(k_buf, ".")
string.strcat(k_buf, name)
return k_buf
def mark_module_class(self, pool: memhub.MemBuddy*, sha1: str, name: str) -> None:
"""Record "{sha1}.{class name}" as declared by the class's defining module."""
if sha1 is None or name is None: return
self.CurDefModuleSha1 = sha1
# Record the class name at the same time (table-independent self-reference test; must be recorded even if the pool/table is missing, since it never queries a table, see _norm_type_arg_name rule 5.5).
self.CurDefClassName = name
if pool is None or self.ModuleClasses is None:
# Not per-file initialized (this path did not call reset_visible_structs): do not register.
# Do not lazily build the table here: it must share a pool with the keys and be reset per file, else strcmp would read reclaimed keys (ASan access-violation).
return
k_buf: str = self._module_class_key(pool, sha1, name)
if k_buf is None: return
if self.ModuleClasses.__contains__(k_buf) != 0: return
self.ModuleClasses.set_int(k_buf, 1)
def is_module_class(self, pool: memhub.MemBuddy*, sha1: str, name: str) -> int:
"""Return 1 when the bare name is declared by the module sha1, regardless of registration."""
if pool is None or sha1 is None or name is None: return 0
if self.ModuleClasses is None: return 0
k_buf: str = self._module_class_key(pool, sha1, name)
if k_buf is None: return 0
return self.ModuleClasses.__contains__(k_buf)
def register_file_classes(self, pool: memhub.MemBuddy*, sha1: str, tree: ast.AST*) -> None:
"""Pre-register the top-level class names and module SHA1 of this file."""
if pool is None or sha1 is None or tree is None: return
self.FileModuleSha1 = sha1
ch: list[ast.AST*]* = tree.children
if ch is None: return
if self.FileClassNames is None: self.FileClassNames = hashtable.HashTable(pool)
if self.FileClassNames is None: return
cn: sizet = len(ch)
ci: sizet
for ci in range(cn):
child: ast.AST* = ch.get(ci)
if child is None: continue
if child.kind() != ast.ASTKind.ClassDef: continue
cd: ast.ClassDef* = ast.ClassDef*(child)
if cd is None or cd.name is None: continue
if self.FileClassNames.__contains__(cd.name) != 0: continue
# Keys must share a pool with the table (AST name strings zero-copy point into src_buf and may become invalid across phases): copy into the pool before registering to avoid strcmp on dangling memory during table resize/query.
nm_len: sizet = string.strlen(cd.name)
nm_buf: str = pool.alloc(nm_len + 1)
if nm_buf is None: continue
string.strcpy(nm_buf, cd.name)
self.FileClassNames.set_int(nm_buf, 1)
def is_file_class(self, pool: memhub.MemBuddy*, name: str) -> int:
"""Return 1 when the bare name is a top-level class declared in this file."""
if name is None or self.FileClassNames is None: return 0
return self.FileClassNames.__contains__(name)
def register_file_funcs(self, pool: memhub.MemBuddy*, tree: ast.AST*) -> None:
"""Pre-register the top-level function names of this file."""
if pool is None or tree is None: return
ch: list[ast.AST*]* = tree.children
if ch is None: return
if self.FileFuncNames is None: self.FileFuncNames = hashtable.HashTable(pool)
if self.FileFuncNames is None: return
# 生命期摘要表:**只分配一次**(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
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
# P2 willFree 表:同样只分配一次,每文件仅重置 WfCount(表内只有 u64 哈希,无指针)
if self.WfTable is None:
self.WfTable = sizet*(esc_pool.alloc(esc_cap * 16))
if self.WfTable is not None: string.memset(self.WfTable, 0, esc_cap * 16)
self.WfCount = 0
# 跨过程常量特化:FunctionDef 表与重算 scratch 同样只分配一次(PermPool),每文件只重置计数。
if self.EscFuncTable is None:
self.EscFuncTable = sizet*(esc_pool.alloc(esc_cap * 16))
if self.EscFuncTable is not None: string.memset(self.EscFuncTable, 0, esc_cap * 16)
if self.EscRefineScratch is None:
self.EscRefineScratch = sizet*(esc_pool.alloc(sizet(EC_SCRATCH_SLOTS) * 8))
if self.EscRefineScratch is not None: string.memset(self.EscRefineScratch, 0, sizet(EC_SCRATCH_SLOTS) * 8)
# "字段逃逸位"表:同样只分配一次,每文件重置计数
if self.EscFieldTable is None:
self.EscFieldTable = sizet*(esc_pool.alloc(esc_cap * 16))
if self.EscFieldTable is not None: string.memset(self.EscFieldTable, 0, esc_cap * 16)
self.EscFieldCount = 0
# 声明式指令表(escape/noescape):同样只分配一次,每文件重置计数
if self.EscDirTable is None:
self.EscDirTable = sizet*(esc_pool.alloc(esc_cap * 24))
if self.EscDirTable is not None: string.memset(self.EscDirTable, 0, esc_cap * 24)
self.EscDirCount = 0
self.EscFuncCount = 0
self.EscConstEnv = None
self.EscIndCount = 0
cn: sizet = len(ch)
ci: sizet
for ci in range(cn):
child: ast.AST* = ch.get(ci)
if child is None: continue
if child.kind() != ast.ASTKind.FunctionDef: continue
fd: ast.FunctionDef* = ast.FunctionDef*(child)
if fd is None or fd.name is None: continue
if self.FileFuncNames.__contains__(fd.name) != 0: continue
# Keys must share a pool with the table (AST name strings zero-copy point into src_buf and may become invalid across phases): copy into the pool before registering to avoid strcmp on dangling memory during table resize/query.
nm_len: sizet = string.strlen(fd.name)
nm_buf: str = pool.alloc(nm_len + 1)
if nm_buf is None: continue
string.strcpy(nm_buf, fd.name)
self.FileFuncNames.set_int(nm_buf, 1)
# 生命期摘要(M2.3 v1b):表内只存 u64 名哈希——不存字符串指针,
# 从根上消除"槽里是坏指针 → strcmp 崩在 0xffff…"这一整类问题。
self._RegisterFuncEscapeSummary(esc_pool, fd, fd.name)
# P2:同一处登记 willFree 标注(声明式,直接来自 @life/@willFree 装饰器)
self._RegisterFuncWillFree(fd, fd.name)
# 嵌套函数用裸名键(与调用点 func_name 一致)
self._RegisterNestedFuncs(esc_pool, fd.children, 0)
# M2.3 覆盖面扩展:类方法用 "Class.method" 键——此前方法与嵌套函数没有摘要,
# 它们的"存对象字段 / 返回形参"在调用点不会被拦截。
for ci2 in range(cn):
child2: ast.AST* = ch.get(ci2)
if child2 is None: continue
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
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."""
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);
# 递归进 if/while/for 的体与 else;**不**进嵌套 def/class。
# 不做传递闭包(v2)、不做跨模块(v3)。
# ============================================================
def _EscLookup(self, name: str) -> int:
"""线性查找摘要项下标;未找到返回 -1"""
if name is None or self.EscTable is None: return -1
h: sizet = self._NameHash(name)
cap: int = 1024
i: int = 0
while i < self.EscCount and i < cap:
# 槽 0 是名哈希(u64/sizet);未写入的槽为 0,与任何非零哈希都不相等,无需判空
if self.EscTable[i * 3 + 0] == h: return i
i += 1
return -1
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 + sizet(uint8(name[i]))
i += 1
return h
def get_param_escapes_hard(self, key: str) -> int:
"""形参"硬逃逸位"掩码(全局写 / 返回形参 / 传递闭包 / base 未定的字段写);0 = 无记录"""
if key is None or self.EscFieldTable is None: return 0
h: sizet = self._NameHash(key)
i: int = 0
while i < self.EscFieldCount:
if self.EscFieldTable[i * 2] == h: return int(self.EscFieldTable[i * 2 + 1])
i += 1
return 0
def get_param_escapes_hard_method(self, pool: memhub.MemBuddy*, class_name: str, method_name: str) -> int:
"""方法版:查 "Class.method" 并沿 MRO 回退(口径同 get_param_escapes_method)"""
if method_name is None or class_name is None: return 0
key_q: str = self._EscKey2(pool, class_name, method_name)
if key_q is not None:
m_q: int = self.get_param_escapes_hard(key_q)
if m_q != 0: return m_q
mro: sizet* = self.get_mro_chain(pool, class_name)
mi2: int = 1
while mro is not None and mro[mi2] != 0:
anc_m: str = rawptr(mro[mi2])
if anc_m is not None and string.strcmp(anc_m, class_name) != 0:
key_a: str = self._EscKey2(pool, anc_m, method_name)
if key_a is not None:
m_a: int = self.get_param_escapes_hard(key_a)
if m_a != 0: return m_a
mi2 += 1
return 0
def get_param_escapes(self, name: str) -> int:
"""形参逃逸掩码:第 k 位=第 k 个形参逃逸;无记录返回 0"""
idx: int = self._EscLookup(name)
if idx < 0: return 0
return int(self.EscTable[idx * 3 + 1])
def has_param_escapes(self, name: str) -> int:
"""本文件摘要表**是否有**该函数的记录(1/0)。
与"掩码是否为 0"必须区分:`@life(noescape(...))` 会把掩码清成 0,但那是**有记录**
(权威结论 = 不逃逸);而无记录 = 判不定(跨模块 / 未登记),调用方应保守处理。"""
if self._EscLookup(name) >= 0: return 1
return 0
def get_ret_borrow(self, name: str) -> int:
"""返回借用标记:1=该函数直接返回某形参;0=无记录"""
idx: int = self._EscLookup(name)
if idx < 0: return 0
return int(self.EscTable[idx * 3 + 2])
# ---- 声明式指令(@life(escape/noescape)):调用点的**最终裁决**表 ----
# 与 EscTable 分开存:EscTable 的掩码是"推断结果"(能被常量特化/同命裁剪改动),
# 指令是"契约"(不能被任何推断推翻),调用点在所有推断之后最后套用一次。
def _DirLookup(self, name: str) -> int:
"""指令表项下标;未找到返回 -1"""
if name is None or self.EscDirTable is None: return -1
h: sizet = self._NameHash(name)
i: int = 0
while i < self.EscDirCount:
if self.EscDirTable[i * 3] == h: return i
i += 1
return -1
def get_param_escape_force(self, name: str) -> int:
"""`@life(escape(...))` 强制逃逸位掩码;0 = 无该标注"""
idx: int = self._DirLookup(name)
if idx < 0: return 0
return int(self.EscDirTable[idx * 3 + 1])
def get_param_noescape_force(self, name: str) -> int:
"""`@life(noescape(...))` 强制不逃逸位掩码;0 = 无该标注"""
idx: int = self._DirLookup(name)
if idx < 0: return 0
return int(self.EscDirTable[idx * 3 + 2])
def _DirForceOf(self, name: str, noescape: int) -> int:
"""按方向取指令位:noescape!=0 → 强制不逃逸槽,否则 → 强制逃逸槽"""
if noescape != 0: return self.get_param_noescape_force(name)
return self.get_param_escape_force(name)
# _DirForceMethod - 方法版指令掩码:查 "Class.method" 并沿 MRO 回退(口径同 get_param_escapes_method)
def _DirForceMethod(self, pool: memhub.MemBuddy*, class_name: str, method_name: str, noescape: int) -> int:
if method_name is None or class_name is None: return 0
key_q: str = self._EscKey2(pool, class_name, method_name)
if key_q is not None:
m_q: int = self._DirForceOf(key_q, noescape)
if m_q != 0: return m_q
mro: sizet* = self.get_mro_chain(pool, class_name)
mi2: int = 1
while mro is not None and mro[mi2] != 0:
anc_m: str = rawptr(mro[mi2])
if anc_m is not None and string.strcmp(anc_m, class_name) != 0:
key_a: str = self._EscKey2(pool, anc_m, method_name)
if key_a is not None:
m_a: int = self._DirForceOf(key_a, noescape)
if m_a != 0: return m_a
mi2 += 1
return 0
def get_param_escape_force_method(self, pool: memhub.MemBuddy*, class_name: str, method_name: str) -> int:
"""方法版 `@life(escape(...))` 掩码;0 = 无该标注"""
return self._DirForceMethod(pool, class_name, method_name, 0)
def get_param_noescape_force_method(self, pool: memhub.MemBuddy*, class_name: str, method_name: str) -> int:
"""方法版 `@life(noescape(...))` 掩码;0 = 无该标注"""
return self._DirForceMethod(pool, class_name, method_name, 1)
# ---- P2 willFree:声明式标注表(直接来自 @life(willFree(...)) / @willFree(...),不做数据流) ----
def _WillFreeLookup(self, name: str) -> int:
"""线性查找 willFree 表项下标;未找到返回 -1"""
if name is None or self.WfTable is None: return -1
h: sizet = self._NameHash(name)
i: int = 0
while i < self.WfCount:
if self.WfTable[i * 2] == h: return i
i += 1
return -1
def get_param_willfree(self, name: str) -> int:
"""willFree 形参掩码:第 k 位=第 k 个形参在本调用后失效;无记录返回 0"""
idx: int = self._WillFreeLookup(name)
if idx < 0: return 0
return int(self.WfTable[idx * 2 + 1])
def get_param_willfree_method(self, pool: memhub.MemBuddy*, class_name: str, method_name: str) -> int:
"""方法 willFree 掩码:查 "Class.method",再沿 MRO 祖先查;无记录返回 0。
与 get_param_escapes_method 同口径:刻意**不**回退裸名键——方法名与模块级函数名可能
重名,回退会把模块函数的掩码错用到方法调用上(产生误报)。查不到即视为无标注。"""
if method_name is None or class_name is None: return 0
key_q: str = self._EscKey2(pool, class_name, method_name)
if key_q is not None:
m_q: int = self.get_param_willfree(key_q)
if m_q != 0: return m_q
mro: sizet* = self.get_mro_chain(pool, class_name)
mi2: int = 1
while mro is not None and mro[mi2] != 0:
anc_m: str = rawptr(mro[mi2])
if anc_m is not None and string.strcmp(anc_m, class_name) != 0:
key_a: str = self._EscKey2(pool, anc_m, method_name)
if key_a is not None:
m_a: int = self.get_param_willfree(key_a)
if m_a != 0: return m_a
mi2 += 1
return 0
def _RegisterFuncWillFree(self, fd: ast.FunctionDef*, key: str) -> int:
"""登记函数的 willFree 掩码(无标注也登记 0,避免同名重复计算);1=失败 / 0=成功"""
if fd is None or key is None or self.WfTable is None: return 1
if self._WillFreeLookup(key) >= 0: return 0
if self.WfCount >= 1024: return 1
mask: int = GenApi.compute_willfree_mask(fd)
slot: int = self.WfCount
self.WfTable[slot * 2 + 0] = sizet(self._NameHash(key))
self.WfTable[slot * 2 + 1] = sizet(mask)
self.WfCount = slot + 1
return 0
def compute_escape_summary(self, pool: memhub.MemBuddy*, fd: ast.FunctionDef*, gset: sizet*, box: sizet*, const_env: sizet* = None) -> int:
"""计算 fd 的摘要:box[0]=形参逃逸掩码、box[1]=返回借用位(写前清零)。
gset = global 名定长数组([0]=计数,[1..]=名哈希),box 必须在 gset 之后同一块内
(调用方给 gset+65 槽)。gset 传 None 时用本文件的 EscScratch(复用区),两者都没有
才从 pool 现分配。两条路径共用同一实现:① 本文件登记(register_file_funcs);
② .vpi 生成时把摘要落盘(v3 跨模块)。
const_env(可选)= 常量绑定环境(2 槽/形参:是否有常量 + 值)。给了它就做**跨过程常量特化**:
形参的常量绑定能把 `if` 条件折叠成常量时,死分支里的写不记为逃逸。None = 完全保守(登记路径)。"""
if fd is None or box is None: return 1
box[0] = 0
box[1] = 0
box[2] = 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)
prev_env: sizet* = self.EscConstEnv
self.EscConstEnv = const_env
self.EscIndCount = 0
self._ScanEscapes(fd, fd.children, g, box, 0)
self.EscConstEnv = prev_env
self.EscIndCount = 0
# 显式标注(声明式契约)**在扫描之后**叠加:escape 无条件置位、noescape 无条件清位。
# 顺序是关键:常量折叠发生在扫描里,所以 `escape(p)` 不会被"死分支"折掉 —— 契约的本意
# 正是"编译器看不见的路径";`noescape(p)` 也不会被任何推导重新置位。
# 输出缓冲借用 box[3]/box[4](= 槽 68/69,此刻 env 已用完),不依赖额外 scratch;
# 这两个槽同时把**声明式指令**交给调用方(_RegisterFuncEscapeSummary 落 EscDirTable →
# 调用点最后再套用一次,那是不可被推断推翻的最终裁决)。
ann_out: sizet* = sizet*(uint64(box) + 3 * 8)
if GenApi.compute_escape_directives(fd, ann_out) == 0:
ef_m: int = int(ann_out[0])
ne_m: int = int(ann_out[1])
if ef_m != 0 or ne_m != 0:
box[0] = (box[0] | sizet(ef_m)) & (~sizet(ne_m))
# 强制逃逸同样算**硬**位:调用点不能再按"同命"把它裁掉
box[2] = (box[2] | sizet(ef_m)) & (~sizet(ne_m))
return 0
# ============================================================
# §13 NLL:非词法生命期 —— 堆对象降级为帧对象
# ============================================================
# 判定"变量 v 在本函数内封闭":不外泄(判据 A)。单函数内所有局部存储同帧消亡,
# 故"末次使用在本帧内"(判据 B)自动成立 —— 判据 A 成立即可降级为 LIFE_FRAME,
# 释放复用 slife 的 RAII 出口(见 §13.3)。
# 保守原则:**任何判不定都返回 0**(调用方维持 LIFE_HEAP = 现状),别名集**只增不减**。
def _NllInSet(self, al: sizet*, name: str) -> int:
"""name 是否在别名集里([0]=计数,[1..]=名哈希);1=是 / 0=否"""
if al is None or name is None: return 0
h: sizet = self._NameHash(name)
c: sizet = al[0]
i: sizet = 0
while i < c:
if al[1 + i] == h: return 1
i += 1
return 0
def _NllAdd(self, al: sizet*, name: str) -> int:
"""并入别名集(只增不减;满 64 则忽略 —— 只增语义下忽略仍是保守的)"""
if al is None or name is None: return 0
if self._NllInSet(al, name) != 0: return 0
c: sizet = al[0]
if c >= 63: return 0
al[1 + c] = self._NameHash(name)
al[0] = c + 1
return 0
def _NllMentions(self, node: ast.AST*, al: sizet*) -> int:
"""表达式/子树里是否出现别名集里的变量(保守:拿不到子树就算"出现")"""
if node is None: return 0
k: int = node.kind()
if k == ast.ASTKind.Name:
nm: ast.Name* = ast.Name*(node)
if nm.id is not None and self._NllInSet(al, nm.id) != 0: return 1
return 0
if k == ast.ASTKind.Constant: return 0
# 属性访问:变量只可能出现在基对象里(`.attr` 是静态字符串),必须显式只递归 .value。
# 否则若该节点的 children 不含 value,就会落进下面的"拿不到子树 = 算提及"分支 →
# 把 `stdio.printf(...)` 之类误判成提及/不安全,整条 NLL 判据被否掉(现场:该误判
# 使 `h = new heap S()` 永不降级,g(l, h) 照旧报逃逸)。
if k == ast.ASTKind.Attribute:
at: ast.Attribute* = ast.Attribute*(node)
return self._NllMentions(at.value, al)
ch: list[ast.AST*]* = node.children
if ch is None: return 1
n: sizet = len(ch)
i: sizet = 0
while i < n:
if self._NllMentions(ch.get(i), al) != 0: return 1
i += 1
return 0
def _NllAliasSource(self, value: ast.AST*, al: sizet*) -> int:
"""RHS 是否"恰好是一个别名变量"(`w = v`)→ 1"""
if value is None or value.kind() != ast.ASTKind.Name: return 0
vn: ast.Name* = ast.Name*(value)
if vn.id is None: return 0
return self._NllInSet(al, vn.id)
def _NllCalleeEscMask(self, cl: ast.Call*) -> int:
"""被调者形参逃逸掩码;**-1 = 判不定**(调用方按"不安全"处理)。
只认"本文件有摘要记录的裸名函数":其掩码是本次编译重算/登记的权威值。
方法调用 / 跨模块 / 认不出的名字(含 extern C:可能留存指针)一律 -1。"""
if cl is None or cl.func is None: return -1
if cl.func.kind() != ast.ASTKind.Name: return -1
fnm: ast.Name* = ast.Name*(cl.func)
if fnm.id is None: return -1
if self.has_param_escapes(fnm.id) == 0: return -1
return self.get_param_escapes(fnm.id)
def _NllExprUnsafe(self, node: ast.AST*, al: sizet*) -> int:
"""表达式里别名是否被放到"不安全位置"→ 1(取址 / 当被调者 / 交给会逃逸的形参位)"""
if node is None: return 0
k: int = node.kind()
if k == ast.ASTKind.UnaryOp:
# 取址(以及任何一元运算):保守视为不安全
uo: ast.UnaryOp* = ast.UnaryOp*(node)
return self._NllMentions(uo.operand, al)
if k == ast.ASTKind.Call:
cl: ast.Call* = ast.Call*(node)
if self._NllMentions(cl.func, al) != 0: return 1
if cl.args is None: return 0
mask: int = self._NllCalleeEscMask(cl)
na: sizet = len(cl.args)
j: sizet = 0
while j < na:
a: ast.AST* = cl.args.get(j)
idx: int = int(j)
j += 1
# 只有"实参恰好是别名变量本身"才算交出该变量(`h.q` 只是读字段值,不算交出 h)
if a is None or a.kind() != ast.ASTKind.Name: continue
an: ast.Name* = ast.Name*(a)
if an.id is None or self._NllInSet(al, an.id) == 0: continue
if mask < 0: return 1
if ((mask >> idx) & 1) != 0: return 1
return 0
ch: list[ast.AST*]* = node.children
if ch is None: return 0
n: sizet = len(ch)
i: sizet = 0
while i < n:
if self._NllExprUnsafe(ch.get(i), al) != 0: return 1
i += 1
return 0
def _NllStmts(self, stmts: list[ast.AST*]*, al: sizet*, def_node: ast.AST*, depth: int) -> int:
"""遍历一个块;1=块内所有别名使用都帧内安全 / 0=发现外泄(放弃降级)
别名集只增不减 ⇒ 单遍即可(无需循环固定点)。控制流感知体现在"if/while/for 的
分支与循环体全部走一遍";**不进嵌套 def/class**(闭包捕获按外泄处理)。"""
if stmts is None or al is None or depth > 64: return 1
n: sizet = len(stmts)
i: sizet = 0
while i < n:
st: ast.AST* = stmts.get(i)
i += 1
if st is None: continue
k: int = st.kind()
if k == ast.ASTKind.Return:
if self._NllMentions(ast.Return*(st).value, al) != 0: return 0
elif k == ast.ASTKind.Assign:
asg: ast.Assign* = ast.Assign*(st)
if def_node is not None and asg.value is not None:
if uint64(asg.value) == uint64(def_node): continue # 定义处本身,跳过
if asg.targets is not None:
nt: sizet = len(asg.targets)
t: sizet = 0
while t < nt:
tg: ast.AST* = asg.targets.get(t)
t += 1
if tg is None: continue
if tg.kind() == ast.ASTKind.Name:
tnm: ast.Name* = ast.Name*(tg)
if tnm.id is not None:
if self._NllAliasSource(asg.value, al) != 0:
self._NllAdd(al, tnm.id) # w = v → w 也是别名
else:
if self._NllInSet(al, tnm.id) != 0: return 0 # 把别名重绑成别的
if self._NllMentions(asg.value, al) != 0: return 0
continue
if self._NllMentions(asg.value, al) != 0: return 0
if self._NllExprUnsafe(asg.value, al) != 0: return 0
elif k == ast.ASTKind.AnnAssign:
aa: ast.AnnAssign* = ast.AnnAssign*(st)
if def_node is not None and aa.value is not None:
if uint64(aa.value) == uint64(def_node): continue
if aa.target is not None and aa.target.kind() == ast.ASTKind.Name:
tnm2: ast.Name* = ast.Name*(aa.target)
if tnm2.id is not None:
if self._NllAliasSource(aa.value, al) != 0:
self._NllAdd(al, tnm2.id)
else:
if self._NllInSet(al, tnm2.id) != 0: return 0
if self._NllMentions(aa.value, al) != 0: return 0
elif self._NllMentions(aa.value, al) != 0: return 0
if self._NllExprUnsafe(aa.value, al) != 0: return 0
elif k == ast.ASTKind.Expr:
if self._NllExprUnsafe(ast.Expr*(st).value, al) != 0: return 0
elif k == ast.ASTKind.If:
ifn: ast.If* = ast.If*(st)
if self._NllExprUnsafe(ifn.test, al) != 0: return 0
if self._NllStmts(ifn.children, al, def_node, depth + 1) == 0: return 0
if self._NllStmts(ifn.orelse, al, def_node, depth + 1) == 0: return 0
elif k == ast.ASTKind.While:
wn: ast.While* = ast.While*(st)
if self._NllExprUnsafe(wn.test, al) != 0: return 0
if self._NllStmts(wn.children, al, def_node, depth + 1) == 0: return 0
if self._NllStmts(wn.orelse, al, def_node, depth + 1) == 0: return 0
elif k == ast.ASTKind.For:
fr: ast.For* = ast.For*(st)
if self._NllExprUnsafe(fr.iter, al) != 0: return 0
if self._NllStmts(fr.children, al, def_node, depth + 1) == 0: return 0
if self._NllStmts(fr.orelse, al, def_node, depth + 1) == 0: return 0
else:
# 认不出的语句(pass/assert/del/嵌套 def 等):整棵子树不提及别名就放行,
# 提及则保守拒绝(嵌套 def 捕获闭包变量走这条)。
if self._NllMentions(st, al) != 0: return 0
return 1
def NllFrameConfined(self, trans: HT.Translator*, var_name: str, def_node: ast.AST*) -> int:
"""§13.2 判据:`new heap` 出来的 var_name 在本函数内封闭(不外泄)→ 1;判不定 → 0。
def_node = 该 `new` 表达式节点本身(用来跳过"定义处那条语句"的重绑定判定)。"""
if var_name is None or self.NllCurFunc is None: return 0
fd: ast.FunctionDef* = ast.FunctionDef*(self.NllCurFunc)
if fd is None or fd.children is None: return 0
al: sizet* = sizet*(stdlib.malloc(64 * 8))
if al is None: return 0
string.memset(al, 0, 64 * 8)
al[0] = 1
al[1] = self._NameHash(var_name)
r: int = self._NllStmts(fd.children, al, def_node, 0)
stdlib.free(al)
return r
# escape_leak_line - **错误路径专用**:重扫本文件被调者函数体,取 want_pi 号形参的
# "首个逃逸写入点"行号(0 = 未知)。只在 fail-fast 报错时调用一次,不进热路径;
# 跨模块拿不到 AST(EscFuncTable 按文件重置)时返回 0,调用点就不打泄漏点帧。
def escape_leak_line(self, key: str, want_pi: int) -> int:
"""返回被调者第 want_pi 号形参的首个逃逸点行号;0=未知(含跨模块)"""
if key is None or want_pi < 0: return 0
fd: ast.FunctionDef* = self._EscFuncLookup(key)
if fd is None: return 0
if self._EnsureRefineScratch() == 0: return 0
g: sizet* = self.EscRefineScratch
if g is None: return 0
box: sizet* = sizet*(uint64(g) + EC_BOX_SLOT * 8)
# 与 compute_escape_summary 同序(先收 global 名单再扫),但**不需要**常量特化/标注叠加:
# 这里只求"首个写入点行号",保守扫描已足够。
# 扫描会动 EscConstEnv/EscIndCount/EscLeakWant,全部先存后还(调用点可能还要接着用)。
prev_env: sizet* = self.EscConstEnv
prev_ind: int = self.EscIndCount
prev_want: int = self.EscLeakWant
g[0] = 0
self._CollectGlobalNames(fd.children, g, 0)
self.EscConstEnv = None
self.EscIndCount = 0
self.EscLeakWant = want_pi
self.EscLeakLine = 0
self._ScanEscapes(fd, fd.children, g, box, 0)
ln: int = self.EscLeakLine
self.EscLeakWant = prev_want
self.EscConstEnv = prev_env
self.EscIndCount = prev_ind
return ln
# ============================================================
# 跨过程常量特化(P3 收尾):调用点用常量实参重算被调者的逃逸摘要
# ============================================================
def _EscFuncLookup(self, key: str) -> ast.FunctionDef*:
"""按名哈希查本文件已登记的 FunctionDef(键口径与 EscTable 一致);未找到返回 None"""
if key is None or self.EscFuncTable is None: return None
h: sizet = self._NameHash(key)
i: int = 0
while i < self.EscFuncCount:
if self.EscFuncTable[i * 2] == h:
v: sizet = self.EscFuncTable[i * 2 + 1]
if v == 0: return None
return ast.FunctionDef*(v)
i += 1
return None
def _EvalConstInt(self, fd: ast.FunctionDef*, node: ast.AST*, depth: int) -> int:
"""按 self.EscConstEnv 把表达式求成整数;1=求得(值写入 EscRefineScratch[EC_EVAL_SLOT])/ 0=求不出
覆盖:整型/布尔/字符字面量、已绑定的常量形参名、一元 ! / - / ~、二元 + - * & | ^ // %。
**折不动一律返回 0**(上层退回保守判定)——绝不猜值。
递归每层算完立刻把临时槽拷进局部变量(临时槽会被下一层覆写)。
"""
if node is None or depth > 32 or self.EscRefineScratch is None: return 0
k: int = node.kind()
if k == ast.ASTKind.Constant:
cn: ast.Constant* = ast.Constant*(node)
if cn is None: return 0
if cn.const_kind == ast.CONST_INT or cn.const_kind == ast.CONST_BOOL or cn.const_kind == ast.CONST_CHAR:
self.EscRefineScratch[EC_EVAL_SLOT] = sizet(cn.int_val)
return 1
return 0
if k == ast.ASTKind.Name:
if self.EscConstEnv is None: return 0
pi: int = self._ParamIndex(fd, node)
if pi < 0 or pi >= EC_ENV_MAX: return 0
if self.EscConstEnv[pi * 2] == 0: return 0
self.EscRefineScratch[EC_EVAL_SLOT] = self.EscConstEnv[pi * 2 + 1]
return 1
if k == ast.ASTKind.UnaryOp:
uo: ast.UnaryOp* = ast.UnaryOp*(node)
if uo is None: return 0
if self._EvalConstInt(fd, uo.operand, depth + 1) == 0: return 0
v: int64 = int64(self.EscRefineScratch[EC_EVAL_SLOT])
if uo.op == ast.OpKind.Not: self.EscRefineScratch[EC_EVAL_SLOT] = sizet(1 if v == 0 else 0)
elif uo.op == ast.OpKind.USub: self.EscRefineScratch[EC_EVAL_SLOT] = sizet(-v)
elif uo.op == ast.OpKind.Invert: self.EscRefineScratch[EC_EVAL_SLOT] = sizet(~v)
elif uo.op == ast.OpKind.UAdd: pass
else: return 0
return 1
if k == ast.ASTKind.BinOp:
bo: ast.BinOp* = ast.BinOp*(node)
if bo is None: return 0
if self._EvalConstInt(fd, bo.left, depth + 1) == 0: return 0
lv: int64 = int64(self.EscRefineScratch[EC_EVAL_SLOT])
if self._EvalConstInt(fd, bo.right, depth + 1) == 0: return 0
rv: int64 = int64(self.EscRefineScratch[EC_EVAL_SLOT])
res: int64 = 0
if bo.op == ast.OpKind.Add: res = lv + rv
elif bo.op == ast.OpKind.Sub: res = lv - rv
elif bo.op == ast.OpKind.Mult: res = lv * rv
elif bo.op == ast.OpKind.BitAnd: res = lv & rv
elif bo.op == ast.OpKind.BitOr: res = lv | rv
elif bo.op == ast.OpKind.BitXor: res = lv ^ rv
elif bo.op == ast.OpKind.FloorDiv:
if rv == 0: return 0
res = lv // rv
elif bo.op == ast.OpKind.Mod:
if rv == 0: return 0
res = lv % rv
else: return 0
self.EscRefineScratch[EC_EVAL_SLOT] = sizet(res)
return 1
if k == ast.ASTKind.Compare or k == ast.ASTKind.BoolOp:
cv: int = self._EvalConstCond(fd, node, depth + 1)
if cv < 0: return 0
self.EscRefineScratch[EC_EVAL_SLOT] = sizet(cv)
return 1
return 0
def _EvalConstCond(self, fd: ast.FunctionDef*, node: ast.AST*, depth: int) -> int:
"""把条件表达式折叠成常量:1=真 / 0=假 / -1=折不动(调用方保守处理)"""
if node is None or depth > 32 or self.EscRefineScratch is None: return -1
k: int = node.kind()
if k == ast.ASTKind.Compare:
cmp: ast.Compare* = ast.Compare*(node)
if cmp is None or cmp.left is None: return -1
ops_list: list[int]* = (list[int]*)(cmp.ops)
comps: list[ast.AST*]* = cmp.comparators
if ops_list is None or comps is None: return -1
# 只处理单比较器:`a < b < c` 的短路语义这里不复刻(折不动更安全)
if len(ops_list) != 1 or len(comps) != 1: return -1
# 循环归纳变量区间:`i == k` / `i < k` 且 k 落在区间外(或区间退化成单值)→ 直接定论
op_v0: int = ops_list.get(0)
ic0: int = self._IndCompare(fd, cmp.left, comps.get(0), op_v0)
if ic0 >= 0: return ic0
ic1: int = self._IndCompare(fd, comps.get(0), cmp.left, self._FlipCmpOp(op_v0))
if ic1 >= 0: return ic1
if self._EvalConstInt(fd, cmp.left, depth + 1) == 0: return -1
lv: int64 = int64(self.EscRefineScratch[EC_EVAL_SLOT])
if self._EvalConstInt(fd, comps.get(0), depth + 1) == 0: return -1
rv: int64 = int64(self.EscRefineScratch[EC_EVAL_SLOT])
op: int = ops_list.get(0)
if op == ast.OpKind.Eq or op == ast.OpKind.Is: return 1 if lv == rv else 0
if op == ast.OpKind.Ne or op == ast.OpKind.IsNot: return 1 if lv != rv else 0
if op == ast.OpKind.Lt: return 1 if lv < rv else 0
if op == ast.OpKind.Le: return 1 if lv <= rv else 0
if op == ast.OpKind.Gt: return 1 if lv > rv else 0
if op == ast.OpKind.Ge: return 1 if lv >= rv else 0
return -1
if k == ast.ASTKind.BoolOp:
bo: ast.BoolOp* = ast.BoolOp*(node)
if bo is None or bo.values is None: return -1
if bo.op != ast.OpKind.And and bo.op != ast.OpKind.Or: return -1
is_and: int = 1 if bo.op == ast.OpKind.And else 0
cnt: sizet = len(bo.values)
if cnt == 0: return -1
i: sizet = 0
while i < cnt:
v: int = self._EvalConstCond(fd, bo.values.get(i), depth + 1)
if v < 0: return -1
# 短路:and 遇假、or 遇真即可定论,其后的项折不动也不影响结论
if is_and != 0 and v == 0: return 0
if is_and == 0 and v == 1: return 1
i += 1
return 0 if is_and != 0 else 1
# 其余表达式:折成整数后按非零判真
if self._EvalConstInt(fd, node, depth + 1) == 0: return -1
return 1 if int64(self.EscRefineScratch[EC_EVAL_SLOT]) != 0 else 0
def _RefineWithConsts(self, pool: memhub.MemBuddy*, fd: ast.FunctionDef*, param_base: int, cargs: list[ast.AST*]*, can: sizet, mask: int) -> int:
"""把常量实参装进 EscConstEnv 后重算一次摘要;返回精化后的掩码(无可用常量时原样返回)"""
if self.EscRefineScratch is None or pool is None: return mask
if self.build_const_env(pool, cargs, can, param_base) == 0: return mask
env: sizet* = sizet*(uint64(self.EscRefineScratch) + EC_ENV_SLOT * 8)
box: sizet* = sizet*(uint64(self.EscRefineScratch) + EC_BOX_SLOT * 8)
self.compute_escape_summary(pool, fd, self.EscRefineScratch, box, env)
return int(box[0])
def refine_escape_mask(self, pool: memhub.MemBuddy*, name: str, param_base: int, cargs: list[ast.AST*]*, can: sizet, mask: int) -> int:
"""跨过程常量特化(普通函数):实参含编译期常量时把常量代进被调者体重算逃逸掩码。
只在**本文件**能拿到函数体时生效(EscFuncTable 按文件重置);跨模块拿不到 AST →
原样返回(保守,与不启用特化时行为一致)。param_base = 用户第一个实参对应的形参下标。"""
if name is None or mask == 0 or cargs is None or can == 0: return mask
fd: ast.FunctionDef* = self._EscFuncLookup(name)
if fd is None: return mask
return self._RefineWithConsts(pool, fd, param_base, cargs, can, mask)
def refine_escape_mask_method(self, pool: memhub.MemBuddy*, class_name: str, method_name: str, cargs: list[ast.AST*]*, can: sizet, mask: int) -> int:
"""跨过程常量特化(方法):键走 "Class.method",沿 MRO 祖先回退(口径同 get_param_escapes_method)"""
if class_name is None or method_name is None or mask == 0 or cargs is None or can == 0: return mask
fd: ast.FunctionDef* = None
key_q: str = self._EscKey2(pool, class_name, method_name)
if key_q is not None: fd = self._EscFuncLookup(key_q)
if fd is None:
mro: sizet* = self.get_mro_chain(pool, class_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, class_name) != 0:
key_a: str = self._EscKey2(pool, anc, method_name)
if key_a is not None:
fd = self._EscFuncLookup(key_a)
if fd is not None: break
mi += 1
if fd is None: return mask
return self._RefineWithConsts(pool, fd, 1, cargs, can, mask)
def _EnsureRefineScratch(self) -> int:
"""确保重算 scratch 就绪(**登记路径与 .vpi 落盘路径都会用到**——后者不走 register_file_funcs,
那时 CompilerFactory.factory / PermPool 可能还没就绪);1=可用 / 0=不可用。
用 stdlib.malloc:与文件头 _g_freed_slots 同口径,跨文件存活且不依赖任何池。"""
if self.EscRefineScratch is not None: return 1
self.EscRefineScratch = sizet*(stdlib.malloc(sizet(EC_SCRATCH_SLOTS) * 8))
if self.EscRefineScratch is None: return 0
string.memset(self.EscRefineScratch, 0, sizet(EC_SCRATCH_SLOTS) * 8)
return 1
def build_const_env(self, pool: memhub.MemBuddy*, cargs: list[ast.AST*]*, can: sizet, param_base: int) -> int:
"""把实参里的**字面量**常量装进 EscConstEnv 区(EC_ENV_SLOT 起);返回 1=装了至少一个常量
顶层实参只认字面量(及其字面量折叠):装之前把 EscConstEnv 置空,_EvalConstInt 遇形参名即放弃,
所以这里不需要调用方的函数体(跨模块调用点同样可用)。"""
if cargs is None or self._EnsureRefineScratch() == 0: return 0
env: sizet* = sizet*(uint64(self.EscRefineScratch) + EC_ENV_SLOT * 8)
string.memset(env, 0, sizet(EC_ENV_MAX) * 2 * 8)
prev_env: sizet* = self.EscConstEnv
self.EscConstEnv = None
found: int = 0
ai: sizet = 0
while ai < can:
pi: int = int(ai) + param_base
arg: ast.AST* = cargs.get(ai)
if arg is not None and pi >= 0 and pi < EC_ENV_MAX:
if self._ConstOfNode(None, arg) != 0:
env[pi * 2] = 1
env[pi * 2 + 1] = self.EscRefineScratch[EC_EVAL_SLOT]
found = 1
ai += 1
self.EscConstEnv = prev_env
return found
def _ConstOfNode(self, fd: ast.FunctionDef*, node: ast.AST*) -> int:
"""字面量常量求值(**不使用**形参绑定):1=成功(值在 EC_EVAL_SLOT)/ 0=不是常量"""
if node is None or self.EscRefineScratch is None: return 0
prev_e: sizet* = self.EscConstEnv
self.EscConstEnv = None
ok: int = self._EvalConstInt(fd, node, 0)
self.EscConstEnv = prev_e
return ok
def _AddGuardCand(self, out: sizet*, n: int, max: int, p: int, kv: int) -> int:
"""加一条守卫候选并按 (形参位, 常量) 去重;返回新条数"""
if out is None or n >= max: return n
i: int = 0
while i < n:
if int(out[i * 2]) == p and int64(out[i * 2 + 1]) == int64(kv): return n
i += 1
out[n * 2] = sizet(p)
out[n * 2 + 1] = sizet(kv)
return n + 1
def _HarvestGuardTest(self, fd: ast.FunctionDef*, node: ast.AST*, out: sizet*, n: int, max: int) -> int:
"""从一个条件表达式收守卫候选:BoolOp 递归 values;单条件只认 `p` / `not p` / `p == k` / `k == p`"""
if node is None or out is None or n >= max: return n
k: int = node.kind()
if k == ast.ASTKind.BoolOp:
bo: ast.BoolOp* = ast.BoolOp*(node)
if bo.values is None: return n
cnt: sizet = len(bo.values)
i: sizet = 0
while i < cnt and n < max:
n = self._HarvestGuardTest(fd, bo.values.get(i), out, n, max)
i += 1
return n
p: int = -1
add_false: int = 0
kv: int = 1
if k == ast.ASTKind.Name:
p = self._ParamIndex(fd, node)
add_false = 1
elif k == ast.ASTKind.UnaryOp:
uo: ast.UnaryOp* = ast.UnaryOp*(node)
if uo is not None and uo.op == ast.OpKind.Not:
p = self._ParamIndex(fd, uo.operand)
add_false = 1
elif k == ast.ASTKind.Compare:
cmp: ast.Compare* = ast.Compare*(node)
if cmp is not None:
ops_l: list[int]* = (list[int]*)(cmp.ops)
comps_l: list[ast.AST*]* = cmp.comparators
if ops_l is not None and comps_l is not None and len(ops_l) == 1 and len(comps_l) == 1:
op_v: int = ops_l.get(0)
if op_v == ast.OpKind.Eq or op_v == ast.OpKind.Ne:
p = self._ParamIndex(fd, cmp.left)
if p >= 0:
if self._ConstOfNode(fd, comps_l.get(0)) != 0: kv = int64(self.EscRefineScratch[EC_EVAL_SLOT])
else: p = -1
else:
p = self._ParamIndex(fd, comps_l.get(0))
if p >= 0:
if self._ConstOfNode(fd, cmp.left) != 0: kv = int64(self.EscRefineScratch[EC_EVAL_SLOT])
else: p = -1
if p < 0: return n
n = self._AddGuardCand(out, n, max, p, kv)
if add_false != 0: n = self._AddGuardCand(out, n, max, p, 0)
return n
def _CollectGuardCandidates(self, fd: ast.FunctionDef*, stmts: list[ast.AST*]*, out: sizet*, max: int, depth: int) -> int:
"""收 `if`/`while` 条件里的守卫候选,递归进体与 else;返回条数"""
if fd is None or stmts is None or out is None or depth > 64: return 0
n: int = 0
cnt: sizet = len(stmts)
i: sizet = 0
while i < cnt and n < max:
st: ast.AST* = stmts.get(i)
i += 1
if st is None: continue
k: int = st.kind()
if k == ast.ASTKind.If:
ifn: ast.If* = ast.If*(st)
n = self._HarvestGuardTest(fd, ifn.test, out, n, max)
n = self._CollectGuardCandidates(fd, ifn.children, out, max, depth + 1)
n = self._CollectGuardCandidates(fd, ifn.orelse, out, max, depth + 1)
elif k == ast.ASTKind.While:
whn: ast.While* = ast.While*(st)
n = self._HarvestGuardTest(fd, whn.test, out, n, max)
n = self._CollectGuardCandidates(fd, whn.children, out, max, depth + 1)
n = self._CollectGuardCandidates(fd, whn.orelse, out, max, depth + 1)
return n
def _PackEscapeClear(self, p: int, kv: int, m: int) -> int:
"""打包 (守卫形参位, 常量, 被清掉的掩码) 成一个整数;超出打包范围返回 -1(只影响精度)"""
if p < 0 or p > 255: return -1
if kv < EC_PACK_K_MIN or kv > EC_PACK_K_MAX: return -1
if m <= 0: return -1
return p + (kv & 255) * 256 + m * 65536
def compute_escape_clears(self, pool: memhub.MemBuddy*, fd: ast.FunctionDef*, out_packed: sizet*, max: int) -> int:
"""算出可落 .vpi 的"常量守卫"记录条数(0=没有)。每条 = 打包的 (守卫形参位, 常量, 被清掉的位掩码)。
含义:"若调用点把形参 p 传成常量 k,则掩码 m 里的位可以清掉"。
做法:先从函数体收守卫候选(`if <p>` / `if <p> == k` / `if not <p>` 及其 and/or 组合),
再对每个候选**真的用该绑定把摘要重算一遍**并与原始掩码求差——只记确实清掉的位,绝不猜。"""
if fd is None or out_packed is None or max <= 0: return 0
if self._EnsureRefineScratch() == 0: return 0
box_b: sizet* = sizet*(uint64(self.EscRefineScratch) + EC_BOX_SLOT * 8)
if self.compute_escape_summary(pool, fd, self.EscRefineScratch, box_b, None) != 0: return 0
base_mask: int = int(box_b[0])
if base_mask == 0: return 0
cand: sizet* = sizet*(uint64(self.EscRefineScratch) + EC_CAND_SLOT * 8)
ncand: int = self._CollectGuardCandidates(fd, fd.children, cand, EC_CAND_MAX, 0)
if ncand <= 0: return 0
env: sizet* = sizet*(uint64(self.EscRefineScratch) + EC_ENV_SLOT * 8)
n_out: int = 0
ci: int = 0
while ci < ncand and n_out < max:
p: int = int(cand[ci * 2])
kv: int = int64(cand[ci * 2 + 1])
ci += 1
if p < 0 or p >= EC_ENV_MAX: continue
string.memset(env, 0, sizet(EC_ENV_MAX) * 2 * 8)
env[p * 2] = 1
env[p * 2 + 1] = sizet(kv)
if self.compute_escape_summary(pool, fd, self.EscRefineScratch, box_b, env) == 0:
cleared: int = base_mask & (~int(box_b[0]))
pk: int = self._PackEscapeClear(p, kv, cleared)
if pk >= 0:
out_packed[n_out] = sizet(pk)
n_out += 1
return n_out
def const_env_ptr(self) -> sizet*:
"""常量环境区指针(`build_const_env` 装填的那块,2 槽/形参);None = 不可用"""
if self.EscRefineScratch is None: return None
return sizet*(uint64(self.EscRefineScratch) + EC_ENV_SLOT * 8)
def apply_escape_clears(self, packed: sizet*, n: int, mask: int) -> int:
"""按调用点常量环境(EscConstEnv 区)套用跨模块守卫记录:命中即清位;返回精化后的掩码"""
if packed is None or n <= 0 or mask == 0 or self.EscRefineScratch is None: return mask
env: sizet* = sizet*(uint64(self.EscRefineScratch) + EC_ENV_SLOT * 8)
res: int = mask
i: int = 0
while i < n:
v: int = int(packed[i])
i += 1
if v <= 0: continue
p: int = v & 255
kv: int = (v >> 8) & 255
if kv >= 128: kv -= 256
m: int = v >> 16
if p >= 0 and p < EC_ENV_MAX and env[p * 2] != 0:
if int64(env[p * 2 + 1]) == int64(kv): res = res & (~m)
return res
# ---- 循环归纳变量区间:`for i in range(<常量>)` 让 `if i == k` 这类比较能按区间判定 ----
def _IsNameNode(self, node: ast.AST*, name: str) -> int:
"""node 是名字且等于 name;1=是 / 0=否"""
if node is None or name is None or node.kind() != ast.ASTKind.Name: return 0
nm: ast.Name* = ast.Name*(node)
if nm.id is None: return 0
return 1 if string.strcmp(nm.id, name) == 0 else 0
def _IndBodyReassigns(self, name: str, stmts: list[ast.AST*]*, depth: int) -> int:
"""这些语句(含嵌套块)里是否给 name 赋过值;1=赋过 → 归纳变量的区间不可信。
Python 允许在循环里重绑循环变量,所以必须查这个;碰到看不透的块语句(with/try/match)
**一律按"赋过"处理**(宁可放弃精度,也不能把会执行到的写当成死代码)。"""
if name is None or stmts is None or depth > 64: return 1
cnt: sizet = len(stmts)
i: sizet = 0
while i < cnt:
if self._StmtAssignsName(name, stmts.get(i), depth) != 0: return 1
i += 1
return 0
def _StmtAssignsName(self, name: str, st: ast.AST*, depth: int) -> int:
"""单条语句(含其嵌套块)是否给 name 赋过值;1=赋过 / 0=没有(看不透的块语句按 1 处理)"""
if name is None or st is None or depth > 64: return 1
k: int = st.kind()
if k == ast.ASTKind.Assign:
tg: list[ast.AST*]* = ast.Assign*(st).targets
if tg is not None:
nt: sizet = len(tg)
j: sizet = 0
while j < nt:
if self._IsNameNode(tg.get(j), name) != 0: return 1
j += 1
elif k == ast.ASTKind.AnnAssign:
if self._IsNameNode(ast.AnnAssign*(st).target, name) != 0: return 1
elif k == ast.ASTKind.AugAssign:
if self._IsNameNode(ast.AugAssign*(st).target, name) != 0: return 1
elif k == ast.ASTKind.If:
ifn: ast.If* = ast.If*(st)
if self._IndBodyReassigns(name, ifn.children, depth + 1) != 0: return 1
if self._IndBodyReassigns(name, ifn.orelse, depth + 1) != 0: return 1
elif k == ast.ASTKind.While:
whn: ast.While* = ast.While*(st)
if self._IndBodyReassigns(name, whn.children, depth + 1) != 0: return 1
if self._IndBodyReassigns(name, whn.orelse, depth + 1) != 0: return 1
elif k == ast.ASTKind.For:
frn: ast.For* = ast.For*(st)
if self._IndBodyReassigns(name, frn.children, depth + 1) != 0: return 1
if self._IndBodyReassigns(name, frn.orelse, depth + 1) != 0: return 1
elif k == ast.ASTKind.With or k == ast.ASTKind.Try or k == ast.ASTKind.Match:
return 1
return 0
def _IndRangeOf(self, node: ast.AST*) -> int:
"""node 是**在册归纳变量**时返回 1 并把区间 [lo, hi] 写进 EC_IND_LO/HI;否则 0"""
if node is None or self.EscRefineScratch is None or self.EscIndCount <= 0: return 0
if node.kind() != ast.ASTKind.Name: return 0
nm: ast.Name* = ast.Name*(node)
if nm.id is None: return 0
h: sizet = self._NameHash(nm.id)
i: int = 0
while i < self.EscIndCount:
base: int = EC_IND_SLOT + i * 4
if self.EscRefineScratch[base] == h:
self.EscRefineScratch[EC_IND_LO] = self.EscRefineScratch[base + 1]
self.EscRefineScratch[EC_IND_HI] = self.EscRefineScratch[base + 2]
return 1
i += 1
return 0
def _IndCompare(self, fd: ast.FunctionDef*, a_side: ast.AST*, b_side: ast.AST*, op: int) -> int:
"""`<归纳变量> <op> <常量>` 按区间判定:1=恒真 / 0=恒假 / -1=判不定"""
if self._IndRangeOf(a_side) == 0: return -1
# 先拷出区间,再做常量侧求值(求值会用到 EC_EVAL_SLOT,且其递归理论上有机会覆写区间槽)
lo: int = int64(self.EscRefineScratch[EC_IND_LO])
hi: int = int64(self.EscRefineScratch[EC_IND_HI])
# 常量侧用当前环境求值(`i == n` 里的 n 也可能被调用点常量绑定)
if self._EvalConstInt(fd, b_side, 0) == 0: return -1
k: int = int64(self.EscRefineScratch[EC_EVAL_SLOT])
if op == ast.OpKind.Eq:
if lo == k and hi == k: return 1
if k < lo or k > hi: return 0
return -1
if op == ast.OpKind.Ne:
if lo == k and hi == k: return 0
if k < lo or k > hi: return 1
return -1
if op == ast.OpKind.Lt:
if hi < k: return 1
if lo >= k: return 0
return -1
if op == ast.OpKind.Le:
if hi <= k: return 1
if lo > k: return 0
return -1
if op == ast.OpKind.Gt:
if lo > k: return 1
if hi <= k: return 0
return -1
if op == ast.OpKind.Ge:
if lo >= k: return 1
if hi < k: return 0
return -1
return -1
def _FlipCmpOp(self, op: int) -> int:
"""`k <op> i` 等价于 `i <flip(op)> k`(等式类保持不变)"""
if op == ast.OpKind.Lt: return ast.OpKind.Gt
if op == ast.OpKind.Le: return ast.OpKind.Ge
if op == ast.OpKind.Gt: return ast.OpKind.Lt
if op == ast.OpKind.Ge: return ast.OpKind.Le
return op
def _PushInductionEntry(self, fd: ast.FunctionDef*, frn: ast.For*) -> int:
"""`for <name> in range(<常量>)`:把归纳变量区间压进表。
返回 2=区间可证为空(循环体一次都不执行)/ 1=已压入区间 / 0=判不定(调用方照旧扫)。
只认内建 `range`(本文件自己定义过 range 就放弃)、只认 step 缺省(正负步长的边界细节不猜)、
且循环体不得重绑该变量。"""
if fd is None or frn is None or self.EscRefineScratch is None: return 0
if self.EscIndCount >= EC_IND_MAX: return 0
tgt: ast.AST* = frn.target
if tgt is None or tgt.kind() != ast.ASTKind.Name: return 0
tnm: ast.Name* = ast.Name*(tgt)
if tnm.id is None: return 0
if self._IndBodyReassigns(tnm.id, frn.children, 0) != 0: return 0
it: ast.AST* = frn.iter
if it is None or it.kind() != ast.ASTKind.Call: return 0
cl: ast.Call* = ast.Call*(it)
if cl.func is None or cl.func.kind() != ast.ASTKind.Name: return 0
fnm: ast.Name* = ast.Name*(cl.func)
if fnm.id is None or string.strcmp(fnm.id, "range") != 0: return 0
if GenApi.is_file_func("range") != 0: return 0
args_i: list[ast.AST*]* = cl.args
na: sizet = 0
if args_i is not None: na = len(args_i)
if na < 1 or na > 3: return 0
start: int64 = 0
stop: int64 = 0
step: int64 = 1
if na == 1:
if self._EvalConstInt(fd, args_i.get(0), 0) == 0: return 0
stop = int64(self.EscRefineScratch[EC_EVAL_SLOT])
else:
if self._EvalConstInt(fd, args_i.get(0), 0) == 0: return 0
start = int64(self.EscRefineScratch[EC_EVAL_SLOT])
if self._EvalConstInt(fd, args_i.get(1), 0) == 0: return 0
stop = int64(self.EscRefineScratch[EC_EVAL_SLOT])
if na == 3:
if self._EvalConstInt(fd, args_i.get(2), 0) == 0: return 0
step = int64(self.EscRefineScratch[EC_EVAL_SLOT])
# step==0 是非法 range(Python 也报错)→ 不猜;正负步长各自算首尾(除法的分子都保证为正)
lo_v: int64 = 0
hi_v: int64 = 0
if step > 0:
if stop <= start: return 2
cnt_p: int64 = (stop - start + step - 1) // step
if cnt_p <= 0: return 2
lo_v = start
hi_v = start + (cnt_p - 1) * step
elif step < 0:
if start <= stop: return 2
nstep: int64 = 0 - step
cnt_n: int64 = (start - stop + nstep - 1) // nstep
if cnt_n <= 0: return 2
hi_v = start
lo_v = start + (cnt_n - 1) * step
else:
return 0
base: int = EC_IND_SLOT + self.EscIndCount * 4
self.EscRefineScratch[base] = self._NameHash(tnm.id)
self.EscRefineScratch[base + 1] = sizet(lo_v)
self.EscRefineScratch[base + 2] = sizet(hi_v)
self.EscIndCount = self.EscIndCount + 1
return 1
def _PopInductionEntry(self) -> int:
"""弹出最近压入的归纳变量区间(栈式)"""
if self.EscIndCount > 0: self.EscIndCount = self.EscIndCount - 1
return 0
def _IsSelfIncrement(self, name: str, value: ast.AST*, is_aug: int) -> int:
"""`i = i + <非负常量>`(is_aug=0)或 `i += <非负常量>`(is_aug=1);1=是
只接受**非负**增量:负增量会让计数器掉到区间下界之外,区间结论就不成立了。
增量为 0 时 i 不动、仍在原区间内,故允许。"""
if name is None or value is None: return 0
if is_aug != 0:
if self._ConstOfNode(None, value) == 0: return 0
return 1 if int64(self.EscRefineScratch[EC_EVAL_SLOT]) >= 0 else 0
if value.kind() != ast.ASTKind.BinOp: return 0
bo: ast.BinOp* = ast.BinOp*(value)
if bo is None or bo.op != ast.OpKind.Add: return 0
if self._IsNameNode(bo.left, name) == 0: return 0
if self._ConstOfNode(None, bo.right) == 0: return 0
return 1 if int64(self.EscRefineScratch[EC_EVAL_SLOT]) >= 0 else 0
def _BodyCounterIncrementLast(self, name: str, stmts: list[ast.AST*]*) -> int:
"""计数循环体的形态检查:**只有最后一条**顶层语句可以自增计数器,之前语句一律不得给它赋值。
为什么必须"自增在最后":区间只在自增**之前**成立(自增后 i 可能已越过上界),
而体内语句是按顺序执行的 —— 自增排在中间,它后面的读就不能再用区间判定。1=形态满足。"""
if name is None or stmts is None: return 0
cnt: sizet = len(stmts)
if cnt == 0: return 0
i: sizet = 0
while i + 1 < cnt:
if self._StmtAssignsName(name, stmts.get(i), 0) != 0: return 0
i += 1
last: ast.AST* = stmts.get(cnt - 1)
if last is None: return 0
lk: int = last.kind()
if lk == ast.ASTKind.Assign:
asg_l: ast.Assign* = ast.Assign*(last)
if asg_l.targets is None or len(asg_l.targets) != 1: return 0
if self._IsNameNode(asg_l.targets.get(0), name) == 0: return 0
return self._IsSelfIncrement(name, asg_l.value, 0)
if lk == ast.ASTKind.AugAssign:
au_l: ast.AugAssign* = ast.AugAssign*(last)
if self._IsNameNode(au_l.target, name) == 0: return 0
return self._IsSelfIncrement(name, au_l.value, 1)
return 0
def _TryWhileCounter(self, fd: ast.FunctionDef*, prev_stmt: ast.AST*, whn: ast.While*) -> int:
"""`i = <常量>; while i < B: …; i = i + <常量>` 计数循环:把 i 的取值区间压表。
返回 2=区间可证为空 / 1=已压入 / 0=判不定。
只认把计数器**往上界推**的 `i < B` / `i <= B`(镜像写法也认);`i != B` 这类不折。"""
if fd is None or prev_stmt is None or whn is None or self.EscRefineScratch is None: return 0
if self.EscIndCount >= EC_IND_MAX: return 0
# 初始化语句:`i = <常量>` 或 `i: int = <常量>`(带注解的写法同样常见,两种都认)
init_tgt: ast.AST* = None
init_val: ast.AST* = None
pk: int = prev_stmt.kind()
if pk == ast.ASTKind.Assign:
pn: ast.Assign* = ast.Assign*(prev_stmt)
if pn.targets is None or len(pn.targets) != 1: return 0
init_tgt = pn.targets.get(0)
init_val = pn.value
elif pk == ast.ASTKind.AnnAssign:
ini: ast.AnnAssign* = ast.AnnAssign*(prev_stmt)
init_tgt = ini.target
init_val = ini.value
else:
return 0
if init_tgt is None or init_tgt.kind() != ast.ASTKind.Name: return 0
inm: ast.Name* = ast.Name*(init_tgt)
if inm.id is None: return 0
if self._EvalConstInt(fd, init_val, 0) == 0: return 0
init_v: int64 = int64(self.EscRefineScratch[EC_EVAL_SLOT])
tst: ast.AST* = whn.test
if tst is None or tst.kind() != ast.ASTKind.Compare: return 0
cmpw: ast.Compare* = ast.Compare*(tst)
if cmpw.left is None: return 0
ops_l: list[int]* = (list[int]*)(cmpw.ops)
comps_l: list[ast.AST*]* = cmpw.comparators
if ops_l is None or comps_l is None or len(ops_l) != 1 or len(comps_l) != 1: return 0
op_w: int = ops_l.get(0)
hi_v: int64 = 0
have_hi: int = 0
if self._IsNameNode(cmpw.left, inm.id) != 0:
if self._EvalConstInt(fd, comps_l.get(0), 0) != 0:
b_v: int64 = int64(self.EscRefineScratch[EC_EVAL_SLOT])
if op_w == ast.OpKind.Lt:
hi_v = b_v - 1
have_hi = 1
elif op_w == ast.OpKind.Le:
hi_v = b_v
have_hi = 1
elif self._IsNameNode(comps_l.get(0), inm.id) != 0:
if self._EvalConstInt(fd, cmpw.left, 0) != 0:
b_v2: int64 = int64(self.EscRefineScratch[EC_EVAL_SLOT])
if op_w == ast.OpKind.Gt:
hi_v = b_v2 - 1
have_hi = 1
elif op_w == ast.OpKind.Ge:
hi_v = b_v2
have_hi = 1
if have_hi == 0: return 0
if hi_v < init_v: return 2
if self._BodyCounterIncrementLast(inm.id, whn.children) == 0: return 0
base: int = EC_IND_SLOT + self.EscIndCount * 4
self.EscRefineScratch[base] = self._NameHash(inm.id)
self.EscRefineScratch[base + 1] = sizet(init_v)
self.EscRefineScratch[base + 2] = sizet(hi_v)
self.EscIndCount = self.EscIndCount + 1
return 1
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/box 复用 EscScratch([0]=global 计数,[1..]=名哈希;box 在偏移 65 槽 = 520 字节)
box: sizet* = sizet*(uint64(self.EscScratch) + 520)
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]
self.EscTable[slot * 3 + 2] = box[1]
self.EscCount = slot + 1
# 硬逃逸位表(同键口径):仅在非空时登记,"查不到" = "没有硬逃逸位"
if self.EscFieldTable is not None and box[2] != 0 and self.EscFieldCount < 1024:
fld_s: int = self.EscFieldCount
self.EscFieldTable[fld_s * 2 + 0] = sizet(self._NameHash(key))
self.EscFieldTable[fld_s * 2 + 1] = box[2]
self.EscFieldCount = fld_s + 1
# 记录 FunctionDef 指针:调用点用常量实参重算摘要时要把常量代进这份函数体。
if self.EscFuncTable is not None and self.EscFuncCount < 1024:
fs: int = self.EscFuncCount
self.EscFuncTable[fs * 2 + 0] = sizet(self._NameHash(key))
self.EscFuncTable[fs * 2 + 1] = sizet(fd)
self.EscFuncCount = fs + 1
# 声明式指令(@life(escape/noescape))单独落表:调用点在所有推断/裁剪之后最后套用。
# box[3]/box[4] 由 compute_escape_summary 借给 compute_escape_directives 当输出(无标注时恒 0)。
if self.EscDirTable is not None and self.EscDirCount < 1024:
if box[3] != 0 or box[4] != 0:
d_s: int = self.EscDirCount
self.EscDirTable[d_s * 3 + 0] = sizet(self._NameHash(key))
self.EscDirTable[d_s * 3 + 1] = box[3]
self.EscDirTable[d_s * 3 + 2] = box[4]
self.EscDirCount = d_s + 1
return 0
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):
st: ast.AST* = stmts.get(i)
if st is None: continue
k: int = st.kind()
if k == ast.ASTKind.Global:
gn: ast.Global* = ast.Global*(st)
if gn is not None and gn.names is not None:
cg: sizet = len(gn.names)
for j in range(cg):
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:
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)
self._CollectGlobalNames(ifn.orelse, gset, depth + 1)
elif k == ast.ASTKind.While:
whn: ast.While* = ast.While*(st)
self._CollectGlobalNames(whn.children, gset, depth + 1)
self._CollectGlobalNames(whn.orelse, gset, depth + 1)
elif k == ast.ASTKind.For:
frn: ast.For* = ast.For*(st)
self._CollectGlobalNames(frn.children, gset, depth + 1)
self._CollectGlobalNames(frn.orelse, gset, depth + 1)
return 0
def _ParamIndex(self, fd: ast.FunctionDef*, value: ast.AST*) -> int:
"""值节点是形参名时返回其下标,否则 -1"""
if value is None or value.kind() != ast.ASTKind.Name: return -1
vnm: ast.Name* = ast.Name*(value)
if vnm.id is None: return -1
# 形参注解可能退化为 ast.AST*(AST 只有 children,没有 args)→ 使用点逐级显式重转
fdef: ast.FunctionDef* = ast.FunctionDef*(fd)
if fdef is None: return -1
# FunctionDef.args 的成员类型在 TPC 侧只登记到 AST*,二级 .args 需再转一次
agr: ast.Arguments* = ast.Arguments*(fdef.args)
if agr is None or agr.args is None: return -1
pargs: list[ast.AST*]* = agr.args
if pargs is None: return -1
cnt: sizet = len(pargs)
for i in range(cnt):
an: ast.AST* = pargs.get(i)
if an is None: continue
pa: ast.Arg* = ast.Arg*(an)
if pa.arg is not None and string.strcmp(pa.arg, vnm.id) == 0: return int(i)
return -1
# _NoteLeak - 逃逸点采集(**只走错误路径**):escape_leak_line 重扫前把想要的形参位放进
# EscLeakWant,这里在"置该位逃逸"的三个来源(字段/全局写、return 形参、传递闭包调用)
# 各调一次,记下**首个**造成逃逸的源码行号。
def _NoteLeak(self, pi: int, node: ast.AST*) -> int:
if self.EscLeakWant < 0 or pi != self.EscLeakWant or self.EscLeakLine != 0: return 0
if node is not None: self.EscLeakLine = node.lineno
return 0
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
tk: int = target.kind()
if tk == ast.ASTKind.Attribute:
box[0] = box[0] | (sizet(1) << pi)
# box[2] = **硬**逃逸位:不含"写进**形参对象**字段"这一类 —— 那类是否真逃逸取决于
# 调用点交出的对象是否长命(`g(l, &local_s)` 两都在本帧 → 同命,不报),留给调用点精化。
# 只有 base 不是形参(全局对象 / 未定)时才无条件算硬逃逸。
hard_f: int = 1
at_m: ast.Attribute* = ast.Attribute*(target)
if at_m.value is not None:
if self._ParamIndex(fd, at_m.value) >= 0: hard_f = 0
if hard_f != 0: box[2] = box[2] | (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 self._IsGlobalNm(gset, tnm.id) != 0:
box[0] = box[0] | (sizet(1) << pi)
box[2] = box[2] | (sizet(1) << pi)
# 逃逸点采集(只在本类 escape_leak_line 指定的形参位上生效;登记/落盘路径 EscLeakWant=-1)
self._NoteLeak(pi, target)
return 0
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)
for i in range(n):
st: ast.AST* = stmts.get(i)
if st is None: continue
k: int = st.kind()
# v2 传递闭包:语句的值若是调用 g(...),把 g 的逃逸位经实参传递上来
vnode: ast.AST* = None
if k == ast.ASTKind.Return:
vnode = ast.Return*(st).value
elif k == ast.ASTKind.Assign:
vnode = ast.Assign*(st).value
elif k == ast.ASTKind.AnnAssign:
vnode = ast.AnnAssign*(st).value
elif k == ast.ASTKind.Expr:
vnode = ast.Expr*(st).value
if vnode is not None and vnode.kind() == ast.ASTKind.Call:
self._PropagateCallEscapes(fd, ast.Call*(vnode), box)
if k == ast.ASTKind.Return:
rt: ast.Return* = ast.Return*(st)
pi: int = self._ParamIndex(fd, rt.value)
if pi >= 0:
box[0] = box[0] | (sizet(1) << pi)
box[1] = 1
# 返回形参 = 硬逃逸(调用点无法再判定去向)
box[2] = box[2] | (sizet(1) << pi)
self._NoteLeak(pi, st)
elif k == ast.ASTKind.Assign:
asg: ast.Assign* = ast.Assign*(st)
if asg.targets is not None:
nt: sizet = len(asg.targets)
for t in range(nt):
self._MarkEscapingStore(fd, asg.targets.get(t), asg.value, gset, box)
elif k == ast.ASTKind.AnnAssign:
aa: ast.AnnAssign* = ast.AnnAssign*(st)
self._MarkEscapingStore(fd, aa.target, aa.value, gset, box)
elif k == ast.ASTKind.If:
ifn: ast.If* = ast.If*(st)
# 跨过程常量特化:形参的常量绑定能把条件折叠成常量时,只扫**被走到**的那一支
# (另一支是死代码,里面的 `a = x` 不会执行 → 不该记成逃逸)。折不出来就退回
# 保守的"两支都扫"(行为与不启用特化时完全一致)。
cond_v: int = self._EvalConstCond(fd, ifn.test, 0)
if cond_v == 1:
self._ScanEscapes(fd, ifn.children, gset, box, depth + 1)
elif cond_v == 0:
self._ScanEscapes(fd, ifn.orelse, gset, box, depth + 1)
else:
self._ScanEscapes(fd, ifn.children, gset, box, depth + 1)
self._ScanEscapes(fd, ifn.orelse, gset, box, depth + 1)
elif k == ast.ASTKind.While:
whn: ast.While* = ast.While*(st)
# 常量特化:`while <常量假>:` 的循环体一次都不执行 → 不扫(`while <常量真>` 体必执行,
# 与不启用特化时一致)。else 支一律照扫:能否到达还取决于循环体里有没有 break,
# 那个不折(保守,宁可多扫)。
wcond_v: int = self._EvalConstCond(fd, whn.test, 0)
if wcond_v == 0:
self._ScanEscapes(fd, whn.orelse, gset, box, depth + 1)
else:
# 计数循环 `i = <常量>; while i < B: …; i = i + k`:把 i 的区间压表供体内判定
prev_st: ast.AST* = None
if i > 0: prev_st = stmts.get(i - 1)
ind_w: int = self._TryWhileCounter(fd, prev_st, whn)
if ind_w != 2:
self._ScanEscapes(fd, whn.children, gset, box, depth + 1)
if ind_w == 1: self._PopInductionEntry()
self._ScanEscapes(fd, whn.orelse, gset, box, depth + 1)
elif k == ast.ASTKind.For:
frn: ast.For* = ast.For*(st)
# 零次迭代 → 不扫循环体:① 字面量空列表;② `range(<常量>)` 的区间可证为空。
empty_iter: int = 0
if frn.iter is not None and frn.iter.kind() == ast.ASTKind.List:
it_elts: list[ast.AST*]* = ast.List*(frn.iter).elts
if it_elts is None or len(it_elts) == 0: empty_iter = 1
# 归纳变量区间:`for i in range(<常量>)` 把 i 的取值区间压表,供体内 `if i == k` 按区间判定
ind: int = 0
if empty_iter == 0: ind = self._PushInductionEntry(fd, frn)
if empty_iter == 0 and ind != 2:
self._ScanEscapes(fd, frn.children, gset, box, depth + 1)
if ind == 1: self._PopInductionEntry()
self._ScanEscapes(fd, frn.orelse, gset, box, depth + 1)
return 0
def _PropagateCallEscapes(self, fd: ast.FunctionDef*, cl: ast.Call*, box: sizet*) -> int:
"""v2 传递:`g(p, ...)` 里 g 会逃逸的实参位若绑到本函数形参 p → p 也逃逸"""
if fd is None or cl is None or box is None: return 0
if cl.func is None or cl.func.kind() != ast.ASTKind.Name: return 0
cnm: ast.Name* = ast.Name*(cl.func)
if cnm.id is None: return 0
gmask: int = self.get_param_escapes(cnm.id)
if gmask == 0: return 0
if cl.args is None: return 0
na: sizet = len(cl.args)
for j in range(na):
if ((gmask >> int(j)) & 1) == 0: continue
pi: int = self._ParamIndex(fd, cl.args.get(j))
if pi >= 0:
box[0] = box[0] | (sizet(1) << pi)
# 传递闭包推上来的位同样算**硬**逃逸(被调者可能把它写进全局,这里看不到具体去向)
box[2] = box[2] | (sizet(1) << pi)
# 泄漏点 = 本函数里这次"往下传"的调用行
self._NoteLeak(pi, cl)
return 0
# ---- M2.3 覆盖面扩展:类方法与嵌套函数的摘要 ----
# 键约定(注册与查询必须一致):
# 模块级函数 / 嵌套函数 → **裸名**(与调用点的 func_name 一致);
# 类方法 → "Class.method"(调用点拿得到 class_name,沿 MRO 回退)。
def _EscKey2(self, pool: memhub.MemBuddy*, a: str, b: str) -> str:
"""构造 "a.b" 键(池上字符串,仅供立即哈希,不常驻)"""
if pool is None or a is None or b is None: return None
klen: sizet = string.strlen(a) + string.strlen(b) + 2
kb: str = pool.alloc(klen)
if kb is None: return None
viperlib.snprintf(kb, klen, "%s.%s", a, b)
return kb
def _RegisterClassMethods(self, pool: memhub.MemBuddy*, cd: ast.ClassDef*) -> int:
"""登记一个类里各方法的摘要(键 "Class.method"),并登记方法体内的嵌套函数"""
if cd is None or cd.name is None: return 0
mch: list[ast.AST*]* = cd.children
if mch is None: return 0
mn: sizet = len(mch)
for mi in range(mn):
m_c: ast.AST* = mch.get(mi)
if m_c is None: continue
if m_c.kind() != ast.ASTKind.FunctionDef: continue
mfd: ast.FunctionDef* = ast.FunctionDef*(m_c)
if mfd is None or mfd.name is None: continue
key_m: str = self._EscKey2(pool, cd.name, mfd.name)
if key_m is not None: self._RegisterFuncEscapeSummary(pool, mfd, key_m)
if key_m is not None: self._RegisterFuncWillFree(mfd, key_m)
self._RegisterNestedFuncs(pool, mfd.children, 0)
return 0
def _RegisterNestedFuncs(self, pool: memhub.MemBuddy*, stmts: list[ast.AST*]*, depth: int) -> int:
"""登记 stmts 中的嵌套 def(裸名键,与调用点一致),递归进其体与 if/while/for 块"""
if stmts is None or depth > 64: return 0
n: sizet = len(stmts)
for i in range(n):
st: ast.AST* = stmts.get(i)
if st is None: continue
k: int = st.kind()
if k == ast.ASTKind.FunctionDef:
nfd: ast.FunctionDef* = ast.FunctionDef*(st)
if nfd is not None and nfd.name is not None:
self._RegisterFuncEscapeSummary(pool, nfd, nfd.name)
self._RegisterFuncWillFree(nfd, nfd.name)
self._RegisterNestedFuncs(pool, nfd.children, depth + 1)
elif k == ast.ASTKind.If:
ifn: ast.If* = ast.If*(st)
self._RegisterNestedFuncs(pool, ifn.children, depth + 1)
self._RegisterNestedFuncs(pool, ifn.orelse, depth + 1)
elif k == ast.ASTKind.While:
whn: ast.While* = ast.While*(st)
self._RegisterNestedFuncs(pool, whn.children, depth + 1)
self._RegisterNestedFuncs(pool, whn.orelse, depth + 1)
elif k == ast.ASTKind.For:
frn: ast.For* = ast.For*(st)
self._RegisterNestedFuncs(pool, frn.children, depth + 1)
self._RegisterNestedFuncs(pool, frn.orelse, depth + 1)
return 0
def get_param_escapes_method(self, pool: memhub.MemBuddy*, class_name: str, method_name: str) -> int:
"""方法形参逃逸掩码:查 "Class.method",再沿 MRO 祖先查;无记录返回 0。
刻意**不**回退裸名键:方法名与模块级函数名可能重名(如 error/init),回退会把
模块函数的掩码错用到方法调用上,产生误报。查不到即视为无摘要(保守放行)。"""
if method_name is None or class_name is None: return 0
key_q: str = self._EscKey2(pool, class_name, method_name)
if key_q is not None:
m_q: int = self.get_param_escapes(key_q)
if m_q != 0: return m_q
mro: sizet* = self.get_mro_chain(pool, class_name)
mi2: int = 1
while mro is not None and mro[mi2] != 0:
anc_m: str = rawptr(mro[mi2])
if anc_m is not None and string.strcmp(anc_m, class_name) != 0:
key_a: str = self._EscKey2(pool, anc_m, method_name)
if key_a is not None:
m_a: int = self.get_param_escapes(key_a)
if m_a != 0: return m_a
mi2 += 1
return 0
def _RescanOne(self, fd: ast.FunctionDef*, key: str) -> int:
"""一次迭代中重扫单个函数:以既有掩码为起点做单调 OR-in"""
if fd is None or key is None or self.EscTable is None or self.EscScratch is None: return 1
idx: int = self._EscLookup(key)
if idx < 0: return 0
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)
self.EscTable[idx * 3 + 1] = box[0]
self.EscTable[idx * 3 + 2] = box[1]
return 0
def _RescanNested(self, pool: memhub.MemBuddy*, stmts: list[ast.AST*]*, depth: int) -> int:
"""重扫 stmts 里的嵌套 def(裸名键),递归进其体与 if/while/for"""
if stmts is None or depth > 64: return 0
n: sizet = len(stmts)
for i in range(n):
st: ast.AST* = stmts.get(i)
if st is None: continue
k: int = st.kind()
if k == ast.ASTKind.FunctionDef:
nfd: ast.FunctionDef* = ast.FunctionDef*(st)
if nfd is not None and nfd.name is not None:
self._RescanOne(nfd, nfd.name)
self._RescanNested(pool, nfd.children, depth + 1)
elif k == ast.ASTKind.If:
ifn: ast.If* = ast.If*(st)
self._RescanNested(pool, ifn.children, depth + 1)
self._RescanNested(pool, ifn.orelse, depth + 1)
elif k == ast.ASTKind.While:
whn: ast.While* = ast.While*(st)
self._RescanNested(pool, whn.children, depth + 1)
self._RescanNested(pool, whn.orelse, depth + 1)
elif k == ast.ASTKind.For:
frn: ast.For* = ast.For*(st)
self._RescanNested(pool, frn.children, depth + 1)
self._RescanNested(pool, frn.orelse, depth + 1)
return 0
def _RescanClassMethods(self, pool: memhub.MemBuddy*, cd: ast.ClassDef*) -> int:
"""重扫一个类的方法(键 "Class.method")与其方法体内的嵌套函数"""
if cd is None or cd.name is None: return 0
mch: list[ast.AST*]* = cd.children
if mch is None: return 0
mn: sizet = len(mch)
for mi in range(mn):
m_c: ast.AST* = mch.get(mi)
if m_c is None: continue
if m_c.kind() != ast.ASTKind.FunctionDef: continue
mfd: ast.FunctionDef* = ast.FunctionDef*(m_c)
if mfd is None or mfd.name is None: continue
key_m: str = self._EscKey2(pool, cd.name, mfd.name)
if key_m is not None: self._RescanOne(mfd, key_m)
self._RescanNested(pool, mfd.children, 0)
return 0
def _RescanEscapes(self, pool: memhub.MemBuddy*, tree: ast.AST*) -> int:
"""v2 传递闭包的一次迭代:重扫本文件全部函数(模块级 + 类方法 + 嵌套),单调 OR-in"""
if pool is None or tree is None or self.EscTable is None: return 1
ch: list[ast.AST*]* = tree.children
if ch is None: return 1
cn: sizet = len(ch)
for ci in range(cn):
child: ast.AST* = ch.get(ci)
if child is None: continue
kc: int = child.kind()
if kc == ast.ASTKind.FunctionDef:
fd: ast.FunctionDef* = ast.FunctionDef*(child)
if fd is None or fd.name is None: continue
self._RescanOne(fd, fd.name)
self._RescanNested(pool, fd.children, 0)
elif kc == ast.ASTKind.ClassDef:
cd_r: ast.ClassDef* = ast.ClassDef*(child)
self._RescanClassMethods(pool, cd_r)
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
def init_struct_table(self, pool: memhub.MemBuddy*) -> int:
"""Initialize the struct registry; returns 1 on success, using the perm pool in 1b."""
if self.StructTable is not None: return 1
entry_size: sizet = GenTypes.StructEntry.__sizeof__()
table_pool: memhub.MemBuddy* = pool
if self.StdlibAlloc != 0:
table_pool = CompilerFactory.factory.PermPool
if table_pool is None: table_pool = pool
self.StructTable = table_pool.alloc(entry_size * STRUCT_MAX)
if self.StructTable is None: return 0
string.memset(self.StructTable, 0, entry_size * STRUCT_MAX)
self.StructCount = 0
# Build the name | type index (allocated from the same pool as the table, so they live and die together).
# The index is an optional accelerator: on allocation failure keep None with StructIdxBuilt=-1 and lookups fall back to the original linear scan (identical semantics), so initialization does not fail.
idx_bytes: sizet = sizet(STRUCT_IDX_CAP) * 8
name_idx: sizet* = table_pool.alloc(idx_bytes)
type_idx: sizet* = table_pool.alloc(idx_bytes)
if name_idx is None or type_idx is None:
self.StructNameIdx = None
self.StructTypeIdx = None
self.StructIdxCap = 0
self.StructIdxBuilt = -1
else:
string.memset(name_idx, 0, idx_bytes)
string.memset(type_idx, 0, idx_bytes)
self.StructNameIdx = name_idx
self.StructTypeIdx = type_idx
self.StructIdxCap = STRUCT_IDX_CAP
self.StructIdxBuilt = 0
return 1
def _get_struct_entry(self, i: int) -> GenTypes.StructEntry*:
"""Return struct entry i, or None when out of range."""
if self.StructTable is None or i < 0 or i >= STRUCT_MAX: return None
entry_size: sizet = GenTypes.StructEntry.__sizeof__()
addr: uint64 = uint64(self.StructTable) + i * entry_size
return GenTypes.StructEntry*(addr)
# Name | type index (open addressing with linear probing; slot stores index+1, 0 = empty).
# Reduces find_struct/find_struct_by_module (name) and find_struct_by_type (type pointer) from O(n) to expected O(1); conflicts compare keys (strcmp for names, Ty pointer value for types). Trusted only when StructIdxBuilt == StructCount; otherwise fall back to a linear scan. Neutralized entries (Name[0]=0 / Ty=None) leave stale keys that simply miss on probe.
def _struct_entry_name(self, i: int) -> str:
"""Return entry i's Name for index probing, or None when out of range or empty."""
e: GenTypes.StructEntry* = self._get_struct_entry(i)
if e is None: return None
return e.Name
def _struct_entry_tyval(self, i: int) -> uint64:
"""Return entry i's Ty pointer value, or 0 when Ty is None."""
e: GenTypes.StructEntry* = self._get_struct_entry(i)
if e is None or e.Ty is None: return 0
return uint64(e.Ty)
def _struct_name_idx_insert(self, ei: int, name: str) -> int:
"""Insert (name, ei) into the name index; returns 0 on success, 1 on failure."""
base: sizet* = self.StructNameIdx
if base is None or self.StructIdxCap <= 0: return 1
h: uint64 = _struct_name_hash(name)
mask: sizet = sizet(self.StructIdxCap - 1)
idx: sizet = sizet(h) & mask
for i in range(self.StructIdxCap):
slot: sizet* = (sizet*)(uint64(base) + uint64(idx * 8))
if slot[0] == 0:
slot[0] = sizet(ei + 1)
return 0
idx = (idx + 1) & mask
return 1
def _struct_type_idx_insert(self, ei: int, tyval: uint64) -> int:
"""Insert (Ty pointer value, ei) into the type index; returns 0 on success, 1 on failure."""
base: sizet* = self.StructTypeIdx
if base is None or self.StructIdxCap <= 0 or tyval == 0: return 1
h: uint64 = _struct_ty_hash(tyval)
mask: sizet = sizet(self.StructIdxCap - 1)
idx: sizet = sizet(h) & mask
for i in range(self.StructIdxCap):
slot: sizet* = (sizet*)(uint64(base) + uint64(idx * 8))
if slot[0] == 0:
slot[0] = sizet(ei + 1)
return 0
idx = (idx + 1) & mask
return 1
def _struct_idx_add(self, ei: int, entry: GenTypes.StructEntry*) -> None:
"""Sync both indexes after adding an entry; mark the index untrusted on any write failure."""
if self.StructNameIdx is None or self.StructTypeIdx is None or self.StructIdxCap <= 0:
self.StructIdxBuilt = -1
return
failed: int = 0
if entry is not None and entry.Name is not None: failed = failed + self._struct_name_idx_insert(ei, entry.Name)
if entry is not None and entry.Ty is not None: failed = failed + self._struct_type_idx_insert(ei, uint64(entry.Ty))
if failed != 0: self.StructIdxBuilt = -1
else:
# ei = StructCount-1, so after syncing indexed entries == StructCount.
self.StructIdxBuilt = self.StructCount
def _struct_name_idx_find(self, name: str) -> int:
"""Return the first registration index for a name (>=0 hit), else -1 to fall back to a scan."""
base: sizet* = self.StructNameIdx
if base is None or self.StructIdxCap <= 0 or self.StructIdxBuilt != self.StructCount: return -1
# Empty-name ("") special case: a neutralized entry's Name is exactly "" (Name[0]=0), and the original scan would hit the first such entry, but index slots are hashed by the original name, so an empty name always falls back to the original scan for byte-consistent behavior.
if name is None or name[0] == '\0': return -1
h: uint64 = _struct_name_hash(name)
mask: sizet = sizet(self.StructIdxCap - 1)
idx: sizet = sizet(h) & mask
for i in range(self.StructIdxCap):
slot: sizet* = (sizet*)(uint64(base) + uint64(idx * 8))
sv: sizet = slot[0]
if sv == 0: return -1
se: int = int(sv) - 1
ek: str = self._struct_entry_name(se)
if ek is not None and string.strcmp(ek, name) == 0: return se
idx = (idx + 1) & mask
return -1
def _struct_type_idx_find(self, tyval: uint64) -> int:
"""Return the first registration index for a Ty value (>=0 hit), else -1 to fall back to a scan."""
base: sizet* = self.StructTypeIdx
if base is None or self.StructIdxCap <= 0 or tyval == 0: return -1
if self.StructIdxBuilt != self.StructCount: return -1
h: uint64 = _struct_ty_hash(tyval)
mask: sizet = sizet(self.StructIdxCap - 1)
idx: sizet = sizet(h) & mask
for i in range(self.StructIdxCap):
slot: sizet* = (sizet*)(uint64(base) + uint64(idx * 8))
sv: sizet = slot[0]
if sv == 0: return -1
se: int = int(sv) - 1
if self._struct_entry_tyval(se) == tyval: return se
idx = (idx + 1) & mask
return -1
def _get_field_entry(self, struct_entry: GenTypes.StructEntry*, i: int) -> GenTypes.FieldEntry*:
"""Return field entry i of a struct, or None when out of range."""
if struct_entry is None or i < 0 or i >= FIELD_MAX: return None
field_size: sizet = GenTypes.FieldEntry.__sizeof__()
addr: uint64 = uint64(struct_entry.Fields) + i * field_size
return GenTypes.FieldEntry*(addr)
def register_struct(self, pool: memhub.MemBuddy*, name: str, struct_ty: mlirlite.LLVMType*, sha1: str = None) -> GenTypes.StructEntry*:
"""Register a struct type by type pointer and return its StructEntry pointer."""
if self.init_struct_table(pool) == 0: return None
# De-duplicate by type pointer (the same type is not re-registered; supports cross-module same-named classes).
# Compare uint64 pointer values (is-comparison triggers a type-safety check that dereferences a dangling Ty). find_struct_by_type's semantics = "first entry with equal Ty pointer" (first registration wins), byte-identical to the original scan.
existing: GenTypes.StructEntry* = self.find_struct_by_type(struct_ty)
if existing is not None: return existing
if self.StructCount >= STRUCT_MAX:
VLogger.error(f"table full, cannot register {name}", "STRUCT")
return None
entry: GenTypes.StructEntry* = self._get_struct_entry(self.StructCount)
if entry is None: return None
# Allocate the field array (in 1b use the perm pool so Fields does not dangle after reset).
field_size: sizet = GenTypes.FieldEntry.__sizeof__()
if self.StdlibAlloc != 0:
perm_sg: memhub.MemBuddy* = CompilerFactory.factory.PermPool
if perm_sg is None: perm_sg = pool
entry.Fields = perm_sg.alloc(field_size * FIELD_MAX)
else: entry.Fields = pool.alloc(field_size * FIELD_MAX)
if entry.Fields is None: return None
string.memset(entry.Fields, 0, field_size * FIELD_MAX)
# Copy the class name (1b StdlibAlloc=1 uses the perm pool to survive reset; 1a uses the main pool, which also survives before mark1). Not copying would make find_struct_by_module's strcmp read a dangling Name.
name_pool: memhub.MemBuddy* = pool
if self.StdlibAlloc != 0:
name_pool = CompilerFactory.factory.PermPool
if name_pool is None: name_pool = pool
name_len: sizet = string.strlen(name)
name_buf: str = name_pool.alloc(name_len + 1)
if name_buf is not None:
string.strcpy(name_buf, name)
entry.Name = name_buf
entry.Ty = struct_ty
entry.FieldCount = 0
# Set SHA1 directly at registration (avoids set_struct_sha1 picking the wrong entry for cross-module same-named classes); SHA1 is also allocated per StdlibAlloc to survive 1a/1b and prevent a dangling strcmp.
if sha1 is not None:
sha1_len: sizet = string.strlen(sha1)
sha1_buf: str = name_pool.alloc(sha1_len + 1)
if sha1_buf is not None:
string.strcpy(sha1_buf, sha1)
entry.ModuleSha1 = sha1_buf
self.StructCount += 1
# Sync the name | type index (new entry index = StructCount-1).
self._struct_idx_add(self.StructCount - 1, entry)
return entry
# ensure_forward_class - forward-declare a class by "class name + defining-module SHA1".
# Aligns TPC semantics: an unregistered class becomes a named empty CStruct placeholder filled in later, so a pointer to an unregistered class returns Ptr(placeholder.Ty) instead of an anonymous Ptr(i8) that loses class identity; the definition side fills the same Ty in place. Idempotent (returns an existing (class_name, sha1) entry); returns None on invalid args or a full table.
def ensure_forward_class(self, pool: memhub.MemBuddy*, sha1: str, class_name: str) -> GenTypes.StructEntry*:
if pool is None or sha1 is None or class_name is None: return None
if sha1[0] == '\0' or class_name[0] == '\0': return None
existing: GenTypes.StructEntry* = self.find_struct_by_module(class_name, sha1)
if existing is not None: return existing
if self.init_struct_table(pool) == 0: return None
if self.StructCount >= STRUCT_MAX:
VLogger.error(f"table full, cannot forward-declare {class_name}", "STRUCT")
return None
# Named opaque struct "%{sha}.{Class}" (fields=None, fcount=0): the name matches ClassGenDef's "sha1.ClassName" verbatim so the definition side can locate and fill it in place.
# Name and Ty must use a persistent pool per StdlibAlloc (same as register_struct): the placeholder survives this file's reset, so Ty/Name in a transient pool would dangle.
fwd_pool: memhub.MemBuddy* = pool
if self.StdlibAlloc != 0:
fwd_pool = CompilerFactory.factory.PermPool
if fwd_pool is None: fwd_pool = pool
sh_len: sizet = string.strlen(sha1)
cn_len: sizet = string.strlen(class_name)
name_buf: str = fwd_pool.alloc(sh_len + 1 + cn_len + 1)
if name_buf is None: return None
string.strcpy(name_buf, sha1)
name_buf[sh_len] = '.'
string.strcpy(name_buf + sh_len + 1, class_name)
fwd_ty: mlirlite.LLVMType* = mlirlite.Struct(fwd_pool, None, 0, name_buf)
if fwd_ty is None: return None
entry: GenTypes.StructEntry* = self.register_struct(pool, class_name, fwd_ty, sha1)
if entry is None: return None
entry.IsForwardDecl = 1
self.add_visible_struct(pool, class_name)
return entry
def register_struct_alias(self, pool: memhub.MemBuddy*, name: str, sha1: str, src_entry: GenTypes.StructEntry*) -> GenTypes.StructEntry*:
"""Append a package re-export alias entry for an already-registered class."""
if name is None or sha1 is None or src_entry is None: return None
if self.init_struct_table(pool) == 0: return None
# Idempotent: skip if (name, sha1) is already registered (find_struct_by_module does not fall back to the bare name).
if sha1[0] != '\0':
existing: GenTypes.StructEntry* = self.find_struct_by_module(name, sha1)
if existing is not None: return existing
if self.StructCount >= STRUCT_MAX: return None
entry: GenTypes.StructEntry* = self._get_struct_entry(self.StructCount)
if entry is None: return None
# Copy the class name and SHA1 (alias registration also picks the allocator per StdlibAlloc to survive 1a/1b reset).
alias_pool: memhub.MemBuddy* = pool
if self.StdlibAlloc != 0:
alias_pool = CompilerFactory.factory.PermPool
if alias_pool is None: alias_pool = pool
name_len: sizet = string.strlen(name)
name_buf: str = alias_pool.alloc(name_len + 1)
if name_buf is None: return None
string.strcpy(name_buf, name)
entry.Name = name_buf
sha1_len: sizet = string.strlen(sha1)
sha1_buf: str = alias_pool.alloc(sha1_len + 1)
if sha1_buf is None: return None
string.strcpy(sha1_buf, sha1)
entry.ModuleSha1 = sha1_buf
# Reuse the source entry's type, fields and class attributes (same class, only the owning module differs).
entry.Ty = src_entry.Ty
entry.FieldCount = src_entry.FieldCount
entry.Fields = src_entry.Fields
entry.IsUnion = src_entry.IsUnion
entry.IsPacked = src_entry.IsPacked
entry.IsOOP = src_entry.IsOOP
entry.HasInit = src_entry.HasInit
entry.HasNew = src_entry.HasNew
entry.HasVTable = src_entry.HasVTable
entry.IsNoVTable = src_entry.IsNoVTable
entry.ParentName = src_entry.ParentName
entry.ParentNames = src_entry.ParentNames
entry.ParentCount = src_entry.ParentCount
entry.VTableMethodCount = src_entry.VTableMethodCount
entry.VTableMethods = src_entry.VTableMethods
entry.MethodsEmitted = 0
entry.SpecTriggerSha1 = None
self.StructCount += 1
# Sync the name | type index (the alias entry has the same name/Ty as the source, so the index keeps the first registration index).
self._struct_idx_add(self.StructCount - 1, entry)
return entry
def set_struct_sha1(self, pool: memhub.MemBuddy*, class_name: str, sha1: str) -> int:
"""Set the owning module SHA1 of a struct; returns 1 on success, 0 on failure."""
if class_name is None or sha1 is None: return 0
entry: GenTypes.StructEntry* = self.find_struct(class_name)
if entry is None: return 0
# Copy the SHA1 string (StdlibAlloc=1 uses the perm pool to survive 1a/1b and prevent a dangling strcmp).
sha1_len: sizet = string.strlen(sha1)
sha1_pool: memhub.MemBuddy* = pool
if self.StdlibAlloc != 0:
sha1_pool = CompilerFactory.factory.PermPool
if sha1_pool is None: sha1_pool = pool
sha1_buf: str = sha1_pool.alloc(sha1_len + 1)
if sha1_buf is None: return 0
string.strcpy(sha1_buf, sha1)
entry.ModuleSha1 = sha1_buf
return 1
def get_struct_sha1(self, class_name: str) -> str:
"""Return the owning module SHA1 of a struct, or None when unset."""
if class_name is None or self.StructTable is None: return None
entry: GenTypes.StructEntry* = self.find_struct(class_name)
if entry is None: return None
return entry.ModuleSha1
def add_field(self, pool: memhub.MemBuddy*, struct_entry: GenTypes.StructEntry*, field_name: str, field_ty: mlirlite.LLVMType*, default_val: ast.AST* = None, annot_class_name: str = None) -> int:
"""Add a field to a struct and return its index, or -1 on failure."""
if struct_entry is None or field_name is None or field_ty is None: return -1
if struct_entry.FieldCount >= FIELD_MAX:
VLogger.error(f"field table full for {struct_entry.Name}", "STRUCT")
return -1
idx: int = struct_entry.FieldCount
fe: GenTypes.FieldEntry* = self._get_field_entry(struct_entry, idx)
if fe is None: return -1
# Copy the field name (StdlibAlloc=1 uses the perm pool to survive reset); otherwise lookup_field's strcmp(fe.Name, ...) reads dangling out-of-bounds memory.
fname_pool: memhub.MemBuddy* = pool
if self.StdlibAlloc != 0:
fname_pool = CompilerFactory.factory.PermPool
if fname_pool is None: fname_pool = pool
name_len: sizet = string.strlen(field_name)
# Tagged call site: if the writer is the field-name buffer near these 32B, the report names this site directly.
name_buf: str = fname_pool.alloc_tagged(name_len + 1, memhub.DETSITE_STRUCTGENMIXIN_570)
if name_buf is not None:
string.strcpy(name_buf, field_name)
fe.Name = name_buf
fe.Index = idx
# In 1b (StdlibAlloc=1) the field type must be deep-copied to stdlib: if the field-level Ty still holds a pool pointer, cross-file reads after this file's reset_to(mark1) (field flattening, MI this-adjust, _apply_struct_defaults' build_gep_struct using fe.Ty) dereference dangling memory.
# Note: annotation class names are merged into Spec.SourceName and Spec is always PermPool-allocated, so it naturally survives reset.
if self.StdlibAlloc != 0:
dp_pool_sg: memhub.MemBuddy* = CompilerFactory.factory.PermPool
if dp_pool_sg is None: dp_pool_sg = pool
ty_copy: mlirlite.LLVMType* = mlirlite.DeepCopyType(dp_pool_sg, field_ty)
if ty_copy is not None: fe.Ty = ty_copy
else: fe.Ty = field_ty
# DefaultVal: cross-file struct construction (_translate_struct_ctor -> _apply_struct_defaults) reads field defaults while translating the caller file, after the original pool AST was reset, so constants are deep-copied to perm_pool (1a also requires DefaultVal to survive per-file reset).
fe.DefaultVal = self._deep_copy_default_ast(dp_pool_sg, default_val)
else:
fe.Ty = field_ty
fe.DefaultVal = default_val
# Instances are always PermPool-allocated (resident or a separate copy), so they naturally survive a 1b reset.
arena_f: TSpec.TypeSpecArena* = self.Gen.TypeGenBase.ensure_spec_arena()
if arena_f is not None:
fe.Spec = TSpec.SpecFromLLVMType(arena_f, field_ty)
# [S4 annotation-side Spec] Fill the field Spec's signedness and annotation source name (annot_class_name carries the real signedness and the annotation spelling; LLVM inference always yields TS_INT with no name).
# When type info is missing (Spec is None) build TS_UNKNOWN to carry the annotation name without asserting a concrete type.
if annot_class_name is not None:
if fe.Spec is None: fe.Spec = TSpec.NewTypeSpec(arena_f, TSpec.TS_UNKNOWN, 0, TSpec.TS_SIGN_UNKNOWN)
if fe.Spec is not None:
sign_f: int = self.Gen.TypeGenCType.spec_sign_of_annot(annot_class_name)
if sign_f != TSpec.TS_SIGN_UNKNOWN: fe.Spec = TSpec.SpecWithSign(arena_f, fe.Spec, sign_f)
fe.Spec = TSpec.SpecWithSource(fe.Spec, annot_class_name)
# Explicitly zero the const flag (not relying on the entry's zero-init): ClassGenDef sets it to 1 only for a confirmed const object, so explicit zeroing prevents a false "uninitialized read as 1".
fe.IsConst = 0
struct_entry.FieldCount = idx + 1
return idx
def _deep_copy_default_ast(self, pool: memhub.MemBuddy*, default_val: ast.AST*) -> ast.AST*:
"""Deep-copy a field default AST (constants and lists) into the perm pool."""
if default_val is None: return None
if default_val.kind() == ast.ASTKind.List: return self._deep_copy_list_ast(pool, default_val)
if default_val.kind() != ast.ASTKind.Constant: return None
src_c: ast.Constant* = ast.Constant*(default_val)
dst_c: ast.Constant* = ast.Constant*(pool.alloc_tagged(ast.Constant.__sizeof__(), memhub.DETSITE_STRUCTGENMIXIN_641))
if dst_c is None: return None
string.memset(dst_c, 0, ast.Constant.__sizeof__())
# Copy the vtable pointer (AST is @t.CVTable; kind() virtual calls depend on the first-field vptr); must match _deep_copy_list_ast (c.Deref read + c.DerefAs write).
# The old form silently failed under TPC: no load-vptr instruction and no store emitted, leaving vptr=0, so a later node.kind() reads `load i8*, i8** [0 + 0]` -> ASan READ at 0x0. Evidence: TransPyV\output\b5883dc9c114a604.ll:10382-10384.
vt_ptr_c: t.CPtr = t.CPtr(*((uint64*)(default_val)))
c.DerefAs(dst_c, vt_ptr_c)
# Copy position fields (for error location).
dst_c.lineno = src_c.lineno
dst_c.col_offset = src_c.col_offset
dst_c.end_lineno = src_c.end_lineno
dst_c.end_col_offset = src_c.end_col_offset
# Copy the constant value (int/float/str/bool distinguished by const_kind).
dst_c.const_kind = src_c.const_kind
dst_c.int_val = src_c.int_val
dst_c.float_val = src_c.float_val
if src_c.str_val is not None:
s_len: sizet = string.strlen(src_c.str_val)
s_copy: str = pool.alloc_tagged(s_len + 1, memhub.DETSITE_STRUCTGENMIXIN_659)
if s_copy is not None:
string.strcpy(s_copy, src_c.str_val)
dst_c.str_val = s_copy
return (ast.AST*)(dst_c)
def _deep_copy_list_ast(self, pool: memhub.MemBuddy*, default_val: ast.AST*) -> ast.AST*:
"""Deep-copy a list-literal default AST node into the perm pool."""
src_l: ast.List* = ast.List*(default_val)
dst_l: ast.List* = ast.List*(pool.alloc(ast.List.__sizeof__()))
if dst_l is None: return None
string.memset(dst_l, 0, ast.List.__sizeof__())
# Copy the vtable pointer (AST is @t.CVTable; kind() virtual calls depend on the first-field vptr).
vt_l: t.CPtr = t.CPtr(*((uint64*)(default_val)))
c.DerefAs(dst_l, vt_l)
# Copy position fields and ctx.
dst_l.lineno = src_l.lineno
dst_l.col_offset = src_l.col_offset
dst_l.end_lineno = src_l.end_lineno
dst_l.end_col_offset = src_l.end_col_offset
dst_l.ctx = src_l.ctx
# Deep-copy elts: manually build a list[AST*] container in stdlib.
src_elts_l: list[ast.AST*]* = src_l.elts
if src_elts_l is not None:
cnt_l: sizet = len(src_elts_l)
lst_copy_l: list[ast.AST*]* = (list[ast.AST*] | t.CPtr)(pool.alloc_tagged(list[ast.AST*].__sizeof__(), memhub.DETSITE_STRUCTGENMIXIN_693))
if lst_copy_l is not None:
string.memset(lst_copy_l, 0, list[ast.AST*].__sizeof__())
# The count must never be fixed from the source count up front; it must be settled by the actual written slot count (written_l below).
# Otherwise a null / non-copyable source element silently skipped would leave __count__ > actual fill and zero slots, so _init_global_array_from_list's elts.get(i) reads a bare 0 and crashes on an empty AST element (ASan access-violation 0x0). Only capacity is set here.
lst_copy_l.__capacity__ = cnt_l
# Single source of truth for element size: allocation (cnt_l * elem_sz_l), zeroing (same expression) and writing (slot base stepped by elem_sz_l) must agree; two separate literal 8s would let "allocate n*8 but write at another width" reappear.
elem_sz_l: sizet = 8
lst_copy_l.__elem_size__ = elem_sz_l
lst_copy_l.__pool__ = None
data_l: t.CPtr = pool.alloc_tagged(cnt_l * elem_sz_l, memhub.DETSITE_STRUCTGENMIXIN_698)
if data_l is not None:
string.memset(data_l, 0, cnt_l * elem_sz_l)
lst_copy_l.__data__ = data_l
written_l: sizet = 0
li_l: sizet
for li_l in range(cnt_l):
elem_src_l: ast.AST* = src_elts_l.get(li_l)
# Explicit null check (repo hard rule: null-check pointers with `is None`). An earlier revision compared `elem_src_l == 0` and noted it was equivalent to `is None`: the IR fact was right but the conclusion misleading.
# A null element in the source list means the producer is corrupted (count > actual fill) -> fail loudly, never backfill 0.
if elem_src_l is None:
self.Gen.TypeGenErr.fatal_error(default_val, f"field default deep copy: element {li_l} of the list literal is an empty AST element (pointer is 0). " f"Something upstream produced an empty AST element (element count and actual fill disagree); " f"refusing to silently backfill 0 and generate a broken default; compilation aborted.", "ValueError")
return None
elem_dst_l: ast.AST* = self._deep_copy_default_ast(pool, elem_src_l)
if elem_dst_l is None:
self.Gen.TypeGenErr.fatal_error(elem_src_l, f"field default deep copy: element {li_l} of the list literal (kind={elem_src_l.kind()})" f" is neither Constant nor List, so it cannot be deep-copied into the permanent pool. Silently skipping this slot" f" would create an empty slot [count > actual fill] (a later elts.get reads a raw 0 and crashes);" f" silently shortening the count would change the array initializer length. Both are silent degradation, so fail loudly.", "TypeError")
return None
# The slot element type must be a pointer: `ast.AST*`'s `[0]` is the object `ast.AST` (48 bytes), so `slot[0] = <pointer>` would silently compile as a 48-byte load + 48-byte store and overrun each cnt_l*elem_sz_l slot by 40 bytes, writing over this block's canary and the next free block's first word (root cause of three generations of [FREEFRAME] crashes).
# Per the repo merge rule (hashtable.vp:82-85) `X*` = Ptr(resolve(X)), so indexing yields a pointer element -> an 8-byte pointer store matching the allocation width.
slot_l: ast.AST** = (ast.AST**)(uint64(data_l) + li_l * elem_sz_l)
slot_l[0] = elem_dst_l
written_l += 1
# count == actual fill (== cnt_l on success; any mismatch already terminated above).
lst_copy_l.__count__ = written_l
dst_l.elts = lst_copy_l
return (ast.AST*)(dst_l)
def get_struct_count(self) -> int:
"""Return the number of registered structs."""
return self.StructCount
def _resolve_module_sha1(self, mod_name: str, from_imports: str = None) -> str:
"""Resolve a module name to its SHA1 by exact name, import alias, then suffix."""
if mod_name is None: return None
s_rm: str = StubMergerStore.LookupSha1ByModName(mod_name)
if s_rm is None and from_imports is not None:
real_rm: str = StubMergerStore.LookupModNameByAlias(from_imports, mod_name)
if real_rm is not None:
s_rm = StubMergerStore.LookupSha1ByModName(real_rm)
if s_rm is None: s_rm = StubMergerStore.LookupSha1ByModNameSuffix(real_rm)
if s_rm is None: s_rm = StubMergerStore.LookupSha1ByModNameSuffix(mod_name)
return s_rm
def find_struct_entry_with_ty(self, name: str, alt_name: str) -> GenTypes.StructEntry*:
"""Find the first struct entry with a valid Ty by name or alt name, else None."""
if self.StructTable is None: return None
for i in range(self.StructCount):
entry: GenTypes.StructEntry* = self._get_struct_entry(i)
if entry is None or entry.Name is None or entry.Ty is None: continue
if entry.Name[0] == '\0': continue
if string.strcmp(entry.Name, name) == 0: return entry
if alt_name is not None and string.strcmp(entry.Name, alt_name) == 0: return entry
return None
def find_struct(self, name: str) -> GenTypes.StructEntry*:
"""Find a struct by class name, supporting module-qualified names; else None."""
if name is None or self.StructTable is None: return None
# Pass 1: exact match.
# StructCount sanity guard (memory corruption can yield a huge value -> index loop false hang).
if self.StructCount < 0 or self.StructCount > 200000:
VLogger.error(f"abnormal StructCount: {self.StructCount} (find_struct)", "STRUCT")
stdlib.exit(1)
# Name-index fast path: a hit gives the first registration index (semantics = the original exact-match scan's first hit).
fi: int = self._struct_name_idx_find(name)
if fi >= 0: return self._get_struct_entry(fi)
# When the name index is trusted (Built==StructCount, index present, non-empty query), an index miss means "definitely no same-named entry", since entries are named at registration and synced into the index and names are at most neutralized to "".
# So the O(n) exact scan can be skipped (hotspot: it accounts for all of find_struct's strcmp). When the index is untrusted / absent / the query name is empty, still fall back to the linear scan; if exact matching truly finds nothing, continue to the module-qualified / bare-name fallback.
name_idx_trusted: int = 0
if self.StructNameIdx is not None and self.StructIdxCap > 0 and self.StructIdxBuilt == self.StructCount: name_idx_trusted = 1
if name_idx_trusted == 0 or name[0] == '\0':
for i in range(self.StructCount):
entry: GenTypes.StructEntry* = self._get_struct_entry(i)
if entry is not None and entry.Name is not None:
if string.strcmp(entry.Name, name) == 0: return entry
# Module-qualified name (module.Class): when the module resolves, match by module ownership exactly, to stop the bare-name fallback from grabbing a cross-module same-named class; if the module misses but the bare name is unique, fall back.
# Take the dot outside generic brackets (numpy.ndarray[t.CDouble] -> numpy + ndarray[t.CDouble]), else strrchr lands inside the subscript and truncates the module name (same root cause as lookup_field_by_class).
dot_pos_q: str = self._last_dot_outside_brackets(name)
if dot_pos_q is not None and dot_pos_q[1] != '\0':
bare_f: str = dot_pos_q + 1
mod_len_f: sizet = string.strlen(name) - string.strlen(bare_f) - 1
if mod_len_f > 0:
mod_buf_f: str = stdlib.malloc(mod_len_f + 1)
if mod_buf_f is not None:
string.strncpy(mod_buf_f, name, mod_len_f)
mod_buf_f[mod_len_f] = '\0'
if string.strcmp(mod_buf_f, "t") != 0 and string.strcmp(mod_buf_f, "c") != 0:
# A 16-hex module segment is a SHA1 literal (e.g. "{9aab}.Scene3D", TypeGenResolve L1397-1402's sha1-prefix form): skip the store module-name lookup (store keys are module names, not sha1) and match name+sha1 exactly.
# Otherwise _resolve_module_sha1("9aab") returns None and a bare-name fallback returns the first-registered wrong cross-module Scene3D entry (VKernel gfx / App gfx both define Scene3D -> wrong ownership -> ret_sha1=0ae -> Phase A _deps_pending deadlock). [C10 convergence] The module segment is exactly 16 lowercase hex = SHA1 literal: go through GenApi.is_sha1_hex16 (single source of truth).
mod_hex_ok: int = GenApi.is_sha1_hex16(mod_buf_f)
if mod_hex_ok != 0:
me_h: GenTypes.StructEntry* = self.find_struct_by_module(bare_f, mod_buf_f)
stdlib.free(mod_buf_f)
return me_h
mod_sha1_f: str = self._resolve_module_sha1(mod_buf_f)
if mod_sha1_f is not None:
me_f: GenTypes.StructEntry* = self.find_struct_by_module(bare_f, mod_sha1_f)
if me_f is not None:
stdlib.free(mod_buf_f)
return me_f
# Module class not registered: count same bare-named classes, fall back when unique (no conflict).
same_cnt_f: int = 0
same_entry_f: GenTypes.StructEntry* = None
for i2 in range(self.StructCount):
e2: GenTypes.StructEntry* = self._get_struct_entry(i2)
if e2 is not None and e2.Name is not None:
if string.strcmp(e2.Name, bare_f) == 0:
same_cnt_f += 1
same_entry_f = e2
stdlib.free(mod_buf_f)
if same_cnt_f == 1: return same_entry_f
return None
stdlib.free(mod_buf_f)
# Pass 2: class-name fallback (handles "HT.Translator" -> "Translator", reached only when the module name cannot be resolved; the dot is also taken outside generic subscripts).
dot_pos: str = self._last_dot_outside_brackets(name)
if dot_pos is not None:
class_name: str = dot_pos + 1
if class_name is not None and class_name[0] != '\0':
# Same-name uniqueness check (aligns with the qualified path above): when a cross-module same-named class exists (e.g. VKernel gfx and App gfx both define Scene3D), do not return the first-registered wrong entry.
# Otherwise an unresolvable module segment is hijacked to the wrong module and the return type resolves against the wrong text.ll.
same_cnt_b: int = 0
same_entry_b: GenTypes.StructEntry* = None
for i in range(self.StructCount):
entry: GenTypes.StructEntry* = self._get_struct_entry(i)
if entry is not None and entry.Name is not None:
if string.strcmp(entry.Name, class_name) == 0:
same_cnt_b += 1
same_entry_b = entry
if same_cnt_b == 1: return same_entry_b
return None
def find_struct_by_module(self, name: str, sha1: str) -> GenTypes.StructEntry*:
"""Find a struct by class name plus SHA1; falls back to find_struct when sha1 is None."""
if name is None or self.StructTable is None: return None
# StructCount sanity guard (memory corruption can yield a huge value -> index loop false hang).
if self.StructCount < 0 or self.StructCount > 200000:
VLogger.error(f"abnormal StructCount: {self.StructCount} (find_struct_by_module)", "STRUCT")
stdlib.exit(1)
if sha1 is not None:
# Name-index fast path: same-named entries all lie at or after the first same-name index, so scan name+sha1 from that index (equivalent to the original full scan's "first name with sha1 hit").
# A miss or untrusted index -> start=0 and use the original full scan (for correctness).
start: int = 0
fri: int = self._struct_name_idx_find(name)
if fri >= 0: start = fri
for i in range(start, self.StructCount):
entry: GenTypes.StructEntry* = self._get_struct_entry(i)
if entry is not None and entry.Name is not None:
# Fast path: strcmp equality implies equal first chars, so skip strcmp for differing first chars.
if entry.Name[0] == name[0] and string.strcmp(entry.Name, name) == 0:
if entry.ModuleSha1 is not None:
# Same: sha1 equality implies equal first chars; compare first chars before strcmp.
if entry.ModuleSha1[0] == sha1[0] and string.strcmp(entry.ModuleSha1, sha1) == 0: return entry
# SHA1 lookup failed: do not fall back to find_struct (it would return a cross-module same-named wrong entry); return None so the caller creates a new type, avoiding Phase B short-circuiting with a wrong type.
return None
# No SHA1: find the first by class name.
return self.find_struct(name)
def reset_module_methods_emitted(self, sha1: str) -> int:
"""Clear MethodsEmitted for classes registered by the given module sha1."""
if sha1 is None or self.StructTable is None: return 0
if self.StructCount < 0 or self.StructCount > 200000:
VLogger.error(f"abnormal StructCount: {self.StructCount} (reset_module_methods_emitted)", "STRUCT")
return 0
reset_count: int = 0
for i in range(self.StructCount):
entry: GenTypes.StructEntry* = self._get_struct_entry(i)
if entry is not None:
# Normal classes match by ModuleSha1.
if entry.ModuleSha1 is not None:
if string.strcmp(entry.ModuleSha1, sha1) == 0:
entry.MethodsEmitted = 0
reset_count += 1
continue
# Generic-specialized classes match by the triggering file's sha1: their ModuleSha1 is the template sha1 (method names carry the template prefix) and cannot match the current obsolete file; without resetting, the specialized body keeps MethodsEmitted=1 and is not retranslated, leaving text.ll with only calls and no define.
if entry.SpecTriggerSha1 is not None:
if string.strcmp(entry.SpecTriggerSha1, sha1) == 0:
entry.MethodsEmitted = 0
reset_count += 1
return reset_count
def find_struct_by_type(self, struct_ty: mlirlite.LLVMType*) -> GenTypes.StructEntry*:
"""Find a struct by type pointer value comparison, or None."""
if struct_ty is None or self.StructTable is None: return None
# StructCount sanity guard (memory corruption can yield a huge value -> index loop false hang).
if self.StructCount < 0 or self.StructCount > 200000:
VLogger.error(f"abnormal StructCount: {self.StructCount} (find_struct_by_type)", "STRUCT")
stdlib.exit(1)
st_val: uint64 = uint64(struct_ty)
# The Ty index is entirely disabled here (its structure is still maintained by _struct_idx_add but not used): an entry's Ty may be rewritten to a new pointer by deep copy (ClassGenDef), leaving the slot keyed by the registration-time Ty value out of sync.
# So an index hit may not be the original scan's "first entry with equal Ty"; restore the original linear exact scan (hit/miss behavior byte-identical to before the index).
for i in range(self.StructCount):
entry: GenTypes.StructEntry* = self._get_struct_entry(i)
if entry is not None and entry.Ty is not None:
if uint64(entry.Ty) == st_val: return entry
return None
def struct_type_is_packed(self, struct_ty: mlirlite.LLVMType*) -> int:
"""Return 1 when the named struct is packed, else 0."""
if struct_ty is None: return 0
sname: str = mlirlite.get_struct_name(struct_ty)
if sname is None: return 0
entry: GenTypes.StructEntry* = self.find_struct_by_type(struct_ty)
if entry is None: entry = self.find_struct(sname)
if entry is None: return 0
return entry.IsPacked
# verify_field_slot_consistency - field-table vs struct-slot consistency guard (fail-fast).
# A field table and the struct layout are produced by two paths from the same field list; a mismatch makes GEPs computed from FieldEntry.Index read a neighbouring field (e.g. ast.Name.id landing on the ctx slot, reading pointer 1 as an integer and crashing strlen). Only clearly different classes (pointer-ness / integer width / float width) are compared; unions and empty field tables (vpi signature-only classes) are exempt.
def verify_field_slot_consistency(self, struct_ty: mlirlite.LLVMType*, entry: GenTypes.StructEntry*) -> int:
"""Assert the field count matches the struct slots and each field type matches its slot."""
if struct_ty is None or entry is None: return 0
if entry.FieldCount <= 0: return 0
# Union exemption: a union's field table has N entries (one per member) but the LLVM layout has only 1 slot (all members at offset 0, sized to the max member), so they are never equal.
# Field access goes through a bitcast GEP and does not depend on the slot index, so this guard does not apply (false-positive evidence: EdgeCaseTest's `class Data(t.CUnion)`, 2 members -> 1 slot).
if entry.IsUnion != 0 or self.is_union_by_type(struct_ty) != 0: return 0
fcount: int = mlirlite.get_struct_fcount(struct_ty)
if fcount <= 0: return 0
if entry.FieldCount != fcount:
self.Gen.TypeGenErr.fatal_error(None, f"struct '{entry.Name}' field table count ({entry.FieldCount}) disagrees with the LLVM slot count ({fcount}): " f"field entries and struct layout come from two paths, and a mismatch makes field access compute GEP with a wrong index" f" (evidence: ast.Name.id landing in the ctx slot => the pointer is read as the integer 1 => strlen(0x1) crashes).", "TypeError")
return 0
node: mlirlite.ParamNode* = mlirlite.get_struct_fields(struct_ty)
i: int = 0
while i < entry.FieldCount and node is not None:
fe: GenTypes.FieldEntry* = self._get_field_entry(entry, i)
slot_ty: mlirlite.LLVMType* = mlirlite.paramnode_get_ty(node)
if fe is not None and fe.Name is not None and fe.Ty is not None and slot_ty is not None:
ptr_f: int = self.Gen.ExprGenBase.is_ptr_type(fe.Ty)
ptr_s: int = self.Gen.ExprGenBase.is_ptr_type(slot_ty)
bits_f: int = self.Gen.ExprGenBase.get_llvm_type_bits(fe.Ty)
bits_s: int = self.Gen.ExprGenBase.get_llvm_type_bits(slot_ty)
fbits_f: int = self.Gen.ExprGenBase.get_llvm_float_bits(fe.Ty)
fbits_s: int = self.Gen.ExprGenBase.get_llvm_float_bits(slot_ty)
if ptr_f != ptr_s or bits_f != bits_s or fbits_f != fbits_s:
self.Gen.TypeGenErr.fatal_error(None, f"field '{fe.Name}' of struct '{entry.Name}' (entry index {i}) is not the same kind as its slot type: " f"entry ptr-ness={ptr_f}/int width={bits_f}/float width={fbits_f}; " f"slot ptr-ness={ptr_s}/int width={bits_s}/float width={fbits_s}. " f"field entries and struct layout are misaligned (evidence: id landing in the ctx slot => the pointer is read as the integer 1 => strlen(0x1) crashes).", "TypeError")
return 0
node = mlirlite.paramnode_get_next(node)
i += 1
return 0
def lookup_field(self, struct_ty: mlirlite.LLVMType*, field_name: str) -> GenTypes.FieldEntry*:
"""Look up a field by struct type and field name using identity then name fallback."""
if struct_ty is None or field_name is None or self.StructTable is None: return None
# Two-pass lookup strategy (root fix for cross-module same-named class conflicts):
# Pass 1 compares type pointers by identity (highest priority; the type uniquely determines the entry, so a missing field returns None, it avoids the union == bug, and it is independent of iteration order). Pass 2 is a name fallback (used for unregistered types or unreliable identity); if a name matches but the field is missing, continue to check other same-named entries.
# Pass 1: identity comparison by uint64 pointer value (avoids the is-comparison path dereferencing a dangling Ty). find_struct_by_type's semantics = "first entry with equal Ty pointer" (first registration wins), consistent with the original identity scan.
# The type uniquely determines the entry: once hit, look up the field; if missing return None directly.
lk_entry: GenTypes.StructEntry* = self.find_struct_by_type(struct_ty)
if lk_entry is not None:
# Field-table vs struct-slot consistency guard (fail-fast): reject a mismatch (see verify_field_slot_consistency header; real crash: ast.Name.id -> slot 9).
self.verify_field_slot_consistency(struct_ty, lk_entry)
for fi in range(lk_entry.FieldCount):
fe: GenTypes.FieldEntry* = self._get_field_entry(lk_entry, fi)
if fe is not None and fe.Name is not None:
if string.strcmp(fe.Name, field_name) == 0: return fe
return None
# Pass 2: name fallback (identity matched nothing).
ty_name: str = self._extract_struct_name(struct_ty)
if ty_name is not None:
# Strengthen SHA1 namespace isolation: for a type name like "3d4afd9cdffeba6d.Thread", compare its 16-hex prefix exactly with the first 16 chars of entry.ModuleSha1 to forbid a strstr bare-name cross-module grab (sched.Thread overridden by includes/vthreading.vp's Thread -> field misalignment -> _thread_entry call 0).
# strncmp allocates no temporary buffer; the prefix test goes through the single source GenApi.sha1_prefix_len.
ty_has_sha1_lf: int = 0
if GenApi.sha1_prefix_len(ty_name) != 0: ty_has_sha1_lf = 1
for i in range(self.StructCount):
entry: GenTypes.StructEntry* = self._get_struct_entry(i)
if entry is not None and entry.Name is not None:
# Type name with a SHA1 prefix must match the first 16 chars of entry.ModuleSha1 exactly, else skip (cross-module same-named class namespace isolation).
if ty_has_sha1_lf != 0:
if entry.ModuleSha1 is None or string.strlen(entry.ModuleSha1) < 16 or string.strncmp(entry.ModuleSha1, ty_name, 16) != 0: continue
matched: int = 0
if string.strcmp(entry.Name, ty_name) == 0: matched = 1
elif string.strstr(ty_name, entry.Name) is not None: matched = 1
if matched != 0:
# Look up the field.
for fi in range(entry.FieldCount):
fe: GenTypes.FieldEntry* = self._get_field_entry(entry, fi)
if fe is not None and fe.Name is not None:
if string.strcmp(fe.Name, field_name) == 0: return fe
# Name matched but the field was not found; keep checking other same-named entries (cross-module same name).
continue
# No entry matched.
return None
def _last_dot_outside_brackets(self, s: str) -> str:
"""Return a pointer to the last '.' outside [...] brackets, or None."""
if s is None: return None
n: sizet = string.strlen(s)
depth: int = 0
i: sizet = n
while i > 0:
i -= 1
ch: char = s[i]
if ch == ']': depth += 1
elif ch == '[' and depth > 0: depth -= 1
elif ch == '.' and depth == 0: return s + i
return None
def lookup_field_by_class(self, class_name: str, field_name: str, sha1: str = None, node: ast.AST* = None, from_imports: str = None) -> GenTypes.FieldEntry*:
"""Look up a field by class name and field name, preferring an exact SHA1 match."""
if class_name is None or field_name is None: return None
# Prefer SHA1 matching (distinguishes cross-module same-named classes).
# Supports module-qualified names: class_name may be "HT.Translator" while entry.Name is "Translator"; try exact match first, then fall back to the strrchr-extracted class name.
if sha1 is not None:
sha1_matched: int = 0
# Compute the class-name fallback ("HT.Translator" -> "Translator"; "numpy.ndarray[t.CDouble]" -> "ndarray[t.CDouble]").
bare_name: str = class_name
dot_pos_lfbc: str = self._last_dot_outside_brackets(class_name)
if dot_pos_lfbc is not None:
bare_name = dot_pos_lfbc + 1
if bare_name is None or bare_name[0] == '\0': bare_name = class_name
for i in range(self.StructCount):
entry: GenTypes.StructEntry* = self._get_struct_entry(i)
if entry is not None and entry.Name is not None:
# Exact match or class-name fallback match.
name_match: int = 0
if string.strcmp(entry.Name, class_name) == 0: name_match = 1
elif bare_name is not class_name and string.strcmp(entry.Name, bare_name) == 0: name_match = 1
if name_match != 0:
if entry.ModuleSha1 is not None and string.strcmp(entry.ModuleSha1, sha1) == 0:
sha1_matched = 1
# SHA1 + class name matched; look up the field.
for fi in range(entry.FieldCount):
fe: GenTypes.FieldEntry* = self._get_field_entry(entry, fi)
if fe is not None and fe.Name is not None and string.strcmp(fe.Name, field_name) == 0: return fe
return None
# Module-qualified name (module.Class, module not t/c built-in): resolve by module ownership and forbid a bare-name fallback grabbing a cross-module same-named class (root cause: VKernel sched.Thread overridden by includes/vthreading.vp's Thread -> missing 'tid').
# Resolve the module name to a sha1 via the StubMergerStore mapping, then match name+sha1 exactly (vpi stub injection / definition-side bare-name entries). For generic names the dot must go through _last_dot_outside_brackets, or the module name truncates.
dot_pos_q: str = self._last_dot_outside_brackets(class_name)
if dot_pos_q is not None and dot_pos_q[1] != '\0':
bare_q: str = dot_pos_q + 1
mod_len_q: sizet = string.strlen(class_name) - string.strlen(bare_q) - 1
if mod_len_q > 0:
mod_buf_q: str = stdlib.malloc(mod_len_q + 1)
if mod_buf_q is not None:
string.strncpy(mod_buf_q, class_name, mod_len_q)
mod_buf_q[mod_len_q] = '\0'
if string.strcmp(mod_buf_q, "t") != 0 and string.strcmp(mod_buf_q, "c") != 0:
# A 16-hex module segment ("{16hex}.Class", the normalized generic type-arg form) is the module SHA1; a registered class proves that module compiled, so match name+sha1 exactly without _resolve_module_sha1 (store keys are module names, so querying the sha1 is always None and would fail-fast wrongly).
# [C10 convergence] The module segment is exactly 16 lowercase hex = SHA1 literal: go through GenApi.is_sha1_hex16; generic subscript dots are already skipped by _last_dot_outside_brackets, so only top-level a.b forms remain here.
mod_hex_ok_q: int = GenApi.is_sha1_hex16(mod_buf_q)
mod_sha1_q: str = None
if mod_hex_ok_q != 0:
# A SHA1 prefix is itself evidence of registration (same semantics as existing resolution): mod_sha1_q aliases mod_buf_q (the return paths below free it once).
mod_sha1_q = mod_buf_q
else: mod_sha1_q = self._resolve_module_sha1(mod_buf_q, from_imports)
if mod_sha1_q is not None:
mod_entry_q: GenTypes.StructEntry* = self.find_struct_by_module(bare_q, mod_sha1_q)
if mod_entry_q is not None:
# Dependencies-not-ready tolerance (aligns the bare-name path): an empty class (FieldCount==0, incomplete registration / source module not compiled) -> return None so the caller marks _deps_pending and retranslates next pass; not fatal here.
if mod_entry_q.FieldCount == 0:
stdlib.free(mod_buf_q)
return None
for fi_q in range(mod_entry_q.FieldCount):
fe_q: GenTypes.FieldEntry* = self._get_field_entry(mod_entry_q, fi_q)
if fe_q is not None and fe_q.Name is not None:
if string.strcmp(fe_q.Name, field_name) == 0:
stdlib.free(mod_buf_q)
return fe_q
# REnum variant field alias fallback (same as the bare-name path): when a module-qualified access resolves the class but not the field, look up the variant alias directly (REnum fields are _p1/_p2 slots).
# Previously this fallback existed only on the bare-name path, so the module-qualified path fataled first and never reached it.
rn_cls_q: str = mod_entry_q.Name
if rn_cls_q is None: rn_cls_q = bare_q
rn_is_renum_q: int = self.Gen.ClassGenEnum._is_renum_class_name(bare_q)
if rn_is_renum_q == 0 and rn_cls_q is not None: rn_is_renum_q = self.Gen.ClassGenEnum._is_renum_class_name(rn_cls_q)
rn_alias_q: int = -1
if rn_is_renum_q != 0:
rn_alias_q = self.Gen.ClassGenEnum._lookup_renum_field_alias(rn_cls_q, field_name)
if rn_alias_q < 1 and bare_q is not rn_cls_q: rn_alias_q = self.Gen.ClassGenEnum._lookup_renum_field_alias(bare_q, field_name)
if rn_alias_q >= 1:
rn_fe_q: GenTypes.FieldEntry* = self._get_field_entry(mod_entry_q, rn_alias_q)
if rn_fe_q is not None and rn_fe_q.Ty is not None:
stdlib.free(mod_buf_q)
return rn_fe_q
# Class hit but field not found: readable error (traceback format).
self.Gen.TypeGenErr.fatal_error(node, f"lookup_field_by_class: class '{class_name}' was resolved by module ownership (ModuleSha1={mod_sha1_q}, FieldCount={mod_entry_q.FieldCount}) but field '{field_name}' was not found", "AttributeError")
stdlib.free(mod_buf_q)
return None
# Module resolvable but class not registered: fail-fast (exposes vpi injection/registration defects; no bare-name cross-library grab of another module's same-named class).
self.Gen.TypeGenErr.fatal_error(node, f"lookup_field_by_class: class '{class_name}' was not found (module '{mod_buf_q}' resolves to sha1={mod_sha1_q}, but no class of that name is registered; check vpi stub injection or compilation order)", "AttributeError")
stdlib.free(mod_buf_q)
return None
# Module name not registered: fail-fast (no more bare-name cross-library fallback).
self.Gen.TypeGenErr.fatal_error(node, f"lookup_field_by_class: module '{mod_buf_q}' of class '{class_name}' cannot be resolved (not registered in the sha1 map; check the imports and compilation order)", "AttributeError")
stdlib.free(mod_buf_q)
return None
stdlib.free(mod_buf_q)
# Fallback: no SHA1 or SHA1 match failed; find the first by class name.
entry = self.find_struct(class_name)
if entry is None:
# Diagnostic: target class not registered (Python traceback style; terminate compilation immediately).
self.Gen.TypeGenErr.fatal_error(node, f"lookup_field_by_class: class '{class_name}' is not registered (struct_count={self.StructCount})", "AttributeError")
return None
# Dependencies-not-ready tolerance: the caller expects a specific module (sha1 non-None) but the bare-name fallback hit an empty FieldCount==0 stub class (vpi injection without fields / defining module not compiled).
# Return None so the caller marks _deps_pending and retranslates (the real definition overrides the stub once the source module compiles). SECURITY_ATTRIBUTES FieldCount=0 root cause (subprocess.vp sa.nLength).
if entry.FieldCount == 0 and sha1 is not None: return None
# Look up the field first (return before diagnostics/error, else the error exits immediately and the lookup always fails).
for fi in range(entry.FieldCount):
fe: GenTypes.FieldEntry* = self._get_field_entry(entry, fi)
if fe is not None and fe.Name is not None:
if string.strcmp(fe.Name, field_name) == 0: return fe
# REnum variant field alias fallback: REnum classes (e.g. LLVMType) have variant inner fields (Ptr.Pointee etc.) that are _p1/_p2 slot aliases, not normal fields.
# Leaving it to the caller would never run because this function's fatal on a missing field calls sys.exit(1) first. Note: _lookup_renum_field_alias compares class_name by pointer value, so it must receive entry.Name (the pool pointer unified by register_struct + _update_renum_alias_class_ptr).
rn_cls_l: str = entry.Name
if rn_cls_l is None: rn_cls_l = class_name
rn_is_renum_l: int = self.Gen.ClassGenEnum._is_renum_class_name(class_name)
if rn_is_renum_l == 0 and rn_cls_l is not None:
# The class name may be module-qualified ("mod.LLVMType") while the REnum table stores bare names; double-check with the bare name too.
rn_is_renum_l = self.Gen.ClassGenEnum._is_renum_class_name(rn_cls_l)
rn_alias_l: int = -1
if rn_is_renum_l != 0:
rn_alias_l = self.Gen.ClassGenEnum._lookup_renum_field_alias(rn_cls_l, field_name)
if rn_alias_l < 1 and class_name is not rn_cls_l:
# Double-check: the alias table may still hold an old AST pointer (before _update_renum_alias_class_ptr / multi-module re-registration), so retry with the caller's class_name.
rn_alias_l = self.Gen.ClassGenEnum._lookup_renum_field_alias(class_name, field_name)
if rn_alias_l >= 1:
rn_fe_l: GenTypes.FieldEntry* = self.Gen.StructGen._get_field_entry(entry, rn_alias_l)
if rn_fe_l is not None and rn_fe_l.Ty is not None: return rn_fe_l
# Diagnostic: the struct was found but the field truly was not; concatenate the registered field list (comma-separated on one line) for a one-glance report.
fb_flds_lst: str = stdlib.malloc(1024)
if fb_flds_lst is not None:
fb_flds_lst[0] = '\0'
for fi in range(entry.FieldCount):
fe: GenTypes.FieldEntry* = self._get_field_entry(entry, fi)
if fe is not None and fe.Name is not None:
if string.strlen(fb_flds_lst) > 0 and string.strlen(fb_flds_lst) + 3 < 1024: string.strcat(fb_flds_lst, ", ")
if string.strlen(fb_flds_lst) + string.strlen(fe.Name) < 1024: string.strcat(fb_flds_lst, fe.Name)
err: str
if rn_is_renum_l != 0:
# Friendly error: for a REnum class state that variant field alias lookup failed, including entry.Name and the alias slot result, so it is clear whether the alias is unregistered (slot=-1) or the class_name pointer is not unified.
err = f"lookup_field_by_class: class '{class_name}' is a REnum class (FieldCount={entry.FieldCount}), variant field '{field_name}' alias lookup failed (entry.Name='{rn_cls_l}', alias slot={rn_alias_l}; check whether this variant field has a registered alias, or whether the class_name pointer is unified)"
else:
# Readability: include entry.Name/ModuleSha1 (cross-module same-name location) + the registered field list + the expected sha1 (caller), to distinguish "cross-module same-named class mismatch" from "incomplete vpi stub injection" from "class definition and access site disagree".
sha1_txt: str = "?"
if entry.ModuleSha1 is not None: sha1_txt = entry.ModuleSha1
want_sha1_txt: str = "?"
if sha1 is not None: want_sha1_txt = sha1
fld_lst_txt: str = "(none)"
if fb_flds_lst is not None and string.strlen(fb_flds_lst) > 0: fld_lst_txt = fb_flds_lst
err = f"lookup_field_by_class: class '{class_name}' is registered (FieldCount={entry.FieldCount}, " f"entry.Name='{entry.Name}', ModuleSha1={sha1_txt}, caller expects sha1={want_sha1_txt}) " f"but field '{field_name}' was not found; registered fields: [{fld_lst_txt}]. Possible causes: cross-module same-named class" f" mis-matched / incomplete vpi stub injection / class definition and access site disagree"
# Output in Python traceback style (traceback frame + file:line + error type) and terminate compilation immediately (same semantics as VLogger.error, format aligned with fatal_error).
self.Gen.TypeGenErr.fatal_error(node, err, "AttributeError")
if fb_flds_lst is not None: stdlib.free(fb_flds_lst)
# Diagnostic: struct found but field not found.
return None
def get_struct_type(self, class_name: str) -> mlirlite.LLVMType*:
"""Return the LLVM type of a struct by class name."""
entry: GenTypes.StructEntry* = self.find_struct(class_name)
if entry is not None: return entry.Ty
return None
def get_field_by_index(self, class_name: str, idx: int) -> GenTypes.FieldEntry*:
"""Return a field entry by class name and field index."""
if class_name is None: return None
entry: GenTypes.StructEntry* = self.find_struct(class_name)
if entry is None: return None
if idx < 0 or idx >= entry.FieldCount: return None
return self._get_field_entry(entry, idx)
def struct_has_defaults(self, class_name: str) -> int:
"""Return 1 when the struct has any field with a default value."""
if class_name is None: return 0
entry: GenTypes.StructEntry* = self.find_struct(class_name)
if entry is None: return 0
for fi in range(entry.FieldCount):
fe: GenTypes.FieldEntry* = self._get_field_entry(entry, fi)
if fe is not None and fe.DefaultVal is not None: return 1
return 0
def get_struct_type_from_value(self, val: mlirlite.Value*) -> mlirlite.LLVMType*:
"""Infer the struct type from a value's type, unwrapping pointers recursively."""
if val is None or val.Ty is None: return None
ty: mlirlite.LLVMType* = val.Ty
while ty is not None:
# Same-class compilation bug (see TypeGenResolve L1463 comment): updating the while-condition variable (ty = pointee) directly in a case body makes the bootstrapped match write the assignment to the subject snapshot slot while the while condition reads the old slot -> infinite loop.
# Fix: bind the case to a local variable and update the loop variable outside the match.
nxt_ty: mlirlite.LLVMType* = None
match ty:
case mlirlite.LLVMType.Ptr(pointee):
nxt_ty = pointee
case mlirlite.LLVMType.Struct(fields, fcount, name):
return ty
case _:
return None
ty = nxt_ty
return None
def mark_as_union(self, name: str) -> int:
"""Mark a registered struct as a union; returns 1 on success, 0 on failure."""
if name is None: return 0
entry: GenTypes.StructEntry* = self.find_struct(name)
if entry is None: return 0
entry.IsUnion = 1
return 1
def is_union_by_name(self, name: str) -> int:
"""Return 1 when the named struct is a union, else 0."""
if name is None: return 0
entry: GenTypes.StructEntry* = self.find_struct(name)
if entry is None: return 0
return entry.IsUnion
def is_union_by_type(self, struct_ty: mlirlite.LLVMType*) -> int:
"""Return 1 when the type-pointer struct is a union, falling back to name matching."""
if struct_ty is None or self.StructTable is None: return 0
ty_name: str = self._extract_struct_name(struct_ty)
st_iu: uint64 = uint64(struct_ty)
for i in range(self.StructCount):
entry: GenTypes.StructEntry* = self._get_struct_entry(i)
if entry is not None and entry.Ty is not None:
matched: int = 0
if uint64(entry.Ty) == st_iu: matched = 1
elif ty_name is not None and entry.Name is not None:
if string.strcmp(entry.Name, ty_name) == 0: matched = 1
elif string.strstr(ty_name, entry.Name) is not None: matched = 1
if matched != 0: return entry.IsUnion
return 0
def mark_as_oop(self, name: str) -> int:
"""Mark a registered struct as OOP; returns 1 on success, 0 on failure."""
if name is None: return 0
entry: GenTypes.StructEntry* = self.find_struct(name)
if entry is None: return 0
entry.IsOOP = 1
return 1
def is_oop_by_name(self, name: str) -> int:
"""Return 1 when the named struct is OOP, else 0."""
if name is None: return 0
entry: GenTypes.StructEntry* = self.find_struct(name)
if entry is None: return 0
return entry.IsOOP
def mark_has_init(self, name: str) -> int:
"""Mark a struct as having an __init__ method; returns 1 on success, 0 on failure."""
if name is None: return 0
entry: GenTypes.StructEntry* = self.find_struct(name)
if entry is None: return 0
entry.HasInit = 1
return 1
def has_init_by_name(self, name: str) -> int:
"""Return 1 when the named struct has an __init__ method, else 0."""
if name is None: return 0
entry: GenTypes.StructEntry* = self.find_struct(name)
if entry is None: return 0
return entry.HasInit
def mark_has_new(self, name: str) -> int:
"""Mark a struct as having a __new__ method; returns 1 on success, 0 on failure."""
if name is None: return 0
entry: GenTypes.StructEntry* = self.find_struct(name)
if entry is None: return 0
entry.HasNew = 1
return 1
def has_new_by_name(self, name: str) -> int:
"""Return 1 when the named struct has a __new__ method, else 0."""
if name is None: return 0
entry: GenTypes.StructEntry* = self.find_struct(name)
if entry is None: return 0
return entry.HasNew
def get_class_name_by_type(self, pool: memhub.MemBuddy*, struct_ty: mlirlite.LLVMType*) -> str:
"""Return the class name for a type pointer, or None."""
if struct_ty is None or self.StructTable is None: return None
# find_struct_by_type's semantics = "first entry with equal Ty pointer" (first registration wins); return its Name. The Ty index is disabled and the linear scan restored, so it is byte-identical.
fast_cn: GenTypes.StructEntry* = self.find_struct_by_type(struct_ty)
if fast_cn is not None: return fast_cn.Name
# Fallback: when the type pointer comparison fails, iterate all registered structs matching by name; extract struct_ty's Name field (may be "SHA1.ClassName").
ty_name: str = self._extract_struct_name(struct_ty)
if ty_name is not None:
for j in range(self.StructCount):
entry2: GenTypes.StructEntry* = self._get_struct_entry(j)
if entry2 is not None and entry2.Name is not None:
# Build the "SHA1.ClassName" form for comparison.
entry2_sha1: str = entry2.ModuleSha1
if entry2_sha1 is not None:
full_nm: str = pool.alloc(128)
if full_nm is not None:
viperlib.snprintf(full_nm, 128, "%s.%s", entry2_sha1, entry2.Name)
if string.strcmp(full_nm, ty_name) == 0: return entry2.Name
# Also compare the class name directly (struct_ty's Name may lack a SHA1 prefix).
if string.strcmp(entry2.Name, ty_name) == 0: return entry2.Name
# Final fallback: suffix match ("SHA1.ClassName" tail) -- safer than a strstr substring match, which would wrongly hit the base name "GSList" inside the specialization "SHA1.GSList[Param]".
# That mixup mis-resolves chained method call class names (e.g. GSList.append lookup failing, then a sibling mixin falling back to LinkedNode.append, shifting this by sizeof(LinkedNode) and writing over the container's first 48 bytes -- the LLvmLiteTest Test 3 crash root cause).
if entry2.Name is not None and string.strlen(entry2.Name) > 0:
_tlen_cn: sizet = string.strlen(ty_name)
_nlen_cn: sizet = string.strlen(entry2.Name)
if _tlen_cn > _nlen_cn and ty_name[_tlen_cn - _nlen_cn - 1] == '.':
_tail_ok_cn: int = 1
_ti_cn: sizet
for _ti_cn in range(_nlen_cn):
if ty_name[_tlen_cn - _nlen_cn + _ti_cn] != entry2.Name[_ti_cn]:
_tail_ok_cn = 0
break
if _tail_ok_cn == 1: return entry2.Name
return None
def _extract_struct_name(self, ty: mlirlite.LLVMType*) -> str:
"""Extract the Name of a Struct LLVMType variant, or None for non-Struct types."""
if ty is None: return None
match ty:
case mlirlite.LLVMType.Struct(fields, fcount, name):
return name
case _:
return None
def mark_has_vtable(self, name: str) -> int:
"""Mark a struct as having a vtable; returns 1 on success, 0 on failure."""
if name is None: return 0
entry: GenTypes.StructEntry* = self.find_struct(name)
if entry is None: return 0
entry.HasVTable = 1
return 1
def has_vtable_by_name(self, name: str) -> int:
"""Return 1 when the named struct has a vtable, else 0."""
if name is None: return 0
entry: GenTypes.StructEntry* = self.find_struct(name)
if entry is None: return 0
return entry.HasVTable
def mark_novtable(self, name: str) -> int:
"""Mark a struct as NoVTable; returns 1 on success, 0 on failure."""
if name is None: return 0
entry: GenTypes.StructEntry* = self.find_struct(name)
if entry is None: return 0
entry.IsNoVTable = 1
return 1
def is_novtable_by_name(self, name: str) -> int:
"""Return 1 when the named struct is NoVTable, else 0."""
if name is None: return 0
entry: GenTypes.StructEntry* = self.find_struct(name)
if entry is None: return 0
return entry.IsNoVTable
def set_parent_name(self, name: str, parent_name: str) -> int:
"""Set a struct's single parent class name; returns 1 on success, 0 on failure."""
if name is None: return 0
entry: GenTypes.StructEntry* = self.find_struct(name)
if entry is None: return 0
entry.ParentName = parent_name
return 1
def set_parent_names(self, pool: memhub.MemBuddy*, name: str, names_ptr: sizet*, count: int) -> int:
"""Set a struct's parent-name array, deep-copied into the perm pool."""
if name is None: return 0
entry: GenTypes.StructEntry* = self.find_struct(name)
if entry is None: return 0
if names_ptr is None or count <= 0:
entry.ParentNames = None
entry.ParentCount = 0
return 1
if count > PARENT_MAX: count = PARENT_MAX
pn_pool: memhub.MemBuddy* = pool
if self.StdlibAlloc != 0:
pn_pool = CompilerFactory.factory.PermPool
if pn_pool is None: pn_pool = pool
arr: sizet* = pn_pool.alloc(8 * count)
if arr is None: return 0
for i in range(count):
addr: sizet = names_ptr[i]
arr[i] = 0
if addr != 0:
src: str = rawptr(addr)
if src is not None:
slen: sizet = string.strlen(src)
dup: str = pn_pool.alloc(slen + 1)
if dup is not None:
string.strcpy(dup, src)
arr[i] = sizet(dup)
entry.ParentNames = arr
entry.ParentCount = count
return 1
def get_parent_count(self, name: str) -> int:
"""Return the parent count (0 none, 1 single inheritance, >1 multiple)."""
if name is None: return 0
entry: GenTypes.StructEntry* = self.find_struct(name)
if entry is None: return 0
return entry.ParentCount
def get_parent_name_at(self, name: str, idx: int) -> str:
"""Return the idx-th parent class name, or None when out of range."""
if name is None or idx < 0: return None
entry: GenTypes.StructEntry* = self.find_struct(name)
if entry is None or entry.ParentNames is None: return None
if idx >= entry.ParentCount: return None
parr: sizet* = (sizet*)(entry.ParentNames)
addr: sizet = parr[idx]
if addr == 0: return None
return rawptr(addr)
def get_mro_chain(self, pool: memhub.MemBuddy*, name: str) -> sizet*:
"""Return the MRO-linearized class-name array (depth-first, declaration order, deduped)."""
if pool is None or name is None: return None
arr: sizet* = pool.alloc(8 * (MRO_MAX + 1))
if arr is None: return None
stack: sizet* = pool.alloc(8 * MRO_MAX)
if stack is None: return None
visited: sizet* = pool.alloc(8 * MRO_MAX)
if visited is None: return None
stack_count: int = 0
out_count: int = 0
visit_count: int = 0
# Push self initially.
stack[0] = sizet(name)
stack_count = 1
while stack_count > 0:
stack_count -= 1
cur_addr: sizet = stack[stack_count]
cur: str = rawptr(cur_addr)
if cur is None: continue
# Visited check (linear search).
dup: int = 0
for vi in range(visit_count):
va: sizet = visited[vi]
if va != 0:
vn: str = rawptr(va)
if vn is not None and string.strcmp(vn, cur) == 0:
dup = 1
break
if dup != 0: continue
# Add to the result and visited sets.
if out_count < MRO_MAX:
arr[out_count] = sizet(cur)
out_count += 1
if visit_count < MRO_MAX:
visited[visit_count] = sizet(cur)
visit_count += 1
# Push parents in reverse (compute the reverse index with a forward range so they pop in declaration order).
entry: GenTypes.StructEntry* = self.find_struct(cur)
if entry is not None and entry.ParentCount > 0 and entry.ParentNames is not None:
pc: int = entry.ParentCount
parr: sizet* = (sizet*)(entry.ParentNames)
for pi2 in range(pc):
pi: int = pc - 1 - pi2
if stack_count < MRO_MAX:
stack[stack_count] = parr[pi]
stack_count += 1
# Terminate with a trailing 0.
arr[out_count] = 0
return arr
def get_parent_name(self, name: str) -> str:
"""Return a struct's single parent class name, or None."""
if name is None: return None
entry: GenTypes.StructEntry* = self.find_struct(name)
if entry is None: return None
return entry.ParentName
def set_vtable_methods(self, name: str, methods: str, count: int) -> int:
"""Set the vtable method-name array; returns 1 on success, 0 on failure."""
if name is None: return 0
entry: GenTypes.StructEntry* = self.find_struct(name)
if entry is None: return 0
entry.VTableMethods = methods
entry.VTableMethodCount = count
return 1
def get_vtable_method_index(self, name: str, method_name: str) -> int:
"""Return the method's vtable index, or -1 when not found."""
if name is None or method_name is None: return -1
entry: GenTypes.StructEntry* = self.find_struct(name)
if entry is None or entry.VTableMethods is None: return -1
# VTableMethods is declared char* but actually stores a sizet array; it must be cast to sizet* to read the 8-byte pointer values correctly.
methods_arr: sizet* = (sizet*)(entry.VTableMethods)
for i in range(entry.VTableMethodCount):
mname_addr: sizet = methods_arr[i]
if mname_addr == 0: continue
mname: str = rawptr(mname_addr)
if mname is not None and string.strcmp(mname, method_name) == 0: return i
return -1
def get_vtable_method_count(self, name: str) -> int:
"""Return the vtable method count."""
if name is None: return 0
entry: GenTypes.StructEntry* = self.find_struct(name)
if entry is None: return 0
return entry.VTableMethodCount
def get_vtable_method_name(self, name: str, idx: int) -> str:
"""Return the idx-th vtable method name, or None when out of range."""
if name is None or idx < 0: return None
entry: GenTypes.StructEntry* = self.find_struct(name)
if entry is None or entry.VTableMethods is None: return None
if idx >= entry.VTableMethodCount: return None
methods_arr: sizet* = (sizet*)(entry.VTableMethods)
mname_addr: sizet = methods_arr[idx]
if mname_addr == 0: return None
return rawptr(mname_addr)
def get_field_count(self, name: str) -> int:
"""Return a struct's field count, or -1 when not found."""
if name is None: return -1
entry: GenTypes.StructEntry* = self.find_struct(name)
if entry is None: return -1
return entry.FieldCount
def ensure_struct_def_in_module(self, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, ty: mlirlite.LLVMType*):
"""Ensure a cross-module struct definition is available in the current module."""
if ty is None or mod is None or pool is None: return
# Pointer type: ensure the pointee has at least an opaque declaration.
match ty:
case mlirlite.LLVMType.Ptr(pointee):
mlirlite.module_ensure_opaque_for_type(mod, pool, ty)
# If the pointee is a Struct, recursively copy the full definition (GEP field access needs a sized type; opaque only makes llc error).
if pointee is not None: self.ensure_struct_def_in_module(pool, mod, pointee)
return
case _:
pass
# Get the struct name (None for non-Struct types).
sname: str = mlirlite.get_struct_name(ty)
if sname is None: return
# Check whether this is a cross-module reference (name contains '.').
if string.strchr(sname, 46) is None:
return # not a cross-module reference; nothing to do
# Extract the class name (the part after the first '.').
# Must use strchr not strrchr: a generic specialization like "list[ast.AST*]" contains a '.' inside (ast.AST), and strrchr would wrongly parse the class name as "AST*]".
dot_pos: str = string.strchr(sname, '.')
if dot_pos is None: return
class_name: str = dot_pos + 1
if class_name is None or class_name[0] == '\0': return
# Extract the SHA1 (between sname and dot_pos, using strlen to avoid pointer subtraction).
total_len: sizet = string.strlen(sname)
class_name_len: sizet = string.strlen(class_name)
sha1_len: sizet = total_len - class_name_len - 1
sha1_buf: str = pool.alloc_tagged(32, memhub.DETSITE_STRUCTGENMIXIN_1804)
if sha1_buf is None: return
# 32-byte buffer: strncpy does not truncate, so a prefix over 31 chars would overrun, plus the manual NUL would overrun again.
sha1_cp: sizet = sha1_len
if sha1_cp > 31: sha1_cp = 31
string.strncpy(sha1_buf, sname, sha1_cp)
sha1_buf[sha1_cp] = '\0'
# Look up the struct definition by class name + SHA1.
entry: GenTypes.StructEntry* = self.find_struct_by_module(class_name, sha1_buf)
if entry is None:
# Fall back to lookup by class name (may find a same-named class in another module, but better than nothing).
entry = self.find_struct(class_name)
if entry is None: return
if entry.Ty is None: return
# Add the full definition to the current module (replaces an existing opaque declaration).
mlirlite.module_add_named_type(mod, pool, entry.Ty)
# B5: a cross-module injected packed class must have its packed flag synced (<{...}>), else the referencing module GEPs with a non-packed layout and misaligns with the defining side's packed layout (gdt/elf/fat32 binary formats misread; root cause of idt_entry/gdt_entry degrading to {…} in merged IR).
if entry.IsPacked != 0: mlirlite.module_set_named_type_packed(mod, pool, entry.Ty, 1)
# Recursively scan field types to ensure value-type sub-structs (e.g. FILETIME inside WIN32_FIND_DATAA) also have full definitions; pointer-type fields only need an opaque declaration (handled by module_ensure_opaque_for_type).
self._ensure_field_types_in_module(pool, mod, entry.Ty)
def _ensure_field_types_in_module(self, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, ty: mlirlite.LLVMType*):
"""Recursively scan a type tree to ensure value-type sub-structs are fully defined."""
if ty is None or mod is None or pool is None: return
# Nested match to walk the type tree.
match ty:
case mlirlite.LLVMType.Struct(fields, fcount, sname):
# Walk the field list.
if fields is not None:
fld: mlirlite.ParamNode* = fields
cnt_ef: int = 0
while fld is not None:
# Step guard: a cyclic field list (memory corruption) would loop forever (G3 LinkedNode.prepend hang candidate); over the limit, report and abort immediately.
cnt_ef += 1
if cnt_ef > 100000:
VLogger.error("field list is likely cyclic (_ensure_field_types_in_module), forcing termination", "STRUCT")
stdlib.exit(1)
if fld.Ty is not None:
# Value-type sub-structs (e.g. FILETIME) need a full definition.
match fld.Ty:
case mlirlite.LLVMType.Struct(f2, fc2, n2):
self.ensure_struct_def_in_module(pool, mod, fld.Ty)
case _:
pass
self._ensure_field_types_in_module(pool, mod, fld.Ty)
fld = fld.Next
case mlirlite.LLVMType.Ptr(pointee):
# Pointer type: the pointee only needs an opaque declaration, not a full definition, but if the pointee is a Struct ensure the opaque declaration exists.
if pointee is not None: mlirlite.module_ensure_opaque_for_type(mod, pool, ty)
case mlirlite.LLVMType.Array(elem_ty, acount):
# Array type: if the element is a value-type struct it needs a full definition (Array elements only recurse into fields without emitting definitions -> llc "use of undefined type named '...'").
if elem_ty is not None:
match elem_ty:
case mlirlite.LLVMType.Struct(_, _, _):
self.ensure_struct_def_in_module(pool, mod, elem_ty)
case _:
pass
self._ensure_field_types_in_module(pool, mod, elem_ty)
case _:
pass
def get_field_name_ptr(self, fe: GenTypes.FieldEntry*) -> str:
"""Return a FieldEntry's field name, or None when unset."""
if fe is None: return None
return fe.Name
def get_field_type_ptr(self, fe: GenTypes.FieldEntry*) -> mlirlite.LLVMType*:
"""Return a FieldEntry's field type, or None when unset."""
if fe is None: return None
return fe.Ty
def get_field_default_ptr(self, fe: GenTypes.FieldEntry*) -> ast.AST*:
"""Return a FieldEntry's default-value AST node, or None when unset."""
if fe is None: return None
return fe.DefaultVal
def get_field_annot_class_name(self, fe: GenTypes.FieldEntry*) -> str:
"""Return a FieldEntry's annotation class name (Spec.SourceName), or None."""
if fe is None: return None
return TSpec.SpecSourceName(fe.Spec)
def field_is_volatile(self, fe: GenTypes.FieldEntry*) -> int:
"""Return 1 when the field is volatile, driving LLVM volatile load/store qualifiers."""
if fe is None: return 0
return fe.IsVolatile
def field_pointee_is_volatile(self, fe: GenTypes.FieldEntry*) -> int:
"""Return 1 when the field's pointee is volatile, driving volatile load/store qualifiers."""
if fe is None: return 0
return fe.PointeeVolatile
def field_is_const(self, fe: GenTypes.FieldEntry*) -> int:
"""Return 1 when the field is a const object, making field assignment fail fast."""
if fe is None: return 0
return fe.IsConst