fix(vtable): 虚方法表按类实际规模动态分配,去掉 32 上限

MemBuddy 继承 MemManager 后虚方法约 60+ 个,VTBL_METHOD_MAX=32 会触发 fail-fast(此前静默只收前 32 个,之后的调用点找不到槽位、退化成静态直调)。现在收集前先算容量上界:各父类已收集的 VTableMethodCount 之和 + 本类 FunctionDef 数量 + 1,三个 PermPool 数组按此一次性分配;两处越界检查保留为 InternalCompilerError(理论不可达)。删除 VTBL_METHOD_MAX。注:本次提交同时带上了该文件既有的 t.CPtr → rawptr 迁移与 _generate_default_init 删除等未提交改动。
This commit is contained in:
TermiNexus committed 2026-10-17 19:20:03 +08:00
1 parent d46fdf152a
commit bf0bbcc89b
1 file changed
+104 -87
+104 -87
View File
@@ -1,6 +1,5 @@
import lib.core.Gen.GenTypes as GenTypes
import lib.core.Gen.Type.TypeSpec as TSpec
import t, c
from stdint import *
import ast
import mlirlite
@@ -19,8 +18,8 @@ import lib.core.Gen.DebugInfoGen as DebugInfoGen
# Method metadata flag: TLS (@t.TLS decorator; local copy avoids a cross-module CDefine lookup).
define META_TLS = 32
# Max number of vtable methods.
define VTBL_METHOD_MAX = 32
# Vtable method tables are sized per class at collection time (parents' count + own methods);
# there is deliberately no fixed VTBL_METHOD_MAX.
define SCOPE_FUNCTION = 1
# ============================================================
@@ -67,15 +66,35 @@ novirtual class ClassGenVTable(ClassGenBase.ClassGenBase):
if children is None: return 0
cn: sizet = len(children)
# Capacity comes from the class itself: the parents' already-collected vtable methods
# (an upper bound — dedupe/override only shrink it) plus the number of own method
# definitions. A fixed 32 silently dropped every later method, after which a call missed
# the slot and fell back to a static call of the base implementation.
parent_count: int = self.Gen.StructGen.get_parent_count(class_name)
vt_cap: int = 0
if parent_count > 0:
for pi0 in range(int(parent_count)):
pname0: str = self.Gen.StructGen.get_parent_name_at(class_name, pi0)
if pname0 is None: continue
pe0: GenTypes.StructEntry* = self.Gen.StructGen.find_struct_by_module(pname0, trans.ModuleSha1)
if pe0 is None: pe0 = self.Gen.StructGen.find_struct(pname0)
if pe0 is not None and pe0.VTableMethodCount > 0: vt_cap = vt_cap + pe0.VTableMethodCount
for ci0 in range(int(cn)):
stmt0: ast.AST* = children.get(ci0)
if stmt0 is not None and stmt0.kind() == ast.ASTKind.FunctionDef: vt_cap = vt_cap + 1
vt_cap = vt_cap + 1 # one spare slot
if vt_cap < 1: vt_cap = 1
vt_bytes: sizet = sizet(8 * vt_cap)
# Allocate with PermPool: VTableMethods must survive per-file pool resets.
full_names_buf: sizet* = CompilerFactory.factory.PermPool.alloc(8 * VTBL_METHOD_MAX)
method_names_buf: sizet* = CompilerFactory.factory.PermPool.alloc(8 * VTBL_METHOD_MAX)
full_names_buf: sizet* = CompilerFactory.factory.PermPool.alloc(vt_bytes)
method_names_buf: sizet* = CompilerFactory.factory.PermPool.alloc(vt_bytes)
# Record each method's parent SHA1 (non-zero = inherited, mangle with parent module SHA1).
method_parent_sha1s: sizet* = CompilerFactory.factory.PermPool.alloc(8 * VTBL_METHOD_MAX)
method_parent_sha1s: sizet* = CompilerFactory.factory.PermPool.alloc(vt_bytes)
if full_names_buf is None or method_names_buf is None or method_parent_sha1s is None: return 0
string.memset(full_names_buf, 0, 8 * VTBL_METHOD_MAX)
string.memset(method_names_buf, 0, 8 * VTBL_METHOD_MAX)
string.memset(method_parent_sha1s, 0, 8 * VTBL_METHOD_MAX)
string.memset(full_names_buf, 0, vt_bytes)
string.memset(method_names_buf, 0, vt_bytes)
string.memset(method_parent_sha1s, 0, vt_bytes)
method_count: int = 0
@@ -83,7 +102,6 @@ novirtual class ClassGenVTable(ClassGenBase.ClassGenBase):
# subclass, others the parent; SHA1-aware lookup avoids cross-module same-name mismatches.
vt_self_entry: GenTypes.StructEntry* = self.Gen.StructGen.find_struct_by_module(class_name, trans.ModuleSha1)
# Multiple inheritance: walk parents in declaration order, dedupe by short name (first wins).
parent_count: int = self.Gen.StructGen.get_parent_count(class_name)
if parent_count > 0:
for pi in range(int(parent_count)):
pname: str = self.Gen.StructGen.get_parent_name_at(class_name, pi)
@@ -97,11 +115,10 @@ 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:
# 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")
if method_count >= vt_cap:
# Unreachable with the upper-bound sizing above; stay loud rather than
# writing past the PermPool array.
self.Gen.TypeGenErr.fatal_error(cd, f"internal error: vtable of '{class_name}' exceeded its computed capacity {vt_cap}", "InternalCompilerError")
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
@@ -169,10 +186,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:
# 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")
if method_count >= vt_cap:
# Unreachable with the upper-bound sizing above (own methods were counted);
# stay loud rather than writing past the PermPool array.
self.Gen.TypeGenErr.fatal_error(cd, f"internal error: vtable of '{class_name}' exceeded its computed capacity {vt_cap}", "InternalCompilerError")
mname: str = fd.name
# Skip constructors and special methods (they must not enter the vtable).
@@ -598,7 +615,7 @@ novirtual class ClassGenVTable(ClassGenBase.ClassGenBase):
if decl_reuse == 0:
# Default-parameter info (args[0] is self for normal methods; @staticmethod has no self).
# Assign the attribute to a typed list[...]* local before __len__() or GEP base becomes i32 0.
md_defaults: t.CPtr = None
md_defaults: rawptr = None
md_default_count: int = 0
md_param_count: int = 0
md_args_node: ast.Arguments* = fd.args
@@ -1114,71 +1131,71 @@ novirtual class ClassGenVTable(ClassGenBase.ClassGenBase):
field_ptr: mlirlite.Value* = mlirlite.build_gep_struct(func_builder, struct_ty, fe.Ty, self_val, fe.Index)
if field_ptr is None: continue
# Coerce and store.
default_val = self.Gen.ExprGenBase.coerce_to_type(func_builder, default_val, fe.Ty, fe.DefaultVal)
mlirlite.build_store(func_builder, default_val, field_ptr)
# 3. ret void
mlirlite.build_ret_void(func_builder)
return 0
# _generate_default_init - emit define void @SHA1.ClassName.__init__(Ptr(StructTy) %self) { ret void }.
# Companion to _generate_before_init: exists purely so that explicit `obj.__init__()` source calls
# (which lower to a direct @SHA1.Class.__init__ call) link even when the class defines no __init__.
# The body must stay empty: __before_init__ already owns zero-fill / defaults / vtable setup.
def _generate_default_init(self, trans: HT.Translator*, struct_ty: mlirlite.LLVMType*, class_name: str) -> int:
"""Emit an empty default __init__ so explicit obj.__init__() call sites link."""
if trans is None or struct_ty is None or class_name is None: return 0
pool: memhub.MemBuddy* = trans.Pool
mod: mlirlite.LLVMModule* = trans.Module
funcs_ptr: GenTypes.FuncEntry* = trans._funcs
func_count: int = trans._func_count
self_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, struct_ty)
void_ty: mlirlite.LLVMType* = mlirlite.Void(pool)
# Build the function name: ClassName.__init__(SHA1 namespace applied by _mangle_name).
full_name_buf: str = pool.alloc(128)
if full_name_buf is None: return 0
viperlib.snprintf(full_name_buf, 128, "%s.__init__", class_name)
full_name: str = full_name_buf
mangled_name: str = self.Gen.FuncGenBase._mangle_name(trans, full_name)
# A call site may have forward-declared __init__ first; upgrade the declaration to a
# definition (clear IsDeclared). If a body already exists, bail to avoid a duplicate symbol.
di_func: mlirlite.Function* = self.Gen.CallGenBase.find_func_in_module(mod, mangled_name)
di_decl_reuse: int = 0
if di_func is not None:
if di_func.IsDeclared == 0: return 0
di_decl_reuse = 1
else:
di_func = mlirlite.create_function(pool, mod, mangled_name, void_ty)
if di_func is None: return 0
func: mlirlite.Function* = di_func
max_funcs: int = 1024
rc_add: int = self.Gen.CallGenLeafKit.add_func_to_table(funcs_ptr, func_count, full_name, func, max_funcs, None, 0, 0, pool)
if rc_add == 0: trans._func_count = func_count + 1
if di_decl_reuse != 0:
# Reusing the declaration: name/type its first param as %self (printer only emits named params).
di_first_param: mlirlite.Param* = mlirlite.function_get_param_head(func)
if di_first_param is not None:
di_first_param.Ty = self_ptr_ty
di_first_param.Name = "%self"
func.IsDeclared = 0
else:
mlirlite.add_param(pool, func, self_ptr_ty, "%self")
self.Gen.CallGenLeafKit.update_global_func_spec(full_name, func)
entry_blk: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "entry")
if entry_blk is None: return 0
func_builder: mlirlite.IRBuilder* = mlirlite.new_builder(pool, func)
if func_builder is None: return 0
mlirlite.position_at_end(func_builder, entry_blk)
mlirlite.build_ret_void(func_builder)
default_val = self.Gen.ExprGenBase.coerce_to_type(func_builder, default_val, fe.Ty, fe.DefaultVal)
mlirlite.build_store(func_builder, default_val, field_ptr)
# 3. ret void
mlirlite.build_ret_void(func_builder)
return 0
# _generate_default_init - emit define void @SHA1.ClassName.__init__(Ptr(StructTy) %self) { ret void }.
# Companion to _generate_before_init: exists purely so that explicit `obj.__init__()` source calls
# (which lower to a direct @SHA1.Class.__init__ call) link even when the class defines no __init__.
# The body must stay empty: __before_init__ already owns zero-fill / defaults / vtable setup.
def _generate_default_init(self, trans: HT.Translator*, struct_ty: mlirlite.LLVMType*, class_name: str) -> int:
"""Emit an empty default __init__ so explicit obj.__init__() call sites link."""
if trans is None or struct_ty is None or class_name is None: return 0
pool: memhub.MemBuddy* = trans.Pool
mod: mlirlite.LLVMModule* = trans.Module
funcs_ptr: GenTypes.FuncEntry* = trans._funcs
func_count: int = trans._func_count
self_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, struct_ty)
void_ty: mlirlite.LLVMType* = mlirlite.Void(pool)
# Build the function name: ClassName.__init__(SHA1 namespace applied by _mangle_name).
full_name_buf: str = pool.alloc(128)
if full_name_buf is None: return 0
viperlib.snprintf(full_name_buf, 128, "%s.__init__", class_name)
full_name: str = full_name_buf
mangled_name: str = self.Gen.FuncGenBase._mangle_name(trans, full_name)
# A call site may have forward-declared __init__ first; upgrade the declaration to a
# definition (clear IsDeclared). If a body already exists, bail to avoid a duplicate symbol.
di_func: mlirlite.Function* = self.Gen.CallGenBase.find_func_in_module(mod, mangled_name)
di_decl_reuse: int = 0
if di_func is not None:
if di_func.IsDeclared == 0: return 0
di_decl_reuse = 1
else:
di_func = mlirlite.create_function(pool, mod, mangled_name, void_ty)
if di_func is None: return 0
func: mlirlite.Function* = di_func
max_funcs: int = 1024
rc_add: int = self.Gen.CallGenLeafKit.add_func_to_table(funcs_ptr, func_count, full_name, func, max_funcs, None, 0, 0, pool)
if rc_add == 0: trans._func_count = func_count + 1
if di_decl_reuse != 0:
# Reusing the declaration: name/type its first param as %self (printer only emits named params).
di_first_param: mlirlite.Param* = mlirlite.function_get_param_head(func)
if di_first_param is not None:
di_first_param.Ty = self_ptr_ty
di_first_param.Name = "%self"
func.IsDeclared = 0
else:
mlirlite.add_param(pool, func, self_ptr_ty, "%self")
self.Gen.CallGenLeafKit.update_global_func_spec(full_name, func)
entry_blk: mlirlite.BasicBlock* = mlirlite.create_block(pool, func, "entry")
if entry_blk is None: return 0
func_builder: mlirlite.IRBuilder* = mlirlite.new_builder(pool, func)
if func_builder is None: return 0
mlirlite.position_at_end(func_builder, entry_blk)
mlirlite.build_ret_void(func_builder)
return 0