diff --git a/toolchain/check/eval.cpp b/toolchain/check/eval.cpp index 9ce40db86818..4281412d00ff 100644 --- a/toolchain/check/eval.cpp +++ b/toolchain/check/eval.cpp @@ -1379,6 +1379,16 @@ static auto MakeConstantForBuiltinCall(EvalContext& eval_context, case SemIR::BuiltinFunctionKind::None: CARBON_FATAL("Not a builtin function."); + case SemIR::BuiltinFunctionKind::NoOp: { + // Return an empty tuple value. + auto type_id = GetTupleType(eval_context.context(), {}); + return MakeConstantResult( + eval_context.context(), + SemIR::TupleValue{.type_id = type_id, + .elements_id = SemIR::InstBlockId::Empty}, + phase); + } + case SemIR::BuiltinFunctionKind::PrintChar: case SemIR::BuiltinFunctionKind::PrintInt: case SemIR::BuiltinFunctionKind::ReadChar: { diff --git a/toolchain/check/function.cpp b/toolchain/check/function.cpp index 2d6468301d7d..8af6e7ba36c1 100644 --- a/toolchain/check/function.cpp +++ b/toolchain/check/function.cpp @@ -73,7 +73,7 @@ auto CheckFunctionTypeMatches(Context& context, } auto CheckFunctionReturnType(Context& context, SemIR::LocId loc_id, - SemIR::Function& function, + const SemIR::Function& function, SemIR::SpecificId specific_id) -> SemIR::ReturnTypeInfo { auto return_info = SemIR::ReturnTypeInfo::ForFunction(context.sem_ir(), diff --git a/toolchain/check/function.h b/toolchain/check/function.h index fe697675b8bc..c7eb4145fa69 100644 --- a/toolchain/check/function.h +++ b/toolchain/check/function.h @@ -54,7 +54,7 @@ inline auto CheckFunctionTypeMatches(Context& context, // necessary, and returns information about how the function returns its return // value. auto CheckFunctionReturnType(Context& context, SemIR::LocId loc_id, - SemIR::Function& function, + const SemIR::Function& function, SemIR::SpecificId specific_id) -> SemIR::ReturnTypeInfo; diff --git a/toolchain/check/testdata/builtins/no_prelude/no_op.carbon b/toolchain/check/testdata/builtins/no_prelude/no_op.carbon new file mode 100644 index 000000000000..e3c5539f95f6 --- /dev/null +++ b/toolchain/check/testdata/builtins/no_prelude/no_op.carbon @@ -0,0 +1,181 @@ +// Part of the Carbon Language project, under the Apache License v2.0 with LLVM +// Exceptions. See /LICENSE for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +// AUTOUPDATE +// TIP: To test this file alone, run: +// TIP: bazel test //toolchain/testing:file_test --test_arg=--file_tests=toolchain/check/testdata/builtins/no_prelude/no_op.carbon +// TIP: To dump output, run: +// TIP: bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/check/testdata/builtins/no_prelude/no_op.carbon + +// --- no_op.carbon + +library "[[@TEST_NAME]]"; + +fn NoOp() = "no_op"; + +fn F() { + NoOp(); +} + +// --- explicit_return.carbon + +library "[[@TEST_NAME]]"; + +fn NoOp() -> () = "no_op"; + +fn F() { + NoOp(); +} + +// --- assign.carbon + +library "[[@TEST_NAME]]"; + +fn NoOp() = "no_op"; + +fn F() { + var x: () = NoOp(); +} + +// --- fail_signature.carbon + +library "[[@TEST_NAME]]"; + +// CHECK:STDERR: fail_signature.carbon:[[@LINE+4]]:1: error: invalid signature for builtin function "no_op" [InvalidBuiltinSignature] +// CHECK:STDERR: fn NoOp() -> {} = "no_op"; +// CHECK:STDERR: ^~~~~~~~~~~~~~~~~ +// CHECK:STDERR: +fn NoOp() -> {} = "no_op"; + +// CHECK:STDOUT: --- no_op.carbon +// CHECK:STDOUT: +// CHECK:STDOUT: constants { +// CHECK:STDOUT: %NoOp.type: type = fn_type @NoOp [concrete] +// CHECK:STDOUT: %empty_tuple.type: type = tuple_type () [concrete] +// CHECK:STDOUT: %NoOp: %NoOp.type = struct_value () [concrete] +// CHECK:STDOUT: %F.type: type = fn_type @F [concrete] +// CHECK:STDOUT: %F: %F.type = struct_value () [concrete] +// CHECK:STDOUT: %empty_tuple: %empty_tuple.type = tuple_value () [concrete] +// CHECK:STDOUT: } +// CHECK:STDOUT: +// CHECK:STDOUT: file { +// CHECK:STDOUT: package: = namespace [concrete] { +// CHECK:STDOUT: .NoOp = %NoOp.decl +// CHECK:STDOUT: .F = %F.decl +// CHECK:STDOUT: } +// CHECK:STDOUT: %NoOp.decl: %NoOp.type = fn_decl @NoOp [concrete = constants.%NoOp] {} {} +// CHECK:STDOUT: %F.decl: %F.type = fn_decl @F [concrete = constants.%F] {} {} +// CHECK:STDOUT: } +// CHECK:STDOUT: +// CHECK:STDOUT: fn @NoOp() = "no_op"; +// CHECK:STDOUT: +// CHECK:STDOUT: fn @F() { +// CHECK:STDOUT: !entry: +// CHECK:STDOUT: %NoOp.ref: %NoOp.type = name_ref NoOp, file.%NoOp.decl [concrete = constants.%NoOp] +// CHECK:STDOUT: %no_op: init %empty_tuple.type = call %NoOp.ref() [concrete = constants.%empty_tuple] +// CHECK:STDOUT: return +// CHECK:STDOUT: } +// CHECK:STDOUT: +// CHECK:STDOUT: --- explicit_return.carbon +// CHECK:STDOUT: +// CHECK:STDOUT: constants { +// CHECK:STDOUT: %empty_tuple.type: type = tuple_type () [concrete] +// CHECK:STDOUT: %NoOp.type: type = fn_type @NoOp [concrete] +// CHECK:STDOUT: %NoOp: %NoOp.type = struct_value () [concrete] +// CHECK:STDOUT: %F.type: type = fn_type @F [concrete] +// CHECK:STDOUT: %F: %F.type = struct_value () [concrete] +// CHECK:STDOUT: %empty_tuple: %empty_tuple.type = tuple_value () [concrete] +// CHECK:STDOUT: } +// CHECK:STDOUT: +// CHECK:STDOUT: file { +// CHECK:STDOUT: package: = namespace [concrete] { +// CHECK:STDOUT: .NoOp = %NoOp.decl +// CHECK:STDOUT: .F = %F.decl +// CHECK:STDOUT: } +// CHECK:STDOUT: %NoOp.decl: %NoOp.type = fn_decl @NoOp [concrete = constants.%NoOp] { +// CHECK:STDOUT: %return.patt: %empty_tuple.type = return_slot_pattern +// CHECK:STDOUT: %return.param_patt: %empty_tuple.type = out_param_pattern %return.patt, call_param0 +// CHECK:STDOUT: } { +// CHECK:STDOUT: %.loc4_15.1: %empty_tuple.type = tuple_literal () +// CHECK:STDOUT: %.loc4_15.2: type = converted %.loc4_15.1, constants.%empty_tuple.type [concrete = constants.%empty_tuple.type] +// CHECK:STDOUT: %return.param: ref %empty_tuple.type = out_param call_param0 +// CHECK:STDOUT: %return: ref %empty_tuple.type = return_slot %return.param +// CHECK:STDOUT: } +// CHECK:STDOUT: %F.decl: %F.type = fn_decl @F [concrete = constants.%F] {} {} +// CHECK:STDOUT: } +// CHECK:STDOUT: +// CHECK:STDOUT: fn @NoOp() -> %empty_tuple.type = "no_op"; +// CHECK:STDOUT: +// CHECK:STDOUT: fn @F() { +// CHECK:STDOUT: !entry: +// CHECK:STDOUT: %NoOp.ref: %NoOp.type = name_ref NoOp, file.%NoOp.decl [concrete = constants.%NoOp] +// CHECK:STDOUT: %no_op: init %empty_tuple.type = call %NoOp.ref() [concrete = constants.%empty_tuple] +// CHECK:STDOUT: return +// CHECK:STDOUT: } +// CHECK:STDOUT: +// CHECK:STDOUT: --- assign.carbon +// CHECK:STDOUT: +// CHECK:STDOUT: constants { +// CHECK:STDOUT: %NoOp.type: type = fn_type @NoOp [concrete] +// CHECK:STDOUT: %empty_tuple.type: type = tuple_type () [concrete] +// CHECK:STDOUT: %NoOp: %NoOp.type = struct_value () [concrete] +// CHECK:STDOUT: %F.type: type = fn_type @F [concrete] +// CHECK:STDOUT: %F: %F.type = struct_value () [concrete] +// CHECK:STDOUT: %empty_tuple: %empty_tuple.type = tuple_value () [concrete] +// CHECK:STDOUT: } +// CHECK:STDOUT: +// CHECK:STDOUT: file { +// CHECK:STDOUT: package: = namespace [concrete] { +// CHECK:STDOUT: .NoOp = %NoOp.decl +// CHECK:STDOUT: .F = %F.decl +// CHECK:STDOUT: } +// CHECK:STDOUT: %NoOp.decl: %NoOp.type = fn_decl @NoOp [concrete = constants.%NoOp] {} {} +// CHECK:STDOUT: %F.decl: %F.type = fn_decl @F [concrete = constants.%F] {} {} +// CHECK:STDOUT: } +// CHECK:STDOUT: +// CHECK:STDOUT: fn @NoOp() = "no_op"; +// CHECK:STDOUT: +// CHECK:STDOUT: fn @F() { +// CHECK:STDOUT: !entry: +// CHECK:STDOUT: name_binding_decl { +// CHECK:STDOUT: %x.patt: %empty_tuple.type = binding_pattern x +// CHECK:STDOUT: %.loc7_3: %empty_tuple.type = var_pattern %x.patt +// CHECK:STDOUT: } +// CHECK:STDOUT: %x.var: ref %empty_tuple.type = var x +// CHECK:STDOUT: %NoOp.ref: %NoOp.type = name_ref NoOp, file.%NoOp.decl [concrete = constants.%NoOp] +// CHECK:STDOUT: %no_op: init %empty_tuple.type = call %NoOp.ref() [concrete = constants.%empty_tuple] +// CHECK:STDOUT: assign %x.var, %no_op +// CHECK:STDOUT: %.loc7_11.1: type = splice_block %.loc7_11.3 [concrete = constants.%empty_tuple.type] { +// CHECK:STDOUT: %.loc7_11.2: %empty_tuple.type = tuple_literal () +// CHECK:STDOUT: %.loc7_11.3: type = converted %.loc7_11.2, constants.%empty_tuple.type [concrete = constants.%empty_tuple.type] +// CHECK:STDOUT: } +// CHECK:STDOUT: %x: ref %empty_tuple.type = bind_name x, %x.var +// CHECK:STDOUT: return +// CHECK:STDOUT: } +// CHECK:STDOUT: +// CHECK:STDOUT: --- fail_signature.carbon +// CHECK:STDOUT: +// CHECK:STDOUT: constants { +// CHECK:STDOUT: %empty_struct_type: type = struct_type {} [concrete] +// CHECK:STDOUT: %NoOp.type: type = fn_type @NoOp [concrete] +// CHECK:STDOUT: %NoOp: %NoOp.type = struct_value () [concrete] +// CHECK:STDOUT: } +// CHECK:STDOUT: +// CHECK:STDOUT: file { +// CHECK:STDOUT: package: = namespace [concrete] { +// CHECK:STDOUT: .NoOp = %NoOp.decl +// CHECK:STDOUT: } +// CHECK:STDOUT: %NoOp.decl: %NoOp.type = fn_decl @NoOp [concrete = constants.%NoOp] { +// CHECK:STDOUT: %return.patt: %empty_struct_type = return_slot_pattern +// CHECK:STDOUT: %return.param_patt: %empty_struct_type = out_param_pattern %return.patt, call_param0 +// CHECK:STDOUT: } { +// CHECK:STDOUT: %.loc8_15.1: %empty_struct_type = struct_literal () +// CHECK:STDOUT: %.loc8_15.2: type = converted %.loc8_15.1, constants.%empty_struct_type [concrete = constants.%empty_struct_type] +// CHECK:STDOUT: %return.param: ref %empty_struct_type = out_param call_param0 +// CHECK:STDOUT: %return: ref %empty_struct_type = return_slot %return.param +// CHECK:STDOUT: } +// CHECK:STDOUT: } +// CHECK:STDOUT: +// CHECK:STDOUT: fn @NoOp() -> %empty_struct_type; +// CHECK:STDOUT: diff --git a/toolchain/lower/handle_call.cpp b/toolchain/lower/handle_call.cpp index d2a73cde4a2f..92b5c4d6c773 100644 --- a/toolchain/lower/handle_call.cpp +++ b/toolchain/lower/handle_call.cpp @@ -160,6 +160,9 @@ static auto HandleBuiltinCall(FunctionContext& context, SemIR::InstId inst_id, case SemIR::BuiltinFunctionKind::None: CARBON_FATAL("No callee in function call."); + case SemIR::BuiltinFunctionKind::NoOp: + CARBON_FATAL("NoOp is a constant expression and won't reach this."); + case SemIR::BuiltinFunctionKind::PrintChar: { auto* i32_type = llvm::IntegerType::getInt32Ty(context.llvm_context()); llvm::Value* arg_value = context.builder().CreateSExtOrTrunc( diff --git a/toolchain/lower/testdata/builtins/no_prelude/no_op.carbon b/toolchain/lower/testdata/builtins/no_prelude/no_op.carbon new file mode 100644 index 000000000000..43d3db886486 --- /dev/null +++ b/toolchain/lower/testdata/builtins/no_prelude/no_op.carbon @@ -0,0 +1,55 @@ +// Part of the Carbon Language project, under the Apache License v2.0 with LLVM +// Exceptions. See /LICENSE for license information. +// SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception +// +// AUTOUPDATE +// TIP: To test this file alone, run: +// TIP: bazel test //toolchain/testing:file_test --test_arg=--file_tests=toolchain/lower/testdata/builtins/no_prelude/no_op.carbon +// TIP: To dump output, run: +// TIP: bazel run //toolchain/testing:file_test -- --dump_output --file_tests=toolchain/lower/testdata/builtins/no_prelude/no_op.carbon + +fn NoOp() = "no_op"; + +fn F() { + NoOp(); +} + +fn G() -> () { + var a: () = NoOp(); + return a; +} + +// CHECK:STDOUT: ; ModuleID = 'no_op.carbon' +// CHECK:STDOUT: source_filename = "no_op.carbon" +// CHECK:STDOUT: +// CHECK:STDOUT: define void @_CF.Main() !dbg !4 { +// CHECK:STDOUT: entry: +// CHECK:STDOUT: ret void, !dbg !7 +// CHECK:STDOUT: } +// CHECK:STDOUT: +// CHECK:STDOUT: define void @_CG.Main() !dbg !8 { +// CHECK:STDOUT: entry: +// CHECK:STDOUT: %a.var = alloca {}, align 8, !dbg !9 +// CHECK:STDOUT: call void @llvm.lifetime.start.p0(i64 0, ptr %a.var), !dbg !9 +// CHECK:STDOUT: ret void, !dbg !10 +// CHECK:STDOUT: } +// CHECK:STDOUT: +// CHECK:STDOUT: ; Function Attrs: nocallback nofree nosync nounwind willreturn memory(argmem: readwrite) +// CHECK:STDOUT: declare void @llvm.lifetime.start.p0(i64 immarg, ptr captures(none)) #0 +// CHECK:STDOUT: +// CHECK:STDOUT: attributes #0 = { nocallback nofree nosync nounwind willreturn memory(argmem: readwrite) } +// CHECK:STDOUT: +// CHECK:STDOUT: !llvm.module.flags = !{!0, !1} +// CHECK:STDOUT: !llvm.dbg.cu = !{!2} +// CHECK:STDOUT: +// CHECK:STDOUT: !0 = !{i32 7, !"Dwarf Version", i32 5} +// CHECK:STDOUT: !1 = !{i32 2, !"Debug Info Version", i32 3} +// CHECK:STDOUT: !2 = distinct !DICompileUnit(language: DW_LANG_C, file: !3, producer: "carbon", isOptimized: false, runtimeVersion: 0, emissionKind: FullDebug) +// CHECK:STDOUT: !3 = !DIFile(filename: "no_op.carbon", directory: "") +// CHECK:STDOUT: !4 = distinct !DISubprogram(name: "F", linkageName: "_CF.Main", scope: null, file: !3, line: 13, type: !5, spFlags: DISPFlagDefinition, unit: !2) +// CHECK:STDOUT: !5 = !DISubroutineType(types: !6) +// CHECK:STDOUT: !6 = !{} +// CHECK:STDOUT: !7 = !DILocation(line: 13, column: 1, scope: !4) +// CHECK:STDOUT: !8 = distinct !DISubprogram(name: "G", linkageName: "_CG.Main", scope: null, file: !3, line: 17, type: !5, spFlags: DISPFlagDefinition, unit: !2) +// CHECK:STDOUT: !9 = !DILocation(line: 18, column: 3, scope: !8) +// CHECK:STDOUT: !10 = !DILocation(line: 19, column: 3, scope: !8) diff --git a/toolchain/sem_ir/builtin_function_kind.cpp b/toolchain/sem_ir/builtin_function_kind.cpp index f656ac3fc9c1..d40d95d34f65 100644 --- a/toolchain/sem_ir/builtin_function_kind.cpp +++ b/toolchain/sem_ir/builtin_function_kind.cpp @@ -218,6 +218,8 @@ using FloatT = TypeParam<0, AnyFloat>; // Not a builtin function. constexpr BuiltinInfo None = {"", nullptr}; +constexpr BuiltinInfo NoOp = {"no_op", ValidateSignatureNoReturn>}; + // Prints a single character. constexpr BuiltinInfo PrintChar = { "print.char", ValidateSignatureAnySizedInt>}; diff --git a/toolchain/sem_ir/builtin_function_kind.def b/toolchain/sem_ir/builtin_function_kind.def index 235c56f7cc95..420d062b112c 100644 --- a/toolchain/sem_ir/builtin_function_kind.def +++ b/toolchain/sem_ir/builtin_function_kind.def @@ -19,6 +19,9 @@ CARBON_SEM_IR_BUILTIN_FUNCTION_KIND(None) +// A no-op function definition; calls should be elided when lowering. +CARBON_SEM_IR_BUILTIN_FUNCTION_KIND(NoOp) + // Temporary builtins for primitive IO. CARBON_SEM_IR_BUILTIN_FUNCTION_KIND(PrintChar) CARBON_SEM_IR_BUILTIN_FUNCTION_KIND(PrintInt)