MPS 2019.2帮助

类型系统

为您的语言定义类型系统


此页面详细描述了MPS类型系统。如果您希望在定义第一个类型系统规则时使用更轻巧的介绍,请考虑查看“ 类型系统食谱”

如果您想熟悉可以从代码中使用类型系统的方式,还可以参阅使用类型系统一章。

什么是类型系统?

类型系统是语言定义的一部分,该语言将类型分配给使用该语言编写的模型中的节点。类型系统语言还用于检查节点及其类型上的某些约束。有关节点类型的信息可用于:

  • 查找类型错误

  • 在生成期间检查节点类型的条件,以仅应用适当的生成器规则

  • 提供某些重构所需的信息(例如,“提取变量”重构)

  • 和更多

种类

任何MPS节点都可以用作类型。为了使MPS能够将类型分配给您的语言节点,您应该为类型系统创建语言方面。您的语言的类型系统模型将以类型系统语言编写。

推理规则

类型系统语言的主要概念是推理规则 。某个概念的推理规则主要负责为该概念的实例计算类型。

推理规则由条件和主体组成。条件用于确定规则是否适用于某个节点。条件可以有两种: 概念参考模式 。具有概念引用形式的条件的规则适用于该概念及其子概念的每个实例。具有模式的规则适用于与模式匹配的节点。如果节点具有与模式相同的属性和引用,并且其子项与模式的子项匹配,则该节点与该模式匹配。模式也可能包含匹配所有内容的多个变量。

推理规则的主体是将规则应用于节点时执行的语句列表。类型系统语言的主要语句类型是用于创建类型之间的方程式不等式的语句。

推论规则可以定义覆盖块,这是一个布尔型标志,它告诉类型检查器,万一还有其他推论规则适用于条件中指定的概念的超概念,则此推论规则优先,并且所有规则超概念被忽略。3.3版带来了使用代码块代替静态标志的可能性。

从版本3.3开始,适用于节点属性实例的推理规则具有其他功能,这些功能允许覆盖或修改应用于属性节点的规则。例如,这可以用于基于存在条件来实现备用类型推断,该条件可以考虑在项目或系统级别指定的参数。

如果推理规则适用于节点属性,则也有可能告诉类型检查器该规则取代适用于属性节点的规则,然后将其忽略。同样,可以在所有规则的代码块中将属性节点作为attributedNode访问

 

推论方法

为了避免重复,可能需要将几种推理规则的相同代码部分提取到一种方法中。推论方法只是标有注解“ @InferenceMethod ”。您只能在推理规则,替换规则和推理方法中使用几种语言构造,它们是: typeof表达式, 等式不等式具体语句, 类型变量声明类型变量引用以及推理方法的调用 。这样做是为了不要在任意方法中使用这样的构造,这些方法可以在任意上下文中调用,也许不能在类型检查期间使用。

覆写

子概念的类型系统规则可以覆盖在超级概念上定义的规则。如果overrides标志设置为false ,则该规则将与为超级概念定义的规则一起添加到应用于概念的规则列表中;而如果将该标志设置为true ,则重写规则将替换以下规则:规则引擎中的超级概念,因此它们不会生效。这适用于推理规则和NonTypeSystem规则。

方程和不等式

类型系统引擎执行的主要过程是求解所有类型之间的方程式不等式的过程。语言设计人员通过在推理规则中编写引擎来告诉引擎应解决的方程式。要将等式添加到引擎中,请使用以下语句:

expr1 :==: expr2 ,在哪里expr1expr2是表达式,其计算结果为node

考虑以下用例。您想说局部变量引用的类型等于它所指向的变量声明的类型。所以,你写typeof (varRef) :==: typeof (varRef.localVariableDeclaration) , 就这样。类型系统引擎将自动求解此类方程。

上述表达typeof(expr) (哪里expr必须评估为MPS节点)是一种语言构造,它返回一个所谓的类型变量,该变量用作该节点的类型 。在方程求解过程中,类型变量逐渐变为具体类型

在某些情况下,您想说某种类型不必完全等于另一种类型 ,但也可以是该类型的子类型类型。例如,方法调用的实际参数的类型不必与方法的形式参数的类型相同-它可以是其子类型。例如,要求对象作为参数的方法也可以应用于字符串。

为了表达这种约束,您可以使用不等式代替方程式 。不等式表示某个类型应该是另一种类型的子类型的事实。内容如下: expr1 :<=: expr2

弱和强子类型化

子类型的关系对于几种不同的情况很有用。您希望实际参数的类型是形式参数类型的子类型,或者希望分配值的类型是变量的声明类型的子类型;在方法调用或字段访问操作中,您希望操作数的类型成为方法的声明类的子类型。

有时,此类需求有些争议:例如,考虑将int和Integer这两种类型传递给方法时希望可互换的两种类型: doSomething(int i) ,拨打电话是合法的doSomething(1)以及doSomething(new Integer(1)) 。但是,当将这些类型用作方法调用的操作数类型时,情况就不同了:例如,您不应该能够调用类型为int的表达式(例如整数常量)的方法。因此,我们必须得出这样的结论: intInteger是彼此的子类型,而在另一方面,它们不是。

为了解决这种争议,我们引入了两个子类型关系:即子类型和子类型。弱子类型将从强子类型开始: 如果一个节点是另一个节点的强子类型,那么它也是它的弱子类型

然后,我们可以说一个例子,即int和Integer是彼此的弱子类型,但它们不是强子类型。赋值和参数传递仅需要弱子类型,方法调用需要强子类型

当您在类型系统中创建不等式时,可以将其选择为强不等式。同样,子类型化规则(声明子类型关系的子类型规则)可以是弱的也可以是强的。弱等式看起来像:<=: ,强烈的不平等看起来像:<<=:

在大多数情况下,您要声明强子类型并检查弱子类型。如果不确定,您需要哪种子类型,对不等式使用弱一,对子类型规则使用强一。

子类型化规则

当类型系统引擎解决不等式时,它需要有关某个类型是否为另一种类型的子类型的信息。但是类型系统引擎如何知道呢?它使用子类型化规则 。子类型化规则用于表达类型之间的子类型关系。实际上,子类型化规则是一个给定类型的函数,它返回其直接超类型。

子类型化规则由条件(可以是概念引用或模式)和主体组成,主体是计算和返回节点的语句列表或给定节点的直接超类型的节点列表。当检查某个类型A是否为另一个类型B的超类型时,类型系统引擎将子类型化规则应用于B,并计算其直接超类型,然后将子类型化规则应用于这些超类型,依此类推。如果类型A在计算的类型B的超类型中,则答案为“是”。

默认情况下,子类型化规则说明的子类型很强。如果只想声明弱子类型,则将规则的“弱”属性设置为“真”。

比较不等式和比较规则

考虑您要为EqualsExpression (运算符==在Java,BaseLanguage和其他语言中):您希望EqualsExpression的左操作数和右操作数具有可比性,即左操作数的类型应为右操作数的(非严格)子类型,反之亦然。为了表达这一点,您以一种形式写了一个比较不等式expr1 :~: expr2 ,其中expr1expr2是表示类型的表达式。如果满足了这样的不等式expr1是...的子类型expr2(expr1 <: expr2) expr2 <: expr1

然后,请考虑,即使任何Java接口都不是彼此的子类型,它们也应该是可比较的。这是因为总是可以编写一个实现两个接口的类,因此接口类型的变量可以包含相同的节点,并且接口类型的变量可以强制转换为任何其他接口。因此, 两种类型都作为接口类型的Equationcastinstanceof表达式应该合法(例如,在Java中是合法的)。

要声明这种可比性(并非源于子类型关系),应使用比较规则 。比较规则由两个适用类型的两个条件和一个主体组成,如果两个类型是可比较的,则主体返回true,否则将返回false。

这是接口类型的比较规则:

比较规则interfaces_are_comparable
适用于concept = ClassifierType作为classifierType1,concept = ClassifierType as classifierType2适用总是会覆盖错误规则{如果(classifierType1.classifier.isInstanceOf(Interface)&& classifierType2.classifier.isInstanceOf(Interface)){返回true; } else {返回false; }}

报价单

引号是一种语言构造,可让您轻松创建具有所需结构的节点。当然,您可以使用smodelLanguage创建一个节点,然后使用相同的smodelLanguage手动为其填充适当的子代,属性和引用。但是,有一种更简单且更直观的方法可以完成此操作。

引号是一个表达式,其值是写在引号内的MPS节点。可以将引号视为“节点文字”,其构造类似于数字常量和字符串文字。也就是说,如果您静态地知道您的意思是什么,就写一个文字。因此,在引号内您无需编写一个计算结果为节点的表达式,而应编写节点本身。例如,一个表达式2 + 3评估为5 , 一种表达< 2 + 3 > (成角度的支架是报价括号)的计算结果为与leftOperand作为一个IntegerConstant 3rightOperandIntegerConstant一个节点PlusExpression 5

(有关报价,反报价和轻报价的更多详细信息,请参见“ 报价”文档)

反引号

因为它是文字,所以应该静态知道报价值。另一方面,如果您仅动态地知道节点的某些部分(例如子级,引用对象或属性),即那些只能在运行时评估而在设计时未知的部分,则您不能使用只需引用即可创建具有此类零件的节点。

不过,好消息是,如果您静态地了解节点的大部分,并且只想用动态评估的节点替换几部分,则可以使用反引号 。反引号可以有4种类型: childreferencepropertylist反引号。它们都包含一个表达式,该表达式将动态求值以用其结果替换引用节点的一部分。子反引用和引用反引用评估为节点,属性反引用评估为字符串,列表反引用评估为节点列表。

例如,您想使用ArrayList类创建一个ClassifierType ,但是只能动态地知道其类型参数,例如通过调用一个方法,例如“ computeMyTypeParameter()”。

因此,您将编写以下表达式: > 。这里的构造%(...)%是节点反引号。

您也可以分别用^(...)^$(...)$反引用引用目标和属性值;或使用*(...)的一个角色的孩子列表*

a)如果要用表达式计算的节点替换带引号的节点内的某个节点,请使用节点反引号,即%()%。您可能会猜到没有必要在内部带有表达式的反引号替换整个被引用的节点,因为在这种情况下,您可以直接在程序中编写这样的表达式。

因此,节点反引号用于替换被引用节点的子代,孙代,曾孙代和其他后代。因此,反引号内的表达式应返回一个节点。要编写这样的反引号,请将插入符号放在孩子的单元格上,然后键入“%”。

b)如果要将引用节点的引用目标替换为由表达式评估的节点,请使用引用反引号,即^(...)^。要编写这样的反引号,请将插入符号放在要引用的单元格上,然后键入“ ^”。

c)如果要替换具有多基数角色的子代(或子代更深的子代),并且由于这个原因,您可能不想将其替换为单个节点,而是将其替换为多个节点,则使用子列表(为简便起见,仅使用列表)反引号*()* 列表反引号内的表达式应返回节点列表,其类型为nlist <..>或兼容类型(即list <..>><..>以及其他一些也可以)。要编写这样的反引号,请将您的插入符号放在子集合中一个孩子的单元格上,然后键入“ *”。您不能在一个空的子集合上使用它,因此在按下“ *”之前,您必须在其中输入一个孩子。

d)如果要用动态计算的值替换引用节点的属性值,请使用属性反引号$()$。引号内的表达式应返回字符串,该字符串将是被引用节点的反引号属性的值。要编写这样的反引号,请将插入符号放在属性的单元格上,然后键入“ $”。

(有关报价,反报价和轻报价的更多详细信息,请参见“ 报价”文档)

推理规则的例子

以下是推理规则的最简单的基本用例:

  • 为概念的所有实例分配相同的类型(主要用于文字):

    适用于concept = StringLiteral作为nodeToCheck {typeof(nodeToCheck):==:<字符串>}
  • 使声明的类型及其引用等同(例如,变量及其用法):

    适用于concept = VariableReference as nodeToCheck {typeof(nodeToCheck):==:typeof(nodeToCheck.variableDeclaration)}
  • 为具有类型注释的节点提供类型(例如,变量声明的类型):

    适用于concept = VariableDeclaration as nodeToCheck {typeof(nodeToCheck):==:nodeToCheck.type}
  • 为特定节点的类型建立限制:对于方法的实际参数,类型变量的初始化程序,赋值的右手部分等有用。

    适用于concept = AssignmentExpression as nodeToCheck {typeof(nodeToCheck.rValue):<=:typeof(nodeToCheck.lValue)}

类型变量

在类型评估期间,在类型系统引擎内部,类型可以是具体类型(节点)或所谓的类型变量 。同样,它可能是一个包含一些类型变量作为其子代或其他后代的节点。类型变量表示未定义的类型,由于求解包含该类型变量的方程式,其可能变为具体类型。

类型变量主要在运行时出现,这是“ typeof”操作的结果,但是您可以根据需要手动创建它们。在类型系统语言中有一个称为TypeVarDeclaration的语句可以做到这一点。您将其写为“ var T”或“ var X”或“ var V”,即“ var”,后跟类型变量的名称。然后,您可以使用变量,例如在反引号中创建一个内部带有类型变量的节点。

示例:“ for each”循环的推理规则。Java中的“ for each”循环由一个循环主体,一个可迭代的迭代对象和一个变量组成,在下一次迭代之前,将一个iterable的下一个成员分配给该变量。一个Iterable应该是Iterable接口的子类的实例,或者是一个数组。为了简化示例,我们不考虑将iterable作为数组的情况。因此,我们需要表达以下内容: iterable的类型应该是某事物的Iterable的子类型 ,而变量的类型应该是 某物的Iterable的 超类型 。例如,您可以编写以下内容:

对于(String s:new ArrayList (...)){...}

或以下内容:

对于(对象o:new ArrayList (...)){...}

上面两个示例中的Iterables的类型均为ArrayList ,它是Iterable的子类型 。变量的类型分别为StringObject ,它们都是String的子类型。

如我们所见,一个Iterable的类型应该是某个Iterable的子类型,而该变量的类型应该是该东西的超类型。但是,如何用类型系统语言说“那很”呢?答案是,这是一个类型变量,我们用它来表示可迭代类型和变量类型之间的联系。因此,我们编写以下推理规则:

适用于concept = ForeachStatement作为nodeToCheck {var T; typeof(nodeToCheck。iterable):<=:可迭代<%(T)%>; typeof(nodeToCheck。variable):> =:T; }

见面和加入类型

MeetJoin类型是特殊类型,类型系统引擎将对它们进行不同的处理。从技术上讲, MeetJoin类型分别是MeetTypeJoinType概念的实例。它们可以具有任意数量的参数类型,可以是任何节点。在语义上, Join类型是一种类型,它是其所有参数的超类型,而节点则具有Join(T1 | T2 |)类型。Tn)可以视为具有T1类型或T2类型或...或Tn类型。Meet类型是一种类型,它是其每个参数的子类型,因此可以说一个节点,其类型为Meet(T1&T2&..&Tn)驻留在类型T1和T2中,并且在类型Tn中。分别选择JoinMeet类型的参数分隔符(即“ |”和“&”)作为助记符。

在某些情况下,Meet和Join类型非常有用。Meet类型甚至出现在MPS BaseLanguage (与Java非常接近)中。例如,这样的表达式的类型:

是真的吗new Integer(1):“你好”

Meet(Serializable&Comparable) ,因为Integer( 新Integer(1)的类型)和String (“ hello”的类型)都实现了SerializableComparable

例如,当您希望某些函数式概念返回两种不同类型(例如节点或节点列表)的值时,联接类型很有用。然后,应将其调用类型设为Join(node <> | list <>>)<>

如果需要,您可以自己创建“ 见面”和“ 加入”类型。与其他类型和其他节点一样,使用引号创建它们。如上所述, MeetJoin类型的概念是MeetTypeJoinType

“当混凝土”砌块

有时,您可能不仅要写某些类型的方程式和不等式,而且要用类型结构执行一些复杂的分析。也就是说,检查具体类型的内部结构:其子代,子代,参照对象等。

似乎可能只是写了typeof(某些表达式),然后分析了这种类型。但是问题是,不能仅仅检查“ typeof”表达式的结果,因为那一刻它可能是类型变量。尽管类型变量通常会在某个时候变成具体的类型,但不能保证在类型系统代码的某些给定点它是具体的。

要解决此问题,您可以使用“ when concrete”块。

当具体(expr as var){body}

在这里,“ expr”是一个表达式,它将评估为仅要检查的类型 (而不是要检查的节点类型),而“ var”是将为其分配表达式的变量。然后,可以在“ when concrete”块的主体内部使用此变量。主体是语句的列表,仅当用“ expr”表示的类型变为具体的时才执行,因此,在具体块的内部,可以根据需要安全地检查其子代,属性等。

如果您已编写了when when块并查看了其检查器,则将看到两个选项:“浅”和“跳过错误”。如果将“是浅”设置为“真”,则当表达式变为浅具体时(即本身不是类型变量而是可能将类型变量作为子代或引用对象),将执行具体块的主体。通常,如果您的表达式在具体块何时不具体的情况下发生,则将报告错误。如果您的表达式表示的类型永远不会成为具体类型是正常的,则可以通过将“跳过错误”设置为true来禁用此类错误报告。

重载运算符

有时,运算符(例如+,-等)在应用于不同的值时会具有不同的语义。例如,Java中的+表示应用于数字时表示加法,如果其操作数之一为String类型,则表示字符串串联。当运算符的语义取决于其操作数的类型时,称为运算符重载 。实际上,我们有许多不同的运算符,它们由相同的语法构造表示。

让我们尝试为plus表达式编写一个推理规则。首先,我们应该检查操作数的类型,因为如果我们不知道操作数的类型(无论它们是数字还是字符串),我们就不能选择操作的类型(它可以是数字或String )。为确保操作数的类型是具体的,我们将在代码时用两个具体的块包围,一个用于左操作数的类型,另一个用于右操作数的类型。

当具体(typeof(plusExpression.leftExpression)作为leftType){当具体(typeof(plusExpression.rightExpression)作为rightType){...}
}

然后,我们可以进行一些检查,检查类型是字符串还是数字,并选择适当的操作类型。但是这里会出现问题:如果有人编写BaseLanguage的扩展名,而他们想在其中使用plus表达式添加一些其他实体(例如矩阵或日期),则他们将无法使用plus表达式,因为加表达式在现有的推理规则中进行了硬编码。因此,我们需要一个扩展点,以允许语言开发人员重载现有的二进制操作

类型系统语言具有这样的扩展点。它包括:

  • 重载操作规则和

  • 一种按操作及其操作数类型提供操作类型的构造。

例如, BaseLanguagePlusExpression的规则编写如下:

当具体(typeof(plusExpression.leftExpression)作为leftType){当具体(typeof(plusExpression.rightExpression)作为rightType){node <> opType =操作类型(plusExpression,leftType,rightType);如果(opType.isNotNull){typeof(plusExpression):==:opType; }其他{错误“ +不能应用于这些操作数”-> plusExpression; }}}

在此,“操作类型”是根据操作的左操作数的类型,右操作数的类型和操作本身来提供操作的类型的构造。为此,它使用重载操作规则。

重载操作规则

重载的操作规则位于概念OverloadedOpRulesContainer的根节点内。每个重载操作规则包括:

  • 适用的操作概念,即对规则适用的操作概念的引用(例如PlusExpression );

  • 向左和向右操作数类型限制,分别包含一个限制向左/向右操作数类型的类型。限制可以是精确的,也可以是不正确的,这意味着操作数的类型应该完全是限制中的类型 (如果限制是精确的),或者它的子类型 (如果不是精确的),则规则适用于该限制操作数类型;

  • 函数本身,该函数将返回一个知道操作概念以及左操作数和右操作数类型的操作类型。

这是BaseLanguagePlusExpression的重载操作规则之一的示例

操作概念:PlusExpression左操作数类型: .descriptor是正确的:false右操作数类型: .descriptor是正确的:false操作类型:(operation,leftOperandType,rightOperandType)-> node <> {if(leftOperandType.isInstanceOf(NullType)|| rightOperandType.isInstanceOf(NullType)){返回null; } else {return Queries.getBinaryOperationType(leftOperandType,rightOperandType); }}

更换规则

动机

考虑以下用例:您有使用您的语言的函数类型,例如(a 1,a 2,... a N)-> r,其中1,1,2,..,N和r是类型:a K是第K个函数参数的类型,r是函数结果的类型。然后,你想说的是,你的函数类型是由它们的返回类型逆变通过他们的参数类型。也就是说,函数类型F =(T 1,..,TN)-> R是函数类型G =(S 1,..,SN)-> Q(写为F <:G)的子类型。并且仅当R <:Q(根据返回类型协变量)并且对于从1到N的任何K,TK:> SK(即根据参数类型相反)。

问题是,如何在类型系统语言中表达协方差和反方差?使用子类型化规则,您可以通过编写以下内容来表达协方差:

nlist <>结果=新的nlist <>; for(节点<> returnTypeSupertype:立即超级类型(functionType。returnType)){节点ft = functionType。复制;英尺returnType = returnTypeSupertype;结果。加(ft);返回结果;

好的,我们已经收集了函数返回类型的所有直接超类型,并创建了一个函数类型列表,这些函数类型将这些收集的类型作为返回类型,并带有原始参数类型。但是,首先,如果我们有很多返回类型的超类型,那么每次需要解决不等式时执行此类操作的效率都不高;其次,尽管现在我们具有函数的返回类型的协方差,但是我们仍然没有函数自变量类型的矛盾。我们无法收集特定类型的直接子类型,因为子类型化规则会给我们超类型,而不是子类型。

实际上,我们只想表达上述性质:F =(T 1,..,TN)-> R <:G =(S 1,..,SN)-> Q(写为F <:G)当且仅当R <:Q且对于从1到N的任何K,TK:> SK。为此和类似的目的,类型系统语言有一个称为“替换规则”的概念。

什么是替代规则?

替换规则提供了解决不等式的便捷方法。标准方法是将子类型化规则可传递地应用到应为子类型,直到在结果中找到应为超类(或在结果中从未发现过),但替换规则(如果适用于不等式)将删除不等式,然后执行其主体(通常包含“创建等式”和“创建不等式”语句)。

例子

上述示例的替换规则如下:

替换规则FunctionType_subtypeOf_FunctionType适用于concept = FunctionType as functionSubType <:concept = FunctionType as functionSuperType rule {if(functionSubType。parameterType。count!= functionSuperType。 parameterType。计数){错误“不同的参数号”-> equationInfo。 getNodeWithError();回报; } functionSubType。 returnType:<=:functionSuperType。 returnType; foreach(节点<> paramType1:functionSubType。parameterType;节点<> paramType 2:functionSuperType。parameterType){paramType2:<=:paramType1; }}

在这里,我们说规则适用于概念FunctionType的应为子类型和概念FunctionType的应为超类型。规则的主体确保函数类型的参数类型的数量相等,否则将报告错误并返回。如果两个函数类型的参数类型的数目相等,则规则将创建一个带有返回类型的不等式,并为适当的参数类型创建一个不等式。

替换规则用法的另一个简单示例是一条规则,该规则指出Null类型(一种null文字)是除原始类型之外的每种类型的子类型。当然,我们不能为Null类型编写子类型规则,该规则将返回所有类型的列表。相反,我们编写以下替换规则:

替换规则any_type_supertypeof_nulltype适用于concept = NullType as nullType <:concept = BaseConcept as baseConcept规则{if(baseConcept.isInstanceOf(PrimitiveType)){错误}}

该规则适用于任何应为超类型和为空类型的应为子类型。该规则唯一要做的就是检查是否应该超类型是PrimitiveType概念的实例。如果是,则规则报告错误。如果不是,则该规则不执行任何操作,因此可以简单地从类型系统引擎中删除要解决的不等式,而不会产生进一步的影响。

不同的语义

如上所述,替换规则的语义是用一些其他方程式和不等式替换不等式或在应用时执行一些其他动作。这种语义实际上并没有说明某种类型在某些情况下是另一种类型的子类型。它只是定义了如何解决这两种类型的不等式。

例如,假设在生成期间需要检查某些静态未知类型是否为String的子类型。当要检查的类型为Null类型时,引擎将回答什么?当我们有一个不等式时,替换规则可以说是真的,但是在这种情况下,它上面提到的语义是没有用的:我们没有不等式,我们有一个问题要回答是或否。对于函数类型,情况更糟,因为一条规则说我们应该创建一些不等式。那么,在我们的用例中,我们与它们有什么关系呢?

为了使替换规则在我们要检查一个类型是否是另一个类型的子类型时可用,在这种情况下,给替换规则赋予了不同的语义。

这种语义如下:每个“加法方程”语句被视为检查两个节点是否匹配;每个“加法不等式”语句都被视为检查一个节点是否是另一个节点的子类型;每个报告错误声明均被视为“返回假”。

考虑以上针对函数类型的替换规则:

替换规则FunctionType_subtypeOf_FunctionType适用于concept = FunctionType as functionSubType <:concept = FunctionType as functionSuperType rule {if(functionSubType。parameterType。count!= functionSuperType。 parameterType。计数){错误“不同的参数号”-> equationInfo。 getNodeWithError();回报; } functionSubType。 returnType:<=:functionSuperType。 returnType; foreach(节点<> paramType1:functionSubType。parameterType;节点<> paramType 2:functionSuperType。parameterType){paramType2:<=:paramType1; }}

用不同的语义,将按以下方式处理:

布尔结果= true;如果(functionSubType。parameterType。count!= functionSuperType。 parameterType。 count){result = false;返回结果; }结果=结果&& isSubtype(functionSubType。returnType <:functionSuperType。returnType); foreach(node <> paramType1:functionSubType。parameterType; node <> paramType2:functionSuperType。parameterType){结果=结果&& isSubtype(paramType2 <:paramType1);返回结果;

因此,正如我们所看到的,另一种语义是在创建方程式/不等式与执行检查之间的直观映射。

类型系统,跟踪

MPS提供了一个方便的调试工具,可让您深入了解类型系统引擎如何评估特定问题的类型系统规则并计算类型。您可以从上下文菜单或键盘快捷键(Control + Shift + X / Cmd + Shift + X)调用它:

TST2

控制台有两个面板。左边的一个显示了所应用的顺序或规则,而右边的一个则为您提供了在评估左侧面板中选择的规则时类型系统引擎工作内存的快照:

TST1

类型错误在“类型系统跟踪”面板内用红色标记:

TST3

此外,如果在代码中发现错误,请使用Control + Alt +单击/ Cmd + Alt + Click快速导航到无法验证类型的规则:

TST4
 

TST5

类型系统语言的高级功能

覆盖默认类型节点

当由于应用方程式或解决不等式而将类型分配给程序节点时,默认情况下将采用表示类型的节点。也就是说,它可以是程序中的节点,也可以是带引号的节点。在这两种情况下,对指定要由方程式或不等式语句指定的类型的表达式求值的结果字面表示目标类型。此功能允许替换另一个节点来表示类型。

例如,一个人可能决定对不同的程序配置使用不同的类型,例如使用int要么long取决于任务是否需要使用一种类型或另一种类型。这与简单地使用生成器来产生正确的“实现”类型不同,因为替换是在执行类型检查时完成的,因此可以尽早发现可能的错误。

以最简单的形式,类型替换可以通过创建一个实例来使用Substitute Type Rule在类型系统模型中。

替代类型规则replaceType_MyType {适用于概念= MyType作为mt替代{如果(mt.isConditionSatisfied()){返回新节点; } 空值; }}

Substitute Type Rule适用于表示类型的节点。只要类型检查器引入了新类型,它就会搜索适用的替换规则并执行它们。该规则必须返回`node <>`的实例作为替换值,或者返回null值,在这种情况下,原始节点用于表示类型(默认行为)。

覆盖类型检查器使用的类型的另一种可能性是使用节点属性。如果原始类型节点包含一个节点属性,则类型检查器将尝试首先查找适用于该属性的替代类型规则。这样一来,即使对于语言,也可以覆盖类型节点,这种实现是密封的。

替代类型规则replaceType_SubstituteAnnotation {适用于概念= SubstituteAnnotation作为替代Annotation替代{如果(substituteAnnotation.condition.isSatisfied(attributedNode)){ } 空值; }}

上面的规则是为属性节点定义的,它是作为显式参数传递给规则的属性节点。该规则可以检查替换类型节点的条件是否满足,还可以通过以下方式访问代表原始类型的属性节点attributedNode表达。

值得一提的是,当一个类型节点刚从替代规则返回时,其本身就是另一替代对象。类型检查器尝试穷举应用所有匹配的替换规则,直到没有更多可用的替换为止。只有这样,类型才会出现在类型检查器的内部模型中。采取了一些预防措施来防止类型检查器进入无尽的替换循环,例如A-> B-> A,但是这些预防措施并不完美,应谨慎行事,以免引入无限循环。

仅检查不等式

基本上,不等式可能会影响节点的类型,例如,如果不等式部分之一是类型变量,则由于这种不等式,它可能成为具体的类型。但是,有时不希望某个不等式创建类型,而只是检查是否满足这种不等式。我们称这种不等式为仅检查不等式。要将不等式标记为仅检查,应转到该不等式的检查器,并将标志“仅检查”设置为“ true”。为了从视觉上区分这些不等式,仅检查式不等式的“小于或等于”符号为灰色,而对于普通检查式不等式为黑色,因此您可以查看不等式是否为仅检查式,而无需查看检查器。

依存关系

在为特定语言编写生成器时(请参见生成器),可能需要在生成器查询中询问特定节点的类型。当generator生成模型时,这样的查询将使类型系统引擎进行一些类型检查以找出所需的类型。对包含节点的根执行完整的类型检查以获得节点的类型是昂贵的,并且几乎总是不必要的。在大多数情况下,类型检查器应仅检查给定的节点。在更困难的情况下,获取给定节点的类型可能需要检查其父节点或其他祖先。如果计算出的类型不是完全具体的(即包含一个或多个类型变量),则类型检查引擎将检查给定节点。然后类型检查器将检查节点的父节点,依此类推。

有时甚至有一个更复杂的情况:孤立计算的某个节点的类型是完全具体的。在同一环境中,同一节点的类型也完全是具体的,但与第一个节点不同。在这种情况下,上述算法将中断,返回的节点类型就像被隔离一样,这不是给定节点的正确类型。

为了解决这种问题,您可以给类型检查器一些提示。这种提示称为依赖关系-它们表示一个节点的类型取决于其他某个节点的事实。因此,当在生成过程中计算某个节点的类型时,如果该节点具有某些依赖性,还将对其进行检查,因此将在适当的环境中对该节点进行类型检查。通过在节点上使用typeOf表达式来表达依赖关系,该节点的类型对于正确计算所请求的类型是必需的。

文字或表达式的覆盖类型

除了仅适用于类型的类型替换规则外,我们还在推理规则中引入了对属性的支持。

推理规则

文字或表达式通常具有关联的类型推断规则,当类型检查器要求所讨论的节点的类型时会触发这些规则。规则具有允许子概念扩展或覆盖预定义规则的机制。

治typeof_IntLiteral {适用于概念=的IntLiteral作为nodeToCheck适用始终覆盖真正做{typeof运算(nodeToCheck):==:; }}

节点属性的推理规则

如果节点具有一个或多个属性,则将适用于这些属性的推理规则先于适用于节点本身的规则应用。可以使用伪代码描述应用推理规则的过程。

lookup-inference-rules(node):让skipAttributed = false,如果attributesOf(node)中的a进行hasInferenceRuleFor(a)的执行,则let规则= isIncedceding(rule)的getInferenceRuleFor(a)屈服规则;如果isOverriding,则让skipAttributed = true结束(规则)然后中断foreach循环,如果结束则结束,如果结束,则执行skipAttributed,然后返回结束,如果/ *照常进行* / end

使用适用于节点属性的推理规则的示例显示了存在条件如何改变文字的类型。请注意,在此示例中,带注释文字的类型受此推理规则和适用于该节点的任何其他推理规则的影响。

规则typeof_Literal {适用于concept = PresenceConditionAnnotation,因为pca适用总是覆盖false取代属性为false do {typeof(pca.parent):<=:pca.alternativeNode}}

有条件的覆盖类型推断

请记住,用户可能希望通过属性覆盖类型推断的条件取决于配置,我们并不总是希望覆盖默认类型。

规则typeof_Literal {适用于concept = PresenceConditionAnnotation,因为pea适用总是取代属性{isConditionSatisfied(pca); }做{typeof(attributedNode):==:pca.replacementType}}

检查规则

检查(或非类型系统)规则可以检查模型以搜索代码中的已知错误模式,并将其报告给用户。这种预编译代码检查通常称为静态代码分析 。用于静态代码分析的典型工具中的错误模式可以分为几类,例如正确性问题,多线程正确性,I18N问题,增加漏洞的错误,样式问题,性能问题等。找到的问题会报告给用户通过交互式报告按需:

正在检查1

或直接在编辑器中通过彩色符号和代码下划线实时显示:

正在检查3

严重程度

MPS通过严重程度区分问题:

  • 错误-以红色显示

  • 警告-以黄色显示

  • 信息-以灰色显示

jetbrains.mps.lang.typesystem语言提供相应的语句,这些语句发出这些问题类别以及它们的描述和要突出显示的节点。附加的sure语句为用户提供了更简洁的语法,以在不满足条件的情况下报告错误:

正在检查6

检查规则通常在给定节点或模型的一小部分中检查一个或几个相关问题,如果发现问题,则向用户报告。检查规则附加在一个具体概念上:

正在检查7

对于作为该概念实例的每个节点都将调用该规则。

可以使用模式语言将规则的适用性限制为指定的模式。这很方便,主要有两个原因:

  • 将规则的适用条件缩小到仅概念的某些出现

  • 方便命名属性,节点的子孙

通过使用规则主体内部的模式语言,尤其是使用match语句,可以实现两个目标:

检查规则模式

模式中可能包含变量部分(节点,属性或引用),这些部分将与模型中的任何值,节点或引用匹配,并且用户可以使用变量名来引用它们。

检查规则的覆盖选项定义当前规则是否覆盖某些其他适用于节点的检查规则。

检查7a
默认情况下,为某个概念定义的检查规则由其所有子概念继承。因此,检查节点包括执行为该节点的概念定义的所有规则以及为其节点的超概念定义的规则。在极少数情况下,应避免这种检查规则的继承, 重写选项提供了这样做的方法。它使您可以显式指定要覆盖的规则列表。

可以选择多个替代规则:

检查7b

模式语言

模式语言为用户提供了合理的灵活性来指定所需的模式。使用Intents可以用模式特定的属性注释模式的各个部分,因此具有特殊的含义:

  • 模式变量 -可用于将模式的节点或参考转换为变量。它将与给定位置上的任何值匹配,并且用户将可以通过参考模式变量来获取该值

  • 模式属性变量 -与上面相同,但应用于节点的属性

  • 列表模式 -将与节点集合匹配,并允许用户对其进行迭代

  • 或样式 -将与几个提供的子样式之一匹配

  • 通配符 -将与给定位置上的任何(甚至不存在)节点匹配。用户无法参考匹配值

match语句可用于从BaseLanguage代码调用节点匹配:

模式3

快速修复

快速修复程序提供了单个模型转换功能,该功能将自动消除所报告的问题:

正在检查5

快速修复程序必须在Intents上下文菜单中提供描述以表示它,除非调用者仅在应用立即设置为true的情况下引用它。快速修复程序还可以声明字段 ,以保存重用的值,并且它可以接受来自调用方的参数

调用快速修复

快速修复可能会通过检查器工具窗口与每个报告的问题相关联, 以进行修复

正在检查4

通常,用户通过“ 意图”上下文菜单调用快速修复 ,该菜单在按下Alt + Enter键快捷方式后显示。但是,如果设置了立即应用标志,则MPS将在即时分析过程中发现问题后立即运行关联的快速修复 ,而无需等待用户触发。

正在检查8

通过检查器配置的其他两个可选属性的使用频率降低:

  • 要突出显示的节点特征 -指定节点的属性,要突出显示的参考子节点是问题的根源,而不是突出显示整个节点

  • 外部消息源 -用户在编辑器中单击报告的错误时( Control / Cmd + Alt + click ),她将进入引起该错误的Checking规则error / warning / info / ensure命令。使用外部消息源属性,您可以覆盖此行为,并提供您自己的节点,用户在单击错误后将被带到该节点。

 

上次修改时间:2019年8月30日