写在前面

完成特定范畴言语(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的显现状况便是:

Langium -- 新的语言工程工具(一)

就这样,一个很简略的DSL编辑器就有模有样的跑起来了

简略的看一看代码

在完成这一块,我就依照以下这四个方面简略的说一下我自己的了解。

  1. Langium 语法
  2. 合法性校验
  3. 自界说CLI
  4. 代码生成

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]*/;

以上代码,

  1. 语法声明,简略的描绘了DSL的名称(咱们这里便是HelloWorld)。

  2. entry便是解析器的入口,Model 能够了解为是笼统语法树的根节点。在这里,界说了一组重复的可选方案 (persons+=Person | greetings+=Greeting)。

    (个人了解:解析器在作业的时分,会遍历用户输入的文本,并将其间的符合person/greetings语法规矩的元素挂在AST上)。

  3. Person 的规矩是:以关键字 ‘Penson’ 最初,然后跟上一个ID来指代name

  4. Greeting的规矩:以关键字 ‘Hello’ 最初,然后穿插引证了Person,([] 里输入穿插引证的变量),最终要以 ‘!’ 做结束

  5. 界说的终端,会用其界说的正则表达式来解析用户输入的语法文本的一部分。(这个我也不是特别理解,那就硬翻吧!)

(假如了解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' });
            }
        }
    }
}
  1. 在 registerValidationChecks 函数中注册校验服务
  2. 校验器(也便是详细的查看逻辑)被写在 HelloWorldValidator 中
  3. 在 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 将在后边介绍。

增加解析和验证操作

  1. 首要,让咱们编写一个自界说操作,以允许咱们用咱们的言语解析验证 程序

    在此,咱们把咱们之前写的语法和基本的验证链接到 CLI 操作,以使其作业。

  2. 增加新指令:在 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官方文档后做了一些记载(感觉,还有许多当地没有了解清楚,目前就这样吧!)。 最终,未完待续。。。