很久之前,就想整理一下解释器的东西. 现在打算休息无聊的时候,一篇篇的从记忆里挤出点东西来.
这篇是 [ 解释器 - 扫描器之旅 ] 第一篇:基础篇.
-
历史
程序语言的本质,就是向计算机硬件说话,告诉计算机做什么. 作为一个中国人,我们学了英语,可以用英语告诉一个老外我们的意图.
计算机起源的故事我已经大体忘了,只记得大师们在开始时,为了计算机的工作逻辑更加简单和接近物理信号的表述,努力使计算机只“会加法”就可以完成所有复杂的计算任务. 这个“只会加法”的方式,最终使得二进制01作为计算机的终极语言.
作为更现代化的后来者,重复古老的文字是费力和容易出错的. 所以,我们需要翻译. 一个可以帮助我们把更简单容易学用的语言,翻译成终极语言01.
现在,新的语言非常之多,大部分可以直接转化成01,有的语言剑走偏锋,翻译成其他的语言再与计算机沟通.
编译器、解释器,就是需要的翻译. 每种语言会有不同的编译器、解释器. 他们的差别,这里不想去理会太多. 我们只关注解释器.
动态语言,一般是采用解释器. 当然也有的可以采用编译器. 我们想用javascript程序实现一个简单的解释器,来看看解释器的原理是怎样的.
-
解释器本源
这张图大致描述了解释器的基本过程:
[1.] 编写语言
var i = a + b * c;
[2.] 送入解释器 [3.] 以位的方式,迭代顺序查看字符 [4.] 根据语言的规则,提取单词、数值、字串、… [5.] 根据目标语言的规则,构建一个语法树 [6.] 根据目标语言和语法树,将语法树的内容转换成另一种语言
其中[3.][4.][5.]便是解释器的工作过程. 而其的工作过程,又可以划分为扫描,词法分析,语法分析,构建语法树,…
-
目标
现在,我们开始编写一个简单的解释器.
比如,我们输入一个javascript代码:
var a = 1; ...
我们希望得到类似这样的一个语法树:
[ [ {expression:'var'}, {left:'a'}, {value:1}, {operator:'='} ], ... ]
最终构建起来的语法树会跟此有很大区别,但至少目的是同样的. 有了这个语法树,我们就可以将原本的语言代码,可控的转换成另一种语言.
比如,将这个语法树转换成这样的语言:
a is 1
-
万丈高楼平地起
前面我们说到了,“以位的方式,迭代顺序查看字符”. 那么我们的解释器中将会需要大量的循环迭代. 我们需要一个类似“游标”的东西,来不停的在输入的语言文本中移动,每次移动一个字符.
现在,开始写码,并构建解释器的基础:迭代.
*为了性能,我们使用了循环,而不是递归,尽管递归更有描述性. * *另外,我们还需要一些解释器“内部的全局变量”,来方便存储修改值状态. 让“纯函数”和“面向对象”完蛋去吧,我们需要“不纯”的函数式. *
我们开始的代码是下面这样的:
var source, // 输入的语言代码 at = -1, // 游标当前所在的位置 length, // 语言代码的长度 code; // 游标所在的字符的Unicode编码
source
就是要解释的语言文本,我们会让source = '要解释的语言文本...'
at
是游标,用来在文本中移动一个字符- 每当
at
移动一次,code
都被设成新的字符的Unicode16进制码
[1.] Unicode与16进制
上边提到了Unicode,现在Unicode双字节基本囊括了当前的大部分字符,还有一些极少数很少用到的字符,需要Unicode四字节来表示. 我们这里将一直使用双字节.
Unicode是用十六进制表示的,比如:
'\u0061'
就是Unicode字符串,使用双字节(16位)表示,ta表示英文字符'a'
,没错:'\u0061' === 'a'
.javascript的字符串,默认采用16位来描述一个字符,以便更容易的操作Unicode字符. 当你输入
'a'
时,其实是输入了一个16位的字符. 在C语言中,默认是8位.'\u0061'
的16进制编码就是0x0061
,很简单吧!-
String.fromCharCode(0x0061)
可以得到一个16进制数字的字符形式,输出'a'
. -
'abc'.charCodeAt(0)
输出字符串第0个位置的16进制字符编码,输出是0x0061
. 如果你用控制台打印,发现输出的是十进制数字97
. 那是因为语言默认调用toString()
,输出的是10进制转换数值. 要输出16进制,可以这样:'abc'.charCodeAt(0).toString(16)
. -
还有一个函数
'abc'.charAt(0)
用于提取第0个位置的字符,这是一个字符串表示,输出是'a'
.
**
/a/.test('abc'.charAt(0))
要比'abc'.charCodeAt(0) === 0x61
判断第0个位置是字符'a'
要慢得多!!!所以,我们的解释器中将会优先使用charCodeAt()
来判断字符的值,而不是charAt()
. **0x61
是0x0061
的简写,事实上可以这么写:0x61 === 0x0061 // true 0x61 === 97 // true
**记住,任何一个字符都有Unicode表示,包括回车空格,任何计算机上奇怪的文字. 我们可以用
s.charCodeAt(1) === 0x0A
来判断字符串s的第1位置是不是回车符'\n'
. **[2.] 迭代器
我们的迭代器,是基于性能和方便来实现的,没有包装大量的对象,非常简单:
function next(c) { code = source.charCodeAt(++at); if (arguments.length > 0 && code !== c) { error('next()', 'not except character want ' + c + ' but ' + code); } return code; }
当调用迭代器
next()
时,我们把游标at
移动一位,并且获取新的位置的Unicode编码,将其返回. 如果输入了想要的参数,同时判断新的编码是不是预期需要的. 如果不是,调用error()
函数抛出一个错误.有了迭代器你可以很灵活的操作游标,并提供很棒的描述性,比如:
next(); next(); next(0x61); // 移动3次游标,判断这个位置的字符是不是'a'
你也可以编写出一个循环:
while (true) { if (isNaN(next())) { break; } if (code === 0x61) { next(); // 移动 console.log(code === 0x0A); // 是否'\n' } }
当游标超过了要解释的字符文本source的长度时,charCodeAt()获取的Unicode编码是
NaN
,可以使用isNaN()
来判断.
未完待续 …