Reactjs+BootStrap开发自制编程语言Monkey的编译器:词法解析1

我们先看一句简单的代码:

let x = y + 5;

编译器在解析这条语句前,它需要做一项分析工作,它会把上面的语句各个要素进行分类如下:

1:let 2: x , y 3:= 4:+, 5:5 6:;

也就说 编译器把一句代码中的不同元素分成了六组,第一组是由关键字’let’组成的集合;第二组是三个字符串或是字符的集合;第三组由等于号’=’组成;第四组是一个个特殊符号’+’组成的集合;第五组是由数字‘5’组成的集合;第六组是符号’;’独自组成的一个集合;为了区分不同的集合,我们为每一个集合赋予一个不同的值,第一组赋值0,第二组赋值1,依次类推,第六组赋值5。直接赋与数值不利于人的理解,于是我们可以用编程中常量定义的方法,用不同的常量来对应不同的值,例如:

const  LET = 0;
const  IDENTIFIER = 1;
const  EQUAL_SIGN = 2;
const  PLUST_SIGN = 3;
const  INTEGER = 4;
const  SEMICOLON = 5;

经过分类后,上面的代码语句在编译器的眼里就变成了:

LET IDENTIFIER EQUAL_SIGN IDENTIFIER PLUS_SIGN INTEGER SEMICOLON

于是我们完成了对代码编译时的第一步抽象。分类的一个原则是,所有关键字自己单独成为一类,后面我们要看到的关键字例如 if else 他们会自己成为一类,所有表示变量的字符串,例如x, y, monkey, 等全部被划入IDENTIFIER一类,所有的特殊符号,例如’:’,’-‘,’*’,’/‘等,都自己形成一类,所有的数字字符串例如’5’, ‘123’,等全部划入INTEGER这类。因此经过第一层处理后,编译器看到的再也不是具体的字符,而是代码中不同元素所对应的分类。

然而仅仅向上面那样分类,那很多信息就丢失了,这样编译器在后面就不能就顺利的解释执行或是代码生成,因此除了分类后,我们还必须附带上必要信息,例如对于分类IDENTIFIER, 我们还需要附带上它对应的字符串,对于分类INTEGER,我们还需要附带上它对应的数值,最好还是要附带上该元素所在的行号,这样以便于输出错误信息或者开发调试器。于是上面的解析再次增强为:

{type: LET, literal: “let”, lineNumber: 0} {type: EQUAL_SIGN, literal:”=”, lineNumber: 0} {type: IDENTIFIER, literal: “x”, lineNumber: 0} {type: EQUAL_SIGN, literal:”=”, lineNumber: 0} {type: IDENTIFIER, literal:”y”} {type: PLUS_SIGN, literal: “+”, lineNumber: 0} {type: INTEGER, literal:”5”, lineNumber: 0} {type: SIMICOLON, literal: “;”, lineNumber: 0}

编译器把代码转变为上面这种结构的过程,就叫词法解析。其中类似{type: LET, literal: “let”, lineNumber: 0} 这种结构体呢,我们就叫Token.我们在src/目录下新建一个组件文件叫MonkeyLexer.js,它将专门用来实现词法解析的功能,然后先在该文件中添加Token对象的定义:

class Token {
    constructor(type, literal, lineNumber) {
        this.type = type
        this.literal = literal
        this.lineNumber = lineNumber    
    }

    type() {
        return this.type
    }

    literal() {
        return this.literal
    }

    lineNumber() {
        return this.lineNumber
    }
}

class MonkeyLexer {
    constructor(sourceCode) {
        this.sourceCode = sourceCode
    }
}

export default MonkeyLexer

类MonkeyLexer将负责把源代码解析成一系列Token的组合。词法解析的基本办法是,先把字符一个个读出来,判断一下读到的单个字符是否是特殊符号,例如’;’, ‘+’等,如果是,那么直接生成对应的Token对象,如果不是,那么就把字符攒起来,直到遇到空格,回车换行为止,接着判断一下攒起来的字符串是关键字,还是变量,还是整形数值,根据不同情况生成不同Token对象。我们看看解析算法的代码是如何实现的:

class MonkeyLexer {
    constructor(sourceCode) {
        this.initTokenType()
        this.sourceCode = sourceCode
        this.poistion = 0
        this.readPosition = 0
        this.lineCount = 0
        this.ch = ''
    }

    initTokenType() {
        this.ILLEGAL = -2
        this.EOF = -1
        this.LET = 0
        this.IDENTIFIER = 1
        this.EQUAL_SIGN = 2
        this.PLUS_SIGN = 3
        this.INTEGER = 4
        this.SEMICOLON = 5
    }
    ....

}

MonkeyLexer 是词法解析器,在他的初始化构造函数constructor中,它调用initTokenType函数,先为不同的元素分类给定一个唯一整数以便加以区分。接着我们需要一个函数,以便把字符从代码字符串中一个个读出来,这个函数实现如下:

class MonkeyLexer {
    ....
    readChar() {
        if (this.readPosition >= this.sourceCode.length) {
            this.ch = 0
        } else {
            this.ch = this.sourceCode[this.readPosition]
        }

        this.poistion = this.readPosition
        this.readPosition++
    }

    skipWhiteSpaceAndNewLine() {
        /*
        忽略空格
        */
        while (this.ch === ' ' || this.ch === '\t' 
            || this.ch === '\n') {
            if (this.ch === '\t' || this.ch === '\n') {
                this.lineCount++;
            }
            this.readChar()
        }
    }
....

}

readChar() 从代码字符串中逐个读取字符,每读取一个字符,让readPosition加一,每次读取时,代码总是从readPoisition指向的位置开始读取。skipWhiteSpaceAndNewLine函数的作用是,判断读取的字符是不是空格,如果是空格,那么就忽略当前读取的字符,继续读取后续字符,如果字符是回车换行,那么把表示当前行号的变量lineCount加1,然后继续往后读取,直到读取到不是空格,或是回车换行字符为止。

当读取到有效字符之后,我们要根据字符的含义把它归类,例如当读取到的字符是’;’时,就创建一个类型为SEMICOLON的Token对象,具体代码实现如下:

class MonkeyLexer {
    ....
    nextToken () {
        var tok
        this.skipWhiteSpaceAndNewLine() 
        var lineCount = this.lineCount

        switch (this.ch) {
            case '=':
            tok = new Token(this.EQUAL_SIGN, "=", lineCount)
            break
            case ';':
            tok = new Token(this.SEMICOLON, ";", lineCount)
            break;
            case '+':
            tok = new Token(this.PLUS_SIGN, "+", lineCount)
            break;
            case 0:
            tok = new Token(this.EOF, "", lineCount)
            break;

            default:
            var res = this.readIdentifier()
            if (res !== false) {
                tok = new Token(this.IDENTIFIER, res, lineCount)
            } else {
                res = this.readNumber()
                if (res !== false) {
                    tok = new Token(this.INTEGER, res, lineCount)
                }
            }

            if (res === false) {
                tok = undefined
            }

        }

        this.readChar()
        return tok
    }
    ....
}

nextToken函数在执行时,先通过调用skipWhiteSpaceAndNewLine,滤掉空格以及回车换行等特殊字符,一旦独到有效字符后,进入switch部分,如果当前的字符是特殊字符,例如’;’,’=’,’+’等,由于这些字符各自属于单独一个分类,因此分别给他们创建里一个Token对象,如果读到的是普通英文字符或者是数字字符,那么就进入default代表的代码处。

当代码连续读入的字符是普通英文字符或是数字字符时,词法解析器会把这些字符凑成一个字符串,假设读入的代码是:

five = 123;

那么解析器读入上面语句时,首先它会连续读入5个字符: f, i, v, e,然后把他们组合成一个字符串”five”,接着为该字符串生成一个分类为IDENTIFIER的Token对象,当解析器读入’=’后面的内容时,它会把后面的数字字符分别读入,也就是分别读取’1’,’2’,’3’三个字符,然后把这三个字符组合成字符串”123”,最后给这个字符串创建一个类型为INTEGER的Token对象。这些工作分别由函数readIdentifier() 和 函数 readNumber()来实现,我们看看他们的代码:

isLetter(ch) {
        return ('a' <= ch && ch <= 'z') || 
               ('A' <= ch && ch <= 'Z') ||
               (ch === '_')
    }

    readIdentifier() {
        var identifier = ""
        while (this.isLetter(this.ch)) {
            identifier += this.ch
            this.readChar()
        }

        if (identifier.length > 0) {
            return identifier
        } else {
            return false
        }
    }

    isDigit(ch) {
        return '0' <= ch && ch <= '9'
    }

    readNumber() {
        var number = ""
        while (this.isDigit(this.ch)) {
            number += this.ch
            this.readChar()
        }

        if (number.length > 0) {
            return number
        } else {
            return false
        }
    }

readIdentifier 在执行时,先调用isLetter来判断当前读入的字符是否是字母,如果是,那么它就把所有字符集合起来,形成一个字符串。readNumber在执行时,它先判断当前读入的字符是否是数字,如果是,它就把所有数字字符集合起来,形成一个数字组成的字符串。

在nextToken的switch语句部分,如果逻辑进入default部分,那么函数会调用readIdentifier()看看当前是否读到了一个由字母组合成的字符串,如果是,那么就创建一个类型为IDENTIFIER的Token对象,如果不是由字母组成的字符串,那么就接着调用readNumber看看当前内容是不是全是由数字组成的字符串,如果是,那么就创建一个类型为INTEGER的Token对象,如果不是,那说明当前读到了词法解析器无法理解的字符,因此返回一个undefined对象。

更详细的讲解和代码调试演示过程,请点击链接

到目前为止,我们的词法解析部分已经基本成型了,现在就看如何调用起MonkeyLexer这个组件,以便用来分析在页面文本框中输入的代码。要想运行MonkeyLexer这个组件,我们需要把页面文本框中的内容得到,然后传入到该组件中。

回到MonkeyCompilerIDE.js文件,页面加载时,该文件里的MonkeyCompilerIDE.render 函数会被调用,以便用于渲染页面。render在执行时返回了一个JSX对象,其中有一个控件是这样的:

<bootstrap.FormControl 
 componentClass = "textarea" 
 style={textAreaStyle}
 placeholder="Enter your code" />

上面这个控件的作用就是在页面上创建出一个输入文本框。当用户在文本框上输入内容后,点击下面的红色按钮,我们如何得到框内的文本内容呢?要想实现这个功能,我们必须要获得控件的实例对象,把上面的控件代码做如下修改:

              <bootstrap.FormControl 
               componentClass = "textarea" 
               style={textAreaStyle}
               inputRef = {
                    (ref) => {this._textAreaControl = ref}
               }
               placeholder="Enter your code" />

注意看,我们增加了部分代码如下:

inputRef = {
              (ref) => {this._textAreaControl = ref}
}

inputRef是Reactjs给我们提供的指令,如果一个控件,如果它要想在页面上绘制或是创建内容的话,它必须实现一个render()接口,render()接口会被reactjs框架调用,于是组件就可以在render中去绘制页面,那么render()是如何被reactjs调用的呢?当一个组件被放入到”<>”,这两个尖括号中时,reactjs解析到后就会自动把尖括号里面的组件对象得到,然后调用它的reander函数。

例如上面代码中,夹在尖括号中的组件叫bootstrap.FormControl, 那么reactjs在解析到上面代码时,会自动调用bootstrap.FormControl.render(),于是一个输入文本框就会显示到页面上了。

如果要想把尖括号包围起来的组件对象获取到,就得依靠inputRef指令,就像我们上面做的那样,当reactjs解读尖括号中的组件时,如果发现其中包含inputRef指令,那么他就会执行后面大括号里面的代码,上面代码中,ref变量就是reactjs框架传给我们的组件对象,其中this指向的是MonkeyCompilerIDE这个组件对象本身,this._textAreaControl = ref 它的意识是,在MonkeyCompilerIDE这个对象内部创建一个名为_textAreaControl的成员变量,然后把ref指向的控件对象赋值给它,这样我们就可以获得文本框控件的实例对象,有了实例对象,我们通过访问它的value属性就可以获得文本框内的文本了。

接下来我们需要关注的是如何响应底层按钮的点击。在JSX中,对应按钮的组件是:

<bootstrap.Button bsStyle="danger">
         Lexing             
</bootstrap.Button>

上面的代码经过reactjs解析后会在页面上绘制出底部那个红色的按钮,其中bsStyle=”danger” 称之为组件的属性,是用来从将信息从外部传入组件内部的,后面我们会详细讲解这个特性。如何响应按钮的点击时间呢?如下:

<bootstrap.Button onClick={this.onLexingClick.bind(this)} 
 bsStyle="danger">
     Lexing            
</bootstrap.Button>

我们增加对onClick事件的捕捉,一旦用户点击按钮后,onClick事件被触发,它会调用我们自己实现的onLexingClick函数,这里一定要使用bind把onLexingClick绑定,要不然被调用时,this指针不指向MonkeyCompilerIDE组件。我们再看看响应函数的实现:

onLexingClick () {
        this.lexer = new MonkeyLexer(this._textAreaControl.value)
        this.lexer.lexing()
    }

我们先通过new 构建一个MonkeyLexer实例,this._textAreaControl.value对应文本框中输入的代码内容,并把创建的实例赋值给当前组件的lexer成员变量,最后调用MonkeyLexer导出的lexing函数开始词法解析流程。

上面代码完成后,加载页面,在文本框中输入几句代码,点击按钮进行词法解析,结果如下:

我在左边输出了两条语句:

let five = 5;
let six = 6;

右边控制台输出了词法解析的结果,其中变量”five”形成的Token对象中,分类为1,对应我们的代码,它就是IDENTIFIER, 第二行的数字6,它对应的Token中,分类值为4,对应到代码中是NUMBER,并且它所在的行号是1,从这两处结果看,词法解析的结果基本正确。但有个问题就是let, 它对应的Token中分类是1,对应的就是IDENTIFIER, 这是有问题的,前面我们说过,let是关键字,它必须对应自己的分类,因此词法解析在这里出了点问题,下一节,我们再处理它。

原文发布于微信公众号 - Coding迪斯尼(gh_c9f933e7765d)

原文发表时间:2017-11-11

本文参与腾讯云自媒体分享计划,欢迎正在阅读的你也加入,一起分享。

发表于

我来说两句

0 条评论
登录 后参与评论

相关文章

来自专栏京东技术

3D绘图小帮手WebGL入门与进阶(中)——着色器的基本编程

19140
来自专栏深度学习之tensorflow实战篇

R语言读CSV、txt文件方式以及read.table read.csv 和readr(大数据读取包)

首先准备测试数据*(mtcars) 分别为CSV.    TXT ? read.table 默认形式读取CSV(×)与TXT(效果理想) ① > test<-r...

1.8K80
来自专栏从流域到海域

《笨办法学Python》 第9课手记

《笨办法学Python》 第9课手记 这节课终于有一点新内容了,新内容也蛮容易理解的。 原代码如下: # Here's some new stuff, reme...

226100
来自专栏黑泽君的专栏

传智播客_毕姥爷_2012年毕向东Java基础教程_毕向东老师

视频百度网盘下载链接:https://pan.baidu.com/s/1bpD3P07#list/path=%2F

16010
来自专栏Golang语言社区

Go语言核心之美 -JSON

JSON(JavaScript Object Notation)是一种发送和接收结构化信息的标准化表示法。类似的标准化协议还有XML、ASN.1、Protobu...

42960
来自专栏从流域到海域

《笨办法学Python》 第25课手记

《笨办法学Python》 第25课手记 本节课内容较多,如果不理解可以先尝试做正确,然后再来理解。我们的学习已经由最初的简单向复杂转变了,希望你能咬牙坚持下来,...

27060
来自专栏沈唁志

Python爬虫之XPath语法和lxml库的用法

23840
来自专栏阿凯的Excel

自定义单元格格式介绍(第一期 数字版)

之前分享金字塔图(有链接哦)的时候,有分享将负数显示为正数的小技巧,当时有朋友让我全面的分析自定义单元格格式,因为我很喜欢一句话:“迟到比不到好”。所以我就故意...

32850
来自专栏逸鹏说道

你可能不知道的字符比较中的“秘密”

有时候,一个简单的字符比较,你可能也会被弄得晕头转向。为什么这样说呢?请看下面这个例子(代码就不贴了,因为后来发现页面不支持这两个字符的显示)。猜测一下,会是什...

20670
来自专栏守候书阁

个人小结--javascript实用技巧和写法建议

从大学到现在,接触前端已经有几年了,感想方面,就是对于程序员而言,想要提高自己的技术水平和编写易于阅读和维护的代码,我觉得不能每天都是平庸的写代码,更要去推敲,...

10110

扫码关注云+社区

领取腾讯云代金券