xtext
介绍
从1.0版开始,在Xtext中构建复杂的语言已经变得可行。 复杂语言的一个常见方面是对表达式的支持。 表达式需要递归语法定义,Xtext中的赋值操作为此提供了合理的支持。 但是,一旦有了表达式,通常还需要一个类型系统。 可以争论的是,类型检查仅是约束,而构建合理大小的类型系统却是很多工作,可以提供比普通约束检查更多的支持。 本文介绍了一种框架,用于为使用Xtext构建的(表达式)语言指定类型系统。
您可以从此处 [2]获得该框架。
什么是类型系统?
在计算机科学中,类型系统可以定义为可处理的句法框架,用于根据它们计算的值的种类对短语进行分类。 类型系统将类型与每个计算值相关联。 通过检查这些值的流,类型系统试图证明不会发生类型错误。 所讨论的类型系统确定了构成类型错误的原因,但是类型系统通常试图保证期望某种值的操作不会与对该操作没有意义的值一起使用。
让我们展示一个直观的例子。 在下面的代码的最后一行,我们将整数值(等于等号右边的计算结果)分配给类型为bool的变量。 这是类型错误。 请注意,该程序在结构和语法上都是正确的,但类型不计算。 类型系统的工作是注意到此问题并将其报告给用户,如屏幕快照所示。
类型系统通常由以下构造块组成:
- 类型分配:某些语言元素(例如int和bool关键字)具有固定的类型。
- 类型计算规则:对于所有语言元素,通常可以根据其组成元素的类型或通过以下引用来计算类型。
- typeonstraints:检查以验证某些元素的类型符合语言设计者定义的期望。
本文描述的类型系统框架允许所有这些构建块的有效实现,并与Xtext验证框架集成。
何时使用类型系统
您可以(并且应该)问这个问题:何时使用类型系统,何时仅使用“常规”约束。
首先,两者不是互斥的。 您可以轻松地将它们混合。 毕竟,作为Xtext验证的一部分,将检查类型系统约束。
对于包含表达式,赋值,函数调用以及(最重要的是)许多不同的运行时变量类型(可能带有类型层次结构)的语言,类型系统特别有用。 例如,您可以使用类型系统来检查(Java风格的)局部变量声明中, init表达式的类型是否与变量的声明类型兼容。 但是,最明显的是,如果您的语言中有表达式树,则类型系统很有用,如下图所示:
在大多数表达式语言中,表达式是树,其中每个节点具有零个,一个或两个子代,并且类型是递归计算的。 因此,在上面的示例中,根的类型( Plus )是通过递归计算子表达式的类型来计算的。
类型系统框架针对这些类型的语言进行了优化。
示例语法
我们从一种简单的语言开始,该语言用于定义变量和用于计算值的表达式。 目前,我们支持整数和布尔类型。 这是一个示例程序/模型。
var int a
var int b
var int ccalc int x = a
calc int y = a + c
calc int z = a * a + b
定义此语言的语法应为Xtext用户所熟悉。 请注意,本文的目的不是解释如何定义(递归)表达语言的语法。 您可能想看一下Xtext文档 [1]来了解这种表示法。 这是语法:
grammar expr.ExprDemo with org.eclipse.xtext.common.Terminals
generate exprDemo " http://www.ExprDemo.expr "
import " http://www.eclipse.org/emf/2002/Ecore " as ecore
Model:
elements+=Element*;
Element:
VarDecl | Formula;
VarDecl returns Symbol :
{VarDecl} "var" type=Type name=ID ";" ;
Type:
IntType | BoolType | FloatType;
IntType:
{IntType} "int" ;
BoolType:
{BoolType} "bool" ;
FloatType:
{FloatType} "float" ;
Formula:
"calc" type=Type name=ID "=" expr=Expr ";" ;
Expr:
Addition;
Addition returns Expression:
Multiplication ({Plus.left= current } "+" right=Multiplication)*;
Multiplication returns Expression:
Atomic ( {Multi.left= current } "*" right=Atomic)*;
Atomic returns Expression:
{SymbolRef} symbol=[Symbol|QID] |
{NumberLiteral} value=NUMBER;
terminal NUMBER returns ecore::EBigDecimal:
( '0' .. '9' )* ('.' ( '0' .. '9' )+)?;
terminal INT returns ecore::EInt:
"$$$don't use this anymore$$$";
QID: ID ( "." ID)*;
注意我们如何对可以引用的项目使用通用概念符号 ; 因为我们希望以后能够定义其他种类的可引用事物,并且由于Xtext链接机制的限制,我们必须采用这种方式。 对于某些引用,我们以后将需要限定的(点号)名称,因此需要QID。
配置
请确保在您的Eclipse安装或工作区中可以使用de.itemis.xtext.typesystem插件。 该插件反过来又依赖于Xtext,因此,当前,如果未安装Xtext,则无法使用它。
然后,确保您的语言项目(示例中的expr )对类型系统插件有依赖性。
类型系统类
编程的第一个任务是实现一个实现类型系统本身的类。 坚持使用Xtext项目结构,我们创建了一个名为ExprTypesystem的类,并将其放在expr语言项目中的适当位置。
从理论上讲,此类必须实现ITypesystem接口,但是在绝大多数情况下,您都希望直接从DefaultTypesystem继承。 为此,将需要您实现其initialize方法,我们将做进一步的介绍。
从理论上讲,此类必须实现ITypesystem接口,但是在绝大多数情况下,您都希望直接从DefaultTypesystem继承。 为此,将需要您实现其initialize方法,我们将做进一步的介绍。
public class ExprTypesystem extends DefaultTypesystem {
@Override protected void initialize() {
}
}
与验证器集成
如上所述,类型系统的一个方面是对类型检查的支持,显然必须将其与Xtext验证框架集成在一起。 为此,请在验证器中插入以下代码:
public class ExprDemoJavaValidator
extends AbstractExprDemoJavaValidator {
@Inject private ITypesystem ts ;
@Check public void checkTypesystemRules( EObject x ) {
ts .checkTypesystemConstraints(x, this );
}
}
如您所见,验证方法为模型中的每个对象调用类型检查方法。 注意我们如何使用Xtext样式的Google Guice注入来获取类型系统。 为了使这项工作有效,我们必须在运行时模块中实现一个绑定程序方法(有关详细信息,请参见Xtext文档),该方法将接口与我们的实现类相关联:
public class ExprDemoRuntimeModule
extends expr.AbstractExprDemoRuntimeModule {
public Class<? extends ITypesystem> bindITypesystem() {
return ExprTypesystem.class;
}
}
信息弹出窗口
能够跟踪程序中的类型很重要。 理想情况下,您想要选择任何程序元素,按某种组合键并获取有关运行时类型的信息,如以下屏幕截图所示:
信息弹出窗口显示元素的限定名称,其元类,其类型以及说明如何计算类型的跟踪。 在上面的示例中,它很简单,因为int概念具有直接与其关联的类型。 如果涉及更复杂的类型计算规则,则此计算将在弹出窗口中显示为树结构,使您能够理解和跟踪自己的键入规则(由于其递归性质,这可能是不平凡的)。
为了使此弹出窗口起作用,您基本上必须实现一个普通的Eclipse文本编辑器弹出窗口。 重要的一点是为弹出窗口组合文本内容的代码:
private String getDescription( final int offset,
final XtextResource resource) {
IParseResult parseResult = resource.getParseResult();
CompositeNode rootNode = parseResult.getRootNode();
AbstractNode currentNode =
ParseTreeUtil. getLastCompleteNodeByOffset (rootNode, offset);
EObject semanticObject = NodeUtil. getNearestSemanticObject (currentNode);
StringBuffer bf = new StringBuffer();
bf.append( "QName: " + qfnp. getQualifiedName(semanticObject )+ "\n" );
bf.append( "Metaclass : "+semanticObject.eClass().getName()+ "\n" );
TypeCalculationTrace trace = new TypeCalculationTrace();
EObject type = ts .typeof(semanticObject, trace);
if ( type != null ) {
bf.append ("Runtime Type: " + ts .typeString(type));
} else {
bf.append ("Runtime Type: <no type>" );
}
bf.append( "\n\nTrace: " );
for (String s: trace.toStringArray()) {
bf.append( "\n " +s);
}
return bf.toString();
}
现在,我们准备实施我们的第一个打字规则。
基本打字规则
键入规则和类型检查分为两类:声明性和过程性。 暂时,我们坚持声明式的。 当前,在Java方法调用中从声明方法中实现声明性代码。 此框架的后续版本可能对该部分使用Xtext DSL。
克隆作为类型
让我们首先定义一个int或bool的类型是其自身的克隆。 这是最简单的键入规则。 它还展示了可以将任何EObject用作类型。 这里是所有规则:
@Overrideprotected void initialize() {
ExprDemoPackage lang = ExprDemoPackage. eINSTANCE ;
try {
useCloneAsType(lang.getIntType());
useCloneAsType(lang.getBoolType());
} catch (TypesystemConfigurationException e) {
// TODO Auto-generated catch block e.printStackTrace(); }
}
上面的代码指出,每当某人或某人想知道IntType ( int )或BoolType ( bool )概念的类型时,都会返回该元素本身的克隆。
我们可以通过运行编辑器并在这些int或bool类型之一上按Ctrl-Shift-I来进行尝试。 弹出窗口将向我们显示类型。
从特征派生类型
现在让我们看一下var的类型(如var int i; )和calc的类型(如calc x = 2 * i;) 。 它们的类型可以从它们的类型的孩子,这是我们已经在上一节中指定的类型推导(抱歉重超载字“型”的,无法避免在这里!)。 我们在类型系统规范中添加以下两行:
useTypeOfFeature(lang.getVarDecl (), lang.getElement_Type());
useTypeOfFeature(lang.getFormula(), lang.getElement_Type());
请注意,这已经是类型计算规则的第一个示例,因为我们没有规定固定类型,而是从各个元素的特征中派生类型。
我们可以为变量(即符号)引用实现类似的键入规则。 符号引用的类型是它引用的符号的类型:
useTypeOfFeature(lang.getSymbolRef(),
lang.getSymbolRef_Symbol());
使用固定类型
现在,让我们还将Plus和Multi表达式的类型指定为ints 。
useFixedType(lang.getPlus(), lang.getIntType() );
useFixedType(lang.getMulti(), lang.getIntType() );
useFixedType方法将固定类型类(第二个参数)与语言构造(第一个参数)相关联。 当询问概念的类型时,将实例化该类。 还可以传入一个对象(可能将某些属性设置为某些值)作为第二个参数,请注意方法的名称不同:
usePrototypeAsType( <concept class>, <an EObject instance> );
目前,这些都是我们需要的所有打字规则。 程序的每个元素现在都应该具有关联的类型,可以通过弹出窗口对其进行验证(稍后将介绍自动测试)。
简单类型检查
当然,您可以使用标准的Xtext验证来实现类型检查。 但是,对于常规检查,有声明性的快捷方式。
在我们的语言中,我们必须确保Plus和Multi的参数类型为int而不是bool 。 可以指定如下:
ensureFeatureType (lang.getPlus(),
lang.getPlus_Left(), lang.getIntType());
ensureFeatureType (lang.getPlus(),
lang.getPlus_Right(), lang.getIntType());
ensureFeatureType (lang.getMulti(),
lang.getMulti_Left(), lang.getIntType());
ensureFeatureType (lang.getMulti(),
lang.getMulti_Right(), lang.getIntType());
您可以将任何类型的类型传递给此函数,以指定替代项。 因此,如果要允许Plus的第二个参数为int或bool ,则可以编写以下内容:
ensureFeatureType (lang.getPlus(), lang.getPlus_Right(),
lang.getIntType(), lang.getBoolType() );
另外,您还可以传入一个或多个CustomTypeChecker实例,以任何您喜欢的方式实现其isValid方法。
public abstract class CustomTypeChecker {
private String info ;
public CustomTypeChecker( String info ) {
this . info = info;
}
@Override public String toString() {
return info ;
}
public abstract boolean isValid( ITypesystem ts,
EObject type, TypeCalculationTrace trace );
}
在最后一步中,我们应确保公式中等号的右侧与左侧兼容。 在简单类型的系统中,“兼容”表示“相同”。 在更复杂的类型系统中,必须支持各种子类型关系(我们将在下面介绍)。 这是我们需要的代码:
ensureOrderedCompatibility (lang.getFormula(),
lang.getElement_Type(), lang.getFormula_Expr());
这指定了对于Formulas , expr的类型必须与该类型的类型 “兼容”(请注意,由于VarDecl和Formula都具有该属性,因此如何将type属性拉到Element上 )。 您还可以编写以下内容:
ensureOrderedCompatibility (lang.getFormula(),
lang.getFormula_Expr());
通过仅使用一个参数调用该方法,该元素本身的类型将用作比较的左侧。 在我们的情况下,这也将起作用,因为上面指定的类型推断规则指出:
useTypeOfFeature(lang.getFormula(), lang.getElement_Type());
实际上有两个相关的方法:ensureOrderedCompatibility和确保人 orderedCompatibility。
打电话时
ensureOrderedCompatibility (c, f1, f2 );
那么约束条件要求f1和f2的类型相同,或者f2是f1的子类型。 在我们的公式示例中,如果int是float的子类型(这很有意义,因为int可以被视为float的特例),则可以使用
calc float x = <something with int type>
会没事的。 sureOrderedCompatibility将对此进行检查。
相反,打电话时
ensureUnorderedCompatibility (c, f1, f2 );
那么约束要求f1和f2的类型相同,或者f2是f1的子类型,反之亦然。 因此,此约束是双向的。 例如,这对于我们的Plus很有用,其中left或right参数可以是ints或floats ,并且仍然有效:
ensureUnrderedCompatibility (lang.getPlus(),
lang.getPlus_Left(), lang.getPlus_Right() );
子类型化
让我们介绍数字文字。 这是对语法的必要更改:
Atomicreturns Expression :
{SymbolRef} var=[Symbol] |
{NumberLiteral} value=NUMBER;
terminal NUMBER returns ecore::EBigDecimal:
( '0' .. '9' )* ( '.' ( '0' .. '9' )+)?;
terminal INT returns ecore::EInt:
"$$$don't use this anymore$$$";
请注意,小数点及其后面的数字是可选的。 换句话说,如果有一个点,那么我们就有一个浮点数。 如果不是,则为int 。 我们必须执行此键入规则。 让我们首先介绍float作为一种类型:
Type:
IntType | BoolType | FloatType;IntType:
{IntType} "int" ;
BoolType:
{BoolType} "bool" ;
FloatType:
{FloatType} "float" ;
这种类型的NumberLiteral就是上面使用的声明方法不起作用的示例。 NumberLiteral的类型不是固定的,而是取决于数字的值。 如果值包含小数点,则为浮点数 ,否则为int 。
这是代码; 它是通过Xtext的多态调度程序调用的类型系统类中的类型方法实现的:
public EObject type( NumberLiteral l,
TypeCalculationTrace trace ) {
if ( l.getValue().toString().indexOf( "." ) > 0 ) {
return Utils.create( lang .getFloatType());
} else {
return Utils.create( lang . getIntType ());
}
}
您可以使用编辑器中的弹出窗口来验证它是否可以正常工作。 当然,我们必须对类型系统进行更多更改才能使键入工作。 这是更改。
首先, Plus和Multi的left和right属性也应该可以是float ,而不仅仅是ints 。 我们只需将其他类型传递给sureFeatureType方法。
ensureFeatureType(lang .getPlus(), lang .getPlus_Left(),
lang .getIntType(), lang .getFloatType());
ensureFeatureType( lang .getPlus(), lang .getPlus_Right(),
lang .getIntType(), lang .getFloatType());
ensureFeatureType( lang .getMulti(), lang .getMulti_Left(),
lang .getIntType(), lang .getFloatType());
ensureFeatureType( lang .getMulti(), lang .getMulti_Right(),
lang .getIntType(), lang .getFloatType());
另外, Plus和Multi本身的类型现在不仅仅是简单的固定类型( int ),而是应该计算两者的公共(即,不太具体)类型的计算:
computeCommonType(lang .getPlus(),
lang .getPlus_Left(), lang .getPlus_Right() );
computeCommonType( lang .getMulti(),
lang .getMulti_Left(), lang .getMulti_Right() );
为了使此工作有效,我们必须将int声明为float的子类型(从数学意义上讲, float更通用!)
declareSubtype(lang .getIntType(), lang .getFloatType());
然后我们可以编写这样的代码,并且应该获得相应的错误标记。
calc int t1 = 2; // works
calc int t2 = 2.2; // error: float cannot be assigned to int
calc float t3 = 2.2; // works
calc float t4 = 2; // works; int can be assigned to float
calc int t5 = 2 + 2; // works; type of plus is int
calc int t6 = 2 + 2.3; // error:common type of 2 and 2.3 is float
// which cannot be assigned to int
结构化类型
到目前为止,我们仅将类型视为不透明的对象。 一个int是一个int是一个int 。 现在让我们考虑结构化类型,其中类型对象的属性与类型检查有关。
类型比较功能
我们以枚举为例。 这是一些示例代码:
enum color {
red green blue
}
enum shape {
rect triangle circle
}
var color col1;
calc color col2 = shape.circle;
下面的代码显示了对语法的必要更改:
Element:
VarDecl | Formula | EnumDecl;EnumDecl:
"enum" name=ID "{" (literals+=EnumLiteral)* "}" ;
EnumLiteral returns Symbol:
{ EnumLiteral } name=ID;
// …
Type:
IntType | BoolType | FloatType | EnumType;
EnumType:
enumRef=[ EnumDecl ];
// …
如果我们不对类型系统进行任何更改,则会收到警告:当系统试图确保calc的类型兼容时,它会注意到颜色符号引用的类型为null 。 这是因为EnumTypes还没有类型。
让我们首先定义EnumDecl的类型。 它应该是一个EnumType,其enumRef引用指向它表示的枚举 。 这就是“结构化”部分的用处。(通常)仅说某物是一个枚举是不够的。 我们必须说它是哪个 枚举 。 下面的自定义类型函数为EnumDecl构建此结构。
public EObject type( EnumDecl l, TypeCalculationTrace trace ) {
EnumType t = (EnumType) Utils.create( lang .getEnumType());
t.setEnumRef (l);
trace.add(l, "enum, type is " +typeString(t));
return t;
}
我们还定义了EnumType的类型是它引用的枚举的类型-即我们在上述方法中创建的东西:
useTypeOfFeature(lang.getEnumType(), lang.getEnumType_EnumRef());
这使警告消失,但又显示了一个新警告。 现在,系统抱怨枚举文字引用(在计算的等号右侧)没有类型。 如果我们查看跟踪,就会发现引用的类型是被引用对象的类型,但是该对象的类型( 枚举常量)为null。 让我们解决这个问题; useTypeOfAncestor使用给定元素的祖先的类型(此处为EnumDecl )作为所讨论元素的类型。
useTypeOfAncestor(lang .getEnumLiteral(), lang .getEnumDecl());
这使所有警告消失,但不能解决明显的问题:我们不应该能够将shape 枚举常量分配给color类型的变量。 尽管它们都是EnumTypes ,但是它们是不同的枚举 ,这使它们成为不同的类型。 我们必须制定最后一个规范:
declareTypeComparisonFeature(lang.getEnumType(),
lang.getEnumType_EnumRef());
这样可以确保在比较类型时,并且两个类型都是EnumTypes ,然后系统将比较enumRef引用的值。 注意,这实际上只是一个相等的比较。 没有考虑子类型化规则等。 在下一个示例中,我们将解决这个问题。
类型递归功能
让我们介绍数组来演示这一点。 这是我们希望能够编写的代码:
var int i;
var array[int] anIntArray;
var array[float] aFloatArray;// works: assign two int arrays
calc array[int] anotherOne = anIntArray;
// error: cannot assign float array to int array
calc array[int] anotherOne2 = aFloatArray;
// works: int array is a "subtype" of float array
calc array[float] arr3 = anIntArray;
// works: array access makes it an int
calc int atest = anIntArray[i+1];
// works :-)
calc float atest2 = anIntArray[i+1] + 3.7;
让我们首先相应地扩展语法。 以下是相关部分:
Type:
PrimitiveType | ArrayType;PrimitiveType:
IntType | BoolType | FloatType | EnumType;
ArrayType:
{ArrayType} "array" "[" baseType=Type "]" ;
Multiplication returns Expression:
PostfixOperators ( { Multi .left= current } "*" right=PostfixOperators)*;
PostfixOperators returns Expression:
Atomic ({ ArrayAccess .expr=current} "[" index=Expr "]")?;
Atomic returns Expression:
{ SymbolRef } symbol=[ Symbol |QID] |
{ NumberLiteral } value=NUMBER;
现在让我们解决输入问题。 我们首先必须像下面那样组装ArrayType的结构化类型
var array[int] anIntArray;
这是执行此操作的代码:
public EObject type( ArrayType a, TypeCalculationTrace trace ) {
ArrayType arraytype =
(ArrayType) Utils.create( lang .getArrayType());
EObject basetype = typeof( a.getBaseType(), trace );
arraytype.setBaseType( (Type) basetype );
trace.add(a, "base type is " +typeString(basetype));
return arraytype;
}
这样做,我们将能够彼此分配任意两个数组,因为我们尚未声明在比较类型时应考虑ArrayType的基本类型。 但是,与枚举示例相反,我们希望能够将一个int数组分配给一个float数组,如以下示例所示:
// works: int array is a "subtype" of float array
calc array[float] arr3 = anIntArray;
因此,我们必须确保考虑了ArrayType内部基本类型的子类型关系。 这是我们的操作方式(请注意与上述比较相比, 递归 ):
declareTypeRecursionFeature(lang .getArrayType(),
lang .getArrayType_BaseType());
这使我们可以使用数组类型变量并使类型系统正常工作。 但是,我们仍然需要解决ArrayAccess问题,如下所示
// works: array access makes it an int
calc int atest = anIntArray[i+1];
首先,我们必须确保应用[]的表达式实际上是一个数组:
ensureFeatureType ( lang .getArrayAccess(),
lang .getArrayAccess_Expr(), lang .getArrayType());
然后,我们必须确保方括号中的表达式的类型为int :
ensureFeatureType(lang .getArrayAccess(),
lang .getArrayAccess_Index(), lang .getIntType());
最后,我们必须定义ArrayAccess的类型并从数组中提取该基本类型:
public EObject type( ArrayAccess a, TypeCalculationTrace trace )
{
ArrayType arrayType =
(ArrayType) typeof( a.getExpr(), trace );
trace.add( a, "array type is " +typeString(arrayType));
Type bt = arrayType.getBaseType();
trace.add( a, "base type is " +typeString(bt));
return bt;
}
而已。
类型特征
类型可以与所谓的特征相关联。 这些有点像标记或标记界面。 本文档中的示例不适合使用特性,因此这里是一个一般性的说明。
您可以通过实例化相应的类来定义特征:
TypeCharacteristic iterable = new TypeCharacteristic("iterable");
您现在可以将任何类型与这种特征相关联,例如:
declareCharacteristic( lang.getSomeClass(), iterable );
现在,您可以将其用作可以检查的类型:
ensureFeatureType( lang.getAnotherClass(),
lang.getAnotherClass_Feature(), iterable );
当然,我们的想法是为几种类型声明相同的特征,然后仅检查该特征。
类型强制
类型强制是隐式类型转换。 例如,如果在给定的上下文中期望一个int (即int类型的表达式将是有效的),但是我们实际上提供了string ,那么在没有强制的情况下,这将是一个错误:
var int v4 = "Hallo";// error without coercion
现在假设我们具有以下强制规则:
- 如果需要一个整数 ,
- 我们实际上提供了一个字符串 ,
- 元素是字符串文字,
- 它的值可以转换为int (即字符串仅包含数字)
- 然后将其视为int 。
然后,我们可以编写以下代码:
var int v4 = "Hallo" ; // error: string, but int expected
var int v5 = "100" ; // works, because of custom coercion rule!
calc int cc = 10 + "100" ; // works, coercion
尽管这是一个有些人为的示例,但我不得不在现实世界项目的许多地方使用强制。 请注意,强制规则与仅更改类型定义不同。 我们不希望仅数字字符串文字在任何地方都被视为int -如果程序否则无效,我们只是想将它们视为int 。
强制规则在类型系统类中作为多态调度方法实现。 这是我们强制性规则的实现:
public EObject typeCoerce( EObject candidateElement,
StringType candidate, IntType expected,
TypeCalculationTrace trace ) {
if ( candidateElement instanceof StringLiteral ) {
try {
Integer. valueOf (
((StringLiteral) candidateElement).getValue());
return create( lang .getIntType());
} catch ( NumberFormatException fallthrough ) {}
}
return null ;
}
签名必须是:
- 我们要强制其类型的元素
- 当前类型(多态)
- 我们试图强制的类型(多态)
- 和TypeCalculationTrace一样。
该方法必须返回计算出的类型,如果没有强制,则返回null 。
测试中
测试语言结构很简单:只需使用所需的语法写下模型,然后查看它是否可以解析。 通过提供相当多的示例模型集(覆盖率),可以确保要编写的所有程序均可用。 批量运行所有这些程序的解析器基本上可以提供必要的自动化。
约束检查,特别是类型检查,测试起来并不容易,尤其是由于回归。 类型系统规则是不平凡的,并且通常是递归的。 专用支持很有用; 支持人员应该能够从JUnit测试中加载模型。
基本
作为JUnit 4测试用例的一部分,类型系统框架随附了几个用于测试约束的帮助程序类。 包含测试的项目需要依赖de.itemis.xtext.typesystem.testing插件,该插件包含所有基类和实用程序。
这是一个简单的测试:
public class Basic extends XTextTestCase {
@Test
public void testTypesOfParams() throws Exception {
EObject root =
initializeAndGetRoot(new ExprDemoStandaloneSetup(),
R. modelroot + "/basic.expr" );
assertConstraints( allIssues .errorsOnly().sizeIs(0) );
}
使用initializeAndGetRoot方法,您可以读取模型文件。 您可以传入任意数量的文件,它们都已加载到同一资源集中,因此可以对其他文件进行交叉引用。 但是, allIssues集合仅包含第一个文件(我们称其为主文件)中的问题。
编写实际测试基于两个主要因素: assertConstraints方法和基于流利接口的问题过滤方法。 在上面的示例中,您可以看到我们断言issue集合包含零个错误。
如果断言失败,则异常错误消息将跟踪哪些约束或过滤器(见下文)实际失败。
您还可以将其他ID作为第一个参数传递给assertConstraints方法。 如果这样做,则此ID将在约束中的错误消息中输出,并帮助您跟踪问题的位置。
筛选问题
在以下示例中,我们断言在计算中存在不兼容的类型两个:
assertConstraints(allIssues .forType(Formula. class ).named( "t2" ).
theOneAndOnlyContains( "incompatible" ) );
assertConstraints( allIssues .forType(Formula. class ).named( "t6" ).
theOneAndOnlyContains( "incompatible" ) );
IssueCollection上有许多可用于过滤的方法。 这是清单:
方法… | …返回一个新的IssueCollection |
forType( t ) | 仅包含附加到t实例的那些问题 |
get( index ) | 在IssueCollection中的位置索引 处 |
inLine( line ) | 是在模型文件逐行 |
withStringFeatureValue( n,v ) | 其名为n的特征的值为(tostring()) v |
errorsOnly() | 不含警告 |
命名( n ) | 仅包含附加到名称属性值为n的元素的那些问题 |
forElement( t,n ) | 仅包含附加到名称为n的类型t元素的那些问题 |
不足( t ) | 仅包含其元素祖先类型为t的那些问题 |
下( t,n ) | 只包含其元素具有名为N的类型t的祖先那些问题 |
我们还使用一些断言方法:
方法… | ……断言 |
sizeIs( s ) | 当前集合的大小为s |
oneOfThemContains( t ) | 集合具有任何大小,并且错误消息之一包含子字符串t |
allOfThemContain( t ) | 集合具有任何大小,并且所有错误消息均包含子字符串t |
theOneAndOnlyContains( t ) | 集合的大小为1,并且singe错误消息包含子字符串t |
最后,您可以在任何IssueCollection上使用dumpIssues()将问题输出到控制台。
杂
特殊类型比较功能
如果默认的类型比较工具(包括子类型)对您不起作用,则可以通过实现compareTypes多态方法来实现自己的策略:
protected Boolean compareTypes( EObject type1, EObject type2,
CheckKind kind, TypeCalculationTrace trace ) {
if ( kind == CheckKind.same ) {
…
} else ….
}
CheckKind (相同,无序,有序)确定期望的比较类型。 不要忘记在跟踪中放入一些信息,以帮助用户了解类型的计算方式。
类型字符串
有时默认的字符串表示形式(由typeString(t)创建)不是很好; 可以使用多态调度的typeToString(t)方法来自定义框架在错误消息中使用的类型的字符串表示形式,如以下示例所示。 这些方法必须在Typesystem类中定义。
public String typeToString( EObject o ) {
String cn = o.eClass().getName();
if ( cn.toLowerCase().endsWith( "type" ))
return cn.substring(0,cn.length()-4);
return null ;
}
public String typeToString( ArrayType a ) {
return "array[" +typeString(a.getBaseType())+ "]" ;
}
自定义错误消息
表示类型系统约束的所有sure ...方法都被重载以将附加字符串作为第一个参数。 该字符串用作自定义错误消息,如
ensureFeatureType("array index must be Int, idiot :)",
lang .getArrayAccess(),
lang .getArrayAccess_Index(),
lang .getIntType());
类型根类
有时,类型系统规则可能并不简单,并且很难始终了解正在发生的事情。 TypeCalculationTrace是一种跟踪方法。 另一种方法是声明应将哪些类用作类型。 您可以为类型声明一组有效的(超级)类,如下所示:
declareTypeRootEClasses(EClass1, EClass2, …);
声明之后,只要您的键入规则返回不是这些类之一(或它们的子类型)的实例的类型对象,您就会收到InvalidType运行时异常。
ITypesystem API
ITypesystem接口是在验证器中与类型系统进行交互的主要API,用于当您不想使用DefaultTypesystem中可用的声明性功能时,或者由于其他原因而需要了解元素的类型时。
看一看该类的JavaDoc,了解如何使用它。 根据上面的教程,根据方法的名称确定方法的作用应该相对简单。
在合并范围中使用类型系统
有时,在范围提供者中查询类型系统很有用,以将代码完成限制为类型兼容的建议。
原则上,这很简单:将类型系统注入范围提供者并使用它来过滤建议。 实际上,这不是那么简单。
如果您的范围是本地的(即在同一文件中),则该方法可能有效。 但是,如果引用的目标位于其他资源中,则问题在于目标对象尚未加载,而您必须处理IEObjectDescriptions 。 但是,它们没有类型,因此不能用于过滤。 诀窍是确保IEObjectDescriptions (存储在索引中)确实包含某些类型信息。
自定义IEObjectDescriptions
我们必须提供我们自己的DefaultResourceDescription实现,如以下代码所示。 本质上,我们计算类型并将其作为字符串存储在IEObjectDescription的用户数据字段中。
public class MyResourceDescription
extends DefaultResourceDescription {
public static final String KEY_TYPE = "type"; private IQualifiedNameProvider nameProv ;
private ITypesystem typesystem ;
public MyResourceDescription (Resource resource,
IQualifiedNameProvider nameProvider, ITypesystem ts) {
super (resource, nameProvider );
this . nameProv = nameProvider;
this . typesystem = ts;
}
@Override protected IEObjectDescription
createIEObjectDescription(EObject from) {
if ( nameProv == null ) return null ;
String qualifiedName = nameProv .getQualifiedName(from);
if (qualifiedName != null) {
if ( from instanceof WhatEverYouWantToIndex ) {
EObject o = // … the element whose type you want to store
EObject type = typesystem .typeof(o,
new TypeCalculationTrace());
return createWithUserData(qualifiedName, from,
KEY TYPE , type.eClass().getName());
}
}
return super .createIEObjectDescription(from);
}
private IEObjectDescription createWithUserData(String qname,
EObject object, String key, String value) {
Map<String, String> userData = new HashMap<String, String>();
userData.put(key, value);
return EObjectDescription.create(qname, object, userData);
}
}
为了确保使用新的ResourceDescription ,我们还必须实现自己的Manager:
public class MyManager
extends DefaultResourceDescriptionManager {
@Inject private ITypesystem ts ;
@Override protected IResourceDescription
internalGetResourceDescription(Resource resource,
IQualifiedNameProvider nameProvider) {
return new MyResourceDescription(resource, nameProvider, ts );
}
}
我们还必须在运行时模块中注册它。
public Class<? extends IResourceDescription.Manager>
bindIResourceDescriptionManager() {
return MyManager. class ;
}
使用范围提供者中的信息
现在,我们终于可以增强范围提供程序了。 它将从IEObjectDescriptions中提取用户数据并将其用于类型过滤:
public IScope scope WhatEverYouWantToIndex(
final ContextType ctx, EReference ref ) {
Map<String, IEObjectDescription> map =
Maps.newLinkedHashMap();
IScope all = delegateGetScope(ctx, ref);
for (IEObjectDescription od: all.getContents()) {
String odtype =
od.getUserData(MyResourceDescription. KEY TYPE );
String contextType = ts .typeof(ctx, new
TypeCalculationTrace()).eClass().getName();
if ( odtype.equals(contextType) ) {
String localName = // … od.getName();
map.put(localName, new
AliasedEObjectDescription(localName, od));
}
}
return new MapBasedScope(IScope. NULLSCOPE , map);
}
结论
当然,本文介绍的类型系统框架不是世界上功能最强大的类型系统框架。 例如, JetBrains MPS使用统一代替此处介绍的相对简单的递归类型计算模型。 另外,我不确定要在此框架内实现Java的类型系统(使用泛型)还是什至像Scala的类型系统。 但是,对于典型的DSL来说已经足够好了。 我(和其他一些人)基于该框架实现了一些非平凡的DSL,并且运行良好。
参考资料
[1] Xtext用户文档
[2] Xtext类型系统框架
关于作者
MarkusVölter在德国斯图加特的itemis AG担任独立研究员,顾问和教练。 他的重点是软件体系结构,模型驱动的软件开发和领域特定的语言以及产品线工程。 马库斯还定期就这些主题写作(文章,图案,书籍)并发表演讲(培训,会议)。 您可以通过www.voelter.de与他联系。
翻译自: https://www.infoq.com/articles/xtext_ts/?topicPageSponsorship=c1246725-b0a7-43a6-9ef9-68102c8d48e1
xtext