Files
ViperC/App/lib/core/Gen/Call/CallGenMixin.vp
T
TermiNexus 97313e9e55 feat(gen): P2 生命期标注 @life(willFree(...)) 驱动释放后使用与二次释放检查
标注面:@life(willFree(<形参名|0起序号>)),兼容叠加式 @willFree(...),可混用累加;序号按形参表下标、self/cls 计入。GenApi.compute_willfree_mask 解析标注并对非法标注 fail-fast。本文件表 StructGenMixin.WfTable + 跨模块 .vpi 行尾 wf=<mask>(Converter 写 / VpiReader 读 / CallGenBase 查)。调用点在实参全部翻译之后把对应实参变量槽置 VarEntry.Freed;其后的读取路径报 LifetimeError。重新赋值清除失效态。
2026-10-08 21:28:49 +08:00

1313 lines
105 KiB
Plaintext

import lib.core.Gen.GenTypes as GenTypes
import t, c
from stdint import *
import ast
import mlirlite
import memhub
import string
import stdio
import stdlib
import viperlib
import lib.core.VLogger as VLogger
import lib.core.Gen.Translator as HT
import lib.core.Gen.Type.TypeSpec as TSpec
import lib.Stub.StubGen.VpiReader as VpiReader
from . import CallGenBase
import lib.core.Gen.GenApi as GenApi
# CallGenMixin - call dispatch mixin (top level after split; phase A moved low-level parts out).
# Constants (kept consistent with TPC's HandlesExprCall).
define _CEXPORT_MAX = 256
define ASM_MAX_OPERANDS = 16
define ASM_TEXT_BUF_SIZE = 2048
define ASM_CONST_BUF_SIZE = 512
define ASM_ARGS_BUF_SIZE = 512
define ASM_LINE_BUF_SIZE = 4096
define LLVMIR_MAX_OPS = 16
define LLVMIR_TEMPLATE_BUF = 512
# CallGenMixin - call dispatch hub (top level, inherits LeafKit).
novirtual class CallGenMixin(CallGenBase.CallGenBase):
"""Call dispatch mixin (top-level; inherits CallGenBase directly)."""
# _CheckStaticCallCrossModule - reject cross-module calls to static symbols (mod.fn(...)).
# Checked centrally at translate_call entry (a per-branch check would miss some paths).
def _CheckStaticCallCrossModule(self, pool: memhub.MemBuddy*, trans: HT.Translator*, cl: ast.Call*) -> int:
if cl is None or cl.func is None or trans is None: return 0
if cl.func.kind() != ast.ASTKind.Attribute: return 0
if trans.ModuleSha1 is None or trans._imported_modules is None: return 0
# Fast path: zero cost when the project has no static symbols (no sha1/table scan).
if self.Gen.CallGenBase.StaticCount <= 0: return 0
sc_at: ast.Attribute* = ast.Attribute*(cl.func)
if sc_at.value is None or sc_at.attr is None: return 0
sc_mod: str = None
if sc_at.value.kind() == ast.ASTKind.Name:
sc_nm: ast.Name* = ast.Name*(sc_at.value)
sc_mod = sc_nm.id
elif sc_at.value.kind() == ast.ASTKind.Attribute: sc_mod = self.Gen.ExprGenAttr._attr_module_path_str(pool, sc_at.value)
if sc_mod is None: return 0
if self.Gen.ImportsGen.is_module_imported(trans._imported_modules, sc_mod) == 0: return 0
sc_sha1: str = self.Gen.StructGen._resolve_module_sha1(sc_mod, trans._from_imports)
if sc_sha1 is None: return 0
if string.strncmp(sc_sha1, trans.ModuleSha1, 16) == 0: return 0
if self.Gen.CallGenBase.IsStaticSymbol(sc_sha1, sc_at.attr) == 0: return 0
self.Gen.TypeGenErr.fatal_error(cl, f"'{sc_mod}.{sc_at.attr}' is a static symbol; cross-module use is forbidden (static is visible only inside its defining module)", "NameError")
return 0
# translate_call - translate Call(func, args) (module-level version).
def translate_call(self, pool: memhub.MemBuddy*, builder: mlirlite.IRBuilder*, mod: mlirlite.LLVMModule*, node: ast.AST*, funcs_ptr: t.CPtr, func_count: int, trans: HT.Translator* = None) -> mlirlite.Value*:
"""Translate a Call node and return the result Value."""
cl: ast.Call* = ast.Call*(node)
# Static symbols must not be used cross-module: fail fast on mod.fn(...) hitting another
# module's static; the check lives in _CheckStaticCallCrossModule to keep this huge function inferable.
self._CheckStaticCallCrossModule(pool, trans, cl)
if cl is None: return None
func_name: str = self.Gen.ExprGenBase.get_func_name(cl.func)
cargs: list[ast.AST*]* = cl.args
can: sizet = 0
if cargs is not None: can = len(cargs)
# M3 §4.3 判据5:`slife` 对象由编译器独占释放,禁止手动 free(否则出口再释放一次 = double-free)。
# 只认裸名/模块限定的 free;实参须是变量名且该槽已登记为 owned slife。
if trans is not None and cargs is not None and func_name is not None:
if string.strcmp(func_name, "free") == 0 and len(cargs) == 1:
a0_mf: ast.AST* = cargs.get(0)
if a0_mf is not None and a0_mf.kind() == ast.ASTKind.Name:
an_mf: ast.Name* = ast.Name*(a0_mf)
if an_mf.id is not None:
slot_mf: mlirlite.Value* = self.Gen.VarGen.lookup_var(trans.SymTab, an_mf.id)
if slot_mf is None: slot_mf = self.Gen.VarGen.lookup_module_var(trans.SymTab, an_mf.id)
if self.Gen.VarGen._SlifeOwned(slot_mf) != 0:
self.Gen.TypeGenErr.fatal_error(a0_mf, f"cannot free '{an_mf.id}' manually: it is a `slife` object owned by the compiler (freed automatically at scope exit), so a manual free would double-free it; if you want to manage the lifetime yourself, allocate it as `new heap hlife ...`", "LifetimeError")
# 调用点生命期检查(M2.3 v1b 步骤4):被调者"会逃逸或直接返回"的形参位上,实参不得
# 交出本帧栈地址(&local / 局部数组退让)。此处拿不到被调者形参类型,故传一个哑指针
# 类型——只用于让 life_of_expr 判定"裸局部数组名处于指针位置 = 数组退让 = 栈地址"。
if cargs is not None and func_name is not None:
esc_mask: int = GenApi.get_param_escapes(func_name)
if esc_mask == 0:
# 跨模块回退(v3):本文件摘要表按文件重置,跨模块的被调者只能靠 .vpi 传递。
# 解析被调者所属模块 sha1:带模块前缀(mod.f)用限定名,否则用 from-import 别名。
mod_sha_ec: str = None
if cl.func is not None and cl.func.kind() == ast.ASTKind.Attribute:
at_ec: ast.Attribute* = ast.Attribute*(cl.func)
mod_p_ec: str = None
if at_ec.value is not None and at_ec.value.kind() == ast.ASTKind.Name:
mn_ec: ast.Name* = ast.Name*(at_ec.value)
if mn_ec.id is not None: mod_p_ec = mn_ec.id
elif at_ec.value is not None and at_ec.value.kind() == ast.ASTKind.Attribute:
mod_p_ec = self.Gen.ExprGenAttr._attr_module_path_str(pool, at_ec.value)
if mod_p_ec is not None:
fi_ec: str = None
if trans is not None: fi_ec = trans._from_imports
mod_sha_ec = self.Gen.StructGen._resolve_module_sha1(mod_p_ec, fi_ec)
if mod_sha_ec is None and trans is not None and trans._from_imports is not None:
mod_sha_ec = self._lookup_mod_sha1(pool, trans._from_imports, func_name, 0)
if mod_sha_ec is not None:
pk_ec: int = self.Gen.CallGenBase.lookup_escape_via_vpi(pool, mod_sha_ec, None, func_name)
if pk_ec != 0: esc_mask = int(pk_ec & 4294967295)
if esc_mask != 0:
_dummy_ptr: mlirlite.LLVMType* = mlirlite.Ptr(pool, mlirlite.Int8(pool))
for ai in range(can):
if ((esc_mask >> int(ai)) & 1) != 0:
GenApi.fail_if_stack_addr_escape(cargs.get(ai), "an argument bound to a parameter that the callee escapes or returns", trans, _dummy_ptr)
# P2 willFree:查被调者的形参失效标注(本文件表 → 跨模块 .vpi 回退)。
# 标记动作在**实参全部翻译完成之后**(见下方 "# Default-argument filling" 之前),
# 否则 `stdlib.free(p)` 自己那次读取 p 会被误判为"使用已失效变量"。
wf_mask: int = 0
if cargs is not None and func_name is not None:
wf_mask = GenApi.get_param_willfree(func_name)
if wf_mask == 0:
mod_sha_wf: str = None
if cl.func is not None and cl.func.kind() == ast.ASTKind.Attribute:
at_wf: ast.Attribute* = ast.Attribute*(cl.func)
mod_p_wf: str = None
if at_wf.value is not None and at_wf.value.kind() == ast.ASTKind.Name:
mn_wf: ast.Name* = ast.Name*(at_wf.value)
if mn_wf.id is not None: mod_p_wf = mn_wf.id
elif at_wf.value is not None and at_wf.value.kind() == ast.ASTKind.Attribute:
mod_p_wf = self.Gen.ExprGenAttr._attr_module_path_str(pool, at_wf.value)
if mod_p_wf is not None:
fi_wf: str = None
if trans is not None: fi_wf = trans._from_imports
mod_sha_wf = self.Gen.StructGen._resolve_module_sha1(mod_p_wf, fi_wf)
if mod_sha_wf is None and trans is not None and trans._from_imports is not None:
mod_sha_wf = self._lookup_mod_sha1(pool, trans._from_imports, func_name, 0)
if mod_sha_wf is not None:
wf_mask = self.Gen.CallGenBase.lookup_willfree_via_vpi(pool, mod_sha_wf, None, func_name)
# Generic function calls (PEP 695 def f[T]) are intercepted first: infer type args and
# monomorphize here (templates emit no plain function); hit_box=1 means handled-with-void.
h0_hit: t.CPtr = stdlib.malloc(4)
if h0_hit is not None: h0_hit[0] = 0
_tc_h0_generic_func_res: mlirlite.Value* = self.Gen.CallGenDispatch._tc_h0_generic_func(pool, builder, mod, cl, funcs_ptr, func_count, trans, func_name, cargs, can, h0_hit)
h0_hit_val: int = 0
if h0_hit is not None:
h0_hit_val = h0_hit[0]
stdlib.free(h0_hit)
if _tc_h0_generic_func_res is not None: return _tc_h0_generic_func_res
if h0_hit_val != 0: return None
# super()/ctraits detection: a void method yields None but is handled, so hit_box must
# separate "handled" from "missed"; otherwise super() falls through and reports undefined.
h1_hit: t.CPtr = stdlib.malloc(4)
if h1_hit is not None: h1_hit[0] = 0
_tc_h1_super_ctraits_res: mlirlite.Value* = self.Gen.CallGenDispatch._tc_h1_super_ctraits(pool, builder, mod, cl, funcs_ptr, func_count, trans, func_name, cargs, can, h1_hit)
h1_hit_val: int = 0
if h1_hit is not None:
h1_hit_val = h1_hit[0]
stdlib.free(h1_hit)
if _tc_h1_super_ctraits_res is not None: return _tc_h1_super_ctraits_res
if h1_hit_val != 0:
# Hit super()/ctraits with no return value (void method/macro call): treated as handled.
return None
# ---- func_name is None: generic instantiation + type casts ----
if func_name is None:
# Subscript as the Call func: a generic class constructor (e.g. list[int](pool)).
if cl.func is not None and cl.func.kind() == ast.ASTKind.Subscript and trans is not None:
sub_node: ast.Subscript* = ast.Subscript*(cl.func)
if sub_node.value is not None and sub_node.value.kind() == ast.ASTKind.Name:
gen_nm_node: ast.Name* = ast.Name*(sub_node.value)
gen_class_name: str = gen_nm_node.id
# Check whether this is a generic class template.
template_cd: ast.ClassDef* = self.Gen.ClassGenGeneric._find_generic_template(gen_class_name)
if template_cd is not None:
# Extract the type arguments.
gen_type_args: list[str]* = self.Gen.CallGenCtor._extract_type_args_from_slice(pool, sub_node.slice, trans._from_imports)
if gen_type_args is not None and len(gen_type_args) > 0:
# Specialize the generic class and return the specialized name.
spec_name: str = self.Gen.ClassGenGeneric._specialize_generic_class(trans, gen_class_name, gen_type_args)
if spec_name is not None:
# Generic ctor call: list[str](pool, 8) -> __new__ + __before_init__ + __init__.
# Do not use spec_name as func_name (star-import fallback would pick a wrong SHA1).
return self.Gen.CallGenCtor._translate_generic_ctor_call(pool, builder, mod, spec_name, cl, trans, gen_class_name)
else:
# No generic class template: maybe an explicit generic-function call xxx[T](x);
# TPV compiles generic functions as plain functions, so drop [T] if a same-name function exists.
if self.find_func_in_module(mod, gen_class_name) is not None: func_name = gen_class_name
else:
# Builtin pointer/type subscript cast: (rawptr[list[int]])(lst), (t.CPtr[T])(x),
# (t.CArray[i8, N])(x); rawptr/CPtr/CArray are type subscripts, not generic templates.
if cl.args is not None and len(cl.args) == 1:
sub_cast_ty: mlirlite.LLVMType* = self.Gen.TypeGenResolve.resolve_annotation_type(pool, cl.func, trans._imported_modules, trans._from_imports, trans, 0)
if sub_cast_ty is not None:
sub_cast_arg: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, cl.args.get(0), funcs_ptr, func_count, trans)
if sub_cast_arg is not None:
return self.Gen.CallGenCtor._translate_t_type_cast(pool, builder, sub_cast_arg, sub_cast_ty, cl.args.get(0), trans)
# Generic class template not found: the defining module may not be imported.
gt_err: str = pool.alloc(256)
if gt_err is not None:
viperlib.snprintf(gt_err, 256, "Generic class '%s': template not found; the defining module may not be imported", gen_class_name)
self.Gen.TypeGenErr.fatal_error(gen_nm_node, gt_err)
self.Gen.TypeGenErr.fatal_error(gen_nm_node, "Generic class template not found")
if func_name is None:
# BinOp as the Call func: a union-type annotation used as a type cast, (A*)(value).
if cl.func is not None and cl.func.kind() == ast.ASTKind.BinOp and trans is not None:
binop_ty: mlirlite.LLVMType* = self.Gen.TypeGenResolve.resolve_annotation_type(pool, cl.func, trans._imported_modules, trans._from_imports, trans, 0)
if binop_ty is not None:
binop_args: list[ast.AST*]* = cl.args
if binop_args is not None and len(binop_args) == 1:
binop_arg_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, binop_args.get(0), funcs_ptr, func_count, trans)
if binop_arg_val is not None: return self.Gen.CallGenCtor._translate_t_type_cast(pool, builder, binop_arg_val, binop_ty)
return None
# c.Asm/c.LLVMIR special calls return None when void but are handled, so use hit_box to
# separate "missed"; otherwise they fall through to the external-function path and mis-report.
h2_hit: t.CPtr = stdlib.malloc(4)
if h2_hit is not None: h2_hit[0] = 0
_tc_h2_c_special_res: mlirlite.Value* = self.Gen.CallGenDispatch._tc_h2_c_special(pool, builder, mod, cl, funcs_ptr, func_count, trans, func_name, cargs, can, h2_hit)
h2_hit_val: int = 0
if h2_hit is not None:
h2_hit_val = h2_hit[0]
stdlib.free(h2_hit)
if _tc_h2_c_special_res is not None: return _tc_h2_c_special_res
if h2_hit_val != 0:
# Hit c.Asm/c.LLVMIR with no return value (void): treated as handled.
return None
_tc_h3_stdarg_cast_res: mlirlite.Value* = self.Gen.CallGenDispatch._tc_h3_stdarg_cast(pool, builder, mod, cl, funcs_ptr, func_count, trans, func_name, cargs, can)
if _tc_h3_stdarg_cast_res is not None: return _tc_h3_stdarg_cast_res
_tc_h4_type_casts_res: mlirlite.Value* = self.Gen.CallGenDispatch._tc_h4_type_casts(pool, builder, mod, cl, funcs_ptr, func_count, trans, func_name, cargs, can)
if _tc_h4_type_casts_res is not None: return _tc_h4_type_casts_res
_tc_h5_sizeof_len_isinst_res: mlirlite.Value* = self.Gen.CallGenDispatch._tc_h5_sizeof_len_isinst(pool, builder, mod, cl, funcs_ptr, func_count, trans, func_name, cargs, can)
if _tc_h5_sizeof_len_isinst_res is not None: return _tc_h5_sizeof_len_isinst_res
# Method dispatch may succeed yet return None (e.g. void __before_init__/__init__), so hit_box
# must separate "recognized method" from "missed"; otherwise the object name is looked up as a module.
h6_hit: t.CPtr = stdlib.malloc(4)
if h6_hit is not None: h6_hit[0] = 0
_tc_h6_method_dispatch_res: mlirlite.Value* = self.Gen.CallGenDispatch._tc_h6_method_dispatch(pool, builder, mod, cl, funcs_ptr, func_count, trans, func_name, cargs, can, h6_hit)
h6_hit_val: int = 0
if h6_hit is not None:
h6_hit_val = h6_hit[0]
stdlib.free(h6_hit)
if _tc_h6_method_dispatch_res is not None: return _tc_h6_method_dispatch_res
if h6_hit_val != 0:
# Hit the method path with no return value (void semantics): treated as handled.
return None
# Fetch keyword arguments.
keywords: list[ast.AST*]* = cl.keywords
kw_count: sizet = 0
if keywords is not None: kw_count = len(keywords)
# Struct constructor: Point() / Point(10, 20) / Point(x=10, y=20). Enforce namespace isolation
# (bare name needs import/local def, module-qualified needs the import); use find_struct_by_module.
struct_ty: mlirlite.LLVMType* = None
gen_ctor_entry: GenTypes.StructEntry* = None
# Module-qualified construction first: resolve the class by module SHA1 before the bare-name
# branch, else cross-module same-name classes resolve to the wrong entry (Scene3D root cause).
mod_qual_done: int = 0
if trans is not None and cl.func is not None and cl.func.kind() == ast.ASTKind.Attribute:
at_qc0: ast.Attribute* = ast.Attribute*(cl.func)
mod_path0: str = None
if at_qc0.value is not None and at_qc0.value.kind() == ast.ASTKind.Name:
mod_nm0: ast.Name* = ast.Name*(at_qc0.value)
if mod_nm0.id is not None: mod_path0 = mod_nm0.id
elif at_qc0.value is not None and at_qc0.value.kind() == ast.ASTKind.Attribute: mod_path0 = self.Gen.ExprGenAttr._attr_module_path_str(pool, at_qc0.value)
if mod_path0 is not None:
if self.Gen.ImportsGen.is_module_imported_or_known(trans._imported_modules, mod_path0) != 0:
mod_sha1_0: str = self._lookup_module_sha1(mod_path0)
if mod_sha1_0 is not None:
gen_ctor_entry = self.Gen.StructGen.find_struct_by_module(func_name, mod_sha1_0)
mod_qual_done = 1
vis: int = self.Gen.StructGen.is_struct_visible(func_name)
if mod_qual_done == 0 and vis != 0:
if trans is not None: gen_ctor_entry = self.Gen.StructGen.find_struct_by_module(func_name, trans.ModuleSha1)
if gen_ctor_entry is None: gen_ctor_entry = self.Gen.StructGen.find_struct(func_name)
# Forward-declaration placeholder (registered for a bare generic template without type args) is not a
# real struct: leave struct_ty unset so generic inference specializes it by the actual arguments.
if gen_ctor_entry is not None and gen_ctor_entry.IsForwardDecl != 0 and self.Gen.ClassGenGeneric._find_generic_template(func_name) is not None: gen_ctor_entry = None
if gen_ctor_entry is not None: struct_ty = gen_ctor_entry.Ty
elif trans is not None and cl.func is not None and cl.func.kind() == ast.ASTKind.Attribute:
at_qc: ast.Attribute* = ast.Attribute*(cl.func)
if at_qc.value is not None and at_qc.value.kind() == ast.ASTKind.Name:
mod_nm_qc: ast.Name* = ast.Name*(at_qc.value)
imp_ok: int = self.Gen.ImportsGen.is_module_imported_or_known(trans._imported_modules, mod_nm_qc.id)
if imp_ok != 0:
# Module-qualified construction: resolve by project module SHA1, not trans.ModuleSha1
# (the current file); otherwise the bare-name fallback picks the wrong same-name class.
mod_sha1_qc2: str = None
if mod_nm_qc.id is not None: mod_sha1_qc2 = self._lookup_module_sha1(mod_nm_qc.id)
if mod_sha1_qc2 is not None: gen_ctor_entry = self.Gen.StructGen.find_struct_by_module(func_name, mod_sha1_qc2)
if gen_ctor_entry is None: gen_ctor_entry = self.Gen.StructGen.find_struct(func_name)
if gen_ctor_entry is not None: struct_ty = gen_ctor_entry.Ty
elif at_qc.value is not None and at_qc.value.kind() == ast.ASTKind.Attribute:
# Multi-level module path construction: w32.fileio.File(...); build the path via
# attr_module_path_str, check the full import, then find the struct by that module's SHA1.
mod_path_qc: str = self.Gen.ExprGenAttr._attr_module_path_str(pool, at_qc.value)
if mod_path_qc is not None:
imp_ok_qc: int = self.Gen.ImportsGen.is_module_imported(trans._imported_modules, mod_path_qc)
if imp_ok_qc != 0:
mod_sha1_qc: str = self._lookup_module_sha1(mod_path_qc)
if mod_sha1_qc is not None: gen_ctor_entry = self.Gen.StructGen.find_struct_by_module(func_name, mod_sha1_qc)
if gen_ctor_entry is None: gen_ctor_entry = self.Gen.StructGen.find_struct(func_name)
if gen_ctor_entry is not None: struct_ty = gen_ctor_entry.Ty
# Aliased class construction (from X import Class as Alias): the struct is registered under its
# original name, so resolve alias -> original before find_struct to avoid a link-time undefined reference.
if gen_ctor_entry is None and trans is not None and cl.func is not None and cl.func.kind() == ast.ASTKind.Name:
if trans._from_imports is not None:
ctor_sha1: str = self._lookup_mod_sha1(pool, trans._from_imports, func_name, 0)
if ctor_sha1 is not None:
ctor_orig: str = self.Gen.ImportsGen.lookup_from_import_original(trans._from_imports, func_name)
if ctor_orig is not None:
# Copy into a separate NUL-terminated buffer (the from_imports buffer is not terminated).
co_len: sizet = 0
while ctor_orig[co_len] != ' ' and ctor_orig[co_len] != '\0': co_len += 1
co_buf: str = pool.alloc(co_len + 1)
if co_buf is not None:
for co_i in range(co_len): co_buf[co_i] = ctor_orig[co_i]
co_buf[co_len] = '\0'
# Look up the struct by original name (visibility already ensured by the alias).
gen_ctor_entry = self.Gen.StructGen.find_struct_by_module(co_buf, ctor_sha1)
if gen_ctor_entry is None: gen_ctor_entry = self.Gen.StructGen.find_struct(co_buf)
if gen_ctor_entry is not None:
struct_ty = gen_ctor_entry.Ty
func_name = co_buf
if struct_ty is not None:
if can == 0 and kw_count == 0:
# OOP struct: call __before_init__ even with no args; use gen_ctor_entry to avoid same-name lookup errors.
ctor_is_oop: int = 0
if gen_ctor_entry is not None: ctor_is_oop = gen_ctor_entry.IsOOP
if ctor_is_oop != 0: return self.Gen.CallGenCtor._translate_struct_ctor(pool, builder, mod, struct_ty, func_name, cargs, sizet(0), trans, cl.IsVolatile)
# @t.NoVTable classes (no methods, pointer/class semantics): no-arg construction must still
# allocate and return a pointer, else the LHS stores a null pointer and dereferencing crashes.
ctor_is_novt: int = 0
if gen_ctor_entry is not None: ctor_is_novt = gen_ctor_entry.IsNoVTable
if ctor_is_novt != 0: return self.Gen.CallGenCtor._translate_struct_ctor(pool, builder, mod, struct_ty, func_name, cargs, sizet(0), trans, cl.IsVolatile)
# Plain struct: check for default values (access via entry directly).
gen_has_defaults: int = 0
if gen_ctor_entry is not None:
for gdi in range(gen_ctor_entry.FieldCount):
gdf: GenTypes.FieldEntry* = self.Gen.StructGen._get_field_entry(gen_ctor_entry, gdi)
if gdf is not None and gdf.DefaultVal is not None:
gen_has_defaults = 1
break
else: gen_has_defaults = self.Gen.StructGen.struct_has_defaults(func_name)
if gen_has_defaults == 1:
# Has defaults: use the constructor path to apply them.
return self.Gen.CallGenCtor._translate_struct_ctor(pool, builder, mod, struct_ty, func_name, cargs, sizet(0), trans, cl.IsVolatile)
# No defaults: return zeroinitializer.
ret: mlirlite.Value* = mlirlite.ConstZero(pool, struct_ty)
return ret
if kw_count > 0:
# Keyword arguments present: assign by field name.
return self.Gen.CallGenCtor._translate_struct_ctor_kw(pool, builder, mod, struct_ty, func_name, cargs, can, keywords, kw_count, trans, cl.IsVolatile)
return self.Gen.CallGenCtor._translate_struct_ctor(pool, builder, mod, struct_ty, func_name, cargs, can, trans, cl.IsVolatile)
# Bare generic class construction: Box(42) -> infer type args from the actual arguments,
# specialize, construct. Also supports module-qualified vector.Vector(...); placeholders must not block inference.
gen_ctor_is_fwd_generic: int = 0
if gen_ctor_entry is not None and gen_ctor_entry.IsForwardDecl != 0:
if self.Gen.ClassGenGeneric._find_generic_template(func_name) is not None: gen_ctor_is_fwd_generic = 1
if struct_ty is None and (gen_ctor_entry is None or gen_ctor_is_fwd_generic != 0) and trans is not None:
is_gen_bare: int = 0
if cl.func is not None and cl.func.kind() == ast.ASTKind.Name: is_gen_bare = 1
elif cl.func is not None and cl.func.kind() == ast.ASTKind.Attribute:
at_gb: ast.Attribute* = ast.Attribute*(cl.func)
if at_gb.value is not None and at_gb.value.kind() == ast.ASTKind.Name:
am_gb: ast.Name* = ast.Name*(at_gb.value)
if am_gb.id is not None:
if self.Gen.ImportsGen.is_module_imported(trans._imported_modules, am_gb.id) != 0: is_gen_bare = 1
if is_gen_bare != 0:
template_cd_bare: ast.ClassDef* = self.Gen.ClassGenGeneric._find_generic_template(func_name)
if template_cd_bare is not None and template_cd_bare.type_params is not None:
tp_bare: list[str]* = template_cd_bare.type_params
tp_count_bare: sizet = len(tp_bare)
if tp_count_bare > 0:
inferred_args_bare: list[str]* = list[str](pool, 8)
if inferred_args_bare is not None:
# Map each type param to the positional arg whose __init__ annotation matches it (not by index);
ctx_args_bare: list[str]* = self.Gen.CallGenCtor._infer_ctor_type_args_from_ctx(pool, trans, func_name, int(tp_count_bare), cl)
tpi_bare: sizet = 0
for tpi_bare in range(tp_count_bare):
aip_bare: int = self.Gen.CallGenCtor._find_ctor_type_param_arg_index(func_name, tp_bare.get(tpi_bare))
if aip_bare < 0: aip_bare = int(tpi_bare)
ais_bare: sizet = sizet(aip_bare)
# [D25 fail-fast] Never default to "int": with no hint and too few args it would
# wrongly specialize to list[int]/Box[int] (4-byte elements, pointer truncation/crash).
ta_bare: str = None
if ais_bare < can: ta_bare = self.Gen.CallGenCtor._infer_generic_ctor_type_arg(pool, cargs.get(ais_bare), func_name, trans)
if ta_bare is None:
# [C2] Keyword-only arg (Box(v=42)): match the keyword by __init__ param name
# and infer from it (order-independent; returns -1 if the slot is positional).
kwi_bare: int = self.Gen.CallGenCtor._find_ctor_type_param_kw_arg_index(func_name, tp_bare.get(tpi_bare), keywords, kw_count, int(can))
if kwi_bare >= 0:
kwn_bare: ast.Keyword* = ast.Keyword*(keywords.get(sizet(kwi_bare)))
if kwn_bare is not None and kwn_bare.value is not None: ta_bare = self.Gen.CallGenCtor._infer_generic_ctor_type_arg(pool, kwn_bare.value, func_name, trans)
if ta_bare is None and ctx_args_bare is not None:
# [C1] The annotation already gives type args (e.g. s: Stack[int] = Stack()): specialize with them.
ta_bare = ctx_args_bare.get(tpi_bare)
if ta_bare is None:
self.Gen.TypeGenErr.fatal_error(cl, f"Cannot infer type argument {tpi_bare + 1} of generic class '{func_name}' from the arguments, " f"silent default to int is forbidden (it produces wrong specializations such as list[int] -> elements treated as 4 bytes/i32 -> pointer truncation crash); " f"annotate the argument type or annotate explicitly, e.g. {func_name}[Type](...)", "TypeError")
inferred_args_bare.append(ta_bare)
spec_name_bare: str = self.Gen.ClassGenGeneric._specialize_generic_class(trans, func_name, inferred_args_bare)
if spec_name_bare is not None: return self.Gen.CallGenCtor._translate_generic_ctor_call(pool, builder, mod, spec_name_bare, cl, trans, func_name)
# REnum variant construction: ClassName.VariantName(args...), e.g. LLVMType.Int(1). Must run
# before cross-module lookup, otherwise ClassName is mistaken for a module name.
if cl.func is not None and cl.func.kind() == ast.ASTKind.Attribute:
rc_at: ast.Attribute* = ast.Attribute*(cl.func)
if rc_at.value is not None and rc_at.value.kind() == ast.ASTKind.Name:
rc_cls_node: ast.Name* = ast.Name*(rc_at.value)
if rc_cls_node.id is not None:
rc_cls_name: str = rc_cls_node.id
# Check whether ClassName is a registered REnum; also use _is_renum_by_entry (a __tag
# makes it an REnum) so a missing variant table does not silently fall through to a misleading error.
if self.Gen.ClassGenEnum._is_renum_class_name(rc_cls_name) != 0 or self.Gen.ClassGenEnum._is_renum_by_entry(rc_cls_name) != 0:
# Look up the variant name func_name in that REnum class.
rc_tag: int = self.Gen.ClassGenEnum._lookup_renum_variant_in_class(rc_cls_name, func_name)
if rc_tag < 0:
# [D32 fail-fast] It is an REnum but the variant is missing: the table is incomplete; report it here.
self.Gen.TypeGenErr.fatal_error(cl, f"REnum class '{rc_cls_name}': variant '{func_name}' not found in the variant table" f" ({self.Gen.ClassGenEnum.RenumTableDump()}); " f"the variant table is not registered or the variant name does not match", "NameError")
if rc_tag >= 0: return self.Gen.CallGenCtor._translate_renum_construct(pool, builder, mod, rc_cls_name, func_name, rc_tag, cargs, can, trans, cl.IsVolatile)
elif rc_at.value is not None and rc_at.value.kind() == ast.ASTKind.Attribute:
# Module-qualified REnum variant: mod.Class.Variant(args...), e.g. mlirlite.LLVMType.Struct(...).
# Only require the leftmost module imported and the class name in the global REnum table.
rc_mq_path: str = self.Gen.ExprGenAttr._attr_module_path_str(pool, rc_at.value)
if rc_mq_path is not None:
rc_mq_dot: str = string.strrchr(rc_mq_path, 46) # 46 = ord('.')
if rc_mq_dot is not None and rc_mq_dot[1] != '\0':
# Module path = everything before the last segment (multi-level supported); use
# strlen integer differences, not pointer subtraction (which emits illegal IR).
rc_mq_len: sizet = string.strlen(rc_mq_path) - string.strlen(rc_mq_dot + 1) - 1
rc_mq_mod: str = pool.alloc(rc_mq_len + 1)
if rc_mq_mod is not None:
string.memcpy(rc_mq_mod, rc_mq_path, rc_mq_len)
rc_mq_mod[rc_mq_len] = '\0'
rc_mq_imp: int = 1
if trans is not None and trans._imported_modules is not None: rc_mq_imp = self.Gen.ImportsGen.is_module_imported(trans._imported_modules, rc_mq_mod)
if rc_mq_imp != 0:
rc_mq_cls: str = rc_mq_dot + 1
# Check whether the class name is a registered REnum class.
if self.Gen.ClassGenEnum._is_renum_class_name(rc_mq_cls) != 0:
rc_mq_tag: int = self.Gen.ClassGenEnum._lookup_renum_variant_in_class(rc_mq_cls, func_name)
if rc_mq_tag >= 0: return self.Gen.CallGenCtor._translate_renum_construct(pool, builder, mod, rc_mq_cls, func_name, rc_mq_tag, cargs, can, trans, cl.IsVolatile)
# Bare async call (TPV-M1/M2): calling an async function (task(5,6)) emits create_<name>(...).
# Intercept before bare-name lookup: async functions register only create_/resume_, not the bare name.
if cl.func is not None and cl.func.kind() == ast.ASTKind.Name:
if func_name is not None and trans is not None:
if self.Gen.AsyncGen.is_async_func(trans, func_name) != 0:
can_buf: str = pool.alloc(64)
if can_buf is not None: viperlib.snprintf(can_buf, 64, "create_%s", func_name)
create_an: str = self.Gen.FuncGenBase._mangle_func_name(trans, can_buf, 0)
create_fn_an: mlirlite.Function* = self.find_func_in_module(mod, create_an)
if create_fn_an is not None:
frame_ty_an: mlirlite.LLVMType* = self.Gen.AsyncGen._lookup_coro_frame(trans, func_name)
if frame_ty_an is not None:
ret_ptr_ty_an: mlirlite.LLVMType* = mlirlite.Ptr(pool, frame_ty_an)
i8_ptr_ty_an: mlirlite.LLVMType* = mlirlite.Ptr(pool, mlirlite.Int8(pool))
# M2: async call arguments (task(5, 6) -> create_task(5, 6)).
arg_head_an: mlirlite.Value* = None
arg_tail_an: mlirlite.Value* = None
arg_cnt_an: int = 0
if cl.args is not None:
nb_an: sizet = len(cl.args)
j_an: sizet = 0
for j_an in range(nb_an):
av_an: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, cl.args.get(j_an), None, 0, trans)
if av_an is None: av_an = mlirlite.const_int32(pool, 0)
av_an.Next = None
if arg_head_an is None:
arg_head_an = av_an
arg_tail_an = av_an
else:
arg_tail_an.Next = av_an
arg_tail_an = av_an
arg_cnt_an = arg_cnt_an + 1
child_an: mlirlite.Value* = mlirlite.build_call(builder, create_an, arg_head_an, arg_cnt_an, ret_ptr_ty_an, 0)
if child_an is not None: return mlirlite.build_bitcast(builder, child_an, i8_ptr_ty_an)
return mlirlite.const_int32(pool, 0)
# Find the function: exact match by bare name in the table (same module), then suffix match in
# the module list (SHA1-prefixed). Module-qualified/attribute calls must not suffix-match (would hit methods).
actual_funcs: GenTypes.FuncEntry* = funcs_ptr
actual_func_count: int = func_count
if actual_funcs is None and trans is not None:
actual_funcs = trans._funcs
actual_func_count = trans._func_count
found_func: mlirlite.Function* = None
if cl.func is not None and cl.func.kind() == ast.ASTKind.Name:
# Bare-name call: allow exact + suffix matching. Custom decorator wrapper rewrite: original
# name -> __decor_wrap_{name} (wrapper is generated at function-translation end and is reachable).
decor_wrap_nm: str = self.Gen.ClassGenBase.lookup_decor_wrapper(func_name)
if decor_wrap_nm is not None: found_func = self.find_func_in_module(mod, decor_wrap_nm)
else:
# Bare-name call to a function defined in this module: hit the module function table first (authoritative for module-level names), then fall back to the module list scan.
mod_fn_tb: mlirlite.Function* = None
if actual_funcs is not None:
# Prefer same-module functions: the _funcs table is polluted by dependency stubs,
# so pass trans.ModuleSha1 to match the "{ModuleSha1}.{name}" entry first (AstTest root cause).
tbl_owner_sha1: str = None
if trans is not None: tbl_owner_sha1 = trans.ModuleSha1
mod_fn_tb = self.Gen.CallGenLeafKit.find_func_in_table(actual_funcs, actual_func_count, func_name, tbl_owner_sha1)
if mod_fn_tb is not None: found_func = mod_fn_tb
else:
# No table entry: fall back to scanning FuncHead, also passing owner_sha1 so an
# inactive same-name forward declaration does not get mis-bound to an earlier stub.
ff_owner_sha1: str = None
if trans is not None: ff_owner_sha1 = trans.ModuleSha1
found_func = self.find_func_in_module(mod, func_name, ff_owner_sha1)
# Reject suffix matches that hit a class method (SHA1.Class.method): a bare-name call sharing a
# name with a module function would bind to the method and recurse infinitely (stack overflow).
if found_func is not None:
bn_name: str = mlirlite.function_get_name(found_func)
if bn_name is not None:
bn_dots: int = 0
bni2: sizet = 0
while bn_name[bni2] != '\0':
if bn_name[bni2] == '.': bn_dots += 1
bni2 += 1
if bn_dots >= 2:
# Extract segment 2 (SHA1.Class.method -> Class; SHA1.submod.func -> submod).
bn_seg2: str = pool.alloc(64)
if bn_seg2 is not None:
bn_seg2[0] = '\0'
bsi2: sizet = 0
while bn_name[bsi2] != '\0' and bsi2 < 17: bsi2 += 1
bse2: sizet = bsi2
while bn_name[bse2] != '\0' and bn_name[bse2] != '.': bse2 += 1
bsl2: sizet = bse2 - bsi2
if bsl2 > 0 and bsl2 < 63:
string.strncpy(bn_seg2, bn_name + bsi2, bsl2)
bn_seg2[bsl2] = '\0'
# Segment 2 is a registered class -> it is a method, reject (use the module SHA1
# path); when X is neither a class nor an imported module it is still a method, reject.
bn_seg2_cls: int = 0
if self.Gen.StructGen.get_struct_type(bn_seg2) is not None: bn_seg2_cls = 1
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
else:
# Module-qualified/attribute call: exact match first; suffix match accepts only module
# functions. Reject suffix hits that are methods (SHA1.Class.method), keeping SHA1.func / SHA1.submod.func.
found_func = self.find_func_in_module(mod, func_name)
if found_func is not None:
# Module-qualified call (mod.func): require found_func's SHA1 prefix to equal that module,
# else the stub suffix match may hit another module's same-name function (wrong symbol/crash).
xm_at: ast.Attribute* = ast.Attribute*(cl.func)
if xm_at is not None and xm_at.value is not None and trans is not None and trans._imported_modules is not None:
# Module path: one-level Name (syslibtest_impl.run) or nested Attribute (os._win32.exists).
# The nested case must be validated too, else it suffix-matches this module's own define (recursion).
xm_mod_path: str = None
if xm_at.value.kind() == ast.ASTKind.Name:
xm_nm: ast.Name* = ast.Name*(xm_at.value)
if xm_nm.id is not None: xm_mod_path = xm_nm.id
elif xm_at.value.kind() == ast.ASTKind.Attribute: xm_mod_path = self.Gen.ExprGenAttr._attr_module_path_str(pool, xm_at.value)
if xm_mod_path is not None:
if self.Gen.ImportsGen.is_module_imported_or_known(trans._imported_modules, xm_mod_path) != 0:
mod_sha1_chk: str = self._lookup_module_sha1(xm_mod_path)
if mod_sha1_chk is None and trans._from_imports is not None:
# import X as Y (e.g. import fat32_diskio as diskio): resolve the alias before
# lookup, else this check is skipped and the suffix match hits a same-name local function.
mod_sha1_chk = self.Gen.StructGen._resolve_module_sha1(xm_mod_path, trans._from_imports)
if mod_sha1_chk is not None:
ff_nm2: str = mlirlite.function_get_name(found_func)
if ff_nm2 is not None:
if string.strncmp(ff_nm2, mod_sha1_chk, 16) != 0: found_func = None
ff_name: str = mlirlite.function_get_name(found_func)
if ff_name is not None:
ff_dots: int = 0
ffi2: sizet = 0
while ff_name[ffi2] != '\0':
if ff_name[ffi2] == '.': ff_dots += 1
ffi2 += 1
if ff_dots >= 2:
# Extract segment 2 (SHA1.Class.method -> Class; SHA1.submod.func -> submod).
seg2: str = pool.alloc(64)
if seg2 is not None:
seg2[0] = '\0'
si: sizet = 0
while ff_name[si] != '\0' and si < 17: si += 1
seg_start: sizet = si
seg_end: sizet = si
while ff_name[seg_end] != '\0' and ff_name[seg_end] != '.': seg_end += 1
seg_len: sizet = seg_end - seg_start
if seg_len > 0 and seg_len < 63:
string.strncpy(seg2, ff_name + seg_start, seg_len)
seg2[seg_len] = '\0'
# Segment 2 is a registered class -> it is a method, reject (use the module SHA1
# path); when X is neither a class nor an imported module it is still a method, reject (else a module call would bind to Logger.info).
is_seg2_cls: int = 0
if self.Gen.StructGen.get_struct_type(seg2) is not None: is_seg2_cls = 1
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
# 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:
abs_arg: ast.AST* = cargs.get(0)
if abs_arg is not None:
abs_cls: str = self.Gen.CallGenProp._resolve_obj_class_name(pool, trans, abs_arg)
if abs_cls is not None:
abs_sc: str = self.Gen.ExprGenOps._resolve_dunder_by_class(pool, mod, abs_cls, "__abs__", trans)
if abs_sc is not None:
abs_val: mlirlite.Value* = self.Gen.ExprGen.translate_value(builder, pool, mod, abs_arg, None, 0, trans)
if abs_val is not None: return self.Gen.CallGenCall._call_method_on_ptr(pool, builder, mod, abs_sc, "__abs__", abs_val, None, 0, trans)
# Bare C library calls: if not found and known, auto-create a declare (memset/memmove/memcmp/strcmp etc.).
if found_func is None and cl.func is not None and cl.func.kind() == ast.ASTKind.Name:
found_func = self.Gen.CallGenLib._ensure_c_lib_declare(pool, mod, func_name)
# builtins auto-link: when no C library matches, link ord/chr etc. from the builtins module by
# creating a {builtins_sha1}.{name} declare with the agreed signature (ord(i8)->i32, chr(i32)->i8).
if found_func is None:
bi_sha1: str = self._lookup_module_sha1("builtins")
if bi_sha1 is not None:
bi_ret: mlirlite.LLVMType* = None
bi_p1: mlirlite.LLVMType* = None
bi_p2: mlirlite.LLVMType* = None
if string.strcmp(func_name, "ord") == 0:
bi_ret = mlirlite.Int32(pool)
bi_p1 = mlirlite.Int8(pool)
elif string.strcmp(func_name, "chr") == 0:
bi_ret = mlirlite.Int8(pool)
bi_p1 = mlirlite.Int32(pool)
elif string.strcmp(func_name, "min") == 0 or string.strcmp(func_name, "max") == 0:
# min/max numeric comparison (builtins.vp takes/returns double; int/float args are coerced).
bi_ret = mlirlite.Double(pool)
bi_p1 = mlirlite.Double(pool)
bi_p2 = mlirlite.Double(pool)
if bi_ret is not None:
bi_sym: str = func_name
if self.Gen.CallGenBase.is_cexport_func(bi_sha1, func_name) == 0:
bi_sym_len: sizet = string.strlen(bi_sha1) + string.strlen(func_name) + 2
bi_sym_buf: str = pool.alloc(bi_sym_len)
if bi_sym_buf is not None:
viperlib.snprintf(bi_sym_buf, bi_sym_len, "%s.%s", bi_sha1, func_name)
bi_sym = bi_sym_buf
bi_decl: mlirlite.Function* = mlirlite.create_declare(pool, mod, bi_sym, bi_ret)
if bi_decl is not None:
mlirlite.add_param(pool, bi_decl, bi_p1, "v")
if bi_p2 is not None: mlirlite.add_param(pool, bi_decl, bi_p2, "w")
found_func = bi_decl
# Use accessors to bypass stub type restrictions.
found_ret_ty: mlirlite.LLVMType* = None
found_params: t.CPtr = None
if found_func is not None:
found_ret_ty = mlirlite.function_get_ret_ty(found_func)
found_params = mlirlite.function_get_params(found_func)
if found_ret_ty is not None: call_ret_ty: mlirlite.LLVMType* = found_ret_ty
else:
# found_func=None (cross-module stub not injected): first get the exact return type from the vpi
# annotation cache (handles tuple[T1,T2] -> anonymous struct), then fall back to the whitelist inference.
vpi_ret_ty: mlirlite.LLVMType* = None
# Source module SHA1 for cross-module return-type resolution (assigned only in the Attribute
# branch). Must be initialized to None, else a bare call reads stack garbage and crashes.
mod_ret_sha1: str = None
# Module path (assigned only for Attribute); declared outside the if so the bare-name fallback can read it safely.
mod_path_ret: str = None
if cl.func is not None and cl.func.kind() == ast.ASTKind.Attribute:
at_ret: ast.Attribute* = ast.Attribute*(cl.func)
# Resolve the callee module path: both one-level (stdlib.free) and multi-level
# (w32.win32file.CreatePipe). A missing multi-level path would wrongly fail-fast a resolvable call.
if at_ret.value is not None and at_ret.value.kind() == ast.ASTKind.Name:
mod_ret_nm: ast.Name* = ast.Name*(at_ret.value)
mod_path_ret = mod_ret_nm.id
elif at_ret.value is not None and at_ret.value.kind() == ast.ASTKind.Attribute: mod_path_ret = self.Gen.ExprGenAttr._attr_module_path_str(pool, at_ret.value)
if mod_path_ret is not None:
# import X as Y (e.g. import lib.core.Gen.GenApi as GenApi): an exact module-name lookup
# cannot resolve the alias and would wrongly fail-fast; use _resolve_module_sha1 (alias restore + exact/suffix).
fi_from_rt: str = None
if trans is not None: fi_from_rt = trans._from_imports
mod_ret_sha1 = self.Gen.StructGen._resolve_module_sha1(mod_path_ret, fi_from_rt)
if mod_ret_sha1 is not None:
# Lazy in-function import: VpiReader imports HandlesExprCall, so a top-level import would cycle.
VpiReader._ensure_vpi_ret_annot_loaded(mod_ret_sha1, self.Gen.CallGenBase.TempDir)
ret_annot_fc: str = VpiReader._lookup_vpi_ret_annot(mod_ret_sha1, None, func_name)
if ret_annot_fc is None:
# Package vpi miss (the function is defined in a submodule, e.g. fltk.Fl_Input_value in
# fltk._cfl_input): use re-export to locate the defining module, then query its vpi.
rr_src: str = self.Gen.ImportsGen.lookup_reexport(mod_ret_sha1, func_name)
if rr_src is None: rr_src = self._lookup_reexport_from_vpi(pool, mod_ret_sha1, func_name)
if rr_src is not None:
rr_total: sizet = string.strlen(rr_src)
rr_i: sizet = 0
while rr_i < rr_total and ret_annot_fc is None:
while rr_i < rr_total and rr_src[rr_i] == ' ': rr_i += 1
if rr_i >= rr_total: break
rr_start: sizet = rr_i
while rr_i < rr_total and rr_src[rr_i] != ' ': rr_i += 1
rr_len: sizet = rr_i - rr_start
rr_buf: str = pool.alloc(rr_len + 1)
if rr_buf is not None:
string.strncpy(rr_buf, rr_src + rr_start, rr_len)
rr_buf[rr_len] = '\0'
rr_sha1: str = self._lookup_module_sha1(rr_buf)
if rr_sha1 is not None:
VpiReader._ensure_vpi_ret_annot_loaded(rr_sha1, self.Gen.CallGenBase.TempDir)
ret_annot_fc = VpiReader._lookup_vpi_ret_annot(rr_sha1, None, func_name)
if ret_annot_fc is not None: vpi_ret_ty = VpiReader._resolve_vpi_ret_annot_str(pool, ret_annot_fc, mlirlite.Int32(pool), mlirlite.Ptr(pool, mlirlite.Int8(pool)))
if vpi_ret_ty is not None: call_ret_ty = vpi_ret_ty
elif mod_ret_sha1 is not None:
# Cross-module bare CExport function (e.g. mm.malloc -> @malloc): resolve the return type
# exactly from the dependency module, else the default i32 truncates the pointer (mm.malloc root cause).
cross_ret: mlirlite.LLVMType* = self._lookup_method_ret_ty_via_vpi(pool, mod_ret_sha1, None, func_name)
if cross_ret is None:
if trans is not None and trans._declare_only == 0: cross_ret = self._LookupMethodRetTyFromTextLL(pool, mod_ret_sha1, None, func_name, 1)
if cross_ret is not None: call_ret_ty = cross_ret
else:
# Dependency text.ll not generated yet (Phase A middle pass): fatal_if_unknown=0, only a temporary placeholder.
call_ret_ty = self.Gen.CallGenLib._infer_external_func_ret_ty(pool, func_name, node, 0)
else:
# Bare call (cl.func is Name, mod_ret_sha1=None): from-import/closure/function-pointer targets are resolvable (placeholder, no fail-fast); pure external C functions fail-fast.
bare_resolvable: int = 0
if self.Gen.CallGenLeafKit.is_closure_name(func_name) != 0: bare_resolvable = 1
if trans is not None and trans.SymTab is not None:
if self.Gen.VarGen.lookup_var(trans.SymTab, func_name) is not None: bare_resolvable = 1
if trans is not None and trans._from_imports is not None:
if self._lookup_mod_sha1(pool, trans._from_imports, func_name, 0) is not None: bare_resolvable = 1
# M3 session functions: a name hit in the session table is also a placeholder (no fail-fast);
# the return type comes from repl_try_session_func_call, and this call_ret_ty is unused.
if trans is not None and trans.ReplCtx is not None and cl.func is not None and cl.func.kind() == ast.ASTKind.Name and self.Gen.VarGen.repl_find_func_entry(trans.ReplCtx, func_name) is not None:
bare_resolvable = 1
call_ret_ty = self.Gen.CallGenLib._infer_external_func_ret_ty(pool, func_name, node, 1 - bare_resolvable)
if found_func is None:
fp_alloca: mlirlite.Value* = None
if cl.func is None or cl.func.kind() == ast.ASTKind.Name:
# Call to a closure variable captured inside a closure: read the closure object pointer from env;
# lookup_var must not hit the parent function's alloca (SSA disorder -> llc error).
if trans is not None and GenApi.is_nonlocal_name(trans, func_name) != 0:
fp_alloca = self.Gen.StmtGenDecl.load_nonlocal_var(trans, func_name)
if fp_alloca is not None:
# translate_closure_call expects i8** storage (one load gives the closure object
# pointer) but load_nonlocal_var returns the pointer directly, so stash it in a temporary alloca.
i8_ty_nl: mlirlite.LLVMType* = mlirlite.Int8(pool)
i8_ptr_ty_nl: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ty_nl)
tmp_alloca_nl: mlirlite.Value* = self.Gen.VarGen._alloca_at_entry(builder, i8_ptr_ty_nl)
if tmp_alloca_nl is not None:
mlirlite.build_store(builder, fp_alloca, tmp_alloca_nl)
fp_alloca = tmp_alloca_nl
else: fp_alloca = self.Gen.VarGen.lookup_var(trans.SymTab, func_name)
if fp_alloca is not None:
# Callable object __call__: obj(args) -> obj.__call__(args) when the variable is a class
# instance whose class (with MRO) defines __call__; otherwise keep the closure/function-pointer path.
if trans is not None and self.Gen.CallGenLeafKit.is_closure_name(func_name) == 0 and self.Gen.CallGenLeafKit._is_func_ptr_alloca(fp_alloca) == 0:
c_obj: mlirlite.Value* = self.Gen.ExprGenBase._deref_if_ptr_ptr(builder, fp_alloca)
if c_obj is None: c_obj = fp_alloca
c_struct: mlirlite.LLVMType* = None
if c_obj is not None: c_struct = self.Gen.StructGen.get_struct_type_from_value(c_obj)
if c_struct is not None:
c_cls: str = self.Gen.StructGen.get_class_name_by_type(pool, c_struct)
if c_cls is not None:
c_call_cls: str = self.Gen.ExprGenOps._resolve_dunder_by_class(pool, mod, c_cls, "__call__", trans)
if c_call_cls is not None: return self.Gen.CallGenCall._translate_method_call(pool, builder, mod, c_call_cls, "__call__", c_obj, cl, trans)
# Closure variable (nested function hoisted and registered): extract fn_ptr/env_ptr and call indirectly.
if self.Gen.CallGenLeafKit.is_closure_name(func_name) != 0:
return self.Gen.CallGenLeafKit.translate_closure_call(pool, builder, mod, fp_alloca, cargs, can, trans)
# Function-pointer variable (t.Callable annotation -> i8** alloca): build the signature from args and call indirectly.
if self.Gen.CallGenLeafKit._is_func_ptr_alloca(fp_alloca) != 0:
# Distinguish t.Callable (true function pointer) from t.CPtr (closure pointer): a t.CPtr
# holds {fn_ptr, env_ptr} and must pass env, else a direct call jumps into closure data and crashes.
ve_fp: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, func_name)
is_cptr_annot_fp: int = 0
fp_annot: str = self.Gen.VarGen.var_annot_class_name(ve_fp)
if fp_annot is not None:
# [S4] CPtr marker = annotation base name exactly "CPtr": t.Callable[[A],R] also resolves to
# Ptr(i8) (same Spec), so only the annotation name can carry the marker; substring matching would misfire.
if TSpec.SpecTypeNameBaseIs(fp_annot, "CPtr") != 0: is_cptr_annot_fp = 1
if is_cptr_annot_fp == 0:
return self.Gen.CallGenLeafKit._translate_func_ptr_call(pool, builder, mod, fp_alloca, func_name, cargs, can, trans)
return self.Gen.CallGenLeafKit.translate_closure_call(pool, builder, mod, fp_alloca, cargs, can, trans)
# Other i8* variables go through the closure call path.
return self.Gen.CallGenLeafKit.translate_closure_call(pool, builder, mod, fp_alloca, cargs, can, trans)
# ---- M3: session-function table hit -> take the ctx function-pointer slot and call indirectly ----
# Placed last in the found_func-is-None block so locals/closures shadow session functions; only foreign bare names reach the table.
sfn_box: t.CPtr = None
if trans is not None and trans.ReplCtx is not None and (cl.func is None or cl.func.kind() == ast.ASTKind.Name):
sfn_box = stdlib.malloc(4)
if sfn_box is not None:
sfn_box[0] = 0
sfn_val: mlirlite.Value* = self.Gen.VarGen.repl_try_session_func_call(trans, pool, builder, mod, func_name, cargs, int(can), int(kw_count), node, sfn_box)
sfn_handled: int = int(sfn_box[0])
stdlib.free(sfn_box)
if sfn_handled != 0:
# Hit (including a deliberate M3 error): do not take any "undefined/not imported" path; void returns None as expected.
return sfn_val
# Translate and link arguments; with keyword args and a known definition, match by parameter name.
if kw_count > 0 and found_func is not None and found_params is not None:
return self.Gen.CallGenLib._translate_call_with_kwargs(pool, builder, mod, func_name, found_func, cargs, can, keywords, kw_count, trans)
head: mlirlite.Value* = None
tail: mlirlite.Value* = None
# memcpy only: collect independent references to the first three Values (avoid polluting a shared alloca's Next).
mc_dst2: mlirlite.Value* = None
mc_src2: mlirlite.Value* = None
mc_num2: mlirlite.Value* = None
# "str/bytes is not a generic pointer wildcard" pre-step: same-module bare functions read param
# annotations from this module's .vpi; load that cache once before the loop (bare names only).
same_mod_vpi: int = 0
if trans is not None and trans.ModuleSha1 is not None and cl.func is not None and cl.func.kind() == ast.ASTKind.Name:
if self.Gen.CallGenBase.TempDir is not None and self.Gen.CallGenBase.TempDirLen > 0:
VpiReader._ensure_vpi_param_annot_loaded(trans.ModuleSha1, self.Gen.CallGenBase.TempDir)
same_mod_vpi = 1
for ai in range(can):
arg: ast.AST* = cargs.get(ai)
if arg is not None:
# Fetch the param type first: a single-quoted char literal (CONST_CHAR) passed to str/pointer
# must materialize a 1-char string, else coercion treats the char value as a pointer and crashes.
fp: mlirlite.Param* = None
fpty: mlirlite.LLVMType* = None
if found_func is not None:
fp = mlirlite.function_get_param_head(found_func)
fpi: int = 0
while fp is not None and fpi < ai:
fp = mlirlite.param_get_next(fp)
fpi += 1
if fp is not None: fpty = mlirlite.param_get_ty(fp)
# Value-reference params (&T / &temp T): auto-address the argument (same-module callee).
ao_name: str = None
if found_func is not None: ao_name = mlirlite.function_get_name(found_func)
arg_val: mlirlite.Value* = self.Gen.CallGenBase.translate_call_arg(builder, pool, mod, trans, ao_name, int(ai), arg, fpty, "param")
if arg_val is not None:
# Arg promotion: when found_func is None (stub not injected), infer the expected type from
# the external signature and promote literals (e.g. i32->i64) to avoid declare/call mismatches.
if found_func is None and arg_val.Ty is not None:
# Array arguments decay to element pointers per C semantics, consistent with _translate_method_call.
if self.Gen.ExprGenBase.is_ptr_type(arg_val.Ty) != 0:
arr_pointee_fc: mlirlite.LLVMType* = arg_val.Ty.Pointee
if arr_pointee_fc is not None:
match arr_pointee_fc:
case mlirlite.LLVMType.Array(arr_et_fc, _):
decayed_fc: mlirlite.Value* = mlirlite.build_gep_array(builder, arr_pointee_fc, arr_et_fc, arg_val, mlirlite.const_int32(pool, 0))
if decayed_fc is not None: arg_val = decayed_fc
case _:
pass
# Known vararg C functions (snprintf/sprintf/vsprintf): coerce fixed args by signature
# (C ABI requires i64 size), else call/declare mismatch and Win64 leaves stack garbage in the high 4 bytes.
kvc_ty: mlirlite.LLVMType* = self.Gen.CallGenVa._get_known_vararg_cfunc_param_ty(pool, func_name, ai)
if kvc_ty is not None: arg_val = self.Gen.ExprGenBase.coerce_to_type(builder, arg_val, kvc_ty, arg)
# Prefer the definition-side vpi param annotation for module-qualified calls: the
# whitelist treats all non-pointer Win32 args as i64, but ULONG is now i32 (call/declare mismatch).
ext_param_ty: mlirlite.LLVMType* = None
if cl.func is not None and cl.func.kind() == ast.ASTKind.Attribute:
at_pt: ast.Attribute* = ast.Attribute*(cl.func)
mpath_pt: str = None
if at_pt.value is not None and at_pt.value.kind() == ast.ASTKind.Name:
nm_pt: ast.Name* = ast.Name*(at_pt.value)
mpath_pt = nm_pt.id
elif at_pt.value is not None and at_pt.value.kind() == ast.ASTKind.Attribute: mpath_pt = self.Gen.ExprGenAttr._attr_module_path_str(pool, at_pt.value)
if mpath_pt is not None:
fi_pt: str = None
if trans is not None: fi_pt = trans._from_imports
sha_pt: str = self.Gen.StructGen._resolve_module_sha1(mpath_pt, fi_pt)
if sha_pt is not None:
if self.Gen.CallGenBase.TempDir is not None and self.Gen.CallGenBase.TempDirLen > 0: VpiReader._ensure_vpi_param_annot_loaded(sha_pt, self.Gen.CallGenBase.TempDir)
ext_param_ty = VpiReader._resolve_vpi_param_annot_seg(pool, sha_pt, None, func_name, ai, mlirlite.Int32(pool), mlirlite.Ptr(pool, mlirlite.Int8(pool)))
# Fallback only: use the whitelist when the definition-side annotation is unavailable.
if ext_param_ty is None: ext_param_ty = self.Gen.CallGenLib._get_external_func_param_ty(pool, func_name, ai)
if ext_param_ty is not None:
val_bits_ap: int = self.Gen.ExprGenBase.get_llvm_type_bits(arg_val.Ty)
tgt_bits_ap: int = self.Gen.ExprGenBase.get_llvm_type_bits(ext_param_ty)
# A pointer target reports bit width 0, so the integer-width guard alone skips it and
# the arg stays an integer (e.g. the null-pointer constant 0 stays i32) in the call,
# mismatching the declared pointer param (llvm.call operand type mismatch). Coerce
# pointer targets through coerce_to_type too: it materializes the compile-time constant
# 0 as a null pointer and fails fast on any other implicit int<->ptr conversion.
if self.Gen.ExprGenBase.is_ptr_type(ext_param_ty) != 0:
arg_val = self.Gen.ExprGenBase.coerce_to_type(builder, arg_val, ext_param_ty, arg)
elif val_bits_ap != 0 and tgt_bits_ap != 0 and val_bits_ap != tgt_bits_ap:
if val_bits_ap == 1 and val_bits_ap < tgt_bits_ap: arg_val = mlirlite.build_zext(builder, arg_val, ext_param_ty)
else: arg_val = self.Gen.ExprGenBase.coerce_to_type(builder, arg_val, ext_param_ty, arg)
elif found_func is not None:
# found_func present: coerce to the param type (e.g. stdlib.malloc(size: sizet)); a
# CDefine/i32 literal passed to an i64 param would otherwise leave call(i32) vs define(i64) mismatched.
if fp is not None:
if fpty is not None:
# "str/bytes is not a generic pointer wildcard": a same-module bare function whose
# param annotation is exactly str/bytes but receives an aggregate pointer fails fast (same rule as the cross-module path).
sm_is_str: int = 0
if same_mod_vpi != 0:
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)
elif found_func.IsVarArg == 1:
# Win64 variadic 8-byte slots: pass sub-64-bit integers as i64 (sext) 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.
ab_va: int = self.Gen.ExprGenBase.get_llvm_type_bits(arg_val.Ty)
af_va: int = self.Gen.ExprGenBase.get_llvm_float_bits(arg_val.Ty)
i64_va: mlirlite.LLVMType* = mlirlite.Int64(pool)
if ab_va != 0 and ab_va < 64: arg_val = mlirlite.build_sext(builder, arg_val, i64_va)
elif af_va == 32: arg_val = mlirlite.build_fpext(builder, arg_val, mlirlite.Double(pool))
arg_val.Next = None
if head is None: head = arg_val
else: tail.Next = arg_val
tail = arg_val
# memcpy only: save independent references to the first three arguments.
if ai == 0: mc_dst2 = arg_val
elif ai == 1: mc_src2 = arg_val
elif ai == 2: mc_num2 = arg_val
# P2 willFree:实参已全部翻译完,此刻把被标注形参位上的实参变量标为"已失效"。
# 只处理位置实参且实参是简单变量名(与编译器"标记实参变量槽"的口径一致);
# 该槽其后被读/取址/再传(含再传 free)时由变量读取路径报 LifetimeError。
# 关键字实参路径(_translate_call_with_kwargs)不支持该标记:见文档记录的限制。
if wf_mask != 0 and cargs is not None and trans is not None:
for ai_wf in range(can):
if ((wf_mask >> int(ai_wf)) & 1) == 0: continue
arg_wf: ast.AST* = cargs.get(ai_wf)
if arg_wf is None or arg_wf.kind() != ast.ASTKind.Name: continue
nm_wf: ast.Name* = ast.Name*(arg_wf)
if nm_wf.id is None: continue
ent_wf: GenTypes.VarEntry* = self.Gen.VarGen.lookup_var_entry(trans.SymTab, nm_wf.id)
if ent_wf is not None: ent_wf.Freed = 1
# Default-argument filling: when fewer args than params, fill the gaps; the defaults array is
# right-aligned to args (defaults[0] maps to args[offset], offset = param_count - default_count).
if can > 0 or (can == 0 and cargs is not None):
df_defaults: t.CPtr = None
df_default_count: int = 0
df_param_count: int = 0
# Compute the callee module SHA1 as a qualifier key (module.func -> that module; bare -> current),
# so usb.uhci.init(1 arg) does not wrongly match mm.init (5 params) and add extra arguments.
dfx_callee_sha: str = None
if cl.func is not None and cl.func.kind() == ast.ASTKind.Attribute:
dfx_at: ast.Attribute* = ast.Attribute*(cl.func)
dfx_mod_path: str = None
if dfx_at.value is not None and dfx_at.value.kind() == ast.ASTKind.Name:
dfx_mn: ast.Name* = ast.Name*(dfx_at.value)
if dfx_mn.id is not None: dfx_mod_path = dfx_mn.id
elif dfx_at.value is not None and dfx_at.value.kind() == ast.ASTKind.Attribute:
# Nested module path (os._win32.exists): resolve the full path, like the call-name path below.
dfx_mod_path = self.Gen.ExprGenAttr._attr_module_path_str(pool, dfx_at.value)
if dfx_mod_path is not None:
# Resolve the alias before the SHA1 lookup: _lookup_module_sha1 is an exact module-name lookup
# and returns None for `import X.Y as Z` (Z is not a module name). Without the alias restore the
# qualifier degrades to the current module SHA1, so find_func_defaults misses the defining module
# and the defaults are never filled (rhttps.get(url, verify) emits 2 operands for a 3-param callee).
dfx_fi: str = None
if trans is not None: dfx_fi = trans._from_imports
dfx_callee_sha = self.Gen.StructGen._resolve_module_sha1(dfx_mod_path, dfx_fi)
if dfx_callee_sha is None and self.Gen.TypeGenBase is not None: dfx_callee_sha = self.Gen.TypeGenBase.CurrentModuleSha1
# Look up the FuncEntry in the module table qualified by the defining module SHA1: the table
# contains bare-name dependency stubs, so an unqualified hit would pick mm.init for usb.uhci.init (N17).
if trans is not None and trans._funcs is not None:
df_entry: GenTypes.FuncEntry* = self.Gen.CallGenLeafKit.find_func_entry_in_table_owned(trans._funcs, trans._func_count, func_name, dfx_callee_sha)
if df_entry is not None:
df_defaults = df_entry.Defaults
df_default_count = df_entry.DefaultCount
df_param_count = df_entry.ParamCount
# Always query the global defaults table: the module table's Defaults point into the pool and
# dangle after 1b reset, so use the permanent stdlib snapshot to rebuild defaults (else gd_entry is uninitialized).
gd_entry: GenTypes.FuncDefaultsEntry* = self.Gen.CallGenLeafKit.find_func_defaults(func_name, dfx_callee_sha)
# Package re-export fallback (e.g. json.write defined in includes/json/__writer.vp): the defaults
# table is keyed by the defining module SHA1, so re-resolve the re-export and query again (else defaults are skipped).
if gd_entry is None and dfx_callee_sha is not None:
rr_src_dx: str = self.Gen.ImportsGen.lookup_reexport(dfx_callee_sha, func_name)
if rr_src_dx is None: rr_src_dx = self._lookup_reexport_from_vpi(pool, dfx_callee_sha, func_name)
if rr_src_dx is not None:
rr_sha_dx: str = self._lookup_module_sha1(rr_src_dx)
if rr_sha_dx is not None: gd_entry = self.Gen.CallGenLeafKit.find_func_defaults(func_name, rr_sha_dx)
# Override when df_default_count<=0 too: the caller table may hold a same-name entry with empty
# Defaults (a VpiReader declare), so the real global entry must win or filling never triggers.
if gd_entry is not None and (df_defaults is None or df_default_count <= 0):
df_defaults = gd_entry.Defaults
df_default_count = gd_entry.DefaultCount
df_param_count = gd_entry.ParamCount
# Cross-check: the global table is keyed by bare name, so same-name functions bleed (uhci.init vs
# mm.init). If found_func's real param count differs from ParamCount, drop the table and fall back.
if found_func is not None and df_default_count > 0:
dfx_cnt: int = 0
dfx_p: mlirlite.Param* = mlirlite.function_get_param_head(found_func)
while dfx_p is not None:
dfx_nm: str = mlirlite.param_get_name(dfx_p)
if dfx_nm is None or (string.strcmp(dfx_nm, "%__eh_msg_out__") != 0 and string.strcmp(dfx_nm, "%__eh_code_out__") != 0):
dfx_cnt += 1
dfx_p = mlirlite.param_get_next(dfx_p)
if dfx_cnt > 0 and dfx_cnt != df_param_count:
df_defaults = None
df_default_count = 0
df_param_count = 0
# Fill in the missing arguments.
if df_default_count > 0 and can < df_param_count and (df_defaults is not None or gd_entry is not None):
df_offset: int = df_param_count - df_default_count
for df_di in range(df_default_count):
df_param_idx: int = df_offset + df_di
if df_param_idx >= can:
# Prefer rebuilding from the stdlib snapshot (gd_entry.Defaults' in-pool AST may already be reclaimed).
df_arg_val: mlirlite.Value* = None
if gd_entry is not None: df_arg_val = self.Gen.CallGenLeafKit._build_const_from_snapshot(pool, mod, gd_entry, df_di, trans, builder)
if df_arg_val is None and df_defaults is not None:
df_defaults_list: list[ast.AST*]* = (list[ast.AST*] | t.CPtr)(uint64(df_defaults))
df_default_node: ast.AST* = df_defaults_list.get(df_di)
if df_default_node is not None: df_arg_val = self.Gen.ExprGen.translate_value(builder, pool, mod, df_default_node, None, 0, trans)
if df_arg_val is not None:
df_arg_val.Next = None
if head is None: head = df_arg_val
else: tail.Next = df_arg_val
tail = df_arg_val
# Appended default args must increment can: the downstream coerce/declare steps rebuild
# the chain bounded by can, so not counting them truncates the trailing defaults (json.write OOM root cause).
can += 1
# fail-fast: this callee is known to have defaults, but they could not be rebuilt into
# values (snapshot missing / not a simple constant). Emitting fewer operands than the
# callee declares yields invalid IR that only the MLIR verifier catches
# ("'llvm.call' op incorrect number of operands (N) for callee"); stop here instead.
if can < df_param_count:
self.Gen.TypeGenErr.fatal_error(cl, f"Cross-module default argument cannot be rebuilt into a value (snapshot missing, or the default is not a simple constant): {func_name}; parameter {can + 1} would be missing, which emits an llvm.call with fewer operands than the callee declares", "TypeError")
elif found_func is not None:
# Zero-value fallback when defaults are missing: use found_func's real param count (excluding EH out-params %__eh_msg_out__/%__eh_code_out__) and fill missing args with zeros.
fc_cnt: int = 0
fc_p: mlirlite.Param* = mlirlite.function_get_param_head(found_func)
while fc_p is not None:
fp_nm_z: str = mlirlite.param_get_name(fc_p)
if fp_nm_z is None or (string.strcmp(fp_nm_z, "%__eh_msg_out__") != 0 and string.strcmp(fp_nm_z, "%__eh_code_out__") != 0):
fc_cnt += 1
fc_p = mlirlite.param_get_next(fc_p)
if fc_cnt == 0: fc_cnt = df_param_count if df_param_count > 0 else 0
if can < fc_cnt:
fp_z: mlirlite.Param* = mlirlite.function_get_param_head(found_func)
fpi_z: int = 0
while fp_z is not None and fpi_z < can:
fp_z = mlirlite.param_get_next(fp_z)
fpi_z += 1
while fp_z is not None and fpi_z < fc_cnt:
zero_ty_z: mlirlite.LLVMType* = mlirlite.param_get_ty(fp_z)
zero_val_z: mlirlite.Value* = mlirlite.ConstZero(pool, zero_ty_z)
if zero_val_z is not None:
zero_val_z.Next = None
if head is None: head = zero_val_z
else: tail.Next = zero_val_z
tail = zero_val_z
can += 1 # Zero-value fallback args also count toward the arg count (like defaults).
fp_z = mlirlite.param_get_next(fp_z)
fpi_z += 1
# Use the actual function name from the module (may carry a SHA1 prefix); CExtern/State/CExport
# functions use the bare name. Both declare and define must use found_func.Name (mismatched names cause undefined references).
call_name: str = func_name
# Original function name (recorded for aliased imports, e.g. from X import parse as json_parse -> "parse"),
# used to re-infer the return type after alias handling so _infer_external_func_ret_ty can match.
orig_func_name: str = None
if found_func is not None:
found_name: str = mlirlite.function_get_name(found_func)
if found_name is not None:
call_name = found_name
# CExport bare-name fallback: a cross-module stub declare may carry a SHA1 prefix while the
# definition uses @malloc, causing a link error; extract the sha1 and use the bare name if it is a CExport.
if found_func.IsDeclared != 0:
# Use the single source of truth GenApi.sha1_prefix_len (converged in C10).
if GenApi.sha1_prefix_len(found_name) != 0:
cx_sha1_buf: str = pool.alloc(17)
if cx_sha1_buf is not None:
string.memcpy(cx_sha1_buf, found_name, 16)
cx_sha1_buf[16] = '\0'
if self.Gen.CallGenBase.is_cexport_func(cx_sha1_buf, func_name) != 0: call_name = func_name
else:
# Cross-module call: when found_func is None, check for a module-qualified call (string.strchr -> @{sha1_of_string}.strchr).
if cl.func is not None and cl.func.kind() == ast.ASTKind.Attribute:
xmod_at: ast.Attribute* = ast.Attribute*(cl.func)
if xmod_at.value is not None and (xmod_at.value.kind() == ast.ASTKind.Name or xmod_at.value.kind() == ast.ASTKind.Attribute):
# Module path: one-level Name (string.strchr) or nested Attribute (os._win32.exists). The nested
# case must be resolved too, else it suffix-matches this module's own define (infinite recursion).
xmod_path: str = None
if xmod_at.value.kind() == ast.ASTKind.Name:
xmod_nm: ast.Name* = ast.Name*(xmod_at.value)
if xmod_nm.id is not None: xmod_path = xmod_nm.id
else: xmod_path = self.Gen.ExprGenAttr._attr_module_path_str(pool, xmod_at.value)
if xmod_path is not None:
mod_sha1: str = self._lookup_module_sha1(xmod_path)
if mod_sha1 is not None:
# fast-fail: a module-qualified call requires the module to be imported by this file;
# the global SHA1 store contains all scanned includes, so an unimported module would silently resolve.
is_xmod_imported: int = 1
if trans is not None and trans._imported_modules is not None:
is_xmod_imported = self.Gen.ImportsGen.is_module_imported_or_known(trans._imported_modules, xmod_path)
if is_xmod_imported == 0:
err_ni: str = pool.alloc(256)
if err_ni is not None:
viperlib.snprintf(err_ni, 256, "Cross-module call '%s.%s': module '%s' not imported (import %s first)", xmod_path, func_name, xmod_path, xmod_path)
self.Gen.TypeGenErr.fatal_error(xmod_at, err_ni)
self.Gen.TypeGenErr.fatal_error(xmod_at, "Cross-module call: module not imported")
# Check whether it is a re-export function (e.g. ast._lexer_init actually defined in ast.lexer).
reexport_src: str = self.Gen.ImportsGen.lookup_reexport(mod_sha1, func_name)
if reexport_src is None:
# Re-export table miss (includes modules with existing stub/text are not fully translated,
# so the table may be empty): resolve the module's .vpi from-import to locate the defining module.
reexport_src = self._lookup_reexport_from_vpi(pool, mod_sha1, func_name)
rs_found: int = 0
if reexport_src is not None:
# Star-import multiple candidates (space-separated, e.g. fltk/__init__ from _types/_enums/_cfl
# import *): verify each via is_cexport_func/_is_state_func_from_vpi to find where func_name is really defined.
cs_total: sizet = string.strlen(reexport_src)
cs_i: sizet = 0
while cs_i < cs_total:
while cs_i < cs_total and reexport_src[cs_i] == ' ': cs_i += 1
if cs_i >= cs_total: break
cs_start: sizet = cs_i
while cs_i < cs_total and reexport_src[cs_i] != ' ': cs_i += 1
cs_seg: sizet = cs_i - cs_start
cs_buf: str = pool.alloc(cs_seg + 1)
if cs_buf is not None:
string.strncpy(cs_buf, reexport_src + cs_start, cs_seg)
cs_buf[cs_seg] = '\0'
cand_sha1: str = self._lookup_module_sha1(cs_buf)
if cand_sha1 is not None:
if self.Gen.CallGenBase.is_cexport_func(cand_sha1, func_name) != 0 or self._is_state_func_from_vpi(pool, cand_sha1, func_name) != 0:
mod_sha1 = cand_sha1
call_name = func_name
rs_found = 1
break
if rs_found == 0:
# All star-import candidates failed: func_name may be defined in the original module
# itself (e.g. stdlib.malloc, while stdlib's `from stdint import *` adds a false candidate).
if self.Gen.CallGenBase.is_cexport_func(mod_sha1, func_name) != 0 or self._is_state_func_from_vpi(pool, mod_sha1, func_name) != 0:
rs_found = 1
call_name = func_name
else:
# Original module has a plain def (not CExport/State): a vpi hit keeps the original
# module sha (@{mod_sha1}.func) instead of falling back to a star candidate that would hijack resolution.
orig_has_def: int = 0
if self.Gen.CallGenBase.TempDir is not None and self.Gen.CallGenBase.TempDirLen > 0:
VpiReader._ensure_vpi_ret_annot_loaded(mod_sha1, self.Gen.CallGenBase.TempDir)
if VpiReader._lookup_vpi_ret_annot(mod_sha1, None, func_name) is not None: orig_has_def = 1
if orig_has_def == 0:
# Original module does not define it: fall back to the first candidate (e.g. ast._lexer_init in ast.lexer).
reexport_sha1: str = self._lookup_module_sha1(reexport_src)
if reexport_sha1 is not None: mod_sha1 = reexport_sha1
# CExport functions define with the bare name (@strlen), so the cross-module call must use the bare name too.
if rs_found != 0:
pass # Re-export defining module already verified and bare name set.
elif self.Gen.CallGenBase.is_cexport_func(mod_sha1, func_name) != 0: call_name = func_name
elif self._is_state_func_from_vpi(pool, mod_sha1, func_name) != 0:
# t.State external C function (e.g. CPython PyList_Size): the definition uses the bare
# name @PyList_Size, so the cross-module call must too (a sha1 prefix causes an undefined symbol).
call_name = func_name
else:
mangled_len: sizet = string.strlen(mod_sha1) + string.strlen(func_name) + 2
mangled: str = pool.alloc(mangled_len)
if mangled is not None:
viperlib.snprintf(mangled, mangled_len, "%s.%s", mod_sha1, func_name)
call_name = mangled
else:
# t and c are internal libraries exempt from SHA1 lookup: use bare-name calls.
if string.strcmp(xmod_path, "t") == 0 or string.strcmp(xmod_path, "c") == 0: call_name = func_name
else:
# Check whether it is an imported module name (e.g. BuildPipeline.run_pipeline): if imported
# but its SHA1 is unregistered the module was not compiled, so error out instead of a misleading from_imports lookup.
is_imported_mod: int = 0
if trans is not None and trans._imported_modules is not None: is_imported_mod = self.Gen.ImportsGen.is_module_imported(trans._imported_modules, xmod_path)
if is_imported_mod != 0:
# Aliased import (import X as Y): Y is in _imported_modules but the exact SHA1 lookup fails
# (mod_arr holds file names); try from_imports (alias -> full name -> suffix SHA1) with allow_star_fallback=0.
alias_sha1: str = None
if trans is not None and trans._from_imports is not None: alias_sha1 = self._lookup_mod_sha1(pool, trans._from_imports, xmod_path, 0)
if alias_sha1 is not None:
if self.Gen.CallGenBase.is_cexport_func(alias_sha1, func_name) != 0: call_name = func_name
elif self._is_state_func_from_vpi(pool, alias_sha1, func_name) != 0:
# t.State external C function: use the bare name (same as the main path).
call_name = func_name
else:
AliasMangledLen: sizet = string.strlen(alias_sha1) + string.strlen(func_name) + 2
alias_mangled: str = pool.alloc(AliasMangledLen)
if alias_mangled is not None:
viperlib.snprintf(alias_mangled, AliasMangledLen, "%s.%s", alias_sha1, func_name)
call_name = alias_mangled
else:
# Imported module but SHA1 unregistered: the module was not compiled, so error out immediately.
err_msg_imp: str = pool.alloc(256)
if err_msg_imp is not None:
viperlib.snprintf(err_msg_imp, 256, "Module '%s' is imported but its SHA1 is not registered (the module was not compiled)", xmod_path)
self.Gen.TypeGenErr.fatal_error(xmod_at, err_msg_imp)
self.Gen.TypeGenErr.fatal_error(xmod_at, "Imported module SHA1 not registered")
else:
# Check whether it is a from-import name (e.g. from .__types import LLVMType): LLVMType is a
# class, not a module, but Ptr is a module-level function of __types; disable star-import fallback.
from_sha1: str = None
if trans is not None and trans._from_imports is not None: from_sha1 = self._lookup_mod_sha1(pool, trans._from_imports, xmod_path, 0)
if from_sha1 is not None:
# CExport functions define with the bare name, so the cross-module call must use the bare name.
if self.Gen.CallGenBase.is_cexport_func(from_sha1, func_name) != 0: call_name = func_name
else:
fmangled_len: sizet = string.strlen(from_sha1) + string.strlen(func_name) + 2
fmangled: str = pool.alloc(fmangled_len)
if fmangled is not None:
viperlib.snprintf(fmangled, fmangled_len, "%s.%s", from_sha1, func_name)
call_name = fmangled
else:
err_msg2: str = pool.alloc(256)
if err_msg2 is not None:
viperlib.snprintf(err_msg2, 256, "Cross-module call '%s.%s': module SHA1 not found", xmod_path, func_name)
self.Gen.TypeGenErr.fatal_error(xmod_at, err_msg2)
self.Gen.TypeGenErr.fatal_error(xmod_at, "Cross-module call: module SHA1 not found")
else:
# Bare-name call (not Attribute): check from_imports. Star-import fallback is disabled for calls
# (star import is for types/constants); a missed function then fails fast at compile time instead of linking wrong.
if trans is not None and trans._from_imports is not None:
bare_sha1: str = self._lookup_mod_sha1(pool, trans._from_imports, func_name, 0)
if bare_sha1 is not None:
# Alias handling: from X import Y as Z -> calling Z must mangle with Y (e.g. from json.__parser
# import parse as json_parse -> SHA1.parse, not SHA1.json_parse).
bare_orig: str = self.Gen.ImportsGen.lookup_from_import_original(trans._from_imports, func_name)
bare_func_name: str = func_name
if bare_orig is not None:
# bare_orig points into the from_imports buffer and is not NUL-terminated, so copy it
# into a separate buffer (truncating at space or NUL).
bo_len: sizet = 0
while bare_orig[bo_len] != ' ' and bare_orig[bo_len] != '\0': bo_len += 1
bo_buf: str = pool.alloc(bo_len + 1)
if bo_buf is not None:
for bo_i in range(bo_len): bo_buf[bo_i] = bare_orig[bo_i]
bo_buf[bo_len] = '\0'
bare_func_name = bo_buf
# Record the original function name for later return-type inference (from json.__parser
# import parse as json_parse -> "parse" matches while "json_parse" would default to i32).
orig_func_name = bare_func_name
# Re-export resolution: a from-imported bare function may actually be defined in a submodule
# (e.g. dump defined in ast.base, ast/__init__ only re-exports), so redirect to the defining module SHA1.
reexport_src2: str = self.Gen.ImportsGen.lookup_reexport(bare_sha1, bare_func_name)
if reexport_src2 is None:
# Re-export table miss: resolve the module's .vpi from-import on demand.
reexport_src2 = self._lookup_reexport_from_vpi(pool, bare_sha1, bare_func_name)
if reexport_src2 is not None:
reexport_sha1_2: str = self._lookup_module_sha1(reexport_src2)
if reexport_sha1_2 is not None: bare_sha1 = reexport_sha1_2
if self.Gen.CallGenBase.is_cexport_func(bare_sha1, bare_func_name) != 0: call_name = bare_func_name
else:
bare_mangled_len: sizet = string.strlen(bare_sha1) + string.strlen(bare_func_name) + 2
bare_mangled: str = pool.alloc(bare_mangled_len)
if bare_mangled is not None:
viperlib.snprintf(bare_mangled, bare_mangled_len, "%s.%s", bare_sha1, bare_func_name)
call_name = bare_mangled
else:
err_msg3: str = pool.alloc(256)
if err_msg3 is not None:
viperlib.snprintf(err_msg3, 256, "Bare-name call '%s' is undefined and not imported", func_name)
self.Gen.TypeGenErr.fatal_error(node, err_msg3)
self.Gen.TypeGenErr.fatal_error(node, "Bare-name call undefined and not imported")
else:
err_msg4: str = pool.alloc(256)
if err_msg4 is not None:
viperlib.snprintf(err_msg4, 256, "Bare-name call '%s' is undefined and not imported", func_name)
self.Gen.TypeGenErr.fatal_error(node, err_msg4)
self.Gen.TypeGenErr.fatal_error(node, "Bare-name call undefined and not imported")
# Direct call. memcpy -> @llvm.memcpy intrinsic (avoids a missing cross-module @memcpy declaration during monomorphization).
if string.strcmp(func_name, "memcpy") == 0 and can >= 3: return self.Gen.CallGenLib._emit_llvm_memcpy_intrinsic(pool, builder, mod, mc_dst2, mc_src2, mc_num2)
# Re-infer the return type for aliased imports: with func_name="json_parse" the whitelist returns i32,
# so use orig_func_name="parse" to match i8* (JsonValue*) and avoid pointer truncation crashes.
if orig_func_name is not None and orig_func_name != func_name:
# Alias attempt only: a temporary inference before the mangled exact resolution (fatal_if_unknown=0);
# a mangled call_name is later overridden by the text.ll/vpi exact resolution.
re_inferred_ty: mlirlite.LLVMType* = self.Gen.CallGenLib._infer_external_func_ret_ty(pool, orig_func_name, node, 0)
if re_inferred_ty is not None: call_ret_ty = re_inferred_ty
# Exact cross-module Viper return-type resolution (fail-fast): the whitelist does not cover pointer-
# returning functions and the default i32 truncates them; the authority is text.ll define, then the vpi annotation.
if self.Gen.CallGenLib._is_sha1_mangled_call(call_name) != 0:
if trans is None or trans._declare_only == 0:
cf_dot: str = string.strchr(call_name, '.')
cf_last: str = string.strrchr(call_name, '.')
if cf_dot is not None and cf_last is not None:
cf_sha1: str = stdlib.malloc(17)
cf_bare_len: sizet = string.strlen(call_name) - (sizet(uint64(cf_last) - uint64(call_name))) - 1
cf_bare: str = stdlib.malloc(cf_bare_len + 1)
if cf_sha1 is not None and cf_bare is not None:
string.strncpy(cf_sha1, call_name, 16)
cf_sha1[16] = '\0'
string.strcpy(cf_bare, cf_last + 1)
# Same-module function (e.g. generic specialization): take the return type from the current
# IR; do not cross-module lookup text.ll (its own is not generated yet in Phase A and would mis-warn).
is_local_mod: int = 0
if trans is not None and trans.ModuleSha1 is not None:
if string.strncmp(cf_sha1, trans.ModuleSha1, 16) == 0: is_local_mod = 1
if is_local_mod != 0:
local_fn: mlirlite.Function* = self.find_func_in_module(mod, call_name)
if local_fn is not None and local_fn.RetTy is not None: call_ret_ty = local_fn.RetTy
else:
cf_vpi: mlirlite.LLVMType* = self._lookup_method_ret_ty_via_vpi(pool, cf_sha1, None, cf_bare)
if cf_vpi is not None: call_ret_ty = cf_vpi
else:
cf_ret: mlirlite.LLVMType* = self._LookupMethodRetTyFromTextLL(pool, cf_sha1, None, cf_bare, 1)
if cf_ret is not None: call_ret_ty = cf_ret
else:
# Dependency text.ll does not exist (module not yet translated): Phase A translates files
# in order (main.vp before its imported mbuddytest.vp), so keep the default type without fail-fast.
if self._dep_text_ll_exists(pool, cf_sha1) != 0: self.Gen.TypeGenErr.fatal_error(node, f"Cannot obtain the cross-module function return type precisely (neither vpi nor text.ll hit); the default i32 will truncate pointers: {call_name}", "TypeError")
stdlib.free(cf_sha1)
stdlib.free(cf_bare)
# Bare C library function (string.memset -> @memset): promote args to the standard signature
# (third arg i32 literal -> i64) before creating the declare, so call/declare/define widths match.
head = self.Gen.CallGenLib._promote_clib_call_args(pool, builder, mod, call_name, head, can)
# Cross-module module-level call ({sha1}.{func}): coerce args to the vpi definition-side param types
# (exact integer widths), so call args / declare / define all agree (B3 generalization).
head = self.Gen.CallGenLib._coerce_args_to_def_params(pool, builder, call_name, head, can, cargs, 0, trans)
# Cross-module call: create the declare to avoid llc's undefined value error.
self._ensure_method_declare(pool, mod, call_name, call_ret_ty, head, can)
# Check whether the callee is variadic (*args) and, if so, promote the variadic args to the C ABI
# (cargs is the arg AST list; self_offset=0 for a plain function call).
is_variadic_call: int = self.Gen.CallGenVa._check_and_promote_variadic_call(pool, builder, mod, call_name, head, can, cargs, sizet(0), trans)
# try-except: when the callee takes EH out-params, append the output args and check for exceptions after the call.
return self.Gen.CallGenLeafKit._emit_eh_guarded_call(builder, pool, mod, trans, found_func, call_name, head, can, call_ret_ty, is_variadic_call)