致亲爱的读者: 我的的文字组织和写文章的功底属实通常, 写的也比较赶时间, 因此系列文章的文字可能比较粗糙, 不免有词不达意或者写的很迷惑抽象的地方 若是您看了有疑问或者以为我写的实在乱七八糟, 这个很抱歉, 确实是个人问题, 您若是有不懂的地方 的地方或者发现个人错误(文字错误, 逻辑错误或者知识点错误都有可能), 能够直接留言, 我看到都会回复您!
因为时间缘由, 目前测试并不完善, 因此推荐以下方式根据您的目的进行阅读 若是您是学习用, 建议您先将整个项目clone到本地, 而后把感兴趣的章节删除, 本身重写对照着重写 书写完每一步测试一下可否正常运行(在指定的路径去读取源码测试可否编译成功并在命令行执行 java Application(类名) 尝试可否输出指望结果, 我没有研究Junit对编译器输出class文件进行测试, 因此目前可能须要您手动测试) 按照以上步骤, 等您将全部模块重写一遍, 大概也对这个系列的脉络有深入理解了! 若是您重头开始重写, 每每可能因为出现某些低级错误致使长时间debug才找获得错误, 因此对于初学者, 推荐采用本身补写替换模块的 方式 对于但愿贡献代码的朋友或者对Cva感兴趣的朋友, 欢迎贡献您的源码与看法, 或者对于该系列一些错误/ bug愿意提出指正的朋友, 您能够留言或者在github发issue, 我看到后必定及时处理!
语义分析器工做基于语法分析器输出的抽象语法树, 经过对该语法树的分析作进一步的检查,
回答咱们, 也回答代码生成器最后一个问题:源程序是否符合语义规则. 若是确实存在问题,
那么这段代码即便翻译成机器码(在这里是咱们后面要生成的JVM汇编指令), 也不可能执行成功,
必然收到JVM的拒绝执行(读者能够本身尝试瞎写命令而后用jasmin汇编成字节码, 看报错反应, 哈哈),
或者形成一些意想不到的问题, 所以语义分析有问题势必形成后面的错误, 因此语义分析旨在搜集
代码出现的逻辑错误(多数是类型检查的类型问题), 并指出, 以让接下来的步骤能正常进行前端
在这个阶段要给出尽量准确的报错信息, 供用户参考并修改源代码.
语义分析中最重要的工做即是类型检查, 此外还会有一些其余的检查, 例如变量在使用前是否声明等.java
语义分析的正常进行少不了符号表的参与. 所谓符号, 程序中的变量、方法、字段、类都是符号.
符号表存储了程序中的符号的相关信息, 这些信息包括类型、做用域、访问控制信息等等, 并且符号表必须很是高效,
由于程序中符号的规模会很是大. 咱们的符号表都是采用HashMap, JVM针对HashMap的优化可谓是比较极致,
咱们的JDK8后引入了红黑树, 对于Map, HotSpot会倾向于将其编译成本地代码, 在一些状况下,
其运行效率甚至超过通常的手写分支判断git
咱们的哈希表以符号名字为键, 以符号的相关信息为值, 创建映射.
并按照树形结构, 组织各个做用域的符号及信息, 创建全局符号表.github
全局符号表的大体结构以下:后端
// TODO: 树形图jvm
+ ClassMap + ClassBinding + BaseClass + FieldMap + Field + ... + MethodMap + Method + ... + ...
+ MethodVariableMap + ClassMap 全局符号表的入口, 直接维护了类名和类的相关信息的 + ClassBinding 存储了类的相关信息, 包括父类, 字段表, 方法表. 其中字段表和方法表是以名为键的映射. + Field 存储了字段的声明类型 + Method 存储了方法的相关信息, 包括声明的返回类型, 参数的个数及各自类型. + MethodVariableMap 参数和本地变量表, 是名字和类型的映射. 当分析某个方法的时候, 对于该方法的参数和本地变量的访问是至关频繁的, 所以将他们单独存储到某个位置, 分析完毕即销毁, 优化空间的占用.
本质上说, 所谓“分析”仅仅是语法树的遍历而已, 只是附带上了附加条件, 要求某子树符合某个要求. 此外咱们应当注意到,
类型的声明、字段的声明和方法的声明, 并无任何值得语义分析的地方, 真正值得咱们去分析和检查的是语句和表达式,
查看它们是否合法. 所以, 分析过程可简要分红两步.ide
信息收集和索引函数
对当前语法树的前几层进行遍历, 扫描并存储类信息, 各自的字段和方法相关信息, 构建全局随时可用的“全局符号表”,
该过程仅在语义分析实际进行以前进行一次.学习
语义分析和检查测试
收集要分析的方法的参数和变量信息, 而后顺序遍历方法的每一条语句和表达式. 若是找到错误, 那么就输出一条信息,
提示用户在某位置发现何种类型的错误, 并尽量进行恢复, 继续检查下文, 在一趟分析中给出尽量多的错误信息.
对于每一个方法, 都执行一遍步骤2, 直到全部的方法都分析过. 若是未发现任何错误, 那么进入下一阶段, 若是发现问题,
那么在给出全部信息后, 退出编译过程.
类型检查是语义分析的重点所在, 若是此处的检查没有经过, 那么这个程序一定存在问题, 一定不能运行.
下面经过几个例子来展现类型检查的工做细节.
对于表达式而言, 类型检查主要分为如下几类
操做符
对于单目运算符逻辑非 !
主要检测其操做数是不是前端boolean
(只有前端用户才会看到boolean, 在编译器后端boolean会被处理为int),
对于双目运算符如+
-
*
/
类型检查的步骤是:
先确认两侧/单侧的表达式类型, 而后确认两侧表达式类型是否匹配,
最后确认当前的操做符可否对该类型进行操做所有没有问题的话, 才认为该表达式经过了检查,
并确认该表达式的值类型(在代码中表达式的结果值直接取双目运算符的左边)
(在这里, 咱们前面的toEnum()方法就派上了用场)
错误语法例子如:
10 + true
显然 +
两侧类型不匹配, 类型检查的给出的信息是,
Cva往后将遵循JVM规范, 基本运算能够是范围小于 int 的整形, 作运算时都强转为 int
如 byte char short 类型转为操做数都视为 int, 编译器不报错
Error: Line 1 Add Expr ression: the type of left is @int, but the type of right is @boolean
true < false
两侧类型是一致的, 但很显然, 这个比较没有任何意义, 所以这个表达式也是个错误
Error: Line 1 only numeric can be compared.
!200
只有布尔值才能取逻辑非, 所以这个表达式显然也是非法的
Error: Line 1 the Expr r cannot calculate to a boolean.
方法调用
Cva目前仅支持实例方法调用, 暂不支持静态方法、方法重载等.
对于方法调用表达式, 类型检查主要关注形参及实参.
首先确认形参和实参数量是相等的, 而后确认参数的类型是一一对应的.
经过检查后, 表达式的值类型被设定成为该方法的返回值类型.
假定本类中有有方法 int compute(int a, int b)
, 对它的两种错误调用以下
this.compute(10)
显然, 这并不能经过第一步: 对于参数个数的检查
Error: Line 1 the count of arguments is not match.
this.compute(10, false)
显然, 第二个参数的类型不是匹配的
Error: Line 32 the parameter 2 needs a int, but got a boolean
应当注意到, 在本程序中咱们认为write(控制台写操做, println, echo, printf)是一个语句,
(内置方法关键字)而不是函数调用表达式. 这样作的目的是为了简化该编译器开发,
但其本质依旧是函数调用, 所以对于它的类型检查等同函数调用. 写操做应检查参数是否为string 或者基本类型,
所以Expr expr最终应当能求得一个string 或者 整形(目前, 之后完善boolean和浮点数情形
返回表达式
这里是确认方法声明处的返回类型和实际的返回类型是匹配的. 首先检查 return
关键字后紧跟的表达式是否有意义,
而后再确认这个有意义的表达式是否符合返回类型. 考虑这样的一个源程序:
boolean doSomething() { // Some VarDecls // Some Statements return 666; }
很明显, 实际返回类型和声明的返回类型不一致, 咱们应当给出相应的错误提示
此外, 在Cva中, void返回类型的方法, 咱们容许不显式return, 也能够在方法结束时 使用return; 语句
Error: Line 3 the return Expr ression's type is not match the method "DoSomething" declared.
标识符 / 字面量 / this
/ new cvaIdentifierExpr r()
这里是讨论剩余的几类特殊的表达式, 变量/字段引用, 字面量, this关键字, 实例化对象表达式.
查找标识符大体分为两步, 首先在参数列表/本地变量表查找该标识符, 若查找失败, 再去类/基类字段声明列表中尝试查找.
若最终查找失败, 则报一个错.
Error: Line 1 you should declare "x" before use it.
字面量 / this
这个种类主要包括常量(数字整形字面量, true ,false), this
关键字. 应当特别注意this
关键字,
它指代当前类的实例, 它的类型天然是该关键字所处的类的类型.
应当注意到本程序不支持构造函数, 所以每一个类只有一个形式上的无参构造器. 除主类外,
对于其余普通类的声明顺序不作要求. 若是尝试实例化一个不存在的类, 也会报告错误.
`Error: Line 1 cannot find the declaration of class "XXX".`
有了前面表达式级的类型检查做为基础, 作语句的类型检查就很方便快捷了.
write
上文(writeExpr )已给出解释
if
/ while
/ for
对于这种类型的语句, 只须要检查是否符合对应的规则便可. 例如条件判断处必须是个布尔类型的表达式
{StatementList}
这种类型的表达式, 按顺序轮流进行检查便可
cvaIdentifierExpr r = Expr r;
赋值语句, 只要等号两侧的类型是互相匹配的, 那就容许赋值.
应当注意到, 咱们以前提到的一直是“类型匹配”, 而不是“类型相等”. 隐含的, 咱们容许合法的类型隐式转换.
在本程序里, 咱们还作了另外两个对于标识符的检查:变量/字段标识符(CvaIdentifierExpr expr),
类名标识符(也是identifier). 因为这两类标识符存在于不一样的符号表里面, 所以本程序能够声明相似
SomeThing SomeThing;
这样类型和名称同样的变量/字段,这种声明是合法的, 在使用时具体的意义取决于这个符号所在位置的语义.
前面已经提到, 在这个阶段, 咱们要在一遍扫描中给出尽量多的信息, 所以咱们须要实现错误恢复功能. 出现的具体错误和恢复思想以下
使用了未声明的变量
一旦发现某处使用了未使用的变量, 那么会当即给出信息提示此处发现一个未声明变量, 可是分析仍是要继续, 因而原地定义该变量是
一个 unkonwn
类型, 使用该类型, 进行接下来的分析.
操做符型表达式
例如 true + 10
, !200
这种类型的错误, 咱们优先考虑操做符的语义, 例如在咱们的程序中, 加减乘一定得出一个整数,
比较运算一定得出布尔类型的值. 咱们假定该操做符被正确使用并得出结果, 而后进行下面的分析.
方法调用
方法调用出现了问题, 例如参数不全、参数类型不匹配, 咱们给出应当提示用户的信息以后, 假定方法正常调用, 按照该有的返回值进行下面的分析.