From 49683fc88120e11b7ae70ddee2d85bd9e9c82b34 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Mon, 2 Nov 2026 10:50:35 +0800 Subject: [PATCH] fix(gen): OOP `__len__`/`__getitem__`/methods beat pointer arithmetic A pointer chain that reaches a class is a reference to the object, not an array of objects. `arr: &list[int]*` (inside `arr` is `list[int]**`) used to be treated as raw pointer arithmetic: `len(arr)` read the pointer slot, `arr[i]` indexed it, and `arr.append(x)` passed the slot as self -- all producing garbage or a runtime crash. - add `_oop_self_ptr`: peel extra pointer levels down to Ptr(Struct); - `len()` normalizes the receiver after resolving the class, so list inline `__count__` and class `__len__` receive the object pointer; - subscript reads dispatch list / class `__getitem__` with the peeled receiver; raw pointer traversal remains the fallback when the chain does not reach a class (or the class has no `__getitem__`); - the list builtin method branch and `_translate_method_call` peel the receiver, so `arr.append(x)` works through a reference; - `is_list_subscript` unwraps every level for the assignment path, so `arr[i] = v` and tuple swaps on elements write the real list; value-type list variables keep their old path. Probe: `&list[int]*` len/read/write/swap/append and `&Box*` custom `__len__`/`__getitem__` all produce the expected values; self-host 110/110; ListTest, GenericTest3, PtrOpProbe and HashLibTest pass. --- App/lib/core/Gen/Call/CallGenCall.vp | 4 ++ App/lib/core/Gen/Call/CallGenDispatch.vp | 20 +++++++--- App/lib/core/Gen/Expr/ExprGenName.vp | 50 ++++++++++++++++++------ App/lib/core/Gen/StructGenMixin.vp | 19 +++++++++ 4 files changed, 75 insertions(+), 18 deletions(-) diff --git a/App/lib/core/Gen/Call/CallGenCall.vp b/App/lib/core/Gen/Call/CallGenCall.vp index d93081a..811c4f0 100644 --- a/App/lib/core/Gen/Call/CallGenCall.vp +++ b/App/lib/core/Gen/Call/CallGenCall.vp @@ -646,6 +646,10 @@ novirtual class CallGenCall(CallGenBase.CallGenBase): """Translate a method call; returns the call result Value.""" if pool is None or builder is None or class_name is None or method_name is None: return None if obj_ptr is None or cl is None: return None + # A pointer chain that reaches the class is a reference to the object: peel the extra + # levels so `arr: &list[int]*` calls `list.append` on the list, not on the pointer slot. + obj_ptr = self.Gen.StructGen._oop_self_ptr(builder, pool, obj_ptr) + if obj_ptr is None: return None # 调用点生命期检查(M2.3 覆盖面扩展):方法摘要键为 "Class.method"(含 MRO 回退)。 # 摘要的形参下标含 self(bit 0),故用户第 ai 个实参对应 bit (ai+1)。语义与模块级 diff --git a/App/lib/core/Gen/Call/CallGenDispatch.vp b/App/lib/core/Gen/Call/CallGenDispatch.vp index 9e71574..fa909ee 100644 --- a/App/lib/core/Gen/Call/CallGenDispatch.vp +++ b/App/lib/core/Gen/Call/CallGenDispatch.vp @@ -699,13 +699,17 @@ novirtual class CallGenDispatch(CallGenBase.CallGenBase): if len_struct_ty is not None: len_cls_nm: char* = self.Gen.StructGen.get_class_name_by_type(pool, len_struct_ty) if len_cls_nm is None: self.Gen.TypeGenErr.fatal_error(len_arg_node, "len() argument type inference failed (unknown struct class name)", "TypeError") + # A pointer chain that reaches the class is a reference to the object: + # peel the extra levels so `len(&list[int]*)` reads the list, not the slot. + len_obj: mlirlite.Value* = self.Gen.StructGen._oop_self_ptr(builder, pool, len_val) + if len_obj is None: return None # list builtin __len__: read __count__ (GEP offset 1, i64). Narrow len_arg_node # to ast.Call* explicitly, since passing a base pointer to a derived param is banned. len_arg_cl: ast.Call* = ast.Call*(len_arg_node) - if self.Gen.CallGenProp._is_list_builtin_receiver(pool, len_val, len_cls_nm, "__len__", len_arg_cl) != 0: + if self.Gen.CallGenProp._is_list_builtin_receiver(pool, len_obj, len_cls_nm, "__len__", len_arg_cl) != 0: i64_ty_lb: mlirlite.LLVMType* = mlirlite.Int64(pool) idx_one_lb: mlirlite.Value* = mlirlite.const_int64(pool, 1) - cnt_ptr_lb: mlirlite.Value* = mlirlite.build_gep(builder, i64_ty_lb, len_val, idx_one_lb) + cnt_ptr_lb: mlirlite.Value* = mlirlite.build_gep(builder, i64_ty_lb, len_obj, idx_one_lb) if cnt_ptr_lb is not None: return mlirlite.build_load(builder, i64_ty_lb, cnt_ptr_lb) len_lookup: char* = pool.alloc(128) if len_lookup is not None: @@ -716,11 +720,11 @@ novirtual class CallGenDispatch(CallGenBase.CallGenBase): len_fn_name2: char* = mlirlite.function_get_name(len_fn2) if len_ret_ty2 is not None and len_fn_name2 is not None: # Align the self parameter type (bitcast to the method's first param) - len_self_arg2: mlirlite.Value* = len_val + len_self_arg2: mlirlite.Value* = len_obj len_first_param2: mlirlite.Param* = mlirlite.function_get_param_head(len_fn2) if len_first_param2 is not None: len_self_ty2: mlirlite.LLVMType* = mlirlite.param_get_ty(len_first_param2) - if len_self_ty2 is not None and len_val.Ty is not None: len_self_arg2 = mlirlite.build_bitcast(builder, len_val, len_self_ty2) + if len_self_ty2 is not None and len_obj.Ty is not None: len_self_arg2 = mlirlite.build_bitcast(builder, len_obj, len_self_ty2) len_self_arg2.Next = None return mlirlite.build_call(builder, len_fn_name2, len_self_arg2, 1, len_ret_ty2, 0) # len_fn2 miss (cross-module method): build the exact name {sha1}.{cls}.__len__ @@ -737,8 +741,8 @@ novirtual class CallGenDispatch(CallGenBase.CallGenBase): if len_ret_x is None: len_ret_x = self._LookupMethodRetTyFromTextLL(pool, len_cls_sha1_x, len_cls_nm, "__len__") if len_ret_x is not None: # self is passed as Ptr(len_struct_ty) to match the definition's type; - # len_val is already the loaded object pointer; use the value chain as param_head. - len_self_x: mlirlite.Value* = len_val + # len_obj is the peeled object pointer; use the value chain as param_head. + len_self_x: mlirlite.Value* = len_obj len_self_x.Next = None self._ensure_method_declare(pool, mod, len_mangled_x, len_ret_x, len_self_x, 1) return mlirlite.build_call(builder, len_mangled_x, len_self_x, 1, len_ret_x, 0) @@ -1294,6 +1298,10 @@ novirtual class CallGenDispatch(CallGenBase.CallGenBase): # [S4] Compare the container base name exactly: the LLVM name carries a module sha1 # prefix; SpecTypeNameBaseIs strips/truncates before comparing, avoiding substring false hits. if lm_conf_nm is not None and TSpec.SpecTypeNameBaseIs(lm_conf_nm, "list") == 0: return None + # A pointer chain that reaches the list is a reference to the list object: + # peel the extra levels before reading __data__/__count__ (&list[T]* -> list[T]*). + lm_obj = self.Gen.StructGen._oop_self_ptr(builder, pool, lm_obj) + if lm_obj is None: return None # Set hit_box for the list builtin branch: void methods (set/clear) return None, # so without the flag translate_call would fall through to a cross-module lookup. if hit_box is not None: hit_box[0] = 1 diff --git a/App/lib/core/Gen/Expr/ExprGenName.vp b/App/lib/core/Gen/Expr/ExprGenName.vp index 62f7210..5dc31f7 100644 --- a/App/lib/core/Gen/Expr/ExprGenName.vp +++ b/App/lib/core/Gen/Expr/ExprGenName.vp @@ -460,26 +460,24 @@ novirtual class ExprGenName(ExprGenBase.ExprGenBase): if self.is_ptr_type(alloca.Ty) == 0: return None pointee: mlirlite.LLVMType* = alloca.Ty.Pointee if pointee is None: return None - # Check whether pointee is Ptr(Struct) - inner_ty: mlirlite.LLVMType* = None - match pointee: - case mlirlite.LLVMType.Ptr(it): - inner_ty = it - case _: - return None - if inner_ty is None: return None - # Check whether inner_ty is a Struct whose name contains "list[" + if self.is_ptr_type(pointee) == 0: return None # value-type list: handled by get_subscript_ptr + # Unwrap every pointer level: a reference to a list pointer (`&list[T]*`) is still + # the list object for __setitem__, not an array of list pointers. + st_ty: mlirlite.LLVMType* = self.Gen.StructGen.get_struct_type_from_value(alloca) + if st_ty is None: return None struct_name: str = None - match inner_ty: + match st_ty: case mlirlite.LLVMType.Struct(_, _, sname): struct_name = sname case _: return None if struct_name is None: return None if string.strstr(struct_name, "list[") is None: return None - # Is a list type: load out the list object pointer + # Is a list type: load out the list object pointer (peel reference levels). builder: mlirlite.IRBuilder* = trans._cur_builder - return mlirlite.build_load(builder, pointee, alloca) + obj: mlirlite.Value* = mlirlite.build_load(builder, pointee, alloca) + if obj is None: return None + return self.Gen.StructGen._oop_self_ptr(builder, trans.Pool, obj) # _nonlocal_subscript_ptr - element pointer for a nonlocal array/pointer subscript: read the # shared heap address from env (_load_raw_var_ptr), bitcast, then GEP (the outer alloca is unusable). def _nonlocal_subscript_ptr(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, trans: HT.Translator*, nm_id: str, idx_val: mlirlite.Value*) -> mlirlite.Value*: @@ -642,6 +640,34 @@ novirtual class ExprGenName(ExprGenBase.ExprGenBase): # inner_ty being Ptr means alloca.Ty is a triple pointer (e.g. T=AST*, T*=AST**), # so [i] is a pointer dereference (stride 8), not a struct __getitem__ call. if self.is_ptr_type(inner_ty) != 0: + # OOP priority over pointer arithmetic: a pointer chain that + # reaches a class dispatches the class __getitem__ (peeling the + # reference levels), no matter how deep the chain is. + oop_st: mlirlite.LLVMType* = self.Gen.StructGen.get_struct_type_from_value(alloca) + if oop_st is not None: + oop_cls: str = self.Gen.StructGen.get_class_name_by_type(pool, oop_st) + if oop_cls is not None: + oop_obj: mlirlite.Value* = mlirlite.build_load(builder, pointee, alloca) + if oop_obj is not None: oop_obj = self.Gen.StructGen._oop_self_ptr(builder, pool, oop_obj) + if oop_obj is not None: + if string.strstr(oop_cls, "list[") is not None: + oop_addr: mlirlite.Value* = self.list_getitem_inline(builder, pool, oop_obj, idx_val, node) + if oop_addr is not None: + oop_elem_ty: mlirlite.LLVMType* = self._list_elem_type_from_name(pool, oop_cls) + if oop_elem_ty is not None: + oop_ptr_ty: mlirlite.LLVMType* = mlirlite.Ptr(pool, oop_elem_ty) + oop_cast: mlirlite.Value* = mlirlite.build_bitcast(builder, oop_addr, oop_ptr_ty) + if oop_cast is not None: return mlirlite.build_load(builder, oop_elem_ty, oop_cast) + return None + gi_lookup_oop: str = pool.alloc(128) + if gi_lookup_oop is not None: + viperlib.snprintf(gi_lookup_oop, 128, "%s.__getitem__", oop_cls) + if self.Gen.CallGenBase.find_func_in_module(mod, gi_lookup_oop) is not None: + oop_args: sizet* = pool.alloc(8) + if oop_args is not None: + oop_args[0] = sizet(idx_val) + oop_ret: mlirlite.Value* = self.Gen.CallGenCall._call_method_on_ptr(pool, builder, mod, oop_cls, "__getitem__", oop_obj, oop_args, 1, trans, sub) + if oop_ret is not None: return oop_ret ptr_val_pp: mlirlite.Value* = mlirlite.build_load(builder, pointee, alloca) if ptr_val_pp is not None: elem_ptr_pp: mlirlite.Value* = mlirlite.build_gep(builder, inner_ty, ptr_val_pp, idx_val) diff --git a/App/lib/core/Gen/StructGenMixin.vp b/App/lib/core/Gen/StructGenMixin.vp index eb537ae..c4393a8 100644 --- a/App/lib/core/Gen/StructGenMixin.vp +++ b/App/lib/core/Gen/StructGenMixin.vp @@ -3097,6 +3097,25 @@ novirtual class StructGenMixin: ty = nxt_ty return None + # _oop_self_ptr - reduce Ptr^k(Struct) to the Ptr(Struct) self pointer. + # A pointer chain that reaches a class is a reference to the object, not an array + # of objects: OOP dispatch (len / []) peels every extra level instead of treating + # the chain as raw pointer arithmetic. + def _oop_self_ptr(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, val: mlirlite.Value*) -> mlirlite.Value*: + """Strip extra pointer levels so the value is the Ptr(Struct) object pointer.""" + if val is None or val.Ty is None: return val + cur: mlirlite.Value* = val + cur_ty: mlirlite.LLVMType* = val.Ty + while cur_ty is not None: + if self.Gen.ExprGenBase.is_ptr_type(cur_ty) == 0: break + pt: mlirlite.LLVMType* = cur_ty.Pointee + if pt is None: break + if self.Gen.ExprGenBase.is_ptr_type(pt) == 0: break + cur = mlirlite.build_load(builder, pt, cur) + if cur is None: return None + cur_ty = cur.Ty + return cur + def mark_as_union(self, name: str) -> int: """Mark a registered struct as a union; returns 1 on success, 0 on failure.""" if name is None: return 0