03.从0实现一个JVM语言系列之语法分析器-Parser-03月02日更新

03.从0实现JVM语言之语法分析器-Parser

相较于以前有较大更新, 老朋友们能够复盘或者针对bug留言, 我会看到以后答复您!

源码github仓库, 若是这个系列对您有帮助, 但愿得到您的一个star!

本节相关语法分析package地址

本节相关前端语法树package地址

系列导读 00.一个JVM语言的诞生

致亲爱的读者:

    我的的文字组织和写文章的功底属实通常, 写的也比较赶时间, 因此系列文章的文字可能比较粗糙,
不免有词不达意或者写的很迷惑抽象的地方 

    若是您看了有疑问或者以为我写的实在乱七八糟, 这个很抱歉, 确实是个人问题, 您若是有不懂的地方
的地方或者发现个人错误(文字错误, 逻辑错误或者知识点错误都有可能), 能够直接留言, 我看到都会回复您!

系列食用方法建议

因为时间缘由, 目前测试并不完善, 因此推荐以下方式根据您的目的进行阅读

    若是您是学习用, 建议您先将整个项目clone到本地, 而后把感兴趣的章节删除, 本身重写对照着重写
    书写完每一步测试一下可否正常运行(在指定的路径去读取源码测试可否编译成功并在命令行执行

    java Application(类名)

尝试可否输出指望结果, 我没有研究Junit对编译器输出class文件进行测试, 因此目前可能须要您手动测试)

    按照以上步骤, 等您将全部模块重写一遍, 大概也对这个系列的脉络有深入理解了! 若是您重头开始重写, 
每每可能因为出现某些低级错误致使长时间debug才找获得错误, 因此对于初学者, 推荐采用本身补写替换模块的
方式

    对于但愿贡献代码的朋友或者对Cva感兴趣的朋友, 欢迎贡献您的源码与看法, 或者对于该系列一些错误/
bug愿意提出指正的朋友, 您能够留言或者在github发issue, 我看到后必定及时处理!

本节提纲

  1. 抽象语法树介绍html

    1.1. 简介前端

    1.2. Cva程序的树形结构示意图java

    1.3. ast包内详细介绍及继承关系git

  2. 语法分析实现程序员

    2.1. 递归降低分析算法github

    2.2. 递归降低算法解析Cva代码算法

    2.3. 语法分析实例简析设计模式

    2.4. 给出语法错误提示及警告jvm

  3. 设计模式浅析函数

    3.1. ast中的设计模式浅析

抽象语法树介绍

简介

前面咱们实现了词法分析器, 会发现词法分析器的功能是有限的, 他只能像个复读机或者说
翻译官同样将咱们的源代码按照必定的规则切割成Token流, 可是咱们的代码实际上是树状的结构,
为了将代码抽象成树状的POJO(咱们的前端ast的Program类), 这个类能够延伸出咱们代码的
每个节点, 每个类, 每个方法, 每个方法本地变量, 每个运算符, 他是以
Program这个节点为root的一棵树, 这棵树叫作抽象语法树(AST:abstract syntax tree),
语法分析器将用户(程序员)写的这段代码按照咱们的语法规则进行分层次理解转化成方便后期处理的
一种中间表示, 这个中间表示是井井有条的树形结构, 就是咱们的抽象语法树

咱们这里没有画图, 按照树的层次, 咱们这里采用自顶向下语法分析, 这课树的各个节点与咱们
表示Cva语言文法的树形结构是相似的

编译器前端抽象语法树Package在 cn.misection.ast 中, 其中对于各类节点都有详细的归类
每一个package如他们的名字所示, 分别包含着Expr, statement, 全部的节点都实现空接口
cn.misection.ast.IASTreeNode, 代表他们都是抽象语法树的节点, 不一样包内会有一个节点接口
继承自 cn.misection.ast.IASTreeNode, 如Expr包中有IExpression.以后会有一个抽象类继承自包内抽象节点, 如 AbstractExpression, 以后全部包内节点都将继承自抽象节点
或者节点接口, 接口内会规定一些方法, 该包中的类必须实现他们, 如toEnum()

引入toEnum()和枚举表示是为了以后咱们更优雅地switch case, 不然对于同属一类的节点,
咱们可能须要反射去获取抽象的表达式究竟是加法表达式仍是乘法表达式(或者instanceof运算),
类名虽然能够表达这些信息, 但这样用并不优雅, 因此咱们给每种节点都绑定一个枚举值, 既方便switch,
又能够取代一些很是简单的不包含信息的类型(如type包中的基本类型基本只是做为别人的属性使用), 这时候
使用他们时, 用他们的枚举单例其实就够了, 语法分析器最后就是须要拿到一个从Program始, 向类
方法等发散的前端语法树, 树发散的每一个节点就是一个个有了行号信息和字面量(有字面量时)的POJO

语法分析器即是抽象语法树的制造者, 分析器读完Token流以后, 在语言的语法规则指导下,
咱们就能获得语法分析器的结晶-抽象语法树了

本质来说, 抽象语法树能够看做是对该语言文法的"类型化"重写, 即对于每一个非终结符,
每一个产生式, 给出确切定义, 并赋予不一样的属性. 类型实例便成为要输出的抽象语法树的"节点"

Cva程序的树形结构示意图

依据以前给出的程序文法, 能够给出一个树形表示Cva程序的大体结构
因为博客园的二级分类展开有点松散有点丑, 因此咱们放到text中

+ Program
  + Entry(能够是入口类, 也能够直接是入口方法)
    + main方法
      + StatementList
  + ClassList
    + Class
      + VarList
        + Var
        + ...
      + MethodList
        + Method
        + ...
    + ...
/**
 * 方法体;
 */
+ Method
  + Return Type
  + Name
  + FormalList
    + Formal
    + ...
  + VarList
    + Var
    + ...
  + StatementList
    + Statement
    + ...
  + Return Expr
/**
 * 变量声明;
 */
+ Var
  + Type
  + Identifier

// 固然, Cva 支持在声明时赋初值, 不过目前的实现比较简单, 存在着必定的问题

上文所述的每个节点(不包含XXXList类型节点), 咱们都有给出确切的定义, 而且在节点内携带足够的信息(行号, 类型信息等), 方便后期的语义分析和错误提示信息。

ast包内详细介绍及继承关系

咱们的AST中共包含8种主要的类型以下

+ Program

  程序实体类, 语法树的根节点, 拥有 EntryClass 和 ClassList 两个属性
  + EntryClass   // 主类(程序入口), 若是只有main方法, 会自动添加到Application类中
  + ClassList   // 用户自定义的类

+ EntryClass

  主类实体类, 程序入口方法所在类, 若是不显示声明(直接上main方法), 那么会生成Application做为其默认名 
  拥有 Name StatementList(从属main方法) 两个属性
  + Name        // 类名
  + StatementList   // main方法中的语句 TODO 能够直接作成BlockStatement

+ Class

  类实体类, 表示用户自定义的类, 拥有 Name BaseClass FieldList MethodList 四个属性
  + Name        // 类名
  + ParentClass   // 父类(默认就是java/lang/Object)
  + FieldList   // 字段列表
  + MethodList  // 实例方法列表

+ Method

  方法实体类, 表示用户定义的实例方法
  + Name            // 方法名
  + ReturnType      // 返回类型
  + ParameterList
    /FormalList
    /ArgList        // 方法的参数
  + LocalVarList    // 方法内声明的本地变量
  + StatementList   // 语句
  + ReturnExpr       // 返回表达式 TODO 不要写那么死 之后要支持方法中return;

+ Statement
    + nullobj // 空对象
    + assign  // 赋值
    + Block     // 语句块
    + if    // if    
    ... 等等, 详细可见pkg内
  语句抽象类, 赋值语句, 输出语句, if-else语句,  while语句, for语句等Statement
     语句块直接继承自此类

  // TODO: 插入树形关系图

+ Expression

  表达式抽象类, 加减乘运算, 小于运算, 与运算, 非运算, 方法调用, this表达式, 
    对象建立表达式(new), 常量(数字, true/false), 变量访问直接继承此抽象类
  + LineNumber  //表达式所带行号
  // TODO: 插入树形关系图

+ Variable

  变量/字段实体类
  + Type    // 该变量/字段的类型
  + Identifier      // 变量/字段名

+ Type
    + basic
        + enum基本类型
    + advance
        + string
        + array
        + pointer
    + reference 
        + classType
  类型抽象类, 整型, 浮点型, 布尔型, string, class类类型直接继承自此抽象类

  // TODO: 插入树形关系图

语法分析实现

语法分析器的任务是读入记号流, 在语言的语法规则指导下生成抽象语法树。

分析算法主要分为自顶向下分析和自底向上分析, 其中自顶向下分析包括递归降低分析算法(预测分析算法)和LL分析算法, 自底向上分析包括LR分析算法。

递归降低分析算法

  • 简单来说, 递归降低法就是对于现有的Token(现有的信息, 如今手里捏的牌), 来预测下一个单词应该是什么
    好比, 如今程序说了: "你吃",
    那么咱们预测下一个词应该是"了吗?" 或者"饭了吗?"
    即咱们但愿获得的完整句子是"你吃了吗?" 或者"你吃饭了吗?"
    咱们能够在获得现有信息的基础上对下一步可能的状况进行手动穷举

  • 回到一个Java中的例子, 好比如今咱们遇到了int这个词, 咱们判断他是int型字面量,
    那么以后的状况无外乎 int var; 或者 int var = 0; 即声明或赋值
    (这里不考虑强转, 由于强转前面有括号, 咱们对其处理通常是在另外一个流程中的)
    因此咱们下一步通常要吃掉咱们预期的Token(Parser的eatToken()方法),
    这个被吃掉的Token其实就是咱们当前读到的Token, 咱们能够先读取他的信息
    (好比字面量等保存到如今, 输出为咱们抽象语法树的节点(方法, 类, 声明变量),
    待他无用以后, 将其eat掉), 若是有多种状况, 其实就是一个或多个分支的事
    (在eat时, 须要指定须要eat的类型, 若是不符合, 就会报错)

Cvac编译器采用的是递归降低分析算法(预测分析), 该算法的主要优势是

  • 分析高效, 线性时间复杂度
  • 容易实现, 方便手工编码
  • 错误定位和诊断信息准确

不少开源编译器, 商业编译器也采用了该算法, 好比GCC4.0, LLVM等

该算法的基本思想是:

  • 为每一个非终结符构造一个分析函数
  • 经过前看符号指导产生式规则的选择

递归降低算法解析Cva代码

咱们选择一个简单的部分来介绍递归降低分析算法在程序中的应用。

如上文所述, 用户自定义的类由两部分构成, 字段列表和实例方法列表(固然, 这个两个列表均可觉得空), 那么咱们就给出这样一个方法

/**
 *  Class
     -> class Id { VarDecList MethodDecList }
     | class Id : Id { VarDecList MethodDecList }
 */
Class ParseClass()
{
    // 其余代码
    fieldList = parseFieldList();
    methodList = parseMethodList();

    return new SomeClass(fieldList, methodList);
}

如上述代码所示, 在从记号流解析一个类型实体时, 调用了字段列表和方法列表的解析方法, 并将解析的结果做为一个类型实体的组成部分(此处并未体现类名, 父类等信息).

很显然的, 在 ParseFieldList, ParseMethodList两个方法内部, 一定含有对于单个字段, 单个方法实体的解析方法的调用, 并将单个的实体结果组织起来, 以返回给外界.

思考一下咱们的文法规定的一个方法的构成形式, 返回类型, 方法名, 参数列表, 本地变量列表, 语句列表, 返回语句。这些部分的一个组织是方法, 那么很天然地, 咱们又能为此写一个解析方法。

以上就体现了分析算法中为每一个非终结符构造一个分析函数思想. 可是还有另一部分思想, 经过前看符号指导产生式规则的选择还未体现。

考虑另一条文法, 关于"语句"。语句在咱们的程序中有五种形式, 由{}组织的语句块, if-else语句, while语句, 赋值语句, 输出语句.
咱们只须要看第一个符号, 就能知道应当选择哪一条产生式来解析, 伪代码描述以下所示.

/**
 *  Statement
       -> { StatementList }
       | if (Expr) Statement else Statement
       | while (Expr) Statement
       | println(Expr);
       | Id = Expr;
 */
Statement parseStatement()
{
    switch(firstToken)
    {
        // 写代码时尽可能不要出现以下的魔数, 这里是为了演示
        // 实际上咱们是把常量放入枚举中的
        case "{":
            return parseStatementBlock();
        case "if":
            return parseIfElseStatement();
        case "while":
            return parseWhileStatement();
        // ...
        default:
            throw new ParseException(message);
    }
}

从伪码中很清晰地体现出来, 咱们只须要经过查看一个"前看符号"就能肯定要选择哪一个方向去解析当前语句。若是解析失败, 那么一定是用户给的源程序出现了问题, 致使咱们程序选择了错误的方向, 或者出现了错误, 这就须要用户修改源代码, 而后从新编译。

语法分析实例简析

对于文法附带给出的程序样例中的一行语句

total = num * (this.compute(num-1));

语法分析完成后的输出的抽象语法树以下示意

其实语法分析给出的语法树就是把源代码又层次打印一遍, 过程比较简单. 若是朋友有问题能够留言, 我看到会回答

// TODO: 树形图

给出语法错误提示及警告

在语法分析阶段, 针对源码中可能出现的错误, 会给出错误提示信息, 用于告知用户程序处理到的位置和出现的错误

在自顶向下语法分析部分, 可能出现的错误的仅有一类, 预期是某个符号, 可是获得的倒是另外的符号, 这样就会出现错误了

举例以下:

void doSomething(int )
{
  // ...
}

容易看出, 在这个方法的参数列表部分, 缺失了参数名, 所以语法分析器将会给出错误提示:

Line 6: Exprects Identifier, but got CLOSE_PAREN.
Syntax error line 6, compilation aborting...

给出错误提示以后就直接退出虚拟机, 拒绝编译, 等待用户修改源代码并从新编译

设计模式浅析

ast中的设计模式

  1. 空对象模式: 在ast中全部的nullobj包都是空对象模式, 空对象模式能省却咱们代码中大量难看的
    判空if (obj != null ) 等等
    在咱们后面遇到空对象时, 也是什么都不作, 把它当作空气(可怜的空对象)

  2. 建造者模式: 在前端的method构造时, 使用了建造者模式, 建造者模式能让咱们的传参更加清晰,
    不易犯错, 也能必定程度上封装复杂的构造过程, 后期将会把全部构造复杂的POJO所有重构成建造者模式构造

  3. 往后会思考将一些构造重构成工厂

相关文章
相关标签/搜索