TechCraft – エンジニアのためのスキルアップメモ

エンジニアのスキルアップを少しでも加速する技術ブログ

Brainfuck コンパイラの自作 第3回

Brainfuck コンパイラの自作 第3回 - 構文解析(Parsing)の実装

1. はじめに

前回は字句解析(Lexing)を実装し、Brainfuck のコードをトークンのリストに変換しました。
今回は 構文解析(Parsing) を行い、特に [] の対応関係を管理する ジャンプテーブル を作成します。


2. 構文解析(Parsing)とは?

構文解析(Parsing)は、字句解析で得たトークンを解析し、文法的な誤りを検出・管理するプロセス です。

Brainfuck における構文解析

Brainfuck構文解析では、特に ループ構造([ ])の対応関係 を管理することが重要です。

処理内容

  1. トークンリストの読み取り
  2. [] のペアを管理するジャンプテーブルの作成
  3. エラー処理(対応しない [ ] があれば警告)

3. 構文解析の実装

プロジェクト構成

brainfuck_compiler/
├── main.cpp         (エントリポイント)
├── lexer.cpp        (字句解析)
├── lexer.hpp
├── parser.cpp       (構文解析)
├── parser.hpp
├── Makefile

1. parser.hpp(Parser クラスの定義)

#ifndef PARSER_HPP
#define PARSER_HPP

#include <vector>
#include <unordered_map>

class Parser {
public:
    explicit Parser(const std::vector<char>& tokens);
    bool parse();  // 構文解析を実行
    std::unordered_map<int, int> getJumpTable() const;

private:
    std::vector<char> tokens;
    std::unordered_map<int, int> jumpTable;  // `[` `]` のペア
};

#endif

2. parser.cpp(Parser クラスの実装)

#include "parser.hpp"
#include <stack>
#include <iostream>

Parser::Parser(const std::vector<char>& tokens) : tokens(tokens) {}

// 構文解析(ループの対応関係をチェック)
bool Parser::parse() {
    std::stack<int> loopStack;

    for (int i = 0; i < tokens.size(); i++) {
        if (tokens[i] == '[') {
            loopStack.push(i);
        } else if (tokens[i] == ']') {
            if (loopStack.empty()) {
                std::cerr << "構文エラー: 余分な ']' が " << i << " 番目にあります。\n";
                return false;
            }
            int openPos = loopStack.top();
            loopStack.pop();
            jumpTable[openPos] = i;
            jumpTable[i] = openPos;
        }
    }

    if (!loopStack.empty()) {
        std::cerr << "構文エラー: 対応しない '[' があります。\n";
        return false;
    }

    return true;
}

// ジャンプテーブルを取得
std::unordered_map<int, int> Parser::getJumpTable() const {
    return jumpTable;
}

3. main.cpp(Parser のテスト)

#include <iostream>
#include <fstream>
#include "lexer.hpp"
#include "parser.hpp"

int main(int argc, char* argv[]) {
    if (argc < 2) {
        std::cerr << "使い方: " << argv[0] << " <brainfuckファイル>\n";
        return 1;
    }
    
    std::ifstream file(argv[1]);
    if (!file) {
        std::cerr << "ファイルを開けませんでした: " << argv[1] << "\n";
        return 1;
    }
    
    std::string code((std::istreambuf_iterator<char>(file)), std::istreambuf_iterator<char>());
    
    // 字句解析
    Lexer lexer(code);
    std::vector<char> tokens = lexer.tokenize();

    // 構文解析
    Parser parser(tokens);
    if (!parser.parse()) {
        std::cerr << "構文解析に失敗しました。\n";
        return 1;
    }

    // ジャンプテーブルの表示
    std::unordered_map<int, int> jumpTable = parser.getJumpTable();
    std::cout << "ジャンプテーブル:\n";
    for (const auto& pair : jumpTable) {
        std::cout << pair.first << " -> " << pair.second << "\n";
    }

    return 0;
}

4. 実行方法

1. コンパイル

g++ main.cpp lexer.cpp parser.cpp -o bf_parser

2. テスト用 Brainfuck コード(loop_test.bf

+[->++<].

このコードは以下の処理を行います: 1. + : 1 をメモリにセット 2. [ : ループ開始 3. - : 10 にする(ループ終了) 4. > : 次のセルへ移動 5. ++ : 2 をセット 6. < : 先頭セルに戻る 7. ] : ループ終了 8. . : ASCII コード 2 (STX) を出力

3. 実行

./bf_parser loop_test.bf

4. 出力結果

ジャンプテーブル:
1 -> 6
6 -> 1

この結果から、[ が 1 番目、対応する ] が 6 番目にあることが分かります。


5. まとめと次回の予定

今回の成果

構文解析(Parsing)の基本を実装
[ ] の対応関係を管理するジャンプテーブルを作成
構文エラーを検出できるようにした

次回予定

次回は、C 言語コードの生成 を行い、Brainfuck のコードを C に変換し、コンパイルして実行できるようにします。 - > < + - . , の C 変換 - [ ] のループ処理を C コードで実装 - generator.cpp の作成


次に学ぶべきこと

  • Brainfuck のコード変換
  • C 言語でのメモリ管理
  • シンプルなコード生成のテクニック

参考リンク - Brainfuck Wikipedia - コンパイラ構築の基本 - C++ の std::unordered_map

次回もお楽しみに!