程序员的星期五,或者正如我为词法和解析代码编写库时

大家好! 作为程序员,我一直在寻找提高技能的方法。 一个星期五晚上,这个想法浮现在我的脑海:“我不写编译器吗?”

谁在乎找出它的来龙去脉,欢迎光临。

根据V. E. Karpov的“经典编译器理论”,编译有5个主要阶段:

  1. 词法分析
  2. 解析中
  3. 中间代码生成
  4. 最佳化
  5. 最终,对象,代码的生成

关于一切,分为五个部分,您可以写很多句子和文章。 但是,今天,我们将讨论前两个以及如何在一个单独的库中选择它们的结构。

当我决定只编写一小部分编译器时,我没有想到要使用哪种语言编写,因此,结果是一个用于对任何语言进行词法和句法分析的库。

代币化


在构建语法和词法树,生成结果代码并执行其他好操作之前,您需要将源代码分解为行,字符,数字。

每个元素将具有精确定义的类型。 在解析期间,未定义的类型标记将被视为语法错误。

在编译的上下文中,源代码被视为源映射,优良作法是将其存储在词法和解析过程中,以征询程序员的反馈并指示源代码中的语法错误。

您可以使用简单的正则表达式将源代码分成令牌数组:

/\S+/gm 

它可以根据其他解析条件进行更改,例如:换行解析,制表符解析,空间解析。

分离的结果将是源代码的单词数组,并且单词在空间之间被解析,即 此设计:

 let hello = 'world'; 

它将转换为以下标记集:

 ["let", "hello", "=", "'world';"] 

为了获得最终的令牌集,您需要使用附加的正则表达式遍历每个令牌:

 /(\W)|(\w+)/gm 

结果将是我们需要的一组令牌:

 ["let", "hello", "=", "'", "world", "'", ";"] 

我们收到的所有令牌都将连同其在源映射中的索引一起写入数组

词法分析


现在我们有了令牌的数组,我们需要确定它们的类型以将它们传递给解析。

定义令牌及其类型的算法称为-Lexer
Lexer定义的令牌及其类型称为令牌。

每个令牌可以具有唯一定义的类型,例如:

 ['let', 'const', 'var'] // Keywords ['=', '+', '-', '*', '/'] // Operators  .. 

但是,我使用所谓的Solver'ov实现了一种用于确定令牌类型的方案。
其工作方式如下:

1.为令牌类型定义常量:

 const DefaultTokenTypes = { KEYWORD: "Keyword", IDENTIFIER: "Identifier", OPERATOR: "Operator", DELIMITER: "Delimiter", LINEBREAK: "Linebreak", STRING: "String", NUMERIC: "Numeric", UNKNOWN: 'Unknown' }; 

2.接下来,有必要确定所谓的Solver'y:

 const solvers = {}; solvers[MyTokenTypes.KEYWORD] = { include: [ 'const', 'let' ] }; solvers[MyTokenTypes.NUMERIC] = { regexp: /^[0-9.]*$/gm }; solvers[DefaultTokenTypes.STRING] = { type: StringSolver, delimiters: ['"', "'", '`'] }; solvers[MyTokenTypes.IDENTIFIER] = { regexp: /^[a-zA-Z_][a-zA-Z_0-9]*$/gm }; solvers[MyTokenTypes.DELIMITER] = { default: true }; 

如您所见,令牌可以具有不同的设置:

包括 -一个单词数组,与此相吻合的是,可以确定令牌的类型。
regexp-一个正则表达式,与此相吻合的是,可以确定令牌的类型。
默认 -未定义令牌的标准类型。

您还可以注意到type参数,它指示此求解器应从type中指定的那个继承。

在这种情况下,求解器定义字符串,并用定界符中指定的字符之一括起来

3.我们对令牌数组使用求解器,并获得类型化令牌的数组。 对于给定的源代码:

 let a = 50; 

我们得到以下树:

 [ { "type": "Keyword", "value": "let", "range": [0, 3] }, { "type": "Identifier", "value": "a", "range": [4, 5] }, { "type": "Delimiter", "value": "=", "range": [6, 7] }, { "type": "Numeric", "value": "50", "range": [8, 10] }, { "type": "Delimiter", "value": ";", "range": [10, 11] } ] 

其中range是源代码中片段的开头和结尾。

解析中


收到令牌数组后,您应该解析它们并确定源代码的语法结构(树)。

解析有多种选择,但我选择了自上而下的直接算法。

令牌是使用一系列模板一一解析的。 如果模板与当前标记序列匹配-在语法树中,则创建一个新分支。

数组中一个模板的示例:

 let declaration = new SequenceNode({ tokenType: MyTokenTypes.KEYWORD, type: MyNodeTypes.DECLARATION, sequence: [ {type: MyTokenTypes.KEYWORD}, {type: MyTokenTypes.IDENTIFIER}, {type: MyTokenTypes.DELIMITER}, {type: [MyTokenTypes.NUMERIC, MyTokenTypes.STRING]}, ';' ], onError: (e) => { //e - Syntax error } }); 

tokenType-描述从中开始检查匹配项的令牌。
type-描述节点的类型,也应定义所有类型,类似于令牌类型。
sequence-序列的数组,它包含标记的类型,特定值或语法树的其他节点。
onError-回调,当发生语法错误将起作用,在解析此节点时,它将返回错误的类型及其在源代码中的位置。

让我们分析该节点的顺序

 [ {type: MyTokenTypes.KEYWORD}, //   ->     {type: MyTokenTypes.IDENTIFIER},//   + 1 ->    {type: MyTokenTypes.DELIMITER},//   + 2 ->    {type: [MyTokenTypes.NUMERIC, MyTokenTypes.STRING]},//   + 2 ->      ';' //   + 3 ->      ], 

我为不同目的实现了几种节点变体:确定标记序列,确定一组元素(参数,块)。 这样就可以解析箭头功能而没有任何问题。

您可以阅读我在该库的文档中实现的求解器和节点的所有变体。

用料


→来源链接: Tyk
经典编译器理论

Source: https://habr.com/ru/post/zh-CN426151/


All Articles