9 Type Checking

In this chapter we will develop a small object-oriented DSL, which can be seen as a smaller version of Java that we call SmallJava. We will use this DSL to show some type checking techniques that deal with object-oriented features such as inheritance and subtyping (type conformance). This will also allow us to learn other features of Xtext grammars and to see some good practices in Xtext DSL implementations.

在本章中,我们将开发一个小型的面向对象DSL,它可以看作是Java的一个较小版本,我们称之为SmallJava。我们将使用这个DSL展示一些处理面向对象特性的类型检查技术,比如继承和子类型(类型一致性)。这也将使我们学习XT语法的其他特性,并在XTXTDSL实现中看到一些良好的实践。在本章中,我们将开发一个小型面向对象DSL,它可以看作是我们称之为SmallJava的较小版本的Java。我们将使用这个DSL展示一些处理面向对象特性的类型检查技术,比如继承和子类型(类型一致性)。这也将允许我们学习Xtext语法的其他特性,并在xtextdsl实现中看到一些好的实践。

SmallJava

The language we develop in this chapter is a simplified version of Java, called SmallJava. This language does not aim at being useful in practice and cannot be used to write real programs such as Java. However, SmallJava contains enough language features that will allow us to explore advanced type checking techniques that can also be reused for other DSLs which have OOP mechanisms such as inheritance and subtyping.

我们在本章中开发的语言是Java的简化版本,称为SmallJava。这种语言的目的不是在实践中有用,也不能用来编写像Java这样的实际程序。然而,SmallJava包含了足够多的语言特性,允许我们探索高级类型检查技术,这些技术也可以被其他具有OOP机制(如继承和子类型)的dsl重用。

The implementation we see in this chapter will not be complete, since some features of this language, such as correct member access, will be implemented in the next chapter when we introduce the mechanism of local and global scoping. In a Java-like language type checking and scoping are tightly connected and complement each other;for the sake of readability, we will split typing and scoping into two separate chapters.

我们在本章中看到的实现并不完整,因为这种语言的一些特性,例如正确的成员访问,将在下一章介绍本地和全局作用域机制时实现。在类似Java的语言中,类型检查和作用域是紧密相连、相辅相成的;为了可读性,我们将把类型和范围划分为两个独立的章节。

We will not write a code generator or an interpreter for SmallJava: we are more interested in statically checking SmallJava programs rather than executing them.However, a code generator or an interpreter could easily be implemented reusing the same techniques illustrated in Chapter 5, Code Generation and Chapter 8,An Expression Language respectively.

我们不会为SmallJava编写代码生成器或解释器:我们更感兴趣的是静态地检查SmallJava程序,而不是执行它们。但是,代码生成器或解释器可以很容易地实现,可以重用第5章代码生成和第8章所述的相同技术,一种表达语言。

Let us stress that implementing the whole Java language and, in particular, its complete type system, would not be feasible in this book. The Java type system is complex due to advanced features such as inner classes, static methods,method overloading, and generics. Furthermore, all these concepts require a solid background on type theory, which is out of the scope of this book. Instead we concentrate on a small subset of Java features, which, as hinted previously, are common also to other object-oriented languages. If your DSL needs to access Java types and to be interoperable with Java, you may want to consider using Xbase,briefly described in Chapter 12, Xbase. If your DSL does not have to interact with Java,the concepts described in this chapter can be reused and adapted to fit your DSL.

让我们强调一下,在本书中实现整个Java语言,尤其是其完整的类型系统是不可行的。由于内部类、静态方法、方法重载和泛型等高级特性,Java类型系统非常复杂。此外,所有这些概念都需要扎实的类型理论背景,这超出了本书的范围。相反,我们将重点放在Java特性的一小部分上,正如前面所暗示的,这些特性对于其他面向对象语言也是通用的。如果您的DSL需要访问Java类型并与Java互操作,那么您可能需要考虑使用Xbase,这在第12章Xbase中有简要介绍。如果您的DSL不必与Java交互,那么可以重用并调整本章中描述的概念以适合您的DSL。

Creating the project 详见P207

SmallJava grammar

在开始开发这种语言之前,我们先简单介绍一下我们将采用的简化方法:

• Classes have no explicit constructors 

• There is no cast expression 

• Arithmetic and boolean expressions are not implemented 

• Basic types (such as int, boolean, and so on) and void methods are not considered (methods must always return something) 

• There is no method overloading

• Member access must always be prefixed with the object (even if it is this) 

• super is not supported (but it will be implemented in the next chapter) 

• The new instance expression does not take arguments (since there are only implicit default constructors)

•类没有显式构造函数 

•没有演员表情 

•未实现算术和布尔表达式 

•基本类型(如int、boolean等)和void方法不可用 已考虑(方法必须始终返回某些内容) 

•没有方法过载....(是我不想翻了)

Basically, the features that we are interested in and that will allow us to have a case study for type checking and scoping (next chapter) are class inheritance,field and method definitions, and blocks of statements with local variable definitions.

基本上,我们感兴趣并允许我们对类型检查和作用域(下一章)进行案例研究的特性是类继承、字段和方法定义以及带有局部变量定义的语句块。

Rules for declarations

The rules in the grammar are prefixed with SJ to avoid confusion with the classes and terms in Java that they mimic.

The first rule is straightforward it states that a SmallJava program is a possibly empty sequence of classes:

语法中的规则以SJ作为前缀,以避免与它们模仿的Java中的类和术语混淆。第一条规则很简单,它指出SmallJava程序可能是一个空的类序列:

SJProgram:

classes+=SJClass*;

SJClass: 'class' name=ID ('extends' superclass=[SJClass])? '{'

members += SJMember*

'}' ;

SJMember:

SJField | SJMethod ;

SJField:

type=[SJClass] name=ID ';' ;

SJMethod:

type=[SJClass] name=ID

'(' (params+=SJParameter (',' params+=SJParameter)*)? ')'

body=SJMethodBody ;

Each class can have a superclass (that is, a reference to another SmallJava class) and a possibly empty sequence of members. An SJMember object can be either an SJField object or an SJMethod object; note that since both fields and methods have a type and a name feature, these two features will end up in their common base class SJMember.

每个类可以有一个超类(即,对另一个SmallJava类的引用)和一个可能为空的成员序列。SJMember对象可以是SJField对象或SJMethod对象;请注意,由于两个字段和方法都有类型和名称特性,因此这两个特性最终将出现在它们的公共基类成员中。

The body of a method is a sequence of SJStatement (defined later) enclosed in curly brackets:

方法的主体是一系列包含在花括号中的语句(稍后定义):

SJMethodBody:

'{' statements += SJStatement* '}';

If we define the rule for the method body as in the preceding code snippet, we get a warning:

如果我们像前面的代码片段那样为方法体定义规则,我们会得到一个警告:

The rule 'SJMethodBody' may be consumed without object instantiation. Add an action to ensure object creation, for example,'{SJMethodBody}'.

In fact, the only assignment is to the feature statements, which is based on SJStatement*; if no statement is parsed, the rule will be valid, but the feature will not be assigned and no object will be instantiated (see the Digression on Xtext grammar rules section, Chapter 8, An Expression Language for an explanation of how object instantiation and feature assignment in a rule are connected). As suggested by the warning, we add an action to ensure object creation:

实际上,唯一的赋值是基于SJStatement*的feature语句;如果没有解析任何语句,则该规则将是有效的,但不会分配功能,也不会实例化任何对象(请参阅Xtext语法规则部分的“表达式语言”第8章,了解规则中的对象实例化和功能分配是如何连接的)。如警告所示,我们添加了一个操作以确保对象创建:

SJMethodBody:

{SJMethodBody} '{' statements += SJStatement* '}';

Rules for statements and syntactic predicates

These are the rules for statements:

SJStatement:

SJVariableDeclaration |

SJReturn |

SJExpression ';' |

SJIfStatement

;

SJReturn:

'return' expression=SJExpression ';'

;

SJVariableDeclaration:

type=[SJClass] name=ID '=' expression=SJExpression ';'

;

SJIfStatement:

'if' '(' expression=SJExpression ')' thenBlock=SJIfBlock

(=>'else' elseBlock=SJIfBlock)?

;

SJIfBlock:

statements += SJStatement

| '{' statements += SJStatement+ '}' ;

The blocks for an if statement can also be specified without curly brackets; in this case, a single statement can be specified.

if语句的块也可以不用花括号来指定;在这种情况下,可以指定一条语句。

The rule for the if statement shows another important feature of Xtext grammars:syntactic predicates represented by the symbol =>. These are useful to solve ambiguities in a grammar; we will use the if statement as an example to describe such situations. If we write the rule for the if statement as follows:

if语句的规则显示了Xtext的另一个重要特性grammars:syntactic predicates 由符号=>表示。这些都有助于解决语法中的歧义;我们将以if语句为例来描述这种情况。如果我们为If语句编写如下规则:

(感觉这段也没有什么用)

Rules for expressions(略)

The complete grammar(略)

Utility methods

As we did for previous DSLs, we write an Xtend class, SmallJavaModelUtil,with static utility methods for accessing the AST model of a SmallJava program:

正如我们对以前的DSL所做的那样,我们编写了一个Xtend类SmallJavaModelUtil,其中包含用于访问SmallJava程序的AST模型的静态实用程序方法:

import static extension org.eclipse.xtext.EcoreUtil2.*

class SmallJavaModelUtil {

def static fields(SJClass c) {

c.members.filter(typeof(SJField))

}

def static methods(SJClass c) {

c.members.filter(typeof(SJMethod))

}

def static returnStatement(SJMethod m) {

m.body.statements.typeSelect(typeof(SJReturn)).head

}

def static containingClass(EObject e) {

e.getContainerOfType(typeof(SJClass))

}

def static containingBlock(EObject e) {

e.getContainerOfType(typeof(SJBlock))

}

def static containingProgram(EObject e) {

e.getContainerOfType(typeof(SJProgram))

}

def static containingMethod(EObject e) {

e.getContainerOfType(typeof(SJMethod))

}

}

Since the feature members in an SJClass object contains both the SJField and SJMethod instances, it is useful to have utility methods to quickly select them based on type. We will use these static methods in other Xtend classes with an import static extension statement so that we will be able to write expressions such as:

• c.methods

• c.fields

由于SJClass对象中的要素成员同时包含SJField和SJMethod实例,因此让实用程序方法根据类型快速选择它们是很有用的。我们将在带有import static extension语句的其他Xtend类中使用这些静态方法,以便能够编写如下表达式:

• c.methods

• c.fields

As we will see in the rest of the chapter, we will often need to directly access the containing class, the containing block, the containing program, and the containing method of an expression or statement thus, we write the corresponding utility methods (recall that getContainerOfType comes from EcoreUtil2, whose static methods are imported as extension methods). Finally, the method to quickly access the return statement will be useful when writing unit tests for the DSL.

我们将在本章的其余部分看到,我们通常需要直接访问表达式或语句的包含类、包含块、包含程序和包含方法,因此,我们编写相应的实用程序方法(记得getContainerOfType来自EcoreUtil2,其静态方法作为扩展方法导入)。最后,快速访问return语句的方法在为DSL编写单元测试时非常有用。

Testing the grammar

As you should know by now, we should write unit tests for the parser as soon as we write some rules for the DSL grammar. In this chapter we show only a few interesting cases, in particular the tests for the associativity of expressions such as assignments and member selection (see Chapter 8, An Expression Language, for the technique for testing associativity). We use the SmallJavaModelUtil utility methods to write cleaner tests:

正如您现在应该知道的,我们应该在为DSL语法编写一些规则之后,立即为解析器编写单元测试。在本章中,我们只展示了几个有趣的案例,特别是表达式的关联性测试,如赋值和成员选择(有关关联性测试的技术,请参见第8章“表达式语言”)。我们使用SmallJavaModelUtil实用程序方法来编写更干净的测试:P203不贴代码了

First validation rules

Before getting to the main subject of this chapter, we will first implement some constraint checks that are complementary to type checking.

在讨论本章的主要主题之前,我们将首先实现一些约束检查,这些约束检查是对类型检查的补充。

Checking cycles in class hierarchies

Checking member selections

Checking return statements

Checking for duplicates

Type checking

Type provider for SmallJava

The type provider for SmallJava expressions we are about to construct will compute the type of any SJExpression. The concept of type will be represented by SJClass since SmallJava does not support native types (such as int, boolean, and so on).

我们将要构造的SmallJava表达式的类型提供程序将计算任何SJExpression的类型。类型的概念将由SJClass表示,因为SmallJava不支持本机类型(如int、boolean等)。

We write a single typeFor method which returns an SJClass object by using a type switch (the default case simply returns null).

我们编写了一个typeFor方法,该方法使用类型开关返回SJClass对象(默认情况下只返回null)。

import static extension org.eclipse.xtext.EcoreUtil2.*

class SmallJavaTypeProvider {

def typeFor(SJExpression e) {

switch (e) {

SJNew: e.type

SJSymbolRef: e.symbol?.type

SJMemberSelection: e.member?.type

SJThis : e.getContainerOfType(typeof(SJClass))

...

}

}...

In the preceding method, the type of a new instance expression is clearly the class that we are instantiating (the feature: type). The type of a symbol reference is the type of the referred symbol. Similarly, the type of a member reference is the type of the referred member. The type for this is simply the type of the containing class. Note that while at runtime the actual object replacing this could be an object of a subclass, statically, its type is always the class where this is being used. In all of the preceding cases, the type always corresponds to an existing SJClass.

在前面的方法中,新实例表达式的类型显然就是我们正在实例化的类(feature:type)。符号引用的类型是所引用符号的类型。类似地,成员引用的类型就是所引用成员的类型。它的类型只是包含类的类型。请注意,虽然在运行时替换它的实际对象可能是子类的对象,但静态地说,它的类型始终是使用它的类。在上述所有情况下,类型始终对应于现有的SJClass。

Now we need to provide a type for the remaining terminal expressions, that is,null and the constant expressions. For these expressions, there are no existing SJClass instances that we can use as types: we will create static instances in SmallJavaTypeProvider (for convenience, we will also give them a name):

现在我们需要为剩余的终端表达式提供一个类型,即null和常量表达式。对于这些表达式,没有可用作类型的现有SJClass实例:我们将在SmallJavaTypeProvider中创建静态实例(为方便起见,我们还将为它们命名):

class SmallJavaTypeProvider {

public static val stringType =

SmallJavaFactory::eINSTANCE.createSJClass => [name='stringType']

public static val intType =

SmallJavaFactory::eINSTANCE.createSJClass => [name = 'intType']

public static val booleanType =

SmallJavaFactory::eINSTANCE.createSJClass =>[name='booleanType']

public static val nullType =

SmallJavaFactory::eINSTANCE.createSJClass => [name = 'nullType']

def typeFor(SJExpression e) { switch (e) { ...continuation SJNull: nullType SJStringConstant: stringType SJIntConstant: intType SJBoolConstant: booleanType } } def isPrimitive(SJClass c) { c.eResource == null } ...

Note that it is convenient to have a way of identifying the types we created for null and for constant expressions, which we call primitive types; we have a specific method for that called isPrimitive. An easy way to identify such types is to check that they are not part of a resource. We will need this distinction in the next chapter. 

注意,有一种方法可以方便地识别为null和常量表达式创建的类型,我们称之为基元类型;我们有一个特殊的方法叫做isPrimitive。识别此类类型的一个简单方法是检查它们是否不是资源的一部分。我们将在下一章中需要这种区分。

To test the type provider and keep the tests clean and compact, we implement a method that contains the skeleton of the test logic where a single passed expression is replaced. This way, the actual test methods are compact and simple since they only specify the expression and the expected type name:

为了测试类型提供程序并保持测试干净紧凑,我们实现了一个方法,该方法包含测试逻辑的框架,其中替换了单个传递的表达式。这样,由于实际的测试方法只指定表达式和预期的类型名,因此它们紧凑而简单:

@RunWith(typeof(XtextRunner))

@InjectWith(typeof(SmallJavaInjectorProvider))class SmallJavaTypeProviderTest { 

@Inject extension ParseHelper 

@Inject extension ValidationTestHelper

@Inject extension SmallJavaTypeProvider

def private assertType(CharSequence testExp,

String expectedClassName) {

'''

class R { public V v; }

class P extends R { public R m() { return null; } }

class V extends R { public N n; }

class N extends R {}

class F extends R {}

class C extends R {

F f;

R m(P p) {

V v = null;

«testExp»;

return null;

}

}

'''.parse => [

assertNoErrors

expectedClassName.assertEquals(

classes.last.methods.last.

body.statements.get(1).statementExpressionType.name

)

]

}

def private statementExpressionType(SJStatement s) {

(s as SJExpression).typeFor

}

@Test def void thisType() {"this".assertType("C")}

@Test def void paramRefType() {"p".assertType("P")}

@Test def void varRefType() {"v".assertType("V") }

@Test def void newType() {"new N()".assertType("N")}

@Test def void fieldSelectionType() {"this.f".assertType("F")}

...other cases...

@Test def void intConstantType() {'10'.assertType("intType")}

@Test def void nullType() {'null'.assertType("nullType")}

This technique is useful when you need a complete program to perform tests.In SmallJava you cannot type an expression without having a containing method and a containing class.

当您需要一个完整的程序来执行测试时,这种技术非常有用。在SmallJava中,如果没有包含方法和包含类,则无法键入表达式。

Type conformance (subtyping)类型一致性(子类型)

In an object-oriented language, the type system must also take type conformance (or Subtyping) into account: an object of class C can be used in a context where an object of a superclass of C is expected. For instance, the following code is well-typed:

在面向对象语言中,类型系统还必须考虑类型一致性(或子类型化):类C的对象可以在需要超类C的对象的上下文中使用。例如,以下代码类型正确:

C c = new D();

provided that D is a subclass (subtype) of C. This holds true in every context where an expression is assigned, for example, when we pass an argument in a method invocation.

假如D是C的一个子类(子类型)。这适用于分配表达式的每个上下文,例如,当我们在方法调用中传递参数时。

We implement type conformance in a separate class, SmallJavaTypeConformance.

我们在一个单独的类SmallJavaTypeConformance中实现类型一致性。

To check whether a class is a subclass of another class, we need to inspect the class hierarchy of the former and see whether we find the latter. In SmallJavaModelUtil,we already have a method that computes the class hierarchy avoiding infinite loops in case of a cyclic hierarchy, and we implement the method isSubclassOf as follows:

为了检查一个类是否是另一个类的子类,我们需要检查前者的类层次结构,看看是否找到后者。在SmallJavaModelUtil中,我们已经有了一个计算类层次结构的方法,在循环层次结构的情况下避免了无限循环,我们实现了isSubclassOf方法,如下所示:

import static extension org.example.smalljava.util.

SmallJavaModelUtil.*

class SmallJavaTypeConformance {

def isSubclassOf(SJClass c1, SJClass c2) {

c1.classHierarchy.contains(c2)

}...

Type conformance deals with subclasses as well as other special cases. For instance,a class is not considered a subclass of itself, but it is of course conformant to itself.Another special case is the expression null; it can be assigned to any variable and field and passed as an argument for any parameter. The type for null, which is represented by the static instance nullType in SmallJavaTypeProvider, must be conformant to any other type.

类型一致性处理子类以及其他特殊情况。例如,一个类不被认为是它自己的子类,但它当然符合它自己。另一种特殊情况是表达式null;它可以分配给任何变量和字段,并作为任何参数的参数传递。null的类型,即由SmallJavaTypeProvider中的静态实例nullType表示,必须是符合任何其他类型。

This is the initial implementation of type conformance for SmallJava:

import static org.example.smalljava.typing.SmallJavaTypeProvider.*

class SmallJavaTypeConformance {

...

def isConformant(SJClass c1, SJClass c2) {

c1 == nullType || // null can be assigned to everything

c1 == c2 ||

c1.isSubclassOf(c2)

}...

For the moment, we are not considering other cases (we need some additional concepts, as we will see in the next chapter). We test this implementation as follows: @RunWith(typeof(XtextRunner)) @InjectWith(typeof(SmallJavaInjectorProvider)) class SmallJavaTypeConformanceTest { @Inject extension ParseHelper @Inject extension SmallJavaTypeConformance @Inject extension SmallJavaTypeProvider

@Test def void testClassConformance() { ''' class A {} class B extends A {} class C {} '''.parse.classes => [ // A subclass of A get(0).isConformant(get(0)).assertTrue // B subclass of A get(1).isConformant(get(0)).assertTrue // C not subclass of A get(2).isConformant(get(0)).assertFalse ] } @Test def void testNullConformance() { ''' class C {} class D { C m() { return null; } } '''.parse.classes => [ val typeOfNull = last.methods.head. returnStatement.expression.typeFor // null can be assigned to anything typeOfNull.isConformant(get(0)).assertTrue typeOfNull.isConformant(get(1)).assertTrue ] }...

Expected types

Checking type conformance

Checking method overriding

Improving the UI

Summary

In this chapter we presented type checking techniques that are typical for a DSL with object-oriented features. A small Java-like language was introduced to demonstrate how to parse features such as member access and inheritance and how to handle validation of type conformance.

在本章中,我们介绍了具有面向对象特性的DSL的典型类型检查技术。引入了一种类似Java的小型语言来演示如何解析成员访问和继承等特性,以及如何处理类型一致性验证。

然而,还有一个关键的方面我们仍然需要处理:对成员(字段和方法)的正确访问。实际上,选择表达式e.f 仅当字段f在类e中声明时才是类型良好的(类似于方法)或者在e类的任何超类中。如果你做一些实验,你会注意到此时,您可以访问未在类中声明的成员接收方表达式,并且不能访问层次结构的所有成员接收方表达式类的。此外,局部变量访问不需要在当前实现中正确工作:还可以引用定义的变量在内部块中定义的变量。

为了正确处理前面涉及交叉引用解决的问题,我们需要实现一个定制的作用域机制,正如我们将看到的那样在下一章。作用域定义在特定上下文中可见的内容,以便Xtext可以正确解析交叉引用。

在下一章中,我们还将为成员(即private、protected和public)添加SmallJava访问级别修饰符。我们将向您展示SmallJava程序如何访问在其他文件中定义的类,以及如何提供包含一些预定义类(例如,对象、字符串等)的库。

你可能感兴趣的:(9 Type Checking)