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.
This commit is contained in:
TermiNexus committed 2026-11-02 10:50:35 +08:00
1 parent 83d309509b
commit 49683fc881
4 files changed
+75 -18

No files matched your search

+4
View File
@@ -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)。语义与模块级
+14 -6
View File
@@ -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
+38 -12
View File
@@ -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)
+19
View File
@@ -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