Perl 6 - Grammars 生成 grammars

现在你可能已经习惯了 Perl 6 中到处出现的前缀"meta"。Metaclasses, Metaobjects, Metaoperators, 还有迷一般的 Meta-Object 协议。听起来一点也不可怕, 你都见过了不是吗?今天, 在 Perl 6 Advent Calendar 上, 我们将进行完全的 meta 化(full meta)。我们将拥有能解析 grammars 的 grammars, 然后生成将用于解析 grammars 的 grammars。

Grammars 无疑是 Perl 6 的杀手级功能。我们拥有了正则表达式曾经没有的东西: 可读性、可组合性当然还有解析 Perl 6 自身的能力。— 如果这不能展示它的强大, 那我不知道什么能够!

为预定义好的 grammars(例如以 Bachus-Naur 形式)写解析器总是有点无趣, 几乎和复制粘贴一样。如果你曾经坐下来重头开始写一个解析器(或者期间温习一遍那本优秀的"让我们构建一个编译器"图书), 你可能会意识到模式是如此相似:从你的 grammar 中拿出单个 rule, 为它写一个子例程, 让它调用(可能是递归的)其它类似的为其它 grmmars rules 定义的子例程, 清洗, 重复。现在我们有了Perl 6 Grammars! 在这个新世界中, 我们不必为每个 token 写上子例程来完成工作了。 现在我们写 grammar 类, 里面放上 tokensrulesregexes 标志。在标志里写正则表达式(或代码)并引用(可能是递归的) Perl 6 gramamr 中的其它标志。如果你曾经使用过这些东西, 你肯定会意识到 Perl 6 中的 gramamrs 是多么的方便。

但是假如我们已经有了一个 grammar, 例如之前提到过的 BNF? 我们所做的就是小心地把已经存在的 grammar(实际上在我们头脑中解析它)重新键入到一个新的 Perl 6 Grammar 中以代表同样的一个东西, 但是那确实有一个可作为可执行代码的优势。对大多数人来说, 那都不是事儿。我们不是普通人, 我们是程序员。我们拥有资源。它们会让这些 Grammars 变得有意义。

绝妙的是 Perl 6 gramamrs 和语言的其它元素没什么两样。Grammars 就像类那样也是头等公民, 可以内省, 扩展。实际上, 你可以查看编译器源代码自身, 你会注意到Grammars 就是一种特定种类的类。它们遵守和类一样的规则, 允许我们就地创建 gramamrs, 就地给 gramamrs 添加 tokens, 最终完结这个 gramamr 以拥有一个合适的能实例化的类对象。现在既然我们能解析 BNF gramamrs(因为它们就是普通的文本)并从代码中创建 Perl 6 grammars, 让我们把这些片段放在一起并写点能手动把 BNF gramamr 转化为 Perl 6 grammar 的东西。

解析 BNF grammar 的 grammar



grammar Grammar::BNF {
    token TOP { \s* <rule>+ \s* }

    token rule {
        <opt-ws> '<' <rule-name> '>' <opt-ws> '::=' <opt-ws> <expression> <line-end>
    }

    token expression {
        <list-of-terms> +% [\s* '|' <opt-ws>]
    }

    token term {
        <literal> | '<' <rule-name> '>'
    }

    token list-of-terms { <term> +% <opt-ws>                }
    token rule-name     { <-[>]>+                           }
    token opt-ws        { \h*                               }
    token line-end      { [ <opt-ws> \n ]+                  }
    token literal       { '"' <-["]>* '"' | "'" <-[']>* "'" }

    ...
}

最上层的 3 个 tokens 发生了有意思的事情。rule 是 BNF grammar 的核心构造块: 一个 <symbol> ::= <expression> 块儿, 后面跟着一个换行符。整个 grammar 就是一列 rules。每个表达式是一列项、或可能的和它们的备选分支。每个项要么是一个字面值, 或一个由尖括号包围的标志名。足够了! 那涵盖了解析部分。让我们看一下生成自身。我们的确有一种"为 grammar 中的每个 token 做某事"的机制, 以 Actions的形式, 让我们继续并使用它:


my class Actions {
    has $.name = 'BNFGrammar';
    method TOP($/) {
        my $grmr := Metamodel::GrammarHOW.new_type(:$.name);
        $grmr.^add_method('TOP',
            EVAL 'token { <' ~ $<rule>[0].ast.key ~ '> }');
        for $<rule>.map(*.ast) -> $rule {
            $grmr.^add_method($rule.key, $rule.value);
        }
        $grmr.^compose;
        make $grmr;
    }

    method expression($/) {
        make EVAL 'token { ' ~ ~$/ ~ ' }';
    }

    method rule($/) {
        make ~$<rule-name> => $<expression>.ast;
    }
}

TOP方法毫无疑问是最魔幻和最恐怖的, 所以擒贼先擒王, 其它小喽啰就无关紧要了。基本上, TOP那儿发生了三件事:

1、我们创建了一个新的 grammar, 作为一个新的 Perl 6 类型

2、我们使用 ^add_method方法为 grammar 添加 tokens

3、我们使用 ^compose方法定型该 grammar

虽然 Perl 6 指定名为 TOP 的 token 是解析开始的地方, 在 BNF 中第一个 rule 总是开始点。为了彼此适应, 我们精巧地制作了一个假的 TOP token, 它正是调用了 BNF grammar 中指定的第一个 rule。不可避免地, 恐怖又令人失望的 EVAL 引起了我们的注意, 就像它说了"这儿发生了可怕的事情" 一样。它那样说并不是完全错误的, 但是因为我们没有其它程序化构建单独正则的方法, 我们不得不�接受这点不适。

TOP之后我们继续为我们的 grammar 添加 BNF rules 的剩余部分, 这一次保留它们原来的名字, 然后 ^compose整个东西, 最后让它(make)成为解析的结果: 一个做好的解析类。

expression 方法中我们把解析过的 BNF 元素粘贴到一块以产生合法的 Perl 6 代码。这变得特别容易, 因为那俩个单独的标志带有空格, 使用管道符号来轮试备选分支, 并使用尖括号包围标志名。目前为止, 一个 rule 看起来像这样:


<foo> ::= 'bar' | <baz>

我们求值(EVAL)的 Perl 6 代码变为:


token { 'bar' | <baz> }

因为我们已经在我们代码的 grammar 部分检测我们解析的 BNF 是正确的, 没有什么能够阻止我们传递解析整个表达式字面值到我们的代码中并使用一个 token { }来包裹它, 所以让我们继续。

最后, 对于我们解析的每一个 BNF rule, 我们产生了一个很不错的 Pair, 所以我们的 TOP 方法很愉快地处理它们中的每个。

看起来我们好像在这儿结束了, 但是仅仅是为了方便使用者, 让我们写一个更好的方法, 接收一个 BNF grammar, 并为我们生成一个准备好使用的类型对象。我们记得, grammars 就是类, 所以我们没有什么能阻止我们直接为我们的 gramamr 添加它:


grammar Grammar::BNF {
    ...

    method generate($source, :$name = 'BNFGrammar') {
        my $actions = Actions.new(:$name);
        my $ret = self.new.parse($source, :$actions).ast;
        return $ret.WHAT;
    }
}

这儿看起来很不错! 在你开始往你自己的项目中复制粘贴所有这些之前, 记得 Grammar::BNF 是一个可在 Perl 6 Module Ecosystem获得的 Perl 6 模块, 使用你喜欢的模块管理器安装。

假设你确实花费时间查看了开头的 post, 你可能会记得我许诺过我们将有 grammars(第一条)来解析 grammars(第二条), 然后生成 grammars(第三条), 使用生成的 grammars 来解析 grammars(第四条)。目前为止, 我们已经看到过 BNF::Grammar grammar(那是第一条), 并解析一个 BNF grammar(那是第二条), 以类对象的形式来生成 Perl 6 grammar(第三条)。 就这些。我们仍旧缺乏最后一部分, 使用整个东西来解析 grammars。 我们只完成了 75% 的 meta化, 今天足够了。为什么现在停止? 为什么不拿一个 BNF grammar , 使用 Perl 6 grammar 来解析 grammar, 使用 Perl 6 BNF grammar 的结果来解析我们原来的 BNF Grammar? 那不是很好吗? 是的, 那很好, 我们只是留了一个练习给你。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 220,458评论 6 513
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 94,030评论 3 396
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 166,879评论 0 358
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 59,278评论 1 295
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 68,296评论 6 397
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 52,019评论 1 308
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 40,633评论 3 420
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 39,541评论 0 276
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 46,068评论 1 319
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 38,181评论 3 340
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 40,318评论 1 352
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 35,991评论 5 347
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 41,670评论 3 331
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 32,183评论 0 23
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 33,302评论 1 272
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 48,655评论 3 375
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 45,327评论 2 358

推荐阅读更多精彩内容