编译程序(compiler)的简单分析

本文深入探讨了编译器的基本工作流程,包括解析、转换和代码生成等关键步骤,并通过具体的例子说明如何将一种编程语言转换为另一种。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在现今前端项目中,模块化是一个避不开的话题。所以就会出现AMD,CMD等模块加载方式。同时由于JS不停的在更新迭代。出现很多实用的新语法。但是由于有些语法有些超前,JS的宿主环境(浏览器/Node没有跟上JS更新步骤),但是为了在项目中使用这些好用到令人发指的新特性,来提高开发效率等。就出现了各种前端编译插件(Babel)。

Babel is a JavaScript compiler

大多数编译程序(compiler)分为三个步骤:Parsing(分析阶段)/Transformation(转换)/Code Generation(代码生成或者说生成目标代码)

  1. Parsing将源代码(raw code)转换为AST(抽象语法树)。
  2. Transformation接收Parsing生成的AST,并且按照compiler内定的规则进行代码的转换。
  3. Code Generation 接受被compiler转换过的代码,按照一定的规则将代码转换为最终想要输出的代码格式。 现在有一个场景: 我们将一些LISP(高级计算机程序语言)方法通过compiler转码为C语言(通用计算机编程语言)的方法。 假如我们有'add'和'subtract'方法

自然语言LISPC
2+2(add 2 2)add(2,2)
4-2(subtract 4 2)subtract(4,2)
2 + (4 - 2)(add 2 (subtract 4 2))add(2, subtract(4, 2))

Parsing(语法解析)

Parsing 一般被分成两个步骤:Lexical Analysis(词法分析)和 Syntactic Analysis(语法分析)

  1. Lexical Analysis 接受raw code 同时通过tokenizer(标记器)或者lexer(词法分析器)将raw code 拆解为许多tokens。Tokens 是一系列描述独立的语法的对象。他们可以是数字,标签,标点符号,操作符等
  2. Syntactic Analysis 接收LA处理过的tokens并且将他们重新构建为能够描述每一个语法代表什么含义并且描绘每个语法之间是如何关联的树行结构-----将每一个token视为一个Node结点,各个token之间存在的关联视为"树枝",从而会构建一个能够表明各个token含义同时各个token之间关系的树形结构-------Abstract Syntax Tree(AST)。

AST 是一个层级很深的对象。


e.g. 对(add 2 (subtract 4 2))进行Parsing处理。 Tokens如下(Note:其实Token是根据lexer生成的,不同的lexer处理结果是不一样的。)

[
    { type: 'paren',  value: '('        },
    { type: 'name',   value: 'add'      },
    { type: 'number', value: '2'        },
    { type: 'paren',  value: '('        },
    { type: 'name',   value: 'subtract' },
    { type: 'number', value: '4'        },
    { type: 'number', value: '2'        },
    { type: 'paren',  value: ')'        },
    { type: 'paren',  value: ')'        },
]
复制代码

对应的Abstract Syntax Tree (AST) 可能如下

{
      type: 'Program',
      body: [{
        type: 'CallExpression',
        name: 'add',
        params: [{
          type: 'NumberLiteral',
          value: '2',
        }, {
          type: 'CallExpression',
          name: 'subtract',
          params: [{
            type: 'NumberLiteral',
            value: '4',
          }, {
            type: 'NumberLiteral',
            value: '2',
          }]
        }]
      }]
    }

复制代码

Transformation(AST转换)

transformation是compiler的第二个阶段。他会接收经过SA处理生成的AST。在该阶段能够利用一些语法规则,将AST转换为想被转换的语言。

通过观察AST会发现,每一个elements(从AST角度看)或者token(从LA角度看)都有一个type属性。这些element是属于AST的Node结点。这些nodes通过对type属性赋特定的值将AST划分成各自独立的区块。

e.g.

NumberLiteral 类型的Node

{

 type: 'NumberLiteral',

 value: '2',

 }
复制代码

CallExpression 类型的Node

{

 type: 'CallExpression',

 name: 'subtract',

 params: [...内嵌的node逻辑...],

 }
复制代码

在transforming AST过程中,我们可以通过adding/removing/replacing 属性来修改nodes,同时我们可以add/remove nodes,甚至我们可以基于现有的AST来重新构建新的AST对象。

由于我们是需要将LISP语法的代码转换为C的,所以我们的关注点就是基于SA输出的AST构建一个全新的适用于目标语言的AST对象。


Traversal(遍历)

为了能够在transforming过程中检测这些nodes。同时由于AST是一个层级很深的对象树,所以需要对AST进行depth-first深度优先遍历)。(其实这和React在Render阶段是一样的)

{
      type: 'Program',
      body: [{
        type: 'CallExpression',
        name: 'add',
        params: [{
          type: 'NumberLiteral',
          value: '2',
        }, {
          type: 'CallExpression',
          name: 'subtract',
          params: [{
            type: 'NumberLiteral',
            value: '4',
          }, {
            type: 'NumberLiteral',
            value: '2',
          }]
        }]
      }]
    }
复制代码

对于上述的AST,在traversal阶段,范围每个node的先后顺序如下

  1. Program - Starting at the top level of the AST
  2. CallExpression (add) - Moving to the first element of the Program's body
  3. NumberLiteral (2) - Moving to the first element of CallExpression's params
  4. CallExpression (subtract) - Moving to the second element of CallExpression's params
  5. NumberLiteral (4) - Moving to the first element of CallExpression's params
  6. NumberLiteral (2) - Moving to the second element of CallExpression's params

Visitors(游标)

为了用代码实现traversal过程,我们构建一个内置能够接收不同node类型函数的"visitor"对象。

var visitor = {
      NumberLiteral() {},
      CallExpression() {},
    };
复制代码

当在遍历AST的时候,在我们访问对应的node结点时,就会触发与之类型匹配的visitor中的方法。

如果只是单纯的在访问结点的时候触发对应的方法,这种情况是无法纪录访问的"轨迹",所以需要对visitor进行改进。传入被访问的node结点,还有该node的直接父级结点。

var visitor = {
      NumberLiteral(node, parent) {},
      CallExpression(node, parent) {},
    };
复制代码

如果没有返回处理,"游标"在遍历到最后的node就会停止,因为他不知道下一步该如何进行。

 - Program
      - CallExpression
        - NumberLiteral
        - CallExpression
          - NumberLiteral
          - NumberLiteral

复制代码

由于在遍历AST的过程中是采用depth-first的方式,就需要在访问到最后的node的时候,需要按照原路返回,直到返回到起点,这样才能被程序识别,这颗树被遍历完成了。

-> Program (enter)
      -> CallExpression (enter)
        -> Number Literal (enter)
        <- Number Literal (exit)
        -> Call Expression (enter)
           -> Number Literal (enter)
           <- Number Literal (exit)
           -> Number Literal (enter)
           <- Number Literal (exit)
        <- CallExpression (exit)
      <- CallExpression (exit)
    <- Program (exit)

复制代码

为了实现上述逻辑,需要对visitor做额外的处理

 var visitor = {
      NumberLiteral: {
        enter(node, parent) {},
        exit(node, parent) {},
      },
      CallExpression:{
        enter(node, parent){},
        exit(node, parent){},
      },
    };

复制代码

Code Generation(生成指定格式的代码)

compiler的最后阶段是code generation。有些compiler在CG阶段做的工作会和transformation的重叠,但是大部分的CG的工作就是接收被处理过的AST然后将AST对象字符化(该操作类似于JSON.stringify(Object))。 一个高效的CG是能够根据AST不同的node type输出对应的code,同时能够在树内进行递归调用直到所有的node都被字符化。

实验一:词法分析程序 一、实验目的     通过设计编制调试一个具体的词法分析程序,加深对词法分析原理的理解。并掌握在对程序设计语言源程序进行扫描过程中将其分解为各类单词的词法分析方法。 编制一个读单词过程,从输入的源程序中,识别出各个具有独立意义的单词,即基本保留字、标识符、常数、运算符、分隔符五大类。并依次输出各个单词的类型码及单词符号的自身值。(遇到错误时可显示“Error”,然后跳过错误部分继续显示) 二、实验要求 用C或C++写一个简单的词法分析程序,程序可以满足下列要求: 1、能分析如下几种简单的语言词法 (1) 标识符: ID=letter(letter|digit)* (2) 关键字(全部小写) main int float double char if then else switch case break continue while do for (3)整型常量:NUM=digit digit* (4)运算符 = + - * / < <= == != > >= ; ( )? : (5)空格由空白、制表符和换行符组成,用以分隔ID、NUM、运算符等,字符分析时被忽略。 2、单词符号和相应的类别码 假定单词符号和相应的类别码如下: 单词符号 种别码 int 1 = 17 float 2 < 20 if 3 <= 21 switch 4 == 22 while 5 != 23 Do 6 > 24 标识符 10 >= 25 整型常量 11 ; 26 + 13 ( 27 - 14 ) 28 * 15 ? 29 / 16 : 30 3、词法分析程序实现的功能 输入:单词序列(以文件形式提供),输出识别的单词的二元组序列到文件和屏幕 输出:二元组构成: (syn,token或sum) 其中: syn 为单词的种别码 token 为存放的单词自身符号串 sum 为整型常数 例: 源程序: int ab; float ef=20; ab=10+ef; 输出: (保留字--1,int) (标识符--10,ab) (分号--26,;) (保留字--2,float) (标识符--10,ef) (等号--17,=) (整数--11,20) (分号--26,;) (标识符--10,ab) (等号--17,=) (整数--11,10) (加号--13,+) (标识符--10,ef) (分号--26,;) 4、自己准备测试数据存放于TestData.txt文件中,测试数据中应覆盖有以上5种数据,测试结果要求以原数据与结果对照的形式输出并保存在Result.txt中,同时要把结果输出到屏幕。 5、提前准备 ① 实验前,先编制好程序,上机时输入并调试程序。 准备好多组测试数据(存放于文件TestData.txt中)。 6、写出实验报告 报告格式:要求有实验名称、实验目的、实验要求、实验内容、实验小结。 其中实验内容包括算法分析、程序流程图及程序代码。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值