Просмотр исходного кода

Add a no-op builtin function which shouldn't generate code. (#5306)

This is part of a broader plan to have noop destructor functions for
trivial destruction.

Note this emits a SemIR call (`%no_op: init %empty_tuple.type = call
%NoOp.ref() [concrete = constants.%empty_tuple]`), but not LLVM IR. My
thought was this was probably okay, since even though it'll be a little
spammy with destructor calls, the flipside is there'll probably already
be a fair amount for the name reference, and this at least shows when
the call is injected (and discarded).

---------

Co-authored-by: Chandler Carruth <chandlerc@gmail.com>
Jon Ross-Perkins 1 год назад
Родитель
Сommit
b49e89e97e

+ 10 - 0
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: {

+ 1 - 1
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(),

+ 1 - 1
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;
 

+ 181 - 0
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> = 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> = 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> = 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> = 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:

+ 3 - 0
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(

+ 55 - 0
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)

+ 2 - 0
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", ValidateSignature<auto()->NoReturn>};
+
 // Prints a single character.
 constexpr BuiltinInfo PrintChar = {
     "print.char", ValidateSignature<auto(AnySizedInt)->AnySizedInt>};

+ 3 - 0
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)