喜歡修復(fù) Perl 6 編譯器中的 bug? 這兒有一個great grammar bugglet: 當(dāng) ?” 引號用在引起的用空白分割的單詞列表構(gòu)造器中時看起來好像不能工作:
say ?hello world”;
.say for qww<?hello world”>;
.say for qww<"hello world">;
# OUTPUT:
# hello world
# ?hello
# world”
# hello world
” 引號不應(yīng)該出現(xiàn)在輸出中并且在輸出中我們應(yīng)該只有 3 行輸出; 這 3 行輸出都是 hello world。看起來像是一個待修復(fù)的有趣的 bug! 我們進(jìn)去看看。
你怎樣拼寫它?
事實(shí)上這段代碼沒能正確解析表明這是一個 grammar bug。大部分的 grammar 住在 src/Perl6/Grammar.nqp中, 但是在我們的手變臟之前, 讓我們來解決我們應(yīng)該查看什么。
二進(jìn)制 perl6 有一個 --target 命令行參數(shù)來接收其中之一的編譯步驟并且會導(dǎo)致那個步驟的輸出被產(chǎn)生出來。那兒有哪些步驟? 根據(jù)你正使用的后端它們也會有所不同, 但是你可以僅僅運(yùn)行 perl6 --stagestats -e '' 把它們都打印出來:
zoffix@leliana:~$ perl6 --stagestats -e ''
Stage start : 0.000
Stage parse : 0.077
Stage syntaxcheck: 0.000
Stage ast : 0.000
Stage optimize : 0.001
Stage mast : 0.004
Stage mbc : 0.000
Stage moar : 0.000
Grammars 是關(guān)于解析的, 所以我們會查詢 parse 目標(biāo)(target)。至于要執(zhí)行的代碼, 我們會僅僅給它有問題的那塊; 即 qww<>:
zoffix@leliana:~$ perl6 --target=parse -e 'qww<?hello world”>'
- statementlist: qww<?hello world”>
- statement: 1 matches
- EXPR: qww<?hello world”>
- value: qww<?hello world”>
- quote: qww<?hello world”>
- quibble: <?hello world”>
- babble:
- B:
- nibble: ?hello world”
- quote_mod: ww
- sym: ww
那很棒! 每一行前面都有能在 grammar 中找到的 token 的名字, 所以現(xiàn)在我們知道了在哪里查找問題。
我們還知道基本的引號能正確地工作, 所以我們也傾倒出它們的解析步驟, 來看看這兩個輸出之間是否有什么不同:
zoffix@leliana:~$ perl6 --target=parse -e 'qww<"hello world">'
- statementlist: qww<"hello world">
- statement: 1 matches
- EXPR: qww<"hello world">
- value: qww<"hello world">
- quote: qww<"hello world">
- quibble: <"hello world">
- babble:
- B:
- nibble: "hello world"
- quote_mod: ww
- sym: ww
那么... 好吧, 除了引號不同, 解析數(shù)完全一樣。所以它看起來好像所有涉及的 tokens 都是相同的, 但是那些 tokens 所做的事情不同。
我們不必檢查輸出中我們看到的每個 tokens。statementlist 和 statement 是匹配普通語句的 tokens, EXPR 是占位符解析器, value 是它正操作的值中的一個。我們會忽略上面那些, 留給我們的是下面這樣一個可疑的列表:
- quote: qww<?hello world”>
- quibble: <?hello world”>
- babble:
- B:
- nibble: ?hello world”
- quote_mod: ww
- sym: ww
讓我們開始質(zhì)問它們。
到兔子洞里去...
你自己搞一份本地的 Rakudo 倉庫, 如果你已經(jīng)有了一份,那么打開 src/Perl6/Grammar.nqp, 然后放松點(diǎn)。
我們會從樹的頂部到底部跟隨我們的 tokens, 所以我們首先需要找到的是 token quote, rule quote, regex quote 或 method quote; 以那個順序搜索, 因?yàn)榈谝豁椇芸赡芫褪钦_的東西。
這種情況下, 它是一個 token quote, 它是一個 proto regex。我們的代碼使用了它的 q 版本并且你還可以認(rèn)出靠近它的 qq 和 Q 版本:
token quote:sym<q> {
:my $qm;
'q'
[
| <quote_mod> {} <.qok($/)> { $qm := $<quote_mod>.Str }
<quibble(%*LANG<Quote>, 'q', $qm)>
| {} <.qok($/)> <quibble(%*LANG<Quote>, 'q')>
]
}
token quote:sym<qq> {
:my $qm;
'qq'
[
| <quote_mod> { $qm := $<quote_mod>.Str } <.qok($/)>
<quibble(%*LANG<Quote>, 'qq', $qm)>
| {} <.qok($/)> <quibble(%*LANG<Quote>, 'qq')>
]
}
token quote:sym<Q> {
:my $qm;
'Q'
[
| <quote_mod> { $qm := $<quote_mod>.Str } <.qok($/)>
<quibble(%*LANG<Quote>, $qm)>
| {} <.qok($/)> <quibble(%*LANG<Quote>)>
]
}
可以看到 qq 和 Q 的主體看起來像 q, 我們也來看看它們是否有我們要找的那個 bug:
zoffix@leliana:~$ perl6 -e '.say for qqww<?hello world”>'
?hello
world”
zoffix@leliana:~$ perl6 -e '.say for Qww<?hello world”>'
?hello
world
是的, 它們也存在, 所以 token quote 不可能是那個問題。我們來分解下 token quote:sym<q> 是做什么的, 來算出怎么進(jìn)行到下一步; 它的備選之一沒有被用在我們當(dāng)前的代碼中, 所以我會省略它:
token quote:sym<q> {
:my $qm;
'q'
[
| <quote_mod> {} <.qok($/)> { $qm := $<quote_mod>.Str }
<quibble(%*LANG<Quote>, 'q', $qm)>
| # (this branch omited)
]
}
在第二行中, 我們創(chuàng)建了一個變量, 然后匹配字面值 q 然后是 quote_mod token。那個是我們的 --target=parse 輸出中的一部分并且如果你像我們找出 quote token 那樣找出它, 你會注意到它是一個 proto regex, 即, 在那種情況下, 匹配我們代碼的 ww 塊。后面跟著的空 {} 塊我們可以忽略(那是一個 bug 的替代方法可能在你讀到這兒時已經(jīng)被修復(fù)了)。目前為止, 我們已經(jīng)匹配了我們代碼的 qww 塊。
再往前走, 我們遇見了對 qok token 的調(diào)用, 當(dāng)前的 Match 對象作為其參數(shù)。<.qok> 中的點(diǎn)號表明這是一個非捕獲 token 匹配, 這就是它為什么它沒有在我們的 --target=parse 輸出中出現(xiàn)的原因。我們定位到那個 token 并看看它是關(guān)于什么的:
token qok($x) {
? <![(]>
[
<?[:]> || <!{
my $n := ~$x; $*W.is_name([$n]) || $*W.is_name(['&' ~ $n])
}>
]
[ \s* '#' <.panic: "# not allowed as delimiter"> ]?
<.ws>
}
我的天吶! 這么多符號, 但是這個家伙很容易了: ? 是一個右單詞邊界后面不能跟著一個開圓括號(<![(]>), 再跟著一個備選分支([]), 再跟著一個檢查, 即我們不想嘗試使用 # 號作為分割符([...]?), 最后跟著一個 <.ws> token 吞噬各種各樣的空白。
在備選分支中, 我們使用了首個token匹配的 || 備選分支(和最長token匹配 | 相反), 并且首個 token 向前查看一個冒號 <?[:]>。 如果失敗了, 我們就字符串化那個給定的參數(shù)(~$x)并且之后在 World對象 身上調(diào)用 is_name 方法, 原樣地傳遞帶有前置 & 符號的字符串化的參數(shù)。傳遞的 ~$x 是目前為止我們的 token quote:sym<q> token 所匹配到的東西(并且那是字符串 qww)。is_name 方法僅僅檢查那個給定的符號是否被定義還有根據(jù)那個返回值檢查我們的 token 匹配會通過還是會失敗。如果那個求值代碼返回一個真值那么我們正在使用的 <!{ ... }> 結(jié)構(gòu)就會失敗。
總而言之, 這個 token 所做的所有事情就是檢查我們沒有使用 # 作為分隔符并且沒有嘗試去調(diào)用一個方法或sub。房間的這個角落沒有 bug 跡象。 讓我們回到我們的 token quote:sym<q> 來查看下一步做什么:
token quote:sym<q> {
:my $qm;
'q'
[
| <quote_mod> {} <.qok($/)> { $qm := $<quote_mod>.Str }
<quibble(%*LANG<Quote>, 'q', $qm)>
| # (this branch omited)
]
}
我們已經(jīng)完成了 <.qok> 的檢查, 所以下一步是 { $qm := $<quote_mod>.Str }, 那僅僅把匹配到 quote_mod token 的字符串值存到 $qm 變量中。在我們的例子中, 那個值就是字符串 ww。
下面跟著的是另外一個 token, 它在我們的 --target=parse s輸出中出現(xiàn)過:
<quibble(%*LANG<Quote>, 'q', $qm)>
這里, 我們使用三個位置參數(shù)引用了那個 token: Quote language braid, 字符串 q 和 我們保存在變量 $qm 中的字符串 ww。我想知道它是做什么的。那是我們的下一站。全力以赴!
Nibble Quibble Babbling Nibbler
這里是完整的 token quibble 并且你馬上可以發(fā)現(xiàn)我們不得不從開始往更深處挖掘, 因?yàn)榈?5 行是另外一個 token 匹配:
token quibble($l, *@base_tweaks) {
:my $lang;
:my $start;
:my $stop;
<babble($l, @base_tweaks)>
{
my $B := $<babble><B>.ast;
$lang := $B[0];
$start := $B[1];
$stop := $B[2];
}
$start <nibble($lang)>
[
$stop
|| {
$/.CURSOR.typed_panic(
'X::Comp::AdHoc',
payload => "Couldn't find terminator $stop (corresponding $start was at line {
HLL::Compiler.lineof(
$<babble><B>.orig(), $<babble><B>.from()
)
})",
expected => [$stop],
)
}
]
{
nqp::can($lang, 'herelang')
&& self.queue_heredoc(
$*W.nibble_to_str(
$/,
$<nibble>.ast[1], -> {
"Stopper '" ~ $<nibble> ~ "' too complex for heredoc"
}
),
$lang.herelang,
)
}
}
我們定義了 3 個變量然后引用了 babble token, 這個 babble 引用了和 quibble token 所引用的同樣的參數(shù)。我們來以和查找所有之前的 tokens 同樣的方式查找它并窺探它的內(nèi)核。為了簡潔, 我移除了大約一半代碼:那部分是處理副詞的, 目前我們不能在我們的代碼中使用它。
token babble($l, @base_tweaks?) {
:my @extra_tweaks;
# <irrelevant portion redacted>
$<B>=[<?before .>]
{
# Work out the delimeters.
my $c := $/.CURSOR;
my @delims := $c.peek_delimiters($c.target, $c.pos);
my $start := @delims[0];
my $stop := @delims[1];
# Get the language.
my $lang := self.quote_lang($l, $start, $stop, @base_tweaks, @extra_tweaks);
$<B>.'!make'([$lang, $start, $stop]);
}
}
我們通過把向前查看捕獲到 $<B> 捕獲中開始, 它用作更新當(dāng)前的 Cursor 位置, 然后進(jìn)入以執(zhí)行那個代碼塊。我們把當(dāng)前的 Cursor 存儲在 $c 中, 然后在它身上調(diào)用 .peek_delimiters 方法。如果我們?yōu)榱怂趦?nèi)置的 rakudo 目錄中進(jìn)行 grep, 我們會看到它被定義在 NQP中, 在 nqp/src/HLL/Grammar.nqp中, 但是在我們沖出去閱讀它的代碼之前, 注意它是怎樣返回兩個分隔符的。我們僅僅把它們打印出來好了?
src/Perl6/Grammar.nqp 的 .nqp 后綴名表明我們正處在 NQP 的地盤兒, 所以我們不要使用 NQP ops僅僅并且不是完全的 Perl 6 代碼。通過把下面這一行代碼添加到 @delim 被賦值給 $start 和 $stop 的地方, 我們能找出 .peek_delimiters 給我們的東西:
nqp::say("$sart $stop");
編譯!
$ perl Configure.pl --gen-moar --gen-nqp --backends=moar &&
make &&
make test &&
make install
即使在編譯期間, 通過吐出額外的東西, 我們的調(diào)試行已經(jīng)給了我們所有那些分隔符是關(guān)于什么的啟發(fā)。再次運(yùn)行我們的有問題的代碼:
$ ./perl6 -e '.say for qww<?hello world”>;'
< >
hello world
打印出的分隔符是 qww 里的尖括號分隔符。我們對那些不感興趣, 所以我們可以忽略 .peek_delimiters 并繼續(xù)。再往上是 .quote_lang 方法。 它的名字里有一個"引號"而我們有一個關(guān)于引號的問題.. 聽起來我們離真相越來越近了。我們來看看我們正傳遞給它的是什么參數(shù):
-
$1— Quote language braid -
$start/$stop— 尖括號分隔符 -
@base_tweaks— 包含一個元素: 字符串ww -
@extra_tweaks— 額外的副詞, 這里我們沒有, 所以這個數(shù)組是空的
定位到 method quote_lang; 它仍然在 src/Perl6/Grammar.nqp文件中:
method quote_lang($l, $start, $stop, @base_tweaks?, @extra_tweaks?) {
sub lang_key() {
# <body redacted>
}
sub con_lang() {
# <body redacted>
}
# Get language from cache or derive it.
my $key := lang_key();
nqp::existskey(%quote_lang_cache, $key) && $key ne 'NOCACHE'
?? %quote_lang_cache{$key}
!! (%quote_lang_cache{$key} := con_lang());
}
我們有兩個詞法子例程 lang_key 和 con_lang, 在它們下面我們把 lang_key 的輸出存儲到 $key 中, 在 %quote_lang_cache 中這個 $key 被用在整個緩存 dance 中, 所以我們可以忽略掉 lang_key sub 并直接進(jìn)入 con_lang, 它被調(diào)用以生成我們的 quote_lang 方法的返回值:
sub con_lang() {
my $lang := $l.'!cursor_init'(self.orig(), :p(self.pos()), :shared(self.'!shared'()));
for @base_tweaks {
$lang := $lang."tweak_$_"(1);
}
for @extra_tweaks {
my $t := $_[0];
if nqp::can($lang, "tweak_$t") {
$lang := $lang."tweak_$t"($_[1]);
}
else {
self.sorry("Unrecognized adverb: :$t");
}
}
nqp::istype($stop,VMArray) ||
$start ne $stop ?? $lang.balanced($start, $stop)
!! $lang.unbalanced($stop);
}
在初始化 Cursor 位置之后, $lang 繼續(xù)包含我們的 Quote 語言編織然后我們落進(jìn)一個 for 循環(huán)來迭代 @base_tweaks, 對于里面的每一個元素, 我們都調(diào)用方法 tweak_$_, 給它傳遞一個真值 1。因?yàn)槲覀儍H僅只有一個 base tweak, 這意味著我們正在Quote braid上調(diào)用方法 tweak_ww。我們來看看那個方法是關(guān)于什么的。
因?yàn)?Quote braid 被定義在同一個文件中, 僅僅搜索 method tweak_ww 好了:
method tweak_ww($v) {
$v ?? self.add-postproc("quotewords").apply_tweak(ww)
!! self
}
很好。我們給它的 $v 為真, 所以我們調(diào)用了 .add-postproc 然后調(diào)用 .apply_tweak(ww)。看一下那個方法的上面和下面, 我們看到 .add-postproc 也用在其它不含 bug 的引號中, 所以我們忽略它并直接跳到 .apply_tweak:
method apply_tweak($role) {
my $target := nqp::can(self, 'herelang') ?? self.herelang !! self;
$target.HOW.mixin($target, $role);
self
}
啊哈! 它的參數(shù)是一個 role 并且它把該 role 混進(jìn)來我們的 Quote braid 中。我們來看看那個 role 是關(guān)于什么的(再一次, 僅僅在文件中搜索 role ww, 或者僅僅向上滾動一點(diǎn)):
role ww {
token escape:sym<' '> {
<?[']> <quote=.LANG('MAIN','quote')>
}
token escape:sym<‘ ’> {
<?[‘]> <quote=.LANG('MAIN','quote')>
}
token escape:sym<" "> {
<?["]> <quote=.LANG('MAIN','quote')>
}
token escape:sym<“ ”> {
<?[“]> <quote=.LANG('MAIN','quote')>
}
token escape:sym<colonpair> {
<?[:]> <!RESTRICTED> <colonpair=.LANG('MAIN','colonpair')>
}
token escape:sym<#> {
<?[#]> <.LANG('MAIN', 'comment')>
}
}
奧, 我的天吶!引號! 如果這個地方不是我們修復(fù) bug 的地方, 那么我就是一個芭蕾舞女演員。 我們找到它了!
我們定位到的 role 把進(jìn)了某些 tokens 混合進(jìn)了我們正使用的 Quote braid 中來解析 qww 的內(nèi)容。我們帶有 bug 的 ?” 引號組合明顯不在那個列表中。我們來把它添加進(jìn)去!
token escape:sym<? ”> {
<?[?]> <quote=.LANG('MAIN','quote')>
}
編譯! 運(yùn)行我們帶有 bug 的代碼:
$ ./perl6 -e '.say for qww<foo ?hello world” bar>'
foo
bar
悲催! 好吧, 我們確實(shí)為引號處理找到了正確的地方, 但是我們讓問題變得更加糟糕了。發(fā)生了什么?
Quotastic Inaction
我們新的 token 肯定解析了那個引號, 但是我們絕對沒有給它添加 Actions 動作... 好吧, 對它起作用。 Action 類和 Grammars 相鄰, 在 src/Perl6/Actions.nqp 中。打開它并定位到匹配的方法那里; 比如 method escape:sym<“ ”>。
method escape:sym<' '>($/) { make mark_ww_atom($<quote>.ast); }
method escape:sym<" ">($/) { make mark_ww_atom($<quote>.ast); }
method escape:sym<‘ ’>($/) { make mark_ww_atom($<quote>.ast); }
method escape:sym<“ ”>($/) { make mark_ww_atom($<quote>.ast); }
并在列表中添加我們自己的版本:
method escape:sym<? ”>($/) { make mark_ww_atom($<quote>.ast); }
編譯! 運(yùn)行我們帶有 bug 的代碼:
$ ./perl6 -e '.say for qww<foo ?hello world” bar>'
foo
hello world
bar
呼! 成功了! 不再有 bug 了。我們修復(fù)了那個 bug!
但是, 等一下...
遺漏了, 但是沒有忘記
看一下所有可能的奢華的引號的列表。盡管我們的 bug 報告中僅僅提到了 ?” 引號對兒, 但是 ?‘ 和 「」 都不在我們的 role ww tokens 中。遠(yuǎn)遠(yuǎn)不止的是, 某些左/右引號, 當(dāng)它們交換位置后, 在引起字符串的時候也剛好能工作, 所以它們也應(yīng)該在 qww 中起效。然而, 添加一整串額外的 tokens 和一整串其它的 actions 方法是相當(dāng)不精彩的。有沒有更好的方法?
我們仔細(xì)看看我們的 tokens:
token escape:sym<“ ”> {
<?[“]> <quote=.LANG('MAIN','quote')>
}
sym<“ ”> 我們可以把它省略了 — 這里它的功能僅僅是作為一個名字。我們留下的是一個向前查看的 “ 引號還有 <quote=.LANG('MAIN','quote')>。所以我們可以向前查看所有的我們關(guān)心的開口引號并讓 MAIN braid 接管所有的細(xì)節(jié)。
所以, 讓我們用這個單個 token 替換掉所有的引號處理 tokens:
token escape:sym<'> {
<?[ ' " ‘ ? ’ “ ? ” 「 ]> <quote=.LANG('MAIN','quote')>
}
并且使用下面這個單個 action 替換掉所有的匹配 actions 方法:
method escape:sym<'>($/) { make mark_ww_atom($<quote>.ast); }
編譯! 運(yùn)行我們的帶有某些引號變體的代碼:
$ ./perl6 -e '.say for qww<?looks like” ?we fixed‘ ?this thing?>'
looks like
we fixed
this thing
精彩! 我們不僅讓所有的引號都能正常工作, 還設(shè)法清理的存在的 tokens 和 actions 方法?,F(xiàn)在所有我們需要做的就是對我們的修復(fù)做測試并且我們已經(jīng)準(zhǔn)備提交了。
享用 bug 烤肉
Perl 6 官方測試套件 Roast 是在 Rakudo 內(nèi)建目錄中的 t/spec 中,如果它不存在, 僅僅運(yùn)行 make spectest 就好了并且在它把 roast 倉庫克隆到 t/spec 中后就中止它。我們需要找到在哪里插入我們的測試而 grep 是干那件事的好朋友:
zoffix@VirtualBox:~/CPANPRC/rakudo/t/spec$ grep -R 'qww' .
Binary file ./.git/objects/pack/pack-5bdee39f28283fef4b500859f5b288ea4eec20d7.pack matches
./S02-literals/allomorphic.t: my @wordlist = qqww[1 2/3 4.5 6e7 8+9i] Z (IntStr, RatStr, RatStr, NumStr, ComplexStr);
./S02-literals/allomorphic.t: isa-ok $val, Str, "'$val' from qqww[] is a Str";
./S02-literals/allomorphic.t: nok $val.isa($wrong-type), "'$val' from qqww[] is not a $wrong-type.perl()";
./S02-literals/allomorphic.t: my @wordlist = qqww:v[1 2/3 4.5 6e7 8+9i];
./S02-literals/allomorphic.t: my @written = qqww:v[1 2/3 $num 6e7 8+9i ten];
./S02-literals/allomorphic.t: is-deeply @angled, @written, "?...? is equivalent to qqww:v[...]";
./S02-literals/quoting.t: is(qqww[$alpha $beta], <foo bar>, 'qqww');
./S02-literals/quoting.t: for (<<$a b c>>, qqww{$a b c}, qqw{$a b c}).kv -> $i, $_ {
./S02-literals/quoting.t: is-deeply qww<a a ‘b b’ ?b b’ ’b b‘ ’b b‘ ’b b’ ?b b‘ ?b b’ “b b” ?b b”
./S02-literals/quoting.t: 'fancy quotes in qww work just like regular quotes';
./integration/advent2014-day16.t: for flat qww/ foo bar 'first second' / Z @a -> $string, $result {
看起來 S02-literals/quoting.t 是它的一個好地方。打開那個文件, 在它的頂部, 通過我們添加的測試的數(shù)量來增加 plan 的數(shù)量 — 在這個例子中僅僅增加一條就好了。然后滾動到底部并創(chuàng)建一個 block 塊, 前面添加一個注釋, 并為我們正修復(fù)的 bug 報告引用那個 RT 標(biāo)簽數(shù)字。
在文件里面, 我們使用 is-deeply 測試函數(shù), 它使用 eqv 操作符語義來做測試。我們會給它一個帶有完整引號串的 qww<> 行并告訴它我們所期望返回的項目列表。還要寫下測試描述:
# RT #128304
{
is-deeply qww<a a ‘b b’ ?b b’ ’b b‘ ’b b‘ ’b b’ ?b b‘ ?b b’ “b b” ?b b”
”b b“ ”b b“ ”b b” ?b b“ ?b b” ?b b? ?b b?>,
('a', 'a', |('b b' xx 16)),
'fancy quotes in qww work just like regular quotes';
}
返回到 Rakudo checkout, 運(yùn)行修改后的測試并保證它通過:
$ make t/spec/S02-literals/quoting.t
# <lots of output>
All tests successful.
Files=1, Tests=185, 3 wallclock secs ( 0.03 usr 0.01 sys + 2.76 cusr 0.11 csys = 2.91 CPU)
Result: PASS
漂亮。 提交測試 bug 修復(fù)好了并且把它們送走! 我們做到了!
結(jié)論
當(dāng)我們在修復(fù) Perl 6 中的解析 bugs 的時候, 把程序減少到能重新產(chǎn)生那個 bug 的最小部分然后使用 --target=parse 命令行參數(shù), 得到解析樹的輸出, 找到所匹配的那個 tokens。statementlist
然后, 在 src/Perl6/Grammar.nqp 中跟隨這些 tokens, 它也繼承自 NQP 的 src/HLL/Grammar.nqp 。 與位于 src/Perl6/Actions.nqp 中的 actions 類協(xié)作, 跟隨著代碼找出正在做什么并期望找出問題出現(xiàn)在什么位置。
修復(fù)它。測試它。發(fā)布它。
充滿了樂趣。