編譯抽象文法樹
對大多數開發人員來說,編譯就意謂着産生本地代碼,給人感覺就是一個字,難。但是,并不一定要産生本地代碼,對于 DSL,通常産生其他更加通用的程式設計語言。.NET 架構提供幾個把抽象文法樹編譯成程式的功能。
技術的選擇取決于幾個因素。例如,如果語言針對的是開發人員,那麼,生成文本檔案就足夠了,内容可以是 F#,也可以是其他語言,或者是編譯過的程式集,能在程式中使用;然而,如果針對的是最終使用者,那麼幾乎可以肯定,必須編譯然後動态[ on the fly ]執行。表12-1 彙總了各種可用選項。
表12-1 .NET 的代碼生成技術
技術
描述
Microsoft.CSharp
.CSharpCodeProvider
這個類支援動态産生的 C# 編譯檔案,既可以通過簡單地字元串連接配接,也可以通過使用 System.CodeDom 命名空間。一旦代碼編譯成程式集,就可以動态加載到記憶體,通過反映執行。這個操作相對來說是昂貴的,因為它要求寫到磁盤,并使用反映去執行方法。
System.CodeDom
這是一組類,目标是在不同語言的可用操作之間進行抽象。其思想是,用在這個命名空間下可用的類來描述操作,然後,用提供程式把它編譯成選擇的語言。.NET 為 C# 和 Visual Basic 釋出了提供程式。
FSharp.Compiler.CodeDom.dll
這是随 F# 發行的 CodeDom 提供程式,它可以用于動态編譯 F#,方法與 C# 的 CodeDom 提供程式相似。
System.Reflection.Emit
有了這個命名空間,可以用中間語言建立程式集。由于中間語言提供了比 F#、C#,System.CodeDom 更多的功能,是以,更具靈活性。然而,由于是低級語言,就需要更多的耐心,為了獲得正确的結果,要花更多的時間。
Mono.Cecil
這個庫廣泛用于 Mono 架構,既可以解析程式集,也可以動态建立程式集。
我們System.Reflection.Emit.DynamicMethod 類,并不是因為我們特别需要中間語言的靈活性,而是因為中間語言内置了浮點運算的指令,可以很适合實作我們的小語言。DynamicMethod 還提供了快捷、容易的方法可以調用産生的程式。
createDynamicMethod 方法才真正編譯抽象文法樹,通過掃描抽象文法樹和生成代碼。首先,建立DynamicMethod 類的執行個體,來操作這個中間語言,是定義來表示這個方法的:
let temp = new DynamicMethod("",(type float), paramsTypes, meth.Module)
然後,createDynamicMethod 開始周遊樹。遇到辨別符,輸出的代碼是加載動态方法的參數:
| Ident name ->
il.Emit(OpCodes.Ldarg, paramNames.IndexOf(name))
當遇到文字時,輸出的中間語言代碼是加載這個文字的值:
| Val x ->il.Emit(OpCodes.Ldc_R8, x)
當遇到操作時,必須遞歸評估計算表達式的兩項,然後輸出的是表示要求操作的指令:
| Multi (e1 , e2) ->
generateIlInner e1
generateIlInner e2
il.Emit(OpCodes.Mul)
注意,如何運算是最後輸出的,在表達式的兩項遞歸評估之後。這樣做原因是中間語言基于堆棧,是以,來自其他運算的資料,在評估運算符之前,必須壓棧。
清單 12-7 是完整的編譯器。
清單 12-7 編譯從指令行輸入生成的抽象文法樹
openSystem.Collections.Generic
open System.Reflection
open System.Reflection.Emit
openStrangelights.ExpressionParser
// get a list of all the parameter names
let rec getParamList e =
let
recgetParamListInner e names =
match e
with
| Identname ->
if not (List.exists (funs
-> s = name) names) then
name :: names
else
names
| Multi(e1 , e2) ->
names
|> getParamListInner e1
|> getParamListInner e2
| Div(e1 , e2) ->
|> getParamListInner e1
| Plus(e1 , e2) ->
| Minus(e1 , e2) ->
| _ -> names
getParamListInner e []
// create the dynamic method
letcreateDynamicMethod e (paramNames: string list) =
let generateIl e (il : ILGenerator) =
let
recgenerateIlInner e =
match e
|Ident name ->
let index = List.find_index (fun s
-> s = name)paramNames
il.Emit(OpCodes.Ldarg, index)
|Val x -> il.Emit(OpCodes.Ldc_R8, x)
|Multi (e1 , e2) ->
generateIlInner e1
generateIlInner e2
il.Emit(OpCodes.Mul)
|Div (e1 , e2) ->
il.Emit(OpCodes.Div)
| Plus (e1 , e2) ->
il.Emit(OpCodes.Add)
|Minus (e1 , e2) ->
il.Emit(OpCodes.Sub)
generateIlInner e
il.Emit(OpCodes.Ret)
let paramsTypes = Array.create paramNames.Length (typeof<float>)
let meth = MethodInfo.GetCurrentMethod()
let temp =
newDynamicMethod("", (typeof<float>), paramsTypes, meth.Module)
let il = temp.GetILGenerator()
generateIle il
temp
// function to read the arguments fromthe command line
let collectArgs(paramNames : string list) =
paramNames
|> Seq.map
(fun n
->
printf "%s: " n
box(float(Console.ReadLine())))
|>Array.ofSeq
// the expression to be interpreted
let e = Multi(Val 2.,Plus(Val 2., Ident "a"))
// get a list of all the parameters fromthe expression
let paramNames =getParamList e
// compile the tree to a dynamic method
let dm =createDynamicMethod e paramNames
// print collect arguments from the user
let args =collectArgs paramNames
// execute and print out final result
printf "result:%O" (dm.Invoke(null, args))
運作結果如下:
編譯清單 12-7 的代碼産生下面的結果:
a: 4
result: 32