Skip to content

Conversation

lenary
Copy link
Member

@lenary lenary commented Oct 16, 2025

This change turns the "nooutline" attribute into an enum attribute
called nooutline, and adds an auto-upgrader for bitcode to make the
same change to existing IR.

This IR attribute disables both the Machine Outliner (enabled at Oz for
some targets), and the IR Outliner (disabled by default).

Copy link
Member Author

lenary commented Oct 16, 2025

Warning

This pull request is not mergeable via GitHub because a downstack PR is open. Once all requirements are satisfied, merge this PR as a stack on Graphite.
Learn more

This stack of pull requests is managed by Graphite. Learn more about stacking.

@llvmbot
Copy link
Member

llvmbot commented Oct 16, 2025

@llvm/pr-subscribers-llvm-transforms

@llvm/pr-subscribers-backend-aarch64

Author: Sam Elliott (lenary)

Changes

This change turns the "nooutline" attribute into an enum attribute
called nooutline, and adds an auto-upgrader for bitcode to make the
same change to existing IR.

This IR attribute disables both the Machine Outliner (enabled at Oz for
some targets), and the IR Outliner (disabled by default).


Full diff: https://github.com/llvm/llvm-project/pull/163665.diff

12 Files Affected:

  • (modified) llvm/docs/LangRef.rst (+1-1)
  • (modified) llvm/include/llvm/Bitcode/LLVMBitCodes.h (+1)
  • (modified) llvm/include/llvm/IR/Attributes.td (+3)
  • (modified) llvm/lib/Bitcode/Reader/BitcodeReader.cpp (+2)
  • (modified) llvm/lib/Bitcode/Writer/BitcodeWriter.cpp (+2)
  • (modified) llvm/lib/CodeGen/MachineOutliner.cpp (+1-1)
  • (modified) llvm/lib/IR/AutoUpgrade.cpp (+6)
  • (modified) llvm/lib/Transforms/IPO/IROutliner.cpp (+1-1)
  • (modified) llvm/lib/Transforms/Utils/CodeExtractor.cpp (+4)
  • (added) llvm/test/Bitcode/upgrade-nooutline.ll (+12)
  • (modified) llvm/test/CodeGen/AArch64/machine-outliner-mapper-debug-output.mir (+1-1)
  • (modified) llvm/test/Transforms/IROutliner/nooutline-attribute.ll (+2-2)
diff --git a/llvm/docs/LangRef.rst b/llvm/docs/LangRef.rst
index 4884e2dcbbe00..73887d1039488 100644
--- a/llvm/docs/LangRef.rst
+++ b/llvm/docs/LangRef.rst
@@ -2738,7 +2738,7 @@ For example:
     to signify an unbounded maximum. The syntax `vscale_range(<val>)` can be
     used to set both `min` and `max` to the same value. Functions that don't
     include this attribute make no assumptions about the value of `vscale`.
-``"nooutline"``
+``nooutline``
     This attribute indicates that outlining passes should not modify the
     function.
 
diff --git a/llvm/include/llvm/Bitcode/LLVMBitCodes.h b/llvm/include/llvm/Bitcode/LLVMBitCodes.h
index 464f475098ec5..95596273aad69 100644
--- a/llvm/include/llvm/Bitcode/LLVMBitCodes.h
+++ b/llvm/include/llvm/Bitcode/LLVMBitCodes.h
@@ -801,6 +801,7 @@ enum AttributeKindCodes {
   ATTR_KIND_CAPTURES = 102,
   ATTR_KIND_DEAD_ON_RETURN = 103,
   ATTR_KIND_SANITIZE_ALLOC_TOKEN = 104,
+  ATTR_KIND_NOOUTLINE = 105,
 };
 
 enum ComdatSelectionKindCodes {
diff --git a/llvm/include/llvm/IR/Attributes.td b/llvm/include/llvm/IR/Attributes.td
index 8e7d9dcebfe2a..46a77ec121039 100644
--- a/llvm/include/llvm/IR/Attributes.td
+++ b/llvm/include/llvm/IR/Attributes.td
@@ -207,6 +207,9 @@ def NoImplicitFloat : EnumAttr<"noimplicitfloat", IntersectPreserve, [FnAttr]>;
 /// inline=never.
 def NoInline : EnumAttr<"noinline", IntersectPreserve, [FnAttr]>;
 
+/// nooutline
+def NoOutline : EnumAttr<"nooutline", IntersectPreserve, [FnAttr]>;
+
 /// Function is called early and/or often, so lazy binding isn't worthwhile.
 def NonLazyBind : EnumAttr<"nonlazybind", IntersectPreserve, [FnAttr]>;
 
diff --git a/llvm/lib/Bitcode/Reader/BitcodeReader.cpp b/llvm/lib/Bitcode/Reader/BitcodeReader.cpp
index aaee1f0a7687c..ab80da376fdf8 100644
--- a/llvm/lib/Bitcode/Reader/BitcodeReader.cpp
+++ b/llvm/lib/Bitcode/Reader/BitcodeReader.cpp
@@ -2257,6 +2257,8 @@ static Attribute::AttrKind getAttrFromCode(uint64_t Code) {
     return Attribute::Captures;
   case bitc::ATTR_KIND_DEAD_ON_RETURN:
     return Attribute::DeadOnReturn;
+  case bitc::ATTR_KIND_NOOUTLINE:
+    return Attribute::NoOutline;
   }
 }
 
diff --git a/llvm/lib/Bitcode/Writer/BitcodeWriter.cpp b/llvm/lib/Bitcode/Writer/BitcodeWriter.cpp
index 54e916e2dcfe1..0efe7e030e0dc 100644
--- a/llvm/lib/Bitcode/Writer/BitcodeWriter.cpp
+++ b/llvm/lib/Bitcode/Writer/BitcodeWriter.cpp
@@ -956,6 +956,8 @@ static uint64_t getAttrKindEncoding(Attribute::AttrKind Kind) {
     return bitc::ATTR_KIND_CAPTURES;
   case Attribute::DeadOnReturn:
     return bitc::ATTR_KIND_DEAD_ON_RETURN;
+  case Attribute::NoOutline:
+    return bitc::ATTR_KIND_NOOUTLINE;
   case Attribute::EndAttrKinds:
     llvm_unreachable("Can not encode end-attribute kinds marker.");
   case Attribute::None:
diff --git a/llvm/lib/CodeGen/MachineOutliner.cpp b/llvm/lib/CodeGen/MachineOutliner.cpp
index 9feb9740de126..d6f7305278f38 100644
--- a/llvm/lib/CodeGen/MachineOutliner.cpp
+++ b/llvm/lib/CodeGen/MachineOutliner.cpp
@@ -1257,7 +1257,7 @@ void MachineOutliner::populateMapper(InstructionMapper &Mapper, Module &M) {
   for (Function &F : M) {
     LLVM_DEBUG(dbgs() << "MAPPING FUNCTION: " << F.getName() << "\n");
 
-    if (F.hasFnAttribute("nooutline")) {
+    if (F.hasFnAttribute(Attribute::NoOutline)) {
       LLVM_DEBUG(dbgs() << "SKIP: Function has nooutline attribute\n");
       continue;
     }
diff --git a/llvm/lib/IR/AutoUpgrade.cpp b/llvm/lib/IR/AutoUpgrade.cpp
index f28b98957cae4..f3b164e5d0603 100644
--- a/llvm/lib/IR/AutoUpgrade.cpp
+++ b/llvm/lib/IR/AutoUpgrade.cpp
@@ -5956,6 +5956,12 @@ void llvm::UpgradeFunctionAttributes(Function &F) {
     F.removeFnAttr("implicit-section-name");
   }
 
+  if (Attribute A = F.getFnAttribute("nooutline");
+      A.isValid() && A.isStringAttribute()) {
+    F.removeFnAttr("nooutline");
+    F.addFnAttr(Attribute::NoOutline);
+  }
+
   if (!F.empty()) {
     // For some reason this is called twice, and the first time is before any
     // instructions are loaded into the body.
diff --git a/llvm/lib/Transforms/IPO/IROutliner.cpp b/llvm/lib/Transforms/IPO/IROutliner.cpp
index fdf0c3ac8007d..cdcff8fb19236 100644
--- a/llvm/lib/Transforms/IPO/IROutliner.cpp
+++ b/llvm/lib/Transforms/IPO/IROutliner.cpp
@@ -2419,7 +2419,7 @@ void IROutliner::pruneIncompatibleRegions(
     if (FnForCurrCand.hasOptNone())
       continue;
 
-    if (FnForCurrCand.hasFnAttribute("nooutline")) {
+    if (FnForCurrCand.hasFnAttribute(Attribute::NoOutline)) {
       LLVM_DEBUG({
         dbgs() << "... Skipping function with nooutline attribute: "
                << FnForCurrCand.getName() << "\n";
diff --git a/llvm/lib/Transforms/Utils/CodeExtractor.cpp b/llvm/lib/Transforms/Utils/CodeExtractor.cpp
index 5ba6f95f5fae8..2a94c62e7f357 100644
--- a/llvm/lib/Transforms/Utils/CodeExtractor.cpp
+++ b/llvm/lib/Transforms/Utils/CodeExtractor.cpp
@@ -933,6 +933,10 @@ Function *CodeExtractor::constructFunctionDeclaration(
       case Attribute::CoroDestroyOnlyWhenComplete:
       case Attribute::CoroElideSafe:
       case Attribute::NoDivergenceSource:
+      // CodeExtractor is used by the IROutliner, so we should not be extracting
+      // from a function with nooutline, but if we do, we should not be
+      // propagating this attribute.
+      case Attribute::NoOutline:
         continue;
       // Those attributes should be safe to propagate to the extracted function.
       case Attribute::AlwaysInline:
diff --git a/llvm/test/Bitcode/upgrade-nooutline.ll b/llvm/test/Bitcode/upgrade-nooutline.ll
new file mode 100644
index 0000000000000..fe0d954408c92
--- /dev/null
+++ b/llvm/test/Bitcode/upgrade-nooutline.ll
@@ -0,0 +1,12 @@
+; RUN: llvm-as < %s | llvm-dis - | FileCheck %s
+
+; CHECK: define void @f() [[ATTR:#[0-9]+]]
+; CHECK-NOT: "nooutline"
+; CHECK: attributes [[ATTR]] = {
+; CHECK-NOT: "nooutline"
+; CHECK-SAME: nooutline
+; CHECK-NOT: "nooutline"
+
+define void @f() "nooutline" {
+  ret void
+}
diff --git a/llvm/test/CodeGen/AArch64/machine-outliner-mapper-debug-output.mir b/llvm/test/CodeGen/AArch64/machine-outliner-mapper-debug-output.mir
index 826157e68d75c..056c5f18d492c 100644
--- a/llvm/test/CodeGen/AArch64/machine-outliner-mapper-debug-output.mir
+++ b/llvm/test/CodeGen/AArch64/machine-outliner-mapper-debug-output.mir
@@ -17,7 +17,7 @@
 
 --- |
   define void @block_too_small() noredzone { unreachable }
-  define void @no_outline() noredzone "nooutline" { unreachable }
+  define void @no_outline() noredzone nooutline { unreachable }
   define void @redzone() { unreachable }
   declare void @no_mf()
   define void @block_addr_fn() noredzone {
diff --git a/llvm/test/Transforms/IROutliner/nooutline-attribute.ll b/llvm/test/Transforms/IROutliner/nooutline-attribute.ll
index eaf3afa3b15a1..a61a1614a3115 100644
--- a/llvm/test/Transforms/IROutliner/nooutline-attribute.ll
+++ b/llvm/test/Transforms/IROutliner/nooutline-attribute.ll
@@ -8,7 +8,7 @@
 
 define void @outlinable() { ret void }
 
-define i8 @nooutline1(ptr noalias %s, ptr noalias %d, i64 %len) "nooutline" {
+define i8 @nooutline1(ptr noalias %s, ptr noalias %d, i64 %len) nooutline {
   %a = load i8, ptr %s
   %b = load i8, ptr %d
   call void @llvm.memcpy.p0.p0.i64(ptr %d, ptr %s, i64 %len, i1 false)
@@ -17,7 +17,7 @@ define i8 @nooutline1(ptr noalias %s, ptr noalias %d, i64 %len) "nooutline" {
   ret i8 %ret
 }
 
-define i8 @nooutline2(ptr noalias %s, ptr noalias %d, i64 %len) "nooutline" {
+define i8 @nooutline2(ptr noalias %s, ptr noalias %d, i64 %len) nooutline {
   %a = load i8, ptr %s
   %b = load i8, ptr %d
   call void @llvm.memcpy.p0.p0.i64(ptr %d, ptr %s, i64 %len, i1 false)

@llvmbot
Copy link
Member

llvmbot commented Oct 16, 2025

@llvm/pr-subscribers-llvm-ir

Author: Sam Elliott (lenary)

Changes

This change turns the "nooutline" attribute into an enum attribute
called nooutline, and adds an auto-upgrader for bitcode to make the
same change to existing IR.

This IR attribute disables both the Machine Outliner (enabled at Oz for
some targets), and the IR Outliner (disabled by default).


Full diff: https://github.com/llvm/llvm-project/pull/163665.diff

12 Files Affected:

  • (modified) llvm/docs/LangRef.rst (+1-1)
  • (modified) llvm/include/llvm/Bitcode/LLVMBitCodes.h (+1)
  • (modified) llvm/include/llvm/IR/Attributes.td (+3)
  • (modified) llvm/lib/Bitcode/Reader/BitcodeReader.cpp (+2)
  • (modified) llvm/lib/Bitcode/Writer/BitcodeWriter.cpp (+2)
  • (modified) llvm/lib/CodeGen/MachineOutliner.cpp (+1-1)
  • (modified) llvm/lib/IR/AutoUpgrade.cpp (+6)
  • (modified) llvm/lib/Transforms/IPO/IROutliner.cpp (+1-1)
  • (modified) llvm/lib/Transforms/Utils/CodeExtractor.cpp (+4)
  • (added) llvm/test/Bitcode/upgrade-nooutline.ll (+12)
  • (modified) llvm/test/CodeGen/AArch64/machine-outliner-mapper-debug-output.mir (+1-1)
  • (modified) llvm/test/Transforms/IROutliner/nooutline-attribute.ll (+2-2)
diff --git a/llvm/docs/LangRef.rst b/llvm/docs/LangRef.rst
index 4884e2dcbbe00..73887d1039488 100644
--- a/llvm/docs/LangRef.rst
+++ b/llvm/docs/LangRef.rst
@@ -2738,7 +2738,7 @@ For example:
     to signify an unbounded maximum. The syntax `vscale_range(<val>)` can be
     used to set both `min` and `max` to the same value. Functions that don't
     include this attribute make no assumptions about the value of `vscale`.
-``"nooutline"``
+``nooutline``
     This attribute indicates that outlining passes should not modify the
     function.
 
diff --git a/llvm/include/llvm/Bitcode/LLVMBitCodes.h b/llvm/include/llvm/Bitcode/LLVMBitCodes.h
index 464f475098ec5..95596273aad69 100644
--- a/llvm/include/llvm/Bitcode/LLVMBitCodes.h
+++ b/llvm/include/llvm/Bitcode/LLVMBitCodes.h
@@ -801,6 +801,7 @@ enum AttributeKindCodes {
   ATTR_KIND_CAPTURES = 102,
   ATTR_KIND_DEAD_ON_RETURN = 103,
   ATTR_KIND_SANITIZE_ALLOC_TOKEN = 104,
+  ATTR_KIND_NOOUTLINE = 105,
 };
 
 enum ComdatSelectionKindCodes {
diff --git a/llvm/include/llvm/IR/Attributes.td b/llvm/include/llvm/IR/Attributes.td
index 8e7d9dcebfe2a..46a77ec121039 100644
--- a/llvm/include/llvm/IR/Attributes.td
+++ b/llvm/include/llvm/IR/Attributes.td
@@ -207,6 +207,9 @@ def NoImplicitFloat : EnumAttr<"noimplicitfloat", IntersectPreserve, [FnAttr]>;
 /// inline=never.
 def NoInline : EnumAttr<"noinline", IntersectPreserve, [FnAttr]>;
 
+/// nooutline
+def NoOutline : EnumAttr<"nooutline", IntersectPreserve, [FnAttr]>;
+
 /// Function is called early and/or often, so lazy binding isn't worthwhile.
 def NonLazyBind : EnumAttr<"nonlazybind", IntersectPreserve, [FnAttr]>;
 
diff --git a/llvm/lib/Bitcode/Reader/BitcodeReader.cpp b/llvm/lib/Bitcode/Reader/BitcodeReader.cpp
index aaee1f0a7687c..ab80da376fdf8 100644
--- a/llvm/lib/Bitcode/Reader/BitcodeReader.cpp
+++ b/llvm/lib/Bitcode/Reader/BitcodeReader.cpp
@@ -2257,6 +2257,8 @@ static Attribute::AttrKind getAttrFromCode(uint64_t Code) {
     return Attribute::Captures;
   case bitc::ATTR_KIND_DEAD_ON_RETURN:
     return Attribute::DeadOnReturn;
+  case bitc::ATTR_KIND_NOOUTLINE:
+    return Attribute::NoOutline;
   }
 }
 
diff --git a/llvm/lib/Bitcode/Writer/BitcodeWriter.cpp b/llvm/lib/Bitcode/Writer/BitcodeWriter.cpp
index 54e916e2dcfe1..0efe7e030e0dc 100644
--- a/llvm/lib/Bitcode/Writer/BitcodeWriter.cpp
+++ b/llvm/lib/Bitcode/Writer/BitcodeWriter.cpp
@@ -956,6 +956,8 @@ static uint64_t getAttrKindEncoding(Attribute::AttrKind Kind) {
     return bitc::ATTR_KIND_CAPTURES;
   case Attribute::DeadOnReturn:
     return bitc::ATTR_KIND_DEAD_ON_RETURN;
+  case Attribute::NoOutline:
+    return bitc::ATTR_KIND_NOOUTLINE;
   case Attribute::EndAttrKinds:
     llvm_unreachable("Can not encode end-attribute kinds marker.");
   case Attribute::None:
diff --git a/llvm/lib/CodeGen/MachineOutliner.cpp b/llvm/lib/CodeGen/MachineOutliner.cpp
index 9feb9740de126..d6f7305278f38 100644
--- a/llvm/lib/CodeGen/MachineOutliner.cpp
+++ b/llvm/lib/CodeGen/MachineOutliner.cpp
@@ -1257,7 +1257,7 @@ void MachineOutliner::populateMapper(InstructionMapper &Mapper, Module &M) {
   for (Function &F : M) {
     LLVM_DEBUG(dbgs() << "MAPPING FUNCTION: " << F.getName() << "\n");
 
-    if (F.hasFnAttribute("nooutline")) {
+    if (F.hasFnAttribute(Attribute::NoOutline)) {
       LLVM_DEBUG(dbgs() << "SKIP: Function has nooutline attribute\n");
       continue;
     }
diff --git a/llvm/lib/IR/AutoUpgrade.cpp b/llvm/lib/IR/AutoUpgrade.cpp
index f28b98957cae4..f3b164e5d0603 100644
--- a/llvm/lib/IR/AutoUpgrade.cpp
+++ b/llvm/lib/IR/AutoUpgrade.cpp
@@ -5956,6 +5956,12 @@ void llvm::UpgradeFunctionAttributes(Function &F) {
     F.removeFnAttr("implicit-section-name");
   }
 
+  if (Attribute A = F.getFnAttribute("nooutline");
+      A.isValid() && A.isStringAttribute()) {
+    F.removeFnAttr("nooutline");
+    F.addFnAttr(Attribute::NoOutline);
+  }
+
   if (!F.empty()) {
     // For some reason this is called twice, and the first time is before any
     // instructions are loaded into the body.
diff --git a/llvm/lib/Transforms/IPO/IROutliner.cpp b/llvm/lib/Transforms/IPO/IROutliner.cpp
index fdf0c3ac8007d..cdcff8fb19236 100644
--- a/llvm/lib/Transforms/IPO/IROutliner.cpp
+++ b/llvm/lib/Transforms/IPO/IROutliner.cpp
@@ -2419,7 +2419,7 @@ void IROutliner::pruneIncompatibleRegions(
     if (FnForCurrCand.hasOptNone())
       continue;
 
-    if (FnForCurrCand.hasFnAttribute("nooutline")) {
+    if (FnForCurrCand.hasFnAttribute(Attribute::NoOutline)) {
       LLVM_DEBUG({
         dbgs() << "... Skipping function with nooutline attribute: "
                << FnForCurrCand.getName() << "\n";
diff --git a/llvm/lib/Transforms/Utils/CodeExtractor.cpp b/llvm/lib/Transforms/Utils/CodeExtractor.cpp
index 5ba6f95f5fae8..2a94c62e7f357 100644
--- a/llvm/lib/Transforms/Utils/CodeExtractor.cpp
+++ b/llvm/lib/Transforms/Utils/CodeExtractor.cpp
@@ -933,6 +933,10 @@ Function *CodeExtractor::constructFunctionDeclaration(
       case Attribute::CoroDestroyOnlyWhenComplete:
       case Attribute::CoroElideSafe:
       case Attribute::NoDivergenceSource:
+      // CodeExtractor is used by the IROutliner, so we should not be extracting
+      // from a function with nooutline, but if we do, we should not be
+      // propagating this attribute.
+      case Attribute::NoOutline:
         continue;
       // Those attributes should be safe to propagate to the extracted function.
       case Attribute::AlwaysInline:
diff --git a/llvm/test/Bitcode/upgrade-nooutline.ll b/llvm/test/Bitcode/upgrade-nooutline.ll
new file mode 100644
index 0000000000000..fe0d954408c92
--- /dev/null
+++ b/llvm/test/Bitcode/upgrade-nooutline.ll
@@ -0,0 +1,12 @@
+; RUN: llvm-as < %s | llvm-dis - | FileCheck %s
+
+; CHECK: define void @f() [[ATTR:#[0-9]+]]
+; CHECK-NOT: "nooutline"
+; CHECK: attributes [[ATTR]] = {
+; CHECK-NOT: "nooutline"
+; CHECK-SAME: nooutline
+; CHECK-NOT: "nooutline"
+
+define void @f() "nooutline" {
+  ret void
+}
diff --git a/llvm/test/CodeGen/AArch64/machine-outliner-mapper-debug-output.mir b/llvm/test/CodeGen/AArch64/machine-outliner-mapper-debug-output.mir
index 826157e68d75c..056c5f18d492c 100644
--- a/llvm/test/CodeGen/AArch64/machine-outliner-mapper-debug-output.mir
+++ b/llvm/test/CodeGen/AArch64/machine-outliner-mapper-debug-output.mir
@@ -17,7 +17,7 @@
 
 --- |
   define void @block_too_small() noredzone { unreachable }
-  define void @no_outline() noredzone "nooutline" { unreachable }
+  define void @no_outline() noredzone nooutline { unreachable }
   define void @redzone() { unreachable }
   declare void @no_mf()
   define void @block_addr_fn() noredzone {
diff --git a/llvm/test/Transforms/IROutliner/nooutline-attribute.ll b/llvm/test/Transforms/IROutliner/nooutline-attribute.ll
index eaf3afa3b15a1..a61a1614a3115 100644
--- a/llvm/test/Transforms/IROutliner/nooutline-attribute.ll
+++ b/llvm/test/Transforms/IROutliner/nooutline-attribute.ll
@@ -8,7 +8,7 @@
 
 define void @outlinable() { ret void }
 
-define i8 @nooutline1(ptr noalias %s, ptr noalias %d, i64 %len) "nooutline" {
+define i8 @nooutline1(ptr noalias %s, ptr noalias %d, i64 %len) nooutline {
   %a = load i8, ptr %s
   %b = load i8, ptr %d
   call void @llvm.memcpy.p0.p0.i64(ptr %d, ptr %s, i64 %len, i1 false)
@@ -17,7 +17,7 @@ define i8 @nooutline1(ptr noalias %s, ptr noalias %d, i64 %len) "nooutline" {
   ret i8 %ret
 }
 
-define i8 @nooutline2(ptr noalias %s, ptr noalias %d, i64 %len) "nooutline" {
+define i8 @nooutline2(ptr noalias %s, ptr noalias %d, i64 %len) nooutline {
   %a = load i8, ptr %s
   %b = load i8, ptr %d
   call void @llvm.memcpy.p0.p0.i64(ptr %d, ptr %s, i64 %len, i1 false)

This change turns the `"nooutline"` attribute into an enum attribute
called `nooutline`, and adds an auto-upgrader for bitcode to make the
same change to existing IR.

This IR attribute disables both the Machine Outliner (enabled at Oz for
some targets), and the IR Outliner (disabled by default).
@lenary lenary force-pushed the users/lenary/llvm-nooutline-enum-attr branch from 75103d1 to a84c645 Compare October 16, 2025 20:51
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants