Perl 6 核心駭客: 詞法的胡言亂語

Perl 6 核心駭客: 詞法的胡言亂語

喜歡修復(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。statementliststatement 是匹配普通語句的 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 quotemethod quote; 以那個順序搜索, 因?yàn)榈谝豁椇芸赡芫褪钦_的東西。

這種情況下, 它是一個 token quote, 它是一個 proto regex。我們的代碼使用了它的 q 版本并且你還可以認(rèn)出靠近它的 qqQ 版本:

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>)>
    ]
}

可以看到 qqQ 的主體看起來像 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ù):

  • $1Quote 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_keycon_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ā)布它。

充滿了樂趣。

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容