- StructGenMixin:EscTable/EscCount(追加在类末尾,避免偏移漂移);register_file_funcs 每文件重置并逐函数算摘要。 - 逃逸 walker:return p / global 赋值 / obj.f 赋值,递归 if/while/for;不进嵌套 def/class;形参访问逐级显式重转(AST 退化)。 - 表内只存 u64 名哈希(djb2-64),不解引用字符串指针,消除槽内坏指针导致的 strcmp 崩溃。 - 表从 PermPool 分配并 memset。 - GenApi:get_param_escapes/get_ret_borrow 查询入口。调用点检查(步骤4)待接。
1786 lines
111 KiB
Plaintext
1786 lines
111 KiB
Plaintext
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
|
||
# 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*
|
||
|
||
# --- 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
|
||
|
||
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.EscTable = None
|
||
self.EscCount = 0
|
||
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.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
|
||
# 生命期摘要表:每次进入本文件时重置(v1b 只覆盖同文件;跨文件属 v3)
|
||
# 两个要点(对照 init_struct_table):
|
||
# 1) 必须从 **PermPool** 分配——临时池在 Phase1 会被 reset_to(mark) 回收,同块内存
|
||
# 被复用后我们会握着已变脏的旧指针(ASan 抓不到,表现为槽里读出垃圾 → strcmp 崩)。
|
||
# 2) 必须显式清零:未写入的槽若不是 0,_EscLookup 的 strcmp 会对脏指针解引用。
|
||
esc_cap: sizet = 1024
|
||
esc_pool: memhub.MemBuddy* = CompilerFactory.factory.PermPool
|
||
if esc_pool is None: esc_pool = pool
|
||
self.EscTable = sizet*(esc_pool.alloc(esc_cap * 24))
|
||
if self.EscTable is not None: string.memset(self.EscTable, 0, esc_cap * 24)
|
||
self.EscCount = 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)
|
||
|
||
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)
|
||
|
||
# ============================================================
|
||
# 生命期摘要(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: u64 = self._NameHash(name)
|
||
cap: int = 1024
|
||
i: int = 0
|
||
while i < self.EscCount and i < cap:
|
||
# 槽 0 是名哈希(u64);未写入的槽为 0,与任何非零哈希都不相等,无需判空
|
||
if u64(self.EscTable[i * 3 + 0]) == h: return i
|
||
i += 1
|
||
return -1
|
||
|
||
def _NameHash(self, name: str) -> u64:
|
||
"""djb2-64(小立即数,溢出按 u64 回绕):把函数名压成表键"""
|
||
h: u64 = 5381
|
||
if name is None: return h
|
||
n: sizet = string.strlen(name)
|
||
i: sizet = 0
|
||
while i < n:
|
||
h = h * 33 + u64(uint8(name[i]))
|
||
i += 1
|
||
return h
|
||
|
||
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 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])
|
||
|
||
def _RegisterFuncEscapeSummary(self, pool: memhub.MemBuddy*, fd: ast.FunctionDef*, key: str) -> int:
|
||
"""扫函数体登记摘要;key 必须是与 EscTable 同池的字符串"""
|
||
if pool is None or fd is None or key is None or self.EscTable is None: return 1
|
||
if self._EscLookup(key) >= 0: return 0
|
||
if self.EscCount >= 1024: return 1
|
||
gset: hashtable.HashTable* = hashtable.HashTable(pool)
|
||
if gset is not None: self._CollectGlobalNames(fd.children, gset, 0)
|
||
box: sizet* = sizet*(pool.alloc(16))
|
||
if box is None: return 1
|
||
box[0] = 0
|
||
box[1] = 0
|
||
self._ScanEscapes(fd, fd.children, gset, box, 0)
|
||
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
|
||
return 0
|
||
|
||
def _CollectGlobalNames(self, stmts: list[ast.AST*]*, gset: hashtable.HashTable*, depth: int) -> int:
|
||
"""收集本函数(含嵌套块)里 `global` 声明的名字"""
|
||
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: gset.set_int(nnm.id, 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
|
||
|
||
def _MarkEscapingStore(self, fd: ast.FunctionDef*, target: ast.AST*, value: ast.AST*, gset: hashtable.HashTable*, 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)
|
||
elif tk == ast.ASTKind.Name:
|
||
tnm: ast.Name* = ast.Name*(target)
|
||
if tnm.id is not None and gset is not None and gset.__contains__(tnm.id) != 0:
|
||
box[0] = box[0] | (sizet(1) << pi)
|
||
return 0
|
||
|
||
def _ScanEscapes(self, fd: ast.FunctionDef*, stmts: list[ast.AST*]*, gset: hashtable.HashTable*, box: sizet*, depth: int) -> int:
|
||
"""递归扫描: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()
|
||
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
|
||
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)
|
||
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)
|
||
self._ScanEscapes(fd, whn.children, gset, box, depth + 1)
|
||
self._ScanEscapes(fd, whn.orelse, gset, box, depth + 1)
|
||
elif k == ast.ASTKind.For:
|
||
frn: ast.For* = ast.For*(st)
|
||
self._ScanEscapes(fd, frn.children, gset, box, depth + 1)
|
||
self._ScanEscapes(fd, frn.orelse, gset, box, depth + 1)
|
||
return 0
|
||
|
||
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
|