JavaScriptでおもちゃのLispを作ろう(2)〜プリンタ編〜

今日は前回に引き続き、リーダ関数の残りとリーダ関数と反対のプリンタ関数を実装します。

S式を読み込むリーダ関数のサブ関数であるreadString()とreadList()関数は次のようになります。Lispの文字列はエスケープもいろいろできるのですが、複雑なことは後回しにして、ここではJavaScriptの文字列のまま扱うようにしましょう。エラー処理も後回しにして、当面は全体が動くものを早く作ることを目指します。

function readString() {
  var s = [];
  var c;
  while ((c = inpc()) != '"') {
    s.push(c);
  }
  return s.join("");
}
function readList() {
  if (!skipSpace()) {
    return null;
  }
  switch (peek()) {
  case ')':
    inpc();
    return NIL;
  default:
    var top = new Cell;
    var q = top;
    top.car = readExp();
    skipSpace();
    while (peek() != ')') {
      if (peek() == '.') {
        inpc();
        if (!skipSpace() || peek() == ')') {
          throw SyntaxError();
        }
        q.cdr = readExp();
        if (!skipSpace()) {
          throw SyntaxError();
        }
      } else {
        var p = new Cell;
        q.cdr = p;
        p.car = readExp();
        q = p;
        if (!skipSpace()) {
          throw SyntaxError();
        }
      }
    }
    inpc();
    return top;
  }
}

リーダの関連関数をまとめてクロージャにしてスコープを限定します。

var Reader = (function () {
    var inp;
    var pos;
    var spaces = " \n\t\r\f";
    var digits = "0123456789";

    function isSpace(c) {
      return c && spaces.indexOf(c) >= 0;
    }

    function isDigit(c) {
      return c && digits.indexOf(c) >= 0;
    }

    function inpc() {
      return inp.charAt(pos++);
    }

    function peek() {
      return inp.charAt(pos);
    }

    function isDelim(c) {
      if (!c) {
        return true;
      } else {
        switch (c) {
        case '\0': case '\r': case '\t': case '\f':
        case ' ':  case '"':  case '#':  case '\'':
        case '(':  case ')':  case ',':  case ';':
        return true;
        default:
        return false;
        }
      }
    }

    function skipSpace() {
      var c;
      while (true) {
        while (isSpace(c = peek())) {
          inpc();
        }
        if (!c) {
          return c;
        } else if ((c = peek()) == ';') {
          // skip comment
          while (peek() != '\n') {
            if (!inpc()) {
              return null;
            }
          }
        } else {
          return c;
        }
      }
    }

    function readToken() {
      if (!skipSpace()) {
        return;
      }
      var ch = inpc();
      if (!isDelim(ch)) {
        var arr = [ ch ];
        while (!isDelim(peek())) {
          arr.push(inpc());
        }
        return arr.join("");
      } else {
        return ch;
      }
    }

    function readExp() {
      var token = readToken();
      if (!token) {
        return null;
      }
      switch (token) {
      case '(': return readList();
      case '"': return readString();
      case "'": return cons(QUOTE, readExp());
      default:
      var c = token.charAt(0);
      if (isDigit(c) || c === '-' || c === '+' || c === '.') {
        var rv = Number(token);
        return isNaN(rv) ? intern(token) : rv;
      } else {
        return intern(token);
      }
      }
    }

    function readList() {
      if (!skipSpace()) {
        return null;
      }
      switch (peek()) {
      case ')':
      inpc();
      return NIL;
      default:
      var top = new Cell;
      var q = top;
      top.car = readExp();
      skipSpace();
      while (peek() != ')') {
        if (peek() == '.') {
          inpc();
          if (!skipSpace() || peek() == ')') {
            throw SyntaxError();
          }
          q.cdr = readExp();
          if (!skipSpace()) {
            throw SyntaxError();
          }
        } else {
          var p = new Cell;
          q.cdr = p;
          p.car = readExp();
          q = p;
          if (!skipSpace()) {
            throw SyntaxError();
          }
        }
      }
      inpc();
      return top;
      }
    }

    function readString() {
      var s = [];
      var c;
      while ((c = inpc()) != '"') {
        s.push(c);
      }
      return s.join("");
    }

    function toString() {
      return "[Reader pos:" + pos + ",inp:" + inp + "]";
    }

    return function (str) {
      inp = str;
      pos = 0;
      this.skipSpace = skipSpace;
      this.readToken = readToken;
      this.readExp = readExp;
      this.readList = readList;
      this.toString = toString;
    };
        
  })();

このReaderオブジェクトを使えば文字列からS式を読み込む関数readFromStringは以下のように作れます。

function readFromString(s) {
  var inp = new Reader(s);
  return inp.readExp();
}

正しく動作するかどうかのテストはプリンタ関数を作った後にまとめて行います。

プリンタ関数

プリンタ関数はリーダ関数の逆で、内部表現されたS式を目に見える表現に変換します。ここでは、JavaScriptには標準出力関数が定義されていませんので、ここでは文字列に変換することにします。詳しい説明は省略しますが、配列変数outputに文字列が蓄積され、最後にjoin("")で一つの文字列として連結しています。

var Printer = (function () {
    var output = [];
    var princ_mode = false;
    function pr(x) {
      output.push(String(x));
    }
    function printExp(x) {
      if (atom(x)) {
        printAtom(x);
      } else if (symbolp(car(x))) {
        var a = car(x);
        if (a === QUOTE) {
          pr("'");
          printExp(cdr(x));
        } else {
          printList(x);
        }
      } else {
        printList(x);
      }
    }
    function printAtom(x) {
      if (stringp(x)) {
        pr('"');
        pr(x);
        pr('"');
      } else {
        pr(x);
      }
    }
    function printList(x) {
      pr("(");
      for (;; x = cdr(x)) {
        printExp(car(x));
        if (atom(cdr(x))) {
          break;
        }
        pr(" ");
      }
      x = cdr(x);
      if (x != NIL) {
        pr(" . ");
        printAtom(x);
      }
      pr(")");
    }
    return function () {
      this.printExp = function (x) {
        output = [];
        printExp(x);
        print(output.join(""));
      };
      this.toStr = function (x) {
        output = [];
        printExp(x);
        return output.join("");
      };
    };
  })();

以上で、文字列からS式を読み込んで内部表現に変換するリーダ関数と、その逆の内部表現から文字列に変換するプリンタ関数が実装できました。エラー処理等がかなり手抜きのコードになっています。次回は、これまで実装してきた部分の簡単な自動テストを実行するコードを書いていきます。そこで、大きな問題がなければ、いよいよ評価器(evaluator)の実装を行っていきます。