Repository: aiscript-dev/aiscript (ts)

Issue:
式にマイナスを使えない
```
-foo
```
ができない

Pull request description:
feat: 式に符号を使えるように
<!-- ℹ お読みください
PRありがとうございます！ PRを作成する前に、以下をご確認ください:
- 可能であればタイトルに、以下で示すようなPRの種類が分かるキーワードをプリフィクスしてください。
  - fix / refactor / feat / enhance / perf / chore
  - また、PRの粒度が適切であることを確認してください。ひとつのPRに複数の種類の変更や関心を含めることは避けてください。
- このPRによって解決されるIssueがある場合は、そのIssueへの参照を本文内に含めてください。
- unreleasedディレクトリ以下にファイルを新規に作成し、変更点を記してください。リファクタリングなど、利用者に影響を与えない変更についてはこの限りではありません。
- この変更により新たに作成、もしくは更新すべきドキュメントがないか確認してください。
- 機能追加やバグ修正をした場合は、可能であればテストケースを追加してください。
- テスト、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 create a new file under the `unreleased` directory and write down the summary of your changes. 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? -->
単項演算子の正号 `+`・負号 `-`が数値リテラル以外の式にも使用できるようになります。
正号のノードは`Plus`、負号のノードは`Minus`になります。
符号の付いた数値リテラルは従来通り単一の数値リテラルとして扱います。

# Why
<!-- なぜそうするのか？ どういう意図なのか？ 何が困っているのか？ -->
<!-- Why do you do it? What are your intentions? What is the problem? -->
Resolve #43

# Additional info (optional)
<!-- テスト観点など -->
<!-- Test perspective, etc -->


Patch:
diff --git a/etc/aiscript.api.md b/etc/aiscript.api.md
index 95be0bb..da923ce 100644
--- a/etc/aiscript.api.md
+++ b/etc/aiscript.api.md
@@ -150,6 +150,8 @@ declare namespace Ast {
         SubAssign,
         Assign,
         Expression,
+        Plus,
+        Minus,
         Not,
         Pow,
         Mul,
@@ -298,7 +300,7 @@ type Exists = NodeBase & {
 function expectAny(val: Value | null | undefined): asserts val is Value;
 
 // @public (undocumented)
-type Expression = If | Fn | Match | Block | Exists | Tmpl | Str | Num | Bool | Null | Obj | Arr | Not | Pow | Mul | Div | Rem | Add | Sub | Lt | Lteq | Gt | Gteq | Eq | Neq | And | Or | Identifier | Call | Index | Prop;
+type Expression = If | Fn | Match | Block | Exists | Tmpl | Str | Num | Bool | Null | Obj | Arr | Plus | Minus | Not | Pow | Mul | Div | Rem | Add | Sub | Lt | Lteq | Gt | Gteq | Eq | Neq | And | Or | Identifier | Call | Index | Prop;
 
 // @public (undocumented)
 const FALSE: {
@@ -502,6 +504,12 @@ type Meta = NodeBase & {
     value: Expression;
 };
 
+// @public (undocumented)
+type Minus = NodeBase & {
+    type: 'minus';
+    expr: Expression;
+};
+
 // @public (undocumented)
 type Mul = NodeBase & {
     type: 'mul';
@@ -598,6 +606,12 @@ export type ParserPlugin = (nodes: Ast.Node[]) => Ast.Node[];
 // @public (undocumented)
 export type PluginType = 'validate' | 'transform';
 
+// @public (undocumented)
+type Plus = NodeBase & {
+    type: 'plus';
+    expr: Expression;
+};
+
 // @public
 type Pos = {
     line: number;
diff --git a/src/interpreter/index.ts b/src/interpreter/index.ts
index c314fd9..2d89264 100644
--- a/src/interpreter/index.ts
+++ b/src/interpreter/index.ts
@@ -642,6 +642,24 @@ export class Interpreter {
 				}
 			}
 
+			case 'plus': {
+				const v = await this._eval(node.expr, scope, callStack);
+				if (isControl(v)) {
+					return v;
+				}
+				assertNumber(v);
+				return v;
+			}
+
+			case 'minus': {
+				const v = await this._eval(node.expr, scope, callStack);
+				if (isControl(v)) {
+					return v;
+				}
+				assertNumber(v);
+				return NUM(-v.value);
+			}
+
 			case 'not': {
 				const v = await this._eval(node.expr, scope, callStack);
 				if (isControl(v)) {
@@ -813,7 +831,14 @@ export class Interpreter {
 				}
 			}
 
+			case 'namedTypeSource':
+			case 'fnTypeSource':
+			case 'attr': {
+				throw new Error('invalid node type');
+			}
+
 			default: {
+				node satisfies never;
 				throw new Error('invalid node type');
 			}
 		}
diff --git a/src/node.ts b/src/node.ts
index d8e9476..19e94e9 100644
--- a/src/node.ts
+++ b/src/node.ts
@@ -133,6 +133,8 @@ export type Expression =
 	Null |
 	Obj |
 	Arr |
+	Plus |
+	Minus |
 	Not |
 	Pow |
 	Mul |
@@ -162,6 +164,16 @@ export function isExpression(x: Node): x is Expression {
 	return expressionTypes.includes(x.type);
 }
 
+export type Plus = NodeBase & {
+	type: 'plus'; // 正号
+	expr: Expression; // 式
+};
+
+export type Minus = NodeBase & {
+	type: 'minus'; // 負号
+	expr: Expression; // 式
+};
+
 export type Not = NodeBase & {
 	type: 'not'; // 否定
 	expr: Expression; // 式
diff --git a/src/parser/syntaxes/expressions.ts b/src/parser/syntaxes/expressions.ts
index 8088768..df3d43a 100644
--- a/src/parser/syntaxes/expressions.ts
+++ b/src/parser/syntaxes/expressions.ts
@@ -69,24 +69,16 @@ function parsePrefix(s: ITokenStream, minBp: number): Ast.Expression {
 
 	switch (op) {
 		case TokenKind.Plus: {
-			// 数値リテラル以外は非サポート
 			if (expr.type === 'num') {
 				return NODE('num', { value: expr.value }, startPos, endPos);
-			} else {
-				throw new AiScriptSyntaxError('currently, sign is only supported for number literal.', startPos);
 			}
-			// TODO: 将来的にサポートされる式を拡張
-			// return NODE('plus', { expr }, startPos, endPos);
+			return NODE('plus', { expr }, startPos, endPos);
 		}
 		case TokenKind.Minus: {
-			// 数値リテラル以外は非サポート
 			if (expr.type === 'num') {
 				return NODE('num', { value: -1 * expr.value }, startPos, endPos);
-			} else {
-				throw new AiScriptSyntaxError('currently, sign is only supported for number literal.', startPos);
 			}
-			// TODO: 将来的にサポートされる式を拡張
-			// return NODE('minus', { expr }, startPos, endPos);
+			return NODE('minus', { expr }, startPos, endPos);
 		}
 		case TokenKind.Not: {
 			return NODE('not', { expr }, startPos, endPos);
@@ -237,7 +229,13 @@ function parseAtom(s: ITokenStream, isStatic: boolean): Ast.Expression {
 					case TokenKind.TemplateExprElement: {
 						// スキャナで埋め込み式として事前に読み取っておいたトークン列をパースする
 						const exprStream: ITokenStream = new TokenStream(element.children!);
+						if (exprStream.is(TokenKind.NewLine)) {
+							exprStream.next();
+						}
 						const expr = parseExpr(exprStream, false);
+						if (exprStream.is(TokenKind.NewLine)) {
+							exprStream.next();
+						}
 						exprStream.expect(TokenKind.EOF);
 						values.push(expr);
 						break;
diff --git a/src/parser/visit.ts b/src/parser/visit.ts
index 52bbf48..36fcf3a 100644
--- a/src/parser/visit.ts
+++ b/src/parser/visit.ts
@@ -49,8 +49,16 @@ function visitNodeInner(node: Ast.Node, fn: (node: Ast.Node, ancestors: Ast.Node
 			result.dest = visitNodeInner(result.dest, fn, ancestors) as Ast.Assign['dest'];
 			break;
 		}
+		case 'plus': {
+			result.expr = visitNodeInner(result.expr, fn, ancestors) as Ast.Plus['expr'];
+			break;
+		}
+		case 'minus': {
+			result.expr = visitNodeInner(result.expr, fn, ancestors) as Ast.Minus['expr'];
+			break;
+		}
 		case 'not': {
-			result.expr = visitNodeInner(result.expr, fn, ancestors) as Ast.Return['expr'];
+			result.expr = visitNodeInner(result.expr, fn, ancestors) as Ast.Not['expr'];
 			break;
 		}
 		case 'if': {
diff --git a/unreleased/new-lines-in-template-expression.md b/unreleased/new-lines-in-template-expression.md
new file mode 100644
index 0000000..e74509c
--- /dev/null
+++ b/unreleased/new-lines-in-template-expression.md
@@ -0,0 +1,1 @@
+- テンプレートリテラル内に埋め込まれた式の先頭および末尾の改行が許容されるようになりました。
diff --git a/unreleased/unary-sign-operators.md b/unreleased/unary-sign-operators.md
new file mode 100644
index 0000000..e064b95
--- /dev/null
+++ b/unreleased/unary-sign-operators.md
@@ -0,0 +1,1 @@
+- 単項演算子の正号 `+`・負号 `-`が数値リテラル以外の式にも使用できるようになりました。


Test patch:
diff --git a/test/literals.ts b/test/literals.ts
index 590edfc..772ba99 100644
--- a/test/literals.ts
+++ b/test/literals.ts
@@ -192,5 +192,23 @@ describe('Template syntax', () => {
 		`);
 		eq(res, STR('1'));
 	});
+
+	test.concurrent('new line before', async () => {
+		const res = await exe(`
+		<: \`{"Hello"
+		// comment
+		}\`
+		`);
+		eq(res, STR('Hello'));
+	});
+
+	test.concurrent('new line after', async () => {
+		const res = await exe(`
+		<: \`{
+		// comment
+		"Hello"}\`
+		`);
+		eq(res, STR('Hello'));
+	});
 });
 
diff --git a/test/syntax.ts b/test/syntax.ts
index 40ec4f6..e6b3b93 100644
--- a/test/syntax.ts
+++ b/test/syntax.ts
@@ -1314,6 +1314,26 @@ describe('operators', () => {
 
 });
 
+describe('plus', () => {
+	test.concurrent('Basic', async () => {
+		const res = await exe(`
+		let a = 1
+		<: +a
+		`);
+		eq(res, NUM(1));
+	})
+})
+
+describe('minus', () => {
+	test.concurrent('Basic', async () => {
+		const res = await exe(`
+		let a = 1
+		<: -a
+		`);
+		eq(res, NUM(-1));
+	})
+})
+
 describe('not', () => {
 	test.concurrent('Basic', async () => {
 		const res = await exe(`