Files
ViperC/App/lib/Stub/StubGen/VpiReader.vp
T

3929 lines
216 KiB
Plaintext
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import lib.core.Gen.GenApi as GenApi
import lib.core.Gen.GenTypes as GenTypes
import lib.core.CompilerFactory as CompilerFactory
import t, c
from stdint import *
import ast
import string
import stdio
import stdlib
import sys
import memhub
import mlirlite
import viperlib
import win32.fileio as fileio
import lib.core.VLogger as VLogger
import lib.core.Gen.Translator as HT
import lib.Stub.StubMerger.StubMergerStore as StubMergerStore
import lib.Projectrans.Config as Config
# REPL stub layout recovery: constants and class entries (impl at file end).
# Declared first so type annotations are visible before the REPL hook uses them; REPL-only.
define _REPL_STUB_CLS_MAX = 1024
_REPL_STUB_FIELD_MAX: int = 128
_REPL_STUB_VT_MAX: int = 128
_REPL_STUB_TXT_CAP: sizet = 4194304
_REPL_STUB_WARN_MAX: int = 10
_REPL_STUB_FINISH_PASSES: int = 8
# Two sentinel values for ReplStubClass.LayoutCount (not field counts).
# Layout metadata now comes from {sha1}.meta STRUCT ... FIELDS n / OPAQUE records, not .stub.ll text.
_REPL_STUB_NOCOUNT_NOCLASS: int = -1 # no STRUCT line for this key in meta (or meta not read)
_REPL_STUB_NOCOUNT_OPAQUE: int = -2 # key is OPAQUE in meta (no body)
# Reason codes for "layout unavailable -> explicit refuse" (_repl_stub_warn_nodata):
# distinguish missing data from untrusted data; both skip StructEntry registration.
define _REPL_SU_NOSTUB = 1 # missing data: module has no {sha1}.meta
define _REPL_SU_NOCLASS = 2 # refused: meta exists but has no STRUCT line for this class
define _REPL_SU_OPAQUE = 3 # refused: class is OPAQUE in meta (no body)
define _REPL_SU_PARENT = 4 # upstream no data: base layout unavailable/not registered (goes to _repl_stub_warn_parent)
# ReplStubClass.Flags / MethodFlags bits
define _REPL_SF_CVTABLE = 1
define _REPL_SF_NOVTABLE = 2
define _REPL_SF_ANYFUNC = 4
define _REPL_SF_PACKED = 8
# ReplStubClass.State
define _REPL_SS_REFUSED = 0 # layout untrusted: never register (field access fails explicitly)
define _REPL_SS_OK = 1 # full layout registered
define _REPL_SS_PENDING = 2 # base not ready yet: defer to final pass
define _REPL_SS_EMPTY = 3 # no base/no vtable/unresolvable fields: use existing vpi injection
class ReplStubClass:
Key: str # "<sha1>.<class name>" (= definition-side named struct name)
ClassName: str
Sha1: str
Flags: int
OwnNames: sizet* # elements = str pointers (this class field names, AnnAssign order)
OwnTypes: sizet* # elements = LLVMType pointers
OwnCount: int
MethodNames: sizet*
MethodFlags: sizet*
MethodCount: int
Bases: sizet*
BaseCount: int
LayoutCount: int # layout field count (>=0 = meta STRUCT ... FIELDS n; negative = sentinel, not count)
# see _REPL_STUB_NOCOUNT_NOCLASS/-OPAQUE: no such class / meta unread / opaque
VtArity: int # meta VTABLE ... SLOTS n (0 = no record in meta)
Names: sizet*
Types: sizet*
Count: int
HasVt: int
IsNoVTable: int
IsOOP: int
HasInit: int
HasNew: int
VtNames: sizet*
VtCount: int
VtTrust: int # 1 = vtable slot count matches stub (VTableMethods can be registered)
OwnFail: int # 1 = a field annotation failed to resolve, so OwnCount is only partially parsed (incomplete)
State: int
# Method return-annotation cache (stdlib-persistent). Deep vpi AST nodes may be
# corrupted later, so return annotations are parsed from raw vpi_buf text by line.
define VPI_RET_MAX = 4096
define VPI_KEY_LEN = 224
define VPI_RET_LEN = 224
_g_vpi_ret_keys: bytes = None
_g_vpi_ret_strs: bytes = None
_g_vpi_ret_count: int = 0
_g_vpi_ret_cap: int = 0
# Method parameter-annotation cache (stdlib-persistent, mirroring the return cache).
# Deep param lists may be corrupted and truncated, so params are parsed from vpi_buf text.
define VPI_PARAM_MAX = 4096
define VPI_PARAM_KEY_LEN = 224
define VPI_PARAM_LEN = 1024
# Open-addressing index for the key tables (name -> slot index). Slots store indices
# (stable across realloc); each registration appends, lookup keeps first-registered.
define VPI_RET_IDX_CAP = 8192
define VPI_PARAM_IDX_CAP = 8192
# Index hash constants (32-bit FNV-1a, same as StructGenMixin).
define VPI_IDX_FNV_OFFSET = 2166136261
define VPI_IDX_FNV_PRIME = 16777619
define VPI_IDX_FNV_MASK = 4294967295
# v3 跨模块逃逸摘要缓存(.vpi 行尾 `# esc=<mask> ret=<ret>`,由 StubGen.Converter._PyiFunction 写出)。
# 键 = "{sha1}.{Class}.{method}"(方法)或 "{sha1}.{func}"(模块级函数);
# 值打包:低 32 位 = 形参逃逸掩码,高 32 位 = 返回借用位。无逃逸/无返回借用的 def 不写条目,
# 因此"查不到"即"无摘要"(调用点保守放行),与本地表口径一致。
define VPI_ESC_MAX = 8192
define VPI_ESC_KEY_LEN = 224
# P2 跨模块 willFree 标注缓存(.vpi 行尾 `# ... wf=<mask>`,由 StubGen.Converter._PyiFunction 写出)。
# 键格式与逃逸摘要完全一致("{sha1}.{Class}.{method}" / "{sha1}.{func}");值 = 形参失效掩码
# (第 k 位 = 第 k 个形参在本调用后失效)。无标注的 def 不写条目,"查不到" 即 "无标注"。
define VPI_WF_MAX = 8192
define VPI_WF_KEY_LEN = 224
_g_vpi_wf_keys: bytes = None
_g_vpi_wf_vals: sizet* = None
_g_vpi_wf_count: int = 0
# 跨模块常量特化(P3 收尾):同一函数可有多条 `escg=`(打包 p|k|m,见 StructGenMixin._PackEscapeClear),
# 所以是"一键多行"表:键列与 wf 表同构,值列是打包整数。
define VPI_ESG_MAX = 8192
define VPI_ESG_KEY_LEN = 224
# 常量环境的形参位上限:必须与 StructGenMixin.EC_ENV_MAX 一致(越界位一律不信)
define VPI_ESG_ENV_PARAM_MAX = 64
_g_vpi_esg_keys: bytes = None
_g_vpi_esg_vals: sizet* = None
_g_vpi_esg_count: int = 0
# 声明式指令契约(`@life(escape(...))` / `@life(noescape(...))` / `@life(ret(...))`):与 escg 表同键
# (sha1[.Cls].func),但每函数只一条(契约唯一);三列分别存"强制逃逸位"、"强制不逃逸位"与
# ret_kind(A2,编码见 GenApi.RET_KIND_*)。
_g_vpi_esd_keys: bytes = None
_g_vpi_esd_e: sizet* = None
_g_vpi_esd_n: sizet* = None
_g_vpi_esd_r: sizet* = None
_g_vpi_esd_count: int = 0
_g_vpi_esc_keys: bytes = None
_g_vpi_esc_vals: sizet* = None
_g_vpi_esc_count: int = 0
# "本模块的 vpi 已加载过"备忘(每项 17 字节 sha1)。调用点每处都要查摘要,
# 若每次都走 _ensure_vpi_ret_annot_loaded 的 O(N) 前缀判定会显著拖慢编译。
define VPI_ESC_MOD_MAX = 512
_g_vpi_esc_mods: bytes = None
_g_vpi_esc_mod_count: int = 0
def _vpi_escape_module_loaded(src_sha1: str) -> int:
"""该模块的 vpi 是否已由逃逸查询路径加载过;1=是 / 0=否"""
if _g_vpi_esc_mods is None or src_sha1 is None: return 0
s0: int = src_sha1[0]
s1: int = src_sha1[1]
i: int = 0
while i < _g_vpi_esc_mod_count:
ent_m: bytes = _g_vpi_esc_mods + sizet(i) * 17
if ent_m[0] == s0 and ent_m[1] == s1 and string.strcmp(ent_m, src_sha1) == 0: return 1
i += 1
return 0
def _vpi_escape_mark_module(src_sha1: str) -> int:
"""记录"该模块的 vpi 已加载";1=成功 / 0=失败"""
global _g_vpi_esc_mods, _g_vpi_esc_mod_count
if src_sha1 is None: return 0
if _g_vpi_esc_mods is None:
_g_vpi_esc_mods = stdlib.malloc(VPI_ESC_MOD_MAX * 17)
if _g_vpi_esc_mods is None: return 0
string.memset(_g_vpi_esc_mods, 0, VPI_ESC_MOD_MAX * 17)
if _vpi_escape_module_loaded(src_sha1) != 0: return 1
if _g_vpi_esc_mod_count >= VPI_ESC_MOD_MAX: return 0
string.strcpy(_g_vpi_esc_mods + sizet(_g_vpi_esc_mod_count) * 17, src_sha1)
_g_vpi_esc_mod_count += 1
return 1
_g_vpi_param_keys: bytes = None
_g_vpi_param_strs: bytes = None
_g_vpi_param_count: int = 0
_g_vpi_param_cap: int = 0
# Return-annotation key-table index (same malloc lifetime; trusted iff Built == Count)
_g_vpi_ret_idx: bytes = None
_g_vpi_ret_idx_cap: int = 0
_g_vpi_ret_idx_built: int = 0
# Parameter-annotation key-table index (same as above)
_g_vpi_param_idx: bytes = None
_g_vpi_param_idx_cap: int = 0
_g_vpi_param_idx_built: int = 0
# Index probe/insert/rebuild (module-level functions shared by both key tables).
def _vpi_idx_hash(key: bytes) -> uint64:
"""Hash key bytes to a 64-bit FNV-1a value until NUL; empty key returns the offset basis."""
h: uint64 = uint64(VPI_IDX_FNV_OFFSET)
if key is None: return h
i: sizet = 0
while key[i] != '\0':
h = (h ^ uint64(int(key[i]))) * uint64(VPI_IDX_FNV_PRIME)
h = h & uint64(VPI_IDX_FNV_MASK)
i += 1
return h
def _vpi_idx_slot(idx_base: bytes, si: sizet) -> sizet*:
"""Return the si-th 8-byte index slot (stores slot_index+1; 0 means empty)."""
return (sizet*)(uint64(idx_base) + uint64(si) * 8)
def _vpi_idx_find(keys: bytes, key_stride: int, idx_base: bytes, cap: int, key: bytes) -> int:
"""Probe the index for key and return the slot index (>=0); miss or unusable index returns -1."""
if keys is None or idx_base is None or cap <= 0 or key is None: return -1
h: uint64 = _vpi_idx_hash(key)
mask: sizet = sizet(cap - 1)
si: sizet = sizet(h) & mask
for i in range(int(cap)):
slot: sizet* = _vpi_idx_slot(idx_base, si)
sv: sizet = slot[0]
if sv == 0: return -1
se: int = int(sv) - 1
ek: bytes = keys + se * key_stride
if string.strcmp(ek, key) == 0: return se
si = (si + 1) & mask
return -1
def _vpi_idx_insert(idx_base: bytes, cap: int, slot_index: int, key: bytes) -> int:
"""Insert (key -> slot_index) into the index, one slot per registration; 0=success / 1=failure."""
if idx_base is None or cap <= 0 or key is None: return 1
h: uint64 = _vpi_idx_hash(key)
mask: sizet = sizet(cap - 1)
si: sizet = sizet(h) & mask
for i in range(int(cap)):
slot: sizet* = _vpi_idx_slot(idx_base, si)
if slot[0] == 0:
slot[0] = sizet(slot_index + 1)
return 0
si = (si + 1) & mask
return 1
def _vpi_rebuild_ret_idx(new_cap: int) -> int:
"""Rebuild the return-annotation index for a new capacity (power of two); 0=trusted / 1=untrusted."""
global _g_vpi_ret_idx, _g_vpi_ret_idx_cap, _g_vpi_ret_idx_built
if new_cap <= 0:
_g_vpi_ret_idx = None
_g_vpi_ret_idx_cap = 0
_g_vpi_ret_idx_built = -1
return 1
new_idx: bytes = None
if _g_vpi_ret_idx is None: new_idx = stdlib.malloc(new_cap * 8)
else: new_idx = stdlib.realloc(_g_vpi_ret_idx, new_cap * 8)
if new_idx is None:
_g_vpi_ret_idx = None
_g_vpi_ret_idx_cap = 0
_g_vpi_ret_idx_built = -1
return 1
string.memset(new_idx, 0, sizet(new_cap) * 8)
_g_vpi_ret_idx = new_idx
_g_vpi_ret_idx_cap = new_cap
fail: int = 0
ri: int
for ri in range(int(_g_vpi_ret_count)):
if _vpi_idx_insert(new_idx, new_cap, ri, _g_vpi_ret_keys + ri * VPI_KEY_LEN) != 0:
fail = 1
break
if fail != 0:
_g_vpi_ret_idx_built = -1
return 1
_g_vpi_ret_idx_built = _g_vpi_ret_count
return 0
def _vpi_rebuild_param_idx(new_cap: int) -> int:
"""Rebuild the param-annotation index for a new capacity; 0=trusted / 1=untrusted."""
global _g_vpi_param_idx, _g_vpi_param_idx_cap, _g_vpi_param_idx_built
if new_cap <= 0:
_g_vpi_param_idx = None
_g_vpi_param_idx_cap = 0
_g_vpi_param_idx_built = -1
return 1
new_idx: bytes = None
if _g_vpi_param_idx is None: new_idx = stdlib.malloc(new_cap * 8)
else: new_idx = stdlib.realloc(_g_vpi_param_idx, new_cap * 8)
if new_idx is None:
_g_vpi_param_idx = None
_g_vpi_param_idx_cap = 0
_g_vpi_param_idx_built = -1
return 1
string.memset(new_idx, 0, sizet(new_cap) * 8)
_g_vpi_param_idx = new_idx
_g_vpi_param_idx_cap = new_cap
fail: int = 0
ri: int
for ri in range(int(_g_vpi_param_count)):
if _vpi_idx_insert(new_idx, new_cap, ri, _g_vpi_param_keys + ri * VPI_PARAM_KEY_LEN) != 0:
fail = 1
break
if fail != 0:
_g_vpi_param_idx_built = -1
return 1
_g_vpi_param_idx_built = _g_vpi_param_count
return 0
# _cache_vpi_return_annots_text - extract method return annotations from vpi_buf text.
# Does not rely on the AST tree. Tracks "class X:" and "def ... -> Ret:" lines.
def _cache_vpi_return_annots_text(vpi_buf: bytes, src_sha1: str) -> int:
"""Extract method return-annotation strings from vpi_buf text into a persistent stdlib cache."""
global _g_vpi_ret_keys, _g_vpi_ret_strs, _g_vpi_ret_count, _g_vpi_ret_cap
global _g_vpi_ret_idx, _g_vpi_ret_idx_cap, _g_vpi_ret_idx_built
if vpi_buf is None or src_sha1 is None: return 0
if _g_vpi_ret_keys is None:
_g_vpi_ret_keys = stdlib.malloc(VPI_RET_MAX * VPI_KEY_LEN)
_g_vpi_ret_strs = stdlib.malloc(VPI_RET_MAX * VPI_RET_LEN)
if _g_vpi_ret_keys is None or _g_vpi_ret_strs is None: return 0
string.memset(_g_vpi_ret_keys, 0, VPI_RET_MAX * VPI_KEY_LEN)
string.memset(_g_vpi_ret_strs, 0, VPI_RET_MAX * VPI_RET_LEN)
_g_vpi_ret_cap = VPI_RET_MAX
# Name index: slots = key-table capacity * 2. On alloc failure stay untrusted
# (Built=-1) and fall back to linear scan (semantics unchanged).
_g_vpi_ret_idx = stdlib.malloc(VPI_RET_IDX_CAP * 8)
if _g_vpi_ret_idx is not None:
string.memset(_g_vpi_ret_idx, 0, VPI_RET_IDX_CAP * 8)
_g_vpi_ret_idx_cap = VPI_RET_IDX_CAP
_g_vpi_ret_idx_built = _g_vpi_ret_count
else:
_g_vpi_ret_idx_cap = 0
_g_vpi_ret_idx_built = -1
sha1_len: sizet = string.strlen(src_sha1)
buf_len: sizet = string.strlen(vpi_buf)
cur_cls: bytes = None
pos: sizet = 0
line_no: int = 0
while pos < buf_len:
ls: sizet = pos
le: sizet = pos
while le < buf_len and vpi_buf[le] != '\n': le += 1
line_len: sizet = le - ls
# skip leading spaces, then check class/def
i: sizet = ls
while i < le and vpi_buf[i] == ' ': i += 1
if i < le:
# class X:
if line_len >= 6 and string.strncmp(vpi_buf + i, "class ", 6) == 0:
cstart: sizet = i + 6
cend: sizet = cstart
# Extract class name: stop at ':' / ' ' / '(' so base-class parens
# are excluded ("class MemBuddy(MemManager):" -> "MemBuddy").
while cend < le and vpi_buf[cend] != ':' and vpi_buf[cend] != ' ' and vpi_buf[cend] != '(': cend += 1
if cend > cstart:
if cur_cls is not None: stdlib.free(cur_cls)
cur_cls = stdlib.malloc(cend - cstart + 1)
if cur_cls is not None:
string.strncpy(cur_cls, vpi_buf + cstart, cend - cstart)
cur_cls[cend - cstart] = '\0'
# def name(...) -> Ret: # noqa
elif line_len >= 4 and string.strncmp(vpi_buf + i, "def ", 4) == 0:
# Module-level function (no indent): clear class context to avoid
# leftovers after a class ends (e.g. json null/int_val).
if i == ls and cur_cls is not None:
stdlib.free(cur_cls)
cur_cls = None
dstart: sizet = i + 4
dend: sizet = dstart
while dend < le and vpi_buf[dend] != '(': dend += 1
# method name = [dstart, dend)
# find "->"
rpos: sizet = dstart
arrow_pos: sizet = le
while rpos < le - 1:
if vpi_buf[rpos] == '-' and vpi_buf[rpos + 1] == '>':
arrow_pos = rpos
break
rpos += 1
if dend > dstart:
# Return type: extract "-> Ret" annotation; unannotated non-special
# methods default to "i32" (matches HandlesType.infer_return_type).
ret_src: str = None
rtype_len: sizet = 0
if arrow_pos < le:
# return type = from arrow_pos+2 (skip spaces) up to ':'
rstart2: sizet = arrow_pos + 2
while rstart2 < le and vpi_buf[rstart2] == ' ': rstart2 += 1
rend2: sizet = rstart2
while rend2 < le and vpi_buf[rend2] != ':': rend2 += 1
if rend2 > rstart2:
rtype_len = rend2 - rstart2
ret_src = vpi_buf + rstart2
else:
# no return annotation: default i32 (except special methods filtered below)
rtype_len = 3
ret_src = "i32"
if ret_src is not None:
# build key and ret_str, store into cache
mname_len: sizet = dend - dstart
cls_len2: sizet = 0
if cur_cls is not None: cls_len2 = string.strlen(cur_cls)
key_len: sizet = sha1_len + 1 + cls_len2 + 1 + mname_len
if key_len < VPI_KEY_LEN and rtype_len < VPI_RET_LEN:
# copy method name to a temp buffer (vpi_buf+dstart is not NUL-terminated)
mname_buf: bytes = stdlib.malloc(mname_len + 1)
if mname_buf is not None:
string.strncpy(mname_buf, vpi_buf + dstart, mname_len)
mname_buf[mname_len] = '\0'
# skip unannotated special methods (init/deinit): they have dedicated paths
skip_def: int = 0
if arrow_pos >= le and (string.strcmp(mname_buf, "__init__") == 0 or string.strcmp(mname_buf, "__new__") == 0 or string.strcmp(mname_buf, "__before_init__") == 0 or string.strcmp(mname_buf, "__after_init__") == 0):
skip_def = 1
if skip_def == 0:
key_buf: bytes = stdlib.malloc(key_len + 1)
if key_buf is not None:
if cur_cls is not None: viperlib.snprintf(key_buf, key_len + 1, "%s.%s.%s", src_sha1, cur_cls, mname_buf)
else: viperlib.snprintf(key_buf, key_len + 1, "%s.%s", src_sha1, mname_buf)
# dedup: skip if key already exists (same file may load multiple times)
dup_key: int = 0
dki: int
# Index fast path: when trusted (Built==Count) a single
# probe detects duplicates; otherwise fall back to linear scan.
if _g_vpi_ret_idx_built == _g_vpi_ret_count and _g_vpi_ret_idx is not None and _g_vpi_ret_idx_cap > 0:
if _vpi_idx_find(_g_vpi_ret_keys, VPI_KEY_LEN, _g_vpi_ret_idx, _g_vpi_ret_idx_cap, key_buf) >= 0:
dup_key = 1
else:
# Fast path: equal keys share the first byte, so skip
# strcmp when the candidate's first byte differs.
dup_first: int = key_buf[0]
for dki in range(int(_g_vpi_ret_count)):
ent_d: bytes = _g_vpi_ret_keys + dki * VPI_KEY_LEN
if ent_d[0] == dup_first and string.strcmp(ent_d, key_buf) == 0:
dup_key = 1
break
grow_ok: int = 1
if dup_key == 0 and _g_vpi_ret_count >= _g_vpi_ret_cap: grow_ok = _grow_vpi_ret_cache()
if dup_key == 0 and grow_ok != 0:
string.strcpy(_g_vpi_ret_keys + _g_vpi_ret_count * VPI_KEY_LEN, key_buf)
string.strncpy(_g_vpi_ret_strs + _g_vpi_ret_count * VPI_RET_LEN, ret_src, rtype_len)
(_g_vpi_ret_strs + _g_vpi_ret_count * VPI_RET_LEN)[rtype_len] = '\0'
_g_vpi_ret_count += 1
# Sync index only if trusted before insert (Built==Count-1,
# incl. rebuild) and insert succeeded; else mark untrusted.
if _g_vpi_ret_idx_built == _g_vpi_ret_count - 1 and _vpi_idx_insert(_g_vpi_ret_idx, _g_vpi_ret_idx_cap, _g_vpi_ret_count - 1, key_buf) == 0:
_g_vpi_ret_idx_built = _g_vpi_ret_count
else: _g_vpi_ret_idx_built = -1
stdlib.free(key_buf)
stdlib.free(mname_buf)
pos = le + 1
line_no += 1
if cur_cls is not None: stdlib.free(cur_cls)
return 1
# _cache_vpi_param_annots_text - extract function/method param annotations from vpi_buf text.
# Stored as key="sha1.ClassName.method"/"sha1.method", val='@'-separated annotation strings.
def _cache_vpi_param_annots_text(vpi_buf: bytes, src_sha1: str) -> int:
"""Extract function/method param-annotation strings from vpi_buf text into a persistent stdlib cache."""
global _g_vpi_param_keys, _g_vpi_param_strs, _g_vpi_param_count, _g_vpi_param_cap
global _g_vpi_param_idx, _g_vpi_param_idx_cap, _g_vpi_param_idx_built
if vpi_buf is None or src_sha1 is None: return 0
if _g_vpi_param_keys is None:
_g_vpi_param_keys = stdlib.malloc(VPI_PARAM_MAX * VPI_PARAM_KEY_LEN)
_g_vpi_param_strs = stdlib.malloc(VPI_PARAM_MAX * VPI_PARAM_LEN)
if _g_vpi_param_keys is None or _g_vpi_param_strs is None: return 0
string.memset(_g_vpi_param_keys, 0, VPI_PARAM_MAX * VPI_PARAM_KEY_LEN)
string.memset(_g_vpi_param_strs, 0, VPI_PARAM_MAX * VPI_PARAM_LEN)
_g_vpi_param_cap = VPI_PARAM_MAX
# Name index: slots = key-table capacity * 2. On alloc failure stay untrusted
# (Built=-1) and fall back to linear scan (semantics unchanged).
_g_vpi_param_idx = stdlib.malloc(VPI_PARAM_IDX_CAP * 8)
if _g_vpi_param_idx is not None:
string.memset(_g_vpi_param_idx, 0, VPI_PARAM_IDX_CAP * 8)
_g_vpi_param_idx_cap = VPI_PARAM_IDX_CAP
_g_vpi_param_idx_built = _g_vpi_param_count
else:
_g_vpi_param_idx_cap = 0
_g_vpi_param_idx_built = -1
sha1_len: sizet = string.strlen(src_sha1)
buf_len: sizet = string.strlen(vpi_buf)
cur_cls: bytes = None
pos: sizet = 0
while pos < buf_len:
ls: sizet = pos
le: sizet = pos
while le < buf_len and vpi_buf[le] != '\n': le += 1
line_len: sizet = le - ls
i: sizet = ls
while i < le and vpi_buf[i] == ' ': i += 1
if i < le:
if line_len >= 6 and string.strncmp(vpi_buf + i, "class ", 6) == 0:
cstart: sizet = i + 6
cend: sizet = cstart
while cend < le and vpi_buf[cend] != ':' and vpi_buf[cend] != ' ' and vpi_buf[cend] != '(': cend += 1
if cend > cstart:
if cur_cls is not None: stdlib.free(cur_cls)
cur_cls = stdlib.malloc(cend - cstart + 1)
if cur_cls is not None:
string.strncpy(cur_cls, vpi_buf + cstart, cend - cstart)
cur_cls[cend - cstart] = '\0'
elif line_len >= 4 and string.strncmp(vpi_buf + i, "def ", 4) == 0:
# module-level function (no indent): clear class context
if i == ls and cur_cls is not None:
stdlib.free(cur_cls)
cur_cls = None
dstart: sizet = i + 4
dend: sizet = dstart
while dend < le and vpi_buf[dend] != '(': dend += 1
if dend >= le:
pos = le + 1
continue
# find matching ')' (depth-aware: annotations may contain [ ] / { } brackets)
p: sizet = dend + 1
pdepth: int = 0
pend: sizet = dend
while p < le:
ch_p: char = vpi_buf[p]
if ch_p == '(' or ch_p == '[' or ch_p == '{': pdepth += 1
elif ch_p == ')' or ch_p == ']' or ch_p == '}':
pdepth -= 1
if pdepth < 0:
pend = p
break
p += 1
# parse params segment by segment (top-level commas; skip *args/**kwargs)
plist: bytes = stdlib.malloc(VPI_PARAM_LEN)
if plist is None:
pos = le + 1
continue
plist[0] = '\0'
plist_pos: sizet = 0
seg_written: int = 0
seg_cur: sizet = dend + 1
while seg_cur < pend:
while seg_cur < pend and (vpi_buf[seg_cur] == ' ' or vpi_buf[seg_cur] == ','): seg_cur += 1
if seg_cur >= pend: break
s_start: sizet = seg_cur
sd: int = 0
while seg_cur < pend:
ch_s: char = vpi_buf[seg_cur]
if ch_s == '(' or ch_s == '[' or ch_s == '{': sd += 1
elif ch_s == ')' or ch_s == ']' or ch_s == '}': sd -= 1
elif ch_s == ',' and sd == 0: break
seg_cur += 1
s_end: sizet = seg_cur
# skip * / ** (vararg/kwarg not added to args list)
st: sizet = s_start
while st < s_end and vpi_buf[st] == ' ': st += 1
if st < s_end and vpi_buf[st] == '*':
if seg_cur < pend and vpi_buf[seg_cur] == ',': seg_cur += 1
continue
# cut default value (top-level '=', e.g. ": str = None")
e_pos: sizet = st
ed: int = 0
while e_pos < s_end:
ch_e: char = vpi_buf[e_pos]
if ch_e == '(' or ch_e == '[' or ch_e == '{': ed += 1
elif ch_e == ')' or ch_e == ']' or ch_e == '}': ed -= 1
elif ch_e == '=' and ed == 0: break
e_pos += 1
s_end = e_pos
# extract annotation after top-level ':' (none -> empty, e.g. self)
c_pos: sizet = st
cd2: int = 0
annot_start: sizet = s_end
while c_pos < s_end:
ch_c: char = vpi_buf[c_pos]
if ch_c == '(' or ch_c == '[' or ch_c == '{': cd2 += 1
elif ch_c == ')' or ch_c == ']' or ch_c == '}': cd2 -= 1
elif ch_c == ':' and cd2 == 0:
annot_start = c_pos + 1
break
c_pos += 1
a_start: sizet = annot_start
while a_start < s_end and vpi_buf[a_start] == ' ': a_start += 1
a_end: sizet = s_end
while a_end > a_start and vpi_buf[a_end - 1] == ' ': a_end -= 1
if plist_pos + (a_end - a_start) + 1 < VPI_PARAM_LEN:
if seg_written > 0:
plist[plist_pos] = '@'
plist_pos += 1
ai2: sizet
for ai2 in range(int(a_end - a_start)):
plist[plist_pos] = vpi_buf[a_start + ai2]
plist_pos += 1
plist[plist_pos] = '\0'
seg_written += 1
if seg_cur < pend and vpi_buf[seg_cur] == ',': seg_cur += 1
# store into cache (key = sha1.class.method / sha1.method, dedup)
if plist_pos > 0 and plist[0] != '\0':
mname_len: sizet = dend - dstart
cls_len2: sizet = 0
if cur_cls is not None: cls_len2 = string.strlen(cur_cls)
key_len: sizet = sha1_len + 1 + cls_len2 + 1 + mname_len
if key_len < VPI_PARAM_KEY_LEN:
mname_buf: bytes = stdlib.malloc(mname_len + 1)
if mname_buf is not None:
string.strncpy(mname_buf, vpi_buf + dstart, mname_len)
mname_buf[mname_len] = '\0'
key_buf: bytes = stdlib.malloc(key_len + 1)
if key_buf is not None:
if cur_cls is not None: viperlib.snprintf(key_buf, key_len + 1, "%s.%s.%s", src_sha1, cur_cls, mname_buf)
else: viperlib.snprintf(key_buf, key_len + 1, "%s.%s", src_sha1, mname_buf)
dup_key: int = 0
dki: int
# Index fast path: when trusted (Built==Count) a single
# probe detects duplicates; otherwise fall back to linear scan.
if _g_vpi_param_idx_built == _g_vpi_param_count and _g_vpi_param_idx is not None and _g_vpi_param_idx_cap > 0:
if _vpi_idx_find(_g_vpi_param_keys, VPI_PARAM_KEY_LEN, _g_vpi_param_idx, _g_vpi_param_idx_cap, key_buf) >= 0:
dup_key = 1
else:
# Fast path: equal keys share the first byte, so skip
# strcmp when the candidate's first byte differs.
dup_first: int = key_buf[0]
for dki in range(int(_g_vpi_param_count)):
ent_d: bytes = _g_vpi_param_keys + dki * VPI_PARAM_KEY_LEN
if ent_d[0] == dup_first and string.strcmp(ent_d, key_buf) == 0:
dup_key = 1
break
grow_ok: int = 1
if dup_key == 0 and _g_vpi_param_count >= _g_vpi_param_cap: grow_ok = _grow_vpi_param_cache()
if dup_key == 0 and grow_ok != 0:
string.strcpy(_g_vpi_param_keys + _g_vpi_param_count * VPI_PARAM_KEY_LEN, key_buf)
string.strncpy(_g_vpi_param_strs + _g_vpi_param_count * VPI_PARAM_LEN, plist, plist_pos)
(_g_vpi_param_strs + _g_vpi_param_count * VPI_PARAM_LEN)[plist_pos] = '\0'
_g_vpi_param_count += 1
# Sync index only if trusted before insert (Built==Count-1,
# incl. rebuild) and insert succeeded; else mark untrusted.
if _g_vpi_param_idx_built == _g_vpi_param_count - 1 and _vpi_idx_insert(_g_vpi_param_idx, _g_vpi_param_idx_cap, _g_vpi_param_count - 1, key_buf) == 0:
_g_vpi_param_idx_built = _g_vpi_param_count
else: _g_vpi_param_idx_built = -1
stdlib.free(key_buf)
stdlib.free(mname_buf)
stdlib.free(plist)
pos = le + 1
if cur_cls is not None: stdlib.free(cur_cls)
return 1
# _grow_vpi_param_cache - grow the param-annotation cache (capacity doubles).
def _grow_vpi_param_cache() -> int:
"""Grow the param-annotation cache to double capacity; returns 1=success / 0=failure."""
global _g_vpi_param_keys, _g_vpi_param_strs, _g_vpi_param_cap
if _g_vpi_param_keys is None or _g_vpi_param_cap <= 0: return 0
new_cap: int = _g_vpi_param_cap * 2
if new_cap > 1048576: return 0
new_keys: bytes = stdlib.realloc(_g_vpi_param_keys, new_cap * VPI_PARAM_KEY_LEN)
if new_keys is None: return 0
new_strs: bytes = stdlib.realloc(_g_vpi_param_strs, new_cap * VPI_PARAM_LEN)
if new_strs is None: return 0
_g_vpi_param_keys = new_keys
_g_vpi_param_strs = new_strs
_g_vpi_param_cap = new_cap
# grow the index with the table and rebuild it (slots = capacity * 2, power of 2)
_vpi_rebuild_param_idx(new_cap * 2)
return 1
# _lookup_vpi_param_annots - look up the param-annotation cache.
def _lookup_vpi_param_annots(src_sha1: str, cls_name: str, method_name: str) -> str:
"""Look up the param-annotation cache; returns the '@'-separated string (leading self placeholder) or None."""
if src_sha1 is None or method_name is None or _g_vpi_param_keys is None: return None
sha1_len: sizet = string.strlen(src_sha1)
mlen: sizet = string.strlen(method_name)
clen: sizet = 0
if cls_name is not None: clen = string.strlen(cls_name)
key_len: sizet = sha1_len + clen + mlen + 2
if key_len >= VPI_PARAM_KEY_LEN: return None
key_buf: bytes = stdlib.malloc(key_len + 1)
if key_buf is None: return None
if cls_name is not None: viperlib.snprintf(key_buf, key_len + 1, "%s.%s.%s", src_sha1, cls_name, method_name)
else: viperlib.snprintf(key_buf, key_len + 1, "%s.%s", src_sha1, method_name)
r: str = None
ki: int
# Index fast path: when trusted (Built==Count) one probe returns the hit slot;
# a miss means truly absent, so no linear scan. Otherwise fall back to linear scan.
hit_p: int = -1
if _g_vpi_param_idx_built == _g_vpi_param_count and _g_vpi_param_idx is not None and _g_vpi_param_idx_cap > 0:
hit_p = _vpi_idx_find(_g_vpi_param_keys, VPI_PARAM_KEY_LEN, _g_vpi_param_idx, _g_vpi_param_idx_cap, key_buf)
else:
# Linear-scan fallback: equal keys share the first byte, so skip strcmp when
# the candidate's first byte differs (negative check only, result unchanged).
key_first: int = key_buf[0]
for ki in range(int(_g_vpi_param_count)):
ent_p: bytes = _g_vpi_param_keys + ki * VPI_PARAM_KEY_LEN
if ent_p[0] == key_first and string.strcmp(ent_p, key_buf) == 0:
hit_p = ki
break
if hit_p >= 0: r = _g_vpi_param_strs + hit_p * VPI_PARAM_LEN
stdlib.free(key_buf)
return r
# _resolve_vpi_param_annot_seg_template - param-annotation segment for specialized generics.
# Locates the {sha1}.{base}[T].{method} template entry and substitutes T with the concrete arg.
def _resolve_vpi_param_annot_seg_template(pool: memhub.MemBuddy*, src_sha1: str, cls_name: str, method_name: str, seg_idx: int, i32_ty: mlirlite.LLVMType*, i8_ptr_ty: mlirlite.LLVMType*) -> mlirlite.LLVMType*:
"""Resolve a param-annotation segment for specialized class X[arg] by substituting T with arg; None on miss."""
if pool is None or src_sha1 is None or cls_name is None or method_name is None or _g_vpi_param_keys is None: return None
lbkt_t: str = string.strchr(cls_name, 91) # '[' = 91
if lbkt_t is None: return None
rbkt_t: str = string.strrchr(cls_name, 93) # ']' = 93
if rbkt_t is None or rbkt_t <= lbkt_t: return None
base_len_t: sizet = uint64(lbkt_t) - uint64(cls_name)
arg_len_t: sizet = uint64(rbkt_t) - uint64(lbkt_t) - 1
if base_len_t <= 0 or arg_len_t <= 0: return None
base_t: bytes = stdlib.malloc(base_len_t + 1)
if base_t is None: return None
arg_t: bytes = stdlib.malloc(arg_len_t + 1)
if arg_t is None:
stdlib.free(base_t)
return None
string.strncpy(base_t, cls_name, base_len_t)
base_t[base_len_t] = '\0'
string.strncpy(arg_t, lbkt_t + 1, arg_len_t)
arg_t[arg_len_t] = '\0'
sha1_len_t: sizet = string.strlen(src_sha1)
ty_t: mlirlite.LLVMType* = None
ki_t: int
for ki_t in range(int(_g_vpi_param_count)):
gk_t: bytes = _g_vpi_param_keys + ki_t * VPI_PARAM_KEY_LEN
if string.strncmp(gk_t, src_sha1, sha1_len_t) != 0 or gk_t[sha1_len_t] != '.': continue
if string.strncmp(gk_t + sha1_len_t + 1, base_t, base_len_t) != 0: continue
if gk_t[sha1_len_t + 1 + base_len_t] != 91: continue
gklb_t: str = string.strchr(gk_t + sha1_len_t + 1 + base_len_t, 93)
if gklb_t is None: continue
tplen_t: sizet = uint64(gklb_t) - uint64(gk_t + sha1_len_t + 1 + base_len_t) - 1
# method name must match fully (gklb_t is followed by ']' and '.'; mangled, so unambiguous)
if tplen_t <= 0 or string.strcmp(gklb_t + 2, method_name) != 0: continue
annots_t: str = _g_vpi_param_strs + ki_t * VPI_PARAM_LEN
if annots_t is None or annots_t[0] == '\0': break
# locate segment (same scheme as _resolve_vpi_param_annot_seg: '@'-separated, seg 0 = self empty)
cur_t: sizet = 0
cur_seg_t: int = 0
while cur_seg_t < seg_idx and annots_t[cur_t] != '\0':
while annots_t[cur_t] != '\0' and annots_t[cur_t] != '@': cur_t += 1
if annots_t[cur_t] == '@': cur_t += 1
cur_seg_t += 1
seg_start_t: sizet = cur_t
seg_len_t: sizet = 0
while annots_t[seg_start_t + seg_len_t] != '\0' and annots_t[seg_start_t + seg_len_t] != '@': seg_len_t += 1
if seg_len_t == 0: break
seg_buf_t: bytes = stdlib.malloc(seg_len_t + 1)
if seg_buf_t is None: break
string.strncpy(seg_buf_t, annots_t + seg_start_t, seg_len_t)
seg_buf_t[seg_len_t] = '\0'
# Replace the type-param token (e.g. T) with the concrete arg (e.g. str / AST*):
# template annotations are cached verbatim, so without this "item: T" resolves to nothing.
tparam_t: bytes = stdlib.malloc(tplen_t + 1)
if tparam_t is not None:
string.strncpy(tparam_t, gk_t + sha1_len_t + 1 + base_len_t + 1, tplen_t)
tparam_t[tplen_t] = '\0'
subst_t: str = _subst_template_arg(seg_buf_t, tparam_t, arg_t)
if subst_t is not None:
# _subst_template_arg may return its input unchanged (tplen==0 or tplen>alen),
# so only free the old buffer when subst_t is a different pointer (avoid double free).
if uint64(subst_t) != uint64(seg_buf_t): stdlib.free(seg_buf_t)
seg_buf_t = subst_t
stdlib.free(tparam_t)
ty_t = _resolve_vpi_ret_annot_str(pool, seg_buf_t, i32_ty, i8_ptr_ty)
stdlib.free(seg_buf_t)
break
stdlib.free(base_t)
stdlib.free(arg_t)
return ty_t
# _repair_declare_params_from_text - complete declare params from the text cache.
# When the vpi tree param list is truncated, append missing params so declare matches define.
def _repair_declare_params_from_text(pool: memhub.MemBuddy*, func: mlirlite.Function*, src_sha1: str, cls_name: str, method_name: str, param_count: int, i32_ty: mlirlite.LLVMType*, i8_ptr_ty: mlirlite.LLVMType*) -> int:
"""Complete declare params from the vpi text cache; returns the completed param count."""
if func is None or src_sha1 is None or method_name is None: return param_count
prm_annots: str = _lookup_vpi_param_annots(src_sha1, cls_name, method_name)
if prm_annots is None or prm_annots[0] == '\0': return param_count
# count text params ('@'-separated; segments = separators + 1, incl. first param)
pa_count: int = 1
pi2: sizet = 0
while prm_annots[pi2] != '\0':
if prm_annots[pi2] == '@': pa_count += 1
pi2 += 1
if pa_count <= param_count: return param_count
# append missing params: segments with index >= param_count are the missing ones
seg_idx: int = 0
seg_start2: sizet = 0
cur2: sizet = 0
while prm_annots[cur2] != '\0':
while prm_annots[cur2] != '\0' and prm_annots[cur2] != '@': cur2 += 1
seg_len2: sizet = cur2 - seg_start2
if prm_annots[cur2] == '@': cur2 += 1
if seg_idx >= param_count:
param_ty_r: mlirlite.LLVMType* = i8_ptr_ty
if seg_len2 > 0:
seg_buf_r: bytes = stdlib.malloc(seg_len2 + 1)
if seg_buf_r is not None:
string.strncpy(seg_buf_r, prm_annots + seg_start2, seg_len2)
seg_buf_r[seg_len2] = '\0'
rt: mlirlite.LLVMType* = _resolve_vpi_ret_annot_str(pool, seg_buf_r, i32_ty, i8_ptr_ty)
if rt is not None: param_ty_r = rt
stdlib.free(seg_buf_r)
mlirlite.add_param(pool, func, param_ty_r, "arg")
seg_start2 = cur2
seg_idx += 1
return pa_count
# _resolve_vpi_param_annot_seg - resolve a vpi param-annotation segment to LLVMType.
# Parses the seg_idx-th '@'-separated segment (0 = self) via _resolve_vpi_ret_annot_str.
def _resolve_vpi_param_annot_seg(pool: memhub.MemBuddy*, src_sha1: str, cls_name: str, method_name: str, seg_idx: int, i32_ty: mlirlite.LLVMType*, i8_ptr_ty: mlirlite.LLVMType*) -> mlirlite.LLVMType*:
"""Resolve a vpi param-annotation segment by index to LLVMType; empty segment or failure returns None."""
if pool is None or src_sha1 is None or method_name is None: return None
prm_annots_p: str = _lookup_vpi_param_annots(src_sha1, cls_name, method_name)
if prm_annots_p is None or prm_annots_p[0] == '\0': return None
cur_p: sizet = 0
cur_seg_p: int = 0
while cur_seg_p < seg_idx and prm_annots_p[cur_p] != '\0':
while prm_annots_p[cur_p] != '\0' and prm_annots_p[cur_p] != '@': cur_p += 1
if prm_annots_p[cur_p] == '@': cur_p += 1
cur_seg_p += 1
seg_start_p: sizet = cur_p
seg_len_p: sizet = 0
while prm_annots_p[seg_start_p + seg_len_p] != '\0' and prm_annots_p[seg_start_p + seg_len_p] != '@': seg_len_p += 1
if seg_len_p == 0: return None
seg_buf_p: bytes = stdlib.malloc(seg_len_p + 1)
if seg_buf_p is None: return None
string.strncpy(seg_buf_p, prm_annots_p + seg_start_p, seg_len_p)
seg_buf_p[seg_len_p] = '\0'
seg_ty_p: mlirlite.LLVMType* = _resolve_vpi_ret_annot_str(pool, seg_buf_p, i32_ty, i8_ptr_ty)
stdlib.free(seg_buf_p)
return seg_ty_p
# _grow_vpi_ret_cache - grow the return-annotation cache (capacity doubles).
# Doubles via stdlib.realloc (cap 1M) so entries are not silently dropped when full.
def _grow_vpi_ret_cache() -> int:
"""Grow the return-annotation cache to double capacity; returns 1=success / 0=failure."""
global _g_vpi_ret_keys, _g_vpi_ret_strs, _g_vpi_ret_cap
if _g_vpi_ret_keys is None or _g_vpi_ret_cap <= 0: return 0
new_cap: int = _g_vpi_ret_cap * 2
if new_cap > 1048576: return 0
new_keys: bytes = stdlib.realloc(_g_vpi_ret_keys, new_cap * VPI_KEY_LEN)
if new_keys is None: return 0
new_strs: bytes = stdlib.realloc(_g_vpi_ret_strs, new_cap * VPI_RET_LEN)
if new_strs is None: return 0
_g_vpi_ret_keys = new_keys
_g_vpi_ret_strs = new_strs
_g_vpi_ret_cap = new_cap
# grow the index with the table and rebuild it (slots = capacity * 2, power of 2)
_vpi_rebuild_ret_idx(new_cap * 2)
return 1
# _ensure_vpi_ret_annot_loaded - lazily load a module's vpi stub return-annotation cache.
# On cache miss reads {temp_dir}/{sha1[0:2]}/{sha1}.vpi directly; idempotent.
def _ensure_vpi_ret_annot_loaded(src_sha1: str, temp_dir: str) -> int:
"""Lazily load the module's vpi stub return-annotation cache; returns 1=ready / 0=failure."""
if src_sha1 is None or temp_dir is None: return 0
sha1_len: sizet = string.strlen(src_sha1)
if sha1_len < 2: return 0
# module already cached -> skip (idempotent)
if _g_vpi_ret_keys is not None and _g_vpi_ret_count > 0:
pre_i: int
# Fast path: equal prefixes share the first two bytes (the module subdir name),
# so checking them skips strncmp for most entries.
s0: int = src_sha1[0]
s1: int = src_sha1[1]
for pre_i in range(int(_g_vpi_ret_count)):
pre_key: bytes = _g_vpi_ret_keys + pre_i * VPI_KEY_LEN
if pre_key[0] == s0 and pre_key[1] == s1 and string.strncmp(pre_key, src_sha1, sha1_len) == 0 and pre_key[sha1_len] == '.': return 1
# build path {temp_dir}/{sha1[0:2]}/{sha1}.vpi (char by char, avoiding %c formatting)
td_len: sizet = string.strlen(temp_dir)
path_len: sizet = td_len + sha1_len + 12
vpi_path: bytes = stdlib.malloc(path_len)
if vpi_path is None: return 0
string.strcpy(vpi_path, temp_dir)
vpi_path[td_len] = '/'
vpi_path[td_len + 1] = src_sha1[0]
vpi_path[td_len + 2] = src_sha1[1]
vpi_path[td_len + 3] = '/'
string.strcpy(vpi_path + td_len + 4, src_sha1)
string.strcpy(vpi_path + td_len + 4 + sha1_len, ".vpi")
f: fileio.File* = fileio.File(vpi_path, fileio.MODE.R)
if f.closed:
stdlib.free(vpi_path)
return 0
vpi_buf: bytes = stdlib.malloc(VPI_READ_BUF_SIZE)
if vpi_buf is None:
f.close()
stdlib.free(vpi_path)
return 0
bytes_read: LONG = f.read_all(vpi_buf, VPI_READ_BUF_SIZE)
f.close()
if bytes_read > 0:
if bytes_read < VPI_READ_BUF_SIZE: vpi_buf[bytes_read] = 0
# 单一入口:一次喂齐 ret / param / 逃逸摘要(见 _cache_vpi_annots_text 注释)
_cache_vpi_annots_text(vpi_buf, src_sha1)
stdlib.free(vpi_buf)
stdlib.free(vpi_path)
return 1
# _cache_vpi_esc_clears_text - 抽取行尾**所有** `escg=<packed>` 标记(跨模块常量特化)。
# 与 wf 表同构,但同一行可有多条(一条一个打包值),故是"一键多行"。值 = p|k|m 打包整数
# (见 StructGenMixin._PackEscapeClear:p=守卫形参位、k=常量、m=可清掉的位掩码)。
def _cache_vpi_esc_clears_text(vpi_buf: bytes, src_sha1: str) -> int:
"""抽取 vpi 行尾的 escg= 标记进缓存;1=处理完成 / 0=缓冲区或分配失败"""
global _g_vpi_esg_keys, _g_vpi_esg_vals, _g_vpi_esg_count
global _g_vpi_esd_keys, _g_vpi_esd_e, _g_vpi_esd_n, _g_vpi_esd_r, _g_vpi_esd_count
if vpi_buf is None or src_sha1 is None: return 0
if _g_vpi_esg_keys is None:
_g_vpi_esg_keys = stdlib.malloc(VPI_ESG_MAX * VPI_ESG_KEY_LEN)
_g_vpi_esg_vals = sizet*(stdlib.malloc(VPI_ESG_MAX * 8))
if _g_vpi_esg_keys is None or _g_vpi_esg_vals is None: return 0
string.memset(_g_vpi_esg_keys, 0, VPI_ESG_MAX * VPI_ESG_KEY_LEN)
string.memset(_g_vpi_esg_vals, 0, VPI_ESG_MAX * 8)
if _g_vpi_esd_keys is None:
_g_vpi_esd_keys = stdlib.malloc(VPI_ESG_MAX * VPI_ESG_KEY_LEN)
_g_vpi_esd_e = sizet*(stdlib.malloc(VPI_ESG_MAX * 8))
_g_vpi_esd_n = sizet*(stdlib.malloc(VPI_ESG_MAX * 8))
_g_vpi_esd_r = sizet*(stdlib.malloc(VPI_ESG_MAX * 8))
if _g_vpi_esd_keys is None or _g_vpi_esd_e is None or _g_vpi_esd_n is None or _g_vpi_esd_r is None: return 0
string.memset(_g_vpi_esd_keys, 0, VPI_ESG_MAX * VPI_ESG_KEY_LEN)
string.memset(_g_vpi_esd_e, 0, VPI_ESG_MAX * 8)
string.memset(_g_vpi_esd_n, 0, VPI_ESG_MAX * 8)
string.memset(_g_vpi_esd_r, 0, VPI_ESG_MAX * 8)
sha1_len: sizet = string.strlen(src_sha1)
buf_len: sizet = string.strlen(vpi_buf)
cur_cls: bytes = None
pos: sizet = 0
while pos < buf_len:
ls: sizet = pos
le: sizet = pos
while le < buf_len and vpi_buf[le] != '\n': le += 1
i: sizet = ls
while i < le and vpi_buf[i] == ' ': i += 1
if i < le:
if le - i >= 6 and string.strncmp(vpi_buf + i, "class ", 6) == 0:
cstart: sizet = i + 6
cend: sizet = cstart
while cend < le and vpi_buf[cend] != ':' and vpi_buf[cend] != ' ' and vpi_buf[cend] != '(': cend += 1
if cend > cstart:
if cur_cls is not None: stdlib.free(cur_cls)
cur_cls = stdlib.malloc(cend - cstart + 1)
if cur_cls is not None:
string.strncpy(cur_cls, vpi_buf + cstart, cend - cstart)
cur_cls[cend - cstart] = '\0'
elif le - i >= 4 and string.strncmp(vpi_buf + i, "def ", 4) == 0:
# 模块级函数(无缩进)清掉类上下文,避免上一类泄漏
if i == ls and cur_cls is not None:
stdlib.free(cur_cls)
cur_cls = None
# 只在注释段('#' 之后)里找 escg=:避开参数名
hm: sizet = le
j: sizet = i
while j < le:
if vpi_buf[j] == '#':
hm = j
break
j += 1
dstart: sizet = i + 4
dend: sizet = dstart
while dend < le and vpi_buf[dend] != '(': dend += 1
if hm < le and dend > dstart:
mname_len: sizet = dend - dstart
cls_len_e: sizet = 0
if cur_cls is not None: cls_len_e = string.strlen(cur_cls)
key_len_e: sizet = sha1_len + 1 + cls_len_e + 1 + mname_len
if key_len_e < VPI_ESG_KEY_LEN:
mname_buf_e: bytes = stdlib.malloc(mname_len + 1)
key_buf_e: bytes = stdlib.malloc(key_len_e + 1)
if mname_buf_e is not None and key_buf_e is not None:
string.strncpy(mname_buf_e, vpi_buf + dstart, mname_len)
mname_buf_e[mname_len] = '\0'
if cur_cls is not None: viperlib.snprintf(key_buf_e, key_len_e + 1, "%s.%s.%s", src_sha1, cur_cls, mname_buf_e)
else: viperlib.snprintf(key_buf_e, key_len_e + 1, "%s.%s", src_sha1, mname_buf_e)
j3: sizet = hm + 1
while j3 + 5 <= le:
if vpi_buf[j3] == 'e' and vpi_buf[j3 + 1] == 's' and vpi_buf[j3 + 2] == 'c' and vpi_buf[j3 + 3] == 'g' and vpi_buf[j3 + 4] == '=':
kq: sizet = j3 + 5
eg_v: int = 0
while kq < le and vpi_buf[kq] >= '0' and vpi_buf[kq] <= '9':
eg_v = eg_v * 10 + (int(vpi_buf[kq]) - 48)
kq += 1
if eg_v > 0 and _g_vpi_esg_count < VPI_ESG_MAX:
string.strcpy(_g_vpi_esg_keys + sizet(_g_vpi_esg_count) * VPI_ESG_KEY_LEN, key_buf_e)
_g_vpi_esg_vals[_g_vpi_esg_count] = sizet(eg_v)
_g_vpi_esg_count += 1
j3 = kq
else:
j3 += 1
# 声明式指令契约(esce= / escn= / escr=):每函数一条,只认 '#' 注释段
de_v: int = 0
dn_v: int = 0
dr_v: int = 0
j4: sizet = hm + 1
while j4 + 5 <= le:
if vpi_buf[j4] == 'e' and vpi_buf[j4 + 1] == 's' and vpi_buf[j4 + 2] == 'c' and vpi_buf[j4 + 3] == 'e' and vpi_buf[j4 + 4] == '=':
kq2: sizet = j4 + 5
v2: int = 0
while kq2 < le and vpi_buf[kq2] >= '0' and vpi_buf[kq2] <= '9':
v2 = v2 * 10 + (int(vpi_buf[kq2]) - 48)
kq2 += 1
if v2 > 0: de_v = v2
j4 = kq2
elif vpi_buf[j4] == 'e' and vpi_buf[j4 + 1] == 's' and vpi_buf[j4 + 2] == 'c' and vpi_buf[j4 + 3] == 'n' and vpi_buf[j4 + 4] == '=':
kq3: sizet = j4 + 5
v3: int = 0
while kq3 < le and vpi_buf[kq3] >= '0' and vpi_buf[kq3] <= '9':
v3 = v3 * 10 + (int(vpi_buf[kq3]) - 48)
kq3 += 1
if v3 > 0: dn_v = v3
j4 = kq3
elif vpi_buf[j4] == 'e' and vpi_buf[j4 + 1] == 's' and vpi_buf[j4 + 2] == 'c' and vpi_buf[j4 + 3] == 'r' and vpi_buf[j4 + 4] == '=':
kq5: sizet = j4 + 5
v5: int = 0
while kq5 < le and vpi_buf[kq5] >= '0' and vpi_buf[kq5] <= '9':
v5 = v5 * 10 + (int(vpi_buf[kq5]) - 48)
kq5 += 1
if v5 > 0: dr_v = v5
j4 = kq5
else:
j4 += 1
if (de_v != 0 or dn_v != 0 or dr_v != 0) and _g_vpi_esd_count < VPI_ESG_MAX:
string.strcpy(_g_vpi_esd_keys + sizet(_g_vpi_esd_count) * VPI_ESG_KEY_LEN, key_buf_e)
_g_vpi_esd_e[_g_vpi_esd_count] = sizet(de_v)
_g_vpi_esd_n[_g_vpi_esd_count] = sizet(dn_v)
_g_vpi_esd_r[_g_vpi_esd_count] = sizet(dr_v)
_g_vpi_esd_count += 1
if mname_buf_e is not None: stdlib.free(mname_buf_e)
if key_buf_e is not None: stdlib.free(key_buf_e)
pos = le + 1
if cur_cls is not None: stdlib.free(cur_cls)
return 1
# _lookup_vpi_escape_clears - 跨模块常量守卫查询:按调用点常量环境算出"该清掉的位";0 = 无命中
def _lookup_vpi_escape_clears(src_sha1: str, cls_name: str, method_name: str, env: sizet*) -> int:
"""env = 常量环境(2 槽/形参:[0]=是否已知、[1]=值,下标 = 形参位);返回该清掉的位掩码 OR 值"""
if _g_vpi_esg_keys is None or _g_vpi_esg_count == 0: return 0
if src_sha1 is None or method_name is None or env is None: return 0
sha1_len: sizet = string.strlen(src_sha1)
cl_len: sizet = 0
if cls_name is not None: cl_len = string.strlen(cls_name)
me_len: sizet = string.strlen(method_name)
key_len: sizet = sha1_len + 1 + cl_len + 1 + me_len
kb: bytes = stdlib.malloc(key_len + 1)
if kb is None: return 0
if cls_name is not None: viperlib.snprintf(kb, key_len + 1, "%s.%s.%s", src_sha1, cls_name, method_name)
else: viperlib.snprintf(kb, key_len + 1, "%s.%s", src_sha1, method_name)
cleared: int = 0
idx: int = 0
while idx < _g_vpi_esg_count:
ent: bytes = _g_vpi_esg_keys + sizet(idx) * VPI_ESG_KEY_LEN
# 键以模块 sha1 打头:先比前 2 个十六进制字符,避免逐项 strcmp
if ent[0] == kb[0] and ent[1] == kb[1] and string.strcmp(ent, kb) == 0:
v: int = int(_g_vpi_esg_vals[idx])
if v > 0:
p: int = v & 255
kv: int = (v >> 8) & 255
if kv >= 128: kv -= 256
m: int = v >> 16
if p >= 0 and p < VPI_ESG_ENV_PARAM_MAX and env[p * 2] != 0:
if int64(env[p * 2 + 1]) == int64(kv): cleared = cleared | m
idx += 1
stdlib.free(kb)
return cleared
# _lookup_vpi_escape_dir - 跨模块声明式指令契约查询(`@life(escape(...))` / `@life(noescape(...))` /
# `@life(ret(...))`)。which: 0=强制逃逸位 / 1=强制不逃逸位 / 2=ret_kind(A2)。
# 0 = 无该标注(含该模块未加载过)。
def _lookup_vpi_escape_dir(src_sha1: str, cls_name: str, method_name: str, which: int) -> int:
if _g_vpi_esd_keys is None or _g_vpi_esd_count == 0: return 0
if src_sha1 is None or method_name is None: return 0
sha1_len: sizet = string.strlen(src_sha1)
cl_len: sizet = 0
if cls_name is not None: cl_len = string.strlen(cls_name)
me_len: sizet = string.strlen(method_name)
key_len: sizet = sha1_len + 1 + cl_len + 1 + me_len
kb: bytes = stdlib.malloc(key_len + 1)
if kb is None: return 0
if cls_name is not None: viperlib.snprintf(kb, key_len + 1, "%s.%s.%s", src_sha1, cls_name, method_name)
else: viperlib.snprintf(kb, key_len + 1, "%s.%s", src_sha1, method_name)
r: int = 0
i: int = 0
while i < _g_vpi_esd_count:
ent: bytes = _g_vpi_esd_keys + sizet(i) * VPI_ESG_KEY_LEN
# 键以模块 sha1 打头:先比前 2 个十六进制字符,避免逐项 strcmp
if ent[0] == kb[0] and ent[1] == kb[1] and string.strcmp(ent, kb) == 0:
if which == 2: r = int(_g_vpi_esd_r[i])
elif which != 0: r = int(_g_vpi_esd_n[i])
else: r = int(_g_vpi_esd_e[i])
break
i += 1
stdlib.free(kb)
return r
# _cache_vpi_annots_text - 所有"从 vpi 文本解析"的缓存**单一入口**。
# 任何新增的文本缓存都加在这里,不要再散落到各加载点——漏喂一处就会**静默退化**
# (现场:逃逸摘要只在 _ensure_vpi_ret_annot_loaded 里喂,而 stub 注入加载点没喂,
# 前者开头又有"本模块 ret 注解已缓存即 return 1"的短路,于是跨模块逃逸查询永久查空,
# EscapeXmodTest 漏检)。
def _cache_vpi_annots_text(vpi_buf: bytes, src_sha1: str) -> int:
"""一次性把 vpi 文本派生出的所有缓存喂齐;1=完成 / 0=入参非法"""
if vpi_buf is None or src_sha1 is None: return 0
_cache_vpi_return_annots_text(vpi_buf, src_sha1)
_cache_vpi_param_annots_text(vpi_buf, src_sha1)
_cache_vpi_escape_annots_text(vpi_buf, src_sha1)
_cache_vpi_willfree_annots_text(vpi_buf, src_sha1)
_cache_vpi_esc_clears_text(vpi_buf, src_sha1)
return 1
# _cache_vpi_escape_annots_text - extract the trailing `# esc=<mask> ret=<ret>` markers.
# v3 跨模块逃逸摘要:不依赖 AST,按行扫描(与 _cache_vpi_return_annots_text 同构)。
def _cache_vpi_escape_annots_text(vpi_buf: bytes, src_sha1: str) -> int:
"""抽取 vpi 行尾逃逸摘要标记进缓存;1=处理完成 / 0=缓冲区或分配失败"""
global _g_vpi_esc_keys, _g_vpi_esc_vals, _g_vpi_esc_count
if vpi_buf is None or src_sha1 is None: return 0
if _g_vpi_esc_keys is None:
_g_vpi_esc_keys = stdlib.malloc(VPI_ESC_MAX * VPI_ESC_KEY_LEN)
_g_vpi_esc_vals = sizet*(stdlib.malloc(VPI_ESC_MAX * 8))
if _g_vpi_esc_keys is None or _g_vpi_esc_vals is None: return 0
string.memset(_g_vpi_esc_keys, 0, VPI_ESC_MAX * VPI_ESC_KEY_LEN)
string.memset(_g_vpi_esc_vals, 0, VPI_ESC_MAX * 8)
sha1_len: sizet = string.strlen(src_sha1)
buf_len: sizet = string.strlen(vpi_buf)
cur_cls: bytes = None
pos: sizet = 0
while pos < buf_len:
ls: sizet = pos
le: sizet = pos
while le < buf_len and vpi_buf[le] != '\n': le += 1
i: sizet = ls
while i < le and vpi_buf[i] == ' ': i += 1
if i < le:
# class X: -> 记录当前类上下文(与方法键一致)
if le - i >= 6 and string.strncmp(vpi_buf + i, "class ", 6) == 0:
cstart: sizet = i + 6
cend: sizet = cstart
while cend < le and vpi_buf[cend] != ':' and vpi_buf[cend] != ' ' and vpi_buf[cend] != '(': cend += 1
if cend > cstart:
if cur_cls is not None: stdlib.free(cur_cls)
cur_cls = stdlib.malloc(cend - cstart + 1)
if cur_cls is not None:
string.strncpy(cur_cls, vpi_buf + cstart, cend - cstart)
cur_cls[cend - cstart] = '\0'
# def name(...) ... # esc=<mask> ret=<ret>
elif le - i >= 4 and string.strncmp(vpi_buf + i, "def ", 4) == 0:
# 模块级函数(无缩进)清掉类上下文,避免上一类泄漏
if i == ls and cur_cls is not None:
stdlib.free(cur_cls)
cur_cls = None
# 找同行 "# esc=" 标记
mark: sizet = le
j: sizet = i
while j + 6 <= le:
if vpi_buf[j] == '#' and vpi_buf[j + 1] == ' ' and string.strncmp(vpi_buf + j + 2, "esc=", 4) == 0:
mark = j
break
j += 1
if mark < le:
kp: sizet = mark + 6
esc_v: int = 0
while kp < le and vpi_buf[kp] >= '0' and vpi_buf[kp] <= '9':
esc_v = esc_v * 10 + (int(vpi_buf[kp]) - 48)
kp += 1
ret_v: int = 0
while kp < le and vpi_buf[kp] != 'e': kp += 1
if kp + 4 <= le:
kp += 4
while kp < le and vpi_buf[kp] >= '0' and vpi_buf[kp] <= '9':
ret_v = ret_v * 10 + (int(vpi_buf[kp]) - 48)
kp += 1
if (esc_v != 0 or ret_v != 0) and _g_vpi_esc_count < VPI_ESC_MAX:
dstart: sizet = i + 4
dend: sizet = dstart
while dend < le and vpi_buf[dend] != '(': dend += 1
if dend > dstart:
mname_len: sizet = dend - dstart
cls_len_e: sizet = 0
if cur_cls is not None: cls_len_e = string.strlen(cur_cls)
key_len_e: sizet = sha1_len + 1 + cls_len_e + 1 + mname_len
if key_len_e < VPI_ESC_KEY_LEN:
mname_buf_e: bytes = stdlib.malloc(mname_len + 1)
key_buf_e: bytes = stdlib.malloc(key_len_e + 1)
if mname_buf_e is not None and key_buf_e is not None:
string.strncpy(mname_buf_e, vpi_buf + dstart, mname_len)
mname_buf_e[mname_len] = '\0'
if cur_cls is not None: viperlib.snprintf(key_buf_e, key_len_e + 1, "%s.%s.%s", src_sha1, cur_cls, mname_buf_e)
else: viperlib.snprintf(key_buf_e, key_len_e + 1, "%s.%s", src_sha1, mname_buf_e)
string.strcpy(_g_vpi_esc_keys + sizet(_g_vpi_esc_count) * VPI_ESC_KEY_LEN, key_buf_e)
_g_vpi_esc_vals[_g_vpi_esc_count] = sizet(esc_v) | (sizet(ret_v) << 32)
_g_vpi_esc_count += 1
if mname_buf_e is not None: stdlib.free(mname_buf_e)
if key_buf_e is not None: stdlib.free(key_buf_e)
pos = le + 1
if cur_cls is not None: stdlib.free(cur_cls)
return 1
# _lookup_vpi_escape_packed - query the cross-module escape summary cache (v3).
def _lookup_vpi_escape_packed(src_sha1: str, cls_name: str, method_name: str) -> int:
"""查跨模块逃逸摘要;返回打包值(低32位 mask | ret<<32),0 = 无摘要"""
if _g_vpi_esc_keys is None or _g_vpi_esc_count == 0: return 0
if src_sha1 is None or method_name is None: return 0
sha1_len: sizet = string.strlen(src_sha1)
cl_len: sizet = 0
if cls_name is not None: cl_len = string.strlen(cls_name)
me_len: sizet = string.strlen(method_name)
key_len: sizet = sha1_len + 1 + cl_len + 1 + me_len
kb: bytes = stdlib.malloc(key_len + 1)
if kb is None: return 0
if cls_name is not None: viperlib.snprintf(kb, key_len + 1, "%s.%s.%s", src_sha1, cls_name, method_name)
else: viperlib.snprintf(kb, key_len + 1, "%s.%s", src_sha1, method_name)
res: int = 0
idx: int = 0
while idx < _g_vpi_esc_count:
ent: bytes = _g_vpi_esc_keys + sizet(idx) * VPI_ESC_KEY_LEN
# 键以模块 sha1 打头(前 2 个十六进制字符足以筛掉绝大多数候选,避免逐项 strcmp)
if ent[0] == kb[0] and ent[1] == kb[1] and string.strcmp(ent, kb) == 0:
res = int(_g_vpi_esc_vals[idx])
break
idx += 1
stdlib.free(kb)
return res
# _cache_vpi_willfree_annots_text - extract the trailing `wf=<mask>` marker (P2 willFree).
# 与逃逸摘要同构:不依赖 AST,按行扫描;只在有 wf 标记的 def 上建条目。
def _cache_vpi_willfree_annots_text(vpi_buf: bytes, src_sha1: str) -> int:
"""抽取 vpi 行尾 P2 willFree 标记(`# ... wf=<mask>`)进缓存;1=处理完成 / 0=缓冲区或分配失败"""
global _g_vpi_wf_keys, _g_vpi_wf_vals, _g_vpi_wf_count
if vpi_buf is None or src_sha1 is None: return 0
if _g_vpi_wf_keys is None:
_g_vpi_wf_keys = stdlib.malloc(VPI_WF_MAX * VPI_WF_KEY_LEN)
_g_vpi_wf_vals = sizet*(stdlib.malloc(VPI_WF_MAX * 8))
if _g_vpi_wf_keys is None or _g_vpi_wf_vals is None: return 0
string.memset(_g_vpi_wf_keys, 0, VPI_WF_MAX * VPI_WF_KEY_LEN)
string.memset(_g_vpi_wf_vals, 0, VPI_WF_MAX * 8)
sha1_len: sizet = string.strlen(src_sha1)
buf_len: sizet = string.strlen(vpi_buf)
cur_cls: bytes = None
pos: sizet = 0
while pos < buf_len:
ls: sizet = pos
le: sizet = pos
while le < buf_len and vpi_buf[le] != '\n': le += 1
i: sizet = ls
while i < le and vpi_buf[i] == ' ': i += 1
if i < le:
# class X: -> 记录当前类上下文(与方法键一致)
if le - i >= 6 and string.strncmp(vpi_buf + i, "class ", 6) == 0:
cstart: sizet = i + 6
cend: sizet = cstart
while cend < le and vpi_buf[cend] != ':' and vpi_buf[cend] != ' ' and vpi_buf[cend] != '(': cend += 1
if cend > cstart:
if cur_cls is not None: stdlib.free(cur_cls)
cur_cls = stdlib.malloc(cend - cstart + 1)
if cur_cls is not None:
string.strncpy(cur_cls, vpi_buf + cstart, cend - cstart)
cur_cls[cend - cstart] = '\0'
# def name(...) ... # [esc=<mask> ret=<ret>] wf=<mask>
elif le - i >= 4 and string.strncmp(vpi_buf + i, "def ", 4) == 0:
# 模块级函数(无缩进)清掉类上下文,避免上一类泄漏
if i == ls and cur_cls is not None:
stdlib.free(cur_cls)
cur_cls = None
# 只在注释段里找 "wf=":避开参数名(如 def f(wf: int))
mark: sizet = le
hm: sizet = le
j: sizet = i
while j < le:
if vpi_buf[j] == '#':
hm = j
break
j += 1
if hm < le:
j2: sizet = hm + 1
while j2 + 3 <= le:
if vpi_buf[j2] == 'w' and vpi_buf[j2 + 1] == 'f' and vpi_buf[j2 + 2] == '=':
mark = j2
break
j2 += 1
if mark < le:
kp: sizet = mark + 3
wf_v: int = 0
while kp < le and vpi_buf[kp] >= '0' and vpi_buf[kp] <= '9':
wf_v = wf_v * 10 + (int(vpi_buf[kp]) - 48)
kp += 1
if wf_v != 0 and _g_vpi_wf_count < VPI_WF_MAX:
dstart: sizet = i + 4
dend: sizet = dstart
while dend < le and vpi_buf[dend] != '(': dend += 1
if dend > dstart:
mname_len: sizet = dend - dstart
cls_len_w: sizet = 0
if cur_cls is not None: cls_len_w = string.strlen(cur_cls)
key_len_w: sizet = sha1_len + 1 + cls_len_w + 1 + mname_len
if key_len_w < VPI_WF_KEY_LEN:
mname_buf_w: bytes = stdlib.malloc(mname_len + 1)
key_buf_w: bytes = stdlib.malloc(key_len_w + 1)
if mname_buf_w is not None and key_buf_w is not None:
string.strncpy(mname_buf_w, vpi_buf + dstart, mname_len)
mname_buf_w[mname_len] = '\0'
if cur_cls is not None: viperlib.snprintf(key_buf_w, key_len_w + 1, "%s.%s.%s", src_sha1, cur_cls, mname_buf_w)
else: viperlib.snprintf(key_buf_w, key_len_w + 1, "%s.%s", src_sha1, mname_buf_w)
string.strcpy(_g_vpi_wf_keys + sizet(_g_vpi_wf_count) * VPI_WF_KEY_LEN, key_buf_w)
_g_vpi_wf_vals[_g_vpi_wf_count] = sizet(wf_v)
_g_vpi_wf_count += 1
if mname_buf_w is not None: stdlib.free(mname_buf_w)
if key_buf_w is not None: stdlib.free(key_buf_w)
pos = le + 1
if cur_cls is not None: stdlib.free(cur_cls)
return 1
# _lookup_vpi_willfree - query the cross-module P2 willFree cache (key shape = escape summary).
def _lookup_vpi_willfree(src_sha1: str, cls_name: str, method_name: str) -> int:
"""查跨模块 willFree 形参掩码;0 = 无标注"""
if _g_vpi_wf_keys is None or _g_vpi_wf_count == 0: return 0
if src_sha1 is None or method_name is None: return 0
sha1_len: sizet = string.strlen(src_sha1)
cl_len: sizet = 0
if cls_name is not None: cl_len = string.strlen(cls_name)
me_len: sizet = string.strlen(method_name)
key_len: sizet = sha1_len + 1 + cl_len + 1 + me_len
kb: bytes = stdlib.malloc(key_len + 1)
if kb is None: return 0
if cls_name is not None: viperlib.snprintf(kb, key_len + 1, "%s.%s.%s", src_sha1, cls_name, method_name)
else: viperlib.snprintf(kb, key_len + 1, "%s.%s", src_sha1, method_name)
res: int = 0
idx: int = 0
while idx < _g_vpi_wf_count:
ent: bytes = _g_vpi_wf_keys + sizet(idx) * VPI_WF_KEY_LEN
# 键以模块 sha1 打头:先比前 2 个十六进制字符,避免逐项 strcmp
if ent[0] == kb[0] and ent[1] == kb[1] and string.strcmp(ent, kb) == 0:
res = int(_g_vpi_wf_vals[idx])
break
idx += 1
stdlib.free(kb)
return res
# _vpi_param_annot_seg_is - equality check for a param-annotation segment (for the "str/bytes is not a generic pointer" rule).
def _vpi_param_annot_seg_is(src_sha1: str, cls_name: str, method_name: str, seg_idx: int, want: str) -> int:
"""Check whether the definition-side param-annotation segment equals want exactly; 1=yes / 0=no."""
if want is None or seg_idx < 0: return 0
prm: str = _lookup_vpi_param_annots(src_sha1, cls_name, method_name)
if prm is None or prm[0] == '\0': return 0
cur: sizet = 0
# locate segment start: skip seg_idx '@'-separated segments
for seg in range(int(seg_idx)):
while prm[cur] != '\0' and prm[cur] != '@': cur += 1
if prm[cur] == '\0': return 0
cur += 1
slen: sizet = 0
while prm[cur + slen] != '\0' and prm[cur + slen] != '@': slen += 1
wlen: sizet = string.strlen(want)
if slen != wlen: return 0
i: sizet
for i in range(int(slen)):
if prm[cur + i] != want[i]: return 0
return 1
# _vpi_param_annot_seg_mode - value-reference mode of a param-annotation segment: 0=none / 1=ref / 2=temp.
# The definition side writes `T | t.CRef` / `T | t.CTempRef`; the call side mirrors it (auto-address).
def _vpi_param_annot_seg_mode(prm_annots: str, seg_idx: int) -> int:
"""Return the value-reference mode encoded in a vpi param-annotation segment."""
if prm_annots is None or seg_idx < 0: return 0
cur: sizet = 0
for seg in range(int(seg_idx)):
while prm_annots[cur] != '\0' and prm_annots[cur] != '@': cur += 1
if prm_annots[cur] == '\0': return 0
cur += 1
slen: sizet = 0
while prm_annots[cur + slen] != '\0' and prm_annots[cur + slen] != '@': slen += 1
if slen == 0: return 0
seg_buf: bytes = stdlib.malloc(slen + 1)
if seg_buf is None: return 0
string.strncpy(seg_buf, prm_annots + cur, slen)
seg_buf[slen] = '\0'
mode: int = 0
if string.strstr(seg_buf, "CTempRef") is not None: mode = 2
elif string.strstr(seg_buf, "CRef") is not None: mode = 1
elif seg_buf[0] == '&':
# The stub may carry the union marker (`T | t.CRef`, written by this compiler's StubGen) or
# the source syntax (`&T` / `&temp T`, written by TPC's StubGen). Accept both, else a
# cross-module value-reference call would silently lose its auto-address.
q: sizet = 0
while seg_buf[q] == ' ' or seg_buf[q] == '\t': q += 1
if seg_buf[q] == '&':
q += 1
while seg_buf[q] == ' ' or seg_buf[q] == '\t': q += 1
if slen - q >= 4 and string.strncmp(seg_buf + q, "temp", 4) == 0: mode = 2
else: mode = 1
stdlib.free(seg_buf)
return mode
# _ensure_vpi_param_annot_loaded - lazily load a module's vpi stub param-annotation cache.
# Mirror of the return variant; reads the .vpi file on miss, idempotent.
def _ensure_vpi_param_annot_loaded(src_sha1: str, temp_dir: str) -> int:
"""Lazily load the module's vpi stub param-annotation cache; returns 1=ready / 0=failure."""
if src_sha1 is None or temp_dir is None: return 0
sha1_len: sizet = string.strlen(src_sha1)
if sha1_len < 2: return 0
# module already has param cache entries -> skip (idempotent)
if _g_vpi_param_keys is not None and _g_vpi_param_count > 0:
pre_i: int
# Fast path: equal prefixes share the first two bytes (the module subdir name),
# so checking them skips strncmp for most entries.
s0: int = src_sha1[0]
s1: int = src_sha1[1]
for pre_i in range(int(_g_vpi_param_count)):
pre_key: bytes = _g_vpi_param_keys + pre_i * VPI_PARAM_KEY_LEN
if pre_key[0] == s0 and pre_key[1] == s1 and string.strncmp(pre_key, src_sha1, sha1_len) == 0 and pre_key[sha1_len] == '.': return 1
# build path {temp_dir}/{sha1[0:2]}/{sha1}.vpi (char by char, avoiding %c formatting)
td_len: sizet = string.strlen(temp_dir)
path_len: sizet = td_len + sha1_len + 12
vpi_path: bytes = stdlib.malloc(path_len)
if vpi_path is None: return 0
string.strcpy(vpi_path, temp_dir)
vpi_path[td_len] = '/'
vpi_path[td_len + 1] = src_sha1[0]
vpi_path[td_len + 2] = src_sha1[1]
vpi_path[td_len + 3] = '/'
string.strcpy(vpi_path + td_len + 4, src_sha1)
string.strcpy(vpi_path + td_len + 4 + sha1_len, ".vpi")
f: fileio.File* = fileio.File(vpi_path, fileio.MODE.R)
if f.closed:
stdlib.free(vpi_path)
return 0
vpi_buf: bytes = stdlib.malloc(VPI_READ_BUF_SIZE)
if vpi_buf is None:
f.close()
stdlib.free(vpi_path)
return 0
bytes_read: LONG = f.read_all(vpi_buf, VPI_READ_BUF_SIZE)
f.close()
if bytes_read > 0:
if bytes_read < VPI_READ_BUF_SIZE: vpi_buf[bytes_read] = 0
_cache_vpi_param_annots_text(vpi_buf, src_sha1)
stdlib.free(vpi_buf)
stdlib.free(vpi_path)
return 1
# _subst_template_arg - replace standalone template-parameter tokens in an annotation string.
# Replaces only boundary-delimited tokens (not inside sizet etc.); returns a new stdlib string.
def _subst_template_arg(annot: str, tparam: str, targ: str) -> str:
"""Replace standalone tparam tokens in annot with targ."""
if annot is None or tparam is None or targ is None: return None
alen: sizet = string.strlen(annot)
tplen: sizet = string.strlen(tparam)
tglen: sizet = string.strlen(targ)
if tplen == 0 or tplen > alen: return annot
# Allocate the worst-case upper bound: each replacement consumes tplen and writes tglen,
# so worst case is alen + (alen/tplen)*tglen + 1 (avoids writing past the heap).
max_sub: sizet = alen / tplen
if max_sub == 0: max_sub = 1
out_size: sizet = alen + max_sub * tglen + 1
out: bytes = stdlib.malloc(out_size)
if out is None: return None
opos: sizet = 0
p: sizet = 0
while p < alen:
# guard: fail fast (return None) if opos would exceed the allocation (never overrun)
if opos >= out_size:
stdlib.free(out)
return None
matched: int = 0
if annot[p] == tparam[0] and p + tplen <= alen:
if string.strncmp(annot + p, tparam, tplen) == 0:
prev_ch: int = 0
next_ch: int = 0
if p > 0: prev_ch = annot[p - 1]
if p + tplen < alen: next_ch = annot[p + tplen]
prev_ok: int = 1
if (prev_ch >= 'a' and prev_ch <= 'z') or (prev_ch >= 'A' and prev_ch <= 'Z') or (prev_ch >= '0' and prev_ch <= '9') or prev_ch == '_': prev_ok = 0
next_ok: int = 1
if (next_ch >= 'a' and next_ch <= 'z') or (next_ch >= 'A' and next_ch <= 'Z') or (next_ch >= '0' and next_ch <= '9') or next_ch == '_': next_ok = 0
if prev_ok != 0 and next_ok != 0:
string.strncpy(out + opos, targ, tglen)
opos += tglen
p += tplen
matched = 1
if matched == 0:
out[opos] = annot[p]
opos += 1
p += 1
out[opos] = '\0'
return out
# _lookup_vpi_ret_annot - look up a method's cached return-annotation string.
def _lookup_vpi_ret_annot(src_sha1: str, cls_name: str, method_name: str) -> str:
"""Look up a method's cached return-annotation string; None if not found."""
if _g_vpi_ret_keys is None or _g_vpi_ret_count == 0: return None
if src_sha1 is None or method_name is None: return None
sha1_len: sizet = string.strlen(src_sha1)
cl_len: sizet = 0
if cls_name is not None: cl_len = string.strlen(cls_name)
me_len: sizet = string.strlen(method_name)
key_len: sizet = sha1_len + 1 + cl_len + 1 + me_len
key_buf: bytes = stdlib.malloc(key_len + 1)
if key_buf is None: return None
if cls_name is not None: viperlib.snprintf(key_buf, key_len + 1, "%s.%s.%s", src_sha1, cls_name, method_name)
else: viperlib.snprintf(key_buf, key_len + 1, "%s.%s", src_sha1, method_name)
i: int
# Index fast path: when trusted (Built==Count) one probe returns the hit slot;
# a miss means truly absent, so skip the exact linear scan. Otherwise fall back.
hit_r: int = -1
if _g_vpi_ret_idx_built == _g_vpi_ret_count and _g_vpi_ret_idx is not None and _g_vpi_ret_idx_cap > 0:
hit_r = _vpi_idx_find(_g_vpi_ret_keys, VPI_KEY_LEN, _g_vpi_ret_idx, _g_vpi_ret_idx_cap, key_buf)
else:
# Linear-scan fallback: equal keys share the first byte; skip candidates whose first byte differs.
key_first: int = key_buf[0]
for i in range(int(_g_vpi_ret_count)):
ent_r: bytes = _g_vpi_ret_keys + i * VPI_KEY_LEN
if ent_r[0] == key_first and string.strcmp(ent_r, key_buf) == 0:
hit_r = i
break
if hit_r >= 0:
stdlib.free(key_buf)
return _g_vpi_ret_strs + hit_r * VPI_RET_LEN
# Generic specialization fallback: list[AST*].get -> list[T].get template cache.
# Substitutes the type-param name (e.g. T) with the concrete arg; no registration needed.
if cls_name is not None:
lbkt_v: str = string.strchr(cls_name, '[')
if lbkt_v is not None:
rbkt_v: str = string.strrchr(cls_name, ']')
if rbkt_v is not None and rbkt_v > lbkt_v:
base_len_v: sizet = sizet(uint64(lbkt_v) - uint64(cls_name))
param_len_v: sizet = sizet(uint64(rbkt_v) - uint64(lbkt_v) - 1)
if base_len_v > 0 and param_len_v > 0:
base_v: str = stdlib.malloc(base_len_v + 1)
param_v: str = stdlib.malloc(param_len_v + 1)
if base_v is not None and param_v is not None:
string.strncpy(base_v, cls_name, base_len_v)
base_v[base_len_v] = '\0'
string.strncpy(param_v, lbkt_v + 1, param_len_v)
param_v[param_len_v] = '\0'
# scan cache for {sha1}.{base}[xxx].{method}
gi: int
for gi in range(int(_g_vpi_ret_count)):
gk: bytes = _g_vpi_ret_keys + gi * VPI_KEY_LEN
if string.strncmp(gk, src_sha1, sha1_len) == 0 and gk[sha1_len] == '.' and string.strncmp(gk + sha1_len + 1, base_v, base_len_v) == 0 and gk[sha1_len + 1 + base_len_v] == '[':
gklb: str = string.strchr(gk + sha1_len + 1 + base_len_v, ']')
if gklb is not None:
tparam_len_v: sizet = sizet(uint64(gklb) - uint64(gk + sha1_len + 1 + base_len_v) - 1)
meth_start_v: str = gklb + 2 # skip ']' and '.'
if tparam_len_v > 0 and string.strcmp(meth_start_v, method_name) == 0:
tparam_v: str = stdlib.malloc(tparam_len_v + 1)
if tparam_v is not None:
string.strncpy(tparam_v, gk + sha1_len + 1 + base_len_v + 1, tparam_len_v)
tparam_v[tparam_len_v] = '\0'
new_annot_v: str = _subst_template_arg(_g_vpi_ret_strs + gi * VPI_RET_LEN, tparam_v, param_v)
stdlib.free(tparam_v)
stdlib.free(base_v)
stdlib.free(param_v)
stdlib.free(key_buf)
return new_annot_v
stdlib.free(base_v)
stdlib.free(param_v)
stdlib.free(key_buf)
return None
# _resolve_single_vpi_type_str - resolve a single type segment to LLVMType.
# Pure string parsing, no AST nodes.
def _resolve_single_vpi_type_str(pool: memhub.MemBuddy*, seg: str) -> mlirlite.LLVMType*:
"""Resolve a single type segment to LLVMType; None if unrecognized."""
if seg is None or pool is None: return None
# Canonical pointer form is `T*` (also rawptr[T] / t.CPtr[T]); rewrite trailing `*`
# into rawptr[...] so specialized args like "{sha}.AST*" resolve instead of silently returning None.
seg_len_ps: sizet = string.strlen(seg)
star_end_ps: sizet = seg_len_ps
while star_end_ps > 0 and seg[star_end_ps - 1] == ' ': star_end_ps -= 1
star_cnt_ps: sizet = 0
star_base_end_ps: sizet = star_end_ps
while star_base_end_ps > 0 and seg[star_base_end_ps - 1] == '*':
star_cnt_ps += 1
star_base_end_ps -= 1
if star_cnt_ps > 0 and star_base_end_ps > 0:
# base segment length = star_base_end_ps; each rawptr[ + ] layer is 8 chars
star_buf_len_ps: sizet = star_base_end_ps + 8 * star_cnt_ps + 1
star_buf_ps: bytes = stdlib.malloc(star_buf_len_ps)
if star_buf_ps is None: return None
star_buf_ps[0] = '\0'
star_k_ps: sizet = 0
while star_k_ps < star_cnt_ps:
string.strcat(star_buf_ps, "rawptr[")
star_k_ps += 1
star_off_ps: sizet = string.strlen(star_buf_ps)
string.strncpy(star_buf_ps + star_off_ps, seg, star_base_end_ps)
star_buf_ps[star_off_ps + star_base_end_ps] = '\0'
for star_k_ps in range(int(star_cnt_ps)):
string.strcat(star_buf_ps, "]")
star_ty_ps: mlirlite.LLVMType* = _resolve_single_vpi_type_str(pool, star_buf_ps)
stdlib.free(star_buf_ps)
return star_ty_ps
i8_ty: mlirlite.LLVMType* = mlirlite.Int8(pool)
i8_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ty)
# built-in aliases
if string.strcmp(seg, "str") == 0 or string.strcmp(seg, "bytes") == 0: return i8_ptr_ty
if string.strcmp(seg, "int") == 0: return mlirlite.Int32(pool)
if string.strcmp(seg, "bool") == 0: return mlirlite.Int1(pool)
# LLVM type strings: vpi stores "i32" for unannotated methods
# (see _cache_vpi_return_annots_text); parse it so generic template calls do not fail fast.
if string.strcmp(seg, "i32") == 0: return mlirlite.Int32(pool)
if string.strcmp(seg, "i64") == 0: return mlirlite.Int64(pool)
if string.strcmp(seg, "i16") == 0: return mlirlite.Int16(pool)
if string.strcmp(seg, "i8") == 0: return i8_ty
if string.strcmp(seg, "i1") == 0: return mlirlite.Int1(pool)
if string.strcmp(seg, "f32") == 0: return mlirlite.Float(pool)
if string.strcmp(seg, "f64") == 0: return mlirlite.Double(pool)
if string.strcmp(seg, "void") == 0: return mlirlite.Void(pool)
# vpi caches `-> None` verbatim as "None"; map None to Void so cross-module
# `-> None` methods do not fail fast with an unresolvable return type.
if string.strcmp(seg, "None") == 0: return mlirlite.Void(pool)
# rawptr[T] built-in raw pointer (equivalent to t.CPtr[T] -> Ptr(T)); vpi keeps the
# Subscript form verbatim. Same as TypeGenResolve's rawptr branch: Ptr(resolve(T)).
if string.strncmp(seg, "rawptr[", 7) == 0:
seg_len_rp: sizet = string.strlen(seg)
if seg_len_rp > 8 and seg[seg_len_rp - 1] == ']':
inner_len_rp: sizet = seg_len_rp - 8
inner_buf_rp: bytes = stdlib.malloc(inner_len_rp + 1)
if inner_buf_rp is not None:
string.strncpy(inner_buf_rp, seg + 7, inner_len_rp)
inner_buf_rp[inner_len_rp] = '\0'
inner_ty_rp: mlirlite.LLVMType* = _resolve_single_vpi_type_str(pool, inner_buf_rp)
stdlib.free(inner_buf_rp)
if inner_ty_rp is not None: return mlirlite.Ptr(pool, inner_ty_rp)
return None
# t.CPtr[T] / c.CPtr[T] subscript pointers (nesting allowed, e.g. t.CPtr[t.CPtr]
# -> Ptr(Ptr(i8))); same as TypeGenResolve's Subscript(CPtr) branch: Ptr(resolve(T)).
cptr_sub_pfx: sizet = 0
if string.strncmp(seg, "t.CPtr[", 7) == 0 or string.strncmp(seg, "c.CPtr[", 7) == 0: cptr_sub_pfx = 7
if cptr_sub_pfx > 0:
seg_len_cp: sizet = string.strlen(seg)
if seg_len_cp > cptr_sub_pfx + 1 and seg[seg_len_cp - 1] == ']':
inner_len_cp: sizet = seg_len_cp - cptr_sub_pfx - 1
inner_buf_cp: bytes = stdlib.malloc(inner_len_cp + 1)
if inner_buf_cp is not None:
string.strncpy(inner_buf_cp, seg + cptr_sub_pfx, inner_len_cp)
inner_buf_cp[inner_len_cp] = '\0'
inner_ty_cp: mlirlite.LLVMType* = _resolve_single_vpi_type_str(pool, inner_buf_cp)
stdlib.free(inner_buf_cp)
if inner_ty_cp is not None: return mlirlite.Ptr(pool, inner_ty_cp)
return None
# t.XXX / c.XXX built-in type mapping
if string.strncmp(seg, "t.", 2) == 0 or string.strncmp(seg, "c.", 2) == 0:
attr_nm: str = seg + 2
# t.CExport is an export marker, not a real type; FuncGenDef maps it to Void,
# so normalize it here too or cross-module return types fail fast.
if string.strcmp(attr_nm, "CExport") == 0: return mlirlite.Void(pool)
# [stage 2] unified: CTypedef dynamic table first (library typedefs analyzed normally),
# on miss fall back to map_t_type (t/c library internal special-casing).
mapped_ty: mlirlite.LLVMType* = GenApi.resolve_typedef_or_map(pool, attr_nm, None, None)
if mapped_ty is not None: return mapped_ty
return None
# module-qualified class name "memhub.MemBuddy" -> use last segment to look up struct
last_dot: str = string.strrchr(seg, '.')
if last_dot is not None:
cls_nm2: str = last_dot + 1
mod_struct_ty: mlirlite.LLVMType* = GenApi.get_struct_type(cls_nm2)
if mod_struct_ty is not None: return mod_struct_ty
# Enum fallback: t.CEnum registers in EnumTable, not StructTable, so get_struct_type
# misses; fall back to the enum base type (usually i32) instead of returning None.
mod_enum_ty: mlirlite.LLVMType* = GenApi.get_enum_base_type(cls_nm2)
if mod_enum_ty is not None: return mod_enum_ty
return None
# bare class name -> global struct registry
bare_struct_ty: mlirlite.LLVMType* = GenApi.get_struct_type(seg)
if bare_struct_ty is not None: return bare_struct_ty
# bare enum name -> EnumTable fallback (as above: enums are not in StructTable)
bare_enum_ty: mlirlite.LLVMType* = GenApi.get_enum_base_type(seg)
if bare_enum_ty is not None: return bare_enum_ty
# bare typedef name (e.g. VOIDPTR): CTypedef dynamic table first, on miss fall back to map_t_type
mapped_bare: mlirlite.LLVMType* = GenApi.resolve_typedef_or_map(pool, seg, None, None)
if mapped_bare is not None: return mapped_bare
return None
# _resolve_vpi_ret_annot_str - resolve a vpi return-annotation string to LLVMType.
# Pure string parsing (no AST nodes) to avoid corrupted kind() from cross-module AST reuse.
def _resolve_vpi_ret_annot_str(pool: memhub.MemBuddy*, ret_annot: str, i32_ty: mlirlite.LLVMType*, i8_ptr_ty: mlirlite.LLVMType*) -> mlirlite.LLVMType*:
"""Resolve a vpi return-annotation string directly to LLVMType without building AST nodes."""
if ret_annot is None or pool is None: return None
# tuple[T1, T2, ...] -> anonymous struct {T1, T2, ...} (multi-return packing),
# matching TypeGenMixin._resolve_generic_subscript_ty's tuple branch.
if string.strncmp(ret_annot, "tuple[", 6) == 0:
inner_len_t: sizet = 0
while ret_annot[6 + inner_len_t] != ']' and ret_annot[6 + inner_len_t] != '\0': inner_len_t += 1
if ret_annot[6 + inner_len_t] != ']' or inner_len_t == 0: return None
# The tuple return-type field list is mlirlite.ParamNode (new_param_node /
# paramnode_set_next / Struct all take rawptr[ParamNode]), not Function*.
tup_head_t: mlirlite.ParamNode* = None
tup_tail_t: mlirlite.ParamNode* = None
tup_count_t: int = 0
p_t: sizet = 0
while p_t < inner_len_t:
# skip leading spaces/commas
while p_t < inner_len_t and (ret_annot[6 + p_t] == ' ' or ret_annot[6 + p_t] == ','): p_t += 1
seg_s_t: sizet = p_t
while p_t < inner_len_t and ret_annot[6 + p_t] != ',': p_t += 1
seg_e_t: sizet = p_t
while seg_e_t > seg_s_t and ret_annot[6 + seg_e_t - 1] == ' ': seg_e_t -= 1
if seg_e_t > seg_s_t:
seg_len_t: sizet = seg_e_t - seg_s_t
seg_buf_t: bytes = stdlib.malloc(seg_len_t + 1)
if seg_buf_t is None: return None
string.strncpy(seg_buf_t, ret_annot + 6 + seg_s_t, seg_len_t)
seg_buf_t[seg_len_t] = '\0'
seg_ty_t: mlirlite.LLVMType* = _resolve_single_vpi_type_str(pool, seg_buf_t)
stdlib.free(seg_buf_t)
if seg_ty_t is None: return None
tup_node_t: mlirlite.ParamNode* = mlirlite.new_param_node(pool, seg_ty_t)
if tup_node_t is None: return None
if tup_head_t is None: tup_head_t = tup_node_t
else: mlirlite.paramnode_set_next(tup_tail_t, tup_node_t)
tup_tail_t = tup_node_t
tup_count_t += 1
if tup_head_t is None: return None
return mlirlite.Struct(pool, tup_head_t, tup_count_t, None)
# Pure marker annotations (t.State / t.CExtern / t.CExport alone, optional t./c. prefix)
# -> void: decorator markers, not real return types, matching _inject_module_declare.
marker_len: sizet = string.strlen(ret_annot)
mk_s: sizet = 0
while mk_s < marker_len and ret_annot[mk_s] == ' ': mk_s += 1
mk_e: sizet = marker_len
while mk_e > mk_s and ret_annot[mk_e - 1] == ' ': mk_e -= 1
mk_off_m: sizet = 0
if mk_e - mk_s >= 2 and (ret_annot[mk_s] == 't' or ret_annot[mk_s] == 'c') and ret_annot[mk_s + 1] == '.': mk_off_m = 2
mk_nlen: sizet = mk_e - mk_s - mk_off_m
if (mk_nlen == 5 and string.strncmp(ret_annot + mk_s + mk_off_m, "State", 5) == 0) or (mk_nlen == 7 and string.strncmp(ret_annot + mk_s + mk_off_m, "CExtern", 7) == 0) or (mk_nlen == 7 and string.strncmp(ret_annot + mk_s + mk_off_m, "CExport", 7) == 0):
return mlirlite.Void(pool)
has_cptr: int = 0
has_cref: int = 0
has_cvoid: int = 0
first_ty: mlirlite.LLVMType* = None
# Attributes of the segment that produced first_ty (for the has_cptr branch, N12):
# first_qual=qualified name; first_wl=whitelisted bare scalar; first_td=resolved bare typedef.
first_qual: int = 0
first_wl: int = 0
first_td: int = 0
# Union-merge params for CPtr subscript segments (t.CPtr[T]/c.CPtr[T], nesting allowed):
# cptr_sub_lvl = outer Ptr level N, cptr_sub_ty = the segment's own type. -> Ptr^N(base).
cptr_sub_lvl: int = 0
cptr_sub_ty: mlirlite.LLVMType* = None
buf_len: sizet = string.strlen(ret_annot)
pos: sizet = 0
while pos <= buf_len:
# skip leading spaces
while pos < buf_len and ret_annot[pos] == ' ': pos += 1
seg_start: sizet = pos
# Split `|` only at the top level: `|` inside `[...]` belongs to a generic arg,
# so naive splitting would truncate `list[ast.AST*]`; depth-count on `[`/`]`.
depth_ra: int = 0
while pos < buf_len:
ch_ra: char = ret_annot[pos]
if ch_ra == '[': depth_ra += 1
elif ch_ra == ']':
if depth_ra > 0: depth_ra -= 1
elif ch_ra == '|':
if depth_ra == 0: break
pos += 1
seg_end: sizet = pos
# trim trailing spaces
while seg_end > seg_start and ret_annot[seg_end - 1] == ' ': seg_end -= 1
if seg_end > seg_start:
seg_len2: sizet = seg_end - seg_start
seg_buf: bytes = stdlib.malloc(seg_len2 + 1)
if seg_buf is None: break
string.strncpy(seg_buf, ret_annot + seg_start, seg_len2)
seg_buf[seg_len2] = '\0'
# CPtr subscript segments must be recognized first: the definition side treats
# `X*[T]` as "CPtr side contributes N pointer levels, the other side the base".
if string.strncmp(seg_buf, "t.CPtr[", 7) == 0 or string.strncmp(seg_buf, "c.CPtr[", 7) == 0:
seg_ty_cs: mlirlite.LLVMType* = _resolve_single_vpi_type_str(pool, seg_buf)
if seg_ty_cs is not None:
# Count outer Ptr nesting (same as the definition side). Do not update the
# loop variable inside a case body: match sinks into a subject snapshot.
lvl_cs: int = 0
cur_cs: mlirlite.LLVMType* = seg_ty_cs
while cur_cs is not None:
nxt_cs: mlirlite.LLVMType* = None
match cur_cs:
case mlirlite.LLVMType.Ptr(pointee_cs):
lvl_cs += 1
nxt_cs = pointee_cs
case _:
break
cur_cs = nxt_cs
# take the max level across subscript segments (equivalent to level accumulation)
if lvl_cs > cptr_sub_lvl:
cptr_sub_lvl = lvl_cs
cptr_sub_ty = seg_ty_cs
elif string.strcmp(seg_buf, "t.CPtr") == 0 or string.strcmp(seg_buf, "CPtr") == 0: has_cptr = 1
elif string.strcmp(seg_buf, "t.CRef") == 0 or string.strcmp(seg_buf, "CRef") == 0: has_cref = 1
elif string.strcmp(seg_buf, "t.CTempRef") == 0 or string.strcmp(seg_buf, "CTempRef") == 0: has_cref = 1
elif string.strcmp(seg_buf, "void") == 0 or string.strcmp(seg_buf, "CVoid") == 0 or string.strcmp(seg_buf, "void") == 0:
# Bare void is an alias of t.CVoid and must be handled the same, else the
# else branch sets first_td and produces the illegal Ptr(Void) (void* = i8*).
has_cvoid = 1
else:
seg_ty: mlirlite.LLVMType* = _resolve_single_vpi_type_str(pool, seg_buf)
if seg_ty is not None and first_ty is None:
first_ty = seg_ty
# Record the segment attribute (N12): the definition side keeps a bare pointee
# only for registered structs, generic params or whitelisted scalars; else i8*.
if string.strchr(seg_buf, 46) is not None: first_qual = 1
if string.strcmp(seg_buf, "int") == 0 or string.strcmp(seg_buf, "char") == 0 or string.strcmp(seg_buf, "str") == 0 or string.strcmp(seg_buf, "bytes") == 0 or string.strcmp(seg_buf, "bool") == 0 or string.strcmp(seg_buf, "float") == 0 or string.strcmp(seg_buf, "double") == 0:
first_wl = 1
# Preserve bare typedef aliases (N13): use GenApi.resolve_typedef_or_map (same API
# as the definition side); set only for non-pointer results (pointer aliases -> i8*).
if first_qual == 0 and first_wl == 0:
if first_ty.kind() != mlirlite.LLVMType.Ptr:
if GenApi.resolve_typedef_or_map(pool, seg_buf, None, None) is not None: first_td = 1
stdlib.free(seg_buf)
pos += 1 # skip '|'
if has_cptr != 0 or has_cref != 0:
if has_cvoid != 0: return i8_ptr_ty
if first_ty is not None:
# The result itself is void (e.g. an opaque typedef resolving to void): CPtr merge
# gives void* = i8*, matching has_cvoid (otherwise it would build illegal Ptr(void)).
match first_ty:
case mlirlite.LLVMType.Void():
return i8_ptr_ty
case _:
pass
# Mirror the definition-side rules (N12/N13): qualified name / whitelisted scalar /
# registered struct / resolvable non-pointer typedef keeps pointee; else collapse to i8*.
if first_qual != 0 or first_wl != 0 or first_td != 0: return mlirlite.Ptr(pool, first_ty)
if first_ty.kind() == mlirlite.LLVMType.Struct: return mlirlite.Ptr(pool, first_ty)
if first_ty.kind() == mlirlite.LLVMType.Ptr:
# `| t.CPtr` always appends exactly one pointer level (same as the definition
# side's BinOp branch); no struct-absorption, so `T*` stays `T*` (never AST*).
return mlirlite.Ptr(pool, first_ty)
return i8_ptr_ty
return i8_ptr_ty
# CPtr subscript union merge: with another side's base type first_ty -> Ptr^N(first_ty);
# with only the subscript segment -> the segment's own type. No struct absorption here.
if cptr_sub_lvl > 0:
if first_ty is None: return cptr_sub_ty
res_ty_cs: mlirlite.LLVMType* = first_ty
for wrap_i_cs in range(int(cptr_sub_lvl)):
res_ty_cs = mlirlite.Ptr(pool, res_ty_cs)
return res_ty_cs
if has_cvoid != 0:
# t.CVoid | t.State: State is a decorator marker (ignored), t.CVoid -> void.
# Without this branch first_ty is None and cross-module return types fail fast.
return mlirlite.Void(pool)
if first_ty is not None: return first_ty
return None
# VpiReader - .vpi AST parser. Reads function signatures and CTypedef definitions from
# .vpi stub files and injects declares, filling in missing cross-module declarations.
# .vpi read buffer size (256KB, matching PYI_BUF_SIZE)
VPI_READ_BUF_SIZE: sizet = 262144
# SHA1 hex char count (16, filename hex only, no trailing NUL). Named uniquely vs Phase1's
# SHA1_LEN (17 = 16 chars + NUL) to avoid cross-module CDefine shadowing.
SHA1_HEX_LEN: sizet = 16
# _extract_sha1_from_path - extract SHA1 from a .vpi path. Takes the first 16 chars of the
# filename after the last separator; returns a malloc'd string (caller frees) or None.
def _extract_sha1_from_path(vpi_path: str) -> str:
"""Extract the SHA1 from a .vpi path (first 16 chars of the filename, malloc'd)."""
if vpi_path is None: return None
# find the last path separator / or \
total_len: sizet = string.strlen(vpi_path)
if total_len < SHA1_HEX_LEN + 4: # need at least "xxxx.vpi" length
return None
# scan backwards for the last / or \
sep_pos: sizet = total_len
while sep_pos > 0:
sep_pos -= 1
ch: char = vpi_path[sep_pos]
if ch == '/' or ch == '\\':
sep_pos += 1
break
if sep_pos + SHA1_HEX_LEN > total_len: return None
# allocate buffer and copy SHA1 (16 chars + null terminator)
sha1_buf: str = stdlib.malloc(SHA1_HEX_LEN + 1)
if sha1_buf is None: return None
string.strncpy(sha1_buf, vpi_path + sep_pos, SHA1_HEX_LEN)
sha1_buf[SHA1_HEX_LEN] = '\0'
return sha1_buf
# Stub import-context bookkeeping goes through StubMergerStore (single source of truth).
# Unified to AppendImportedModule / AppendFromImport / NotePlainImport; pure bookkeeping.
# Recursive typedef pre-registration over the import closure. CTypedef tables are per-file,
# so unimported typedefs would degrade param types; this registers them (cycle/depth guarded).
define _TYPEDEF_VISIT_MAX = 1024
_g_vpi_typedef_visited: bytes = None
_g_vpi_typedef_visited_count: int = 0
# Current visit-table capacity (elements). Must grow when full rather than silently dropping
# marks: dropping breaks cycle detection and re-parses the closure, risking pool OOM.
_g_vpi_typedef_visited_cap: int = 0
def ResetVpiTypedefVisited() -> None:
"""Clear the recursive-typedef cycle-detection marks before each module's stub injection."""
global _g_vpi_typedef_visited, _g_vpi_typedef_visited_count, _g_vpi_typedef_visited_cap
if _g_vpi_typedef_visited is not None:
stdlib.free(_g_vpi_typedef_visited)
_g_vpi_typedef_visited = None
_g_vpi_typedef_visited_count = 0
_g_vpi_typedef_visited_cap = 0
def _vpi_typedef_visited_check(sha1: str) -> int:
"""Check whether sha1 was already recursively registered; 1=visited / 0=not visited (query only)."""
global _g_vpi_typedef_visited, _g_vpi_typedef_visited_count
if sha1 is None: return 1
if _g_vpi_typedef_visited is None: return 0
vi: int
for vi in range(int(_g_vpi_typedef_visited_count)):
if string.strcmp(_g_vpi_typedef_visited + vi * 17, sha1) == 0: return 1
return 0
def _vpi_typedef_visited_mark(sha1: str) -> None:
"""Mark sha1 as recursively registered (called after success, for cycle detection)."""
global _g_vpi_typedef_visited, _g_vpi_typedef_visited_count, _g_vpi_typedef_visited_cap
if sha1 is None: return
if _g_vpi_typedef_visited is None:
_g_vpi_typedef_visited = stdlib.malloc(_TYPEDEF_VISIT_MAX * 17)
if _g_vpi_typedef_visited is None:
# fail-fast: without the cycle table the closure re-parses and exhausts the pool, so do not continue
stdio.printf("[VPI] cycle-proof visit table allocation failed (without the table the closure reparse explodes; aborting)\n")
stdio.fflush(None)
stdlib.exit(1)
string.memset(_g_vpi_typedef_visited, 0, _TYPEDEF_VISIT_MAX * 17)
_g_vpi_typedef_visited_count = 0
_g_vpi_typedef_visited_cap = _TYPEDEF_VISIT_MAX
if _g_vpi_typedef_visited_count >= _g_vpi_typedef_visited_cap:
# Table full: grow (x2); never silently drop marks, which would break cycle detection
# and re-parse the same .vpi batch, allocating from the pool until it OOMs.
old_cap: int = _g_vpi_typedef_visited_cap
new_cap: int = old_cap * 2
nb: bytes = stdlib.realloc(_g_vpi_typedef_visited, new_cap * 17)
if nb is None:
stdio.printf("[VPI] cycle-proof visit table growth failed cap=%d (lost marks cause a reparse explosion; aborting)\n", old_cap)
stdio.fflush(None)
stdlib.exit(1)
_g_vpi_typedef_visited = nb
string.memset(nb + old_cap * 17, 0, old_cap * 17)
_g_vpi_typedef_visited_cap = new_cap
string.strcpy(_g_vpi_typedef_visited + _g_vpi_typedef_visited_count * 17, sha1)
_g_vpi_typedef_visited_count += 1
def _vpi_register_typedef_closure(pool: memhub.MemBuddy*, import_mods: str, temp_dir: str, depth: int) -> int:
"""Recursively register CTypedef/CDefine from import-closure modules; always returns 0 (side-effect only)."""
if pool is None or import_mods is None or temp_dir is None: return 0
if depth > 8: return 0
td_len: sizet = string.strlen(temp_dir)
total_len: sizet = string.strlen(import_mods)
pos: sizet = 0
while pos < total_len:
while pos < total_len and import_mods[pos] == ' ': pos += 1
if pos >= total_len: break
m_start: sizet = pos
while pos < total_len and import_mods[pos] != ' ': pos += 1
m_len: sizet = pos - m_start
if m_len == 0: continue
# t/c built-in libs are not compiled, so they have no vpi stub
if m_len == 1 and (import_mods[m_start] == 't' or import_mods[m_start] == 'c'): continue
mod_buf: bytes = stdlib.malloc(m_len + 1)
if mod_buf is None: continue
string.strncpy(mod_buf, import_mods + m_start, m_len)
mod_buf[m_len] = '\0'
sha1_r: str = StubMergerStore.LookupSha1ByModName(mod_buf)
stdlib.free(mod_buf)
if sha1_r is None: continue
if _vpi_typedef_visited_check(sha1_r) != 0: continue
# build {temp_dir}/{sha1[0:2]}/{sha1}.vpi by hand (same as _ensure_vpi_ret_annot_loaded,
# avoiding a StubMergerCollect cyclic dependency)
path_len_r: sizet = td_len + 16 + 12
vpi_path_r: bytes = stdlib.malloc(path_len_r)
if vpi_path_r is None: continue
string.strcpy(vpi_path_r, temp_dir)
vpi_path_r[td_len] = '/'
vpi_path_r[td_len + 1] = sha1_r[0]
vpi_path_r[td_len + 2] = sha1_r[1]
vpi_path_r[td_len + 3] = '/'
string.strcpy(vpi_path_r + td_len + 4, sha1_r)
string.strcpy(vpi_path_r + td_len + 4 + 16, ".vpi")
f_r: fileio.File* = fileio.File(vpi_path_r, fileio.MODE.R)
if f_r.closed:
stdlib.free(vpi_path_r)
continue
vpi_buf_r: bytes = stdlib.malloc(VPI_READ_BUF_SIZE)
if vpi_buf_r is None:
f_r.close()
stdlib.free(vpi_path_r)
continue
bytes_read_r: LONG = f_r.read_all(vpi_buf_r, VPI_READ_BUF_SIZE)
f_r.close()
if bytes_read_r > 0:
# mark as processed only after successfully reading the vpi (missing/read failure
# is not marked, so a later pass's stub injection can retry)
_vpi_typedef_visited_mark(sha1_r)
if bytes_read_r < VPI_READ_BUF_SIZE: vpi_buf_r[bytes_read_r] = 0
lx_r: ast.Lexer* = ast.new_lexer(pool)
if lx_r is not None:
ast._lexer_init(lx_r, vpi_buf_r, pool)
tokens_r: ast.Token* = ast.tokenize(lx_r)
tree_r: ast.AST* = ast.parse_tokens(pool, tokens_r)
if tree_r is not None and tree_r.children is not None:
ch_r: list[ast.AST*]* = tree_r.children
cn_r: sizet = len(ch_r)
# Collect child imports (recursive closure) in two passes: first sum the required
# length, then allocate exactly and fill (no fixed cap, so nothing is dropped).
sub_need: sizet = 1 # trailing NUL
ci_c: sizet
for ci_c in range(int(cn_r)):
child_c: ast.AST* = ch_r.get(ci_c)
if child_c is None: continue
kk_c: int = child_c.kind()
if kk_c == ast.ASTKind.Import:
imp_c: ast.Import* = ast.Import*(child_c)
if imp_c.names is not None:
in_nc: sizet = len(imp_c.names)
for ii_c in range(int(in_nc)):
al_c: ast.Alias* = ast.Alias*(imp_c.names.get(ii_c))
if al_c is not None and al_c.name is not None: sub_need += string.strlen(al_c.name) + 1
elif kk_c == ast.ASTKind.ImportFrom:
ifr_c: ast.ImportFrom* = ast.ImportFrom*(child_c)
if ifr_c.module is not None: sub_need += string.strlen(ifr_c.module) + 1
sub_imports: bytes = stdlib.malloc(sub_need)
if sub_imports is not None:
sub_imports[0] = '\0'
sub_len: sizet = 0
ci_r: sizet
for ci_r in range(int(cn_r)):
child_r: ast.AST* = ch_r.get(ci_r)
if child_r is None: continue
kk_r: int = child_r.kind()
if kk_r == ast.ASTKind.Import:
imp_r: ast.Import* = ast.Import*(child_r)
if imp_r.names is not None:
in_n: sizet = len(imp_r.names)
ii_r: sizet
for ii_r in range(int(in_n)):
al_r: ast.Alias* = ast.Alias*(imp_r.names.get(ii_r))
if al_r is not None and al_r.name is not None:
il_r: sizet = string.strlen(al_r.name)
if sub_len > 0:
sub_imports[sub_len] = ' '
sub_len += 1
string.strcpy(sub_imports + sub_len, al_r.name)
sub_len += il_r
sub_imports[sub_len] = '\0'
elif kk_r == ast.ASTKind.ImportFrom:
ifr_r: ast.ImportFrom* = ast.ImportFrom*(child_r)
if ifr_r.module is not None:
ml_r: sizet = string.strlen(ifr_r.module)
if sub_len > 0:
sub_imports[sub_len] = ' '
sub_len += 1
string.strcpy(sub_imports + sub_len, ifr_r.module)
sub_len += ml_r
sub_imports[sub_len] = '\0'
# second pass: register this module's AnnAssign CTypedef/CDefine
ci2_r: sizet
for ci2_r in range(int(cn_r)):
child2_r: ast.AST* = ch_r.get(ci2_r)
if child2_r is None: continue
if child2_r.kind() != ast.ASTKind.AnnAssign: continue
aa_r: ast.AnnAssign* = ast.AnnAssign*(child2_r)
if aa_r.annotation is None or aa_r.target is None or aa_r.value is None: continue
if GenApi._is_ctypedef_annotation(aa_r.annotation) != 0: _register_ctypedef_from_annassign(pool, aa_r)
elif GenApi.is_cdefine_annotation(aa_r.annotation) != 0:
if aa_r.target.kind() != ast.ASTKind.Name: continue
cdef_t: ast.Name* = ast.Name*(aa_r.target)
if cdef_t.id is None: continue
if aa_r.value.kind() == ast.ASTKind.Constant:
cn_v: ast.Constant* = ast.Constant*(aa_r.value)
if cn_v.const_kind == ast.CONST_FLOAT: GenApi.register_cdefine_double(pool, cdef_t.id, cn_v.float_val)
elif cn_v.const_kind == ast.CONST_INT:
cdef_val: int64 = GenApi.extract_cdefine_int_value(aa_r.value)
# 位宽与定义方同一套裁决(注解 → 右侧强转 → 值的最小可存宽度),
# 否则使用方拿 bits=0 只能兜底,跨模块位宽会与定义方不一致
GenApi.register_cdefine_constant(pool, cdef_t.id, cdef_val, GenApi.cdefine_decl_bits(pool, aa_r.annotation, aa_r.value))
# recurse into child imports (cycle guarded by the visited marks)
if sub_imports[0] != '\0': _vpi_register_typedef_closure(pool, sub_imports, temp_dir, depth + 1)
stdlib.free(sub_imports)
stdlib.free(vpi_buf_r)
stdlib.free(vpi_path_r)
return 0
# ParseVpiIntoModule - parse a .vpi file and inject function declares into the current module.
# Walks top-level nodes, creates declares for FunctionDefs (deduped) and registers CTypedefs.
def ParseVpiIntoModule(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, vpi_path: str, trans: HT.Translator*) -> int:
"""Parse a .vpi file and inject function declares into the current module."""
if pool is None or mod is None or vpi_path is None or trans is None: return 0
saved_cur: str = GenApi.get_current_file()
GenApi.set_current_file_type(vpi_path)
ret: int = _ParseVpiIntoModuleImpl(pool, mod, vpi_path, trans)
GenApi.set_current_file_type(saved_cur)
return ret
def _ParseVpiIntoModuleImpl(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, vpi_path: str, trans: HT.Translator*) -> int:
"""Actual implementation of ParseVpiIntoModule: parse a .vpi file and inject function declares."""
if pool is None or mod is None or vpi_path is None or trans is None: return 0
# 1. read the .vpi file
f: fileio.File* = fileio.File(vpi_path, fileio.MODE.R)
if f.closed: return 0
vpi_buf: bytes = stdlib.malloc(VPI_READ_BUF_SIZE)
if vpi_buf is None:
f.close()
return 0
bytes_read: LONG = f.read_all(vpi_buf, VPI_READ_BUF_SIZE)
f.close()
if bytes_read <= 0:
stdlib.free(vpi_buf)
return 0
if bytes_read < VPI_READ_BUF_SIZE: vpi_buf[bytes_read] = 0
# 2. parse into AST
lx: ast.Lexer* = ast.new_lexer(pool)
if lx is None:
stdlib.free(vpi_buf)
return 0
ast._lexer_init(lx, vpi_buf, pool)
tokens: ast.Token* = ast.tokenize(lx)
tree: ast.AST* = ast.parse_tokens(pool, tokens)
# Delay freeing vpi_buf: AST string fields may point into it (zero-copy tokens),
# so freeing early would dangle Name.id/Attribute.attr; freed at the end instead.
if tree is None:
stdlib.free(vpi_buf)
return 0
# extract the source module SHA1 for name mangling (after AST parse success to avoid leaks on early return)
src_sha1: str = _extract_sha1_from_path(vpi_path)
# Cache method return annotations from vpi_buf text into stdlib (no AST tree: deep nodes
# may be corrupted later, but the raw vpi_buf text stays valid until the function ends).
# 单一入口:ret / param / 逃逸摘要一次喂齐(见 _cache_vpi_annots_text 注释)。
# 本函数是 stub 注入路径的加载点,历史上这里漏喂逃逸摘要 → 跨模块漏检。
_cache_vpi_annots_text(vpi_buf, src_sha1)
# 3. walk top-level AST nodes
ch: list[ast.AST*]* = tree.children
if ch is None:
if src_sha1 is not None: stdlib.free(src_sha1)
return 1
n: sizet = len(ch)
# Collect the read module's own imports from the stub to build an isolated annotation
# context, replacing the current module's imports (e.g. t.CArray[str] needs 't' imported).
stub_imported_modules: str = None
stub_from_imports: str = None
# Bookkeeping goes through StubMergerStore (same as ImportsGenMixin.translate_import):
# `import X as Y -> from_imports "Y:X"`; `import a.b.c -> imported_modules "a"`.
stub_ctx: StubMergerStore.ImportCtx* = StubMergerStore.AllocImportCtx(pool, None, None)
for si in range(int(n)):
schild: ast.AST* = ch.get(si)
if schild is None: continue
sk: int = schild.kind()
if sk == ast.ASTKind.Import:
simp: ast.Import* = ast.Import*(schild)
if simp.names is not None:
sn_n: sizet = len(simp.names)
for sni in range(int(sn_n)):
sal: ast.Alias* = ast.Alias*(simp.names.get(sni))
if sal is not None and sal.name is not None: StubMergerStore.NotePlainImport(pool, stub_ctx, sal.name, sal.asname)
elif sk == ast.ASTKind.ImportFrom:
sifr: ast.ImportFrom* = ast.ImportFrom*(schild)
sifr_mod: str = sifr.module
# Relative imports: resolve the package prefix and __init__ detection from the module
# name table. Falls back to the old rel_path route when no module-name evidence exists.
pkg_prefix_rt: str = None
if sifr.level > 0 and sifr_mod is not None and sifr_mod[0] != '\0':
_sib_sha_rt: str = StubMergerStore.LookupSha1ByModNameSuffix(sifr_mod)
if _sib_sha_rt is not None:
_sub_mod_rt: str = StubMergerStore.LookupModNameBySha1(_sib_sha_rt)
if _sub_mod_rt is not None:
_dot_rt: str = string.strrchr(_sub_mod_rt, '.')
if _dot_rt is not None:
_pfx_len_rt: sizet = sizet(uint64(_dot_rt) - uint64(_sub_mod_rt))
if _pfx_len_rt > 0:
_my_mod_rt: str = StubMergerStore.LookupModNameBySha1(src_sha1)
# Confirm: this stub's module name must equal the prefix exactly
# (i.e. this stub is the package __init__); else defer to rel_path.
_conf_rt: int = 0
if _my_mod_rt is not None and string.strlen(_my_mod_rt) == _pfx_len_rt:
if string.strncmp(_my_mod_rt, _sub_mod_rt, _pfx_len_rt) == 0: _conf_rt = 1
if _conf_rt != 0:
pkg_prefix_rt = pool.alloc(_pfx_len_rt + 1)
if pkg_prefix_rt is not None:
string.strncpy(pkg_prefix_rt, _sub_mod_rt, _pfx_len_rt)
pkg_prefix_rt[_pfx_len_rt] = '\0'
sifr_mod_q: str = sifr_mod
if sifr_mod is not None and sifr_mod[0] != '\0' and sifr.level == 1:
if pkg_prefix_rt is not None:
qtotal_p: sizet = string.strlen(pkg_prefix_rt) + 1 + string.strlen(sifr_mod) + 1
qbuf_p: str = pool.alloc(qtotal_p)
if qbuf_p is not None:
qbuf_p[0] = '\0'
string.strcat(qbuf_p, pkg_prefix_rt)
string.strcat(qbuf_p, ".")
string.strcat(qbuf_p, sifr_mod)
sifr_mod_q = qbuf_p
else:
rel_p_r: str = StubMergerStore.LookupSha1RelPath(src_sha1)
if rel_p_r is not None:
slash_r: str = string.strrchr(rel_p_r, '/')
if slash_r is not None:
pkg_len_r: sizet = string.strlen(rel_p_r) - string.strlen(slash_r)
if pkg_len_r > 0:
qtotal_r: sizet = pkg_len_r + 1 + string.strlen(sifr_mod) + 1
qbuf_r: str = pool.alloc(qtotal_r)
if qbuf_r is not None:
string.strncpy(qbuf_r, rel_p_r, pkg_len_r)
qbuf_r[pkg_len_r] = '.'
string.strcpy(qbuf_r + pkg_len_r + 1, sifr_mod)
sifr_mod_q = qbuf_r
if sifr_mod is not None:
if stub_ctx is not None:
stub_ctx.ImportedModules = StubMergerStore.AppendImportedModule(pool, stub_ctx.ImportedModules, sifr_mod_q)
# Plan A: package __init__ re-exported classes are registered as alias entries under
# the package sha1 (only for __init__; uses pkg_prefix_rt, falling back to rel_path).
is_pkg_init_rt: int = 0
if pkg_prefix_rt is not None: is_pkg_init_rt = 1
if is_pkg_init_rt == 0:
rel_path_rt: str = StubMergerStore.LookupSha1RelPath(src_sha1)
if rel_path_rt is not None:
# [FIX] Use strstr(rel_path, "__init__.vp") instead of an offset-based strcmp,
# which was always false for real package paths and never triggered re-export.
if string.strstr(rel_path_rt, "__init__.vp") is not None: is_pkg_init_rt = 1
# Resolve the from-imported module sha1: relative imports put leading dots in `level`,
# not in `module`, so prefix-qualified name lookup must be used.
sub_sha1_rt: str = None
if sifr.level > 0:
if sifr_mod is not None and sifr_mod[0] != '\0':
# Prefer the prefix-qualified exact name (sifr_mod_q, e.g. ast.base); the
# suffix match is only a fallback since it can hit another same-suffix module.
sub_sha1_rt = StubMergerStore.LookupSha1ByModName(sifr_mod_q)
if sub_sha1_rt is None: sub_sha1_rt = StubMergerStore.LookupSha1ByModNameSuffix(sifr_mod)
else: sub_sha1_rt = StubMergerStore.LookupSha1ByModName(sifr_mod)
# [D26 fail-fast] Do not silently skip when a package __init__ from-import cannot
# resolve the submodule sha1: report package and submodule names for diagnosis.
if is_pkg_init_rt != 0 and sub_sha1_rt is None and sifr_mod is not None and sifr_mod[0] != '\0':
GenApi.fatal_error(sifr, f"Package stub from-import submodule sha1 lookup failed: " f"package={rel_path_rt if rel_path_rt is not None else '?'}, " f"submodule='{sifr_mod}' (after prefix completion='{sifr_mod_q}'); " f"classes of this submodule cannot be registered, and annotations referencing them will fail to resolve later", "NameError")
if sifr.names is not None:
sifr_nn: sizet = len(sifr.names)
sifri: sizet
for sifri in range(int(sifr_nn)):
sial: ast.Alias* = ast.Alias*(sifr.names.get(sifri))
if sial is not None and sial.name is not None:
slocal: str = sial.name
sorig: str = None
if sial.asname is not None:
slocal = sial.asname
sorig = sial.name
# `from X import *` also writes "*:mod" (same as
# ImportsGenMixin.translate_from_import_names); lookup_from_import relies on it.
if stub_ctx is not None:
stub_ctx.FromImports = StubMergerStore.AppendFromImport(pool, stub_ctx.FromImports, slocal, sifr_mod_q, sorig)
# Register re-exported classes under the package sha1 (by alias if asname);
# star has no concrete class name and is only recorded above.
if string.strcmp(sial.name, "*") != 0:
if is_pkg_init_rt != 0 and sub_sha1_rt is not None:
_inject_reexport_class_declare(pool, mod, sial.name, slocal, sub_sha1_rt, src_sha1, trans)
# replace the current module's imports with the stub's own (correct annotation context)
if stub_ctx is not None:
stub_imported_modules = stub_ctx.ImportedModules
stub_from_imports = stub_ctx.FromImports
imported_modules: str = trans._imported_modules
from_imports: str = trans._from_imports
if stub_imported_modules is not None: imported_modules = stub_imported_modules
if stub_from_imports is not None: from_imports = stub_from_imports
# Add the source module itself to the stub annotation context: stub annotations may
# reference their own CDefines, but stub imports only list imported modules.
if src_sha1 is not None:
stub_rel_src: str = StubMergerStore.LookupSha1RelPath(src_sha1)
if stub_rel_src is not None:
sr_len_src: sizet = string.strlen(stub_rel_src)
if sr_len_src > 3 and string.strncmp(stub_rel_src + sr_len_src - 3, ".vp", 3) == 0: sr_len_src -= 3
if sr_len_src > 0:
src_mod_buf: str = stdlib.malloc(sr_len_src + 1)
if src_mod_buf is not None:
sri_src: sizet
for sri_src in range(int(sr_len_src)):
ch_src: char = stub_rel_src[sri_src]
if ch_src == '/' or ch_src == '\\': src_mod_buf[sri_src] = '.'
else: src_mod_buf[sri_src] = ch_src
src_mod_buf[sr_len_src] = '\0'
imported_modules = StubMergerStore.AppendImportedModule(pool, imported_modules, src_mod_buf)
stdlib.free(src_mod_buf)
# Recursively pre-register the stub import closure's CTypedef/CDefine: CTypedef tables are
# per-file, so an unimported typedef would degrade param types to i8* and misreport CastError.
if imported_modules is not None: _vpi_register_typedef_closure(pool, imported_modules, GenApi.get_temp_dir(), 0)
i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool)
i8_ty: mlirlite.LLVMType* = mlirlite.Int8(pool)
i8_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ty)
# function table info
funcs_ptr: GenTypes.FuncEntry* = trans._funcs
max_funcs: int = 1024
# REPL (ReplStubLayoutMode): restore full class layouts and vtable method indices from
# {sha1}.meta first, then run the vpi injection below. Always 0 in batch mode.
if ReplStubLayoutMode != 0:
_repl_stub_collect_module(pool, mod, ch, n, trans, src_sha1, stub_imported_modules, stub_from_imports)
# Pass 1A: register ALL ClassDefs (fields + structs) first, without methods, so a class
# defined after a global's AnnAssign is still registered before that AnnAssign resolves.
i0: sizet
for i0 in range(int(n)):
child0: ast.AST* = ch.get(i0)
if child0 is None: continue
if child0.kind() != ast.ASTKind.ClassDef: continue
class_cd0: ast.ClassDef* = ast.ClassDef*(child0)
if class_cd0 is None or class_cd0.name is None: continue
cls_name_v0: str = class_cd0.name
# Skip generic classes (class X[T]): the stub template signature has a class-level type
# param T that cannot be resolved; specialized method defines are already emitted in Phase1.
tp_stub0: list[str]* = class_cd0.type_params
if tp_stub0 is not None and len(tp_stub0) > 0: continue
_inject_class_struct_declare(pool, mod, class_cd0, cls_name_v0, trans, src_sha1, imported_modules, from_imports)
# App pre-registration (Phase A-pre, _pre_allow_dep_inject) skips method/function declare
# injection: its only goal is structs/enums and package re-export aliases, not transient declares.
pre_skip_fn_declares: int = 0
if trans is not None and trans._pre_allow_dep_inject != 0: pre_skip_fn_declares = 1
# Pass 1B: process method declares only after all classes are registered, since method
# params/returns may reference classes defined later in the file (forward references).
for i1 in range(int(n)):
child1: ast.AST* = ch.get(i1)
if child1 is None: continue
if child1.kind() != ast.ASTKind.ClassDef: continue
class_cd1: ast.ClassDef* = ast.ClassDef*(child1)
if class_cd1 is None or class_cd1.name is None: continue
cls_name_v1: str = class_cd1.name
# skip generic classes (same criterion as pass 1A)
tp_stub1: list[str]* = class_cd1.type_params
if tp_stub1 is not None and len(tp_stub1) > 0: continue
cls_children1: list[ast.AST*]* = class_cd1.children
if cls_children1 is None: continue
cn21: sizet = len(cls_children1)
for ci21 in range(int(cn21)):
m_child1: ast.AST* = cls_children1.get(ci21)
if m_child1 is None: continue
if m_child1.kind() != ast.ASTKind.FunctionDef: continue
m_fd1: ast.FunctionDef* = ast.FunctionDef*(m_child1)
if m_fd1 is None or m_fd1.name is None: continue
# App pre-registration skips method declares (see pre_skip_fn_declares above).
if pre_skip_fn_declares == 0:
_inject_method_declare(pool, mod, m_fd1, cls_name_v1, trans, src_sha1, imported_modules, from_imports, i32_ty, i8_ptr_ty, funcs_ptr, max_funcs)
i: sizet
for i in range(int(n)):
child: ast.AST* = ch.get(i)
if child is None: continue
kd: int = child.kind()
# FunctionDef -> extract name/params/return type -> inject declare
if kd == ast.ASTKind.FunctionDef:
fd: ast.FunctionDef* = ast.FunctionDef*(child)
if fd is None or fd.name is None: continue
# App pre-registration skips function declares (see pre_skip_fn_declares):
# their annotations may reference classes not yet registered this pass.
if pre_skip_fn_declares != 0: continue
_inject_function_declare(pool, mod, fd, trans, src_sha1, imported_modules, from_imports, i32_ty, i8_ptr_ty, funcs_ptr, max_funcs)
continue
# AnnAssign with t.CTypedef -> register CTypedef;
# plain module-level globals (e.g. tokens._op_head) -> external global declare
if kd == ast.ASTKind.AnnAssign:
aa: ast.AnnAssign* = ast.AnnAssign*(child)
if aa is None: continue
if aa.target is not None and aa.target.kind() == ast.ASTKind.Name:
dbg_tn: ast.Name* = ast.Name*(aa.target)
if dbg_tn.id is not None and dbg_tn.id[0] != '\0': # questionable
if string.strcmp(dbg_tn.id, "irq_handler_t") == 0 or string.strcmp(dbg_tn.id, "gdt") == 0 or string.strcmp(dbg_tn.id, "KBD_BUF_SIZE") == 0:
dbg_ct: int = GenApi._is_ctypedef_annotation(aa.annotation)
dbg_cd: int = GenApi.is_cdefine_annotation(aa.annotation)
if GenApi._is_ctypedef_annotation(aa.annotation) != 0: _register_ctypedef_from_annassign(pool, aa)
elif GenApi.is_cdefine_annotation(aa.annotation) != 0:
# Register vpi CDefine definitions (e.g. keyboard.vp KBD_BUF_SIZE) into the current
# file's CDefine table so later t.CArray counts in the same vpi can resolve them.
if aa.target is not None and aa.target.kind() == ast.ASTKind.Name and aa.value is not None:
cdef_tgt: ast.Name* = ast.Name*(aa.target)
if cdef_tgt.id is not None:
# Skip string CDefines (version numbers etc.): extract_cdefine_int_value
# fails fast on string constants, and count refs only need integer CDefines.
cdef_is_str: int = 0
cdef_is_float: int = 0
cdef_float_val: double = 0.0
if aa.value.kind() == ast.ASTKind.Constant:
cn_v: ast.Constant* = ast.Constant*(aa.value)
if cn_v.const_kind == ast.CONST_STR: cdef_is_str = 1
elif cn_v.const_kind == ast.CONST_FLOAT:
cdef_is_float = 1
cdef_float_val = cn_v.float_val
if cdef_is_str == 0 and cdef_is_float != 0:
# Float CDefine (e.g. FRAGMENTATION_THRESHOLD: t.CDefine = 0.7): register via
# register_cdefine_double (integer extraction would fail fast).
GenApi.register_cdefine_double(pool, cdef_tgt.id, cdef_float_val)
elif cdef_is_str == 0:
cdef_val: int64 = GenApi.extract_cdefine_int_value(aa.value)
# 位宽与定义方同一套裁决(注解 → 右侧强转 → 值的最小可存宽度)
GenApi.register_cdefine_constant(pool, cdef_tgt.id, cdef_val, GenApi.cdefine_decl_bits(pool, aa.annotation, aa.value))
else:
_inject_global_variable_declare(pool, mod, aa, trans, stub_imported_modules, stub_from_imports, i32_ty, i8_ptr_ty, src_sha1)
continue
# ClassDef -> already registered in the first passes (fields + method declares), skip here.
# (A single pass would fail when classes are defined after globals; see pass 1A above.)
# Import/ImportFrom -> skip (nothing to process)
# 不要 free src_sha1:它会被后续阶段继续引用(模块/类型注册按 `<sha1>.Name` 命名,
# 之后 resolve_annotation_type 仍会对它 strcmp)——在此 free 会造成 heap-use-after-free
# (现场:alloc @ _extract_sha1_from_path → free @ 本函数 → 读 @ resolve_annotation_type:2065)。
# 跨阶段字符串按本仓惯例常驻;若将来确需回收,应改为在**存入注册表处**拷贝一份到 PermPool,
# 而不是在解析结束时释放。
# free vpi_buf only after all tree processing (see the parsing comment)
stdlib.free(vpi_buf)
return 1
# On-demand dependency vpi injection (resolves the mutual wait between package __init__
# re-exports and submodule stubs). Re-entrancy table dedups by sha1; failures stay non-fatal.
define VPI_INJ_MAX = 1024
_vpi_inj_sha1s: bytes = None
_vpi_inj_count: int = 0
_vpi_inj_depth: int = 0
def ResetVpiInjectVisited() -> None:
"""Clear the on-demand injection reentry table before each module's stub-injection session."""
global _vpi_inj_sha1s, _vpi_inj_count, _vpi_inj_depth
if _vpi_inj_sha1s is not None:
stdlib.free(_vpi_inj_sha1s)
_vpi_inj_sha1s = None
_vpi_inj_count = 0
_vpi_inj_depth = 0
def _ensure_dep_vpi_injected(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, dep_sha1: str, trans: HT.Translator*) -> int:
"""Inject dep_sha1's .vpi into the current module on demand (reentry-guarded); 1=ready / 0=failure."""
global _vpi_inj_sha1s, _vpi_inj_count, _vpi_inj_depth
if pool is None or mod is None or dep_sha1 is None or trans is None: return 0
if dep_sha1[0] == '\0': return 0
if _vpi_inj_sha1s is None:
_vpi_inj_sha1s = stdlib.malloc(VPI_INJ_MAX * 17)
if _vpi_inj_sha1s is None: return 0
string.memset(_vpi_inj_sha1s, 0, VPI_INJ_MAX * 17)
_vpi_inj_count = 0
# already injected / currently injecting (cycle) -> treat as ready and short-circuit
ii: int
for ii in range(int(_vpi_inj_count)):
if string.strcmp(_vpi_inj_sha1s + sizet(ii) * 17, dep_sha1) == 0: return 1
if _vpi_inj_count >= VPI_INJ_MAX or _vpi_inj_depth >= 32: return 0
temp_dir: str = GenApi.get_temp_dir()
if temp_dir is None: return 0
td_len_i: sizet = string.strlen(temp_dir)
sh_len_i: sizet = string.strlen(dep_sha1)
sub_i: str = Config.slice_subdir(dep_sha1, Config.Sha1SliceLevel)
sub_len_i: sizet = 0
if sub_i is not None: sub_len_i = string.strlen(sub_i)
path_len_i: sizet = td_len_i + sub_len_i + sh_len_i + 8
path_i: bytes = stdlib.malloc(path_len_i)
if path_i is None: return 0
if sub_len_i > 0: viperlib.snprintf(path_i, path_len_i, "%s/%s/%s.vpi", temp_dir, sub_i, dep_sha1)
else: viperlib.snprintf(path_i, path_len_i, "%s/%s.vpi", temp_dir, dep_sha1)
vf_i: fileio.File* = fileio.File(path_i, fileio.MODE.R)
if vf_i.closed:
stdlib.free(path_i)
return 0
vf_i.close()
# register before injecting: recursive injection of the same sha1 short-circuits (cycle guard)
my_idx_i: int = _vpi_inj_count
string.strcpy(_vpi_inj_sha1s + sizet(my_idx_i) * 17, dep_sha1)
_vpi_inj_count += 1
_vpi_inj_depth += 1
ret_i: int = ParseVpiIntoModule(pool, mod, path_i, trans)
_vpi_inj_depth -= 1
stdlib.free(path_i)
if ret_i != 1:
# Injection failed: unregister (move the last entry into this slot, keeping LIFO order),
# else later requests for the same sha1 stay short-circuited and the class never registers.
_vpi_inj_count -= 1
if my_idx_i != _vpi_inj_count:
string.strcpy(_vpi_inj_sha1s + sizet(my_idx_i) * 17, _vpi_inj_sha1s + sizet(_vpi_inj_count) * 17)
return 0
return 1
# _inject_reexport_class_declare - register a package re-exported class as an alias entry under the
# package sha1, reusing the submodule's registered entry so ast.Attribute resolves by package.
def _inject_reexport_class_declare(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, src_name: str, local_name: str, sub_sha1: str, pkg_sha1: str, trans: HT.Translator*) -> int:
"""Register a package re-exported class as an alias entry under the package sha1."""
if pool is None or mod is None or src_name is None or local_name is None: return 0
if sub_sha1 is None or sub_sha1[0] == '\0' or pkg_sha1 is None or pkg_sha1[0] == '\0': return 0
if src_name[0] == '\0' or local_name[0] == '\0': return 0
# idempotent: skip if local_name is already registered under the package sha1
if GenApi.find_struct_by_module(local_name, pkg_sha1) is not None: return 1
# find the submodule source entry (only classes have entries; functions/constants are skipped)
src_entry: GenTypes.StructEntry* = GenApi.find_struct_by_module(src_name, sub_sha1)
if src_entry is None or src_entry.Ty is None:
# Source stub not injected yet (package __init__ and submodule wait on each other; see
# _ensure_dep_vpi_injected): inject sub_sha1's vpi on demand and retry once; else skip.
if _ensure_dep_vpi_injected(pool, mod, sub_sha1, trans) != 0: src_entry = GenApi.find_struct_by_module(src_name, sub_sha1)
if src_entry is None or src_entry.Ty is None: return 0
# register the alias entry (bypassing register_struct's Ty dedup)
if GenApi.register_struct_alias(pool, local_name, pkg_sha1, src_entry) is None: return 0
GenApi.add_visible_struct(pool, local_name)
return 1
# _vpi_tattr_name / _vpi_class_has_packed - detect the vpi class packed decorator.
# The stub keeps @c.Attribute(t.attr.packed); struct injection must mirror the packed flag (B5).
def _vpi_tattr_name(node: ast.AST*, attr_name: str) -> int:
"""Extract the outermost attribute name from t.attr.<name> / t.attr.<name>(args) and compare."""
if node is None: return 0
if node.kind() == ast.ASTKind.Call:
cl2: ast.Call* = ast.Call*(node)
if cl2.func is None: return 0
return _vpi_tattr_name(cl2.func, attr_name)
if node.kind() == ast.ASTKind.Attribute:
at2: ast.Attribute* = ast.Attribute*(node)
if at2.attr is None: return 0
return 1 if string.strcmp(at2.attr, attr_name) == 0 else 0
return 0
def _vpi_class_has_packed(decorator_list: list[ast.AST*]*) -> int:
"""Check whether a vpi ClassDef decorator list contains t.attr.packed."""
if decorator_list is None: return 0
dn: sizet = len(decorator_list)
if dn == 0: return 0
for di in range(int(dn)):
deco: ast.AST* = decorator_list.get(di)
if deco is None: continue
if deco.kind() == ast.ASTKind.Call:
cl: ast.Call* = ast.Call*(deco)
if cl.func is None or cl.func.kind() != ast.ASTKind.Attribute: continue
at0: ast.Attribute* = ast.Attribute*(cl.func)
if at0.attr is None or string.strcmp(at0.attr, "Attribute") != 0: continue
if cl.args is None: continue
an: sizet = len(cl.args)
for ai in range(int(an)):
ag: ast.AST* = cl.args.get(ai)
if ag is not None and _vpi_tattr_name(ag, "packed") != 0: return 1
else:
if _vpi_tattr_name(deco, "packed") != 0: return 1
return 0
def _vpi_class_is_renum(class_cd: ast.ClassDef*) -> int:
"""Check whether a vpi ClassDef is a REnum (base class contains t.REnum); 1=yes / 0=no."""
if class_cd is None or class_cd.bases is None: return 0
bases: list[ast.AST*]* = class_cd.bases
bn: sizet = len(bases)
for bi in range(int(bn)):
base_node: ast.AST* = bases.get(bi)
if base_node is None: continue
bk: int = base_node.kind()
# Attribute(t.REnum) form
if bk == ast.ASTKind.Attribute:
at: ast.Attribute* = ast.Attribute*(base_node)
if at.attr is not None and string.strcmp(at.attr, "REnum") == 0: return 1
# Name(REnum) form (from t import REnum)
elif bk == ast.ASTKind.Name:
nm: ast.Name* = ast.Name*(base_node)
if nm.id is not None and string.strcmp(nm.id, "REnum") == 0: return 1
return 0
# _inject_class_struct_declare - register a vpi ClassDef as a struct type (with fields) so bare
# class-name annotations in the same vpi resolve. Mirrors ClassGenDef.translate_class_def.
def _inject_class_struct_declare(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, class_cd: ast.ClassDef*, cls_name: str, trans: HT.Translator*, src_sha1: str, imported_modules: str, from_imports: str, name_prefix: str = None) -> int:
"""Register a vpi ClassDef as a struct type (with fields) so bare class-name annotations resolve."""
if pool is None or mod is None or class_cd is None or cls_name is None: return 0
# Flattened name for nested classes (`{outer}_{inner}`); register even if the base class is not
# registered (the "no registration on field failure" rule lets multi-pass convergence handle it).
real_name: str = cls_name
if name_prefix is not None and name_prefix[0] != '\0':
p_len: sizet = string.strlen(name_prefix)
c_len: sizet = string.strlen(cls_name)
flat_buf_c: str = pool.alloc(p_len + 1 + c_len + 1)
if flat_buf_c is not None:
string.strcpy(flat_buf_c, name_prefix)
flat_buf_c[p_len] = '_'
string.strcpy(flat_buf_c + p_len + 1, cls_name)
real_name = flat_buf_c
# REPL (ReplStubLayoutMode): if this class is already driven by the {sha1}.meta path, yield to it:
# mark visible if registered, defer if the base is pending, skip if the layout is untrusted.
if ReplStubLayoutMode != 0:
se: ReplStubClass* = _repl_stub_find(_repl_stub_key(pool, src_sha1, real_name))
if se is not None and se.State != _REPL_SS_EMPTY:
if se.State == _REPL_SS_PENDING: _repl_stub_try(se, mod)
if se.State == _REPL_SS_OK: GenApi.add_visible_struct(pool, real_name)
return 1
# 1. Already-registered check: just mark visible, except forward-decl placeholders
# (IsForwardDecl != 0) which must go through the registration path to fill the same Ty.
existing_c: GenTypes.StructEntry* = None
fill_fwd_c: int = 0
if src_sha1 is not None and src_sha1[0] != '\0':
existing_c = GenApi.find_struct_by_module(real_name, src_sha1)
if existing_c is not None and existing_c.Ty is not None:
if existing_c.IsForwardDecl != 0: fill_fwd_c = 1
else:
# entry 已在全局 StructTable(可能由别的模块先注入/注册),但**本模块**的 NamedTypeHead
# 不保证有它的完整定义:只有被按值使用(值返回类型 / 值字段 / 数组元素)时打印才回查登记
# 表,而指针引用只会登记 opaque 前向声明 → 打印签名/类型时报 "still opaque"。故这里补齐
# 本模块的完整定义(幂等:add_named_type 同名去重,完整定义替换 opaque)。
mlirlite.module_add_named_type(mod, pool, existing_c.Ty)
if existing_c.IsPacked != 0: mlirlite.module_set_named_type_packed(mod, pool, existing_c.Ty, 1)
GenApi.add_visible_struct(pool, real_name)
return 1
# Skip REnum classes (class X(t.REnum)): their layout is computed from variants by
# ClassGenEnum._translate_renum_def; registering as a plain struct would be wrong.
if _vpi_class_is_renum(class_cd) != 0: return 0
# Rule 5.5 evidence must be set before field annotation resolution: mark_module_class
# records "{src_sha1}.{real_name}" and points the class-definition window at this class.
GenApi.mark_module_class(pool, src_sha1, real_name)
# 1.5 Nested classes/structs: vpi keeps them as indented ClassDefs; the definition side emits
# flat names (`{outer}_{inner}`), so recurse first with the same naming before field resolution.
_inject_nested_class_declares(pool, mod, class_cd, real_name, trans, src_sha1, imported_modules, from_imports)
# Restore the "defining class" window: the nested recursion's mark_module_class changes
# CurDefClassName to the nested name (A_B), which would corrupt rule 5.5 self-reference evidence.
GenApi.mark_module_class(pool, src_sha1, real_name)
# 2. collect field types (AnnAssign children, mirroring ClassGenDef.translate_class_def)
field_names_buf: sizet* = pool.alloc(8 * GenTypes.FIELD_MAX)
field_types_buf: sizet* = pool.alloc(8 * GenTypes.FIELD_MAX)
# Per-field marker buffers, index-aligned with field_names_buf / add_field order. The .vpi text
# keeps each field's raw annotation (Converter._PyiTypeStr renders BinOp/Attribute verbatim, so
# `X | t.BigEndian` / `t.CVolatile | int` survive), so the same markers ClassGenDef extracts on
# the definition side must be re-extracted here; otherwise a struct referenced across a module
# boundary loses its volatile/const/byte-order semantics and the same field compiles differently
# per module (the bug this fixes).
field_volatile_buf: sizet* = pool.alloc(8 * GenTypes.FIELD_MAX)
field_pointee_volatile_buf: sizet* = pool.alloc(8 * GenTypes.FIELD_MAX)
field_const_buf: sizet* = pool.alloc(8 * GenTypes.FIELD_MAX)
field_byteorder_buf: sizet* = pool.alloc(8 * GenTypes.FIELD_MAX)
if field_names_buf is None or field_types_buf is None: return 0
if field_volatile_buf is None or field_pointee_volatile_buf is None: return 0
if field_const_buf is None or field_byteorder_buf is None: return 0
string.memset(field_names_buf, 0, 8 * GenTypes.FIELD_MAX)
string.memset(field_types_buf, 0, 8 * GenTypes.FIELD_MAX)
string.memset(field_volatile_buf, 0, 8 * GenTypes.FIELD_MAX)
string.memset(field_pointee_volatile_buf, 0, 8 * GenTypes.FIELD_MAX)
string.memset(field_const_buf, 0, 8 * GenTypes.FIELD_MAX)
string.memset(field_byteorder_buf, 0, 8 * GenTypes.FIELD_MAX)
field_count: int = 0
cls_children_c: list[ast.AST*]* = class_cd.children
if cls_children_c is not None:
cn_c: sizet = len(cls_children_c)
for ci_c in range(int(cn_c)):
m_child_c: ast.AST* = cls_children_c.get(ci_c)
if m_child_c is None: continue
if m_child_c.kind() != ast.ASTKind.AnnAssign: continue
m_aa_c: ast.AnnAssign* = ast.AnnAssign*(m_child_c)
if m_aa_c is None or m_aa_c.target is None: continue
if m_aa_c.target.kind() != ast.ASTKind.Name: continue
m_tgt_c: ast.Name* = ast.Name*(m_aa_c.target)
if m_tgt_c.id is None: continue
field_ty_c: mlirlite.LLVMType* = GenApi.resolve_annotation_type(pool, m_aa_c.annotation, imported_modules, from_imports, trans, 0)
if field_ty_c is None:
# field type parse failed: do not register this class (leave it to the source module), not fatal
return 0
if field_count < GenTypes.FIELD_MAX:
field_names_buf[field_count] = sizet(m_tgt_c.id)
field_types_buf[field_count] = sizet(field_ty_c)
# Re-extract the field-level markers from the raw annotation, exactly as
# ClassGenDef.translate_class_def does on the definition side (same predicates, same
# precedence), so a cross-module reference gets identical field semantics.
annot_fc: ast.AST* = m_aa_c.annotation
if annot_fc is not None:
if GenApi.has_decorator_marker(annot_fc, "CVolatile", 0) != 0: field_volatile_buf[field_count] = 1
if GenApi.has_pointee_volatile_marker(annot_fc) != 0: field_pointee_volatile_buf[field_count] = 1
if GenApi.is_const_object_marker(annot_fc, field_ty_c) != 0: field_const_buf[field_count] = 1
# Byte order: 1 = t.BigEndian / 2 = t.LittleEndian (0 stays native);
# annotation `|` means "and", so `X | t.BigEndian` is a BigEndian X.
if GenApi.has_decorator_marker(annot_fc, "BigEndian", 0) != 0: field_byteorder_buf[field_count] = 1
elif GenApi.has_decorator_marker(annot_fc, "LittleEndian", 0) != 0: field_byteorder_buf[field_count] = 2
field_count += 1
# 3. The vpi stub has no class fields -> field_count is 0; registering an empty class would make
# the source module hit an empty struct and raise InternalCompilerError, so do not register.
if field_count == 0: return 0
# 4. build the ParamNode list
first_node_c: mlirlite.ParamNode* = None
prev_node_c: mlirlite.ParamNode* = None
for fi_c in range(int(field_count)):
fty_c: mlirlite.LLVMType* = (mlirlite.LLVMType*)(field_types_buf[fi_c])
pnode_c: mlirlite.ParamNode* = pool.alloc(mlirlite.ParamNode.__sizeof__())
if pnode_c is None: continue
string.memset(pnode_c, 0, mlirlite.ParamNode.__sizeof__())
pnode_c.Ty = fty_c
if first_node_c is None: first_node_c = pnode_c
if prev_node_c is not None: prev_node_c.Next = pnode_c
prev_node_c = pnode_c
# 4. Named struct type name "sha1.ClassName" (matching ClassGenDef); nested classes use the
# flat real_name, consistent with ClassGenDef._translate_nested_classdefs.
type_name_c: str = real_name
if src_sha1 is not None and src_sha1[0] != '\0':
# [D7 truncation discipline] The old fixed 64-byte buffer silently truncated class names
# over 46 chars; allocate the exact length so the struct name matches ClassGenDef.
sh_len_c: sizet = string.strlen(src_sha1)
cn_len_c: sizet = string.strlen(real_name)
name_buf_c: str = pool.alloc(sh_len_c + 1 + cn_len_c + 1)
if name_buf_c is not None:
string.strcpy(name_buf_c, src_sha1)
name_buf_c[sh_len_c] = '.'
string.strcpy(name_buf_c + sh_len_c + 1, real_name)
type_name_c = name_buf_c
struct_ty_c: mlirlite.LLVMType* = mlirlite.Struct(pool, first_node_c, field_count, type_name_c)
if struct_ty_c is None: return 0
# 5. Under StdlibAlloc (Phase B) deep-copy into perm_pool to survive cross-file resets
# (matching ClassGenDef.translate_class_def).
if GenApi.get_stdlib_alloc() != 0:
st_copy_c: mlirlite.LLVMType* = mlirlite.DeepCopyType(CompilerFactory.factory.PermPool, struct_ty_c)
if st_copy_c is not None: struct_ty_c = st_copy_c
# 5.5 Forward-decl placeholder hit -> write in place into *existing.Ty (same as ClassGenDef's
# fill_fwd) so register_struct's Ty-pointer dedup works and the StructTable does not overflow.
if fill_fwd_c != 0 and existing_c is not None and existing_c.Ty is not None:
fwd_name_c: str = mlirlite.get_struct_name(existing_c.Ty)
fwd_fields_c: mlirlite.ParamNode* = mlirlite.get_struct_fields(struct_ty_c)
fwd_fc_c: int = mlirlite.get_struct_fcount(struct_ty_c)
*existing_c.Ty = mlirlite.LLVMType.Struct(fwd_fields_c, fwd_fc_c, fwd_name_c)
struct_ty_c = existing_c.Ty
type_name_c = fwd_name_c
existing_c.IsForwardDecl = 0
existing_c.FieldCount = 0
# 6. register into the current module (emit the type def) + global StructTable + visibility
mlirlite.module_add_named_type(mod, pool, struct_ty_c)
# B5: vpi keeps @c.Attribute(t.attr.packed); cross-module injected structs must be packed too
# (<{...}>), else the referencing module computes GEPs with a non-packed layout (fields misread).
if _vpi_class_has_packed(class_cd.decorator_list) != 0: mlirlite.module_set_named_type_packed(mod, pool, struct_ty_c, 1)
# Register under the flat real_name: cross-module `Outer.Inner` lookups query the flat-name
# entry (same source as the definition-side StructGen registration name).
entry_c: GenTypes.StructEntry* = GenApi.register_struct(pool, real_name, struct_ty_c, src_sha1)
if entry_c is None: return 0
# B5: sync IsPacked on the registered entry; ensure_struct_def_in_module reads it to decide
# packing, otherwise the injected cross-module struct is non-packed (GEP layout mismatch).
if _vpi_class_has_packed(class_cd.decorator_list) != 0: entry_c.IsPacked = 1
# Also register fields into the StructEntry field table (name + type): a set Ty with an empty
# field table makes lookup_field_by_class fail by name, which silently zeroes reads.
afi_c: int = 0
for afi_c in range(int(field_count)):
af_name_c: str = str(field_names_buf[afi_c])
af_ty_c: mlirlite.LLVMType* = (mlirlite.LLVMType*)(field_types_buf[afi_c])
af_idx_c: int = GenApi.add_field(pool, entry_c, af_name_c, af_ty_c)
# Restore the per-field markers AFTER add_field, because StructGenMixin.add_field always
# resets them to 0 (it cannot see the annotation). Buffers are index-aligned with add_field
# order, so afi_c indexes them directly (mirrors ClassGenDef's post-add_field set).
if af_idx_c >= 0 and GenApi._g_gen is not None:
af_fe_c: GenTypes.FieldEntry* = GenApi._g_gen.StructGen._get_field_entry(entry_c, af_idx_c)
if af_fe_c is not None:
if field_volatile_buf[afi_c] != 0: af_fe_c.IsVolatile = 1
if field_pointee_volatile_buf[afi_c] != 0: af_fe_c.PointeeVolatile = 1
if field_const_buf[afi_c] != 0: af_fe_c.IsConst = 1
if field_byteorder_buf[afi_c] != 0: af_fe_c.ByteOrder = int(field_byteorder_buf[afi_c])
GenApi.add_visible_struct(pool, real_name)
return 1
# _inject_nested_class_declares - recursively register nested classes of a vpi class.
# Uses the flat name {outer}_{inner} (matching ClassGenDef._translate_nested_classdefs).
def _inject_nested_class_declares(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, class_cd: ast.ClassDef*, prefix_name: str, trans: HT.Translator*, src_sha1: str, imported_modules: str, from_imports: str) -> None:
"""Recursively register all nested ClassDefs of class_cd as flat-named types."""
if pool is None or mod is None or class_cd is None or prefix_name is None: return
children: list[ast.AST*]* = class_cd.children
if children is None: return
cn: sizet = len(children)
for ci in range(int(cn)):
child_ast: ast.AST* = children.get(ci)
if child_ast is None: continue
if child_ast.kind() != ast.ASTKind.ClassDef: continue
child_cd: ast.ClassDef* = ast.ClassDef*(child_ast)
if child_cd is None or child_cd.name is None or child_cd.name[0] == '\0': continue
_inject_class_struct_declare(pool, mod, child_cd, child_cd.name, trans, src_sha1, imported_modules, from_imports, prefix_name)
# _ensure_full_value_types - register full definitions for cross-module value types.
# Opaque is not enough for llc (value structs/arrays/full def needed); pointer refs need only opaque.
def _ensure_full_value_types(mod: mlirlite.LLVMModule*, pool: memhub.MemBuddy*, ty: mlirlite.LLVMType*) -> None:
"""Recursively register full definitions for value structs and array element structs."""
if mod is None or pool is None or ty is None: return
match ty:
case mlirlite.LLVMType.Struct(_, _, _):
full_se_h: GenTypes.StructEntry* = GenApi.find_struct_by_type(ty)
if full_se_h is not None and full_se_h.Ty is not None:
mlirlite.module_add_named_type(mod, pool, full_se_h.Ty)
# B5: sync the packed flag when injecting value types (matching
# ensure_struct_def_in_module); without it the referencing module emits a non-packed {..}.
if full_se_h.IsPacked != 0: mlirlite.module_set_named_type_packed(mod, pool, full_se_h.Ty, 1)
case mlirlite.LLVMType.Array(aelem_h, _):
if aelem_h is not None: _ensure_full_value_types(mod, pool, aelem_h)
case _:
pass
# _AggregateParamTy - unified rule for bare-struct (aggregate) param types; must stay in sync with
# FuncGenDef's extern branch and the call side. C ABI -> by value (or i64 fallback); internal -> Ptr(struct).
def _AggregateParamTy(mod: mlirlite.LLVMModule*, pool: memhub.MemBuddy*, ty: mlirlite.LLVMType*, is_c_abi: int) -> mlirlite.LLVMType*:
"""Bare-struct param type: C ABI by value / internal Ptr(struct)."""
if mod is None or pool is None or ty is None: return ty
if is_c_abi != 0:
# true opaque sentinel: fcount < 0
is_opaque_ap: int = 0
match ty:
case mlirlite.LLVMType.Struct(_, fcount_ap, _):
if fcount_ap < 0: is_opaque_ap = 1
case _:
pass
if is_opaque_ap == 0 and GenApi.find_struct_by_type(ty) is not None:
_ensure_full_value_types(mod, pool, ty)
return ty
return mlirlite.Int64(pool)
ptr_ty_ap: mlirlite.LLVMType* = mlirlite.Ptr(pool, ty)
mlirlite.module_ensure_opaque_for_type(mod, pool, ptr_ty_ap)
return ptr_ty_ap
# _inject_global_variable_declare - create a module-level global variable declare from a vpi AnnAssign.
# Needed so cross-module from-import reads of globals can find them via find_global_in_module.
def _inject_global_variable_declare(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, aa: ast.AnnAssign*, trans: HT.Translator*, imported_modules: str, from_imports: str, i32_ty: mlirlite.LLVMType*, i8_ptr_ty: mlirlite.LLVMType*, src_sha1: str) -> None:
"""Create a module-level global variable external declare from a vpi AnnAssign."""
if pool is None or mod is None or aa is None or trans is None: return
tgt_v: ast.AST* = aa.target
if tgt_v is None or tgt_v.kind() != ast.ASTKind.Name: return
nm_v: ast.Name* = ast.Name*(tgt_v)
if nm_v is None or nm_v.id is None: return
gname_v: str = nm_v.id
if gname_v[0] == '\0': return
# Self-injection guard (full-translation stage only): a module's own globals are emitted by
# TranslatorGenMixin.handle_module_level_var; re-injecting its own vpi would cause llc redefinition.
if trans is not None and trans._declare_only == 0 and trans.ModuleSha1 is not None and src_sha1 is not None:
if string.strcmp(src_sha1, trans.ModuleSha1) == 0: return
# Mangled name: prefix with the source module SHA1 so the define side and from-import lookup
# agree; a bare short name would remain undefined.
gv_name_m: str = gname_v
# CExport globals: the definition side uses the bare name (@gp), so the stub declare must too,
# else cross-module references emit @sha1.gp and ld.lld reports an undefined symbol.
is_gv_cexport: int = 0
if aa.annotation is not None: is_gv_cexport = GenApi.has_decorator_marker(aa.annotation, "CExport")
if src_sha1 is not None and src_sha1[0] != '\0' and is_gv_cexport == 0:
ml_v: sizet = string.strlen(src_sha1) + 1 + string.strlen(gname_v) + 1
gv_name_m = pool.alloc(ml_v)
if gv_name_m is not None: viperlib.snprintf(gv_name_m, ml_v, "%s.%s", src_sha1, gname_v)
# dedup: walk the module's global variable list (matching the mangled name)
cur_v: mlirlite.GlobalVariable* = mod.GlobalHead
while cur_v is not None:
if cur_v.Name is not None and string.strcmp(cur_v.Name, gv_name_m) == 0: return
cur_v = cur_v.Next
# resolve the annotation type (bare class -> Ptr(struct), CArray -> element pointer)
resolved_v: mlirlite.LLVMType* = None
if aa.annotation is not None:
resolved_v = GenApi.resolve_annotation_type(pool, aa.annotation, imported_modules, from_imports, trans)
if resolved_v is None:
return # type parse failed, skip: the call side has a fail-fast location
gv_ty_v: mlirlite.LLVMType* = resolved_v
match resolved_v:
case mlirlite.LLVMType.Struct(_, _, _):
# Object classes (@t.Object/@t.CVTable: object-as-pointer) -> Ptr(struct), matching
# handle_module_level_var; plain t.CStruct value classes keep a value instance.
gv_entry_v: GenTypes.StructEntry* = GenApi.find_struct_by_type(resolved_v)
if gv_entry_v is not None and (gv_entry_v.HasNew != 0 or gv_entry_v.HasVTable != 0 or gv_entry_v.IsOOP != 0): gv_ty_v = mlirlite.Ptr(pool, resolved_v)
else: gv_ty_v = resolved_v
case mlirlite.LLVMType.Array(aelem_v, acount_v):
# Fixed-size arrays (CArray[T, N]) keep the full [N x T] type, matching
# handle_module_level_var; decaying to an element pointer would later read garbage.
if acount_v > 0: gv_ty_v = resolved_v
else:
# variable-length array (CArray[T, None]): element pointer (C array decay)
gv_ty_v = mlirlite.Ptr(pool, aelem_v)
case _:
gv_ty_v = resolved_v
# Ensure embedded structs have an opaque declaration (llc "use of undefined type"),
# matching TranslatorGenMixin; value types get full definitions via _ensure_full_value_types.
_ensure_full_value_types(mod, pool, gv_ty_v)
mlirlite.module_ensure_opaque_for_type(mod, pool, gv_ty_v)
gv_v: mlirlite.GlobalVariable* = mlirlite.new_global_variable(pool, gv_name_m, gv_ty_v)
if gv_v is None: return
mlirlite.module_add_global(mod, gv_v)
gv_v.Linkage = "external"
# _inject_function_declare - create a declare for a single FunctionDef: detect CExport/CExtern/State,
# compute the mangled name, dedup via find_func_in_module, create the declare and register it.
def _inject_function_declare(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, fd: ast.FunctionDef*, trans: HT.Translator*, src_sha1: str, imported_modules: str, from_imports: str, i32_ty: mlirlite.LLVMType*, i8_ptr_ty: mlirlite.LLVMType*, funcs_ptr: GenTypes.FuncEntry*, max_funcs: int) -> None:
"""Create and inject a declare for a FunctionDef into the module."""
if fd.name is None: return
# Self-injection guard (full-translation stage only): a module's own top-level functions are
# emitted by FuncGenDef; re-injecting its own vpi would create a conflicting duplicate declare.
if trans is not None and trans._declare_only == 0 and trans.ModuleSha1 is not None:
if string.strcmp(src_sha1, trans.ModuleSha1) == 0: return
# detect CExtern/State/CExport markers (from the return-type annotation)
has_extern: int = 0
has_state: int = 0
has_export: int = 0
if fd.returns is not None:
has_extern = GenApi.has_decorator_marker(fd.returns, "CExtern")
has_state = GenApi.has_decorator_marker(fd.returns, "State")
has_export = GenApi.has_decorator_marker(fd.returns, "CExport")
# Bare-struct param rule (see _AggregateParamTy): C ABI externs (t.CExtern/t.State) by value;
# the rest (incl. t.CExport) keep Ptr(struct). Same source as FuncGenDef.is_extern_decl.
agg_is_c_abi: int = 0
if has_extern != 0 or has_state != 0: agg_is_c_abi = 1
# compute the mangled name:
# CExport/CExtern/State/main functions use the bare name, others get the source module SHA1 prefix
use_bare_name: int = 0
if has_extern != 0 or has_state != 0 or has_export != 0 or string.strcmp(fd.name, "main") == 0:
mangled_name: str = fd.name
use_bare_name = 1
else: mangled_name = GenApi._mangle_name_with_sha1(src_sha1, fd.name)
# This file's own bare-name declarations take priority: extern/state/export symbols carry no SHA1
# prefix, so an injected dependency of the same name would silently win. Only bare names matter.
if use_bare_name != 0 and GenApi.is_file_func(fd.name) != 0: return
# dedup: skip if the function already exists
existing: mlirlite.Function* = GenApi.find_func_in_module(mod, mangled_name)
if existing is not None: return
# also check the bare name (in non-bare mode a CExport function may have been registered another way)
if use_bare_name == 0:
existing_bare: mlirlite.Function* = GenApi.find_func_in_module(mod, fd.name)
if existing_bare is not None: return
# Infer the return type: prefer the cached return-annotation string (extracted right after tree
# parse) and resolve it directly, avoiding corrupted kind() from cross-module AST reuse.
ret_ty: mlirlite.LLVMType* = None
ret_annot_fd: str = _lookup_vpi_ret_annot(src_sha1, None, fd.name)
if ret_annot_fd is not None: ret_ty = _resolve_vpi_ret_annot_str(pool, ret_annot_fd, i32_ty, i8_ptr_ty)
if ret_ty is None and fd.returns is not None:
ret_ty = GenApi.resolve_annotation_type(pool, fd.returns, imported_modules, from_imports, trans)
# if the return type is a pure t.State (no real type), use Void
if ret_ty is None and fd.returns is not None:
# t.State/t.CExtern are storage-class decorator markers, not real return types. An annotation
# like "HANDLE | t.State" has a real type and must not map to Void; only pure markers do.
is_pure_marker_rt: int = 0
rk2_rt: int = fd.returns.kind()
if rk2_rt == ast.ASTKind.Name or rk2_rt == ast.ASTKind.Constant: is_pure_marker_rt = 1
elif rk2_rt == ast.ASTKind.Attribute:
pm_at: ast.Attribute* = ast.Attribute*(fd.returns)
if pm_at.attr is not None and (string.strcmp(pm_at.attr, "State") == 0 or string.strcmp(pm_at.attr, "CExtern") == 0 or string.strcmp(pm_at.attr, "CExport") == 0):
is_pure_marker_rt = 1
if is_pure_marker_rt != 0: ret_ty = mlirlite.Void(pool)
elif has_state != 0 or has_extern != 0 or has_export != 0:
# Has a real type but resolution failed: fall back to the external function return-type
# whitelist (e.g. FindFirstFileA/GetStdHandle -> i8* HANDLE) instead of truncating to i32.
ret_ty = GenApi.infer_external_func_ret_ty(pool, fd.name)
# [S4 fail-fast] A return annotation that does not resolve is not silently downgraded to i32;
# no annotation (fd.returns is None) is a legitimate default and keeps i32 below.
if ret_ty is None and fd.returns is not None:
GenApi.fatal_error(fd.returns, f"extern declaration '{fd.name}' return type annotation failed to resolve", "NameError")
# default return i32
if ret_ty is None: ret_ty = i32_ty
# A value struct return type (e.g. Mat4) needs a full definition: declaring %Mat4 opaque makes
# llc report "use of undefined type". Register the full definition.
_ensure_full_value_types(mod, pool, ret_ty)
mlirlite.module_ensure_opaque_for_type(mod, pool, ret_ty)
# create the declare
func: mlirlite.Function* = mlirlite.create_declare(pool, mod, mangled_name, ret_ty)
if func is None: return
# extract params and add them to the declare
param_count: int = 0
fd_defaults: t.CPtr = None
fd_default_count: int = 0
if fd.args is not None:
args_node: ast.Arguments* = ast.Arguments*(fd.args)
if args_node.args is not None:
args_list: list[ast.AST*]* = args_node.args
param_count = int(len(args_list))
# Param-count upper-bound sentinel (fail-fast): a corrupted vpi tree node makes __len__()
# return a huge value, so add_param runs massively and the Params chain balloons.
if param_count < 0 or param_count > 128:
stdio.printf("[VPIS] abnormal parameter count: function=%s param_count=%d (fd.args=%llu, tree may be overwritten; aborting)\n", fd.name, param_count, uint64(fd.args))
# convergence point: longjmp back to the REPL loop in a REPL session (diagnostics printed)
VLogger.repl_abort()
pi: sizet
for pi in range(int(param_count)):
ag: ast.Arg* = ast.Arg*(args_list.get(pi))
if ag is None: continue
pname: str = ag.arg
if pname is None: pname = "arg"
# resolve the param type
param_ty: mlirlite.LLVMType* = i8_ptr_ty
# Prefer the vpi text annotation cache (same source as return types): corrupted tree
# nodes make AST resolve return garbage types and mis-size cross-module declare params.
pseg_ty: mlirlite.LLVMType* = _resolve_vpi_param_annot_seg(pool, src_sha1, None, fd.name, int(pi), i32_ty, i8_ptr_ty)
if pseg_ty is not None:
match pseg_ty:
case mlirlite.LLVMType.Struct(_, _, _):
# Bare struct param: C ABI by value / internal Ptr(struct)
# (criterion and fallback in _AggregateParamTy; keep the three sites in sync)
param_ty = _AggregateParamTy(mod, pool, pseg_ty, agg_is_c_abi)
case mlirlite.LLVMType.Array(aelem_ty, _):
param_ty = mlirlite.Ptr(pool, aelem_ty)
case _:
param_ty = pseg_ty
elif ag.annotation is not None:
resolved_ty: mlirlite.LLVMType* = GenApi.resolve_annotation_type(pool, ag.annotation, imported_modules, from_imports, trans)
if resolved_ty is not None:
# CArray param -> element pointer ty* (C array decay, matching the compiler side).
# Bare struct param -> _AggregateParamTy (same criterion).
match resolved_ty:
case mlirlite.LLVMType.Struct(_, _, _):
param_ty = _AggregateParamTy(mod, pool, resolved_ty, agg_is_c_abi)
case mlirlite.LLVMType.Array(aelem_ty, _):
param_ty = mlirlite.Ptr(pool, aelem_ty)
case _:
param_ty = resolved_ty
else:
# [S4 fail-fast] An annotation that does not resolve is not silently downgraded
# to i8* (no annotation is a legitimate default and keeps i8*).
GenApi.fatal_error(ag.annotation, f"extern declaration '{fd.name}' parameter '{pname}' type annotation failed to resolve", "NameError")
mlirlite.add_param(pool, func, param_ty, pname)
# Text-cache repair: a corrupted vpi tree truncates args, leaving too few declare params;
# complete them from the vpi text param annotations so declare matches define.
param_count = _repair_declare_params_from_text(pool, func, src_sha1, None, fd.name, param_count, i32_ty, i8_ptr_ty)
# *args varargs: set IsVarArg=1 so the call side applies C ABI promotions (i8/i16 -> i32,
# float -> double), avoiding misread %d/%x formats.
if args_node.vararg is not None: func.IsVarArg = 1
# Default params: vpi signatures include '= <default>'; register the defaults list so
# cross-module calls missing args fill from the table instead of reading stack garbage.
if args_node.defaults is not None:
fd_dlist: list[ast.AST*]* = args_node.defaults
fd_default_count = int(len(fd_dlist))
if fd_default_count < 0 or fd_default_count > 128:
# A corrupted args.defaults node makes len return a huge value, so
# _register_global_defaults would over-allocate and write out of bounds; skip it.
stdio.printf("[VPIS] abnormal default parameter count: function=%s default_count=%d (tree may be overwritten; skipping default value registration)\n", fd.name, fd_default_count)
fd_defaults = None
fd_default_count = 0
else: fd_defaults = fd_dlist
# register into the function table (using the bare fd.name, not mangled_name)
if funcs_ptr is not None:
cur_count: int = trans._func_count
if GenApi.add_func_to_table(funcs_ptr, cur_count, fd.name, func, max_funcs, fd_defaults, fd_default_count, param_count, None, src_sha1) == 0:
trans._func_count = cur_count + 1
# Complete the global function-table Spec (params all added, matching FuncGenDef): without it
# find_func_entry_global() sees an empty Spec and vtable generation fails to find the type.
GenApi.update_global_func_spec(fd.name, func)
# _inject_method_declare - create a declare for a ClassDef method. Mangled name is
# {src_sha1}.{ClassName}.{method}; param types come from vpi annotations (self defaults to i8*).
def _inject_method_declare(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, m_fd: ast.FunctionDef*, cls_name: str, trans: HT.Translator*, src_sha1: str, imported_modules: str, from_imports: str, i32_ty: mlirlite.LLVMType*, i8_ptr_ty: mlirlite.LLVMType*, funcs_ptr: GenTypes.FuncEntry*, max_funcs: int) -> None:
"""Create and inject a declare for a ClassDef method into the module."""
if m_fd is None or m_fd.name is None or cls_name is None: return
if src_sha1 is None: return
# Self-injection guard (full-translation stage only): a module's own method defines are emitted
# by ClassGenVTable; re-injecting its own vpi would create conflicting duplicate declares.
if trans is not None and trans._declare_only == 0 and trans.ModuleSha1 is not None:
if string.strcmp(src_sha1, trans.ModuleSha1) == 0: return
# build the method lookup name: ClassName.method
lookup_name_len: sizet = string.strlen(cls_name) + 1 + string.strlen(m_fd.name) + 1
lookup_name: str = pool.alloc(lookup_name_len)
if lookup_name is None: return
viperlib.snprintf(lookup_name, lookup_name_len, "%s.%s", cls_name, m_fd.name)
# mangled name: {src_sha1}.{ClassName}.{method}
mangled_len: sizet = string.strlen(src_sha1) + 1 + lookup_name_len
mangled_name: str = pool.alloc(mangled_len)
if mangled_name is None: return
viperlib.snprintf(mangled_name, mangled_len, "%s.%s", src_sha1, lookup_name)
# dedup: skip if the method already exists
existing: mlirlite.Function* = GenApi.find_func_in_module(mod, mangled_name)
if existing is not None: return
# Infer the return type: prefer the cached return-annotation string (extracted right after tree
# parse); resolving m_fd.returns directly may yield garbage from a corrupted vpi tree node.
ret_ty: mlirlite.LLVMType* = None
ret_annot: str = _lookup_vpi_ret_annot(src_sha1, cls_name, m_fd.name)
if ret_annot is not None: ret_ty = _resolve_vpi_ret_annot_str(pool, ret_annot, i32_ty, i8_ptr_ty)
if ret_ty is None and m_fd.returns is not None:
ret_ty = GenApi.resolve_annotation_type(pool, m_fd.returns, imported_modules, from_imports, trans)
# infer __new__ without annotation as a class pointer (it returns a self pointer; i32 would truncate)
if ret_ty is None and string.strcmp(m_fd.name, "__new__") == 0:
cls_ty: mlirlite.LLVMType* = GenApi.get_struct_type(cls_name)
if cls_ty is not None: ret_ty = mlirlite.Ptr(pool, cls_ty)
# [S4 fail-fast] A return annotation that does not resolve is not silently downgraded to i32;
# unannotated __new__ / normal methods keep i32 as the legitimate default below.
if ret_ty is None and m_fd.returns is not None:
GenApi.fatal_error(m_fd.returns, f"extern method '{cls_name}.{m_fd.name}' return type annotation failed to resolve", "NameError")
# Unannotated __init__ -> Void, matching the definition side (ClassGenVTable always uses Void);
# defaulting to i32 would split declare i32 / define void.
if ret_ty is None and string.strcmp(m_fd.name, "__init__") == 0: ret_ty = mlirlite.Void(pool)
if ret_ty is None: ret_ty = i32_ty
# 跨模块方法**按值返回类**时,引用模块必须登记该具名结构体的完整定义:否则打印方法签名回查登记表,
# 命中的只是指针引用登记的 opaque 前向声明 → MLIR 报 "still opaque at print time"。与
# _inject_function_declare 同口径(那里对值返回类型调 _ensure_full_value_types)。
_ensure_full_value_types(mod, pool, ret_ty)
# create the declare
func: mlirlite.Function* = mlirlite.create_declare(pool, mod, mangled_name, ret_ty)
if func is None: return
# extract params and add them to the declare
param_count: int = 0
md_defaults: t.CPtr = None
md_default_count: int = 0
if m_fd.args is not None:
args_node: ast.Arguments* = ast.Arguments*(m_fd.args)
if args_node.args is not None:
args_list: list[ast.AST*]* = args_node.args
param_count = int(len(args_list))
# Param-count upper-bound sentinel (fail-fast): same cause as _inject_function_declare
# (a corrupted vpi tree node makes __len__() return a huge value).
if param_count < 0 or param_count > 128:
stdio.printf("[VPIS] abnormal parameter count: method=%s.%s param_count=%d (m_fd.args=%llu, tree may be overwritten; aborting)\n", cls_name, m_fd.name, param_count, uint64(m_fd.args))
# convergence point: longjmp back to the REPL loop in a REPL session (diagnostics printed)
VLogger.repl_abort()
pi: sizet
for pi in range(int(param_count)):
ag: ast.Arg* = ast.Arg*(args_list.get(pi))
if ag is None: continue
pname: str = ag.arg
if pname is None: pname = "arg"
# self/cls (first param): cross-module declares use the class pointer (%"sha1.Class"*)
# instead of i8*, matching _translate_method's Ptr(StructTy) and removing ABI ambiguity.
if pi == 0 and (string.strcmp(pname, "self") == 0 or string.strcmp(pname, "cls") == 0):
cls_ty_sd: mlirlite.LLVMType* = GenApi.get_struct_type(cls_name)
if cls_ty_sd is not None:
mlirlite.add_param(pool, func, mlirlite.Ptr(pool, cls_ty_sd), pname)
continue
param_ty: mlirlite.LLVMType* = i8_ptr_ty
# Prefer the vpi text annotation cache (same source as return types): corrupted tree
# nodes mis-size declare params, causing trunc i64->i32 and a caller/callee mismatch.
pseg_ty: mlirlite.LLVMType* = _resolve_vpi_param_annot_seg(pool, src_sha1, cls_name, m_fd.name, int(pi), i32_ty, i8_ptr_ty)
if pseg_ty is not None:
match pseg_ty:
case mlirlite.LLVMType.Struct(_, _, _):
# Bare struct param: methods are always internal VPC symbols (mangled
# {sha1}.{Class}.{method}, no C ABI define), so is_c_abi=0 -> Ptr(struct).
param_ty = _AggregateParamTy(mod, pool, pseg_ty, 0)
case mlirlite.LLVMType.Array(aelem_ty, _):
param_ty = mlirlite.Ptr(pool, aelem_ty)
case _:
param_ty = pseg_ty
elif ag.annotation is not None:
resolved_ty: mlirlite.LLVMType* = GenApi.resolve_annotation_type(pool, ag.annotation, imported_modules, from_imports, trans)
if resolved_ty is not None:
# CArray param -> element pointer ty* (C array decay, matching the compiler side).
# Bare struct param -> _AggregateParamTy (same criterion, is_c_abi=0).
match resolved_ty:
case mlirlite.LLVMType.Struct(_, _, _):
param_ty = _AggregateParamTy(mod, pool, resolved_ty, 0)
case mlirlite.LLVMType.Array(aelem_ty, _):
param_ty = mlirlite.Ptr(pool, aelem_ty)
case _:
param_ty = resolved_ty
else:
# [S4 fail-fast] An annotation that does not resolve is not silently downgraded
# to i8* (no annotation is a legitimate default and keeps i8*).
GenApi.fatal_error(ag.annotation, f"extern method '{cls_name}.{m_fd.name}' parameter '{pname}' type annotation failed to resolve", "NameError")
mlirlite.add_param(pool, func, param_ty, pname)
# Text-cache repair: a corrupted vpi tree truncates args, leaving too few declare params;
# complete them from the vpi text param annotations so declare matches define.
param_count = _repair_declare_params_from_text(pool, func, src_sha1, cls_name, m_fd.name, param_count, i32_ty, i8_ptr_ty)
# OOP iter protocol: the __next__ stub declare carries a hidden __stop_iter_flag__ (i1*),
# matching _translate_method's define; `for x in obj` calls __next__(self, flag).
if m_fd.name is not None and string.strcmp(m_fd.name, "__next__") == 0:
i1_ty_si: mlirlite.LLVMType* = mlirlite.Int1(pool)
if i1_ty_si is not None:
i1_ptr_ty_si: mlirlite.LLVMType* = mlirlite.Ptr(pool, i1_ty_si)
if i1_ptr_ty_si is not None: mlirlite.add_param(pool, func, i1_ptr_ty_si, "__stop_iter_flag__")
# Default params: register the defaults list so cross-module calls missing args fill from the
# table instead of reading stack garbage.
if m_fd.args is not None:
md_args_node: ast.Arguments* = ast.Arguments*(m_fd.args)
if md_args_node.defaults is not None:
md_dlist: list[ast.AST*]* = md_args_node.defaults
md_default_count = int(len(md_dlist))
if md_default_count < 0 or md_default_count > 128:
# Same as _inject_function_declare: a corrupted tree node makes defaults len huge,
# which would write out of bounds into perm_pool; skip registration.
stdio.printf("[VPIS] abnormal default parameter count: method=%s.%s default_count=%d (tree may be overwritten; skipping default value registration)\n", cls_name, m_fd.name, md_default_count)
md_defaults = None
md_default_count = 0
else: md_defaults = md_dlist
# Register into the function table (bare ClassName.method, matching the caller's lookup_name).
# ParamCount excludes self (matching _translate_method's len(args) - 1), so default filling aligns.
if funcs_ptr is not None:
cur_count: int = trans._func_count
param_count_no_self: int = param_count
if param_count_no_self > 0: param_count_no_self -= 1
if GenApi.add_func_to_table(funcs_ptr, cur_count, lookup_name, func, max_funcs, md_defaults, md_default_count, param_count_no_self, None, src_sha1) == 0:
trans._func_count = cur_count + 1
# Complete the global function-table Spec (matching ClassGenVTable): otherwise
# find_func_entry_global().Spec is empty and vtable generation fails to find the method type.
GenApi.update_global_func_spec(lookup_name, func)
# _register_ctypedef_from_annassign - register a CTypedef from an AnnAssign node.
# Extracts the NAME and type expression from `NAME: t.CTypedef = Type` and registers it.
def _register_ctypedef_from_annassign(pool: memhub.MemBuddy*, aa: ast.AnnAssign*) -> None:
"""Extract and register a CTypedef from an AnnAssign node."""
if aa is None or pool is None: return
# extract the target name (must be a Name node)
if aa.target is None or aa.target.kind() != ast.ASTKind.Name: return
tgt: ast.Name* = ast.Name*(aa.target)
if tgt.id is None: return
# extract the type expression string (from the value AST node)
if aa.value is None: return
type_str: str = GenApi._get_type_name_from_annotation(pool, aa.value)
if type_str is None: return
# register the CTypedef
GenApi.register_ctypedef(pool, tgt.id, type_str)
# REPL: restore full layouts and vtables for includes classes, driven by {sha1}.meta.
# Only active when ReplStubLayoutMode != 0; layouts are validated against meta field/slot counts.
# session-level state (PermPool; valid during session init)
ReplStubLayoutMode: int = 0
ReplStubPool: memhub.MemBuddy* = None
ReplStubTempDir: str = None
ReplStubTable: sizet* = None
ReplStubCount: int = 0
ReplStubOk: int = 0
ReplStubVtOk: int = 0
ReplStubRefused: int = 0
ReplStubRefusedNoStub: int = 0 # refusal cause distribution (_REPL_SU_*): missing data (module has no {sha1}.meta)
ReplStubRefusedNoClass: int = 0 # refused: meta has the module but not this class
ReplStubRefusedOpaque: int = 0 # refused: the class is OPAQUE in meta
ReplStubRefusedParent: int = 0 # upstream no data: base unavailable/not registered
ReplStubNoStub: int = 0
ReplStubWarnLeft: int = 0
ReplStubTmpCount: int = -1
ReplStubTmpPacked: int = 0
ReplStubTmpArity: int = 0
def _repl_stub_dupstr(pool: memhub.MemBuddy*, src: str) -> str:
"""Deep-copy a string into the given pool."""
if src is None or pool is None: return None
ln: sizet = string.strlen(src)
dst: str = pool.alloc(ln + 1)
if dst is None: return None
string.strcpy(dst, src)
return dst
def _repl_stub_key(pool: memhub.MemBuddy*, sha1: str, cls: str) -> str:
"""Build "<sha1>.<class name>", matching the definition-side named struct exactly."""
if pool is None or sha1 is None or cls is None: return None
if sha1[0] == '\0' or cls[0] == '\0': return None
ln: sizet = string.strlen(sha1) + 1 + string.strlen(cls) + 1
buf: str = pool.alloc(ln)
if buf is None: return None
viperlib.snprintf(buf, ln, "%s.%s", sha1, cls)
return buf
def _repl_stub_path(sha1: str, ext: str) -> str:
"""Build the slice path <temp_dir>/<sub>/<sha1>.<ext> under the REPL temp dir (stdlib-allocated)."""
if ReplStubTempDir is None or sha1 is None: return None
td_len: sizet = string.strlen(ReplStubTempDir)
sh_len: sizet = string.strlen(sha1)
sub: str = Config.slice_subdir(sha1, Config.Sha1SliceLevel)
sub_len: sizet = 0
if sub is not None: sub_len = string.strlen(sub)
cap: sizet = td_len + sub_len + sh_len + 8
buf: str = stdlib.malloc(cap)
if buf is None: return None
if sub_len > 0: viperlib.snprintf(buf, cap, "%s/%s/%s.%s", ReplStubTempDir, sub, sha1, ext)
else: viperlib.snprintf(buf, cap, "%s/%s.%s", ReplStubTempDir, sha1, ext)
return buf
def _repl_stub_readtext(path: str) -> str:
"""Read stub text (stdlib.malloc, caller frees); None if missing or over the size cap."""
if path is None: return None
f: fileio.File* = fileio.File(path, fileio.MODE.R)
if f is None or f.closed: return None
sz: sizet = f.size()
if sz <= 0 or sz >= _REPL_STUB_TXT_CAP:
f.close()
return None
buf: str = stdlib.malloc(sz + 1)
if buf is None:
f.close()
return None
rd: LONG = f.read_all(buf, sz + 1)
f.close()
if rd <= 0:
stdlib.free(buf)
return None
if rd < LONG(sz + 1): buf[rd] = 0
return buf
# parse a decimal integer from txt[start, end) (skip leading spaces; no digits -> 0)
def _meta_atoi(txt: str, start: sizet, end: sizet) -> int:
i: sizet = start
while i < end and (txt[i] == ' ' or txt[i] == '\t'): i += 1
neg: int = 0
if i < end and txt[i] == '-':
neg = 1
i += 1
v: int = 0
while i < end and txt[i] >= '0' and txt[i] <= '9':
v = v * 10 + (int(txt[i]) - 48)
i += 1
if neg != 0: return -v
return v
# Scan {sha1}.meta text for `STRUCT <key> FIELDS <n> PACKED <0|1>` (or `OPAQUE`) and
# `VTABLE <key> SLOTS <n>`; results go to global scratch. Exact prefix match per line.
def _repl_meta_scan_layout(txt: str, key: str) -> None:
global ReplStubTmpCount, ReplStubTmpPacked, ReplStubTmpArity
ReplStubTmpCount = -1
ReplStubTmpPacked = 0
ReplStubTmpArity = 0
if txt is None or key is None: return
n: sizet = string.strlen(txt)
key_len: sizet = string.strlen(key)
if key_len == 0: return
i: sizet = 0
while i < n:
ls: sizet = i
while i < n and txt[i] != '\n': i += 1
le: sizet = i
if i < n: i += 1
if le > ls and txt[le - 1] == '\r': le -= 1
if ls + 7 <= le and string.strncmp(txt + ls, "STRUCT ", 7) == 0:
ns: sizet = ls + 7
ne: sizet = ns + key_len
if ne > le or string.strncmp(txt + ns, key, key_len) != 0: continue
if ne >= le or txt[ne] != ' ': continue
# OPAQUE
if ne + 7 <= le and string.strncmp(txt + ne + 1, "OPAQUE", 6) == 0:
ReplStubTmpCount = -2
continue
# FIELDS <n> PACKED <0|1>
if ne + 8 <= le and string.strncmp(txt + ne + 1, "FIELDS ", 7) == 0:
ReplStubTmpCount = _meta_atoi(txt, ne + 8, le)
ps: sizet = ne + 8
while ps < le and txt[ps] >= '0' and txt[ps] <= '9': ps += 1
if ps + 8 <= le and string.strncmp(txt + ps + 1, "PACKED ", 7) == 0: ReplStubTmpPacked = _meta_atoi(txt, ps + 8, le)
continue
if ls + 7 <= le and string.strncmp(txt + ls, "VTABLE ", 7) == 0:
vs: sizet = ls + 7
ve: sizet = vs + key_len
if ve > le or string.strncmp(txt + vs, key, key_len) != 0: continue
if ve >= le or txt[ve] != ' ': continue
if ve + 7 <= le and string.strncmp(txt + ve + 1, "SLOTS ", 6) == 0: ReplStubTmpArity = _meta_atoi(txt, ve + 7, le)
continue
return
def ReplStubLayoutBegin(temp_dir: str) -> int:
"""Enable session-level stub layout restore (called once before registering includes classes); 0=success."""
global ReplStubLayoutMode, ReplStubPool, ReplStubTempDir, ReplStubTable
global ReplStubCount, ReplStubOk, ReplStubVtOk, ReplStubRefused, ReplStubNoStub
global ReplStubWarnLeft
global ReplStubRefusedNoStub, ReplStubRefusedNoClass, ReplStubRefusedOpaque, ReplStubRefusedParent
pool: memhub.MemBuddy* = CompilerFactory.factory.PermPool
if pool is None or temp_dir is None: return 1
ReplStubPool = pool
ReplStubTempDir = _repl_stub_dupstr(pool, temp_dir)
if ReplStubTempDir is None: return 1
ReplStubTable = pool.alloc(8 * _REPL_STUB_CLS_MAX)
if ReplStubTable is None: return 1
string.memset(ReplStubTable, 0, 8 * _REPL_STUB_CLS_MAX)
ReplStubCount = 0
ReplStubOk = 0
ReplStubVtOk = 0
ReplStubRefused = 0
ReplStubRefusedNoStub = 0
ReplStubRefusedNoClass = 0
ReplStubRefusedOpaque = 0
ReplStubRefusedParent = 0
ReplStubNoStub = 0
ReplStubWarnLeft = _REPL_STUB_WARN_MAX
ReplStubLayoutMode = 1
return 0
def _repl_stub_at(i: int) -> ReplStubClass*:
"""Return the i-th class-table entry, or None."""
if ReplStubTable is None or i < 0 or i >= ReplStubCount: return None
addr: sizet = ReplStubTable[i]
if addr == 0: return None
return ReplStubClass*(uint64(addr))
def _repl_stub_find(key: str) -> ReplStubClass*:
"""Look up a class-table entry by "<sha1>.<class name>"; None if not found."""
if key is None: return None
i: int
for i in range(int(ReplStubCount)):
e: ReplStubClass* = _repl_stub_at(int(i))
if e is None or e.Key is None: continue
if string.strcmp(e.Key, key) == 0: return e
return None
def _repl_stub_find_base(e: ReplStubClass*, bi: int) -> ReplStubClass*:
"""Find a base-class entry: same-module SHA1 exact match first, then first bare-name match."""
if e is None or e.Bases is None or bi < 0 or bi >= e.BaseCount: return None
pname: str = rawptr(e.Bases[bi])
if pname is None: return None
i: int
for i in range(int(ReplStubCount)):
pe: ReplStubClass* = _repl_stub_at(int(i))
if pe is None or pe.ClassName is None: continue
if string.strcmp(pe.ClassName, pname) != 0: continue
if e.Sha1 is not None and pe.Sha1 is not None:
if string.strcmp(pe.Sha1, e.Sha1) == 0: return pe
for i in range(int(ReplStubCount)):
pe2: ReplStubClass* = _repl_stub_at(int(i))
if pe2 is None or pe2.ClassName is None: continue
if string.strcmp(pe2.ClassName, pname) == 0: return pe2
return None
def _repl_stub_has_name(arr: sizet*, cnt: int, name: str) -> int:
"""Check whether the name is already in the list (first-declaration wins)."""
if arr is None or name is None: return 0
i: int
for i in range(int(cnt)):
addr: sizet = arr[i]
if addr == 0: continue
ex: str = rawptr(addr)
if ex is not None and string.strcmp(ex, name) == 0: return 1
return 0
def _repl_stub_deco_flags(deco_list: list[ast.AST*]*) -> int:
"""Return class/method decorator flags (bit 0=@t.NoVTable, bit 1=@t.CVTable)."""
if deco_list is None: return 0
flags: int = 0
dn: sizet = len(deco_list)
di: sizet
for di in range(int(dn)):
deco: ast.AST* = deco_list.get(di)
if deco is None: continue
nm: str = None
k: int = deco.kind()
if k == ast.ASTKind.Attribute:
at: ast.Attribute* = ast.Attribute*(deco)
if at.attr is not None and at.value is not None and at.value.kind() == ast.ASTKind.Name:
vn: ast.Name* = ast.Name*(at.value)
if vn.id is not None and string.strcmp(vn.id, "t") == 0: nm = at.attr
elif k == ast.ASTKind.Name:
nn: ast.Name* = ast.Name*(deco)
nm = nn.id
if nm is None: continue
if string.strcmp(nm, "NoVTable") == 0: flags = flags | 1
elif string.strcmp(nm, "CVTable") == 0: flags = flags | 2
return flags
def _repl_stub_is_virtual(e: ReplStubClass*, mi: int, first: ReplStubClass*) -> int:
"""Return whether this class's mi-th method goes into the vtable."""
if e is None or e.MethodFlags is None or mi < 0 or mi >= e.MethodCount: return 0
mf: int = int(e.MethodFlags[mi])
if (mf & 1) != 0: return 0
if (mf & 2) != 0: return 1
if (e.Flags & _REPL_SF_CVTABLE) != 0: return 1
if first is not None:
if first.IsNoVTable != 0: return 0
return first.HasVt
return 0
def _repl_stub_warn_refuse(e: ReplStubClass*, why: str, a: int, b: int) -> None:
"""Warn on a criterion rejection: stub field count a differs from flattened+vtable inference b."""
global ReplStubWarnLeft, ReplStubRefused
ReplStubRefused += 1
if ReplStubWarnLeft <= 0: return
ReplStubWarnLeft -= 1
stdio.printf("includes class layout: layout of %s.%s is untrusted => explicitly refused (reason=criterion refused: %s; " "stub field count=%d != flattened field count+vtable slots=%d). Field access on this class is explicitly refused " "(method calls are unaffected)\n", e.Sha1, e.ClassName, why, a, b)
def _repl_stub_warn_nodata(e: ReplStubClass*, cause: int) -> None:
"""Warn on a missing-data refusal: no comparison was possible, so print the data sources instead."""
global ReplStubWarnLeft, ReplStubRefused
global ReplStubRefusedNoStub, ReplStubRefusedNoClass
global ReplStubRefusedOpaque, ReplStubRefusedParent
ReplStubRefused += 1
if cause == _REPL_SU_NOSTUB: ReplStubRefusedNoStub += 1
elif cause == _REPL_SU_NOCLASS: ReplStubRefusedNoClass += 1
elif cause == _REPL_SU_OPAQUE: ReplStubRefusedOpaque += 1
else: ReplStubRefusedParent += 1
if ReplStubWarnLeft <= 0: return
ReplStubWarnLeft -= 1
if cause == _REPL_SU_NOSTUB:
# missing data: not even module layout metadata (LayoutCount = -1 is just a "not read" sentinel)
stdio.printf("includes class layout: layout of %s.%s unavailable => explicitly refused (reason=data missing: " "module %s has no {sha1}.meta layout metadata; meta field count=none / inferred field count=not computed => " "no data on either side, no comparison was made, and this is not a field count mismatch). Field access on this class is " "refused (method calls are unaffected); add the module meta (run batch processing once in the same project, " "Phase A/B writes {sha1}.meta), then rerun REPL with the same --project to recover\n", e.Sha1, e.ClassName, e.Sha1)
elif cause == _REPL_SU_OPAQUE:
# refused: the key is OPAQUE in meta (no body, so no field count to compare)
stdio.printf("includes class layout: layout of %s.%s unavailable => explicitly refused (reason=criterion refused: " "key %s in meta %s.meta is declared OPAQUE (no body); meta field count=unavailable / " "inferred field count=not computed => no comparison was made). Field access on this class is refused " "(method calls are unaffected)\n", e.Sha1, e.ClassName, e.Sha1, e.Key)
else:
# refused: meta exists but has no STRUCT line for this class (LayoutCount = -1 is a "not found" sentinel)
stdio.printf("includes class layout: layout of %s.%s unavailable => explicitly refused (reason=criterion refused: " "meta %s.meta exists but has no `STRUCT ... FIELDS n / OPAQUE` record for key %s; " "meta field count=not found / inferred field count=not computed => no comparison was made). Field access on this class is " "refused (method calls are unaffected); if this class is indeed defined in that module, the meta is stale/" "incomplete, please rebuild\n", e.Sha1, e.ClassName, e.Sha1, e.Key)
def _repl_stub_warn_parent(e: ReplStubClass*, detail: str, pname: str) -> None:
"""Warn on an upstream-no-data refusal: the base layout is unavailable or unregistered."""
global ReplStubWarnLeft, ReplStubRefused, ReplStubRefusedParent
ReplStubRefused += 1
ReplStubRefusedParent += 1
if ReplStubWarnLeft <= 0: return
ReplStubWarnLeft -= 1
stdio.printf("includes class layout: layout of %s.%s unavailable => explicitly refused (reason=upstream no data: %s, " "parent=%s; parent fields and vtable slots cannot be flattened => no field count comparison was done for this class). Field access on this class is " "refused (method calls are unaffected)\n", e.Sha1, e.ClassName, detail, pname)
def _repl_stub_try(e: ReplStubClass*, mod: mlirlite.LLVMModule*) -> int:
"""Validate and register the full layout from the stub: 1=registered / 0=base pending / -1=refused."""
global ReplStubOk, ReplStubVtOk, ReplStubWarnLeft
if e is None: return -1
if e.State == _REPL_SS_OK: return 1
if e.State == _REPL_SS_REFUSED: return -1
if e.State != _REPL_SS_PENDING: return -1
pool: memhub.MemBuddy* = ReplStubPool
if pool is None:
e.State = _REPL_SS_REFUSED
return -1
# (1) base classes must be ready (not ready -> defer; refused -> this layout is untrusted)
bi: int
for bi in range(int(e.BaseCount)):
pe: ReplStubClass* = _repl_stub_find_base(e, bi)
if pe is None: return 0
if pe.State == _REPL_SS_PENDING: return 0
if pe.State == _REPL_SS_REFUSED:
e.State = _REPL_SS_REFUSED
_repl_stub_warn_parent(e, "Parent layout untrusted", pe.ClassName)
return -1
if pe.State == _REPL_SS_EMPTY and pe.OwnFail != 0:
# The base itself had field-annotation failures, so its Count is only partially parsed:
# the flattened field count is unusable, so neither compare nor allow it (M4.2: refuse).
e.State = _REPL_SS_REFUSED
_repl_stub_warn_parent(e, "Not all field types of the parent are resolved (its field count is incomplete)", pe.ClassName)
return -1
first: ReplStubClass* = None
if e.BaseCount > 0: first = _repl_stub_find_base(e, 0)
# (2) HasVTable (transcribed from ClassGenBase._detect_vtable_status)
hv: int = 0
if (e.Flags & _REPL_SF_CVTABLE) != 0: hv = 1
elif (e.Flags & _REPL_SF_NOVTABLE) != 0: hv = 1 if (e.Flags & _REPL_SF_ANYFUNC) != 0 else 0
elif first is not None:
if first.IsNoVTable != 0: hv = 1 if (e.Flags & _REPL_SF_ANYFUNC) != 0 else 0
else: hv = first.HasVt
else: hv = 1 if (e.Flags & _REPL_SF_ANYFUNC) != 0 else 0
# (3) field flattening (base fields first + own fields, deduped by short name, first wins)
cn: int = 0
for bi in range(int(e.BaseCount)):
pe: ReplStubClass* = _repl_stub_find_base(e, bi)
fi: int
for fi in range(int(pe.Count)):
if cn >= _REPL_STUB_FIELD_MAX: break
nm: str = rawptr(pe.Names[fi])
if nm is None: continue
if _repl_stub_has_name(e.Names, int(cn), nm) != 0: continue
e.Names[cn] = pe.Names[fi]
e.Types[cn] = pe.Types[fi]
cn += 1
oi: int
for oi in range(int(e.OwnCount)):
if cn >= _REPL_STUB_FIELD_MAX: break
nm2: str = rawptr(e.OwnNames[oi])
if nm2 is None: continue
if _repl_stub_has_name(e.Names, int(cn), nm2) != 0: continue
e.Names[cn] = e.OwnNames[oi]
e.Types[cn] = e.OwnTypes[oi]
cn += 1
e.Count = cn
e.HasVt = hv
# (4) layout check (reached only when both sides are usable): stub field count ==
# __vtable__ (if any) + flattened field count.
if e.LayoutCount != cn + hv:
e.State = _REPL_SS_REFUSED
_repl_stub_warn_refuse(e, "Stub field count does not match the field name list", e.LayoutCount, cn + hv)
return -1
# (5) virtual method short-name order (base virtuals first + own eligible methods in class order)
vn: int = 0
if hv == 1:
for bi in range(int(e.BaseCount)):
pe: ReplStubClass* = _repl_stub_find_base(e, bi)
vi: int
for vi in range(int(pe.VtCount)):
if vn >= _REPL_STUB_VT_MAX: break
mn: str = rawptr(pe.VtNames[vi])
if mn is None: continue
if _repl_stub_has_name(e.VtNames, vn, mn) != 0: continue
e.VtNames[vn] = pe.VtNames[vi]
vn += 1
mi: int
for mi in range(int(e.MethodCount)):
if vn >= _REPL_STUB_VT_MAX: break
mn2: str = rawptr(e.MethodNames[mi])
if mn2 is None: continue
if string.strcmp(mn2, "__init__") == 0: continue
if string.strcmp(mn2, "__before_init__") == 0: continue
if string.strcmp(mn2, "__new__") == 0: continue
if _repl_stub_has_name(e.VtNames, vn, mn2) != 0: continue
if _repl_stub_is_virtual(e, mi, first) == 0: continue
e.VtNames[vn] = e.MethodNames[mi]
vn += 1
e.VtCount = vn
e.VtTrust = 0
if hv == 1 and vn > 0:
if vn == e.VtArity: e.VtTrust = 1
elif ReplStubWarnLeft > 0:
ReplStubWarnLeft -= 1
stdio.printf("includes class layout: vtable slot count of %s.%s does not match the stub (%d vs %d) " "-- this class skips virtual dispatch (degrades to direct call, session continues)\n", e.Sha1, e.ClassName, vn, e.VtArity)
# (6) build the LLVM struct (named "%<sha1>.<ClassName>", same as the definition side)
total: int = int(cn + hv)
i8_ptr: mlirlite.LLVMType* = mlirlite.Ptr(pool, mlirlite.Int8(pool))
if i8_ptr is None: return 0
first_node: mlirlite.ParamNode* = None
prev_node: mlirlite.ParamNode* = None
if hv == 1:
nd0: mlirlite.ParamNode* = pool.alloc(mlirlite.ParamNode.__sizeof__())
if nd0 is None: return 0
string.memset(nd0, 0, mlirlite.ParamNode.__sizeof__())
nd0.Ty = i8_ptr
first_node = nd0
prev_node = nd0
fi2: int
for fi2 in range(int(cn)):
fty: mlirlite.LLVMType* = (mlirlite.LLVMType*)(e.Types[fi2])
if fty is None: continue
nd: mlirlite.ParamNode* = pool.alloc(mlirlite.ParamNode.__sizeof__())
if nd is None: continue
string.memset(nd, 0, mlirlite.ParamNode.__sizeof__())
nd.Ty = fty
if first_node is None: first_node = nd
if prev_node is not None: prev_node.Next = nd
prev_node = nd
struct_ty: mlirlite.LLVMType* = mlirlite.Struct(pool, first_node, total, e.Key)
if struct_ty is None: return 0
# (7) register (under StdlibAlloc=1 registration goes to PermPool, surviving across fragments)
entry: GenTypes.StructEntry* = GenApi.register_struct(pool, e.ClassName, struct_ty, e.Sha1)
if entry is None:
e.State = _REPL_SS_REFUSED
return -1
if mod is not None: mlirlite.module_add_named_type(mod, pool, struct_ty)
if (e.Flags & _REPL_SF_PACKED) != 0:
entry.IsPacked = 1
if mod is not None: mlirlite.module_set_named_type_packed(mod, pool, struct_ty, 1)
if hv == 1:
entry.HasVTable = 1
GenApi.add_field(pool, entry, "__vtable__", i8_ptr)
fi3: int
for fi3 in range(int(cn)):
fname: str = rawptr(e.Names[fi3])
fty3: mlirlite.LLVMType* = (mlirlite.LLVMType*)(e.Types[fi3])
if fname is None or fty3 is None: continue
GenApi.add_field(pool, entry, fname, fty3)
if e.IsNoVTable != 0: entry.IsNoVTable = 1
entry.IsOOP = e.IsOOP
entry.HasInit = e.HasInit
entry.HasNew = e.HasNew
# parent names (multiple-inheritance array; declaration order, for vtable/MRO/field-inheritance queries)
if e.BaseCount > 0:
arr: sizet* = pool.alloc(8 * e.BaseCount)
if arr is not None:
string.memset(arr, 0, 8 * e.BaseCount)
bi3: int
for bi3 in range(int(e.BaseCount)):
bnm: str = rawptr(e.Bases[bi3])
bdup: str = _repl_stub_dupstr(pool, bnm)
if bdup is not None: arr[bi3] = sizet(bdup)
entry.ParentNames = arr
entry.ParentCount = e.BaseCount
pn: str = rawptr(e.Bases[0])
entry.ParentName = _repl_stub_dupstr(pool, pn)
if hv == 1 and e.VtTrust == 1 and e.VtCount > 0:
entry.VTableMethods = e.VtNames
entry.VTableMethodCount = e.VtCount
ReplStubVtOk += 1
e.State = _REPL_SS_OK
ReplStubOk += 1
return 1
def _repl_stub_add_class(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, cd: ast.ClassDef*, sha1: str, txt: str, have_stub: int, trans: HT.Translator*, imported_modules: str, from_imports: str) -> int:
"""Collect layout elements of one vpi ClassDef and try to register it; 1=registered/deferred, 0=skipped."""
global ReplStubCount
if pool is None or cd is None or cd.name is None or ReplStubTable is None: return 0
if cd.name[0] == '\0' or ReplStubCount >= _REPL_STUB_CLS_MAX: return 0
key: str = _repl_stub_key(pool, sha1, cd.name)
if key is None: return 0
if _repl_stub_find(key) is not None: return 0
e: ReplStubClass* = pool.alloc(ReplStubClass.__sizeof__())
if e is None: return 0
string.memset(e, 0, ReplStubClass.__sizeof__())
e.Key = key
e.ClassName = _repl_stub_dupstr(pool, cd.name)
e.Sha1 = _repl_stub_dupstr(pool, sha1)
e.OwnNames = pool.alloc(8 * _REPL_STUB_FIELD_MAX)
e.OwnTypes = pool.alloc(8 * _REPL_STUB_FIELD_MAX)
e.MethodNames = pool.alloc(8 * _REPL_STUB_FIELD_MAX)
e.MethodFlags = pool.alloc(8 * _REPL_STUB_FIELD_MAX)
e.Bases = pool.alloc(8 * _REPL_STUB_FIELD_MAX)
e.Names = pool.alloc(8 * _REPL_STUB_FIELD_MAX)
e.Types = pool.alloc(8 * _REPL_STUB_FIELD_MAX)
e.VtNames = pool.alloc(8 * _REPL_STUB_VT_MAX)
if e.OwnNames is None or e.OwnTypes is None or e.MethodNames is None or e.MethodFlags is None or e.Bases is None or e.Names is None or e.Types is None or e.VtNames is None: return 0
string.memset(e.OwnNames, 0, 8 * _REPL_STUB_FIELD_MAX)
string.memset(e.OwnTypes, 0, 8 * _REPL_STUB_FIELD_MAX)
string.memset(e.MethodNames, 0, 8 * _REPL_STUB_FIELD_MAX)
string.memset(e.MethodFlags, 0, 8 * _REPL_STUB_FIELD_MAX)
string.memset(e.Bases, 0, 8 * _REPL_STUB_FIELD_MAX)
string.memset(e.Names, 0, 8 * _REPL_STUB_FIELD_MAX)
string.memset(e.Types, 0, 8 * _REPL_STUB_FIELD_MAX)
string.memset(e.VtNames, 0, 8 * _REPL_STUB_VT_MAX)
# class decorators
cf: int = _repl_stub_deco_flags(cd.decorator_list)
if (cf & 2) != 0: e.Flags = e.Flags | _REPL_SF_CVTABLE
if (cf & 1) != 0:
e.Flags = e.Flags | _REPL_SF_NOVTABLE
e.IsNoVTable = 1
if _vpi_class_has_packed(cd.decorator_list) != 0: e.Flags = e.Flags | _REPL_SF_PACKED
# real bases (reuse ClassGenBase._collect_parents: alias resolution + marker removal)
pb: sizet* = pool.alloc(8 * GenTypes.PARENT_MAX)
bcnt: int = 0
if pb is not None and GenApi._g_gen is not None:
string.memset(pb, 0, 8 * GenTypes.PARENT_MAX)
bcnt = GenApi._g_gen.ClassGenBase._collect_parents(pool, cd, pb, GenTypes.PARENT_MAX, trans)
if bcnt > 0:
if bcnt > _REPL_STUB_FIELD_MAX: bcnt = _REPL_STUB_FIELD_MAX
bi: int
for bi in range(int(bcnt)):
bnm: str = rawptr(pb[bi])
bdup: str = _repl_stub_dupstr(pool, bnm)
if bdup is not None: e.Bases[bi] = sizet(bdup)
e.BaseCount = bcnt
# scan the class body: fields (AnnAssign) + methods (FunctionDef)
children: list[ast.AST*]* = cd.children
own_fail: int = 0
if children is not None:
cn: sizet = len(children)
ci: sizet
for ci in range(int(cn)):
ch2: ast.AST* = children.get(ci)
if ch2 is None: continue
k2: int = ch2.kind()
if k2 == ast.ASTKind.AnnAssign:
if e.OwnCount >= _REPL_STUB_FIELD_MAX: continue
aa2: ast.AnnAssign* = ast.AnnAssign*(ch2)
if aa2 is None or aa2.target is None or aa2.annotation is None: continue
if aa2.target.kind() != ast.ASTKind.Name: continue
tg: ast.Name* = ast.Name*(aa2.target)
if tg.id is None or tg.id[0] == '\0': continue
fty2: mlirlite.LLVMType* = GenApi.resolve_annotation_type(pool, aa2.annotation, imported_modules, from_imports, trans, 0)
if fty2 is None:
own_fail = 1
continue
fdup: str = _repl_stub_dupstr(pool, tg.id)
if fdup is None:
own_fail = 1
continue
e.OwnNames[e.OwnCount] = sizet(fdup)
e.OwnTypes[e.OwnCount] = sizet(fty2)
e.OwnCount += 1
continue
if k2 == ast.ASTKind.FunctionDef:
fd2: ast.FunctionDef* = ast.FunctionDef*(ch2)
if fd2 is None or fd2.name is None or fd2.name[0] == '\0': continue
mf2: int = _repl_stub_deco_flags(fd2.decorator_list)
if (mf2 & 2) != 0: e.Flags = e.Flags | _REPL_SF_ANYFUNC
if string.strcmp(fd2.name, "__init__") == 0: e.HasInit = 1
elif string.strcmp(fd2.name, "__new__") == 0: e.HasNew = 1
e.IsOOP = 1
if e.MethodCount < _REPL_STUB_FIELD_MAX:
e.MethodNames[e.MethodCount] = sizet(_repl_stub_dupstr(pool, fd2.name))
e.MethodFlags[e.MethodCount] = sizet(mf2)
e.MethodCount += 1
continue
# stub metadata (LayoutCount stays at the "not found" sentinel when missing; have_stub distinguishes causes)
e.LayoutCount = _REPL_STUB_NOCOUNT_NOCLASS
if txt is not None:
_repl_meta_scan_layout(txt, key)
e.LayoutCount = ReplStubTmpCount
e.VtArity = ReplStubTmpArity
if ReplStubTmpPacked != 0: e.Flags = e.Flags | _REPL_SF_PACKED
# state decision
if own_fail != 0:
# Field-type resolution failed: defer to the existing vpi injection (which skips the class);
# OwnFail records that OwnCount is only partially parsed and unusable for subclass flattening.
e.OwnFail = 1
e.State = _REPL_SS_EMPTY
e.Count = e.OwnCount
e.Names = e.OwnNames
e.Types = e.OwnTypes
elif e.LayoutCount >= 0: e.State = _REPL_SS_PENDING
elif e.BaseCount == 0 and (e.Flags & (_REPL_SF_CVTABLE | _REPL_SF_ANYFUNC)) == 0:
# no base, no vtable -> this class's own fields are the full layout (equivalent to the existing rule)
e.State = _REPL_SS_EMPTY
e.Count = e.OwnCount
e.Names = e.OwnNames
e.Types = e.OwnTypes
else:
# No layout data -> explicitly refuse (no StructEntry). Report the causes separately:
# missing module meta (recoverable) vs OPAQUE/no STRUCT record (untrusted).
e.State = _REPL_SS_REFUSED
if have_stub == 0: _repl_stub_warn_nodata(e, _REPL_SU_NOSTUB)
elif e.LayoutCount == _REPL_STUB_NOCOUNT_OPAQUE: _repl_stub_warn_nodata(e, _REPL_SU_OPAQUE)
else: _repl_stub_warn_nodata(e, _REPL_SU_NOCLASS)
ReplStubTable[ReplStubCount] = sizet(e)
ReplStubCount += 1
if e.State == _REPL_SS_PENDING: _repl_stub_try(e, mod)
return 1
def _repl_stub_collect_module(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, ch: list[ast.AST*]*, n: sizet, trans: HT.Translator*, src_sha1: str, imported_modules: str, from_imports: str) -> int:
"""(REPL) Collect layout elements for the current module's classes and try to register them."""
global ReplStubNoStub, ReplStubWarnLeft
if ReplStubLayoutMode == 0 or pool is None or ch is None: return 0
if src_sha1 is None or src_sha1[0] == '\0' or ReplStubTable is None: return 0
# layout metadata ({sha1}.meta; if missing, classes with bases/vtables are refused as "missing data")
txt: str = None
sp: str = _repl_stub_path(src_sha1, "meta")
if sp is not None:
txt = _repl_stub_readtext(sp)
stdlib.free(sp)
have_stub: int = 0
if txt is not None: have_stub = 1
else:
ReplStubNoStub += 1
if ReplStubWarnLeft > 0:
ReplStubWarnLeft -= 1
stdio.printf("includes class layout: module %s has no .meta stub (data missing) -- this module " "classes with parent/vtable will each be explicitly refused as data missing (layout metadata cannot be read => no " "field count to compare, not a field count mismatch). First run batch processing once in the same project to generate stubs, " "then reuse them with `--project .\\project.vpj --repl` (in the ViperC directory)\n", src_sha1)
cnt: int = 0
i: sizet
for i in range(int(n)):
child: ast.AST* = ch.get(i)
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
# Generic template classes are skipped (same as _inject_class_struct_declare / pass 1A),
# but the owning module must be registered so generic subscripts can build type placeholders.
tp: list[str]* = cd.type_params
if tp is not None and len(tp) > 0:
StubMergerStore.RegisterGenericOwner(cd.name, src_sha1)
continue
# REnum classes: layout {i32 tag, payload} is computed by ClassGenEnum, so skip them here
if _vpi_class_is_renum(cd) != 0: continue
if _repl_stub_add_class(pool, mod, cd, src_sha1, txt, have_stub, trans, imported_modules, from_imports) != 0:
cnt += 1
if txt is not None: stdlib.free(txt)
return cnt
def ReplStubLayoutFinish() -> int:
"""Finish: converge remaining class registrations, print a summary, and return the registered count."""
global ReplStubRefused, ReplStubWarnLeft
if ReplStubLayoutMode == 0: return 0
rounds: int
for rounds in range(int(_REPL_STUB_FINISH_PASSES)):
prog: int = 0
i: int
for i in range(int(ReplStubCount)):
e: ReplStubClass* = _repl_stub_at(int(i))
if e is None or e.State != _REPL_SS_PENDING: continue
if _repl_stub_try(e, None) != 0: prog += 1
if prog == 0: break
# still not ready -> the base was never registered (missing stub/refused): explicitly refuse
i2: int
for i2 in range(int(ReplStubCount)):
e2: ReplStubClass* = _repl_stub_at(i2)
if e2 is None or e2.State != _REPL_SS_PENDING: continue
e2.State = _REPL_SS_REFUSED
# find the still-unready base class name (to identify the upstream)
pbad: str = "(not located)"
bi2: int
for bi2 in range(int(e2.BaseCount)):
peb: ReplStubClass* = _repl_stub_find_base(e2, bi2)
if peb is not None and peb.State == _REPL_SS_OK: continue
if peb is not None and peb.ClassName is not None: pbad = peb.ClassName
break
_repl_stub_warn_parent(e2, "Parent not registered in the session or its layout was refused", pbad)
stdio.printf("includes class layout: %d complete layouts (%d with a usable vtable index) / " "%d refused / %d modules missing layout metadata\n", ReplStubOk, ReplStubVtOk, ReplStubRefused, ReplStubNoStub)
stdio.printf(" refusal reason distribution (self-witnessing data source): data missing-module has no {sha1}.meta %d classes / " "criterion refused-meta has no such class %d / criterion refused-meta is opaque %d / " "upstream no data-parent unavailable %d\n", ReplStubRefusedNoStub, ReplStubRefusedNoClass, ReplStubRefusedOpaque, ReplStubRefusedParent)
if ReplStubRefused > 0 or ReplStubNoStub > 0:
stdio.printf(" Note: field access on refused classes reports class not registered/attribute access failed " "(it does not silently access at a wrong offset); method calls are unaffected.\n")
if ReplStubRefusedNoStub > 0:
stdio.printf(" Of these, the %d classes with data missing simply failed to read the module layout metadata (the session temp directory %s " "is missing these {sha1}.meta): only batch processing writes meta to disk (the REPL branch runs before Phase1 " "and returns, so --repl itself does not generate stubs). Fix: run batch processing once in the same project " "(ViperC directory: .\\tpv_build\\output\\viperc.exe --project " ".\\project.vpj; add --rebuild-includes when includes changed), then " "reuse its temp directory with --project .\\project.vpj --repl\n", ReplStubRefusedNoStub, ReplStubTempDir)
if ReplStubRefusedNoClass + ReplStubRefusedOpaque > 0:
stdio.printf(" Of these, the %d classes with criterion refused: data exists but is untrusted (the stub has no field count for this class, " "or it is declared opaque); per M4.2 none are allowed through -- please confirm whether this class should have been registered as a struct " "in the first place: if it should be registered as a struct but is not found in the stub => the stub is stale/incomplete (rebuild includes " "stubs)\n", ReplStubRefusedNoClass + ReplStubRefusedOpaque)
return ReplStubOk
def ReplStubLayoutEnd() -> None:
"""Disable stub layout restore (the batch path is unaffected afterwards)."""
global ReplStubLayoutMode
ReplStubLayoutMode = 0