Nemerle で Brainfuck の PEG parser を作る


Nemerle には標準で PEG のパーサージェネレーターのライブラリが付属しています。それを使って Brainfuck の単純なパーサーを書いてみます。

まず Brainfuck の構文を表す variant[A] を定義します。

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
using System.Collections.Generic;
 
variant BrainfuckExpression
{
   | MoveRight
   | MoveLeft
   | Increment
   | Decrement
   | Put
   | Get
   | BeginLoop
   | EndLoop
   | Loop { Begin : BeginLoop;
            Body : List[BrainfuckExpression];
            End : EndLoop; }
}

次にパーサーです。 PegGrammerAttribute に直接 PEG の文法を放り込んでやり,それぞれの実装をクラス内で定義やるだけです。文法と実装がマッチしていなければコンパイルエラーになります。

簡単のために,改行を含む空白文字やコメントを一切許容しない,もっとも単純な文法を定義します。

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
using System;
using System.Collections.Generic;
using Nemerle.Peg;
 
[PegGrammar(Options = EmitDebugSources, Commands,
grammar
{
   SingleCommand : BrainfuckExpression = '>' / '<' / '+' / '-' / ',' / '.';
   BeginLoop : BrainfuckExpression = '[';
   EndLoop : BrainfuckExpression = ']';
   Loop : BrainfuckExpression = BeginLoop Commands* EndLoop;
   Commands : List[BrainfuckExpression] = (Loop / SingleCommand)*;
})]
class BrainfuckParser
{
   SingleCommand(token : NToken) : BrainfuckExpression
   {
      match (this._parsingSource.OriginalText[token.StartPos])
      {
         | '>' => BrainfuckExpression.MoveRight();
         | '<' => BrainfuckExpression.MoveLeft();
         | '+' => BrainfuckExpression.Increment();
         | '-' => BrainfuckExpression.Decrement();
         | ',' => BrainfuckExpression.Get();
         | '.' => BrainfuckExpression.Put();
         | _ => throw NotSupportedException();
      }
   }
 
   BeginLoop(_ : NToken) : BrainfuckExpression
   {
      BrainfuckExpression.BeginLoop();
   }
 
   EndLoop(_ : NToken) : BrainfuckExpression
   {
      BrainfuckExpression.EndLoop();
   }
 
   Loop(begin : BrainfuckExpression, body : List[List[BrainfuckExpression]], end : BrainfuckExpression) : BrainfuckExpression.Loop
   {
      BrainfuckExpression.Loop(begin :> BrainfuckExpression.BeginLoop,
                               body[0],
                               end :> BrainfuckExpression.EndLoop);
   }
 
   Commands(commands : List[BrainfuckExpression]) : List[BrainfuckExpression]
   {
      commands;
   }
}

これだけで Brainfuck のパーサーが完成です。小難しいことはすべて勝手にやってくれます。なお,コンパイルする際には, Nemerle.Peg.dll と Nemerle.Peg.Macros.dll を参照に加える必要があります。

実際に使う際は,普通にインスタンスを生成して Parse メソッドを呼ぶだけです。パースに成功すれば option.Some が,失敗すれば option.None が返ってきます。

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
using System;
using System.Console;
using System.ComponentModel;
using Nemerle.Assertions;
 
module Program
{
   Main() : void
   {
      // Hello, world!
      def source = "+++++++++[>++++++++>+++++++++++>+++++<<<-]>.>++.+++++++..+++.>-.------------.<++++++++.--------.+++.------.--------.>+.";
      _ = Execute(source);
   }
 
   Execute(source : string, memorySize = 0x1000 : int) : array[char]
      requires source != null
         otherwise throw ArgumentNullException("source")
      requires memorySize > 0
         otherwise throw ArgumentOutOfRangeException("memorySize")
      ensures value != null
   {
      def parser = BrainfuckParser();
      def memory = array (memorySize);
      mutable pointer = 0;
      match (parser.Parse(source))
      {
         | option.Some(commands) =>
              foreach (c in commands)
              {
                 Proceed(c, memory, ref pointer);
              }
         | option.None => throw FormatException();
      }
      memory;
   }
 
   Proceed(command : BrainfuckExpression, memory : array[char], pointer : ref int) : void
      requires memory != null
         otherwise throw ArgumentNullException("memory")
      requires 0 <= pointer && pointer < memory.Length
         otherwise throw ArgumentOutOfRangeException("pointer")
      ensures 0 <= pointer && pointer < memory.Length
         otherwise throw OutOfMemoryException()
   {
      match (command)
      {
         | BrainfuckExpression.MoveRight => ++pointer;
         | BrainfuckExpression.MoveLeft => --pointer;
         | BrainfuckExpression.Increment => ++memory[pointer];
         | BrainfuckExpression.Decrement => --memory[pointer];
         | BrainfuckExpression.Get => memory[pointer] = ReadKey(true).KeyChar;
         | BrainfuckExpression.Put => Write(memory[pointer]);
         | BrainfuckExpression.Loop as loop =>
              while (memory[pointer] != 0)
              {
                 foreach (c in loop.Body)
                 {
                    Proceed(c, memory, ref pointer);
                 }
              }
         | _ => throw InvalidEnumArgumentException();
      }
   }
}

このコードだけでも, Nemerle の特徴が随所に表れている気がします。

感想

Nemerle は Java 界隈で言うところの Scala 的な位置づけの言語だと思います。 C# のようなメジャーな言語とのギャップも小さく,かつ関数型の強力な表現力も持っています。マクロによる構文拡張はチート級です[B]

.NET の言語としては Visual Studio によるサポートが弱いのがネックですが[C],とても書きやすい言語だと思います。

ソースコード

ソースコードを Bitbucket にアップロードしています

参考文献

脚注

  1. F# の判別共用体に相当するものだと思います。 []
  2. 例えばコード中の foreachwhile は実はマクロです。 []
  3. Nemerle 1.1 RC では Visual Studio 2010 用の拡張も含まれています。しかし,プロジェクトとして扱えたりエディターでシンタックスハイライトができるくらいで,リファクタリングまわりはまったく使い物にならず,インテリセンスも弱いです。 []