From d4b685b229a6d7072129e4d5f64fcb33ac7c2400 Mon Sep 17 00:00:00 2001 From: Dima Granetchi Date: Fri, 28 Aug 2026 11:27:36 +0300 Subject: [PATCH] infer: don't trust a transiently void call when accepting 'return void_expr' PR #3861 accepts 'return void_expr' and resolves a bare auto result to void through the regular generic-infer machinery. That commit is irreversible, and the subexpression's void type is not always final: a [generic] instance still pending [constant_expression] folding is an empty void function for a pass or two (its body sits under 'static_if !typeinfo is_argument(...)'), and the transform then swaps the call for a clone that briefly carries the stale void type with no function bound. Locking the caller's auto result to void off that state, or lowering the return to { expr; return; }, poisons the function for good; every pre-#3861 compiler simply erred on that pass and retried. Add InferTypes::isVoidReturnValueSettled: a void-typed ExprCallFunc only counts once its callee settled (builtin, fully inferred, or from an already compiled module; a call with no function bound never does). Until then the return defers with not-resolved-yet, same as any other unresolved subexpression. Non-call void subexpressions (block invokes, etc.) are unaffected. Adds a regression test: a bare-auto function returning calls to a [generic, constant_expression] function must infer the folded instance's type, not void. --- include/daScript/ast/ast_infer_type.h | 1 + src/ast/ast_infer_type.cpp | 3 +- src/ast/ast_infer_type_helper.cpp | 20 ++++++++++ tests/language/return_void_expression.das | 47 +++++++++++++++++++++++ 4 files changed, 70 insertions(+), 1 deletion(-) diff --git a/include/daScript/ast/ast_infer_type.h b/include/daScript/ast/ast_infer_type.h index 0b8f4f656c..be795a8da8 100644 --- a/include/daScript/ast/ast_infer_type.h +++ b/include/daScript/ast/ast_infer_type.h @@ -534,6 +534,7 @@ namespace das { ExpressionPtr visit(ExprTryCatch *expr) override; // ExprReturn bool inferReturnType(TypeDeclPtr &resType, ExprReturn *expr); + bool isVoidReturnValueSettled(Expression *subexpr) const; void reportUnresolvedReturnValue(ExprReturn *expr); virtual void preVisit(ExprReturn *expr) override; void getDetailsAndSuggests(ExprReturn *expr, string &details, string &suggestions) const; diff --git a/src/ast/ast_infer_type.cpp b/src/ast/ast_infer_type.cpp index b66a602d75..e9eaefbd59 100644 --- a/src/ast/ast_infer_type.cpp +++ b/src/ast/ast_infer_type.cpp @@ -4717,7 +4717,8 @@ namespace das { TypeDecl::clone(expr->returnType, func->result); } } - if (expr->subexpr && expr->subexpr->type && expr->subexpr->type->isVoid() && !expr->moveSemantics) { + if (expr->subexpr && expr->subexpr->type && expr->subexpr->type->isVoid() && !expr->moveSemantics + && isVoidReturnValueSettled(expr->subexpr)) { const auto & resT = blocks.size() ? blocks.back()->type : func->result; if (resT && resT->isVoid()) { // lower 'return void_expr' to { void_expr; return; } so the backends diff --git a/src/ast/ast_infer_type_helper.cpp b/src/ast/ast_infer_type_helper.cpp index 09ec3ce206..a3a00df3c7 100644 --- a/src/ast/ast_infer_type_helper.cpp +++ b/src/ast/ast_infer_type_helper.cpp @@ -1050,6 +1050,22 @@ namespace das { expr->at, CompilationError::not_resolved_yet_expression_type); } } + bool InferTypes::isVoidReturnValueSettled(Expression *subexpr) const { + if (!subexpr->rtti_isCallFunc()) { + return true; + } + auto callee = ((ExprCallFunc *)subexpr)->func; + if (!callee) { + return false; + } + if (callee->builtIn || callee->isFullyInferred) { + return true; + } + if (callee->module && callee->module != program->thisModule.get()) { + return true; + } + return callee->hasReturn; + } bool InferTypes::inferReturnType(TypeDeclPtr &resType, ExprReturn *expr) { if (expr->subexpr && expr->subexpr->type && expr->subexpr->type->isVoid()) { // 'return void_expr' is legal when the result is void, or a bare auto which the @@ -1061,6 +1077,10 @@ namespace das { expr->at, CompilationError::invalid_result); return false; } + if (!isVoidReturnValueSettled(expr->subexpr)) { + error("subexpression type is not fully resolved yet", "", "", expr->at, CompilationError::not_resolved_yet_expression_type); + return false; + } if (resType->isVoid()) { return false; } diff --git a/tests/language/return_void_expression.das b/tests/language/return_void_expression.das index c0d078c455..02a013254c 100644 --- a/tests/language/return_void_expression.das +++ b/tests/language/return_void_expression.das @@ -1,5 +1,6 @@ options gen2 require dastest/testing_boost public +require daslib/constant_expression var g_hits = 0 @@ -49,6 +50,41 @@ def mixed_result(n : int) : int { return g_hits + n } +// until [constant_expression] folds the argument in, the raw instance is an empty void function +[generic, constant_expression(name), unused_argument(name)] +def get_by_name(name : string) { + static_if (!typeinfo is_argument(name)) { + static_if (name == "one") { + return 1 + } static_elif (name == "two") { + return 2 + } + } +} + +def pick_by_name(sel : int) { + if (sel == 1) { + return get_by_name("one") + } + return get_by_name("two") +} + +def ping(n : int) : void { + if (n <= 0) { + return + } + g_hits++ + return pong(n - 1) +} + +def pong(n : int) : void { + if (n <= 0) { + return + } + g_hits++ + return ping(n - 1) +} + [test] def test_return_void_expression(t : T?) { t |> run("block arrow with void body") @(t : T?) { @@ -133,4 +169,15 @@ def test_return_void_forwarding(t : T?) { g_hits = 0 t |> equal(mixed_result(10), 13) } + + t |> run("transiently-void generic call does not poison a bare-auto result") @(t : T?) { + t |> equal(pick_by_name(1), 1) + t |> equal(pick_by_name(2), 2) + } + + t |> run("mutually recursive void functions forward through return") @(t : T?) { + g_hits = 0 + ping(5) + t |> equal(g_hits, 5) + } }