跳转到内容

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) 目录 下一章:例外处理