精华内容
下载资源
问答
  • 手写解释器

    千次阅读 2018-06-07 08:55:38
    ”这跟你是新手还是经验丰富的软件开发人员无关:如果你不知道编译器和解释器是怎样工作的,那么你就不知道计算机是怎样工作的。就这么简单。那么,你知道编译器和解释器是怎样工作的吗?我的意思是,你 100% ...

    谷歌大牛 Steve Yegge 曾说过:“如果你不知道编译器是怎样工作的,那你也并不知道计算机是怎样工作的。如果你不是 100% 确定你是否知道编译器是怎样工作的,那你其实并不知道它们是怎样工作的。”

    这跟你是新手还是经验丰富的软件开发人员无关:如果你不知道编译器和解释器是怎样工作的,那么你就不知道计算机是怎样工作的。就这么简单。

    那么,你知道编译器和解释器是怎样工作的吗?我的意思是,你 100% 确定自己知道它们是怎样工作的吗?如果你不知道。

    或者说如果你不知道,并且因此而感到不安。

    别着急。如果你留下来学习完整个系列,并且和我一起构造一个解释器和编译器,你最终将会知道它们是怎样工作的。并且你将会变成一个自信快乐的人,至少我希望是这样。

    为什么要学习解释器和编译器?有三个理由:

    1、为了写一个解释器或者一个编译器,你必须综合应用一些技能。编写一个解释器或者编译器,将会帮助你提高这些技能,让你变成一个更优秀的软件开发者。同时这些技术在编写其它软件(非编译器和解释器)时同样很有用。

    2.你确实想知道计算机是怎样工作的。常常解释器和编译器看起来像魔术。而你对这种魔术觉得不太舒服。你想弄清楚构造一个解释器和编译器的过程,弄明白它们是怎样工作的,弄明白这里面所有的事。

    3.你想创建你自己的语言或者是特定领域的语言。如果你创建了它,那么你同样需要为它创建一个编译器或解释器。最近,人们重新兴起了对新语言的兴趣。你几乎每天都能看新语言的出现:Elixir、Go、Rust,只是随便举几个例子。

     

    好了,那什么是解释器,什么是编译器呢?

    解释器和编译器的目标就是将使用高级语言编写的源程序转换成另一种形式。什么形式?稍安勿燥,在本系列的后续部分中,你将会很确切地了解到源程序将被转换成什么。

    现在你可能会对解释器和编译器之间有什么区别感到好奇。对于本系列,我们约定,如果一个翻译器将源程序翻译成机器语言,那么它就是一个编译器。如果一个翻译器直接处理并运行源程序,不先把源程序翻译成机器语言,那么它就是一个解释器。直观上它看起来会是这个样子:

    我希望此时此刻,你很确信你的愿意学习,并且构建一个解释器和编译器。关于这个解释器系列,你有什么期待呢?

    你看这样行不行。我们为 Pascal 语言的一个大子集创建一个简单的解释器。在这个系列的最后,你将得到一个能够工作的解释器以及像 Python 的 pdb 一样的源代码级别的调试器。

    那么问题来了,为什么选 Pascal?首先,这不是我为了这个系列捏造的语言:这是一个真实的编程语言,具有许多重要的语言结构。其次,有些计算机书籍虽然旧,但实用,这些书的例子用了 Pascal 语言。(我承认这不是一个选择它来构造解释器的不可抗拒的理由,但我认为这也是一个很好的学习非主流语言的机会:)

    这是有一个使用 Pascal 编写的阶乘的例子。你将可以用自己的解释器和调试器来解释和调试这段代码。

    我们使用 Python 来实现 Pascal 的解释器,但是你也可以使用其它任何语言来实现它,思想的表达不应局限于任何特定的语言。

     

    好了,让我们开始行动吧。预备,开始!

    你将通过编写一个四则运算表达式的解释器(俗称计算器),来完成对解释器和编译器的第一次进军。今天的目标很简单:让计算器能够处理个位整数的加法,比如 3 + 5。下面是你的计算器的代码,对不起,是你的解释器的代码。

    将上面的代码保存为文件 calc1.py ,或者直接从 Github 上下载。在深入代码之前,以命令行运行,观察其运行结果。好好把玩这个程序。这是在我的笔记本上的一个示例会话(如果你使用 Python3,代码中 raw_input 需改写为 input )

    为了让计算器工作正常,不抛出异常,输入应该遵循几条规则:

    • 输入只能是个位数的整数
    • 当前支持的算术运算只有加法
    • 在输入中不允许出现空格

    为了使计算器简单这些限制是必要的。别担心,很快你将使它变得很复杂。

    好了,现在让我们深入理解解释器是怎样工作的,以及它是怎样计算算术表达式的。

    当你在命令行下输入 3+5 时,解释器得到一串字符 “3+5”。为了让解释器真正地理解怎么处理这一字符串,第一步需要将 “3+5”切分成不同的部分,我们称之为记号(tokens)。一个记号(token)是一对类型·值。举例来说,记号 “3”的类型为 INTEGER,相对应的值为整数3。

    将输入的字符串切分成记号的过程被称作词法分析。所以,第一步解释器需要读取输入并把它转换成一系列的记号。解释器做这部分工作的组件被称作词法分析器(lexical ananlyzer,简称lexer)。你也许碰到过其它的叫法,像扫描程序(scanner),分词器(tokenizer)。它们意思都相同:解释器或者编译器中把输入字符串转换成一串记号的组件。

    Interpreter 类中的 get_next_token 方法就是一个词法分析器。每当你调用它,就能得到从传入的字符串中创建的记号里的下一个。我们来仔细看看这个方法,看它是怎样把字符串转换成记号的。输入的字符串被保存在变量 text 中,pos 是字符串的一个索引值(把字符串想象成一个字符的数组)。pos 被初始化成 0,指向字符 ‘3’。get_next_token 首先测试这个字符是不是一个数字。如果是,pos 加 1 右移并返回一个类型是 INTEGER 值为 3 的 Token 实例,代表一个整型数字 3:

    现在 pos 指向 text 中的字符 ‘+’。当你下次调用 get_next_token 方法时,它先测试在 pos 位置的字符是不是一个数字,然后测试它是不是一个加号,现在它的确是加号。于是 get_next_token 对 pos 加 1 并返回一个类型为 PLUS 值为 ‘+’ 的 Token 实例:

    现在 pos 指向字符 ‘5’。当你再次调用 get_next_token 方法时,这个方法检查它是不是一个数字,它是,所以它对 pos 加 1 并且返回一个新的类型为 INTEGER,值被设置成 5 的 Token 实例。

    由于现在 pos 指向字符串 “3+5”尾部的后一个位置,每次调用 get_next_token 将会返回一个 EOF Token 对象。

    动手试试,看看你的计算器中词法分析器是怎样工作的:

    现在解释器能访问从输入字符串中等到的记号流,解释器需要对这些记号做一些事:在平滑的记号流中找到一种结构,记号流是从 lexer 的 get_next_token 而来。你的解释器期望在这串记号中找到这么一种结构:INTERGER -> PLUS -> INTEGER。也就是说,解释器尝试找到这种序列的记号:整数后面跟着一个加号,再后面跟着一个整数。

    查找这种结构并对其进行解释的方法是 expr。这个方法验证记号的序列是不是跟期望的一样,比如 INTEGER -> PLUS -> INTEGER。验证成功后,用 PLUS 左边的记号的值加上 PLUS 右边的记号的值就得到了结果,这样就成功地解释了传递给解释器的算术表达式。

    expr 方法使用 辅助方法(helper method) eat 来验证传递给 eat 方法的记号类型与 current token 类型是否匹配。匹配成功 eat 方法获取下一个记号,并把下一个记号赋值给变量 current_token,如此实际上就把匹配的记号“吃掉了”,并把一个虚拟的指向记号流的指针向前移动了。如果在记号中的结构与所期望的 INTEGER PLUS INTEGER 序列不对等,eat 方法抛出一个异常。

    概括一下解释器是怎样计算算术表达式的:

    • 解释器接受一个输入字符串,比如说“3+5”
    • 解释器调用 expr 方法从词法分析器 get_next_token 得到的记号流中查找一种结构。这种结构的形式为:INTEGER PLUS INTEGER。在确认了这种结构后,它将通过对两个 INTEGER 的记号做加法来解释输入。对于解释器来说在,这时要做的就是把两个整数加起来,即 3 和 5。

    恭喜自己吧。你刚学会怎样构建你的第一个解释器!

     

    现在是练习时间。

    你不会认为读了这篇文章就足够了,对吗?那好,不要嫌弄脏手,完成下面的练习:

    1. 修改代码,使得多位的整型数字也能做为输入,比如 “12+3”
    2. 添加一个方法处理空格,使得你的计算器能处理包含空格的输入,比如 “  12 + 3”
    3. 修改代码用 ‘-’ 代替 ‘+’,使其能计算像 “7-5” 的表达式

     

    检查你是否理解了

    1.什么是解释器?
    2.什么是编译器?
    3.解释器和编译器何不同?
    4.什么是记号(token)?
    5.将输入切分成记号的过程名叫什么?
    6.解释器中负责词法分析的部分叫什么?
    7.这部分在解释器及编译器中共同的名字叫什么?

    在结束这篇文章前,我希望你能保证学习解释器和编译器。并且我希望你马上做这件事。不要把它放在一边。不要等。如果你已经略读了这篇文章,请再看一遍。如果你已经仔细阅读但没做练习——现在就做。如果你只做了部分练习,完成剩下的。签下承诺保证,今天就开始学习解释器和编译器!

    我,      ,身体健康,思想健全,在此郑重保证从今天开始学习解释器和编译器直到有一天 100% 知道它们是怎么工作的!

    签名:
    日期:

    签上你的名字、日期,把它放在你能天天看到的地方,确保坚持你的承诺。并且记住承诺的定义:

    “承诺就是做你曾经说过要做的事,即使说这话时的好心情早已不在了。”——Darren Hardy

    好了,今天就这么多了,在这个小系列的下一篇文章中,你将得以扩展你的计算器,让它支持更多的算术运算。敬请期待。

    如果你不想等本系列的第二部分,并且迫不及待地想开始深入解释器和编译器,这有我推荐的一张有帮助的书单。

    1.《编程语言实现模式》(实用主义程序员)
    2.《Writing Compilers and Interpreters: A Software Engineering Approach》
    3.《现代编译原理——java语言描述》
    4.《现代编译程序设计》
    5.《编译原理技术和工具》(第二版)

    展开全文
  • 文章目录一、编写解释器的动机二、part1三、part2四、part3 一、编写解释器的动机 学习了Vue之后,我发现对字符串的处理对于编写一个程序框架来说是非常重要的,就拿Vue来说,我们使用该框架时可以通过如v-on:, v-...

    一、编写解释器的动机

    学习了Vue之后,我发现对字符串的处理对于编写一个程序框架来说是非常重要的,就拿Vue来说,我们使用该框架时可以通过如v-on:, v-model等html的属性时,我们能够在里面嵌入js代码,其实这块就已经使用了编译原理的知识来对输入的字符串进行解析,然后将它们嵌入到js代码中去,这也是我们在Vue中可以如此轻松地进行双向绑定,使用v-for进行列表渲染等等的技术基础。此外在做ccf csp的题目时,我也往往被一些字符串处理的题目给卡住,有时候虽然可以做出来,但有种只见树木不见森林之感。所以我希望可以快点先学习到一些编译原理的知识(而且自学往往比上课学习的效率和积极性高得多),为后面的学习打个基础。

    至于为什么学习编译原理先学习编写解释器呢?之前我是直接啃“龙书”(《compiler》)来着,可是里面真的许多东西比较晦涩难懂,之后我先去逛下知乎看看大神们是怎么学习编译原理的,看到有位大佬说可以多抄几遍这个解释器项目,很多东西自然就理解了:学习编译原理有什么好的书籍? - 时雨的回答 - 知乎。然后我就点进去那个GitHub项目的链接,发现居然star数高达1.3k:
    在这里插入图片描述
    于是我决定先通过它来进行学习了。

    二、part1

    (补上gitee项目地址,欢迎clone项目:https://gitee.com/warrior__night/learn-writing-interpreter

    资料链接:https://ruslanspivak.com/lsbasi-part1/

    part1的任务比较简单,它需要输入一个带有两个操作数的字符串,且只支持“+”号,如输入“1+2”,它会输出“3”。

    项目的源码是使用python编写的,而我为了印象深刻些(也防止自己不加思考嗯抄),就使用java进行重新编写了。
    在这里插入图片描述
    编写了这样几个类,由于java是半静态语言,故各个Token都使用类来进行封装了(如TK_Interger,即为整形数字,继承自Token类,其他的类似),使用起来比较方便一些。

    主要的类为Interpreter类,重点解读一些Interpreter类干了什么事:

    类的成员变量:

    private final String text;
    private int pos;
    private Token currentToken;
    

    text是输入的表达式字符串,pos是当前指向哪个位置的字符,currentToken是当前的Token是什么。

    构造函数:

    public Interpreter(String text){
        this.text = text;
        this.pos = 0;
        this.currentToken = null;
    }
    

    接收一个表达式字符串,其他的变量都清零

    抛出异常函数:

    public void error() throws Exception {
        throw new Exception("Error parsing input");
    }
    

    当输入不符合当前规则时抛出“Error parsing input”异常

    获取下一个Token的函数getNextToken:

    public Token getNextToken() throws Exception {
    	// 如果下标到了字符串的尽头,则返回TK_EOF
        if (pos > text.length()-1){
            return new TK_EOF();
        }
    
        char currentChar = text.charAt(pos);
        // 如果是数字,则返回TK_Interger(即数字Token),由于part1只考虑一个数字的情况,故只需解析一个数字
        if (Character.isDigit(currentChar)){
            Token token = new TK_Integer(Integer.parseInt(currentChar+""));
            // 指针移动到读取完Token的位置
            pos++;
            return token;
        }
    	// 解析符号也是相同的过程
        if (currentChar == '+'){
            Token token = new TK_Plus();
            pos++;
            return token;
        }
        // 如果不是数字或者“+”,则抛出异常
        this.error();
        return null;
    }
    

    eat函数:

    public void eat(Token.TokenType tokenType) throws Exception {
        if (currentToken.type == tokenType){
            currentToken = getNextToken();
        }
        else {
            this.error();
        }
    }
    

    判断当前读取到的Token和预想的是不是一样的类型,然后读取下一个Token。

    完成整个解析过程的函数:

    public int expr() throws Exception {
        currentToken = getNextToken();
    	// 第一个数
        Token left = currentToken;
        // 查看第一个Token是否是数字,并读取下一个Token
        eat(Token.TokenType.INTEGER);
    	
    	// 查看这个Token是否是“+”
        eat(Token.TokenType.PLUS);
    
    	// 第二个数
        Token right = currentToken;
        // 查看第二个Token是否是数字,并读取下一个Token
        eat(Token.TokenType.INTEGER);
    	// 最后返回运算结果
        return (Integer)left.value + (Integer)right.value;
    }
    

    客户端类Main:

    public class Main {
        public static void main(String[] args) throws Exception {
            Scanner scanner = new Scanner(System.in);
            while (true){
                System.out.print("calc> ");
                String text = scanner.nextLine();
                if (text.equals("exit"))
                    break;
                Interpreter interpreter = new Interpreter(text); <--使用解释器进行解释
                int res = interpreter.expr();
                System.out.println("res: "+res);
            }
        }
    }
    

    运行结果:
    在这里插入图片描述
    运行结果符合预期,part1搞定!

    三、part2

    资料链接:https://ruslanspivak.com/lsbasi-part2/

    part2相较于part1增加了“-”(减法)的支持,然后可以跳过表达式中间的空格。

    增加的函数或修改:

    成员变量添加currentChar,代表当前指针指向的字符:

    ...
    private Character currentChar;
    

    构造函数添加currentChar的初始化:

    public Interpreter(String text){
        ...
        this.currentChar = text.charAt(pos);
    }
    

    添加指针向前的函数advance():

    private void advance(){
        pos++;
        if (pos>text.length()-1){
            this.currentChar = null;
        }
        else {
            this.currentChar = text.charAt(pos);
        }
    }
    

    添加跳过空格函数:

    private void skipWhitespace(){
        while (this.currentChar!=null && this.currentChar==' '){
            this.advance();
        }
    }
    

    读取整形数字函数:

    private int integer(){
        StringBuilder sb = new StringBuilder();
        while (currentChar!=null && Character.isDigit(currentChar)){
            sb.append(currentChar);
            this.advance();
        }
        return Integer.parseInt(sb.toString());
    }
    

    有了这个函数,我们就可以读出多位的数字了。

    修改getNextToken()函数:

    private Token getNextToken() throws Exception {
        while (currentChar != null){
        	// 如果是空格则直接跳过
            if (this.currentChar == ' '){
                this.skipWhitespace();
                continue;
            }
            // 如果是数字则交给integer函数读取处理
            if (Character.isDigit(currentChar)){
                return new TK_Integer(this.integer());
            }
            // 如果是“+”或者“-”则返回TK_Plus和TK_Minus
            if (currentChar=='+'){
                this.advance();
                return new TK_Plus();
            }
            if (currentChar=='-'){
                this.advance();
                return new TK_Minus();
            }
            // 如果是其他情况则抛出异常
            this.error();
        }
        // 如果为currentChar空则返回TK_EOF
        return new TK_EOF();
    }
    

    修改expr函数:

    public int expr() throws Exception {
        currentToken = getNextToken();
    
        Token left = currentToken;
        eat(Token.TokenType.INTEGER);
    
        Token op = currentToken;
        if (op.type == Token.TokenType.PLUS)
            eat(Token.TokenType.PLUS);
        else
            eat(Token.TokenType.MINUS);
    
        Token right = currentToken;
        eat(Token.TokenType.INTEGER);
    
        if (op.type == Token.TokenType.PLUS)
            return (Integer)left.value + (Integer)right.value;
        else
            return (Integer)left.value - (Integer)right.value;
    }
    

    和part1的基本相同,这里不再赘述。

    运行结果:
    在这里插入图片描述

    四、part3

    资料链接:https://ruslanspivak.com/lsbasi-part3/

    以下图片为大佬博客翻译的内容:
    在这里插入图片描述
    在这里插入图片描述

    part3的任务是可以解析多个操作数的加减运算,如“1+2 -3 +4”等。

    添加或修改的部分:

    添加term()函数:

    public int term() throws Exception {
        Token token = currentToken;
        this.eat(Token.TokenType.INTEGER);
        return (Integer) token.value;
    }
    

    其实就是eat+返回int的值

    修改expr函数:

    public int expr() throws Exception {
        currentToken = getNextToken();
    
    	// 获取当前Token的值并移动指针
        int result = this.term();
        while (currentToken.type == Token.TokenType.PLUS || currentToken.type== Token.TokenType.MINUS){
            Token token = currentToken;
            if (token.type== Token.TokenType.PLUS){
            	// 判断并移动
                eat(Token.TokenType.PLUS);
                // 加上下一个数并移动指针
                result+=term();
            }
            else{
            	// 解析同上
                eat(Token.TokenType.MINUS);
                result-=term();
            }
        }
        return result;
    }
    

    运行结果:
    在这里插入图片描述

    下一篇:手写Pascal解释器(二)

    展开全文
  • 原来我们的分析工作全部都是放在Interpreter类中完成,但到了现在的阶段,我们将解析的工作放到两个类中进行完成,即原Interpreter类被分解为Lexer和Interpreter类(即Lexer为词法分析,Interpreter现在为语法分析...

    一、part4

    承接上次的内容,我们继续编写part4,这个部分我们的任务是完成输入一个仅带乘除运算符的表达式,然后返回表达式的结果。

    主要修改或添加的内容:

    原来我们的分析工作全部都是放在Interpreter类中完成,但到了现在的阶段,我们将解析的工作放到两个类中进行完成,即原Interpreter类被分解为Lexer和Interpreter类(即Lexer为词法分析器,Interpreter现在为语法分析器):
    在这里插入图片描述
    Lexer类的职责是将字符串根据各个部分的起始字符将其解析成各个Token,如一个空格隔开的子字符串若以数字开头,则解析为TK_Interger,若为“+”开头,则解析为TK_Plus等等。

    而Interpreter类则是进行语法分析,如某个结构的表达是:expr : term ((PLUS | MINUS) term)*,则Interpreter类中的某个函数则会调用Lexer类的接口获得每一个Token然后检验结构是否符合预期,并给出总的语法结构的结果。

    先看Lexer类:
    成员变量和构造函数(将原来Interpreter类中与解析Token相关的变量都移到了Lexer类中):

    private final String text;
    private int pos;
    private Character currentChar;
    
    public Lexer(String text){
        this.text = text;
        this.pos = 0;
        this.currentChar = this.text.charAt(pos);
    }
    

    其他解析Token的函数基本都原封不动的搬了过来,只看一下getNextToken()函数:

    public Token getNextToken() throws Exception {
        while (currentChar != null){
            if (isSpace(this.currentChar)){
                this.skipWhitespace();
                continue;
            }
            if (Character.isDigit(currentChar)){
                return new TK_Integer(this.integer());
            }
            if (currentChar=='*'){
                this.advance();
                return new TK_Mul();
            }
            if (currentChar=='/'){
                this.advance();
                return new TK_Div();
            }
            this.error();
        }
        return new TK_EOF();
    }
    

    将对“+”和“-”的解析改为了对“*”和“/”的解析。

    Interpreter类:

    成员变量和构造函数:

    private final Lexer lexer;
    private Token currentToken;
    
    public Interpreter(Lexer lexer) throws Exception {
        this.lexer = lexer;
        this.currentToken = this.lexer.getNextToken();
    }
    

    lexer变成了Interpreter类的一个成员,调用它提供的接口来读取Token。

    补充理论知识

    资料来源:https://ruslanspivak.com/lsbasi-part4/

    先来看看我们目标的翻译结构:
    在这里插入图片描述
    expr翻译成中文应该是初中就学过的数学概念,“多项式”的“项”,而factor翻译为中文应该是“因数”,从上面的结构可以看出,“项”可以是一个“因数”,也可以是多个因数进行若干次相乘除获得;而“因数”是由“整数”构成的

    得到结构后如何使用代码进行处理?

    链接博客的大佬给了我们一个普遍性的翻译方案:
    在这里插入图片描述
    ① 每个规则R可以翻译为一个函数
    ② 替代项(a1 | a2 | aN)成为if-elif-else 语句
    ③ 可选的分组(…)*成为while语句,可以循环零次或多次
    ④ 每个Token引用T都成为对eat方法的调用:eat(T)。如果当前的Token与已经写好的Token相匹配,则调用eat,即进行类型判断,然后从词法分析器中获得一个新的Token赋值到current_token成员变量。

    这样,我们即可根据写出的生成式和翻译规则对输入的字符串进行相应的处理了。

    factor()函数:

    private int factor() throws Exception {
    	// factor : INTEGER
        Token token = currentToken;
        eat(Token.TokenType.INTEGER);
        return (Integer) token.value;
    }
    

    expr()函数:

    public intexpr() throws Exception {
    	// term : factor ((MUL | DIV) factor)*
        int result = factor();
    
        while (currentToken.type == Token.TokenType.MUL || currentToken.type == Token.TokenType.DIV){
            Token token = currentToken;
            if (token.type == Token.TokenType.MUL){
                eat(Token.TokenType.MUL);
                result *= factor();
            }
            else {
                eat(Token.TokenType.DIV);
                result /= factor();
            }
        }
    
        return result;
    }
    

    客户端使用:

    public class Main {
        public static void main(String[] args) throws Exception {
            Scanner scanner = new Scanner(System.in);
            while (true){
                System.out.print("calc> ");
                String text = scanner.nextLine();
                if (text.equals("exit"))
                    break;
                Lexer lexer = new Lexer(text);
                Interpreter interpreter = new Interpreter(lexer);
                int res = interpreter.expr();
                System.out.println("res: "+res);
            }
        }
    }
    

    运行结果:
    在这里插入图片描述

    二、part5

    part5的任务是在原来支持乘除的基础上加入加减运算。

    设计生成式

    资料来源:https://ruslanspivak.com/lsbasi-part5/

    加入了加减后,表达式的计算就出现了优先级,即“*”、“/”的优先级高于“+”、“-”的优先级。

    在这里插入图片描述
    由优先级表如何构造语法规则:
    在这里插入图片描述
    谷歌翻译的结果:(英文太菜😂)

    以下是有关如何根据优先级表构造语法的规则:

    1. 为每个优先级定义一个非终结符。非终端产品的主体应包含该级别的算术运算符和下一个更高优先级的非终端产品。
    2. 为基本的表达单位(在我们的情况下为整数)创建一个附加的非终止因子。一般规则是,如果您具有N个优先级,则总共将需要N + 1个非末端:每个级别一个非末端,再加上一个基本表达单元的非末端。

    大佬给出的生成式:
    在这里插入图片描述
    表达式是一个项,或是一个项进行若干次加减运算得到。乘除运算的优先级是高于加减运算的,故加减运算需要包含下一个更高优先级的非终结符,即通过若干乘除运算得到的term。
    在这里插入图片描述
    项是一个因数,或是一个因数进行若干次乘除运算得到。
    在这里插入图片描述
    因数是一个整数。

    有了生成式和翻译规则,我们就可以轻松写出代码:

    expr()函数:

    public int expr() throws Exception {
        /*
        expr   : term ((PLUS | MINUS) term)*
        term   : factor ((MUL | DIV) factor)*
        factor : INTEGER
         */
        int result = term();
    
        while (currentToken.type == Token.TokenType.PLUS || currentToken.type == Token.TokenType.MINUS){
            Token token = currentToken;
            if (token.type == Token.TokenType.PLUS){
                eat(Token.TokenType.PLUS);
                result += term();
            }
            else {
                eat(Token.TokenType.MINUS);
                result -= term();
            }
        }
        return result;
    }
    

    term()函数:

    private int term() throws Exception {
        // term : factor ((MUL | DIV) factor)*
        int result = factor();
        while (currentToken.type == Token.TokenType.MUL || currentToken.type == Token.TokenType.DIV){
            Token token = currentToken;
            if (token.type == Token.TokenType.MUL){
                eat(Token.TokenType.MUL);
                result *= factor();
            }
            else {
                eat(Token.TokenType.DIV);
                result /= factor();
            }
        }
        return result;
    }
    

    factor()函数:

    private int factor() throws Exception {
        // factor : INTEGER
        Token token = currentToken;
        eat(Token.TokenType.INTEGER);
        return (Integer) token.value;
    }
    

    运行结果:
    在这里插入图片描述

    三、part6

    资料来源:https://ruslanspivak.com/lsbasi-part6/

    part6的任务是在前面的基础上添加括号(即“(”和“)”)的支持。

    更新后的生成式:
    在这里插入图片描述
    从图中也可以看出,因数除了是一个整数,也可以是由左右括号包含的表达式,而且由前面的根据优先级编写生成式的方法也可以知道,左右括号的优先级是比带有乘除运算的expr的优先级要更高的。

    由生成式,我们只需要添加两个Token:TK_Lparen和TK_Rparen,以及修改factor()函数实现即可。

    添加Token较为简单,这里不再赘述。

    更新后的factor():

    private int factor() throws Exception {
        // factor : INTEGER | LPAREN expr RPAREN
        Token token = currentToken;
        if (currentToken.type == Token.TokenType.INTEGER){
            eat(Token.TokenType.INTEGER);
            return (Integer) token.value;
        }
        else {
            eat(Token.TokenType.LPAREN);
            int result = expr();
            eat(Token.TokenType.RPAREN);
            return result;
        }
    }
    

    运行结果:
    在这里插入图片描述
    至此,我们完成了part6的内容,现在我们的表达式解析器已经可以解析几乎大部分的加减乘除表达式了!

    上一篇:手写Pascal解释器(一)
    下一篇:手写Pascal解释器(三)

    展开全文
  • 文章目录一、part7抽象语法树和具体语法树(解析树)代码实现 一、part7 ...解析树记录了解析应用于识别输入的一系列规则。 语法分析树的根标有语法开始符号。 每个内部节点代表一个非终结符,也就是说

    一、part7

    资料来源:https://ruslanspivak.com/lsbasi-part7/
    在这里插入图片描述
    看作者博客的标题就知道,这一节我们需要完成抽象语法树的功能。

    抽象语法树和具体语法树(解析树)

    在这里插入图片描述
    例如这个表达式的例子(2 * 7 + 3)就形成了这样的一棵抽象语法树。

    而该表达式的解析树(具体语法树)如下图所示:
    在这里插入图片描述

    • 解析树记录了解析器应用于识别输入的一系列规则。
    • 语法分析树的根标有语法开始符号。
    • 每个内部节点代表一个非终结符,也就是说,它代表一个语法规则应用程序,例如本例中的expr,term或factor。
    • 每个叶节点代表一个Token。

    两者的区别:
    在这里插入图片描述

    • AST使用运算符/操作作为根节点和内部节点,并使用操作数作为其子节点。
    • 与解析树不同,AST不使用内部节点表示语法规则。
    • AST不能代表真实语法中的每个细节(这就是为什么它们被称为abstract)的原因,例如,没有规则节点也没有括号。
    • 与相同语言结构的分析树相比,AST的密度更高。

    如何在AST中对运算符优先级进行编码?

    In order to encode the operator precedence in AST, that is, to represent that “X happens before Y” you just need to put X lower in the tree than Y. And you’ve already seen that in the previous pictures.

    为了在AST中编码运算符优先级,即表示“ X发生在Y之前”,您只需要在树中将X放到比Y低的位置即可。并且您已经在上一张图片中看到了。
    在这里插入图片描述

    代码实现

    先编写抽象语法树接口 AST

    public interface AST {
    }
    

    (它确实是一个空接口,只是为了实现多态)

    二元运算符节点:

    public class BinOp implements AST {
        public Token op;
        public AST left;
        public AST right;
    
        public BinOp(AST left, Token op, AST right){
            this.left = left;
            this.op = op;
            this.right = right;
        }
    }
    

    数字(整数)节点:

    public class Num implements AST {
        public Token token;
        public int value;
    
        public Num(Token token){
            this.token = token;
            this.value = (Integer) token.value;
        }
    }
    

    原有的Lexer类不做改变(原来也已经说明过,Lexer类的职责是读取字符串并将它分解为各个Token)。

    新增Parser类(语法解析器类,生成抽象语法树):
    (把原来的Interpreter类的一些功能划分到了它的身上,原来是返回各个部分的值,而这个时候返回各个部分合成的解析树)

    public class Parser {
        private final Lexer lexer;
        private Token currentToken;
    
        public Parser(Lexer lexer) throws Exception {
            this.lexer = lexer;
            this.currentToken = this.lexer.getNextToken();
        }
    
        private void error() throws Exception {
            throw new Exception("Invalid syntax");
        }
    
        private void eat(Token.TokenType tokenType) throws Exception {
            if (currentToken.type == tokenType){
                currentToken = lexer.getNextToken();
            }
            else {
                this.error();
            }
        }
    
        private AST factor() throws Exception {
            // factor : INTEGER | LPAREN expr RPAREN
            Token token = currentToken;
            if (currentToken.type == Token.TokenType.INTEGER){
                eat(Token.TokenType.INTEGER);
                return new Num(token);
            }
            else {
                eat(Token.TokenType.LPAREN);
                AST result = expr();
                eat(Token.TokenType.RPAREN);
                return result;
            }
        }
    
        private AST term() throws Exception {
            // term : factor ((MUL | DIV) factor)*
            AST node = factor();
            while (currentToken.type == Token.TokenType.MUL || currentToken.type == Token.TokenType.DIV){
                Token token = currentToken;
                if (token.type == Token.TokenType.MUL){
                    eat(Token.TokenType.MUL);
                }
                else {
                    eat(Token.TokenType.DIV);
                }
    
                node = new BinOp(node, token, this.factor());
            }
            return node;
        }
    
        private AST expr() throws Exception {
            /*
            expr   : term ((PLUS | MINUS) term)*
            term   : factor ((MUL | DIV) factor)*
            factor : INTEGER | LPAREN expr RPAREN
             */
            AST node = term();
    
            while (currentToken.type == Token.TokenType.PLUS || currentToken.type == Token.TokenType.MINUS){
                Token token = currentToken;
                if (token.type == Token.TokenType.PLUS){
                    eat(Token.TokenType.PLUS);
                }
                else {
                    eat(Token.TokenType.MINUS);
                }
                node = new BinOp(node, token, term());
            }
            return node;
        }
    
        public AST parse() throws Exception {
            return this.expr();
        }
    }
    

    parser类接受一个lexer对象,职责是接收lexer对象将字符串转化为的多个token,输出表达式对应的抽象语法树AST。

    NodeVisitor类,访问各个AST节点的基类(使用反射进行实现)

    public abstract class NodeVisitor {
    	// 调用visit方法时,先使用反射得到AST子类具体的类名,即className,
    	// 然后调用"visit"+className的方法,如若node为BinOp,则调用visitBinOp()方法
    	// 使用反射大大提高了编码实现的灵活性
        protected int visit(AST node) throws Exception {
            String[] strings = node.getClass().getName().split("\\.");
            String className = strings[strings.length-1];
    
            Method visitMethod = this.getClass().getDeclaredMethod("visit"+className, AST.class);
            return (int) visitMethod.invoke(this, node);
        }
        protected void genericVisit() throws Exception {
            throw new Exception("No this type to visit");
        }
        // 写好访问各个节点的接口方法,供实现类来实现
        abstract int visitBinOp(AST node) throws Exception;
        abstract int visitNum(AST node);
    }
    

    该类的职责是定义AST visitor需要完成的接口,以及使用反射使访问多种类型的AST变得简单。

    最后是Interpreter类

    public class Interpreter extends NodeVisitor {
    	// 成员变量parser
        private final Parser parser;
    
        public Interpreter(Parser parser){
            this.parser = parser;
        }
    
        @Override
        protected int visitBinOp(AST node) throws Exception {
            BinOp binOp = (BinOp)node;
            int res = 0;
            switch (binOp.op.type){
                case PLUS:
                    res = visit(binOp.left) + visit(binOp.right);
                    break;
                case MINUS:
                    res = visit(binOp.left) - visit(binOp.right);
                    break;
                case MUL:
                    res = visit(binOp.left) * visit(binOp.right);
                    break;
                case DIV:
                    res = visit(binOp.left) / visit(binOp.right);
                    break;
            }
            return res;
        }
    
        @Override
        protected int visitNum(AST node) {
            return ((Num)node).value;
        }
    
        public int interpret() throws Exception {
            AST tree = parser.parse();
            return this.visit(tree);
        }
    }
    

    Interpreter类的接收一个parser对象,其对应的职责是接收parser对象调用parser后得到的AST,即抽象语法树,然后遍历抽象语法树的各个节点,最后将语法树所代表的表达式的值输出出来。

    总结各个类的职责(摘自大佬博客):
    在这里插入图片描述

    客户端类Main:

    public class Main {
        public static void main(String[] args) throws Exception {
            Scanner scanner = new Scanner(System.in);
            while (true) {
                System.out.print("spi> ");
                String text = scanner.nextLine();
                if (text.equals("exit"))
                    break;
                Lexer lexer = new Lexer(text);
                Parser parser = new Parser(lexer);
                Interpreter interpreter = new Interpreter(parser);
                int res = interpreter.interpret();
                System.out.println("res: " + res);
            }
        }
    }
    

    运行结果:
    在这里插入图片描述

    二、part8

    资料来源:https://ruslanspivak.com/lsbasi-part8/

    这一part我们主要要完成一元运算符的功能,在我们之前看起来好像已经完成了计算加减乘除表达式的所有功能,实际上我们还无法计算像这样一些表达式:+1 -3,(-1)*4,(-2-3) * (+4-5)等等这些带正负号的数,这一节我们就是为了解决正负号这种一元操作符无法表达的问题。

    修改或新增的类:

    由于我们只是增加一个新语法,所以词法解析器Lexer的代码是完全不用修改的,主要需要修改Parser和添加一个AST的子类来代表一元运算符节点,而增加了一个新类型的节点后,我们自然还需要添加访问这个新类型节点的方法,因此我们还需要为NodeVisitor编写新接口visitUnaryOp,并为实现类Interpreter添加对应的方法实现。

    一元运算符节点类UnaryOp:

    public class UnaryOp implements AST {
        public Token op;
        public AST expr;
    
        public UnaryOp(Token op, AST expr){
            this.op = op;
            this.expr = expr;
        }
    }
    

    和二元操作符节点BinOp类非常的类似,不细说。

    然后一元运算符应该是属于factor(因数)生成式的一部分,如:-5*3,(-5)整体应该是一个因数
    故factor的生成式可修改为:

    factor : (PLUS | MINUS) factor | INTEGER | LPAREN expr RPAREN
    

    由此,我们只需为Parser的factor函数添加一种情况即可:

    factor函数:

    private AST factor() throws Exception {
        // factor : (PLUS | MINUS) factor | INTEGER | LPAREN expr RPAREN
        Token token = currentToken;
        // ++++++这部分是添加的代码
        if (token.type == Token.TokenType.PLUS || token.type == Token.TokenType.MINUS){
            if (token.type == Token.TokenType.PLUS){
                eat(Token.TokenType.PLUS);
            }
            else {
                eat(Token.TokenType.MINUS);
            }
            return new UnaryOp(token, factor());
        }
        // ++++++++++++++++++++++
        else if (currentToken.type == Token.TokenType.INTEGER){
            eat(Token.TokenType.INTEGER);
            return new Num(token);
        }
        else {
            eat(Token.TokenType.LPAREN);
            AST result = expr();
            eat(Token.TokenType.RPAREN);
            return result;
        }
    }
    

    NodeVisitor类:

    public abstract class NodeVisitor {
        ...
        abstract int visitUnaryOp(AST node) throws Exception;
    }
    

    使用反射的巧妙之处就体现出来了,在这里我们就只需要添加新函数即可,而无需为新类型添加判断之类的新的操作,代码维护起来非常方便。

    Interpreter.visitUnaryOp函数:

    @Override
    int visitUnaryOp(AST node) throws Exception {
        UnaryOp unaryOp = (UnaryOp)node;
        if (unaryOp.op.type== Token.TokenType.PLUS){
            return +visit(unaryOp.expr);
        }
        else {
            return -visit(unaryOp.expr);
        }
    }
    

    运行效果:
    在这里插入图片描述
    至此,我们已经完成所有的解析四则运算表达式的功能。

    上一篇:手写Pascal解释器(二)
    下一篇:未完待续

    展开全文
  • 贵宾 一种带有手写词法分析,解析,编译器和自定义字节码解释vm的小语言
  • 本章将在前两章的基础上编写一个简单的解释器,并且实现大约150条指令。 一、字节码和指令集 字节码就是运行在Java虚拟机上的机器码。我们已经知道,每一个类或者接口都会被Java编译器编译成一个class文件,类或接口...

空空如也

空空如也

1 2 3 4 5 ... 11
收藏数 217
精华内容 86
关键字:

手写解释器