From 3e1e1fc1b37a1c09c8608945474d7286b5be748a Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Sat, 17 Oct 2026 18:46:18 +0800 Subject: [PATCH] =?UTF-8?q?fix(gen):=20=E6=B6=88=E9=99=A4=20ViperC=20?= =?UTF-8?q?=E4=BE=A7=20P0=20=E7=BA=A7=E9=9D=99=E9=BB=98=E6=88=AA=E6=96=AD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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。 --- App/lib/Stub/StubGen/Converter.vp | 17 ++++-- App/lib/Stub/StubGen/VpiReader.vp | 13 ++++ App/lib/core/Gen/AsyncGenMixin.vp | 12 ++-- App/lib/core/Gen/Class/ClassGenVTable.vp | 78 ++++++++++++++++-------- App/lib/core/Gen/Expr/ExprGenFstr.vp | 66 ++++++++++++++------ App/lib/core/Gen/Func/FuncGenDef.vp | 24 +++++--- App/lib/core/Gen/Func/FuncGenMixin.vp | 11 ++-- App/lib/core/Gen/ImportsGenMixin.vp | 11 +++- App/lib/core/Gen/MatchGenMixin.vp | 7 ++- App/lib/core/IncludesScanner.vp | 16 ++++- 10 files changed, 183 insertions(+), 72 deletions(-) diff --git a/App/lib/Stub/StubGen/Converter.vp b/App/lib/Stub/StubGen/Converter.vp index bdc7dc5..c3d823f 100644 --- a/App/lib/Stub/StubGen/Converter.vp +++ b/App/lib/Stub/StubGen/Converter.vp @@ -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 diff --git a/App/lib/Stub/StubGen/VpiReader.vp b/App/lib/Stub/StubGen/VpiReader.vp index c30b2a3..7f7e059 100644 --- a/App/lib/Stub/StubGen/VpiReader.vp +++ b/App/lib/Stub/StubGen/VpiReader.vp @@ -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: diff --git a/App/lib/core/Gen/AsyncGenMixin.vp b/App/lib/core/Gen/AsyncGenMixin.vp index 84debe4..74ba61d 100644 --- a/App/lib/core/Gen/AsyncGenMixin.vp +++ b/App/lib/core/Gen/AsyncGenMixin.vp @@ -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_") - 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_ 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_") 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) diff --git a/App/lib/core/Gen/Class/ClassGenVTable.vp b/App/lib/core/Gen/Class/ClassGenVTable.vp index 98ac420..28cc43f 100644 --- a/App/lib/core/Gen/Class/ClassGenVTable.vp +++ b/App/lib/core/Gen/Class/ClassGenVTable.vp @@ -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 (* @ 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) diff --git a/App/lib/core/Gen/Expr/ExprGenFstr.vp b/App/lib/core/Gen/Expr/ExprGenFstr.vp index 3110bfc..c024ee0 100644 --- a/App/lib/core/Gen/Expr/ExprGenFstr.vp +++ b/App/lib/core/Gen/Expr/ExprGenFstr.vp @@ -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) diff --git a/App/lib/core/Gen/Func/FuncGenDef.vp b/App/lib/core/Gen/Func/FuncGenDef.vp index 58c1b6c..a263426 100644 --- a/App/lib/core/Gen/Func/FuncGenDef.vp +++ b/App/lib/core/Gen/Func/FuncGenDef.vp @@ -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) diff --git a/App/lib/core/Gen/Func/FuncGenMixin.vp b/App/lib/core/Gen/Func/FuncGenMixin.vp index 3d4f73e..1f01da8 100644 --- a/App/lib/core/Gen/Func/FuncGenMixin.vp +++ b/App/lib/core/Gen/Func/FuncGenMixin.vp @@ -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*) diff --git a/App/lib/core/Gen/ImportsGenMixin.vp b/App/lib/core/Gen/ImportsGenMixin.vp index 25fc391..bce4511 100644 --- a/App/lib/core/Gen/ImportsGenMixin.vp +++ b/App/lib/core/Gen/ImportsGenMixin.vp @@ -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) diff --git a/App/lib/core/Gen/MatchGenMixin.vp b/App/lib/core/Gen/MatchGenMixin.vp index 50933de..b3057ee 100644 --- a/App/lib/core/Gen/MatchGenMixin.vp +++ b/App/lib/core/Gen/MatchGenMixin.vp @@ -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 diff --git a/App/lib/core/IncludesScanner.vp b/App/lib/core/IncludesScanner.vp index 42cebac..bb9b560 100644 --- a/App/lib/core/IncludesScanner.vp +++ b/App/lib/core/IncludesScanner.vp @@ -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")