Wait the light to fall

NQP 练习题

焉知非鱼

练习 1

本练习将让您在 NQP 中执行一些简单的任务,以熟悉基本语法。

Hello, world #

最简单的事情:

say('Hello, world');

现在把它弄错:

say 'Hello, world';

请注意你将如何大声地抱怨。

变量 #

  1. 将值绑定到标量。
  2. 使用 say 来输出它.
  3. 尝试使用赋值代替绑定。 观察它的失败。
  4. 声明一个数组。 使用索引, 例如 @a[0], 去绑定并访问元素。 注意在 Raku 中, 符号是不变的; 使用 $a[0] 则会抱怨未声明的 $a!
  5. 尝试绑定一个数组字面量, 例如 [1,2,3] 到一个 @arr 变量上。现在把该数组字面量绑定到一个 $scalar 变量上。注意观察索引是怎样在这两种结构上都工作良好的。
  6. 声明一个哈希变量, %h. 尝试使用字面值语法索引 (%h<key>) 和花括号语法索引 (%h{'key'})。

循环 #

  1. 使用 for 来迭代数组, 一次迭代一个元素, 先使用 $_ 迭代, 然后使用 pointy block (for @a -> $val { ... }) 迭代。
  2. 你可以一次迭代两个元素吗? 试试看!
  3. 迭代散列变量, 打印出该散列每个 pair 对儿的 .key.value

子例程 #

实现一个递归的阶乘子例程, sub fac。 如果你做对了 , fac(10) 会返回 3628800。

类 #

  1. 声明一个 BarTab 类。为它提供表号的标量属性和项的数组属性。
  2. 写一个返回表号的 table方法。
  3. 写一个接收项名和价格的 add_order 方法。在项属性上放上一个带有键 name 和键 price 的散列。
  4. 写一个能产生字符串的 render_tab 方法, 它描述 tab 上有什么(items, price for each item, total)
  5. 创建一个 tab (BarTab.new(table => 42))。 检查 table 方法是否返回 42。添加一些项并确保工作正常。你甚至想使用 NQP 的内置函数 plan(...)ok(...) - 来测试!

Multi-methods #

  1. add_order 添加一个方法。然后把当前的 add_order 方法变成一个 multi 方法 (只须在它前面加上关键字 multi)。确保所有的都正常工作。
  2. add_order 添加另一个接收 3 个参数的 multi 候选者, 第三个参数是数量。对于此候选者,将指定次数的项散列添加到数组中。

如果时间允许… #

  1. 写一个词法类 Item (在你的 BarTab 类中用 my 声明的类). 给它加上 nameprice 属性, 和访问这些属性的方法。
  2. 重构你的代码以使用该内部类, 所以你拥有一个 Item 的数组, 而不是一个散列的数组。
  3. 重构你的代码,以便代替项目列表中显示的项目, 如果被订购多次,则每个项都有一个数量。 (您可能希望在 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 实现。查看我们目前看到的代码,并运行一些简单的 INSERTSELECT 查询。

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 子句:SELECTDELETEUPDATE。你能想出来吗?

如果你是 10x 工程师… #

扩展 WHERE 子句中可用的运算符范围以包括 !=<<=>>=

练习 4

创建一个只支持 echo 语句的最小 php 编译器:

echo "<h2>PHP is fun!</h2>";
echo "<blink>So 1990s!</blink>";

基础 #

  1. 创建一个新的 NQP 源文件, 然后 use NQPHLL
  2. 创建 HLL::Grammar, HLL::ActionsHLL::Compiler 的子类。
  3. 写一个 MAIN 子例程, 就像那个 Rubyish 那样, 设立编译器对象并调用 command_line。验证运行没有参数的程序现在进入 REPL,但尝试运行任何代码失败,因为语法没有 TOP 规则。
  4. 只需要足够的语法就可以解析一个 echo 状态列表。最后一个不需要后跟分号。再次运行 REPL 并确保您的语法解析,并且您得到的错误是因为没有构建 AST。
  5. 写一个 action 方法来制作 QAST。确保您的程序现在运行。

走得更远 #

是时候变得更 PHP-ish 了!

  1. echo 语句不会自动在输出的末尾添加新行,因此请使用 print op 而不是 say
  2. 在你的引用解析器中打开反斜杠序列的解析。这是通过 <quote_EXPR: ':q', ':b'> (注意 :b) 来完成的。 确保现在你可以在 echo "stuff\n" 时是否带有换行。
  3. 还可以使用 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";

似乎在 elseifelse 形式中还有其他奇妙的违规行为,其中也涉及冒号,谁知道它是什么。 但如果你这样做,最好去喝点酒。 或者,我也不知道。

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 的调用(稍后会写入)。要查找父级,请使用作用域设置为 lexicalQAST::Var 节点。

您现在应该收到一条错误消息,指出 add_parent 尚未被实现。从这里开始,所有工作都将在 PHPClassHOW 中进行。

添加属性 @!parents。实现将添加到此数组的 add_parent,但仅限于还没有存在父级。如果有,那么 nqp::die 就可以了。这应该清除错误,但同样,最后的ssnell 调用还不起作用。

接下来,更新 find_method 方法。如果要搜索的方法不在 %!methods 中,则应该查看所有父项的方法表。为此,您需要实现方法 parentmethod_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 方法。 您的目标是实现此功能。 这里有一些提示。

  1. 首先在其中添加对接口和方法需求的解析。
  2. 一旦有效,编写一个 PHPInterfaceHOW,可用于保存所需方法的列表。 因为你应该使用 Uninstantiable REPR, 因为不允许创建接口的实例。
  3. implements 关键字解析添加到 class 声明中。 设置在 PHPClassHOW 上调用 add_interface 方法的 action 方法,应该将接口添加到 @!interfaces 属性。
  4. 你现在应该发现上面的例子都编译了,但第二个例子没有被拒绝。 为了实现这一点,在 PHPClassHOWcompose 方法中,编写代码,获取每个接口需求方法的列表。并检查类是否提供它们。

练习 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+