从零开始开发JVM语言(二)词法分析
2016-06-01 00:00
218 查看
摘要: 编译器 JVM语言 词法分析
目录戳这里
词法分析的工作是,将输入的字符串转化为结构清晰的
通常来说,这个
##词
什么是词呢?例如,输入串如下:
从直觉上(语法高亮也帮助了我们)可以看出,它由如下几部分组成
我们不会把
“词”表示用于解析的最小单元。词的规定很宽泛,像emoji语言,它的一个词很可能就是一个emoji图像(虽然我没研究过不过大概如此吧。。)而对于主流的编程语言,词通常为一串字母,数字,某些特殊符号(运算符)
注:如下方式的原理和自动机一致,但是性能没有自动机高。好处在于手写实现几乎不可能出错。
在
如下的符号
被视为“只要遇到便需要分割的”符号,我把它们称做“分隔符”。什么意思呢?
观察
它写成这些形式都应当是同一种含义
虽然后两个看上去不太好看,但其含义不应与第一种相异
它为何会被分割为那几个小块?很明显,空格是一个“分隔符”,在空格之前的视为一个Token,在空格之后的视为一个Token。而空格本身,比较特殊,它不但是“分隔符”,还是“不需要被记录的分隔符”,将会被忽略。前后token将直接串联起来
而
匹配串很简单,直接使用
例如
首先匹配的应当为
将优先匹配
所以得出一个比较通用的结论: 最优匹配项为 1.下标最靠前 2.下标相同时长度最长
在java中也非常容易实现。首先将所有符号放到一个集合中,然后进行长度从大到小的排序。在匹配时按排序后的次序进行匹配,并记录下标。当下标更靠前时更换为匹配项为新的项
可以在这里看出来:排序 Scanner.java#L183 最优匹配 Scanner.java#L631
这样做法的扩展性极强,在修改时,只需要改动一下要修改的词即可。例如一开始设计的lambda为
不过这个方法也是一种DFA,和明确的状态机编程方式并没有本质区别
不过要注意的是,按上述方式分隔将产生一个对于小数的莫名其妙的结果
匹配
所以token串为
##类型
Token也有其自身的类型。
每个token的类型为
实际上,记录了token的“内容”也就相当于记录了token的类型。比如
希望看官能关注一下我的编程语言哦~ Latte
目录戳这里
词法分析的工作是,将输入的字符串转化为结构清晰的
Token
通常来说,这个
Token需要包含两样东西,词 和 类型
##词
什么是词呢?例如,输入串如下:
val num = 3 * 7
从直觉上(语法高亮也帮助了我们)可以看出,它由如下几部分组成
val
num
=
3
*
7
我们不会把
val拆成
v
al或者其他什么形式,因为它是一个“词”
“词”表示用于解析的最小单元。词的规定很宽泛,像emoji语言,它的一个词很可能就是一个emoji图像(虽然我没研究过不过大概如此吧。。)而对于主流的编程语言,词通常为一串字母,数字,某些特殊符号(运算符)
注:如下方式的原理和自动机一致,但是性能没有自动机高。好处在于手写实现几乎不可能出错。
在
Latte中,词有如下规定 Scanner.java#L95
如下的符号
// SPLIT ".", ":", "::", "=", "+=", "-=", "*=", "/=", "%=", "<<", ">>", ">>>", "&", "^", "|", "~", "^^", "!", "&&", "||", "!=", "==", "!==", "===", "<", ">", "<=", ">=", "+", "-", "*", "/", "%", "++", "--", "@", "=:=", "!:=", "..", ".:", "..." // LAYER "->" // STRING "\"", "'", "`" // NO_RECORD " " // ENDING "," // COMMENT ";" // PAIR "{", "}", "[", "]", "(", ")"
被视为“只要遇到便需要分割的”符号,我把它们称做“分隔符”。什么意思呢?
观察
val num = 3 * 7
它写成这些形式都应当是同一种含义
val num=3*7 val num = 3* 7 val num = 3 *7
虽然后两个看上去不太好看,但其含义不应与第一种相异
它为何会被分割为那几个小块?很明显,空格是一个“分隔符”,在空格之前的视为一个Token,在空格之后的视为一个Token。而空格本身,比较特殊,它不但是“分隔符”,还是“不需要被记录的分隔符”,将会被忽略。前后token将直接串联起来
而
*也是一个“分隔符”,遇到
*时将自动分隔前后,并且
*本身也是一个词,将被记录在Token串中。
匹配串很简单,直接使用
java的
indexOf(str)即可。但是我们想要的并不是简单的匹配,而是“最优匹配”
例如
3<<2
首先匹配的应当为
<<而不是
<。因为
<<长度为2,
<长度为1。它们下标在同一位置,长度更长的将更加优先
3<(2<<1)
将优先匹配
<,因为它的位置相比
<<更靠前。
所以得出一个比较通用的结论: 最优匹配项为 1.下标最靠前 2.下标相同时长度最长
在java中也非常容易实现。首先将所有符号放到一个集合中,然后进行长度从大到小的排序。在匹配时按排序后的次序进行匹配,并记录下标。当下标更靠前时更换为匹配项为新的项
可以在这里看出来:排序 Scanner.java#L183 最优匹配 Scanner.java#L631
这样做法的扩展性极强,在修改时,只需要改动一下要修改的词即可。例如一开始设计的lambda为
=>,后来经过调查(其实就是在一个java的群里问了问大家更喜欢那种= =)修改为
->,仅仅改动一个字符串就完成了所有的修改。使用词法分析器生成器也可以方便的修改,但是手写自动机再修改就不太容易了。
不过这个方法也是一种DFA,和明确的状态机编程方式并没有本质区别
不过要注意的是,按上述方式分隔将产生一个对于小数的莫名其妙的结果
1.2
匹配
.,分隔为
1,
.,然后,遇到行末,便记录
2
所以token串为
1
.
2而不是
1.2。幸运的是我们只会在小数上遇到这个问题(更准确的描述为
.在不向前读时将产生歧义,这个歧义仅会在小数上出现)。在生成后做一个
final check,转换一下就行了。或者每当遇到
.都向前读一个字符,若为数字则,在生成时就生成为
1.2
##类型
Token也有其自身的类型。
val num = 3 * 7
每个token的类型为
val : 修饰符 MODIFIER num : 合法的名称 VALID_NAME = : 符号 SYMBOL 3 : 数字 NUMBER_LITERAL * : 符号 SYMBOL 7 : 数字 NUMBER_LITERAL
实际上,记录了token的“内容”也就相当于记录了token的类型。比如
val,再怎么变它也是一个修饰符。所以,标记类型仅仅是出于性能和扩展性考虑(当然,由于词法分析器需要检查token是否合法,所以在检查的时候顺带加上类型也是很自然的事)。
希望看官能关注一下我的编程语言哦~ Latte
相关文章推荐
- 浅谈汇编器、编译器和解释器
- 让我们做个简单的解释器(三)
- 让我们做个简单的解释器(一)
- 用 350 行代码从零开始,将 Lisp 编译成 JavaScript
- Python的词法分析与语法分析
- 基于JSP编译器基本语法的使用详解
- C#命令行编译器配置方法
- Java虚拟机JVM性能优化(二):编译器
- AngularJS HTML编译器介绍
- 实现接口时@Override注解问题
- 条款06:若不想使用编译器自动生成的函数,就该明确拒绝
- g++编译 参数 .
- 关于 ndk和jni的区别
- vim中的杀手级插件: YouCompleteMe
- Google C++ unit test 在ARM Android 2.3 上的编译与使用
- 从代码示例了解ECMAScript5新特性
- Java的可移植性受到广泛使用
- C++ .H .CPP
- Windows Server 2003远程桌面多用户连接问题