-
Notifications
You must be signed in to change notification settings - Fork 434
Dyno: move to a more principled resolution strategy #28006
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?
Conversation
Signed-off-by: Danila Fedorin <[email protected]>
Signed-off-by: Danila Fedorin <[email protected]>
Signed-off-by: Danila Fedorin <[email protected]>
Signed-off-by: Danila Fedorin <[email protected]>
Signed-off-by: Danila Fedorin <[email protected]>
Signed-off-by: Danila Fedorin <[email protected]>
Signed-off-by: Danila Fedorin <[email protected]>
Signed-off-by: Danila Fedorin <[email protected]>
Signed-off-by: Danila Fedorin <[email protected]>
Signed-off-by: Danila Fedorin <[email protected]>
Signed-off-by: Danila Fedorin <[email protected]>
Signed-off-by: Danila Fedorin <[email protected]>
This is a more conservative approach to tracking all errors encountered in all queries involved in resolving an AST, since that second approach is way too broad and coarse. Signed-off-by: Danila Fedorin <[email protected]>
Signed-off-by: Danila Fedorin <[email protected]>
Signed-off-by: Danila Fedorin <[email protected]>
Signed-off-by: Danila Fedorin <[email protected]>
Signed-off-by: Danila Fedorin <[email protected]>
Signed-off-by: Danila Fedorin <[email protected]>
Signed-off-by: Danila Fedorin <[email protected]>
Signed-off-by: Danila Fedorin <[email protected]>
Signed-off-by: Danila Fedorin <[email protected]>
Signed-off-by: Danila Fedorin <[email protected]>
Signed-off-by: Danila Fedorin <[email protected]>
Signed-off-by: Danila Fedorin <[email protected]>
Signed-off-by: Danila Fedorin <[email protected]>
Signed-off-by: Danila Fedorin <[email protected]>
Signed-off-by: Danila Fedorin <[email protected]>
Signed-off-by: Danila Fedorin <[email protected]>
Signed-off-by: Danila Fedorin <[email protected]>
Signed-off-by: Danila Fedorin <[email protected]>
Signed-off-by: Danila Fedorin <[email protected]>
Signed-off-by: Danila Fedorin <[email protected]>
9104783 to
22e9c53
Compare
Signed-off-by: Danila Fedorin <[email protected]>
Signed-off-by: Danila Fedorin <[email protected]>
Signed-off-by: Danila Fedorin <[email protected]>
Signed-off-by: Danila Fedorin <[email protected]>
Signed-off-by: Danila Fedorin <[email protected]>
Signed-off-by: Danila Fedorin <[email protected]>
Signed-off-by: Danila Fedorin <[email protected]>
Signed-off-by: Danila Fedorin <[email protected]>
Signed-off-by: Danila Fedorin <[email protected]>
Signed-off-by: Danila Fedorin <[email protected]>
Signed-off-by: Danila Fedorin <[email protected]>
…types Signed-off-by: Danila Fedorin <[email protected]>
Signed-off-by: Danila Fedorin <[email protected]>
Signed-off-by: Danila Fedorin <[email protected]>
Signed-off-by: Danila Fedorin <[email protected]>
Signed-off-by: Danila Fedorin <[email protected]>
Signed-off-by: Danila Fedorin <[email protected]>
Signed-off-by: Danila Fedorin <[email protected]>
Signed-off-by: Danila Fedorin <[email protected]>
benharsh
left a comment
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! Just a couple of questions.
|
|
||
| r->emittedErrors = errorCollectionStack.empty(); | ||
| r->emittedErrors = | ||
| errorCollectionStack.empty() || !errorCollectionStack.back().silenceErrors(); |
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.
Can silenceErrors() be different at different levels of the stack?
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.
Now that I think about it, although it can, looking at the top of the stack is not right. This field ought to track whether the user saw errors from this field. They didn't if any of the stack entries was silencing errors.
| return false; | ||
| } | ||
|
|
||
| bool Resolver::shouldUseUnknownTypeForGeneric(const ID& id) { |
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.
Can we get rid of this now?
|
|
||
| // Now, consider the formals | ||
| int nActuals = call->numActuals(); | ||
| int faActualIndex = 0; // faMap doesn't count '?' for actual indexing. |
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 wonder if we should change this (in a separate effort). I'm guessing this would have been motivated by the use of ? in type constructors, rather than function calls.
|
|
||
| instantiationState = anyFormalNeedsInstantiation(context, formalTypes, | ||
| untypedSignature, | ||
| &newSubstitutions); |
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.
Very nice!
Signed-off-by: Danila Fedorin <[email protected]>
Dyno, by its nature as a library, has been written to compute "as much as possible" when performing resolution. Even if one variable in a module is incorrectly declared (emitting an error), Dyno can proceed to resolve other variables in the module. Since Dyno doesn't immediately halt upon errors, to prevent cascading failures, Dyno's resolver will skip resolving calls when they have erroneous expressions as arguments, or whose arguments depend on erroneous expressions.
Initial Signatures
This skipping logic has been re-used for a second mechanism in Dyno: the 'initial function signature'. This is a kind of optimization. In the presence of generics, each call can in theory produce a different instantiation of a generic function. It would be costly to recompute instantiated functions for each call if we can tell "at a glance" that they will fail. For example:
Above, even though generic resolution dictates we need to go through each formal at a time to compute successive substitutions for
x,y, andz, the call is "destined to fail" since the last argument is a string, but the last formal is an integer. To short-circuit these cases, Dyno computes a single "initial signature" for any function, which doesn't external type information, and is by nature partial. In the case above, it will be roughly:Unknown types are permissively permitted to accept any actual. Checking initial signatures can be done quickly, without computing substitutions, just by testing
canPasson corresponding formals.To compute initial signatures in the presence of necessarily missing information (e.g., substitutions from the call), Dyno's skipping logic has been written to avoid resolving some calls. The precise definition of the skipping logic (captured in
shouldSkipCallResolution) has evolved over time; however, it roughly boiled down to skipping calls with generic actuals with some exceptions. These exceptions, however, grew to be very complicated.Problems with
shouldSkipCallResolutionCall calls, whether it be during initial signature computation or "regular" resolution, go through the skipping logic, at the very least because we still need to skip erroneous computations. However, this led to a tension between the two contexts in which the skipping logic is used. When computing initial signatures, we tend to err on the side of skipping. The worst that can happen when skipping a call is that we produce an
UnknownType, which allows the initial signature to be considered for "real" instantiation, and discarded there if necessary. However, when computing "final" signatures, or resolving elsewhere, we don't want to skip when we can help it: "real" functions can sometimes accept generic arguments (when they are types and params), and to match production, we eventually need to resolve them.This led to a list of workarounds in
shouldSkipCallResolutionwhich try to strike the right balance between skipping and not skipping, sometimes trying to divine nearby clues whether we are computing an initial or a final signature. PRs as recent as #27963 introduced new workarounds and heuristics.However, the bottom line is that in general, identical calls ought to be skipped in some contexts, and resolved in others. Consider this example:
The function
foodoesn't support generic types. Thus,foo(numeric)will produce a compiler error. This is a distillation of thechpl__buildArrayRuntimetype, which we've recently run into problems with. Notice that during the initial signature resolution ofbar, before the arguments are incorporated, thefoo(x)call isfoo(numeric). However, we shouldn't emit an error yet, since we are simply missing some information. However, duringbar(numeric), the same callfoo(numeric)in thewhereclause ought not to be skipped, and should be resolved and trigger the error. Thus, how we treatfoo(numeric)is context-sensitive.The new approach
To resolve the tension, I've decided to simply distinguish, in the
shouldSkipCallResolutionlogic, between calls in "initial" vs. "real" signatures. Outside of "initial" signatures (and, also, partial instantiations of types, which similarly work with partial information), this PR only skips call resolution for errors (the original motivating case).Since we can now make this distinction, this approach removes a lot of special cases in
shouldSkipCallResolutionthat served to "uphold the balance". It also removesshouldUseUnknownTypeForGeneric, which was an earlier form of what is now done by theshouldSkipCallResolutionmechanism.The PR adjusts the logic of
shouldSkipCallResolutionby investigating the question: when do we want to skip calls?foo(x)in example above, becausexis numeric.class?(which is a call?(class)), is skipped, becauseclassis generic. Intuitively this call shouldn't be skipped, sinceclassis well-defined and "static". On the other hand, 'x' may well be "dynamic", since it can receive a substitution from the call site.numericorclassare not skipped. This enables patterns likeclass?,owned myGenericType, and more. But what actuals/arguements can be affected by substitutions?(t1, t2)for generict1,t2. Tough this can make sense (in general, type constructors do not expect to be instantiated with incomplete information, and(t1, t2)is a type constructor), the tuple type constructor is particularly well-behaved, and we can do more. So, as a special case, allow tuples to be resolved with partial arguments. This is intended strictly as an optimization, so that we can reject initial signatures earlier. It's not intended to affect resolution semantics.isPartialResultfield onResolvedExpression, which corresponds to whether we skiped call resolution, OR would've skipped it if not making the semantic-preserving tuple improvement. This way, though(t1, t2)is resolved, calls likedoesntExpectPartial((t1, t2))are not untilt1andt2are substituted.I've applied this approach to both generic
params andtypes. All existing Dyno tests pass (which lock down previous workarounds' goals), as does the previously-failing testtypes/records/bharshbarger/coercedImmediate....While there
It turns out that we were skipping some calls that ought not to be skipped (yay for the new approach!). To fix up testing, I did or will do the following:
owned. These are compiler-generated. Seems very similar to Dyno: implement builtin borrow #27152.var x, y, zbecause it expected to find at least one declaration with a type or initialization expression. When it doesn't, instead of assigningx,yandztheAnyTypeas it should in the non-multi-decl case, it instead left them unknown.?which is trailed by other type parameters. The logic didn't account for differing indexing schemes betweenuast::FnCallandresolution::FormalActualMap. The former assigns indices to?, but the latter does not. Also,CallInfo::createUnknowndidn't correctly computed by-name actuals (for a similar indexing reason).mainalready:runAndDetectErrorsfunctionality of the query system. However, this is too broad: to be precise,runAndDetectErrorstracks if errors were occurred while computing any required information for the called queries. In particular, this includes unrelated syntax resulting from parsing a module in which a function / variable / etc were declared. Though sound, discarding candidates for this reason goes against our attempts to provide as much information as possible in the editor.runAndDetectErrorsfunctionality previously always hid error messages emitted while detecting errors, the use case being__primitive("resolves", ...)and friends. As I was using it, however, I needed to show errors to the user as before. Thus, I had to adjust the logic to cope with potentially having issued errors. Even though I didn't end up using it, I kept it in case it ever comes up again.canUseSavedResultPushIfNot.runAndDetectErrors, I settled on only counting "local" errors for the purposes of candidate rejection. A proxy for a "local" / relevant error is whether or not it was issued directly by theResolverthat's handling the candidates' formal. To track this, I added a new field in the resolver, and adjusted all calls toerrorandreportto set that field.