蛙蛙推薦:蛙蛙教你發明一種新語言之一--詞法分析和語法分析

摘要

程序開發行業中有很多種編程語言,每個程序員大概也都會一兩種,可你有沒有想過自己DIY一種語言呢,本文就帶你用.net DIY一種新語言--WawaSharp,我們將定義語法,實現詞法分析,建立語法樹,代碼生成幾個過程。


引言

不要爲摘要裏的那些名詞嚇住了,什麼詞法分析,語法樹之類的,其實要實現一個簡單的語言並不複雜,就是做一些字符串的操作,以及運用幾個IL指令。以前我也以爲很複雜,很神祕,直到我發現瞭如下這篇帖子
創建 .NET Framework 語言編譯器

http://msdn.microsoft.com/zh-cn/magazine/cc136756.aspx

本文也是根據這篇帖子來的,只是在其基礎上支持了更多的語句和表達式,所以大家可以先看懂這篇文章,本文對這篇文章裏講到的東西也就不再詳細重複。

我們要提高一個程序靈活性的時候常常把一些變量做成配置,這時候需求變了的話,修改一下配置就可以滿足需求了,可有時候配置不足以滿足需求的變化,所以一些更NB的程序,可以提供一個SDK及一套自定義語言讓使用者去二次開發,今天我們發明的語言就可以去當作這種場景下的自定義語言。另外一個目的就是和大家一起了解一下一個語言背後的故事,我們寫的文本代碼是如何變成可執行的程序的。


語法定義

摘要裏也講了,發明語言的第一步是定義語法,定義語法一般用BNF,我也不懂這是嘛,比貓畫虎做了一個,如下,大家也不用去折騰它到底是啥意思,就掃一眼,憑直覺,能看懂多少算多少。

<stmt> := var <ident> = <expr>
    
| <ident> = <expr>
    
| for <ident> = <expr> to <expr> do <stmt> end
    
| foreach <ident> in <expr> do <stmt> end
    
| if <expr> then <stmt> end
    
| read_int <ident>
    
| print <expr>
    
| <stmt> ; <stmt>
    
| append <expr> <expr>

<expr> := <string>
    
| <int>
    
| <arith_expr>
    
| <ident>
    
| match <expr> <expr>
    
| newsb
    
| len <expr>

<bin_expr> := <expr> <bin_op> <expr>
<bin_op> := + | - | * | / | == | eq

<ident> := <char> <ident_rest>*
<ident_rest> := <char> | <digit>

<int> := <digit>+
<digit> := 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9

<string> := " <string_elem>* "
<string_elem> := <any char other than ">


語法有了,我們看下我們要實現的語言大概是什麼樣子,如下
var input = ""11|222|33|44|55"";
var arr 
= match input ""/d+|"";
var sb 
= newsb;
foreach item in arr do
    print item;
    var l 
= len item;
    
if l eq 2 then
        var arr_ 
= match item ""/d"";
        
foreach item_ in arr_ do
            append sb 
""/r/n"";
            append sb item_;
        end;
    end;
end;
print sb;

綜合語法定義和例子,我們可以看到,我們定義了string,bool,int,enumerable幾種數據類型,var,foreach,if,print,append等幾種語句,還有賦值,match,len,Equals,整形常量,字符串常量等幾種表達式。


詞法分析

所謂詞法分析,就是把文本拆成一個一個的塊兒(token),用過lucene的應該比較熟悉,類似lucene分詞的過程,比如去除停止詞,就是把對程序無關的,比如空白字符,回車字符等刪除掉,然後做一個List,順序把拆除來的有效字符塊兒放進去,比如以上的例子,就會拆出var,input,=,"11|222|33|44|55",;,var,arr,=...等。我們的變量名只支持字母和下劃線,不支持數字。

把需求說清楚了,實現這個應該很簡單吧,就寫一個while循環,一個一個的讀取字符,每滿足一個token規則就放到list裏面一個就行了,僞碼如下


while (input.Peek() != -1)
{
    
char ch = (char)input.Peek();
    
if (char.IsWhiteSpace(ch))
    {
        
//忽略空白字符
        input.Read();
    }
    
else if (char.IsLetter(ch) || ch == '_')
    {
        
//取出標識符,以字母和下劃線組成
    }
    
else if (ch == '"')
    {
        
//取出字符串常量
    }
    
else if (char.IsDigit(ch))
    {
        
//取出數字常量
    }
    
else switch (ch)
    {
        
//取出單字符操作符,如+,-,*,/等
    }
}

 

到此,我們有了一個IList<object>,裏面順序放着我們分析出來的文本塊兒。


語法分析

語法分析是把上一步生成的文本塊序列折騰成一棵樹,叫語法樹,我們得先定義各種抽象數據結構,如順序,分支,循環語句,賦值,比較等表達式等,舉幾個例子如下。
public abstract class Stmt{}
public abstract class Expr {}
public class DeclareVar : Stmt
{
    
public Expr Expr;
    
public string Ident;
}
public class Assign : Stmt
{
    
public Expr Expr;
    
public string Ident;
}
public class Sequence : Stmt
{
    
public Stmt First;
    
public Stmt Second;
}
public class Foreach : Stmt
{
    
public Stmt Body;
    
public string Ident;
    
public Expr IEnumerable;
}
public class If : Stmt {
    
public Stmt Body;
    
public Expr Condition;
}
public class StrLen : Expr
{
    
public Expr Input;
}        
public class Match : Expr
{
    
public Expr Input;
    
public Expr Pattern;
}

意思就是我們要把token序列轉換成一個由上述這些對象組成的一顆樹,差不多是二叉樹,這一步叫語法分析,據說要多複雜有多複雜,但咱們這裏還算比較簡單,也是一個while循環,比如碰到if語句,就根據語法定義if <expr> then <stmt> end,下一個token應該是個表達式,表示if語句的條件,那麼就繼續出一個表達式作爲if語句的Condition樹形,再往後要有一個then,再往後是一個語句,解析出一條完整語句,作爲if的Body樹形,完了最後以一個end結尾,大概僞碼如下吧。
private Stmt ParseStmt() {
    
if (tokens[index].Equals("print")) {
        index
++;
        var print 
= new Print();
        print.Expr 
= ParseExpr();
        result 
= print;
    }
    
else if (tokens[index].Equals("append"))
    {
            index
++;
            var append 
= new Append();
            append.Buider 
= ParseExpr();
            append.ToAppend 
= ParseExpr();
    }
    
else if (tokens[index].Equals("var")) {}
    
else if (tokens[index].Equals("foreach")) {}
    
else if (tokens[index].Equals("if")) {}
    
else{ thr ex;}
    
if (index < tokens.Count && tokens[index] == Scanner.Semi) {
        index
++;    
        
if (index < tokens.Count &&!tokens[index].Equals("end")) {
            var sequence 
= new Sequence();
            sequence.First 
= result;
            sequence.Second 
= ParseStmt();
            result 
= sequence;
        }
    }
}    
private Expr ParseExpr() {
    
if (tokens[index] is StringBuilder) {//StringLiteral}
    else if (tokens[index] is int) {//IntLiteral}
    else if (tokens[index] is string) {
        
string value = (string)tokens[index];
    
if (value.Equals("match")) {//Match}
    else if (value.Equals("newsb")) {//Builder}
    else if (value.Equals("len")) {//Strlen}
    }
}

最終,我們會形成一個語法樹,比如我們示例代碼的分析結果如下
[Sequence]
  
|First:
  
|  |[DeclareVar]
  
|  |  |Ident:input
  
|  |  |Expr:
  
|  |  |  |<StringLiteral>:"11|222|33|44|55"
  
|Second:
  
|  |[Sequence]
  
|  |  |First:
  
|  |  |  |[DeclareVar]
  
|  |  |  |  |Ident:arr
  
|  |  |  |  |Expr:
  
|  |  |  |  |  |<Match>:
  
|  |  |  |  |  |  |Input:
  
|  |  |  |  |  |  |  |<Variable>:input
  
|  |  |  |  |  |  |Pattern:
  
|  |  |  |  |  |  |  |<StringLiteral>:"/d+|"
  
|  |  |Second:
  
|  |  |  |[Sequence]
  
|  |  |  |  |First:
  
|  |  |  |  |  |[DeclareVar]
  
|  |  |  |  |  |  |Ident:sb
  
|  |  |  |  |  |  |Expr:
  
|  |  |  |  |  |  |  |[Builder]
  
|  |  |  |  |Second:
  
|  |  |  |  |  |[Sequence]
  
|  |  |  |  |  |  |First:
  
|  |  |  |  |  |  |  |<Foreach>:item
  
|  |  |  |  |  |  |  |  |IEnumerable:
  
|  |  |  |  |  |  |  |  |  |<Variable>:arr
  
|  |  |  |  |  |  |  |  |Body:
  
|  |  |  |  |  |  |  |  |  |[Sequence]
  
|  |  |  |  |  |  |  |  |  |  |First:
  
|  |  |  |  |  |  |  |  |  |  |  |[Print]
  
|  |  |  |  |  |  |  |  |  |  |  |  |Expr:
  
|  |  |  |  |  |  |  |  |  |  |  |  |  |<Variable>:item
  
|  |  |  |  |  |  |  |  |  |  |Second:
  
|  |  |  |  |  |  |  |  |  |  |  |[Sequence]
  
|  |  |  |  |  |  |  |  |  |  |  |  |First:
  
|  |  |  |  |  |  |  |  |  |  |  |  |  |[DeclareVar]
  
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |Ident:l
  
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |Expr:
  
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |<StrLen>:
  
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |Input:
  
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |<Variable>:item
  
|  |  |  |  |  |  |  |  |  |  |  |  |Second:
  
|  |  |  |  |  |  |  |  |  |  |  |  |  |<If>:
  
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |Condition:
  
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |<BinExpr>:Eq
  
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |left:
  
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |<Variable>:l
  
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |Right:
  
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |<IntLiteral>:2
  
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |Body:
  
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |[Sequence]
  
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |First:
  
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |[DeclareVar]
  
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |Ident:arr_
  
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |Expr:
  
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |<Match>:
  
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |Input:
  
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |<Variable>:item
  
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |Pattern:
  
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |<StringLiteral>:"/d"
  
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |Second:
  
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |<Foreach>:item_
  
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |IEnumerable:
  
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |<Variable>:arr_
  
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |Body:
  
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |[Sequence]
  
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |First:
  
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |[Append]:
  
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |Buider:
  
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |<Variable>:sb
  
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |ToAppend:
  
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |<StringLiteral>:""
  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |Second:
  
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |[Append]:
  
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |Buider:
  
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |<Variable>:sb
  
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |ToAppend:
  
|  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |  |<Variable>:item_
  
|  |  |  |  |  |  |Second:
  
|  |  |  |  |  |  |  |[Print]
  
|  |  |  |  |  |  |  |  |Expr:
  
|  |  |  |  |  |  |  |  |  |<Variable>:sb

我在Stmt和Expr類各定義了個ToString方法來用文本顯示這棵樹,最終結果類似上面這樣的顯示,雖然不是很美,但意思能表達出來,我們的分析結果確實是棵樹,CodeDom有樹,表達式樹有樹,咱們的WawaSharp也得有樹。

看到這裏,大家可以稍微休息休息,目前爲止,我們還沒碰到什麼新鮮的,就是一些while語句和一些字符串拆分邏輯,下一篇會講到代碼生成,就是根據這棵樹生成可執行的IL代碼,有趣的是生成IL代碼後還能用Reflector反編譯成c#和vb代碼,呵呵。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章