Generator 0.2

前回作成したパーサージェネレーターでは空白の読み飛ばしがうまくいっていなかったため、これを修正しました。

実行結果

以下は実際に使用している EBNF の構文です。
	ここに構文が入ります。
上記の EBNF の構文の文字列を EBNFParser に渡し、EBNFParser 自身のパーサーのコードを以下の通り生成しました。 字下げは Aptana Studio 等の IDE で簡単にできるのでジェネレーターの中では行っていません。
	ここに結果が入ります。

ドキュメント

JsDoc Toolkit で作成したドキュメントはこちら

今回、一つのクラス EBNFParser を自動生成した部分 parser02.js と 手入力している部分 generator02.js に 分割したのですが、JsDoc Toolkit では EBNFParser クラスが認識されなくなりました。プログラムとしては 動作するのですが、ドキュメントに難ありなので、次回から自動生成部分と手入力部分を一つにまとめるか、手入力している 部分を別のクラスにするか、そのような対策をしたいと思います。

[註] クラス EBNFParser のドキュメントができなかったのは、ファイルを分割したためではなく、@class の指定が されていなかったと判明しました。上記の記述には誤りがあります。

ソース


generatortest02.html

このソースコードは Jasmine のサンプル SpecRunner.html から大部分を持ってきました。前半はスタイルシートとスクリプトを読み込んでいる部分です。
<head>
<meta charset="UTF-8">
<title>JavaScriptでプログラミング - Generator 0.2</title>

<link type="text/css" rel="stylesheet" href="lib/jasmine-1.3.1/jasmine.css">
<link type="text/css" rel="stylesheet" href="css/spec.css">
<link type="text/css" rel="stylesheet" href="css/shCoreEclipse.css">
<link type="text/css" rel="stylesheet" href="css/shThemeEclipse.css">
<script type="text/javascript" src="js/XRegExp.js"></script>
<script type="text/javascript" src="js/shCore.js"></script>
<script type="text/javascript" src="js/shBrushJScript.js"></script>
<script type="text/javascript" src="js/shBrushXml.js"></script>
<script type="text/javascript" src="js/shBrushCss.js"></script>
<script type="text/javascript" src="lib/jasmine-1.3.1/jasmine.js"></script>
<script type="text/javascript" src="lib/jasmine-1.3.1/jasmine-html.js"></script>

<!-- include source files here... -->
<script type="text/javascript" src="js/generator02/source01.js"></script>
<script type="text/javascript" src="js/generator02/lex03.js"></script>
<script type="text/javascript" src="js/generator02/parse02.js"></script>
<script type="text/javascript" src="js/generator02/generator02.js"></script>

<!-- include spec files here... -->
<script type="text/javascript" src="js/generator02/sourcespec01.js"></script>
<script type="text/javascript" src="js/generator02/lexspec03.js"></script>
<script type="text/javascript" src="js/generator02/generatorspec02.js"></script>
後半は Jasmine によるテスト結果を HTML 上で展開するレポーターと呼ばれる部分です。前回から変更ありません。
<script type="text/javascript">
  SyntaxHighlighter.all();
  (function() {
    var jasmineEnv = jasmine.getEnv();
    jasmineEnv.updateInterval = 1000;

    var htmlReporter = new jasmine.HtmlReporter();
    jasmineEnv.addReporter(htmlReporter);
    jasmineEnv.specFilter = function(spec) {
      return htmlReporter.specFilter(spec);
    };

    var currentWindowOnload = window.onload;
    window.onload = function() {
      if (currentWindowOnload) {
        currentWindowOnload();
      }
      execJasmine();
    };

    function execJasmine() {
      jasmineEnv.execute();
    }
  })();
</script>

generator02.js

クラス EBNFParser のソースコードです。パーサーを生成するためのツールです。

以下の部分では HTML にある EBNF の構文から EBNFParser クラスの一番上位のメソッド syntax() を呼び出してパーサーを生成し HTML に格納しています。今回は空白の読み飛ばしが不要な id(非終端記号) を gapFree という引数で EBNFParser のコンストラクタに渡しています。

window.onload = function() {
	// define EBNF syntax
	var syntax = document.querySelector('#syntax');
	var gapFree = ["idl", "id", "terminal"];
	var ebnfSyntax = "syntax = rule, {rule}.\n";
	ebnfSyntax += "rule = idl, '=', alternative, '.'.\n";
	ebnfSyntax += "alternative = sequence, {'|', sequence}.\n";
	ebnfSyntax += "sequence = term, {',', term}.\n";
	ebnfSyntax += "term = primary.\n";
	ebnfSyntax += "primary = terminal | id | repeat | option | group.\n";
	ebnfSyntax += "terminal = sq, character, {character}, sq.\n";
	ebnfSyntax += "idl = letter, {letter | digit}.\n";
	ebnfSyntax += "id = letter, {letter | digit}.\n";
	ebnfSyntax += "option = '[', alternative, ']'.\n";
	ebnfSyntax += "repeat = '{', alternative, '}'.\n";
	ebnfSyntax += "group = '(', alternative, ')'.\n";
	ebnfSyntax += "letter = lower | upper.\n";
	ebnfSyntax += "character = symbol | letter | digit.\n";
	ebnfSyntax += "symbol = '=' | ',' | '.' | '|' |\n";
	ebnfSyntax += "         '[' | ']' | '{' | '}' | '(' | ')'.\n";
	syntax.innerHTML = ebnfSyntax;
	var ebnf = new EBNFParser(ebnfSyntax, gapFree, "EBNF", "0.2");
	// get element from HTML and set text (code)
	var result = document.querySelector('#result');
	result.innerHTML = ebnf.syntax();
};
以下コードはパーサーで解析した結果からコードを生成する部分です。ランナーと命名しました。 この部分を変更することでジェネレーター、インタープリターなどさまざまな出力を行うことができます。 今回は空白を読み飛ばす処理と、大文字と小文字を区別するかしないかを変更する処理を追加しました。
/**
 * Parser Runner (Generator) for EBNF Syntax
 * @param {String} n number of match
 * @param {Object} match array of parsed result
 * @param {String} parser caller method name
 * @return {String} generated code
 * @since 0.2
 */
EBNFParser.prototype.run = function(n, match, parser) {
	var code = "";
	if (parser == "syntax") {
		code += "/**\n";
		code += " * @fileOverview " + this.name + "Parser - " + this.name + " Parser Object\n";
		code += " * @version " + this.ver + "\n";
		code += " * @author EBNFParser written by Nonki Takahashi\n";
		code += " */\n";
		code += "\n";
		code += "/**\n";
		code += " * " + this.name + " Parser Generator Object\n";
		code += " * @this {" + this.name + "Parser}\n";
		code += " * @param {String} syntax syntax object for generate parser\n";
		code += " * @param {Object} gapFree array of gap free id\n";
		code += " * @param {String} name name of the parser\n";
		code += " * @param {String} ver version of the parser\n";
		code += " * @param {Boolean} ignoreCase true if ignore case (optional)\n";
		code += " * @property {String} buf syntax buffer\n";
		code += " * @property {Integer} ptr syntax buffer pointer\n";
		code += " * @property {Object} gapFree array of gap free id\n";
		code += " * @property {String} name name of the parser\n";
		code += " * @property {String} ver version of the parser\n";
		code += " * @property {Boolean} ignoreCase true if ignore case\n";
		code += " * @since " + this.ver + "\n";
		code += " */\n";
		code += this.name + "Parser = function(syntax, gapFree, name, ver, ignoreCase) {\n";
		code += "this.gapFree = gapFree;\n";
		code += "this.name = name;\n";
		code += "this.ver = ver;\n";
		code += "if (ignoreCase)\n";
		code += "this.ignoreCase = true;\n";
		code += "else\n";
		code += "this.ignoreCase = false;\n";
		code += "// inherit the methods of class Lex\n";
		code += "Lex.call(this, syntax);\n";
		code += "};\n";
		code += this.name + "Parser.prototype = new Lex();\n";
		code += "\n";
		for (var i = 0; i <= n; i++) {
			if (match[i] != true && match[i] != null)
				code += match[i];
		}
	} else if (parser == "rule") {
		code += "/**\n";
		var ptr = this.buf.lastIndexOf(match[0], this.ptr);
		code += " * " + this.buf.slice(ptr, this.ptr) + "\n";
		code += " * @returns {String} code generated if matched, null if not matched\n";
		code += " * @since " + this.ver + "\n";
		code += " */\n";
		code += this.name + "Parser.prototype." + match[0] + " = function() {\n";
		code += "var match = [];\n";
		code += "var save = this.ptr;\n";
		code += "var n = -1;\n";
		var inGapFree = false;
		for (var i in this.gapFree) {
			if (this.gapFree[i] == match[0]) {
				code += "this.sp();\n";
				inGapFree = true;
				break;
			}
		}
		if (inGapFree)
			code += match[2].replace(/this\.sp\(\);\n/g, "");
		else
			code += match[2];
		code += "if (!match[n]) {\n";
		code += "this.ptr = save;\n";
		code += "return null;\n";
		code += "}\n";
		code += "return this.run(n, match, \"" + match[0] + "\");\n";
		code += "};\n\n";
	} else if (parser == "alternative") {
		var i = 0;
		code += match[i];
		for ( i = 2; i <= n; i += 2) {
			if (match[i] != true && match[i] != null) {
				code += "if (!match[n]) {\n";
				code += "n--;\n";
				code += match[i];
				code += "}\n";
			}
		}
	} else if (parser == "sequence") {
		var i = 0;
		code += "this.sp();\n";
		code += match[i];
		for ( i = 2; i <= n; i += 2) {
			if (match[i] != true && match[i] != null) {
				code += "if (match[n]) {\n";
				code += "this.sp();\n";
				code += match[i];
				code += "}\n";
			}
		}
	} else if (parser == "term" || parser == "primary") {
		code += match[0];
	} else if (parser == "terminal") {
		code += "match[++n] = this.text(";
		for (var i = 0; i <= n; i++) {
			if (match[i] != true && match[i] != null)
				code += match[i];
		}
		code += ");\n";
	} else if (parser == "idl") {
		for (var i = 0; i <= n; i++) {
			if (match[i] != true && match[i] != null)
				code += match[i];
		}
	} else if (parser == "id") {
		code += "match[++n] = this.";
		for (var i = 0; i <= n; i++) {
			if (match[i] != true && match[i] != null)
				code += match[i];
		}
		code += "();\n";
	} else if (parser == "option") {
		code += match[1];
		code += "match[++n] = true;\n";
	} else if (parser == "repeat") {
		code += "while(match[n]) {\n";
		code += match[1];
		code += "}\n";
		code += "match[++n] = true;\n";
	} else if (parser == "group") {
		code += match[1];
	} else if (parser == "letter" || parser == "character" || parser == "symbol") {
		code += match[0];
	}
	return code;
};
詳細は、リンク先をご覧下さい。

parser02.js

クラス EBNFParser のソースコードです。パーサージェネレーターによって生成されたコードです。 字下げは Aptana Studio 3 でメニュー Source > Format を選択して行いました。また著作権のコメントを追加しています。 リンク先をご覧下さい。

lex03.js

クラス Lex のソースコードです。字句解析のためのツールです。メソッド text() を追加しました。このメソッドの中で大文字・小文字を区別するかどうか指定するようにしました。 リンク先をご覧下さい。

source01.js

クラス Source のソースコードです。Lex, Parser の対象となるテキストを格納するクラスです。前回から変更ありません。 リンク先をご覧下さい。

sourcespec01.js

このソースでは Source のテスト仕様を定義しています。前回から変更ありません。
describe("Source 仕様 0.1", function() {
  var exp = "1+2=";
  var src;

  beforeEach(function() {
    src = new Source(exp);	// Lexer and Parser Source
  });

  it("eod() は false を返す", function() {
    expect(src.eod()).toBeFalsy();
  });

  describe("先頭でpush()しptr++したとき、", function() {
    beforeEach(function() {
      src.rewind();
      src.push();
      src.ptr++;
    });

    it("pop(true)後、ptrは 0 を返す", function() {
      var p = src.ptr;
      src.pop(true);
      expect(src.ptr).toEqual(0);
    });

    it("pop(false)後、ptrは 1 を返す", function() {
      var p = src.ptr;
      src.pop(false);
      expect(src.ptr).toEqual(1);
    });

  });

});

lexspec03.js

このソースでは Lex のテスト仕様を定義しています。メソッド text() のテストを追加しました。
describe("Lex 仕様 0.3", function() {
  var exp = "1+2=";
  var la;

  beforeEach(function() {
    la = new Lex(exp);	// Lexical Analiser
  });

  it("eod() は false を返す", function() {
    expect(la.eod()).toBeFalsy();
  });

  it("digit() は '1' を返す", function() {
    expect(la.digit()).toEqual('1');
  });

  it("upper() は null を返す", function() {
    expect(la.upper()).toBe(null);
  });

  it("lower() は null を返す", function() {
    expect(la.lower()).toBe(null);
  });

  it("sp() は null を返す", function() {
    expect(la.sp()).toBeFalsy();
  });

  it("sq() は null を返す", function() {
    expect(la.sq()).toBeFalsy();
  });

  it("ch('1') は '1' を返す", function() {
    expect(la.ch('1')).toEqual('1');
  });

  it("text('1') は '1' を返す", function() {
    expect(la.text('1')).toBe('1');
  });

  it("text('1+') は '1+' を返す", function() {
    expect(la.text('1+')).toBe('1+');
  });

  it("text('2') は null を返す", function() {
    expect(la.text('2')).toBe(null);
  });

  describe("3文字読んだ時点で、", function() {
    beforeEach(function() {
      la.rewind();
      la.digit();
      la.ch('+');
      la.digit();
    });

    it("eod() は false を返す", function() {
      expect(la.eod()).toBe(false);
    });

    it("digit() は null を返す", function() {
      expect(la.digit()).toBe(null);
    });

    it("upper() は null を返す", function() {
      expect(la.upper()).toBe(null);
    });

    it("lower() は null を返す", function() {
      expect(la.lower()).toEqual(null);
    });

    it("ch('=') は '=' を返す", function() {
      expect(la.ch('=')).toBe('=');
    });

  });

  describe("4文字読んだ時点で、", function() {
    beforeEach(function() {
      la.rewind();
      la.digit();
      la.ch('+');
      la.digit();
      la.ch('=');
    });

    it("eod() は true を返す", function() {
      expect(la.eod()).toBeTruthy();
    });

  });

});

generatorspec02.js

このソースでは Generator のテスト仕様を定義しています。新たな仕様に合わせて書き直しました。
describe("Generator 仕様 0.2", function() {
  var g = new EBNFParser("syntax = {line, nl}.", [], "Test", "0.2");
  var code = "/**\n";
  code += " * @fileOverview TestParser - Test Parser Object\n";
  code += " * @version 0.2\n";
  code += " * @author EBNFParser written by Nonki Takahashi\n";
  code += " */\n";
  code += "\n";
  code += "/**\n";
  code += " * Test Parser Generator Object\n";
  code += " * @this {TestParser}\n";
  code += " * @param {String} syntax syntax object for generate parser\n";
  code += " * @param {Object} gapFree array of gap free id\n";
  code += " * @param {String} name name of the parser\n";
  code += " * @param {String} ver version of the parser\n";
  code += " * @param {Boolean} ignoreCase true if ignore case (optional)\n";
  code += " * @property {String} buf syntax buffer\n";
  code += " * @property {Integer} ptr syntax buffer pointer\n";
  code += " * @property {Object} gapFree array of gap free id\n";
  code += " * @property {String} name name of the parser\n";
  code += " * @property {String} ver version of the parser\n";
  code += " * @property {Boolean} ignoreCase true if ignore case\n";
  code += " * @since 0.2\n";
  code += " */\n";
  code += "TestParser = function(syntax, gapFree, name, ver, ignoreCase) {\n";
  code += "this.gapFree = gapFree;\n";
  code += "this.name = name;\n";
  code += "this.ver = ver;\n";
  code += "if (ignoreCase)\n";
  code += "this.ignoreCase = true;\n";
  code += "else\n";
  code += "this.ignoreCase = false;\n";
  code += "// inherit the methods of class Lex\n";
  code += "Lex.call(this, syntax);\n";
  code += "};\n";
  code += "TestParser.prototype = new Lex();\n";
  code += "\n";
  code += "/**\n";
  code += " * syntax = {line, nl}.\n";
  code += " * @returns {String} code generated if matched, null if not matched\n";
  code += " * @since 0.2\n";
  code += " */\n";
  code += "TestParser.prototype.syntax = function() {\n";
  code += "var match = [];\n";
  code += "var save = this.ptr;\n";
  code += "var n = -1;\n";
  code += "this.sp();\n";
  code += "while(match[n]) {\n";
  code += "this.sp();\n";
  code += "match[++n] = this.line();\n";
  code += "if (match[n]) {\n";
  code += "this.sp();\n";
  code += "match[++n] = this.nl();\n";
  code += "}\n";
  code += "}\n";
  code += "match[++n] = true;\n";
  code += "if (!match[n]) {\n";
  code += "this.ptr = save;\n";
  code += "return null;\n";
  code += "}\n";
  code += "return this.run(n, match, \"syntax\");\n";
  code += "};\n\n";

  it("syntax() は " + code + " を返す", function() {
    expect(g.syntax()).toEqual(code);
  });

});

テスト結果