BOO大全/自訂巨集

維基教科書,自由的教學讀本

上一章:自訂屬性(Attribute) 目錄 下一章:例外處理


自訂Macro[編輯]

在 Boo 裡的某些述句,像 printusing,其實都是以 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")
	))))

上一章:自訂屬性(Attribute) 目錄 下一章:例外處理