9. Binding
This section may be outdated! |
9.1. Design Rationale
Binding references to declarations follows the Xtext mechanism based on local N4JSScopeProvider
and a global N4JSGlobalScopeProvider
scope providers. The basic question is: to which elements are references bound to. This in particular interesting for all kind of type declarations, including functions as they are interpreted as types. These declarations are Janus-faced: On the one side, they are targets of type references as Type
, and on the other side they can also be target of identifier references bound to some so called IdentifiableElement
. As explained in [_type_index], special type objects (TClass
etc.) are created from the original declarations. These type objects are used as targets for both kind of references. The following table summarizes the reference-target relations relevant for N4JS (not the standalone type grammar).
Reference | Target Type |
---|---|
N4JS |
|
ImportDeclaration.importedModule |
TModule |
NamedImportSpecifier.importedElement |
types.IdentifiableElement |
IdentifierRef.id |
types.IdentifiableElement |
ParameterizedPropertyAccessExpression.property |
types.TMethod |
PropertyAccessExpression.property |
types.TMember |
N4Getter/N4SetterDeclaration.field |
N4FieldDeclaration |
Continue/Break-Statement.label |
LabelledStatement |
Type Expressions |
|
ParameterizedTypeRef.declaredType |
Type |
Overview Scoping Package gives an overview over the most important classes in the scoping package, with the N4JSScopeProvider
and the used customized scopes created by the scope providers.
9.2. Binding to Members
Members of different types, such as classes and also record types or enumerations, are bound using the MemberScopeProvider
. This often returns a MemberScope
, which directly works on the members. Most types with members are implemented by subclasses of ContainerType
, using CollectMembersHelper
to collect all members and FindMemberHelper
for retrieving a member by its name via ContainerTypes
. Ensure that when types with members are added to override appropriate methods in all of these related classes (e.g., CollectMembersHelper
, AbstractHierachyTraverser
and FindMemberHelper
uses polymorphic dispatch to handle different subtypes – so you won’t be able to find a member if you do not adjust these helpers).
9.3. Getter / Setter Binding
For customized binding of getters / setters, see Accessors.
9.4. Static Member Binding
For customized binding of static members, see Static Members.
9.5. Enumeration Literals Binding
-
introduced new type ref EnumTypeRef: it behaves comparable to ClassifierTypeRef, but with the difference that the MemberScopeProvider filters for a given EnumTypeRef filters all literals of the contained TEnum (in comparison the MemberScopeProvider filters for a given ClassifierTypeRef all static members of the contained classifier)
-
it isn’t possible to access literals on a enumeration literal itself, although this literal is typed as TEnum (that contains TEnumLiterals)
-
as there are currently no additional fields and operations for enumeration literals defined (in Java there is name and value()), the scope for literals is currently empty
9.6. Accessibility of types and members
Member access and type access has to be constrained and validated against the accessibility rules of N4JS. Therefore, the scoping annotates certain elements as erroneous to detect invalid references.
Basically two different approaches are used to implement that behavior:
-
The
VisibilityAwareTypeScope
andVisibilityAwareMemberScope
decorate an existing scope to validate the result on access. This allows to lazily check the visibility of the returned element. If it is not accessible, it is wrapped in aAbstractDescriptionWithError
which will be indentified as such by theErrorAwareLinkingService
. Before the binding is resolved and the EMF proxy is replaced, the error message is used to create an EMF diagnostic. -
For other cases, the scopes are produced differently, e.g. if all elements are easily enumerable and have to be collected before the scope is created (e.g. for imported elements), the scoped elements are validated eagerly to put them into the correct layer of scopes. That is, the valid descriptions may shadow the invalid description. Since there are more error conditions for these cases, e.g. duplicate imports and similar cases, the accessibility is checked before the concrete member is accessed. All the instances
AbstractDescriptionWithError
are put into theMapBasedScope
immediately.
In that sense, accessibility checks are basically implemented as decorators for the scoping itself. Bindings are established but flagged as errors.
Default visibility of members is calculated in Types.xcore
(in getTypeAccessModifier
and getMemberAccessModifier
etc.). Visibility is checked in org.eclipse.n4js.scoping.accessModifiers.MemberVisibilityChecker
and validators.
9.7. Member Scope Example
In this section, we are going to have a look at the creation process of MemberScope
.
export public class C {
private m1: int;
public m2: int;
}
import { C } from "C";
let c: C = new C();
c.m1; // Error -> The field m1 is not visible
c.m2; // OK -> m2 is visible at this context
Assume that we need to figure out to which element the ParameterizedPropertyAccessExpression c.m1
in the ExpressionStatement c.m1
binds to. To answer this question, N4JSScopeProvider.getScope(context, reference)
is triggered whereby context
is the ParameterizedPropertyAccessExpression
and reference
is EReference property
(property
is the cross-reference element defined in ParameterizedPropertyAccessExpression
's grammar).
N4JSScopeProvider.getScope(context, reference)
does not implement the scoping but delegates to corresponding methods based on the type of context
. In our example, since context
is a ParameterizedPropertyAccessExpression
, the scoping logic is delegated to the method that creates a MemberScope for the context ParameterizedPropertyAccessExpression c.m1
based on the receiver type of c
which is class C
.
The resulting scope instance returned by N4JSScopeProvider.getScope()
in our example is of type TypingStrategyAwareMemberScope
as shown in Member scope hierarchy .
In the hierarchy, the top-level scope is the NULL scope. Directly below the NULL scope is a MemberScope which contains all members of N4Object
since the class C
implicitly inherits N4Object
. The other MemberScope
instance beneath contains all members of the class C
regardless of their visibility. These members are m1
and m2
. While m2
is can be accessed by c.m2
, m1
it not visible at c.m1
. The VisibilityAwareMemberScope
implements precisely this behavior. In particular, it returns all members of C
that are visible at the current context
(here the element m2
), while wrapping non-visible members (here the element m
) in InvisibleMemberDescription
instances. These InvisibleMemberDescription
instances of type IEObjectDescriptionWithError
contain issue code and error message related to accessibility problems and are recognized during the error-aware linking phase done by ErrorAwareLinkingService
. It is worth to emphasize the motivation behind use of IEObjectDescriptionWithError
is to provide more informative error messages to the user other than Cannot reference element… Another example of IEObjectDescriptionWithError
is WrongWriteAccessDescription
that is used when we, try to write to a getter and no corresponding setter exists.
9.8. Scoping for Members of Composed Type (Union/Intersection) Example
In this section, we will have a look at how scoping is implemented for composed type, i.e. union or intersection type with an example of union type. Intersection is done similarly. Before reading this, it is strongly recommended to read Member Scope Example first.
export public class C {
private m1: int;
public m2: int;
}
export public class D {
private m1: int;
get m2(): int { return 42; };
}
import { C, D } from "Defs";
let cud : C|D;
cud.m2 = 10;
Assume that we need to find out to what element the ParameterizedPropertyAccessExpression cud.m2
in the ExpressionStatement cud.m2
binds to.
This is a question for scoping. Since the receiver type of cud
is a union type C|D
, a UnionMemberScope
is created that contains two subscopes, each of which corresponds to an individual type in the union. The resulting scope hierarchy is graphically depicted in Union member scope hierarchy.
The two subscopes are of type TypingStrategyAwareMemberScope
and created exactly the same way as described in Member Scope Example.
The UnionMemberScope
instance contains a list of subscopes for all types involved in the union and is responsible for constructing an IEObjectDescription
instance for m2
by merging all members of the name m2
found in all subscopes.
Merging members requires considering a variety of combinations (fields, setters getters, optional/variadic parameters etc.) and thus can become very complicated. To reduce the complexity, the recently refactored implementation splits the proccess into three separate steps.
Step 1: Collect information
During this phase, members with the name m2
are looked up in each subscope and collected into an ComposedMemberInfo
instance by ComposedMemberInfoBuilder
.
The first subscope (left branch in the Union member scope hierarchy) returns an EObjectDescription
wrapping the TField m2
of class C
and hence TField m2
is added to the ComposedMemberInfo
instance. The second subscope (right branch in the Union member scope hierarchy) returns a WrongWriteAccessDescription
wrapping the TGetter m2
of class D
and hence TGetter m2
is added to ComposedMemberInfo
instance. The reason for WrongWriteAccessDescription
because cud.m2
is trying to write to the getter of the same name in D
.
At the end of this step, two members public TField m2: int
and project TGetter m2(): int
are added to ComposedMemberInfo
.
Step 2: Merge members
This phase merges members of the same name into a composed member based on the information about these members collected in Step 1. Note that merge rules can become quite complicated as many situations must be considered. Sometimes, it is not possible to merge at all. If the merge is possible, we need to consider the following properties, among others,
-
Member kind: what kind of member is the merge result. For instance, what do we get when we merge a field with a setter?
-
Type of merge member: What is the return/parameter type of the merge result?
-
Accessibility: what is the accessibility of the merge result?
-
Optionality/Variadic: Should a parameter of the merge be optional or variadic?
The actual merge rules are implemented in the class UnionMemberFactory
which delegates to either of the classes UnionMethodFactory
, UnionFieldFactory
, UnionGetterFactory
and UnionSetterFactory
.
In our example,
The merge result of public TField m2: int
and project TGetter m2(): int
are merged into a project TGetter m2: int
.
Step 3: Construct the scope entry
In this final step, the actual IEObjectDescription for m2
is constructed. In our example, since there exists one subscope exposing an EObjectDescriptionWithError
(here WrongWriteAccessDescription
), the final result is an instance of UnionMemberDescriptionWithError
. This error instance is recognized during the linking phase and the error message of the subscope regarding WrongWriteAccessDescription
is displayed: Union combines fields and getters with name m2 and therefore property m2 is read-only.
More details can be found in the API documentation in the code. A good starting point is the class ComposedMemberScope
.
9.9. Structurally References Types
Scoping of structurally referenced types is similar to binding of members. The structural typing modifier basically filters the members of a type. That is, the structural modifier filters out all non-public members, and the field-only modifier only accept fields. Thus, similar to accessibility aware scoping (Accessibility of types and members), the TypingStrategyAwareMemberScope
encapsulates an original scope and applies these additional filters.
Bindings to additional members of a structurally referenced type is implemented in MemberScopeProvider.members(ParameterizedTypeRefStructural ..)
. Note that the current implementation does not necessarily bind to the type model (TModule) instance, as these members are part of a type reference. That is, usually these bindings refer to the AST elements. Thus, it is not possible to compare these members directly, instead, a structural comparison has to be applied.
9.10. Building
9.10.1. Build Phases
Phase 0 |
Loading Resources |
Phase 1: prelinking |
Create symbols for all resources, includes creation of temporary pre-linked type models |
Phase 2: linking |
Resolve all links, includes fully-resolved typed models |
That is, not each resource is loaded, pre-linked and linked separately. Instead, all resources are first loaded, then all resources are pre -inked, and only then all resources are linked.
9.10.2. Build Scenarios
Consequences:
-
do not try to set any types in types builder, only create symbols there (probably not even members of types)
9.10.3. Lazy linking problem
Lazy linking proxies in the indes may trigger reloading of AST (which leads to invalid disconnected type models):
Lazy links (ending with |x in which x is an index entry of a temporary list used to resolve the link) must not be written into index.
9.11. Proxies and Proxy Resolution (Overview)
Here we give a brief overview of the different kinds of proxies and when / how they are created and resolved.
9.11.1. Xtext’s Lazy Linking Proxies
-
URI fragment is
|
(where is a non-negative integer).
platform:/resource/Project/src/A.n4js#|3
-
created by Xtext’s
LazyLinkingResource
in the AST after parsing (they are only ever created in the AST, but the types builder may copy them to the TModule, so they may appear there as well. -
used to represent cross-references defined in the source code (i.e. name of an identifiable element used in source code to refer to that element).
Since the types builder sometimes copies proxies from AST to TModule (e.g. type of an element that was provided with an explicit type declaration in the source code), these proxies may also appear in the TModule, but only between the types builder phase and the end of the post-processing phase (or later, in case they are unresolvable).
-
resolution is handled by
#getEObject(String)
inLazyLinkingResource
, which recognizes lazy linking URI fragments and then forwards them to#getEObject(String,Triple)
, which in turn relies on the Xtext infrastructure. -
latest time of resolution: post processing. After post processing has completed, they should all be gone (unless they are unresolvable, e.g. typo in source code).
-
fun facts:
-
the number after the pipe character is the index of a
Triple
stored in fieldproxyInformation
in eachLazyLinkingResource
. -
the resource given before the fragment (e.g.
A.n4js
in the above example) is not the resource the proxy is pointing to (i.e. the resource containing the target EObject), but the resource from where the link originates.
-
9.11.2. Standard EMF Proxies
-
URI fragment contains a path to an EObject, using reference names and indices:
platform:/resource/Project/src/A.n4js#/1/@topLevelTypes.1/@ownedMembers.0
-
created automatically by EMF
-
during deserialization of a TModule from the Xtext index for all references to a different TModule (see
UserdataMapper
). -
when unloading a resource.
-
…
-
-
used to represent
-
cross-references from one TModule to another TModule.
-
astElement
links from TModule to AST whenever the AST is not present (e.g. resource was loaded from Xtext index). -
definedType
links from AST to TModule after deleting the TModule (this happens in the incremental builder after the pre-linking phase). -
all kinds of links after demand-loading an AST by resolving an
astElement
link (pathological case).
-
-
resolution is handled in two ways:
-
if the context
EObject
of the proxy, i.e. the one where the proxified cross-reference originates, is contained in an N4JSResource , then resolution is handled byN4JSResource#doResolveProxy()
(see alsoProxyResolvingResource
for details).In this case, special handling is performed to make sure that (a) the target resource is loaded from the index, if possible, and (b) post-processing of the target resource is initiated iff the target resource was loaded from AST (instead from the Xtext index) AND post-processing of resource is currently in progress or has already been completed.
-
otherwise, resolution is handled by standard EMF functionality.
-
-
latest time of resolution: none. In fact, some of those proxies (those representing
astElement
links from TModule to AST) must not be resolved at all, because this is not yet properly handled.
9.11.3. How is Proxy Resolution Triggered?
Resolution of proxies throughout the N4JS implementation is triggered as usually when using EMF, which means: whenever the getter of a EMF cross-reference is invoked and the value is still a proxy, the EMF-generated code of the getter will automatically trigger resolution of this proxy. For details look at the EMF-generated code of the getter of any cross-reference (IdentifierRefImpl#getId()
would be a good example).
9.11.4. When is Proxy Resolution Allowed?
So, at what time is it legal to trigger such a proxy resolution? Or, more concretely, during which resource load states (N4JS Resource Load States) is it legal to trigger proxy resolution? In fact, asking the question in this way is incorrect or at least not very helpful, because the answer would be (almost) always. The better question is: which components of the system / which parts of the code base are allowed to trigger proxy resolution?
For example, triggering resolution is disallowed in the ASTStructureValidator
and N4JSTypesBuilder
, but for the outside client code such as a JUnit test it is allowed to trigger proxy resolution as early as right after parsing. For an example of the latter see test #testStateFullyProcessed_triggeredOnlyThroughProxyResolution()
in N4JSResourceLoadStatesTest
.
In summary, we can state the rule that the internal N4JS implementation must not trigger any proxy resolution until installation of the derived state has completed, i.e. not before resource load state "Fully Initialized", but client code may trigger proxy resolution as early as right after parsing, i.e. already in resource load state "Loaded".