1. 程式人生 > 其它 >手寫Pascal直譯器(三)

手寫Pascal直譯器(三)

目錄

一、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直譯器(二)
下一篇:未完待續