From 3b3cd42d1c5fead3082e67ff11cbae1c16f1779b Mon Sep 17 00:00:00 2001 From: TermiNexus Date: Thu, 29 Oct 2026 18:13:06 +0800 Subject: [PATCH] fix(codegen): a procedure never emits a value return Unannotated functions were typed by scanning the body, which inferred i32, so falling off the end emitted `llvm.return 0 : i32` inside a function declared void. The MLIR verifier rejects that ("expected no operands"), which is what broke Scheduler._yield. - an unannotated `def` is void in both the forward declare and the body, so the implicit epilogue emits `ret void` - translate_return treats a procedure (no annotation, `-> None`, or a void-typed function) as void: the expression is evaluated for its side effects and `ret void` is emitted. Bare `return`, `return f()` where f is void, and `return ` are legal and never carry a value - body_has_value_return accepts `return f()` and `return ` in a procedure - `define X = None` is a named None: registered as the string sentinel None, materialized as a None value at use sites, and exported through the vpi --- App/lib/Stub/StubGen/VpiReader.vp | 10 ++++++- App/lib/core/Gen/AssignGenMixin.vp | 6 ++++ App/lib/core/Gen/Expr/ExprGenName.vp | 6 +++- App/lib/core/Gen/Func/FuncGenDef.vp | 26 +++++++++--------- App/lib/core/Gen/GenApi.vp | 6 ++++ App/lib/core/Gen/Stmt/StmtGenCifMixin.vp | 35 ++++++++++++++++++------ App/lib/core/Gen/Type/TypeGenErr.vp | 11 ++++++++ 7 files changed, 76 insertions(+), 24 deletions(-) diff --git a/App/lib/Stub/StubGen/VpiReader.vp b/App/lib/Stub/StubGen/VpiReader.vp index 8f00545..f7f12db 100644 --- a/App/lib/Stub/StubGen/VpiReader.vp +++ b/App/lib/Stub/StubGen/VpiReader.vp @@ -2329,6 +2329,9 @@ def _vpi_register_typedef_closure(pool: memhub.MemBuddy*, import_mods: str, temp # 位宽与定义方同一套裁决(注解 → 右侧强转 → 值的最小可存宽度), # 否则使用方拿 bits=0 只能兜底,跨模块位宽会与定义方不一致 GenApi.register_cdefine_constant(pool, cdef_t.id, cdef_val, GenApi.cdefine_decl_bits(pool, aa_r.annotation, vnode_r), sha1_r) + elif cn_v.const_kind == ast.CONST_NONE: + # `define X = None`: a named None (see _register_cdefine_from_value). + GenApi.register_cdefine_string(pool, cdef_t.id, "None", sha1_r) # recurse into child imports (cycle guarded by the visited marks) if sub_imports[0] != '\0': _vpi_register_typedef_closure(pool, sub_imports, temp_dir, depth + 1) stdlib.free(sub_imports) @@ -3339,15 +3342,20 @@ def _ParseVpiIntoModuleImpl(pool: memhub.MemBuddy*, mod: mlirlite.LLVMModule*, v cdef_neg_f = 1 cdef_is_str: int = 0 cdef_is_float: int = 0 + cdef_is_none: int = 0 cdef_float_val: double = 0.0 if vnode.kind() == ast.ASTKind.Constant: cn_v: ast.Constant* = ast.Constant*(vnode) if cn_v.const_kind == ast.CONST_STR: cdef_is_str = 1 + elif cn_v.const_kind == ast.CONST_NONE: cdef_is_none = 1 elif cn_v.const_kind == ast.CONST_FLOAT: cdef_is_float = 1 cdef_float_val = cn_v.float_val if cdef_neg_f != 0: cdef_float_val = -cdef_float_val - if cdef_is_str == 0 and cdef_is_float != 0: + if cdef_is_none != 0: + # `define X = None`: a named None (see _register_cdefine_from_value). + GenApi.register_cdefine_string(pool, cdef_tgt.id, "None", def_mod) + elif cdef_is_str == 0 and cdef_is_float != 0: # Float DefineT (e.g. FRAGMENTATION_THRESHOLD: t.DefineT = 0.7): register via # register_cdefine_double (integer extraction would fail fast). # 注解声明 float 时带上 f32 种类(与定义方同一套裁决)。 diff --git a/App/lib/core/Gen/AssignGenMixin.vp b/App/lib/core/Gen/AssignGenMixin.vp index 39691cd..a9b3dad 100644 --- a/App/lib/core/Gen/AssignGenMixin.vp +++ b/App/lib/core/Gen/AssignGenMixin.vp @@ -909,6 +909,12 @@ novirtual class AssignGenMixin: return if val_node is not None and val_node.kind() == ast.ASTKind.Constant: cn_fp: ast.Constant* = ast.Constant*(val_node) + if cn_fp.const_kind == ast.CONST_NONE: + # `define X = None` is a named None: usable wherever None is (e.g. `return X` + # from a procedure must emit `ret void`, never a value). Stored as the string + # constant "None" and materialized as a None value at use sites. + self.Gen.TypeGenCDef.register_cdefine_string(pool, name, "None") + return if cn_fp.const_kind == ast.CONST_STR: # String DefineT: register as a string constant (reader returns a global string) self.Gen.TypeGenCDef.register_cdefine_string(pool, name, cn_fp.str_val) diff --git a/App/lib/core/Gen/Expr/ExprGenName.vp b/App/lib/core/Gen/Expr/ExprGenName.vp index 96fd69d..62f7210 100644 --- a/App/lib/core/Gen/Expr/ExprGenName.vp +++ b/App/lib/core/Gen/Expr/ExprGenName.vp @@ -52,7 +52,11 @@ novirtual class ExprGenName(ExprGenBase.ExprGenBase): # String DefineT (e.g. ZLIB_VERSION: t.DefineT = "1.3.2"): return a global # string (i8*) for `return ZLIB_VERSION`. cdef_sv: str = self.Gen.TypeGenCDef.lookup_cdefine_string(nm_id) - if cdef_sv is not None: return self.Gen.ExprGenConst.create_global_string(builder, pool, mod, cdef_sv, trans) + if cdef_sv is not None: + # `define X = None`: a named None, materialized like the None literal so a + # procedure's `return X` becomes `ret void` (never a value). + if string.strcmp(cdef_sv, "None") == 0: return self._NoneValueAsTarget(builder, target_ty) + return self.Gen.ExprGenConst.create_global_string(builder, pool, mod, cdef_sv, trans) self.Gen.TypeGenErr.fatal_error(node, f"DefineT string value missing: {nm_id}", "InternalCompilerError") # 浮点常量:声明为 32 位 float(`define PI: float = ...`)时直接出 f32 常量, # 否则出 f64 —— 出 f64 会把后续 f32 字面量整体提升成 double(fpext/cvtss2sd)。 diff --git a/App/lib/core/Gen/Func/FuncGenDef.vp b/App/lib/core/Gen/Func/FuncGenDef.vp index c492074..ecdd3f1 100644 --- a/App/lib/core/Gen/Func/FuncGenDef.vp +++ b/App/lib/core/Gen/Func/FuncGenDef.vp @@ -516,10 +516,11 @@ novirtual class FuncGenDef(CallGenBase.CallGenBase): 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 keeps i32. + # (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 = i32_ty + if ret_ty is None: ret_ty = mlirlite.Void(pool) # Detect ExternT/State/ExportT has_extern: int = 0 @@ -748,8 +749,12 @@ novirtual class FuncGenDef(CallGenBase.CallGenBase): return 0 self.Gen.TypeGenErr.fatal_error(fd, "function has no return type annotation but returns a non-None value", "TypeError") return 0 - 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) + # 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) # 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 @@ -1406,15 +1411,10 @@ novirtual class FuncGenDef(CallGenBase.CallGenBase): # 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 no-value procedure function, return a zero default - if ret_ty is not None: - zero_val: mlirlite.Value* = mlirlite.ConstZero(pool, ret_ty) - if zero_val is not None: - # M3:隐式收尾出口,先释放 slife 再返回 - self.Gen.VarGen._SlifeEmitFrees(pool, func_builder, mod, trans) - mlirlite.build_ret(func_builder, zero_val) - else: 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") + # No return annotation -> a procedure: the epilogue must never carry a value + # (`llvm.return ` 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). diff --git a/App/lib/core/Gen/GenApi.vp b/App/lib/core/Gen/GenApi.vp index 1084f0b..71fdb0e 100644 --- a/App/lib/core/Gen/GenApi.vp +++ b/App/lib/core/Gen/GenApi.vp @@ -210,6 +210,12 @@ def register_cdefine_double(pool: memhub.MemBuddy*, name: str, value: double, is if _g_gen is not None: _g_gen.TypeGenCDef.register_cdefine_double(pool, name, value, is_f32, module) +# ---------------- register_cdefine_string (delegates to TypeGenCDef.register_cdefine_string) ---------------- +def register_cdefine_string(pool: memhub.MemBuddy*, name: str, value: str, module: str = None): + """Delegate to gen.register_cdefine_string (a string DefineT, or the "None" sentinel for `define X = None`)""" + if _g_gen is not None: _g_gen.TypeGenCDef.register_cdefine_string(pool, name, value, module) + + # ---------------- cdefine_decl_float_bits (delegates to AssignGenMixin.cdefine_decl_float_bits) ---------------- def cdefine_decl_float_bits(pool: memhub.MemBuddy*, annot: ast.AST*) -> int: """define 声明的**浮点**宽度裁决:注解显式写了 float → 1(f32),double/未写 → 0。 diff --git a/App/lib/core/Gen/Stmt/StmtGenCifMixin.vp b/App/lib/core/Gen/Stmt/StmtGenCifMixin.vp index 9029c1e..bb501a4 100644 --- a/App/lib/core/Gen/Stmt/StmtGenCifMixin.vp +++ b/App/lib/core/Gen/Stmt/StmtGenCifMixin.vp @@ -586,17 +586,34 @@ novirtual class StmtGenCifMixin: cur_func_rt: mlirlite.Function* = trans._cur_func ret_ty_rt: mlirlite.LLVMType* = None if cur_func_rt is not None and val is not None and val.Ty is not None: ret_ty_rt = mlirlite.function_get_ret_ty(cur_func_rt) - # void function with `return `: drop the value and emit ret void (valid Python); - # returning the value would emit ret i32 and conflict with define void. - if ret_ty_rt is not None and val is not None: - rv_void: int = 0 + # A procedure (`def f():`, no -> annotation) or a void-typed function never returns a value: + # `return ` evaluates the expression for its side effects and emits `ret void`. + # Emitting the value would produce `llvm.return %v : i32` inside a void function, which the + # MLIR verifier rejects ("expected no operands"). Legal forms: bare `return`, + # `return f()` where f is void, and `return `. + proc_ret: int = 0 + if ret_ty_rt is None: + if trans.CurFuncRetAnnot is None: proc_ret = 1 + else: match ret_ty_rt: case mlirlite.LLVMType.Void(): - rv_void = 1 - if rv_void != 0: - self.Gen.VarGen._SlifeEmitFrees(pool, builder, mod, trans) - mlirlite.build_ret_void(builder) - return 0 + proc_ret = 1 + case _: + pass + if proc_ret == 0 and trans.CurFuncRetAnnot is not None: + # `def f() -> None:` is a procedure too. + if trans.CurFuncRetAnnot.kind() == ast.ASTKind.Constant: + cn_rt: ast.Constant* = ast.Constant*(trans.CurFuncRetAnnot) + if cn_rt.const_kind == ast.CONST_NONE: proc_ret = 1 + if proc_ret != 0: + self.Gen.VarGen._SlifeEmitFrees(pool, builder, mod, trans) + mlirlite.build_ret_void(builder) + return 0 + if ret_ty_rt is None: + # fail-fast: a value return with an unresolvable function return type must not emit a + # guessed type (no i32 fallback). + self.Gen.TypeGenErr.fatal_error(rt, "cannot determine the function return type", "TypeError") + return 0 if ret_ty_rt is not None: # str/bytes is no generic-pointer wildcard: a bare str/bytes return annotation with a # pointer-to-aggregate value → fail-fast, before coerce_to_type silently bitcasts ptr→ptr. diff --git a/App/lib/core/Gen/Type/TypeGenErr.vp b/App/lib/core/Gen/Type/TypeGenErr.vp index ecb613e..c352298 100644 --- a/App/lib/core/Gen/Type/TypeGenErr.vp +++ b/App/lib/core/Gen/Type/TypeGenErr.vp @@ -257,6 +257,17 @@ novirtual class TypeGenErr(TypeGenBase.TypeGenBase): if rt.value.kind() == ast.ASTKind.Constant: cn: ast.Constant* = ast.Constant*(rt.value) if cn.const_kind == ast.CONST_NONE: continue + # `return f()`: a procedure may return another procedure's result; a callee that + # does return a value has it dropped by translate_return (a void function never + # emits a value return), so this is not a "value return" shape. + if rt.value.kind() == ast.ASTKind.Call: continue + # `return ` where the define is the None sentinel (`define X = None`) + if rt.value.kind() == ast.ASTKind.Name: + nm_rv: ast.Name* = ast.Name*(rt.value) + if nm_rv.id is not None: + sv_rv: str = self.Gen.TypeGenCDef.lookup_cdefine_string(nm_rv.id) + if self.Gen.TypeGenCDef.is_cdefine_str() != 0 and sv_rv is not None: + if string.strcmp(sv_rv, "None") == 0: continue return 1 elif k == ast.ASTKind.FunctionDef or k == ast.ASTKind.ClassDef: continue elif k == ast.ASTKind.If or k == ast.ASTKind.While or k == ast.ASTKind.For or k == ast.ASTKind.Try or k == ast.ASTKind.With or k == ast.ASTKind.Match: