写在前面
完成特定范畴言语(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官方文档后做了一些记载(感觉,还有许多当地没有了解清楚,目前就这样吧!)。 最终,未完待续。。。