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.
This commit is contained in:
TermiNexus committed 2026-10-29 16:43:35 +08:00
1 parent 4c34e396a4
commit 1f067aa58f
4 files changed
+49 -8

No files matched your search

+6 -5
View File
@@ -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:
+18
View File
@@ -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:
+8
View File
@@ -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 _:
+17 -3
View File
@@ -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.