Perl 6 - Rakudo and NQP Internals

標(biāo)題: Rakudo and NQP Internals
子標(biāo)題: The guts tormented implementers made
作者: Jonathan Worthington

關(guān)于這個(gè)課程

Perl 6 是一種大型語(yǔ)言, 包含許多要求正確實(shí)現(xiàn)的功能蘑拯。

這樣的軟件項(xiàng)目很容易被難控制的復(fù)雜性淹沒(méi)族奢。
Rakudo 和 NQP 項(xiàng)目的早期階段已經(jīng)遭受了這樣的困難, 因?yàn)槲覀儗W(xué)到了 - 艱難的方式 - 關(guān)于復(fù)雜性, 出現(xiàn)并可能在實(shí)現(xiàn)過(guò)程中不受限制地?cái)U(kuò)散。

本課程將教您如何使用 Rakudo 和 NQP 內(nèi)部攀隔。 在他們的設(shè)計(jì)中編碼是一個(gè)大量學(xué)習(xí)的過(guò)程, 關(guān)于如何(以及如何不)寫一個(gè) Perl 6 實(shí)現(xiàn), 這個(gè)過(guò)程持續(xù)了多年。 因此, 本課程還將教你事情的來(lái)龍去脈。

關(guān)于講師

  • 計(jì)算機(jī)科學(xué)背景
  • 選擇旅行世界,并幫助實(shí)現(xiàn) Perl 6, 而不是做博士
  • 有不止一種方法來(lái)獲得"永久頭部損傷" :-)
  • 不知何故在 Edument AB 被聘用, 作為講師/顧問(wèn)
  • 從 2008 年開(kāi)始成為 Rakudo Perl 6 核心開(kāi)發(fā)者
  • 6model, MoarVM, NQP 和 Rakudo 各個(gè)方面的締造者

課程大綱 - 第一天

  • 鷹的視角: 編譯器和 NQP/Rakudo 架構(gòu)
  • NQP 語(yǔ)言
  • 編譯管道
  • QAST
  • 探索 nqp::ops

課程大綱 - 第二天

  • 6model
  • 有界序列化和模塊加載
  • 正則表達(dá)式和 grammar 引擎
  • JVM 后端
  • MoarVM 后端

鷹的視角

編譯器和 NQP/Rakudo 架構(gòu)

編譯器做什么

編譯器真的是"只是"翻譯朋蔫。

編譯器把高級(jí)語(yǔ)言 (例如 Perl 6) 翻譯成低級(jí)語(yǔ)言 (例如 JVM 字節(jié)碼)。

input-compiler-output.png

接收直截了當(dāng)?shù)妮斎?文本)并產(chǎn)生直截了當(dāng)?shù)妮敵?文本或二進(jìn)制), 但內(nèi)部的數(shù)據(jù)結(jié)構(gòu)很豐富

像字符串那樣處理東西, 通常是最后的手段

運(yùn)行時(shí)做什么

運(yùn)行像 Perl 6 這樣的語(yǔ)言不僅僅是將它轉(zhuǎn)換為低級(jí)代碼却汉。 此外, 它需要運(yùn)行時(shí)支持來(lái)提供:

  • 內(nèi)存管理
  • I/O, IPC, OS 交互
  • 并發(fā)
  • 動(dòng)態(tài)優(yōu)化

構(gòu)建我們需要的東西來(lái)構(gòu)建東西

我們已經(jīng)以現(xiàn)有的編譯器構(gòu)造技術(shù)進(jìn)行了各種嘗試來(lái)構(gòu)建 Perl 6驯妄。 編譯器的早期設(shè)計(jì)至少有一部分是基于常規(guī)假設(shè)的。

這樣的嘗試是信息性的, 但從長(zhǎng)遠(yuǎn)來(lái)看還不夠好合砂。

Perl 6 提出了一些有趣的挑戰(zhàn)...

Perl 6 是使用 Perl 6 解析的

Perl 6 的標(biāo)準(zhǔn)文法(grammar)是用 Perl 6 寫的青扔。它依賴于...

  • 可傳遞的 最長(zhǎng) token 匹配 (我們會(huì)在之后看到更多關(guān)于它的東西)
  • 能夠在不同語(yǔ)言之間來(lái)回切換 (主語(yǔ)言, 正則表達(dá)式語(yǔ)言, 引用(quoting)語(yǔ)言)
  • 能夠動(dòng)態(tài)地派生出新語(yǔ)言 (新運(yùn)算符, 自定義引用構(gòu)造)
  • 在自下而上的表達(dá)式解析和自頂向下的更大的結(jié)構(gòu)解析之間無(wú)縫集成
  • 保持令人驚嘆的錯(cuò)誤報(bào)告的各種狀態(tài)

所有這些本質(zhì)上代表了解析中的新范例。

非靜態(tài)類型或動(dòng)態(tài)類型

Perl 6 是一種 漸進(jìn)類型化的語(yǔ)言。

my int $distance = distance-between('Lund', 'Kiev');
my int $time = prompt('Travel time: ').Int;
say "Average speed: { $distance / $time }";

我們想利用 $distance$time 原生整數(shù)來(lái)產(chǎn)生更好的代碼, 如果我們不知道類型(應(yīng)該只是輸出代碼中的原生除法指令)赎懦。

模糊編譯時(shí)和運(yùn)行時(shí)

運(yùn)行時(shí)可以做一些編譯時(shí):

EVAL slurp @demos[$n];

編譯時(shí)可以做一些運(yùn)行時(shí):

my $comp-time = BEGIN now;

注意編譯時(shí)計(jì)算的結(jié)果必須持久化直到運(yùn)行時(shí), 這它們之間可能有一個(gè)處理(process)邊界!

NQP 作為語(yǔ)言

Perl 6 文法顯然需要用 Perl 6 表示雀鹃。這反過(guò)來(lái)將需要集成到編譯器的其余部分。用 Perl 6 編寫整個(gè)編譯器是很自然的励两。

然而, 一個(gè)完整的 Perl 6 太大, 給它寫一個(gè)好的優(yōu)化器花費(fèi)太多時(shí)間黎茎。
因此, NQP (Not Quite Perl 6) 語(yǔ)言誕生了:它是 Perl 6 的一個(gè)子集, 用于實(shí)現(xiàn)編譯器。NQP 和 Rakudo 大部分都是用 NQP 寫的当悔。

NQP 作為編譯器構(gòu)造工具鏈

NQP src 目錄中不僅僅是 NQP 本身傅瞻。

  • NQP, how, core: 這些包含 NQP 編譯器, 元對(duì)象(它指定了 NQP 的類和 roles 的工作方式)和內(nèi)置函數(shù)。
  • HLL: 構(gòu)造高級(jí)語(yǔ)言編譯器的通用結(jié)構(gòu), 在 Rakudo 和 NQP 之間共享盲憎。
  • QAST: Q 抽象語(yǔ)法樹(shù)的節(jié)點(diǎn)嗅骄。代表著程序語(yǔ)法的樹(shù)節(jié)點(diǎn)。(即, 當(dāng)它執(zhí)行時(shí)會(huì)做什么)饼疙。
  • QRegex: 解析和執(zhí)行 regexes 和 grammars 時(shí)所提到的對(duì)象溺森。
  • vm: 虛擬機(jī)抽象層。因?yàn)?NQP 和 Rakudo 可以運(yùn)行在 Parrot, JVM 和 MoarVM 上窑眯。

QAST

QAST 樹(shù)是 NQP 和 Rakudo 內(nèi)部最重要的數(shù)據(jù)結(jié)構(gòu)之一屏积。

抽象語(yǔ)法樹(shù)表示程序在執(zhí)行時(shí)執(zhí)行的操作。它是抽象的意思是從一個(gè)程序被寫入的特定語(yǔ)言中抽象出來(lái)磅甩。

example-qast.png

QAST

不同的 QAST 節(jié)點(diǎn)代表著像下面這樣的東西:

  • 變量
  • 運(yùn)算 (算術(shù), 字符串, 調(diào)用, 等等.)
  • 字面值
  • Blocks

注意像類那樣的東西沒(méi)有 QAST 節(jié)點(diǎn), 因?yàn)槟切┦蔷幾g時(shí)聲明而非運(yùn)行時(shí)執(zhí)行炊林。

nqp::op 集合

編譯器工具鏈的另一個(gè)重要部分是 nqp::op 指令集。你會(huì)有兩種方式遇到它, 并且了解它們之間的差異很重要卷要!

您可以在 NQP 代碼 中使用它們, 在這種情況下, 您說(shuō)您希望在程序中的那個(gè)點(diǎn)上執(zhí)行該操作:

say(nqp::time_n())

在代表著正在編譯的程序的 QAST樹(shù) 中也使用完全相同的指令集:

QAST::Op.new(
    :op('call'), :name('&say'),
    QAST::Op.new( :op('time_n') )
)

Bootstrapping in a nutshell

人們可能會(huì)想知道 NQP 幾乎完全用 NQP 編寫時(shí)是如何編譯的渣聚。

在每個(gè) vm 子目錄中都有一個(gè) stage0 目錄。它包含一個(gè)編譯的 NQP(Parrot上是PIR文件, JVM上是JAR文件等)然后:

nqp-bootstrapping-stages.png

因此, 你 make test 的 NQP 是可以重新創(chuàng)建自身的 NQP僧叉。

通常, 我們使用最新版本來(lái)更新 stage0

How Rakudo uses NQP

Rakudo 本身不是一個(gè)自舉編譯器, 這使得它的開(kāi)發(fā)容易一點(diǎn)奕枝。大部分 Rakudo 是用 NQP 編寫的。這包括:

  • 編譯器本身的核心, 它解析 Perl 6 源代碼, 構(gòu)建 QAST, 管理聲明并進(jìn)行各種優(yōu)化
  • 元對(duì)象, 它指定了不同類型(類, roles, 枚舉, subsets)是如何工作的
  • bootstrap, 它將足夠的 Perl 6 核心類型組合在一起, 以便能夠在 Perl 6 中編寫內(nèi)置的類, 角色和例程

因此, 雖然一些 Rakudo 是可訪問(wèn)的, 如果你知道 Perl 6, 知道 NQP - 既作為一種語(yǔ)言又作為一種編譯器工具鏈 - 是和大部分 Rakudo 的其他部分工作的入口彪标。

NQP 語(yǔ)言

它不完全是 Perl 6(Not Quite Perl 6), 但是能很好地構(gòu)建 Perl 6

設(shè)計(jì)目標(biāo)

NQP 被設(shè)計(jì)為……

  • 理想的編寫編譯器相關(guān)的東西
  • 幾乎是 Perl 6 的一個(gè)子集
  • 比 Perl 6 更容易編譯和優(yōu)化

注意, 它避免了

  • 賦值
  • Flattening and laziness
  • 操作符的多重分派(因此沒(méi)有重載)
  • 有很多 built-ins

字面量

整數(shù)字面量

0       42      -100

浮點(diǎn)字面量 (NQP 中沒(méi)有 rat!)

0.25    1e10    -9.9e-9

字符串字面量

'non-interpolating'         "and $interpolating"
q{non-interpolating}        qq{and $interpolating}
Q{not even backslashes}

Sub 調(diào)用

在 NQP 中這些總是需要圓括號(hào):

say('Mushroom, mushroom');

像 Perl 6 中一樣, 這為子例程的名字添加 & 符號(hào)并對(duì)該例程做詞法查詢倍权。

然而, 沒(méi)有列表操作調(diào)用語(yǔ)法:

plan 42;    # "Confused" parse error
foo;        # Does not call foo; always a term

這可能是 NQP 初學(xué)者最常見(jiàn)的錯(cuò)誤。

變量

可以是 my (lexical) 或 our (package) 作用域的:

my $pony;
our $stable;

常用的符號(hào)集也是可用的:

my $ark;                # Starts as NQPMu
my @animals;            # Starts as []
my %animal_counts;      # Starts as {}
my &lasso;              # Starts as 

也支持動(dòng)態(tài)變量

my @*blocks;

綁定

NQP 沒(méi)有提供 = 賦值操作符捞烟。只提供了 := 綁定操作符薄声。這使 NQP 免于 Perl 6 容器語(yǔ)義的復(fù)雜性。

下面是一個(gè)簡(jiǎn)單的標(biāo)量示例:

my $ast := QAST::Op.new( :op('time_n') );

綁定與數(shù)組

注意綁定擁有item 賦值優(yōu)先, 所以你不可以這樣寫:

my @states := 'start', 'running', 'done';    # Wrong!

相反, 這應(yīng)該被表示為下面的其中之一:

my @states := ['start', 'running', 'done'];  # Fine
my @states := ('start', 'running', 'done');  # Same thing
my @states := <start running done>;          # Cutest

原生類型化變量

目前, NQP 并不真正支持對(duì)變量的類型約束题画。唯一的例外是它會(huì)注意原生類型默辨。

my int $idx := 0;
my num $vel := 42.5;
my str $mug := 'coffee'; 

注意: 在 NQP 中, 綁定用于原生類型!這在 Perl 6 中是非法的, Perl 6 中原生類型只能被賦值苍息。盡管這是非常武斷的, 目前 Perl 6 中原生類型的賦值實(shí)際上被編譯到 nqp::bind(...) op 中缩幸!

控制流

大部分 Perl 6 條件結(jié)構(gòu)和循環(huán)結(jié)構(gòu)也存在于 NQP 中壹置。就像在真實(shí)的 Perl 6 中一樣, 條件的周圍不需要圓括號(hào), 并且還能使用 pointy 塊。循環(huán)結(jié)構(gòu)支持 next/last/redo.

if $optimize {
    $ast := optimize($ast);
}
elsif $trace {
    $ast := insert_tracing($ast);
}

可用的: if, unless, while, until, repeat, for

還沒(méi)有的: loop, given/when, FIRST/NEXT/LAST phasers

子例程

與 Perl 6 中子例程的聲明很像, 但是 NQP 中即使沒(méi)有參數(shù), 參數(shù)列表也是強(qiáng)制的表谊。你可以 return 或使用最后一個(gè)語(yǔ)句作為隱式返回值钞护。

sub mean(@numbers) {
    my $sum;
    for @numbers { $sum := $sum + $_ }
    return $sum / +@numbers;
}

Slurpy 參數(shù)也是可用的, 就像 | 用來(lái)展開(kāi)參數(shù)列表那樣。

注意: 參數(shù)可以獲得類型約束, 但是與變量一樣, 當(dāng)前只有原生類型爆办。(例外:多重分派;以后會(huì)有更多难咕。)

Named arguments and parameters

支持命名參數(shù):

sub make_op(:$name) {
    QAST::Op.new( :op($name) )
}

make_op(name => 'time_n');  # 胖箭頭語(yǔ)法
make_op(:name<time_n>);     # Colon-pair 語(yǔ)法
make_op(:name('time_n'));   # The same

注意: NQP 中沒(méi)有 Pair 對(duì)象!Pairs - colonpairs 或 fat-arrow 對(duì)兒 - 僅在參數(shù)列表上下文中有意義距辆。

Blocks 和 pointy blocks

尖尖塊提供了熟悉的 Perl 6 語(yǔ)法:

sub op_maker_for($op) {
    return -> *@children, *%adverbs {
        QAST::Op.new( :$op, |@children, |%adverbs )
    }
}

從這個(gè)例子可以看出, 它們有閉包語(yǔ)義余佃。

注意: 普通塊也可用作閉包, 但不像 Perl 6 那樣使用隱式的 $_ 參數(shù)。

Built-ins 和 nqp::ops

NQP 具有相對(duì)較少的內(nèi)置函數(shù)跨算。但是, 它提供了對(duì) NQP 指令集的完全訪問(wèn)爆土。這里有幾個(gè)常用的指令, 知道它們會(huì)很有用。

# On arrays
nqp::elems, nqp::push, nqp::pop, nqp::shift, nqp::unshift

# On hashes
nqp::elems, nqp::existskey, nqp::deletekey

# On strings
nqp::substr, nqp::index, nqp::uc, nqp::lc

我們將在課程中發(fā)現(xiàn)更多诸蚕。

異常處理

可以使用 nqp::die 指令拋出一個(gè)異常:

nqp::die('Oh gosh, something terrible happened');

tryCATCH 指令也是可用的, 盡管不像完全的 Perl 6, 你沒(méi)有期望去智能匹配 CATCH 的內(nèi)部步势。一旦你到了那兒, 就認(rèn)為異常被捕獲到了(彈出一個(gè)顯式的 nqp::rethrow)。

try {
    something();
    CATCH { say("Oops") }
}

類, 屬性和方法

就像在 Perl 6 中一樣, 使用 class, hasmethod 關(guān)鍵字來(lái)聲明背犯。類可以是詞法(my)作用域的或包(our)作用域的(默認(rèn))立润。

class VariableInfo {
    has @!usages;
    
    method remember_usage($node) {
        nqp::push(@!usages, $node)
    }
    
    method get_usages() {
        @!usages
    }
}

self 關(guān)鍵字也是可用的, 方法可以具有像 subs 那樣的參數(shù)。

More on attributes

NQP 沒(méi)有自動(dòng)的存取器生成, 所以你不能這樣做:

has @.usages; # 不支持

支持原生類型的屬性, 并且將直接有效地存儲(chǔ)在對(duì)象體中媳板。任何其他類型都被忽略。

has int $!flags;

與 Perl 6 不同, 默認(rèn)構(gòu)造函數(shù)可用于設(shè)置私有屬性, 因?yàn)檫@是我們所擁有的泉哈。

my $vi := VariableInfo.new(usages => @use_so_far);

Roles (1)

NQP 支持 roles. 像類那樣, roles 可以擁有屬性和方法蛉幸。

role QAST::CompileTimeValue {
    has $!compile_time_value;
    
    method has_compile_time_value() {
        1
    }
    
    method compile_time_value() {
        $!compile_time_value
    }
    
    method set_compile_time_value($value) {
        $!compile_time_value := $value
    }
}

Roles (2)

role 可以使用 does trait 組合到類中:

class QAST::WVal is QAST::Node does QAST::CompileTimeValue {
    # ...
}

或者, MOP 可用于將 role 混合到單個(gè)對(duì)象中:

method set_compile_time_value($value) {
    self.HOW.mixin(self, QAST::CompileTimeValue);
    self.set_compile_time_value($value);
}

多重分派

支持基本多重分派。它是 Perl 6 語(yǔ)義的一個(gè)子集, 使用更簡(jiǎn)單(但兼容)的候選排序算法版本丛晦。

與完全的 Perl 6 不同, 你**必須寫一個(gè) proto ** sub 或方法; 沒(méi)有自動(dòng)生成奕纫。

proto method as_jast($node) {*}

multi method as_jast(QAST::CompUnit $cu) {
    # compile a QAST::CompUnit
}

multi method as_jast(QAST::Block $block) {
    # compile a QAST::Block
}

練習(xí) 1

有機(jī)會(huì)熟悉基本的 NQP 語(yǔ)法, 如果你還沒(méi)有這樣做。

還有機(jī)會(huì)學(xué)習(xí)常見(jiàn)的錯(cuò)誤看起來(lái)是什么樣的, 所以如果你在實(shí)際工作中遇到他們, 你可以認(rèn)出它們烫沙。 :-)

Grammars

雖然在許多領(lǐng)域 NQP 相比完全的 Perl 6 相當(dāng)有限, 但是 grammar 幾乎支持相同的水平匹层。這是因?yàn)?NQP 語(yǔ)法必須足夠好以處理解析 Perl 6 本身。

Grammars 是一種類, 并且使用 grammar 關(guān)鍵字引入锌蓄。

grammar INIFile {
}

事實(shí)上, grammars 太像類了, 以至于在 NQP 中, 它們是由相同的元對(duì)象實(shí)現(xiàn)的升筏。區(qū)別是它們默認(rèn)繼承于什么, 并且你把什么放在它們里面。

INI 文件

作為一個(gè)簡(jiǎn)單的例子, 我們將考慮解析 INI 文件瘸爽。

帶有值的鍵, 可能按章節(jié)排列您访。

name = Animal Facts
author = jnthn

[cat]
desc = The smartest and cutest
cuteness = 100000

[dugong]
desc = The cow of the sea
cuteness = -10

整體方法

grammar 包含一組用關(guān)鍵字 token, ruleregex 聲明的規(guī)則。真的, 他們就像方法一樣, 但是用規(guī)則語(yǔ)法寫成剪决。

token integer { \d+ }       # one or more digits
token sign    { <[+-]> }    # + or - (character class)

更復(fù)雜的規(guī)則由調(diào)用現(xiàn)有規(guī)則組成:

token signed_integer { <sign>? <integer> }

這些對(duì)其他規(guī)則的調(diào)用可以被量化, 放在備選分支中, 等等灵汪。

旁白:grammar和正則表達(dá)式

在這一點(diǎn)上, 你可能想知道 grammar 和正則表達(dá)式是如何關(guān)聯(lián)的檀训。畢竟, grammar 似乎是由正則表達(dá)式那樣的東西組成的。

還有一個(gè) regex 聲明符, 可以在 grammar 中使用享言。

regex email { <[\w.-]>+ '@' <[\w.-]>+ '.' \w+ }

關(guān)鍵的區(qū)別是 regex 會(huì)回溯, 而 ruletoken 不會(huì)峻凫。支持回溯涉及保持大量狀態(tài), 并且對(duì)于復(fù)雜的 grammar 解析大量輸入, 這將快速消耗大量?jī)?nèi)存!大語(yǔ)言往往在解析器中避免回溯览露。

旁白: NQP 中的正則表達(dá)式

對(duì)于較小規(guī)模的東西, NQP 確實(shí)也在普通場(chǎng)景中為正則表達(dá)式提供支持荧琼。

if $addr ~~ /<[\w.-]>+ '@' <[\w.-]>+ '.' \w+/ {
    say("I'll mail you maybe");
}
else {
    say("That's no email address!");
}

這被求值為匹配對(duì)象。

解析條目

一個(gè)條目有一個(gè)鍵(一些單詞字符)和一個(gè)值(直到行尾的所有內(nèi)容):

token key   { \w+ }
token value { \N+ }

合在一起, 它們組成了一個(gè)條目:

token entry { <key> \h* '=' \h* <value> }

\h 匹配任何水平空白(空格, 制表符等)肛循。 = 號(hào)必須加引號(hào), 因?yàn)槿魏畏亲帜笖?shù)字都被視為 Perl 6 中的正則表達(dá)式語(yǔ)法铭腕。

TOP 開(kāi)始

grammar 的入口點(diǎn)是一個(gè)特殊的規(guī)則, “TOP”。現(xiàn)在, 我們查找整個(gè)文件是否含有包含條目的行, 或者只是沒(méi)有多糠。

token TOP {
    ^
    [
    | <entry> \n
    | \n
    ]+
    $
}

注意在 Perl 6 中, 方括號(hào)是非捕獲組(Perl 5 的 (?:...)), 而非字符類.

嘗試我們的 grammar

我們可以通過(guò)在 grammar 上調(diào)用 parse 方法來(lái)嘗試我們的 grammar累舷。這將返回一個(gè)匹配對(duì)象

my $m := INIFile.parse(Q{
name = Animal Facts
author = jnthn
});
example-match-object.png

迭代結(jié)果

每個(gè) rule 調(diào)用都產(chǎn)生一個(gè) match 對(duì)象, <entry> 調(diào)用語(yǔ)法會(huì)把它捕獲到 match 對(duì)象中夹孔。

因?yàn)槲覀兤ヅ淞撕芏?entries, 所以我們?cè)?match 對(duì)象中的 entry 鍵下面得到一個(gè)數(shù)組被盈。

因此, 我們能夠遍歷它以得到每一個(gè) entry:

for $m<entry> -> $entry {
    say("Key: {$entry<key>}, Value: {$entry<value>}");
}

追蹤我們的 grammar

NQP 自帶一些內(nèi)置支持, 用于跟蹤 grammars 的去向。它不是一個(gè)完整的調(diào)試器, 但用它來(lái)查看 grammar 在失敗之前走的有多遠(yuǎn)是有用的搭伤。它使用 trace-on 函數(shù)開(kāi)啟:

INIFile.HOW.trace-on(INIFile);

并且產(chǎn)生的結(jié)果像下面這樣:

Calling parse
  Calling TOP
    Calling entry
      Calling key
      Calling value
    Calling entry
      Calling key
      Calling value

token vs. rule

當(dāng)我們使用 rule 代替 token 時(shí), 原子后面的任何空白被轉(zhuǎn)換為對(duì) ws非捕獲調(diào)用只怎。即:

rule entry { <key> '=' <value> }

等價(jià)于:

token entry { <key> <.ws> '=' <.ws> <value> <.ws> } # . = non-capturing

我們繼承了一個(gè)默認(rèn)的 ws, 但是我們也能提供我們自己的:

token ws { \h* }

解析 sections (1)

一個(gè) section 有一個(gè)標(biāo)題和許多條目。但是, 頂層也可以有條目怜俐。因此, 把這個(gè)分解是有意義的身堡。

token entries {
    [
    | <entry> \n
    | \n
    ]+
}

然后 TOP 規(guī)則可以變?yōu)?

token TOP {
    ^
    <entries>
    <section>+
    $
}

解析 sections (2)

最后但并非最不重要的是 section token:

token section {
    '[' ~ ']' <key> \n
    <entries>
}

這個(gè) ~ 語(yǔ)法很漂亮. 第一行就像這樣:

'[' <key> ']' \n

然而, 如果找不到閉合 ] 就會(huì)產(chǎn)生一個(gè)描述性的錯(cuò)誤消息, 而不只是失敗匹配。

Actions

解析 grammar 可以使用 actions 類; 它的方法具有所匹配 grammar 中的某些或所有規(guī)則的名字拍鲤。

相應(yīng)規(guī)則成功匹配之后, actions 方法被調(diào)用贴谎。

top-down-bottom-up.png

在 Rakudo 和 NQP 編譯器中, actions構(gòu)造QAST樹(shù)。對(duì)于這個(gè)例子, 我們將做一些更簡(jiǎn)單的事情季稳。

Actions 示例: aim

給定像下面這樣的 INI 文件:

name = Animal Facts
author = jnthn

[cat]
desc = The smartest and cutest
cuteness = 100000

我們想使用 actions 類來(lái)建立一個(gè)散列的散列擅这。頂級(jí)哈希將包含鍵 cat_(下劃線收集不在 section 中的任何鍵)。其值是該 section 中鍵/值對(duì)的哈希景鼠。

Actions 示例: entries

Action 方法將剛剛匹配過(guò)的 rule 的 Match 對(duì)象作為參數(shù)仲翎。
把這個(gè) Match 對(duì)象放到 $/ 里很方便, 所以我們能夠使用 $<entry> 語(yǔ)法糖 (它映射到 $/<entry> 上)。這個(gè)語(yǔ)法糖看起來(lái)像普通的標(biāo)量, 第一眼看上去的時(shí)候有點(diǎn)懵, 再看一眼發(fā)現(xiàn)它有一對(duì) <> 后環(huán)綴, 而這正是散列中才有的, <entry> 相當(dāng)于 {'entry'}, 不過(guò)前者更漂亮铛漓。

class INIFileActions {
    method entries($/) { # Match Object 放在參數(shù) $/ 中
        my %entries;
        for $<entry> -> $e {
            %entries{$e<key>} := ~$e<value>;
        }
        make %entries;
    }
}

最后, make 將生成的散列附加$/ 上溯香。這就是為什么 'TOP` action 方法能夠在構(gòu)建頂級(jí)哈希時(shí)檢索它。

Actions example: TOP

TOP action 方法在由 entries action 方法創(chuàng)建的散列中構(gòu)建頂級(jí)散列浓恶。當(dāng) make 將某個(gè)東西附加到 $/ 上時(shí), .ast 方法檢索附加到其他匹配對(duì)象上的東西逐哈。

method TOP($/) {
    my %result;
    %result<_> := $<entries>.ast;
    for $<section> -> $sec {
        %result{$sec<key>} := $sec<entries>.ast;
    }
    make %result;
}

因此, 頂層散列通過(guò) section 名獲取到由安裝到其中的 entries action 方法產(chǎn)生的散列。

Actions 示例: 用 actions 解析

actions 作為具名參數(shù)傳遞給 parse:

my $m := INIFile.parse($to_parse, :actions(INIFileActions.new));

結(jié)果散列可以使用 .ast 從結(jié)果匹配對(duì)象中獲得, 正如我們已經(jīng)看到的问顷。

my %sections := $m.ast;
for %ini -> $sec {
    say("Section {$sec.key}");
    for $sec.value -> $entry {
        say("    {$entry.key}: {$entry.value}");
    }
}

Actions 示例: 輸出

上一張幻燈片上的轉(zhuǎn)儲(chǔ)代碼產(chǎn)生如下輸出:

Section _
    name: Animal Facts
    author: jnthn
Section cat
    desc: The smartest and cutest
    cuteness: 100000
Section dugong
    desc: The cow of the sea
    cuteness: -10

練習(xí) 2

使用 gramamrs 和 actions 進(jìn)行小練習(xí)的一次機(jī)會(huì)昂秃。

目標(biāo)是解析 Perl 6 IRC 日志的文本格式; 例如, 參見(jiàn) http://irclog.perlgeek.de/perl6/2013-07-19/text

另外一個(gè)例子: SlowDB

解析 INI 文件這個(gè)例子是一個(gè)很好的開(kāi)端, 但是離編譯器還差的遠(yuǎn)禀梳。作為那個(gè)方向的進(jìn)一步深入, 我們會(huì)使用查詢解釋器創(chuàng)建一個(gè)小的, 無(wú)聊的, 在內(nèi)存中的數(shù)據(jù)庫(kù)。

它應(yīng)該像下面這樣工作:

INSERT name = 'jnthn', age = 28
[
    result: Inserted 1 row
]
SELECT name WHERE age = 28
[
    name: jnthn
]
SELECT name WHERE age = 50
Nothing found

查詢解釋器 (1)

我們解析 INSERTSELECT 查詢的任意之一.

token TOP {
    ^ [ <insert> | <select> ] $
}

token insert {
    'INSERT' :s <pairlist>
}

token select {
    'SELECT' :s <keylist>
    [ 'WHERE' <pairlist> ]?
}

注意 :s 開(kāi)啟了自動(dòng) <.ws> 插入.

The query parser (2)

pairlistkeylist rules 的定義如下:

rule pairlist { <pair>+ % [ ',' ] }
rule pair     { <key> '=' <value>  }
rule keylist  { <key>+ % [ ',' ] }
token key     { \w+ }

這兒有一個(gè)有意思的新的語(yǔ)法是 %. 它附件到末尾的量詞上, 表明某些東西(這里是逗號(hào))應(yīng)該出現(xiàn)在每個(gè)量詞化的元素之間肠骆。

逗號(hào)字面值周圍的方括號(hào)是為了確保 <.ws> 調(diào)用被生成為分割符的一部分算途。

查詢解析器 (3)

最后, 這兒是關(guān)于值是這樣被解析的。

token value { <integer> | <string> }
token integer { \d+ }
token string  { \' <( <-[']>+ )> \' }

注意 <()> 語(yǔ)法的使用蚀腿。這些表明通過(guò) string token 整體應(yīng)該捕獲什么的限制嘴瓤。意味著引號(hào)字符不會(huì)被捕獲。

備選分支和 LTM (1)

回憶一下 top rule:

token TOP {
    ^ [ <insert> | <select> ] $
}

如果我們追蹤 SELECT 查詢的解析, 我們會(huì)看到像下面這樣的東西:

Calling parse
  Calling TOP
    Calling select
      Calling ws
      Calling keylist

所以它怎么知道不去麻煩嘗試 <insert> 呢?

備選分支和 LTM (2)

答案是可傳遞的最長(zhǎng)Token匹配(Transitive Longest Token Matching). grammar 引擎創(chuàng)建了一個(gè) NFA (狀態(tài)機(jī)), 一旦遇到一個(gè)備選分支(alternation), 就按照這個(gè)備選分支能夠匹配到的字符數(shù)對(duì)分支進(jìn)行排序莉钙。然后 Grammar 引擎在這些分支中首先嘗試匹配最多字符的那個(gè), 而不麻煩那些它認(rèn)為不可能的分支廓脆。

備選分支和 LTM (3)

Gramamr 引擎不會(huì)僅僅孤立地看一個(gè) rule。相反, 它 可傳遞性地考慮 subrule 調(diào)用 (considers subrule calls transitively). 這意味著導(dǎo)致某種不可能的整個(gè)調(diào)用鏈可以被忽略磁玉。

ltm-transformation.png

它由非聲明性構(gòu)造(如向前查看, 代碼塊或?qū)δJ(rèn)ws規(guī)則的調(diào)用)或遞歸 subrule 調(diào)用界定停忿。

輕微的痛點(diǎn)

令我們討厭的一件事情就是我們的 TOP action 方法最后看起來(lái)像這樣:

method TOP($/) {
    make $<select> ?? $<select>.ast !! $<insert>.ast;
}

顯而易見(jiàn), 一旦我們添加 UPDATEDELETE 查詢, 維護(hù)起來(lái)將會(huì)多么痛苦

我們的 value action 方法類似:

method value($/) {
    make $<integer> ?? $<integer>.ast !! $<string>.ast;
}

Protoregexes

令我們痛苦的答案是 protoregexes。 它們提供了一個(gè)更可擴(kuò)展的方式來(lái)表達(dá)備選分支

proto token value {*}
token value:sym<integer> { \d+ }
token value:sym<string>  { \' <( <-[']>+ )> \' }

本質(zhì)上, 我們引入了一個(gè)新的語(yǔ)法類別, value, 然后定義這個(gè)類別下不同的案例(cases)蚊伞。像 value 這樣的調(diào)用會(huì)使用 LTM 來(lái)對(duì)候選者進(jìn)行排序和嘗試 - 就像備選分支所做的那樣席赂。

Protoregexes 和 action 方法 (1)

回到 actions 類, 我們需要更新我們的 action 方法來(lái)匹配 rules 的名字:

method value:sym<integer>($/) { make ~$/ }
method value:sym<string>($/)  { make ~$/ }

然而, 我們不需要 value 自身這個(gè) action 方法。任何查看 $<value> 的東西會(huì)被提供一個(gè)來(lái)自成功候選者的匹配對(duì)象 - 并且 $<value>.ast 因此會(huì)獲得正確的東西时迫。

Protoregexes and action methods (2)

例如, 在我們重構(gòu)查詢之后:

token TOP { ^ <query> $ }

proto token query {*}
token query:sym<insert> {
    'INSERT' :s <pairlist>
}
token query:sym<select> {
    'SELECT' :s <keylist>
    [ 'WHERE' <pairlist> ]?
}

TOP action 方法可以簡(jiǎn)化為:

method TOP($/) {
    make $<query>.ast;
}

keylist 和 pairlist

這兩個(gè)是無(wú)聊的 action 方法, 包含完整性颅停。

method pairlist($/) {
    my %pairs;
    for $<pair> -> $p {
        %pairs{$p<key>} := $p<value>.ast;
    }
    make %pairs;
}

method keylist($/) {
    my @keys;
    for $<key> -> $k {
        nqp::push(@keys, ~$k)
    }
    make @keys;
}

解釋查詢

那么我們?nèi)绾芜\(yùn)行查詢呢?好吧, 下面是 INSERT 查詢的 action 方法:

method query:sym<insert>($/) {
    my %to_insert := $<pairlist>.ast;
    make -> @db {
        nqp::push(@db, %to_insert);
        [nqp::hash('result', 'Inserted 1 row' )]
    };
}

在這里, 我們不使用數(shù)據(jù)結(jié)構(gòu)。我們 make 了一個(gè)閉包來(lái)接收當(dāng)前數(shù)據(jù)庫(kù)狀態(tài)(一個(gè)散列的數(shù)組, 其中每一個(gè)散列是一行)并把由 pairlist action 方法產(chǎn)生的散列推到那個(gè)數(shù)組中掠拳。

SlowDB 類自身

class SlowDB {
    has @!data;
    
    method execute($query) {
        if QueryParser.parse($query, :actions(QueryActions.new)) -> $parsed {
            my $evaluator := $parsed.ast;
            if $evaluator(@!data) -> @results {
                for @results -> %data {
                    say("[");
                    say("    {$_.key}: {$_.value}") for %data;
                    say("]");
                }
            } else {
                say("Nothing found");
            }
        } else {
            say('Syntax error in query');
        }
    }
}

練習(xí) 3

一次練習(xí) protoregexes 的機(jī)會(huì)并自己學(xué)習(xí)我們已經(jīng)復(fù)習(xí)過(guò)的癞揉。

拿 SlowDB 這個(gè)我們已經(jīng)思考過(guò)的例子來(lái)說(shuō)。給這個(gè)例子添加 UPDATEDELETE 查詢支持溺欧。

限制和與完整 Perl 6 的其它區(qū)別

這兒有一個(gè)值得了解的其它事情的雜燴烧董。

  • 有一個(gè) use 語(yǔ)句, 但它期望它使用已經(jīng)預(yù)編譯的任何東西。
  • 沒(méi)有數(shù)組展平; [@a, @b] 總是兩個(gè)元素的數(shù)組
  • 散列構(gòu)造 {} 符只對(duì)空散列有效; 除了它之外的任何一個(gè) {} 都會(huì)被當(dāng)作一個(gè) block
  • BEGIN 塊存在, 但在我們能看見(jiàn)的外部作用域中是高度受限的(只有類型, 而不是變量)

后端的區(qū)別

JVM 和 MoarVM 上的 NQP 相對(duì)比較一致胧奔。Parrot 上的 NQP 有點(diǎn)古怪: 不是所有的東西都是 6model 對(duì)象。即雖然在 JVM 和 MoarVM 上, NQP 中的 .WHAT.HOW 會(huì)工作良好, 但是在 Parrot 上它會(huì)失敗预吆。這發(fā)生在整數(shù), 數(shù)字和字符串字面值, 數(shù)組和散列, 異常和某些種類的代碼對(duì)象身上龙填。

異常處理程序也有所不同。那些在 JVM 和 MoarVM 上運(yùn)行的堆棧頂部的異常拋出點(diǎn), 就像 Perl 6 語(yǔ)義那樣拐叉。那些在 Parrot 上的 NQP 將解開(kāi)然后運(yùn)行, 恢復(fù)由continuation提供岩遗。注意, Rakudo 在所有后端都是一致的。

總而言之...

NQP 盡管是 Perl 6 的一個(gè)相對(duì)較小的子集, 但仍然包含相當(dāng)多強(qiáng)大的語(yǔ)言特性凤瘦。

一般來(lái)說(shuō), 對(duì)他們的需求是由在 Rakudo 中工作的人的需求所驅(qū)動(dòng)的宿礁。因此, NQP 功能集是由編譯器編寫需求定義的。

我們所覆蓋的 grammar 和 action 方法資料可能是最重要的, 因?yàn)檫@是理解 NQP 和 Perl 6 是如何編譯的起點(diǎn)蔬芥。

編譯管道

一步一步我們編譯完了那個(gè)程序...

從開(kāi)始到完成

現(xiàn)在我們了解了一點(diǎn)作為語(yǔ)言的 NQP, 是時(shí)候潛入到下面, 看看當(dāng)我們喂給 NQP 一個(gè)程序運(yùn)行時(shí)會(huì)發(fā)生什么梆靖。

首先, 我們將考慮這個(gè)簡(jiǎn)單的例子...

nqp -e "say('Hello, world')"

...一路從 NQP 的 sub MAIN 到出現(xiàn)輸出控汉。

我們將選擇 JVM 后端來(lái)檢查這個(gè)。

"stagestats" 選項(xiàng)

我們可以通過(guò)使用 --stagestats 選項(xiàng)來(lái)了解 NQP 內(nèi)部發(fā)生的情況, 該選項(xiàng)顯示了編譯器每個(gè)階段經(jīng)歷的時(shí)間返吻。

nqp --stagestats -e "say('Hello, world')"

Stage start      :   0.000      # 啟動(dòng)
Stage classname  :   0.010      # 計(jì)算類名
Stage parse      :   0.067      # 解析源文件, 構(gòu)造 AST
Stage ast        :   0.000      # 獲取 AST
Stage jast       :   0.106      # 轉(zhuǎn)換成 JVM AST
Stage classfile  :   0.032      # 轉(zhuǎn)換成 JVM 字節(jié)碼
Stage jar        :   0.000      # 可能創(chuàng)建一個(gè) JAR
Stage jvm        :   0.002      # 真正地運(yùn)行該代碼

傾倒解析樹(shù)

我們可以得到一些轉(zhuǎn)儲(chǔ)的階段姑子。例如, --target = parse 將產(chǎn)生一個(gè)解析樹(shù)的轉(zhuǎn)儲(chǔ)。

- statementlist: say('Hello world')
  - statement: 1 matches
    - EXPR: say('Hello world')
      - deflongname: say
        - identifier: say
      - args: ('Hello world')
        - arglist: 'Hello world'
          - EXPR: 'Hello world'
            - value: 'Hello world'
              - quote: 'Hello world'
                - quote_EXPR: 'Hello world'
                  - quote_delimited: 'Hello world'
                    - quote_atom: 1 matches
                    - stopper: '
                    - starter: '

傾倒 AST

有時(shí)有用的是 --target = ast, 它轉(zhuǎn)儲(chǔ)了 QAST(下面的輸出已經(jīng)被簡(jiǎn)化)测僵。

- QAST::CompUnit
  - QAST::Block
    - QAST::Var(lexical @ARGS :decl(param))
    - QAST::Stmts
      - QAST::Var(lexical GLOBALish :decl(static))
      - QAST::Var(lexical $?PACKAGE :decl(static))
      - QAST::Var(lexical EXPORT :decl(static))
    - QAST::Stmts say('Hello world')
      - QAST::Stmts
        - QAST::Op(call &say) 'Hello world'
          - QAST::SVal(Hello world)

傾倒 JVM AST

你甚至可以得到一些代表性的低級(jí) AST, 它使用 --target = jast 變成 Java 字節(jié)碼, 但它完全令人腦抽(下面的一小部分用以說(shuō)明)街佑。 :-)

.push_sc Hello world
58 __TMP_S_0
.push_sc &say
.push_idx 1
43 
25 __TMP_S_0
.try
186 subcall_noa org/perl6/nqp/runtime/IndyBootstrap subcall_noa 0
:reenter_1
.catch Lorg/perl6/nqp/runtime/SaveStackException;
.push_idx 1
167 SAVER
.endtry

一窺究竟

我們的旅程從 NQP 的 MAIN sub 開(kāi)始, 它位于 src/NQP/Compiler.nqp
這里是一個(gè)略微簡(jiǎn)化的版本(剝離設(shè)置命令行選項(xiàng)和其它細(xì)節(jié))捍靠。

class NQP::Compiler is HLL::Compiler {
}

# 創(chuàng)建并配置編譯器對(duì)象
my $nqpcomp := NQP::Compiler.new();
$nqpcomp.language('nqp');
$nqpcomp.parsegrammar(NQP::Grammar);
$nqpcomp.parseactions(NQP::Actions);

sub MAIN(*@ARGS) {
    $nqpcomp.command_line(@ARGS, :encoding('utf8'));
}

HLL::Compiler 類

command_line 方法繼承自位于 src/HLL/Compiler.nqp 中的 HLL::Compiler沐旨。此類包含協(xié)調(diào)編譯過(guò)程的邏輯。

其功能包括:

  • 參數(shù)處理(委托給 HLL::CommandLine)
  • 從磁盤讀取源文件
  • 調(diào)用每個(gè)階段, 如果指定, 停在--target
  • 提供 REPL
  • 提供可插拔的方式來(lái)處理未捕獲的異常

通過(guò) HLL::Compiler 的路徑

command_line 解析參數(shù), 然后調(diào)用 command_eval

command_eval 工作, 基于參數(shù), 如果我們應(yīng)該從磁盤加載源文件, 從 -e 獲取源或進(jìn)入 REPL榨婆。路徑調(diào)用了一系列方法, 但是所有方法都在 eval 中收斂磁携。

eval 調(diào)用 compile 來(lái)編譯代碼, 然后調(diào)用它

compile 循環(huán)遍歷這些階段, 將前一個(gè)的結(jié)果作為下一個(gè)的輸入

簡(jiǎn)化版的編譯

Big takeaway:階段是編譯器對(duì)象或后端對(duì)象的方法。

method compile($source, :$from, *%adverbs) {
    my $target := nqp::lc(%adverbs<target>);
    my $result := $source;
    for self.stages() {
        if nqp::can(self, $_) {
            $result := self."$_"($result, |%adverbs);
        }
        elsif nqp::can($!backend, $_) {
            $result := $!backend."$_"($result, |%adverbs);
        }
        else {
            nqp::die("Unknown compilation stage '$_'");
        }
        last if $_ eq $target;
    }
    return $result;
}

階段管理

編譯器可以在管道中插入額外的階段纲辽。例如, Rakudo 插入其優(yōu)化器颜武。

$comp.addstage('optimize', :after<ast>);

之后, 在 Perl6::Compiler 中, 它提供了一個(gè) optimize 方法:

method optimize($ast, *%adverbs) {
    %adverbs<optimize> eq 'off' ??
        $ast !!
        Perl6::Optimizer.new.optimize($ast, |%adverbs)
}

前端和后端

早些時(shí)候, 我們看到`compile'在當(dāng)前編譯器對(duì)象上, 然后在后端對(duì)象上尋找階段方法。

編譯器對(duì)象是關(guān)于我們正在編譯的語(yǔ)言(NQP, Perl 6等)的拖吼。我們共同稱這些階段為前端鳞上。

后端對(duì)象是關(guān)于目標(biāo)VM的, 我們想為(Parrot, JVM, MoarVM等)生成代碼。它不與任何特定語(yǔ)言綁定吊档。我們將這些階段統(tǒng)稱為后端**篙议。

frontend-backend.png

前端, 后端和它們之間的 QAST

frontend-backend-qast-between.png

前端的最后一個(gè)階段總是給出一個(gè) QAST樹(shù), 后端的第一個(gè)階段總是期望一個(gè)QAST樹(shù)。

一個(gè)交叉編譯器設(shè)置只是有一個(gè)不同于我們正在運(yùn)行的當(dāng)前 VM 的后端怠硼。

在 NQP 中解析

解析階段在語(yǔ)言的 grammar(對(duì)于我們的例子, NQP::Grammar)上調(diào)用 parse, 傳遞源代碼和 NQP::Actions鬼贱。它也可以打開(kāi)跟蹤。

method parse($source, *%adverbs) {
    my $grammar := self.parsegrammar;
    my $actions;
    $actions    := self.parseactions unless %adverbs<target> eq 'parse';
    $grammar.HOW.trace-on($grammar) if %adverbs<rxtrace>;
    my $match   := $grammar.parse($source, p => 0, actions => $actions);
    $grammar.HOW.trace-off($grammar) if %adverbs<rxtrace>;
    self.panic('Unable to parse source') unless $match;
    return $match;
}

NQP::Grammar.TOP (1)

正如在我們已經(jīng)看到的 grammar 中, 執(zhí)行從 TOP 開(kāi)始香璃。在 NQP 中, 我們發(fā)現(xiàn)它實(shí)際上是一個(gè) 方法, 而不是 tokenrule这难!

method TOP() {
    # Various things we'll consider in a moment.
    ...
    
    # Then delegate to comp_unit
    self.comp_unit;
}

這實(shí)際上是 OK 的, 只要它最終返回一個(gè) Cursor 對(duì)象。因?yàn)?comp_unit 會(huì)返回一個(gè), 所有的都會(huì)工作的很好葡秒。

這是一個(gè)方法, 因?yàn)樗蛔鋈魏谓馕? 只是做設(shè)置工作姻乓。

NQP::Grammar.TOP (2)

TOP 做的第一件事是建立一個(gè)語(yǔ)言編織鞠抑。

my %*LANG;
%*LANG<Regex>         := NQP::Regex;
%*LANG<Regex-actions> := NQP::RegexActions;
%*LANG<MAIN>          := NQP::Grammar;
%*LANG<MAIN-actions>  := NQP::Actions;

雖然我們沒(méi)有太早地區(qū)分, 當(dāng)我們開(kāi)始解析一個(gè) token, ruleregex 時(shí), 我們實(shí)際上是切換語(yǔ)言场斑。嵌套在正則表達(dá)式內(nèi)部的塊將輪流切換回主語(yǔ)言。

因此, %*LANG 會(huì)跟蹤我們?cè)诮馕鲋惺褂玫漠?dāng)前語(yǔ)言集, 糾纏在一起編織美麗的頭發(fā)杆逗。 Rakudo 在其穗帶中有第三種語(yǔ)言:Q, 即引用語(yǔ)言学少。

NQP::Grammar.TOP (3)

接下來(lái), 設(shè)置當(dāng)前元對(duì)象的集合剪个。每個(gè)包聲明器(class, role, grammar, module, knowhow)被映射到一個(gè)實(shí)現(xiàn)這種包的對(duì)象。

my %*HOW;
%*HOW<knowhow>      := nqp::knowhow();
%*HOW<knowhow-attr> := nqp::knowhowattr();

我們只有一個(gè)內(nèi)置函數(shù) - knowhow版确。它支持擁有方法和屬性, 但不支持角色組合或繼承扣囊。

所有更有趣的元對(duì)象都是用 KnowHOW 編寫的, 并且是在啟動(dòng)時(shí)加載的模塊中乎折。我們將在第2天更詳細(xì)地回到這個(gè)主題。

NQP::Grammar.TOP (4)

接下來(lái), 創(chuàng)建一個(gè) NQP::World 對(duì)象如暖。這表示一個(gè)程序的聲明方面(如類聲明)笆檀。

my $file := nqp::getlexdyn('$?FILES');
my $source_id := nqp::sha1(self.target()) ~
    (%*COMPILING<%?OPTIONS><stable-sc> ?? '' !! '-' ~ ~nqp::time_n());
my $*W := nqp::isnull($file) ??
    NQP::World.new(:handle($source_id)) !!
    NQP::World.new(:handle($source_id), :description($file));

每個(gè)編譯單元需要有一個(gè)全局唯一句柄。由于 NQP 引導(dǎo), 我們通常必須使用比源更多的東西, 否則運(yùn)行的編譯器和正被編譯的編譯器將有重疊的句柄盒至!

(當(dāng)移植到新的 VM 時(shí)需要交叉編譯 NQP 本身時(shí), --stable-sc 選項(xiàng)禁止這種情況, )

NQP::Grammar.comp_unit

接下來(lái), 我們到達(dá) comp_unit酗洒。在這里, 剝離出本質(zhì)。

token comp_unit {
    :my $*UNIT := $*W.push_lexpad($/);
    
    # Create GLOBALish - the current GLOBAL view.
    :my $*GLOBALish := $*W.pkg_create_mo(%*HOW<knowhow>,
                                         :name('GLOBALish'));
    {
        $*GLOBALish.HOW.compose($*GLOBALish);
        $*W.install_lexical_symbol($*UNIT, 'GLOBALish', $*GLOBALish);
    }
    
    # This is also the starting package.
    :my $*PACKAGE := $*GLOBALish;
    { $*W.install_lexical_symbol($*UNIT, '$?PACKAGE', $*PACKAGE); }
    
    <.outerctx>
    <statementlist>
    [ $ || <.panic: 'Confused'> ]
}

剖析 comp_unit: 作用域

$*W 上有與作用域相關(guān)的各種方法枷遂。

$*W.push_lexpad($/) 用于輸入一個(gè)新的詞法作用域, 嵌套在當(dāng)前詞法作用域的里面樱衷。它返回一個(gè)新的 QAST::Block 對(duì)象來(lái)表示它。

$*W.pop_lexpad() 用于退出當(dāng)前詞法作用域, 返回它酒唉。

$*W.cur_lexpad() 用于獲取當(dāng)前作用域矩桂。

正如名字所暗示的, 它只是一個(gè)堆棧。

剖析 comp_unit: pkg_create_mo

NQP::World 上的各種方法都是關(guān)于包的痪伦。 pkg_create_mo 方法用于創(chuàng)建表示新包的類型對(duì)象和元對(duì)象侄榴。

:my $*GLOBALish := $*W.pkg_create_mo(%*HOW<knowhow>, :name('GLOBALish'));

由于單獨(dú)編譯, NQP 中的所有內(nèi)容都以 GLOBAL 的干凈的, 空白視圖開(kāi)始, 我們稱之為 GLOBALish。這些在模塊加載時(shí)是統(tǒng)一的网沾。

pkg_create_mo 方法也用于處理像 class 這樣的關(guān)鍵字; 在這種情況下, 它使用 %*HOW<class>癞蚕。

剖析 comp_unit: install_lexical_symbol

請(qǐng)考慮以下 NQP 代碼段。

for @acts {
    my class Act { ... }
    my $a := Act.new(:name($_));
}

這個(gè)詞法作用域?qū)⑶宄負(fù)碛蟹?hào) Act$a辉哥。然而, 它們?cè)谝粋€(gè)重要的方面有所不同桦山。 Act編譯時(shí)是固定的, 而 $a 在每次循環(huán)時(shí)都是新的。編譯時(shí)詞法作用域中固定的符號(hào)安裝有:

$*W.install_lexical_symbol($*UNIT, 'GLOBALish', $*GLOBALish);

剖析 comp_unit: outer_ctx

outer_ctx token 看起來(lái)像這樣:

token outerctx { <?> }

嗯?這是一個(gè)"永遠(yuǎn)成功"的斷言醋旦!然而, 成功會(huì)觸發(fā) NQP::Actions 中的 outer_ctx 動(dòng)作方法恒水。其最重要的行是:

my $SETTING := $*W.load_setting(
    %*COMPILING<%?OPTIONS><setting> // 'NQPCORE');

它加載 NQP 設(shè)置(默認(rèn)為NQPCORE), 這反過(guò)來(lái)會(huì)引入元對(duì)象(class, role等), 以及類似于 NQPMuNQPArray 的類型。

statementlist

comp_unit token 的最后一件事是調(diào)用 statementlist, 它做了它名字所暗示的:解析語(yǔ)句列表饲齐。

rule statementlist {
    | $
    | [<statement><.eat_terminator> ]*
}

eat_terminator 規(guī)則將匹配分號(hào), 但也處理閉合花括號(hào)的使用來(lái)終止語(yǔ)句钉凌。注意它之后是一個(gè)空格所以一個(gè) <.ws> 將被插入。

語(yǔ)句

statement 規(guī)則期望找到一個(gè) statement_control(像 if, whileCATCH 這樣的東西 - 這是一個(gè) protoregex捂人!)或一個(gè)表達(dá)式, 其后跟著一個(gè)語(yǔ)句修飾條件和/或循環(huán)御雕。

# **0..1 is like Perl 5 {0,1}; forces an array, which ? does not.
token statement {
    <!before <[\])}]> | $ >
    [
    | <statement_control>
    | <EXPR> <.ws>
        [
        || <?MARKED('endstmt')>
        || <statement_mod_cond> <statement_mod_loop>**0..1
        || <statement_mod_loop>
        ]**0..1
    ]
}

旁白: 表達(dá)式解析

當(dāng)我們需要解析類似下面這樣的東西時(shí)...

$x * -$grad + $c

...我們需要注意優(yōu)先級(jí)。嘗試將優(yōu)先級(jí)編碼為一堆互相調(diào)用的規(guī)則將是非常低效的(表中每個(gè)級(jí)別一個(gè)調(diào)用先慷!)并且很難維護(hù)。

因此, EXPR 實(shí)際上調(diào)用了一個(gè)運(yùn)算符優(yōu)先級(jí)解析器咨察。它的實(shí)現(xiàn)存在于 HLL::Grammar 中, 雖然我們不會(huì)在這個(gè)課程中研究它; 它稍微有點(diǎn)可怕, 不是你可能需要改變的東西论熙。

然而, 我們會(huì)在之后看到如何配置它。

Terms

EXPR 中的運(yùn)算符優(yōu)先級(jí)解析器不僅對(duì)運(yùn)算符感興趣,
而且對(duì)運(yùn)算符所應(yīng)用的項(xiàng)也感興趣摄狱。當(dāng)它想要一個(gè)術(shù)語(yǔ),
它調(diào)用 termish, 它反過(guò)來(lái)調(diào)用 term, 另一個(gè)是 proto-regex脓诡。

對(duì)于我們的 say('Hello, world') 例子, 有趣的術(shù)語(yǔ)是解析一個(gè)函數(shù)調(diào)用:

token term:sym<identifier> {
    <deflongname> <?[(]> <args>  # <?[(]> is a lookahead
}

現(xiàn)在我們到那里了无午!我們只需要解析一個(gè)名字和一個(gè)參數(shù)列表。

deflongname

解析標(biāo)識(shí)符(這里沒(méi)有什么小聰明), 后面跟一個(gè)可選的 colonpair(因?yàn)橄?infix<+> 這樣的東西是有效的函數(shù)名)祝谚。

token deflongname {
    <identifier> <colonpair>**0..1
}

我們解析這個(gè)之后, 我們(終于宪迟!)以調(diào)用我們的第一個(gè)動(dòng)作方法結(jié)束:

method deflongname($/) {
    make $<colonpair>
         ?? ~$<identifier> ~ ':' ~ $<colonpair>[0].ast.named 
                ~ '<' ~ colonpair_str($<colonpair>[0].ast) ~ '>'
         !! ~$/;
}

它的目的是規(guī)范化 colonpair, 如果有的話。無(wú)論如何, 它 make 出了一個(gè)簡(jiǎn)單的字符串結(jié)果交惯。

解析參數(shù)

解析圓括號(hào), 然后再次委派給運(yùn)算符優(yōu)先解析器來(lái)解析單個(gè)參數(shù)或逗號(hào)分隔的參數(shù)列表次泽。

token args {
    '(' <arglist> ')'
}

token arglist {
    <.ws>
    [
    | <EXPR('f=')>
    | <?>
    ]
}

f= 表示允許的最松散的優(yōu)先級(jí)。

解析值

再一次, 運(yùn)算符優(yōu)先級(jí)解析器調(diào)用 term, 這次我們來(lái)到了 term:sym<value>席爽。

token term:sym<value> { <value> }
token value {
    | <quote>
    | <number>
}

我們有一個(gè)帶引號(hào)的字符串, 因此結(jié)束在 quote protoregex 中, 這反過(guò)來(lái)使我們成為解析單個(gè)引號(hào)字符串的候選人意荤。

token quote:sym<apos> { <?[']> <quote_EXPR: ':q'>  }

動(dòng)作一路向上!

我們現(xiàn)在已經(jīng)到達(dá)了解析這個(gè)語(yǔ)句的底部。 然而, 我們還沒(méi)有構(gòu)建任何 QAST 節(jié)點(diǎn), 這些節(jié)點(diǎn)將指示程序應(yīng)該實(shí)際什么, 當(dāng)我們運(yùn)行它時(shí)只锻。

HELL::Actions 繼承的 quote_EXPR 動(dòng)作對(duì)我們引用的字符串做了很多工作:

method quote:sym<apos>($/) { make $<quote_EXPR>.ast; }

它產(chǎn)生一個(gè) QAST::SVal 節(jié)點(diǎn), 其代表一個(gè)字符串字面值:

QAST::SVal.new( :value('Hello, world!') )

value actions

value 動(dòng)作方法只是檢查我們是否解析了一個(gè)引號(hào)或一個(gè)數(shù)字, 然后用我們解析的 AST 調(diào)用 make玖像。

method value($/) {
    make $<quote> ?? $<quote>.ast !! $<number>.ast;
}

而一個(gè) term 的 value case 只是向上傳遞值 QAST:

method term:sym<value>($/) { make $<value>.ast; }

arglist actions

arglist 動(dòng)作方法創(chuàng)建一個(gè)代表 callQAST::Op 節(jié)點(diǎn)。
名稱將在以后附加齐饮。 它必須處理 3 種情況: 零參數(shù)(因此 $<EXPR> 不匹配), 單個(gè)參數(shù)或逗號(hào)分隔的參數(shù)列表
參數(shù)捐寥。

method args($/) { make $<arglist>.ast; }
method arglist($/) {
    my $ast := QAST::Op.new( :op('call'), :node($/) );
    if $<EXPR> {
        my $expr := $<EXPR>.ast;
        if nqp::istype($expr, QAST::Op) && $expr.name eq '&infix:<,>' {
            for $expr.list { $ast.push($_); }
        }
        else { $ast.push($expr); }
    }
    make $ast;
}

函數(shù)調(diào)用 actions

現(xiàn)在我們已經(jīng)規(guī)范化了名稱并構(gòu)建了代表調(diào)用的 QAST 節(jié)點(diǎn)。 因此, 作為函數(shù)調(diào)用的術(shù)語(yǔ)的action方法使用 call QAST 節(jié)點(diǎn), 設(shè)置其名稱(在前面加上&)并將其傳遞祖驱。

method term:sym<identifier>($/) {
    my $ast := $<args>.ast;
    $ast.name('&' ~ $<deflongname>.ast);
    make $ast;
}

更高的 action 方法傾向于將由解析中較低的 action 方法產(chǎn)生的 AST 組合成更大的 AST握恳。

statement actions

這是一個(gè)簡(jiǎn)化版本。 在這里看不到什么真正新的東西羹膳。 真正的事情只是更復(fù)雜, 因?yàn)樗幚碚Z(yǔ)句修改條件和循環(huán)睡互。

method statement($/, $key?) {
    my $ast;
    if $<EXPR> { $ast := $<EXPR>.ast; }
    elsif $<statement_control> { $ast := $<statement_control>.ast; }
    else { $ast := 0; }
    make $ast;
}

0 只是意味著“我們?cè)谶@里沒(méi)有找到任何東西來(lái)解析” - 可能是由于到達(dá)了源的末端。

statementlist actions

稍微簡(jiǎn)化下, 但不是很多陵像。 QAST::Stmts 節(jié)點(diǎn)表示一組順序執(zhí)行的操作就珠。 我們將每個(gè)語(yǔ)句(在我們的例子中, 一個(gè))的 QAST 節(jié)點(diǎn)推送到它上面。

method statementlist($/) {
    my $ast := QAST::Stmts.new( :node($/) );
    if $<statement> {
        for $<statement> {
            $ast.push($_.ast);
        }
    }
    else {
        $ast.push(default_for('$'));
    }
    make $ast;
}

else 確保我們永遠(yuǎn)不會(huì)生成一個(gè)求值為“null”的空的 QAST::Stmts, 而是被計(jì)算為“NQPMu”醒颖。

comp_unit actions

最后, 我們到達(dá)頂部妻怎! comp_unit 動(dòng)作方法 - 再次略微簡(jiǎn)化 - 將 QAST::Stmts 推送到 QAST::Block 節(jié)點(diǎn)上, 使這些語(yǔ)句由該塊執(zhí)行。 然后, 所有一切都被包裝在一個(gè) QAST::CompUnit 中, 它還指定了代碼所來(lái)自的語(yǔ)言泞歉。

method comp_unit($/) {
    # Push mainline statements into UNIT.
    my $mainline := $<statementlist>.ast;
    my $unit     := $*W.pop_lexpad();
    $unit.push($mainline);
    
    # Wrap everything in a QAST::CompUnit.
    make QAST::CompUnit.new(
        :hll('nqp'),
        # 這里省略很多, 稍后詳細(xì)介紹逼侦。
        $unit
    );
}

前端的結(jié)束

在這個(gè)時(shí)候, 階段 parse 完成了! 我們已經(jīng)成功地執(zhí)行了這個(gè) grammar, 它產(chǎn)生了一個(gè) Match 對(duì)象腰耙。 而附加到這個(gè)匹配對(duì)象上的是一個(gè)表示程序語(yǔ)義的 QAST 樹(shù)榛丢。

因此, 階段 ast 是相當(dāng)簡(jiǎn)單的。

method ast($source, *%adverbs) {
    my $ast := $source.ast();
    self.panic("Unable to obtain AST"
        unless $ast ~~ QAST::Node;
    $ast;
}

從這兒開(kāi)始, 我們現(xiàn)在進(jìn)入后端挺庞。

旁白:為什么交錯(cuò)解析和AST創(chuàng)建呢?

你可能想知道為什么解析沒(méi)有完全完成, 然后就構(gòu)建了AST晰赞。
答案是在許多情況下, 當(dāng)我們著手解析時(shí)我們需要計(jì)算AST片段。 例如, 在:

BEGIN { say("OMG I'm alive!") }
1 2

那個(gè) BEGIN 塊應(yīng)該實(shí)際運(yùn)行并產(chǎn)生它的輸出, 即使它之后有一個(gè)語(yǔ)法錯(cuò)誤。

BEGIN-time 的東西可能有副作用, 實(shí)際上影響從他們的那兒的解析掖鱼。

代碼生成:快速概覽

后端的工作是接收一個(gè) QAST 樹(shù)并為目標(biāo)運(yùn)行時(shí)(target runtime)生成代碼然走。這, 再一次, 是由一組階段(stages)組織的。它們的名字會(huì)根據(jù)你是在 Parrot, JVM, MoarVM, etc 上而不同戏挡。

我們會(huì)推遲查看那些階段中的任何一個(gè)的詳情, 直到之后, 我們甚至還不能太深入它們芍瑞。它們中包含的大部分代碼在將來(lái)都不會(huì)改變, 為了掌握它們的大部分東西, 我們需要詳細(xì)了解一些后端的知識(shí)。

現(xiàn)在, 我們會(huì)把這些階段當(dāng)作神奇的黑盒子褐墅。 :-)

從頭開(kāi)始創(chuàng)建一個(gè)小語(yǔ)言

所以, 這是深入到 NQP拆檬。 它有點(diǎn)洶涌, 所以它適合練習(xí)一些更小的東西。

因此, 我們將自己建立幾個(gè)小的編譯器掌栅。 我會(huì)在這里做一個(gè), 你會(huì)在練習(xí)中做一個(gè)秩仆。

有趣的是, 我的將是一個(gè) Ruby 子集。

更有趣的是, 你的將是一個(gè) PHP 子集猾封。

我們將從實(shí)現(xiàn) “Hello, world” 開(kāi)始, 然后在下一部分 - 當(dāng)我們更多地了解 QAST 后 - 開(kāi)始添加語(yǔ)言功能澄耍。

Stubbing a compiler

只需從 NQPHLL 庫(kù)中繼承的三個(gè)東西。

use NQPHLL;

grammar Rubyish::Grammar is HLL::Grammar {
}

class Rubyish::Actions is HLL::Actions {
}

class Rubyish::Compiler is HLL::Compiler {
}

sub MAIN(*@ARGS) {
    my $comp := Rubyish::Compiler.new();
    $comp.language('rubyish');
    $comp.parsegrammar(Rubyish::Grammar);
    $comp.parseactions(Rubyish::Actions);
    $comp.command_line(@ARGS, :encoding('utf8'));
}

我們已經(jīng)擁有了 REPL

如果我們運(yùn)行前一張幻燈片中的代碼, 我們發(fā)現(xiàn)我們已經(jīng)有了一個(gè)簡(jiǎn)單的 REPL(Read Eval Print Loop)晌缘。

不出所料, 試圖運(yùn)行的東西不能工作:

> puts "Hello world"
Method 'TOP' not found for invocant of class 'Rubyish::Grammar'

當(dāng)然, 它也準(zhǔn)確地告訴了我們下一步該做什么 齐莲。。磷箕。

一個(gè)基本的 grammar

Rubyish 是面向行的, 所以每個(gè)語(yǔ)句由換行符分隔, 并且在 tokens 之間只允許水平空格选酗。

grammar Rubyish::Grammar is HLL::Grammar {
    token TOP          { <statementlist> }
    
    rule statementlist { [ <statement> \n+ ]* }
    
    proto token statement {*}
    token statement:sym<puts> {
        <sym> <.ws> <?["]> <quote_EXPR: ':q'>
    }
    
    # Whitespace required between alphanumeric tokens
    token ws { <!ww> \h* || \h+ }
}

我們現(xiàn)在有什么?

有了這個(gè), 我們現(xiàn)在可以解析我們的簡(jiǎn)單程序, 但是當(dāng)嘗試獲取 AST 時(shí)卻失敗了:

> puts "Hello world"
Unable to obtain AST from NQPMatch

這再一次告訴我們下一步需要做什么:actions!

基本的 actions

class Rubyish::Actions is HLL::Actions {
    method TOP($/) {
        make QAST::Block.new( $<statementlist>.ast );
    }
    
    method statementlist($/) {
        my $stmts := QAST::Stmts.new( :node($/) );
        for $<statement> {
            $stmts.push($_.ast)
        }
        make $stmts;
    }
    
    method statement:sym<puts>($/) {
        make QAST::Op.new(
            :op('say'),
            $<quote_EXPR>.ast
        );
    }
}

有效果了!

回想一下, 后端是獨(dú)立于語(yǔ)言的; 他們只需要一個(gè) QAST 樹(shù)作為輸入岳枷。 我們的 actions 產(chǎn)生一個(gè)芒填。 因此, 我們現(xiàn)在有一個(gè)非常非常簡(jiǎn)單的能工作的語(yǔ)言編譯器。

> puts "Hello world"
Hello World

我們還可以輸出這個(gè) AST:

- QAST::Block
  - QAST::Stmts puts \"Hello, world\"\n
    - QAST::Op(say)
      - QAST::SVal(Hello, world)

總之...

我們?cè)诿钚兄姓{(diào)用 NQP 的控制流程, 看到它解析我們的程序, 構(gòu)建一個(gè) QAST 樹(shù), 并將其傳遞給后端進(jìn)行編譯空繁。

然后我們使用這種技術(shù)從頭開(kāi)始構(gòu)建一個(gè)小編譯器殿衰。

因?yàn)樗⒃谂c NQP 和 Rakudo 相同的技術(shù)之上, 它獲得相同的好處。 例如, 已經(jīng)工作在 Parrot 和 JVM 上開(kāi)箱即用的編譯器盛泡。

練習(xí) 4

在本練習(xí)中, 您將構(gòu)建相當(dāng)于我的Rubyish 編譯器的 PHPish 編譯器 闷祥。

主要的區(qū)別是, 你想要的關(guān)鍵字是 “echo”, 并且行是用分號(hào)而不是換行符分隔語(yǔ)句。

QAST

在前端和后端之間: Q Abstract Syntax Tree

進(jìn)一步深入 QAST

到目前為止, 我們已經(jīng)構(gòu)建了一些非常簡(jiǎn)單的 QAST 樹(shù)傲诵。 然而, 他們幾乎都只留于 QAST 的表面凯砍。

在本課程的這一部分中, 我們將討論更廣泛的節(jié)點(diǎn)類型及它們支持的選項(xiàng)。

為了提供具體的例子, Rubyish 將被擴(kuò)展以支持更廣泛的語(yǔ)言功能拴竹。

QAST::Node: children

所有的 QAST 節(jié)點(diǎn)類型都繼承自基類 QAST::Node悟衩。

所有 QAST 節(jié)點(diǎn)都支持擁有孩子節(jié)點(diǎn)。 初始孩子節(jié)點(diǎn)集可以作為位置參數(shù)傳遞給 “new”栓拜。 在任何節(jié)點(diǎn)上, 可以:

my $first := $ast[0];       # get first child
$ast[0] := $child;          # set first child
$ast.push($child);          # push a child
$child := $ast.pop();       # pop a child
$ast.unshift($child);       # unshift a child
$child := $ast.shift();     # shift a child
@children := $ast.list();   # get underlying children list

QAST::Node: annotations

通過(guò)在節(jié)點(diǎn)上使用散列索引, 可以給所有 QAST 節(jié)點(diǎn)提供任意的注解座泳。

$var<used> := 1;

這可能非常有用, 但它很容易過(guò)度使用, 并造成混亂斑响。
是的, 我費(fèi)了一番功夫才學(xué)會(huì)它。

所有注解都可以使用 hash 方法獲取:

my %anno := $var.hash();

QAST::Node: 返回類型

使用 QAST 節(jié)點(diǎn)你還可以做其他兩件重要的事情钳榨。 所有節(jié)點(diǎn)都可以使用他們將被求值的類型進(jìn)行注解。

$ast.returns($some_type);

注意, 你指定一個(gè)類型對(duì)象來(lái)表示類型, 不是類型的字符串名字纽门! 在某些情況下, 這里設(shè)置的類型用于代碼生成(例如, 原生類型的變量通過(guò)這個(gè)來(lái)分配它們的原生存儲(chǔ))薛耻。

這也可以在創(chuàng)建節(jié)點(diǎn)時(shí)首先設(shè)置:

QAST::Var.new( ..., :returns(int) )

QAST::Node: node

我們可能希望做的另一個(gè)重要的事情是將 QAST 節(jié)點(diǎn)與源位置相關(guān)聯(lián)。 該信息由后端代碼生成來(lái)持久化, 使得當(dāng)發(fā)生運(yùn)行時(shí)錯(cuò)誤時(shí), 它可以用于產(chǎn)生有意義的回溯赏陵。

node 方法需要接收一個(gè)匹配對(duì)象:

$ast.node($/);

再次, 它可以被指定(通常在QAST::Stmts節(jié)點(diǎn)上)為節(jié)點(diǎn)構(gòu)造函數(shù)的參數(shù)饼齿。

my $ast := QAST::Stmts.new( :node($/) );

樹(shù)的頂部

在頂層, QAST 樹(shù)必須要么具有 QAST::CompUnit, 要么具有 QAST::Block

QAST::CompUnit 代表一個(gè)編譯單元蝙搔。 它應(yīng)該有一個(gè)單獨(dú)的 QAST::Block 孩子缕溉。 然而, 它也可以指定許多其他位的配置; 我們將在后面看到。

QAST::Block 代表詞法作用域吃型。 每當(dāng)一個(gè) QAST::Block 嵌套在另一個(gè)中時(shí), 它代表一個(gè)嵌套的詞法作用域, 它可以看到外部的變量证鸥。 結(jié)合克隆, 這也有利于閉包語(yǔ)義。

字面值: QAST::IVal, QAST::NVal 和 QAST::SVal

這三個(gè)節(jié)點(diǎn)類型表示整數(shù), 浮點(diǎn)和字符串字面值勤晚。
如果我們更新我們的 grammar 來(lái)解析不同類型的值:

proto token value {*}
token value:sym<string>  { <?["]> <quote_EXPR: ':q'> }
token value:sym<integer> { '-'? \d+ }
token value:sym<float>   { '-'? \d+ '.' \d+ }

然后我們可以把 actions 寫為:

method value:sym<string>($/) {
    make $<quote_EXPR>.ast;
}
method value:sym<integer>($/) {
    make QAST::IVal.new( :value(+$/.Str) )
}
method value:sym<float>($/) {
    make QAST::NVal.new( :value(+$/.Str) )
}

嘗試我們的字面值

經(jīng)過(guò)一個(gè)小小的調(diào)整, 使puts 能夠解析...

token statement:sym<puts> {
    <sym> <.ws> <value>
}

...加上 actions 中的匹配調(diào)整, 我們現(xiàn)在可以做:

> puts 42
42
> puts 0.999
0.999
> puts "It's not a bacon tree, it's a hambush!"
It's not a bacon tree, it's a hambush!

Operations: QAST::Op

QAST::Op 節(jié)點(diǎn)是到達(dá)令人難以置信數(shù)量的運(yùn)算符的網(wǎng)關(guān)枉层。 它們同樣是通過(guò) nqp::op(...) 語(yǔ)法可用的。

通常, QAST::Op 節(jié)點(diǎn)看起來(lái)像這樣:

QAST::Op.new(
    :op('add_n'),
    $left_child_ast,
    $right_child_ast
)

該運(yùn)算符由 :op(...) 命名參數(shù)指定, 操作數(shù)是節(jié)點(diǎn)的孩子赐写。

解析一些數(shù)學(xué)運(yùn)算符 (1)

讓我們添加加法, 減法, 乘法和除法運(yùn)算符鸟蜡。 對(duì)于這些, 我們需要設(shè)置運(yùn)算符優(yōu)先級(jí)解析器, 配置兩個(gè)優(yōu)先級(jí)別。

INIT {
    # 從 Perl 6 Grammar 竊取優(yōu)先級(jí)別的名稱
    Rubyish::Grammar.O(':prec<u=>, :assoc<left>', '%multiplicative');
    Rubyish::Grammar.O(':prec<t=>, :assoc<left>', '%additive');
}

注意, 我們?cè)谶@里調(diào)用的 O 方法繼承自 HLL::Grammar挺邀。 第一個(gè)參數(shù)指定優(yōu)先級(jí)別和結(jié)合性揉忘。 然后第二個(gè)按照名稱保存這個(gè)特定的配置, 所以我們可以在聲明操作符時(shí)引用它。

解析一些數(shù)學(xué)運(yùn)算符 (2)

就地使用優(yōu)先級(jí)別, 我們可以向 grammar 中添加一些運(yùn)算符端铛。 這是通過(guò)將它們添加到 infix protoregex 中來(lái)完成的, 它是我們繼承自 HLL::Grammar 的泣矛。

token infix:sym<*> { <sym> <O('%multiplicative, :op<mul_n>')> }
token infix:sym</> { <sym> <O('%multiplicative, :op<div_n>')> }
token infix:sym<+> { <sym> <O('%additive, :op<add_n>')> }
token infix:sym<-> { <sym> <O('%additive, :op<sub_n>')> }

:op<...> 語(yǔ)法指示我們從 HLL::Actions 繼承的 EXPR action 方法來(lái)為我們構(gòu)造該運(yùn)算符的 QAST::Op 節(jié)點(diǎn)!

Terms

我們幾乎準(zhǔn)備好使用運(yùn)算符優(yōu)先級(jí)解析器了, 但還不完全是。 我們還必須指導(dǎo)它如何獲得一個(gè) term沦补。 我們從 HLL::Grammar 繼承了一個(gè) term protoregex, 因此只需要為它添加候選者乳蓄。

對(duì)我們來(lái)說(shuō), 這意味著一個(gè)term的候選者是一個(gè)值:

token term:sym<value> { <value> }

和匹配的 action 方法:

method term:sym<value>($/) { make $<value>.ast; }

把所有的東西連接到一塊

最后我們需要做的是更新 puts 的 grammar 規(guī)則:

token statement:sym<puts> {
    <sym> <.ws> <EXPR>
}

還有 action 方法:

method statement:sym<puts>($/) {
    make QAST::Op.new(
        :op('say'),
        $<EXPR>.ast
    );
}

試驗(yàn)我們的運(yùn)算符

基本的算術(shù)運(yùn)算現(xiàn)在工作了,并且運(yùn)算符的優(yōu)先級(jí)也被正確地處理了。

> puts 10 * 9 + 1
91

我們還可以檢查 AST 以查看 QAST::Op 節(jié)點(diǎn):

- QAST::Block
  - QAST::Stmts puts 10 * 9 + 1\n
    - QAST::Op(say)
      - QAST::Op(add_n &infix:<+>) +
        - QAST::Op(mul_n &infix:<*>) *
          - QAST::IVal(10)
          - QAST::IVal(9)
        - QAST::IVal(1)

Sequencing: QAST::Stmts 和 QAST::Stmt

有兩種按順序運(yùn)行每個(gè)子節(jié)點(diǎn)的節(jié)點(diǎn)類型夕膀。

QAST::Stmts, 確實(shí), 沒(méi)有什么比這更多的了虚倒。

QAST::Stmt 具有附加的效果, 它規(guī)定在代碼生成期間創(chuàng)建的任何臨時(shí)值在該節(jié)點(diǎn)執(zhí)行結(jié)束之后都不再需要了。

一般來(lái)說(shuō), 在語(yǔ)言使用者認(rèn)為的那樣, 具有一組語(yǔ)句和單個(gè)語(yǔ)句的地方使用它們是有意義的产舞。

Block 結(jié)構(gòu)

一個(gè)常見(jiàn)的用法, 雖然沒(méi)有強(qiáng)制性, 是一個(gè) QAST::Block 擁有兩個(gè) QAST::Stmts 節(jié)點(diǎn)魂奥。

第一個(gè)用于保存聲明, 例如變量或嵌套例程。

第二個(gè)用于保存由該塊的 statementlist 解析的語(yǔ)句易猫。

這個(gè)慣用法在 NQP 和 Rakudo 中都可用; 例如:

$block[0].push(QAST::Var.new(:name<$/>, :scope<lexical>, :decl<var>));

變量

現(xiàn)在是時(shí)候添加變量到 Rubyish 中了耻煤! 在 Rubyish 中, 變量不是顯式地聲明的。 相反, 它們?cè)谑状钨x值時(shí)在當(dāng)前作用域中被聲明。

首先, 讓我們?yōu)橘x值添加一個(gè)優(yōu)先級(jí):

Rubyish::Grammar.O(':prec<j=>, :assoc<right>',  '%assignment');

并解析賦值運(yùn)算符, 使用 bind NQP 運(yùn)算符, 它將右側(cè)的表達(dá)式綁定給左側(cè)的變量:

token infix:sym<=> { <sym> <O('%assignment, :op<bind>')> }

表達(dá)式作為語(yǔ)句

你可能還記得在 NQP grammar 中, 表達(dá)式也是一個(gè)有效的語(yǔ)句哈蝇。 我們需要在 Rubyish 也這樣做棺妓。

這意味著將表達(dá)式添加到 grammar:

token statement:sym<EXPR> { <EXPR> }

還有相應(yīng)的 actions:

method statement:sym<EXPR>($/) { make $<EXPR>.ast; }

標(biāo)識(shí)符解析

現(xiàn)在, 我們將所有標(biāo)識(shí)符視為變量一樣。 我們這樣解析標(biāo)識(shí)符:

token term:sym<ident> {
    :my $*MAYBE_DECL := 0;
    <ident>
    [ <?before \h* '=' [\w | \h+] { $*MAYBE_DECL := 1 }> || <?> ]
}

注意這里使用了向前查看以查看我們是否可以找到一個(gè)賦值運(yùn)算符, 在其周圍有空格或標(biāo)識(shí)符緊跟其后(不能將==視為賦值炮赦!)

動(dòng)態(tài)變量用于傳達(dá)是否發(fā)生了賦值, 這可能意味著我們有一個(gè)聲明怜跑。

標(biāo)識(shí)符 actions

Here is a first, cheating attempt at the actions for an identifier.
這是第一個(gè), 欺騙嘗試的標(biāo)識(shí)符的 action。

method term:sym<ident>($/) {
    if $*MAYBE_DECL {
        make QAST::Var.new( :name(~$<ident>), :scope('lexical'),
                            :decl('var') );
    }
    else {
        make QAST::Var.new( :name(~$<ident>), :scope('lexical') );
    }
}

這允許我們運(yùn)行:

a = 7
b = 6
puts a * b

問(wèn)題

不幸的是, 事情變化得相當(dāng)快吠勘。 每個(gè)賦值現(xiàn)在都被視為聲明性芬。 因此:

a = 1
puts a
a = 2
puts a

失敗了:

Error while compiling block: Error while compiling op bind:
Lexical 'a' already declared

符號(hào)表

每個(gè) QAST::Block 都有一個(gè)符號(hào)表, 可以用來(lái)存儲(chǔ)其中聲明的符號(hào)的額外信息。

真的, 它只是一個(gè)散列哈希, 第一個(gè)散列鍵在符號(hào)和內(nèi)部散列存儲(chǔ)任何我們希望的信息剧防。

我們可以通過(guò)執(zhí)行以下操作來(lái)添加或更新符號(hào)的條目(entries):

$block.symbol($ident, :declared(1));

我們可以通過(guò)做以下事情來(lái)獲取符號(hào)上保存的當(dāng)前信息:

my %sym := $block.symbol($ident);

我們可以用這個(gè)來(lái)跟蹤聲明植锉!

下一個(gè)挑戰(zhàn):跟蹤塊

我們需要訪問(wèn)我們正在聲明的當(dāng)前塊, 然后才能使用符號(hào)。 將它放在一個(gè)動(dòng)態(tài)變量中是最容易處理的, 在 TOP grammar 規(guī)則中創(chuàng)建它:

token TOP {
    :my $*CUR_BLOCK := QAST::Block.new(QAST::Stmts.new());
    <statementlist>
    [ $ || <.panic('Syntax error')> ]
}

相應(yīng)的 TOP action 方法變?yōu)?

method TOP($/) {
    $*CUR_BLOCK.push($<statementlist>.ast);
    make $*CUR_BLOCK;
}

使用符號(hào)

現(xiàn)在, 我們可以使用 symbol 跟蹤已經(jīng)聲明過(guò)的內(nèi)容, 而不是重新聲明它峭拘。

method term:sym<ident>($/) {
    my $name := ~$<ident>;
    my %sym  := $*CUR_BLOCK.symbol($name);
    if $*MAYBE_DECL && !%sym<declared> {
        $*CUR_BLOCK.symbol($name, :declared(1));
        make QAST::Var.new( :name($name), :scope('lexical'),
                            :decl('var') );
    }
    else {
        make QAST::Var.new( :name($name), :scope('lexical') );
    }
}

其它作用域

QAST::Var 節(jié)點(diǎn)不只是用于詞法作用域俊庇。 其可用作用域如下:

lexical         對(duì)嵌套塊可見(jiàn)
local           像 lexical, 但對(duì)嵌套塊不可見(jiàn)
contextual      動(dòng)態(tài)作用域詞法的查找
attribute       對(duì)象屬性 (children: invocant, package)
positional      數(shù)組索引 (children: array, index)
associative     散列索引 (children: hash, key)

注意只有前 3 個(gè)作為聲明是有意義的。 還要注意, Rakudo 不使用最后 2 個(gè)(它的數(shù)組和散列處理因素不同), 雖然 NQP 使用鸡挠。

例程

為了更多的說(shuō)明詞法作用域, 讓我們添加例程暇赤。 聲明和調(diào)用例程的語(yǔ)法如下:

def greet
    puts "hello"
end
greet()

我們將通過(guò)不處理其他形式的調(diào)用來(lái)保持簡(jiǎn)單。

解析例程聲明

這里沒(méi)有什么特別新的東西宵凌。 我們注意啟動(dòng)一個(gè)新的詞法作用域, 所以任何聲明都不會(huì)污染周圍的作用域鞋囊。 split 因此是第一個(gè) token 的 action 方法可以看到 $* CUR_BLOCK 安裝進(jìn)去。

token statement:sym<def> {
    'def' \h+ <defbody>
}
rule defbody {
    :my $*CUR_BLOCK := QAST::Block.new(QAST::Stmts.new());
    <ident> \n
    <statementlist>
    'end'
}

NQP 和 Rakudo 做的幾乎相同, 唯一的區(qū)別是, 他們抽象了塊的推入/彈出并保持追蹤瞎惫。

解析調(diào)用

調(diào)用是一個(gè)標(biāo)識(shí)符, 后跟一些圓括號(hào)溜腐。 我們也會(huì)小心以避免使用關(guān)鍵字。

token term:sym<call> {
    <!keyword>
    <ident> '(' ')'
}

<!keyword> 也適用于 term:sym<ident>.

例程聲明的 Actions

defbody 完成了 QAST::Block, 它通過(guò) statement:sym<def> 被安裝為詞法瓜喇。

method statement:sym<def>($/) {
    my $install := $<defbody>.ast;
    $*CUR_BLOCK[0].push(QAST::Op.new(
        :op('bind'),
        QAST::Var.new( :name($install.name), :scope('lexical'),
                       :decl('var') ),
        $install
    ));
    make QAST::Op.new( :op('null') );
}
method defbody($/) {
    $*CUR_BLOCK.name(~$<ident>);
    $*CUR_BLOCK.push($<statementlist>.ast);
    make $*CUR_BLOCK;
}

調(diào)用

調(diào)用是一個(gè)操作, 因此使用 QAST::Op 來(lái)完成挺益。 默認(rèn)情況下, 要調(diào)用的東西的名稱(將在詞法中解析)在 name 命名參數(shù)中指定。

method term:sym<call>($/) {
    make QAST::Op.new( :op('call'), :name(~$<ident>) );
}

任何沒(méi)有指定 name 的情況都會(huì)把節(jié)點(diǎn)的第一個(gè)子節(jié)點(diǎn)作為要調(diào)用的東西乘寒。 因此, 我們本可以這樣寫:

method term:sym<call>($/) {
    make QAST::Op.new(
        :op('call'),
        QAST::Var.new( :name(~$<ident>), :scope('lexical')
    );
}

但是(至少在JVM上)這阻礙了優(yōu)化, 所以不要這樣做望众。

形參和實(shí)參

實(shí)參和形參處理涉及到我們以前沒(méi)有見(jiàn)過(guò)的節(jié)點(diǎn)類型。 形參只是 QAST::Op 調(diào)用節(jié)點(diǎn)的子節(jié)點(diǎn), 參數(shù)只是 decl 設(shè)置為 paramQAST::Var 節(jié)點(diǎn)伞辛。

首先, 讓我們?yōu)閰?shù)添加解析烂翰。

rule defbody {
    :my $*CUR_BLOCK := QAST::Block.new(QAST::Stmts.new());
    <ident> <signature>? \n
    <statementlist>
    'end'
}
rule signature {
    '(' <param>* % [ ',' ] ')'
}
token param { <ident> }

Parameter actions

param action 方法看起來(lái)像這樣:

method param($/) {
    $*CUR_BLOCK[0].push(QAST::Var.new(
        :name(~$<ident>), :scope('lexical'), :decl('param')
    ));
    $*CUR_BLOCK.symbol(~$<ident>, :declared(1));
}

有趣的是, 它從來(lái)不做 make。 這可能看起來(lái)奇怪, 因?yàn)槠渌胤降?action 方法已經(jīng)這樣做蚤氏。 但它沒(méi)有理由那樣做; 我們真正想做的是將已聲明的參數(shù)安裝到當(dāng)前塊中甘耿。 它更容易在上下文中獲取。

參數(shù)傳遞

這里有一個(gè)快速和簡(jiǎn)單的方法來(lái)解析參數(shù):

token term:sym<call> {
    <!keyword>
    <ident> '(' :s <EXPR>* % [ ',' ] ')'
}

之后我們更新相應(yīng)的 actions:

method term:sym<call>($/) {
    my $call := QAST::Op.new( :op('call'), :name(~$<ident>) );
    for $<EXPR> {
        $call.push($_.ast);
    }
    make $call;
}

目前為止...

到目前為止, 我們使用了以下的 QAST 節(jié)點(diǎn)類型:

QAST::Block     一個(gè)詞法作用域
QAST::Stmts     要執(zhí)行的一系列東西
QAST::Stmt      同上, 還是臨時(shí)邊界
QAST::Op        某種操作
QAST::Var       變量或參數(shù)使用/聲明
QAST::IVal      整數(shù)字面值
QAST::NVal      浮點(diǎn)數(shù)字面值
QAST::SVal      字符串字面值

我們現(xiàn)在將考慮更多的節(jié)點(diǎn)類型; 我們將推遲一些(QAST::WValQAST::Regex), 直到明天竿滨。

用 QAST::BVal 引用 Block

QAST::Block 應(yīng)該只在一個(gè) QAST 樹(shù)中出現(xiàn)一次佳恬。 它放在哪里就在哪里定義它的詞匯作用域捏境。

那么, 如果你想在樹(shù)中的其他地方引用 QAST::Block 呢?
這就是 QAST::BVal, Block Value的縮寫, 的作用所在。 例如, 它在發(fā)出代碼時(shí)使用, 使CORE設(shè)置成為程序的外部詞法作用域毁葱。

my $set_outer := QAST::Op.new(
    :op('forceouterctx'),
    QAST::BVal.new( :value($*UNIT) ),
    QAST::Op.new(
        :op('callmethod'), :name('load_setting'),
        # stuff left out here
    ));

Boxed vs. unboxed, void vs. non-void 上下文

當(dāng)后端代碼生成發(fā)生時(shí), 它可能需要裝載和/或解包東西, 或者它可以確定某個(gè)東西是否會(huì)在空(sink)上下文中垫言。

雖然它可以可靠地生成工作代碼, 但它可能不高效。 在這里考慮整數(shù)常量的處理:

my int $x = 42;     # Needs an unboxed native int
my $x = 42;         # Needs a boxed Int object

當(dāng)我們寫整數(shù)字面值的 action 方法時(shí), 我們有一個(gè)困境倾剿。 我們應(yīng)該發(fā)出一個(gè) QAST::IVal 嗎, 這將在第二種情況下被裝箱骏掀。 或者我們應(yīng)該將一個(gè)Int常量42放入常量池, 并使用 QAST::WVal(明天更多的討論這個(gè)節(jié)點(diǎn)類型)來(lái)引用它嗎?

QAST::Want 來(lái)救場(chǎng)

與其選擇, 我們倒不如把這兩個(gè)選項(xiàng)都呈現(xiàn)出來(lái), 讓代碼生成器選擇最有效率的那個(gè)。 這是通過(guò) QAST::Want 節(jié)點(diǎn)來(lái)完成的柱告。

QAST::Want.new(
    QAST::WVal.new( :value($boxed_constant) ),
    'Ii', QAST::IVal.new( :value($the_value) )
)

第一個(gè)孩子是默認(rèn)的東西。 它后面是一組選擇器, 用于我們可能所處的不同上下文中笑陈。

Ii      原生整數(shù)
Nn      原生浮點(diǎn)數(shù)
Ss      原生字符串
v       void

后端逃生艙口: QAST::VM (1)

有時(shí), 有必要通過(guò)后端有條件地做事情, 或做一些特定于虛擬機(jī)的操作际度。 QAST::VM 節(jié)點(diǎn)處理這一需求。

例如, 下面是來(lái)自 NQP 的一些代碼, 用于加載 NQP 模塊加載器涵妥。 它需要知道后端要查找的文件名乖菱。

QAST::Op.new(
    :op('loadbytecode'),
    QAST::VM.new(
        :parrot(QAST::SVal.new( :value('ModuleLoader.pbc') )),
        :jvm(QAST::SVal.new( :value('ModuleLoader.class') ))
    ))

如果當(dāng)前后端沒(méi)有適用的選項(xiàng), 代碼生成器將拋出異常。

后端逃生艙口: QAST::VM (2)

QAST::VM 節(jié)點(diǎn)類型也在 pir::op_SIG(...) 語(yǔ)法的后面, 它在 NQP 和 Rakudo 中可用蓬网。 這里是 pir::op 在 NQP 中是如何解析和實(shí)現(xiàn)的窒所。

token term:sym<pir::op> {
    'pir::' $<op>=[\w+] <args>**0..1
}

method term:sym<pir::op>($/) {
    my @args := $<args> ?? $<args>[0].ast.list !! [];
    my $pirop := ~$<op>;
    $pirop := join(' ', nqp::split('__', $pirop));
    make QAST::VM.new( :pirop($pirop), :node($/), |@args );
}

At the top: QAST::CompUnit

Rakudo 和 NQP 生成的 QAST 樹(shù)在頂部有一個(gè) QAST::CompUnit

qast-compunit-and-blocks.png

我們可以用 QAST::CompUnit 做什么

這里看看 QAST::CompUnit 能做些什么(我們明天會(huì)再看一下)帆锋。

my $compunit := QAST::CompUnit.new(
    # Set the language this contains.
    :hll('nqp'),
    
    # What to do if the compilation unit is loaded as a module.
    :load(QAST::Op.new(
        :op('call'),
        QAST::BVal.new( :value($unit) )
    )),
    
    # What to do if the compilation unit is invoked as the main,
    # top-level program.
    :main(...),

    # 1 child, which is the top-level QAST::Block
    $unit
);

練習(xí) 5

在本練習(xí)中, 您將向 PHPish 添加一些功能, 以便探索我們一直在研究的 QAST 節(jié)點(diǎn)吵取。

看看 NQP grammar 和 actions, 了解他們的工作原理 :-)

探索 nqp::ops

學(xué)習(xí)所有的運(yùn)算符!

僅僅瞄一眼

有幾百個(gè)可用的 nqp::ops。 它們的范圍從算術(shù)到字符串操作, 從流控制(如循環(huán))到類型創(chuàng)建锯厢。

我們已經(jīng)看到了一些運(yùn)算符皮官。 明天, 我們將看到一堆更多的運(yùn)算符, 因?yàn)槲覀兛纯?6model 和序列化上下文, 它們有一堆與它們相關(guān)的 nqp::ops

在本節(jié)中, 我們將概述“其余的”实辑。 概述不是詳盡無(wú)遺, 因?yàn)槟菚?huì)令人精疲力盡捺氢。

記住它們可以以 nqp::op 形式QAST::Op 節(jié)點(diǎn)中使用, 因此這些知識(shí)可以重復(fù)使用!

算術(shù)

這些是以原生整數(shù)形式:

add_i   sub_i   mul_i   div_i   mod_i
neg_i   abs_i

還有原生浮點(diǎn)數(shù)形式:

add_n   sub_n   mul_n   div_n   mod_n
neg_n   abs_n

為了幫助實(shí)現(xiàn)有理數(shù), 我們還有:

lcm_i   gcd_i

數(shù)字

基礎(chǔ)的東西:

pow_n       ceil_n      floor_n
ln_n        sqrt_n      log_n
exp_n       isnanorinf  inf
neginf      nan

三角函數(shù):

sin_n   asin_n  cos_n   acos_n  tan_n
atan_n  atan2_n sinh_n  cosh_n  tanh_n
sec_n   asec_n  sech_n

關(guān)系

為了比較原生整數(shù), 原生浮點(diǎn)數(shù)和原生字符串(如有必要代碼生成器將會(huì)拆箱)剪撬。 例如, 原生整數(shù)形式是:

cmp_i       compare; returns -1, 0, or 1
iseq_i      non-zero if equal
isne_i      non-zero if non-equal
islt_i      non-zero if less than
isle_i      non-zero if less than or equal to
isgt_i      non-zero if greater than
isge_i      non-zero if greater than or equal to

_n_s 形式也都存在摄乒。

數(shù)組操作

有各種運(yùn)算符操作數(shù)組:

atpos       atpos_i     atpos_n     atpos_s
bindpos     bindpos_i   bindpos_n   bindpos_s
push        push_i      push_n      push_s
pop         pop_i       pop_n       pop_s
shift       shift_i     shift_n     shift_s
unshift     unshift_i   unshift_n   unshift_s
splice      existspos   elems       setelems

請(qǐng)注意, 原生類型的版本是非強(qiáng)制的, 但只適用于原生類型的數(shù)組。

散列操作

看起來(lái)跟數(shù)組操作并無(wú)二至残黑。

atkey       atkey_i     atkey_n     atkey_s
bindkey     bindkey_i   bindkey_n   bindkey_s
existskey   deletekey   elems

這些都假定鍵是字符串; 任何非字符串鍵將首先被強(qiáng)制轉(zhuǎn)換為字符串馍佑。

旁白: Perl 6 數(shù)組/散列 ops 的用法

在 Perl 6 中, 像下面這樣的東西:

@a[0] = 42;

實(shí)際上使用 atpos 來(lái)使標(biāo)量容器綁定到底層的數(shù)組存儲(chǔ), 然后分配給該容器。 bindpos 只用于做:

@a[0] := 42;

此外, 你永遠(yuǎn)不會(huì)直接在 Perl 6 的 ArrayHash 對(duì)象上這樣做梨水。 這些對(duì)象包含一個(gè)較低級(jí)別(lower-level)的數(shù)組或散列作為屬性, 而方法在那上面使用這個(gè) ops挤茄。

創(chuàng)建列表和散列

nqp::list op 創(chuàng)建一個(gè)(低級(jí))數(shù)組, 其元素傳遞給它。 因此, 它是一個(gè)可變參數(shù) op冰木。

nqp::list($foo, $bar, $baz)

原生類型的列表可以使用 list_i, list_nlist_s 創(chuàng)建穷劈。

有一個(gè)類似的 nqp::hash, 它期望一個(gè)鍵, 一個(gè)值 ...

nqp::hash('name', $name, 'age', $age)

最后, islistishash 告訴你某個(gè)東西是否是低級(jí)的數(shù)組或散列笼恰。

字符串

字符串操作大多數(shù)命名為 Perl 6 那樣。

chars       uc          lc          x
concat      chr         join        split
flip        replace     substr      ord
index       rindex      codepointfromname

還有用于檢查字符類成員資格的操作歇终。 這些主要是在編譯正則表達(dá)式或正則表達(dá)式相關(guān)的類時(shí)發(fā)出的, 但可以在其他地方使用社证。 他們是:

nqp::iscclass(class, str, index)
nqp::findcclass(class, str, index, limit)
nqp::findnotcclass(class, str, index, limit)

其中 classnqp::const::CCLASS_* 其中之一。

條件

ifunless ops 期望兩個(gè)或三個(gè)孩子: 一個(gè)條件, 一個(gè) "then" 和一個(gè)可選的 "else"评凝。 注意, NQP 和 Perl 6 中的 elsif 是通過(guò)嵌套 if QAST::Op 節(jié)點(diǎn)來(lái)編譯的追葡。

# AST for '$/.ast ?? $/.ast !! $/.Str'
QAST::Op.new(
    :op('if'),
    QAST::Op.new(
        :op('callmethod'), :name('ast'),
        QAST::Var.new( :name('$/'), :scope('lexical') )
    ),
    QAST::Op.new(
        :op('callmethod'), :name('ast'),
        QAST::Var.new( :name('$/'), :scope('lexical') )
    ),
    QAST::Op.new(
        :op('callmethod'), :name('Str'),
        QAST::Var.new( :name('$/'), :scope('lexical') )
    )
)

條件和一元塊

NQP 和 Perl 6 都支持像下面這樣的東西:

if %core_ops{$name} -> $mapper {
    return $mapper($qastcomp, $op);
}

這將計(jì)算 %core_ops{$name}, 然后將它傳遞給 $mapper, 如果它是一個(gè)真值的話。

在 QAST 級(jí)別, 這由 if op 的第二個(gè)孩子代表 QAST::Block, 它的元數(shù) arity 被設(shè)置為一個(gè)非零值奕短。

循環(huán)

有四個(gè)相關(guān)的循環(huán)結(jié)構(gòu):

                            Loop while true    Loop while false
                            ---------------    ---------------
Condition, then body      | while              until
Body, then condition      | repeat_while       repeat_until               

它們接收兩個(gè)或三個(gè)孩子:

  • 條件
  • 循環(huán)體
  • 可選的, 在循環(huán)體之后要做的事情

如果拋出 redo 控制異常, 則重新計(jì)算第二個(gè)子節(jié)點(diǎn)宜肉。 第三個(gè)只有在任何 redo 發(fā)生后才被計(jì)算。 它由 Perl 6(C風(fēng)格)的 loop 結(jié)構(gòu)使用翎碑。

Loop example

Perl 6 的 lettemp 關(guān)鍵字保存容器及其原始值的列表(容器, 值等)谬返。這是在塊退出時(shí)遍歷此列表進(jìn)行恢復(fù)的循環(huán)。

$phaser_block.push(QAST::Op.new(
    :op('while'),
    QAST::Var.new( :name($value_stash), :scope('lexical') ),
    QAST::Op.new(
        :op('p6store'),
        QAST::Op.new(
            :op('shift'),
            QAST::Var.new( :name($value_stash), :scope('lexical') )
        ),
        QAST::Op.new(
            :op('shift'),
            QAST::Var.new( :name($value_stash), :scope('lexical') )
        ))));

其它控制結(jié)構(gòu)

還有三個(gè)值得了解的控制結(jié)構(gòu):

  • for 需要兩個(gè)孩子, 一個(gè)可迭代(通常是一個(gè)低級(jí)數(shù)組或列表)和一個(gè)塊日杈。 它為迭代中的每個(gè)東西調(diào)用該塊遣铝。 僅用于 NQP; Rakudo 中處理迭代是完全不同的。
  • ifnull 需要兩個(gè)孩子莉擒。 它計(jì)算第一個(gè)酿炸。 如果它不為 null, 它只是產(chǎn)生這個(gè)值。 如果 null, 它將計(jì)算第二個(gè)孩子涨冀。
  • deforifnull 相同, 但是考慮的是定義而不是 nullness

拋出異常

有各種創(chuàng)建和拋出異常的操作:

newexception    創(chuàng)建一個(gè)新的, 空的異常對(duì)象
setextype       設(shè)置異常類別 (nqp::const::CONTROL_*)
setmessage      設(shè)置異常信息 (string)
setpayload      設(shè)置異常有效負(fù)載(object)
throw           拋出異常對(duì)象
die             執(zhí)行/拋出帶有字符串信息的異常

有一個(gè)更簡(jiǎn)單的方法來(lái)拋出一些常見(jiàn)的控制異常:

QAST::Op.new( :op('control'), :name('next') )

這里其它有效的名字有 redolast填硕。

處理異常

handle op 用于表示異常處理。 第一個(gè)孩子是用處理程序保護(hù)的代碼鹿鳖。 然后處理程序被指定為一個(gè)字符串, 指定要處理的異常的類型, 后面跟著要運(yùn)行的 QAST 來(lái)處理它廷支。

NQP 和 Rakudo 保留每個(gè)塊 %*HANDLERS, 并且當(dāng)塊被完全解析時(shí)在塊外面構(gòu)建它的 handle op。

my $ast := $<statementlist>.ast;
if %*HANDLERS {
    $ast := QAST::Op.new( :op('handle'), $ast );
    for %*HANDLERS {
        $past.push($_.key);
        $past.push($_.value);
    }
}

使用異常對(duì)象

在處理程序內(nèi), 可以使用以下操作栓辜。 除了第一個(gè)之外, 他們都接受一個(gè)異常對(duì)象恋拍。

exception       獲取當(dāng)前的異常對(duì)象
getextype       獲取異常類別 (nqp::const::CONTROL_*)
getmessage      獲取異常信息 (string)
getpayload      獲取異常有效負(fù)載 (object)
rethrow         重新拋出異常
resume          如果可能的話, 恢復(fù)異常

最后, 還有兩個(gè)與回溯相關(guān)的操作; backtrace 返回一個(gè)散列數(shù)組, 每個(gè)散列描述一個(gè)回溯條目, backtracestrings 只返回一個(gè)描述條目的字符串?dāng)?shù)組。

上下文自省

可以使用各種操作來(lái)對(duì)詞法作用域中的符號(hào)進(jìn)行內(nèi)省, 或者遍歷動(dòng)態(tài)(調(diào)用者)或靜態(tài)(詞法的)作用域鏈藕甩。 它們通常用于實(shí)現(xiàn)諸如 Perl 6 中的 CALLEROUTER 偽包功能施敢。

ctx             獲取一個(gè)代表當(dāng)前上下文的對(duì)象
ctxouter        接收一個(gè)上下文并返回它的外部上下文或 null
ctxcaller       接收一個(gè)上下文并返回它的調(diào)用者上下文或 null
ctxlexpad       接收一個(gè)上下文并返回它的 lexpad
curlexpad       獲取當(dāng)前的 lexpad
lexprimspec     給定一個(gè) lexpad 和 名字, 獲取名字的原始類型

lexpad 本身可以與適當(dāng)?shù)纳⒘胁僮?atkey, bindkey)一起使用, 以操作其中包含的符號(hào)。

大整數(shù)

Perl 6 需要大的整數(shù)支持其 Int 類型狭莱。 因此, 它在 NQP 操作中提供僵娃。 大整數(shù)操作僅對(duì)具有 P6bigint 表示(明天更多地表示)的對(duì)象有效, 或者對(duì)其進(jìn)行封裝。

具有大整數(shù)結(jié)果的那些操作與它們的原生親屬不同, 通過(guò)采用額外的操作數(shù)(這是結(jié)果的類型對(duì)象)腋妙。
下面的來(lái)自于 Perl 6 設(shè)置的 mults 說(shuō)明了這一點(diǎn)默怨。

multi infix:<+>(Int:D \a, Int:D \b) returns Int:D {
    nqp::add_I(nqp::decont(a), nqp::decont(b), Int);
}
multi infix:<+>(int $a, int $b) returns int {
    nqp::add_i($a, $b)
}

_I 后綴用于大整數(shù) ops.

練習(xí) 6

如果時(shí)間允許, 您可以通過(guò)添加對(duì) PHPish 的支持來(lái)探索一些 nqp::ops(按順序, 或選擇你覺(jué)得最有趣的):

  • 基本的數(shù)字關(guān)系運(yùn)算 (<, >, ==, etc.)
  • if/else if/else
  • while 循環(huán)

參見(jiàn)練習(xí)表得到一些提示。

今天就到這兒了

今天, 我們已經(jīng)涉及了很多領(lǐng)域, 從 NQP 語(yǔ)言開(kāi)始, 然后建立如何使用它來(lái)實(shí)現(xiàn)一個(gè)簡(jiǎn)單的編譯器骤素。

這是一個(gè)好的開(kāi)始, 但我們?nèi)匀蝗鄙賻讉€(gè) NQP 和 Rakudo 嚴(yán)重依賴的非常重要的部分匙睹。這包括對(duì)象和序列化上下文的概念愚屁。 我們明天再來(lái)看看。

還有什么問(wèn)題嗎?

(修理)

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末痕檬,一起剝皮案震驚了整個(gè)濱河市霎槐,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌梦谜,老刑警劉巖丘跌,帶你破解...
    沈念sama閱讀 219,427評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異唁桩,居然都是意外死亡闭树,警方通過(guò)查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,551評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門荒澡,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái)报辱,“玉大人饶碘,你說(shuō)我怎么就攤上這事£希” “怎么了麻车?”我有些...
    開(kāi)封第一講書人閱讀 165,747評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵,是天一觀的道長(zhǎng)余黎。 經(jīng)常有香客問(wèn)我,道長(zhǎng),這世上最難降的妖魔是什么躏升? 我笑而不...
    開(kāi)封第一講書人閱讀 58,939評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮狼忱,結(jié)果婚禮上膨疏,老公的妹妹穿的比我還像新娘。我一直安慰自己钻弄,他們只是感情好佃却,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,955評(píng)論 6 392
  • 文/花漫 我一把揭開(kāi)白布。 她就那樣靜靜地躺著窘俺,像睡著了一般饲帅。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上瘤泪,一...
    開(kāi)封第一講書人閱讀 51,737評(píng)論 1 305
  • 那天灶泵,我揣著相機(jī)與錄音,去河邊找鬼对途。 笑死赦邻,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的实檀。 我是一名探鬼主播惶洲,決...
    沈念sama閱讀 40,448評(píng)論 3 420
  • 文/蒼蘭香墨 我猛地睜開(kāi)眼按声,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼!你這毒婦竟也來(lái)了湃鹊?” 一聲冷哼從身側(cè)響起儒喊,我...
    開(kāi)封第一講書人閱讀 39,352評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤,失蹤者是張志新(化名)和其女友劉穎币呵,沒(méi)想到半個(gè)月后怀愧,有當(dāng)?shù)厝嗽跇?shù)林里發(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,834評(píng)論 1 317
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡余赢,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,992評(píng)論 3 338
  • 正文 我和宋清朗相戀三年芯义,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片妻柒。...
    茶點(diǎn)故事閱讀 40,133評(píng)論 1 351
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡扛拨,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出举塔,到底是詐尸還是另有隱情绑警,我是刑警寧澤,帶...
    沈念sama閱讀 35,815評(píng)論 5 346
  • 正文 年R本政府宣布央渣,位于F島的核電站计盒,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏芽丹。R本人自食惡果不足惜北启,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,477評(píng)論 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望拔第。 院中可真熱鬧咕村,春花似錦、人聲如沸蚊俺。這莊子的主人今日做“春日...
    開(kāi)封第一講書人閱讀 32,022評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)泳猬。三九已至肩钠,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間暂殖,已是汗流浹背价匠。 一陣腳步聲響...
    開(kāi)封第一講書人閱讀 33,147評(píng)論 1 272
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒(méi)想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留呛每,地道東北人踩窖。 一個(gè)月前我還...
    沈念sama閱讀 48,398評(píng)論 3 373
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像晨横,于是被迫代替她去往敵國(guó)和親洋腮。 傳聞我的和親對(duì)象是個(gè)殘疾皇子箫柳,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,077評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容

  • 從匹配中返回值 Match 對(duì)象 成功的匹配總是返回一個(gè) Match 對(duì)象, 這個(gè)對(duì)象通常也被放進(jìn) $/ 中, (...
    焉知非魚閱讀 1,800評(píng)論 0 1
  • 允許的修飾符 有些修飾符能在所有允許的地方出現(xiàn), 但并非所有的都這樣. 通常, 影響 regex 編譯的修飾符(...
    焉知非魚閱讀 1,343評(píng)論 0 1
  • 2009 有用的和有意思的循環(huán) 讓我們來(lái)看一個(gè)基本的例子. 這是一個(gè)最簡(jiǎn)單清晰的語(yǔ)法的例子.在這并沒(méi)有使用括號(hào)來(lái)包...
    焉知非魚閱讀 553評(píng)論 0 0
  • 現(xiàn)在你可能已經(jīng)習(xí)慣了 Perl 6 中到處出現(xiàn)的前綴"meta"。Metaclasses, Metaobjects...
    焉知非魚閱讀 258評(píng)論 0 0
  • 捕獲 簽名不僅僅是語(yǔ)法啥供,它們是含有一列參數(shù)對(duì)象的 first-class 對(duì)象 悯恍。同樣地,有一種含有參數(shù)集的數(shù)據(jù)...
    焉知非魚閱讀 562評(píng)論 0 0