feat(vpc): literal-container membership, one vpi scanner, extern/default fixes

- `x in (a,b,c)` / `[..]` / `{..}` lowers to an OR of equality tests (string
  literals via strcmp) and `not in` negates it; literal containers used to
  silently evaluate to false
- add VpiText (ParenEnd/ParamSpans/ParamCount/HasVararg/FindDefaultEq) and
  drop the five hand-written copies of the parameter scanner; escape skips no
  longer jump past the parameter-list end
- fill omitted arguments from recorded defaults before any zero-fill, and fail
  fast when no default is recorded (memcpy keeps its intrinsic lowering)
- resolve package-qualified `extern def` calls without a SHA1 prefix:
  explicit allow_star=1, rel-path module fallback, raw vpi bare-declaration
  scan, and `extern def`/`export def` flags honoured during declare injection
- fix exact-size read_all usage in VpiStub.Load/Ensure and the vpi scanner
This commit is contained in:
TermiNexus committed 2026-11-01 00:05:44 +08:00
1 parent 84f11a69e7
commit 83d15289ef
6 files changed
+401 -233

No files matched your search

+82 -125
View File
@@ -15,6 +15,7 @@ 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.Stub.StubGen.VpiText as VpiText
import lib.Projectrans.Config as Config
@@ -1892,6 +1893,59 @@ def _lookup_vpi_bare(src_sha1: str, cls_name: str, method_name: str) -> int:
return int(_g_vpi_bare_flags[hit_b])
# _vpi_declares_bare - raw text scan for a no-mangle declaration of func_name.
# The return-annotation cache is the fast path, but it can lack a module that was loaded
# before the declaration existed; the file on disk is the authority for extern/export/state.
def _vpi_declares_bare(src_sha1: str, temp_dir: str, func_name: str) -> int:
"""1 when the module's .vpi declares func_name as extern/export/state (no SHA1 prefix)."""
if src_sha1 is None or temp_dir is None or func_name is None: return 0
vpi_path: str = _vpi_build_path(temp_dir, src_sha1, ".vpi")
if vpi_path is None: return 0
f: io.File* = io.File(vpi_path, io.MODE.R)
stdlib.free(vpi_path)
if f.closed: return 0
sz: LONGLONG = f.size()
if sz <= 0 or sz > 4194304:
f.close()
return 0
buf: bytes = stdlib.malloc(sizet(sz) + 1)
if buf is None:
f.close()
return 0
rd: LONG = f.read_all(buf, sizet(sz) + 1)
f.close()
if int64(rd) != sz:
stdlib.free(buf)
return 0
buf[sizet(sz)] = 0
total: sizet = sizet(sz)
nlen: sizet = string.strlen(func_name)
pos: sizet = 0
hit: int = 0
while pos < total:
le: sizet = pos
while le < total and buf[le] != 0x0A: le += 1
i: sizet = pos
while i < le and buf[i] == ' ': i += 1
prefixed: int = 0
if le - i >= 7 and (string.strncmp(buf + i, "extern ", 7) == 0 or string.strncmp(buf + i, "export ", 7) == 0):
i += 7
prefixed = 1
elif le - i >= 6 and string.strncmp(buf + i, "state ", 6) == 0:
i += 6
prefixed = 1
if prefixed != 0 and le - i >= 4 and string.strncmp(buf + i, "def ", 4) == 0:
ns: sizet = i + 4
ne: sizet = ns
while ne < le and buf[ne] != '(' and buf[ne] != ' ': ne += 1
if ne - ns == nlen and string.strncmp(buf + ns, func_name, nlen) == 0:
hit = 1
break
pos = le + 1
stdlib.free(buf)
return hit
# _vpi_struct_or_enum - 结构体优先,但 **opaque 前向占位** 必须让位给已注册的枚举。
# 占位来源:枚举名曾在别的模块被当成类名 ensure_forward_class(StructTable 是进程级的),
# 若这里按裸名命中它,`-> types.FRESULT` 就会变成"返回一个空结构体",与函数本体的 i32 冲突。
@@ -2529,26 +2583,8 @@ define VPI_DEF_SCAN_MAX = 128
# _VpiParamListEnd - 与 open_pos 处 '(' 配对的 ')' 下标;找不到返回 le
def _VpiParamListEnd(vbuf: bytes, open_pos: sizet, le: sizet) -> sizet:
"""返回与 open_pos 处 '(' 配对的 ')' 下标;找不到返回 le"""
depth: int = 0
q: uint8 = 0
i: sizet = open_pos
while i < le:
ch: uint8 = vbuf[i]
if q != 0:
if ch == 0x5C:
i += 2
continue
if ch == q: q = 0
i += 1
continue
if ch == 0x22 or ch == 0x27: q = ch
elif ch == 0x28 or ch == 0x5B or ch == 0x7B: depth += 1
elif ch == 0x29 or ch == 0x5D or ch == 0x7D:
depth -= 1
if depth <= 0: return i
i += 1
return le
"""Index of the ')' matching the '(' at open_pos (le when unbalanced)."""
return VpiText.ParenEnd(vbuf, open_pos, le)
# _VpiParseIntRange - 把 [s, e) 解析成整数(十进制 / 0x 十六进制 / 带符号);1=成功
def _VpiParseIntRange(vbuf: bytes, s: sizet, e: sizet, out_v: int64*) -> int:
@@ -2633,72 +2669,20 @@ def _VpiFillDefaultLit(pool: memhub.MemBuddy*, vbuf: bytes, s: sizet, e: sizet,
# _VpiParseParamDefaults - 解析参数区间:返回 (参数个数 << 8) | 缺省值个数;-1=有不可重建缺省值
def _VpiParseParamDefaults(pool: memhub.MemBuddy*, vbuf: bytes, ps: sizet, pe: sizet, kinds: int*, ints: int64*, strs: sizet*) -> int:
"""解析 [ps, pe) 参数列表,统计参数个数与尾部缺省值字面量"""
pc: int = 0
"""Count parameters and snapshot trailing defaults; (count << 8) | defaults, or -1."""
spans: sizet* = pool.alloc(sizet(VPI_DEF_SCAN_MAX) * 16)
if spans is None: return -1
pc: int = VpiText.ParamSpans(vbuf, ps, pe, spans, VPI_DEF_SCAN_MAX, None)
if pc < 0: return -1
dc: int = 0
ss: sizet = ps
i: sizet = ps
depth: int = 0
q: uint8 = 0
while i <= pe:
is_sep: int = 0
if i == pe:
is_sep = 1
else:
ch: uint8 = vbuf[i]
if q != 0:
if ch == 0x5C:
i += 2
continue
if ch == q: q = 0
i += 1
continue
if ch == 0x22 or ch == 0x27: q = 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: is_sep = 1
if is_sep == 0:
i += 1
continue
a: sizet = ss
b: sizet = i
while a < b and vbuf[a] == 0x20: a += 1
while b > a and vbuf[b - 1] == 0x20: b -= 1
if b > a:
pc += 1
# 找顶层 '='(排除 == / >= / <= / !=)
eq: sizet = b
d2: int = 0
q2: uint8 = 0
j: sizet = a
while j < b:
cj: uint8 = vbuf[j]
if q2 != 0:
if cj == 0x5C:
j += 2
continue
if cj == q2: q2 = 0
j += 1
continue
if cj == 0x22 or cj == 0x27: q2 = cj
elif cj == 0x28 or cj == 0x5B or cj == 0x7B: d2 += 1
elif cj == 0x29 or cj == 0x5D or cj == 0x7D: d2 -= 1
elif cj == 0x3D and d2 == 0:
prev: uint8 = 0
nxt: uint8 = 0
if j > a: prev = vbuf[j - 1]
if j + 1 < b: nxt = vbuf[j + 1]
if nxt != 0x3D and prev != 0x3D and prev != 0x3C and prev != 0x3E and prev != 0x21:
eq = j
break
j += 1
if eq < b:
if dc >= VPI_DEF_SCAN_MAX: return -1
if _VpiFillDefaultLit(pool, vbuf, eq + 1, b, kinds, ints, strs, dc) == 0: return -1
dc += 1
if i >= pe: break
i += 1
ss = i
for pi in range(int(pc)):
a: sizet = spans[pi * 2]
b: sizet = spans[pi * 2 + 1]
eq: sizet = VpiText.FindDefaultEq(vbuf, a, b)
if eq < b:
if dc >= VPI_DEF_SCAN_MAX: return -1
if _VpiFillDefaultLit(pool, vbuf, eq + 1, b, kinds, ints, strs, dc) == 0: return -1
dc += 1
if pc > 255 or dc > 255: return -1
return (pc << 8) | dc
@@ -2988,43 +2972,11 @@ def PreRegisterVpiEnums(pool: memhub.MemBuddy*, temp_dir: str) -> int:
# _VpiParamCountAndVararg - 数参数个数;含 *args / **kwargs 时返回 -1(调用方跳过 arity 校验)
def _VpiParamCountAndVararg(vbuf: bytes, ps: sizet, pe: sizet) -> int:
"""数参数个数;含可变参数返回 -1"""
pc: int = 0
ss: sizet = ps
i: sizet = ps
depth: int = 0
q: uint8 = 0
while i <= pe:
is_sep: int = 0
if i == pe:
is_sep = 1
else:
ch: uint8 = vbuf[i]
if q != 0:
if ch == 0x5C:
i += 2
continue
if ch == q: q = 0
i += 1
continue
if ch == 0x22 or ch == 0x27: q = 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: is_sep = 1
if is_sep == 0:
i += 1
continue
a: sizet = ss
b: sizet = i
while a < b and vbuf[a] == 0x20: a += 1
while b > a and vbuf[b - 1] == 0x20: b -= 1
if b > a:
if vbuf[a] == 0x2A: return -1
pc += 1
if i >= pe: break
i += 1
ss = i
return pc
"""Number of parameters; -1 when the list contains *args/**kwargs."""
n: int = VpiText.ParamCount(vbuf, ps, pe)
if n < 0: return -1
if VpiText.HasVararg(vbuf, ps, pe) != 0: return -1
return n
# LookupSignDefaultsByName - 直接在定义模块 .vpi 文本里查一个函数/方法的缺省参数快照
# 与 PreRegisterVpiDefaults 的区别:不登记进全局表,只按名字现查现用(表键不可靠时的兜底)。
@@ -4324,10 +4276,15 @@ def _inject_function_declare(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*,
has_extern: int = 0
has_state: int = 0
has_export: int = 0
# `extern def` / `export def` recorded as the vpi keyword are the same storage markers as the
# legacy t.ExternT / t.ExportT return annotations: the defining side emits the bare C name, so
# the injected declare must be bare as well (a SHA1 prefix leaves the symbol undefined).
if (fd.flags & ast.FLAG_IS_EXTERN) != 0: has_extern = 1
if (fd.flags & ast.FLAG_IS_EXPORT) != 0: has_export = 1
if fd.returns is not None:
has_extern = GenApi.has_decorator_marker(fd.returns, "ExternT")
has_state = GenApi.has_decorator_marker(fd.returns, "State")
has_export = GenApi.has_decorator_marker(fd.returns, "ExportT")
if GenApi.has_decorator_marker(fd.returns, "ExternT") != 0: has_extern = 1
if GenApi.has_decorator_marker(fd.returns, "State") != 0: has_state = 1
if GenApi.has_decorator_marker(fd.returns, "ExportT") != 0: has_export = 1
# Bare-struct param rule (see _AggregateParamTy): C ABI externs (t.ExternT/t.State) by value;
# the rest (incl. t.ExportT) keep Ptr(struct). Same source as FuncGenDef.is_extern_decl.
agg_is_c_abi: int = 0
+20 -64
View File
@@ -3,6 +3,7 @@ import string
import stdlib
import io
import lib.Projectrans.Config as Config
import lib.Stub.StubGen.VpiText as VpiText
# VpiStub - binary companion of the human-readable .vpi stub.
#
@@ -238,59 +239,25 @@ def _vpb_strip_prefixes(p: sizet, buf: bytes, le: sizet) -> sizet:
# _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
"""Arity from a parameter list slice; first_self (optional) reports self/cls."""
if first_self is not None:
name_len: sizet = 0
k: sizet = first_name_start
first_self[0] = 0
ns: sizet = s
while ns < e and buf[ns] == ' ': ns += 1
nl: sizet = 0
k: sizet = ns
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
nl += 1
if nl == 4 and string.strncmp(buf + ns, "self", 4) == 0: first_self[0] = 1
elif nl == 3 and string.strncmp(buf + ns, "cls", 3) == 0: first_self[0] = 1
return VpiText.ParamCount(buf, s, e)
# _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
"""1 when a top-level parameter starts with '*'."""
return VpiText.HasVararg(buf, s, e)
# Build - convert a .vpi text buffer into a stub blob (records are allocated from stdlib).
@@ -374,18 +341,7 @@ def Build(text: bytes, text_len: sizet, sha1: str) -> VpiStub*:
# 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
pe: sizet = VpiText.ParenEnd(text, ps, le) + 1
inner_end: sizet = pe - 1
if inner_end > ps:
fs: int = 0
@@ -569,10 +525,10 @@ def Ensure(vpi_path: str, sha1: str) -> int:
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)
hdr: bytes = stdlib.malloc(9)
ok: int = 0
if hdr is not None:
rd0: LONG = f.read_all(hdr, 8)
rd0: LONG = f.read_all(hdr, 9)
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)
@@ -587,11 +543,11 @@ def Ensure(vpi_path: str, sha1: str) -> int:
if sz <= 0 or sz > 4194304:
rf.close()
return 0
buf: bytes = stdlib.malloc(sizet(sz))
buf: bytes = stdlib.malloc(sizet(sz) + 1)
if buf is None:
rf.close()
return 0
rd: LONG = rf.read_all(buf, sizet(sz))
rd: LONG = rf.read_all(buf, sizet(sz) + 1)
rf.close()
if int64(rd) != sz:
stdlib.free(buf)
@@ -641,11 +597,11 @@ def Load(sha1: str, temp_dir: str) -> VpiStub*:
if sz <= 0 or sz > 4194304:
f.close()
return None
buf: bytes = stdlib.malloc(sizet(sz))
buf: bytes = stdlib.malloc(sizet(sz) + 1)
if buf is None:
f.close()
return None
rd: LONG = f.read_all(buf, sizet(sz))
rd: LONG = f.read_all(buf, sizet(sz) + 1)
f.close()
if int64(rd) != sz:
stdlib.free(buf)
+122
View File
@@ -0,0 +1,122 @@
from stdint import *
import string
# VpiText - shared scanning of .vpi signature text.
#
# Parenthesis matching, parameter splitting and vararg detection all use one
# quote/escape aware state machine here; callers only add their own semantics
# (counting, defaults parsing, stub building).
def ParenEnd(buf: bytes, open_pos: sizet, le: sizet) -> sizet:
"""Index of the ')' matching the '(' at open_pos; le when unbalanced."""
depth: int = 0
quote: int = 0
i: sizet = open_pos
while i < le:
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
if depth <= 0: return i
i += 1
return le
def ParamSpans(buf: bytes, ps: sizet, pe: sizet, spans: sizet*, max: int, vararg: int* = None) -> int:
"""Split [ps, pe) into top-level comma-separated parameter spans.
Each span is trimmed and stored as (start, end) at spans[2*i] / spans[2*i+1].
spans may be None to count only; -1 means the caller's buffer is too small.
When vararg is given it becomes 1 if any span starts with '*'.
"""
if vararg is not None: vararg[0] = 0
if buf is None or pe <= ps: return 0
depth: int = 0
quote: int = 0
count: int = 0
start: sizet = ps
i: sizet = ps
while i <= pe:
sep: int = 0
if i == pe:
sep = 1
else:
ch: int = buf[i]
if quote != 0:
if ch == 0x5C:
i += 2
if i > pe: i = pe
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: sep = 1
if sep != 0:
a: sizet = start
b: sizet = i
while a < b and (buf[a] == 0x20 or buf[a] == 0x09): a += 1
while b > a and (buf[b - 1] == 0x20 or buf[b - 1] == 0x09): b -= 1
if b > a:
if spans is not None:
if count >= max: return -1
spans[count * 2] = a
spans[count * 2 + 1] = b
if vararg is not None and buf[a] == 0x2A: vararg[0] = 1
count += 1
if i >= pe: break
start = i + 1
i += 1
return count
def ParamCount(buf: bytes, ps: sizet, pe: sizet) -> int:
return ParamSpans(buf, ps, pe, None, 0, None)
def HasVararg(buf: bytes, ps: sizet, pe: sizet) -> int:
flag: int = 0
ParamSpans(buf, ps, pe, None, 0, &flag)
return flag
def FindDefaultEq(buf: bytes, a: sizet, b: sizet) -> sizet:
"""Index of the top-level '=' that starts a default value; b when absent.
Single '=' only: '==' / '>=' / '<=' / '!=' do not start a default.
"""
depth: int = 0
quote: int = 0
j: sizet = a
while j < b:
ch: int = buf[j]
if quote != 0:
if ch == 0x5C:
j += 2
continue
if ch == quote: quote = 0
j += 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 == 0x3D and depth == 0:
prev: int = 0
nxt: int = 0
if j > a: prev = buf[j - 1]
if j + 1 < b: nxt = buf[j + 1]
if nxt != 0x3D and prev != 0x3D and prev != 0x3C and prev != 0x3E and prev != 0x21:
return j
j += 1
return b
+52 -2
View File
@@ -327,6 +327,56 @@ novirtual class CallGenBase:
"""Look up a module SHA1 by suffix (e.g. "__types" matches "mlirlite.__types") for relative imports, via StubMerger.LookupSha1ByModNameSuffix."""
return StubMergerStore.LookupSha1ByModNameSuffix(mod_suffix)
def _lookup_module_sha1_by_rel(self, pool: memhub.MemBuddy*, mod_name: char*) -> str:
"""Resolve a dotted module name through the SHA1 store's rel paths.
The module-name table is the fast path, but a package's star-imported submodule can be
absent from it while the scanner still registered the file; the rel path
("includes/vqt6/_qtwidgets.vp") identifies the module exactly and never drifts.
"""
if pool is None or mod_name is None: return None
rel_arr: bytes = StubMergerStore.GetSha1StoreRelArrPtr()
sha1_arr: bytes = StubMergerStore.GetSha1StoreArrPtr()
count: int = StubMergerStore.GetSha1StoreCount()
if rel_arr is None or sha1_arr is None or count <= 0: return None
n: sizet = string.strlen(mod_name)
cap: sizet = n + 24
tgt: char* = pool.alloc(cap)
if tgt is None: return None
string.memcpy(tgt, "includes/", 9)
ti: sizet = 9
mi: sizet = 0
while mi < n:
ch: char = mod_name[mi]
tgt[ti] = '/' if ch == '.' else ch
ti += 1
mi += 1
tgt[ti] = '\0'
rel_sz: sizet = StubMerger.MAX_REL_PATH_LEN
i: int = 0
while i < count:
rel: char* = rel_arr + sizet(i) * rel_sz
rl: sizet = string.strlen(rel)
# workspace-relative ("includes/vqt6/x.vp") first, then include-root-relative ("vqt6/x.vp")
if rl == ti + 3 and string.strncmp(rel, tgt, ti) == 0:
return sha1_arr + sizet(i) * 17
if ti > 9 and rl == ti - 6 and string.strncmp(rel, tgt + 9, ti - 9) == 0:
return sha1_arr + sizet(i) * 17
i += 1
# package __init__ form: "vqt6" -> "includes/vqt6/__init__.vp"
string.memcpy(tgt + ti, "/__init__.vp", 12)
tgt[ti + 12] = '\0'
init_len: sizet = ti + 12
i = 0
while i < count:
rel2: char* = rel_arr + sizet(i) * rel_sz
if string.strcmp(rel2, tgt) == 0:
return sha1_arr + sizet(i) * 17
if init_len > 9 and string.strcmp(rel2, tgt + 9) == 0:
return sha1_arr + sizet(i) * 17
i += 1
return None
def _module_package_from_sha1(self, pool: memhub.MemBuddy*, mod_sha1: char*) -> str:
"""Compute the module's package name (directory part, '/' to '.') from the SHA1 store's rel_path"""
if pool is None or mod_sha1 is None: return None
@@ -1123,7 +1173,7 @@ novirtual class CallGenBase:
if owner_full_name[osuf - 1] != '.': return 0
if string.strcmp(owner_full_name + osuf, func_name) != 0: return 0
rr_src: char* = self.Gen.ImportsGen.lookup_reexport_call(mod_sha1, func_name)
if rr_src is None: rr_src = self._lookup_reexport_from_vpi(pool, mod_sha1, func_name)
if rr_src is None: rr_src = self._lookup_reexport_from_vpi(pool, mod_sha1, func_name, 1)
if rr_src is None: return 0
total: sizet = string.strlen(rr_src)
ri: sizet = 0
@@ -1155,7 +1205,7 @@ novirtual class CallGenBase:
if depth >= 4 or pool is None or cls_sha1 is None or method_name is None: return None
if cls_name is not None: return None
rr_src: char* = self.Gen.ImportsGen.lookup_reexport_call(cls_sha1, method_name)
if rr_src is None: rr_src = self._lookup_reexport_from_vpi(pool, cls_sha1, method_name)
if rr_src is None: rr_src = self._lookup_reexport_from_vpi(pool, cls_sha1, method_name, 1)
if rr_src is None: return None
total: sizet = string.strlen(rr_src)
ri: sizet = 0
+59 -41
View File
@@ -182,17 +182,23 @@ novirtual class CallGenMixin(CallGenBase.CallGenBase):
def _fill_defaults_late(self, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, builder: mlirlite.IRBuilder*, trans: HT.Translator*, cl: ast.Call*, call_name: char*, head: mlirlite.Value*, can: sizet) -> sizet:
"""按 call_name 里的定义模块 SHA1 前缀补缺省参数;返回新的实参个数"""
if head is None or can == 0 or call_name is None: return can
if self.Gen.CallGenLib._is_sha1_mangled_call(call_name) == 0: return can
sh_buf: char* = pool.alloc(17)
if sh_buf is None: return can
string.strncpy(sh_buf, call_name, 16)
sh_buf[16] = '\0'
cname: char* = call_name + 17
# {sha1}.func / {sha1}.Class.method carry the defining module; a bare or Class.method
# name is resolved against the current translator (same-module calls).
sh_buf: char* = None
cname: char* = call_name
if self.Gen.CallGenLib._is_sha1_mangled_call(call_name) != 0:
sh_buf = pool.alloc(17)
if sh_buf is None: return can
string.strncpy(sh_buf, call_name, 16)
sh_buf[16] = '\0'
cname = call_name + 17
elif trans is not None and trans.ModuleSha1 is not None:
sh_buf = trans.ModuleSha1
if cname[0] == '\0': return can
# 只做 SHA1 隔离的查找:表按 (名字, 定义模块 SHA1) 定位;表里没有就直接读该模块的 .vpi。
# 绝不按裸名跨模块搜(`run` 这类名字在多个模块里都有,会拿错签名去补参/校验)。
de: GenTypes.FuncDefaultsEntry* = self.Gen.CallGenLeafKit.find_func_defaults(cname, sh_buf)
if de is None: de = self._defaults_entry_from_vpi(pool, sh_buf, cname)
if de is None and sh_buf is not None: de = self._defaults_entry_from_vpi(pool, sh_buf, cname)
if de is None: return can
n: int = int(can)
if de.DefaultCount <= 0 or n >= de.ParamCount: return can
@@ -253,6 +259,26 @@ novirtual class CallGenMixin(CallGenBase.CallGenBase):
self.Gen.TypeGenErr.fatal_error(at, f"call '{nm.id}.{at.attr}(...)' targets module '{nm.id}', but this file does not import it; add `import {nm.id}`. Without the import the call degrades into an external C-function call and its return type cannot be resolved (pointer returns would be truncated to i32)", "NameError")
return 0
def _is_bare_module_symbol(self, pool: memhub.MemBuddy*, mod_sha1: char*, func_name: char*) -> int:
"""1 when the module declares func_name as extern/export/state (no SHA1 on the symbol)."""
if mod_sha1 is None or func_name is None: return 0
if self.Gen.CallGenBase.is_cexport_func(mod_sha1, func_name) != 0: return 1
return self._is_state_func_from_vpi(pool, mod_sha1, func_name)
def _is_bare_module_symbol_deep(self, pool: memhub.MemBuddy*, mod_sha1: char*, func_name: char*) -> int:
"""Bare check that also scans the .vpi text.
Used for star-import candidates only: their names are resolved through a package, so the
return-annotation cache may hold no entry for them while the file on disk does declare
`extern def` (the pre-fix symptom: `89c1....vqt6_qwidget_...`). One read per candidate.
"""
if self._is_bare_module_symbol(pool, mod_sha1, func_name) != 0: return 1
if mod_sha1 is None or func_name is None: return 0
td: char* = self.Gen.CallGenBase.TempDir
if td is None or self.Gen.CallGenBase.TempDirLen <= 0: td = self.Gen.TypeGenBase.get_temp_dir()
if td is None: return 0
return VpiReader._vpi_declares_bare(mod_sha1, td, func_name)
def translate_call(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, node: ast.AST*, funcs_ptr: rawptr, func_count: int, trans: HT.Translator* = None) -> mlirlite.Value*:
"""Translate a Call node and return the result Value."""
cl: ast.Call* = ast.Call*(node)
@@ -975,7 +1001,7 @@ novirtual class CallGenMixin(CallGenBase.CallGenBase):
# Package vpi miss (the function is defined in a submodule, e.g. fltk.Fl_Input_value in
# fltk._cfl_input): use re-export to locate the defining module, then query its vpi.
rr_src: char* = self.Gen.ImportsGen.lookup_reexport_call(mod_ret_sha1, func_name)
if rr_src is None: rr_src = self._lookup_reexport_from_vpi(pool, mod_ret_sha1, func_name)
if rr_src is None: rr_src = self._lookup_reexport_from_vpi(pool, mod_ret_sha1, func_name, 1)
if rr_src is not None:
rr_total: sizet = string.strlen(rr_src)
rr_i: sizet = 0
@@ -1300,7 +1326,7 @@ novirtual class CallGenMixin(CallGenBase.CallGenBase):
# table is keyed by the defining module SHA1, so re-resolve the re-export and query again (else defaults are skipped).
if gd_entry is None and dfx_callee_sha is not None:
rr_src_dx: char* = self.Gen.ImportsGen.lookup_reexport_call(dfx_callee_sha, func_name)
if rr_src_dx is None: rr_src_dx = self._lookup_reexport_from_vpi(pool, dfx_callee_sha, func_name)
if rr_src_dx is None: rr_src_dx = self._lookup_reexport_from_vpi(pool, dfx_callee_sha, func_name, 1)
if rr_src_dx is not None:
rr_sha_dx: char* = self._lookup_module_sha1(rr_src_dx)
if rr_sha_dx is not None: gd_entry = self.Gen.CallGenLeafKit.find_func_defaults(func_name, rr_sha_dx)
@@ -1355,7 +1381,10 @@ novirtual class CallGenMixin(CallGenBase.CallGenBase):
if can < df_param_count:
self.Gen.TypeGenErr.fatal_error(cl, f"Cross-module default argument cannot be rebuilt into a value (snapshot missing, or the default is not a simple constant): {func_name}; parameter {can + 1} would be missing, which emits an llvm.call with fewer operands than the callee declares", "TypeError")
elif found_func is not None:
# Zero-value fallback when defaults are missing: use found_func's real param count (excluding EH out-params %__eh_msg_out__/%__eh_code_out__) and fill missing args with zeros.
# Defaults first: the callee's own name carries the defining module
# ({sha1}.Class.method), so method defaults resolve through its vpi even when the
# call site lives in another class/module. Zero-filling without a recorded default
# silently changes program behaviour, so a short call without defaults is an error.
fc_cnt: int = 0
fc_p: mlirlite.Param* = mlirlite.function_get_param_head(found_func)
while fc_p is not None:
@@ -1365,22 +1394,14 @@ novirtual class CallGenMixin(CallGenBase.CallGenBase):
fc_p = mlirlite.param_get_next(fc_p)
if fc_cnt == 0: fc_cnt = df_param_count if df_param_count > 0 else 0
if can < fc_cnt:
fp_z: mlirlite.Param* = mlirlite.function_get_param_head(found_func)
fpi_z: int = 0
while fp_z is not None and fpi_z < can:
fp_z = mlirlite.param_get_next(fp_z)
fpi_z += 1
while fp_z is not None and fpi_z < fc_cnt:
zero_ty_z: mlirlite.LLVMType* = mlirlite.param_get_ty(fp_z)
zero_val_z: mlirlite.Value* = mlirlite.ConstZero(pool, zero_ty_z)
if zero_val_z is not None:
zero_val_z.Next = None
if head is None: head = zero_val_z
else: tail.Next = zero_val_z
tail = zero_val_z
can += 1 # Zero-value fallback args also count toward the arg count (like defaults).
fp_z = mlirlite.param_get_next(fp_z)
fpi_z += 1
fc_late: char* = mlirlite.function_get_name(found_func)
if fc_late is not None:
can = self._fill_defaults_late(pool, mod, builder, trans, cl, fc_late, head, can)
if head is not None:
tail = head
while tail.Next is not None: tail = tail.Next
if can < fc_cnt and not (string.strcmp(func_name, "memcpy") == 0 and can >= 3):
self.Gen.TypeGenErr.fatal_error(cl, f"call to '{func_name}' passes {can} argument(s) but the callee declares {fc_cnt}; no default value is recorded for the missing parameter(s) (refusing to zero-fill silently)", "TypeError")
# Use the actual function name from the module (may carry a SHA1 prefix); ExternT/State/ExportT
# functions use the bare name. Both declare and define must use found_func.Name (mismatched names cause undefined references).
@@ -1401,7 +1422,7 @@ novirtual class CallGenMixin(CallGenBase.CallGenBase):
if cx_sha1_buf is not None:
string.memcpy(cx_sha1_buf, found_name, 16)
cx_sha1_buf[16] = '\0'
if self.Gen.CallGenBase.is_cexport_func(cx_sha1_buf, func_name) != 0: call_name = func_name
if self._is_bare_module_symbol(pool, cx_sha1_buf, func_name) != 0: call_name = func_name
else:
# Cross-module call: when found_func is None, check for a module-qualified call (string.strchr -> @{sha1_of_string}.strchr).
if cl.func is not None and cl.func.kind() == ast.ASTKind.Attribute:
@@ -1433,7 +1454,7 @@ novirtual class CallGenMixin(CallGenBase.CallGenBase):
if reexport_src is None:
# Re-export table miss (includes modules with existing stub/text are not fully translated,
# so the table may be empty): resolve the module's .vpi from-import to locate the defining module.
reexport_src = self._lookup_reexport_from_vpi(pool, mod_sha1, func_name)
reexport_src = self._lookup_reexport_from_vpi(pool, mod_sha1, func_name, 1)
rs_found: int = 0
if reexport_src is not None:
# Star-import multiple candidates (space-separated, e.g. fltk/__init__ from _types/_enums/_cfl
@@ -1452,8 +1473,9 @@ novirtual class CallGenMixin(CallGenBase.CallGenBase):
cs_buf[cs_seg] = '\0'
cand_sha1: char* = self._lookup_module_sha1(cs_buf)
if cand_sha1 is None: cand_sha1 = self._lookup_module_sha1_suffix(cs_buf)
if cand_sha1 is None: cand_sha1 = self.Gen.CallGenBase._lookup_module_sha1_by_rel(pool, cs_buf)
if cand_sha1 is not None:
if self.Gen.CallGenBase.is_cexport_func(cand_sha1, func_name) != 0 or self._is_state_func_from_vpi(pool, cand_sha1, func_name) != 0:
if self._is_bare_module_symbol_deep(pool, cand_sha1, func_name) != 0:
mod_sha1 = cand_sha1
call_name = func_name
rs_found = 1
@@ -1461,7 +1483,7 @@ novirtual class CallGenMixin(CallGenBase.CallGenBase):
if rs_found == 0:
# All star-import candidates failed: func_name may be defined in the original module
# itself (e.g. stdlib.malloc, while stdlib's `from stdint import *` adds a false candidate).
if self.Gen.CallGenBase.is_cexport_func(mod_sha1, func_name) != 0 or self._is_state_func_from_vpi(pool, mod_sha1, func_name) != 0:
if self._is_bare_module_symbol_deep(pool, mod_sha1, func_name) != 0:
rs_found = 1
call_name = func_name
else:
@@ -1479,10 +1501,9 @@ novirtual class CallGenMixin(CallGenBase.CallGenBase):
# ExportT functions define with the bare name (@strlen), so the cross-module call must use the bare name too.
if rs_found != 0:
pass # Re-export defining module already verified and bare name set.
elif self.Gen.CallGenBase.is_cexport_func(mod_sha1, func_name) != 0: call_name = func_name
elif self._is_state_func_from_vpi(pool, mod_sha1, func_name) != 0:
# t.State external C function (e.g. CPython PyList_Size): the definition uses the bare
# name @PyList_Size, so the cross-module call must too (a sha1 prefix causes an undefined symbol).
elif self._is_bare_module_symbol(pool, mod_sha1, func_name) != 0:
# extern/export/state: the definition uses the bare name, so the
# cross-module call must too (a SHA1 prefix is an undefined symbol).
call_name = func_name
else:
mangled_len: sizet = string.strlen(mod_sha1) + string.strlen(func_name) + 2
@@ -1504,10 +1525,7 @@ novirtual class CallGenMixin(CallGenBase.CallGenBase):
alias_sha1: char* = None
if trans is not None and trans._from_imports is not None: alias_sha1 = self._lookup_mod_sha1(pool, trans._from_imports, xmod_path, 0)
if alias_sha1 is not None:
if self.Gen.CallGenBase.is_cexport_func(alias_sha1, func_name) != 0: call_name = func_name
elif self._is_state_func_from_vpi(pool, alias_sha1, func_name) != 0:
# t.State external C function: use the bare name (same as the main path).
call_name = func_name
if self._is_bare_module_symbol(pool, alias_sha1, func_name) != 0: call_name = func_name
else:
AliasMangledLen: sizet = string.strlen(alias_sha1) + string.strlen(func_name) + 2
alias_mangled: char* = pool.alloc(AliasMangledLen)
@@ -1528,7 +1546,7 @@ novirtual class CallGenMixin(CallGenBase.CallGenBase):
if trans is not None and trans._from_imports is not None: from_sha1 = self._lookup_mod_sha1(pool, trans._from_imports, xmod_path, 0)
if from_sha1 is not None:
# ExportT functions define with the bare name, so the cross-module call must use the bare name.
if self.Gen.CallGenBase.is_cexport_func(from_sha1, func_name) != 0: call_name = func_name
if self._is_bare_module_symbol(pool, from_sha1, func_name) != 0: call_name = func_name
else:
fmangled_len: sizet = string.strlen(from_sha1) + string.strlen(func_name) + 2
fmangled: char* = pool.alloc(fmangled_len)
@@ -1585,11 +1603,11 @@ novirtual class CallGenMixin(CallGenBase.CallGenBase):
reexport_src2: char* = self.Gen.ImportsGen.lookup_reexport_call(bare_sha1, bare_func_name)
if reexport_src2 is None:
# Re-export table miss: resolve the module's .vpi from-import on demand.
reexport_src2 = self._lookup_reexport_from_vpi(pool, bare_sha1, bare_func_name)
reexport_src2 = self._lookup_reexport_from_vpi(pool, bare_sha1, bare_func_name, 1)
if reexport_src2 is not None:
reexport_sha1_2: char* = self._lookup_module_sha1(reexport_src2)
if reexport_sha1_2 is not None: bare_sha1 = reexport_sha1_2
if self.Gen.CallGenBase.is_cexport_func(bare_sha1, bare_func_name) != 0: call_name = bare_func_name
if self._is_bare_module_symbol(pool, bare_sha1, bare_func_name) != 0: call_name = bare_func_name
else:
bare_mangled_len: sizet = string.strlen(bare_sha1) + string.strlen(bare_func_name) + 2
bare_mangled: char* = pool.alloc(bare_mangled_len)
+66 -1
View File
@@ -485,6 +485,67 @@ novirtual class ExprGenOps(ExprGenBase.ExprGenBase):
if annot is not None:
if self._spec_sign_from_annot(annot) == TSpec.TS_SIGN_UNSIGNED: return 1
return 0
# _translate_in_literal_container - `x in (a, b, c)` / `[a, b]` / `{a, b}`.
# A literal container cannot own __contains__, so the membership test is the
# short-circuit-free OR of `x == ai`. Returns None when rhs is not a literal
# container (the caller falls back to __contains__ / i1 0).
def _translate_in_literal_container(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, lhs_node: ast.AST*, rhs_node: ast.AST*, trans: HT.Translator*, invert: int) -> mlirlite.Value*:
if lhs_node is None or rhs_node is None: return None
k: int = rhs_node.kind()
elts: list[ast.AST*]* = None
if k == ast.ASTKind.Tuple: elts = (ast.Tuple*(rhs_node)).elts
elif k == ast.ASTKind.List: elts = (ast.List*(rhs_node)).elts
elif k == ast.ASTKind.Set: elts = (ast.Set*(rhs_node)).elts
else: return None
lhs: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, lhs_node, None, 0, trans)
if lhs is None: return None
acc: mlirlite.Value* = None
if elts is not None:
for ei in range(int(len(elts))):
el: ast.AST* = elts.get(ei)
ev: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, el, None, 0, trans)
if ev is None: return None
eq: mlirlite.Value* = self._eq_scalar_value(builder, pool, mod, lhs, ev, el, trans)
if eq is None: return None
if acc is None: acc = eq
else: acc = mlirlite.build_or(builder, acc, eq)
if acc is None:
i1_empty: mlirlite.LLVMType* = mlirlite.Int1(pool)
if i1_empty is None: return None
acc = mlirlite.ConstInt(pool, i1_empty, 0, "0")
if invert != 0:
one: mlirlite.Value* = mlirlite.ConstInt(pool, mlirlite.Int1(pool), 1, "1")
if one is not None: acc = mlirlite.build_xor(builder, acc, one)
return acc
# _eq_scalar_value - `lhs == rhs` for a literal container element.
# String literals compare by content (strcmp); everything else is coerced to the
# lhs type and compared with icmp eq.
def _eq_scalar_value(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, lhs: mlirlite.Value*, rhs: mlirlite.Value*, rhs_node: ast.AST*, trans: HT.Translator*) -> mlirlite.Value*:
if lhs is None or rhs is None: return None
is_str_lit: int = 0
if rhs_node is not None and rhs_node.kind() == ast.ASTKind.Constant:
rc: ast.Constant* = ast.Constant*(rhs_node)
if rc is not None and rc.const_kind == ast.CONST_STR: is_str_lit = 1
if is_str_lit != 0 and self.get_ptr_level(lhs.Ty) == 1:
fn: mlirlite.Function* = self.Gen.CallGenBase.find_func_in_module(mod, "strcmp")
if fn is None: fn = self.Gen.CallGenLib._ensure_c_lib_declare(pool, mod, "strcmp")
if fn is not None:
fn_name: str = mlirlite.function_get_name(fn)
i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool)
if fn_name is not None and i32_ty is not None:
lhs.Next = rhs
rhs.Next = None
r: mlirlite.Value* = mlirlite.build_call(builder, fn_name, lhs, 2, i32_ty, 0)
if r is not None:
zero: mlirlite.Value* = mlirlite.ConstInt(pool, i32_ty, 0, "0")
if zero is not None: return mlirlite.build_icmp(builder, mlirlite.ICMP_EQ, r, zero)
return None
tgt_ty: mlirlite.LLVMType* = lhs.Ty
tv: mlirlite.Value* = self.Gen.ExprGenBase.coerce_to_type(builder, rhs, tgt_ty, rhs_node, trans)
if tv is None: return None
return mlirlite.build_icmp(builder, mlirlite.ICMP_EQ, lhs, tv)
def translate_compare(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, node: ast.AST*, trans: HT.Translator* = None) -> mlirlite.Value*:
"""Translate a comparison expression and return an i1 result."""
cmp: ast.Compare* = ast.Compare*(node)
@@ -497,12 +558,16 @@ novirtual class ExprGenOps(ExprGenBase.ExprGenBase):
comparators: list[ast.AST*]* = cmp.comparators
if comparators is None or len(comparators) == 0: return None
rhs_node: ast.AST* = comparators.get(0)
lhs_node: ast.AST* = cmp.left
# Literal containers have no __contains__: lower `in` to an OR of equality tests.
if (op == ast.OpKind.In or op == ast.OpKind.NotIn) and lhs_node is not None and rhs_node is not None:
lit_in: mlirlite.Value* = self._translate_in_literal_container(builder, pool, mod, lhs_node, rhs_node, trans, 1 if op == ast.OpKind.NotIn else 0)
if lit_in is not None: return lit_in
rhs: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, rhs_node, None, 0, trans)
if rhs is None: return None
# === None comparison optimization: x == None / x != None use identity comparison (icmp eq/ne null) ===
# Avoids emitting invalid __eq__/__ne__ calls for struct pointers; Python treats == None as identity comparison.
lhs_node: ast.AST* = cmp.left
cmp_with_none: int = 0
if op == ast.OpKind.Eq or op == ast.OpKind.Ne or op == ast.OpKind.Is or op == ast.OpKind.IsNot:
if rhs_node is not None and rhs_node.kind() == ast.ASTKind.Constant: