refactor(codegen): one body pipeline for module functions and class methods

Module-level functions and class methods each carried their own copy of "resolve the
return type", "create the parameter allocas" and "emit the implicit epilogue". The copies
drifted: an unannotated method was still typed i32 by the class-method body path while its
declaration said void, so the epilogue emitted `llvm.return 0 : i32` inside a void function
and the MLIR verifier rejected it ("'llvm.return' op expected no operands", Scheduler._yield).

- FuncGenBase.resolve_func_ret_ty: the single return-type policy (annotation -> storage
  marker -> __init__/__new__ hooks -> unannotated = procedure void -> fail-fast). Used by
  the forward-declare pass, the body pass, class methods and generic specializations
- FuncGenBase.param_llvm_ty: the single parameter mapping (CVoid skipped, bare class ->
  Ptr(struct), ArrayT -> element pointer), used by every signature and alloca site
- FuncGenBase.emit_func_epilogue: the single implicit-return implementation (procedure ->
  ret void, unreachable merge, fail-fast for an annotated fall-through), shared by module
  functions, methods and nested functions
- FuncGenBase.translate_func_body: the shared body pipeline (scope entry, self/parameter
  storage, *args va_list, try/except out-params, pre-scan, capture preparation, slife,
  statements, epilogue, state restore). ClassGenVTable._translate_method now only handles
  method naming, vtable/overload registration and add_param
- the same policy reaches cross-module and generic declares: unannotated vpi functions and
  methods declare void, generic specialization declares use the shared resolver, and the
  list.append call sites emit a void call
- method parameters now carry the const / volatile / pointee-volatile / pure markers that
  the module-level path always had
- FuncGenMixin: the two dry-run helpers that return ints are annotated, as the language
  now requires (an unannotated def is a procedure)

Verified: 41 Test projects pass (MethodProcTest, CrossModuleSigTest, RawPtrParamTest,
AstTest, ListTest, OopInheritTest, ...), ViperC self-hosts with the new compiler, and the
6 tests that still fail reproduce identically with the previous compiler.
This commit is contained in:
TermiNexus committed 2026-10-29 19:37:55 +08:00
1 parent 3b3cd42d1c
commit eda8e4465f
8 files changed
+547 -683

No files matched your search

+7 -3
View File
@@ -4062,8 +4062,10 @@ def _inject_function_declare(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*,
# no annotation (fd.returns is None) is a legitimate default and keeps i32 below.
if ret_ty is None and fd.returns is not None:
GenApi.fatal_error(fd.returns, f"extern declaration '{fd.name}' return type annotation failed to resolve", "NameError")
# default return i32
if ret_ty is None: ret_ty = i32_ty
# No annotation and no cached return text: a procedure (void), matching the definition side.
# The vpi records "void" for unannotated functions, so this is the fallback when the entry is
# missing; an i32 default here would split "declare i32" from "define void".
if ret_ty is None: ret_ty = mlirlite.Void(pool)
# A value struct return type (e.g. Mat4) needs a full definition: declaring %Mat4 opaque makes
# llc report "use of undefined type". Register the full definition.
@@ -4202,7 +4204,9 @@ def _inject_method_declare(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, m_
# Unannotated __init__ -> Void, matching the definition side (ClassGenVTable always uses Void);
# defaulting to i32 would split declare i32 / define void.
if ret_ty is None and string.strcmp(m_fd.name, "__init__") == 0: ret_ty = mlirlite.Void(pool)
if ret_ty is None: ret_ty = i32_ty
# Unannotated method -> Void, matching the definition side (an unannotated method is a
# procedure). The vpi records "void" for those; i32 here would split declare i32 / define void.
if ret_ty is None: ret_ty = mlirlite.Void(pool)
# 跨模块方法**按值返回类**时,引用模块必须登记该具名结构体的完整定义:否则打印方法签名回查登记表,
# 命中的只是指针引用登记的 opaque 前向声明 → MLIR 报 "still opaque at print time"。与
+45 -4
View File
@@ -318,9 +318,15 @@ novirtual class CallGenCall(CallGenBase.CallGenBase):
if ret_cm is None and cls_sha1 is not None: ret_cm = self._LookupMethodRetTyFromTextLL(pool, cls_sha1, class_name, method_name)
if ret_cm is not None: call_ret_ty = ret_cm
else:
# Phase A: the dependency's text.ll is not generated yet, so keep
# the default type and continue (dependency not ready).
if cls_sha1 is None or (cls_sha1 is not None and self._dep_text_ll_exists(pool, cls_sha1) == 0):
# Phase A: the dependency's text.ll is not generated yet. A generic class
# instance (Class[T]) is emitted in *this* module only after all bodies, so
# the call site cannot read its signature either. Predict the definition's
# type from the template method: no return annotation means a procedure
# (void), matching the unified body pipeline.
pred_void: int = self._predicted_generic_method_is_void(pool, class_name, method_name)
if pred_void != 0:
call_ret_ty = mlirlite.Void(pool)
elif cls_sha1 is None or (cls_sha1 is not None and self._dep_text_ll_exists(pool, cls_sha1) == 0):
# Dependency not ready: keep default i32 (build_call(Void)
# returns None and breaks expression translation).
call_ret_ty = mlirlite.Int32(pool)
@@ -589,6 +595,38 @@ novirtual class CallGenCall(CallGenBase.CallGenBase):
if uint64(self_ptr.Ty) != uint64(cmop_i8_ptr_ty): cmop_bar_ptr = mlirlite.build_bitcast(builder, self_ptr, cmop_i8_ptr_ty)
if cmop_bar_ptr is not None: mlirlite.build_load(builder, cmop_i8_ty, cmop_bar_ptr, 1)
return call_res_cmop
# _predicted_generic_method_is_void - whether a call to a generic class instance's method will
# land on a procedure. A generic instance (Class[T]) is emitted in this module only after all
# bodies, so the call site cannot read its signature; the template method is the only source.
# No return annotation = procedure = void, matching the unified body pipeline.
def _predicted_generic_method_is_void(self, pool: memhub.MemBuddy*, class_name: str, method_name: str) -> int:
"""1 when the template method (of a Class[T] instance) has no return annotation."""
if class_name is None or method_name is None: return 0
br: str = string.strchr(class_name, '[')
if br is None: return 0
bare_len: sizet = sizet(uint64(br) - uint64(class_name))
if bare_len == 0: return 0
bare: str = pool.alloc(bare_len + 1)
if bare is None: return 0
string.strncpy(bare, class_name, bare_len)
bare[bare_len] = '\0'
tpl: ast.ClassDef* = self.Gen.ClassGenGeneric._find_generic_template(bare)
if tpl is None: return 0
ch: list[ast.AST*]* = tpl.children
if ch is None: return 0
n: sizet = len(ch)
for i in range(int(n)):
st: ast.AST* = ch.get(i)
if st is None: continue
if st.kind() != ast.ASTKind.FunctionDef: continue
mfd: ast.FunctionDef* = ast.FunctionDef*(st)
if mfd is None or mfd.name is None: continue
if string.strcmp(mfd.name, method_name) != 0: continue
if mfd.returns is None: return 1
return 0
return 0
def _MarkWillFreeArgName(self, trans: HT.Translator*, arg_node: ast.AST*) -> int:
"""P2 willFree:把实参变量槽标为"已失效";只处理简单变量名(1=标记过 / 0=未处理)。
@@ -1571,7 +1609,10 @@ novirtual class CallGenCall(CallGenBase.CallGenBase):
mlirlite.value_set_next(lm_obj, None)
mlirlite.value_set_next(ap_item_val, None)
mlirlite.value_set_next(lm_obj, ap_item_val)
ap_ret_ty: mlirlite.LLVMType* = mlirlite.Int32(pool)
# `_list.append(self, item)` has no return annotation: it is a
# procedure, so the call must not produce a value (the definition
# side emits `ret void`).
ap_ret_ty: mlirlite.LLVMType* = mlirlite.Void(pool)
self._ensure_method_declare(pool, mod, ap_mangled, ap_ret_ty, lm_obj, 2)
return mlirlite.build_call(builder, ap_mangled, lm_obj, 2, ap_ret_ty, 0)
+4 -1
View File
@@ -1440,7 +1440,10 @@ novirtual class CallGenDispatch(CallGenBase.CallGenBase):
mlirlite.value_set_next(lm_obj, None)
mlirlite.value_set_next(ap_item_val, None)
mlirlite.value_set_next(lm_obj, ap_item_val)
ap_ret_ty: mlirlite.LLVMType* = mlirlite.Int32(pool)
# `_list.append(self, item)` has no return annotation: it
# is a procedure, so the call must not produce a value
# (the definition side emits `ret void`).
ap_ret_ty: mlirlite.LLVMType* = mlirlite.Void(pool)
self._ensure_method_declare(pool, mod, ap_mangled, ap_ret_ty, lm_obj, 2)
return mlirlite.build_call(builder, ap_mangled, lm_obj, 2, ap_ret_ty, 0)
+8 -17
View File
@@ -374,12 +374,11 @@ novirtual class ClassGenGeneric(ClassGenBase.ClassGenBase):
saved_cur: str = self.Gen.TypeGenResolve.get_current_file()
tpl_file: str = self._find_generic_func_template_file(template_key)
if tpl_file is not None: self.Gen.TypeGenResolve.set_current_file(tpl_file)
ret_ty: mlirlite.LLVMType* = None
if spec_fd.returns is not None:
# Value-reference markers are parameter-only: `-> &T` fails fast on a generic too.
self.Gen.TypeGenResolve.reject_ref_marker_position(spec_fd.returns, trans._imported_modules, "a function return type")
ret_ty = self.Gen.TypeGenResolve.resolve_annotation_type(pool, spec_fd.returns, trans._imported_modules, trans._from_imports, trans, 0)
if ret_ty is None: ret_ty = i32_ty
# Return type: one shared policy (unannotated = procedure void), so a specialization's
# declare agrees with the body the unified pipeline emits.
gg_ctx: str = f"generic function '{template_fd.name}'"
ret_ty: mlirlite.LLVMType* = self.Gen.FuncGenBase.resolve_func_ret_ty(trans, spec_fd, gg_ctx, 0, None, None, 0)
if ret_ty is None: return None
func: mlirlite.Function* = mlirlite.create_declare(pool, mod, spec_name, ret_ty)
if func is not None and spec_fd.args is not None and spec_fd.args.kind() == ast.ASTKind.Arguments:
ags: ast.Arguments* = ast.Arguments*(spec_fd.args)
@@ -392,17 +391,9 @@ novirtual class ClassGenGeneric(ClassGenBase.ClassGenBase):
if pa is None or pa.kind() != ast.ASTKind.Arg: continue
arg: ast.Arg* = ast.Arg*(pa)
if arg.arg is None: continue
param_ty: mlirlite.LLVMType* = i32_ty
if arg.annotation is not None:
resolved: mlirlite.LLVMType* = self.Gen.TypeGenResolve.resolve_annotation_type(pool, arg.annotation, trans._imported_modules, trans._from_imports, trans, 0)
if resolved is not None:
match resolved:
case mlirlite.LLVMType.Struct(_, _, _):
param_ty = mlirlite.Ptr(pool, resolved)
case mlirlite.LLVMType.Array(aelem_ty, _):
param_ty = mlirlite.Ptr(pool, aelem_ty)
case _:
param_ty = resolved
# One shared parameter mapping (CVoid skipped, class -> Ptr, array -> element pointer).
param_ty: mlirlite.LLVMType* = self.Gen.FuncGenBase.param_llvm_ty(trans, arg, gg_ctx, 0)
if param_ty is None: continue
pname: str = pool.alloc(32)
if pname is not None: viperlib.snprintf(pname, 32, "%%%s", arg.arg)
mlirlite.add_param(pool, func, param_ty, pname)
+40 -235
View File
@@ -619,29 +619,13 @@ novirtual class ClassGenVTable(ClassGenBase.ClassGenBase):
# SHA1 namespace mangling.
mangled_name: str = self.Gen.FuncGenBase._mangle_name(trans, full_name)
# Infer the return type.
ret_ty: mlirlite.LLVMType* = None
if fd.returns is not None:
# Value-reference markers are parameter-only: `-> &T` fails fast (never a silent Ptr(T) return).
self.Gen.TypeGenResolve.reject_ref_marker_position(fd.returns, imported_modules, "a method return type")
ret_ty = self.Gen.TypeGenResolve.resolve_annotation_type(pool, fd.returns, imported_modules, from_imports, trans, 0)
if ret_ty is None and fd.returns is not None:
# t.State / t.ExportT / t.ExternT markers mean Void: such annotations carry no real return
# type (written as "<original> | t.<marker>"); only the marker itself reaches here.
if self.Gen.TypeGenErr.has_decorator_marker(fd.returns, "State", 0) != 0 or self.Gen.TypeGenErr.has_decorator_marker(fd.returns, "ExportT", 0) != 0 or self.Gen.TypeGenErr.has_decorator_marker(fd.returns, "ExternT", 0) != 0:
ret_ty = mlirlite.Void(pool)
else:
# [S4 fail-fast] return annotation failed to resolve: do not fall back to infer_return_type (silent mismatch).
self.Gen.TypeGenErr.fatal_error(fd, f"method '{class_name}.{method_name}' return type annotation failed to resolve", "NameError")
return 0
if ret_ty is None:
# __init__ and __before_init__ return void.
if string.strcmp(method_name, "__init__") == 0: ret_ty = mlirlite.Void(pool)
# __new__ returns Ptr(struct_ty) (the struct pointer serves as storage).
elif string.strcmp(method_name, "__new__") == 0: ret_ty = mlirlite.Ptr(pool, struct_ty)
else:
param_types_str: str = self.Gen.TypeGenErr.build_param_types_str(pool, fd.args)
ret_ty = self.Gen.TypeGenErr.infer_return_type(pool, fd.children, param_types_str)
# Return type: one shared policy (annotation -> marker -> unannotated = procedure void).
# The same call types the forward declare, so a declare and its body cannot disagree.
m_ctx: str = f"method '{class_name}.{method_name}'"
m_check: int = 1
if sig_only != 0: m_check = 0
ret_ty: mlirlite.LLVMType* = self.Gen.FuncGenBase.resolve_func_ret_ty(trans, fd, m_ctx, 1, method_name, struct_ty, m_check)
if ret_ty is None: return 0
# Create/reuse the LLVM function. When the sig_only pre-scan already created a same-name
# declaration, skip param/table setup and just clear IsDeclared to define (mirrors FuncGenDef).
@@ -712,31 +696,13 @@ novirtual class ClassGenVTable(ClassGenBase.ClassGenBase):
if is_static_m == 0: start_idx = 1
for ai in range(int(start_idx), int(an)):
arg: ast.Arg* = ast.Arg*(alist.get(ai))
if arg is not None and arg.arg is not None:
param_ty: mlirlite.LLVMType* = i32_ty
if arg.annotation is not None:
resolved: mlirlite.LLVMType* = self.Gen.TypeGenResolve.resolve_annotation_type(pool, arg.annotation, imported_modules, from_imports, trans, 0)
if resolved is None:
# [S4 fail-fast] annotation present but failed to resolve: no silent i32 (signature/call/impl mismatch).
self.Gen.TypeGenErr.fatal_error(arg, f"method '{class_name}.{method_name}' param '{arg.arg}' type annotation failed to resolve", "NameError")
return 0
# Bare-class param -> Ptr(struct) (objects are pointers; by-value would dangle after append).
# ArrayT param -> element pointer ty* (C array decay, same as function params).
match resolved:
case mlirlite.LLVMType.Void():
# void is not a legal param type (only void* and typedef opaque structs); void*
# is already merged into i8* upstream, so fail fast at compile time instead of emitting invalid IR.
self.Gen.TypeGenErr.fatal_error(arg, f"method '{class_name}.{method_name}' param '{arg.arg}' cannot be void type (void is only allowed for void* and typedef aliases)", "TypeError")
return 0
case mlirlite.LLVMType.Struct(_, _, _):
param_ty = mlirlite.Ptr(pool, resolved)
case mlirlite.LLVMType.Array(aelem_ty, _):
param_ty = mlirlite.Ptr(pool, aelem_ty)
case _:
param_ty = resolved
pname: str = pool.alloc(32)
if pname is not None: viperlib.snprintf(pname, 32, "%%%s", arg.arg)
mlirlite.add_param(pool, func, param_ty, pname)
if arg is None or arg.arg is None: continue
# One shared type mapping: bare class -> Ptr(struct), ArrayT -> element pointer.
param_ty: mlirlite.LLVMType* = self.Gen.FuncGenBase.param_llvm_ty(trans, arg, m_ctx, 0)
if param_ty is None: continue
pname: str = pool.alloc(32)
if pname is not None: viperlib.snprintf(pname, 32, "%%%s", arg.arg)
mlirlite.add_param(pool, func, param_ty, pname)
# Register value-reference (&T / &temp T) parameter modes (index 0 = self for non-static methods).
mode_buf: str = pool.alloc(an + 1)
if mode_buf is not None:
@@ -759,6 +725,12 @@ novirtual class ClassGenVTable(ClassGenBase.ClassGenBase):
mode_buf[mode_cnt] = char(0)
if has_ref_m != 0: self.Gen.CallGenBase._register_auto_addr_modes(mangled_name, mode_buf, mode_cnt)
# *args variadic: set IsVarArg=1 so the signature carries the ... suffix (the body
# pipeline allocates the va_list buffer, same as a module-level function).
if args_node is not None:
m_ags_v: ast.Arguments* = ast.Arguments*(args_node)
if m_ags_v.vararg is not None: func.IsVarArg = 1
# OOP iter protocol: __next__ gets a hidden __stop_iter_flag__ (i1*), set to 1 by raise
# StopIteration. It is not counted in md_param_count; the for loop passes it explicitly.
if string.strcmp(method_name, "__next__") == 0:
@@ -834,201 +806,34 @@ novirtual class ClassGenVTable(ClassGenBase.ClassGenBase):
tlmo: mlirlite.Value* = mlirlite.ConstInt(pool, i1_ty_m, 1, "1")
if tlmo is not None: mlirlite.build_store(func_builder, tlmo, tls_m_ref)
# Enter the function scope.
self.Gen.VarGen.enter_scope(trans.SymTab, SCOPE_FUNCTION)
# Receiver name for the body: a @classmethod's first parameter may be named `cls`...
self_name_m: str = "self"
if is_static_m == 0 and is_class_m != 0 and args_node is not None:
cags_m: ast.Arguments* = ast.Arguments*(args_node)
if cags_m.args is not None and len(cags_m.args) > 0:
cfirst_m: ast.Arg* = ast.Arg*(cags_m.args.get(0))
if cfirst_m is not None and cfirst_m.arg is not None: self_name_m = cfirst_m.arg
skip_self_m: int = 0
if is_static_m == 0: skip_self_m = 1
# Create and store alloca for self/cls (like other params); skip for @staticmethod. self must use
# alloca: an SSA value would load once more, turning Ptr(struct_ty) into a struct value (ABI mismatch).
if is_static_m == 0:
self_alloca: mlirlite.Value* = mlirlite.build_alloca(func_builder, self_ptr_ty)
if self_alloca is not None:
self_var_name: str = "self"
self_ssa_name: str = "%self"
if is_class_m != 0 and args_node is not None:
cags: ast.Arguments* = ast.Arguments*(args_node)
if cags.args is not None and len(cags.args) > 0:
cfirst: ast.Arg* = ast.Arg*(cags.args.get(0))
if cfirst is not None and cfirst.arg is not None:
self_var_name = cfirst.arg
self_ssa_name_buf: str = pool.alloc(32)
if self_ssa_name_buf is not None:
viperlib.snprintf(self_ssa_name_buf, 32, "%%%s", cfirst.arg)
self_ssa_name = self_ssa_name_buf
self.Gen.VarGen.define_var(trans.SymTab, self_var_name, self_alloca)
# Set the self/cls annotation class name (fallback for attribute lookup_field).
self.Gen.VarGen.set_var_annot_class_name(trans.SymTab, self_var_name, class_name)
self_val: mlirlite.Value* = mlirlite.SSAValue(pool, self_ptr_ty, self_ssa_name)
mlirlite.build_store(func_builder, self_val, self_alloca)
# Create and store alloca for other params (like plain functions; @staticmethod starts at 0).
if args_node is not None:
ags2: ast.Arguments* = ast.Arguments*(args_node)
if ags2.args is not None:
alist2: list[ast.AST*]* = ags2.args
an2: sizet = len(alist2)
start_idx2: int = 0
if is_static_m == 0: start_idx2 = 1
for ai2 in range(int(start_idx2), int(an2)):
arg2: ast.Arg* = ast.Arg*(alist2.get(ai2))
if arg2 is not None and arg2.arg is not None:
param_ty2: mlirlite.LLVMType* = i32_ty
if arg2.annotation is not None:
resolved2: mlirlite.LLVMType* = self.Gen.TypeGenResolve.resolve_annotation_type(pool, arg2.annotation, imported_modules, from_imports, trans, 0)
if resolved2 is None:
# fail-fast: param has an annotation but type resolution failed; no silent i32.
self.Gen.TypeGenErr.fatal_error(arg2, f"method '{class_name}.{method_name}' argument '{arg2.arg}' failed to resolve type", "NameError")
resolved2 = i32_ty # Unreachable (fatal_error already exits).
if resolved2 is not None:
# Keep in sync with add_param: bare class -> Ptr(struct) (objects are pointers); ArrayT -> element pointer ty*.
match resolved2:
case mlirlite.LLVMType.Void():
# void is not a legal param type (same rule as above).
self.Gen.TypeGenErr.fatal_error(arg2, f"method '{class_name}.{method_name}' param '{arg2.arg}' cannot be void type (void is only allowed for void* and typedef aliases)", "TypeError")
return 0
case mlirlite.LLVMType.Struct(_, _, _):
param_ty2 = mlirlite.Ptr(pool, resolved2)
case mlirlite.LLVMType.Array(aelem_ty2, _):
param_ty2 = mlirlite.Ptr(pool, aelem_ty2)
case _:
param_ty2 = resolved2
alloca: mlirlite.Value* = mlirlite.build_alloca(func_builder, param_ty2)
if alloca is not None:
self.Gen.VarGen.define_var(trans.SymTab, arg2.arg, alloca)
# Store the raw annotation class name (call detection falls back from Ptr(i8) to a class-name lookup).
if arg2.annotation is not None:
cls_nm_oc: str = self.Gen.TypeGenResolve.extract_class_name_from_annotation(arg2.annotation, imported_modules, pool, trans._from_imports)
if cls_nm_oc is not None: self.Gen.VarGen.set_var_annot_class_name(trans.SymTab, arg2.arg, cls_nm_oc)
pname2: str = pool.alloc(32)
if pname2 is not None: viperlib.snprintf(pname2, 32, "%%%s", arg2.arg)
param_val: mlirlite.Value* = mlirlite.SSAValue(pool, param_ty2, pname2)
mlirlite.build_store(func_builder, param_val, alloca)
# OOP iter protocol: initialize __next__'s stop-flag (store 0) and save the pointer to trans
# so raise StopIteration writes 1; the name must match add_param's "%__stop_iter_flag__".
# OOP iter protocol: initialize __next__'s stop flag (store 0) and hand the pointer to
# the body pipeline; raise StopIteration writes 1. Not counted in md_param_count.
stop_flag_m: mlirlite.Value* = None
if string.strcmp(method_name, "__next__") == 0:
i1_ty_sf: mlirlite.LLVMType* = mlirlite.Int1(pool)
if i1_ty_sf is not None:
i1_ptr_ty_sf: mlirlite.LLVMType* = mlirlite.Ptr(pool, i1_ty_sf)
if i1_ptr_ty_sf is not None:
flag_ref_sf: mlirlite.Value* = mlirlite.SSAValue(pool, i1_ptr_ty_sf, "%__stop_iter_flag__")
if flag_ref_sf is not None:
trans._stop_iter_flag_ptr = flag_ref_sf
stop_flag_m = mlirlite.SSAValue(pool, i1_ptr_ty_sf, "%__stop_iter_flag__")
if stop_flag_m is not None:
zero_i1_sf: mlirlite.Value* = mlirlite.ConstInt(pool, i1_ty_sf, 0, "0")
if zero_i1_sf is not None: mlirlite.build_store(func_builder, zero_i1_sf, flag_ref_sf)
if zero_i1_sf is not None: mlirlite.build_store(func_builder, zero_i1_sf, stop_flag_m)
# Save the module-level scope state.
old_func: mlirlite.Function* = trans._cur_func
old_builder: mlirlite.IRBuilder* = trans._cur_builder
old_global_count: int = trans._global_name_count
old_nonlocal_count: int = trans._nonlocal_name_count
old_env_count: int = trans._closure_env_count
old_stop_iter_flag: mlirlite.Value* = trans._stop_iter_flag_ptr
trans._cur_func = func
trans._cur_builder = func_builder
GenApi.clear_scope_names(trans)
# P2 willFree 失效表:方法体也走独立路径 → 同样做函数作用域复位(栈槽地址会被后续函数复用)
self.Gen.VarGen.freed_clear()
# M3 owned slife:方法体也走**独立**的转译路径(与 FuncGenMixin 的嵌套函数同款),
# 必须自己抬升/弹回登记区间,否则方法内登记会落进外层的区间 → 内层出口 free 外层对象,
# 外层出口再 free 一次 = double free(堆损坏)。
old_slife_base: int = self.Gen.VarGen._SlifeReset(pool)
# Pre-scan the body to create local allocas; pre-scan Global/Nonlocal first, otherwise a
# later-declared global is created as a local alloca and never written (reads all zeros).
body: list[ast.AST*]* = fd.children
if body is not None:
bn: sizet = len(body)
for bi in range(int(bn)):
stmt: ast.AST* = body.get(bi)
if stmt is not None: self.Gen.StmtGenDecl.pre_scan_scope_decls(trans, stmt)
for bi in range(int(bn)):
stmt: ast.AST* = body.get(bi)
if stmt is not None: self.Gen.StmtGenDecl.pre_scan_allocas(trans, stmt)
# Translate the method body.
if body is not None:
bn2: sizet = len(body)
for bi2 in range(int(bn2)):
stmt2: ast.AST* = body.get(bi2)
if stmt2 is not None: self.Gen.StmtGen.translate_stmt(trans, stmt2)
# Epilogue mirrors module-level functions: void/no-annotation falls through (ret void / ConstZero);
# control-flow last stmt with unreachable merge -> unreachable; annotated non-void fall-through -> fail-fast.
last_is_return: int = 0
# Init bn3 early so it is not undefined when body is None.
bn3: sizet = 0
if body is not None:
bn3 = len(body)
if bn3 > 0:
last_stmt_m: ast.AST* = body.get(bn3 - 1)
if last_stmt_m is not None and last_stmt_m.kind() == ast.ASTKind.Return: last_is_return = 1
if last_is_return == 0:
if mlirlite.builder_cur_block_is_terminated(func_builder) == 0:
last_is_ctrl: int = 0
if bn3 > 0:
last_stmt3_m: ast.AST* = body.get(bn3 - 1)
if last_stmt3_m is not None:
k3m: int = last_stmt3_m.kind()
if k3m == ast.ASTKind.If or k3m == ast.ASTKind.While or k3m == ast.ASTKind.For or k3m == ast.ASTKind.Try or k3m == ast.ASTKind.With or k3m == ast.ASTKind.Match:
last_is_ctrl = 1
_is_void: int = 0
if ret_ty is not None:
match ret_ty:
case mlirlite.LLVMType.Void():
_is_void = 1
case _:
pass
# Void detection at the annotation level (same as the module-level _is_void double-check):
# t.CVoiX / bare CVoid markers and -> None all count as void; the TypePrint fallback is not reliable.
if _is_void == 0 and fd.returns is not None:
if self.Gen.TypeGenResolve._is_cvoid_marker(fd.returns, imported_modules) != 0: _is_void = 1
elif fd.returns.kind() == ast.ASTKind.Constant:
ret_none_cn_m: ast.Constant* = (ast.Constant*)(fd.returns)
if ret_none_cn_m.const_kind == ast.CONST_NONE: _is_void = 1
if _is_void != 0:
# M3:隐式收尾也是函数出口,先释放本方法登记的 slife
self.Gen.VarGen._SlifeEmitFrees(pool, func_builder, mod, trans)
mlirlite.build_ret_void(func_builder)
elif last_is_ctrl != 0 and trans._ctrl_merge_reachable == 0:
# Non-void with a control-flow last statement and a structurally unreachable merge ->
# unreachable; if the merge is reachable the normal epilogue must run (unreachable on a reachable block is UB).
mlirlite.build_unreachable(func_builder)
elif fd.returns is None:
# No return annotation -> treat as a void procedure, return a zero value.
if ret_ty is not None:
_m_zero_val: mlirlite.Value* = mlirlite.ConstZero(pool, ret_ty)
if _m_zero_val is not None:
# M3:隐式收尾出口,先释放 slife 再返回
self.Gen.VarGen._SlifeEmitFrees(pool, func_builder, mod, trans)
mlirlite.build_ret(func_builder, _m_zero_val)
else: self.Gen.TypeGenErr.fatal_error(fd, "method flow falls off the end without an explicit return", "TypeError")
else: self.Gen.TypeGenErr.fatal_error(fd, "method flow falls off the end without an explicit return", "TypeError")
else:
# fail-fast: has a -> annotation but control truly falls off the end (traceback-style).
if ret_ty is not None:
rt_name_m: str = pool.alloc_tagged(4096, memhub.DETSITE_CLASSGENVTABLE_819)
if rt_name_m is not None:
mlirlite.TypePrint(rt_name_m, 4096, ret_ty, pool)
if rt_name_m[0] == '\0': rt_name_m = "?"
err_fn_m: str = pool.alloc(256)
if err_fn_m is not None:
viperlib.snprintf(err_fn_m, 256, "method '%s' flow falls off the end without an explicit return (return type %s), please add an explicit return (e.g. return 0 / return null)", method_name, rt_name_m)
self.Gen.TypeGenErr.fatal_error(fd, err_fn_m, "TypeError")
self.Gen.TypeGenErr.fatal_error(fd, "method flow falls off the end without an explicit return", "TypeError")
else: self.Gen.TypeGenErr.fatal_error(fd, "method flow falls off the end without an explicit return", "TypeError")
# Restore the module-level scope.
self.Gen.VarGen.exit_scope(trans.SymTab)
# M3:弹回外层登记区间(本方法的释放已在各出口完成)
self.Gen.VarGen._SlifeRestore(old_slife_base)
trans._cur_func = old_func
trans._cur_builder = old_builder
trans._global_name_count = old_global_count
trans._nonlocal_name_count = old_nonlocal_count
trans._closure_env_count = old_env_count
trans._stop_iter_flag_ptr = old_stop_iter_flag
# The body itself: one shared pipeline with module-level functions
# (FuncGenBase.translate_func_body) - scope entry, self/parameter storage, pre-scan,
# capture preparation, statements, implicit return, state restore. Methods get no EH
# output params (their call sites do not pass them), so eh_params stays 0.
self.Gen.FuncGenBase.translate_func_body(trans, fd, func, func_builder, ret_ty, m_ctx, skip_self_m, self_ptr_ty, self_name_m, class_name, 0, 0, 1, stop_flag_m)
if dbg_pushed != 0: DebugInfoGen.PopFunction(func_builder)
+413 -1
View File
@@ -8,8 +8,13 @@ import stdlib
import lib.core.VLogger as VLogger
import lib.core.Gen.Translator as HT
import lib.core.Gen.GenApi as GenApi
import lib.core.Gen.GenTypes as GenTypes
import lib.core.Gen.Call.CallGenBase as CallGenBase
# Symbol-table scope kind of a function body (same value as FuncGenMixin.SCOPE_FUNCTION;
# a literal here keeps FuncGenBase from importing FuncGenMixin, which would be a cycle).
define FG_SCOPE_FUNCTION = 1
# FuncGenBase - split from FuncGenMixin (stage D): attrs / naming / mangle / create_function helpers.
novirtual class FuncGenBase(CallGenBase.CallGenBase):
"""Shared helpers for function attributes, naming, mangling and creation."""
@@ -279,4 +284,411 @@ novirtual class FuncGenBase(CallGenBase.CallGenBase):
if pname is not None: viperlib.snprintf(pname, 32, "%%%s", arg.arg)
mlirlite.add_param(pool, func, param_ty, pname)
return func
return func
# ============================================================================
# Unified function-body pipeline (module-level functions and class methods).
#
# Both used to carry their own copy of "resolve the return type", "create the
# parameter allocas" and "emit the implicit epilogue". The copies drifted: an
# unannotated method was still typed i32 by the class-method body path while its
# declaration said void, so the epilogue emitted `llvm.return 0 : i32` inside a
# void function (MLIR: "'llvm.return' op expected no operands"). One
# implementation per rule, shared by every caller, is what keeps the declaration
# and the body from disagreeing again.
# ============================================================================
# ty_is_void - whether an LLVM return type is the void type.
def ty_is_void(self, ret_ty: mlirlite.LLVMType*) -> int:
"""1 when ret_ty is void (the function is a procedure), else 0."""
if ret_ty is None: return 0
is_void: int = 0
match ret_ty:
case mlirlite.LLVMType.Void():
is_void = 1
case _:
pass
return is_void
# resolve_func_ret_ty - the single return-type policy for functions and methods.
#
# Order: annotation -> storage/marker annotations (State/ExportT/ExternT mean void)
# -> __init__/__new__ hooks -> no annotation = procedure (void) -> fail-fast.
# `check_value_return`: reject `def f(): return 1` (a procedure that returns a value)
# at the body stage; the forward-declare pass skips it (defines may not be registered yet).
def resolve_func_ret_ty(self, trans: HT.Translator*, fd: ast.FunctionDef*, ctx_label: str, is_method: int, method_name: str, struct_ty: mlirlite.LLVMType*, check_value_return: int) -> mlirlite.LLVMType*:
"""Resolve a function/method return type; unannotated means procedure (void)."""
if trans is None or fd is None: return None
pool: memhub.MemBuddy* = trans.Pool
imported_modules: str = trans._imported_modules
from_imports: str = trans._from_imports
ret_ty: mlirlite.LLVMType* = None
if fd.returns is not None:
# Value-reference markers are parameter-only: `-> &T` fails fast (never a silent Ptr(T) return).
self.Gen.TypeGenResolve.reject_ref_marker_position(fd.returns, imported_modules, "a function return type")
ret_ty = self.Gen.TypeGenResolve.resolve_annotation_type(pool, fd.returns, imported_modules, from_imports, trans, 0)
# A lone ExportT annotation is an export modifier with no real type -> Void.
if fd.returns.kind() == ast.ASTKind.Name:
ce_at: ast.Name* = ast.Name*(fd.returns)
if ce_at.id is not None and string.strcmp(ce_at.id, "ExportT") == 0: ret_ty = mlirlite.Void(pool)
# A pure marker annotation (t.State/t.ExportT/t.ExternT) carries no real type.
if ret_ty is None:
if self.Gen.TypeGenErr.has_decorator_marker(fd.returns, "State", 0) != 0: ret_ty = mlirlite.Void(pool)
elif self.Gen.TypeGenErr.has_decorator_marker(fd.returns, "ExportT", 0) != 0: ret_ty = mlirlite.Void(pool)
elif self.Gen.TypeGenErr.has_decorator_marker(fd.returns, "ExternT", 0) != 0: ret_ty = mlirlite.Void(pool)
# @c.CReturn was removed: encountering it is a compile-time error (use -> tuple[...]).
if ret_ty is None: self.RejectCReturnDecorator(fd, fd.decorator_list)
if ret_ty is not None: return ret_ty
if fd.returns is not None:
# Return annotation present but unresolvable: no silent inference, no i32 fallback
# (that is how a declare and its body ended up with different types).
self.Gen.TypeGenErr.fatal_error(fd, f"{ctx_label} return type annotation failed to resolve", "NameError")
return None
# No annotation: a procedure. __init__/__new__ keep their fixed shapes.
if is_method != 0 and method_name is not None:
if string.strcmp(method_name, "__init__") == 0: return mlirlite.Void(pool)
if string.strcmp(method_name, "__new__") == 0:
if struct_ty is not None: return mlirlite.Ptr(pool, struct_ty)
if check_value_return != 0:
if self.Gen.TypeGenErr.body_has_value_return(pool, fd.children) != 0:
self.Gen.TypeGenErr.fatal_error(fd, f"{ctx_label} has no return type annotation but returns a non-None value, add an explicit return type annotation (e.g. -> int)", "TypeError")
return None
return mlirlite.Void(pool)
# param_llvm_ty - the single parameter-type mapping (signature and alloca must agree).
# Returns None for a `CVoid` placeholder (the parameter is omitted), else the LLVM type:
# bare class -> Ptr(struct), ArrayT -> element pointer (C array decay), others as resolved.
def param_llvm_ty(self, trans: HT.Translator*, arg: ast.Arg*, ctx_label: str, pure_mode: int) -> mlirlite.LLVMType*:
"""Map one parameter annotation to its LLVM type; None means the parameter is omitted."""
if trans is None or arg is None or arg.arg is None: return None
pool: memhub.MemBuddy* = trans.Pool
imported_modules: str = trans._imported_modules
from_imports: str = trans._from_imports
i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool)
if arg.annotation is None: return i32_ty
# CVoid means empty param: skip, do not add to the signature or allocate it.
if self.Gen.TypeGenResolve._is_builtin_marker(arg.annotation, "CVoid", imported_modules) != 0: return None
resolved: mlirlite.LLVMType* = self.Gen.TypeGenResolve.resolve_annotation_type(pool, arg.annotation, imported_modules, from_imports, trans, 0)
if resolved is None:
# [S4 fail-fast] annotation present but unresolvable: no silent i32 (ABI/call mismatch).
self.Gen.TypeGenErr.fatal_error(arg, f"{ctx_label} parameter '{arg.arg}' type annotation failed to resolve", "NameError")
return None
if pure_mode != 0:
if self.Gen.ExprGenBase.is_ptr_type(resolved) != 0 or resolved.kind() == mlirlite.LLVMType.Array:
self.Gen.TypeGenErr.fatal_error(arg, f"{ctx_label} parameter '{arg.arg}' cannot be a pointer or array: pure parameters are immutable value copies (use str/bytes for a pure_str value, or a pure class value)", "TypeError")
return None
match resolved:
case mlirlite.LLVMType.Void():
# void is not a legal param type (only void* and typedef aliases).
self.Gen.TypeGenErr.fatal_error(arg, f"{ctx_label} parameter '{arg.arg}' cannot be void type (void is only allowed for void* and typedef aliases)", "TypeError")
return None
case mlirlite.LLVMType.Struct(_, _, _):
return mlirlite.Ptr(pool, resolved)
case mlirlite.LLVMType.Array(aelem_ty, _):
return mlirlite.Ptr(pool, aelem_ty)
case _:
return resolved
# emit_param_allocas - create the local storage for every parameter (plus self/cls and *args).
# `skip_self`: the first parameter is the receiver, stored from the already-declared self slot.
def emit_param_allocas(self, trans: HT.Translator*, fd: ast.FunctionDef*, builder: mlirlite.IRBuilder*, ctx_label: str, skip_self: int, self_ty: mlirlite.LLVMType*, self_name: str, self_class_name: str, pure_mode: int) -> int:
"""Create allocas for self/cls and every parameter; register their var annotations."""
if trans is None or fd is None or builder is None: return 0
pool: memhub.MemBuddy* = trans.Pool
# The receiver: @staticmethod has none. It must live in an alloca, not an SSA value:
# loading it once more would turn Ptr(struct) into a struct value (ABI mismatch).
if skip_self != 0 and self_ty is not None:
self_alloca: mlirlite.Value* = mlirlite.build_alloca(builder, self_ty)
if self_alloca is not None:
self_var_name: str = "self"
if self_name is not None: self_var_name = self_name
self.Gen.VarGen.define_var(trans.SymTab, self_var_name, self_alloca)
# The annotation class name is the fallback for attribute lookup.
if self_class_name is not None: self.Gen.VarGen.set_var_annot_class_name(trans.SymTab, self_var_name, self_class_name)
self_ssa_buf: str = pool.alloc(32)
if self_ssa_buf is not None:
viperlib.snprintf(self_ssa_buf, 32, "%%%s", self_var_name)
self_val: mlirlite.Value* = mlirlite.SSAValue(pool, self_ty, self_ssa_buf)
mlirlite.build_store(builder, self_val, self_alloca)
args_node: ast.Arguments* = fd.args
if args_node is None: return 0
ags2: ast.Arguments* = ast.Arguments*(args_node)
if ags2.args is not None:
alist2: list[ast.AST*]* = ags2.args
an2: sizet = len(alist2)
start_idx2: int = 0
if skip_self != 0: start_idx2 = 1
for ai2 in range(int(start_idx2), int(an2)):
arg2: ast.Arg* = ast.Arg*(alist2.get(ai2))
if arg2 is None or arg2.arg is None: continue
param_ty2: mlirlite.LLVMType* = self.param_llvm_ty(trans, arg2, ctx_label, pure_mode)
if param_ty2 is None: continue
alloca: mlirlite.Value* = mlirlite.build_alloca(builder, param_ty2)
if alloca is None: continue
self.Gen.VarGen.define_var(trans.SymTab, arg2.arg, alloca)
# Store the raw annotation class name (method-call detection falls back from Ptr(i8) to the class).
if arg2.annotation is not None:
cls_nm: str = self.Gen.TypeGenResolve.extract_class_name_from_annotation(arg2.annotation, trans._imported_modules, pool, trans._from_imports)
if cls_nm is not None: self.Gen.VarGen.set_var_annot_class_name(trans.SymTab, arg2.arg, cls_nm)
# pure: str params are borrowed pure_str values; `self` is a borrowed receiver.
# Frame lifetime makes both unable to escape.
if pure_mode != 0:
if string.strcmp(arg2.arg, "self") == 0: self.Gen.VarGen.set_var_life(trans.SymTab, arg2.arg, GenTypes.LIFE_FRAME)
elif self.Gen.ExprGenBase._pure_str_struct_ty(pool, param_ty2) is not None: self.Gen.VarGen.set_var_life(trans.SymTab, arg2.arg, GenTypes.LIFE_FRAME)
# t.ConstT / t.VolatileT / volatile pointee markers are per-parameter properties.
if arg2.annotation is not None:
if self.Gen.TypeGenErr.has_decorator_marker(arg2.annotation, "ConstT", 0) != 0: self.Gen.VarGen.set_var_const(trans.SymTab, arg2.arg)
if self.Gen.TypeGenErr.has_decorator_marker(arg2.annotation, "VolatileT", 0) != 0: self.Gen.VarGen.set_var_volatile(trans.SymTab, arg2.arg)
if self.Gen.TypeGenErr.has_pointee_volatile_marker(arg2.annotation) != 0: self.Gen.VarGen.set_var_pointee_volatile(trans.SymTab, arg2.arg)
pname2: str = pool.alloc(32)
if pname2 is not None: viperlib.snprintf(pname2, 32, "%%%s", arg2.arg)
param_val: mlirlite.Value* = mlirlite.SSAValue(pool, param_ty2, pname2)
vol_p2: int = self.Gen.VarGen.var_is_volatile(self.Gen.VarGen.lookup_var_entry(trans.SymTab, arg2.arg))
mlirlite.build_store(builder, param_val, alloca, vol_p2)
# *args: allocate a 24-byte va_list buffer, bitcast to i8*, call @llvm.va_start, register
# the name. The buffer is set on trans so arg()/vararg-name translation can find it.
if ags2.vararg is not None:
vararg_node: ast.Arg* = ast.Arg*(ags2.vararg)
if vararg_node is not None and vararg_node.arg is not None:
i8_ty_va: mlirlite.LLVMType* = mlirlite.Int8(pool)
i8_ptr_ty_va: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ty_va)
va_buf_ty: mlirlite.LLVMType* = mlirlite.Array(pool, i8_ty_va, 24)
va_list_alloca: mlirlite.Value* = mlirlite.build_alloca_aligned(builder, va_buf_ty, 16)
if va_list_alloca is not None:
va_list_addr: mlirlite.Value* = mlirlite.build_bitcast(builder, va_list_alloca, i8_ptr_ty_va)
if va_list_addr is not None:
self.Gen.VarGen.define_var(trans.SymTab, vararg_node.arg, va_list_alloca)
mlirlite.build_call(builder, "llvm.va_start", va_list_addr, 1, mlirlite.Void(pool), 0)
trans._va_list_ptr = va_list_addr
trans._vararg_name = vararg_node.arg
return 0
# add_eh_params - append the try/except output params (__eh_msg_out__/__eh_code_out__).
# Returns 1 when they were added; the body pipeline then wires them onto trans.
def add_eh_params(self, trans: HT.Translator*, fd: ast.FunctionDef*, func: mlirlite.Function*) -> int:
"""Add the try/except output params; 1=added, 0=not needed."""
if trans is None or fd is None or func is None: return 0
if fd.name is not None and string.strcmp(fd.name, "main") == 0: return 0
if self._body_has_raise(fd) == 0: return 0
pool: memhub.MemBuddy* = trans.Pool
i8_ty_eh: mlirlite.LLVMType* = mlirlite.Int8(pool)
i8_ptr_ty_eh: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ty_eh)
i8_ptr_ptr_ty_eh: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ptr_ty_eh)
i32_ty_eh: mlirlite.LLVMType* = mlirlite.Int32(pool)
i32_ptr_ty_eh: mlirlite.LLVMType* = mlirlite.Ptr(pool, i32_ty_eh)
if i8_ptr_ptr_ty_eh is None or i32_ptr_ty_eh is None: return 0
mlirlite.add_param(pool, func, i8_ptr_ptr_ty_eh, "%__eh_msg_out__")
mlirlite.add_param(pool, func, i32_ptr_ty_eh, "%__eh_code_out__")
return 1
# emit_func_epilogue - the single implicit-return implementation.
#
# `unannotated_void`: 1 for module functions and methods (no annotation = procedure), 0 for
# the promoted nested-function ABI, which keeps its legacy i32 default.
def emit_func_epilogue(self, trans: HT.Translator*, fd: ast.FunctionDef*, builder: mlirlite.IRBuilder*, ret_ty: mlirlite.LLVMType*, ctx_label: str, unannotated_void: int) -> int:
"""Emit the implicit return when the body falls off the end; never a value in a procedure."""
if trans is None or fd is None or builder is None: return 0
pool: memhub.MemBuddy* = trans.Pool
mod: mlirlite.LLVMModule* = trans.Module
body: list[ast.AST*]* = fd.children
last_is_return: int = 0
bn3: sizet = 0
if body is not None:
bn3 = len(body)
if bn3 > 0:
last_stmt: ast.AST* = body.get(bn3 - 1)
if last_stmt is not None and last_stmt.kind() == ast.ASTKind.Return: last_is_return = 1
if last_is_return != 0: return 0
if mlirlite.builder_cur_block_is_terminated(builder) != 0: return 0
last_is_ctrl: int = 0
if bn3 > 0:
last_stmt3: ast.AST* = body.get(bn3 - 1)
if last_stmt3 is not None:
k3: int = last_stmt3.kind()
if k3 == ast.ASTKind.If or k3 == ast.ASTKind.While or k3 == ast.ASTKind.For or k3 == ast.ASTKind.Try or k3 == ast.ASTKind.With or k3 == ast.ASTKind.Match: last_is_ctrl = 1
# A procedure never returns a value: explicit type, plus the annotation-level checks
# (`-> None` / t.CVoid) for the case where the REnum class-pattern match degrades.
is_void: int = self.ty_is_void(ret_ty)
if is_void == 0 and unannotated_void != 0:
if fd.returns is None: is_void = 1
elif self.Gen.TypeGenResolve._is_cvoid_marker(fd.returns, trans._imported_modules) != 0: is_void = 1
elif fd.returns.kind() == ast.ASTKind.Constant:
ret_none_cn: ast.Constant* = (ast.Constant*)(fd.returns)
if ret_none_cn.const_kind == ast.CONST_NONE: is_void = 1
if is_void != 0:
self.Gen.VarGen._SlifeEmitFrees(pool, builder, mod, trans)
mlirlite.build_ret_void(builder)
return 0
if last_is_ctrl != 0 and trans._ctrl_merge_reachable == 0:
# Non-void with a control-flow last statement whose merge is structurally unreachable
# (all branches return): the merge block still needs a terminator.
mlirlite.build_unreachable(builder)
return 0
if fd.returns is None:
if unannotated_void != 0:
# Belt and braces: the policy says procedure, so never emit a value return.
self.Gen.VarGen._SlifeEmitFrees(pool, builder, mod, trans)
mlirlite.build_ret_void(builder)
return 0
# Nested-function ABI: an unannotated nested def keeps a value return type.
zero_val: mlirlite.Value* = mlirlite.ConstZero(pool, ret_ty)
if zero_val is None:
self.Gen.TypeGenErr.fatal_error(fd, f"{ctx_label}: the flow falls off the end and the return type has no zero value", "TypeError")
return 0
self.Gen.VarGen._SlifeEmitFrees(pool, builder, mod, trans)
mlirlite.build_ret(builder, zero_val)
return 0
# Annotated non-void with a reachable fall-through: the compiler must not invent a value.
if ret_ty is not None:
rt_name: str = pool.alloc(4096)
if rt_name is not None:
mlirlite.TypePrint(rt_name, 4096, ret_ty, pool)
if rt_name[0] == '\0': rt_name = "?"
self.Gen.TypeGenErr.fatal_error(fd, f"{ctx_label} flow falls off the end of the function without an explicit return (return type {rt_name}), please add an explicit return (e.g. return 0 / return null)", "TypeError")
self.Gen.TypeGenErr.fatal_error(fd, f"{ctx_label} flow falls off the end without an explicit return", "TypeError")
return 0
# translate_func_body - the shared body pipeline: scope entry, parameter storage,
# local pre-scan, capture preparation, statement translation, implicit return, restore.
#
# `eh_params`: 1 when add_eh_params() appended the try/except output params.
# `unannotated_void`: see emit_func_epilogue.
def translate_func_body(self, trans: HT.Translator*, fd: ast.FunctionDef*, func: mlirlite.Function*, builder: mlirlite.IRBuilder*, ret_ty: mlirlite.LLVMType*, ctx_label: str, skip_self: int, self_ty: mlirlite.LLVMType*, self_name: str, self_class_name: str, pure_mode: int, eh_params: int, unannotated_void: int, stop_iter_flag: mlirlite.Value*) -> int:
"""Translate a function/method body end to end (one implementation for both)."""
if trans is None or fd is None or func is None or builder is None: return 0
pool: memhub.MemBuddy* = trans.Pool
mod: mlirlite.LLVMModule* = trans.Module
# Save the enclosing function's state (nested defs/lambdas run inside a body).
old_func: mlirlite.Function* = trans._cur_func
old_builder: mlirlite.IRBuilder* = trans._cur_builder
old_in_pure: int = trans._in_pure
old_break_bb: mlirlite.BasicBlock* = trans._break_bb
old_continue_bb: mlirlite.BasicBlock* = trans._continue_bb
old_insw: int = trans._in_switch_case
old_global_count: int = trans._global_name_count
old_nonlocal_count: int = trans._nonlocal_name_count
old_env_count: int = trans._closure_env_count
old_ret_annot: ast.AST* = trans.CurFuncRetAnnot
old_eh_msg_out: mlirlite.Value* = trans._eh_msg_out_param
old_eh_code_out: mlirlite.Value* = trans._eh_code_out_param
old_cur_func_has_eh: int = trans._cur_func_has_eh
old_va_list_ptr: mlirlite.Value* = trans._va_list_ptr
old_vararg_name: str = trans._vararg_name
old_stop_iter_flag: mlirlite.Value* = trans._stop_iter_flag_ptr
# Enter the function scope: parameters and locals live in it.
self.Gen.VarGen.enter_scope(trans.SymTab, FG_SCOPE_FUNCTION)
# arg()/va_list scope reset: a non-variadic function must not inherit the caller's
# buffer (the printer emits values by NAME, so a stale name would bind to a local of
# this function). emit_param_allocas re-sets it for a variadic callee.
trans._va_list_ptr = None
trans._vararg_name = None
# Stack slots for self/cls and every declared parameter.
self.emit_param_allocas(trans, fd, builder, ctx_label, skip_self, self_ty, self_name, self_class_name, pure_mode)
# try/except: this function's own EH output params (added to the signature by the caller).
trans._eh_msg_out_param = None
trans._eh_code_out_param = None
trans._cur_func_has_eh = 0
if eh_params != 0:
i8_ty_eh: mlirlite.LLVMType* = mlirlite.Int8(pool)
i8_ptr_ty_eh: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ty_eh)
i8_ptr_ptr_ty_eh: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ptr_ty_eh)
i32_ty_eh: mlirlite.LLVMType* = mlirlite.Int32(pool)
i32_ptr_ty_eh: mlirlite.LLVMType* = mlirlite.Ptr(pool, i32_ty_eh)
if i8_ptr_ptr_ty_eh is not None and i32_ptr_ty_eh is not None:
trans._eh_msg_out_param = mlirlite.SSAValue(pool, i8_ptr_ptr_ty_eh, "%__eh_msg_out__")
trans._eh_code_out_param = mlirlite.SSAValue(pool, i32_ptr_ty_eh, "%__eh_code_out__")
trans._cur_func_has_eh = 1
# Switch the translation context to this function.
trans._cur_func = func
trans._cur_builder = builder
trans.CurFuncRetAnnot = fd.returns
trans._stop_iter_flag_ptr = stop_iter_flag
self.Gen.VarGen.freed_clear()
trans._break_bb = None
trans._continue_bb = None
trans._in_switch_case = 0
GenApi.clear_scope_names(trans)
# Nested-def bookkeeping is per function scope, never inherited.
trans._nl_chain_done = 0
trans._nl_chain_count = 0
trans._nl_migrated = None
# Pre-scan: Global/Nonlocal first (they apply to the whole scope), then AnnAssign allocas.
body: list[ast.AST*]* = fd.children
if body is not None:
bn: sizet = len(body)
for bi in range(int(bn)):
stmt: ast.AST* = body.get(bi)
if stmt is not None: self.Gen.StmtGenDecl.pre_scan_scope_decls(trans, stmt)
for bi in range(int(bn)):
stmt: ast.AST* = body.get(bi)
if stmt is not None: self.Gen.StmtGenDecl.pre_scan_allocas(trans, stmt)
# Captured locals must move to the heap before any statement runs: a `&local` handed out
# earlier (widget registration, reverse pointers) would otherwise point at a stack copy.
if body is not None:
i8_ty_b: mlirlite.LLVMType* = mlirlite.Int8(pool)
i8_ptr_ty_b: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ty_b)
self.Gen.FuncGen._prepare_body_chain_nonlocals(trans, body, pool, builder, mod, i8_ty_b, i8_ptr_ty_b)
self.Gen.FuncGen._prepare_body_captures(trans, body, pool, builder, mod, i8_ty_b, i8_ptr_ty_b)
# M3 owned slife: the registration range starts at this stack depth.
old_slife_base: int = self.Gen.VarGen._SlifeReset(pool)
trans._in_pure = pure_mode
# The body itself.
if body is not None:
bn2: sizet = len(body)
for bi2 in range(int(bn2)):
stmt2: ast.AST* = body.get(bi2)
if stmt2 is not None:
# Before a nested def: pre-scan its in-chain nonlocals and build the heap
# (different levels may capture different variables; see _prepare_chain_nonlocals).
if stmt2.kind() == ast.ASTKind.FunctionDef:
self.Gen.FuncGen._prepare_chain_nonlocals(trans, stmt2, pool, builder, mod, mlirlite.Int8(pool), mlirlite.Ptr(pool, mlirlite.Int8(pool)))
self.Gen.StmtGen.translate_stmt(trans, stmt2)
# Implicit return (one implementation for every caller).
self.emit_func_epilogue(trans, fd, builder, ret_ty, ctx_label, unannotated_void)
# Leave the function scope and hand back the enclosing state.
self.Gen.VarGen.exit_scope(trans.SymTab)
self.Gen.VarGen._SlifeRestore(old_slife_base)
trans._cur_func = old_func
trans._cur_builder = old_builder
trans._in_pure = old_in_pure
trans._break_bb = old_break_bb
trans._continue_bb = old_continue_bb
trans._in_switch_case = old_insw
trans._global_name_count = old_global_count
trans._nonlocal_name_count = old_nonlocal_count
trans._closure_env_count = old_env_count
trans._va_list_ptr = old_va_list_ptr
trans._vararg_name = old_vararg_name
trans.CurFuncRetAnnot = old_ret_annot
trans._eh_msg_out_param = old_eh_msg_out
trans._eh_code_out_param = old_eh_code_out
trans._cur_func_has_eh = old_cur_func_has_eh
trans._stop_iter_flag_ptr = old_stop_iter_flag
return 0
+24 -403
View File
@@ -499,28 +499,11 @@ novirtual class FuncGenDef(CallGenBase.CallGenBase):
continue
tp_names_fd: list[str]* = None
# Infer the return type
ret_ty: mlirlite.LLVMType* = None
if fd.returns is not None:
# Value-reference markers are parameter-only: `-> &T` fails fast (never a silent Ptr(T) return).
self.Gen.TypeGenResolve.reject_ref_marker_position(fd.returns, imported_modules, "a function return type")
ret_ty = self.Gen.TypeGenResolve.resolve_annotation_type(pool, fd.returns, imported_modules, from_imports, trans, 0)
# A lone ExportT return annotation is an export modifier with no real type -> Void
# (ExportT fell back to i32, forcing the tail check to return i32: mm.vp free root cause).
if fd.returns.kind() == ast.ASTKind.Name:
ce_at: ast.Name* = ast.Name*(fd.returns)
if ce_at.id is not None and string.strcmp(ce_at.id, "ExportT") == 0: ret_ty = mlirlite.Void(pool)
if ret_ty is None and fd.returns is not None:
if self.Gen.TypeGenErr.has_decorator_marker(fd.returns, "State", 0) != 0: ret_ty = mlirlite.Void(pool)
# @c.CReturn decorator removed: encountering it is a compile-time error (use -> tuple[...] for multiple returns)
if ret_ty is None: self.Gen.FuncGenBase.RejectCReturnDecorator(fd, fd.decorator_list)
if ret_ty is None and fd.returns is not None:
# Return annotation present but unresolvable: compile-time error, no silent i32 fallback
# (that forced return i32 and misaligned cross-module return types). No annotation is a
# procedure (void), matching the definition side and the vpi `def f():` form.
self.Gen.TypeGenErr.fatal_error(fd, "function return type resolution failed: annotation is not resolvable", "TypeError")
return 0
if ret_ty is None: ret_ty = mlirlite.Void(pool)
# Infer the return type (shared policy: annotation -> marker -> unannotated = procedure).
# The value-return check runs in the body pass, where DefineT entries are registered.
fd_ctx_fwd: str = f"function '{fd.name}'"
ret_ty: mlirlite.LLVMType* = self.Gen.FuncGenBase.resolve_func_ret_ty(trans, fd, fd_ctx_fwd, 0, None, None, 0)
if ret_ty is None: return 0
# Detect ExternT/State/ExportT
has_extern: int = 0
@@ -716,45 +699,10 @@ novirtual class FuncGenDef(CallGenBase.CallGenBase):
i32_ty: mlirlite.LLVMType* = mlirlite.Int32(pool)
# Infer the return type: prefer the return annotation
ret_ty: mlirlite.LLVMType* = None
if fd.returns is not None:
# Value-reference markers are parameter-only: `-> &T` fails fast (never a silent Ptr(T) return).
self.Gen.TypeGenResolve.reject_ref_marker_position(fd.returns, imported_modules, "a function return type")
ret_ty = self.Gen.TypeGenResolve.resolve_annotation_type(pool, fd.returns, imported_modules, from_imports, trans, 0)
# A lone ExportT return annotation is an export modifier with no real type -> Void
# (ExportT fell back to i32, forcing return i32: mm.vp free root cause). Mixed types keep the resolved result.
if fd.returns.kind() == ast.ASTKind.Name:
ce_at2: ast.Name* = ast.Name*(fd.returns)
if ce_at2.id is not None and string.strcmp(ce_at2.id, "ExportT") == 0: ret_ty = mlirlite.Void(pool)
# If the return annotation is purely a decorator marker (e.g. t.State, no real type), use void.
# Must be checked before infer_return_type, which would return at least i32.
if ret_ty is None and fd.returns is not None:
if self.Gen.TypeGenErr.has_decorator_marker(fd.returns, "State", 0) != 0: ret_ty = mlirlite.Void(pool)
# @c.CReturn decorator removed: encountering it is a compile-time error (use -> tuple[...] for multiple returns)
if ret_ty is None: self.Gen.FuncGenBase.RejectCReturnDecorator(fd, fd.decorator_list)
# If still None, infer by scanning return statements (at least i32)
if ret_ty is None:
# fail-fast: no return annotation (fd.returns is None) but the body returns a non-None value -> error, no silent
# inference. Returning a value requires an explicit -> annotation; unannotated functions allow only procedure form.
if fd.returns is None:
if self.Gen.TypeGenErr.body_has_value_return(pool, fd.children) != 0:
rtn_err: str = pool.alloc(256)
if rtn_err is not None:
viperlib.snprintf(rtn_err, 256, "function '%s' has no return type annotation but returns a non-None value, add an explicit return type annotation (e.g. -> int)", fd.name)
self.Gen.TypeGenErr.fatal_error(fd, rtn_err, "TypeError")
return 0
self.Gen.TypeGenErr.fatal_error(fd, "function has no return type annotation but returns a non-None value", "TypeError")
return 0
# A procedure: void, so the implicit epilogue emits `ret void` instead of a value
# (a value return inside a void function is rejected by the MLIR verifier).
ret_ty = mlirlite.Void(pool)
else:
param_types_str: str = self.Gen.TypeGenErr.build_param_types_str(pool, fd.args)
ret_ty = self.Gen.TypeGenErr.infer_return_type(pool, fd.children, param_types_str)
# Return type: one shared policy (annotation -> marker -> unannotated = procedure void).
tfd_ctx: str = f"function '{fd.name}'"
ret_ty: mlirlite.LLVMType* = self.Gen.FuncGenBase.resolve_func_ret_ty(trans, fd, tfd_ctx, 0, None, None, 1)
if ret_ty is None: return 0
# Detect an extern declaration (t.ExternT or t.State): t.ExternT ignores the body and emits only a declare;
# t.State = t.ExternT + t.ExportT. t.ExternT/t.State/t.ExportT get no SHA1 prefix and map to C library names.
is_extern_decl: int = 0
@@ -1031,7 +979,7 @@ novirtual class FuncGenDef(CallGenBase.CallGenBase):
if self.Gen.CallGenLeafKit.add_func_to_table(funcs_ptr, func_count, fd.name, func, max_funcs, tfd_defaults, tfd_default_count, tfd_param_count, trans.Pool) == 0:
trans._func_count = func_count + 1
# Add parameters (with type annotation support)
# Add parameters (one shared type mapping: bare class -> Ptr(struct), ArrayT -> element pointer).
if args_node is not None:
ags: ast.Arguments* = ast.Arguments*(args_node)
if ags.args is not None:
@@ -1039,54 +987,19 @@ novirtual class FuncGenDef(CallGenBase.CallGenBase):
an: sizet = len(alist)
for ai in range(int(an)):
arg: ast.Arg* = ast.Arg*(alist.get(ai))
if arg is not None and arg.arg is not None:
# CVoid means empty param: skip, do not add to the function signature
if arg.annotation is not None and self.Gen.TypeGenResolve._is_builtin_marker(arg.annotation, "CVoid", imported_modules) != 0: continue
param_ty: mlirlite.LLVMType* = i32_ty
if arg.annotation is not None:
resolved: mlirlite.LLVMType* = self.Gen.TypeGenResolve.resolve_annotation_type(pool, arg.annotation, imported_modules, from_imports, trans, 0)
if resolved is None:
# [S4 fail-fast] Annotation present but unresolvable: no silent i32
# (wrong param type -> ABI misalignment / wrong field access)
self.Gen.TypeGenErr.fatal_error(arg, f"param '{arg.arg}' type annotation failed to resolve", "NameError")
return 0
# Pure parameters are borrowed value copies, never pointers/arrays.
if has_pure != 0:
if self.Gen.ExprGenBase.is_ptr_type(resolved) != 0 or resolved.kind() == mlirlite.LLVMType.Array:
self.Gen.TypeGenErr.fatal_error(arg, f"pure function '{fd.name}' parameter '{arg.arg}' cannot be a pointer or array: pure parameters are immutable value copies (use str/bytes for a pure_str value, or a pure class value)", "TypeError")
return 0
# Bare class param -> Ptr(struct); ArrayT param -> element pointer ty* (C array decay). Passing
# Ptr(array) would make out[i]'s GEP stride by the whole array (garbage summary root cause).
match resolved:
case mlirlite.LLVMType.Void():
# void is not a legal param type (see the same rule above)
self.Gen.TypeGenErr.fatal_error(arg, f"param '{arg.arg}' cannot be void type (void is only allowed for void* and typedef aliases)", "TypeError")
return 0
case mlirlite.LLVMType.Struct(_, _, _):
param_ty = mlirlite.Ptr(pool, resolved)
case mlirlite.LLVMType.Array(aelem_ty, _):
param_ty = mlirlite.Ptr(pool, aelem_ty)
case _:
param_ty = resolved
pname: str = pool.alloc(32)
if pname is not None: viperlib.snprintf(pname, 32, "%%%s", arg.arg)
mlirlite.add_param(pool, func, param_ty, pname)
if arg is None or arg.arg is None: continue
param_ty: mlirlite.LLVMType* = self.Gen.FuncGenBase.param_llvm_ty(trans, arg, tfd_ctx, has_pure)
if param_ty is None: continue
pname: str = pool.alloc(32)
if pname is not None: viperlib.snprintf(pname, 32, "%%%s", arg.arg)
mlirlite.add_param(pool, func, param_ty, pname)
# *args variadic: set IsVarArg=1 to emit the ... suffix
if ags.vararg is not None: func.IsVarArg = 1
# try-except: a body containing raise gets extra __eh_msg_out__ (i8**) / __eh_code_out__ (i32*) output params
# (TPC HandlesFunctions.py L706-711; except main). Must be outside the args_node block so no-arg functions are covered.
if string.strcmp(fd.name, "main") != 0:
if self.Gen.FuncGenBase._body_has_raise(fd) != 0:
i8_ty_eh: mlirlite.LLVMType* = mlirlite.Int8(pool)
i8_ptr_ty_eh: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ty_eh)
i8_ptr_ptr_ty_eh: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ptr_ty_eh)
i32_ty_eh: mlirlite.LLVMType* = mlirlite.Int32(pool)
i32_ptr_ty_eh: mlirlite.LLVMType* = mlirlite.Ptr(pool, i32_ty_eh)
if i8_ptr_ptr_ty_eh is not None and i32_ptr_ty_eh is not None:
mlirlite.add_param(pool, func, i8_ptr_ptr_ty_eh, "%__eh_msg_out__")
mlirlite.add_param(pool, func, i32_ptr_ty_eh, "%__eh_code_out__")
# try-except: a body containing raise gets extra __eh_msg_out__ (i8**) / __eh_code_out__ (i32*) output
# params (never main). The body pipeline wires them onto trans.
eh_added: int = self.Gen.FuncGenBase.add_eh_params(trans, fd, func)
# Params added; fill the global func-table Spec
self.Gen.CallGenLeafKit.update_global_func_spec(fd.name, func, self.Gen.TypeGenResolve.annot_ret_sign(fd.returns))
@@ -1157,303 +1070,11 @@ novirtual class FuncGenDef(CallGenBase.CallGenBase):
tlone: mlirlite.Value* = mlirlite.ConstInt(pool, i1_ty_t, 1, "1")
if tlone is not None: mlirlite.build_store(func_builder, tlone, tls_flag_ref)
# Enter the function scope (nested symbol table)
self.Gen.VarGen.enter_scope(trans.SymTab, FuncGenMixin.SCOPE_FUNCTION)
# Generic function templates already returned early (see the top of this function); here fd.type_params is always
# None (specialization clones clear it). The generic context is set by _specialize_generic_function before the call.
# arg()/va_list scope reset: save the ENCLOSING function's va_list state and start this body with
# none of it. A non-variadic function must not inherit the previous function's buffer: that Value
# belongs to another function while the printer emits it by NAME, so the name re-binds to
# whatever local happens to carry it here (scene: viperlib vsnprintf emitted `va_arg ptr %9`
# where %9 is `count_mode`'s i32 alloca -> va_arg loaded a garbage va_list pointer ->
# access-violation inside vsnprintf). The *args setup below re-sets both for a variadic callee,
# and old_va_list_ptr/old_vararg_name are restored after the body so that nested functions still
# see the enclosing variadic function's buffer.
old_va_list_ptr: mlirlite.Value* = trans._va_list_ptr
old_vararg_name: str = trans._vararg_name
trans._va_list_ptr = None
trans._vararg_name = None
# Create allocas for the parameters
if args_node is not None:
ags2: ast.Arguments* = ast.Arguments*(args_node)
if ags2.args is not None:
alist2: list[ast.AST*]* = ags2.args
an2: sizet = len(alist2)
for ai2 in range(int(an2)):
arg2: ast.Arg* = ast.Arg*(alist2.get(ai2))
if arg2 is not None and arg2.arg is not None:
# CVoid means empty param: skip, do not create an alloca
if arg2.annotation is not None:
if self.Gen.TypeGenResolve._is_builtin_marker(arg2.annotation, "CVoid", imported_modules) != 0: continue
param_ty2: mlirlite.LLVMType* = i32_ty
if arg2.annotation is not None:
resolved2: mlirlite.LLVMType* = self.Gen.TypeGenResolve.resolve_annotation_type(pool, arg2.annotation, imported_modules, from_imports, trans, 0)
if resolved2 is None:
# fail-fast: param has an annotation but is unresolvable; no silent i32
self.Gen.TypeGenErr.fatal_error(arg2, f"argument '{arg2.arg}' failed to resolve type: {self.Gen.TypeGenResolve.annotation_to_str(arg2.annotation)}", "NameError")
resolved2 = i32_ty # unreachable (fatal_error already exited)
if resolved2 is not None:
# Consistent with add_param: bare class param -> Ptr(struct); ArrayT param -> element pointer ty*
# (array decay; the alloca type must match the param or storing ty* into [N x ty]* mismatches).
match resolved2:
case mlirlite.LLVMType.Void():
# void is not a legal param type (see the same rule above)
self.Gen.TypeGenErr.fatal_error(arg2, f"param '{arg2.arg}' cannot be void type (void is only allowed for void* and typedef aliases)", "TypeError")
return 0
case mlirlite.LLVMType.Struct(_, _, _):
param_ty2 = mlirlite.Ptr(pool, resolved2)
case mlirlite.LLVMType.Array(aelem_ty2, _):
param_ty2 = mlirlite.Ptr(pool, aelem_ty2)
case _:
param_ty2 = resolved2
alloca: mlirlite.Value* = mlirlite.build_alloca(func_builder, param_ty2)
if alloca is not None:
self.Gen.VarGen.define_var(trans.SymTab, arg2.arg, alloca)
# Store the original annotation's class name (method-call detection falls back from Ptr(i8) to the class).
# Pass pool: a generic Subscript returns a mangled name (aligned with the HandlesAnnAssign fix).
cls_nm: str = None
if arg2.annotation is not None: cls_nm = self.Gen.TypeGenResolve.extract_class_name_from_annotation(arg2.annotation, imported_modules, pool, trans._from_imports)
if cls_nm is not None: self.Gen.VarGen.set_var_annot_class_name(trans.SymTab, arg2.arg, cls_nm)
# pure: str params are borrowed pure_str values and `self` is a
# borrowed receiver. Frame lifetime makes both unable to escape.
if has_pure != 0:
if string.strcmp(arg2.arg, "self") == 0:
self.Gen.VarGen.set_var_life(trans.SymTab, arg2.arg, GenTypes.LIFE_FRAME)
elif resolved2 is not None and self.Gen.ExprGenBase._pure_str_struct_ty(pool, resolved2) is not None:
self.Gen.VarGen.set_var_life(trans.SymTab, arg2.arg, GenTypes.LIFE_FRAME)
# t.VolatileT param: marked at declaration time so loads/stores of this name carry volatile
# (a volatile param object's read-modify-write must not be hoisted/merged, e.g. a spin-wait arg).
if arg2.annotation is not None:
# const param: reuse TPC's historical param criterion (only whether the annotation contains
# t.ConstT, regardless of pointer depth); TPC HandlesFunctions IsParam=True uses the same condition.
if self.Gen.TypeGenErr.has_decorator_marker(arg2.annotation, "ConstT", 0) != 0: self.Gen.VarGen.set_var_const(trans.SymTab, arg2.arg)
if self.Gen.TypeGenErr.has_decorator_marker(arg2.annotation, "VolatileT", 0) != 0: self.Gen.VarGen.set_var_volatile(trans.SymTab, arg2.arg)
# rawptr[t.VolatileT | T] param (volatile pointee): dereferences in the body
# (p[i]/p.field/c.Deref(p)) carry volatile.
if self.Gen.TypeGenErr.has_pointee_volatile_marker(arg2.annotation) != 0: self.Gen.VarGen.set_var_pointee_volatile(trans.SymTab, arg2.arg)
pname2: str = pool.alloc(32)
if pname2 is not None: viperlib.snprintf(pname2, 32, "%%%s", arg2.arg)
param_val: mlirlite.Value* = mlirlite.SSAValue(pool, param_ty2, pname2)
vol_p2: int = self.Gen.VarGen.var_is_volatile(self.Gen.VarGen.lookup_var_entry(trans.SymTab, arg2.arg))
mlirlite.build_store(func_builder, param_val, alloca, vol_p2)
# *args variadic: allocate a 24-byte va_list buffer, bitcast to i8*, call @llvm.va_start, register in the symbol table.
# Must be inside the param alloca section but outside the EH block, or args is unregistered (NameError: 'args').
if ags2.vararg is not None:
vararg_node: ast.Arg* = ast.Arg*(ags2.vararg)
if vararg_node is not None and vararg_node.arg is not None:
vararg_name_str: str = vararg_node.arg
# va_list buffer is 24 bytes (Win64 __va_list_tag = {i32,i32,i8*,i8*}) with 16-byte alignment
# (aligned with TPC HandlesFunctions.py:1739-1741).
i8_ty_va: mlirlite.LLVMType* = mlirlite.Int8(pool)
i8_ptr_ty_va: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ty_va)
va_buf_ty: mlirlite.LLVMType* = mlirlite.Array(pool, i8_ty_va, 24)
va_list_alloca: mlirlite.Value* = mlirlite.build_alloca_aligned(func_builder, va_buf_ty, 16)
if va_list_alloca is not None:
# i8* address (pointing at the 24-byte va_list buffer) - the value semantics of the vararg name
va_list_addr: mlirlite.Value* = mlirlite.build_bitcast(func_builder, va_list_alloca, i8_ptr_ty_va)
if va_list_addr is not None:
# Register in the symbol table (translate_name_value special-cases vararg names to return the address)
self.Gen.VarGen.define_var(trans.SymTab, vararg_name_str, va_list_alloca)
# Call @llvm.va_start(i8* %va_list_addr)
void_ty_va: mlirlite.LLVMType* = mlirlite.Void(pool)
mlirlite.build_call(func_builder, "llvm.va_start", va_list_addr, 1, void_ty_va, 0)
# Save on trans for the arg(type) sugar and vararg-name translation
trans._va_list_ptr = va_list_addr
trans._vararg_name = vararg_name_str
# try-except: a new function scope starts with zero EH state. Save the enclosing function's EH output params
# before and restore them after; otherwise nested functions corrupt the EH state chain.
old_eh_msg_out: mlirlite.Value* = trans._eh_msg_out_param
old_eh_code_out: mlirlite.Value* = trans._eh_code_out_param
old_cur_func_has_eh: int = trans._cur_func_has_eh
trans._eh_msg_out_param = None
trans._eh_code_out_param = None
trans._cur_func_has_eh = 0
# try-except: save this function's EH output params (__eh_msg_out__/__eh_code_out__) for translate_raise /
# UnhandledBB to write and propagate. Functions without EH params are set to None.
if string.strcmp(fd.name, "main") != 0 and self.Gen.FuncGenBase._body_has_raise(fd) != 0:
i8_ty_eh2: mlirlite.LLVMType* = mlirlite.Int8(pool)
i8_ptr_ty_eh2: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ty_eh2)
i8_ptr_ptr_ty_eh2: mlirlite.LLVMType* = mlirlite.Ptr(pool, i8_ptr_ty_eh2)
i32_ty_eh2: mlirlite.LLVMType* = mlirlite.Int32(pool)
i32_ptr_ty_eh2: mlirlite.LLVMType* = mlirlite.Ptr(pool, i32_ty_eh2)
if i8_ptr_ptr_ty_eh2 is not None and i32_ptr_ty_eh2 is not None:
msg_out_ref: mlirlite.Value* = mlirlite.SSAValue(pool, i8_ptr_ptr_ty_eh2, "%__eh_msg_out__")
code_out_ref: mlirlite.Value* = mlirlite.SSAValue(pool, i32_ptr_ty_eh2, "%__eh_code_out__")
trans._eh_msg_out_param = msg_out_ref
trans._eh_code_out_param = code_out_ref
trans._cur_func_has_eh = 1
# Save module-level scope state (non-variable-table related only)
old_func: mlirlite.Function* = trans._cur_func
old_builder: mlirlite.IRBuilder* = trans._cur_builder
old_global_count: int = trans._global_name_count
old_nonlocal_count: int = trans._nonlocal_name_count
old_env_count: int = trans._closure_env_count
# old_va_list_ptr / old_vararg_name were saved above (before the *args setup) so that this body
# could start from a clean va_list state; the restore below hands the enclosing value back.
# Return-annotation context ([C①], one source of type-argument fallback for bare generic construction):
# record this function's return annotation before entering the body and restore the outer value on exit.
old_ret_annot: ast.AST* = trans.CurFuncRetAnnot
# break/continue targets and the match-case flag must be cleared for the new function scope (see assignments below),
# otherwise break/continue in a nested function inherits the outer (even cross-function) br targets.
old_break_bb: mlirlite.BasicBlock* = trans._break_bb
old_continue_bb: mlirlite.BasicBlock* = trans._continue_bb
old_insw: int = trans._in_switch_case
trans._cur_func = func
trans._cur_builder = func_builder
trans.CurFuncRetAnnot = fd.returns
# P2 willFree 失效表:**函数作用域复位**(表按栈槽地址记录,槽地址会被后续函数复用)
self.Gen.VarGen.freed_clear()
trans._break_bb = None
trans._continue_bb = None
trans._in_switch_case = 0
# Clear the global/nonlocal name sets (new function scope)
GenApi.clear_scope_names(trans)
# Reset the in-chain nonlocal heap prebuild flag (built once per function; multiple nested function definitions
# share one heap, preventing repeated malloc + double VarEntry redirection).
trans._nl_chain_done = 0
# 名字表与"已迁移"标记同样**按函数隔离**:它们只对当前函数体内的局部有效,
# 跨函数累积会撞上 64 槽上限并让 later 名字被丢弃。
trans._nl_chain_count = 0
trans._nl_migrated = None
# Pre-scan the body: 1) Global/Nonlocal declarations first (Python semantics: they apply to the whole function
# scope regardless of position, so register before any assignment); 2) AnnAssign to create local allocas early.
body: list[ast.AST*]* = fd.children
if body is not None:
bn: sizet = len(body)
for bi in range(int(bn)):
stmt: ast.AST* = body.get(bi)
if stmt is not None: self.Gen.StmtGenDecl.pre_scan_scope_decls(trans, stmt)
for bi in range(int(bn)):
stmt: ast.AST* = body.get(bi)
if stmt is not None: self.Gen.StmtGenDecl.pre_scan_allocas(trans, stmt)
# 【修复】把"嵌套闭包捕获的局部对象 → 堆迁移"提前到**任何语句执行之前**。
# 迁移会 malloc 新存储并把 VarEntry.Alloca 改指向它;若等到第一个嵌套 def 处才做,
# 在此之前已经交出去的 `&local`(`_register_widget(&widget)` 存进 root.widgets、
# 对象内部的反向指针)会永远指向栈副本 —— 现场:Terminal 的控件全注册在栈副本上,
# 回车读 on_command 得 None(打印 `(no callback)`),按钮/滚动条回调也全丢。
if body is not None:
self.Gen.FuncGen._prepare_body_chain_nonlocals(trans, body, pool, func_builder, mod,
mlirlite.Int8(pool), mlirlite.Ptr(pool, mlirlite.Int8(pool)))
# 同上,但覆盖**隐式捕获**(嵌套 def 只是读外层局部、没有 nonlocal 语句)。
# 这是 Terminal/Explorer 这类 UI 代码的主要路径。
self.Gen.FuncGen._prepare_body_captures(trans, body, pool, func_builder, mod,
mlirlite.Int8(pool), mlirlite.Ptr(pool, mlirlite.Int8(pool)))
# M3 owned slife(§4.2 函数级 scope guard):进入函数体前把登记区间起点抬到当前栈顶。
# 表是函数间共享的一段栈式区间,嵌套函数各自独立;退出时由 _SlifeRestore 弹回外层起点。
# 放在这里(而非更早)是因为上方存在多条提前返回分支(泛型模板/async/extern),
# 只有从这里开始才保证"有 reset 必有 restore"。
old_slife_base: int = self.Gen.VarGen._SlifeReset(pool)
# Translate the function body
old_in_pure: int = trans._in_pure
trans._in_pure = has_pure
if body is not None:
bn2: sizet = len(body)
for bi2 in range(int(bn2)):
stmt2: ast.AST* = body.get(bi2)
if stmt2 is not None:
# Before a nested function definition: pre-scan its in-chain nonlocals and build the heap
# (for cases where different levels capture different variables; see _prepare_chain_nonlocals).
if stmt2.kind() == ast.ASTKind.FunctionDef:
self.Gen.FuncGen._prepare_chain_nonlocals(trans, stmt2, pool, func_builder, mod, mlirlite.Int8(pool), mlirlite.Ptr(pool, mlirlite.Int8(pool)))
self.Gen.StmtGen.translate_stmt(trans, stmt2)
# If the last body statement is not Return, add an implicit ret
last_is_return: int = 0
# bn3 is initialized early so the fail-fast branch below does not reference an undefined bn3 when body is None.
bn3: sizet = 0
if body is not None:
bn3 = len(body)
if bn3 > 0:
last_stmt: ast.AST* = body.get(bn3 - 1)
if last_stmt is not None and last_stmt.kind() == ast.ASTKind.Return: last_is_return = 1
if last_is_return == 0:
if mlirlite.builder_cur_block_is_terminated(func_builder) == 0:
# The compiler never invents a return value: falling off the end without an explicit return is fail-fast.
# Legal endings: void -> ret void; unreachable merge (all branches terminated) -> unreachable; unannotated -> zero return.
last_is_ctrl: int = 0
if bn3 > 0:
last_stmt3: ast.AST* = body.get(bn3 - 1)
if last_stmt3 is not None:
k3: int = last_stmt3.kind()
if k3 == ast.ASTKind.If or k3 == ast.ASTKind.While or k3 == ast.ASTKind.For or k3 == ast.ASTKind.Try or k3 == ast.ASTKind.With or k3 == ast.ASTKind.Match: last_is_ctrl = 1
_is_void: int = 0
if ret_ty is not None:
match ret_ty:
case mlirlite.LLVMType.Void():
_is_void = 1
case _:
pass
# Annotation-level "no return value" criterion (a second check alongside the ret_ty type check): if the
# REnum class-pattern match degrades, _is_void stays 0 and a void function is misreported as missing a return.
if _is_void == 0 and fd.returns is not None:
if self.Gen.TypeGenResolve._is_cvoid_marker(fd.returns, imported_modules) != 0: _is_void = 1
elif fd.returns.kind() == ast.ASTKind.Constant:
ret_none_cn: ast.Constant* = (ast.Constant*)(fd.returns)
if ret_none_cn.const_kind == ast.CONST_NONE: _is_void = 1
if _is_void != 0:
# M3:隐式收尾也是一个函数出口,先释放本函数登记的 slife 再返回
self.Gen.VarGen._SlifeEmitFrees(pool, func_builder, mod, trans)
mlirlite.build_ret_void(func_builder)
elif last_is_ctrl != 0 and trans._ctrl_merge_reachable == 0:
# A non-void function whose last statement is control flow with a structurally unreachable merge (all
# branches return, no br to merge) -> LLVM unreachable. Reachability uses only `_ctrl_merge_reachable`.
mlirlite.build_unreachable(func_builder)
elif fd.returns is None:
# No return annotation -> a procedure: the epilogue must never carry a value
# (`llvm.return <val>` in a void function is rejected by the verifier).
self.Gen.VarGen._SlifeEmitFrees(pool, func_builder, mod, trans)
mlirlite.build_ret_void(func_builder)
else:
# fail-fast: has a -> annotation but the flow falls off the end (pointing at the function definition
# node; fatal_error prints a traceback-style error).
if ret_ty is not None:
# mlirlite has no type_get_name API; serialize the type with the real interface
# TypePrint(buf, size, ty, pool).
rt_name: str = pool.alloc_tagged(4096, memhub.DETSITE_FUNCGENDEF_792)
if rt_name is not None:
mlirlite.TypePrint(rt_name, 4096, ret_ty, pool)
if rt_name[0] == '\0': rt_name = "?"
err_fn: str = pool.alloc(256)
if err_fn is not None:
viperlib.snprintf(err_fn, 256, "function '%s' flow falls off the end of the function without an explicit return (return type %s), please add an explicit return (e.g. return 0 / return null)", fd.name, rt_name)
self.Gen.TypeGenErr.fatal_error(node, err_fn, "TypeError")
self.Gen.TypeGenErr.fatal_error(node, "function flow falls off the end without an explicit return", "TypeError")
else: self.Gen.TypeGenErr.fatal_error(node, "function flow falls off the end without an explicit return", "TypeError")
# Restore module-level scope (exit the function scope)
self.Gen.VarGen.exit_scope(trans.SymTab)
# M3 owned slife:丢弃本函数的登记区间、弹回外层起点(释放已在各出口用 _SlifeEmitFrees 完成)
self.Gen.VarGen._SlifeRestore(old_slife_base)
trans._cur_func = old_func
trans._cur_builder = old_builder
trans._in_pure = old_in_pure
trans._break_bb = old_break_bb
trans._continue_bb = old_continue_bb
trans._in_switch_case = old_insw
trans._global_name_count = old_global_count
trans._nonlocal_name_count = old_nonlocal_count
trans._closure_env_count = old_env_count
trans._va_list_ptr = old_va_list_ptr
trans._vararg_name = old_vararg_name
# Hand back the outer function's return annotation (see old_ret_annot above)
trans.CurFuncRetAnnot = old_ret_annot
# Restore the outer (caller) function's EH output params: this function's raise propagation uses its own params,
# so hand them back on exit or nested/chained calls leave stale EH state and write to the wrong out pointer.
trans._eh_msg_out_param = old_eh_msg_out
trans._eh_code_out_param = old_eh_code_out
trans._cur_func_has_eh = old_cur_func_has_eh
# The body itself - scope entry, parameter storage, pre-scan, capture preparation,
# statement translation, implicit return and state restore - is one shared pipeline
# with class methods (FuncGenBase.translate_func_body). Keeping a single copy is what
# stops a declare and its body from disagreeing on the return type.
self.Gen.FuncGenBase.translate_func_body(trans, fd, func, func_builder, ret_ty, tfd_ctx, 0, None, None, None, has_pure, eh_added, 1, None)
# Custom decorator: register and generate a wrapper (aligned with TPC DecoratorPass.run). Module-level functions
# only; wrapper name is __decor_wrap_{bare name}; call sites are rewritten via lookup_decor_wrapper.
+6 -19
View File
@@ -248,23 +248,10 @@ novirtual class FuncGenMixin(CallGenBase.CallGenBase):
stmt2: ast.AST* = body.get(bi2)
if stmt2 is not None: self.Gen.StmtGen.translate_stmt(trans, stmt2)
# 10. Implicit return
last_is_return: int = 0
if body is not None:
bn3: sizet = len(body)
if bn3 > 0:
last_stmt: ast.AST* = body.get(bn3 - 1)
if last_stmt is not None and last_stmt.kind() == ast.ASTKind.Return: last_is_return = 1
if last_is_return == 0:
# M3:隐式收尾也是函数出口,先释放本嵌套函数登记的 slife
self.Gen.VarGen._SlifeEmitFrees(pool, func_builder, mod, trans)
# 零值必须与**本函数的返回类型**一致(原实现写死 const_int32(0):一旦返回类型改为
# `char*` 之类,`ret i32 0` 与签名不符,llc 会直接拒绝)。ret_ty_n 见第 2 步。
zero_val: mlirlite.Value* = mlirlite.ConstZero(pool, ret_ty_n)
if zero_val is None:
self.Gen.TypeGenErr.fatal_error(fd, f"nested function '{fd.name}': flow falls off the end without an implicit return and the return type has no zero value", "TypeError")
else:
mlirlite.build_ret(func_builder, zero_val)
# 10. Implicit return (one shared implementation; the promoted-closure ABI keeps the
# legacy "unannotated = i32" policy, hence unannotated_void=0).
nf_ctx: str = f"nested function '{fd.name}'"
self.Gen.FuncGenBase.emit_func_epilogue(trans, fd, func_builder, ret_ty_n, nf_ctx, 0)
# M3:弹回外层登记区间(释放已在本嵌套函数各出口完成)
self.Gen.VarGen._SlifeRestore(old_slife_base)
@@ -923,7 +910,7 @@ novirtual class FuncGenMixin(CallGenBase.CallGenBase):
return self._build_nl_heaps(trans, pool, builder, mod, i8_ptr_ty, 0)
# _dry_define_annassign_names - 干跑用:把节点里所有 AnnAssign 目标登记成"局部名字"(不产生指令)
def _dry_define_annassign_names(self, trans: HT.Translator*, node: ast.AST*, dummy: mlirlite.Value*):
def _dry_define_annassign_names(self, trans: HT.Translator*, node: ast.AST*, dummy: mlirlite.Value*) -> int:
"""Register every AnnAssign target inside node as a local name (no code emitted)"""
if node is None or dummy is None: return 0
k: int = node.kind()
@@ -981,7 +968,7 @@ novirtual class FuncGenMixin(CallGenBase.CallGenBase):
return 0
# _dry_define_nested_locals - 干跑用:把嵌套 def 的形参与 AnnAssign 局部登记成"当前作用域局部"
def _dry_define_nested_locals(self, trans: HT.Translator*, fd: ast.FunctionDef*, pool: memhub.MemBuddy*, dummy_ty: mlirlite.LLVMType*):
def _dry_define_nested_locals(self, trans: HT.Translator*, fd: ast.FunctionDef*, pool: memhub.MemBuddy*, dummy_ty: mlirlite.LLVMType*) -> int:
"""Register the nested def's params and AnnAssign locals as locals (no alloca emitted)"""
if fd is None or pool is None: return 0
dummy: mlirlite.Value* = mlirlite.SSAValue(pool, dummy_ty, "%dry")