跳转至内容

鹦鹉虚拟机/Squaak 教程/Squaak 细节和第一步

来自 Wikibooks,开放世界中的开放书籍

在之前的几集中,我们介绍了 Parrot 编译器工具 (PCT)。从高层次概述开始,我们快速创建了自己的小型脚本语言 Squaak,使用 Parrot 提供的 Perl 脚本。我们讨论了基于 PCT 的编译器的通用结构,以及每个默认的四个转换阶段。第三集是乐趣开始的地方。在本集中,我们将介绍 Squaak 的完整规范。在本集及后续集中,我们将逐步实现此规范,以易于理解的小增量进行。一旦你掌握了窍门,你就会注意到实现 Squaak 几乎是微不足道的,最重要的是,非常有趣!所以,让我们开始吧!

Squaak 语法

[编辑 | 编辑源代码]

事不宜迟,以下是 Squaak 的完整语法规范。此规范使用以下元语法

   statement   indicates a non-terminal, named "statement"
   {statement} indicates zero or more statements
   [step]      indicates an optional step
   'do'        indicates the keyword 'do'

以下是 Squaak 的语法。起始符号是 program。

    program              ::= {stat-or-def}

    stat-or-def          ::= statement
                           | sub-definition

    statement            ::= if-statement
                           | while-statement
                           | for-statement
                           | try-statement
                           | throw-statement
                           | variable-declaration
                           | assignment
                           | sub-call
                           | do-block

    block                ::= {statement}

    do-block             ::= 'do' block 'end'

    if-statement         ::= 'if' expression 'then' block
                             ['else' block]
                             'end'

    while-statement      ::= 'while' expression 'do'
                             block 'end'

    for-statement        ::= 'for' for-init ',' expression [step]
                             'do'
                             block
                             'end'

    step                 ::= ',' expression

    for-init             ::= 'var' identifier '=' expression

    try-statement        ::= 'try' block 'catch' identifier
                             block
                             'end'

    throw-statement      ::= 'throw' expression

    sub-definition       ::= 'sub' identifier parameters
                             block
                             'end'

    parameters           ::= '(' [identifier {',' identifier}] ')'

    variable-declaration ::= 'var' identifier ['=' expression]

    assignment           ::= primary '=' expression

    sub-call             ::= primary arguments

    primary              ::= identifier postfix-expression*

    postfix-expression   ::= key
                           | index
                           | member

    key                  ::= '{' expression '}'

    index                ::= '[' expression ']'

    member               ::= '.' identifier

    arguments            ::= '(' [expression {',' expression}] ')'

    expression           ::= expression {binary-op expression}
                           | unary-op expression
                           | '(' expression ')'
                           | term

    term                 ::= float-constant
                           | integer-constant
                           | string-constant
                           | array-constructor
                           | hash-constructor
                           | primary

    hash-constructor     ::= '{' [named-field {',' named-field}] '}'

    named-field          ::= string-constant '=>' expression

    array-constructor    ::= '[' [expression {',' expression} ] ']'

    binary-op            ::= '+'  | '-'  | '/'  | '*'  | '%'  | '..'
                           | 'and | 'or' | '>'  | '>=' | '<'  | '<='
                           | '==' | '!='

    unary-op             ::= 'not' | '-'

哇,真多,不是吗?实际上,与 C 等“现实世界”语言相比,这个语法相当小,更不用说 Perl 6 了。不过不用担心,我们不会一次性实现所有内容,而是分步进行。更重要的是,练习部分包含了足够的练习,让你自己学习使用 PCT!这些练习的解答将在几天后发布(但你只需要几个小时就能想出来)。

Squaak 语言的大部分内容都非常简单;if 语句的执行方式完全符合你的预期。当我们讨论语法规则(及其实现)时,会包含语义规范。这样做是为了避免我编写完整的语言手册,这可能需要几页纸。

交互式 Squaak

[编辑 | 编辑源代码]

尽管 Squaak 编译器可以在交互模式下使用,但需要注意一点。当使用 'var' 关键字定义局部变量时,此变量将在任何连续的命令中丢失。该变量仅对同一命令内的其他语句可用(命令是在你按下 Enter 键之前的语句集)。这与 PCT 的代码生成有关,将在以后修复。目前,请记住它不起作用。

让我们开始吧!

[编辑 | 编辑源代码]

在本集的剩余部分,我们将实现语法的基本部分,例如基本数据类型和赋值。在本集结束时,你将能够将简单值赋给(全局)变量。这不算多,但这是非常重要的一步。一旦这些基础到位,你就会注意到添加特定的语法结构只需几分钟。

首先,打开你的编辑器并打开文件 src/Squaak/Grammar.pmsrc/Squaak/Actions.pm。前者使用 Perl 6 规则实现解析器,后者包含解析操作,这些操作在解析阶段执行。

在文件 Grammar.pm 中,你会看到顶级规则,名为“TOP”。它位于……顶部。当调用解析器时,它将从此规则开始(规则只不过是语法类的某种方法)。

当我们生成这种语言(在第一集中)时,定义了一些默认规则。现在我们要进行一些小的更改,足以让我们开始。首先,将 statement 规则更改为

   rule statement {
       <assignment>
       {*}
   }

并添加以下规则

    rule assignment {
        <primary> '=' <expression>
        {*}
    }

    rule primary {
        <identifier>
        {*}
    }

    token identifier {
        <!keyword> <ident>
        {*}
   }

    token keyword {
        ['and'|'catch'|'do'   |'else' |'end' |'for' |'if'
        |'not'|'or'   |'sub'  |'throw'|'try' |'var'|'while']>>
    }

现在,将规则“value”更改为此(重命名为“expression”)

   rule expression {
       | <string_constant> {*}        #= string_constant
       | <integer_constant> {*}       #= integer_constant
   }

将规则“integer”重命名为“integer_constant”,将“quote”重命名为“string_constant”(以更好地匹配我们的语言规范)。

呼,信息量很大!让我们仔细看看一些可能看起来不熟悉的东西。第一个新东西在规则“identifier”中。你看到的是关键字“token”,而不是“rule”关键字。简而言之,token 不会跳过 token 中指定的不同部分之间的空格,而 rule 会跳过。目前,记住如果要匹配不包含任何空格的字符串(例如文字常量和标识符),请使用 token,如果你的字符串包含(并且应该包含)空格(例如 if 语句),请使用 rule 就足够了。我们将以一般意义上使用“rule”一词,它可以指代 token。有关规则和 token 的更多信息(以及第三种类型,称为“regex”),请参阅概要 5。

在 token“identifier”中,第一个子规则称为断言。它断言“identifier”不匹配规则关键字。换句话说,不能将关键字用作标识符。第二个子规则称为“ident”,它是类 PCT::Grammar 中的内置规则,该语法是该类的子类。

在 token“keyword”中,列出了 Squaak 的所有关键字。最后有一个“>>”标记,表示单词边界。如果没有此标记,则诸如“forloop”之类的标识符将被错误地取消资格,因为“for”部分将匹配规则关键字,而“loop”部分将匹配规则“ident”。但是,由于断言<!keyword> 为假(因为可以匹配“for”),因此字符串“forloop”不能作为标识符匹配。所需单词边界的存在可以防止这种情况。

最后一个规则是“expression”。表达式要么是字符串常量,要么是整数常量。无论哪种方式,都会执行一个操作。但是,当执行操作时,它不知道解析器匹配了什么;是字符串常量还是整数常量?当然,可以检查匹配对象,但考虑一下你有 10 个备选方案的情况,然后进行 9 次检查只是为了发现最后一个备选方案被匹配的情况效率低下(并且添加新的备选方案需要你更新此检查)。这就是为什么你会看到以“#=”字符开头的特殊注释。使用此表示法,你可以指定一个键,该键将作为第二个参数传递给操作方法。正如我们将看到的,这使我们能够为诸如 expression 之类的规则编写非常简单高效的操作方法。(请注意,#= 和键名称之间有一个空格)。

测试解析器

[编辑 | 编辑源代码]

在编写任何操作方法之前测试解析器很有用。这可以为你节省大量工作;如果你在编写语法规则后立即编写操作,并且只是在稍后发现必须更新解析器,那么你的操作方法可能也需要更新。在第 2 集中,我们看到了目标命令行选项。为了测试解析器,“parse”目标特别有用。指定此选项时,你的编译器将打印输入字符串的解析树,或打印语法错误。明智的做法是用正确和不正确的输入测试你的解析器,这样你才能确定你的解析器不会接受不应该接受的输入。

然后……行动!

[编辑 | 编辑源代码]

现在我们已经实现了 Squaak 语法的初始版本,是时候实现我们之前提到的解析动作了。这些动作写在名为 src/Squaak/Actions.pm 的文件中。如果你查看此文件中的方法,你会发现这里和那里 Match 对象 ($/) ,或者更确切地说,它的哈希字段(例如 $<statement>)是在标量上下文中通过编写“$( ... )”来评估的。

如概要 5 中所述,在标量上下文中评估 Match 对象会返回其结果对象。通常,结果对象是源文本中匹配的部分,但可以使用特殊的 make 函数将结果对象设置为其他值。

这意味着解析树中的每个节点(一个 Match 对象)也可以保存其 PAST 表示。因此,我们使用 make 函数设置解析树中当前节点的 PAST 表示,然后使用 $( ... ) 运算符从中检索 PAST 表示。

概括来说,Match 对象 ($/) 及其任何子规则(例如 $<statement>)表示解析树;当然,$<statement> 只表示 <statement> 规则匹配的解析树。因此,任何动作方法都可以访问与同名语法规则匹配的解析树,因为 Match 对象始终作为参数传递。在标量上下文中评估解析树会产生 PAST 表示(显然,此 PAST 对象应使用 make 函数设置)。

如果你正在学习本教程,我强烈建议你动手实践,完成练习。记住,学而不练等于没学(或者类似的话 :-)。本周的练习并不难,完成之后,你将实现我们的小型 Squaak 语言的第一部分。

接下来是什么?

[编辑 | 编辑源代码]

在本节中,我们介绍了 Squaak 的完整语法。我们迈出了实现这门语言的第一步。第一个,也是目前唯一的语句类型是赋值语句。我们简要介绍了如何在解析阶段编写调用的动作方法。在下一节中,我们将更仔细地研究不同的 PAST 节点类型,并实现 Squaak 语言的更多部分。一旦我们所有基本部分都到位,添加语句类型将变得相当简单。在此期间,如果您有任何疑问或遇到困难,请随时发表评论或联系我。

本节的练习非常简单,可以帮助你开始实现 Squaak。

问题 1

根据我们在语法规则上所做的名称更改,重命名动作方法的名称。因此,“integer”变为“integer_constant”,“value”变为“expression”,依此类推。

问题 2

查看 statement 语法规则。目前,statement 由一个赋值语句组成。实现动作方法“statement”以检索此赋值语句的结果对象,并使用特殊的 make 函数将其设置为 statement 的结果对象。对 primary 规则执行相同的操作。

解答
   method statement($/) {
       make $( $<assignment> );
   }

请注意,在这一点上,statement 规则没有为每种类型的语句定义不同的 #= 键,因此我们不声明参数 $key。这将在以后更改。

    method primary($/) {
        make $( $<identifier> );
    }
问题 3

编写 identifier 规则的动作方法。作为此“匹配”的结果对象,应设置一个新的 PAST::Var 节点,其名称为匹配对象 ($/) 的字符串表示形式。目前,你可以将作用域设置为“package”。有关 PAST::Var 节点的详细信息,请参阅“pdd26: ast”。

解答
method identifier($/) {
    make PAST::Var.new( :name(~$/),
                        :scope('package'),
                        :node($/) );
}
问题 4

编写 assignment 规则的动作方法。检索“primary”和“expression”的结果对象,并创建一个 PAST::Op 节点,将表达式绑定到 primary。(查看 pdd26 以了解 PAST::Op 节点类型,并了解如何进行此类绑定)。

解答
   method assignment($/) {
       my $lhs := $( $<primary> );
       my $rhs := $( $<expression> );
       $lhs.lvalue(1);
       make PAST::Op.new( $lhs, $rhs,
                          :pasttype('bind'),
                          :node($/) );
   }

请注意,我们在 $lhs 上设置了 lvalue 标志。有关此标志的详细信息,请参阅 PDD26。

问题 5

在脚本或交互模式下运行你的编译器。使用 target 选项查看输入“x = 42”生成的 PIR 代码。

解答
   .namespace
   .sub "_block10"
       new $P11, "Integer"
       assign $P11, 42
       set_global "x", $P11
       .return ($P11)
   .end

子程序中的前两行代码创建了一个对象来存储数字 42,第三行将此数字存储为“x”。PAST 编译器将始终生成一条指令以返回最后一个语句的结果,在本例中为 $P11。

一些说明

[编辑 | 编辑源代码]
  • 帮助!我收到错误消息“no result object”。

这意味着结果对象未正确设置(显而易见!)。确保每个动作方法都被调用(检查每个规则的“{*}”标记),并且该规则存在一个动作方法,并且“make”用于设置相应的 PAST 节点。请注意,并非所有规则都有动作方法,例如“keyword”规则(这样做没有意义)。

  • 在我们构建 Squaak 语法的一部分时,有时会采取捷径,暂时忽略某些规则。例如,你可能已经注意到我们现在忽略了浮点数常量。没关系。当我们需要它们时,将添加这些规则。

参考文献

[编辑 | 编辑源代码]
  • pdd26: ast
  • 概要 5:规则
  • docs/pct/*.pod
华夏公益教科书