JSONParserWithParseYapp

構文解析をちゃんと学ばないとなー、と前々から思いつつしばらく放置だったけど、なんとなくやる気になったのでやってみる。

ZIGOROu さんの Parse::Yapp ヨチヨチ歩きParse::Yapp の使い方を学びつつ、Rubyソースコード完全解説 第9章 速習yacc でパーサの書き方の基礎がなんとなくわかった気になり、でも実際は自分で書いてみなきゃわからん、ってことで、hakobe さんの Parse::RecDescentでJSONをパース を参考に、Parse::Yapp で JSON パーサを書いてみることにした。

パーサ+スキャナは以下のような感じ。

#!perl
%{
use strict;
use warnings;
%}
%%
JSON       : value
           ;
value      : atom
           | number
           | string
           | '[' array             { return $_[2] }
           | '{' object            { return $_[2] }
           ;
array      : ']'                   { return [] }
           | ',' ']'               { return [] }
           | element array_cdr     { unshift @{$_[2]}, $_[1];return $_[2] }
           ;
array_cdr  : ']'                   { return [] }
           | ',' ']'               { return [] }
           | ',' element array_cdr { unshift @{$_[3]}, $_[2];return $_[3] }
           ;
element    : value
           ;
object     : '}'                   { return {} }
           | ',' '}'               { return {} }
           | member object_cdr     { return { %{ $_[2] }, %{ $_[1] } } }
           ;
object_cdr : '}'                   { return {} }
           | ',' '}'               { return {} }
           | ',' member object_cdr { return { %{ $_[3] }, %{ $_[2] } } }
           ;
member     : key ':' value         { return { $_[1] => $_[3] } }
           ;
key        : string
           | identifier
           ;
%%
sub yyerror {}

sub yylex {
    my ( $self ) = @_;
    my $number = qr/-?(?:0|[1-9]\d*)(?:\.\d+)?(?:[eE][+-]\d+)?/;
    my $double_quoted_string = qr/
            ^"([^\x00-\x1f\"\\]*
            (?:\\(?:[\"\\\/bfnrt]|u[0-9a-fA-F]{4})
            [^\x00-\x1f\"\\]*)*)"
        /xms;
    my $single_quoted_string = qr/
            ^'([^\x00-\x1f\'\\]*
            (?:\\(?:[\'\\\/bfnrt]|u[0-9a-fA-F]{4})
            [^\x00-\x1f\'\\]*)*)'
        /xms;

    for ( $self->YYData->{INPUT} ) {
        s|\s+||;
        s|^($number)||                and return ('number', $1);
        s/^(true|false|null)//        and return ('atom', $1);
        s|^([_a-zA-Z][_a-zA-Z0-9]*)|| and return ('identifier', $1);
        s|$double_quoted_string||     and return ('string', $1);
        s|$single_quoted_string||     and return ('string', $1);
        s|^(.)||                      and return ($1, $1);
    }
    return ('', undef);
}

sub run {
    my $self = shift;
    return $self->YYParse( yylex => \&yylex, yyerror => \&yyerror );
}

こいつを yapp コマンドにかけるとパーサモジュールができる。

#!sh
$ yapp -m JSONParser json.yp

このモジュールを使ってパースしてみる。

#!perl
#!/usr/bin/perl

use strict;
use warnings;
use Test::More qw(no_plan);
use JSONParser;

my $parser = JSONParser->new;
$parser->YYData->{INPUT} = q{[
        'Mercury',
        "Venus",
        ["Earth", ['Moon']],
        ['Mars', ["Phobos", 'Deimos']],
        ['Jupiter',["Io", 'Europa', "Ganymede", 'Callisto']]
]};
my $result = $parser->run;

is_deeply($result, ['Mercury', "Venus", ["Earth", ['Moon']], ['Mars', ["Phobos", 'Deimos']], ['Jupiter',["Io", 'Europa', "Ganymede", 'Callisto']]]);

うん、なんとなくわかってきた気がする。

Rubyソースコード完全解説 第9章 速習yacc の中で「客観的に見てもこの分野に関しては一番わかりやすい本」とオススメされていたRubyを256倍使うための本 無道編 も買ってみた。