Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 9 additions & 8 deletions toolchain/check/cpp/import.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -1738,14 +1738,15 @@ static auto ImportFunctionDecl(Context& context, SemIR::LocId loc_id,
builder.Note(loc_id, InCppThunk);
});

clang::FunctionDecl* thunk_clang_decl =
BuildCppThunk(context, function_info);
if (thunk_clang_decl) {
SemIR::FunctionId thunk_function_id = *ImportFunction(
context, loc_id, thunk_clang_decl, thunk_clang_decl->getNumParams());
SemIR::InstId thunk_function_decl_id =
context.functions().Get(thunk_function_id).first_owning_decl_id;
function_info.SetHasCppThunk(thunk_function_decl_id);
if (clang::FunctionDecl* thunk_clang_decl =
BuildCppThunk(context, function_info)) {
if (auto thunk_function_id =
ImportFunction(context, loc_id, thunk_clang_decl,
thunk_clang_decl->getNumParams())) {
SemIR::InstId thunk_function_decl_id =
context.functions().Get(*thunk_function_id).first_owning_decl_id;
function_info.SetHasCppThunk(thunk_function_decl_id);
}
}
}

Expand Down
158 changes: 64 additions & 94 deletions toolchain/check/cpp/thunk.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -50,106 +50,30 @@ static auto GenerateThunkMangledName(
return mangled_name_stream.TakeStr();
}

// Returns true if a C++ thunk is required for the given type. A C++ thunk is
// required for any type except for void, pointer types and signed 32-bit and
// 64-bit integers.
static auto IsThunkRequiredForType(Context& context, SemIR::TypeId type_id)
-> bool {
if (!type_id.has_value() || type_id == SemIR::ErrorInst::TypeId) {
return false;
}

type_id = context.types().GetUnqualifiedType(type_id);

switch (context.types().GetAsInst(type_id).kind()) {
case SemIR::PointerType::Kind: {
return false;
}

case SemIR::ClassType::Kind: {
if (!context.types().IsComplete(type_id)) {
// Signed integers of 32 or 64 bits should be completed when imported.
return true;
}

auto int_info = context.types().TryGetIntTypeInfo(type_id);
if (!int_info || !int_info->bit_width.has_value()) {
return true;
}

llvm::APInt bit_width = context.ints().Get(int_info->bit_width);
return bit_width != 32 && bit_width != 64;
}

default:
return true;
}
}

auto IsCppThunkRequired(Context& context, const SemIR::Function& function)
-> bool {
if (!function.clang_decl_id.has_value()) {
return false;
}

// A thunk is required if any parameter or return type requires it. However,
// we don't generate a thunk if any relevant type is erroneous.
bool thunk_required = false;

const auto& decl_info = context.clang_decls().Get(function.clang_decl_id);
const auto* decl = cast<clang::FunctionDecl>(decl_info.key.decl);
if (decl_info.key.num_params !=
static_cast<int>(decl->getNumNonObjectParams())) {
// We require a thunk if the number of parameters we want isn't all of them.
// This happens if default arguments are in use, or (eventually) when
// calling a varargs function.
thunk_required = true;
} else {
// We require a thunk if any parameter is of reference type, even if the
// corresponding SemIR function has an acceptable parameter type.
// TODO: We should be able to avoid thunks for reference parameters.
for (auto* param : decl->parameters()) {
if (param->getType()->isReferenceType()) {
thunk_required = true;
break;
}
}
// Returns whether the Carbon lowering for a parameter or return of this type is
// known to match the C++ lowering.
static auto IsSimpleAbiType(clang::ASTContext& ast_context,
clang::QualType type, bool for_parameter) -> bool {
if (type->isVoidType() || type->isPointerType()) {
return true;
}

SemIR::TypeId return_type_id =
function.GetDeclaredReturnType(context.sem_ir());
if (return_type_id.has_value()) {
if (return_type_id == SemIR::ErrorInst::TypeId) {
return false;
}
thunk_required =
thunk_required || IsThunkRequiredForType(context, return_type_id);
if (!for_parameter && type->isLValueReferenceType()) {
// An lvalue reference return type maps to a pointer, which uses the same
// lowering rule.
return true;
}

for (auto param_id :
context.inst_blocks().GetOrEmpty(function.call_params_id)) {
if (param_id == SemIR::ErrorInst::InstId) {
if (const auto* enum_decl = type->getAsEnumDecl()) {
// An enum type has a simple ABI if its underlying type does.
type = enum_decl->getIntegerType();
if (type.isNull()) {
return false;
}
thunk_required =
thunk_required ||
IsThunkRequiredForType(
context, context.insts().GetAs<SemIR::AnyParam>(param_id).type_id);
}

return thunk_required;
}

// Returns whether the type is void, a pointer, or a signed int of 32 or 64
// bits.
static auto IsSimpleAbiType(clang::ASTContext& ast_context,
clang::QualType type) -> bool {
if (type->isVoidType() || type->isPointerType()) {
return true;
}

if (const auto* builtin_type = type->getAs<clang::BuiltinType>()) {
if (builtin_type->isSignedInteger()) {
if (builtin_type->isIntegerType()) {
uint64_t type_size = ast_context.getIntWidth(type);
return type_size == 32 || type_size == 64;
}
Expand All @@ -174,8 +98,8 @@ struct CalleeFunctionInfo {
effective_return_type =
is_ctor ? ast_context.getCanonicalTagType(method_decl->getParent())
: decl->getReturnType();
has_simple_return_type =
IsSimpleAbiType(ast_context, effective_return_type);
has_simple_return_type = IsSimpleAbiType(ast_context, effective_return_type,
/*for_parameter=*/false);
}

// Returns whether this callee has an implicit `this` parameter.
Expand Down Expand Up @@ -234,6 +158,52 @@ struct CalleeFunctionInfo {
};
} // namespace

auto IsCppThunkRequired(Context& context, const SemIR::Function& function)
-> bool {
if (!function.clang_decl_id.has_value()) {
return false;
}

const auto& decl_info = context.clang_decls().Get(function.clang_decl_id);
auto* decl = cast<clang::FunctionDecl>(decl_info.key.decl);
if (decl_info.key.num_params !=
static_cast<int>(decl->getNumNonObjectParams())) {
// We require a thunk if the number of parameters we want isn't all of them.
// This happens if default arguments are in use, or (eventually) when
// calling a varargs function.
return true;
}

CalleeFunctionInfo callee_info(decl, decl_info.key.num_params);
if (!callee_info.has_simple_return_type) {
return true;
}

auto& ast_context = context.ast_context();
if (callee_info.has_implicit_object_parameter()) {
// TODO: The object parameter is a reference parameter, but we don't force a
// thunk here like we do for explicit reference parameters in the case where
// we would map the parameter to an `addr` parameter. We should make this
// behavior consistent.
auto* method_decl = cast<clang::CXXMethodDecl>(decl);
if (method_decl->getRefQualifier() == clang::RQ_RValue ||
method_decl->getMethodQualifiers().hasConst()) {
return true;
}
}

const auto* function_type =
decl->getType()->castAs<clang::FunctionProtoType>();
for (int i : llvm::seq(decl->getNumParams())) {
if (!IsSimpleAbiType(ast_context, function_type->getParamType(i),
/*for_parameter=*/true)) {
return true;
}
}

return false;
}

// Given a pointer type, returns the corresponding _Nonnull-qualified pointer
// type.
static auto GetNonnullType(clang::ASTContext& ast_context,
Expand All @@ -255,7 +225,7 @@ static auto GetNonNullablePointerType(clang::ASTContext& ast_context,
static auto GetThunkParameterType(clang::ASTContext& ast_context,
clang::QualType callee_type)
-> clang::QualType {
if (IsSimpleAbiType(ast_context, callee_type)) {
if (IsSimpleAbiType(ast_context, callee_type, /*for_parameter=*/true)) {
return callee_type;
}
return GetNonNullablePointerType(ast_context, callee_type);
Expand Down
50 changes: 13 additions & 37 deletions toolchain/check/testdata/interop/cpp/enum/anonymous.carbon
Original file line number Diff line number Diff line change
Expand Up @@ -47,9 +47,8 @@ fn G() {
// CHECK:STDOUT: %F.cpp_overload_set.value: %F.cpp_overload_set.type = cpp_overload_set_value @F.cpp_overload_set [concrete]
// CHECK:STDOUT: %.4f0: type = class_type @.1 [concrete]
// CHECK:STDOUT: %int_1.81a: %.4f0 = int_value 1 [concrete]
// CHECK:STDOUT: %ptr.793: type = ptr_type %.4f0 [concrete]
// CHECK:STDOUT: %F__carbon_thunk.type.eda1ac.1: type = fn_type @F__carbon_thunk.1 [concrete]
// CHECK:STDOUT: %F__carbon_thunk.0cd6a8.1: %F__carbon_thunk.type.eda1ac.1 = struct_value () [concrete]
// CHECK:STDOUT: %F.type: type = fn_type @F [concrete]
// CHECK:STDOUT: %F: %F.type = struct_value () [concrete]
// CHECK:STDOUT: %C: type = class_type @C [concrete]
// CHECK:STDOUT: %C.C.cpp_overload_set.type: type = cpp_overload_set_type @C.C.cpp_overload_set [concrete]
// CHECK:STDOUT: %C.C.cpp_overload_set.value: %C.C.cpp_overload_set.type = cpp_overload_set_value @C.C.cpp_overload_set [concrete]
Expand All @@ -60,19 +59,12 @@ fn G() {
// CHECK:STDOUT: %C.F.cpp_overload_set.value: %C.F.cpp_overload_set.type = cpp_overload_set_value @C.F.cpp_overload_set [concrete]
// CHECK:STDOUT: %.bb7: type = class_type @.2 [concrete]
// CHECK:STDOUT: %int_1.1d6: %.bb7 = int_value 1 [concrete]
// CHECK:STDOUT: %ptr.73d: type = ptr_type %.bb7 [concrete]
// CHECK:STDOUT: %F__carbon_thunk.type.eda1ac.2: type = fn_type @F__carbon_thunk.2 [concrete]
// CHECK:STDOUT: %F__carbon_thunk.0cd6a8.2: %F__carbon_thunk.type.eda1ac.2 = struct_value () [concrete]
// CHECK:STDOUT: %C.F.type: type = fn_type @C.F [concrete]
// CHECK:STDOUT: %C.F: %C.F.type = struct_value () [concrete]
// CHECK:STDOUT: %type_where: type = facet_type <type where .Self impls <CanDestroy>> [concrete]
// CHECK:STDOUT: %facet_value.597: %type_where = facet_value %.bb7, () [concrete]
// CHECK:STDOUT: %DestroyT.binding.as_type.as.Destroy.impl.Op.type.52d: type = fn_type @DestroyT.binding.as_type.as.Destroy.impl.Op, @DestroyT.binding.as_type.as.Destroy.impl(%facet_value.597) [concrete]
// CHECK:STDOUT: %DestroyT.binding.as_type.as.Destroy.impl.Op.afc: %DestroyT.binding.as_type.as.Destroy.impl.Op.type.52d = struct_value () [concrete]
// CHECK:STDOUT: %facet_value.b21: %type_where = facet_value %C, () [concrete]
// CHECK:STDOUT: %DestroyT.binding.as_type.as.Destroy.impl.Op.type.b92: type = fn_type @DestroyT.binding.as_type.as.Destroy.impl.Op, @DestroyT.binding.as_type.as.Destroy.impl(%facet_value.b21) [concrete]
// CHECK:STDOUT: %facet_value: %type_where = facet_value %C, () [concrete]
// CHECK:STDOUT: %DestroyT.binding.as_type.as.Destroy.impl.Op.type.b92: type = fn_type @DestroyT.binding.as_type.as.Destroy.impl.Op, @DestroyT.binding.as_type.as.Destroy.impl(%facet_value) [concrete]
// CHECK:STDOUT: %DestroyT.binding.as_type.as.Destroy.impl.Op.841: %DestroyT.binding.as_type.as.Destroy.impl.Op.type.b92 = struct_value () [concrete]
// CHECK:STDOUT: %facet_value.21d: %type_where = facet_value %.4f0, () [concrete]
// CHECK:STDOUT: %DestroyT.binding.as_type.as.Destroy.impl.Op.type.02d: type = fn_type @DestroyT.binding.as_type.as.Destroy.impl.Op, @DestroyT.binding.as_type.as.Destroy.impl(%facet_value.21d) [concrete]
// CHECK:STDOUT: %DestroyT.binding.as_type.as.Destroy.impl.Op.246: %DestroyT.binding.as_type.as.Destroy.impl.Op.type.02d = struct_value () [concrete]
// CHECK:STDOUT: }
// CHECK:STDOUT:
// CHECK:STDOUT: imports {
Expand All @@ -84,7 +76,7 @@ fn G() {
// CHECK:STDOUT: }
// CHECK:STDOUT: %F.cpp_overload_set.value: %F.cpp_overload_set.type = cpp_overload_set_value @F.cpp_overload_set [concrete = constants.%F.cpp_overload_set.value]
// CHECK:STDOUT: %int_1.81a: %.4f0 = int_value 1 [concrete = constants.%int_1.81a]
// CHECK:STDOUT: %F__carbon_thunk.decl.e1b8ec.1: %F__carbon_thunk.type.eda1ac.1 = fn_decl @F__carbon_thunk.1 [concrete = constants.%F__carbon_thunk.0cd6a8.1] {
// CHECK:STDOUT: %F.decl: %F.type = fn_decl @F [concrete = constants.%F] {
// CHECK:STDOUT: <elided>
// CHECK:STDOUT: } {
// CHECK:STDOUT: <elided>
Expand All @@ -98,7 +90,7 @@ fn G() {
// CHECK:STDOUT: }
// CHECK:STDOUT: %C.F.cpp_overload_set.value: %C.F.cpp_overload_set.type = cpp_overload_set_value @C.F.cpp_overload_set [concrete = constants.%C.F.cpp_overload_set.value]
// CHECK:STDOUT: %int_1.1d6: %.bb7 = int_value 1 [concrete = constants.%int_1.1d6]
// CHECK:STDOUT: %F__carbon_thunk.decl.e1b8ec.2: %F__carbon_thunk.type.eda1ac.2 = fn_decl @F__carbon_thunk.2 [concrete = constants.%F__carbon_thunk.0cd6a8.2] {
// CHECK:STDOUT: %C.F.decl: %C.F.type = fn_decl @C.F [concrete = constants.%C.F] {
// CHECK:STDOUT: <elided>
// CHECK:STDOUT: } {
// CHECK:STDOUT: <elided>
Expand All @@ -115,10 +107,7 @@ fn G() {
// CHECK:STDOUT: %F.ref.loc8: %F.cpp_overload_set.type = name_ref F, imports.%F.cpp_overload_set.value [concrete = constants.%F.cpp_overload_set.value]
// CHECK:STDOUT: %Cpp.ref.loc8_9: <namespace> = name_ref Cpp, imports.%Cpp [concrete = imports.%Cpp]
// CHECK:STDOUT: %b.ref: %.4f0 = name_ref b, imports.%int_1.81a [concrete = constants.%int_1.81a]
// CHECK:STDOUT: %.loc8_12.1: ref %.4f0 = temporary_storage
// CHECK:STDOUT: %.loc8_12.2: ref %.4f0 = temporary %.loc8_12.1, %b.ref
// CHECK:STDOUT: %addr.loc8_14: %ptr.793 = addr_of %.loc8_12.2
// CHECK:STDOUT: %F__carbon_thunk.call.loc8: init %empty_tuple.type = call imports.%F__carbon_thunk.decl.e1b8ec.1(%addr.loc8_14)
// CHECK:STDOUT: %F.call: init %empty_tuple.type = call imports.%F.decl(%b.ref)
// CHECK:STDOUT: %Cpp.ref.loc10_3: <namespace> = name_ref Cpp, imports.%Cpp [concrete = imports.%Cpp]
// CHECK:STDOUT: %C.ref.loc10_6: type = name_ref C, imports.%C.decl [concrete = constants.%C]
// CHECK:STDOUT: %C.ref.loc10_8: %C.C.cpp_overload_set.type = name_ref C, imports.%C.C.cpp_overload_set.value [concrete = constants.%C.C.cpp_overload_set.value]
Expand All @@ -133,25 +122,12 @@ fn G() {
// CHECK:STDOUT: %C.ref.loc10_18: type = name_ref C, imports.%C.decl [concrete = constants.%C]
// CHECK:STDOUT: %e.ref: %.bb7 = name_ref e, imports.%int_1.1d6 [concrete = constants.%int_1.1d6]
// CHECK:STDOUT: %addr.loc10_11.2: %ptr.d9e = addr_of %.loc10_11.3
// CHECK:STDOUT: %.loc10_20.1: ref %.bb7 = temporary_storage
// CHECK:STDOUT: %.loc10_20.2: ref %.bb7 = temporary %.loc10_20.1, %e.ref
// CHECK:STDOUT: %addr.loc10_22: %ptr.73d = addr_of %.loc10_20.2
// CHECK:STDOUT: %F__carbon_thunk.call.loc10: init %empty_tuple.type = call imports.%F__carbon_thunk.decl.e1b8ec.2(%addr.loc10_11.2, %addr.loc10_22)
// CHECK:STDOUT: %DestroyT.binding.as_type.as.Destroy.impl.Op.bound.loc10_20: <bound method> = bound_method %.loc10_20.2, constants.%DestroyT.binding.as_type.as.Destroy.impl.Op.afc
// CHECK:STDOUT: %C.F.call: init %empty_tuple.type = call imports.%C.F.decl(%addr.loc10_11.2, %e.ref)
// CHECK:STDOUT: %DestroyT.binding.as_type.as.Destroy.impl.Op.bound: <bound method> = bound_method %.loc10_11.3, constants.%DestroyT.binding.as_type.as.Destroy.impl.Op.841
// CHECK:STDOUT: <elided>
// CHECK:STDOUT: %bound_method.loc10_20: <bound method> = bound_method %.loc10_20.2, %DestroyT.binding.as_type.as.Destroy.impl.Op.specific_fn.1
// CHECK:STDOUT: %addr.loc10_20: %ptr.73d = addr_of %.loc10_20.2
// CHECK:STDOUT: %DestroyT.binding.as_type.as.Destroy.impl.Op.call.loc10_20: init %empty_tuple.type = call %bound_method.loc10_20(%addr.loc10_20)
// CHECK:STDOUT: %DestroyT.binding.as_type.as.Destroy.impl.Op.bound.loc10_11: <bound method> = bound_method %.loc10_11.3, constants.%DestroyT.binding.as_type.as.Destroy.impl.Op.841
// CHECK:STDOUT: <elided>
// CHECK:STDOUT: %bound_method.loc10_11: <bound method> = bound_method %.loc10_11.3, %DestroyT.binding.as_type.as.Destroy.impl.Op.specific_fn.2
// CHECK:STDOUT: %bound_method.loc10_11: <bound method> = bound_method %.loc10_11.3, %DestroyT.binding.as_type.as.Destroy.impl.Op.specific_fn
// CHECK:STDOUT: %addr.loc10_11.3: %ptr.d9e = addr_of %.loc10_11.3
// CHECK:STDOUT: %DestroyT.binding.as_type.as.Destroy.impl.Op.call.loc10_11: init %empty_tuple.type = call %bound_method.loc10_11(%addr.loc10_11.3)
// CHECK:STDOUT: %DestroyT.binding.as_type.as.Destroy.impl.Op.bound.loc8: <bound method> = bound_method %.loc8_12.2, constants.%DestroyT.binding.as_type.as.Destroy.impl.Op.246
// CHECK:STDOUT: <elided>
// CHECK:STDOUT: %bound_method.loc8: <bound method> = bound_method %.loc8_12.2, %DestroyT.binding.as_type.as.Destroy.impl.Op.specific_fn.3
// CHECK:STDOUT: %addr.loc8_12: %ptr.793 = addr_of %.loc8_12.2
// CHECK:STDOUT: %DestroyT.binding.as_type.as.Destroy.impl.Op.call.loc8: init %empty_tuple.type = call %bound_method.loc8(%addr.loc8_12)
// CHECK:STDOUT: %DestroyT.binding.as_type.as.Destroy.impl.Op.call: init %empty_tuple.type = call %bound_method.loc10_11(%addr.loc10_11.3)
// CHECK:STDOUT: return
// CHECK:STDOUT: }
// CHECK:STDOUT:
Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,8 @@ fn F() {
// CHECK:STDOUT: %OptionalStorage.impl_witness.01b: <witness> = impl_witness imports.%OptionalStorage.impl_witness_table.377, @ptr.as.OptionalStorage.impl(%i32) [concrete]
// CHECK:STDOUT: %OptionalStorage.facet: %OptionalStorage.type = facet_value %ptr.235, (%OptionalStorage.impl_witness.01b) [concrete]
// CHECK:STDOUT: %Optional.8fd: type = class_type @Optional, @Optional(%OptionalStorage.facet) [concrete]
// CHECK:STDOUT: %TakesArray__carbon_thunk.type: type = fn_type @TakesArray__carbon_thunk [concrete]
// CHECK:STDOUT: %TakesArray__carbon_thunk: %TakesArray__carbon_thunk.type = struct_value () [concrete]
// CHECK:STDOUT: %TakesArray.type: type = fn_type @TakesArray [concrete]
// CHECK:STDOUT: %TakesArray: %TakesArray.type = struct_value () [concrete]
// CHECK:STDOUT: %ImplicitAs.type.6cc: type = facet_type <@ImplicitAs, @ImplicitAs(%Optional.8fd)> [concrete]
// CHECK:STDOUT: %ImplicitAs.Convert.type.770: type = fn_type @ImplicitAs.Convert, @ImplicitAs(%Optional.8fd) [concrete]
// CHECK:STDOUT: %T.binding.as_type.as.ImplicitAs.impl.Convert.type.6f7: type = fn_type @T.binding.as_type.as.ImplicitAs.impl.Convert.2, @T.binding.as_type.as.ImplicitAs.impl.3a5(%T.76d) [symbolic]
Expand Down Expand Up @@ -153,7 +153,7 @@ fn F() {
// CHECK:STDOUT: %Core.import_ref.6db = import_ref Core//prelude/types/optional, loc{{\d+_\d+}}, unloaded
// CHECK:STDOUT: %Core.import_ref.5a7 = import_ref Core//prelude/types/optional, loc{{\d+_\d+}}, unloaded
// CHECK:STDOUT: %OptionalStorage.impl_witness_table.377 = impl_witness_table (%Core.import_ref.8c0, %Core.import_ref.566, %Core.import_ref.637, %Core.import_ref.6db, %Core.import_ref.5a7), @ptr.as.OptionalStorage.impl [concrete]
// CHECK:STDOUT: %TakesArray__carbon_thunk.decl: %TakesArray__carbon_thunk.type = fn_decl @TakesArray__carbon_thunk [concrete = constants.%TakesArray__carbon_thunk] {
// CHECK:STDOUT: %TakesArray.decl: %TakesArray.type = fn_decl @TakesArray [concrete = constants.%TakesArray] {
// CHECK:STDOUT: <elided>
// CHECK:STDOUT: } {
// CHECK:STDOUT: <elided>
Expand Down Expand Up @@ -207,7 +207,7 @@ fn F() {
// CHECK:STDOUT: %.loc11_18.2: init %Optional.8fd = converted %addr.loc11_18.1, %T.binding.as_type.as.ImplicitAs.impl.Convert.call
// CHECK:STDOUT: %.loc11_18.3: ref %Optional.8fd = temporary %.loc11_18.1, %.loc11_18.2
// CHECK:STDOUT: %.loc11_18.4: %Optional.8fd = bind_value %.loc11_18.3
// CHECK:STDOUT: %TakesArray__carbon_thunk.call: init %empty_tuple.type = call imports.%TakesArray__carbon_thunk.decl(%.loc11_18.4)
// CHECK:STDOUT: %TakesArray.call: init %empty_tuple.type = call imports.%TakesArray.decl(%.loc11_18.4)
// CHECK:STDOUT: %DestroyT.binding.as_type.as.Destroy.impl.Op.bound.loc11: <bound method> = bound_method %.loc11_18.3, constants.%DestroyT.binding.as_type.as.Destroy.impl.Op.af3
// CHECK:STDOUT: <elided>
// CHECK:STDOUT: %bound_method.loc11_18.3: <bound method> = bound_method %.loc11_18.3, %DestroyT.binding.as_type.as.Destroy.impl.Op.specific_fn.1
Expand Down
Loading
Loading