大家好! 作为程序员,我一直在寻找提高技能的方法。 一个星期五晚上,这个想法浮现在我的脑海:“我不写编译器吗?”
谁在乎找出它的来龙去脉,欢迎光临。
根据V. E. Karpov的“经典编译器理论”,编译有5个主要阶段:
- 词法分析
- 解析中
- 中间代码生成
- 最佳化
- 最终,对象,代码的生成
关于一切,分为五个部分,您可以写很多句子和文章。 但是,今天,我们将讨论前两个以及如何在一个单独的库中选择它们的结构。
当我决定只编写一小部分编译器时,我没有想到要使用哪种语言编写,因此,结果是一个用于对任何语言进行词法和句法分析的库。
代币化
在构建语法和词法树,生成结果代码并执行其他好操作之前,您需要将源代码分解为行,字符,数字。
每个元素将具有精确定义的类型。 在解析期间,未定义的类型标记将被视为语法错误。
在编译的上下文中,源代码被视为源映射,优良作法是将其存储在词法和解析过程中,以征询程序员的反馈并指示源代码中的语法错误。
您可以使用简单的正则表达式将源代码分成令牌数组:
/\S+/gm
它可以根据其他解析条件进行更改,例如:换行解析,制表符解析,空间解析。
分离的结果将是源代码的单词数组,并且单词在空间之间被解析,即 此设计:
let hello = 'world';
它将转换为以下标记集:
["let", "hello", "=", "'world';"]
为了获得最终的令牌集,您需要使用附加的正则表达式遍历每个令牌:
/(\W)|(\w+)/gm
结果将是我们需要的一组令牌:
["let", "hello", "=", "'", "world", "'", ";"]
我们收到的所有令牌都将连同其在源映射中的索引一起写入数组
词法分析
现在我们有了令牌的数组,我们需要确定它们的类型以将它们传递给解析。
定义令牌及其类型的算法称为-Lexer
Lexer定义的令牌及其类型称为令牌。
每个令牌可以具有唯一定义的类型,例如:
['let', 'const', 'var']
但是,我使用所谓的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) => {
tokenType-描述从中开始检查匹配项的令牌。
type-描述节点的类型,也应定义所有类型,类似于令牌类型。
sequence-序列的数组,它包含标记的类型,特定值或语法树的其他节点。
onError-回调,当发生语法错误
时将起作用,在解析此节点时,它将返回错误的类型及其在源代码中的位置。
让我们分析该节点的
顺序 :
[ {type: MyTokenTypes.KEYWORD},
我为不同目的实现了几种节点变体:确定标记序列,确定一组元素(参数,块)。 这样就可以解析箭头功能而没有任何问题。
您可以阅读我在该库的文档中实现的求解器和节点的所有变体。
用料
→来源链接:
Tyk→
经典编译器理论