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")
))))