精华 解释器 - 扫描器之旅: [1] 基础篇
发布于 7天前 作者 tulayang 283 次浏览 来自 分享

→ FROM

很久之前,就想整理一下解释器的东西. 现在打算休息无聊的时候,一篇篇的从记忆里挤出点东西来.

这篇是 [ 解释器 - 扫描器之旅 ] 第一篇:基础篇.

  1. 历史

    程序语言的本质,就是向计算机硬件说话,告诉计算机做什么. 作为一个中国人,我们学了英语,可以用英语告诉一个老外我们的意图.

    计算机起源的故事我已经大体忘了,只记得大师们在开始时,为了计算机的工作逻辑更加简单和接近物理信号的表述,努力使计算机只“会加法”就可以完成所有复杂的计算任务. 这个“只会加法”的方式,最终使得二进制01作为计算机的终极语言.

    作为更现代化的后来者,重复古老的文字是费力和容易出错的. 所以,我们需要翻译. 一个可以帮助我们把更简单容易学用的语言,翻译成终极语言01.

    现在,新的语言非常之多,大部分可以直接转化成01,有的语言剑走偏锋,翻译成其他的语言再与计算机沟通.

    编译器、解释器,就是需要的翻译. 每种语言会有不同的编译器、解释器. 他们的差别,这里不想去理会太多. 我们只关注解释器.

    动态语言,一般是采用解释器. 当然也有的可以采用编译器. 我们想用javascript程序实现一个简单的解释器,来看看解释器的原理是怎样的.

  2. 解释器本源

    解释器

    这张图大致描述了解释器的基本过程:

    [1.] 编写语言

     var i = a + b * c;
    

    [2.] 送入解释器 [3.] 以位的方式,迭代顺序查看字符 [4.] 根据语言的规则,提取单词、数值、字串、… [5.] 根据目标语言的规则,构建一个语法树 [6.] 根据目标语言和语法树,将语法树的内容转换成另一种语言

    其中[3.][4.][5.]便是解释器的工作过程. 而其的工作过程,又可以划分为扫描,词法分析,语法分析,构建语法树,…

  3. 目标

    现在,我们开始编写一个简单的解释器.

    比如,我们输入一个javascript代码:

     var a = 1;
     ...
    

    我们希望得到类似这样的一个语法树:

    [
       [
         {expression:'var'},
         {left:'a'},
         {value:1},
         {operator:'='} 
       ],
       ...
    ]
    

    最终构建起来的语法树会跟此有很大区别,但至少目的是同样的. 有了这个语法树,我们就可以将原本的语言代码,可控的转换成另一种语言.

    比如,将这个语法树转换成这样的语言:

    a is 1
    
  4. 万丈高楼平地起

    前面我们说到了,“以位的方式,迭代顺序查看字符”. 那么我们的解释器中将会需要大量的循环迭代. 我们需要一个类似“游标”的东西,来不停的在输入的语言文本中移动,每次移动一个字符.

    现在,开始写码,并构建解释器的基础:迭代.

    *为了性能,我们使用了循环,而不是递归,尽管递归更有描述性. * *另外,我们还需要一些解释器“内部的全局变量”,来方便存储修改值状态. 让“纯函数”和“面向对象”完蛋去吧,我们需要“不纯”的函数式. *

    我们开始的代码是下面这样的:

    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(). **

    0x610x0061的简写,事实上可以这么写:

    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()来判断.

未完待续 …

1 回复

这里也有一篇关于解释器的,或许更好理解,分享给你

回到顶部