04.从0实现一个JVM语言系列之语义分析器-Semantic
从0实现JVM语言之语义分析-Semantic
源码github, 如果这个系列文章对您有帮助, 希望获得您的一个star
本节相关语义分析package地址
致亲爱的读者:
个人的文字组织和写文章的功底属实一般, 写的也比较赶时间, 所以系列文章的文字可能比较粗糙,
难免有词不达意或者写的很迷惑抽象的地方
如果您看了有疑问或者觉得我写的实在乱七八糟, 这个很抱歉, 确实是我的问题, 您如果有不懂的地方
的地方或者发现我的错误(文字错误, 逻辑错误或者知识点错误都有可能), 可以直接留言, 我看到都会回复您!
系列食用方法建议
由于时间原因, 目前测试并不完善, 所以推荐如下方式根据您的目的进行阅读
如果您是学习用, 建议您先将整个项目clone到本地, 然后把感兴趣的章节删除, 自己重写对照着重写
书写完每一步测试一下能否正常运行(在指定的路径去读取源码测试能否编译成功并在命令行执行
java Application(类名)
尝试能否输出期望结果, 我没有研究Junit对编译器输出class文件进行测试, 所以目前可能需要您手动测试)
按照以上步骤, 等您将所有模块重写一遍, 大概也对这个系列的脉络有深刻理解了! 如果您重头开始重写,
往往可能由于出现某些低级错误导致长时间debug才找得到错误, 所以对于初学者, 推荐采用自己补写替换模块的
方式
对于希望贡献代码的朋友或者对Cva感兴趣的朋友, 欢迎贡献您的源码与见解, 或者对于该系列一些错误/
bug愿意提出指正的朋友, 您可以留言或者在github发issue, 我看到后一定及时处理!
语义分析器工作基于语法分析器输出的抽象语法树, 通过对该语法树的分析做进一步的检查,
回答我们, 也回答代码生成器最后一个问题:源程序是否符合语义规则. 如果确实存在问题,
那么这段代码即使翻译成机器码(在这里是我们后面要生成的JVM汇编指令), 也不可能执行成功,
必然收到JVM的拒绝执行(读者可以自己尝试瞎写命令然后用jasmin汇编成字节码, 看报错反应, 哈哈),
或者造成一些意想不到的问题, 因此语义分析有问题势必造成后面的错误, 所以语义分析旨在搜集
代码出现的逻辑错误(多数是类型检查的类型问题), 并指出, 以让接下来的步骤能正常进行
在这个阶段要给出尽可能准确的报错信息, 供用户参考并修改源代码.
语义分析中最重要的工作便是类型检查, 此外还会有一些其他的检查, 例如变量在使用前是否声明等.
语义分析实现
符号表
语义分析的正常进行少不了符号表的参与. 所谓符号, 程序中的变量、方法、字段、类都是符号.
符号表存储了程序中的符号的相关信息, 这些信息包括类型、作用域、访问控制信息等等, 而且符号表必须非常高效,
因为程序中符号的规模会非常大. 我们的符号表都是采用HashMap, JVM针对HashMap的优化可谓是比较极致,
我们的JDK8后引入了红黑树, 对于Map, HotSpot会倾向于将其编译成本地代码, 在一些情况下,
其运行效率甚至超过一般的手写分支判断
我们的哈希表以符号名字为键, 以符号的相关信息为值, 建立映射.
并按照树形结构, 组织各个作用域的符号及信息, 建立全局符号表.
全局符号表的大致结构如下:
// TODO: 树形图
+ ClassMap
+ ClassBinding
+ BaseClass
+ FieldMap
+ Field
+ ...
+ MethodMap
+ Method
+ ...
+ ...
+ MethodVariableMap
+ ClassMap
全局符号表的入口, 直接维护了类名和类的相关信息的
+ ClassBinding
存储了类的相关信息, 包括父类, 字段表, 方法表. 其中字段表和方法表是以名为键的映射.
+ Field
存储了字段的声明类型
+ Method
存储了方法的相关信息, 包括声明的返回类型, 参数的个数及各自类型.
+ MethodVariableMap
参数和本地变量表, 是名字和类型的映射. 当分析某个方法的时候, 对于该方法的参数和本地变量的访问是相当频繁的,
因此将他们单独存储到某个位置, 分析完毕即销毁, 优化空间的占用.
分析思路
本质上说, 所谓“分析”仅仅是语法树的遍历而已, 只是附带上了附加条件, 要求某子树符合某个要求. 此外我们应当注意到,
类型的声明、字段的声明和方法的声明, 并没有任何值得语义分析的地方, 真正值得我们去分析和检查的是语句和表达式,
查看它们是否合法. 因此, 分析过程可简要分成两步.
信息收集和索引
对当前语法树的前几层进行遍历, 扫描并存储类信息, 各自的字段和方法相关信息, 构建全局随时可用的“全局符号表”,
该过程仅在语义分析实际进行之前进行一次.语义分析和检查
收集要分析的方法的参数和变量信息, 然后顺序遍历方法的每一条语句和表达式. 如果找到错误, 那么就输出一条信息,
提示用户在某位置发现何种类型的错误, 并尽可能进行恢复, 继续检查下文, 在一趟分析中给出尽可能多的错误信息.
对于每个方法, 都执行一遍步骤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
这种类型的错误, 我们优先考虑操作符的语义, 例如在我们的程序中, 加减乘必定得出一个整数,
比较运算必定得出布尔类型的值. 我们假定该操作符被正确使用并得出结果, 然后进行下面的分析.方法调用
方法调用出现了问题, 例如参数不全、参数类型不匹配, 我们给出应当提示用户的信息之后, 假定方法正常调用, 按照该有的返回值进行下面的分析.
04.从0实现一个JVM语言系列之语义分析器-Semantic的更多相关文章
- 03.从0实现一个JVM语言系列之语法分析器-Parser-03月01日更新
从0实现JVM语言之语法分析器-Parser 相较于之前有较大更新, 老朋友们可以复盘或者针对bug留言, 我会看到之后答复您! 源码github仓库, 如果这个系列文章对你有帮助, 希望获得你的一个 ...
- 00.从0实现一个JVM语言系列
00.一个JVM语言的诞生 由于方才才获悉博客园文章默认不放在首页的, 原创文章主要通过随笔显示, 所以将文章迁移到随笔; 这篇帖子将后续更新, 欢迎关注! 这段时间要忙着春招实习, 所以项目更新会慢 ...
- 01.从0实现一个JVM语言之架构总览
00.一个JVM语言的诞生过程 文章集合以及项目展望 源码github地址 这一篇将是架构总览, 将自顶向下地叙述自制编译器的要素; 文章目录 01.从0实现一个JVM语言之架构总览 架构总览目前完成 ...
- 02.从0实现一个JVM语言之词法分析器-Lexer-03月02日更新
从0实现JVM语言之词法分析器-Lexer 本次有较大幅度更新, 老读者如果对前面的一些bug, 错误有疑问可以复盘或者留言. 源码github仓库, 如果这个系列文章对你有帮助, 希望获得你的一个s ...
- 05.从0实现一个JVM语言之目标平台代码生成-CodeGenerator
从0实现JVM语言之目标平台代码生成-CodeGenerator 源码github仓库, 如果这个系列文章对你有帮助, 希望获得你的一个star 本节相关代码生成package地址 阶段性的告别 非常 ...
- JVM基础系列第1讲:Java 语言的前世今生
Java 语言是一门存在了 20 多年的语言,其年纪比我自己还大.虽然存在了这么长时间,但 Java 至今都是最大的工业级语言,许多大型互联网公司均采用 Java 来实现其业务系统.大到国际电商巨头阿 ...
- 用SignalR 2.0开发客服系统[系列5:使用SignalR的中文简体语言包和其他技术点]
前言 交流群:195866844 目录: 用SignalR 2.0开发客服系统[系列1:实现群发通讯] 用SignalR 2.0开发客服系统[系列2:实现聊天室] 用SignalR 2.0开发客服系统 ...
- JavaRebel 2.0 发布,一个JVM插件
JavaRebel是一个JVM插件(-javaagent),能够即时重载java class更改,因此不需要重新部署一个应用或者重启容器,节约开发者时间. JavaRebel 2.0的新特征: 改变了 ...
- JVM基础系列第15讲:JDK性能监控命令
查看虚拟机进程:jps 命令 jps 命令可以列出所有的 Java 进程.如果 jps 不加任何参数,可以列出 Java 程序的进程 ID 以及 Main 函数短名称,如下所示. $ jps 6540 ...
随机推荐
- 2019牛客暑期多校训练营(第七场)H.Pair(数位dp)
题意:给你三个数A,B,C 现在要你找到满足 A and B >C 或者 A 异或 B < C 的对数. 思路:我们可以走对立面 把既满足 A and B <= C 也满足 A 异 ...
- 利用github+hexo搭建的博客
用github+hexo新建了一个博客,欢迎来访,如果想要搭建类似框架的博客,可以联系我. 新博客地址:只为自由书写的博客
- 【noi 2.6_9265】取数游戏(DP)
题意:从自然数1到N中不取相邻2数地取走任意个数,问方案数. 解法:f[i][1]表示在前i个数中选了第i个的方案数,f[i][0]表示没有选第i个.f[i][1]=f[i-1][0]; f[i][ ...
- 西南民族大学第十二届程序设计竞赛(同步赛) A.逃出机房 (bfs)
题意:有来两个人A和B,A追B,A和B每次向上下左右移动一个单位,一共有两扇门,问A是否可以追上B(在门口追上也算合法). 题解:当时看题意说在门口也算?就觉得是判断两个人到门口的时间,对他们两个人分 ...
- Pollard_rho算法进行质因素分解
Pollard_rho算法进行质因素分解要依赖于Miller_Rabbin算法判断大素数,没有学过的可以看一下,也可以当成模板来用 讲一下Pollard_rho算法思想: 求n的质因子的基本过程是,先 ...
- 【一天一个基础系列】- java之泛型篇
简介 说起各种高级语言,不得不谈泛型,当我们在使用java集合的时候,会发现集合有个缺点:把一个对象"丢进"集合之后,集合就会"忘记"这个对象的数据类型,当再次 ...
- Django用户注册、登录
一.用户注册 1 ''' 2 注册的表单模型 3 forms.py 的例子 4 ''' 5 6 from django import forms #表单功能 7 from django.contrib ...
- kubernetes实战-配置中心(三)配置服务使用apollo配置中心
使用配置中心,需要开发对代码进行调整,将一些配置,通过变量的形式配置到apollo中,服务通过配置中心来获取具体的配置 在配置中心修改新增如下配置: 项目信息: 配置: 重新打包镜像,使用apollo ...
- VXLAN学习之路-结合VRF在Linux中实践VXLAN网络
一.概述 近期在在搞网络安全HCIE.CISP的认证的事,顺便将VXLAN技术再次系统的学习一下,学习过程中看到云原生实验室里的一篇文章,就是关于VXLAN在Linux系统中的实践,感觉文章写得很好, ...
- ERROR 1045 (28000): Access denied for user 'ODBC'@'localhost' (using password: NO)
cmd mysql -h localhost -u root -p r然后报错 ERROR 1045 (28000): Access denied for user 'ODBC'@'localhost ...