-
Notifications
You must be signed in to change notification settings - Fork 427
Dyno: unify return, break, throw etc. handling across Resolver, type inference, and split init etc. #26955
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
Conversation
…itor Signed-off-by: Danila Fedorin <[email protected]>
b77fb80
to
3391feb
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]>
Signed-off-by: Danila Fedorin <[email protected]>
Signed-off-by: Danila Fedorin <[email protected]>
3391feb
to
d7d1a12
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]>
This reverts commit ee57b16. The changes introduced in preceding commits mean that the Resolver can handle early returns. Signed-off-by: Danila Fedorin <[email protected]>
Signed-off-by: Danila Fedorin <[email protected]>
6dc996d
to
0a4902c
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]>
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.
Great improvements and the refactored code is a plus too!
All my comments are just nits that are either simple typos or an inconsistency with use of single quotes or back ticks around terms in the comments. Specifically there is inconsistent use of '' around the reserved words used in control flow and the use of back ticks around param
-known and param
-decided, etc. (note that I didn't call all these out with individual comments)
return false; | ||
} | ||
|
||
/** Incororate the control flow information from a child statement into |
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.
"Incororate" -> "Incorporate"
} | ||
} | ||
|
||
/** When exiting an AST node, incorporate the information form its |
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.
"form" -> "from"
} | ||
|
||
/** code to run right after we entered a new scope and pushed a new frame. | ||
The default implemnetation sets up subFrame slots for handling Conditionals etc. */ |
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.
"implemnetation" -> "implementation"
virtual const types::Param* determineWhenCaseValue(const uast::AstNode* ast, ExtraData extraData) = 0; | ||
|
||
/** Overriden by subclasses to determine the value of an if condition. | ||
This is used for evaluating short-circuited / `param known` (or not) Conditional statements. */ |
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.
discrepancy in tick marks used with this and the above usage of "param known
"
} | ||
return false; | ||
} | ||
// Not param-known condition; visit both branches as normal. |
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.
`param`-known?
Signed-off-by: Danila Fedorin <[email protected]>
Signed-off-by: Danila Fedorin <[email protected]>
Signed-off-by: Danila Fedorin <[email protected]>
fab0632
to
2644ef8
Compare
the check_large_files check is malfunctioning. I will merge if all other checks pass. |
…7004) Resolves Cray/chapel-private#7229. Depends on #26999. Quoting from that issue: a lot of Chapel production code (including in library modules) looks like this: ```Chapel proc foo(param cond) { if cond then compilerError("This condition should not be met"); codeThatOnlyCompilesIfCondIsFalse(); } ``` These rely on the fact that `codeThatOnlyCompilesIfCondIsFalse()` will not be resolved if an error is already emitted. This means terminating resolution of `foo()`, much like we do with `return` in #26955. This PR implements that. One conceptual curiosity is that this conflicts with Dyno's existing approach of attempting to continue resolving programs to get partial information. Specifically, expressions that emitted errors are marked with `ErroneousType`, and if the rest of the types can be inferred despite the error, we infer them. This is desirable (partial information is useful in editors, for example), but counter to stopping resolution in its tracks. This PR goes for a two-phase handling of `compilerError`, which distinguishes functions that call compiler error directly and functions that simply observe errors emitted by a call. * __For functions that invoke `compilerError` directly__, this PR makes use of the changes in #26955 to stop resolution in its tracks. This requires some communication between the resolver and the other branch-sensitive passes to inform them of the fatal error, which I achieve using a new flag on `ResolvedExpression` called `causedFatalError_`[^1]. * Functions that are meant as "helpers" for invoking `compilerError` (i.e., those that are ignored using the `depth` argument) are considered "direct" callers to `compilerError` for the purposes of this phase. That's because they mostly just serve to augment the `compilerError` in some way. * __For invocations of functions that error__, this PR simply marks the call as `ErroneousType`, without interrupting the resolution of the current function etc. Thus, if we call `foo()`, and `foo()` issues a `compilerError`, we print the error and mark `foo()` as `ErroneousType`, but (as described above) continue resolving with partial information. This way, I believe we can reconcile the expectation of `compilerError` as terminating resolution, while still allowing us to compute partial information in other contexts. See the program tested by this PR: ```Chapel proc foo() { bar(); return 42; } proc bar() { compilerError("Hello"); compilerWarnning("inside bar, after call to compilerError"); } var x = foo(); ``` Here, the error "Hello" immediately terminates resolution of `bar()` (so the "inside bar" warning is never printed). This error is issued on the `bar();` line, but we proceed past this line, and, since the `return 42` doesn't depend on the call to `bar()`, correctly determine that the `var x` is an `int`. ## Testing - [x] dyno tests - [x] paratest - [x] spectests still work as before Reviewed by @benharsh -- thanks! [^1]: I have checked and on my machine, this flag does not cause the `ResolvedExpression` type to increaase in size, presumably due to the way that struct is padded. So this does not cause size / memory penalties .
Makes progress on https://github.com/Cray/chapel-private/issues/7229, but does not resolve it.
This PR takes the first step towards handling early-return or early-error patterns in the standard library. At the same time, it improves various passes' handling of diverse control flow (enables
continue
,return
, etc in resolution,break
andcontinue
in init/deinit etc.). It also reduces cases in which default init etc. was resolved for declarations that are not reachable.See future work below for cases this PR does not handle.
I approached this by making the following observation: a large number of passes / visitors rely on an approximating control flow information.
ReturnTypeInferrer
(used for computing function return types) keeps a stack of frames along with information about whether they continued, had abreak
, or returned. This is used for iterating overparam
loops and ignoring unreachablereturn
s.VarScopeVisitor
(used for not handling init/deinit/etc. in dead code, but also for enabling more split-init cases)Resolver
(not used prior to this PR, but would need to reason about returns etc. to avoid resolving dead code)In each case, this is done by creating a stack of frames (which contain data like
returnsOrThrows
,continues
, etc), and updating that stack during traversals. In each case, there is distinct logic to reconcile information from various frames, and to handle constructs likeif
andselect
whereparam
-true branches cause other branches to be ignored.This PR unifies those traversals by defining a new abstract base class called
BranchSensitiveVisitor
. This class does not have anyvisit
methods of its own (the subclasses are intended to provide that). It provides methods such asenterScope
andexitScope
which take care of tracking and combining control flow information (e.g., deciding if all branches return/throw). It also provides methodsbranchSensitivelyTraverse
, which implement commonparam
-folding logic for conditionals and if/elses. It then switches all frame-based traversals to use this newBranchSensitiveVisitor
. This has numerous advantages:return-type-inference.cpp
, which started as a watered down duplication ofVarScopeVisitor
's code, but evolved in an orthogonal direction).Resolver
skips code because it's dead, we know thatReturnTypeInferrer
andVarScopeVisitor
will skip that code too).Resolver
) with control flow handling without much additional code.As a nice side effect,
VarScopeVisitor
became aware ofcontinue
andbreak
(previously only handled by return type inference), andResolver
became better at handlingbreak
andcontinue
inparam
loop).This PR takes an opinionated stance on #26968 and disallows split-init across returns. This is a consequence of not visiting dead code for computing split-init types.
AST Traversal Changers
In all traversals, we want to skip visiting code when it's dead. This is both a performance concern (why waste time resolving functions that are not reached?) and a correctness concern (e.g.,
compilerError
is guaranteed not to do anything directly after areturn
). How does one add this logic to every singlevisit
/traverse
call?I considered several options:
visit
method that returns early if theBranchSensitiveVisitor
has determined that it will not be reached. This prelude could be a macro to avoid duplicating a lot of code. However, this approach is undesirable because it leads to some duplication (invocations of the macro at least; duplicate code at worst) in every singlevisit
method. Further, authors ofvisit
methods must remember to include this prelude lest the dead-code sensitivity be affected.ResolvedVisitor
and having a customvisit
method that dispatches to some other method (e.g.,visitImpl
). This way, thevisit
can be defined once, and the dead-code checking can happen before the dispatch tovisitImpl
. However, this introduces a second dispatch, introduces churn (changing allvisit
tovisitImpl
on resolver), and is anti-compositional (ifvisit
has different signatures, likevisit(node)
in Resolver andvisit(node, RV)
inVarScopeVisitor
,visitImpl
would have to also have a different signature; this introduces an m-by-n implementation problem).traverse
function to exit early before even invokingenter
andexit
. This can be done using a template specialization to provide a "default value", and to avoid runtime overhead.I settled for 3. Specifically, all invocations of
traverse
now reference a template class with a static method,AstVisitorPrecondition<T>::skipSubtree(node, visitor)
to decide whether to invokeenter
/exit
on a node and its children. The default template forAstVisitorPrecondition
always returns true, which means the default behavior oftraverse
remains the same. However, visitors that want to return early / skip visiting code altogether can specialize the template and define a condition.Resolver
,VarScopeVisitor
, andReturnTypeInferrer
all do this; they check the current frame for signs thatreturn
has already been encountered, and if so, do not descend into any more AST nodes. ForResolver
, this does not happen whenscopeResolveOnly
is set, since we always want to enter every node in that mode.I find this appealing because user code saw very little churn (10 lines to specialize the template for
Resolver
, e,g.), and each newenter
/exit
method added toResolver
will automatically work with early returns (since thetraverse
call which invokesenter
/exit
knows to check). The precondition could be easily defined for all visitors affected by this PR.Macro Tweaks
@jabraham17 and I have settled on a pattern in
chapel-py
in which the X-macro files (e.g.,uast-methods.h
) auto-defined missing X-macros and undefine them automatically. This helps reduce a lot of boilerplate. As I was experimenting with dispatch (approach 2) above, I found it convenient to apply this pattern to the Dyno code proper. Rather than pollutinguast-classes-list.h
andtype-classes-list.h
(which IMO serve as good references), I added wrapper files that handle the aforementioned niceties. I then switchedAstNode.h
andType.h
to using these wrappers, saving a considerable amount of noise. I did not end up with new uses of the adapters in my code (since I went with template specialization instead), but I thought the cleanup is worth including here.Module Changes
This PR reverts (some) changes to module code from b789f5b and ee57b16, which were previously required to work around the resolver's inability to reason about early returns. Some code, however (
ChapelDomain.chpl
from b789f5b), relies oncompilerError
being treated as early return, which is future work.Reviewed by @arezaii -- thanks!
Future Work
compilerError
, which is considered an "early return" in productionif bla then continue else break
does not execute)VarScopeVisitor
family of passes (init/deinit, split init, copy elision) (see https://github.com/Cray/chapel-private/issues/4209)break
/continue
(https://github.com/Cray/chapel-private/issues/7245)Testing
Tested cases
VarScopeVisitor
now knows aboutbreak
andcontinue
VarScopeVisitor
now stops traversing afterreturn
, which should enable reasoning about early returns (already tested)Resolver
now reasons about control flow and skips resolving dead codeResolver
now properly handlescontinue
,break
, andreturn
inparam
loops