Skip to content

Devirtualize calls to protocol composition type methods #28043

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 40 commits into from
Feb 18, 2020

Conversation

zoecarver
Copy link
Contributor

This patch devirtualizes calls to methods of protocol composition types when those types are composed of a class and method. (when possible) It removes init_existential_ref and open_existential_ref instructions (existentials/existential unwrapping). This also allows for optimization of the methods directly (inlining, etc.).

Here's an example:
Before:

// witnessEntryPoint(c:)
sil @$s3run17witnessEntryPoint1cSiAA6ClassBC_tF : $@convention(thin) (@guaranteed ClassB) -> Int {
// %0                                             // users: %2, %1
bb0(%0 : $ClassB):
  debug_value %0 : $ClassB, let, name "c", argno 1 // id: %1
  %2 = init_existential_ref %0 : $ClassB : $ClassB, $ClassA<String> & ProtocolA // users: %4, %3
  debug_value %2 : $ClassA<String> & ProtocolA, let, name "x", argno 1 // id: %3
  %4 = open_existential_ref %2 : $ClassA<String> & ProtocolA to $@opened("154CD8E6-FE7C-11E9-9975-ACBC329C418B") ClassA<String> & ProtocolA // users: %8, %7, %6, %5
  %5 = alloc_stack $@opened("154CD8E6-FE7C-11E9-9975-ACBC329C418B") ClassA<String> & ProtocolA // type-defs: %4; users: %9, %8, %6
  store %4 to %5 : $*@opened("154CD8E6-FE7C-11E9-9975-ACBC329C418B") ClassA<String> & ProtocolA // id: %6
  %7 = witness_method $@opened("154CD8E6-FE7C-11E9-9975-ACBC329C418B") ClassA<String> & ProtocolA, #ProtocolA.foo!1 : <Self where Self : ProtocolA> (Self) -> () -> Int, %4 : $@opened("154CD8E6-FE7C-11E9-9975-ACBC329C418B") ClassA<String> & ProtocolA : $@convention(witness_method: ProtocolA) <τ_0_0 where τ_0_0 : ProtocolA> (@in_guaranteed τ_0_0) -> Int // type-defs: %4; user: %8
  %8 = apply %7<@opened("154CD8E6-FE7C-11E9-9975-ACBC329C418B") ClassA<String> & ProtocolA>(%5) : $@convention(witness_method: ProtocolA) <τ_0_0 where τ_0_0 : ProtocolA> (@in_guaranteed τ_0_0) -> Int // type-defs: %4; user: %10
  dealloc_stack %5 : $*@opened("154CD8E6-FE7C-11E9-9975-ACBC329C418B") ClassA<String> & ProtocolA // id: %9
  return %8 : $Int                                // id: %10
} // end sil function '$s3run17witnessEntryPoint1cSiAA6ClassBC_tF'

After:

// witnessEntryPoint(c:)
sil @$s3run17witnessEntryPoint1cSiAA6ClassBC_tF : $@convention(thin) (@guaranteed ClassB) -> Int {
bb0(%0 : $ClassB):
  %1 = integer_literal $Builtin.Int64, 10         // user: %2
  %2 = struct $Int (%1 : $Builtin.Int64)          // user: %3
  return %2 : $Int                                // id: %3
} // end sil function '$s3run17witnessEntryPoint1cSiAA6ClassBC_tF'

Partially resolves SR-11695 (doesn't optimize the overridden class -- I can work on that in another PR).

Once reviewed, can someone with commit access run swift-ci to test and benchmark, please?

@zoecarver
Copy link
Contributor Author

cc @atrick

auto firstUse = *instance->getUses().begin();
if (auto *store = dyn_cast<StoreInst>(firstUse->getUser())) {
openedVal = store->getSrc();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The existential value stored on the stack here might not be the same value on the stack when it is opened.

Also, the value stored on the stack might no longer be live at the apply site.

There's a whole bunch of complexity in getAddressOfStackInit and canReplaceCopiedValue to deal with on-stack existentials.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, I think I understand your first comment but, could you elaborate on the following?

Also, the value stored on the stack might no longer be live at the apply site.

I've updated line 222 to use getAddressOfStackInit(..., firstUse->getUser(), ...) instead of a dyn_cast<StoreInst>(...). Do you think this will be sufficient?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the abstract, we want to avoid producing a sequence like

r = open_existential_ref
destroy(r)
apply(r)

That shouldn't actually be a problem though because we're not in OSSA form so there will only be 'retain/release_value(r)' operations. As long as none of the retains get eliminated during this transformation, it should be safe.

A lot of the complexity in the existing code comes from the fact that it's handling 'open_existential_addr', which can be destroyed with 'destroy_addr'.

The main thing here is to prove that the stored value is the value used by the apply. One way to do that is to recognize all uses of the alloc_stack and make sure only one of them is a store. getStackInitInst already does that, but only handles copy_addr, not store.

Please try to write some .sil test cases to for corner cases.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you, that was extremely helpful.

A lot of the complexity in the existing code comes from the fact that it's handling 'open_existential_addr', which can be destroyed with 'destroy_addr'.

The constructor I modified does handle open_existential_addr but the specific optimization I'm working on should only handle open_existential_ref, right?

The main thing here is to prove that the stored value is the value used by the apply. One way to do that is to recognize all uses of the alloc_stack and make sure only one of them is a store.

I've added more checking around the above code but, I suspect it isn't quite right yet. Should the stack alloc instruction ever be used in more than the initial store, the apply, and then the dealloc instructions? If not it may make it a bit easier to filter out what we can optimize. But that might be too harsh of a filter.

Anyway, I've added many more tests and have uncovered several bugs. Right now I'm in the process of testing those bugs against master to see if they're my issue and if so, fixing them. I'm going to be pretty busy this week with other things so, turn around on this patch might be a bit slow.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@atrick I've added a dominance order check which should ensure that there aren't those kinds of issues. I've also added one .sil test. I plan on writing some more, what other edge cases should I cover?

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Developing a breadth of .sil test cases is extremely helpful. That's what this code was missing.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want to generalize this pattern match, I think it should be part of the existing utilities so there's no new code in this function. getStackInitInst should be extended so it can return a store. It already checks for dominance of ASIUser. It's a bit more general than your new code. Whether it should set isCopied in that case is very subtle. The purpose of that flag is to make sure the original value isn't destroyed before the apply that needs to use it. In the case of a store it's simpler of we do not set isCopied, but then we should assert that the store's ownership qualifier is Unqualified (non-OSSA) and comment when we support OSSA here, we need to insert a new copy of the value before the store (and make sure that copy is destroyed when replacing the apply operand).

FYI: in non-OSSA it should just work. If we have

retain val
store val to address
destroy val
use address
destroy_addr

It's actually ok in non-OSSA to reuse the original value like this:

retain val
store val to address
destroy val
use val
destroy_addr

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea-- that's much cleaner. Also, thanks for the information about OSSA/non-OSSA and when copies are needed. Very interesting.

assert(srcTy.isAddress() == destTy.isAddress()
&& "Addresses aren't compatible with values");
if (srcTy.isAddress() == destTy.isAddress())
return nullptr;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is this change intentional?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. This assertion was triggered after the above change. I suspect some other optimization caused the store instruction to trigger this assertion. This was the simplest fix but, maybe I should try to do some more digging.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It's not part of this APIs contract to return nullptr and it doesn't seem to be anticipated by its callers.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am now thinking that this assertion shouldn't exist. The next condition srcTy.isAddress() && destTy.isAddress() can never succeed if this assertion is triggered (if bool != bool then bool && bool == false).

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is valid code
assert(A == B); if (A && B)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, you're right. I got confused for a second :P

I'm not sure what the best solution is, but it seems to be OK when I remove the assertion (not a reason on its own to remove the assertion, though).

assert(srcTy.isAddress() == destTy.isAddress()
&& "Addresses aren't compatible with values");
if (srcTy.isAddress() == destTy.isAddress())
return nullptr;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is valid code
assert(A == B); if (A && B)

auto firstUse = *instance->getUses().begin();
if (auto *store = dyn_cast<StoreInst>(firstUse->getUser())) {
openedVal = store->getSrc();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

In the abstract, we want to avoid producing a sequence like

r = open_existential_ref
destroy(r)
apply(r)

That shouldn't actually be a problem though because we're not in OSSA form so there will only be 'retain/release_value(r)' operations. As long as none of the retains get eliminated during this transformation, it should be safe.

A lot of the complexity in the existing code comes from the fact that it's handling 'open_existential_addr', which can be destroyed with 'destroy_addr'.

The main thing here is to prove that the stored value is the value used by the apply. One way to do that is to recognize all uses of the alloc_stack and make sure only one of them is a store. getStackInitInst already does that, but only handles copy_addr, not store.

Please try to write some .sil test cases to for corner cases.

return shouldOptimize1(c)
}

// TODO: create SR -- this causes a crash on master too
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's an example. This crashes because of the issue #28012 resolved but even after that commit is applied, it still crashes because of another issue.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch. Now that you've been looking at this code, can you see what the problem is?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll take another look and see if I can figure it out.

@@ -907,7 +907,7 @@ SILCombiner::buildConcreteOpenedExistentialInfoFromSoleConformingType(
(archetypeTy->getConformsTo().size() == 1)) {
PD = archetypeTy->getConformsTo()[0];
} else if (ArgType.isExistentialType() && !ArgType.isAnyObject() &&
!SwiftArgType->isAny()) {
!SwiftArgType->isAny() && SwiftArgType->getAnyNominal()) {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Woops, didn't rebase off master.


public func test(x: B, y: C) -> Int

// CHECK-LABEL: sil shared [noinline] @${{.*}}impl{{.*}}
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's the relevant bit of this test.

@zoecarver zoecarver requested a review from atrick December 1, 2019 18:57
@zoecarver
Copy link
Contributor Author

@atrick friendly ping.

@atrick
Copy link
Contributor

atrick commented Dec 12, 2019

@zoecarver sorry I keep getting side tracked. This is next on my list to review.

@zoecarver
Copy link
Contributor Author

@atrick don't worry about it. Thanks for the update.

Copy link
Contributor

@atrick atrick left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It looks like you put a lot of work into this. Thanks. There are a few small cleanups below and an optional suggestion for generalizing the implementation.

store = foundStore;
} else if (auto *foundApply = dyn_cast<ApplyInst>(use->getUser())) {
apply = foundApply;
} else if (auto *foundDealloc = dyn_cast<DeallocStackInst>(use->getUser())) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

80-col violation

for (auto *use : instance->getUses()) {
if (auto *foundStore = dyn_cast<StoreInst>(use->getUser())) {
store = foundStore;
} else if (auto *foundApply = dyn_cast<ApplyInst>(use->getUser())) {
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You can just check that user->getUser() == user

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Do you mean use->getUser() == user? How would that check be useful? Wouldn't it fail if use (the argument) and use (the for-loop element) were different uses of the alloc_stack instruction?

I'm going to rename some of these variables, I didn't realize there were two use variables.

auto firstUse = *instance->getUses().begin();
if (auto *store = dyn_cast<StoreInst>(firstUse->getUser())) {
openedVal = store->getSrc();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Developing a breadth of .sil test cases is extremely helpful. That's what this code was missing.

auto firstUse = *instance->getUses().begin();
if (auto *store = dyn_cast<StoreInst>(firstUse->getUser())) {
openedVal = store->getSrc();
}
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If you want to generalize this pattern match, I think it should be part of the existing utilities so there's no new code in this function. getStackInitInst should be extended so it can return a store. It already checks for dominance of ASIUser. It's a bit more general than your new code. Whether it should set isCopied in that case is very subtle. The purpose of that flag is to make sure the original value isn't destroyed before the apply that needs to use it. In the case of a store it's simpler of we do not set isCopied, but then we should assert that the store's ownership qualifier is Unqualified (non-OSSA) and comment when we support OSSA here, we need to insert a new copy of the value before the store (and make sure that copy is destroyed when replacing the apply operand).

FYI: in non-OSSA it should just work. If we have

retain val
store val to address
destroy val
use address
destroy_addr

It's actually ok in non-OSSA to reuse the original value like this:

retain val
store val to address
destroy val
use val
destroy_addr

return shouldOptimize1(c)
}

// TODO: create SR -- this causes a crash on master too
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great catch. Now that you've been looking at this code, can you see what the problem is?

dealloc_stack %5 : $*@opened("A1F1B526-1463-11EA-932F-ACBC329C418B") A & P
return %8 : $Int
} // end sil function 'impl'

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We certainly want as many optimizer issues as possible covered by SIL tests, but the tests themselves should be minimal. The maintenance burden of these tests is a primary reason that it's so difficult to make SIL changes. It seems like everything after this point should be deleted, except maybe the SIL declaration of "foo". You don't need to include bodies of called SIL functions.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

You're right. I removed much of this sil test. Any other test cases I should cover?

@zoecarver
Copy link
Contributor Author

@atrick thank you for the review, I really appreciate all the info you gave me here. I might not get to updating this until next week, just FYI.

@atrick
Copy link
Contributor

atrick commented Feb 4, 2020

@swift-ci test

@swift-ci
Copy link
Contributor

swift-ci commented Feb 4, 2020

Build failed
Swift Test Linux Platform
Git Sha - bdd9480

@zoecarver
Copy link
Contributor Author

OK, at least I don't have to fix anything :P

@swift-ci
Copy link
Contributor

swift-ci commented Feb 4, 2020

Build failed
Swift Test OS X Platform
Git Sha - bdd9480

@zoecarver
Copy link
Contributor Author

Looks like all the tests passed. Maybe you could trigger the benchmarks again and if it looks good to you, can you merge it for me?

Thanks again for all the help with this patch, @atrick!

@@ -914,7 +924,29 @@ SILInstruction *SILCombiner::createApplyWithConcreteType(
});
}

if (!UpdatedArgs) {
bool canUpdateArgs, madeUpdate;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This whole section of new code was confusing at first. I think what it's doing is determining both whether it is possible to rewrite the call at all and whether at least one argument has changed. If not, avoid creating a new call and delete any temporary instructions to avoid the top-level SILCombine driver from infinitely attempting to optimize. If that's right, then the code looks good but please explain that in a comment before new code.

I'm not sure why it's a written as a closure. The results need to be exposed anyway and there's not much else to hide here.

Copy link
Contributor Author

@zoecarver zoecarver Feb 4, 2020

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, that's right. I'll factor it out and add a comment.

Copy link
Contributor

@atrick atrick left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I requested a comment, but otherwise this PR looks good now.

@zoecarver
Copy link
Contributor Author

@atrick thanks, as I've said before, I really appreciate all your reviews of this patch. I updated it based on your comment.

@atrick
Copy link
Contributor

atrick commented Feb 5, 2020

@zoecarver migrating passes to OSSA is one of the most useful things that can be done. I'm not sure which pass is easiest to start with. It's something we'll be figuring over the next few months. @gottesmm is looking into it.

@atrick
Copy link
Contributor

atrick commented Feb 5, 2020

@swift-ci smoke test

@atrick
Copy link
Contributor

atrick commented Feb 5, 2020

@zoecarver thanks for iterating on this to get it right!

@zoecarver
Copy link
Contributor Author

@zoecarver thanks for iterating on this to get it right!

Of course.

@zoecarver migrating passes to OSSA is one of the most useful things that can be done. I'm not sure which pass is easiest to start with. It's something we'll be figuring over the next few months. @gottesmm is looking into it.

I think I remember @gottesmm saying that it would be hard to find a good pass for me to work on but, let me know if that changes, or if there are other things that I can do that would be helpful.

@zoecarver
Copy link
Contributor Author

@atrick could you also run the benchmarks? The benchmark I added should now work properly.

@atrick
Copy link
Contributor

atrick commented Feb 5, 2020

@swift-ci benchmark

@swift-ci
Copy link
Contributor

swift-ci commented Feb 5, 2020

Performance: -O

Added MIN MAX MEAN MAX_RSS
DevirtualizeProtocolComposition 22 22 22

Code size: -O

Performance: -Osize

Regression OLD NEW DELTA RATIO
ObjectiveCBridgeStubDateAccess 130 152 +16.9% 0.86x (?)
 
Added MIN MAX MEAN MAX_RSS
DevirtualizeProtocolComposition 39 39 39

Code size: -Osize

Performance: -Onone

Added MIN MAX MEAN MAX_RSS
DevirtualizeProtocolComposition 231079 241556 235573

Code size: -swiftlibs

Benchmark Check Report
⛔️⏱ DevirtualizeProtocolComposition has setup overhead of 4 μs (17.4%).
Move initialization of benchmark data to the setUpFunction registered in BenchmarkInfo.
⚠️ DevirtualizeProtocolComposition execution took 19 μs.
Increase the workload of DevirtualizeProtocolComposition to be more than 20 μs.
How to read the data The tables contain differences in performance which are larger than 8% and differences in code size which are larger than 1%.

If you see any unexpected regressions, you should consider fixing the
regressions before you merge the PR.

Noise: Sometimes the performance results (not code size!) contain false
alarms. Unexpected regressions which are marked with '(?)' are probably noise.
If you see regressions which you cannot explain you can try to run the
benchmarks again. If regressions still show up, please consult with the
performance team (@eeckstein).

Hardware Overview
  Model Name: Mac mini
  Model Identifier: Macmini8,1
  Processor Name: Intel Core i7
  Processor Speed: 3.2 GHz
  Number of Processors: 1
  Total Number of Cores: 6
  L2 Cache (per Core): 256 KB
  L3 Cache: 12 MB
  Memory: 64 GB

return sum
}

func shouldOptimize1<T>(_ x: ClassA<T> & ProtocolA, count: Int) -> Int {
Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'll fix the names here.

@atrick
Copy link
Contributor

atrick commented Feb 7, 2020

@swift-ci smoke test

@atrick
Copy link
Contributor

atrick commented Feb 7, 2020

@swift-ci benchmark

@swift-ci
Copy link
Contributor

swift-ci commented Feb 7, 2020

Performance: -O

Regression OLD NEW DELTA RATIO
FlattenListFlatMap 7200 10162 +41.1% 0.71x (?)
FlattenListLoop 4450 5651 +27.0% 0.79x (?)
RemoveWhereMoveInts 36 39 +8.3% 0.92x (?)
ObjectiveCBridgeStubFromNSDateRef 3740 4050 +8.3% 0.92x (?)
Array2D 7520 8112 +7.9% 0.93x (?)
 
Added MIN MAX MEAN MAX_RSS
DevirtualizeProtocolComposition 291 293 292

Code size: -O

Performance: -Osize

Regression OLD NEW DELTA RATIO
FlattenListFlatMap 5612 7048 +25.6% 0.80x (?)
ObjectiveCBridgeStubDateAccess 228 257 +12.7% 0.89x (?)
FlattenListLoop 4841 5289 +9.3% 0.92x (?)
 
Improvement OLD NEW DELTA RATIO
ObjectiveCBridgeStubDateMutation 285 257 -9.8% 1.11x
RandomShuffleLCG2 832 768 -7.7% 1.08x (?)
Array2D 8112 7520 -7.3% 1.08x (?)
ArrayPlusEqualFiveElementCollection 9065 8436 -6.9% 1.07x
 
Added MIN MAX MEAN MAX_RSS
DevirtualizeProtocolComposition 308 311 309

Code size: -Osize

Performance: -Onone

Improvement OLD NEW DELTA RATIO
ObjectiveCBridgeStubFromNSDateRef 4710 4310 -8.5% 1.09x (?)
StringBuilderWithLongSubstring 2610 2420 -7.3% 1.08x (?)
 
Added MIN MAX MEAN MAX_RSS
DevirtualizeProtocolComposition 6530 6581 6548

Code size: -swiftlibs

Benchmark Check Report
How to read the data The tables contain differences in performance which are larger than 8% and differences in code size which are larger than 1%.

If you see any unexpected regressions, you should consider fixing the
regressions before you merge the PR.

Noise: Sometimes the performance results (not code size!) contain false
alarms. Unexpected regressions which are marked with '(?)' are probably noise.
If you see regressions which you cannot explain you can try to run the
benchmarks again. If regressions still show up, please consult with the
performance team (@eeckstein).

Hardware Overview
  Model Name: Mac Pro
  Model Identifier: MacPro6,1
  Processor Name: 12-Core Intel Xeon E5
  Processor Speed: 2.7 GHz
  Number of Processors: 1
  Total Number of Cores: 12
  L2 Cache (per Core): 256 KB
  L3 Cache: 30 MB
  Memory: 64 GB

@zoecarver
Copy link
Contributor Author

@atrick friendly ping. Anything else I need to do with this?

@atrick
Copy link
Contributor

atrick commented Feb 17, 2020

@swift-ci smoke test

@zoecarver
Copy link
Contributor Author

Looks like a swiftpm issue.

@theblixguy
Copy link
Collaborator

@swift-ci please smoke test Linux

@zoecarver
Copy link
Contributor Author

@theblixguy thanks!

@atrick
Copy link
Contributor

atrick commented Feb 18, 2020

@swift-ci smoke test linux

@atrick atrick merged commit 1486290 into swiftlang:master Feb 18, 2020
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

5 participants