Repository: syuilo/aiscript
Pull request #141 (2021-08-14 01:40:53)

modify APIs
<!-- ℹ お読みください
PRありがとうございます！ PRを作成する前に、以下をご確認ください:
- 可能であればタイトルに、以下で示すようなPRの種類が分かるキーワードをプリフィクスしてください。
  - fix / refactor / feat / enhance / perf / chore
  - また、PRの粒度が適切であることを確認してください。ひとつのPRに複数の種類の変更や関心を含めることは避けてください。
- このPRによって解決されるIssueがある場合は、そのIssueへの参照を本文内に含めてください。
- CHANGELOG.mdに変更点を追記してください。リファクタリングなど、利用者に影響を与えない変更についてはこの限りではありません。
- この変更により新たに作成、もしくは更新すべきドキュメントがないか確認してください。
- 機能追加やバグ修正をした場合は、可能であればテストケースを追加してください。
- テスト、Lintが通っていることを予め確認してください。
  - `npm run test`、`npm run lint`でぞれぞれ実施可能です
- `npm run api`を実行してAPIレポートを更新し、差分がある場合はコミットしてください。
ご協力ありがとうございます🤗
-->
<!-- ℹ README
Thank you for your PR! Before creating a PR, please check the following:
- If possible, prefix the title with a keyword that identifies the type of this PR, as shown below.
  - fix / refactor / feat / enhance / perf / chore
  - Also, make sure that the granularity of this PR is appropriate. Please do not include more than one type of change or interest in a single PR.
- If there is an Issue which will be resolved by this PR, please include a reference to the Issue in the text.
- Please add the summary of the changes to CHANGELOG.md. However, this is not necessary for changes that do not affect the users, such as refactoring.
- Check if there are any documents that need to be created or updated due to this change.
- If you have added a feature or fixed a bug, please add a test case if possible.
- Please make sure that tests and Lint are passed in advance.
  - You can run it with `npm run test` and `npm run lint`.
- Run `npm run api` to update the API report and commit it if there are any diffs.
Thanks for your cooperation 🤗
-->

# What
<!-- このPRで何をしたのか？ どう変わるのか？ -->
<!-- What did you do with this PR? How will it change things? -->

For #111 

* Parser APIの一新
* Analyze APIの廃止、意味解析処理はParser APIにパーサプラグインとして統合
* 旧parse APIの互換性テストの追加
* テストのAPI呼び出し処理の更新

これまでのparse関数の互換性は維持できそう。

# Why
<!-- なぜそうするのか？ どういう意図なのか？ 何が困っているのか？ -->
<!-- Why do you do it? What are your intentions? What is the problem? -->

# Additional info (optional)
<!-- テスト観点など -->
<!-- Test perspective, etc -->



Patch:
diff --git a/etc/aiscript.api.md b/etc/aiscript.api.md
index 12740b2..130f2ce 100644
--- a/etc/aiscript.api.md
+++ b/etc/aiscript.api.md
@@ -29,11 +29,6 @@ export class AiScript {
     unregisterAbortHandler(handler: () => void): void;
 }
 
-// Warning: (ae-forgotten-export) The symbol "RawAST" needs to be exported by the entry point index.d.ts
-//
-// @public (undocumented)
-export function analyze(ast: RawAST): Node_2[];
-
 // @public (undocumented)
 const ARR: (arr: VArr['value']) => {
     type: "arr";
@@ -132,7 +127,21 @@ const OBJ: (obj: VObj['value']) => {
 };
 
 // @public (undocumented)
-export function parse(input: string): RawAST;
+export function parse(input: string): Node_2[];
+
+// @public (undocumented)
+export class Parser {
+    constructor();
+    // (undocumented)
+    addPlugin(plugin: ParserPlugin): void;
+    // (undocumented)
+    static parse(input: string): Node_2[];
+    // (undocumented)
+    parse(input: string): Node_2[];
+}
+
+// @public (undocumented)
+export type ParserPlugin = (nodes: Node_2[]) => Node_2[];
 
 // @public (undocumented)
 const RETURN: (v: VReturn['value']) => {
diff --git a/parse.js b/parse.js
index 3359301..33aeac0 100644
--- a/parse.js
+++ b/parse.js
@@ -1,5 +1,5 @@
 const fs = require('fs');
-const { parse, analyze } = require('./built/index.js');
+const { parse } = require('./built/index.js');
 
 function transformMapToObj(key, value) {
 	if (Object.prototype.toString.call(value) == '[object Map]') {
@@ -16,5 +16,5 @@ function transformMapToObj(key, value) {
 }
 
 const script = fs.readFileSync('./test.is', 'utf8');
-const ast = analyze(parse(script));
+const ast = parse(script);
 console.log(JSON.stringify(ast, transformMapToObj, 2));
diff --git a/playground/src/App.vue b/playground/src/App.vue
index b3589dc..a25455e 100644
--- a/playground/src/App.vue
+++ b/playground/src/App.vue
@@ -43,7 +43,7 @@
 
 <script setup>
 import { ref, watch } from 'vue';
-import { AiScript, parse, analyze, utils, serialize } from '../../src';
+import { AiScript, Parser, utils, serialize } from '../../src';
 
 import { PrismEditor } from 'vue-prism-editor';
 import 'vue-prism-editor/dist/prismeditor.min.css';
@@ -63,7 +63,7 @@ const isSyntaxError = ref(false);
 watch(script, () => {
 	window.localStorage.setItem('script', script.value);
 	try {
-		ast.value = analyze(parse(script.value));
+		ast.value = Parser.parse(script.value);
 		isSyntaxError.value = false;
 	} catch (e) {
 		isSyntaxError.value = true;
diff --git a/src/analyzer/index.ts b/src/analyzer/index.ts
deleted file mode 100644
index a39213e..0000000
--- a/src/analyzer/index.ts
+++ /dev/null
@@ -1,15 +0,0 @@
-import { Node } from '../node';
-import { RawAST } from '../parser';
-import { setAttribute } from './set-attribute';
-import { validateKeyword } from './validate-keyword';
-
-export function analyze(ast: RawAST): Node[] {
-
-	// validation
-	validateKeyword(ast.nodes);
-
-	// transform
-	const nodes = setAttribute(ast.nodes);
-
-	return nodes;
-}
diff --git a/src/index.ts b/src/index.ts
index 2cf8103..d35b2f5 100644
--- a/src/index.ts
+++ b/src/index.ts
@@ -8,7 +8,6 @@ import * as values from './interpreter/value';
 export { AiScript };
 export { utils };
 export { values };
-export { parse } from './parser';
-export { analyze } from './analyzer';
+export { Parser, ParserPlugin, parse } from './parser';
 export { serialize, deserialize } from './serializer';
 export { SyntaxError, SemanticError } from './error';
diff --git a/src/parser/index.ts b/src/parser/index.ts
index 36f6b9f..a42e1ec 100644
--- a/src/parser/index.ts
+++ b/src/parser/index.ts
@@ -2,24 +2,57 @@ import * as aiscript from '..';
 import { Node } from '../node';
 import * as parser from './parser.js';
 
-const parseInternal = parser.parse;
-
-export type RawAST = {
-	nodes: Node[];
-};
-
-export function parse(input: string): RawAST {
-	let nodes: Node[];
-	try {
-		nodes = parseInternal(input);
-	} catch (e) {
-		if (e.location) {
-			throw new aiscript.SyntaxError(`Line ${e.location.start.line}:${e.location.start.column}`);
+import { validateKeyword } from './plugins/validate-keyword';
+import { setAttribute } from './plugins/set-attribute';
+
+export type ParserPlugin = (nodes: Node[]) => Node[];
+
+export class Parser {
+	private static instance?: Parser;
+	private plugins: ParserPlugin[];
+
+	constructor() {
+		this.plugins = [
+			validateKeyword,
+			setAttribute,
+		];
+	}
+
+	public static parse(input: string): Node[] {
+		if (Parser.instance == null) {
+			Parser.instance = new Parser();
+		}
+		return Parser.instance.parse(input);
+	}
+
+	public addPlugin(plugin: ParserPlugin) {
+		this.plugins.push(plugin);
+	}
+
+	public parse(input: string): Node[] {
+		let nodes: Node[];
+
+		// generate a node tree
+		try {
+			nodes = parser.parse(input);
+		} catch (e) {
+			if (e.location) {
+				throw new aiscript.SyntaxError(`Line ${e.location.start.line}:${e.location.start.column}`);
+			}
+			throw e;
+		}
+
+		// validate and transform the node tree
+		for (const plugin of this.plugins) {
+			nodes = plugin(nodes);
 		}
-		throw e;
+
+		return nodes;
 	}
+}
+
+// alias of legacy api
 
-	return {
-		nodes
-	};
+export function parse(input: string): Node[] {
+	return Parser.parse(input);
 }
diff --git a/src/analyzer/set-attribute.ts b/src/parser/plugins/set-attribute.ts
similarity index 92%
rename from src/analyzer/set-attribute.ts
rename to src/parser/plugins/set-attribute.ts
index d6183c7..26f003f 100644
--- a/src/analyzer/set-attribute.ts
+++ b/src/parser/plugins/set-attribute.ts
@@ -1,5 +1,5 @@
-import * as aiscript from '..';
-import { Node, NAttr } from '../node';
+import * as aiscript from '../..';
+import { Node, NAttr } from '../../node';
 
 export function setAttribute(nodes: Node[]): Node[] {
 	const result: Node[] = [];
diff --git a/src/analyzer/validate-keyword.ts b/src/parser/plugins/validate-keyword.ts
similarity index 94%
rename from src/analyzer/validate-keyword.ts
rename to src/parser/plugins/validate-keyword.ts
index 959a4cc..fc0bebd 100644
--- a/src/analyzer/validate-keyword.ts
+++ b/src/parser/plugins/validate-keyword.ts
@@ -1,5 +1,5 @@
-import * as aiscript from '..';
-import { Node } from '../node';
+import * as aiscript from '../..';
+import { Node } from '../../node';
 
 const reservedWord = [
 	'null',


Test patch:
diff --git a/test/index.ts b/test/index.ts
index 4bde4e9..6bc6d10 100644
--- a/test/index.ts
+++ b/test/index.ts
@@ -4,7 +4,7 @@
  */
 
 import * as assert from 'assert';
-import { parse, analyze, serialize, deserialize } from '../src';
+import { parse, Parser, serialize, deserialize } from '../src';
 import { AiScript } from '../src/interpreter';
 import { NUM, STR, NULL, ARR, OBJ, BOOL } from '../src/interpreter/value';
 import { NAttr, Node } from '../src/node';
@@ -16,7 +16,8 @@ const exe = (program: string): Promise<any> => new Promise((ok, err) => {
 		},
 	});
 
-	const ast = analyze(parse(program));
+	const parser = new Parser();
+	const ast = parser.parse(program);
 
 	const _ast = deserialize(serialize(ast));
 
@@ -24,7 +25,8 @@ const exe = (program: string): Promise<any> => new Promise((ok, err) => {
 });
 
 const getMeta = (program: string) => {
-	const ast = analyze(parse(program));
+	const parser = new Parser();
+	const ast = parser.parse(program);
 
 	const metadata = AiScript.collectMetadata(ast);
 
@@ -42,11 +44,30 @@ it('Hello, world!', async () => {
 });
 
 it('empty script', async () => {
-	const ast = analyze(parse(''));
+	const parser = new Parser();
+	const ast = parser.parse('');
 	const _ast = deserialize(serialize(ast));
 	assert.deepEqual(_ast, []);
 });
 
+it('legacy parser api', async () => {
+	const exeLegacy = (program: string): Promise<any> => new Promise((ok, err) => {
+		const aiscript = new AiScript({}, {
+			out(value) {
+				ok(value);
+			},
+		});
+
+		const ast = parse(program);
+		const _ast = deserialize(serialize(ast));
+
+		aiscript.exec(_ast).catch(err);
+	});
+
+	const res = await exeLegacy('<: "Hello, world!"');
+	eq(res, STR('Hello, world!'));
+});
+
 describe('ops', () => {
 	it('=', async () => {
 		eq(await exe('<: (1 = 1)'), BOOL(true));
@@ -1354,12 +1375,13 @@ describe('Attribute', () => {
 	it('single attribute with function (str)', async () => {
 		let node: Node;
 		let attr: NAttr;
-		const nodes = analyze(parse(`
+		const parser = new Parser();
+		const nodes = parser.parse(`
 		#[Event "Recieved"]
 		@onRecieved(data) {
 			data
 		}
-		`));
+		`);
 		assert.equal(nodes.length, 1);
 		node = nodes[0];
 		if (node.type != 'def') assert.fail();
@@ -1376,14 +1398,15 @@ describe('Attribute', () => {
 	it('multiple attributes with function (obj, str, bool)', async () => {
 		let node: Node;
 		let attr: NAttr;
-		const nodes = analyze(parse(`
+		const parser = new Parser();
+		const nodes = parser.parse(`
 		#[Endpoint { path: "/notes/create"; }]
 		#[Desc "Create a note."]
 		#[Cat yes]
 		@createNote(text) {
 			<: text
 		}
-		`));
+		`);
 		assert.equal(nodes.length, 1);
 		node = nodes[0];
 		if (node.type != 'def') assert.fail();
@@ -1425,9 +1448,10 @@ describe('Attribute', () => {
 describe('Location', () => {
 	it('function', async () => {
 		let node: Node;
-		const nodes = analyze(parse(`
+		const parser = new Parser();
+		const nodes = parser.parse(`
 		@f(a) { a }
-		`));
+		`);
 		assert.equal(nodes.length, 1);
 		node = nodes[0];
 		if (!node.loc) assert.fail();