关于typescript:笔记写Flink-SQL-Helper时学到的一些姿势

4次阅读

共计 3963 个字符,预计需要花费 10 分钟才能阅读完成。

版本 日期 备注
1.0 2023.8.23 文章首发

前阵子向大家分享了我写的插件 https://marketplace.visualstudio.com/items?itemName=CamileSin…,最近梳理了我之前的学习相干常识时的笔记,心愿可能帮到对这一块实现感兴趣的同学。

1. TypeScirpt

开发 VS Code,能够抉择应用了 TypeScript or JavaScript。尽管我没学过 TypeScript,然而我马上抉择开始学习 TypeScript。我想起大学工作室的时候,身边有小伙伴就特地喜爱 JavaScript 这种写起来很快的语言,然而我却更喜爱 Java 这种语言。因为有些时候我基本不晓得 JavaScript 里的一些变量的值到底是什么。

TS 在官网是用一句话形容了它TypeScript is a strongly typed programming language that builds on JavaScript, giving you better tooling at any scale。一段时间用下来,发现 TS 真香,我自身接触的语言也不算少,所以上手很快。而且它的类型零碎十分弱小,让我十分有好感。

这个语言让我比拟印象粗浅的是,它不仅设置了相似 Java 中 Object 的 Unknown,还有所有类型子类的 Never 类型,用来代表其永远不会产生,比方:

function foo(x: string | number): boolean {if (typeof x === 'string') {return true;} else if (typeof x === 'number') {return false;}

  // 如果不是一个 never 类型,这会报错:// - 不是所有条件都有返回值(严格模式下)// - 或者查看到无法访问的代码
  // 然而因为 TypeScript 了解 `fail` 函数返回为 `never` 类型
  // 它能够让你调用它,因为你可能会在运行时用它来做平安或者具体的查看。return fail('Unexhaustive');
}

function fail(message: string): never {throw new Error(message);
}

另外就是对于范型的反对也很有意思,下面这个函数签名能够写出 foo(x: string | number) 这样的写法。对于范型反对的更好意味着能够让程序员更好的去做形象。

在学习 TypeScript 的时候还接触到了一本书,叫做《编程与类型零碎》,被一些网友戏称“一周入门 TypeScript”。整体内容还是比拟不错的,讲到了类型零碎来自于数学中的领域论,以及类型零碎的长处:类型的次要长处在于正确性、不可变性、封装、可组合性和可读性。这 5 种长处是优良的软件设计和行为的基本个性。零碎中总有呈现凌乱或者无序状态的偏向,而上述个性则起到抗衡这种偏向的作用。以此开展聊 TypeScript 的一些语法,以及比照 JavaScript,TS 做了哪些有用的改良。

2. 谬误检测能力:词法、语法分析

插件的谬误检测能力,其实是基于词法、语法分析实现的。咱们先来解释一下名词:

  • 词法剖析:一个个词去找,有些状况下须要多看一个乃至多几个个单词能力确定这个词是哪个类型的 token(这种行为在编译器外面叫 peek)。
  • 语法分析:依据已有 token 序列,剖析每一行代码是什么属于什么语句类型——也是一个个 token 进来剖析,有些状况下须要 peek 下一个乃至下下个单词能力确定。

这块其实是编译原理的一部分,属于前端编译局部,并未波及后端编译。见:https://github.com/camilesing/Flink-SQL-Helper-VSCode/blob/main/src/extension.ts 中的

// 应用生成的词法分析器和解析器进行语法查看
const inputStream = new ANTLRInputStream(event.getText());
// 词法解析
const lexer = new FlinkSQLLexer(inputStream);
const tokenStream = new CommonTokenStream(lexer);
// 语法解析
const parser = new FlinkSQLParser(tokenStream);
parser.removeErrorListeners();
parser.addErrorListener({syntaxError: (recognizer: Recognizer<any, any>, offendingSymbol: any, line: number, charPositionInLine: number, msg: string, e: RecognitionException | undefined): void => {vscode.window.showErrorMessage("Parser flink sql error. line:" + line + "position:" + charPositionInLine + "msg:" + msg);
  },
})
parser.compileParseTreePattern
// 解析文件内容并获取语法树
const parseTree = parser.program();

写这块代码我用到了 Antlr4-TS 这个库。我依据一些 Antlr4 的语法规定,生成了对应的代码,并将输出内容丢进这些类,让它们吐出后果。在理解 Antlr 相干的语法规定时,让我特地震撼——相似于刚毕业一年时接触到 DSL 时的震撼。通过一系列规定的形容,居然能够生产如此简单、繁多的代码,巨幅解放生产力。这些规定是一种很美又具备理论价值的形象

那让咱们抛开 Antlr 这个框架的能力,如果去手写一个词法、语法分析的实现,该怎么做呢?

在编程语言里,个别会有保留字和标识符的概念。保留字就是这个语言的关键字,比方 SQL 中的 select,Java 中的 int 等等,标识符就是你用于命名的文字。比方 public class Person 中的 Person, select f1 as f1_v2 from t1 中的 f1,f1_v2,t1。

再扩大一下概念,咱们以 int a=1; 这样一段代码为例子,int 是关键字,a 是标识符,= 是操作符,; 是符号(结束符)。搞清楚哪些词属于什么类型,这就是词法解析器要做的事。那怎么做呢?最简略的办法其实就是依照肯定规定(比方 A -Za-z$)一个个去读取,比方读到 i 的时候,它要去看前面是不是结束符或者空格,也就上文提到的的 peek,如果不为空,就要持续往后读,直到读到空格或者结束符。那么读取进去是个 int,就晓得这是个关键字。

伪代码如下:

循环读取字符
  case 空白字符
    解决,并持续循环
  case 行结束符
    解决,并持续循环
  case A-Za-z$_
    调用 scanIden()辨认标识符和关键字,并完结循环
  case 0 之后是 X 或 x,或者 1 -9
    调用 scanNumber()辨认数字,并完结循环   
  case , ; () []等字符
    返回代表这些符号的 Token,并完结循环
  case isSpectial(),也就是 % * + - | 等特殊字符
    调用 scanOperator()辨认操作符
  ...    

这下咱们晓得了 int a=1; 在词法解析器看来其实就是 关键字(类型)标识符 操作符 数字 结束符 。这样的写法其实是合乎 Java 的语法规定的。反过来说:int int=1; 是可能通过词法剖析的,然而无奈通过语法分析,因为 关键字(类型)关键字(类型)操作符 数字 结束符 是不合乎 Java 的语法定义的。

这个时候可能会有人问,为啥要有词法剖析这一层?都放到语法分析这一层也是能够做的啊 。能够做,但会很简单。 而且个别软件工程中会都做分层,防止里面的变动影响到外面的外围逻辑。 举个例子:后续 Java 新增了一个类型,如果词法剖析、语法分析是拆开的,那么只有改词法剖析层的一些代码就行了,语法分析不必。然而如果没有词法剖析这一层,语法分析的代码会有很多,而且一点点改变就很容易影响到这一层。

在此之后就会生成语法树。后续我打算做一些基于语法树的剖析,Antlr 提供了两种读语法节点的形式,一种是 Vistor,一种是 Listeners。前者意味着你能够被动的去遍历一些节点,而后者就像注册了钩子,Antlr 遍历到这里的时候会被动“喊”你。

// 创立拜访器实例并拜访语法树,以获取语法错误和正告
const visitor = new MyFlinkSQLVisitor();
visitor.visit(parseTree);
const errors = visitor.getErrors();

编译器其实分前端编译局部和后端编译局部的。语义剖析也是在前端,在语义分析阶段,其实是能够定义一些规定去做优化的。

编译器的后端,次要是负责语法树到指标代码(平台无关),到平台无关代码——比方,同一段源代码生成的 x86 体系下的可执行程序和 MIPS 体系下的可执行程序,其运行时构造会有较大的区别,这种区别会体现在指标代码上。如果一步到位由语法树转换为指标代码,就须要为每种 CPU 去写一套齐全独立的后端。为了防止这种状况以及便于优化,于是在语法树和蕴含机器特色的指标代码之间建设了一个两头构造,这样就能更加不便地将语法树转换为适宜不同 CPU 的指标代码,这是设计两头构造的最后目标。高端 gimple、低端 gimple、cfg、ssa、RTL(Register Transfer Language)就是这样的两头构造。这块没有什么理论的业务场景能够接触,所以就没有深刻去看了。

3. 小结

业余开发这款插件,确实花了我很多工夫。当初想来还是很值得的——在这外面学到了很多,而且还把本人想做的货色做进去了。后续迭代中,有新的学习笔记或感悟,我也会整顿上来,分享给大家。

正文完
 0