查看源码 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 变量 TupleNumber。这是使用 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 开头的整数,例如 9091909123

在 前缀之后,可以使用一个或多个 _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 绑定到表示原子 abc 的各个语法树的列表。这也可以与序列中的静态前缀和后缀元素一起使用。例如

?Q("{a, b, _@@Elements}") = ?Q({a, b, c, d})

将 Elements 绑定到 cd 子树的列表,并且

?Q("{_@@Elements, c, d}") = ?Q({a, b, c, d})

将 Elements 绑定到 ab 子树的列表。您甚至可以在前缀或后缀中使用普通元变量

?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,但不将结果从模板转换回树。

创建一个变量。

类型

链接到此类型

default_action()

查看源代码 (未导出)
-type default_action() :: fun(() -> any()).
-type env() :: [{Key :: id(), pattern_or_patterns()}].
链接到此类型

guard_test()

查看源代码 (未导出)
-type guard_test() :: fun((env()) -> boolean()).
链接到此类型

guarded_action()

查看源代码 (未导出)
-type guarded_action() :: switch_action() | {guard_test(), switch_action()}.
链接到此类型

guarded_actions()

查看源代码 (未导出)
-type guarded_actions() :: guarded_action() | [guarded_action()].
-type id() :: atom() | integer().
链接到此类型

location()

查看源代码 (未导出)
-type location() :: erl_anno:location().
链接到此类型

pattern()

查看源代码 (未导出)
-type pattern() :: tree() | template().
链接到此类型

pattern_or_patterns()

查看源代码 (未导出)
-type pattern_or_patterns() :: pattern() | [pattern()].
链接到此类型

switch_action()

查看源代码 (未导出)
-type switch_action() :: fun((env()) -> any()).
链接到此类型

switch_clause()

查看源代码 (未导出)
链接到此类型

template()

查看源代码 (未导出)
-type template() :: tree() | {id()} | {'*', id()} | {template, atom(), term(), [[template()]]}.
链接到此类型

template_or_templates()

查看源代码 (未导出)
-type template_or_templates() :: template() | [template()].
-type text() :: string() | binary() | [string()] | [binary()].
-type tree() :: erl_syntax:syntaxTree().
链接到此类型

tree_or_trees()

查看源代码 (未导出)
-type tree_or_trees() :: tree() | [tree()].

函数

链接到此函数

alpha(TreeOrTrees, Env)

查看源代码
-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, [])

链接到此函数

compile(Code, Options)

查看源代码
-spec compile(tree_or_trees(), [compile:option()]) -> compile:comp_ret().

将表示模块的语法树或语法树列表编译为二进制 BEAM 对象。

另请参阅:compile/1, compile_and_load/2

链接到此函数

compile_and_load(Code)

查看源代码
-spec compile_and_load(tree_or_trees()) ->
                          {ok, binary()} | error | {error, Errors :: list(), Warnings :: list()}.

等价于 compile_and_load(Code, [])

链接到此函数

compile_and_load(Code, Options)

查看源代码
-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

链接到此函数

meta_template(TemplateOrTemplates)

查看源代码
-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)

链接到此函数

qquote(StartPos, 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.

将语法树或模板的结构打印到标准输出。

这是一个用于开发和调试的实用函数。

链接到此函数

subst(TreeOrTrees, Env)

查看源代码
-spec subst(pattern_or_patterns(), env()) -> tree_or_trees().

替换模式或模式列表中的元变量,生成语法树或树列表作为结果。

对于普通元变量和全局元变量,替换的值可以是单个元素或元素列表。例如,如果在 [foo, _@var, bar][foo, _@var, bar] 中将表示 1, 2, 3 的列表替换为 var,则结果表示 [foo, 1, 2, 3, bar]

链接到此函数

switch(Trees, Clauses)

查看源代码
-spec switch(tree_or_trees(), [switch_clause()]) -> any().

使用模式和可选的守卫匹配一个或多个子句。

请注意,默认操作之后的子句将被忽略。

另请参阅:match/2

-spec template(pattern_or_patterns()) -> template_or_templates().

将语法树或树列表转换为模板或模板列表。

可以使用 tree/1 来实例化或匹配模板,并将其还原为正常的语法树。如果输入已经是模板,则不会对其进行进一步修改。

另请参阅:match/2, subst/2, tree/1

链接到此函数

template_vars(Template)

查看源代码
-spec template_vars(template_or_templates()) -> [id()].

返回模板中元变量的有序列表。

-spec term(term()) -> tree().

为常量项创建语法树。

链接到此函数

tree(TemplateOrTemplates)

查看源代码
-spec tree(template_or_templates()) -> tree_or_trees().

将模板还原为正常的语法树。

任何剩余的元变量都将转换为以 @ 为前缀的原子或以 909 为前缀的整数。

另请参阅:template/1

链接到此函数

tsubst(TreeOrTrees, Env)

查看源代码
-spec tsubst(pattern_or_patterns(), env()) -> template_or_templates().

类似于 subst/2,但不将结果从模板转换回树。

如果您想进行多次单独的替换,这将很有用。

另请参阅:subst/2, tree/1

-spec var(atom()) -> tree().

创建一个变量。