用Langium快速创建DSL
发布者::Federico Tomassetti inSoftware Development October 15th, 2021 0 Views
Langium是一个语言工程工具,旨在帮助创建DSL和低代码平台:你可以用Langium快速创建DSL。Langium是轻量级的,基于Visual Studio Code,允许你在一个步骤中创建一个语言和一个编辑器。
在这篇文章中,我们要看一下这个新工具,并用Langium创建一个语言的例子。本文将从已经有解析经验的人的角度对该工具进行快速回顾和指导。它并不是一个从头开始的解析的好介绍。
Langium和Xtext有什么相似之处
Langium是TypeFox的又一个有趣的创造,TypeFox是一家咨询和研究公司,创建了 Eclipse Theia、Gitpod 和 Xtext。这个新工具和上一个提到的工具类似:它们都是为帮助创建DSL而设计的工具。它们也都是建立在开放源码库和工具之上。Xtext建立在Eclipse 和 ANTLR之上,而Langium则建立在Visual Code 和 Chevrotain之上。
一个有趣的侧面说明是,Chevrotain本身并不是一个解析器生成器,它被描述为一个解析构建工具包。我们在关于JavaScript解析的文章中回顾了它。这意味着Langium开发人员自己创建了生成器的步骤。基本上,这都是TypeScript代码。因此,你有可能改变这个过程以满足你的需求。
Xtext和Langium也有一个类似的策略:一切都取决于你所创建的语言的语法声明。例如,你可以使用语法来定义一些验证规则,比如允许一个属性dogs_name ,只有在当前文件中已经定义过的dogs 的值。
Langium有什么不同
两者之间的差异是由于新的开发环境和新的目标。
Xtext是一个为设计传统编程语言和DSL而建立的工具。相反,Langium帮助你创建DSL或低代码语言。换句话说,Xtext可以帮助你创建不同复杂度的语言生态系统,而目前,Langium可以帮助你快速创建单独的、简单的语言。Xtext是EMF工具星系的一部分,所以它与其他使用该技术的工具兼容。这是一项强大而广泛的技术,为其他语言工程工具提供动力,但它的工作相当复杂。
Visual Studio Code已经成为开发环境的新标准。事实上,它也是Theia的基础。当然还有无数其他好的选择,但是对于大多数使用情况来说,它已经足够好了。
所以它是开发工具的安全选择。由于Visual Studio代码使用TypeScript,整个Langium项目也使用TypeScript。这是为了避免有一个多语言的代码库或需要整合不同的工具或运行时。这也是为什么Langium不使用解析器生成器,而只是创建代码。
Langium是新的Xtext吗?
这些差异的一个有趣的结果是,你可以更容易地整合用Xtext或其他技术创建的不同语言。这是因为 Xtext 遵循语言执行的经典结构(即解析创建 AST,它基于 Eclipse 建模框架等)。所以,如果你是一个语言工程师,你知道该把你的手放在哪里,以整合不同的语言或使用EMF的工具。
另一方面,对于Langium来说,这更难做到,因为它全部依赖于TypeScript。例如,AST是基于TypeScript接口的。所以,所有的东西都是TypeScript的代码,是专门针对Langium的。
这本身并不是一个缺陷,因为这使得用Langium创建DSL更容易和更快。这也是为了更好地实现Langium的目标。因此,Langium并没有取代Xtext,但它服务于不同的目的。
或者换一种说法,Xtext的竞争是一系列定制设计的工具,涵盖了创建语言工具的部分或整个过程,这些工具要么更有效,要么更好地与项目的其他部分结合。例如,你可以不在你的工作流程中使用Xtext,而只是建立一个在解析服务中使用的解析器,或者选择从头开始创建一个自定义转译器。
Langium可以替代什么
Langium的替代品是由非开发人员和开发人员共同使用的自定义环境/应用程序(例如,自定义桌面应用程序)。有些人可能会对这个想法嗤之以鼻,因为Langium是基于VS Code的,而VS Code是一个IDE,也就是说是为开发者提供的工具。
然而,VS Code使用起来并不复杂。事实上,它看起来并不比一个标准的文本编辑器更复杂。因此,非开发人员也可以使用它。另一方面,Xtext在未经训练的人眼里看起来非常复杂。公平地说,Xtext允许你只用你需要的用户界面元素来创建IDE的自定义分布,但这需要一些工作。
现在你理解了Langium背后的想法,让我们看看它的工作情况。
开始使用Langium
第一个令人惊喜的是开始使用Langium是多么容易。你只需要安装相应的Yeoman生成器。
npm install -g yo generator-langium
然后你就可以启动它。
yo langium
这将启动生成器,只需回答几个问题,就可以创建你的语言的骨架。这个过程还将创建一个langium-quickstart.md 文件,其中包含langium项目的结构信息。
不幸的是,这也是Langium可用的文档的范围。这个项目仍处于发展的早期阶段,所以这就是开始时的情况。在这一点上,Langium只有几个月的历史。
生成器创建了一个readme文件,解释了Langium项目的结构。生成的项目还附带一些例子文件,比如一个语言定义和验证的文件。在项目的github资源库中也有语言例子。看一下算术例子,你可以看到Langium不支持轻松处理表达式,像ANTLR一样。你将不得不创建一串越来越具体的表达式,而不是一个规则表达式。
Langium是建立在开放源码和现成的组件上的,这在一定程度上缓解了文档有限的问题。因此,你可以通过阅读其他文档来开始使用。例如,npm包的文档说。
Langium的语法声明语言与Xtext非常相似。请按照Xtext的文档来学习如何使用这种语言。
所以,如果你需要语法语言的参考,你可以通过搜索Xtext语法语言来寻找。不过要注意的是,虽然规则大体相同,但并不存在完美的对应关系。例如,Langium不支持Xtext的特殊的until标记。
Langium的工作流程
每个已经为Visual Studio Code创建了语言服务器的人也会认识到相同的基本组件。例如,language-configuration.json 文件是标准的VS Code文件,包含语法高亮的定义。它包含用于启用注释或括号等元素的语法高亮的定义。 要了解更多关于这些元素的信息,你可以跟随我们关于为Visual Studio Code创建语言服务器的教程,如在Visual Studio Code中整合代码自动完成 - 使用语言服务器协议。
基本上,要在这个阶段利用Langium的优势,你应该已经有一些语言工程的经验。否则,你必须准备好从其他来源挖掘一些信息,或者认命地进行试验和错误。另一个办法是继续阅读这篇文章。
你可能想做的另一件事是安装VS Code可用的Langium VS Code扩展。这个扩展为langium文件本身增加了语言支持,如语法高亮、自动完成等。
从技术上讲,使用Langium,你只是在开发一个VS Code扩展,所以工作流程对任何VS Code扩展的开发者都应该是熟悉的。
你首先运行这个命令来观察并自动编译你的TypeScript代码。
npm run watch
你专门使用这个命令来运行Langium。
npm run langium:generate
这将使Langium发挥其魔力,从你的语法定义和代码中生成代码。
然后你使用F5 来启动扩展。
Langium项目的结构
langium-quickstart.md 文件解释了Langium项目的结构。
你可以安全地忽略文件package.json, `extension.ts`和main.ts ,因为它们基本上包含了将Langium集成到VS Code中的代码。
你可能想看一下language-configuration.json 文件。这是语言配置,用于在VS Code中启用代码块或注释等元素的语法高亮。
文件<language-name>-module.ts 是用来设置Langium项目的。你可以在这里添加模块或语言服务,对语言文件进行一些操作。例如,一个模块来序列化或验证一个文件。
默认情况下,Langium生成器会创建一个验证器模块,根据你的语言规则检查文件是否有效。例如,你的语言可能要求变量名以大写字母开头。这个默认模块在文件*<language-name>-validator.ts*.验证是用标准的TypeScript代码完成的。
你要处理的主要文件是语法文件:<language-name>.langium 。这包含了你的语言的定义。在大多数情况下,它定义了分析器。然而,你也可以用它来定义AST中相应节点的类型。例如,生成的示例文件就包含这个规则。
terminal INT returns number: /[0-9]+/;
这确保了INT 元素的类型是一个数字,而不是默认的字符串。
创建我们的语言。Lexer
在我们的例子中,我们生成了一个Langium项目,名称为LangiumGame 。如果我们打开文件langium-game.langium ,我们可以看到默认例子语言的规则。这是一种用于创建对人的问候的语言。这不是很有用,所以我们要为我们的例子做一些不同的事情。我们将创建一个定义简单游戏的语言,这显然更有用,更有成效。好吧,它也同样无用,但至少它是不常见的。
我们的Langium文件以名称和不会出现在AST中的标记(终端)开始。这些标记是用命令hidden 。
grammar LangiumGame
hidden(WS, COMMENT)
我们选择只有一种类型的注释。我们忽略了这一点和空白处。
terminal COMMENT: /§[^\n\r]*[\n\r]*/;
我们的注释以字符§ (节号)开始,以换行结束。这个字符可能不在你的键盘上,所以现在你知道我看到~ (tilde)字符时的感受了。
在Langium中,词典规则的定义(即终端或标记)是由斜线字符划分的。除此以外,规则的定义是直观的,取决于典型的正则表达式格式。
终端COMMENT ,以及所有的终端,都放在语法文件的最后,在所有的分析器规则之后。因此,在同一个文件中,你在开头有语法名称和隐藏标记的列表,然后是分析器规则,最后是词典规则。
terminal NEWLINE: /[\n\r]+/;
terminal ID: /[_a-zA-Z][\w_]*/;
terminal WS: /\s+/;
terminal INT returns number: /[0-9]+/;
terminal TEXT: /"[^"]*"|'[^']*'/;
创建我们的语言。剖析器
解析器规则非常容易理解和定义。这是因为,就像终端一样,它们遵循EBNF格式的典型规则。然而,有几件事情值得一提。
Game:
'Game' name=ID description=TEXT NEWLINE (rules+=Riddle)+ (suggestions+=Suggestion)* ;
Riddle:
'Riddle' name=ID question=TEXT 'Answer' answer=TEXT NEWLINE ;
Suggestion:
'Suggestion' TEXT 'for' ID 'at' 'time' minutes=INT ':' seconds=INT NEWLINE ;
规则game ,捕捉到一个带有名称、描述和一系列规则和建议的游戏定义。第一个特殊之处在于,你需要给所有你想在后面的AST中轻松访问的东西一个标签。
例如,游戏的ID ,在AST中可以通过属性name 。终端NEWLINE 或字符串‘Game’ 将不能在AST中使用。你可以使用属性$cstNode?.text ,获得规则所匹配的整个文本,但似乎没有一个简单的方法来直接访问终端。
发生这种情况是因为这些标签在TypeScript接口中使用,该接口对应于该规则的一个节点。例如,这是规则的接口Game 。
export interface Game extends AstNode {
description: string
name: string
rules: Array<Riddle>
suggestions: Array<Suggestion>
}
第二件重要的事是,你定义的第一个规则是主规则。这个规则必须捕获文件的全部内容。在我们的例子中,Langium生成的解析器将尝试用规则Game 来解析该文件。
我们的小游戏本质上是一系列有一个正确答案的问题。该格式还支持在特定时间后向被卡住的玩家提供建议。
这种格式可以很好地用于小游戏,但也可以支持更复杂的逻辑游戏,因为它允许提供建议。由于它不支持定义环境或发现物体,所以它对逃生室之类的游戏效果并不好。
一个小问题
从理论上讲,这种简单的语法可以很好地工作。然而,事实并非如此。这个问题是当你的语法没有使用交叉引用时触发的一个错误。这个bug在Langium的开发版本中已经解决了,但这还没有公布。
事实上,如果你在此时生成语法,你会看到generated/ast.ts 文件中包含这一行。
export type LangiumGameAstReference = ;
这不是有效的TypeScript,这在编译时产生了一个错误。你可以通过手动修改这一行来修复这个错误。
export type LangiumGameAstReference = never;
或者通过添加一个带有交叉引用的假规则,比如下面这个。
Description:
'Description' desc=[Riddle];
现在,你可能会问,什么是交叉引用?这是Langium(和Xtext)语法的一个很好的特点。从图形上看,它是用两个封闭的方括号来表示的。在前面的例子中,它被用在desc=[Riddle] 。
这是一个约束条件,它只允许该属性具有指定类型的已定义值。为了更好地理解这意味着什么,让我们改变我们语法中的规则Suggestion 。
Suggestion:
'Suggestion' text=TEXT 'for' riddle=[Riddle] 'at' 'time' minutes=INT ':' seconds=INT NEWLINE ;
现在,建议中的属性riddle ,只能引用先前定义的谜语规则。
如果你现在试图引用一个不存在的Riddle ,你会得到一个错误。
为了使交叉引用发挥作用,被引用的规则应该有一个属性name 。如果你还记得,我们的规则Riddle ,就是这个样子的。
Riddle:
'Riddle' name=ID question=TEXT 'Answer' answer=TEXT NEWLINE ;
这是一个技术上属于验证阶段的特性,而不是解析阶段。然而,直接在语法中定义它使它易于使用和理解。这就是我们说Langium在很大程度上依赖于语法声明的意思。
交叉引用是一个很酷的例子,说明它与Visual Studio代码整合得很好。如果你定义了一个交叉引用,你会自动得到特定项目的自动完成。在我们的例子中,当写一个Suggestion ,你会得到建议的Riddle 名称。
验证我们的游戏
说到验证阶段,Langium带有一个默认的验证模块。在我们的例子中,验证包含在一个叫做langium-game-validator.ts 的文件中。
首先,你需要将验证方法与特定的类型联系起来。
/**
* Registry for validation checks.
*/
export class LangiumGameValidationRegistry extends ValidationRegistry {
constructor(services: LangiumGameServices) {
super(services);
const validator = services.validation.LangiumGameValidator;
const checks: LangiumGameChecks = {
Game: validator.checkDescriptionIsLongEnough
};
this.register(checks, validator);
}
}
在这个例子中,我们确保所有的游戏对象都由方法checkDescriptionIsLongEnough 来检查。
然后我们定义上述的方法。
/**
* Implementation of custom validations.
*/
export class LangiumGameValidator {
checkDescriptionIsLongEnough(game: Game, accept: ValidationAcceptor): void {
if (game.description.length < 50) {
accept('warning', 'The description of the game should be longer.', { node: game, property: 'description' });
}
}
}
我们希望所有的游戏描述至少要有50个字符的长度。由于这只是一个建议,而不是一个要求,所以在失败的情况下你只会得到一个警告。你还可以看到,我们可以明确指出检查指的是哪个属性。
总结
我们刚刚看到了Langium的一个简单介绍。它是我们生活的VS Code世界中的一个新的语言工程工具。它是建立在VS Code之上的,这使得它成为设计和提供简单DSL或格式的最佳选择。
尽管仍有一些粗糙的边缘和错误,但它已经是一个值得关注的工具。在达到0.1版本后,它的实用性令人惊讶。
如果你已经熟悉Xtext或VS Code的扩展开发,它就特别有用,但即使对那些只有一些解析经验的人来说,它也是一个不错的选择。
如果你在这些方面都没有经验,使用它可能还为时过早。从长远来看,它可能是初学者的一个很好的选择,因为它不需要包括一个解析器生成器。它只是依赖于TypeScript代码。这简化了工作流程和解析器的分发。
不过我们还没有到那一步,因为它缺乏独立的文档。它也没有像ANTLR那样支持用一个统一的规则轻松处理表达式的初学者友好功能。这对于格式或声明式DSL来说不是一个大问题,但对于不熟悉解析模式的人来说,这使得它在开始时变得有点复杂。
由我们JCG项目的合伙人Federico Tomassetti授权发表在Java Code Geeks上。点击这里查看原文。用Langium快速创建DSLs Java Code Geeks撰稿人所表达的观点属于他们自己。 |
2021-10-15