我们提供安全,免费的手游软件下载!
我们是 袋鼠云数栈 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 '';
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() !== '') {
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() !== '') {
str += this.input[this.pos++];
}
if(this.peek() === ''){
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 "";
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
热门资讯