C++でインタープリタパターンを使った文法解析の実装方法

C++でインタープリタパターンを利用した文法解析の概要と重要性について説明します。インタープリタパターンは、プログラミング言語の文法を解析し、特定の処理を実行するためのデザインパターンの一つです。このパターンを用いることで、コードの可読性や保守性が向上し、複雑な文法ルールを簡潔に定義できます。本記事では、C++でインタープリタパターンを使用して文法解析を行う方法について詳しく解説し、実際のコード例を交えてその効果を確認します。

目次

インタープリタパターンの基本概念

インタープリタパターンは、文法規則をクラスとして表現し、それらのクラスを使って文を解析および実行するデザインパターンです。このパターンの利点には以下の点があります。

コードの可読性向上

各文法規則を個別のクラスとして定義するため、コードの構造が明確になり可読性が向上します。

柔軟な拡張性

新しい文法規則を追加する際に、既存のコードに大きな変更を加えることなく対応できるため、拡張性が高いです。

再利用性

一度定義した文法規則は他のプロジェクトでも再利用可能であり、開発の効率化が図れます。

インタープリタパターンは特に、構文解析や評価器など、複雑な文法ルールを扱うシステムで有用です。

文法解析の基礎

文法解析は、入力された文字列を特定の文法ルールに基づいて解析し、構文木(AST:Abstract Syntax Tree)を生成するプロセスです。この構文木は、プログラムの構造をツリー形式で表現したもので、各ノードが文法要素を表します。

字句解析と構文解析

文法解析は通常、字句解析(Lexer)と構文解析(Parser)の二つのステップに分かれます。

字句解析

字句解析は、入力文字列をトークンと呼ばれる意味のある最小単位に分割します。例えば、「int x = 10;」というコードは、「int」、「x」、「=」、「10」、「;」のようなトークンに分解されます。

構文解析

構文解析は、字句解析で得られたトークンをもとに、文法規則に従って構文木を構築します。この過程では、トークンが適切に並んでいるかどうかを確認し、不適切な場合はエラーを報告します。

C++での実装例

C++で文法解析を実装するためには、まず字句解析を行うクラスを作成し、その後構文解析を行うクラスを定義します。以下は、基本的な字句解析クラスの例です。

#include <iostream>
#include <vector>
#include <string>
#include <regex>

class Lexer {
public:
    Lexer(const std::string& source) : source(source), position(0) {}

    std::vector<std::string> tokenize() {
        std::vector<std::string> tokens;
        std::regex tokenPattern(R"(\s*(int|x|=|10|;))");
        std::smatch match;

        while (std::regex_search(source.substr(position), match, tokenPattern)) {
            tokens.push_back(match.str(1));
            position += match.position() + match.length();
        }

        return tokens;
    }

private:
    std::string source;
    size_t position;
};

int main() {
    Lexer lexer("int x = 10;");
    auto tokens = lexer.tokenize();

    for (const auto& token : tokens) {
        std::cout << token << std::endl;
    }

    return 0;
}

この例では、入力文字列をトークンに分解する字句解析器を実装しています。次のステップでは、これらのトークンを使って構文木を構築する方法を解説します。

インタープリタパターンのクラス設計

インタープリタパターンを効果的に実装するためには、文法規則を表現するクラスの設計が重要です。ここでは、基本的な文法規則をクラスとしてどのように定義するかを説明します。

抽象構文木(AST)ノードの定義

まず、構文木の各ノードを表すための抽象クラスを定義します。これにより、具体的な文法規則クラスはこの抽象クラスを継承して実装します。

class ASTNode {
public:
    virtual ~ASTNode() = default;
    virtual void interpret() const = 0;
};

具体的な文法規則クラスの実装

次に、具体的な文法規則を表すクラスを実装します。例えば、変数宣言や代入文を表現するクラスを以下のように定義します。

変数宣言クラス

class VariableDeclaration : public ASTNode {
public:
    VariableDeclaration(const std::string& type, const std::string& name)
        : type(type), name(name) {}

    void interpret() const override {
        std::cout << "Declare variable of type " << type << " with name " << name << std::endl;
    }

private:
    std::string type;
    std::string name;
};

代入文クラス

class Assignment : public ASTNode {
public:
    Assignment(const std::string& variable, const std::string& value)
        : variable(variable), value(value) {}

    void interpret() const override {
        std::cout << "Assign value " << value << " to variable " << variable << std::endl;
    }

private:
    std::string variable;
    std::string value;
};

構文解析クラスの実装

次に、トークン列を入力として受け取り、構文木を構築する構文解析クラスを実装します。

class Parser {
public:
    explicit Parser(const std::vector<std::string>& tokens) : tokens(tokens), position(0) {}

    std::unique_ptr<ASTNode> parse() {
        if (tokens[position] == "int") {
            return parseVariableDeclaration();
        } else if (tokens[position + 1] == "=") {
            return parseAssignment();
        }
        return nullptr;
    }

private:
    std::unique_ptr<ASTNode> parseVariableDeclaration() {
        std::string type = tokens[position++];
        std::string name = tokens[position++];
        position++; // skip ';'
        return std::make_unique<VariableDeclaration>(type, name);
    }

    std::unique_ptr<ASTNode> parseAssignment() {
        std::string variable = tokens[position++];
        position++; // skip '='
        std::string value = tokens[position++];
        position++; // skip ';'
        return std::make_unique<Assignment>(variable, value);
    }

    std::vector<std::string> tokens;
    size_t position;
};

この構文解析クラスでは、トークン列を解析して適切なASTノードを生成します。これにより、インタープリタパターンを用いた文法解析の基礎が完成します。次に、具体的な文法ルールの定義方法を説明します。

基本的な文法ルールの定義

基本的な文法ルールを定義することで、C++でインタープリタパターンを使った文法解析を実装するための土台を作ります。ここでは、変数宣言と代入文を例に、文法ルールの定義方法を説明します。

文法ルールの概要

文法ルールは、プログラムの文をどのように構成するかを決定する規則です。例えば、変数宣言は「型 名;」、代入文は「名 = 値;」という形式で記述されます。

変数宣言の文法ルール

変数宣言の文法ルールを定義するためには、型と変数名のトークンを解析し、それらを表現するASTノードを生成します。

class VariableDeclarationRule {
public:
    std::unique_ptr<ASTNode> parse(const std::vector<std::string>& tokens, size_t& position) {
        if (tokens[position] == "int") {
            std::string type = tokens[position++];
            std::string name = tokens[position++];
            position++; // skip ';'
            return std::make_unique<VariableDeclaration>(type, name);
        }
        return nullptr;
    }
};

代入文の文法ルール

代入文の文法ルールも同様に、変数名と値のトークンを解析し、対応するASTノードを生成します。

class AssignmentRule {
public:
    std::unique_ptr<ASTNode> parse(const std::vector<std::string>& tokens, size_t& position) {
        std::string variable = tokens[position++];
        position++; // skip '='
        std::string value = tokens[position++];
        position++; // skip ';'
        return std::make_unique<Assignment>(variable, value);
    }
};

文法ルールの適用

構文解析クラスで文法ルールを適用し、トークンを解析してASTノードを生成します。

class Parser {
public:
    explicit Parser(const std::vector<std::string>& tokens) : tokens(tokens), position(0) {}

    std::unique_ptr<ASTNode> parse() {
        VariableDeclarationRule varDeclRule;
        AssignmentRule assignmentRule;

        if (tokens[position] == "int") {
            return varDeclRule.parse(tokens, position);
        } else if (tokens[position + 1] == "=") {
            return assignmentRule.parse(tokens, position);
        }
        return nullptr;
    }

private:
    std::vector<std::string> tokens;
    size_t position;
};

これにより、基本的な変数宣言と代入文の解析が可能になります。次に、文法解析ツールのセットアップ方法と必要なライブラリの紹介を行います。

文法解析ツールのセットアップ

C++でインタープリタパターンを利用した文法解析を実装するために必要なツールとライブラリのセットアップ方法を紹介します。これらのツールを使用することで、効率的に文法解析を行うことができます。

開発環境の準備

まず、C++の開発環境を整えます。以下のツールが必要です。

コンパイラ

最新のC++標準に対応したコンパイラ(例:GCC、Clang、MSVC)を使用します。

ビルドツール

CMakeやMakeなどのビルドツールを使用してプロジェクトを管理します。

IDE

Visual Studio、CLion、VSCodeなどの統合開発環境(IDE)を使用すると、コードの補完やデバッグが容易になります。

必要なライブラリの導入

文法解析を効率的に行うために、以下のライブラリを導入します。

Boost.LexicalCast

トークンを適切な型に変換するために使用します。

#include <boost/lexical_cast.hpp>

Boost.Regex

正規表現を使用して字句解析を行います。

#include <boost/regex.hpp>

プロジェクトのセットアップ

CMakeを使用してプロジェクトをセットアップする例を示します。

CMakeLists.txtの作成

以下の内容でCMakeLists.txtを作成し、プロジェクトのビルド設定を行います。

cmake_minimum_required(VERSION 3.10)
project(InterpreterPattern)

set(CMAKE_CXX_STANDARD 17)

find_package(Boost REQUIRED COMPONENTS regex lexical_cast)

include_directories(${Boost_INCLUDE_DIRS})

add_executable(InterpreterPattern main.cpp)
target_link_libraries(InterpreterPattern ${Boost_LIBRARIES})

ソースコードの配置

プロジェクトディレクトリにmain.cppなどのソースコードファイルを配置します。先ほどの字句解析や構文解析のクラス定義を含めたファイルを用意します。

main.cppの例

#include <iostream>
#include <vector>
#include <string>
#include <boost/regex.hpp>
#include <boost/lexical_cast.hpp>

// Lexer, ASTNode, VariableDeclaration, Assignment, Parserなどのクラス定義をここに記述

int main() {
    std::string source = "int x = 10;";
    Lexer lexer(source);
    auto tokens = lexer.tokenize();

    Parser parser(tokens);
    auto ast = parser.parse();

    if (ast) {
        ast->interpret();
    }

    return 0;
}

プロジェクトのビルドと実行

CMakeを使用してプロジェクトをビルドし、実行します。

mkdir build
cd build
cmake ..
make
./InterpreterPattern

これで、文法解析ツールのセットアップが完了し、基本的な文法解析の実装が動作することを確認できます。次に、簡単な文法解析の実装例を示します。

簡単な文法解析の実装例

ここでは、前章でセットアップした環境を使用して、C++で簡単な文法解析の実装例を示します。実際のコードを交えて、変数宣言と代入文の解析を行います。

字句解析の実装

まず、字句解析クラスを実装します。これにより、入力文字列をトークンに分割します。

#include <iostream>
#include <vector>
#include <string>
#include <boost/regex.hpp>

class Lexer {
public:
    Lexer(const std::string& source) : source(source), position(0) {}

    std::vector<std::string> tokenize() {
        std::vector<std::string> tokens;
        boost::regex tokenPattern(R"(\s*(int|[a-zA-Z_]\w*|=|\d+|;))");
        boost::smatch match;

        while (boost::regex_search(source.substr(position), match, tokenPattern)) {
            tokens.push_back(match.str(1));
            position += match.position() + match.length();
        }

        return tokens;
    }

private:
    std::string source;
    size_t position;
};

文法解析の実装

次に、構文解析クラスを実装します。このクラスは字句解析の結果を受け取り、ASTノードを生成します。

#include <memory>

class ASTNode {
public:
    virtual ~ASTNode() = default;
    virtual void interpret() const = 0;
};

class VariableDeclaration : public ASTNode {
public:
    VariableDeclaration(const std::string& type, const std::string& name)
        : type(type), name(name) {}

    void interpret() const override {
        std::cout << "Declare variable of type " << type << " with name " << name << std::endl;
    }

private:
    std::string type;
    std::string name;
};

class Assignment : public ASTNode {
public:
    Assignment(const std::string& variable, const std::string& value)
        : variable(variable), value(value) {}

    void interpret() const override {
        std::cout << "Assign value " << value << " to variable " << variable << std::endl;
    }

private:
    std::string variable;
    std::string value;
};

class Parser {
public:
    explicit Parser(const std::vector<std::string>& tokens) : tokens(tokens), position(0) {}

    std::unique_ptr<ASTNode> parse() {
        if (tokens[position] == "int") {
            return parseVariableDeclaration();
        } else if (tokens[position + 1] == "=") {
            return parseAssignment();
        }
        return nullptr;
    }

private:
    std::unique_ptr<ASTNode> parseVariableDeclaration() {
        std::string type = tokens[position++];
        std::string name = tokens[position++];
        position++; // skip ';'
        return std::make_unique<VariableDeclaration>(type, name);
    }

    std::unique_ptr<ASTNode> parseAssignment() {
        std::string variable = tokens[position++];
        position++; // skip '='
        std::string value = tokens[position++];
        position++; // skip ';'
        return std::make_unique<Assignment>(variable, value);
    }

    std::vector<std::string> tokens;
    size_t position;
};

実装の確認

最後に、字句解析と構文解析の両方を使って簡単な文法解析を実行し、その結果を確認します。

int main() {
    std::string source = "int x = 10;";
    Lexer lexer(source);
    auto tokens = lexer.tokenize();

    Parser parser(tokens);
    auto ast = parser.parse();

    if (ast) {
        ast->interpret();
    }

    return 0;
}

この例では、「int x = 10;」という入力文字列を解析し、変数宣言と代入文を認識して、それぞれのASTノードを生成し、解釈します。このようにして、基本的な文法解析の実装が動作することを確認できます。次に、より高度な文法解析の実装方法について説明します。

高度な文法解析の実装

ここでは、基本的な文法解析の実装を基に、より複雑な文法解析を行う方法について説明します。例えば、条件文やループなどの高度な構文を解析する方法を紹介します。

条件文の解析

条件文(if文)の解析を追加します。条件文には、条件式とそれに従って実行される文が含まれます。

条件文クラスの定義

まず、条件文を表すASTノードクラスを定義します。

class Conditional : public ASTNode {
public:
    Conditional(std::unique_ptr<ASTNode> condition, std::unique_ptr<ASTNode> trueBranch)
        : condition(std::move(condition)), trueBranch(std::move(trueBranch)) {}

    void interpret() const override {
        std::cout << "If ";
        condition->interpret();
        std::cout << " then ";
        trueBranch->interpret();
    }

private:
    std::unique_ptr<ASTNode> condition;
    std::unique_ptr<ASTNode> trueBranch;
};

条件文の解析ルール

次に、条件文を解析するルールをParserクラスに追加します。

class Parser {
public:
    explicit Parser(const std::vector<std::string>& tokens) : tokens(tokens), position(0) {}

    std::unique_ptr<ASTNode> parse() {
        if (tokens[position] == "int") {
            return parseVariableDeclaration();
        } else if (tokens[position + 1] == "=") {
            return parseAssignment();
        } else if (tokens[position] == "if") {
            return parseConditional();
        }
        return nullptr;
    }

private:
    std::unique_ptr<ASTNode> parseVariableDeclaration() {
        std::string type = tokens[position++];
        std::string name = tokens[position++];
        position++; // skip ';'
        return std::make_unique<VariableDeclaration>(type, name);
    }

    std::unique_ptr<ASTNode> parseAssignment() {
        std::string variable = tokens[position++];
        position++; // skip '='
        std::string value = tokens[position++];
        position++; // skip ';'
        return std::make_unique<Assignment>(variable, value);
    }

    std::unique_ptr<ASTNode> parseConditional() {
        position++; // skip 'if'
        auto condition = parseExpression();
        auto trueBranch = parse();
        return std::make_unique<Conditional>(std::move(condition), std::move(trueBranch));
    }

    std::unique_ptr<ASTNode> parseExpression() {
        // 単純な例として変数の解析のみを行う
        std::string variable = tokens[position++];
        return std::make_unique<VariableDeclaration>("int", variable);
    }

    std::vector<std::string> tokens;
    size_t position;
};

ループの解析

次に、ループ(例えばwhile文)の解析を追加します。

ループ文クラスの定義

ループ文を表すASTノードクラスを定義します。

class Loop : public ASTNode {
public:
    Loop(std::unique_ptr<ASTNode> condition, std::unique_ptr<ASTNode> body)
        : condition(std::move(condition)), body(std::move(body)) {}

    void interpret() const override {
        std::cout << "While ";
        condition->interpret();
        std::cout << " do ";
        body->interpret();
    }

private:
    std::unique_ptr<ASTNode> condition;
    std::unique_ptr<ASTNode> body;
};

ループ文の解析ルール

Parserクラスにループ文の解析ルールを追加します。

class Parser {
public:
    explicit Parser(const std::vector<std::string>& tokens) : tokens(tokens), position(0) {}

    std::unique_ptr<ASTNode> parse() {
        if (tokens[position] == "int") {
            return parseVariableDeclaration();
        } else if (tokens[position + 1] == "=") {
            return parseAssignment();
        } else if (tokens[position] == "if") {
            return parseConditional();
        } else if (tokens[position] == "while") {
            return parseLoop();
        }
        return nullptr;
    }

private:
    std::unique_ptr<ASTNode> parseVariableDeclaration() {
        std::string type = tokens[position++];
        std::string name = tokens[position++];
        position++; // skip ';'
        return std::make_unique<VariableDeclaration>(type, name);
    }

    std::unique_ptr<ASTNode> parseAssignment() {
        std::string variable = tokens[position++];
        position++; // skip '='
        std::string value = tokens[position++];
        position++; // skip ';'
        return std::make_unique<Assignment>(variable, value);
    }

    std::unique_ptr<ASTNode> parseConditional() {
        position++; // skip 'if'
        auto condition = parseExpression();
        auto trueBranch = parse();
        return std::make_unique<Conditional>(std::move(condition), std::move(trueBranch));
    }

    std::unique_ptr<ASTNode> parseLoop() {
        position++; // skip 'while'
        auto condition = parseExpression();
        auto body = parse();
        return std::make_unique<Loop>(std::move(condition), std::move(body));
    }

    std::unique_ptr<ASTNode> parseExpression() {
        // 単純な例として変数の解析のみを行う
        std::string variable = tokens[position++];
        return std::make_unique<VariableDeclaration>("int", variable);
    }

    std::vector<std::string> tokens;
    size_t position;
};

高度な文法解析の確認

次に、条件文とループを含むコードを解析し、その結果を確認します。

int main() {
    std::string source = "int x = 10; if x while x;";
    Lexer lexer(source);
    auto tokens = lexer.tokenize();

    Parser parser(tokens);
    auto ast = parser.parse();

    if (ast) {
        ast->interpret();
    }

    return 0;
}

この例では、「int x = 10; if x while x;」という入力文字列を解析し、変数宣言、条件文、ループを認識してそれぞれのASTノードを生成し、解釈します。これにより、高度な文法解析の実装が完了し、複雑な構文の解析が可能になります。次に、エラー処理とデバッグ方法について説明します。

エラー処理とデバッグ方法

文法解析においては、入力が正しい形式であることを保証するためのエラー処理と、開発時のデバッグが重要です。ここでは、文法解析で発生する可能性のあるエラーの処理方法と、デバッグのテクニックについて説明します。

エラー処理の基本

エラー処理を適切に行うことで、ユーザーにわかりやすいエラーメッセージを提供し、問題の特定と修正を容易にします。以下のように、文法解析中にエラーが発生した場合に例外を投げて処理する方法を紹介します。

エラー例外クラスの定義

まず、エラーを表す例外クラスを定義します。

#include <exception>
#include <string>

class ParseException : public std::exception {
public:
    explicit ParseException(const std::string& message) : message(message) {}

    const char* what() const noexcept override {
        return message.c_str();
    }

private:
    std::string message;
};

エラー検出と例外の投げ方

次に、Parserクラスでエラーを検出し、例外を投げる方法を実装します。

class Parser {
public:
    explicit Parser(const std::vector<std::string>& tokens) : tokens(tokens), position(0) {}

    std::unique_ptr<ASTNode> parse() {
        try {
            if (tokens[position] == "int") {
                return parseVariableDeclaration();
            } else if (tokens[position + 1] == "=") {
                return parseAssignment();
            } else if (tokens[position] == "if") {
                return parseConditional();
            } else if (tokens[position] == "while") {
                return parseLoop();
            } else {
                throw ParseException("Unexpected token: " + tokens[position]);
            }
        } catch (const std::out_of_range&) {
            throw ParseException("Unexpected end of input");
        }
    }

private:
    std::unique_ptr<ASTNode> parseVariableDeclaration() {
        std::string type = tokens.at(position++);
        std::string name = tokens.at(position++);
        if (tokens.at(position++) != ";") {
            throw ParseException("Expected ';' after variable declaration");
        }
        return std::make_unique<VariableDeclaration>(type, name);
    }

    std::unique_ptr<ASTNode> parseAssignment() {
        std::string variable = tokens.at(position++);
        if (tokens.at(position++) != "=") {
            throw ParseException("Expected '=' in assignment");
        }
        std::string value = tokens.at(position++);
        if (tokens.at(position++) != ";") {
            throw ParseException("Expected ';' after assignment");
        }
        return std::make_unique<Assignment>(variable, value);
    }

    std::unique_ptr<ASTNode> parseConditional() {
        position++; // skip 'if'
        auto condition = parseExpression();
        auto trueBranch = parse();
        return std::make_unique<Conditional>(std::move(condition), std::move(trueBranch));
    }

    std::unique_ptr<ASTNode> parseLoop() {
        position++; // skip 'while'
        auto condition = parseExpression();
        auto body = parse();
        return std::make_unique<Loop>(std::move(condition), std::move(body));
    }

    std::unique_ptr<ASTNode> parseExpression() {
        // 単純な例として変数の解析のみを行う
        std::string variable = tokens.at(position++);
        return std::make_unique<VariableDeclaration>("int", variable);
    }

    std::vector<std::string> tokens;
    size_t position;
};

デバッグ方法

デバッグを効率的に行うためには、適切なツールやテクニックを活用することが重要です。ここでは、いくつかのデバッグ手法を紹介します。

デバッグツールの利用

IDEに内蔵されているデバッガを使用すると、ブレークポイントの設定やステップ実行が可能です。Visual StudioやCLionなどのIDEを使用してデバッグを行うことで、コードの実行状態を詳細に確認できます。

ログ出力の追加

コードの各ステップでログを出力することで、どの部分でエラーが発生しているかを確認できます。以下は、ログ出力を追加した例です。

#include <iostream>

class Parser {
    // ...
    std::unique_ptr<ASTNode> parse() {
        try {
            if (tokens[position] == "int") {
                std::cout << "Parsing variable declaration" << std::endl;
                return parseVariableDeclaration();
            } else if (tokens[position + 1] == "=") {
                std::cout << "Parsing assignment" << std::endl;
                return parseAssignment();
            } else if (tokens[position] == "if") {
                std::cout << "Parsing conditional" << std::endl;
                return parseConditional();
            } else if (tokens[position] == "while") {
                std::cout << "Parsing loop" << std::endl;
                return parseLoop();
            } else {
                throw ParseException("Unexpected token: " + tokens[position]);
            }
        } catch (const std::out_of_range&) {
            throw ParseException("Unexpected end of input");
        }
    }
    // ...
};

ユニットテストの実施

ユニットテストを作成し、コードの各部分が正しく動作するかを確認します。Google Testなどのテストフレームワークを使用することで、自動化されたテストを容易に実施できます。

エラーハンドリングとデバッグの統合

エラーハンドリングとデバッグを統合することで、より堅牢な文法解析器を構築できます。例外処理を適切に行い、デバッグ情報を活用することで、問題の早期発見と解決が可能になります。

これにより、エラー処理とデバッグ方法についての解説が完了しました。次に、インタープリタパターンを使った文法解析の実用的な応用例を紹介します。

実用的な応用例

インタープリタパターンを使った文法解析は、実際のソフトウェア開発において多くの応用が可能です。ここでは、いくつかの具体的な応用例を紹介します。

DSL(ドメイン固有言語)の構築

インタープリタパターンは、特定の問題領域に特化した言語、いわゆるドメイン固有言語(DSL: Domain Specific Language)の構築に非常に有用です。例えば、テストスクリプト言語、ビルドスクリプト言語、クエリ言語などが挙げられます。

DSLの例:簡単な算術演算言語

簡単な算術演算を行うDSLを構築する例を示します。このDSLでは、数値の加減乗除をサポートします。

class Number : public ASTNode {
public:
    explicit Number(int value) : value(value) {}

    void interpret() const override {
        std::cout << value;
    }

private:
    int value;
};

class BinaryOperation : public ASTNode {
public:
    BinaryOperation(std::unique_ptr<ASTNode> left, char op, std::unique_ptr<ASTNode> right)
        : left(std::move(left)), op(op), right(std::move(right)) {}

    void interpret() const override {
        left->interpret();
        std::cout << " " << op << " ";
        right->interpret();
    }

private:
    std::unique_ptr<ASTNode> left;
    char op;
    std::unique_ptr<ASTNode> right;
};

算術演算DSLの解析

Parserクラスに算術演算の解析ルールを追加します。

class Parser {
public:
    explicit Parser(const std::vector<std::string>& tokens) : tokens(tokens), position(0) {}

    std::unique_ptr<ASTNode> parse() {
        return parseExpression();
    }

private:
    std::unique_ptr<ASTNode> parseExpression() {
        auto left = parseTerm();
        while (position < tokens.size() && (tokens[position] == "+" || tokens[position] == "-")) {
            char op = tokens[position++][0];
            auto right = parseTerm();
            left = std::make_unique<BinaryOperation>(std::move(left), op, std::move(right));
        }
        return left;
    }

    std::unique_ptr<ASTNode> parseTerm() {
        auto left = parseFactor();
        while (position < tokens.size() && (tokens[position] == "*" || tokens[position] == "/")) {
            char op = tokens[position++][0];
            auto right = parseFactor();
            left = std::make_unique<BinaryOperation>(std::move(left), op, std::move(right));
        }
        return left;
    }

    std::unique_ptr<ASTNode> parseFactor() {
        if (std::isdigit(tokens[position][0])) {
            int value = std::stoi(tokens[position++]);
            return std::make_unique<Number>(value);
        }
        return nullptr;
    }

    std::vector<std::string> tokens;
    size_t position;
};

DSLの使用例

このDSLを使って簡単な算術演算を解析・実行します。

int main() {
    std::string source = "3 + 5 * 2";
    Lexer lexer(source);
    auto tokens = lexer.tokenize();

    Parser parser(tokens);
    auto ast = parser.parse();

    if (ast) {
        ast->interpret(); // 3 + 5 * 2 と出力されます
    }

    return 0;
}

設定ファイルの解析

設定ファイルを解析してプログラムの動作を制御する場合にもインタープリタパターンが役立ちます。例えば、JSONやXML形式の設定ファイルを解析して、プログラムの設定値を取得することができます。

JSONパーサーの実装例

簡単なJSONパーサーをインタープリタパターンで実装します。

class JSONValue : public ASTNode {
public:
    explicit JSONValue(const std::string& value) : value(value) {}

    void interpret() const override {
        std::cout << value;
    }

private:
    std::string value;
};

class JSONObject : public ASTNode {
public:
    void addMember(const std::string& key, std::unique_ptr<ASTNode> value) {
        members[key] = std::move(value);
    }

    void interpret() const override {
        std::cout << "{ ";
        for (const auto& member : members) {
            std::cout << "\"" << member.first << "\": ";
            member.second->interpret();
            std::cout << ", ";
        }
        std::cout << " }";
    }

private:
    std::map<std::string, std::unique_ptr<ASTNode>> members;
};

JSONパーサーの解析ルール

ParserクラスにJSON解析のルールを追加します。

class JSONParser {
public:
    explicit JSONParser(const std::vector<std::string>& tokens) : tokens(tokens), position(0) {}

    std::unique_ptr<ASTNode> parse() {
        if (tokens[position] == "{") {
            return parseObject();
        }
        return nullptr;
    }

private:
    std::unique_ptr<ASTNode> parseObject() {
        position++; // skip '{'
        auto obj = std::make_unique<JSONObject>();

        while (tokens[position] != "}") {
            std::string key = tokens[position++];
            position++; // skip ':'
            auto value = parseValue();
            obj->addMember(key, std::move(value));
            if (tokens[position] == ",") {
                position++; // skip ','
            }
        }
        position++; // skip '}'
        return obj;
    }

    std::unique_ptr<ASTNode> parseValue() {
        if (tokens[position][0] == '"') {
            std::string value = tokens[position++];
            return std::make_unique<JSONValue>(value);
        }
        return nullptr;
    }

    std::vector<std::string> tokens;
    size_t position;
};

JSON解析の使用例

このJSONパーサーを使って簡単なJSONオブジェクトを解析・表示します。

int main() {
    std::string source = R"({ "name": "John", "age": "30" })";
    Lexer lexer(source);
    auto tokens = lexer.tokenize();

    JSONParser parser(tokens);
    auto ast = parser.parse();

    if (ast) {
        ast->interpret(); // { "name": "John", "age": "30" } と出力されます
    }

    return 0;
}

これにより、インタープリタパターンを使った文法解析の実用的な応用例の紹介が完了しました。次に、演習問題とその解答例を提示します。

演習問題と解答例

ここでは、C++でインタープリタパターンを使った文法解析の理解を深めるための演習問題とその解答例を提示します。実際に手を動かして、文法解析の仕組みを学びましょう。

演習問題 1: 論理演算の追加

論理演算(AND, OR)をサポートする文法解析を実装してください。以下の条件を満たすようにコードを拡張してください。

  1. AND演算子 &&
  2. OR演算子 ||
// 例: (true && false) || true
std::string source = "true && false || true";

解答例 1

#include <iostream>
#include <vector>
#include <string>
#include <memory>
#include <boost/regex.hpp>

class ASTNode {
public:
    virtual ~ASTNode() = default;
    virtual void interpret() const = 0;
};

class Boolean : public ASTNode {
public:
    explicit Boolean(bool value) : value(value) {}

    void interpret() const override {
        std::cout << (value ? "true" : "false");
    }

private:
    bool value;
};

class LogicalOperation : public ASTNode {
public:
    LogicalOperation(std::unique_ptr<ASTNode> left, const std::string& op, std::unique_ptr<ASTNode> right)
        : left(std::move(left)), op(op), right(std::move(right)) {}

    void interpret() const override {
        left->interpret();
        std::cout << " " << op << " ";
        right->interpret();
    }

private:
    std::unique_ptr<ASTNode> left;
    std::string op;
    std::unique_ptr<ASTNode> right;
};

class Lexer {
public:
    Lexer(const std::string& source) : source(source), position(0) {}

    std::vector<std::string> tokenize() {
        std::vector<std::string> tokens;
        boost::regex tokenPattern(R"(\s*(true|false|&&|\|\||\(|\)))");
        boost::smatch match;

        while (boost::regex_search(source.substr(position), match, tokenPattern)) {
            tokens.push_back(match.str(1));
            position += match.position() + match.length();
        }

        return tokens;
    }

private:
    std::string source;
    size_t position;
};

class Parser {
public:
    explicit Parser(const std::vector<std::string>& tokens) : tokens(tokens), position(0) {}

    std::unique_ptr<ASTNode> parse() {
        return parseExpression();
    }

private:
    std::unique_ptr<ASTNode> parseExpression() {
        auto left = parseTerm();
        while (position < tokens.size() && (tokens[position] == "||")) {
            std::string op = tokens[position++];
            auto right = parseTerm();
            left = std::make_unique<LogicalOperation>(std::move(left), op, std::move(right));
        }
        return left;
    }

    std::unique_ptr<ASTNode> parseTerm() {
        auto left = parseFactor();
        while (position < tokens.size() && (tokens[position] == "&&")) {
            std::string op = tokens[position++];
            auto right = parseFactor();
            left = std::make_unique<LogicalOperation>(std::move(left), op, std::move(right));
        }
        return left;
    }

    std::unique_ptr<ASTNode> parseFactor() {
        if (tokens[position] == "true" || tokens[position] == "false") {
            bool value = tokens[position++] == "true";
            return std::make_unique<Boolean>(value);
        }
        return nullptr;
    }

    std::vector<std::string> tokens;
    size_t position;
};

int main() {
    std::string source = "true && false || true";
    Lexer lexer(source);
    auto tokens = lexer.tokenize();

    Parser parser(tokens);
    auto ast = parser.parse();

    if (ast) {
        ast->interpret(); // true && false || true と出力されます
    }

    return 0;
}

演習問題 2: 関数呼び出しの解析

関数呼び出しの文法解析を実装してください。関数は名前と引数リストを持ちます。以下の条件を満たすようにコードを拡張してください。

  1. 関数名はアルファベットの文字列
  2. 引数はカンマで区切られた数値
// 例: foo(1, 2, 3)
std::string source = "foo(1, 2, 3)";

解答例 2

#include <iostream>
#include <vector>
#include <string>
#include <memory>
#include <boost/regex.hpp>

class ASTNode {
public:
    virtual ~ASTNode() = default;
    virtual void interpret() const = 0;
};

class Number : public ASTNode {
public:
    explicit Number(int value) : value(value) {}

    void interpret() const override {
        std::cout << value;
    }

private:
    int value;
};

class FunctionCall : public ASTNode {
public:
    FunctionCall(const std::string& name, std::vector<std::unique_ptr<ASTNode>> arguments)
        : name(name), arguments(std::move(arguments)) {}

    void interpret() const override {
        std::cout << name << "(";
        for (size_t i = 0; i < arguments.size(); ++i) {
            arguments[i]->interpret();
            if (i < arguments.size() - 1) {
                std::cout << ", ";
            }
        }
        std::cout << ")";
    }

private:
    std::string name;
    std::vector<std::unique_ptr<ASTNode>> arguments;
};

class Lexer {
public:
    Lexer(const std::string& source) : source(source), position(0) {}

    std::vector<std::string> tokenize() {
        std::vector<std::string> tokens;
        boost::regex tokenPattern(R"(\s*([a-zA-Z_]\w*|\d+|,|\(|\)))");
        boost::smatch match;

        while (boost::regex_search(source.substr(position), match, tokenPattern)) {
            tokens.push_back(match.str(1));
            position += match.position() + match.length();
        }

        return tokens;
    }

private:
    std::string source;
    size_t position;
};

class Parser {
public:
    explicit Parser(const std::vector<std::string>& tokens) : tokens(tokens), position(0) {}

    std::unique_ptr<ASTNode> parse() {
        return parseFunctionCall();
    }

private:
    std::unique_ptr<ASTNode> parseFunctionCall() {
        std::string name = tokens[position++];
        position++; // skip '('
        std::vector<std::unique_ptr<ASTNode>> arguments;
        while (tokens[position] != ")") {
            arguments.push_back(parseNumber());
            if (tokens[position] == ",") {
                position++; // skip ','
            }
        }
        position++; // skip ')'
        return std::make_unique<FunctionCall>(name, std::move(arguments));
    }

    std::unique_ptr<ASTNode> parseNumber() {
        int value = std::stoi(tokens[position++]);
        return std::make_unique<Number>(value);
    }

    std::vector<std::string> tokens;
    size_t position;
};

int main() {
    std::string source = "foo(1, 2, 3)";
    Lexer lexer(source);
    auto tokens = lexer.tokenize();

    Parser parser(tokens);
    auto ast = parser.parse();

    if (ast) {
        ast->interpret(); // foo(1, 2, 3) と出力されます
    }

    return 0;
}

これにより、インタープリタパターンを使った文法解析の演習問題とその解答例を提供しました。次に、記事全体のまとめを行います。

まとめ

本記事では、C++でインタープリタパターンを使用した文法解析の実装方法について詳しく解説しました。まず、インタープリタパターンの基本概念とその利点について説明し、文法解析の基礎から実際の実装例までをステップバイステップで紹介しました。また、エラー処理とデバッグ方法、高度な文法解析の実装例、そして実用的な応用例についても触れました。最後に、理解を深めるための演習問題とその解答例を提供しました。

インタープリタパターンは、複雑な文法規則を簡潔に定義し、拡張性と保守性を高めるための強力な手法です。これを活用することで、さまざまな応用分野で効率的に文法解析を行うことができます。ぜひ、今回の内容を基に実際のプロジェクトでインタープリタパターンを活用してみてください。

コメント

コメントする

目次