查看源码 merl (语法工具 v3.2.1)
Erlang 中的元编程。
Merl 是 erl_syntax
模块的一个用户友好接口,它使得从头开始构建新的 AST 以及匹配和分解现有的 AST 都变得很容易。有关 Merl 本身范围之外的详细信息,请参阅 erl_syntax
的文档。
快速开始
要启用 Merl 的全部功能,您的模块需要包含 Merl 头文件
-include_lib("syntax_tools/include/merl.hrl").
然后,您可以在代码中使用 ?Q(Text)
宏来创建 AST 或匹配现有 AST。例如
Tuple = ?Q("{foo, 42}"),
?Q("{foo, _@Number}") = Tuple,
Call = ?Q("foo:bar(_@Number)")
调用 merl:print(Call)
将打印以下代码
foo:bar(42)
?Q
宏将引用的代码片段转换为 AST,并将元变量(例如 _@Tuple
和 _@Number
)提升到您的 Erlang 代码级别,因此您可以直接使用相应的 Erlang 变量 Tuple
和 Number
。这是使用 Merl 最直接的方法,并且在许多情况下,它就是您所需要的全部。
您甚至可以使用 ?Q
宏作为模式编写 case 开关。例如
case AST of
?Q("{foo, _@Foo}") -> handle(Foo);
?Q("{bar, _@Bar}") when erl_syntax:is_integer(Bar) -> handle(Bar);
_ -> handle_default()
end
这些 case 开关只允许 ?Q(...)
或 _
作为子句模式,并且保护条件可以包含任何表达式,而不仅仅是 Erlang 保护条件表达式。
如果在包含 merl.hrl
头文件之前定义了宏 MERL_NO_TRANSFORM
,则 Merl 使用的解析转换将被禁用,在这种情况下,匹配表达式 ?Q(...) = ...
、使用 ?Q(...)
模式的 case 开关以及自动元变量(如 _@Tuple
)无法在您的代码中使用,但 Merl 宏和函数仍然有效。要进行元变量替换,您需要使用 ?Q(Text, Map)
宏。例如
Tuple = ?Q("{foo, _@bar, _@baz}", [{bar, Bar}, {baz,Baz}])
传递给 ?Q(Text)
宏的文本可以是单个字符串,也可以是字符串列表。当您需要将长表达式拆分为多行时,后者很有用。例如
?Q(["case _@Expr of",
" {foo, X} -> f(X);",
" {bar, X} -> g(X)",
" _ -> h(X)"
"end"])
如果文本中的某个地方存在语法错误(例如上面第二个子句中缺少分号),则 Merl 可以生成一条错误消息,指向您源代码中的确切行。(请记住用逗号分隔列表中的字符串,否则 Erlang 会将字符串片段连接起来,就像它们是一个单独的字符串一样。)
元变量语法
在引用的代码中,有几种方法可以编写元变量
- 以
@
开头的原子,例如'@foo'
或'@Foo'
- 以
_@
开头的变量,例如_@bar
或_@Bar
- 以
"'@
开头的字符串,例如"'@File"
- 以 909 开头的整数,例如
9091
或909123
在 前缀之后,可以使用一个或多个 _
或 0
字符来指示将变量“提升”一个或多个级别,之后,@
或 9
字符表示全局元变量(匹配序列中零个或多个元素),而不是普通元变量。例如
'@_foo'
提升一个级别,而_@__foo
提升两个级别_@@bar
是一个全局变量,而_@_@bar
是一个提升的全局变量90901
是一个提升的变量,90991
是一个全局变量,而9090091
是一个提升两个级别的全局变量
(请注意,名称中的最后一个字符永远不会被视为提升或全局标记,因此,_@__
和 90900
只提升一个级别,而不是两个。还要注意,全局变量仅对匹配有意义;在进行替换时,非全局变量可以用于注入一系列元素,反之亦然。)
如果前缀后的名称以及任何提升和全局标记是 _
或 0
,则该变量在匹配中被视为匿名捕获所有模式。例如,_@_
、_@@_
、_@__
,甚至 _@__@_
。
最后,如果名称在没有任何前缀或提升/全局标记的情况下以大写字符开头,如 _@Foo
或 _@_@Foo
,它将成为 Erlang 级别的变量,并且可以很容易地用于解构和构造语法树
case Input of
?Q("{foo, _@Number}") -> ?Q("foo:bar(_@Number)");
...
我们将其称为“自动元变量”。如果此外,名称以 @
结尾,如 _@Foo@
,则该变量作为 Erlang 术语的值将在用于构造更大的树时自动转换为相应的抽象语法树。例如,在
Bar = {bar, 42},
Foo = ?Q("{foo, _@Bar@}")
(其中 Bar 只是一个术语,而不是语法树)结果 Foo
将是一个表示 {foo, {bar, 42}}
的语法树。这避免了为了注入数据而需要临时变量,如在
TmpBar = erl_syntax:abstract(Bar),
Foo = ?Q("{foo, _@TmpBar}")
如果上下文需要一个整数而不是一个变量、原子或字符串,则不能使用大写约定来标记自动元变量。相反,如果整数(不带 909
前缀和提升/全局标记)以 9
结尾,则该整数将变为带有前缀 Q
的 Erlang 级别变量,如果它以 99
结尾,它也会被自动抽象。例如,以下代码将增加导出的函数 f 的元数
case Form of
?Q("-export([f/90919]).") ->
Q2 = erl_syntax:concrete(Q1) + 1,
?Q("-export([f/909299]).");
...
何时使用各种形式的元变量
仅当文本片段遵循 Erlang 的基本语法规则时,Merl 才能解析它。在大多数情况下,普通的 Erlang 变量可以用作元变量,例如
?Q("f(_@Arg)") = Expr
但是,如果您想匹配函数名称之类的内容,则必须使用原子作为元变量
?Q("'@Name'() -> _@@_." = Function
(请注意匿名全局变量 _@@_
,以忽略函数体)。
在某些上下文中,只允许使用字符串或整数。例如,指令 -file(Name, Line)
要求 Name
是一个字符串文字,Line
是一个整数文字
?Q("-file(\"'@File\", 9090).") = ?Q("-file(\"foo.erl\", 42).")).
这将把字符串文字 "foo.erl"
提取到变量 Foo
中。请注意使用匿名变量 9090
来忽略行号。要匹配并绑定必须是整数文字的元变量,我们可以使用以 9 结尾的整数的约定,将其转换为 Erlang 级别带有 Q 前缀的变量(请参阅上一节)。
全局变量
每当您想要匹配序列中的多个元素(零个或多个)而不是一组固定的元素时,您需要使用全局变量。例如
?Q("{_@@Elements}") = ?Q({a, b, c})
将 Elements 绑定到表示原子 a
、b
和 c
的各个语法树的列表。这也可以与序列中的静态前缀和后缀元素一起使用。例如
?Q("{a, b, _@@Elements}") = ?Q({a, b, c, d})
将 Elements 绑定到 c
和 d
子树的列表,并且
?Q("{_@@Elements, c, d}") = ?Q({a, b, c, d})
将 Elements 绑定到 a
和 b
子树的列表。您甚至可以在前缀或后缀中使用普通元变量
?Q("{_@First, _@@Rest}") = ?Q({a, b, c})
或
?Q("{_@@_, _@Last}") = ?Q({a, b, c})
(忽略除最后一个元素之外的所有元素)。但是,您不能在同一序列中包含两个全局变量。
提升的元变量
在某些情况下,Erlang 语法规则使得无法直接将元变量放置在您想要的位置。例如,您不能编写
?Q("-export([_@@Name]).")
来匹配导出列表中的所有名称/元数对,或者在声明中插入导出列表,因为 Erlang 解析器只允许 A/I
形式的元素(其中 A
是原子,I
是整数)在导出列表中。不允许使用如上所述的变量,但也不允许使用单个原子或整数,因此 '@@Name'
或 909919
也将不起作用。
在这种情况下,您必须执行的操作是在语法上有效的位置编写元变量,并使用提升标记来表示它应该实际应用的位置,如
?Q("-export(['@_@Name'/0]).")
这会导致该变量在(解析后)提升到语法树中的下一个更高级别,替换整个子树。在这种情况下,'@_@Name'/0
将被 '@@Name'
替换,并且 /0
部分仅用作虚拟符号,将被丢弃。
您甚至可能需要多次应用提升。要将整个导出列表匹配为单个语法树,您可以编写
?Q("-export(['@__Name'/0]).")
这次使用两个下划线,但没有全局标记。这将使整个 ['@__Name'/0]
部分被 '@Name'
替换。
有时,代码片段的树结构不是很明显,并且当作为源代码打印时,结构的一部分可能是不可见的。例如,像下面这样的简单函数定义
zero() -> 0.
由名称(原子 zero
)和包含单个子句 () -> 0
的子句列表组成。该子句由参数列表(为空)、保护条件(为空)和一个包含单个表达式 0
的主体(始终是表达式列表)组成。这意味着要匹配任何函数的名称和子句列表,您需要使用 ?Q("'@Name'() -> _@_@Body.")
这样的模式,使用其主体被提升一级全局变量的虚拟子句。
要可视化语法树的结构,可以使用函数 merl:show(T)
,它会打印摘要。例如,在 Erlang shell 中输入
merl:show(merl:quote("inc(X, Y) when Y > 0 -> X + Y."))
将打印以下内容(其中 +
号分隔同一级别的子树组)
function: inc(X, Y) when ... -> X + Y.
atom: inc
+
clause: (X, Y) when ... -> X + Y
variable: X
variable: Y
+
disjunction: Y > 0
conjunction: Y > 0
infix_expr: Y > 0
variable: Y
+
operator: >
+
integer: 0
+
infix_expr: X + Y
variable: X
+
operator: +
+
variable: Y
这显示了另一个重要的不明显的情况:子句保护条件,即使它像 Y > 0
这样简单,也总是由一个或多个测试的单个析取组成,很像元组的元组。因此
"when _@Guard ->"
将只匹配只有一个测试的保护条件"when _@@Guard ->"
将匹配一个带有一个或多个逗号分隔的测试(但没有分号)的守卫,并将Guard
绑定到测试列表"when _@_Guard ->"
的匹配方式与之前的模式相同,但会将Guard
绑定到合取子树"when _@_@Guard ->"
将匹配任意非空的守卫,并将Guard
绑定到合取子树的列表"when _@__Guard ->"
的匹配方式与之前的模式相同,但会将Guard
绑定到整个析取子树- 最后,
"when _@__@Guard ->"
将匹配任何子句,如果守卫为空,则将Guard
绑定到[]
,否则绑定到[Disjunction]
因此,以下模式匹配所有可能的子句
"(_@Args) when _@__@Guard -> _@Body"
总结
函数
Alpha 转换模式(重命名变量)。
将表示模块的语法树或语法树列表编译为二进制 BEAM 对象。
编译表示模块的语法树或语法树列表,并将生成的模块加载到内存中。
将模式与语法树(或将模式与语法树列表)进行匹配,返回一个将变量名映射到子树的环境;该环境始终按键排序。
将模板转换为表示该模板的语法树。
将语法树或模板美观地打印到标准输出。
解析文本并替换元变量。
解析文本。
将语法树或模板的结构打印到标准输出。
替换模式或模式列表中的元变量,生成语法树或树列表作为结果。
使用模式和可选的守卫匹配一个或多个子句。
将语法树或树列表转换为模板或模板列表。
返回模板中元变量的有序列表。
为常量项创建语法树。
将模板还原为正常的语法树。
类似于 subst/2
,但不将结果从模板转换回树。
创建一个变量。
类型
-type default_action() :: fun(() -> any()).
-type env() :: [{Key :: id(), pattern_or_patterns()}].
-type guarded_action() :: switch_action() | {guard_test(), switch_action()}.
-type guarded_actions() :: guarded_action() | [guarded_action()].
-type location() :: erl_anno:location().
-type switch_clause() :: {pattern_or_patterns(), guarded_actions()} | {pattern_or_patterns(), guard_test(), switch_action()} | default_action().
-type tree() :: erl_syntax:syntaxTree().
函数
-spec alpha(pattern_or_patterns(), [{id(), id()}]) -> template_or_templates().
Alpha 转换模式(重命名变量)。
类似于 tsubst/1,但仅重命名变量(包括全局变量)。
另请参阅:tsubst/2
。
-spec compile(tree_or_trees()) -> compile:comp_ret().
等价于 compile(Code, [])
。
-spec compile(tree_or_trees(), [compile:option()]) -> compile:comp_ret().
将表示模块的语法树或语法树列表编译为二进制 BEAM 对象。
另请参阅:compile/1
, compile_and_load/2
。
-spec compile_and_load(tree_or_trees()) -> {ok, binary()} | error | {error, Errors :: list(), Warnings :: list()}.
-spec compile_and_load(tree_or_trees(), [compile:option()]) -> {ok, binary()} | error | {error, Errors :: list(), Warnings :: list()}.
编译表示模块的语法树或语法树列表,并将生成的模块加载到内存中。
另请参阅:compile/2
, compile_and_load/1
。
-spec match(pattern_or_patterns(), tree_or_trees()) -> {ok, env()} | error.
将模式与语法树(或将模式与语法树列表)进行匹配,返回一个将变量名映射到子树的环境;该环境始终按键排序。
注意
模式中不允许出现多次元变量,但不会进行检查。
另请参阅:switch/2
, template/1
。
-spec meta_template(template_or_templates()) -> tree_or_trees().
将模板转换为表示该模板的语法树。
如果模板中元变量的名称(在元变量前缀字符之后)以大写字符开头,则将它们转换为普通的 Erlang 变量。例如,模板中的 _@Foo
变成了元模板中的变量 Foo
。此外,以 @
结尾的变量会自动包装在对 merl:term/1 的调用中,因此模板中的 _@Foo@
变成了元模板中的 merl:term(Foo)
。
-spec print(tree_or_trees()) -> ok.
将语法树或模板美观地打印到标准输出。
这是一个用于开发和调试的实用函数。
-spec qquote(Text :: text(), Env :: env()) -> tree_or_trees().
等价于 qquote(1, Text, Env)
。
-spec qquote(StartPos :: location(), Text :: text(), Env :: env()) -> tree_or_trees().
解析文本并替换元变量。
将初始扫描器的起始位置作为第一个参数。
宏 ?Q(Text, Env)
展开为 merl:qquote(?LINE, Text, Env)
。
另请参阅:quote/2
。
-spec quote(Text :: text()) -> tree_or_trees().
等价于 quote(1, Text)
。
-spec quote(StartPos :: location(), Text :: text()) -> tree_or_trees().
解析文本。
将初始扫描器的起始位置作为第一个参数。
宏 ?Q(Text)
展开为 merl:quote(?LINE, Text)
。
另请参阅:quote/1
。
-spec show(tree_or_trees()) -> ok.
将语法树或模板的结构打印到标准输出。
这是一个用于开发和调试的实用函数。
-spec subst(pattern_or_patterns(), env()) -> tree_or_trees().
替换模式或模式列表中的元变量,生成语法树或树列表作为结果。
对于普通元变量和全局元变量,替换的值可以是单个元素或元素列表。例如,如果在 [foo, _@var, bar]
或 [foo, _@var, bar]
中将表示 1, 2, 3
的列表替换为 var
,则结果表示 [foo, 1, 2, 3, bar]
。
-spec switch(tree_or_trees(), [switch_clause()]) -> any().
使用模式和可选的守卫匹配一个或多个子句。
请注意,默认操作之后的子句将被忽略。
另请参阅:match/2
。
-spec template(pattern_or_patterns()) -> template_or_templates().
将语法树或树列表转换为模板或模板列表。
可以使用 tree/1
来实例化或匹配模板,并将其还原为正常的语法树。如果输入已经是模板,则不会对其进行进一步修改。
-spec template_vars(template_or_templates()) -> [id()].
返回模板中元变量的有序列表。
为常量项创建语法树。
-spec tree(template_or_templates()) -> tree_or_trees().
将模板还原为正常的语法树。
任何剩余的元变量都将转换为以 @
为前缀的原子或以 909
为前缀的整数。
另请参阅:template/1
。
-spec tsubst(pattern_or_patterns(), env()) -> template_or_templates().
类似于 subst/2
,但不将结果从模板转换回树。
如果您想进行多次单独的替换,这将很有用。
创建一个变量。