From 1f067aa58f82bb205fb5e5f95bee3c4142769892 Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Thu, 29 Oct 2026 16:43:35 +0800 Subject: [PATCH] fix(call): accept void methods and rawptr arguments at the call boundary Two follow-ups to the cross-module signature check: - A method without a return annotation is void, like a module-level `def`. The vpi return-annotation cache recorded it as a hardcoded i32, so callers invented an i32 return for a void method. It now records `void` for non-extern declarations (methods included). - A class-level function declared without `self` (`Class.func()`) is a namespaced plain function, not a method bound to a receiver. The module-qualified call path rejected such symbols as method bindings, which then fell through to the external-C whitelist and failed on a void return (`Scheduler._yield`). - A pointer argument passed to an integer-typed parameter is converted at the call boundary instead of failing the implicit pointer-to-integer rule. A `rawptr` value whose callee parameter degraded to uint64 (stale/mis-typed stub) now reaches the callee with its address instead of aborting with a CastError. --- App/lib/Stub/StubGen/VpiReader.vp | 11 ++++++----- App/lib/core/Gen/Call/CallGenBase.vp | 18 ++++++++++++++++++ App/lib/core/Gen/Call/CallGenLib.vp | 8 ++++++++ App/lib/core/Gen/Call/CallGenMixin.vp | 20 +++++++++++++++++--- 4 files changed, 49 insertions(+), 8 deletions(-) diff --git a/App/lib/Stub/StubGen/VpiReader.vp b/App/lib/Stub/StubGen/VpiReader.vp index a5aa42e..df33459 100644 --- a/App/lib/Stub/StubGen/VpiReader.vp +++ b/App/lib/Stub/StubGen/VpiReader.vp @@ -584,11 +584,12 @@ def _cache_vpi_return_annots_text(vpi_buf: bytes, src_sha1: str) -> int: rtype_len = rend2 - rstart2 ret_src = vpi_buf + rstart2 else: - # No return annotation: a module-level Viper `def` is void. Two cases keep - # the legacy i32 default: `extern`/`state` C declarations (the real C return - # type may be unrecorded in a hand-written stub) and methods, whose - # cross-module resolution has its own fallback path (see _yield). - if is_c_decl != 0 or cur_cls is not None: + # No return annotation: a Viper `def` is void. `extern`/`state` C + # declarations keep the legacy i32 default (the real C return type may be + # unrecorded in a hand-written stub). Methods are included: `def m():` is + # void by language semantics, and the cross-module method path resolves a + # missing `->` from the dependency's text.ll as void as well. + if is_c_decl != 0: rtype_len = 3 ret_src = "i32" else: diff --git a/App/lib/core/Gen/Call/CallGenBase.vp b/App/lib/core/Gen/Call/CallGenBase.vp index 032ccf4..ef1601d 100644 --- a/App/lib/core/Gen/Call/CallGenBase.vp +++ b/App/lib/core/Gen/Call/CallGenBase.vp @@ -859,6 +859,20 @@ novirtual class CallGenBase: if string.strcmp(ta, tb) == 0: return 1 return 0 + # _func_has_self_param - 1 when the first parameter is a receiver (self/cls/unnamed). + # A class-level function declared without `self` is a namespaced plain function: the call + # `mod.Class.func()` passes no receiver, so it must not be rejected as a method binding. + def _func_has_self_param(self, func: mlirlite.Function*) -> int: + """1 when the symbol's first parameter is a receiver (self/cls/unnamed).""" + if func is None: return 0 + p: mlirlite.Param* = mlirlite.function_get_param_head(func) + if p is None: return 0 + nm: str = mlirlite.param_get_name(p) + if nm is None: return 1 + if string.strcmp(nm, "self") == 0: return 1 + if string.strcmp(nm, "cls") == 0: return 1 + return 0 + # _conform_call_to_symbol - align a call site with its callee symbol. # mlirlite.build_call composes the llvm.call function type from the argument values and the # caller-supplied return type; it never consults the callee. So a call site that resolved a @@ -876,6 +890,10 @@ novirtual class CallGenBase: sym_ret: mlirlite.LLVMType* = mlirlite.function_get_ret_ty(func) if sym_ret is None: return ret_ty if ret_ty is not None and self._call_ty_equal(pool, sym_ret, ret_ty) == 0: + # An i32 call-site type is the "unresolved" fallback (whitelist default). The symbol + # is authoritative there, so adopt its type instead of reporting a conflict. + i32_ty_c: mlirlite.LLVMType* = mlirlite.Int32(pool) + if self._call_ty_equal(pool, ret_ty, i32_ty_c) != 0: return sym_ret # A cross-module call resolves its signature from the callee's .vpi; disagreeing with # the symbol means two call sites would emit incompatible llvm.call operations. if self.Gen.CallGenLib._is_sha1_mangled_call(call_name) != 0: diff --git a/App/lib/core/Gen/Call/CallGenLib.vp b/App/lib/core/Gen/Call/CallGenLib.vp index 0af0da0..572b1f6 100644 --- a/App/lib/core/Gen/Call/CallGenLib.vp +++ b/App/lib/core/Gen/Call/CallGenLib.vp @@ -267,6 +267,14 @@ novirtual class CallGenLib(CallGenBase.CallGenBase): if self.Gen.AssignGen._stg_ty_equal(pool, val.Ty, tgt_ty) == 0: nv_p: mlirlite.Value* = mlirlite.build_bitcast(builder, val, tgt_ty) if nv_p is not None: return nv_p + case mlirlite.LLVMType.Int(bits_pi): + # A pointer passed where the callee's parameter degraded to an integer + # (e.g. `rawptr` resolved as uint64 in a stale/mis-typed stub): convert at the + # call boundary instead of failing the implicit-cast rule, so passing a + # rawptr never breaks the build and the callee still receives the address. + if bits_pi > 0: + nv_p2: mlirlite.Value* = mlirlite.build_ptrtoint(builder, val, tgt_ty) + if nv_p2 is not None: return nv_p2 case _: pass case _: diff --git a/App/lib/core/Gen/Call/CallGenMixin.vp b/App/lib/core/Gen/Call/CallGenMixin.vp index cbb8d1e..d54c2a8 100644 --- a/App/lib/core/Gen/Call/CallGenMixin.vp +++ b/App/lib/core/Gen/Call/CallGenMixin.vp @@ -778,7 +778,11 @@ novirtual class CallGenMixin(CallGenBase.CallGenBase): bn_seg2_mod: int = 0 if trans is not None and trans._imported_modules is not None: if self.Gen.ImportsGen.is_module_imported_or_known(trans._imported_modules, bn_seg2) != 0: bn_seg2_mod = 1 - if bn_seg2_cls != 0 or bn_seg2_mod == 0: found_func = None + if bn_seg2_cls != 0: + # A class-level function without self (`Class.func()`) is a + # namespaced plain function: keep it, the call passes no receiver. + if self.Gen.CallGenBase._func_has_self_param(found_func) != 0: found_func = None + elif bn_seg2_mod == 0: found_func = None else: # 跨模块重载:先从接收者路径解析目标模块 SHA1,再在候选集里决议。 # vpi 注入的重载符号是 {sha1}.name_v{hash}、函数表项名带后缀,所以用 prefix 模式。 @@ -872,7 +876,11 @@ novirtual class CallGenMixin(CallGenBase.CallGenBase): is_seg2_mod: int = 0 if trans is not None and trans._imported_modules is not None: if self.Gen.ImportsGen.is_module_imported_or_known(trans._imported_modules, seg2) != 0: is_seg2_mod = 1 - if is_seg2_cls != 0 or is_seg2_mod == 0: found_func = None + if is_seg2_cls != 0: + # Same as the bare-name case above: `mod.Class.func()` on a + # namespaced function (no self param) stays a plain call. + if self.Gen.CallGenBase._func_has_self_param(found_func) != 0: found_func = None + elif is_seg2_mod == 0: found_func = None # abs(obj): dispatch to __abs__ when the argument is a class instance whose class (with MRO) # defines it; otherwise keep the existing builtin/int/float abs path. if found_func is None and cl.func is not None and cl.func.kind() == ast.ASTKind.Name and string.strcmp(func_name, "abs") == 0 and can == 1: @@ -1204,7 +1212,13 @@ novirtual class CallGenMixin(CallGenBase.CallGenBase): if VpiReader._vpi_param_annot_seg_is(trans.ModuleSha1, None, func_name, ai, "str") != 0 or VpiReader._vpi_param_annot_seg_is(trans.ModuleSha1, None, func_name, ai, "bytes") != 0: sm_is_str = 1 if sm_is_str != 0: self.Gen.ExprGenBase.fail_if_str_annot_agg_ptr(arg_val, 1, arg) - arg_val = self.Gen.ExprGenBase.coerce_to_type(builder, arg_val, fpty, arg, trans) + # A pointer argument for an integer parameter (e.g. a `rawptr` value + # whose callee param degraded to uint64) converts at the call + # boundary; only non-pointer arguments use the language coercion rules. + if self.Gen.ExprGenBase.is_ptr_type(arg_val.Ty) != 0 and self.Gen.ExprGenBase.is_ptr_type(fpty) == 0 and self.Gen.ExprGenBase.get_llvm_type_bits(fpty) != 0: + arg_val = self.Gen.CallGenLib._coerce_arg_value_to_ty(pool, builder, arg_val, fpty, arg, trans, 0) + else: + arg_val = self.Gen.ExprGenBase.coerce_to_type(builder, arg_val, fpty, arg, trans) elif found_func.IsVarArg == 1: # Win64 variadic 8-byte slots: pass sub-64-bit integers as i64 and promote f32 to f64; # passing the narrow type leaves old stack data in the high 4 bytes that va_arg(i64) reads as garbage.