-
Notifications
You must be signed in to change notification settings - Fork 132
ECJ Lambda
< JDT Core Programmer Guide | ECJ
Lambda expressions are particularly challenging for compilation.
-
Parsing lambdas already requires some tricks because the lambda
syntax cannot be written in the LALR(1) style that is required by
jikespg
- Complexity is highest when the CompletionParser has to handle code containing (incomplete?) lambdas, see JDT Core Programmer Guide/ECJ/Completion#Lambda Specifics
- Resolving a lambda expression is a major source for complexity in the type inference engine, see below.
Resolving a lambda expression requires a target type that determines functional type being implemented by the lambda expression. In some situations, the target type can be directly deduced from the context (e.g., when assigning a lambda to a variable), but in many situations determining the target type is part of the same type inference that should eventually assign types to the lambda itself. This happens, e.g.,
- when passing a lambda expression as an argument of an invocation to a generic method.
- when passing a lambda expression as an argument to a method with overloads
During this flow of type information up and down, attempts may be made to resolve the same lambda against several candidates target type. Further complexity is added by the need to resolve not only the signature of the lambda, but also it's entire body, in order, e.g., to determine the type of a return statement. Since all this resolving modifies the AST, we need a way to start resolving from a fresh start, once for each candidate target type.
When implementing this part, we discussed three options:
- implement an "unresolve" method to wipe out all effects of any prior resolving attempt
- copy the lambda AST using either a visitor or by a hierarchy of methods directly as ASTNode.copy()
- re-create the lambda repeatedly by parsing its region of input text.
I don't exactly recall why (1) was not chosen, but it may be related to the fact, that unresolving would be wasteful, as the same information could be needed later, and thus would need to be computed several times.
(2) was not chosen because it would require a significant amount of boiler plate code (with lots of chances for introducing bugs).
So option (3) it was in the end. The result can be found in these locations:
- LambdaExpression.cachedResolvedCopy(), which orchestrates the
following steps:
- given a candidate target type compute the matching SAM
- if successful create a copy of the lambda
- resolve the body of the lambda copy against the candidate target type
- remember this resolved lambda in LambdaExpression#copiesPerTargetType
- in some situation (decided per call site) in addition to regular resolving also an analysis of thrown exceptions is performed (since exceptions contribute to type inference, too).
- flag LambdaExpression#committed signals when finally the original lambda AST is resolved using the final result of type inference.
- The details of copying can be found in LambdaExpression.copy(), d'uh.
This approach is problematic in assist situations (select or complete): we are creating a standard Parser for the task of copying, but as the original parse happened using some AssistParser, that parser may have invested efforts of recovering input with syntax errors. If a regular parser is used for copying, it may be less capable in syntax recovery, and bail out with syntax errors, while globally compilation has already reached the resolution phase.
Failure to parse the copy will be signaled as a CopyFailureException. Two locations requesting a cachedResolvedCopy() actually catch CopyFailureException, and a special flag LambdaExpression#assistNode is consulted to opportunistically help the current assist operation. During this opportunistic exit we may leave the lambda in a half-resolved state.