[RFC] Improve `-fstandalone-debug` with templated types

Standalone debug increase the debug content of single object files by producing debug information on types that are only seen in that particular translation unit via pointers/references. This often makes the object file debuggable in isolation, as opposed to as part of a larger binary compiled with the same flags.

-fstandalone-debug working as intended with non-templated types

byref.cpp:

struct Foo { int a; };
void bar(const Foo&) {}

without -fstandalone-debug:

$ clang++ -g -c byref.cpp
$ llvm-dwarfdump byref.o
byref.o:        file format elf64-x86-64

.debug_info contents:
0x00000000: Compile Unit: length = 0x00000043, format = DWARF32, version = 0x0005, unit_type = DW_UT_compile, abbr_offset = 0x0000, addr_size = 0x08 (next unit at 0x00000047)

0x0000000c: DW_TAG_compile_unit
              DW_AT_producer    ("clang version 18.0.0 (https://github.com/llvm/llvm-project.git 5bae3a0b0ccf8f4f2bcffc86453197f3cc5a9829)")
              DW_AT_language    (DW_LANG_C_plus_plus_14)
              DW_AT_name        ("byref.cpp")
              DW_AT_str_offsets_base    (0x00000008)
              DW_AT_stmt_list   (0x00000000)
              DW_AT_comp_dir    ("/tmp/test-debug")
              DW_AT_low_pc      (0x0000000000000000)
              DW_AT_high_pc     (0x000000000000000a)
              DW_AT_addr_base   (0x00000008)

0x00000023:   DW_TAG_subprogram
                DW_AT_low_pc    (0x0000000000000000)
                DW_AT_high_pc   (0x000000000000000a)
                DW_AT_frame_base        (DW_OP_reg6 RBP)
                DW_AT_linkage_name      ("_Z3barRK3Foo")
                DW_AT_name      ("bar")
                DW_AT_decl_file ("/tmp/test-debug/byref.cpp")
                DW_AT_decl_line (2)
                DW_AT_external  (true)

0x0000002f:     DW_TAG_formal_parameter
                  DW_AT_location        (DW_OP_fbreg -8)
                  DW_AT_decl_file       ("/tmp/test-debug/byref.cpp")
                  DW_AT_decl_line       (2)
                  DW_AT_type    (0x0000003a "const Foo &")

0x00000039:     NULL

0x0000003a:   DW_TAG_reference_type
                DW_AT_type      (0x0000003f "const Foo")

0x0000003f:   DW_TAG_const_type
                DW_AT_type      (0x00000044 "Foo")

0x00000044:   DW_TAG_structure_type
                DW_AT_name      ("Foo")
                DW_AT_declaration       (true)

0x00000046:   NULL

with -fstandalone-debug:

$ clang++ -g -fstandalone-debug -c byref.cpp
$ llvm-dwarfdump byref.o
byref.o:        file format elf64-x86-64

.debug_info contents:
0x00000000: Compile Unit: length = 0x00000055, format = DWARF32, version = 0x0005, unit_type = DW_UT_compile, abbr_offset = 0x0000, addr_size = 0x08 (next unit at 0x00000059)

0x0000000c: DW_TAG_compile_unit
              DW_AT_producer    ("clang version 18.0.0 (https://github.com/llvm/llvm-project.git 5bae3a0b0ccf8f4f2bcffc86453197f3cc5a9829)")
              DW_AT_language    (DW_LANG_C_plus_plus_14)
              DW_AT_name        ("byref.cpp")
              DW_AT_str_offsets_base    (0x00000008)
              DW_AT_stmt_list   (0x00000000)
              DW_AT_comp_dir    ("/tmp/test-debug")
              DW_AT_low_pc      (0x0000000000000000)
              DW_AT_high_pc     (0x000000000000000a)
              DW_AT_addr_base   (0x00000008)

0x00000023:   DW_TAG_subprogram
                DW_AT_low_pc    (0x0000000000000000)
                DW_AT_high_pc   (0x000000000000000a)
                DW_AT_frame_base        (DW_OP_reg6 RBP)
                DW_AT_linkage_name      ("_Z3barRK3Foo")
                DW_AT_name      ("bar")
                DW_AT_decl_file ("/tmp/test-debug/byref.cpp")
                DW_AT_decl_line (2)
                DW_AT_external  (true)

0x0000002f:     DW_TAG_formal_parameter
                  DW_AT_location        (DW_OP_fbreg -8)
                  DW_AT_decl_file       ("/tmp/test-debug/byref.cpp")
                  DW_AT_decl_line       (2)
                  DW_AT_type    (0x0000003a "const Foo &")

0x00000039:     NULL

0x0000003a:   DW_TAG_reference_type
                DW_AT_type      (0x0000003f "const Foo")

0x0000003f:   DW_TAG_const_type
                DW_AT_type      (0x00000044 "Foo")

0x00000044:   DW_TAG_structure_type
                DW_AT_calling_convention        (DW_CC_pass_by_value)
                DW_AT_name      ("Foo")
                DW_AT_byte_size (0x04)
                DW_AT_decl_file ("/tmp/test-debug/byref.cpp")
                DW_AT_decl_line (1)

0x0000004a:     DW_TAG_member
                  DW_AT_name    ("a")
                  DW_AT_type    (0x00000054 "int")
                  DW_AT_decl_file       ("/tmp/test-debug/byref.cpp")
                  DW_AT_decl_line       (1)
                  DW_AT_data_member_location    (0x00)

0x00000053:     NULL

0x00000054:   DW_TAG_base_type
                DW_AT_name      ("int")
                DW_AT_encoding  (DW_ATE_signed)
                DW_AT_byte_size (0x04)

0x00000058:   NULL

However, this does not provide a fully debuggable object file in many cases if the types are templated.

No type information emitted when a templated type is passed into a function by reference
template <typename T>
struct Foo { T a; };
void bar(const Foo<int>&) {}
$ clang++ -g -fstandalone-debug -c byref_templated.cpp
$ llvm-dwarfdump byref_templated.o
byref_templated.o:      file format elf64-x86-64

.debug_info contents:
0x00000000: Compile Unit: length = 0x00000043, format = DWARF32, version = 0x0005, unit_type = DW_UT_compile, abbr_offset = 0x0000, addr_size = 0x08 (next unit at 0x00000047)

0x0000000c: DW_TAG_compile_unit
              DW_AT_producer    ("clang version 18.0.0 (https://github.com/llvm/llvm-project.git 5bae3a0b0ccf8f4f2bcffc86453197f3cc5a9829)")
              DW_AT_language    (DW_LANG_C_plus_plus_14)
              DW_AT_name        ("byref_templated.cpp")
              DW_AT_str_offsets_base    (0x00000008)
              DW_AT_stmt_list   (0x00000000)
              DW_AT_comp_dir    ("/tmp/test-debug")
              DW_AT_low_pc      (0x0000000000000000)
              DW_AT_high_pc     (0x000000000000000a)
              DW_AT_addr_base   (0x00000008)

0x00000023:   DW_TAG_subprogram
                DW_AT_low_pc    (0x0000000000000000)
                DW_AT_high_pc   (0x000000000000000a)
                DW_AT_frame_base        (DW_OP_reg6 RBP)
                DW_AT_linkage_name      ("_Z3barRK3FooIiE")
                DW_AT_name      ("bar")
                DW_AT_decl_file ("/tmp/test-debug/byref_templated.cpp")
                DW_AT_decl_line (3)
                DW_AT_external  (true)

0x0000002f:     DW_TAG_formal_parameter
                  DW_AT_location        (DW_OP_fbreg -8)
                  DW_AT_decl_file       ("/tmp/test-debug/byref_templated.cpp")
                  DW_AT_decl_line       (3)
                  DW_AT_type    (0x0000003a "const Foo<int> &")

0x00000039:     NULL

0x0000003a:   DW_TAG_reference_type
                DW_AT_type      (0x0000003f "const Foo<int>")

0x0000003f:   DW_TAG_const_type
                DW_AT_type      (0x00000044 "Foo<int>")

0x00000044:   DW_TAG_structure_type
                DW_AT_name      ("Foo<int>")
                DW_AT_declaration       (true)

0x00000046:   NULL
Full type information emitted when a templated type is passed into a function by value and holds a pointer to a non-templated value
template <typename T>
struct PointerHolder { T* ptr; };
void foo(PointerHolder<int>) {}
$ clang++ -g -fstandalone-debug -c pointer_holder_int.cpp
$ llvm-dwarfdump pointer_holder_int.o
pointer_holder_int.o:   file format elf64-x86-64

.debug_info contents:
0x00000000: Compile Unit: length = 0x00000056, format = DWARF32, version = 0x0005, unit_type = DW_UT_compile, abbr_offset = 0x0000, addr_size = 0x08 (next unit at 0x0000005a)

0x0000000c: DW_TAG_compile_unit
              DW_AT_producer    ("clang version 18.0.0 (https://github.com/llvm/llvm-project.git 5bae3a0b0ccf8f4f2bcffc86453197f3cc5a9829)")
              DW_AT_language    (DW_LANG_C_plus_plus_14)
              DW_AT_name        ("pointer_holder_int.cpp")
              DW_AT_str_offsets_base    (0x00000008)
              DW_AT_stmt_list   (0x00000000)
              DW_AT_comp_dir    ("/tmp/test-debug")
              DW_AT_low_pc      (0x0000000000000000)
              DW_AT_high_pc     (0x000000000000000a)
              DW_AT_addr_base   (0x00000008)

0x00000023:   DW_TAG_subprogram
                DW_AT_low_pc    (0x0000000000000000)
                DW_AT_high_pc   (0x000000000000000a)
                DW_AT_frame_base        (DW_OP_reg6 RBP)
                DW_AT_linkage_name      ("_Z3foo13PointerHolderIiE")
                DW_AT_name      ("foo")
                DW_AT_decl_file ("/tmp/test-debug/pointer_holder_int.cpp")
                DW_AT_decl_line (3)
                DW_AT_external  (true)

0x0000002f:     DW_TAG_formal_parameter
                  DW_AT_location        (DW_OP_fbreg -8)
                  DW_AT_decl_file       ("/tmp/test-debug/pointer_holder_int.cpp")
                  DW_AT_decl_line       (3)
                  DW_AT_type    (0x0000003a "PointerHolder<int>")

0x00000039:     NULL

0x0000003a:   DW_TAG_structure_type
                DW_AT_calling_convention        (DW_CC_pass_by_value)
                DW_AT_name      ("PointerHolder<int>")
                DW_AT_byte_size (0x08)
                DW_AT_decl_file ("/tmp/test-debug/pointer_holder_int.cpp")
                DW_AT_decl_line (2)

0x00000040:     DW_TAG_template_type_parameter
                  DW_AT_type    (0x00000050 "int")
                  DW_AT_name    ("T")

0x00000046:     DW_TAG_member
                  DW_AT_name    ("ptr")
                  DW_AT_type    (0x00000054 "int *")
                  DW_AT_decl_file       ("/tmp/test-debug/pointer_holder_int.cpp")
                  DW_AT_decl_line       (2)
                  DW_AT_data_member_location    (0x00)

0x0000004f:     NULL

0x00000050:   DW_TAG_base_type
                DW_AT_name      ("int")
                DW_AT_encoding  (DW_ATE_signed)
                DW_AT_byte_size (0x04)

0x00000054:   DW_TAG_pointer_type
                DW_AT_type      (0x00000050 "int")

0x00000059:   NULL
Limited type information emitted when a templated type is passed into a function by value and holds a pointer to another templated value
template <typename T>
struct PointerHolder { T* ptr; };
void foo(PointerHolder<PointerHolder<int>>) {}
$ clang++ -g -fstandalone-debug -c pointer_holder_nested.cpp
$ llvm-dwarfdump pointer_holder_nested.o
pointer_holder_nested.o:        file format elf64-x86-64

.debug_info contents:
0x00000000: Compile Unit: length = 0x00000054, format = DWARF32, version = 0x0005, unit_type = DW_UT_compile, abbr_offset = 0x0000, addr_size = 0x08 (next unit at 0x00000058)

0x0000000c: DW_TAG_compile_unit
              DW_AT_producer    ("clang version 18.0.0 (https://github.com/llvm/llvm-project.git 5bae3a0b0ccf8f4f2bcffc86453197f3cc5a9829)")
              DW_AT_language    (DW_LANG_C_plus_plus_14)
              DW_AT_name        ("pointer_holder_nested.cpp")
              DW_AT_str_offsets_base    (0x00000008)
              DW_AT_stmt_list   (0x00000000)
              DW_AT_comp_dir    ("/tmp/test-debug")
              DW_AT_low_pc      (0x0000000000000000)
              DW_AT_high_pc     (0x000000000000000a)
              DW_AT_addr_base   (0x00000008)

0x00000023:   DW_TAG_subprogram
                DW_AT_low_pc    (0x0000000000000000)
                DW_AT_high_pc   (0x000000000000000a)
                DW_AT_frame_base        (DW_OP_reg6 RBP)
                DW_AT_linkage_name      ("_Z3foo13PointerHolderIS_IiEE")
                DW_AT_name      ("foo")
                DW_AT_decl_file ("/tmp/test-debug/pointer_holder_nested.cpp")
                DW_AT_decl_line (3)
                DW_AT_external  (true)

0x0000002f:     DW_TAG_formal_parameter
                  DW_AT_location        (DW_OP_fbreg -8)
                  DW_AT_decl_file       ("/tmp/test-debug/pointer_holder_nested.cpp")
                  DW_AT_decl_line       (3)
                  DW_AT_type    (0x0000003a "PointerHolder<PointerHolder<int> >")

0x00000039:     NULL

0x0000003a:   DW_TAG_structure_type
                DW_AT_calling_convention        (DW_CC_pass_by_value)
                DW_AT_name      ("PointerHolder<PointerHolder<int> >")
                DW_AT_byte_size (0x08)
                DW_AT_decl_file ("/tmp/test-debug/pointer_holder_nested.cpp")
                DW_AT_decl_line (2)

0x00000040:     DW_TAG_template_type_parameter
                  DW_AT_type    (0x00000050 "PointerHolder<int>")
                  DW_AT_name    ("T")

0x00000046:     DW_TAG_member
                  DW_AT_name    ("ptr")
                  DW_AT_type    (0x00000052 "PointerHolder<int> *")
                  DW_AT_decl_file       ("/tmp/test-debug/pointer_holder_nested.cpp")
                  DW_AT_decl_line       (2)
                  DW_AT_data_member_location    (0x00)

0x0000004f:     NULL

0x00000050:   DW_TAG_structure_type
                DW_AT_name      ("PointerHolder<int>")
                DW_AT_declaration       (true)

0x00000052:   DW_TAG_pointer_type
                DW_AT_type      (0x00000050 "PointerHolder<int>")

0x00000057:   NULL

In these examples we see that any templated type which has not been initialised is emitted with a DW_AT_declaration (true) tag and no structural information. Further, we can show that with explicit initialisation the full debug information is emitted.

Full type information emitted when a templated type is passed into a function by value and holds a pointer to another templated value which has been explicitly initialised
template <typename T>
struct PointerHolder { T* ptr; };

template struct PointerHolder<int>;
void foo(PointerHolder<PointerHolder<int>>) {}
$ clang++ -g -fstandalone-debug -c pointer_holder_explicit.cpp
$ llvm-dwarfdump pointer_holder_explicit.o
pointer_holder_explicit.o:      file format elf64-x86-64

.debug_info contents:
0x00000000: Compile Unit: length = 0x00000071, format = DWARF32, version = 0x0005, unit_type = DW_UT_compile, abbr_offset = 0x0000, addr_size = 0x08 (next unit at 0x00000075)

0x0000000c: DW_TAG_compile_unit
              DW_AT_producer    ("clang version 18.0.0 (https://github.com/llvm/llvm-project.git 5bae3a0b0ccf8f4f2bcffc86453197f3cc5a9829)")
              DW_AT_language    (DW_LANG_C_plus_plus_14)
              DW_AT_name        ("pointer_holder_explicit.cpp")
              DW_AT_str_offsets_base    (0x00000008)
              DW_AT_stmt_list   (0x00000000)
              DW_AT_comp_dir    ("/tmp/test-debug")
              DW_AT_low_pc      (0x0000000000000000)
              DW_AT_high_pc     (0x000000000000000a)
              DW_AT_addr_base   (0x00000008)

0x00000023:   DW_TAG_structure_type
                DW_AT_calling_convention        (DW_CC_pass_by_value)
                DW_AT_name      ("PointerHolder<int>")
                DW_AT_byte_size (0x08)
                DW_AT_decl_file ("/tmp/test-debug/pointer_holder_explicit.cpp")
                DW_AT_decl_line (4)

0x00000029:     DW_TAG_template_type_parameter
                  DW_AT_type    (0x00000039 "int")
                  DW_AT_name    ("T")

0x0000002f:     DW_TAG_member
                  DW_AT_name    ("ptr")
                  DW_AT_type    (0x0000003d "int *")
                  DW_AT_decl_file       ("/tmp/test-debug/pointer_holder_explicit.cpp")
                  DW_AT_decl_line       (2)
                  DW_AT_data_member_location    (0x00)

0x00000038:     NULL

0x00000039:   DW_TAG_base_type
                DW_AT_name      ("int")
                DW_AT_encoding  (DW_ATE_signed)
                DW_AT_byte_size (0x04)

0x0000003d:   DW_TAG_pointer_type
                DW_AT_type      (0x00000039 "int")

0x00000042:   DW_TAG_subprogram
                DW_AT_low_pc    (0x0000000000000000)
                DW_AT_high_pc   (0x000000000000000a)
                DW_AT_frame_base        (DW_OP_reg6 RBP)
                DW_AT_linkage_name      ("_Z3foo13PointerHolderIS_IiEE")
                DW_AT_name      ("foo")
                DW_AT_decl_file ("/tmp/test-debug/pointer_holder_explicit.cpp")
                DW_AT_decl_line (5)
                DW_AT_external  (true)

0x0000004e:     DW_TAG_formal_parameter
                  DW_AT_location        (DW_OP_fbreg -8)
                  DW_AT_decl_file       ("/tmp/test-debug/pointer_holder_explicit.cpp")
                  DW_AT_decl_line       (5)
                  DW_AT_type    (0x00000059 "PointerHolder<PointerHolder<int> >")

0x00000058:     NULL

0x00000059:   DW_TAG_structure_type
                DW_AT_calling_convention        (DW_CC_pass_by_value)
                DW_AT_name      ("PointerHolder<PointerHolder<int> >")
                DW_AT_byte_size (0x08)
                DW_AT_decl_file ("/tmp/test-debug/pointer_holder_explicit.cpp")
                DW_AT_decl_line (2)

0x0000005f:     DW_TAG_template_type_parameter
                  DW_AT_type    (0x00000023 "PointerHolder<int>")
                  DW_AT_name    ("T")

0x00000065:     DW_TAG_member
                  DW_AT_name    ("ptr")
                  DW_AT_type    (0x0000006f "PointerHolder<int> *")
                  DW_AT_decl_file       ("/tmp/test-debug/pointer_holder_explicit.cpp")
                  DW_AT_decl_line       (2)
                  DW_AT_data_member_location    (0x00)

0x0000006e:     NULL

0x0000006f:   DW_TAG_pointer_type
                DW_AT_type      (0x00000023 "PointerHolder<int>")

0x00000074:   NULL

Our use case requires debug information of the entire type tree for processing. I would like to know:

  1. Is this a bug? Does -fstandalone-debug imply that the structural debug information will be emitted for the templated types in these case? To me it’s surprising behaviour.
  2. If it’s not a bug, would we be able to implement a further flag like -femit-unused-template-debug-types to generate this? It seems like in the case where an explicit instantiation request works with no additional includes it should be possible for the compiler to emit this information.

While looking into the code in this area I found the flag -Xclang -debug-forward-template-params. This gets me a lot closer to what I want, but the templated types themselves still lack structural information.

Example code:

template <typename T>
struct PointerHolder { T* ptr; };
void foo(PointerHolder<PointerHolder<int>>) {}
$ clang++ -g -fstandalone-debug -Xclang -debug-forward-template-params -c pointer_holder_nested.cpp
$ llvm-dwarfdump pointer_holder_nested.o
pointer_holder_nested.o:        file format elf64-x86-64

.debug_info contents:
0x00000000: Compile Unit: length = 0x0000005f, format = DWARF32, version = 0x0005, unit_type = DW_UT_compile, abbr_offset = 0x0000, addr_size = 0x08 (next unit at 0x00000063)

0x0000000c: DW_TAG_compile_unit
              DW_AT_producer    ("clang version 18.0.0")
              DW_AT_language    (DW_LANG_C_plus_plus_14)
              DW_AT_name        ("pointer_holder_nested.cpp")
              DW_AT_str_offsets_base    (0x00000008)
              DW_AT_stmt_list   (0x00000000)
              DW_AT_comp_dir    ("/tmp/clang")
              DW_AT_low_pc      (0x0000000000000000)
              DW_AT_high_pc     (0x000000000000000a)
              DW_AT_addr_base   (0x00000008)

0x00000023:   DW_TAG_subprogram
                DW_AT_low_pc    (0x0000000000000000)
                DW_AT_high_pc   (0x000000000000000a)
                DW_AT_frame_base        (DW_OP_reg6 RBP)
                DW_AT_linkage_name      ("_Z3foo13PointerHolderIS_IiEE")
                DW_AT_name      ("foo")
                DW_AT_decl_file ("/tmp/clang/pointer_holder_nested.cpp")
                DW_AT_decl_line (3)
                DW_AT_external  (true)

0x0000002f:     DW_TAG_formal_parameter
                  DW_AT_location        (DW_OP_fbreg -8)
                  DW_AT_decl_file       ("/tmp/clang/pointer_holder_nested.cpp")
                  DW_AT_decl_line       (3)
                  DW_AT_type    (0x0000003a "PointerHolder<PointerHolder<int> >")

0x00000039:     NULL

0x0000003a:   DW_TAG_structure_type
                DW_AT_calling_convention        (DW_CC_pass_by_value)
                DW_AT_name      ("PointerHolder<PointerHolder<int> >")
                DW_AT_byte_size (0x08)
                DW_AT_decl_file ("/tmp/clang/pointer_holder_nested.cpp")
                DW_AT_decl_line (2)

0x00000040:     DW_TAG_template_type_parameter
                  DW_AT_type    (0x00000050 "PointerHolder<int>")
                  DW_AT_name    ("T")

0x00000046:     DW_TAG_member
                  DW_AT_name    ("ptr")
                  DW_AT_type    (0x0000005d "PointerHolder<int> *")
                  DW_AT_decl_file       ("/tmp/clang/pointer_holder_nested.cpp")
                  DW_AT_decl_line       (2)
                  DW_AT_data_member_location    (0x00)

0x0000004f:     NULL

0x00000050:   DW_TAG_structure_type
                DW_AT_name      ("PointerHolder<int>")
                DW_AT_declaration       (true)

0x00000052:     DW_TAG_template_type_parameter
                  DW_AT_type    (0x00000059 "int")
                  DW_AT_name    ("T")

0x00000058:     NULL

0x00000059:   DW_TAG_base_type
                DW_AT_name      ("int")
                DW_AT_encoding  (DW_ATE_signed)
                DW_AT_byte_size (0x04)

0x0000005d:   DW_TAG_pointer_type
                DW_AT_type      (0x00000050 "PointerHolder<int>")

0x00000062:   NULL

This compares to the code in the OP without the -debug-forward-template-params flag. Although PointerHolder<int> is still DW_AT_declaration (true), the template parameter exists and the int held by it is complete.

@adrian.prantl @dblaikie @echristo

Really interested in feedback from debug info owners here. I’m looking at the code now in CGDebugInfo.cpp and it seems like this under a new flag or an extension of the existing flags wouldn’t be crazy hard to add, but I’m new to the codebase. Are there any obvious gotchas or a reason this would be a bad idea?

Gut reaction: There’s probably no bugs here/not especially able to “fix” this in a way that addresses your needs.

I think what you’re observing is the nature of the C++ programming language, which distinguishes instantiating a template declaration and instantiating a template definition.

To take the concrete example mentioned:

template <typename T>
struct PointerHolder { T* ptr; };
void foo(PointerHolder<PointerHolder<int>>) {}

The reason there’s no debug info for PointerHolder<int>'s definition because the definition is not instantiated. One way to observe this is with a static_assert that would trigger if the definition was instantiated:

#include <type_traits>
template <typename T>
struct PointerHolder { T* ptr; 
  static_assert(!std::is_same_v<T, int>);
};
void foo(PointerHolder<PointerHolder<int>>) {}

The C++ language says where the compiler is allowed to instantiate template declarations and definitions - without some, I would think, fairly difficult work (there’s certain contexts where SFINAE-type functionality can be used to speculatively instantiate things, but I don’t think this sort of class template definition instantiations could be done with that technique) to conditionally instantiate definitions and throw out the result if it’s wrong.

Some of the problem is that these rules about points of instantiation can be relied upon by the developer, and if we instantiate in the wrong places it’s possible to get not an error, but silently get the wrong answer.

(some of the scenarios you described don’t sound quite right - but if you could reduce them to something you can share here/on godbolt, I’m guessing we’d find them to be equivalent to the above situation - where the C++ language rules preclude instantiating the definition of the type)