從零開始寫個編譯器吧 - tao 語言的文法定義(上)

各位久等了,本系列在新一年的連載中,形式會加入少量變化。首先,我會將 tao 語言編譯器(以及運行環境)的所有內容貼在 GitHub 上,在閱讀本系列的時候,須要對照 GitHub 上的內容。以取代以前一段一段貼代碼的這種形式。此外,本系列也不會像以前那樣貼出每一行代碼,並對其進行講解,取而待之,僅僅對核心代碼和有表明性的代碼進行講解。若有疑問,歡迎評論。java

GitHub 中的 tao 語言編譯器項目node

目前爲止,咱們已經寫好了以下幾個文件:git

- com.taozeyu.taolan.analysis
|- FirstSetConstructor
|- LexicalAnalysis
|- LexicalAnalysisException
|- NonTerminalSymbol
|- SignParser
|- TerminalSymbol
|- Token

上一章中 NonTerminalSymbol.java 文件中留着一個 TODO,如今我把它填上:github

static enum Exp {
    //空白
    SplitSpaceSign, SpaceOrEnter, Space,
    //基本單元
    Enter, This, Null, Boolean,
    Number, Variable, String, RegEx,
    Element,
    //表達式相關
    L0Expression, L0ParamExpression, L0Sign,
    L1Expression, L1ParamExpression,
    L2Expression, L2ParamExpression, L2Sign,
    L3Expression, L3ParamExpression, L3Sign,
    L4Expression, L4ParamExpression, L4Sign,
    L5Expression, L5ParamExpression, L5Sign,
    L6Expression, L6ParamExpression, L6Sign,
    L7Expression, L7ParamExpression, L7Sign,
    L8Expression, L8ParamExpression, L8Sign,
    L9Expression, L9ParamExpression, L9Sign,
    L10Expression, L10ParamExpression, L10Tail, L10TailOperation,
    L11Expression,
    //控制流語法
    Chunk, StartChunk, Line,
    Command, Operate, When,
    DefineVariable, DefineVariableElement,
    DefineFunction, ParamsList,
    IfElseChunk, TryCatch,
    WhileChunk, DoUntilChunk,
    ForEachChunk, ForEachCommand, ForEachCondition,
    //語法糖
    Lambda,
    List, Map, MapEntry,
    Invoker, InvokerBraceless, InvokerBanLambda, InvokerBracelessBanLambda,
    ParamList, ParamListBanTokens ,
    Array, Container,
}

這個 Exp 的枚舉類型表明着一系列被命名了的非終結符。less

以後,咱們須要作如下幾個工做:spa

  1. 定義出 tao 語言的文法。code

  2. 寫一個 Complier-complier,並用它分析以前定義的 tao 語言文法,得出一部分必要的信息,並將這些信息保存在 NonTerminalSymbol 節點中。token

  3. 寫一個 Parser,結合文法定義,以及 Complier-complier 分析出的信息,將一段 tao 語言的源代碼分析成語法樹。get

OK,上面列出的清單就是一個寫出一個可用的 Parser 的一個大體計劃了。咱們一步一步來吧。編譯器

首先,咱們先寫出一個 SyntacticDefine.java 文件。

源代碼我就不貼了,請你們自行打開鏈接觀看。(PS:以後的源代碼文件會愈來愈長,我不必定會貼所有,請你們適應經過打開 GitHub 頁面查看源代碼的方式。)

能夠看出,SyntacticDefine.java 中這個巨大的 static 塊中定義了 tao 語言的所有內容。

在此,我稍微解釋一下我是如何定義 tao 語言的文法的。例如:

node(Exp.Line).or(Exp.Command)
                .or(Exp.Operate)
                .or(Exp.DefineVariable)
                .or(Exp.DefineFunction)
                .or(Exp.IfElseChunk)
                .or(Exp.WhileChunk)
                .or(Exp.DoUntilChunk)
                .or(Exp.ForEachChunk)
                .or(Exp.TryCatch)

每個 Exp 枚舉類型都表明一個被命名的非終結符,如上這個定義表明以下一組展開式:

Line -> Command | Operate | DefineVariable ...

注意到,Line 這個非終結符存在不少中展開式,即它可能被展開爲多種形式,所以在 static 塊中,使用 or() 方法將其並列。

node(Exp.Array).or(token(Type.Sign, "["), Exp.SpaceOrEnter, Exp.List, Exp.SpaceOrEnter, token(Type.Sign, "]"))

一個非終結符能夠被展開稱爲一個串,如上定義即是將 Array 這個非終結符展開稱爲一個又終結符和非終結符混合而成的串。

我使用 token() 這個方法來定義終結符,例如 token(Type.Sign, "[") 則定義了一個左方括號的終結符。因爲 Array 只有一種展開形式,所以只調用了一次 or 方法。

node(Exp.SplitSpaceSign).or(token(Type.Space)).sign('+')

對非終結符可使用量詞進行修飾,我使用 sign() 方法來表明量詞。如上這行定義,表明 SplitSpaceSign 這個非終結符能夠展開爲 1~N 個 space 類型的終結符。(特別注意:我定義的 sign() 方法僅僅用於修飾非終結符,而非展開式,雖然這個例子中個人 sign 方法更靠近 or 方法,但並意味着 sign 用於修飾展開式。)

node(Exp.L2ParamExpression).or(Exp.L3ParamExpression, node().or(Exp.L2Sign, Exp.L3ParamExpression).sign('*'))

在展開式中能夠加入匿名非終結符,即調用 node 方法,但參數留空。匿名非終結符可讓我很方便的定義某些只在展開式中出現一次的非終結符,這樣我就沒有必要爲每個非終結符起一個名字了。

被命名的非終結符和匿名非終結符沒有任何區別,它們僅僅就是形式上的不一樣罷了,請勿把它們當成有實質不一樣的東西。

相關文章
相關標籤/搜索