写在前面
实现特定领域语言(Domain-Specific Language)之前最方便的技术路线大概就是:Xtext。因为,Xtext可以实现语言服务协议(language service protocol):简单且粗糙的来说,就是在编辑器上实现关键字高亮、语法智能提醒和语法错误提醒等一般编辑器该有的那样特性。同时,Xtext也能实现将DSL生成为类似java、c、python等可编译的语言。
简而言之,Xtext可以使我们的DSL能在编辑器中,像普通程序语言一样被友好的书写;同时,也可以像python、c等程序一样被执行。
不过,随着VS Code的盛行,Xtext多少有点不那么好用了。首先,Xtext最初的目的是给Eclipse平台做语言服务插件的,虽然现在也可以开发VS Code的扩展,但总觉不是那么好用。其次,Xtext的技术栈是Java,这个对js/ts开发者来说,就真的有点头秃了呀。最后,最后就是真的不想学Java啊😭
现在,Xtext团队就推出了新一代的语言工程工具 —— Langium!
- Xtext能实现的功能,Langium都可以实现!
- 已有的Xtext工程 Langium 也提供了工具,能将它转换成Langium 工程(这部分,我只看到了相关文档,并没有去try 一try,好不好用还得另说);
- Langium可以用 TS 来开发 DSL
简单的Try一Try吧
以下这些操作都是跟着 Langium官方文档 敲出来的,如果大家对Langium感兴趣的话,可以去仔细看一看。
前置条件
-
Node >= 12
-
安装 Yeoman 和 Langium 扩展生成器
// 安装 Yeoman 和 Langium 扩展生成器 npm i -g yo generator-langium // 创建第一个DSL yo langium // 扩展名称:hello-world // Language name:Hello World // 文件扩展名: .hello
用vs code打开 hello-world文件夹,按F5启动扩展。在新打开的扩展实例窗口中打开一个文件夹并新建一个扩展名为.hello的新文件,并写入以下内容:
person Alice Hello Alice! person Bob Hello Bob!
此时在vscode的显示情况就是:
就这样,一个很简单的DSL编辑器就有模有样的跑起来了😄
简单的看一看代码
在实现这一块,我就按照以下这四个方面简单的说一下我自己的理解。
- Langium 语法
- 合法性校验
- 自定义CLI
- 代码生成
Langium 语法
我们要实现的语言(比如,以上实现的Hello World语言是DSL)的语法规则,需要用langium的语法规则来描述。(langium本身也是一种DSL)。
通过 yo langium 指令生成的工程中,我们可以在 src/language-server/hello-world.langium 中看到,langium语言是如何描述Hello World语言的语法规则的。
grammar HelloWorld
entry Model:
(persons+=Person | greetings+=Greeting)*;
Person:
'person' name=ID;
Greeting:
'Hello' person=[Person:ID] '!';
hidden terminal WS: /\s+/;
terminal ID: /[_a-zA-Z][\w_]*/;
terminal INT returns number: /[0-9]+/;
terminal STRING: /"[^"]*"|'[^']*'/;
hidden terminal ML_COMMENT: //*[\s\S]*?*//;
hidden terminal SL_COMMENT: ///[^\n\r]*/;
以上代码,
-
语法声明,简单的描述了DSL的名称(我们这里就是HelloWorld)。
-
entry就是解析器的入口,Model 可以理解为是抽象语法树的根节点。在这里,定义了一组重复的可选方案 (persons+=Person | greetings+=Greeting)。
(个人理解:解析器在工作的时候,会遍历用户输入的文本,并将其中的符合person/greetings语法规则的元素挂在AST上)。
-
Person 的规则是:以关键字 'Penson' 开头,然后跟上一个ID来指代name
-
Greeting的规则:以关键字 'Hello' 开头,然后交叉引用了Person,([] 里输入交叉引用的变量),最后要以 '!' 做结尾
-
定义的终端,会用其定义的正则表达式来解析用户输入的语法文本的一部分。(这个我也不是特别明白,那就硬翻吧!)
(如果了解Xtext的话,应该能看出来Langium的语法规则跟Xtext的语法规则还是很像的!
合法性校验
合法性校验主要是实现:当用户输入相同函数名、关键字输入错误...等一系列不符合我们的语法规则时,编辑器能给出相应的错误提醒。
src/language-server/hello-world-validator.ts
import { ValidationAcceptor, ValidationChecks } from 'langium';
import { HelloWorldAstType, Person } from './generated/ast';
import type { HelloWorldServices } from './hello-world-module';
/**
* Register custom validation checks.
*/
export function registerValidationChecks(services: HelloWorldServices) {
const registry = services.validation.ValidationRegistry;
const validator = services.validation.HelloWorldValidator;
const checks: ValidationChecks<HelloWorldAstType> = {
Person: validator.checkPersonStartsWithCapital
};
registry.register(checks, validator);
}
/**
* Implementation of custom validations.
*/
export class HelloWorldValidator {
checkPersonStartsWithCapital(person: Person, accept: ValidationAcceptor): void {
if (person.name) {
const firstChar = person.name.substring(0, 1);
if (firstChar.toUpperCase() !== firstChar) {
accept('warning', 'Person name should start with a capital.', { node: person, property: 'name' });
}
}
}
}
- 在 registerValidationChecks 函数中注册校验服务
- 校验器(也就是具体的检查逻辑)被写在 HelloWorldValidator 中
- 在 checks 中注册具体的校验逻辑
如,新增Greeting的校验,就看起来像下面一样。
const checks: ValidationChecks<MiniLogoAstType> = {
Person: validator.checkPersonStartsWithCapital
Greeting: validator.checkGreetingStartsWithCapital
};
export class HelloWorldValidator {
checkPersonStartsWithCapital(): void {
...
}
checkGreetingStartsWithCapital(): void {
...
}
}
自定义CLI
- 概述
- 关于命令行界面
- 添加解析和验证操作
- 构建和运行 CLI
概述
当描述清楚语法规则,并且添加了一些验证了之后,我们接下来就可以期待代码能按照我们的预期跑起来。
在 src/cli/index.ts 文件中可以找到自定义的 CLI。在默认的情况下,CLI提供一个命令,即generate命令。
generate命令,它允许你获得用 DSL 编写的程序(读取用户输入的文档)、解析它并遍历AST 以生成某种输出。有关 generate 将在后面介绍。
添加解析和验证操作
-
首先,让我们编写一个自定义操作,以允许我们用我们的语言解析和验证 程序
在此,我们把我们之前写的语法和基本的验证链接到 CLI 操作,以使其工作。
-
添加新命令:在 index.ts 文件中的默认导出中注册它。
在这个函数中,有一个命令对象,它是我们的 CLI 命令的集合。让我们调用我们的command generate ,并给它一些额外的细节,比如
- arguments: 表示它需要一个文件
- description:详细说明此操作的作用的描述
- action:执行实际解析和验证的操作
我们可以像这样注册我们的 解析和验证 操作
program
.command('generate')
.argument('<file>', `source file (possible file extensions: ${fileExtensions})`)
.option('-d, --destination <dir>', 'destination directory of generating')
.description('generates JavaScript code that prints "Hello, {name}!" for each greeting in a source file')
.action(generateAction);
最后,我们需要实现 generate 函数本身。这将使我们能够解析和验证我们的程序,但不会产生任何输出。我们只想知道,我们的程序何时在我们的语言实现的约束下是“正确的”
import { extraceDocument } from './cli-util';
export const generateAction = async (fileName: string, opts: GenerateOptions): Promise<void> => {
const services = createHelloWorldServices(NodeFileSystem).HelloWorld;
const model = await extractAstNode<Model>(fileName, services);
const generatedFilePath = generateJavaScript(model, fileName, opts.destination);
console.log(chalk.green(`JavaScript code generated successfully: ${generatedFilePath}`));
};
生成代码
生成代码,就是将 AST 从基于 Langium 的语言转换成为某个输出目标。这里可能是另一种具有类似功能(转译)的语言、一种较低级别的语言(编译),或者生成一些将被另一个应用使用的文件/数据。
这一部分是DSL语言最核心的一部分,因为这决定了DSL语言是否能被编译执行。
这一部分,以后有空在记录吧!
最后
这些主要是看Langium官方文档后做了一些记录(感觉,还有很多地方没有理解清楚,目前就这样吧!😭)。 最后,未完待续。。。