编译
前言
前端开发人员为什么要了解编译原理?
大家或许总能听到编译,但真正要说出个因为所以,多数人可能会差些意思,可能是工作环境的使然,也可能是个人目标的不同吧!
那么,前端开发人员了解编译原理、学习编译原理、应用编译原理有什么意义?或者有什么好处呢?我总的认为是可以使前端开发人员更好地理解代码执行的底层原理,从而提高代码质量和性能,并为他们开发复杂的应用程序提供更多的灵活性和控制力。编译可以让你更好的还原代码真相,因为编译的过程就是将高级语言不断解析从而达到本质即机器语言。了解本质更利于全平台的开发,即使接触未开发过的平台,也更容易找到切入点,毕竟新技术新平台是无止境的。了解代码的解析过程,将会更直观的发现缺陷是发生在代码执行链路的哪一层,从而在根原处解决掉它。在这优化与兼容的过程中,或许你会发现某一个库、框架 甚至 语言的缺陷,从而迫使自己进行语言设计和拓展,再而得到更贴合业务场景的开发工具。
记录入门编译的一个过程,留给或许未来将“短暂性失忆”的自己。
原理
编译的原理是将高级语言代码转换成机器语言代码的过程。
语言处理器
编译器
编译器可以将一种语言(源语言)编写的程序转换为另一种语言(目标语言)编写的程序。若该程序本就具备可执行性,则语言转换完成后,可再进行输入数据分析得出输出结果。
解释器
解释器可以将源代码(源语言编写的程序)逐行解释执行。解释器执行的结果即程序运行后的结果,不会产生中间代码。
编译步骤
词法分析(Lexical Analysis)
词法分析是编译器的第一个阶段,也称为扫描(Scanning),它负责将源代码分割成标记或令牌(Token)序列,每个标记表示源代码中的一个基本单元,例如关键字、运算符、变量名等等。词法分析器通常使用正则表达式或有限状态自动机来实现。
语法分析(Syntax Analysis)
语法分析是编译器的第二个阶段,也称为解析(Parsing),它负责将标记序列转换为抽象语法树(Abstract Syntax Tree,AST),AST 是一种用于表示程序语法结构的数据结构,其中每个节点代表一个语法结构,例如表达式、语句、函数定义等等。语法分析器通常使用上下文无关文法和递归下降分析等技术来实现。
语义分析(Semantic Analysis)
语义分析是编译器的第三个阶段,它负责检查源代码是否符合语法规则,并且执行类型检查、常量折叠、作用域分析、错误检查等操作。语义分析器通常需要访问符号表来获取变量和函数的信息。
中间代码生成(Intermediate Code Generation)
中间代码生成是编译器的第四个阶段,它负责将 AST 转换为中间代码,中间代码是一种与具体机器无关的代码形式,通常采用三地址码、虚拟机指令等形式。
代码优化(Code Optimization)
代码优化是编译器的第五个阶段,它负责对中间代码进行优化,以提高程序的性能和效率,例如常量传播、死代码消除、循环展开、指令调度等等。
目标代码生成(Code Generation)
目标代码生成是编译器的第六个阶段,它负责将中间代码转换为目标机器的汇编代码或机器码。目标代码生成器通常需要了解目标机器的指令集、寄存器分配、堆栈分配等相关信息。
链接(Linking)
链接是生成编译器的最后一个阶段,将目标代码与库文件等其他程序代码组合起来,生成可执行文件。
符号表管理(Symbol Table Management)
在整个编译过程中,需要记录变量名、函数名等符号信息的使用情况,以便于后续的代码生成和错误处理。
错误处理(Error Handling)
在整个编译过程中,可能会出现各种各样的错误,需要在编译器中建立相应的错误处理机制,及时发现并报告错误。
入门 Demo
四则运算解释器
逆波兰表达式解析
// 定义操作符优先级
const OperatorPriority = {
'+': 1,
'-': 1,
'*': 2,
'/': 2
};
/**
* 将表达式分解为操作数和运算符
* @param {string} expression - 待分解的表达式
* @returns {Array} 操作数和运算符组成的数组
*/
function parseExpression(expression) {
const tokens = [];
let currentNumber = '';
for (let i = 0; i < expression.length; i++) {
const char = expression[i];
// 如果当前字符是数字,则将其添加到当前操作数中
if (!isNaN(char)) {
currentNumber += char;
} else {
// 否则,将当前操作数添加到tokens数组中,并重置currentNumber
if (currentNumber !== '') {
tokens.push(currentNumber);
currentNumber = '';
}
// 将当前运算符添加到tokens数组中
tokens.push(char);
}
}
// 将最后一个操作数添加到tokens数组中
if (currentNumber !== '') {
tokens.push(currentNumber);
}
return tokens;
}
/**
* 将中缀表达式转换为逆波兰表达式
* @param {Array} tokens - 操作数和运算符组成的数组
* @returns {Array} 逆波兰表达式
*/
function toReversePolishNotation(tokens) {
const outputQueue = [];
const operatorStack = [];
for (let i = 0; i < tokens.length; i++) {
const token = tokens[i];
// 如果当前token是数字,则将其添加到输出队列中
if (!isNaN(token)) {
outputQueue.push(token);
} else {
// 否则,如果当前token是运算符,则将其加入运算符栈中
const op1 = token;
while (operatorStack.length > 0) {
const op2 = operatorStack[operatorStack.length - 1];
if (OperatorPriority[op1] <= OperatorPriority[op2]) {
outputQueue.push(operatorStack.pop());
} else {
break;
}
}
operatorStack.push(op1);
}
console.log('outputQueue:',outputQueue)
console.log('operatorStack:',operatorStack)
}
// 将剩余的运算符从运算符栈中弹出并加入输出队列中
while (operatorStack.length > 0) {
outputQueue.push(operatorStack.pop());
}
return outputQueue;
}
/**
* 计算逆波兰表达式的值
* @param {Array} rpn - 逆波兰表达式
* @returns {number} 表达式的值
*/
function evaluate(rpn) {
const stack = [];
for (let i = 0; i < rpn.length; i++) {
const token = rpn[i];
// 如果当前token是数字,则将其压入栈中
if (!isNaN(token)) {
stack.push(parseFloat(token));
} else {
console.log('stack:',stack)
// 否则,弹出栈顶的两个数字进行运算,并将结果压入栈中
const b = stack.pop();
const a = stack.pop();
switch (token) {
case '+':
stack.push(a + b);
break;
case '-':
stack.push(a - b);
break;
case '*':
stack.push(a * b);
break;
case '/':
stack.push(a / b);
break;
}
}
}
// 最终栈中只剩下一个元素,即为计算结果
return stack.pop();
}
// 测试
const expression = '1+5+2*3*2-6-4/2';
const tokens = parseExpression(expression);
const rpn = toReversePolishNotation(tokens);
const result = evaluate(rpn);
console.log(`'${expression}' 的计算结果为:${result}`);
LL(1) 文法
-
文法设计
-
定义终结符和非终结符
- 终结符:
+、-、*、/、(、)、十进制数(digit,即 0~9); - 非终结符:表达式(expresion)、项(term)、因子(factor)。
- 终结符:
-
梳理关系
- E 包含多个 T,T 包含多个 F,F 是数字或带有括号的表达式。
-
根据梳理关系得出产生式
-
E -> E+T | E-T | T T -> T*F | T/F | F F -> D | (E)
-
-
将上一步产生式转换为 LL(1) 文法,即去除左递归和二义性,上述产生式不存在二义性,因此去除左递归即可
- 去除左递归,即将左递归转换为右递归
E -> TE' E' -> +TE' | -TE' | ε T -> FT' T' -> *FT' | /FT' | ε F -> D | (E)
-
-
构造预测分析表
-
将产生式拆分出FIRST集合和FLLOW集合
FIRST 集合: FIRST(E) = { D, ( } FIRST(E') = { +, -, ε } FIRST(T) = { D, ( } FIRST(T') = { *, /, ε } FIRST(F) = { D, ( } FOLLOW 集合: FOLLOW(E) = {$, ), +, -} FOLLOW(E') = {$, ), +, -} FOLLOW(T) = {+, -, *, /, ), $} FOLLOW(T') = {+, -, *, /, ), $} FOLLOW(F) = {+, -, *, /, ), $} -
根据集合填充预测分析表
+ - * / ( ) D $ E E -> TE' E -> TE' E' E' -> +TE' E' -> -TE' E' -> ε E' -> ε T T -> FT' T -> FT' T' T' -> *FT' T' -> /FT' T' -> ε T' -> ε F F -> (E) F -> D
-
-
实现解释器
// 四则运算解释器 class Parser { /** * 初始化数据 * @param {string} input 四则运算表达式 */ constructor(input) { this.input = input.replace(/\s+/g, ""); this.pos = 0; this.currentToken = null; } /** * 获取下一个 token * @returns 下一个 token */ getNextToken() { if (this.pos >= this.input.length) { return null; } const currentChar = this.input[this.pos]; if (/^\d+$/.test(currentChar)) { let value = currentChar; let i = this.pos + 1; while (i < this.input.length && /^\d+$/.test(this.input[i])) { value += this.input[i]; i++; } this.pos = i; return { type: "NUMBER", value }; } else if (currentChar === "+") { this.pos++; return { type: "PLUS" }; } else if (currentChar === "-") { this.pos++; return { type: "MINUS" }; } else if (currentChar === "*") { this.pos++; return { type: "MULTIPLY" }; } else if (currentChar === "/") { this.pos++; return { type: "DIVIDE" }; } else if (currentChar === "(") { this.pos++; return { type: "LPAREN" }; } else if (currentChar === ")") { this.pos++; return { type: "RPAREN" }; } throw new Error( `非法字符'${value}',仅允许'+'、'-'、'*'、'/'、'('、')' 以及十进制数。` ); } /** * 解析入口函数 * @returns 解析结果 */ parse() { this.currentToken = this.getNextToken(); return this.expr(); } /** * 解析表达式 * * 即:E -> TE',E' -> +TE' | -TE' | ε * @returns 结果 */ expr() { let result = this.term(); while ( this.currentToken !== null && (this.currentToken.type === "PLUS" || this.currentToken.type === "MINUS") ) { if (this.currentToken.type === "PLUS") { this.eat("PLUS"); result += this.term(); } else if (this.currentToken.type === "MINUS") { this.eat("MINUS"); result -= this.term(); } } return result; } /** * 解析项 * * 即:T -> FT',T' -> *FT' | /FT' | ε * @returns 结果 */ term() { let result = this.factor(); while ( this.currentToken !== null && (this.currentToken.type === "MULTIPLY" || this.currentToken.type === "DIVIDE") ) { if (this.currentToken.type === "MULTIPLY") { this.eat("MULTIPLY"); result *= this.factor(); } else if (this.currentToken.type === "DIVIDE") { this.eat("DIVIDE"); result /= this.factor(); } } return result; } /** * 解析因子 * * 即:F -> D | (E) * @returns 结果 */ factor() { if (this.currentToken.type === "LPAREN") { this.eat("LPAREN"); const result = this.expr(); this.eat("RPAREN"); return result; } else if (this.currentToken.type === "NUMBER") { const result = parseInt(this.currentToken.value, 10); this.eat("NUMBER"); return result; } throw new Error("语法存在问题"); } /** * 类型匹配则继续下一个token的处理 * @param {string} tokenType token 类型 */ eat(tokenType) { if (this.currentToken.type === tokenType) { this.currentToken = this.getNextToken(); } else { throw new Error( `当前的 token 类型是 ${this.currentToken.type},希望是 ${tokenType}` ); } } } const input = "(2 + 3) * 4 - 9 / 3 - 1"; const parser = new Parser(input); console.log(parser.parse()); // 输出 16
设计一款 DSL
领域特定语言(Domain Specific Language)即 DSL,是一种针对特定领域的编程语言,可以更加简单、高效的解决特定使用场景的问题和需求。
设计原则
- 易上手:简单易懂,可以快速入门,减少用户学习曲线;
- 高表达:表达能力强,用最少的代码来完成复杂的任务,提高用户开发效率;
- 可拓展:使用过程中可以根据需要不断进行功能拓展,保证其成长性;
- 高性能:执行过程迅速,避免用户存在等待时间;
- 稳定性:正确理解用户描述内容,保证目标代码的可靠性;
- 易维护:具有清晰的结构以及良好的报错机制,便于修改与升级。
设计步骤
确定领域范围和目标用户
- 领域范围:用于管理系统前后端代码的生成;
- 目标用户:拥有编程基础的开发人员。
定义语言语法
- 终结符:最基础的描述单位。如:变量名、关键字、操作符、标点符号等;
- 非终结符:非终结符是由一个或多个终结符组成的语言元素。如:表达式、函数等;
- 语句:具有明确意义的可操作任务指令;
- 表达式:可进行计算最后得到一个结果的语句;
- 操作符:具有明确操作的符号:如:
=、+、#等; - 注释:描述代码作用的解释性文本,无实际意义。
定义语言语法的前提是,你对目标代码足够了解,清楚你所设计的 DSL 转换为目标代码的意义所在。
可以尝试写一套尽可能完整的目标代码,进行(不考虑目标代码语法)简化,针对最终简化结果进行解耦、结构重排,就可以得到一款初代版本的 DSL,根据设计原则不断优化,将会得到一款不错的可迭代升级的 DSL。
选择 DSL 实现方式
- 内部 DSL:嵌入在诸语言中;
- 外部 DSL:需要有独立的解析器和执行器。
实现解析器
- 将源代码解析成一段可描述源代码结构的数据,例如抽象语法树(Abstract Syntax Tree,AST),可以使用解析器生成器来创建,如:ANTLR 或 PEG.js。
实现执行引擎
- 执行 AST,并生成可用的目标代码。
完善 DSL 库和工具
- 丰富语法,并添加测试与调试工具,提高其稳定性;
- 撰写文档,帮助使用者快速了解并使用。
常用术语
- 词法分析(Lexical Analysis):将源代码分解为单词或标记的过程。
- 语法分析(Syntax Analysis):将单词或标记组成的序列转换为语法结构并生成抽象语法树(Abstract Syntax Tree)的过程。
- 语义分析(Semantic Analysis):检查源代码是否符合语言规范。
- 中间代码生成(Intermediate Code Generation):将源代码转换为一种中间表示形式的过程。
- 代码优化(Code Optimization):对中间代码进行重写,以使生成的目标代码更高效。
- 目标代码生成(Code Generation):将中间代码转换为机器码或汇编语言的过程。
- 链接(Linking):将不同的目标文件组合成一个可执行文件的过程。
- 调试(Debugging):排除代码错误或逻辑错误的过程。
- 编译器(Compiler):执行编译过程的程序。
- 解释器(Interpreter):逐行解释源代码并执行的程序。
- JIT编译器(Just-In-Time Compiler):在程序运行时动态地将字节码编译为本地机器码的编译器。