Nemerle で Brainfuck の PEG parser を作る


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

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

using System.Collections.Generic;

variant BrainfuckExpression
{
   | MoveRight
   | MoveLeft
   | Increment
   | Decrement
   | Put
   | Get
   | BeginLoop
   | EndLoop
   | Loop { Begin : BeginLoop;
            Body : List[BrainfuckExpression];
            End : EndLoop; }
}

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

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

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 が返ってきます。

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 用の拡張も含まれています。しかし,プロジェクトとして扱えたりエディターでシンタックスハイライトができるくらいで,リファクタリングまわりはまったく使い物にならず,インテリセンスも弱いです。 []