Raku Grammars, Part 1

Raku Grammars, Part 1

Raku 语言内置了对 grammar 的支持。您可以将 grammars 视为众所周知的正则表达式和诸如 yaccbison 等实用程序或更复杂的 grammar 工具(如ANTLR)的组合。所有这些 - 词法分析器,语法分析器和语义处理 - 通常是编译器的独立部分,在 Raku 中它们都是内置的,并且可以通过全新的 Raku 安装 进行开箱即用。

要感受 grammar 的力量,Raku 自己的 grammar 就是用 Raku 庞大的 grammar 类 Raku::Grammar写成的就足以说明了。

在本文中,我将通过几个例子来说明 grammar 的基础知识。所有必需的语言结构将在我们进行的时候进行解释。

解析数字

在你开始思考用户可以使用不同格式的数字,包括负数,浮点数,科学记数法中的数字,特殊形式的数字(如C的长整数)之前,解析数字似乎是一项简单的任务。

让我们从最简单的形式开始:一个数字作为数字序列。例如,1,42,123 或 1000. Raku 中的 grammar 是一种特殊的类,它有自己的关键字。grammar 的第一个 rule 必须(默认情况下)称为 TOP,以下是解析第一组数字的完整程序:

grammar N {
    token TOP {
        <digit>+
    }
}

for <1 42 123 1000> -> $n {
    say N.parse($n) ?? "OK $n" !! "NOT OK $n";
}

当调用 N grammar 的 parse 方法时,Perl 会尝试将给定的字符串与 TOP 方法进行匹配。在我们的例子中,这是一个 token,这意味着字符串不能在 token 的各个部分之间包含任何可选空格。 TOP 只在消耗整个字符串时才成功,因此不需要使用显式锚点 ^$ 来绑定 token 的边缘。

与正则表达式一样,token 和 rule 可以包含其他 tokens,rules 或由其名称引用的正则表达式。在我们的第一个例子中,TOP token 需要与数字匹配的 digit 内置方法。 + 量词与标准的 Perl 5 正则表达式中的量词相同:它允许前一个原子重复一次或多次。

我们简单的 grammar 到目前为止只能解析无符号整数。任何负数都不能被解析:

OK 1
OK 42
OK 123
OK 1000
NOT OK -3

让我们更新 grammar 并引入可选符号的 token,它可以是 +-:

grammar N {
    token TOP {
        ['+' | '-']?
        <digit>+
    }
}

在这里,方括号将两个选项组合在一起:'+' | '-'? 量词要求只有一个这样的字符,或者没有。在 Raku 中,方括号只创建一个分组,但不捕获它的内容。还要注意,+- 都被引号引住了,因为 Raku 将任何非字母数字字符视为特殊字符,除非它被引号引起来或被转义。

下一步是添加对浮点的支持。临时解决方案可以创建包含数字和 . 符号的字符类。但这是完全错误的,例如,带有两个点的字符串(如 3..14)通过此过滤器。所以,做点不一样的事情:

grammar N {
    token TOP {
        ['+' | '-']?
        <digit>+
        ['.' <digit>+]?
    }
}

该 grammar 现在允许包含一个可选的点和另一个数字序列,并且在数字是整数或包含明确的小数部分(例如 3.14)时可以很好地工作。对于其中一个部分丢失的那些数字,则失败:3..14

通过使用量词使得零件可选的尝试使得 grammar 变得难以阅读并且容易出错。例如,以下 token 匹配上述所有数字,还有单个 .:

grammar N {
    token TOP {
        ['+' | '-']?
        <digit>*
        ['.' <digit>*]?
    }
}

现在是时候引入更多的 token 了。将数字序列分解为单独的 token 并明确列出所有变体:

grammar N {
    token TOP {
        <sign>?
        <value>
    }
    token sign {
        '+' | '-'
    }
    token digits {
        <digit>+
    }
    token value {
        | <digits> '.' <digits>
        | '.' <digits>
        | <digits> '.'
        | <digits>
    }
}

value token 封装了这些变体:它包含可接受数字的四种替代表示。垂直条 | 分割它们。为了统一起见,允许在第一个备选分支之前添加一个附加的竖线 |,以便所有这些都通过简单的 ASCII 艺术强调。

当前的 grammar 已经足够聪明以拒绝单个点号:

OK 1
OK 42
OK 123
OK 1000
OK -3
OK 3.14
OK 3.
OK .14
NOT OK .

最后一步是以科学记数法支持数字。增加另一个备选分支易如反掌:

grammar N {
    token TOP {
        <sign>?
        [
            | <value> <exp> <sign>? <digits>
            | <value>
        ]
    }
    token sign {
        '+' | '-'
    }
    token exp {
        'e' | 'E'
    }
    token digits {
        <digit>+
    }
    token value {
        | <digits> '.' <digits>
        | '.' <digits>
        | <digits> '.'
        | <digits>
    }
}

用以下例子测试我们的 grammar:

for <1 42 123 1000 -3
     3.14 3. .14 .
     -3.14 -3. -.14
     10E2 10e2 -10e2 -1.2e3 10e-3 -10e-3 -10.2e-33
    > -> $n {
    say N.parse($n) ?? "OK $n" !! "NOT OK $n";
}

一切正常。但在 Perl 中,数字中也允许有下划线! 有一个合适的语法,增加对此的支持是容易的; 应只修改 digits token:

token digits {
    <digit>+ ['_' <digit>+]?
}

不遵循该 rule 的字符串仍然会被忽略:

OK 100_000
NOT OK _1
NOT OK 1_
NOT OK 1__0

结论

只需几个简单的步骤,我们就可以创建一个能够理解不同格式数字的 grammar。作为练习,您可以添加前缀 0x0b0o(十六进制,二进制和八进制)和后缀(如C中的 1000L)的支持。Grammars 只被用来检查数字格式的有效性,并且它们的能力并没有在那里结束。在 Raku 中,您可以将 actions 添加到 grammar 中; 这些是在相应的 rule 或 token 已成功匹配时执行的代码块。但那是另一天的故事。

Raku 

comments powered by Disqus