feat(vpc): binary .vpb stub companion and str->char* migration in call gen

- add VpiStub: fixed-layout blob written next to every .vpi (dual write at all
  writers, Ensure() backfills a missing or stale companion on cache reuse)
- serve arity/signature queries for top-level functions from the blob, with a
  silent fallback to the text scanner (methods, varargs, duplicates, corrupt file)
- prefer the injected declare over the text arity in the cross-module check
- CDefine/type text indexes and in-place SSA renumbering and bulk appends
- WIP: char* conversion in CallGenBase and CallGenLeafKit
This commit is contained in:
TermiNexus committed 2026-10-31 16:25:18 +08:00
1 parent 451d89b39a
commit 4bf04c2452
15 files changed
+1697 -463

No files matched your search

+3 -1
View File
@@ -21,6 +21,7 @@ import lib.core.Gen.Translator as HandlesTranslator
import lib.core.Gen.DebugInfoGen as DebugInfoGen
import lib.Stub.StubMerger as StubMerger
import lib.Stub.StubGen.Converter as StubConverter
import lib.Stub.StubGen.VpiStub as VpiStub
import lib.Projectrans.Config as Config
import lib.core.Gen.Generator as GenGenerator
import lib.core.Gen.Expr.ExprGenCls as ExprGenCls
@@ -127,7 +128,7 @@ def _phaseb_ircore_active() -> int:
if _ircore_ready != 0: return 1
if _ircore_load() == 0:
_phaseb_backend = PHASEB_BACKEND_LEGACY
VLogger.warning("ircore.dll is unavailable (or its LLVM runtime could not be loaded); PhaseB falls back to the legacy mlir-opt/mlir-translate/llc flow", "PhaseB")
VLogger.warning("ircore.dll is unavailable (or its LLVM runtime could not be loaded); PhaseB falls back to the legacy mlir-opt/mlir-translate/llc flow (3 child processes per module) -- put ircore.dll next to viperc.exe (or on PATH) to use the in-process backend", "PhaseB")
return 0
_ircore_ready = 1
return 1
@@ -347,6 +348,7 @@ def TranslateFileGetTrans(mb: memhub.MemBuddy*, file_path: str, sha1_val: str, c
if not pf.closed:
pf.write(pyi_buf, pyi_pos)
pf.close()
VpiStub.Emit(pyi_path, pyi_buf, pyi_pos, sha1_val)
stdlib.free(pyi_path)
stdlib.free(pyi_buf)
tr: HandlesTranslator.Translator* = mb.alloc(HandlesTranslator.Translator.__sizeof__())
+5
View File
@@ -16,6 +16,7 @@ import lib.core.CompilerFactory as CompilerFactory
import lib.Phase.BuildPipeline as BuildPipeline
import lib.Projectrans.Config as Config
import lib.Stub.StubGen.Converter as StubConverter
import lib.Stub.StubGen.VpiStub as VpiStub
import lib.Stub.StubMerger.StubMergerBuild as StubMergerBuild
import lib.Stub.StubMerger.StubMergerCollect as StubMergerCollect
import lib.Stub.StubMerger.StubMergerIR as StubMergerIR
@@ -589,6 +590,7 @@ def RunPhase1(mb: memhub.MemBuddy*, includes_dir: str, temp_dir: str, log: VLogg
vf_pv: io.File* = io.File(vpi_path_pv, io.MODE.R)
if not vf_pv.closed:
vf_pv.close()
VpiStub.Ensure(vpi_path_pv, sha1_pv)
stdlib.free(vpi_path_pv)
continue
stdlib.free(vpi_path_pv)
@@ -646,6 +648,7 @@ def RunPhase1(mb: memhub.MemBuddy*, includes_dir: str, temp_dir: str, log: VLogg
if not pf_pv.closed:
pf_pv.write(pyi_buf_pv, pyi_pos_pv)
pf_pv.close()
VpiStub.Emit(pyi_path_pv, pyi_buf_pv, pyi_pos_pv, sha1_pv)
stdlib.free(pyi_path_pv)
stdlib.free(pyi_buf_pv)
else:
@@ -868,6 +871,7 @@ def RunPhase1(mb: memhub.MemBuddy*, includes_dir: str, temp_dir: str, log: VLogg
vpi_exists_1b: int = 0
if not vf_1b.closed:
vf_1b.close()
VpiStub.Ensure(vpi_path_1b, sha1)
vpi_exists_1b = 1
if vpi_exists_1b == 0:
pyi_buf: bytes = stdlib.malloc(Phase1.PYI_BUF_SIZE)
@@ -878,6 +882,7 @@ def RunPhase1(mb: memhub.MemBuddy*, includes_dir: str, temp_dir: str, log: VLogg
if not pf_1b.closed:
pf_1b.write(pyi_buf, pyi_pos)
pf_1b.close()
VpiStub.Emit(vpi_path_1b, pyi_buf, pyi_pos, sha1)
stdlib.free(pyi_buf)
stdlib.free(vpi_path_1b)
+3
View File
@@ -29,6 +29,7 @@ import lib.Stub.StubMerger.StubMergerStore as StubMergerStore
import lib.Stub.StubMerger.StubMergerTopo as StubMergerTopo
import lib.Stub.StubGen.Converter as StubConverter
import lib.Stub.StubGen.VpiReader as VpiReader
import lib.Stub.StubGen.VpiStub as VpiStub
# Global mbuddy pointer
_mbuddy: memhub.MemBuddy*
@@ -629,6 +630,7 @@ def RunMultiFileProject(mb: memhub.MemBuddy*, source_dir: str, temp_dir: str, ou
vf_vp2: io.File* = io.File(vpi_path_vp2, io.MODE.R)
if not vf_vp2.closed:
vf_vp2.close()
VpiStub.Ensure(vpi_path_vp2, ent_vp2.Sha1)
vpi_skipped_p2 += 1
stdlib.free(vpi_path_vp2)
continue
@@ -699,6 +701,7 @@ def RunMultiFileProject(mb: memhub.MemBuddy*, source_dir: str, temp_dir: str, ou
VLogger.error(f"[Phase A-pre-vpi] {ent_vp2.Path} | cannot open the vpi for writing: {pyi_path_vp2}", "PhaseA")
pf_vp2.write(pyi_buf_vp2, pyi_pos_vp2)
pf_vp2.close()
VpiStub.Emit(pyi_path_vp2, pyi_buf_vp2, pyi_pos_vp2, ent_vp2.Sha1)
vpi_written_p2 += 1
stdlib.free(pyi_path_vp2)
stdlib.free(pyi_buf_vp2)
+346 -20
View File
@@ -14,6 +14,7 @@ import io
import lib.core.VLogger as VLogger
import lib.core.Gen.Translator as HT
import lib.Stub.StubMerger.StubMergerStore as StubMergerStore
import lib.Stub.StubGen.VpiStub as VpiStub
import lib.Projectrans.Config as Config
@@ -1315,14 +1316,71 @@ def _lookup_vpi_escape_dir(src_sha1: str, cls_name: str, method_name: str, which
# (现场:逃逸摘要只在 _ensure_vpi_ret_annot_loaded 里喂,而 stub 注入加载点没喂,
# 前者开头又有"本模块 ret 注解已缓存即 return 1"的短路,于是跨模块逃逸查询永久查空,
# EscapeXmodTest 漏检)。
# --- per-sha1 annotation scan guard ---------------------------------------
# _cache_vpi_annots_text walks the whole .vpi five times (one pass per annotation table) and
# ParseVpiIntoModule runs it once per (dependent module, dependency): 1,020 scans for ~67
# distinct .vpi files, 51 s of a profiled build. The tables are keyed by "{sha1}.{name}" and
# are never cleared, so a repeated scan only rewrites identical entries -- skip it.
define VPI_ANNOT_SEEN_SLOTS = 256
_vpi_annots_seen_tab: rawptr = None # str[slots] owned sha1 copies
def _vpi_annots_hash(sha1: str) -> uint64:
h: uint32 = uint32(2166136261)
n: sizet = string.strlen(sha1)
i: sizet = 0
while i < n:
h = (h ^ uint32(uint8(sha1[i]))) * uint32(16777619)
i += 1
return uint64(h) | uint64(1)
def _vpi_annots_seen_slot(sha1: str) -> str*:
global _vpi_annots_seen_tab
if sha1 is None: return None
if _vpi_annots_seen_tab is None:
_vpi_annots_seen_tab = stdlib.malloc(VPI_ANNOT_SEEN_SLOTS * 8)
if _vpi_annots_seen_tab is None: return None
string.memset(_vpi_annots_seen_tab, 0, VPI_ANNOT_SEEN_SLOTS * 8)
h: int = int(_vpi_annots_hash(sha1) & uint64(VPI_ANNOT_SEEN_SLOTS - 1))
return (str*)(uint64(_vpi_annots_seen_tab) + uint64(h) * 8)
# _vpi_annots_seen - 1 when this sha1 was already scanned.
def _vpi_annots_seen(sha1: str) -> int:
sp: str* = _vpi_annots_seen_slot(sha1)
if sp is None: return 0
if sp[0] is not None and string.strcmp(sp[0], sha1) == 0: return 1
return 0
# _vpi_annots_mark - record the sha1 as scanned. Called only after every table has been
# filled, so a concurrent reader can never skip a half-scanned .vpi.
def _vpi_annots_mark(sha1: str) -> None:
sp: str* = _vpi_annots_seen_slot(sha1)
if sp is None: return
if sp[0] is not None and string.strcmp(sp[0], sha1) == 0: return
n: sizet = string.strlen(sha1)
cp: str = stdlib.malloc(n + 1)
if cp is None: return
string.memcpy(cp, sha1, n)
cp[n] = '\0'
if sp[0] is not None: stdlib.free(sp[0])
sp[0] = cp
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
# Repeated scans of the same .vpi only rewrite identical cache entries.
if _vpi_annots_seen(src_sha1) != 0: return 1
_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)
_vpi_annots_mark(src_sha1)
return 1
@@ -2085,6 +2143,119 @@ def _resolve_vpi_ret_annot_str(pool: memhub.MemBuddy*, ret_annot: str, i32_ty: m
# .vpi read buffer size (256KB, matching PYI_BUF_SIZE)
VPI_READ_BUF_SIZE: sizet = 262144
# --- .vpi text cache ---------------------------------------------------------
# LookupSignDefaultsByName is called hundreds of times per module and used to re-open and
# re-read the whole {temp}/{sha1}.vpi for every query (4,592 reads in a profiled compiler
# build, 9.3 ms each). sha1 is the module content hash and its .vpi is written once per
# build (Phase1 regenerates it only when missing), so caching the text per sha1 cannot go
# stale inside a run. Consumers get a private copy, so they keep owning/freeing their
# buffer exactly as before.
define VPI_CACHE_SLOTS = 32
_vpi_cache_sha1: rawptr = None
_vpi_cache_buf: rawptr = None
_vpi_cache_len: rawptr = None
_vpi_cache_used: int = 0
_vpi_cache_next: int = 0
def _vpi_cache_init() -> int:
global _vpi_cache_sha1, _vpi_cache_buf, _vpi_cache_len
if _vpi_cache_sha1 is not None: return 1
sh: rawptr = stdlib.malloc(VPI_CACHE_SLOTS * 8)
bf: rawptr = stdlib.malloc(VPI_CACHE_SLOTS * 8)
ln: rawptr = stdlib.malloc(VPI_CACHE_SLOTS * 8)
if sh is None or bf is None or ln is None:
if sh is not None: stdlib.free(sh)
if bf is not None: stdlib.free(bf)
if ln is not None: stdlib.free(ln)
return 0
string.memset(sh, 0, VPI_CACHE_SLOTS * 8)
string.memset(bf, 0, VPI_CACHE_SLOTS * 8)
string.memset(ln, 0, VPI_CACHE_SLOTS * 8)
_vpi_cache_sha1 = sh
_vpi_cache_buf = bf
_vpi_cache_len = ln
return 1
def _vpi_cache_put(sha1: str, src: bytes, n: sizet) -> int:
"""Store a private copy of [src, src+n) under sha1 (ring eviction); 0 = not cached."""
global _vpi_cache_used, _vpi_cache_next
if _vpi_cache_init() == 0: return 0
slot: int = 0
if _vpi_cache_used < VPI_CACHE_SLOTS:
slot = _vpi_cache_used
_vpi_cache_used += 1
else:
slot = _vpi_cache_next
_vpi_cache_next = (_vpi_cache_next + 1) % VPI_CACHE_SLOTS
old_s: str* = (str*)(uint64(_vpi_cache_sha1) + uint64(slot) * 8)
old_b: bytes* = (bytes*)(uint64(_vpi_cache_buf) + uint64(slot) * 8)
if old_s[0] is not None: stdlib.free(old_s[0])
if old_b[0] is not None: stdlib.free(old_b[0])
old_s[0] = None
old_b[0] = None
sn: sizet = string.strlen(sha1)
nm: str = stdlib.malloc(sn + 1)
cp: bytes = stdlib.malloc(n + 1)
if nm is None or cp is None:
if nm is not None: stdlib.free(nm)
if cp is not None: stdlib.free(cp)
return 0
string.memcpy(nm, sha1, sn)
nm[sn] = '\0'
if n > 0: string.memcpy(cp, src, n)
cp[n] = '\0'
sp: str* = (str*)(uint64(_vpi_cache_sha1) + uint64(slot) * 8)
bp: bytes* = (bytes*)(uint64(_vpi_cache_buf) + uint64(slot) * 8)
lp: sizet* = (sizet*)(uint64(_vpi_cache_len) + uint64(slot) * 8)
sp[0] = nm
bp[0] = cp
lp[0] = n
return 1
# _vpi_read_cached - contents of {temp}/{sha1}.vpi (NUL-terminated, malloc'd: caller frees).
def _vpi_read_cached(sha1: str) -> bytes:
if sha1 is None: return None
if _vpi_cache_init() == 0: return None
i: int = 0
while i < _vpi_cache_used:
sp: str* = (str*)(uint64(_vpi_cache_sha1) + uint64(i) * 8)
if sp[0] is not None and string.strcmp(sp[0], sha1) == 0:
bp: bytes* = (bytes*)(uint64(_vpi_cache_buf) + uint64(i) * 8)
lp: sizet* = (sizet*)(uint64(_vpi_cache_len) + uint64(i) * 8)
n: sizet = lp[0]
cp: bytes = stdlib.malloc(n + 1)
if cp is None: return None
if n > 0 and bp[0] is not None: string.memcpy(cp, bp[0], n)
cp[n] = '\0'
return cp
i += 1
# Miss: read once, then keep a copy for the next queries.
temp_dir: str = GenApi.get_temp_dir()
if temp_dir is None: return None
fpath: str = _vpi_build_path(temp_dir, sha1, ".vpi")
if fpath is None: return None
f: io.File* = io.File(fpath, io.MODE.R)
stdlib.free(fpath)
if f.closed: return None
buf: bytes = stdlib.malloc(VPI_READ_BUF_SIZE)
if buf is None:
f.close()
return None
nrd: LONG = f.read_all(buf, VPI_READ_BUF_SIZE)
f.close()
if nrd <= 0:
stdlib.free(buf)
return None
n: sizet = sizet(nrd)
if n >= VPI_READ_BUF_SIZE: n = VPI_READ_BUF_SIZE - 1
buf[n] = '\0'
_vpi_cache_put(sha1, buf, n)
return buf
# 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 DefineT shadowing.
SHA1_HEX_LEN: sizet = 16
@@ -2232,6 +2403,7 @@ def _vpi_register_typedef_closure(pool: memhub.MemBuddy*, import_mods: str, temp
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
@@ -2857,6 +3029,127 @@ def _VpiParamCountAndVararg(vbuf: bytes, ps: sizet, pe: sizet) -> int:
# LookupSignDefaultsByName - 直接在定义模块 .vpi 文本里查一个函数/方法的缺省参数快照
# 与 PreRegisterVpiDefaults 的区别:不登记进全局表,只按名字现查现用(表键不可靠时的兜底)。
# 返回 (参数个数 << 8) | 缺省值个数;-1 = 未找到或有不可重建的缺省值。
# --- signature range cache --------------------------------------------------
# LookupSignDefaultsByName re-scanned the whole .vpi text per query (one
# _vpi_class_name_offset + _vpi_func_name_offset per line, ~220 calls per query):
# 4,592 queries / 60 s in a profiled build. The resolved parameter-list range depends only
# on (sha1, name) -- sha1 is the module content hash and its .vpi is immutable in a run --
# so cache it and let a repeat query parse just that one list. Misses are cached as well
# (an ambiguous/absent name costs the same full scan).
define VPI_SIG_CACHE_SLOTS = 1024
_vpi_sig_sha1: rawptr = None # str[slots] owned sha1 copy
_vpi_sig_name: rawptr = None # str[slots] owned name copy
_vpi_sig_ps: rawptr = None # int[slots] '(' offset in the .vpi text
_vpi_sig_pe: rawptr = None # int[slots] ')' offset
_vpi_sig_meth: rawptr = None # int[slots] method flag (indent == 4)
_vpi_sig_flag: rawptr = None # int[slots] 0=empty 1=found 2=not found/ambiguous
def _vpi_sig_hash(sha1: str, name: str) -> uint64:
h: uint32 = uint32(2166136261)
n: sizet = string.strlen(sha1)
i: sizet = 0
while i < n:
h = (h ^ uint32(uint8(sha1[i]))) * uint32(16777619)
i += 1
h = (h ^ uint32(46)) * uint32(16777619)
n = string.strlen(name)
i = 0
while i < n:
h = (h ^ uint32(uint8(name[i]))) * uint32(16777619)
i += 1
return uint64(h) | uint64(1)
def _vpi_sig_init() -> int:
global _vpi_sig_sha1, _vpi_sig_name, _vpi_sig_ps, _vpi_sig_pe, _vpi_sig_meth, _vpi_sig_flag
if _vpi_sig_flag is not None: return 1
a: rawptr = stdlib.malloc(VPI_SIG_CACHE_SLOTS * 8)
b: rawptr = stdlib.malloc(VPI_SIG_CACHE_SLOTS * 8)
c: rawptr = stdlib.malloc(VPI_SIG_CACHE_SLOTS * 4)
d: rawptr = stdlib.malloc(VPI_SIG_CACHE_SLOTS * 4)
e: rawptr = stdlib.malloc(VPI_SIG_CACHE_SLOTS * 4)
f: rawptr = stdlib.malloc(VPI_SIG_CACHE_SLOTS * 4)
if a is None or b is None or c is None or d is None or e is None or f is None:
if a is not None: stdlib.free(a)
if b is not None: stdlib.free(b)
if c is not None: stdlib.free(c)
if d is not None: stdlib.free(d)
if e is not None: stdlib.free(e)
if f is not None: stdlib.free(f)
return 0
string.memset(a, 0, VPI_SIG_CACHE_SLOTS * 8)
string.memset(b, 0, VPI_SIG_CACHE_SLOTS * 8)
string.memset(c, 0, VPI_SIG_CACHE_SLOTS * 4)
string.memset(d, 0, VPI_SIG_CACHE_SLOTS * 4)
string.memset(e, 0, VPI_SIG_CACHE_SLOTS * 4)
string.memset(f, 0, VPI_SIG_CACHE_SLOTS * 4)
_vpi_sig_sha1 = a
_vpi_sig_name = b
_vpi_sig_ps = c
_vpi_sig_pe = d
_vpi_sig_meth = e
_vpi_sig_flag = f
return 1
# _vpi_sig_get - 0 = hit (ps/pe/meth filled), 1 = known miss, -1 = not cached.
def _vpi_sig_get(sha1: str, name: str, ps: int*, pe: int*, meth: int*) -> int:
if sha1 is None or name is None or _vpi_sig_flag is None: return -1
h: int = int(_vpi_sig_hash(sha1, name) & uint64(VPI_SIG_CACHE_SLOTS - 1))
sp: str* = (str*)(uint64(_vpi_sig_sha1) + uint64(h) * 8)
if sp[0] is None or string.strcmp(sp[0], sha1) != 0: return -1
np: str* = (str*)(uint64(_vpi_sig_name) + uint64(h) * 8)
if np[0] is None or string.strcmp(np[0], name) != 0: return -1
fp: int* = (int*)(uint64(_vpi_sig_flag) + uint64(h) * 4)
st: int = int(fp[0])
if st == 2: return 1
if st != 1: return -1
if ps is not None:
xp: int* = (int*)(uint64(_vpi_sig_ps) + uint64(h) * 4)
ps[0] = int(xp[0])
if pe is not None:
yp: int* = (int*)(uint64(_vpi_sig_pe) + uint64(h) * 4)
pe[0] = int(yp[0])
if meth is not None:
mp: int* = (int*)(uint64(_vpi_sig_meth) + uint64(h) * 4)
meth[0] = int(mp[0])
return 0
def _vpi_sig_put(sha1: str, name: str, found: int, ps: int, pe: int, meth: int) -> None:
if sha1 is None or name is None: return
if _vpi_sig_init() == 0: return
h: int = int(_vpi_sig_hash(sha1, name) & uint64(VPI_SIG_CACHE_SLOTS - 1))
sn: sizet = string.strlen(sha1)
nn: sizet = string.strlen(name)
a: str = stdlib.malloc(sn + 1)
b: str = stdlib.malloc(nn + 1)
if a is None or b is None:
if a is not None: stdlib.free(a)
if b is not None: stdlib.free(b)
return
string.memcpy(a, sha1, sn)
a[sn] = '\0'
string.memcpy(b, name, nn)
b[nn] = '\0'
sp: str* = (str*)(uint64(_vpi_sig_sha1) + uint64(h) * 8)
np: str* = (str*)(uint64(_vpi_sig_name) + uint64(h) * 8)
if sp[0] is not None: stdlib.free(sp[0])
if np[0] is not None: stdlib.free(np[0])
sp[0] = a
np[0] = b
xp: int* = (int*)(uint64(_vpi_sig_ps) + uint64(h) * 4)
yp: int* = (int*)(uint64(_vpi_sig_pe) + uint64(h) * 4)
mp: int* = (int*)(uint64(_vpi_sig_meth) + uint64(h) * 4)
fp: int* = (int*)(uint64(_vpi_sig_flag) + uint64(h) * 4)
xp[0] = int(ps)
yp[0] = int(pe)
mp[0] = int(meth)
fp[0] = 1 if found != 0 else 2
def LookupSignDefaultsByName(pool: memhub.MemBuddy*, sha1: str, name: str, kinds: int*, ints: int64*, strs: sizet*, parse_defaults: int = 1) -> int:
"""在 {temp}/{sha1}.vpi 里按名字查缺省参数;-1 = 未找到
@@ -2864,23 +3157,14 @@ def LookupSignDefaultsByName(pool: memhub.MemBuddy*, sha1: str, name: str, kinds
if pool is None or sha1 is None or name is None: return -1
if parse_defaults != 0:
if kinds is None or ints is None or strs is None: return -1
temp_dir: str = GenApi.get_temp_dir()
if temp_dir is None: return -1
fpath: str = _vpi_build_path(temp_dir, sha1, ".vpi")
if fpath is None: return -1
vf: io.File* = io.File(fpath, io.MODE.R)
stdlib.free(fpath)
if vf.closed: return -1
vbuf: bytes = stdlib.malloc(VPI_READ_BUF_SIZE)
if vbuf is None:
vf.close()
return -1
nrd: LONG = vf.read_all(vbuf, VPI_READ_BUF_SIZE)
vf.close()
if nrd <= 0:
stdlib.free(vbuf)
return -1
if nrd < VPI_READ_BUF_SIZE: vbuf[nrd] = 0
if parse_defaults == 0:
fast_arity: int = _vpb_param_count(sha1, name)
if fast_arity >= 0: return fast_arity << 8
# Hot path: hundreds of queries per module. Serve the .vpi text from the per-sha1 cache
# instead of re-opening and re-reading the file every time (_vpi_read_cached returns a
# private copy, so the stdlib.free(vbuf) below is still the owner).
vbuf: bytes = _vpi_read_cached(sha1)
if vbuf is None: return -1
buf_len: sizet = string.strlen(vbuf)
name_len: sizet = string.strlen(name)
cls: bytes = None
@@ -2891,6 +3175,28 @@ def LookupSignDefaultsByName(pool: memhub.MemBuddy*, sha1: str, name: str, kinds
hit_ps: sizet = 0
hit_pe: sizet = 0
hit_method: int = 0
# Lookaside: the scan result only depends on (sha1, name).
sig_ps: int = 0
sig_pe: int = 0
sig_meth: int = 0
sig_st: int = _vpi_sig_get(sha1, name, &sig_ps, &sig_pe, &sig_meth)
if sig_st == 1:
stdlib.free(vbuf)
return -1
if sig_st == 0:
# Self-check: a usable entry must still bracket a parameter list in this text.
# (The .vpi for a sha1 is immutable in a run, but a cache/probe mismatch must never
# turn into a wrong arity, so anything that does not line up is rescanned.)
sig_ok: int = 0
if sig_ps > 0 and sig_pe > sig_ps and sizet(sig_pe) < buf_len:
if vbuf[sizet(sig_ps) - 1] == '(' and vbuf[sizet(sig_pe)] == ')': sig_ok = 1
if sig_ok == 0: sig_st = -1
if sig_st == 0:
hits = 1
hit_ps = sizet(sig_ps)
hit_pe = sizet(sig_pe)
hit_method = sig_meth
pos = buf_len # skip the full-text scan
while pos < buf_len:
ls: sizet = pos
le: sizet = pos
@@ -2950,6 +3256,11 @@ def LookupSignDefaultsByName(pool: memhub.MemBuddy*, sha1: str, name: str, kinds
hit_method = 0
if indent == 4: hit_method = 1
if cls is not None: stdlib.free(cls)
if sig_st < 0:
if hits == 1:
_vpi_sig_put(sha1, name, 1, int(hit_ps), int(hit_pe), hit_method)
else:
_vpi_sig_put(sha1, name, 0, 0, 0, 0)
if hits != 1:
stdlib.free(vbuf)
return -1
@@ -2993,10 +3304,25 @@ def _VpiFirstParamIsSelf(buf: bytes, ps: sizet, pe: sizet) -> int:
return 0
# LookupParamCountByName - 取定义模块 .vpi 里某函数/方法的形参个数(方法不含 self)
# 返回 -1 表示未找到或签名含可变参数(调用方应跳过 arity 校验)。
# _vpb_param_count - arity of a top-level function from the binary companion.
# Methods stay on the text scanner (the blob does not index them), and -1 makes the
# caller fall back for varargs, duplicates and missing records.
def _vpb_param_count(sha1: str, name: str) -> int:
if sha1 is None or name is None: return -1
if string.strchr(name, '.') is not None: return -1
st: VpiStub.VpiStub* = VpiStub.Load(sha1, GenApi.get_temp_dir())
if st is None: return -1
rec: VpiStub.VpiFuncRec* = st.Find(name)
if rec is None: return -1
# Varargs have no fixed arity; the text scanner reports -1 for them as well.
if rec.VarArg != 0: return -1
return rec.Arity
def LookupParamCountByName(pool: memhub.MemBuddy*, sha1: str, name: str) -> int:
"""按名字取形参个数(方法不含 self);-1=未找到/含可变参数"""
"""Arity of a named function (self excluded); -1 when unknown or varargs."""
fast: int = _vpb_param_count(sha1, name)
if fast >= 0: return fast
enc: int = LookupSignDefaultsByName(pool, sha1, name, None, None, None, 0)
if enc < 0: return -1
return enc >> 8
+693
View File
@@ -0,0 +1,693 @@
from stdint import *
import string
import stdlib
import io
import lib.Projectrans.Config as Config
# VpiStub - binary companion of the human-readable .vpi stub.
#
# The .vpi text remains the serialized interface (types, classes, enums, defines, typedefs).
# Signature queries (arity, self flag, varargs, annotation masks, return text) are served from
# this fixed-layout blob instead of rescanning the text. Records are indexed by name at load.
#
# Layout, little endian:
# "VPB1" | u32 version | u32 n_imports | imports | u32 n_funcs | funcs
# str = u32 len, bytes, NUL (so readers can use the bytes as a C string)
# import = str
# func = u8 flags (1 vararg, 2 method, 4 has wf) | 3 pad
# | i32 arity | i32 esc | i32 ret | i32 wf
# | str name | str ret_text
# | (arity + method) x (str param_name, str param_type, str param_default)
define VPB_MAGIC = 0x31504256 # bytes 'V','P','B','1' (little endian)
define VPB_VERSION = 2
define VPB_HEADER = 16
define VPB_FLAG_VARARG = 1
define VPB_FLAG_METHOD = 2
define VPB_FLAG_WF = 4
novirtual class VpiFuncRec:
Arity: int
Method: int
VarArg: int
Esc: int
Ret: int
Wf: int
Name: str
RetText: str
novirtual class VpiStub:
Sha1: str
Buf: bytes
Size: sizet
Funcs: bytes # VpiFuncRec[FuncCount]
FuncCount: int
Keys: bytes # uint64[Cap]
Vals: bytes # int32[Cap]
Cap: int
Imports: bytes # str*[ImportCount]
ImportCount: int
def Find(self, name: str) -> VpiFuncRec*:
if name is None or self.Cap <= 0: return None
mask: int = self.Cap - 1
h: int = int(_vpb_hash(name) & uint64(mask))
kp: uint64* = (uint64*)(uint64(self.Keys) + uint64(h) * 8)
probe: int = 0
while probe < self.Cap:
if kp[0] == uint64(0): return None
vp: int32* = (int32*)(uint64(self.Vals) + uint64(h) * 4)
idx: int = int(vp[0]) - 1
if idx >= 0:
rec: VpiFuncRec* = (VpiFuncRec*)(uint64(self.Funcs) + uint64(idx) * VpiFuncRec.__sizeof__())
if rec.Name is not None and string.strcmp(rec.Name, name) == 0: return rec
h = (h + 1) & mask
probe += 1
return None
def ImportAt(self, i: int) -> str:
if i < 0 or i >= self.ImportCount: return None
sp: str* = (str*)(uint64(self.Imports) + uint64(i) * 8)
return sp[0]
# --- primitive encoding ------------------------------------------------------
def _vpb_hash(name: str) -> uint64:
h: uint32 = uint32(2166136261)
if name is None: return 1
n: sizet = string.strlen(name)
i: sizet = 0
while i < n:
h = (h ^ uint32(uint8(name[i]))) * uint32(16777619)
i += 1
return uint64(h) | uint64(1)
def _vpb_u32(buf: bytes, off: sizet) -> uint32:
p: uint8* = (uint8*)(uint64(buf) + uint64(off))
return uint32(p[0]) | (uint32(p[1]) << 8) | (uint32(p[2]) << 16) | (uint32(p[3]) << 24)
def _vpb_i32(buf: bytes, off: sizet) -> int:
return int(int32(_vpb_u32(buf, off)))
def _vpb_put32(buf: bytes, off: sizet, v: uint32) -> None:
p: uint8* = (uint8*)(uint64(buf) + uint64(off))
p[0] = uint8(v & uint32(0xFF))
p[1] = uint8((v >> 8) & uint32(0xFF))
p[2] = uint8((v >> 16) & uint32(0xFF))
p[3] = uint8((v >> 24) & uint32(0xFF))
def _vpb_put_str(buf: bytes, off: sizet, s: str) -> sizet:
n: sizet = 0
if s is not None: n = string.strlen(s)
_vpb_put32(buf, off, uint32(n))
if n > 0: string.memcpy(buf + off + 4, s, n)
p: uint8* = (uint8*)(uint64(buf) + uint64(off) + 4 + uint64(n))
p[0] = uint8(0)
return off + 5 + n
def _vpb_read_str(buf: bytes, size: sizet, off: sizet, out_len: sizet*) -> str:
n: sizet = 0
if off + 4 <= size:
n = sizet(_vpb_u32(buf, off))
if off + 5 + n > size: n = 0 if size < off + 5 else size - off - 5
if out_len is not None: out_len[0] = n
if n == 0: return None
return (str)(uint64(buf) + uint64(off) + 4)
# --- loading -----------------------------------------------------------------
def _vpb_alloc(n: sizet) -> bytes:
if n <= 0: return None
b: bytes = stdlib.malloc(n)
if b is not None: string.memset(b, 0, n)
return b
# Parse - decode a blob into records plus a name index. Returns None on a format mismatch.
def Parse(sha1: str, buf: bytes, size: sizet, keep: int) -> VpiStub*:
if buf is None or size < VPB_HEADER: return None
if _vpb_u32(buf, 0) != uint32(VPB_MAGIC): return None
if _vpb_u32(buf, 4) != uint32(VPB_VERSION): return None
st: VpiStub* = stdlib.malloc(VpiStub.__sizeof__())
if st is None: return None
string.memset(st, 0, VpiStub.__sizeof__())
st.Sha1 = sha1
st.Buf = buf
st.Size = size
off: sizet = 8
ni: int = int(_vpb_u32(buf, off))
off += 4
if ni > 65536: return None
st.ImportCount = ni
if ni > 0:
arr: bytes = _vpb_alloc(sizet(ni) * 8)
if arr is None: return None
st.Imports = arr
i: int = 0
while i < ni:
l: sizet = 0
s: str = _vpb_read_str(buf, size, off, &l)
off = off + 5 + l
sp: str* = (str*)(uint64(arr) + uint64(i) * 8)
sp[0] = s
i += 1
nf: int = int(_vpb_u32(buf, off))
off += 4
if nf > 65536: return None
st.FuncCount = nf
if nf > 0:
recs: bytes = _vpb_alloc(sizet(nf) * VpiFuncRec.__sizeof__())
if recs is None: return None
st.Funcs = recs
i = 0
while i < nf:
rec: VpiFuncRec* = (VpiFuncRec*)(uint64(recs) + uint64(i) * VpiFuncRec.__sizeof__())
flags: int = int(_vpb_u32(buf, off) & uint32(0xFF))
rec.Arity = _vpb_i32(buf, off + 4)
rec.Esc = _vpb_i32(buf, off + 8)
rec.Ret = _vpb_i32(buf, off + 12)
rec.Wf = _vpb_i32(buf, off + 16)
rec.VarArg = 1 if (flags & VPB_FLAG_VARARG) != 0 else 0
rec.Method = 1 if (flags & VPB_FLAG_METHOD) != 0 else 0
rec.Name = _vpb_read_str(buf, size, off + 20, None)
ln: sizet = 0
_vpb_read_str(buf, size, off + 20, &ln)
off = off + 25 + ln
rec.RetText = _vpb_read_str(buf, size, off, &ln)
off = off + 5 + ln
# Skip the parameter list: (arity + method) triples of strings.
skip: int = rec.Arity + rec.Method
if skip > 0:
k: int = 0
while k < skip * 3:
_vpb_read_str(buf, size, off, &ln)
off = off + 5 + ln
k += 1
i += 1
cap: int = 32
while cap < nf * 2: cap = cap * 2
keys: bytes = _vpb_alloc(sizet(cap) * 8)
vals: bytes = _vpb_alloc(sizet(cap) * 4)
if keys is None or vals is None: return None
st.Keys = keys
st.Vals = vals
st.Cap = cap
mask: int = cap - 1
i = 0
while i < nf:
rec2: VpiFuncRec* = (VpiFuncRec*)(uint64(recs) + uint64(i) * VpiFuncRec.__sizeof__())
if rec2.Name is not None and rec2.Method == 0:
h: int = int(_vpb_hash(rec2.Name) & uint64(mask))
kh: uint64 = _vpb_hash(rec2.Name)
probe: int = 0
while probe < cap:
kp: uint64* = (uint64*)(uint64(keys) + uint64(h) * 8)
if kp[0] == uint64(0):
kp[0] = kh
vp: int32* = (int32*)(uint64(vals) + uint64(h) * 4)
vp[0] = int32(i + 1)
break
if kp[0] == kh:
# Same name twice: ambiguous, so the text scanner decides.
vp2: int32* = (int32*)(uint64(vals) + uint64(h) * 4)
vp2[0] = int32(0)
break
h = (h + 1) & mask
probe += 1
i += 1
if keep == 0: stdlib.free(buf)
return st
# --- text to records ---------------------------------------------------------
def _vpb_strip_prefixes(p: sizet, buf: bytes, le: sizet) -> sizet:
while True:
if le - p >= 7 and string.strncmp(buf + p, "export ", 7) == 0: p += 7
elif le - p >= 7 and string.strncmp(buf + p, "extern ", 7) == 0: p += 7
elif le - p >= 7 and string.strncmp(buf + p, "static ", 7) == 0: p += 7
elif le - p >= 8 and string.strncmp(buf + p, "virtual ", 8) == 0: p += 8
elif le - p >= 10 and string.strncmp(buf + p, "novirtual ", 10) == 0: p += 10
else: return p
# _vpb_param_count - top-level comma count of a parameter list slice.
def _vpb_param_count(buf: bytes, s: sizet, e: sizet, first_self: int*) -> int:
depth: int = 0
quote: int = 0
count: int = 0
seen: int = 0
first_name_start: sizet = s
while first_name_start < e and buf[first_name_start] == ' ': first_name_start += 1
i: sizet = s
while i < e:
ch: int = buf[i]
if quote != 0:
if ch == 0x5C: i += 2; continue
if ch == quote: quote = 0
i += 1
continue
if ch == 0x22 or ch == 0x27: quote = ch
elif ch == 0x28 or ch == 0x5B or ch == 0x7B: depth += 1
elif ch == 0x29 or ch == 0x5D or ch == 0x7D: depth -= 1
elif ch == 0x2C and depth == 0:
if seen == 0: seen = 1
count += 1
elif ch != ' ' and ch != 0x09:
seen = 1
i += 1
if count > 0: count += 1
elif seen != 0: count = 1
if first_self is not None:
name_len: sizet = 0
k: sizet = first_name_start
while k < e and (buf[k] == '_' or (buf[k] >= 'a' and buf[k] <= 'z') or (buf[k] >= 'A' and buf[k] <= 'Z') or (buf[k] >= '0' and buf[k] <= '9')):
k += 1
name_len += 1
first_self[0] = 0
if name_len == 4 and string.strncmp(buf + first_name_start, "self", 4) == 0: first_self[0] = 1
elif name_len == 3 and string.strncmp(buf + first_name_start, "cls", 3) == 0: first_self[0] = 1
return count
# _vpb_has_vararg - 1 when a top-level parameter starts with '*'.
def _vpb_has_vararg(buf: bytes, s: sizet, e: sizet) -> int:
depth: int = 0
fresh: int = 1
i: sizet = s
while i < e:
ch: int = buf[i]
if ch == 0x28 or ch == 0x5B or ch == 0x7B: depth += 1
elif ch == 0x29 or ch == 0x5D or ch == 0x7D: depth -= 1
elif depth == 0:
if ch == 0x2C: fresh = 1
elif ch != ' ' and ch != 0x09:
if fresh != 0 and ch == 0x2A: return 1
fresh = 0
i += 1
return 0
# Build - convert a .vpi text buffer into a stub blob (records are allocated from stdlib).
def Build(text: bytes, text_len: sizet, sha1: str) -> VpiStub*:
if text is None or text_len == 0 or sha1 is None: return None
n_imp: int = 0
n_fun: int = 0
# Pass 1: count imports and functions.
pos: sizet = 0
while pos < text_len:
le: sizet = pos
while le < text_len and text[le] != 0x0A: le += 1
i: sizet = pos
while i < le and text[i] == ' ': i += 1
if i < le:
if string.strncmp(text + i, "import ", 7) == 0:
n_imp += 1
j2: sizet = i + 7
while j2 < le:
if text[j2] == ',': n_imp += 1
j2 += 1
elif string.strncmp(text + i, "from ", 5) == 0: n_imp += 1
elif i - pos == 0 or i - pos == 4:
p: sizet = _vpb_strip_prefixes(i, text, le)
if le - p >= 4 and string.strncmp(text + p, "def ", 4) == 0: n_fun += 1
pos = le + 1
recs: bytes = None
imps: bytes = None
if n_fun > 0:
recs = _vpb_alloc(sizet(n_fun) * VpiFuncRec.__sizeof__())
if recs is None: return None
if n_imp > 0:
imps = _vpb_alloc(sizet(n_imp) * 8)
if imps is None: return None
nf: int = 0
ni: int = 0
# Pass 2: fill records.
pos = 0
while pos < text_len:
le = pos
while le < text_len and text[le] != 0x0A: le += 1
i = pos
while i < le and text[i] == ' ': i += 1
indent: sizet = i - pos
if i < le:
if string.strncmp(text + i, "from ", 5) == 0:
s: sizet = i + 5
e: sizet = s
while e < le and text[e] != ' ': e += 1
if e > s and ni < n_imp:
sl: str = _vpb_slice(s, e, text)
sp: str* = (str*)(uint64(imps) + uint64(ni) * 8)
sp[0] = sl
ni += 1
i = le # one name per "from" line
elif string.strncmp(text + i, "import ", 7) == 0:
s: sizet = i + 7
while s < le:
e: sizet = s
while e < le and text[e] != ',': e += 1
a: sizet = s
while a < e and text[a] == ' ': a += 1
b: sizet = e
while b > a and text[b - 1] == ' ': b -= 1
if b > a and ni < n_imp:
sp2: str* = (str*)(uint64(imps) + uint64(ni) * 8)
sp2[0] = _vpb_slice(a, b, text)
ni += 1
s = e + 1
i = le
if i < le and (indent == 0 or indent == 4):
p: sizet = _vpb_strip_prefixes(i, text, le)
if nf < n_fun and le - p >= 4 and string.strncmp(text + p, "def ", 4) == 0:
rec: VpiFuncRec* = (VpiFuncRec*)(uint64(recs) + uint64(nf) * VpiFuncRec.__sizeof__())
rec.Method = 1 if indent == 4 else 0
ns: sizet = p + 4
ne: sizet = ns
while ne < le and text[ne] != '(' and text[ne] != ' ' and text[ne] != ':': ne += 1
if ne > ns:
rec.Name = _vpb_slice(ns, ne, text)
# parenthesised parameter list
ps: sizet = ne
while ps < le and text[ps] != '(': ps += 1
pe: sizet = ps + 1
depth: int = 1
quote: int = 0
while pe < le and depth > 0:
ch: int = text[pe]
if quote != 0:
if ch == 0x5C: pe += 2; continue
if ch == quote: quote = 0
elif ch == 0x22 or ch == 0x27: quote = ch
elif ch == 0x28: depth += 1
elif ch == 0x29: depth -= 1
pe += 1
inner_end: sizet = pe - 1
if inner_end > ps:
fs: int = 0
total: int = _vpb_param_count(text, ps + 1, inner_end, &fs)
rec.VarArg = _vpb_has_vararg(text, ps + 1, inner_end)
rec.Arity = total
if rec.Method != 0 and fs != 0:
rec.Arity = total - 1
if rec.Arity < 0: rec.Arity = 0
# return annotation text
rt: sizet = pe
while rt + 1 < le and not (text[rt] == '-' and text[rt + 1] == '>'):
if text[rt] == ':': break
rt += 1
if rt + 1 < le and text[rt] == '-' and text[rt + 1] == '>':
rs: sizet = rt + 2
while rs < le and text[rs] == ' ': rs += 1
re2: sizet = rs
while re2 < le and text[re2] != ':': re2 += 1
if re2 > rs: rec.RetText = _vpb_slice(rs, re2, text)
# annotation masks: "# esc=<n> ret=<n> [wf=<n>]"
hk: sizet = pe
while hk < le and text[hk] != '#': hk += 1
if hk < le:
rec.Esc = _vpb_key_int(text, hk, le, "esc=")
rec.Ret = _vpb_key_int(text, hk, le, "ret=")
rec.Wf = _vpb_key_int(text, hk, le, "wf=")
nf += 1
pos = le + 1
st: VpiStub* = stdlib.malloc(VpiStub.__sizeof__())
if st is None: return None
string.memset(st, 0, VpiStub.__sizeof__())
st.Sha1 = sha1
st.Funcs = recs
st.FuncCount = nf
st.Imports = imps
st.ImportCount = ni
cap: int = 32
while cap < nf * 2: cap = cap * 2
keys: bytes = _vpb_alloc(sizet(cap) * 8)
vals: bytes = _vpb_alloc(sizet(cap) * 4)
if keys is None or vals is None: return None
st.Keys = keys
st.Vals = vals
st.Cap = cap
mask: int = cap - 1
k: int = 0
while k < nf:
rec3: VpiFuncRec* = (VpiFuncRec*)(uint64(recs) + uint64(k) * VpiFuncRec.__sizeof__())
if rec3.Name is not None and rec3.Method == 0:
kh: uint64 = _vpb_hash(rec3.Name)
h: int = int(kh & uint64(mask))
probe: int = 0
while probe < cap:
kp: uint64* = (uint64*)(uint64(keys) + uint64(h) * 8)
if kp[0] == uint64(0):
kp[0] = kh
vp: int32* = (int32*)(uint64(vals) + uint64(h) * 4)
vp[0] = int32(k + 1)
break
if kp[0] == kh:
vp2: int32* = (int32*)(uint64(vals) + uint64(h) * 4)
vp2[0] = int32(0)
break
h = (h + 1) & mask
probe += 1
k += 1
return st
def _vpb_slice(s: sizet, e: sizet, text: bytes) -> str:
n: sizet = e - s
b: str = stdlib.malloc(n + 1)
if b is None: return None
string.memcpy(b, text + s, n)
b[n] = '\0'
return b
# _vpb_key_int - value following key in [from, le).
def _vpb_key_int(text: bytes, from: sizet, le: sizet, key: str) -> int:
kl: sizet = string.strlen(key)
i: sizet = from
while i + kl <= le:
if string.strncmp(text + i, key, kl) == 0:
v: int = 0
j: sizet = i + kl
while j < le and text[j] >= '0' and text[j] <= '9':
v = v * 10 + int(text[j]) - 48
j += 1
return v
i += 1
return 0
# Save - serialize a stub to path; 1 = written.
def Save(st: VpiStub*, path: str) -> int:
if st is None or path is None: return 0
total: sizet = VPB_HEADER
i: int = 0
while i < st.ImportCount:
s: str = st.ImportAt(i)
total += 5 + (string.strlen(s) if s is not None else 0)
i += 1
i = 0
while i < st.FuncCount:
rec: VpiFuncRec* = (VpiFuncRec*)(uint64(st.Funcs) + uint64(i) * VpiFuncRec.__sizeof__())
params: int = rec.Arity + rec.Method
total += 30 + params * 15
if rec.Name is not None: total += string.strlen(rec.Name)
if rec.RetText is not None: total += string.strlen(rec.RetText)
i += 1
blob: bytes = _vpb_alloc(total)
if blob is None: return 0
_vpb_put32(blob, 0, uint32(VPB_MAGIC))
_vpb_put32(blob, 4, uint32(VPB_VERSION))
off: sizet = 8
_vpb_put32(blob, off, uint32(st.ImportCount))
off += 4
i = 0
while i < st.ImportCount:
off = _vpb_put_str(blob, off, st.ImportAt(i))
i += 1
_vpb_put32(blob, off, uint32(st.FuncCount))
off += 4
i = 0
while i < st.FuncCount:
rec: VpiFuncRec* = (VpiFuncRec*)(uint64(st.Funcs) + uint64(i) * VpiFuncRec.__sizeof__())
flags: uint32 = uint32(0)
if rec.VarArg != 0: flags = flags | uint32(VPB_FLAG_VARARG)
if rec.Method != 0: flags = flags | uint32(VPB_FLAG_METHOD)
if rec.Wf != 0: flags = flags | uint32(VPB_FLAG_WF)
_vpb_put32(blob, off, flags)
_vpb_put32(blob, off + 4, uint32(int32(rec.Arity)))
_vpb_put32(blob, off + 8, uint32(int32(rec.Esc)))
_vpb_put32(blob, off + 12, uint32(int32(rec.Ret)))
_vpb_put32(blob, off + 16, uint32(int32(rec.Wf)))
off = _vpb_put_str(blob, off + 20, rec.Name)
off = _vpb_put_str(blob, off, rec.RetText)
params: int = rec.Arity + rec.Method
k: int = 0
while k < params * 3:
off = _vpb_put_str(blob, off, None)
k += 1
i += 1
f: io.File* = io.File(path, io.MODE.W)
if f.closed:
stdlib.free(blob)
return 0
f.write(blob, off)
f.close()
stdlib.free(blob)
return 1
# --- process-wide cache ------------------------------------------------------
define VPB_CACHE_SLOTS = 32
_vpb_cache: rawptr = None # VpiStub*[slots]
_vpb_cache_next: int = 0
# Emit - write <vpipath>.vpb for a freshly generated .vpi text buffer.
def Emit(vpi_path: str, text: bytes, text_len: sizet, sha1: str) -> int:
if vpi_path is None or text is None: return 0
st: VpiStub* = Build(text, text_len, sha1)
if st is None: return 0
p: str = _vpb_path(vpi_path)
rc: int = 0
if p is not None:
rc = Save(st, p)
stdlib.free(p)
return rc
# Ensure - make sure a .vpb companion exists for vpi_path, converting the text once when absent.
def Ensure(vpi_path: str, sha1: str) -> int:
if vpi_path is None or sha1 is None: return 0
p: str = _vpb_path(vpi_path)
if p is None: return 0
f: io.File* = io.File(p, io.MODE.R)
if not f.closed:
# Trust an existing companion only when its header matches this format/version.
hdr: bytes = stdlib.malloc(8)
ok: int = 0
if hdr is not None:
rd0: LONG = f.read_all(hdr, 8)
if int64(rd0) == 8:
if _vpb_u32(hdr, 0) == uint32(VPB_MAGIC) and _vpb_u32(hdr, 4) == uint32(VPB_VERSION): ok = 1
stdlib.free(hdr)
f.close()
if ok != 0:
stdlib.free(p)
return 1
stdlib.free(p)
rf: io.File* = io.File(vpi_path, io.MODE.R)
if rf.closed: return 0
sz: LONGLONG = rf.size()
if sz <= 0 or sz > 4194304:
rf.close()
return 0
buf: bytes = stdlib.malloc(sizet(sz))
if buf is None:
rf.close()
return 0
rd: LONG = rf.read_all(buf, sizet(sz))
rf.close()
if int64(rd) != sz:
stdlib.free(buf)
return 0
rc: int = Emit(vpi_path, buf, sizet(sz), sha1)
stdlib.free(buf)
return rc
# _vpb_path - "<...>.vpi" -> "<...>.vpb" (same length).
def _vpb_path(vpi_path: str) -> str:
n: sizet = string.strlen(vpi_path)
if n < 4: return None
b: str = stdlib.malloc(n + 1)
if b is None: return None
string.memcpy(b, vpi_path, n)
b[n] = '\0'
b[n - 3] = 'v'
b[n - 2] = 'p'
b[n - 1] = 'b'
return b
# Load - cached parse of <sha1>.vpb from the active temp dir; None when absent.
def Load(sha1: str, temp_dir: str) -> VpiStub*:
global _vpb_cache, _vpb_cache_next
if sha1 is None: return None
if _vpb_cache is None:
_vpb_cache = stdlib.malloc(sizet(VPB_CACHE_SLOTS) * 8)
if _vpb_cache is None: return None
string.memset(_vpb_cache, 0, sizet(VPB_CACHE_SLOTS) * 8)
i: int = 0
while i < VPB_CACHE_SLOTS:
sp: VpiStub** = (VpiStub**)(uint64(_vpb_cache) + uint64(i) * 8)
st: VpiStub* = sp[0]
if st is not None and st.Sha1 is not None and string.strcmp(st.Sha1, sha1) == 0: return st
i += 1
if temp_dir is None: temp_dir = Config.TempDir
if temp_dir is None: return None
sub: str = Config.slice_subdir(sha1, Config.Sha1SliceLevel)
p: str = _vpb_join(temp_dir, sub, sha1)
if p is None: return None
f: io.File* = io.File(p, io.MODE.R)
stdlib.free(p)
if f.closed: return None
sz: LONGLONG = f.size()
if sz <= 0 or sz > 4194304:
f.close()
return None
buf: bytes = stdlib.malloc(sizet(sz))
if buf is None:
f.close()
return None
rd: LONG = f.read_all(buf, sizet(sz))
f.close()
if int64(rd) != sz:
stdlib.free(buf)
return None
st2: VpiStub* = Parse(sha1, buf, sizet(sz), 1)
if st2 is None:
stdlib.free(buf)
return None
# Cache by sha1 copy so a caller's transient string is not retained.
sn: sizet = string.strlen(sha1)
cp: str = stdlib.malloc(sn + 1)
if cp is not None:
string.memcpy(cp, sha1, sn)
cp[sn] = '\0'
st2.Sha1 = cp
slot: int = _vpb_cache_next
_vpb_cache_next = (_vpb_cache_next + 1) % VPB_CACHE_SLOTS
sp2: VpiStub** = (VpiStub**)(uint64(_vpb_cache) + uint64(slot) * 8)
sp2[0] = st2
return st2
def _vpb_join(temp_dir: str, sub: str, sha1: str) -> str:
tl: sizet = string.strlen(temp_dir)
sl: sizet = 0
if sub is not None: sl = string.strlen(sub)
hl: sizet = string.strlen(sha1)
cap: sizet = tl + sl + hl + 12
b: str = stdlib.malloc(cap)
if b is None: return None
if sl > 0:
string.memcpy(b, temp_dir, tl)
b[tl] = '/'
string.memcpy(b + tl + 1, sub, sl)
b[tl + 1 + sl] = '/'
string.memcpy(b + tl + 2 + sl, sha1, hl)
string.memcpy(b + tl + 2 + sl + hl, ".vpb", 4)
b[tl + 6 + sl + hl] = '\0'
else:
string.memcpy(b, temp_dir, tl)
b[tl] = '/'
string.memcpy(b + tl + 1, sha1, hl)
string.memcpy(b + tl + 1 + hl, ".vpb", 4)
b[tl + 5 + hl] = '\0'
return b
+23 -23
View File
@@ -66,15 +66,15 @@ novirtual class CallGenBase:
LLVMIRRegSeq: int = 0
# --- temp directory ---
TempDir: str = None
TempDir: char* = None
TempDirLen: sizet = 0
# --- closure name table (was _g_closure_names/_g_closure_count) ---
ClosureNames: str = None
ClosureNames: char* = None
ClosureCount: int = 0
# --- function-pointer signature table (was _g_funcptr_names/_g_funcptr_rets/_g_funcptr_count) ---
FuncptrNames: str = None
FuncptrNames: char* = None
FuncptrRets: uint64* = None
FuncptrCount: int = 0
@@ -107,7 +107,7 @@ novirtual class CallGenBase:
return 0
# _register_auto_addr_modes - store per-parameter value-reference modes for a callee symbol name.
def _register_auto_addr_modes(self, name: str, modes: str, count: int) -> int:
def _register_auto_addr_modes(self, name: char*, modes: char*, count: int) -> int:
"""Store per-parameter auto-address modes (PermPool-persistent) keyed by callee symbol name."""
if name is None or modes is None or count <= 0: return 0
perm: memhub.MemBuddy* = CompilerFactory.factory.PermPool
@@ -115,10 +115,10 @@ novirtual class CallGenBase:
if self.AutoAddrModes is None:
self.AutoAddrModes = hashtable.HashTable(perm)
if self.AutoAddrModes is None: return 0
key: str = perm.alloc(string.strlen(name) + 1)
key: char* = perm.alloc(string.strlen(name) + 1)
if key is None: return 0
string.strcpy(key, name)
val: str = perm.alloc(sizet(count) + 1)
val: char* = perm.alloc(sizet(count) + 1)
if val is None: return 0
string.memcpy(val, modes, sizet(count))
val[count] = char(0)
@@ -126,12 +126,12 @@ novirtual class CallGenBase:
return 1
# _auto_addr_mode - look up the value-reference mode of parameter idx (0=none / 1=ref / 2=temp).
def _auto_addr_mode(self, name: str, idx: int) -> int:
def _auto_addr_mode(self, name: char*, idx: int) -> int:
"""Return the value-reference mode for parameter idx of a callee symbol name (0 if none/unknown)."""
if self.AutoAddrModes is None or name is None or idx < 0: return 0
v: rawptr = self.AutoAddrModes.__getitem__(name)
if v is None: return 0
modes: str = rawptr(v)
modes: char* = rawptr(v)
i: int = 0
while i < idx and modes[i] != char(0): i += 1
if i != idx: return 0
@@ -142,8 +142,8 @@ novirtual class CallGenBase:
# translate_call_arg - translate one call argument, applying ref/temp auto-address when required.
def translate_call_arg(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*,
trans: HT.Translator*, callee_name: str, arg_idx: int, arg_node: ast.AST*,
fpty: mlirlite.LLVMType*, use_ctx: str = "param") -> mlirlite.Value*:
trans: HT.Translator*, callee_name: char*, arg_idx: int, arg_node: ast.AST*,
fpty: mlirlite.LLVMType*, use_ctx: char* = "param") -> mlirlite.Value*:
"""Translate a call argument; value-reference params take the address (ref) or a temp copy (temp)."""
# 记录“当前实参位置”:被调者内部若因**合成节点**(lineno==0,如构造器内联出的参数赋值)
# 报错,可据此把报错精确定位到这个实参表达式(如 `&_on_submit`),而不是退回函数定义行。
@@ -188,27 +188,27 @@ novirtual class CallGenBase:
# both call it, so there is a single implementation. cur_sha1 is the module being compiled: its own
# functions are covered by AutoAddrModes, so the (guaranteed-miss) vpi load is skipped for them.
# Returns 0=none / 1=ref / 2=temp.
def _vpi_auto_addr_mode(self, call_name: str, cur_sha1: str, idx: int) -> int:
def _vpi_auto_addr_mode(self, call_name: char*, cur_sha1: char*, idx: int) -> int:
"""Cross-module ({sha1}.cls.method / {sha1}.func) value-reference mode from the vpi cache; 0 if none/unknown."""
if call_name is None or idx < 0: return 0
prm_annots: str = self._vpi_param_annots_for(call_name, cur_sha1)
prm_annots: char* = self._vpi_param_annots_for(call_name, cur_sha1)
if prm_annots is None: return 0
return VpiReader._vpi_param_annot_seg_mode(prm_annots, idx)
# _vpi_param_annots_for - cross-module callee 的 vpi 形参注解串;None = 同模块/不可用。
# 解析只在 {sha1}.{cls}.{method} / {sha1}.{func} 的拆分这一处实现,_vpi_auto_addr_mode 与
# _fail_on_value_arg_to_ptr_param 共用。
def _vpi_param_annots_for(self, call_name: str, cur_sha1: str) -> str:
def _vpi_param_annots_for(self, call_name: char*, cur_sha1: char*) -> str:
"""Return the callee's vpi param-annotation string (None if same-module or unavailable)."""
if call_name is None: return None
if GenApi.sha1_prefix_len(call_name) == 0: return None
# Same-module callee: AutoAddrModes (registration during this compilation) is authoritative.
if cur_sha1 is not None and string.strncmp(call_name, cur_sha1, 16) == 0: return None
# split {sha1}.{rest}: if rest has '.', it is {cls}.{method}, else {func}
rest: str = call_name + 17
cls_nm: str = None
meth_nm: str = rest
dot_r: str = string.strrchr(rest, 46)
rest: char* = call_name + 17
cls_nm: char* = None
meth_nm: char* = rest
dot_r: char* = string.strrchr(rest, 46)
if dot_r is not None:
cls_len: sizet = uint64(dot_r) - uint64(rest)
if cls_len > 0:
@@ -228,7 +228,7 @@ novirtual class CallGenBase:
string.strncpy(sha1_buf, call_name, 16)
sha1_buf[16] = '\0'
if self.TempDir is not None and self.TempDirLen > 0: VpiReader._ensure_vpi_param_annot_loaded(sha1_buf, self.TempDir)
prm_annots: str = VpiReader._lookup_vpi_param_annots(sha1_buf, cls_nm, meth_nm)
prm_annots: char* = VpiReader._lookup_vpi_param_annots(sha1_buf, cls_nm, meth_nm)
stdlib.free(sha1_buf)
if cls_nm is not None: stdlib.free(cls_nm)
return prm_annots
@@ -236,14 +236,14 @@ novirtual class CallGenBase:
# _fail_on_value_arg_to_ptr_param - 强类型(2026-10-22):值语义变量不能隐式取地址传给
# 声明为 `T*` 的形参,必须写 `&a`。判定依据是被调者 vpi 里该形参的源码注解(跨模块可用);
# 同模块形参的源码写法无法从 declare 反推,保持放行。
def _fail_on_value_arg_to_ptr_param(self, trans: HT.Translator*, callee_name: str, idx: int,
def _fail_on_value_arg_to_ptr_param(self, trans: HT.Translator*, callee_name: char*, idx: int,
arg_node: ast.AST*, fpty: mlirlite.LLVMType*) -> int:
"""Fail fast when a value-semantics variable is implicitly addressed into a `T*` parameter."""
if trans is None or callee_name is None or arg_node is None or fpty is None: return 0
if self.Gen.ExprGenBase.is_ptr_type(fpty) == 0: return 0
# 只判定"变量引用"实参(字段/下标/调用结果的类型来源更复杂,交由 coerce 的通用规则)
if arg_node.kind() != ast.ASTKind.Name: return 0
nm_va: str = (ast.Name*(arg_node)).id
nm_va: char* = (ast.Name*(arg_node)).id
if nm_va is None: return 0
ve_va: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, nm_va)
if ve_va is None or ve_va.Alloca is None or ve_va.Alloca.Ty is None: return 0
@@ -252,15 +252,15 @@ novirtual class CallGenBase:
if self.Gen.ExprGenBase.is_ptr_type(ve_va.Alloca.Ty) != 0: return 0
ak_va: int = ve_va.Alloca.Ty.kind()
if ak_va != mlirlite.LLVMType.Struct: return 0
prm_annots: str = self._vpi_param_annots_for(callee_name, trans.ModuleSha1)
prm_annots: char* = self._vpi_param_annots_for(callee_name, trans.ModuleSha1)
if prm_annots is None: return 0
# vpi 的形参注解段**含 self**(方法第 0 段为空注解),故方法调用的实参下标要 +1
rest_va: str = callee_name + 17
rest_va: char* = callee_name + 17
seg_idx: int = idx
if string.strchr(rest_va, '.') is not None: seg_idx = idx + 1
if VpiReader._vpi_param_annot_seg_is_ptr(prm_annots, seg_idx) != 1: return 0
pf_va: int = GenApi.sha1_prefix_len(callee_name)
short_va: str = callee_name + pf_va if pf_va != 0 else callee_name
short_va: char* = callee_name + pf_va if pf_va != 0 else callee_name
self.Gen.TypeGenErr.fatal_error(arg_node, f"parameter {idx + 1} of '{short_va}' is declared as a pointer (T*), but the argument '{nm_va}' is a value-semantics variable; it would be copied into a temporary and the callee would modify the copy — pass '&{nm_va}' explicitly, or declare the parameter as a value type (T)", "CastError")
return 1
File diff suppressed because it is too large. Load diff
+9 -1
View File
@@ -1693,7 +1693,15 @@ novirtual class CallGenMixin(CallGenBase.CallGenBase):
ar_name: str = call_name + 17
ar_n: int = VpiReader.LookupParamCountByName(pool, ar_sha, ar_name)
if ar_n >= 0 and int(can) != ar_n:
self.Gen.TypeGenErr.fatal_error(cl, f"call to '{ar_name}' passes {int(can)} argument(s) but the definition declares {ar_n} parameter(s) (self excluded)", "TypeError")
# ar_n comes from a scan of the .vpi *text*. What this call actually binds to
# is the declare injected from the dependency's AST stub (the same source the
# linker sees), so when the two disagree the declare wins: a text-side miss
# (stale/partial .vpi cache) must not reject a valid call. Only a declare
# that agrees with the text is treated as a real arity error.
dcl: mlirlite.Function* = self.find_func_in_module(mod, call_name)
if dcl is not None:
if mlirlite.function_get_param_count(dcl) != int(can):
self.Gen.TypeGenErr.fatal_error(cl, f"call to '{ar_name}' passes {int(can)} argument(s) but the definition declares {ar_n} parameter(s) (self excluded)", "TypeError")
# Bare C library function (string.memset -> @memset): promote args to the standard signature
# (third arg i32 literal -> i64) before creating the declare, so call/declare/define widths match.
head = self.Gen.CallGenLib._promote_clib_call_args(pool, builder, mod, call_name, head, can)
+7 -1
View File
@@ -3113,7 +3113,13 @@ novirtual class StructGenMixin:
return entry.IsUnion
def is_union_by_type(self, struct_ty: mlirlite.LLVMType*) -> int:
"""Return 1 when the type-pointer struct is a union, falling back to name matching."""
"""Return 1 when the type-pointer struct is a union, falling back to name matching.
Deliberately a linear scan: an entry's Ty may be rewritten to a new pointer by a deep
copy (see find_struct_by_type), so pointer-keyed memoization would go stale; and the
name fallback uses substring matching, so a name-keyed cache could change which entry
wins when two modules register the same class name.
"""
if struct_ty is None or self.StructTable is None: return 0
ty_name: str = self._extract_struct_name(struct_ty)
st_iu: uint64 = uint64(struct_ty)
+23 -1
View File
@@ -62,7 +62,12 @@ novirtual class TypeGenCDef(TypeGenBase.TypeGenBase):
def _cdef_idx_find(self, name: str) -> int:
"""Probe the name index for name; return the slot index, or -1 if missing/unusable."""
if self.CDefineNameIdx is None or self.CDefineNameIdxCap <= 0 or name is None: return -1
if self.CDefineNameIdxBuilt != int(len(self.CDefineNames)): return -1
if self.CDefineNameIdxBuilt != int(len(self.CDefineNames)):
# Out of sync (a registration desynced it): rebuild once instead of falling back to a
# full-table scan on every lookup. Indexing is only used below 75% load; otherwise a
# rebuild would fail too and every lookup would retry it (worse than scanning).
if int(len(self.CDefineNames)) * 4 >= int(self.CDefineNameIdxCap) * 3: return -1
if self._cdef_idx_rebuild() != 0: return -1
h: uint64 = self._cdef_idx_hash(name)
mask: sizet = sizet(self.CDefineNameIdxCap - 1)
si: sizet = sizet(h) & mask
@@ -90,6 +95,23 @@ novirtual class TypeGenCDef(TypeGenBase.TypeGenBase):
si = (si + 1) & mask
return 1
# _cdef_idx_rebuild - rebuild the name index from CDefineNames (one O(n) pass per desync).
# 0 = rebuilt, 1 = cannot index (caller keeps the linear scan).
def _cdef_idx_rebuild(self) -> int:
if self.CDefineNameIdx is None or self.CDefineNameIdxCap <= 0: return 1
string.memset(self.CDefineNameIdx, 0, sizet(self.CDefineNameIdxCap) * 8)
n: int = int(len(self.CDefineNames))
i: int = 0
while i < n:
nm: str = self.CDefineNames.get(sizet(i))
if nm is not None:
if self._cdef_idx_insert(i, nm) != 0:
self.CDefineNameIdxBuilt = -1
return 1
i += 1
self.CDefineNameIdxBuilt = n
return 0
def _cdef_idx_sync_append(self, name: str) -> None:
"""Sync the index after CDefineNames.append (slot = len-1); keep it untrusted if so."""
n: int = int(len(self.CDefineNames))
+3
View File
@@ -21,6 +21,7 @@ import lib.Projectrans.Config as Config
import lib.Stub.StubMerger.StubMergerStore as StubMergerStore
import lib.Stub.StubMerger.StubMergerCollect as StubMergerCollect
import lib.Stub.StubGen.Converter as StubConverter
import lib.Stub.StubGen.VpiStub as VpiStub
import lib.Stub.StubGen.VpiReader as VpiReader
@@ -934,6 +935,7 @@ def _repl_gen_includes_vpi(mb: memhub.MemBuddy*, sr: IncludesScanner.ScanResult*
vf: io.File* = io.File(vpi_path, io.MODE.R)
if not vf.closed:
vf.close()
VpiStub.Ensure(vpi_path, ent.Sha1)
stdlib.free(vpi_path)
ReplIncVpiReuse += 1
mb.reset_to(fm)
@@ -994,6 +996,7 @@ def _repl_gen_includes_vpi(mb: memhub.MemBuddy*, sr: IncludesScanner.ScanResult*
if not pf.closed:
pf.write(pyi_buf, pos)
pf.close()
VpiStub.Emit(vpi_path, pyi_buf, pos, ent.Sha1)
ReplIncVpiNew += 1
else: ReplIncVpiFail += 1
else: ReplIncVpiFail += 1
+51
View File
@@ -199,6 +199,40 @@ def _SpawnDetached(cmd: str, cwd: str) -> int:
return 0
# _SelfExeDir - directory containing the running compiler ("D:/x/viperc.exe" -> "D:/x").
def _SelfExeDir(self_exe: str) -> str:
if self_exe is None: return None
n: sizet = string.strlen(self_exe)
cut: sizet = n
while cut > 0:
ch: char = self_exe[cut - 1]
if ch == '\\' or ch == '/':
cut -= 1
break
cut -= 1
if cut >= n: return None
out: str = stdlib.malloc(cut + 1)
if out is None: return None
if cut > 0: string.strncpy(out, self_exe, cut)
out[cut] = '\0'
return out
# _CrashHint - name the common NTSTATUS exit codes. The child inherits the parent console, so
# its own output is already visible; these codes are raised as fail-fast and carry no SEH
# exception, which is why a traceback handler inside the child cannot print a stack.
def _CrashHint(rc: int) -> None:
u: uint32 = uint32(rc)
if u == uint32(0xC0000005):
stdio.printf(" child crashed: access violation (0xC0000005)\n")
elif u == uint32(0xC0000374):
stdio.printf(" child crashed: heap corruption (0xC0000374) -- fail-fast, no SEH traceback\n")
elif u == uint32(0xC00000FD):
stdio.printf(" child crashed: stack overflow (0xC00000FD)\n")
elif u == uint32(0xC0000409):
stdio.printf(" child crashed: fail-fast / security check (0xC0000409)\n")
def RunSelfBuild(extra: str) -> int:
self_exe: str = _GetSelfExe()
if self_exe is None:
@@ -228,6 +262,7 @@ def RunSelfBuild(extra: str) -> int:
rc: int = BuildPipeline._run_cmd_wait(cmd)
if rc != 0:
stdio.printf("FAIL: --build-self compilation failed (exit=%d)\n", rc)
_CrashHint(rc)
return 1
built_exe: str = os.path.join(os.path.join(build_dir, "output"), "viperc.exe")
@@ -246,6 +281,22 @@ def RunSelfBuild(extra: str) -> int:
stdio.printf("FAIL: --build-self could not create %s (Win32 error %d)\n",
copyself_cwd, int(err_mkdir))
return 1
# copyself replaces ViperC\__vpcbuild__ wholesale, so everything the compiler needs next to
# viperc.exe must be in the new output directory *before* the copy runs. ircore.dll is the
# important one: without it every PhaseB module silently falls back to spawning
# mlir-opt/mlir-translate/llc (3 processes per module), which is both slower and a
# different code path than the one being profiled.
self_dir: str = _SelfExeDir(self_exe)
if self_dir is not None:
src_dll: str = os.path.join(self_dir, "ircore.dll")
dst_dll: str = os.path.join(os.path.join(build_dir, "output"), "ircore.dll")
if os.isfile(src_dll) and not os.isfile(dst_dll):
cop: BOOL = win32.win32file.CopyFileA(src_dll, dst_dll, 0)
if cop == 0:
stdio.printf("build-self: warning: could not copy ircore.dll next to the new viperc.exe\n")
else:
stdio.printf("build-self: copied ircore.dll into the new output directory\n")
copy_cmd: str = _FormatCopySelfCommand(copyself_exe)
if copy_cmd is None:
stdio.printf("FAIL: --build-self failed to build the copyself command line\n")
+109 -21
View File
@@ -149,6 +149,62 @@ def _TokenContains(tok: ast.Token*, line0: int, bcol: int) -> int:
return 1
def _IsUnaryKeyword(word: str) -> int:
if word is None:
return 0
if string.strcmp(word, "return") == 0 or string.strcmp(word, "if") == 0 \
or string.strcmp(word, "elif") == 0 or string.strcmp(word, "else") == 0 \
or string.strcmp(word, "while") == 0 or string.strcmp(word, "for") == 0 \
or string.strcmp(word, "in") == 0 or string.strcmp(word, "not") == 0 \
or string.strcmp(word, "and") == 0 or string.strcmp(word, "or") == 0 \
or string.strcmp(word, "is") == 0 or string.strcmp(word, "case") == 0 \
or string.strcmp(word, "raise") == 0 or string.strcmp(word, "assert") == 0 \
or string.strcmp(word, "yield") == 0 or string.strcmp(word, "await") == 0 \
or string.strcmp(word, "lambda") == 0:
return 1
return 0
def _NegativeLiteral(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*,
line0: int, bcol: int) -> int:
"""Whether the literal at bcol is directly preceded by a unary minus."""
if doc is None or line0 < 0 or line0 >= doc.index.Count():
return 0
i: int = bcol
while i > 0:
ch: char = _ByteAt(doc, line0, i - 1)
if ch == ' ' or ch == '\t' or ch == '\r':
i -= 1
else:
break
if i <= 0:
return 0
if _ByteAt(doc, line0, i - 1) != '-':
return 0
j: int = i - 1
while j > 0:
ch2: char = _ByteAt(doc, line0, j - 1)
if ch2 == ' ' or ch2 == '\t' or ch2 == '\r':
j -= 1
else:
break
if j == 0:
return 1
prev: char = _ByteAt(doc, line0, j - 1)
if string.strchr("=([{,:;+-*/%<>&|^!~", int(prev)) is not None:
return 1
k: int = j
while k > 0:
if LspSource._IsIdentChar(_ByteAt(doc, line0, k - 1)) != 0:
k -= 1
else:
break
if k == j:
return 0
word: str = LspSource.SliceBytes(pool, doc, line0, k, line0, j)
return _IsUnaryKeyword(word)
def _FindLiteral(pool: memhub.MemManager*, doc: LspDocuments.LspDocument*,
line0: int, ch: int) -> ast.Token*:
"""Return the number/string token under the cursor, or None."""
@@ -361,39 +417,66 @@ def _IntFloatLine(pool: memhub.MemManager*, v: uint64) -> str:
"f16 ", f16), ", f32 "), f32), ", f64 "), f64)
def _IntMarkdown(pool: memhub.MemManager*, tok: ast.Token*) -> str:
def _SignedBitsForNegative(v: uint64) -> int:
"""Two's-complement width needed for -v (v is the magnitude)."""
if v == uint64(0):
return 1
return _U64Bits(v - uint64(1)) + 1
def _IntMarkdown(pool: memhub.MemManager*, tok: ast.Token*, neg: int) -> str:
ovf: int = 0
v: uint64 = _ParseMagnitude(tok.str_val, &ovf)
head: str = tok.str_val
if head is None:
head = "0"
if neg != 0:
head = _Cat(pool, "-", head)
body: str = head
if ovf != 0:
return _CodeBlock(pool, _Cat(pool, head, "\nvalue exceeds 64 bits (unsigned)"))
body = _Cat(pool, body, _Cat(pool, "\ndec ", _DecU64(pool, v)))
body = _Cat(pool, body, _Cat(pool, "\nhex ", _HexU64(pool, v)))
body = _Cat(pool, body, _Cat(pool, "\nbinary ", _BinU64(pool, v)))
ub: int = _U64Bits(v)
sb: int = ub + 1
bits_line: str = _Cat(pool, _Cat(pool, _Cat(pool, _Cat(pool,
"\nbits ", _U64ToString(pool, uint64(ub))),
" unsigned, "),
_U64ToString(pool, uint64(sb))),
" signed")
body = _Cat(pool, body, bits_line)
uw: int = _PowTwoWidth(ub)
iw: int = _PowTwoWidth(sb)
body = _Cat(pool, body, _Cat(pool, "\nuint u", _U64ToString(pool, uint64(uw))))
body = _Cat(pool, body, _Cat(pool, "\nint i", _U64ToString(pool, uint64(iw))))
dec: str = _DecU64(pool, v)
if neg != 0 and v != uint64(0):
dec = _Cat(pool, "-", dec)
body = _Cat(pool, body, _Cat(pool, "\ndec ", dec))
hx: str = _HexU64(pool, v)
bn: str = _BinU64(pool, v)
if neg != 0 and v != uint64(0):
hx = _Cat(pool, "-", hx)
bn = _Cat(pool, "-", bn)
body = _Cat(pool, body, _Cat(pool, "\nhex ", hx))
body = _Cat(pool, body, _Cat(pool, "\nbinary ", bn))
if neg != 0:
sb: int = _SignedBitsForNegative(v)
bits_line: str = _Cat(pool, _Cat(pool, _Cat(pool,
"\nbits ", _U64ToString(pool, uint64(sb))),
" signed (negative; unsigned storage cannot hold it)"), "")
body = _Cat(pool, body, bits_line)
iw: int = _PowTwoWidth(sb)
body = _Cat(pool, body, "\nuint n/a (negative)")
body = _Cat(pool, body, _Cat(pool, "\nint i", _U64ToString(pool, uint64(iw))))
else:
ub: int = _U64Bits(v)
sb: int = ub + 1
bits_line: str = _Cat(pool, _Cat(pool, _Cat(pool, _Cat(pool,
"\nbits ", _U64ToString(pool, uint64(ub))),
" unsigned, "),
_U64ToString(pool, uint64(sb))),
" signed")
body = _Cat(pool, body, bits_line)
uw: int = _PowTwoWidth(ub)
iw: int = _PowTwoWidth(sb)
body = _Cat(pool, body, _Cat(pool, "\nuint u", _U64ToString(pool, uint64(uw))))
body = _Cat(pool, body, _Cat(pool, "\nint i", _U64ToString(pool, uint64(iw))))
body = _Cat(pool, body, _Cat(pool, "\nfloat ", _IntFloatLine(pool, v)))
if tok.int_width > 0:
decl: str = "u" if tok.int_unsigned != 0 else "i"
decl = _Cat(pool, decl, _U64ToString(pool, uint64(tok.int_width)))
fits: int = 0
if tok.int_unsigned != 0:
fits = IntFacts.FitsUnsignedValue(tok.int_width, v)
fits = 0 if neg != 0 else IntFacts.FitsUnsignedValue(tok.int_width, v)
else:
fits = IntFacts.FitsSignedValue(tok.int_width, v, 0)
fits = IntFacts.FitsSignedValue(tok.int_width, v, neg)
state: str = "fits" if fits != 0 else "overflow"
body = _Cat(pool, body, _Cat(pool, _Cat(pool, _Cat(pool,
"\ndeclared ", decl), " ("), _Cat(pool, state, ")")))
@@ -500,13 +583,17 @@ def _FloatFitLine(pool: memhub.MemManager*, v: double) -> str:
"f16 ", s16), ", f32 "), s32), ", f64 "), s64)
def _FloatMarkdown(pool: memhub.MemManager*, tok: ast.Token*) -> str:
def _FloatMarkdown(pool: memhub.MemManager*, tok: ast.Token*, neg: int) -> str:
v: double = tok.float_val
if neg != 0:
v = -v
bits: uint64 = uint64(0)
*&bits = *&v
body: str = tok.str_val
if body is None:
body = "0.0"
if neg != 0:
body = _Cat(pool, "-", body)
body = _Cat(pool, body, _Cat(pool, "\nhex ", _HexU64Fixed(pool, bits)))
body = _Cat(pool, body, _Cat(pool, "\nbinary ", _BinU64(pool, bits)))
body = _Cat(pool, body, _Cat(pool, "\nfloat ", _FloatFitLine(pool, v)))
@@ -650,14 +737,15 @@ def Hover(pool: memhub.MemManager*, tr: LspTransport.LspTransport*,
if text is None:
tok: ast.Token* = _FindLiteral(pool, doc, line0, ch)
if tok is not None:
neg: int = _NegativeLiteral(pool, doc, tok.lineno - 1, tok.col_offset)
if tok.type == ast.TokenType.String:
text = _StringMarkdown(pool, doc, tok)
elif tok.is_complex != 0:
text = None
elif tok.is_float != 0:
text = _FloatMarkdown(pool, tok)
text = _FloatMarkdown(pool, tok, neg)
else:
text = _IntMarkdown(pool, tok)
text = _IntMarkdown(pool, tok, neg)
if text is None:
text = _KeywordMarkdown(pool, _WordAt(pool, doc, line0, ch))
if text is None:
+13
View File
@@ -98,6 +98,19 @@ def string_use() -> int:
return len("hello")
def negative_use() -> int:
return -128
def negative_float_use() -> float:
return -3.5
def subtract_use() -> int:
x: int = 5
return x - 1
def else_block(flag: int) -> int:
if flag != 0:
return 1
+14
View File
@@ -1885,6 +1885,20 @@ def gate10(exe: str) -> int:
for piece in ("type str", "length 5 bytes"):
if piece not in string_hover:
raise AssertionError(f"string hover lacks {piece!r}: {string_hover}")
neg_hover = hover_text(at("return -128", len("return -")))
for piece in ("dec -128", "hex -0x80", "binary -0b1000_0000",
"bits 8 signed", "uint n/a (negative)", "int i8",
"float f16 exact, f32 exact, f64 exact"):
if piece not in neg_hover:
raise AssertionError(f"negative hover lacks {piece!r}: {neg_hover}")
neg_float_hover = hover_text(at("return -3.5", len("return -")))
if "hex 0xC00C000000000000" not in neg_float_hover:
raise AssertionError(f"negative float bit pattern is wrong: {neg_float_hover}")
if "f16 exact, f32 exact, f64 exact" not in neg_float_hover:
raise AssertionError(f"negative float exactness is wrong: {neg_float_hover}")
minus_hover = hover_text(at("return x - 1", len("return x - ")))
if "dec 1" not in minus_hover:
raise AssertionError(f"binary minus was treated as unary: {minus_hover}")
def reference_ranges(pos: dict[str, int]) -> list[tuple[str, int, int]]:
result = request("textDocument/references", pos,