-
Notifications
You must be signed in to change notification settings - Fork 10.5k
[WIP][CodeCompletion] Migrate argument position completion to the solver-based implementation #38266
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
base: main
Are you sure you want to change the base?
[WIP][CodeCompletion] Migrate argument position completion to the solver-based implementation #38266
Conversation
@swift-ci please test |
4a2d8c4
to
6ddfa4b
Compare
@swift-ci please test |
@swift-ci please smoke test |
@swift-ci please build toolchain macOS |
macOS Toolchain Install command |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Solver changes look good to me, I have left a couple of minor comments inline.
lib/Sema/CSSimplify.cpp
Outdated
Type nextArgTy = args[nextArgIdx].getPlainType(); | ||
if (auto *TVT = nextArgTy->getAs<TypeVariableType>()) { | ||
if (TVT->getImpl().isCodeCompletionToken() && | ||
TVT->getASTContext().CompletionCallback) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Maybe remove CompletionCallback
and wrap whole block (starting at line 346) into if (cs.isForCodeCompletion())
instead?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
matchCallArgumentssImpl
doesn't have cs
currently. Should I pass it in? I think it'd need to be added to constraints::matchCallArguments
and areConservativelyCompatibleArgumentLabels
too.
lib/Sema/ConstraintSystem.cpp
Outdated
increaseScore(SK_KeyPathSubscript); | ||
// ...except if we're solving for code completion and the index expression | ||
// contains the completion location | ||
auto SE = dyn_cast_or_null<SubscriptExpr>(locator->getAnchor().dyn_cast<Expr*>()); |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could be simplified down to getAsExpr<SubscriptExpr>(locator->getAnchor())
@swift-ci please test |
@swift-ci please test |
Build failed |
@swift-ci please test Linux platform |
Build failed |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Solver related changes look good to me! Left a couple of minor comments inline.
lib/Sema/CSSimplify.cpp
Outdated
} | ||
|
||
static bool | ||
isAfterCompletionToken(Optional<CompletionArgInfo> info, unsigned argIdx) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Should this be a method on CompletionArgInfo
instead?
lib/Sema/CSSimplify.cpp
Outdated
// with the same impact as that for the fix to ignore arguments after the | ||
// completion position when matching arguments. | ||
if (isForCodeCompletion()) { | ||
auto getArgIndex = [&](ConstraintLocator *loc) -> Optional<unsigned> { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Could you please make it work on ConstraintLocatorBuilder
instead of allocating a new locator to extract index?
lib/Sema/CSSimplify.cpp
Outdated
// position once. | ||
bool isFirst = llvm::all_of(getFixes(), [&](const ConstraintFix *fix) { | ||
auto *fixLocator = fix->getLocator(); | ||
if (fixLocator->getAnchor() != locator.getAnchor()) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think you'd want to check the kind of fix here as well, otherwise it's possible to find some unrelated fix that has ApplyArgToParam
in the path and use it.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Thanks a lot for tackling this @nathawes! I’ve added a few comments/questions regarding the code inline. I haven’t had a chance to look at the test cases yet. I’ll do that hopefully tomorrow.
/// The type associated with the code completion expression itself. | ||
Type ExpectedType; | ||
/// The innermost call expression containing the completion location. | ||
Expr *CallE; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the name CallE
is a bit confusing because I would expecting CallE
to be a CallExpr
but it can also be a SubscriptExpr
.
It appears like you are only using it to check if the call is a subscript. Wouldn’t it be easier to just have a boolean flag?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah, that's nicer and sidesteps the naming. Thanks!
/// The innermost call expression containing the completion location. | ||
Expr *CallE; | ||
/// The FuncDecl or SubscriptDecl associated with the call. | ||
Decl *FuncD; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Just to provide a little bit more context and compile-time safety, I think this could be a ValueDecl
instead of a Decl
.
/// True if the completion is a noninitial term in a variadic argument. | ||
bool IsNoninitialVariadic; | ||
/// The base type of the call. | ||
Type BaseType; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
If we are completing on a free function, is BaseType
a nullptr
? I think it might be worth adding documentation for that.
Optional<unsigned> firstTrailingIdx; | ||
unsigned argCount; | ||
|
||
/// \returns true if the given argument index is possibly about to be written |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Nitpick: Two spaces after true
lib/Sema/CSSimplify.cpp
Outdated
// missing label. Completion will suggest the label in such cases. | ||
if (completionInfo && nextArgIdx < numArgs && | ||
args[nextArgIdx].getLabel().empty()) { | ||
if (isCodeCompletionTypeVar(args[nextArgIdx].getPlainType())) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Style: I think you can just merge this condition with the previous if
.
lib/Sema/TypeCheckCodeCompletion.cpp
Outdated
// solver can't know what kind of keypath is expected (e.g. KeyPath vs | ||
// WritableKeyPath) so it ends up as a hole. Just assume KeyPath so we show | ||
// the root type to users. | ||
if (SelectedOverload->choice.getKind() == OverloadChoiceKind::KeyPathApplication) { |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I don’t quite understand when we need this special case. Do you have a test case at hand that fails without it? I haven’t had a chance to look at all of them yet.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This was for an existing test case in IDE/complete_subscript.swift:
let _ = MyStruct<Int>()[#^INSTANCE_INT_BRACKET^#
// INSTANCE_INT_BRACKET: Begin completions
// INSTANCE_INT_BRACKET-DAG: Decl[Subscript]/CurrNominal/Flair[ArgLabels]: ['[']{#(x): Int#}, {#instance: Int#}[']'][#Int#];
// INSTANCE_INT_BRACKET-DAG: Pattern/CurrNominal/Flair[ArgLabels]: ['[']{#keyPath: KeyPath<MyStruct<Int>, Value>#}[']'][#Value#];
// INSTANCE_INT_BRACKET: End completions
}
Without it the second result has no param or result type information (both are UnresolvedType), so it ends up being printed something like the below from memory:
['[']{#keyPath: _#}[']'][#_#];
I'll update the comment (it should say subscript rather than method too).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
OK, that makes sense. But wouldn’t the betters approach be that the constraint solver yields two solutions to us. IIUC, we implicitly have the following two subscripts defined on every type
subscript<Value>(keyPath keyPath: KeyPath<Self, Value>) -> Value { get }
subscript<Value>(keyPath keyPath: WritableKeyPath<Self, Value>) -> Value { get set }
so I would expect the constraint solver to return two solutions (one for KeyPath
and one for WritableKeyPath
).
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I suspect it's not actually implemented like that, since regular overloaded subscripts do give us two solutions. @xedin do you know how keypath application is set up in the solver at the moment?
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Unfortunately solver can't do anything about this. subscript[keyPath: ...]
is not a regular overload so there is no parameter information for it, instead it infers whether it's KeyPath
or WritableKeyPath
based on the argument and in case of code completion it can't do that because there is no argument, so Nathan had to hardcode it to KeyPath to be safe if I remember correctly.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Ah, OK. That’s unfortunate but probably the best solution for now.
lib/IDE/CodeCompletion.cpp
Outdated
return true; | ||
} | ||
|
||
bool showGlobalCompletions = false; |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Style: Capitalize this variable to be consistent.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
And as promised, I’ve got a few comments on the test cases.
test/IDE/complete_ambiguous.swift
Outdated
|
||
// ARG_NO_LABEL_PREV_WRONG: Begin completions, 2 items | ||
// ARG_NO_LABEL_PREV_WRONG-DAG: Pattern/Local/Flair[ArgLabels]: {#c: String#}[#String#]; name=c: String | ||
// ARG_NO_LABEL_PREV_WRONG-DAG: Pattern/Local/Flair[ArgLabels]: {#d: String#}[#String#]; name=d: String |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What’s the rationale for not suggesting b
here? I could see an argument for not suggesting it because wrongLabel
matches b
by positional matching but by that reasoning we also shouldn’t suggest c
.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yeah this seems wrong looking at it again. It's not matching positionally, the b overload is considered worse in the scoring because it fails to find an arg for b after a (and b is required) and then reports wrongLabel and the completion as extraneous arguments. The c:d: overload has nothing missing because c and d are defaulted, so just sees wrongLabel and the completion as extraneous and gets a better score. We don't actually bind any parameter to the completion in either case so it hits my fallback logic (needed for the existing OVERLOAD_LABEL2
case in test/IDE/completion_call_arg.swift) that binds it to the first unfulfilled parameter after the last fulfilled one (if there is one). I think there's a few things to try:
- Update the matching logic to somehow skip over the bad label when binding, so there's no missing param, the completion arg is bound in both overloads, and they both just have one extraneous argument (and end up with the same score). This might have some undesirable consequences elsewhere though.
- Just give up when we don't get a binding for the completion, giving no results in this case and the existing
OVERLOAD_LABEL2
case (regressing it, but perhaps understandably?). - Change the filtering logic to keep all invalid solutions when there are no valid ones, rather than just the best one. From memory that caused some tests to fail, mostly around completion after == because it has tons of overloads and the fix to allow type mismatches made them all viable regardless of the type on the LHS. I suspect it might also give us some nonsense results for solutions with tons of fixes.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Got option 1 working without any other impact.
test/IDE/complete_call_arg.swift
Outdated
let _ = obj.hasError(a1: #^HASERROR2^# | ||
let _ = obj.hasError(a1: IC1, #^HASERROR3^# | ||
let _ = obj.hasError(a1: #^HASERROR2?xfail=FIXME^# | ||
let _ = obj.hasError(a1: IC1, #^HASERROR3?xfail=FIXME^# |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
What’s the issue why these need to be xfailed? Is the type checker just bailing out completely because of the invalid type? I think we should at least have a bug report for the xfail.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think it's a general solver-based completion issue. The lookup the solver does to get the overloads does an isValid() check on the decls it finds and ignores them if they're not. I can change it to let them through when solving for code completion but because it changes what was previously an invariant I wanted to leave that as a separate change if you/Rintaro are ok with the regression in the meantime. I'll file a bug if so, or try to fix it here if not.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
I think the regression is OK if it only applies if the function signature contains an error type. Would be something that we should fix at some point but I think it’s OK to do it in another PR. I’d just like a bug report so we don’t forget about it 😉
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Awesome, I'm add it in the next push!
aadb18b
to
94a817e
Compare
@swift-ci please smoke test |
@swift-ci please build toolchain macOS |
@swift-ci please smoke test |
@swift-ci please build toolchain macOS |
Once the other tests passed, I’d also like to run the stress tester to make sure we aren’t introducing any unknown regressions through this change. |
@swift-ci Please smoke test macOS |
macOS Toolchain Install command |
@swift-ci Please SourceKit stress test |
The stress tester found 353 new issues (one unexpected failure in key path completion is known but not in XFail yet). Would you mind taking a look at them? It looks to me like a lot of semantic functionality is failing if the source file contains an uncompleted call. Note that the stress tester now continues execution on failure. The way I currently reduce stress tester failures is to give the project I’m interested in a new tag and then run the stress tester with the env SK_STRESS_FILE_FILTER='/ItemDetailInfoView.swift' DEVELOPER_DIR='/Applications/Xcode-GPE.app' CODE_SIGN_IDENTITY= CODE_SIGNING_REQUIRED=NO ENTITLEMENTS_REQUIRED=NO ENABLE_BITCODE=NO INDEX_ENABLE_DATA_STORE=NO GCC_TREAT_WARNINGS_AS_ERRORS=NO SWIFT_TREAT_WARNINGS_AS_ERRORS=NO ./run_sk_stress_test --filter-by-tag 'achn' --swiftc ~/swift-src/nbuild/toolchains/Release+Asserts/usr/local/bin/swiftc --skip-tools-clone --skip-tools-build main --add-xcodebuild-flags 'ARCHS=arm64 "OTHER_SWIFT_FLAGS=$(OTHER_SWIFT_FLAGS) -swift-version 5 -disallow-use-new-driver"' From there I extract the build argument and run |
253d15e
to
b60add0
Compare
@swift-ci please test |
1 similar comment
@swift-ci please test |
@swift-ci please SourceKit stress test |
Nice, this fixes three big-ish XFails in the stress tester 🥳 Could you also add their associated test cases and mark them as fixed on bugs.swift.org once this PR is merged? (SR-14693, SR-14705, SR-14888) Still a couple of XFails though. The |
I just took a look at the code completion performance measurements taken by the stress tester and it looks like there are some notable regressions. E.g. |
…he solver-based implementation. This hooks up call argument position completion to the typeCheckForCodeCompletion API to generate completions from all the solutions the constraint solver produces (even those requiring fixes), rather than relying on a single solution being applied to the AST (if any).
b60add0
to
85fea89
Compare
@swift-ci please test |
1 similar comment
@swift-ci please test |
@swift-ci please SourceKit stress test |
@swift-ci please build toolchain macOS |
macOS Toolchain Install command |
There are still 56 stress tester failures 😬 |
No description provided.