前端编译入门

80 阅读10分钟

编译

前言

前端开发人员为什么要了解编译原理?

大家或许总能听到编译,但真正要说出个因为所以,多数人可能会差些意思,可能是工作环境的使然,也可能是个人目标的不同吧!

那么,前端开发人员了解编译原理、学习编译原理、应用编译原理有什么意义?或者有什么好处呢?我总的认为是可以使前端开发人员更好地理解代码执行的底层原理,从而提高代码质量和性能,并为他们开发复杂的应用程序提供更多的灵活性和控制力。编译可以让你更好的还原代码真相,因为编译的过程就是将高级语言不断解析从而达到本质即机器语言。了解本质更利于全平台的开发,即使接触未开发过的平台,也更容易找到切入点,毕竟新技术新平台是无止境的。了解代码的解析过程,将会更直观的发现缺陷是发生在代码执行链路的哪一层,从而在根原处解决掉它。在这优化与兼容的过程中,或许你会发现某一个库、框架 甚至 语言的缺陷,从而迫使自己进行语言设计和拓展,再而得到更贴合业务场景的开发工具。

记录入门编译的一个过程,留给或许未来将“短暂性失忆”的自己。

原理

编译的原理是将高级语言代码转换成机器语言代码的过程。

语言处理器

编译器

编译器可以将一种语言(源语言)编写的程序转换为另一种语言(目标语言)编写的程序。若该程序本就具备可执行性,则语言转换完成后,可再进行输入数据分析得出输出结果。

解释器

解释器可以将源代码(源语言编写的程序)逐行解释执行。解释器执行的结果即程序运行后的结果,不会产生中间代码。

编译步骤

词法分析(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) 文法

  1. 文法设计

    1. 定义终结符和非终结符

      • 终结符:+-*/()十进制数(digit,即 0~9)
      • 非终结符:表达式(expresion)、项(term)、因子(factor)。
    2. 梳理关系

      • E 包含多个 T,T 包含多个 F,F 是数字或带有括号的表达式。
    3. 根据梳理关系得出产生式

      • E -> E+T | E-T | T
        T -> T*F | T/F | F
        F -> D | (E)
        
    4. 将上一步产生式转换为 LL(1) 文法,即去除左递归和二义性,上述产生式不存在二义性,因此去除左递归即可

      • 去除左递归,即将左递归转换为右递归
      E  -> TE'
      E' -> +TE' | -TE' | ε
      T  -> FT'
      T' -> *FT' | /FT' | ε
      F  -> D | (E)
      
  2. 构造预测分析表

    1. 将产生式拆分出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) = {+, -, *, /, ), $}
      
    2. 根据集合填充预测分析表

      +-*/()D$
      EE -> TE'E -> TE'
      E'E' -> +TE'E' -> -TE'E' -> εE' -> ε
      TT -> FT'T -> FT'
      T'T' -> *FT'T' -> /FT'T' -> εT' -> ε
      FF -> (E)F -> D
  3. 实现解释器

    // 四则运算解释器
    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 库和工具

  • 丰富语法,并添加测试与调试工具,提高其稳定性;
  • 撰写文档,帮助使用者快速了解并使用。

常用术语

  1. 词法分析(Lexical Analysis):将源代码分解为单词或标记的过程。
  2. 语法分析(Syntax Analysis):将单词或标记组成的序列转换为语法结构并生成抽象语法树(Abstract Syntax Tree)的过程。
  3. 语义分析(Semantic Analysis):检查源代码是否符合语言规范。
  4. 中间代码生成(Intermediate Code Generation):将源代码转换为一种中间表示形式的过程。
  5. 代码优化(Code Optimization):对中间代码进行重写,以使生成的目标代码更高效。
  6. 目标代码生成(Code Generation):将中间代码转换为机器码或汇编语言的过程。
  7. 链接(Linking):将不同的目标文件组合成一个可执行文件的过程。
  8. 调试(Debugging):排除代码错误或逻辑错误的过程。
  9. 编译器(Compiler):执行编译过程的程序。
  10. 解释器(Interpreter):逐行解释源代码并执行的程序。
  11. JIT编译器(Just-In-Time Compiler):在程序运行时动态地将字节码编译为本地机器码的编译器。