From 94d8c0d151cfb2d12ea6a2fef4c93c370f28f26a Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Mon, 2 Nov 2026 09:46:31 +0800 Subject: [PATCH] fix(gen): keep `&T` auto-address correct when T is itself a pointer `f(a: &T)` lowers to `T*` and takes the argument's address at the call site. The old rule passed any pointer argument through unchanged, so `bubble_sort(nums)` with `arr: &list[int]*` (T = list[int]*) sent `list[int]*` where `list[int]**` was declared and failed with a pointer level 1 -> 2 CastError. - _apply_auto_addr_arg compares the argument's structural pointer depth with the parameter type instead of "is a pointer". For a Name whose own alloca already has the parameter depth, the alloca is passed directly: the `&obj` fold on an object-pointer variable drops one level, which is wrong for `&T` with a pointer T. - translate_call_arg falls back to the definition-side vpi annotation when the same-module table misses, so a cross-module callee that has not been body-translated yet still gets its &T modes. - _coerce_args_to_def_params uses the injected declare's parameter type when the vpi text cannot resolve it (e.g. a generic class name), and passes that type to the auto-address transform. - a null literal passed to `&T` keeps passing through (no lvalue). Probe: `bubble_sort(nums)` sorts through `&list[int]*`; `bump(x)` and `bump(p)` with `p: int*` both increment x (value addressed, pointer passed through). Self-host 110/110; GenericTest3, ListTest, PtrOpProbe pass; EscapeTest still reports its expected LifetimeError. --- App/lib/core/Gen/Call/CallGenBase.vp | 31 +++++++++++++++++++++++++--- App/lib/core/Gen/Call/CallGenLib.vp | 14 ++++++++++++- 2 files changed, 41 insertions(+), 4 deletions(-) diff --git a/App/lib/core/Gen/Call/CallGenBase.vp b/App/lib/core/Gen/Call/CallGenBase.vp index 33fc373..926fdb3 100644 --- a/App/lib/core/Gen/Call/CallGenBase.vp +++ b/App/lib/core/Gen/Call/CallGenBase.vp @@ -153,26 +153,51 @@ novirtual class CallGenBase: # 临时槽再传地址,被调者改的是副本(跨模块按 vpi 形参注解判定,见 helper)。 self._fail_on_value_arg_to_ptr_param(trans, callee_name, arg_idx, arg_node, fpty) mode: int = self._auto_addr_mode(callee_name, arg_idx) + if mode == 0 and trans is not None: + # Cross-module callee whose definition has not been translated yet in this + # process: the definition-side vpi annotation still carries &T / &temp T. + mode = self._vpi_auto_addr_mode(callee_name, trans.ModuleSha1, arg_idx) if mode == 0: return self.Gen.ExprGen.translate_value(builder, pool, mod, arg_node, None, 0, trans, fpty, use_ctx) # Value-reference param: translate without the pointer target hint (a char/str literal must # not be turned into a string), then apply the auto-address transform. av: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, arg_node, None, 0, trans, None, use_ctx) - return self._apply_auto_addr_arg(builder, pool, mod, trans, mode, arg_node, av) + return self._apply_auto_addr_arg(builder, pool, mod, trans, mode, arg_node, av, fpty) # _apply_auto_addr_arg - ref: take the lvalue address; temp: copy into a fresh entry-block slot. def _apply_auto_addr_arg(self, builder: mlirlite.IRBuilder*, pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, - trans: HT.Translator*, mode: int, node: ast.AST*, av: mlirlite.Value*) -> mlirlite.Value*: + trans: HT.Translator*, mode: int, node: ast.AST*, av: mlirlite.Value*, + fpty: mlirlite.LLVMType* = None) -> mlirlite.Value*: """Apply a value-reference mode to a translated argument value.""" if av is None: return None - if self.Gen.ExprGenBase.is_ptr_type(av.Ty) != 0: return av # already a pointer: pass through + # `&T` lowers to `T*`: an argument already at the parameter's pointer depth IS the + # reference (passing `p: int*` to `&int`). Compare structural pointer depth, not type + # text: opaque pointers print identically (`!llvm.ptr`), so text equality cannot tell + # `list[int]*` from `list[int]**` and used to pass `nums` through unaddressed. + if fpty is not None: + if self.Gen.ExprGenBase.get_ptr_level(av.Ty) == self.Gen.ExprGenBase.get_ptr_level(fpty): return av + else: + if self.Gen.ExprGenBase.is_ptr_type(av.Ty) != 0: return av # already a pointer: pass through if mode == 1: if node is None: return av nk: int = node.kind() + # `&obj` on an object-pointer variable normally folds to the object pointer + # (one level less). When the callee expects exactly the variable's storage + # address (`&T` with T itself a pointer, e.g. `&list[int]*` -> `list[int]**`), + # use the alloca directly so the address-of fold does not drop a level. + if nk == ast.ASTKind.Name and trans is not None and fpty is not None: + nm_a: ast.Name* = ast.Name*(node) + if nm_a.id is not None: + v_a: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, nm_a.id) + if v_a is not None and self.Gen.ExprGenBase.get_ptr_level(v_a.Ty) == self.Gen.ExprGenBase.get_ptr_level(fpty): return v_a if nk == ast.ASTKind.Name or nk == ast.ASTKind.Attribute or nk == ast.ASTKind.Subscript: ao_node: ast.AST* = ast.UnaryOp(pool, ast.OpKind.AddrOf, node) if ao_node is None: return av return self.Gen.ExprGen.translate_value(builder, pool, mod, ao_node, None, 0, trans, None, None) + # Null literal: there is no lvalue to address; pass the null value through. + if nk == ast.ASTKind.Constant: + cst: ast.Constant* = ast.Constant*(node) + if cst is not None and cst.const_kind == ast.CONST_NONE: return av self.Gen.TypeGenErr.fatal_error(node, "value-reference parameter '&T' needs an lvalue argument (variable / field / element) or an existing pointer; a literal or expression result has no address (use '&temp T' to pass a temporary copy)", "CastError") return av if mode == 2: diff --git a/App/lib/core/Gen/Call/CallGenLib.vp b/App/lib/core/Gen/Call/CallGenLib.vp index caa4c4c..48e9bb2 100644 --- a/App/lib/core/Gen/Call/CallGenLib.vp +++ b/App/lib/core/Gen/Call/CallGenLib.vp @@ -198,6 +198,18 @@ novirtual class CallGenLib(CallGenBase.CallGenBase): tgt_ty: mlirlite.LLVMType* = None if tpl_src != 0: tgt_ty = VpiReader._resolve_vpi_param_annot_seg_template(pool, sha1_buf, cls_nm, meth_nm, idx, i32_ty, i8_ptr_ty) else: tgt_ty = VpiReader._resolve_vpi_param_annot_seg(pool, sha1_buf, cls_nm, meth_nm, idx, i32_ty, i8_ptr_ty) + if tgt_ty is None and trans is not None and trans.Module is not None: + # vpi text could not be resolved in this file's context (e.g. a generic + # class name). The injected declare was built with the caller's context, + # so its parameter type is the authoritative ABI type. + dcl: mlirlite.Function* = self.Gen.CallGenBase.find_func_in_module(trans.Module, call_name) + if dcl is not None: + p_d: mlirlite.Param* = mlirlite.function_get_param_head(dcl) + pi_d: int = 0 + while p_d is not None and pi_d < idx: + p_d = mlirlite.param_get_next(p_d) + pi_d += 1 + if p_d is not None: tgt_ty = mlirlite.param_get_ty(p_d) if tgt_ty is not None and cur.Ty is not None: # Argument AST (for source-symbolic checks): AST index = chain index idx - self offset arg_idx: int = idx - self_off @@ -210,7 +222,7 @@ novirtual class CallGenLib(CallGenBase.CallGenBase): if trans is not None: vpi_cur_sha = trans.ModuleSha1 mode_cm: int = self.Gen.CallGenBase._vpi_auto_addr_mode(call_name, vpi_cur_sha, idx) if mode_cm != 0 and trans is not None and trans.Module is not None: - node_v = self.Gen.CallGenBase._apply_auto_addr_arg(builder, pool, trans.Module, trans, mode_cm, arg_node, node_v) + node_v = self.Gen.CallGenBase._apply_auto_addr_arg(builder, pool, trans.Module, trans, mode_cm, arg_node, node_v, tgt_ty) # "str / bytes are not generic pointer wildcards": void* / rawptr may # convert to any pointer, but str/bytes may not, even though both are i8* at LLVM level. if self.Gen.ExprGenBase._str_rejects_ptr_kind(cur) != 0: