Repository: syuilo/aiscript (ts)

Issue:
エディション明示構文
今後AiScriptがバージョンアップで構文が変わったりしたときの対応を容易にするために、スクリプトの先頭にエディションを明示できる構文を追加する

```
@ 2021
```

とか

Pull request description:
add lang version syntax
<!-- ℹ お読みください
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? -->

Resolve #154 

# Why
<!-- なぜそうするのか？ どういう意図なのか？ 何が困っているのか？ -->
<!-- Why do you do it? What are your intentions? What is the problem? -->

# Additional info (optional)
<!-- テスト観点など -->
<!-- Test perspective, etc -->


Patch:
diff --git a/README.md b/README.md
index 577cd72..d75e488 100644
--- a/README.md
+++ b/README.md
@@ -41,9 +41,9 @@ todo
 ### Fizz Buzz
 ```
 for (#i, 100) {
-  <: if ((i % 15) = 0) "FizzBuzz"
-    elif ((i % 3) = 0) "Fizz"
-    elif ((i % 5) = 0) "Buzz"
+  <: if ((i % 15) == 0) "FizzBuzz"
+    elif ((i % 3) == 0) "Fizz"
+    elif ((i % 5) == 0) "Buzz"
     else i
 }
 ```
diff --git a/etc/aiscript.api.md b/etc/aiscript.api.md
index 130f2ce..434839b 100644
--- a/etc/aiscript.api.md
+++ b/etc/aiscript.api.md
@@ -103,6 +103,9 @@ const FN_NATIVE: (fn: VFn['native']) => {
     }) => Value | Promise<Value> | void) | undefined;
 };
 
+// @public (undocumented)
+function getLangVersion(input: string): string | null;
+
 // @public (undocumented)
 function jsToVal(val: any): Value;
 
@@ -190,7 +193,8 @@ declare namespace utils {
         valToString,
         nodeToString,
         valToJs,
-        jsToVal
+        jsToVal,
+        getLangVersion
     }
 }
 export { utils }
diff --git a/src/interpreter/infix-to-fncall.ts b/src/interpreter/infix-to-fncall.ts
index 20c5894..bade491 100644
--- a/src/interpreter/infix-to-fncall.ts
+++ b/src/interpreter/infix-to-fncall.ts
@@ -1,9 +1,8 @@
 import { AiScriptError } from './error';
-import { Node, NInfix, NOperator, NCall, Loc } from '../node';
+import { Node, NInfix, NCall } from '../node';
 
 type OperatorInfo = {
 	func: string; // 対応する関数
-	loc?: Loc;
 	prec: number; // 優先度（高いほど優先して計算される値）
 };
 
@@ -15,34 +14,15 @@ type OperatorInfo = {
  * (1) (3)
  * ```
  */
-type InfixTree = Node | {
+type InfixTree = {
 	type: 'infixtree';
 	opInfo: OperatorInfo;
-	lchild: InfixTree;
-	rchild: InfixTree;
+	left: InfixTree | Node;
+	right: InfixTree | Node;
 };
 
-function makeOperatorInfo(operator: NOperator): OperatorInfo {
-	switch (operator.op) {
-		case '+': return { func: 'Core:add', prec: 6, loc: operator.loc };
-		case '-': return { func: 'Core:sub', prec: 6, loc: operator.loc };
-		case '*': return { func: 'Core:mul', prec: 7, loc: operator.loc };
-		case '/': return { func: 'Core:div', prec: 7, loc: operator.loc };
-		case '%': return { func: 'Core:mod', prec: 7, loc: operator.loc };
-		case '==': return { func: 'Core:eq', prec: 4, loc: operator.loc };
-		case '!=': return { func: 'Core:neq', prec: 4, loc: operator.loc };
-		case '&&': return { func: 'Core:and', prec: 3, loc: operator.loc };
-		case '||': return { func: 'Core:or', prec: 2, loc: operator.loc };
-		case '<': return { func: 'Core:lt', prec: 4, loc: operator.loc };
-		case '>': return { func: 'Core:gt', prec: 4, loc: operator.loc };
-		case '<=': return { func: 'Core:lteq', prec: 4, loc: operator.loc };
-		case '>=': return { func: 'Core:gteq', prec: 4, loc: operator.loc };
-		default: throw new AiScriptError(`No such operator: ${operator.op}.`);
-	}
-}
-
-function makeInfixTree(opInfo: OperatorInfo, lchild: InfixTree, rchild: InfixTree): InfixTree {
-	return { opInfo, lchild, rchild, type: 'infixtree' };
+function INFIX_TREE(opInfo: OperatorInfo, left: InfixTree | Node, right: InfixTree | Node): InfixTree {
+	return { type: 'infixtree', opInfo, left, right };
 }
 
 /**
@@ -68,45 +48,63 @@ function makeInfixTree(opInfo: OperatorInfo, lchild: InfixTree, rchild: InfixTre
  * - NOTE: 右結合性の演算子としては代入演算子などが挙げられる
  * - NOTE: 比較の演算子などは非結合性とされる
  */
-function insert(tree: InfixTree, nextOpInfo: OperatorInfo, nextTree: InfixTree): InfixTree {
+function insert(tree: InfixTree | Node, nextOpInfo: OperatorInfo, nextTree: InfixTree | Node): InfixTree {
 	if (tree.type !== 'infixtree') {
-		return makeInfixTree(nextOpInfo, tree, nextTree);
-	} else if (tree.opInfo.prec >= nextOpInfo.prec) {
-		return makeInfixTree(nextOpInfo, tree, nextTree);
-	} else {
-		const { opInfo, lchild, rchild } = tree;
-		return makeInfixTree(opInfo, lchild, insert(rchild, nextOpInfo, nextTree));
+		return INFIX_TREE(nextOpInfo, tree, nextTree);
+	}
+	if (tree.opInfo.prec >= nextOpInfo.prec) {
+		return INFIX_TREE(nextOpInfo, tree, nextTree);
+	}
+	const { opInfo, left, right } = tree;
+	return INFIX_TREE(opInfo, left, insert(right, nextOpInfo, nextTree));
+}
+
+const opInfoTable = {
+	'+': { func: 'Core:add', prec: 6 },
+	'-': { func: 'Core:sub', prec: 6 },
+	'*': { func: 'Core:mul', prec: 7 },
+	'/': { func: 'Core:div', prec: 7 },
+	'%': { func: 'Core:mod', prec: 7 },
+	'==': { func: 'Core:eq', prec: 4 },
+	'!=': { func: 'Core:neq', prec: 4 },
+	'&&': { func: 'Core:and', prec: 3 },
+	'||': { func: 'Core:or', prec: 2 },
+	'<': { func: 'Core:lt', prec: 4 },
+	'>': { func: 'Core:gt', prec: 4 },
+	'<=': { func: 'Core:lteq', prec: 4 },
+	'>=': { func: 'Core:gteq', prec: 4 },
+};
+
+function getOpInfo(operator: string): OperatorInfo {
+	const op = opInfoTable[operator];
+	if (op == null) {
+		throw new AiScriptError(`No such operator: ${operator}.`);
 	}
+	return op;
 }
 
 /**
  * 中置演算子式を表す木を対応する関数呼び出しの構造体に変換する
  */
-function ascall(tree: InfixTree): Node {
+function asCall(tree: InfixTree | Node): Node {
 	if (tree.type !== 'infixtree') {
 		return tree;
-	} else {
-		return {
-			type: 'call',
-			loc: tree.opInfo.loc,
-			name: tree.opInfo.func,
-			args: [ ascall(tree.lchild), ascall(tree.rchild) ],
-		} as NCall;
 	}
+	return {
+		type: 'call',
+		name: tree.opInfo.func,
+		args: [ asCall(tree.left), asCall(tree.right) ],
+	} as NCall;
 }
 
 /**
  * NInfix を関数呼び出し形式に変換する
  */
 export function infixToFnCall(node: NInfix): Node {
-	const infos = node.operators.map(makeOperatorInfo);
-	const nodes = node.operands;
-
-	let tree = makeInfixTree(infos[0], nodes[0], nodes[1]);
-
-	for (let i = 1; i < infos.length; i++) {
-		tree = insert(tree, infos[i], nodes[i + 1]);
+	const opInfos = node.operators.map(op => getOpInfo(op));
+	let tree = INFIX_TREE(opInfos[0], node.operands[0], node.operands[1]);
+	for (let i = 1; i < opInfos.length; i++) {
+		tree = insert(tree, opInfos[i], node.operands[i + 1]);
 	}
-
-	return ascall(tree);
+	return asCall(tree);
 }
diff --git a/src/interpreter/util.ts b/src/interpreter/util.ts
index da0be8d..c5787d1 100644
--- a/src/interpreter/util.ts
+++ b/src/interpreter/util.ts
@@ -112,3 +112,8 @@ export function jsToVal(val: any): Value {
 	}
 	return NULL;
 }
+
+export function getLangVersion(input: string): string | null {
+	const match = /^\s*\/\/\s*@\s*([A-Z0-9_.-]+)(?:[\r\n][\s\S]*)?$/i.exec(input);
+	return (match != null) ? match[1] : null;
+}
diff --git a/src/node.ts b/src/node.ts
index e63ed69..ba5a45e 100644
--- a/src/node.ts
+++ b/src/node.ts
@@ -233,15 +233,9 @@ export type NInfix = {
 	type: 'infix'; // 中置演算子式
 	loc?: Loc; // コード位置
 	operands: Node[]; // 項
-	operators: NOperator[]; // 演算子
+	operators: string[]; // 演算子
 };
 
-export type NOperator = {
-	type: 'operator'; // 中置演算子
-	loc?: Loc; // コード位置
-	op: string; // 実際の記号列
-}
-
 // TODO: analyze前のNodeとanalyze後のNodeが区別されておらず気持ち悪いのをなんとかしたい
 export type Node =
 	NDef |
@@ -275,5 +269,4 @@ export type Node =
 	NNs |
 	NMeta |
 	NAttr |
-	NInfix |
-	NOperator;
+	NInfix;
diff --git a/src/parser/parser.peggy b/src/parser/parser.peggy
index 835b47c..13d5d63 100644
--- a/src/parser/parser.peggy
+++ b/src/parser/parser.peggy
@@ -344,7 +344,7 @@ ReservedOps
 	= "<<<" / "=>" / "<-" / "+<-" / "-<-"
 
 Op
-	= !(ReservedOps ![-+*/%&|=~<>!?]) [-+*/%&|=~<>!?]+ { return createNode('operator', { op: text() }); }
+	= !(ReservedOps ![-+*/%&|=~<>!?]) [-+*/%&|=~<>!?]+ { return text(); }
 
 // if statement
 
diff --git a/src/parser/plugins/validate-keyword.ts b/src/parser/plugins/validate-keyword.ts
index 88ab2ee..04c9459 100644
--- a/src/parser/plugins/validate-keyword.ts
+++ b/src/parser/plugins/validate-keyword.ts
@@ -22,7 +22,6 @@ const reservedWord = [
 	// 'attribute',
 	// 'static',
 	// 'const',
-	// 'null',
 	// 'var',
 	// 'def',
 	// 'fn',
diff --git a/src/serializer.ts b/src/serializer.ts
index 5972203..40bbda3 100644
--- a/src/serializer.ts
+++ b/src/serializer.ts
@@ -1,4 +1,4 @@
-import { Node, NDef, NAssign, NCall, NReturn, NIf, NFor, NForOf, NVar, NNull, NBool, NNum, NStr, NArr, NFn, NObj, NProp, NPropCall, NIndex, NBlock, NTmpl, NNs, NMatch, NMeta, NPropAssign, NIndexAssign, NInc, NDec, NAttr, NBreak, NLoop, NContinue, NInfix, NOperator } from './node';
+import { Node, NDef, NAssign, NCall, NReturn, NIf, NFor, NForOf, NVar, NNull, NBool, NNum, NStr, NArr, NFn, NObj, NProp, NPropCall, NIndex, NBlock, NTmpl, NNs, NMatch, NMeta, NPropAssign, NIndexAssign, NInc, NDec, NAttr, NBreak, NLoop, NContinue, NInfix } from './node';
 
 const types = {
 	def: 0,
@@ -33,7 +33,6 @@ const types = {
 	loop: 29,
 	continue: 30,
 	infix: 31,
-	operator: 32,
 };
 
 type Bin = any[];
@@ -72,8 +71,7 @@ export function serializeOne(node: Node | null | undefined): Bin | null {
 		case 'break': return [types[node.type], null];
 		case 'loop': return [types[node.type], serialize(node.statements)];
 		case 'continue': return [types[node.type], null];
-		case 'infix': return [types[node.type], serialize(node.operands), serialize(node.operators)];
-		case 'operator': return [types[node.type], node.op];
+		case 'infix': return [types[node.type], serialize(node.operands), node.operators];
 	}
 }
 
@@ -116,8 +114,7 @@ export function deserializeOne(bin: Bin | null): Node | undefined {
 		case types.break: return { type, name: bin[1], value: deserializeOne(null) } as NBreak;
 		case types.loop: return { type, statements: deserialize(bin[1]), } as NLoop;
 		case types.continue: return { type, name: bin[1], value: deserializeOne(null) } as NContinue;
-		case types.infix: return { type, operands: deserialize(bin[1]), operators: deserialize(bin[2])} as NInfix;
-		case types.operator: return { type, op: bin[1] } as NOperator;
+		case types.infix: return { type, operands: deserialize(bin[1]), operators: bin[2]} as NInfix;
 	}
 	throw new Error('deserialization failed');
 }


Test patch:
diff --git a/test/index.ts b/test/index.ts
index b113857..d00a596 100644
--- a/test/index.ts
+++ b/test/index.ts
@@ -4,7 +4,7 @@
  */
 
 import * as assert from 'assert';
-import { parse, Parser, serialize, deserialize } from '../src';
+import { parse, Parser, serialize, deserialize, utils } from '../src';
 import { AiScript } from '../src/interpreter';
 import { NUM, STR, NULL, ARR, OBJ, BOOL } from '../src/interpreter/value';
 import { NAttr, Node } from '../src/node';
@@ -1429,6 +1429,49 @@ describe('meta', () => {
 	});
 });
 
+describe('lang version', () => {
+	it('number', async () => {
+		const res = utils.getLangVersion(`
+		// @2021
+		@f(x) {
+			x
+		}
+		`);
+		assert.strictEqual(res, '2021');
+	});
+
+	it('chars', async () => {
+		const res = utils.getLangVersion(`
+		// @ canary
+		const a = 1
+		@f(x) {
+			x
+		}
+		f(a)
+		`);
+		assert.strictEqual(res, 'canary');
+	});
+
+	it('complex', async () => {
+		const res = utils.getLangVersion(`
+		// @ 2.0-Alpha
+		@f(x) {
+			x
+		}
+		`);
+		assert.strictEqual(res, '2.0-Alpha');
+	});
+
+	it('no specified', async () => {
+		const res = utils.getLangVersion(`
+		@f(x) {
+			x
+		}
+		`);
+		assert.strictEqual(res, null);
+	});
+});
+
 describe('Attribute', () => {
 	it('single attribute with function (str)', async () => {
 		let node: Node;