標(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é)碼)。
接收直截了當(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)磅甩。
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文件等)然后:
因此, 你 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');
try
和 CATCH
指令也是可用的, 盡管不像完全的 Perl 6, 你沒(méi)有期望去智能匹配 CATCH
的內(nèi)部步势。一旦你到了那兒, 就認(rèn)為異常被捕獲到了(彈出一個(gè)顯式的 nqp::rethrow
)。
try {
something();
CATCH { say("Oops") }
}
類, 屬性和方法
就像在 Perl 6 中一樣, 使用 class
, has
和 method
關(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
, rule
或 regex
聲明的規(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ì)回溯, 而 rule
或 token
不會(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
});
迭代結(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)用贴谎。
在 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)
我們解析 INSERT
或 SELECT
查詢的任意之一.
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)
pairlist
和 keylist
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)用鏈可以被忽略磁玉。
它由非聲明性構(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), 一旦我們添加 UPDATE
和 DELETE
查詢, 維護(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è)例子添加 UPDATE
和 DELETE
查詢支持溺欧。
限制和與完整 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)稱為后端**篙议。
前端, 后端和它們之間的 QAST
前端的最后一個(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è) 方法
, 而不是 token
或 rule
这难!
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
, rule
或 regex
時(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
等), 以及類似于 NQPMu
和 NQPArray
的類型。
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
, while
和 CATCH
這樣的東西 - 這是一個(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è)代表 call
的 QAST::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è)置為 param
的 QAST::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::WVal
和 QAST::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 做什么
這里看看 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::op
s。 它們的范圍從算術(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 的 Array
或 Hash
對(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_n
和 list_s
創(chuàng)建穷劈。
有一個(gè)類似的 nqp::hash
, 它期望一個(gè)鍵, 一個(gè)值 ...
nqp::hash('name', $name, 'age', $age)
最后, islist
和 ishash
告訴你某個(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)
其中 class
是 nqp::const::CCLASS_*
其中之一。
條件
if
和 unless
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 的 let
和 temp
關(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è)孩子涨冀。 -
defor
與ifnull
相同, 但是考慮的是定義而不是 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') )
這里其它有效的名字有 redo
和 last
填硕。
處理異常
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 中的 CALLER
和 OUTER
偽包功能施敢。
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è)置的 mult
s 說(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)題嗎?
(修理)