【編譯原理】手工打造詞法分析器

大数据王小皮發表於2024-03-28

難點:

  • 如何拆詞?如何定義分隔符?
  • 匹配的優先順序是什麼?

關鍵點:

  • 有限自動機
  • 元素拆分

解析 age >= 45

為了入門字詞是如何拆分識別的,我們舉一個最簡單的例子age >= 45

  • 只有三種型別:識別符號(age)、大於號(GE)、數字字面量(IntLiteral)
  • 使用空格分隔不同的元素

思路:

  • 從左到右依次讀取字串
  • 使用有限自動機,根據讀到的字元進行狀態轉換,狀態機如下

image.png

先上程式碼,理解一下上述過程,也可以除錯進去看看執行的邏輯是什麼樣的。
SimpleToken.java

/**
 * Token的一個簡單實現。只有型別和文字值兩個屬性。
 */
public final class SimpleToken implements Token {
    //Token型別
    public TokenType type = null;

    //文字值
    public String text = null;


    @Override
    public TokenType getType() {
        return type;
    }

    @Override
    public String getText() {
        return text;
    }
}

public interface Token{
    public TokenType getType();
    public String getText();
}

SimpleTokenReader

public class SimpleTokenReader implements TokenReader {
    List<Token> tokens = null;
    int pos = 0;

    public SimpleTokenReader(List<Token> tokens) {
        this.tokens = tokens;
    }

    @Override
    public Token read() {
        if (pos < tokens.size()) {
            return tokens.get(pos++);
        }
        return null;
    }

    @Override
    public Token peek() {
        if (pos < tokens.size()) {
            return tokens.get(pos);
        }
        return null;
    }

    @Override
    public void unread() {
        if (pos > 0) {
            pos--;
        }
    }

    @Override
    public int getPosition() {
        return pos;
    }

    @Override
    public void setPosition(int position) {
        if (position >=0 && position < tokens.size()){
            pos = position;
        }
    }
}


public interface TokenReader{
    public Token read();
    public Token peek();
    public void unread();
    public int getPosition();
    public void setPosition(int position);
}

MyLexer.java

public class MyLexer {
    private StringBuffer tokenText = null;   //臨時儲存token的文字
    private List<Token> tokens = null;       //儲存解析出來的Token
    private SimpleToken token = null;        //當前正在解析的Token


    public static void main(String[] args) {
        MyLexer lexer = new MyLexer();

        String script = "age >= 45";
        System.out.println("parse: " + script);
        SimpleTokenReader tokenReader = lexer.tokenize(script);
        dump(tokenReader);
    }

    //是否是字母
    private boolean isAlpha(int ch) {
        return ch >= 'a' && ch <= 'z' || ch >= 'A' && ch <= 'Z';
    }

    //是否是數字
    private boolean isDigit(int ch) {
        return ch >= '0' && ch <= '9';
    }

    //是否是空白字元
    private boolean isBlank(int ch) {
        return ch == ' ' || ch == '\t' || ch == '\n';
    }

    // 有限狀態機的各種狀態。
    private enum DfaState {
        Initial,

        Id, GT, GE,

        IntLiteral
    }

    /**
     * 有限狀態機進入初始狀態。
     * 這個初始狀態其實並不做停留,它馬上進入其他狀態。
     * 開始解析的時候,進入初始狀態;某個Token解析完畢,也進入初始狀態,在這裡把Token記下來,然後建立一個新的Token。
     */
    private DfaState initToken(char ch) {
        if (tokenText.length() > 0) {
            token.text = tokenText.toString();
            tokens.add(token);

            tokenText = new StringBuffer();
            token = new SimpleToken();
        }

        DfaState newState = DfaState.Initial;
        if (isAlpha(ch)) {              //第一個字元是字母
            newState = DfaState.Id; //進入Id狀態
            token.type = TokenType.Identifier;
            tokenText.append(ch);
        } else if (isDigit(ch)) {       //第一個字元是數字
            newState = DfaState.IntLiteral;
            token.type = TokenType.IntLiteral;
            tokenText.append(ch);
        } else if (ch == '>') {         //第一個字元是>
            newState = DfaState.GT;
            token.type = TokenType.GT;
            tokenText.append(ch);
        } else {
            newState = DfaState.Initial; // skip all unknown patterns
        }
        return newState;
    }


    /**
     * 解析字串,形成Token。
     * 這是一個有限狀態自動機,在不同的狀態中遷移。
     * @param code
     * @return
     */
    public SimpleTokenReader tokenize(String code) {
        tokens = new ArrayList<Token>();
        CharArrayReader reader = new CharArrayReader(code.toCharArray());
        tokenText = new StringBuffer();
        token = new SimpleToken();
        int ich = 0;
        char ch = 0;
        DfaState state = DfaState.Initial;
        try {
            while ((ich = reader.read()) != -1) {
                ch = (char) ich;
                switch (state) {
                    case Initial:
                        state = initToken(ch);          //重新確定後續狀態
                        break;
                    case Id:
                        if (isAlpha(ch) || isDigit(ch)) {
                            tokenText.append(ch);       //保持識別符號狀態
                        } else {
                            state = initToken(ch);      //退出識別符號狀態,並儲存Token
                        }
                        break;
                    case GT:
                        if (ch == '=') {
                            token.type = TokenType.GE;  //轉換成GE
                            state = DfaState.GE;
                            tokenText.append(ch);
                        } else {
                            state = initToken(ch);      //退出GT狀態,並儲存Token
                        }
                        break;
                    case IntLiteral:
                        if (isDigit(ch)) {
                            tokenText.append(ch);       //繼續保持在數字字面量狀態
                        } else {
                            state = initToken(ch);      //退出當前狀態,並儲存Token
                        }
                        break;
                    default:
                }
            }
            // 把最後一個token送進去
            if (tokenText.length() > 0) {
                initToken(ch);
            }
        } catch (IOException e) {
            e.printStackTrace();
        }

        return new SimpleTokenReader(tokens);
    }

    public static void dump(SimpleTokenReader tokenReader){
        System.out.println("text\ttype");
        Token token = null;
        while ((token= tokenReader.read())!=null){
            System.out.println(token.getText()+"\t\t"+token.getType());
        }
    }

}

不難理解,對吧。
無非就是在 tokenize 函式中挨個讀取字串,根據上面自動機實現的邏輯。
遇到分隔字元(如空格)就會觸發 initToken 將前面讀取到的字元和型別進行儲存。

你可能會有疑問:
搞這麼複雜幹什麼?按空格切分然後再字串匹配不就行了?

確實可以實現,使用這種方式實現還更簡單,但是我們想要做的是一個更通用的處理邏輯。
如果按照提議的方式,對於更復雜的字串(比如不是空格分隔、空格不一定是分隔符、關鍵字保留等)那就需要更多的人工邏輯來處理,而且會越來越複雜和難以擴充套件,很可能一個特例導致需要推倒重來。

相關文章