从0实现JVM语言之词法分析器-Lexer

本次有较大幅度更新, 老读者如果对前面的一些bug, 错误有疑问可以复盘或者留言.

源码github仓库, 如果这个系列文章对你有帮助, 希望获得你的一个star

本节相关词法分析package地址

致亲爱的读者:

    个人的文字组织和写文章的功底属实一般, 写的也比较赶时间, 所以系列文章的文字可能比较粗糙,
难免有词不达意或者写的很迷惑抽象的地方 如果您看了有疑问或者觉得我写的实在乱七八糟, 这个很抱歉, 确实是我的问题, 您如果有不懂的地方
的地方或者发现我的错误(文字错误, 逻辑错误或者知识点错误都有可能), 可以直接留言, 我看到都会回复您!

本节提纲

  1. 引言

  2. 存储符号定义以及信息的类

  3. 词法分析器实现过程走代码详解

  4. 样例输出分析

引言

编译源码的第一个步骤是进行词法分析, 词法分析器读入源代码的字符流,

将他们识别成Token流, 如

num++;

经过词法分析

=> identifier + increment + semi (标识符 * 1, 自增 * 1, 分号 * 1)

大家可以看到, 语法分析器做的事情非常简单, 就是将源码与Token一一对应, 类似建立一个映射关系,

我们这里每一个Token都new了一个对象, 这样的做法并不高效

(个人认为高效的方法是用枚举取代一些字面量固定的值, 主要是符号如+, -等),

但是可以保存行号信息, 以便之后向用户提供足够的报错信息.

得到词法分析的结果后(在这里是和语法分析器配合每次读一个Token并前看,

而不是一次性读完再转交语法分析器处理),词法分析器将其输出交由语法分析器Parser处理

Cva符号简介

  • Cva目前支持的运算符有:

    基本运算 + - * / % && = . ! <

    自增运算 ++ --

    以及位操作运算 & | ^ >> << >>> 等其他运算符

    域限定符 { } ( ) ;``,
  • 支持解析语法糖 += -= *= /= &= |= ^= >>= <<= >>>= 等赋值运算符,

    详可见EnumCvaToken类
  • 打算在将来支持的有 三目运算符? :, == ||
  • 其中
    • ! ~ ++ -- 是单目操作符, unary, 目前 i++ ++i 还不区分压栈帧和自增顺序
    • : ? 属于三目操作符的一部分
    • 除了一元运算符其他都是双目运算符, binary
    • .圆点运算符调用方法, 目前还没有做对象的属性调用
    • { } ( ) 是定界符
  • Cva定义的关键字有:

    void byte short char int long float double boolean string

    class new if else while true false this return 等, 还有部分保留字, 是希望在未来实现的

    • void byte short char int long float double boolean string 是类型声明,
    • class 声明一个类型时使用的前导关键字
    • extends 是继承声明, 目前的继承很弱鸡, 很多功能没有实现
    • new 申请内存分配给新对象, 或者数组, 目前数组还不支持
    • if else while for 分支/循环结构关键字, 目前还不支持switch case
    • true false 上文所述 boolean 类型的两字面量
    • this 当前对象实例指针
    • return 返回语句的前导关键字, 目前只做了最后一句返回, 后期希望实现方法中的返回
  • 特别说明: Identifier ConstInt comment space \n \r EOF println/echo
    • Identifier 标识符, 将类型/变量名等处理成一个标识符类型的记号, 具体意义取决于所处的位置
    • 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. 初始化源文件的按字符输入流, 并读入一个字符, 初始化全局行号为1,

    然后循环进行后面的所有步骤

  2. 跳转到handleWhiteSpace方法检查是否是空白符(空格, 换行符, 回车符), 如果是, 直接尝试读下一个字符, 但如果发生换行, 行号++

  3. 读取符号, 会switch之, 如果落在前缀字符的区间内, 前缀字符即某个运算符的前缀, 如 +++, += 的前缀,

    这样的字符在词法分析器中都有其特殊处理方法, 在这里面, 也包含了对注释的处理,

    会忽略Cva行注释// ... \n 与块注释/*... */

    编译器后端的任务很重, 所以在前端, 我们应尽量确定变量的类型, 删除无关的注释, 空符, 好给后端腾出

  4. 同时这里要注意, 我们在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

  5. 进入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, 否则会进入下一步

  6. 当前的字符不是我们支持的特殊字符, 那就一直读取下去, 直到空格符/换行符/回车符/文件结束符/注释,

    这样是尽可能找到了一个最长的序列.

    首先检查这个序列是否是我们定义的关键字之一, 这个过程也是查我们前述的哈希表,

    如果是关键字, 那么就返回正确类型和行号的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日更新的更多相关文章

  1. 01.从0实现一个JVM语言之架构总览

    00.一个JVM语言的诞生过程 文章集合以及项目展望 源码github地址 这一篇将是架构总览, 将自顶向下地叙述自制编译器的要素; 文章目录 01.从0实现一个JVM语言之架构总览 架构总览目前完成 ...

  2. 00.从0实现一个JVM语言系列

    00.一个JVM语言的诞生 由于方才才获悉博客园文章默认不放在首页的, 原创文章主要通过随笔显示, 所以将文章迁移到随笔; 这篇帖子将后续更新, 欢迎关注! 这段时间要忙着春招实习, 所以项目更新会慢 ...

  3. 05.从0实现一个JVM语言之目标平台代码生成-CodeGenerator

    从0实现JVM语言之目标平台代码生成-CodeGenerator 源码github仓库, 如果这个系列文章对你有帮助, 希望获得你的一个star 本节相关代码生成package地址 阶段性的告别 非常 ...

  4. 04.从0实现一个JVM语言系列之语义分析器-Semantic

    从0实现JVM语言之语义分析-Semantic 源码github, 如果这个系列文章对您有帮助, 希望获得您的一个star 本节相关语义分析package地址 致亲爱的读者: 个人的文字组织和写文章的 ...

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

    从0实现JVM语言之语法分析器-Parser 相较于之前有较大更新, 老朋友们可以复盘或者针对bug留言, 我会看到之后答复您! 源码github仓库, 如果这个系列文章对你有帮助, 希望获得你的一个 ...

  6. BabelMap 9.0.0.3 汉化版(2016年12月27日更新)

    软件简介 BabelMap 是一个免费的字体映射表工具,可辅助使用<汉字速查>程序. 该软件可使用系统上安装的所有字体浏览 Unicode 中的十万个字符,还带有拼音及部首检字法,适合文献 ...

  7. BabelMap 12.0.0.1 汉化版(2019年3月11日更新)

    软件简介 BabelMap 是一个免费的字体映射表工具,可辅助使用<汉字速查>程序. 该软件可使用系统上安装的所有字体浏览 Unicode 中的十万个字符,还带有拼音及部首检字法,适合文献 ...

  8. 批量添加删除Windows server DNS服务 恶意域名 * A记录 指向 127.0.0.1(2019年6月5日更新)

    下载链接:https://pan.baidu.com/s/1OUHyvnIfXYF0PdiT-VRyHw  密码:7gjj 注意!本解决方案在本地的Windows server服务器上把恶意域名指向1 ...

  9. SudaMod-81.0 / crDroidAndroid-8.1(android-8.1.0_r20)红米3 2018年5月3日更新

    一.写在前面 我只是个人爱好,本ROM未集成任何第三方推广软件,我只是喜欢把好的资源分享出来,若可以,我们一起学习,一起进步. 请不要问我怎么刷机! 请不要问我玩游戏卡不卡(有钱你就换好点的手机)! ...

随机推荐

  1. 记一次由sql注入到拿下域控的渗透测试实战演练(下)

    本次渗透使用工具: mimikatz,PsExec 本次渗透网络拓扑图: 开始渗透: 上一篇文章中我们讲到我们成功渗透了目标内网中web服务器并提取其管理员明文密码,通过明文密码又成功登陆了内网中的另 ...

  2. P4755 Beautiful Pair (分治 + 主席树)

    题意:1e5的数组 计算有多少对 ai * aj <= max(ai ai+1...aj-1 aj) 题解:在处理这种涉及到区间极值的题时 好像是个套路分治 从级值中间分成两个区间 从区间短的那 ...

  3. A. Crazy Town

    Crazy Town is a plane on which there are n infinite line roads. Each road is defined by the equation ...

  4. java——接口、多态性、对象转型

    接口定义:  默认方法: 默认方法的作用: 如果在你的接口已经投入使用了,这个时候你想要在接口里面加一个方法,这个时候如果你加一个抽象方法的话,所有实现类都要改变源代码(因为实现类要把接口中的所有抽象 ...

  5. 抓取QQ音乐歌单

    抓取QQ音乐歌单1.通过分析歌曲下载路径来分析所需参数: 通过比较, 得出其中歌曲下载url与参数vkey是可变的,歌曲下载url中可变得值是请求歌单返回的歌曲数据的strMediaMid参数, 而v ...

  6. Git 初始化及仓库创建及操作

    一.基本信息设置 1.初始化设置用户名 2.初始化设置用户名邮箱 备注:该设置在Github仓库主页显示谁提交了该文件. 二.初始化一个新的Git仓库 1.创建文件夹 mkdir test 2.在文件 ...

  7. K8S(10)配置中心实战-configmap资源

    k8s配置中心实战-configmap资源 目录 k8s配置中心实战-configmap资源 0 configmap前置说明 0.1.1 configmap和secret 0.1.2 怎么使用conf ...

  8. XCTF攻防世界web进阶练习—mfw

    XCTF攻防世界web进阶练习-mfw题目为mfw,没有任何提示.直接打开题目,是一个网站 大概浏览一下其中的内容,看到其中url变化其实只是get的参数的变化查看它的源码,看到有一个?page=fl ...

  9. js 检测屏幕分辨率

    js 检测屏幕分辨率 class screenChecker { constructor() { this.screen = window.screen; this.fullscreen = fals ...

  10. Parcel all in one

    Parcel all in one Parcel https://parceljs.org/ # cli $ yarn global add parcel-bundler $ npm install ...