Add support for -> auto deduced return types. (#7931)

The return type is deduced from the function's `return` statements
(currently required to all have the same type), or from its first
`returned var`. Calls to generic functions with deduced return types are
not yet supported.

When a function has a deduced return type, we defer the return type
portion of the callee pattern match until deduction, and during
deduction we extend the decl block and the pattern block with the
return-related instructions.

Conversions for `return expr;` statements are deferred until the return
type has been deduced. The design requires that we take the common type
of the returned types as the deduced return type, so we can't in general
perform any conversions for returned expressions until we reach the end
of the function. This is accomplished by inserting a `splice_block`
where the conversions will go, and later updating that instruction to
contain the conversion.

Assisted-by: Claude Opus 5.5 via Antigravity
This commit is contained in:
Richard Smith authored and GitHub committed 2026-10-15 00:30:09 +00:00
1 parent 1c69fe3447
commit bf5d2a2ace
41 files changed
+2631 -214

No files matched your search

+137 -51
View File
@@ -23,6 +23,7 @@
#include "toolchain/check/type.h"
#include "toolchain/check/type_completion.h"
#include "toolchain/check/unused.h"
#include "toolchain/diagnostics/format_providers.h"
#include "toolchain/lex/token_kind.h"
#include "toolchain/parse/node_ids.h"
#include "toolchain/sem_ir/builtin_function_kind.h"
@@ -50,24 +51,30 @@ auto HandleParseNode(Context& context, Parse::FunctionIntroducerId node_id)
return true;
}
// Handles a `->` or `->?` return declaration.
static auto HandleReturnDecl(Context& context, Parse::AnyReturnDeclId node_id)
-> bool {
// Handles a `->` or `->?` return specifier.
static auto HandleReturnSpecifier(Context& context,
Parse::AnyReturnSpecifierId node_id) -> bool {
auto [expr_node_id, expr_inst_id] = context.node_stack().PopExprWithNodeId();
Context::FormExpr form_expr = [&] {
// `-> T` provides a type expression, whereas `-> val T`, `-> ref T`,
// `-> var T`, and `->? F` provide a form.
auto expr_kind = context.parse_tree().node_kind(expr_node_id);
bool is_primitive_form = expr_kind == Parse::ValPrimitiveFormId::Kind ||
expr_kind == Parse::RefPrimitiveFormId::Kind ||
expr_kind == Parse::VarPrimitiveFormId::Kind;
if (context.parse_tree().node_kind(node_id) == Parse::ReturnTypeId::Kind &&
!is_primitive_form) {
return ReturnExprAsForm(context, expr_node_id, expr_inst_id);
} else {
return FormExprAsForm(context, expr_node_id, expr_inst_id);
}
}();
// `-> T` provides a type expression, whereas `-> val T`, `-> ref T`,
// `-> var T`, and `->? F` provide a form.
auto expr_kind = context.parse_tree().node_kind(expr_node_id);
bool is_primitive_form = expr_kind == Parse::ValPrimitiveFormId::Kind ||
expr_kind == Parse::RefPrimitiveFormId::Kind ||
expr_kind == Parse::VarPrimitiveFormId::Kind;
bool is_return_type =
context.parse_tree().node_kind(node_id) == Parse::ReturnTypeId::Kind &&
!is_primitive_form;
if (is_return_type && context.parse_tree().node_kind(expr_node_id) ==
Parse::NodeKind::AutoTypeLiteral) {
// `-> auto`: the return type is deduced from the function body, so there is
// no return form or pattern yet. Push a marker in place of the pattern; see
// `PopReturnSpecifier`.
context.node_stack().Push(node_id, SemIR::AutoType::TypeInstId);
return true;
}
Context::FormExpr form_expr =
is_return_type ? ReturnExprAsForm(context, expr_node_id, expr_inst_id)
: FormExprAsForm(context, expr_node_id, expr_inst_id);
context.PushReturnForm(form_expr);
context.node_stack().Push(node_id,
AddReturnPattern(context, node_id, form_expr));
@@ -75,11 +82,11 @@ static auto HandleReturnDecl(Context& context, Parse::AnyReturnDeclId node_id)
}
auto HandleParseNode(Context& context, Parse::ReturnTypeId node_id) -> bool {
return HandleReturnDecl(context, node_id);
return HandleReturnSpecifier(context, node_id);
}
auto HandleParseNode(Context& context, Parse::ReturnFormId node_id) -> bool {
return HandleReturnDecl(context, node_id);
return HandleReturnSpecifier(context, node_id);
}
// Diagnoses issues with the modifiers, removing modifiers that shouldn't be
@@ -367,6 +374,67 @@ static auto RequestVtableIfVirtual(
context.vtable_stack().AddInstId(decl_id);
}
// Diagnoses if a deduced return type is not permitted for the given function
// declaration, and returns whether it is permitted. If not, clears any virtual
// modifier, because a virtual function needs a known return type.
static auto CheckDeducedReturnTypeAllowed(
Context& context, Parse::AnyFunctionDeclId node_id,
Parse::NodeId return_node_id, bool is_definition, bool is_extern,
SemIR::Function::VirtualModifier& virtual_modifier,
const std::optional<SemIR::Inst>& parent_scope_inst) -> bool {
enum class Reason : int8_t { Builtin, Extern, InterfaceMember, Virtual };
std::optional<Reason> reason;
if (context.parse_tree().node_kind(node_id) ==
Parse::NodeKind::BuiltinFunctionDefinitionStart) {
// A builtin function has no body to deduce from.
reason = Reason::Builtin;
} else if (is_extern) {
// An `extern` function is declared elsewhere.
reason = Reason::Extern;
} else if (parent_scope_inst &&
parent_scope_inst->Is<SemIR::InterfaceWithSelfDecl>()) {
// An associated function's return type is part of the interface's contract
// with its impls, so shouldn't depend on a `default` definition.
// TODO: A `final` function can't be overridden, so deducing its return
// type from its definition would be reasonable. Consider allowing that
// once calls to generic functions with deduced return types are supported.
reason = Reason::InterfaceMember;
} else if (virtual_modifier != SemIR::Function::VirtualModifier::None) {
// The vtable is built before the function body is checked.
reason = Reason::Virtual;
virtual_modifier = SemIR::Function::VirtualModifier::None;
}
if (reason) {
CARBON_DIAGNOSTIC(DeducedReturnTypeNotAllowed, Error,
"{0:=0:builtin function|=1:`extern` function|=2:"
"interface member|=3:virtual function} with deduced "
"return type",
Diagnostics::IntAsSelect);
context.emitter().Emit(return_node_id, DeducedReturnTypeNotAllowed,
static_cast<int>(*reason));
return false;
}
// The return type is deduced from the function body, so only a definition
// can declare one. As a consequence, the function can't be redeclared.
if (!is_definition) {
CARBON_DIAGNOSTIC(DeducedReturnTypeNotDefinition, Error,
"forward declaration of function with deduced return "
"type");
context.emitter().Emit(return_node_id, DeducedReturnTypeNotDefinition);
return false;
}
if (parent_scope_inst && parent_scope_inst->Is<SemIR::ImplDecl>()) {
// TODO: The impl witness and any thunks are built before the function body
// is checked.
context.TODO(return_node_id, "deduced return type for `impl` member");
return false;
}
return true;
}
// Build a FunctionDecl describing the signature of a function. This
// handles the common logic shared by function declaration syntax and function
// definition syntax.
@@ -374,20 +442,10 @@ static auto BuildFunctionDecl(Context& context,
Parse::AnyFunctionDeclId node_id,
bool is_definition)
-> std::pair<SemIR::FunctionId, SemIR::InstId> {
auto return_pattern_id = SemIR::InstId::None;
auto return_type_inst_id = SemIR::TypeInstId::None;
auto return_form_inst_id = SemIR::InstId::None;
if (auto [return_node, maybe_return_pattern_id] =
context.node_stack()
.PopWithNodeIdIf<Parse::NodeCategory::ReturnDecl>();
maybe_return_pattern_id) {
return_pattern_id = *maybe_return_pattern_id;
auto return_form = context.PopReturnForm();
return_type_inst_id = return_form.type_component_inst_id;
return_form_inst_id = return_form.form_inst_id;
}
auto return_specifier = PopReturnSpecifier(context);
auto name = PopNameComponent(context, return_pattern_id);
auto name = PopNameComponent(context, return_specifier.pattern_id,
return_specifier.has_return_specifier());
auto name_context = context.decl_name_stack().FinishName(name);
context.node_stack()
@@ -422,6 +480,14 @@ static auto BuildFunctionDecl(Context& context,
auto evaluation_mode = GetEvaluationMode(introducer.modifier_set);
auto interface_modifier = GetInterfaceModifier(introducer.modifier_set);
// If the return type is deduced in a context where that's not allowed, we
// will treat the function as having an invalid return type.
bool is_invalid_deduced_return =
return_specifier.is_deduced &&
!CheckDeducedReturnTypeAllowed(context, node_id, return_specifier.node_id,
is_definition, is_extern, virtual_modifier,
parent_scope_inst);
// Add the function declaration.
SemIR::FunctionDecl function_decl = {SemIR::TypeId::None,
SemIR::FunctionId::None,
@@ -430,21 +496,22 @@ static auto BuildFunctionDecl(Context& context,
// Build the function entity. This will be merged into an existing function if
// there is one, or otherwise added to the function store.
auto function_info =
SemIR::Function{name_context.MakeEntityWithParamsBase(
name, decl_id, is_extern, introducer.extern_library),
{
.call_param_patterns_id = name.call_param_patterns_id,
.call_params_id = name.call_params_id,
.call_param_ranges = name.param_ranges,
.return_type_inst_id = return_type_inst_id,
.return_form_inst_id = return_form_inst_id,
.return_pattern_id = return_pattern_id,
.virtual_modifier = virtual_modifier,
.evaluation_mode = evaluation_mode,
.interface_modifier = interface_modifier,
.self_param_id = self_param_id,
}};
auto function_info = SemIR::Function{
name_context.MakeEntityWithParamsBase(name, decl_id, is_extern,
introducer.extern_library),
{
.call_param_patterns_id = name.call_param_patterns_id,
.call_params_id = name.call_params_id,
.call_param_ranges = name.param_ranges,
.return_type_inst_id = return_specifier.form.type_component_inst_id,
.return_form_inst_id = return_specifier.form.form_inst_id,
.return_pattern_id = return_specifier.pattern_id,
.virtual_modifier = virtual_modifier,
.evaluation_mode = evaluation_mode,
.interface_modifier = interface_modifier,
.has_deduced_return_type = return_specifier.is_deduced,
.self_param_id = self_param_id,
}};
if (is_definition) {
function_info.definition_id = decl_id;
}
@@ -480,6 +547,13 @@ static auto BuildFunctionDecl(Context& context,
// Write the function ID into the FunctionDecl.
ReplaceInstBeforeConstantUse(context, decl_id, function_decl);
// If we diagnosed the deduced return type, recover by giving the function an
// invalid return type.
if (is_invalid_deduced_return) {
SetDeducedReturnType(context, function_decl.function_id,
return_specifier.node_id, SemIR::ErrorInst::TypeId);
}
// Diagnose 'definition of `abstract` function' using the canonical Function's
// modifiers.
if (is_definition &&
@@ -495,8 +569,12 @@ static auto BuildFunctionDecl(Context& context,
MaybeAddToNameLookup(context, name_context, introducer.modifier_set,
name_context.parent_scope_id, decl_id);
ValidateForEntryPoint(context, node_id, function_decl.function_id,
function_info);
// If the return type is deduced, we validate the entry point at the end of
// the definition, once the return type is known.
if (!return_specifier.is_deduced) {
ValidateForEntryPoint(context, node_id, function_decl.function_id,
function_info);
}
if (!is_definition && context.sem_ir().is_impl() && !is_extern) {
context.definitions_required_by_decl().push_back(decl_id);
@@ -621,11 +699,19 @@ auto HandleParseNode(Context& context, Parse::FunctionDefinitionStartId node_id)
auto HandleParseNode(Context& context, Parse::FunctionDefinitionId node_id)
-> bool {
SemIR::FunctionId function_id =
context.node_stack().Pop<Parse::NodeKind::FunctionDefinitionStart>();
auto [start_node_id, function_id] =
context.node_stack()
.PopWithNodeId<Parse::NodeKind::FunctionDefinitionStart>();
CheckFunctionReturnOnFinish(context, node_id, function_id);
FinishFunctionDefinition(context, function_id);
// Now that the return type has been deduced, we can validate the entry point.
const auto& function = context.functions().Get(function_id);
if (function.has_deduced_return_type) {
ValidateForEntryPoint(context, start_node_id, function_id, function);
}
context.decl_name_stack().PopScope(/*check_unused=*/true);
return true;