From 83d15289efe3b582d14ee0bf05bf331de432dc6b Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Sun, 1 Nov 2026 00:05:44 +0800 Subject: [PATCH] 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 --- App/lib/Stub/StubGen/VpiReader.vp | 207 ++++++++++---------------- App/lib/Stub/StubGen/VpiStub.vp | 84 +++-------- App/lib/Stub/StubGen/VpiText.vp | 122 +++++++++++++++ App/lib/core/Gen/Call/CallGenBase.vp | 54 ++++++- App/lib/core/Gen/Call/CallGenMixin.vp | 100 ++++++++----- App/lib/core/Gen/Expr/ExprGenOps.vp | 67 ++++++++- 6 files changed, 401 insertions(+), 233 deletions(-) create mode 100644 App/lib/Stub/StubGen/VpiText.vp diff --git a/App/lib/Stub/StubGen/VpiReader.vp b/App/lib/Stub/StubGen/VpiReader.vp index e0dc0e0..69f38c8 100644 --- a/App/lib/Stub/StubGen/VpiReader.vp +++ b/App/lib/Stub/StubGen/VpiReader.vp @@ -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 diff --git a/App/lib/Stub/StubGen/VpiStub.vp b/App/lib/Stub/StubGen/VpiStub.vp index eef1064..d25d953 100644 --- a/App/lib/Stub/StubGen/VpiStub.vp +++ b/App/lib/Stub/StubGen/VpiStub.vp @@ -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) diff --git a/App/lib/Stub/StubGen/VpiText.vp b/App/lib/Stub/StubGen/VpiText.vp new file mode 100644 index 0000000..2457c3a --- /dev/null +++ b/App/lib/Stub/StubGen/VpiText.vp @@ -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 diff --git a/App/lib/core/Gen/Call/CallGenBase.vp b/App/lib/core/Gen/Call/CallGenBase.vp index 6a978d4..33fc373 100644 --- a/App/lib/core/Gen/Call/CallGenBase.vp +++ b/App/lib/core/Gen/Call/CallGenBase.vp @@ -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 diff --git a/App/lib/core/Gen/Call/CallGenMixin.vp b/App/lib/core/Gen/Call/CallGenMixin.vp index fcc47f6..e845cab 100644 --- a/App/lib/core/Gen/Call/CallGenMixin.vp +++ b/App/lib/core/Gen/Call/CallGenMixin.vp @@ -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) diff --git a/App/lib/core/Gen/Expr/ExprGenOps.vp b/App/lib/core/Gen/Expr/ExprGenOps.vp index 6316b15..011e2ad 100644 --- a/App/lib/core/Gen/Expr/ExprGenOps.vp +++ b/App/lib/core/Gen/Expr/ExprGenOps.vp @@ -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: