02.从0实现一个JVM语言之词法分析器-Lexer-03月02日更新
从0实现JVM语言之词法分析器-Lexer
本次有较大幅度更新, 老读者如果对前面的一些bug, 错误有疑问可以复盘或者留言.
源码github仓库, 如果这个系列文章对你有帮助, 希望获得你的一个star
本节相关词法分析package地址
致亲爱的读者:
个人的文字组织和写文章的功底属实一般, 写的也比较赶时间, 所以系列文章的文字可能比较粗糙,
难免有词不达意或者写的很迷惑抽象的地方
如果您看了有疑问或者觉得我写的实在乱七八糟, 这个很抱歉, 确实是我的问题, 您如果有不懂的地方
的地方或者发现我的错误(文字错误, 逻辑错误或者知识点错误都有可能), 可以直接留言, 我看到都会回复您!
本节提纲
引言
存储符号定义以及信息的类
词法分析器实现过程走代码详解
样例输出分析
引言
编译源码的第一个步骤是进行词法分析, 词法分析器读入源代码的字符流,
将他们识别成Token流, 如
num++;
经过词法分析
=> identifier + increment + semi (标识符 * 1, 自增 * 1, 分号 * 1)
大家可以看到, 语法分析器做的事情非常简单, 就是将源码与Token一一对应, 类似建立一个映射关系,
我们这里每一个Token都new了一个对象, 这样的做法并不高效
(个人认为高效的方法是用枚举取代一些字面量固定的值, 主要是符号如+, -等),
但是可以保存行号信息, 以便之后向用户提供足够的报错信息.
得到词法分析的结果后(在这里是和语法分析器配合每次读一个Token并前看,
而不是一次性读完再转交语法分析器处理),词法分析器将其输出交由语法分析器Parser处理
Cva符号简介
- Cva目前支持的运算符有:
基本运算+-*/%&&=.!<
自增运算++--
以及位操作运算&|^>><<>>>等其他运算符
域限定符{}();``, - 支持解析语法糖
+=-=*=/=&=|=^=>>=<<=>>>=等赋值运算符,
详可见EnumCvaToken类 - 打算在将来支持的有 三目运算符
?:,==|| - 其中
!~++--是单目操作符, unary, 目前i++++i还不区分压栈帧和自增顺序:?属于三目操作符的一部分- 除了一元运算符其他都是双目运算符, binary
.圆点运算符调用方法, 目前还没有做对象的属性调用{}()是定界符
- Cva定义的关键字有:
voidbyteshortcharintlongfloatdoublebooleanstring
classnewifelsewhiletruefalsethisreturn等, 还有部分保留字, 是希望在未来实现的voidbyteshortcharintlongfloatdoublebooleanstring是类型声明,class声明一个类型时使用的前导关键字extends是继承声明, 目前的继承很弱鸡, 很多功能没有实现new申请内存分配给新对象, 或者数组, 目前数组还不支持ifelsewhilefor分支/循环结构关键字, 目前还不支持switch casetruefalse上文所述boolean类型的两字面量this当前对象实例指针return返回语句的前导关键字, 目前只做了最后一句返回, 后期希望实现方法中的返回
- 特别说明:
IdentifierConstIntcommentspace\n\rEOFprintln/echoIdentifier标识符, 将类型/变量名等处理成一个标识符类型的记号, 具体意义取决于所处的位置ConstInt整数常数字面量, 其他类型同space\n\r分别是空格符 换行符 回车符, 处理源代码文件时将忽略它们// comment行注释/* comment */块注释EOF是源代码的文件结束符println&&echo输出语句,printf的支持打算在后期的版本更新
存储符号定义以及信息的类
按照我们规定的程序文法, 能给出记号的定义
EnumCvaToken类
package cn.misection.cvac.lexer;
import java.util.EnumSet;
import java.util.HashMap;
import java.util.Map;
public enum EnumCvaToken
{
/**
* +
*/
ADD,
/**
* -
*/
SUB,
/**
* *
*/
STAR,
/**
* /
*/
DIV,
/**
* 求余;
*/
REM,
/**
* &
*/
BIT_AND,
/**
* |
*/
BIT_OR,
/**
* ...更多的在这里略去, 可见源码中
*/
;
}
CvaToken类
package cn.misection.cvac.lexer;
/**
* @author MI6 root
*/
public final class CvaToken
{
/**
* the kind of the token
*/
private final EnumCvaToken enumToken;
/**
* extra literal of the token
*/
private String literal;
/**
* the line number of the token
*/
private final int lineNum;
public CvaToken(EnumCvaToken enumToken, int lineNum)
{
this.enumToken = enumToken;
this.lineNum = lineNum;
}
public CvaToken(EnumCvaToken enumToken, int lineNum, String literal)
{
this.enumToken = enumToken;
this.lineNum = lineNum;
this.literal = literal;
}
@Override
public String toString()
{
return String.format("Token {%s literal: %s : at line %d}",
this.enumToken.toString(),
literal == null ? "null" : this.literal,
this.lineNum);
}
public EnumCvaToken toEnum()
{
return enumToken;
}
public String getLiteral()
{
return literal;
}
public int getLineNum()
{
return lineNum;
}
}
其中 enumToken 字段标记了该记号的类型(即以上的EnumCvaToken枚举类),
若有值, 例如数字或者字符串或者Identifier, 将在 literal 字段中给出当前记号的值
此外, 还会在 lineNum 字段给出当前记号的行号, 主要是报错环节给出尽可能定位准确的错误信息
词法分析器实现过程走代码详解
词法分析部分采用了手动实现的方式, 大致步骤如下, 大家可以参照lexer package中的Lexer类查看过程,
当然, 这一切还要借助io包内的文件流将文件读入成StringBuffer(姑且把它当做buffer吧)
private CvaToken lex()
{
char ch = stream.poll();
// skip all kinds of blanks
ch = handleWhiteSpace(ch);
switch (ch)
{
case LexerCommon.EOF:
return new CvaToken(EnumCvaToken.EOF, lineNum);
case '+':
return handlePlus();
case '-':
return handleMinus();
case '*':
return handleStar();
case '&':
return handleAnd();
case '|':
return handleOr();
case '=':
return handleEqual();
case '<':
return handleLessThan();
case '>':
return handleMoreThan();
case '^':
return handleXOr();
case '~':
return handleBitNegate();
case '/':
return handleSlash();
case '%':
return handlePercent();
case '\'':
return handleApostrophe();
case '"':
return handleDoubleQuotes();
default:
return handleNorPrefOrIdOrNum(ch);
}
}
每当外部调用lexer的nextToken()方法(主要是Parser调用), 都会进行如下流程:
初始化源文件的按字符输入流, 并读入一个字符, 初始化全局行号为1,
然后循环进行后面的所有步骤跳转到handleWhiteSpace方法检查是否是空白符(空格, 换行符, 回车符), 如果是, 直接尝试读下一个字符, 但如果发生换行, 行号++
读取符号, 会switch之, 如果落在前缀字符的区间内, 前缀字符即某个运算符的前缀, 如
+是++,+=的前缀,
这样的字符在词法分析器中都有其特殊处理方法, 在这里面, 也包含了对注释的处理,
会忽略Cva行注释// ... \n 与块注释/*... */
编译器后端的任务很重, 所以在前端, 我们应尽量确定变量的类型, 删除无关的注释, 空符, 好给后端腾出同时这里要注意, 我们在Lexer的lex()方法中, 查看当前的字符是否为case块中的字符, 例如
+,-,*, 等等, 如果是, 那就跳转到处理他们的方法中
,为什么要跳转方法? 因为他们是前缀字符, 即分析器看到了+, 并不能确定这是一个加号, 要和其后面的字符一起看
后面的字符可能是+, 那么该符号应该是++, 自增符号, 如果后一个连续的的符号是=, 那么这个符号应该是+=
否则, 我们才返回+, 其他的同理, 每次lex()方法被调用, 词法分析器能返回一个类型符合的, 携带行号信息的记号,
如, 遇到+号:private CvaToken handlePlus()
{
if (stream.hasNext())
{
switch (stream.peek())
{
case '+':
{
// 截取两个;
stream.poll();
return new CvaToken(EnumCvaToken.INCREMENT, lineNum);
}
case '=':
{
stream.poll();
return new CvaToken(EnumCvaToken.ADD_ASSIGN, lineNum);
}
default:
{
break;
}
}
}
return new CvaToken(EnumCvaToken.ADD, lineNum);
}
我们这里把stream当做队列, 每次poll()会弹出队列首的字符, 这个首字符就是我们peek()到的字符.
如果遇到的不是这些前缀字符, 我们会到进入default分支中, 执行handleNorPrefOrIdOrNum()方法private CvaToken handleNorPrefOrIdOrNum(char ch)
{
// 先看c是否是非前缀字符, 这里是 int, 必须先转成char看在不在表中;
if (EnumCvaToken.containsKind(String.valueOf(ch)))
{
return new CvaToken(EnumCvaToken.selectReverse(String.valueOf(ch)), lineNum);
}
StringBuilder builder = new StringBuilder();
builder.append(ch);
while (true)
{
ch = stream.peek();
// Cva命名容许_和$符号;
if (ch != LexerCommon.EOF
&& !Character.isWhitespace(ch)
&& !isSpecialCharacter(ch))
{
builder.append(ch);
this.stream.poll();
continue;
}
break;
}
String literal = builder.toString();
// 关键字;
if (EnumCvaToken.containsKind(literal))
{
return new CvaToken(EnumCvaToken.selectReverse(literal), lineNum);
}
else
{
if (isNumber(literal))
{
// FIXME 自动机;
if (isInt(literal))
{
return new CvaToken(EnumCvaToken.CONST_INT, lineNum, builder.toString());
}
}
else if (isIdentifier(literal))
{
return new CvaToken(EnumCvaToken.IDENTIFIER, lineNum, builder.toString());
}
else
{
errorLog("identifier or number which can only include alphabet, number or _, $",
"an illegal identifier with illegal char");
}
}
return null;
}
// 请注意, 一般不要return null, 尽量抛出异常或者返回空对象, 这里是因为根本走不到这个分支, 才 return null
进入default分支的方法首先会查表, 这个Hash表是EnumCvaToken中的利用Enum的literal字面量属性反查Enum类型建的表,
这个表的key是Token的字面量, value是该Token的枚举, 在Enum 中构建如下private static final Map<String, EnumCvaToken> lookup = new HashMap<>(); static
{
for (EnumCvaToken kind : EnumSet.allOf(EnumCvaToken.class))
{
if (kind.kindLiteral != null)
{
lookup.put(kind.kindLiteral, kind);
}
}
} public static boolean containsKind(String literal)
{
return lookup.containsKey(literal);
}
// 以上是常见的建立枚举反查表的方法, 如果有看不懂的地方欢迎留言, 我看到会回复.
在这一步, 如果遇到{,}这类非前缀字符(即不会产生歧义, 单字符只能单独出现在代码中),
直接返回该Token, 否则会进入下一步当前的字符不是我们支持的特殊字符, 那就一直读取下去, 直到空格符/换行符/回车符/文件结束符/注释,
这样是尽可能找到了一个最长的序列.首先检查这个序列是否是我们定义的关键字之一, 这个过程也是查我们前述的哈希表,
如果是关键字, 那么就返回正确类型和行号的Token;如果不是关键字, 那么查看这个序列是不是一串数字, 如果是数字, 那么返回一个类型为
ConstInt
(目前还没有做自动机识别浮点数, 后期有空了会做正确的行号和数字串的记号;如果也不是数字串, 那么检查是否符合程序对于标识符的规定, 如果符合,
那么返回一个类型为Identifier(一般是用户自定义的变量常量名/类名/方法名),
正确行号和标识符串的记号, Cva的变量名如同Java也需要字母开头/下划线或者不常见美元符号开头,
如果不符合, 那么只能是一个错误, 给出提示并结束分析.
样例输出分析
例如, 对于语句样例
echo "hello, world!\n";
println new Increment().incre();
echo "2 * 3 = ";
println 2 * 3; // 目前暂时不支持 printf;
词法分析后的Token流是
{Token {WRITE literal: null : at line 93}}
{Token {STRING literal: hello, world! : at line 93}}
{Token {SEMI literal: null : at line 93}}
{Token {WRITE_LINE literal: null : at line 94}}
{Token {NEW literal: null : at line 94}}
{Token {IDENTIFIER literal: Increment : at line 94}}
{Token {OPEN_PAREN literal: null : at line 94}}
{Token {CLOSE_PAREN literal: null : at line 94}}
{Token {DOT literal: null : at line 94}}
{Token {IDENTIFIER literal: incre : at line 94}}
{Token {OPEN_PAREN literal: null : at line 94}}
{Token {CLOSE_PAREN literal: null : at line 94}}
{Token {SEMI literal: null : at line 94}}
{Token {WRITE literal: null : at line 95}}
{Token {STRING literal: 2 * 3 = : at line 95}}
{Token {SEMI literal: null : at line 95}}
可以看到对于空白符和注释, 在词法分析阶段我们就进行了跳过, 源代码文件流就会转换成这样的Token流供语法分析器处理
以上结果应该不难理解, 如果朋友们有任何疑问, 欢迎留言
02.从0实现一个JVM语言之词法分析器-Lexer-03月02日更新的更多相关文章
- 01.从0实现一个JVM语言之架构总览
00.一个JVM语言的诞生过程 文章集合以及项目展望 源码github地址 这一篇将是架构总览, 将自顶向下地叙述自制编译器的要素; 文章目录 01.从0实现一个JVM语言之架构总览 架构总览目前完成 ...
- 00.从0实现一个JVM语言系列
00.一个JVM语言的诞生 由于方才才获悉博客园文章默认不放在首页的, 原创文章主要通过随笔显示, 所以将文章迁移到随笔; 这篇帖子将后续更新, 欢迎关注! 这段时间要忙着春招实习, 所以项目更新会慢 ...
- 05.从0实现一个JVM语言之目标平台代码生成-CodeGenerator
从0实现JVM语言之目标平台代码生成-CodeGenerator 源码github仓库, 如果这个系列文章对你有帮助, 希望获得你的一个star 本节相关代码生成package地址 阶段性的告别 非常 ...
- 04.从0实现一个JVM语言系列之语义分析器-Semantic
从0实现JVM语言之语义分析-Semantic 源码github, 如果这个系列文章对您有帮助, 希望获得您的一个star 本节相关语义分析package地址 致亲爱的读者: 个人的文字组织和写文章的 ...
- 03.从0实现一个JVM语言系列之语法分析器-Parser-03月01日更新
从0实现JVM语言之语法分析器-Parser 相较于之前有较大更新, 老朋友们可以复盘或者针对bug留言, 我会看到之后答复您! 源码github仓库, 如果这个系列文章对你有帮助, 希望获得你的一个 ...
- BabelMap 9.0.0.3 汉化版(2016年12月27日更新)
软件简介 BabelMap 是一个免费的字体映射表工具,可辅助使用<汉字速查>程序. 该软件可使用系统上安装的所有字体浏览 Unicode 中的十万个字符,还带有拼音及部首检字法,适合文献 ...
- BabelMap 12.0.0.1 汉化版(2019年3月11日更新)
软件简介 BabelMap 是一个免费的字体映射表工具,可辅助使用<汉字速查>程序. 该软件可使用系统上安装的所有字体浏览 Unicode 中的十万个字符,还带有拼音及部首检字法,适合文献 ...
- 批量添加删除Windows server DNS服务 恶意域名 * A记录 指向 127.0.0.1(2019年6月5日更新)
下载链接:https://pan.baidu.com/s/1OUHyvnIfXYF0PdiT-VRyHw 密码:7gjj 注意!本解决方案在本地的Windows server服务器上把恶意域名指向1 ...
- SudaMod-81.0 / crDroidAndroid-8.1(android-8.1.0_r20)红米3 2018年5月3日更新
一.写在前面 我只是个人爱好,本ROM未集成任何第三方推广软件,我只是喜欢把好的资源分享出来,若可以,我们一起学习,一起进步. 请不要问我怎么刷机! 请不要问我玩游戏卡不卡(有钱你就换好点的手机)! ...
随机推荐
- SCZ 20170812 T2 MFS
题面照例十分暴力,我再次重写一下吧-- 题目描述 有\(n\)个数构成的数列\(A\)元素为\(a_i\),你要构造一个数列\(B\),元素为\(b_i\),使得满足\(b_{i}>0,a_{i ...
- POJ - 2406 Power Strings (后缀数组DC3版)
题意:求最小循环节循环的次数. 题解:这个题其实可以直接用kmp去求最小循环节,然后在用总长度除以循环节.但是因为在练后缀数组,所以写的后缀数组版本.用倍增法会超时!!所以改用DC3法.对后缀数组还不 ...
- java.awt.event.MouseEvent鼠标事件的定义和使用 以及 Java Swing-JTextArea的使用
最近发现一个CSDN大佬写的Java-Swing全部组件的介绍:Java Swing 图形界面开发(目录) JTextArea 文本区域.JTextArea 用来编辑多行的文本.JTextArea 除 ...
- Java 并发机制底层实现 —— volatile 原理、synchronize 锁优化机制
本书部分摘自<Java 并发编程的艺术> 概述 相信大家都很熟悉如何使用 Java 编写处理并发的代码,也知道 Java 代码在编译后变成 Class 字节码,字节码被类加载器加载到 JV ...
- python = 赋值顺序 && C++ side effect
title: python = 赋值顺序 && C++ side effect date: 2020-03-17 15:00:00 categories: [python][c++] ...
- bnuoj-53073 萌萌哒身高差 【数学】【非原创】
"清明时节雨纷纷,路上行人欲断魂." 然而wfy同学的心情是愉快的,因为BNU ACM队出去春游啦!并且,嗯... 以下是wfy同学的日记: 昨天,何老师告诉我们:明天我们去春游, ...
- 利用FFmpeg 将 rtsp 获取H264裸流并保存到文件中
既然已经可以通过 RTSP 获取h264 裸流了.那么通过 FFmpeg 将其保存到文件中怎么做呢? 一.首先RTSP获取 h264 裸流 我们上面两篇文章主要讲的是通过 rtsp://Your ip ...
- Netty(三)基于Bio和Netty 的简易版Tomcat
参考代码: https://github.com/FLGBetter/tomcat-rpc-demo
- Shell 编程快速上手
Shell 编程快速上手 test.sh #!/bin/sh cd ~ mkdir shell_tut cd shell_tut for ((i=0; i<10; i++)); do touch ...
- <U+200B> for, Zero Width Space ❌
<U+200B> for, Zero Width Space zsh, bash https://www.cnblogs.com/xgqfrms/p/14233264.html#47944 ...