- VisualStudio2022插件的安装及使用-编程手把手系列文章
- pprof-在现网场景怎么用
- C#实现的下拉多选框,下拉多选树,多级节点
- 【学习笔记】基础数据结构:猫树
我们是袋鼠云数栈 UED 团队,致力于打造优秀的一站式数据中台产品。我们始终保持工匠精神,探索前端道路,为社区积累并传播经验价值.
本文作者:奇铭 。
要弄清楚什么是词法分析,需要先搞清楚代码是如何执行的。高级编程语言的代码通常需要通过翻译才能被机器执行,而翻译的方式分为两种:
词法分析属于编译的一部分,也是编译的一个阶段。编译通常被分为两个部分:
在编译前端部分中:
编译器的作用将一种语言(通常是高级语言)的源代码转换成另一种语言,其中包含词法分析、 语法分析、语义分析和代码生成等所有编译功能。常见的编译器有 GCC 和 CLANG,前端领域最常见的编译器就是Babel.
解析器则是只负责对源程序进行词法分析和语法分析并将源程序转化为 AST 的部分,但其并不包含语义分析和代码生成功能。前端领域常见的解析器则是 Acorn,webpack 和 rollup 都适用它作为解析器.
解释器的任务是读取和执行源代码,而不需要(通常来说)先将源代码转换成机器代码。解释器通常会一边解析源代码一边执行它,或者先将源代码全部解析成AST或某种中间表示形式,然后再执行。Python 和 JavaScript 通常就是以解释执行的方式运行的.
另外,应用广泛的Antl4则是一个解析器生成器,它会根据语法文件生成解析器,生成产物中就包含用于词法分析的 Lexer 和 语法分析的 Parser。类似的解析器生成器还有 Bison、Jison、Yacc、Peg.js。其中 Antlr4、Jison 和 Peg.js 都可以在 javascript 环境中使用.
源代码中的每一行都是由字符构成的,这些字符可能组成了数字,变量名,关键字等各种元素。词法分析器并不关心元素之间的关系(比如一个变量是否被赋值了,或者是否在使用它之前就被声明了),它只关心将字符逐个归类,生成 token。 一个 token 可以看作是编程语言中的最小有意义的单位,比如一个整数,一个字符串,一个标识符,一个关键字等。一个 token 最少由两部分组成--类型和值。比如 'hello'的类型为 string 值为 hello。 一门典型的语言中 token 类型大概有下面5大类 。
IF
、ELSE
、FOR
等等100
,字符串字面量-'hello'
等+
、*
、\
、>
等;
、{
、(
等以一个简单的赋值语句举例 。
const str = 'hello' + 1;
首先我们需要一张 token 类型对照表,例如
token | tokenType |
---|---|
keyword | 1 |
identifier | 2 |
stringLiteral | 3 |
NumberLiteral | 4 |
operator | 5 |
delimiter | 6 |
那么上述表达式的词法分析结果就应该是
[
{ type: 1, value: "const" },
{ type: 2, value: "str" },
{ type: 5, value: "=" },
{ type: 3, value: "'hello'" },
{ type: 5, value: "+" },
{ type: 4, value: "1" },
{ type: 6, value: ";" },
]
关键字即为保留字,比如 ES6 中 break、case、class等都是保留字,它们不能作为标识符使用,详情请看 javascript 词法文法。在典型语言中,区分保留字和非保留字的重要依据就是是否能作为标识符,比如 undefind虽然印象中应该是一个关键字/保留字,但是其实不是,undefined 也是可以作为标识符的.
一般来说,在典型语言中,标识符用于标识某种实体,比如在 js 中表示符可以用来标识变量名、函数名,在 sql 中标识符可以用来标识表名、字段名。常见的大多数语言的标识符都由数字,英文字母和下划线组成。上文中提到的关键字虽然也符合这些规则,但是仍然不能作为标识符,因为这会导致难以进行语法分析,容易产生歧义。这也是为什么很多种语言都不支持标识符中包含中划线。 另外在 javascript 中像 TRUE / FALSE 此类字面量也不能作为标识符.
对于空格/注释等不影响最终结果的代码片段/字符,一般可以忽略或者暂存,在大部分情况下,它们不需要进入最终的 token 序列中.
词法分析的过程简单来说就是逐个扫描字符,并根据给定的模式/规则生成 Token 的过程,大概包含以下步骤:
根据上文中的词法分析过程,我们来实现一个简单的词法分析器,这个词法分析器能够生成数字,字符串,标识符和一些符号,在此之前我们首先需要定义 token 。
用术语表达,这一步就是种别码定义 。
enum TOKEN_TYPE {
number = 'NUMBER',
string = 'STRING',
identifier = 'IDENTIFIER',
punctuation = 'PUNCTUATION',
};
在假设输入的 token 都能被正确识别的情况下使用直接扫描的方式实现如下:
class Lexer {
constructor(input: string) {
/** 输入流 */
this.input = input;
/** 扫描位置 */
this.pos = 0;
}
input: string;
pos: number;
/** 取出当前字符 */
peek() {
if (this.pos >= this.input.length) return '<EOF>';
return this.input[this.pos];
}
/** 创建 token */
createToken(value: unknown, type: TOKEN_TYPE) {
return { value, type };
}
/** generate token */
nextToken() {
while (this.pos < this.input.length) {
if (/\s/.test(this.peek())) {
this.pos++;
continue;
}
if (/\d/.test(this.peek())) {
return this.createNumberToken();
}
if (this.peek() === '"') {
return this.createStringToken();
}
if (/[a-zA-Z]/.test(this.peek())) {
return this.createIdentifierToken();
}
if (/[{}(),:;+\-*/]/.test(this.peek())) {
return this.createToken(this.input[this.pos++], TOKEN_TYPE.punctuation);
}
}
}
createNumberToken() {
let numStr = '';
while (/\d/.test(this.peek())) {
numStr += this.input[this.pos++];
}
return this.createToken(numStr, TOKEN_TYPE.number);
}
createStringToken() {
let str = '"';
this.pos++
while (this.peek() !== '"' && this.peek() !== '<EOF>') {
str += this.input[this.pos++];
}
str+='"'
this.pos++
return this.createToken(str, TOKEN_TYPE.string);
}
createIdentifierToken() {
let idStr = '';
while (/[a-zA-Z]/.test(this.peek())) {
idStr += this.input[this.pos++];
}
return this.createToken(idStr, TOKEN_TYPE.identifier);
}
}
// test code
const lexer = new Lexer('let a "Hello, World";123');
const tokenList = [];
let token
while (token = lexer.nextToken()) {
tokenList.push(token)
}
console.log(tokenList);
// outout
/*
[
{ value: 'let', type: 'IDENTIFIER' },
{ value: 'a', type: 'IDENTIFIER' },
{ value: '"Hello, World"', type: 'STRING' },
{ value: ';', type: 'PUNCTUATION' },
{ value: '123', type: 'NUMBER' }
]
*/
显然上述代码在遇到错误时就无法运行了,所以我们还需要一些错误恢复的机制,当前的 lexer 中的错误大概分为两种 。
首先在 tokenType 中新增一个 undefined 类型 。
enum TOKEN_TYPE {
undefined = 'UNDEFINED',
};
然后在错误处返回 undefined token 。
class Lexer {
// ...
nextToken() {
while (this.pos < this.input.length) {
// ....
return this.errorRecovery();
}
}
// ...
createStringToken() {
let str = '"';
this.pos++
while (this.peek() !== '"' && this.peek() !== '<EOF>') {
str += this.input[this.pos++];
}
if(this.peek() === '<EOF>'){
console.warn('Unfinished strings', str)
return this.createToken(str, TOKEN_TYPE.undefined)
}
str+=this.peek();
this.pos++
return this.createToken(str, TOKEN_TYPE.string);
}
// ...
errorRecovery() {
console.warn('Unexpected character: ' + this.peek());
const unknownChar = this.peek();
this.pos++;
return this.createToken(unknownChar, TOKEN_TYPE.undefined)
}
}
// test code
const lexer = new Lexer('let a "Hello, World";123');
const tokenList = [];
let token
while (token = lexer.nextToken()) {
tokenList.push(token)
}
console.log(tokenList);
// output
/*
Unexpected character: =
Unfinished strings "Hello, World
[
{ value: 'let', type: 'IDENTIFIER' },
{ value: 'a', type: 'IDENTIFIER' },
{ value: '=', type: 'UNDEFINED' },
{ value: '"Hello, World', type: 'UNDEFINED' }
]
*/
在词法分析领域,更常见或者说应用更广的词法分析技术是 DFA (确定有限自动机)。DFA 具有如下优点 。
enum STATE_TYPE {
START = "start", // 初始状态
NUMBER = "number",
STRING_OPEN = "string_open",
STRING_ESCAPE = "string_escape",
STRING_CLOSE = "string_close",
IDENTIFIER = "identifier",
PUNCTUATION = 'punctuation',
UNKNOWN = "unknown",
}
const TRANSITIONS: Transition[] = [
// skip whitespace
{ state: STATE_TYPE.START, regex: /\s/, nextState: STATE_TYPE.START },
/** ==== PUNCTUATION */
{
state: STATE_TYPE.START,
regex: /[{}(),:;+\-*/]/,
nextState: STATE_TYPE.PUNCTUATION,
tokenType: TOKEN_TYPE.PUNCTUATION
},
{
state: STATE_TYPE.PUNCTUATION,
regex: /[\w\W]/,
nextState: STATE_TYPE.START,
tokenType: TOKEN_TYPE.PUNCTUATION
},
/** ==== identifier */
{
state: STATE_TYPE.START,
regex: /[a-z_A-Z]/,
tokenType: TOKEN_TYPE.IDENTIFIER,
nextState: STATE_TYPE.IDENTIFIER,
},
{
state: STATE_TYPE.IDENTIFIER,
regex: /[a-z_A-Z]/,
tokenType: TOKEN_TYPE.IDENTIFIER,
nextState: STATE_TYPE.IDENTIFIER,
},
{
state: STATE_TYPE.IDENTIFIER,
regex: /[^a-z_A-Z]/,
tokenType: TOKEN_TYPE.IDENTIFIER,
nextState: STATE_TYPE.START,
},
/** ===== number */
{
state: STATE_TYPE.START,
regex: /[0-9]/,
tokenType: TOKEN_TYPE.NUMBER,
nextState: STATE_TYPE.NUMBER,
},
{
state: STATE_TYPE.NUMBER,
regex: /[0-9]/,
tokenType: TOKEN_TYPE.NUMBER,
nextState: STATE_TYPE.NUMBER,
},
{
state: STATE_TYPE.NUMBER,
regex: /[^0-9]/,
tokenType: TOKEN_TYPE.NUMBER,
nextState: STATE_TYPE.START,
},
/** ===== string */
{
state: STATE_TYPE.START,
regex: /"/,
tokenType: TOKEN_TYPE.UNDEFINED,
nextState: STATE_TYPE.STRING_OPEN,
},
{
state: STATE_TYPE.STRING_OPEN,
regex: /[^"]/,
tokenType: TOKEN_TYPE.UNDEFINED,
nextState: STATE_TYPE.STRING_ESCAPE,
},
{
state: STATE_TYPE.STRING_ESCAPE,
regex: /[^"]/,
tokenType: TOKEN_TYPE.UNDEFINED,
nextState: STATE_TYPE.STRING_ESCAPE,
},
{
state: STATE_TYPE.STRING_ESCAPE,
regex: /"/,
tokenType: TOKEN_TYPE.STRING,
nextState: STATE_TYPE.STRING_CLOSE,
},
{
state: STATE_TYPE.STRING_CLOSE,
regex: /[\w\W]/,
tokenType: TOKEN_TYPE.STRING,
nextState: STATE_TYPE.START,
},
];
class StateMachine {
constructor() {
this.transitions = TRANSITIONS;
}
transitions: Transition[];
addTransition(transition: Transition) {
this.transitions.push(transition);
}
performTransition(currentState: STATE_TYPE, char: string) {
const transition = TRANSITIONS.find(
(t) => t.state === currentState && t.regex.test(char)
);
// 遇到未知字符串时,直接回到开始状态
return (
transition ?? {
state: STATE_TYPE.UNKNOWN,
regex: /./,
tokenType: TOKEN_TYPE.UNDEFINED,
nextState: STATE_TYPE.START,
}
);
}
}
class Lexer {
constructor(input: string) {
this.currentState = STATE_TYPE.START;
this.input = input;
this.pos = 0;
this.stateMachine = new StateMachine();
}
stateMachine: StateMachine;
currentState: STATE_TYPE;
input: string;
pos: number;
peek() {
if (this.pos >= this.input.length) return "<EOF>";
return this.input[this.pos];
}
createToken(value: unknown, type: TOKEN_TYPE) {
return { value, type };
}
nextToken() {
let buffer = ""; // 缓冲区
let tokenType: TOKEN_TYPE | undefined = undefined;
while (this.pos < this.input.length) {
const transition = this.stateMachine.performTransition(
this.currentState,
this.peek()
);
this.currentState = transition.nextState;
tokenType = transition.tokenType;
if(transition.nextState !== STATE_TYPE.START) {
buffer += this.peek();
this.pos++;
continue;
}
if(!transition.tokenType) {
buffer = '';
this.pos++;
continue;
}
if(transition.state === STATE_TYPE.UNKNOWN) {
buffer += this.peek();
this.pos++;
}
return this.createToken(buffer, transition.tokenType)
}
if(tokenType && buffer) {
return this.createToken(buffer, tokenType)
}
}
}
欢迎关注【袋鼠云数栈UED团队】~ 袋鼠云数栈 UED 团队持续为广大开发者分享技术成果,相继参与开源了欢迎 star 。
最后此篇关于词法分析基础的文章就讲到这里了,如果你想了解更多关于词法分析基础的内容请搜索CFSDN的文章或继续浏览相关文章,希望大家以后支持我的博客! 。
我试图了解传递给 setTimeout 的箭头函数如何记住上一个执行上下文中的 this 的值。我知道在执行箭头函数时会使用词法作用域规则查找 this 值。这是否意味着箭头函数关闭变量和 this
这个问题已经有答案了: How does the "this" keyword in Javascript act within an object literal? [duplicate] (4 个
我已阅读 this问题,我想我已经理解了投票最多的答案,但他说 since basically every programming language in wide use today uses le
如何让这段宏发挥预期的作用? -- 我想从词法环境中捕获 p 而不必将其作为参数发送给宏。 (define-syntax-rule (fi a b) (if p a b)) ;--->capt
Program A() { x, y, z: integer; procedure B() { y: integer; y=0;
我正在用 Java 实现自己的链表。节点类只有一个名为“name”的字符串字段和一个名为“link”的节点。现在我有一个测试驱动程序类,它只按顺序插入几个名字。现在,我正在尝试编写一种排序方法来按字母
考虑到这个question SO,其中调用了整个 C# 内存中编译器。只有lexical and syntactic analyzing时是必需的:将文本解析为词素流,检查它们并退出。 在System
我有 2 个场景。 这失败了: class F { public X X { get; set; } } 错误 CS0102:类型“F” ' 已经包含 ' X 的定义| ' 这个有效: class
我有一个用 NodeJS 执行的 .js 文件。这是我的文件的内容: var ctry = "America"; function outer(msg) { console.log(msg +
我对编写汇编程序的概念非常陌生,即使在阅读了大量 Material 之后,我仍然很难理解几个概念。 将源文件实际分解为 token 的过程是什么?我相信这个过程称为词法分析,我已经到处搜索有意义的真实
在 static scoping,标识符可以通过分析/解析源代码来确定(与动态作用域不同,动态作用域或多或少需要了解调用者环境)。 我的问题是这样的,因为静态作用域只需要解析源代码以了解作用域和标识符
编辑:我在第一个答案后更改了示例代码,因为我想出了一个简单的版本来回避相同的问题。 我目前正在学习 Common Lisp 的作用域属性。在我认为我有一个坚实的理解之后,我决定编写一些我可以预测结果的
考虑这段代码: class Bar(object): pass class Foo(object): def bar(self): return Bar() f = Foo() def Bar
将 ES6 箭头函数与词法 this 绑定(bind)结合使用非常棒。 但是,我刚才在使用典型的 jQuery 单击绑定(bind)时遇到了一个问题: class Game { foo() {
将 ES6 箭头函数与词法 this 绑定(bind)结合使用非常好。 但是,我刚才在将它与典型的 jQuery 点击绑定(bind)一起使用时遇到了一个问题: class Game { foo(
我是一名优秀的程序员,十分优秀!