内容

名称

B::Concise - 遍历 Perl 语法树,打印有关操作的简洁信息

概要

perl -MO=Concise[,OPTIONS] foo.pl

use B::Concise qw(set_style add_callback);

说明

此编译器后端以适合于调试 perl 或其他编译器后端的内部工作原理的几种节省空间的文本格式之一打印 Perl 程序语法树的内部操作。它可以按操作在操作树中出现的顺序、按它们将执行的顺序或按其树结构的文本近似打印操作,并且所显示信息的格式是可自定义的。它的功能类似于 perl 的 -Dx 调试标志或 B::Terse 模块,但它更复杂、更灵活。

示例

以下是两个输出(或“渲染”),在相同的代码片段上使用 -exec 和 -basic(即默认)格式约定。

   % perl -MO=Concise,-exec -e '$a = $b + 42'
   1  <0> enter
   2  <;> nextstate(main 1 -e:1) v
   3  <#> gvsv[*b] s
   4  <$> const[IV 42] s
*  5  <2> add[t3] sK/2
   6  <#> gvsv[*a] s
   7  <2> sassign vKS/2
   8  <@> leave[1 ref] vKP/REFC

在此 -exec 渲染中,每个操作码按显示的顺序执行。标记为“*”的 add 操作码将进行更详细的讨论。

第 1 列是操作码的序列号,从 1 开始,默认以 36 进制显示。此处它们完全是线性的;在查看带有循环和分支的代码时,这些序列非常有用。

尖括号中的符号表示操作码的类型,例如;<2> 是 BINOP,<@> 是 LISTOP,<#> 是 PADOP,它用于线程化 perl。(请参阅 “OP 类缩写”)。

操作码名称,如 “add[t1]”,后面可能是括号或方括号中的操作码特定信息(例如 “[t1]”)。

操作码标志(例如 “sK/2”)在 (“OP 标志缩写”) 中进行描述。

   % perl -MO=Concise -e '$a = $b + 42'
   8  <@> leave[1 ref] vKP/REFC ->(end)
   1     <0> enter ->2
   2     <;> nextstate(main 1 -e:1) v ->3
   7     <2> sassign vKS/2 ->8
*  5        <2> add[t1] sK/2 ->6
   -           <1> ex-rv2sv sK/1 ->4
   3              <$> gvsv(*b) s ->4
   4           <$> const(IV 42) s ->5
   -        <1> ex-rv2sv sKRM*/1 ->7
   6           <$> gvsv(*a) s ->7

默认渲染方式是从上到下,因此它们不是按执行顺序排列的。此形式反映了使用堆栈来解析和计算表达式的过程;add 操作树中其下方的两个项。

空操作显示为 ex-opname,其中 opname 是已被 perl 优化掉的 op。它们显示的序列号为“ -”,因为它们不会执行(它们不会出现在前面的示例中),它们在此处显示是因为它们反映了解析。

箭头指向下一个操作码的序列号;出于显而易见的原因,它们不会在 -exec 模式中显示。

请注意,由于此渲染是在非线程化 perl 上完成的,因此前面示例中的 PADOP 现在是 SVOP,并且一些(但不是全部)方括号已被圆括号替换。这是一个微妙的功能,可以提供线程化和非线程化 perl 渲染之间的一些视觉区别。

选项

不以连字符开头的参数被视为要渲染的子例程或格式的名称;如果没有指定此类函数,则将渲染程序的主体(不在任何子例程中,也不包括 use 或 require 的文件)。传递 BEGINUNITCHECKCHECKINITEND 将导致打印所有相应的特殊块。参数必须遵循选项。

选项会影响事物的渲染方式(即打印方式)。它们在此处按其视觉效果呈现,第一个是最强的。它们根据它们之间的相互关系进行分组;在每个组中,选项是互斥的(除非另有说明)。

操作码排序选项

这些选项控制操作码的“垂直显示”。显示“顺序”在本文档的其他位置也称为“模式”。

-basic

按 OP 在 OP 树中出现的顺序打印 OP(先序遍历,从根节点开始)。每个 OP 的缩进显示其在树中的级别,行尾的 '->' 指示执行顺序中的下一个操作码。此模式是默认模式,因此仅为完整性而包含该标志。

-exec

按 OP 通常执行的顺序打印 OP(对于大多数结构,这是树的后序遍历,以根节点结束)。在大多数情况下,通常跟随给定 OP 的 OP 将直接出现在其下方;备用路径通过缩进显示。在循环等控制跳出线性路径的情况下,将生成一个“goto”行。

-tree

在树的文本近似中打印 OP,树的根节点在左侧,“自左向右”的子节点顺序转换为“自上而下”。由于此模式同时向右和向下增长,因此不适用于大型程序(除非您有非常宽的终端)。

行样式选项

这些选项选择用于呈现每个操作码的行样式(或仅样式),并规定实际打印到每行的信息。

-concise

使用作者最喜欢的格式化约定集。当然,这是默认值。

-terse

使用模拟 B::Terse 输出的格式化约定。基本模式几乎与真正的 B::Terse 无法区分,并且 exec 模式看起来非常相似,但顺序更合乎逻辑且没有大括号。B::Terse 没有树模式,因此树模式仅与 B::Terse 隐约相似。

-linenoise

使用格式化约定,其中每个 OP 的名称不是写成全称,而是由一或两个字符的缩写表示。这主要是一个玩笑。

-debug

使用类似于 CPAN 模块 B::Debug 的格式化约定;这些约定一点也不简洁。

-env

使用从环境变量 B_CONCISE_FORMATB_CONCISE_GOTO_FORMATB_CONCISE_TREE_FORMAT 中读取的格式化约定。

特定于树的格式化的选项

-compact

使用树格式,其中用于连接节点的行的空间使用量最少(在大多数情况下为一个字符)。这会挤出屏幕房地产上的几列宝贵空间。

-loose

使用树格式,该格式使用较长的边来分隔 OP 节点。这种格式通常看起来比紧凑格式更好,尤其是在 ASCII 中,并且是默认格式。

-vt

使用从 VT100 线绘制集中绘制的树连接字符。如果您的终端支持,这看起来会更好。

-ascii

使用标准 ASCII 字符(如 +|)绘制树。这些字符看起来不像 VT100 字符那样干净,但它们几乎适用于任何终端(或 less(1) 的水平滚动模式),并且适用于文本文档或电子邮件。这是默认值。

这些是成对互斥的,即紧凑或松散,vt 或 ascii。

控制序列编号的选项

-basen

以基数 n 打印 OP 序列号。如果 n 大于 10,则 11 的数字将是“a”,依此类推。如果 n 大于 36,则 37 的数字将是“A”,依此类推,直到 62。目前不支持大于 62 的值。默认值为 36。

-bigendian

先打印最高有效数字,再打印序列号。这是阿拉伯数字的惯例,也是默认值。

-littleendian

按最低有效位数字优先打印序列号。这显然与大端字节序互斥。

其他选项

-src

使用此选项,每个语句的渲染(从 nextstate OP 开始)都将以生成它的源代码的第一行开头。例如

1  <0> enter
# 1: my $i;
2  <;> nextstate(main 1 junk.pl:1) v:{
3  <0> padsv[$i:1,10] vM/LVINTRO
# 3: for $i (0..9) {
4  <;> nextstate(main 3 junk.pl:3) v:{
5  <0> pushmark s
6  <$> const[IV 0] s
7  <$> const[IV 9] s
8  <{> enteriter(next->j last->m redo->9)[$i:1,10] lKS
k  <0> iter s
l  <|> and(other->9) vK/1
# 4:     print "line ";
9      <;> nextstate(main 2 junk.pl:4) v
a      <0> pushmark s
b      <$> const[PV "line "] s
c      <@> print vK
# 5:     print "$i\n";
...
-stash="somepackage"

使用此选项,将需要“somepackage”,然后检查 stash,并渲染每个函数。

以下选项成对互斥。

-main

即使指定了子例程,也将在输出中包含主程序。当给出子例程名称或引用时,通常会禁止这种渲染。

-nomain

在使用“-main”更改默认行为后,此选项将恢复默认行为(通常不需要)。如果未给出子例程名称/引用,则将渲染 main,而不管此标志如何。

-nobanner

渲染通常包括标识函数名称或字符串化子例程的横幅行。这将禁止打印横幅。

待办事项:删除字符串化 coderef;虽然它为每个渲染的函数提供了一个“cookie”,但使用的 cookie 应该是 1、2、3...而不是随机十六进制地址。它还使两个不同树的字符串比较复杂化。

-banner

恢复默认横幅行为。

-banneris => subref

待办事项:一个钩点(和一个设置它的选项),用于用户提供的函数来生成适合用户需求的横幅。这不是理想的,因为渲染状态变量(这是 concise.t 中自然候选使用的变量)对用户不可用。

选项粘性

如果您在程序中多次调用 Concise,您应该知道这些选项是“粘性”的。这意味着您在第一次调用中提供的选项将被记住,用于第二次调用,除非您重新指定或更改它们。

缩写

简洁的风格使用符号以最少的混乱(如十六进制地址)传达最大信息。只需稍加练习,你就可以开始看到树中的花朵,而不仅仅是树枝。

OP 类缩写

这些符号出现在 op 名称之前,并指示表示 Perl 代码中 op 的 B:: 命名空间。

0      OP (aka BASEOP)  An OP with no children
1      UNOP             An OP with one child
+      UNOP_AUX         A UNOP with auxillary fields
2      BINOP            An OP with two children
|      LOGOP            A control branch OP
@      LISTOP           An OP that could have lots of children
/      PMOP             An OP with a regular expression
$      SVOP             An OP with an SV
"      PVOP             An OP with a string
{      LOOP             An OP that holds pointers for a loop
;      COP              An OP that marks the start of a statement
#      PADOP            An OP with a GV on the pad
.      METHOP           An OP with method call info

OP 标志缩写

OP 标志是公共的或私有的。公共标志以一致的方式改变每个操作码的行为,并由 0 个或更多单个字符表示。

v      OPf_WANT_VOID    Want nothing (void context)
s      OPf_WANT_SCALAR  Want single value (scalar context)
l      OPf_WANT_LIST    Want list of any length (list context)
                        Want is unknown
K      OPf_KIDS         There is a firstborn child.
P      OPf_PARENS       This operator was parenthesized.
                         (Or block needs explicit scope entry.)
R      OPf_REF          Certified reference.
                         (Return container, not containee).
M      OPf_MOD          Will modify (lvalue).
S      OPf_STACKED      Some arg is arriving on the stack.
*      OPf_SPECIAL      Do something weird for this op (see op.h)

如果为操作码设置了任何私有标志,则它们会显示在“/”之后

8  <@> leave[1 ref] vKP/REFC ->(end)
7     <2> sassign vKS/2 ->8

它们是操作码特定的,并且出现得比公共标志少,因此它们由短助记符而不是单字符表示;有关更多详细信息,请参阅 B::Op_private 和 regen/op_private

请注意,“/”后面的数字通常表示参数的数量。在上面的 sassign 示例中,OP 采用 2 个参数。这些值有时在运行时使用:特别是,MAXARG 宏使用它们。

格式化规范

对于每种行样式(“简洁”、“简洁”、“行噪声”等),有 3 个格式规范控制 OP 的呈现方式。

第一个是“默认”格式,它在基本模式和执行模式中都用于打印所有操作码。第二个,goto 格式,在执行模式中遇到分支时使用。它们不是真正的操作码,而是插入的,看起来像一个闭合的大括号。树格式是树特定的。

呈现一行时,将复制正确的格式规范并扫描以下项;对于呈现的每个操作码,都会替换数据,并执行其他操作(如基本缩进)。

可能有 3 种类型的项填充;特殊模式、#vars 和逐字复制的文本。(是的,这是一组 s///g 步骤。)

特殊模式

这些项是用于执行缩进和从备选方案中选择文本的基本元素。

(x(exec_text;basic_text)x)

在执行模式中生成 exec_text,或在基本模式中生成 basic_text

(*(text)*)

为每个缩进级别生成一份 text 的副本。

(*(text1;text2)*)

生成比缩进级别少一份 text1 的副本,如果缩进级别大于 0,则后面跟一份 text2 的副本。

(?(text1#varText2)?)

如果 var 的值为真(不为空或零),则生成被 text1Text2 包围的 var 的值,否则什么也不生成。

~

任何数量的波浪线和周围的空格都将折叠成一个空格。

# 变量

这些 #vars 代表您可能希望作为渲染一部分的操作码属性。'#' 被用作私有标记;#var 的值被插值到样式行中,就像“read $this”一样。

这些变量有 3 种形式

#var

假设操作码存在名为 'var' 的属性,并将其插值到渲染中。

#varN

生成 var 的值,左对齐以填充 N 个空格。请注意,这意味着虽然您可以拥有属性 'foo' 和 'foo2',但您无法渲染 'foo2',但可以使用 'foo2a' 渲染。您最好不要依赖这种行为 ;-)

#Var

这种 #var 的 ucfirst 形式生成其自身的一个标签值形式以进行显示;它将 '#Var' 转换为 'Var => #var' 样式,然后按上述方式处理。(Imp-note:#Vars 不能用于条件填充,因为 => #var 转换是在检查 #Var 的值之后完成的)。

以下变量由 B::Concise '定义';当它们用于样式时,其各自的值将插入到每个操作码的渲染中。

其中只有部分被标准样式使用,其他部分供您深入了解 optree 机制,如果您希望添加使用它们的新的样式(请参见下面的 "add_style"),您可以使用 "add_callback" 添加新的样式。

#addr

OP 的地址,以十六进制表示。

#arg

OP 的 OP 特定信息(例如 SVOP 的 SV,LOOP 的非本地退出指针等),用括号括起来。

#class

OP 的 B 确定类,全部大写。

#classsym

一个缩写 OP 类的符号。

#coplabel

OP 声明或块的标签(如果有)。

#exname

OP 的名称,如果 OP 是曾经是 foo 的空值,则为“ex-foo”。

#extarg

OP 的目标,对于空值 OP 则无目标。

#firstaddr

OP 的第一个子项的地址(十六进制)。

#flags

OP 的标志,缩写为一系列符号。

#flagval

OP 的标志的数值。

#hints

COP 的提示标志,如果可能,则使用缩写名称呈现。如果这不是 COP,则为空字符串。以下是使用的符号

 $ strict refs
 & strict subs
 * strict vars
x$ explicit use/no strict refs
x& explicit use/no strict subs
x* explicit use/no strict vars
 i integers
 l locale
 b bytes
 { block scope
 % localise %^H
 < open in
 > open out
 I overload int
 F overload float
 B overload binary
 S overload string
 R overload re
 T taint
 E eval
 X filetest access
 U utf-8

 us      use feature 'unicode_strings'
 fea=NNN feature bundle number
#hintsval

COP 的提示标志的数值,如果这不是 COP,则为空字符串。

#hyphseq

OP 的序列号,如果没有序列号,则为连字符。

#label

如果 OP 是执行模式中其中一个目标,则为“NEXT”、“LAST”或“REDO”,否则为空。

#lastaddr

OP 的最后一个子项的地址(十六进制)。

#name

OP 的名称。

#NAME

OP 的名称(全部大写)。

#next

OP 的下一个 OP 的序列号。

#nextaddr

OP 的下一个 OP 的地址(十六进制)。

#noise

OP 名称的一到两个字符缩写。

#private

OP 的私有标志,如果可能,则使用缩写名称呈现。

#privval

OP 的私有标志的数值。

#seq

OP 的序列号。请注意,这是 B::Concise 生成的序列号。

#opt

该操作是否已被窥孔优化器优化。

#sibaddr

OP 的下一个最年轻兄弟的地址(十六进制)。

#svaddr

OP 的 SV 的地址(如果存在 SV),以十六进制表示。

#svclass

OP 的 SV 的类(如果存在),全部大写(例如,“IV”)。

#svval

OP 的 SV 的值(如果存在),以简短的人类可读格式表示。

#targ

OP 的 targ 的数值。

#targarg

OP 的 targ 引用的变量的名称(如果存在),否则为字母 t 后跟 OP 的 targ(十进制)。

#targarglife

#targarg 相同,但后面跟的是 COP 序列号,这些序列号为变量的生存期(或对于开放作用域中的变量为“end”)划定界限。

#typenum

OP 的类型的数值(十进制)。

单行命令提示

perl -MO=Concise,bar foo.pl

仅从 foo.pl 渲染 bar()。要查看 main,请删除“,bar”。要同时查看,请添加“,-main”。

perl -MDigest::MD5=md5 -MO=Concise,md5 -e1

将 md5 标识为 XS 函数。需要导出,以便 BC 可以在 main 中找到它。

perl -MPOSIX -MO=Concise,_POSIX_ARG_MAX -e1

将 _POSIX_ARG_MAX 标识为常量子例程,优化为 IV。虽然 POSIX 在不同平台上并不完全一致,但它很可能存在于几乎所有平台上。

perl -MPOSIX -MO=Concise,a -e 'print _POSIX_SAVED_IDS'

这会渲染一个 print 语句,其中包括对函数的调用。它与使用 use 调用和该单个语句渲染文件相同,除了出现在 nextstate ops 中的文件名之外。

perl -MPOSIX -MO=Concise,a -e 'sub a{_POSIX_SAVED_IDS}'

这与上一个非常相似,只有前两个操作不同。这个子例程渲染更具代表性,因为一个主程序会有许多子例程。

perl -MB::Concise -e 'B::Concise::compile("-exec","-src", \%B::Concise::)->()'

这会用源代码行渲染 B::Concise 包中的所有函数。它避开了 O 框架,以便可以直接将 stashref 传递给 B::Concise::compile()。请参阅 -stash 选项,了解渲染包的更便捷方式。

在 O 框架外使用 B::Concise

B::Concise 的常见(也是最初的)用法是命令行渲染简单代码,如 EXAMPLE 中所示。但是,你也可以从代码中使用B::Concise,并直接反复调用 compile()。通过这样做,你可以避免 O.pm 的仅编译时间操作,甚至可以使用调试器逐步执行 B::Concise::compile() 本身。

一旦你这样做,就可以通过添加新的渲染样式以及通过可选地添加回调例程(如果从这些(刚添加的)样式中引用了这些回调例程,则这些回调例程会填充新变量)来更改 Concise 输出。

示例:更改 Concise 渲染

use B::Concise qw(set_style add_callback);
add_style($yourStyleName => $defaultfmt, $gotofmt, $treefmt);
add_callback
  ( sub {
        my ($h, $op, $format, $level, $stylename) = @_;
        $h->{variable} = some_func($op);
    });
$walker = B::Concise::compile(@options,@subnames,@subrefs);
$walker->();

set_style()

set_style 接受 3 个参数,并更新构成行样式(basic-exec、goto、tree)的三个格式规范。不过,它有一个小缺点;它不会在新的名称下注册样式。如果你多次渲染并切换样式,这可能会成为一个问题。因此,你可能更喜欢使用 add_style() 和/或 set_style_standard()。

set_style_standard($name)

这将恢复一种标准行样式:terseconciselinenoisedebugenv。它还接受先前使用 add_style() 定义的样式名称。

add_style ()

此子例程接受一个新的样式名称和三个样式参数(如上所述),并创建、注册和选择新命名的样式。重新添加样式是一个错误;调用 set_style_standard() 在几种样式之间切换。

add_callback ()

如果新创建的样式引用任何新的 #variables,则需要定义一个回调子例程来填充(或修改)这些变量。然后可以在你选择的样式中使用它们。

回调是按 Concise 访问的每个操作码调用的,其顺序与添加它们的顺序相同。每个子例程都会传递五个参数。

1. A hashref, containing the variable names and values which are
   populated into the report-line for the op
2. the op, as a B<B::OP> object
3. a reference to the format string
4. the formatting (indent) level
5. the selected stylename

要定义您自己的变量,只需将它们添加到哈希中,或根据需要更改现有值。级别和格式作为标量的引用传递,但不太可能需要更改甚至使用它们。

运行 B::Concise::compile()

compile 接受如上文 "OPTIONS" 中所述的选项和参数,这些参数可以是 coderef 或子例程名称。

它构造并返回一个 $treewalker coderef,当调用时,它遍历或游走并渲染给定参数的 optree 到 STDOUT。您可以重复使用它,并且每次都可以更改所使用的渲染样式;此后,coderef 将以新样式渲染。

walk_output 允许您将打印目标从 STDOUT 更改为另一个打开的文件句柄,或更改为作为引用传递的字符串(除非您使用 -Uuseperlio 构建 perl)。

my $walker = B::Concise::compile('-terse','aFuncName', \&aSubRef); # 1
walk_output(\my $buf);
$walker->();			        # 1 renders -terse
set_style_standard('concise');	# 2
$walker->();  		        # 2 renders -concise
$walker->(@new);			# 3 renders whatever
print "3 different renderings: terse, concise, and @new: $buf\n";

当调用 $walker 时,它遍历在创建时提供的子例程,并使用当前样式渲染它们。您可以在之后通过多种不同方式更改样式

1. call C<compile>, altering style or mode/order
2. call C<set_style_standard>
3. call $walker, passing @new options

向 $walker 传递新选项是在任何预定义样式之间更改的最简单方法(您添加的样式会自动识别为选项),并且是无需再次调用 compile 即可更改渲染顺序的唯一方法。但请注意,渲染状态仍然在多个 $walker 对象之间共享,因此它们仍然必须以协调的方式使用。

B::Concise::reset_sequence()

此函数(未导出)允许您重置序列号(请注意,它们是任意编号的,其目标是便于人类阅读)。它的目的是主要支持测试,即比较两个相同的匿名子例程(但不同的实例)的简洁输出。如果没有重置,B::Concise 会看到它们是单独的 optree,并在输出中生成不同的序列号。

错误

渲染错误(不存在的函数名称、不存在的 coderef)将写入 STDOUT,或您通过 walk_output() 设置的任何位置。

使用各种 *style* 调用以及 walk_output() 的错误参数会导致 die()。如果您希望捕获这些错误并继续处理,请使用 eval。

作者

Stephen McCamant,<[email protected]>。