1. カレイドスコープ:カレイドスコープの概要と字句解析器

1.1. カレイドスコープ言語

このチュートリアルは、「カレイドスコープ」(「美しい意味、形態、そして眺め」に由来)と呼ばれるおもちゃの言語を使用して説明されています。カレイドスコープは、関数の定義、条件分岐、数学演算などを可能にする手続き型言語です。チュートリアルの過程で、カレイドスコープを拡張して、if/then/else構文、forループ、ユーザー定義演算子、シンプルなコマンドラインインターフェースを使用したJITコンパイル、デバッグ情報などをサポートします。

シンプルさを保つため、カレイドスコープのデータ型は64ビット浮動小数点数型(C言語でいうところの'double')のみです。そのため、すべての値は暗黙的に倍精度であり、言語は型宣言を必要としません。これにより、言語は非常にシンプルで分かりやすい構文になります。例えば、以下の簡単な例ではフィボナッチ数を計算します。

# Compute the x'th fibonacci number.
def fib(x)
  if x < 3 then
    1
  else
    fib(x-1)+fib(x-2)

# This expression will compute the 40th number.
fib(40)

また、カレイドスコープは標準ライブラリの関数呼び出しを許可します - LLVM JITにより、これは非常に簡単になります。つまり、「extern」キーワードを使用して、関数を最初に使用する前に定義できます(これは相互再帰関数にも役立ちます)。例えば

extern sin(arg);
extern cos(arg);
extern atan2(arg1 arg2);

atan2(sin(.4), cos(42))

より興味深い例は第6章に含まれており、マンデルブロ集合を表示する小さなカレイドスコープアプリケーションを作成します。

この言語の実装に飛び込みましょう!

1.2. 字句解析器

言語を実装する際には、まずテキストファイルを読み込んでその内容を認識する機能が必要です。従来の方法としては、「字句解析器」(別名「スキャナ」)を使用して入力を「トークン」に分割します。字句解析器によって返される各トークンには、トークンコードと、場合によってはメタデータ(数値の値など)が含まれています。まず、可能性を定義します。

// The lexer returns tokens [0-255] if it is an unknown character, otherwise one
// of these for known things.
enum Token {
  tok_eof = -1,

  // commands
  tok_def = -2,
  tok_extern = -3,

  // primary
  tok_identifier = -4,
  tok_number = -5,
};

static std::string IdentifierStr; // Filled in if tok_identifier
static double NumVal;             // Filled in if tok_number

字句解析器によって返される各トークンは、Token列挙値のいずれか、または'+'のような'不明'文字(ASCII値として返される)になります。現在のトークンが識別子の場合、IdentifierStrグローバル変数には識別子の名前が格納されます。現在のトークンが数値リテラル(1.0など)の場合、NumValにはその値が格納されます。簡潔にするためにグローバル変数を使用していますが、これは実際の言語実装としては最良の選択ではありません。

字句解析器の実際の処理は、gettokという名前の単一関数です。gettok関数は、標準入力から次のトークンを返すために呼び出されます。その定義は以下のように始まります。

/// gettok - Return the next token from standard input.
static int gettok() {
  static int LastChar = ' ';

  // Skip any whitespace.
  while (isspace(LastChar))
    LastChar = getchar();

gettokは、Cのgetchar()関数を呼び出して、標準入力から一度に1文字ずつ読み取ります。認識した文字を読み取り、処理されていない最後に読み取った文字をLastCharに格納します。まず、トークン間の空白を無視する必要があります。これは上記のループで行われます。

次にgettokが必要とするのは、識別子と「def」のような特定のキーワードを認識することです。カレイドスコープは、この単純なループで行います。

if (isalpha(LastChar)) { // identifier: [a-zA-Z][a-zA-Z0-9]*
  IdentifierStr = LastChar;
  while (isalnum((LastChar = getchar())))
    IdentifierStr += LastChar;

  if (IdentifierStr == "def")
    return tok_def;
  if (IdentifierStr == "extern")
    return tok_extern;
  return tok_identifier;
}

このコードは、識別子を字句解析するたびに' IdentifierStr'グローバル変数を設定することに注意してください。また、言語キーワードは同じループで一致するため、ここでインラインで処理します。数値は同様です。

if (isdigit(LastChar) || LastChar == '.') {   // Number: [0-9.]+
  std::string NumStr;
  do {
    NumStr += LastChar;
    LastChar = getchar();
  } while (isdigit(LastChar) || LastChar == '.');

  NumVal = strtod(NumStr.c_str(), 0);
  return tok_number;
}

これは、入力処理のための非常に分かりやすいコードです。入力から数値を読み取る際に、Cのstrtod関数を使用して、NumValに格納する数値に変換します。これは十分なエラーチェックを行っていないことに注意してください。「1.23.45.67」を正しく読み取らず、「1.23」と入力した場合のように処理します。自由に拡張してください!次に、コメントを処理します。

if (LastChar == '#') {
  // Comment until end of line.
  do
    LastChar = getchar();
  while (LastChar != EOF && LastChar != '\n' && LastChar != '\r');

  if (LastChar != EOF)
    return gettok();
}

行末までスキップして次のトークンを返すことでコメントを処理します。最後に、入力が上記のいずれにも一致しない場合、'+'のような演算子文字かファイルの終わりです。これらは次のコードで処理されます。

  // Check for end of file.  Don't eat the EOF.
  if (LastChar == EOF)
    return tok_eof;

  // Otherwise, just return the character as its ascii value.
  int ThisChar = LastChar;
  LastChar = getchar();
  return ThisChar;
}

これにより、基本的なカレイドスコープ言語の字句解析器が完成します(字句解析器の完全なコードリストは、チュートリアルの次の章で確認できます)。次に、この字句解析器を使用して抽象構文木(AST)を作成する単純なパーサーを構築します。それができたら、字句解析器とパーサーを組み合わせて使用できるドライバを含めます。

次へ:パーサーとASTの実装