技術

6.3. ユーザ定義二項演算子

LLVMによるプログラミング言語の実装チュートリアル日本語訳
第6章 万華鏡: 言語の拡張: ユーザ定義演算子
第3節 ユーザ定義二項演算子

我々の現在のフレームワークだと、ユーザ定義二項演算子のサポートを追加するのは実に簡単である。
まず、unary(単項演算子用)とbinary(二項演算子用)のためのキーワードのサポートを追加する。

enum Token {
  ...
  // 演算子
  tok_binary = -11, tok_unary = -12
};
...
static int gettok() {
...
    if (IdentifierStr == "for") return tok_for;
    if (IdentifierStr == "in") return tok_in;
    if (IdentifierStr == "binary") return tok_binary;
    if (IdentifierStr == "unary") return tok_unary;
    return tok_identifier;

これにより、字句解析器にunaryとbinaryというキーワードに対するサポートが追加される。(これは前の章でもやった作業である。)
現在の我々のASTの良いところは、ASCIIコードをそのままopcodeとして用いることによって二項演算子を完全に一般化して表現してることである。
これから我々が拡張する演算子についても、同じ表現を使用するので、新しいASTクラスや構文解析器のサポートは必要ない。

一方、関数定義の一種”def binary| 5″のような形で新しい演算子の定義を表現できるようにしなければならない。
これまで我々の文法では、関数定義のための名前は、プロトタイプとしてPrototypeASTノードの中に(構文解析された結果)保持されていた。
新しいユーザ定義演算子をプロトタイプとして表現するには、以下のようにしてPrototypeASTノードを拡張する必要がある。

/// PrototypeAST - このクラスは関数のプロトタイプを表している。
/// もし演算子だったとしてもその引数名をキャプチャする。
class PrototypeAST {
  std::string Name;
  std::vector<std::string> Args;
  bool isOperator;
  unsigned Precedence;  // 二項演算子の場合の優先順位
public:
  PrototypeAST(const std::string &name, const std::vector<std::string> &args,
               bool isoperator = false, unsigned prec = 0)
  : Name(name), Args(args), isOperator(isoperator), Precedence(prec) {}

  bool isUnaryOp() const { return isOperator && Args.size() == 1; }
  bool isBinaryOp() const { return isOperator && Args.size() == 2; }

  char getOperatorName() const {
    assert(isUnaryOp() || isBinaryOp());
    return Name[Name.size()-1];
  }

  unsigned getBinaryPrecedence() const { return Precedence; }

  Function *Codegen();
};

基本的に、プロトタイプの名前を保持することに加えて、それが演算子かどうかの情報を保持し、さらに演算子の場合にはその優先順位を保持する。
優先順位は二項演算子でのみ使用される。(次の節でも見る通り、単項演算子には使用しない。)
これで、ユーザ定義演算子のためのプロトタイプを表現する方法を得た。
次はこれを解析する必要がある。

/// prototype
///   ::= id '(' id* ')'
///   ::= binary LETTER number? (id, id)
static PrototypeAST *ParsePrototype() {
  std::string FnName;

  unsigned Kind = 0;  // 0 = 識別子, 1 = 単項, 2 = 二項.
  unsigned BinaryPrecedence = 30;

  switch (CurTok) {
  default:
    return ErrorP("Expected function name in prototype");
  case tok_identifier:
    FnName = IdentifierStr;
    Kind = 0;
    getNextToken();
    break;
  case tok_binary:
    getNextToken();
    if (!isascii(CurTok))
      return ErrorP("Expected binary operator");
    FnName = "binary";
    FnName += (char)CurTok;
    Kind = 2;
    getNextToken();

    // 優先順位が指定されていればそれを読み込む。
    if (CurTok == tok_number) {
      if (NumVal < 1 || NumVal > 100)
        return ErrorP("Invalid precedecnce: must be 1..100");
      BinaryPrecedence = (unsigned)NumVal;
      getNextToken();
    }
    break;
  }

  if (CurTok != '(')
    return ErrorP("Expected '(' in prototype");

  std::vector<std::string> ArgNames;
  while (getNextToken() == tok_identifier)
    ArgNames.push_back(IdentifierStr);
  if (CurTok != ')')
    return ErrorP("Expected ')' in prototype");

  // 成功。
  getNextToken();  // ')'を消化。

  // 演算子なら引数の数がそれに合ってるか確認する。
  if (Kind && ArgNames.size() != Kind)
    return ErrorP("Invalid number of operands for operator");

  return new PrototypeAST(FnName, ArgNames, Kind != 0, BinaryPrecedence);
}

これは全て実に素直な構文解析のコードであり、これに似たコードをこれまでよく見てきた。
上のコードで面白いのは、二項演算子のためにFnNameを準備してる2行の部分である。
もし”@”演算子を新しく定義したとすると、この2行によって”binary@”という名前となる。
これは、LLVMのシンボルテーブルに登録するシンボル名として、ヌル文字を含む任意の文字列が許可されているという事実を利用している。

次の興味深い点は、これらの二項演算子のためのコード生成機能の追加部分でる。
これは、既存の二項演算子のノードのコードに、組み込みの演算子じゃない場合について簡単な追加を行うだけで済む。

Value *BinaryExprAST::Codegen() {
  Value *L = LHS->Codegen();
  Value *R = RHS->Codegen();
  if (L == 0 || R == 0) return 0;

  switch (Op) {
  case '+': return Builder.CreateFAdd(L, R, "addtmp");
  case '-': return Builder.CreateFSub(L, R, "subtmp");
  case '*': return Builder.CreateFMul(L, R, "multmp");
  case '<':
    L = Builder.CreateFCmpULT(L, R, "cmptmp");
    // 真偽値の0, 1 を doubleの0.0, 1.0に変換する。
    return Builder.CreateUIToFP(L, Type::getDoubleTy(getGlobalContext()),
                                "booltmp");
  default: break;
  }

  // 組み込みの二項演算子でない場合は、ユーザ定義二項演算子である。
  // それを呼び出すコードを生成する。
  Function *F = TheModule->getFunction(std::string("binary")+Op);
  assert(F && "binary operator not found!");

  Value *Ops[2] = { L, R };
  return Builder.CreateCall(F, Ops, "binop");
}

上のコードで見られる通り、追加したコードは本当にシンプルである。
シンボルテーブルから適切な演算子を探し、それを呼び出すコードを生成するだけである。
ユーザ定義演算子は、単に普通の関数として構築される(プロトタイプは正しい名前で関数になる)ので、全てつじつまがあう。

最後に、トップレベルにおけるちょっとした魔法を追加する。

Function *FunctionAST::Codegen() {
  NamedValues.clear();

  Function *TheFunction = Proto->Codegen();
  if (TheFunction == 0)
    return 0;

  // もし演算子だったら、それをインストールする。
  if (Proto->isBinaryOp())
    BinopPrecedence[Proto->getOperatorName()] = Proto->getBinaryPrecedence();

  // 挿入を開始するため、新しい基本ブロックを作成する。
  BasicBlock *BB = BasicBlock::Create(getGlobalContext(), "entry", TheFunction);
  Builder.SetInsertPoint(BB);

  if (Value *RetVal = Body->Codegen()) {
    ...

基本的には、関数のコード生成を行う前に、もしそれがユーザ定義演算子なら、それを優先順位テーブルに登録する。
これによって、二項演算子の構文解析ロジックでユーザ定義演算子を処理することが出来るようになる。
我々の構文解析器は、完全に汎用的な演算子優先順位を扱えるので、以上が”文法の拡張”に必要な事全てである。

これで便利なユーザ定義演算子を得た。
以上の事は、既存の演算子のために構築した前章までのフレームワークにかなり基礎を置いてる。
我々はまだ単項演算子を扱うフレームワークを持っていないので、単項演算子の追加に関してはちょっとチャレンジングである。
次はそれについて説明する。

コメントを残す

メールアドレスが公開されることはありません。



※画像をクリックして別の画像を表示