fix(gen): 消除 ViperC 侧 P0 级静默截断

match:case 数超过 MAX_MATCH_CASES 时 fail-fast(原先 break 会静默少分支,need_case 检查也发现不了)。vtable:初始化串按真实方法表动态分配,_vtable_strcat 溢出改 fatal;VTBL_METHOD_MAX 超限不再 break(原先子类 override 静默失效)。符号名:coro_pool_/__tls_init_done_/__decor_rec_/闭包 fn_ptr 的定义与引用改为按实际长度分配,消除 96 vs 128、64 vs 128 的不对称。Stub:_PyiEmit 溢出改 VLogger.error(半截 .vpi 会被当完整文件解析);VpiReader 参数段/返回注解缓存槽放不下时 fail-fast,不再丢段导致形参注解错位。Includes:文件超过 1MiB 拒绝哈希(SHA1 冲突会让两个模块共用缓存);add_file_entry 失败不再被忽略。f-string:格式串按字面量/转换符实际长度分配,字面量与 format_spec 溢出 fail-fast。
This commit is contained in:
TermiNexus committed 2026-10-17 18:46:18 +08:00
1 parent 15952d3852
commit 3e1e1fc1b3
10 files changed
+183 -72

No files matched your search

+11 -6
View File
@@ -144,15 +144,20 @@ def _pyi_cdefine_found() -> int:
# ============================================================
# _PyiEmit - append text to buf at pos, returning the new pos (truncates on overflow)
# _PyiEmit - append text to buf at pos, returning the new pos (overflow is fatal)
def _PyiEmit(buf: bytes, buf_size: sizet, pos: sizet, text: str) -> sizet:
"""Append text to buf at pos and return the new pos"""
if buf is None or text is None or pos >= buf_size: return pos
"""Append text to buf at pos and return the new pos.
Overflow used to truncate silently, and the truncated .vpi was then parsed as if complete:
the consuming module silently lost declarations or got half a signature.
"""
if buf is None or text is None: return pos
tlen: sizet = string.strlen(text)
if tlen == 0: return pos
remain: sizet = buf_size - pos
if tlen >= remain: tlen = remain - 1
if tlen > 0: string.memcpy(buf + pos, text, tlen)
if pos + tlen + 1 > buf_size:
VLogger.error(f"generated .vpi stub text exceeds the {buf_size}-byte buffer; raise PYI_BUF_SIZE", "StubGen")
return pos
string.memcpy(buf + pos, text, tlen)
return pos + tlen
+13
View File
@@ -400,6 +400,11 @@ def _cache_vpi_return_annots_text(vpi_buf: bytes, src_sha1: str) -> int:
cls_len2: sizet = 0
if cur_cls is not None: cls_len2 = string.strlen(cur_cls)
key_len: sizet = sha1_len + 1 + cls_len2 + 1 + mname_len
# fail-fast: cache entries are fixed-size slots. Skipping a too-long key or
# return text leaves the method without a cached return type, and callers
# then fall back to i32 (pointer returns get truncated).
if key_len >= VPI_KEY_LEN or rtype_len >= VPI_RET_LEN:
VLogger.error(f"return-annotation cache slot too small (key {key_len}/{VPI_KEY_LEN} bytes, return {rtype_len}/{VPI_RET_LEN} bytes); shorten the class/method name or the return annotation", "VpiReader")
if key_len < VPI_KEY_LEN and rtype_len < VPI_RET_LEN:
# copy method name to a temp buffer (vpi_buf+dstart is not NUL-terminated)
mname_buf: bytes = stdlib.malloc(mname_len + 1)
@@ -575,6 +580,10 @@ def _cache_vpi_param_annots_text(vpi_buf: bytes, src_sha1: str) -> int:
while a_start < s_end and vpi_buf[a_start] == ' ': a_start += 1
a_end: sizet = s_end
while a_end > a_start and vpi_buf[a_end - 1] == ' ': a_end -= 1
# fail-fast: dropping a segment shifts every later parameter's annotation onto
# the wrong parameter (declare/define type mismatch), which is worse than a miss.
if plist_pos + (a_end - a_start) + 1 >= VPI_PARAM_LEN:
VLogger.error(f"parameter annotation list exceeds the {VPI_PARAM_LEN}-byte cache slot; shorten this method's annotations", "VpiReader")
if plist_pos + (a_end - a_start) + 1 < VPI_PARAM_LEN:
if seg_written > 0:
plist[plist_pos] = '@'
@@ -592,6 +601,10 @@ def _cache_vpi_param_annots_text(vpi_buf: bytes, src_sha1: str) -> int:
cls_len2: sizet = 0
if cur_cls is not None: cls_len2 = string.strlen(cur_cls)
key_len: sizet = sha1_len + 1 + cls_len2 + 1 + mname_len
if key_len >= VPI_PARAM_KEY_LEN:
# fail-fast: a skipped key silently loses the whole parameter annotation
# list (the caller then rebuilds declare params from the degraded text).
VLogger.error(f"parameter-annotation cache key exceeds {VPI_PARAM_KEY_LEN} bytes (class/method name too long)", "VpiReader")
if key_len < VPI_PARAM_KEY_LEN:
mname_buf: bytes = stdlib.malloc(mname_len + 1)
if mname_buf is not None:
+6 -6
View File
@@ -635,8 +635,8 @@ novirtual class AsyncGenMixin(CallGenBase.CallGenBase):
# M7 frame pool: i64 head (0=empty); first 8 bytes hold next (state/done zeroed on reuse)
pool_gv: mlirlite.GlobalVariable* = self._get_coro_pool(pool, mod, trans, name)
# GlobalVariable is not a Value: build a Value view (Ty=Ptr(i64), Name="@coro_pool_<name>")
pool_ref_name: str = pool.alloc(96)
if pool_ref_name is not None: viperlib.snprintf(pool_ref_name, 96, "@coro_pool_%s", name)
pool_ref_name: str = pool.alloc(string.strlen(name) + 16)
if pool_ref_name is not None: viperlib.snprintf(pool_ref_name, string.strlen(name) + 16, "@coro_pool_%s", name)
pool_ref: mlirlite.Value* = mlirlite.SSAValue(pool, i64_ptr_ty, pool_ref_name)
pool_head: mlirlite.Value* = mlirlite.build_load(builder, i64_ty, pool_ref)
if pool_head is None: return 0
@@ -700,8 +700,8 @@ novirtual class AsyncGenMixin(CallGenBase.CallGenBase):
# M7: per-frame-type global free-list pool head (i64, 0 = empty)
def _get_coro_pool(self, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, trans: HT.Translator*, name: str) -> mlirlite.GlobalVariable*:
"""Get or create the coro_pool_<name> global (i64 pool head)"""
pn_buf: str = pool.alloc(128)
if pn_buf is not None: viperlib.snprintf(pn_buf, 128, "coro_pool_%s", name)
pn_buf: str = pool.alloc(string.strlen(name) + 16)
if pn_buf is not None: viperlib.snprintf(pn_buf, string.strlen(name) + 16, "coro_pool_%s", name)
# idempotent: create/destroy both call this, so scan the module global list (no find_global API)
g: mlirlite.GlobalVariable* = mod.GlobalHead
while g is not None:
@@ -1664,8 +1664,8 @@ novirtual class AsyncGenMixin(CallGenBase.CallGenBase):
pool_gv: mlirlite.GlobalVariable* = self._get_coro_pool(pool, mod, trans, async_name)
# GlobalVariable is not a Value: build a Value view (Ty=Ptr(i64), Name="@coro_pool_<name>")
i64_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, i64_ty)
pool_ref_name: str = pool.alloc(96)
if pool_ref_name is not None: viperlib.snprintf(pool_ref_name, 96, "@coro_pool_%s", async_name)
pool_ref_name: str = pool.alloc(string.strlen(async_name) + 16)
if pool_ref_name is not None: viperlib.snprintf(pool_ref_name, string.strlen(async_name) + 16, "@coro_pool_%s", async_name)
pool_ref: mlirlite.Value* = mlirlite.SSAValue(pool, i64_ptr_ty, pool_ref_name)
pool_head: mlirlite.Value* = mlirlite.build_load(builder, i64_ty, pool_ref)
node_next: mlirlite.Value* = mlirlite.build_bitcast(builder, frame_ptr, i64_ptr_ty)
+52 -26
View File
@@ -28,16 +28,21 @@ define SCOPE_FUNCTION = 1
novirtual class ClassGenVTable(ClassGenBase.ClassGenBase):
"""vtable collection, generation, and method translation."""
# _vtable_strcat - append src to the end of dst.
# _vtable_strcat - append src to the end of dst; fail-fast when it does not fit.
def _vtable_strcat(self, dst: str, dst_size: sizet, src: str):
"""Append src to the end of dst."""
"""Append src to the end of dst.
A truncated vtable initializer stays plausible LLVM syntax but is cut in the middle of a
bitcast, which then surfaces as a verifier error pointing at an unrelated method.
"""
if dst is None or src is None: return
dlen: sizet = string.strlen(dst)
slen: sizet = string.strlen(src)
remain: sizet = dst_size - dlen
if remain <= 0: return
if dlen + slen + 1 > dst_size:
self.Gen.TypeGenErr.fatal_error(None, f"vtable initializer text exceeds its buffer: {dlen} byte(s) used + {slen} byte(s) to append + 1 NUL > capacity {dst_size} byte(s); the class has too many virtual methods for the fixed buffer", "OverflowError")
return
i: sizet = 0
while i < slen and i + 1 < remain:
while i < slen:
dst[dlen + i] = src[i]
i += 1
dst[dlen + i] = '\0'
@@ -92,7 +97,11 @@ novirtual class ClassGenVTable(ClassGenBase.ClassGenBase):
if vt_parent_entry is not None: parent_vt_count = vt_parent_entry.VTableMethodCount
if parent_vt_count > 0:
for pvi in range(int(parent_vt_count)):
if method_count >= VTBL_METHOD_MAX: break
if method_count >= VTBL_METHOD_MAX:
# fail-fast: the VTableMethods arrays are fixed at VTBL_METHOD_MAX;
# dropping the excess makes call sites miss the slot and silently fall
# back to a static call of the base implementation.
self.Gen.TypeGenErr.fatal_error(cd, f"class '{class_name}' inherits more than {VTBL_METHOD_MAX} virtual methods (compiler limit); raise VTBL_METHOD_MAX", "OverflowError")
pmethod_name: str = None
if vt_parent_entry is not None: pmethod_name = self.Gen.StructGen.get_vtable_method_name(pname, pvi)
if pmethod_name is None: continue
@@ -160,7 +169,10 @@ novirtual class ClassGenVTable(ClassGenBase.ClassGenBase):
if stmt.kind() != ast.ASTKind.FunctionDef: continue
fd: ast.FunctionDef* = ast.FunctionDef*(stmt)
if fd is None or fd.name is None: continue
if method_count >= VTBL_METHOD_MAX: break
if method_count >= VTBL_METHOD_MAX:
# fail-fast: see the inheritance branch above; a dropped method silently loses
# its vtable slot (subclass override stops taking effect).
self.Gen.TypeGenErr.fatal_error(cd, f"class '{class_name}' declares more than {VTBL_METHOD_MAX} virtual methods (compiler limit); raise VTBL_METHOD_MAX", "OverflowError")
mname: str = fd.name
# Skip constructors and special methods (they must not enter the vtable).
@@ -247,18 +259,28 @@ novirtual class ClassGenVTable(ClassGenBase.ClassGenBase):
vtable_ty: mlirlite.LLVMType* = mlirlite.Struct(pool, first_node, method_count, None)
if vtable_ty is None: return 0
# Build the initializer string; 16384 bytes so large vtables (MemBuddy 28+) are not truncated.
init_buf: str = pool.alloc(16384)
# Size the initializer from the real method list: each entry is
# "i8* bitcast (<func ty>* @<mangled> to i8*)", where the function type text alone can
# reach 1024 bytes and the mangled name carries a SHA1 prefix. The old fixed 16384 bytes
# silently cut long vtables in the middle of a bitcast.
init_cap: sizet = 64
for si in range(int(method_count)):
name_addr0: sizet = full_names_buf[si]
if name_addr0 == 0:
init_cap = init_cap + 12
else:
init_cap = init_cap + 1024 + 96 + string.strlen(rawptr(name_addr0)) + 128
init_buf: str = pool.alloc(init_cap)
if init_buf is None: return 0
init_buf[0] = '\0'
self._vtable_strcat(init_buf, 16384, "{ ")
self._vtable_strcat(init_buf, init_cap, "{ ")
for i in range(int(method_count)):
if i > 0: self._vtable_strcat(init_buf, 16384, ", ")
if i > 0: self._vtable_strcat(init_buf, init_cap, ", ")
full_name_addr: sizet = full_names_buf[i]
if full_name_addr == 0:
self._vtable_strcat(init_buf, 16384, "i8* null")
self._vtable_strcat(init_buf, init_cap, "i8* null")
continue
full_name: str = rawptr(full_name_addr)
@@ -278,7 +300,7 @@ novirtual class ClassGenVTable(ClassGenBase.ClassGenBase):
ge: GenTypes.FuncEntry* = self.Gen.CallGenLeafKit.find_func_entry_global(full_name)
func_ty_buf: str = pool.alloc(1024)
if func_ty_buf is None:
self._vtable_strcat(init_buf, 16384, "i8* null")
self._vtable_strcat(init_buf, init_cap, "i8* null")
continue
func_ty_buf[0] = '\0'
if ge is None or ge.Spec is None or TSpec.SpecToLLVMText(ge.Spec, func_ty_buf, 1024) != 0:
@@ -286,18 +308,18 @@ novirtual class ClassGenVTable(ClassGenBase.ClassGenBase):
continue
# Build the bitcast string: i8* bitcast (func_ty* @mangled to i8*).
self._vtable_strcat(init_buf, 16384, "i8* bitcast (")
self._vtable_strcat(init_buf, 16384, func_ty_buf)
self._vtable_strcat(init_buf, 16384, "* @")
self._vtable_strcat(init_buf, init_cap, "i8* bitcast (")
self._vtable_strcat(init_buf, init_cap, func_ty_buf)
self._vtable_strcat(init_buf, init_cap, "* @")
if self._vtable_name_needs_quote(mangled_name) != 0:
self._vtable_strcat(init_buf, 16384, "\"")
self._vtable_strcat(init_buf, 16384, mangled_name)
self._vtable_strcat(init_buf, 16384, "\"")
else: self._vtable_strcat(init_buf, 16384, mangled_name)
self._vtable_strcat(init_buf, 16384, " to i8*)")
self._vtable_strcat(init_buf, init_cap, "\"")
self._vtable_strcat(init_buf, init_cap, mangled_name)
self._vtable_strcat(init_buf, init_cap, "\"")
else: self._vtable_strcat(init_buf, init_cap, mangled_name)
self._vtable_strcat(init_buf, init_cap, " to i8*)")
self._vtable_strcat(init_buf, 16384, " }")
self._vtable_strcat(init_buf, init_cap, " }")
# 4. Build the vtable global variable name.
vtable_name_buf: str = pool.alloc(128)
@@ -694,18 +716,22 @@ novirtual class ClassGenVTable(ClassGenBase.ClassGenBase):
# @t.TLS one-time init guard (same implementation as FuncGenMixin.translate_function_def, also for methods).
if is_tls_m != 0:
tls_m_flag_name: str = pool.alloc(128)
# Exact capacities: the reference used to be 64 while the definition was 128, so a long
# method name produced a reference to a different (nonexistent) global.
tls_m_flag_cap: sizet = string.strlen(method_name) + 24
tls_m_flag_name: str = pool.alloc(tls_m_flag_cap)
if tls_m_flag_name is not None:
viperlib.snprintf(tls_m_flag_name, 128, "__tls_init_done_%s", method_name)
viperlib.snprintf(tls_m_flag_name, tls_m_flag_cap, "__tls_init_done_%s", method_name)
tls_m_gv: mlirlite.GlobalVariable* = mlirlite.create_global_int(pool, mod, tls_m_flag_name, 1, 0)
if tls_m_gv is not None:
i1_ty_m: mlirlite.LLVMType* = mlirlite.Int1(pool)
if i1_ty_m is not None:
# GlobalVariable is not a Value subclass: build a Value view before load/store.
tls_m_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, i1_ty_m)
tls_m_rn: str = pool.alloc(64)
tls_m_rn_cap: sizet = string.strlen(tls_m_flag_name) + 2
tls_m_rn: str = pool.alloc(tls_m_rn_cap)
if tls_m_rn is not None:
viperlib.snprintf(tls_m_rn, 64, "@%s", tls_m_flag_name)
viperlib.snprintf(tls_m_rn, tls_m_rn_cap, "@%s", tls_m_flag_name)
tls_m_ref: mlirlite.Value* = mlirlite.SSAValue(pool, tls_m_ptr_ty, tls_m_rn)
if tls_m_ref is not None:
tls_m_ld: mlirlite.Value* = mlirlite.build_load(func_builder, i1_ty_m, tls_m_ref)
+47 -19
View File
@@ -26,8 +26,25 @@ novirtual class ExprGenFstr(ExprGenBase.ExprGenBase):
js: ast.JoinedStr* = ast.JoinedStr*(node)
if js is None or js.values is None: return None
parts: list[ast.AST*]* = js.values
pn: sizet = len(parts)
# Size the format buffer from the actual parts: each literal may double ('%' -> '%%') and
# each conversion contributes a short spec. A fixed 1024 silently dropped trailing
# literals/converters while their arguments were still appended -> wrong runtime output.
fmt_cap: sizet = 16
for pi0 in range(int(pn)):
part0: ast.AST* = parts.get(pi0)
if part0 is None: continue
if part0.kind() == ast.ASTKind.Constant:
pc0: ast.Constant* = ast.Constant*(part0)
if pc0.const_kind == ast.CONST_STR and pc0.str_val is not None:
fmt_cap = fmt_cap + string.strlen(pc0.str_val) * 2
elif part0.kind() == ast.ASTKind.FormattedValue:
fmt_cap = fmt_cap + FSTR_SPEC_MAX + 16
# Compile-time format string buffer (pool-allocated)
fmt_buf: str = pool.alloc(FSTR_BUF_SIZE)
fmt_buf: str = pool.alloc(fmt_cap)
if fmt_buf is None: return None
fmt_buf[0] = '\0'
@@ -36,15 +53,13 @@ novirtual class ExprGenFstr(ExprGenBase.ExprGenBase):
arg_tail: mlirlite.Value* = None
arg_count: int = 0
parts: list[ast.AST*]* = js.values
pn: sizet = len(parts)
for pi in range(int(pn)):
part: ast.AST* = parts.get(pi)
if part is None: continue
if part.kind() == ast.ASTKind.Constant:
pcn: ast.Constant* = ast.Constant*(part)
if pcn.const_kind == ast.CONST_STR and pcn.str_val is not None:
self._fstr_append_literal(fmt_buf, pcn.str_val)
self._fstr_append_literal(fmt_buf, fmt_cap, pcn.str_val)
continue
if part.kind() != ast.ASTKind.FormattedValue: continue
fv: ast.FormattedValue* = ast.FormattedValue*(part)
@@ -66,7 +81,7 @@ novirtual class ExprGenFstr(ExprGenBase.ExprGenBase):
# fail-fast: cannot infer the f-string format (missing value type); abort
self.Gen.TypeGenErr.fatal_error(fv.value, "f-string format cannot be inferred (value type missing)", "TypeError")
# Append the format specifier
if string.strlen(fmt_buf) + string.strlen(c_fmt) < FSTR_BUF_SIZE - 1: string.strcat(fmt_buf, c_fmt)
self._fstr_append_raw(fmt_buf, fmt_cap, c_fmt)
# Convert the argument type (i8/i16->i32, bool->i32, f32->f64)
cast_val: mlirlite.Value* = self._fstr_cast_value(builder, pool, val, c_fmt, fv.value, trans)
if cast_val is None: cast_val = val
@@ -82,7 +97,10 @@ novirtual class ExprGenFstr(ExprGenBase.ExprGenBase):
if node is None or builder is None or trans is None: return None
fv: ast.FormattedValue* = ast.FormattedValue*(node)
if fv is None or fv.value is None: return None
fmt_buf: str = pool.alloc(FSTR_BUF_SIZE)
# A bare FormattedValue contributes exactly one conversion spec (bounded by FSTR_SPEC_MAX
# plus the C prefix/width); size the buffer from that instead of the fixed 1024.
fmt_cap: sizet = FSTR_SPEC_MAX + 64
fmt_buf: str = pool.alloc(fmt_cap)
if fmt_buf is None: return None
fmt_buf[0] = '\0'
arg_head: mlirlite.Value* = None
@@ -103,7 +121,7 @@ novirtual class ExprGenFstr(ExprGenBase.ExprGenBase):
if c_fmt is None:
# fail-fast: cannot infer the f-string format (missing value type); abort
self.Gen.TypeGenErr.fatal_error(fv.value, "f-string format cannot be inferred (value type missing)", "TypeError")
if string.strlen(fmt_buf) + string.strlen(c_fmt) < FSTR_BUF_SIZE - 1: string.strcat(fmt_buf, c_fmt)
self._fstr_append_raw(fmt_buf, fmt_cap, c_fmt)
cast_val: mlirlite.Value* = self._fstr_cast_value(builder, pool, val, c_fmt, fv.value, trans)
if cast_val is None: cast_val = val
cast_val.Next = None
@@ -149,34 +167,38 @@ novirtual class ExprGenFstr(ExprGenBase.ExprGenBase):
if arg_tail is not None: arg_tail.Next = None
mlirlite.build_call(builder, "snprintf", buf_ptr, 3 + arg_count, i32_ty, 1)
return buf_ptr
def _fstr_append_literal(self, fmt_buf: str, lit: str) -> None:
"""Append a literal to the format string, escaping percent signs."""
def _fstr_append_literal(self, fmt_buf: str, cap: sizet, lit: str) -> None:
"""Append a literal to the format string, escaping percent signs; fail-fast on overflow."""
if fmt_buf is None or lit is None: return
cur_len: sizet = string.strlen(fmt_buf)
rem: sizet = FSTR_BUF_SIZE - 1 - cur_len
if rem == 0: return
src_len: sizet = string.strlen(lit)
# Worst case: every character expands to "%%".
if cur_len + src_len * 2 + 1 > cap:
self.Gen.TypeGenErr.fatal_error(None, f"f-string format text exceeds its {cap}-byte buffer ({cur_len} used + up to {src_len * 2} escaped + 1 NUL); shorten the literal", "OverflowError")
si: sizet = 0
while si < src_len and rem > 0:
while si < src_len:
ch: char = lit[si]
if ch == '%':
if rem < 2: break
fmt_buf[cur_len] = '%'
fmt_buf[cur_len + 1] = '%'
cur_len += 2
rem -= 2
else:
fmt_buf[cur_len] = ch
cur_len += 1
rem -= 1
si += 1
fmt_buf[cur_len] = '\0'
def _fstr_append_raw(self, fmt_buf: str, s: str) -> None:
"""Append a raw string without percent escaping."""
def _fstr_append_raw(self, fmt_buf: str, cap: sizet, s: str) -> None:
"""Append a raw string without percent escaping; fail-fast on overflow."""
if fmt_buf is None or s is None: return
cur_len: sizet = string.strlen(fmt_buf)
src_len: sizet = string.strlen(s)
if cur_len + src_len < FSTR_BUF_SIZE: string.strcpy(fmt_buf + cur_len, s)
if cur_len + src_len + 1 > cap:
self.Gen.TypeGenErr.fatal_error(None, f"f-string format text exceeds its {cap}-byte buffer ({cur_len} used + {src_len} to append + 1 NUL); shorten the format spec", "OverflowError")
i: sizet = 0
while i < src_len:
fmt_buf[cur_len + i] = s[i]
i += 1
fmt_buf[cur_len + src_len] = '\0'
def _fstr_parse_format_spec(self, pool: memhub.MemBuddy*, spec_node: ast.AST*, val: mlirlite.Value*, arg_node: ast.AST*) -> str:
"""Convert a Python format_spec into a C printf format string."""
if spec_node is None or pool is None or val is None: return None
@@ -195,10 +217,16 @@ novirtual class ExprGenFstr(ExprGenBase.ExprGenBase):
if vpart.kind() != ast.ASTKind.Constant: return None
vcn: ast.Constant* = ast.Constant*(vpart)
if vcn.const_kind != ast.CONST_STR or vcn.str_val is None: return None
if string.strlen(spec) + string.strlen(vcn.str_val) < FSTR_SPEC_MAX - 1: string.strcat(spec, vcn.str_val)
# fail-fast: a truncated spec can still look valid (e.g. ">10.5f" cut to ">10."),
# which silently changes the output format.
if string.strlen(spec) + string.strlen(vcn.str_val) >= FSTR_SPEC_MAX - 1:
self.Gen.TypeGenErr.fatal_error(arg_node, f"f-string format spec exceeds {FSTR_SPEC_MAX - 1} characters; shorten it", "OverflowError")
string.strcat(spec, vcn.str_val)
elif sk == ast.ASTKind.Constant:
cn: ast.Constant* = ast.Constant*(spec_node)
if cn.const_kind != ast.CONST_STR or cn.str_val is None: return None
if string.strlen(cn.str_val) >= FSTR_SPEC_MAX - 1:
self.Gen.TypeGenErr.fatal_error(arg_node, f"f-string format spec exceeds {FSTR_SPEC_MAX - 1} characters; shorten it", "OverflowError")
string.strncpy(spec, cn.str_val, FSTR_SPEC_MAX - 1)
else: return None
slen: sizet = string.strlen(spec)
+16 -8
View File
@@ -599,9 +599,12 @@ novirtual class FuncGenDef(CallGenBase.CallGenBase):
# @t.TLS one-time-init guard (aligned with TPC HandlesFunctions.py): the first call runs the body and sets
# __tls_init_done_{name}=1; later calls return the default. Without it every call re-runs TlsAlloc and loses state.
if has_tls != 0:
tls_flag_name: str = pool.alloc(128)
# Exact capacity: the reference below used to be 64 while the definition was 128, so a
# long function name produced a reference to a different (nonexistent) global.
tls_flag_cap: sizet = string.strlen(fd.name) + 24
tls_flag_name: str = pool.alloc(tls_flag_cap)
if tls_flag_name is not None:
viperlib.snprintf(tls_flag_name, 128, "__tls_init_done_%s", fd.name)
viperlib.snprintf(tls_flag_name, tls_flag_cap, "__tls_init_done_%s", fd.name)
tls_flag_gv: mlirlite.GlobalVariable* = mlirlite.create_global_int(pool, mod, tls_flag_name, 1, 0)
if tls_flag_gv is not None:
i1_ty_t: mlirlite.LLVMType* = mlirlite.Int1(pool)
@@ -609,9 +612,10 @@ novirtual class FuncGenDef(CallGenBase.CallGenBase):
# GlobalVariable is not a Value subclass (different field layout), so build_load/store reading
# ptr.Ty/ptr.Name as a Value misaligns. As in create_global_string, build a Value view (Ty=Ptr(i1), Name="@name").
tls_gv_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, i1_ty_t)
tls_ref_name: str = pool.alloc(64)
tls_ref_cap: sizet = string.strlen(tls_flag_name) + 2
tls_ref_name: str = pool.alloc(tls_ref_cap)
if tls_ref_name is not None:
viperlib.snprintf(tls_ref_name, 64, "@%s", tls_flag_name)
viperlib.snprintf(tls_ref_name, tls_ref_cap, "@%s", tls_flag_name)
tls_flag_ref: mlirlite.Value* = mlirlite.SSAValue(pool, tls_gv_ptr_ty, tls_ref_name)
if tls_flag_ref is not None:
tls_flag_ld: mlirlite.Value* = mlirlite.build_load(func_builder, i1_ty_t, tls_flag_ref)
@@ -994,13 +998,17 @@ novirtual class FuncGenDef(CallGenBase.CallGenBase):
# Recursion guard flag (outermost): @__decor_rec_{wrapper} = global i8 0
rec_flag_ref: mlirlite.Value* = None
rec_flag_name: str = pool.alloc(128)
# Exact capacities: definition and reference used different fixed sizes (128 vs 96), so a
# long wrapper name produced two different globals (guard silently bypassed / undefined).
rec_flag_cap: sizet = string.strlen(wrapper_name) + 24
rec_flag_name: str = pool.alloc(rec_flag_cap)
if rec_flag_name is not None:
viperlib.snprintf(rec_flag_name, 128, "__decor_rec_%s", wrapper_name)
viperlib.snprintf(rec_flag_name, rec_flag_cap, "__decor_rec_%s", wrapper_name)
mlirlite.create_global_int(pool, mod, rec_flag_name, 8, 0)
rec_ref_nm: str = pool.alloc(96)
rec_ref_cap: sizet = string.strlen(rec_flag_name) + 2
rec_ref_nm: str = pool.alloc(rec_ref_cap)
if rec_ref_nm is not None:
viperlib.snprintf(rec_ref_nm, 96, "@%s", rec_flag_name)
viperlib.snprintf(rec_ref_nm, rec_ref_cap, "@%s", rec_flag_name)
rec_flag_ref = mlirlite.SSAValue(pool, mlirlite.Ptr(pool, i8_ty), rec_ref_nm)
wb: mlirlite.IRBuilder* = mlirlite.new_builder(pool, wrapper)
+7 -4
View File
@@ -381,12 +381,15 @@ novirtual class FuncGenMixin(CallGenBase.CallGenBase):
fn_func_ty: mlirlite.LLVMType* = mlirlite.Func(pool, i32_ty, fn_param_node, 1)
fn_func_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, fn_func_ty)
# Create the function reference Value (type i32(i8*)*, using the SHA1 mangled name)
fn_ptr_name: str = pool.alloc(64)
# Create the function reference Value (type i32(i8*)*, using the SHA1 mangled name).
# Exact capacity: the mangled name (SHA1 prefix + closure name) can exceed the old 64-byte
# buffer, and a truncated name makes closure slot 0 point at a nonexistent symbol.
fn_ptr_cap: sizet = string.strlen(mangled_promoted) + 4
fn_ptr_name: str = pool.alloc(fn_ptr_cap)
if fn_ptr_name is not None:
# SHA1-prefixed names contain '.' so they need quoting (e.g. @"sha1.__closure_func")
if string.strchr(mangled_promoted, '.') is not None: viperlib.snprintf(fn_ptr_name, 64, "@\"%s\"", mangled_promoted)
else: viperlib.snprintf(fn_ptr_name, 64, "@%s", mangled_promoted)
if string.strchr(mangled_promoted, '.') is not None: viperlib.snprintf(fn_ptr_name, fn_ptr_cap, "@\"%s\"", mangled_promoted)
else: viperlib.snprintf(fn_ptr_name, fn_ptr_cap, "@%s", mangled_promoted)
fn_ptr_val: mlirlite.Value* = mlirlite.SSAValue(pool, fn_func_ptr_ty, fn_ptr_name)
# bitcast the function pointer to i8* (the closure stores i8*)
+10 -1
View File
@@ -326,7 +326,16 @@ novirtual class ImportsGenMixin:
# for `from .X import Y`). Idempotent; memory is stdlib.malloc'd to persist across phases.
def register_reexport(self, mod_sha1: str, func_name: str, source_module: str) -> int:
"""Register a re-export mapping; returns 0 on success, -1 on failure."""
if mod_sha1 is None or func_name is None or source_module is None: return -1
if mod_sha1 is None or func_name is None or source_module is None: return -1
# fail-fast: the table is a flat slab with fixed slots (17 bytes for the SHA1, 64 for the
# name/module). strncpy used to clamp silently, after which lookup_reexport's full-name
# strcmp never matched (or matched another module sharing the 63-byte prefix).
if string.strlen(mod_sha1) > 16:
self.Gen.TypeGenErr.fatal_error(None, "re-export module SHA1 is longer than its 16-byte table slot", "OverflowError")
if string.strlen(func_name) > 62:
self.Gen.TypeGenErr.fatal_error(None, f"re-exported name '{func_name}' exceeds the 62-byte table slot; shorten the identifier", "OverflowError")
if string.strlen(source_module) > 62:
self.Gen.TypeGenErr.fatal_error(None, f"re-export source module '{source_module}' exceeds the 62-byte table slot; shorten the module name", "OverflowError")
# Lazy initialization
if self.ReexportSha1s is None:
self.ReexportSha1s = CompilerFactory.factory.PermPool.alloc(MAX_REEXPORT * 17)
+6 -1
View File
@@ -343,6 +343,10 @@ novirtual class MatchGenMixin:
# fail-fast: match cases missing.
self.Gen.TypeGenErr.fatal_error(node, "match cases missing", "TypeError")
case_count: sizet = len(cases_list)
# fail-fast: the dispatch arrays are fixed at MAX_MATCH_CASES; silently dropping the excess
# arms produced a switch with fewer branches (wrong dispatch, no diagnostic).
if case_count > sizet(MAX_MATCH_CASES):
self.Gen.TypeGenErr.fatal_error(node, f"match has {case_count} cases but the compiler limit is {MAX_MATCH_CASES}; split the match statement", "OverflowError")
# BB pointer array, case-value pointer array, default-flag array, nobreak-flag array.
bb_arr: sizet* = pool.alloc(8 * MAX_MATCH_CASES)
@@ -432,7 +436,8 @@ novirtual class MatchGenMixin:
# Create the case BB.
if is_def == 0:
if collected >= MAX_MATCH_CASES: break
if collected >= MAX_MATCH_CASES:
self.Gen.TypeGenErr.fatal_error(node, f"match case count exceeds the compiler limit {MAX_MATCH_CASES}; split the match statement", "OverflowError")
viperlib.snprintf(name_buf, 32, "match.case.%d.%d", cnt, i)
bb: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, name_buf)
else: bb = default_bb
+15 -1
View File
@@ -6,6 +6,7 @@ import stdio
import stdlib
import win32.win32file as win32file
import win32.win32base as win32base
import win32.fileio as fileio
import hashlib
import viperlib
import lib.Projectrans.Utils as Utils
@@ -119,6 +120,16 @@ def compute_file_sha1(pool: memhub.MemBuddy*, file_path: str) -> str:
# read file content; BUF_SIZE must match SRC_BUF_SIZE (1MB) or trailing edits won't change SHA1
BUF_SIZE: sizet = 1048576
# Refuse larger files: hashing only the first 1 MiB gives two files that differ later the same
# SHA1, and the SHA1 keys the stub/cache files (colliding modules would reuse each other's IR).
sf_sz: fileio.File* = fileio.File(file_path, fileio.MODE.R)
if sf_sz.closed: return None
too_big: int = 0
if sf_sz.size() >= BUF_SIZE: too_big = 1
sf_sz.close()
if too_big != 0:
VLogger.error(f"source file exceeds the {BUF_SIZE}-byte limit and cannot be hashed completely: {file_path}", "IncludesScanner")
return None
buf: bytes = Utils.read_file_to_buf(file_path, BUF_SIZE)
if buf is None: return None
@@ -231,7 +242,10 @@ def scan_directory_recursive(pool: memhub.MemBuddy*, root_dir: str, rel_prefix:
# compute SHA1
sha1: str = compute_file_sha1(pool, full_path)
if sha1 is not None:
add_file_entry(result, pool, full_path, rel_path, sha1, inc_dir)
# fail-fast: a full table used to drop the file silently, so it never got a
# SHA1 entry / dependency edge and was therefore never compiled.
if add_file_entry(result, pool, full_path, rel_path, sha1, inc_dir) != 0:
VLogger.error(f"includes scan could not register '{full_path}' (the {MAX_FILES}-file capacity or an allocation failed); raise MAX_FILES", "scan")
VLogger.debug(f"scan: {rel_path} -> {sha1}", "scan")
else: VLogger.warning(f"SHA1 computation failed: {full_path}", "scan")