BOO大全/自訂巨集
上一章:自訂屬性(Attribute) 目錄 下一章:例外處理
自訂Macro
編輯在 Boo 裏的某些述句,像 print 和 using,其實都是以 macro 實作出來的。macro 可以接收引數,也可以擁有程式區塊。收到的引數並不能直接使用,它們被編譯為 AST (Abstract Syntax Tree) 運算式以進行操作,也就是說,引數並沒有真正被賦值(evaluate)。Macro 通常用來產生代碼。實際上存在於 DLL(組件)裏面,使用 macro 的代碼會參照 macro 所在的組件。它們會在編譯時期以真正的代碼取代。
下面是一個簡化版的 print macro:
import Boo.Lang.Compiler
import Boo.Lang.Compiler.Ast
import Boo.Lang.Compiler.Ast.AstUtil
class PrntMacro(AbstractAstMacro):
override def Expand(macro as MacroStatement):
li = macro.LexicalInfo
block = Block()
for arg in macro.Arguments:
block.Add(CreateMethodInvocationExpression(li,
CreateReferenceExpression("System.Console.Write"),
arg
))
block.Add(CreateMethodInvocationExpression(li,
CreateReferenceExpression("System.Console.Write"),
StringLiteralExpression(" ")
))
block.Add(CreateMethodInvocationExpression(li,
CreateReferenceExpression("System.Console.WriteLine"),
StringLiteralExpression("")
))
return block
如同自訂屬性(Attribute)一樣,macro 的類別名稱必須以 'Macro' 結尾,而 macro 的名稱則必須使用大寫放在 'Macro' 前面。macro 做的事情很直覺,但就是需要用很多代碼。我們建立一個 AST 區塊,然後為每個引數加上 System.Console.Write以印出引數,每個引數中間以空白分隔,最後再使用 System.Console.WriteLine 來分行。
我們可以把 block.Add 這個動作重新整理一下,這邊是另外一個例子,display,這在除錯時很有用:
import Boo.Lang.Compiler
import Boo.Lang.Compiler.Ast
import Boo.Lang.Compiler.Ast.AstUtil
class DisplayMacro(AbstractAstMacro):
static Write = "System.Console.Write"
static WriteLine = "System.Console.WriteLine"
def L(s as string):
return StringLiteralExpression(s)
def Call(li as LexicalInfo, block as Block, name as string, expr as Expression):
block.Add(CreateMethodInvocationExpression(li,
CreateReferenceExpression(name),
expr
))
override def Expand(macro as MacroStatement):
li = macro.LexicalInfo
block = Block()
for arg in macro.Arguments:
Call(li,block,Write,L(arg.ToString()))
Call(li,block,Write,L(" = "))
Call(li,block,Write,arg)
Call(li,block,Write,L(" "))
Call(li,block,WriteLine,L(""))
return block
重整以後,主要的迴圈變得比較清楚了。我們將引數當作字串輸出,再接着輸出引數值。這邊是使用 display 的範例:
# @compile{booc -r:DisplayMacro.dll TestDisplayMacro.boo}
x = 20
s = "hello dolly"
z = 2.3
display x,s,z+2
輸出結果
x = 20 s = hello dolly z + 2 = 4.3
使用 macro 時有一些重要的限制。他們操作語法,但是無法知道 s 是個字串變數(以上面為例),所以無法針對 s 加上雙引號以表示 s 是字串。
Macro 也接受有程式區塊的情況,所以也可以針對區塊內的每個述句作轉換。下面我們就實作一個類似 VB 或 Pascal 的 with 述句。因為我們在編譯器處理 macro 的階段時,沒有任何語意的資訊,所以我們使用 '_' 來表示該行要參照到 with 之後的變數。
aLongVariable = Client()
with aLongVariable:
_Name = "Joe Dog"
_Age = 34
with會被擴展為:
aLongVariable.Name = "Joe Dog"
aLongVariable.Age = 34
這邊就列出 withMacro 的代碼,擷取自 examples/macros/with 目錄下:
import Boo.Lang.Compiler.Ast
import Boo.Lang.Compiler.Ast.Visitors
class WithMacro(AbstractAstMacro):
private class NameExpander(DepthFirstTransformer):
_inst as ReferenceExpression
def constructor(inst as ReferenceExpression):
_inst = inst
override def OnReferenceExpression(node as ReferenceExpression):
// if the name of the reference begins with '_'
// then convert the reference to a member reference
// of the provided instance
if node.Name.StartsWith('_'):
// create the new member reference and set it up
mre = MemberReferenceExpression(node.LexicalInfo)
mre.Name = node.Name[1:]
mre.Target = _inst.CloneNode()
// replace the original reference in the AST
// with the new member-reference
ReplaceCurrentNode(mre)
override def Expand(macro as MacroStatement) as Statement:
assert 1 == macro.Arguments.Count
assert macro.Arguments[0] isa ReferenceExpression
inst = macro.Arguments[0] as ReferenceExpression
// convert all _<ref> to inst.<ref>
block = macro.Block
ne = NameExpander(inst)
ne.Visit(block)
return block
要使用自訂macro,你會遇到最大的問題在於你必須對 AST 類別有相當程度的了解,而要了解 AST 類別最主要的來源卻又來自源碼。要建立運算式和述句,你需要了解編譯器如何表現它們。這聽起來很嚇人吧,與 AST 相關的類別都放在 src/Boo.Lang.Compiler/Ast 目錄下,每個類別都放在獨立的檔案中。
這裏有一些技巧可以讓你發掘運算式的結構。在 booish 裏使用 ast 可以取得給定運算式的 AST 表格,然後就可以進行查看。
>>> e = ast {2*x + y} >>> e (BinaryExpression) (2 * x) + y >>> e.Left (BinaryExpression) (2 * x) >>> e.Right (ReferenceExpression) y >>> e = e.Left >>> e.Left (IntegerLiteralExpression) 2 >>> e.Right (ReferenceExpression) x
第二種方法是將 AST 轉換為 XML。在 Boo 源碼的 examples 目錄下有個 ast-to-xml.boo 的檔案,這可以用來作轉換的工作。產出的 xml 非常的詳細,即使程式只有兩行:
s = "hello"
k = s.Length
這樣就產生了 68 行的 XML 檔案,這邊我只把 k=s.Length 的部份放上來:
<Statements xsi:type="ExpressionStatement">
<Expression xsi:type="BinaryExpression">
<Operator>Assign</Operator>
<Left xsi:type="ReferenceExpression" Name="k" />
<Right xsi:type="MethodInvocationExpression">
<Target xsi:type="MemberReferenceExpression" Name="get_Length">
<Target xsi:type="ReferenceExpression" Name="s" />
</Target>
</Right>
</Expression>
</Statements>
我們可以再手動轉換為 Boo 的代碼:
stmt = ExpressionStatement(BinaryExpression(
Operator: BinaryOperatorType.Assign,
Left: ReferenceExpression("k"),
Right: MethodInvocationExpression(Target:
MemberReferenceExpression(
Name:"get_Length",
Target: ReferenceExpression("s")
))))