NQP 练习题
— 焉知非鱼练习 1
本练习将让您在 NQP 中执行一些简单的任务,以熟悉基本语法。
Hello, world #
最简单的事情:
say('Hello, world');
现在把它弄错:
say 'Hello, world';
请注意你将如何大声地抱怨。
变量 #
- 将值绑定到标量。
- 使用
say
来输出它. - 尝试使用赋值代替绑定。 观察它的失败。
- 声明一个数组。 使用索引, 例如
@a[0]
, 去绑定并访问元素。 注意在 Raku 中, 符号是不变的; 使用$a[0]
则会抱怨未声明的$a
! - 尝试绑定一个数组字面量, 例如
[1,2,3]
到一个@arr
变量上。现在把该数组字面量绑定到一个$scalar
变量上。注意观察索引是怎样在这两种结构上都工作良好的。 - 声明一个哈希变量,
%h
. 尝试使用字面值语法索引 (%h<key>
) 和花括号语法索引 (%h{'key'}
)。
循环 #
- 使用
for
来迭代数组, 一次迭代一个元素, 先使用$_
迭代, 然后使用 pointy block (for @a -> $val { ... }
) 迭代。 - 你可以一次迭代两个元素吗? 试试看!
- 迭代散列变量, 打印出该散列每个 pair 对儿的
.key
和.value
。
子例程 #
实现一个递归的阶乘子例程, sub fac
。 如果你做对了 , fac(10)
会返回 3628800。
类 #
- 声明一个
BarTab
类。为它提供表号的标量属性和项的数组属性。 - 写一个返回表号的
table
方法。 - 写一个接收项名和价格的
add_order
方法。在项属性上放上一个带有键name
和键price
的散列。 - 写一个能产生字符串的
render_tab
方法, 它描述 tab 上有什么(items, price for each item, total) - 创建一个 tab (
BarTab.new(table => 42)
)。 检查table
方法是否返回 42。添加一些项并确保工作正常。你甚至想使用 NQP 的内置函数plan(...)
和ok(...)
- 来测试!
Multi-methods #
- 给
add_order
添加一个方法。然后把当前的add_order
方法变成一个 multi 方法 (只须在它前面加上关键字multi
)。确保所有的都正常工作。 - 给
add_order
添加另一个接收 3 个参数的 multi 候选者, 第三个参数是数量。对于此候选者,将指定次数的项散列添加到数组中。
如果时间允许… #
- 写一个词法类
Item
(在你的BarTab
类中用my
声明的类). 给它加上name
和price
属性, 和访问这些属性的方法。 - 重构你的代码以使用该内部类, 所以你拥有一个
Item
的数组, 而不是一个散列的数组。 - 重构你的代码,以便代替项目列表中显示的项目, 如果被订购多次,则每个项都有一个数量。 (您可能希望在
BarTab
中切换到使用散列而不是数组,因此您可以按名称查找项,只更新已订购的项的数量。)
练习 2
Grammar #
以纯文本格式,来自 Raku irc 频道的日志如下所示:
14:30 colomon r: say 2**100
14:30 camelia rakudo 624ff7: OUTPUT«1267650600228229401496703205376»
14:30 colomon sweet!!!!!
14:30 TimToady bet that took a lot of electrons...
也就是说,时间,昵称和一些文本。首先写一个解析任意行数的 grammar,捕获时间,昵称和文本。遍历匹配对象并检查它是否具有正确的输出。
Actions #
声明一个 Utterance
类来表示一行聊天。它应该有三个属性:time,nick 和 text,以及访问它们的方法。您可以依赖默认构造函数来设置它们。
现在编写一个 actions 类,为每行文本构建此类的实例。最后,添加一个生成数组的 TOP
action 方法。
额外任务: Actions #
除了正常的聊天行外,还有一些 action 行,它们在昵称前有一个 *
:
07:26 moritz r: say (1, 2, 3).map({()}).flat.perl
07:26 camelia rakudo 099f0b: OUTPUT«().list»
07:26 * moritz wonders if .grep itself flattens
添加对解析此内容的支持,并将其作为额外属性记录在 Utterance
中。
额外任务: Collect lines #
用一个数组替换 Utterance
中的 text 属性。然后,如果单个昵称说出多个连续的文本行,则将它们放入单个数组中并为它们创建单个 Utterance
实例。
练习 3
熟悉 #
采用 SlowDB 实现。查看我们目前看到的代码,并运行一些简单的 INSERT
和 SELECT
查询。
DELETE #
实现删除行的功能。 DELETE
查询的示例如下:
DELETE WHERE name = 'jnthn'
DELETE WHERE name = 'jnthn', is_action = 1
也就是说,可以存在一个条件或由逗号分隔的条件的组合。
UPDATE #
实现更新现有行的功能。 UPDATE
查询的示例如下:
UPDATE WHERE type = 'stout' SET tasty = 1
UPDATE WHERE type = 'stout' SET tasty = 1, dark = 1
UPDATE WHERE type = 'stotu' SET type = 'stout'
Refactor #
现在有三件事支持 WHERE
子句:SELECT
,DELETE
和 UPDATE
。你能想出来吗?
如果你是 10x 工程师… #
扩展 WHERE
子句中可用的运算符范围以包括 !=
,<
,<=
,>
和 >=
。
练习 4
创建一个只支持 echo
语句的最小 php 编译器:
echo "<h2>PHP is fun!</h2>";
echo "<blink>So 1990s!</blink>";
基础 #
- 创建一个新的 NQP 源文件, 然后
use NQPHLL
。 - 创建
HLL::Grammar
,HLL::Actions
和HLL::Compiler
的子类。 - 写一个
MAIN
子例程, 就像那个 Rubyish 那样, 设立编译器对象并调用command_line
。验证运行没有参数的程序现在进入 REPL,但尝试运行任何代码失败,因为语法没有TOP
规则。 - 只需要足够的语法就可以解析一个
echo
状态列表。最后一个不需要后跟分号。再次运行 REPL 并确保您的语法解析,并且您得到的错误是因为没有构建 AST。 - 写一个 action 方法来制作
QAST
。确保您的程序现在运行。
走得更远 #
是时候变得更 PHP-ish 了!
echo
语句不会自动在输出的末尾添加新行,因此请使用print
op 而不是say
。- 在你的引用解析器中打开反斜杠序列的解析。这是通过
<quote_EXPR: ':q', ':b'>
(注意:b
) 来完成的。 确保现在你可以在echo "stuff\n"
时是否带有换行。 - 还可以使用
echo("with parentheses")
。请实现它。
练习 5
在本练习中,您将为 PHPish 添加一些功能,以便探索我们一直在研究的 QAST 节点。到最后,您应该能够运行以下程序:
Values #
到目前为止,echo
只能处理字符串参数。是时候改变它了!添加一个 value
protoregex,以及解析整数,浮点数和字符串的候选者。确保您的 string token 如下所示:
token value:sym<string> { <?["]> <quote_EXPR: ':q', ':b'> }
这样你就能够处理 \n
。 添加适当的 action 方法(记住 protoregex 本身不需要一个 action 方法)。 更新 echo
规则和 action 方法以接收 value
,而不仅仅是字符串。 确保您可以像以前一样运行相同的操作,并且:
echo 42;
echo 1.5;
Operators #
研究运算符优先级解析材料。 设置两个优先级,一个用于加法,一个用于乘法运算符。 像 Rubyish 一样添加四个常用的算术运算(+
, -
,*
,/
)。 再添加 .
用于字符串连接(NQP 用于连接操作的叫 concat
)。
接下来,为术语
protoregex 添加一个候选项,该术语应该调用 value
来解析一个值。 这需要匹配的 action 方法。
最后,更新 echo
以解析 EXPR
而不是 value
。 你现在应该能够运行:
echo 2 * 5 . "\n";
Variables #
奇怪的是,PHP 局部变量对 Ruby 有类似的声明语义:首先赋值声明。 围绕全局变量有一些更有趣的规则,但我们暂时将它们放在一边。
首先,为赋值添加优先级,并且赋值运算符被映射到 bind
op。 此外,添加 grammar 规则和 action 方法,以便表达式可以用作语句。 在此之后,您应该能够解析:
1 = 1
但是,这是无稽之谈,所以它会在代码生成过程中出错。
我们现在需要更多地关注块结构。 更新您的 TOP
grammar 规则以声明 $*CUR_BLOCK
,就像我们在 Rubyish 中所做的那样,并对 action 方法进行匹配更新。
接下来,实现变量解析。 再一次,您可以从我们在 Rubyish 中所做的工作中获得灵感,但变量命名规则如下:
- 变量以
$
符号开头,后跟变量名。 - 变量名必须以字母或下划线字符开头(不是数字)
- 变量名只能包含字母数字字符和下划线(A..Z,a..z,0..9,和
_
)
还要记住 PHP 不是面向行的。 完成后,您应该能够执行以下程序:
$emotion = "awesome";
echo "PHP is " . $emotion . "\n";
$emotion = $emotion . "st";
echo "Raku is " . $emotion . "\n";
额外任务: functions #
实现带参数和参数传递的函数! 完成后,您应该能够运行:
function greet($name) {
echo "Good day, " . $name . "\n";
}
greet("Lena");
你可以从 Rubyish 的例子中汲取灵感。 但是,您还需要注意确保函数声明后面不需要分号。
练习 6
这个练习是为了在第一天结束时填补时间,如果我们以某种方式通关了! 以下是您可以做的一些想法。
基本的数字关系运算符 #
现在,我们将忽略 PHP 的关系运算符所做的类型杂耍,以处理字符串和数字,并处理数字情况。 PHP 有两个比较运算符的优先级:
Tighter level: < <= > >=
Looser level: == !=
它们同时比加法操作更宽松,比赋值操作更紧凑。 不需要任何 action 方法,因为: :op<...>
可以在 <O(...)>
中使用。
if/else if/else #
既然我们有关系运算符,那么实现 if
语句是有意义的。 从简单开始:
if ($a > $b) {
echo "a is bigger than b";
}
然后添加 else
:
if ($a == $b) {
echo "a is the same as b";
}
else {
echo "a and b differ";
}
最后添加上 elseif
:
if ($a > $b) {
echo "a is bigger than b";
} elseif ($a == $b) {
echo "a is equal to b";
} else {
echo "a is smaller than b";
}
要更好地理解如何处理 elseif
,请阅读 NQP 的 grammar 和 action 中的适用代码。 在 grammar 和 action 中搜索 statement_control:sym<if>
。
最后但并非最不重要的,您甚至可能想尝试实现悬空形式:
if ($a > $b)
echo "a is bigger than b";
似乎在 elseif
和 else
形式中还有其他奇妙的违规行为,其中也涉及冒号,谁知道它是什么。 但如果你这样做,最好去喝点酒。 或者,我也不知道。
while 循环 #
解析并实现 while 循环:
$i = 1;
while ($i <= 10) {
$i = $i + 1;
echo $i;
}
恭喜。你的 PHP 实现现在是图灵完备的了,现在一切就绪了! (旁白:实际上图灵完成的时间早一些,因为递归函数和 while 循环是等价的。)
练习 7
在本练习中,您将为类,方法,对 PHPish 的 new
运算符和方法调用添加基本支持。当你完成后,你应该能够运行:
class Greeter {
function greet($who) {
echo "Greetings, " . $who . "\n";
}
}
$g = new Greeter();
$g->greet("Anna");
首先, 元对象 #
创建一个简单的元对象,就像 Rubyish 一样,可以支持添加和查找方法。
接下来,解析类并为它们生成代码 #
基础知识类似于 Rubyish:类中的函数成为类的方法。因此,您可以窃取该方法,确保使用 PHP 语法。
添加新项 #
这非常相似,你几乎可以直接从 Rubyish 窃取它。请注意不同的空格规则(换行符并不重要)。
添加方法调用 #
再次,类似的方法工作,模数语法:方法调用运算符是 ->
。添加之后,您应该能够运行本节开头的程序 - 但它仍然会失败。问题是我们没有让方法接受调用者(它们所调用的对象)。
支持 $this #
方法的第一个参数是调用它的对象。在 PHP 中,这被分配给 $this
。因此,在类中时,请确保函数始终使用 $this
作为第一个参数。
好吧,这是很多复制粘贴… #
对。大多数基于类的对象系统在这个级别上相对类似。尽管如此,你自己有希望能把它们整合到一块儿会让整个事情变得不那么神奇!
顺便说一下,现在你支持 $this
,这也应该有效:
class Greeter {
function greet($who) {
$this->greet_custom("Hej", $who);
}
function greet_custom($greeting, $who) {
echo $greeting . ", " . $who . "\n";
}
}
$g = new Greeter();
$g->greet("masak");
练习 8
在本练习中,您将稍微扩展 PHPish 对象系统。。。或许很多。
Method 缓存 #
首先,使用方法在 NQP 中手动创建一个新的 PHP 类。它看起来像这样:
my $c := PHPClassHOW.new_type(:name('Foo'));
$c.HOW.add_method($c, 'bar', -> $obj { 1 });
计算 100,000 次方法调用的时间。
my $start := nqp::time_n();
my int $i := 0;
while $i < 100000 {
$obj.bar();
$i++;
}
say(nqp::time_n() - $start);
在您的 PHPClassHOW
中添加一个 compose
方法。在其中,使用 setmethcache
指令。您现在可以使用 %!methods
作为方法缓存散列。添加对您的 compose
方法的调用。重复计时。缓存应该更快(在练习题作者的机器上,没有缓存是 0.194s, 有缓存是 0.066s)。
现在,更新 statement:sym<class>
action 方法中的代码, 以便在添加所有方法后调用 compose
方法。
继承 #
这是我们想要让其能够工作的例子:
class Dog {
function bark() { echo "woof!\n"; }
function smell() { echo "terrible\n"; }
}
class Puppy extends Dog {
function bark() { echo "yap!\n"; }
}
$d = new Dog();
$d->bark();
$d->smell();
$p = new Puppy();
$p->bark();
$p->smell();
首先,从解析开始。它是解析类声明的一个非常小的补充,可选择接受 extends
关键字。一旦它解析,你应该能够运行程序,但它将无法进行最后的 smell
调用,因为它是继承的,我们还没有继承。
接下来,更新类的 action 方法。在添加方法的代码之前,如果有 extends
,请在元对象上添加一个对 add_parent
的调用(稍后会写入)。要查找父级,请使用作用域设置为 lexical
的 QAST::Var
节点。
您现在应该收到一条错误消息,指出 add_parent
尚未被实现。从这里开始,所有工作都将在 PHPClassHOW
中进行。
添加属性 @!parents
。实现将添加到此数组的 add_parent
,但仅限于还没有存在父级。如果有,那么 nqp::die
就可以了。这应该清除错误,但同样,最后的ssnell
调用还不起作用。
接下来,更新 find_method
方法。如果要搜索的方法不在 %!methods
中,则应该查看所有父项的方法表。为此,您需要实现方法 parent
和 method_table
,以便从基类获取所需的信息。
到目前为止,事情应该能工作 - 但效率不高!为此,您现在还应该更新方法缓存。您可以通过在 find_method
中添加调试语句来确保正确执行此操作,如果缓存正在运行,则不应调用该语句。
如果时间允许: interfaces #
在 PHP 中,接口表示要求某个方法列表由声明它实现它的任何类提供。例如,给定:
interface iBarkable {
function bark();
}
那么这个类就是好的:
class Dog implements iBarkable {
function bark() { echo "woof!\n"; }
function smell() { echo "terrible\n"; }
}
而这个类应该导致错误:
class BadDog implements iBarkable {
function smell() { echo "terrible\n"; }
}
因为它缺少 bark
方法。 您的目标是实现此功能。 这里有一些提示。
- 首先在其中添加对接口和方法需求的解析。
- 一旦有效,编写一个
PHPInterfaceHOW
,可用于保存所需方法的列表。 因为你应该使用Uninstantiable
REPR, 因为不允许创建接口的实例。 - 将
implements
关键字解析添加到class
声明中。 设置在PHPClassHOW
上调用add_interface
方法的 action 方法,应该将接口添加到@!interfaces
属性。 - 你现在应该发现上面的例子都编译了,但第二个例子没有被拒绝。 为了实现这一点,在
PHPClassHOW
的compose
方法中,编写代码,获取每个接口需求方法的列表。并检查类是否提供它们。
练习 9
本练习将带您进行正则表达式实现的简短介绍。
解析和 actions #
运行以下代码:
nqp --rxtrace -e "/a+/"
现在,让我们试着理解它。
在 NQP 仓库中, 打开 src/NQP/Gramamr.nqp
并定位到 quote:sym</ />
。
观察到它调用了 LANG
, 那是做语言切换. 这可以在 src/HLL/Grammar.nqp
中找到。 注意它如何使用编织,还有切换操作以及语言类型。
下面, 打开 src/QRegex/P6Regex/Grammar.nqp
并定位到 nibbler
。尝试挑出对 termaltseq
的调用. 看看 termaltseq
自身, 然后该 action 方法所在的 Action 文件。 往下找, 找到 termish
, 并尝试理解它在那儿做什么: 处理不同的备选分支和连词的相对优先级。
再往下, 看看下面的 atom
。 注意这里我们的正则表达式中的 a
是怎样被解析的。然后回头查看 quantified_atom
。 阅读 action 方法,看看原子如何放入量词节点。
您可能希望探索如何处理其他一些结构。
探索嵌入式代码块 #
在 NQP grammar 中, 查看 NQP::Regex
。 注意它是你正在看的基本 QRegex::P6Regex::Grammar
的一个子类。这是 grammar 真正成为类的力量的一部分:我们可以将它们子类化以专门化它们。
让我们来看一下 /[a { say('here') }]+/
是怎样被处理的. 这很有趣, 因为我们从 NQP 开始, 然后解析正则表达式语法, 然后以在块中再次解析 NQP 代码结束。定位到 assertion:sym<{ }>
, 然后查看 codeblock
。 享受相对简单的事情!
看一下 NQP::RegexActions
里面的 codeblock
action 方法。 你能弄清楚发生了什么吗? 如果没有,请问你的老师或你周围的人!
探索 NFAs #
保存的 NFA 形式存储在规则的代码对象中。 因此,我们可以获得它并返回到一个对象,如下所示:
grammar Example {
token t { 'foo' }
}
my $t := nqp::findmethod(Example, 't');
my $nfa := QRegex::NFA.from_saved($t.NFA());
但是怎么办呢? 接下来,打开 src/QRegex/NFA.nqp
。 查看顶部附近的边缘类型常量列表。 NFA 的整体结构是列表。 外部列表表示状态 - 图中的节点。 每个节点由其出局边缘列表组成,由 3 个值表示:
- 一个 action,它是 NFA.nqp 顶部的边缘类型常量之一
- action 的参数
- 如果我们匹配,将转到下一个状态的索引
我们可以写一些代码来转储这个:
my $i := 0;
for $nfa.states -> @edges {
say("State $i");
for @edges -> $act, $arg, $to {
say(" $act ($arg) ==> $to");
}
$i++;
}
对于我们的简单示例,我们得到:
State 0
State 1
2 (102) ==> 2
State 2
2 (111) ==> 3
State 3
2 (111) ==> 0
边缘类型 2 表示代码点,这里的参数是我们应该匹配的字符代码,如果我们要进入下一个状态。 看看我们如何从状态 1 开始,到达状态 0 是成功的。 在 token 中尝试一些不同的东西,看看 NFA 是什么样的。 这是一些想法。
a+
<[abc]>
<[abc]>*
<foo>?
\w*
:i abc
:i a+