Repository: aiscript-dev/aiscript
Pull request #776 (2024-09-01 02:32:26)

enhance: each文と仮引数で分割代入に対応

# What

#763 でvar/let文での分割代入に対応しましたが、each文と関数の仮引数でも対応します。
for文はnumしか変数に入らないため対応していません。

# Why

#511

分割代入のために一々変数宣言を介さなくてもよくなる。
each文ではletキーワードがあるため、使えないと混乱の元

# Additional info (optional)



Patch:
diff --git a/docs/syntax.md b/docs/syntax.md
index 7e12e97..bd4ad0f 100644
--- a/docs/syntax.md
+++ b/docs/syntax.md
@@ -151,6 +151,18 @@ let ai_kun = {
 // 宣言で分割代入を使うことも可能
 let [hoge, fuga] = ['hoge', 'fuga']
 
+each let { value: a }, [{ value: 1 }, { value: 2 }] {
+	<: a // 1, 2
+}
+
+// 引数でも使用可能
+@func([a, b] = [0, 0]) {
+	[a, b]
+}
+func([1, 2]) // [1, 2]
+func([1]) // [1, null], not [1, 0]
+func() // [0, 0]
+
 // 再宣言を含む宣言は不可
 var a = 0
 let [a, b] = [1, 'piyo'] // Runtime Error
diff --git a/etc/aiscript.api.md b/etc/aiscript.api.md
index da1f46a..34eb921 100644
--- a/etc/aiscript.api.md
+++ b/etc/aiscript.api.md
@@ -254,7 +254,7 @@ type Div = NodeBase & {
 // @public (undocumented)
 type Each = NodeBase & {
     type: 'each';
-    var: string;
+    var: Expression;
     items: Expression;
     for: Statement | Expression;
 };
@@ -311,7 +311,7 @@ const FN: (args: VUserFn["args"], statements: VUserFn["statements"], scope: VUse
 type Fn = NodeBase & {
     type: 'fn';
     args: {
-        name: string;
+        dest: Expression;
         optional: boolean;
         default?: Expression;
         argType?: TypeSource;
@@ -798,7 +798,7 @@ type VFn = VUserFn | VNativeFn;
 
 // @public (undocumented)
 type VFnArg = {
-    name: string;
+    dest: Expression;
     type?: Type;
     default?: Value;
 };
diff --git a/src/interpreter/index.ts b/src/interpreter/index.ts
index 80cfbd7..9f8c772 100644
--- a/src/interpreter/index.ts
+++ b/src/interpreter/index.ts
@@ -236,16 +236,12 @@ export class Interpreter {
 			// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
 			return result ?? NULL;
 		} else {
-			const _args = new Map<string, Variable>();
+			const fnScope = fn.scope!.createChildScope();
 			for (const i of fn.args.keys()) {
 				const argdef = fn.args[i]!;
 				if (!argdef.default) expectAny(args[i]);
-				_args.set(argdef.name, {
-					isMutable: true,
-					value: args[i] ?? argdef.default!,
-				});
+				this.define(fnScope, argdef.dest, args[i] ?? argdef.default!, true);
 			}
-			const fnScope = fn.scope!.createChildScope(_args);
 			return unWrapRet(await this._run(fn.statements!, fnScope));
 		}
 	}
@@ -369,12 +365,9 @@ export class Interpreter {
 				const items = await this._eval(node.items, scope);
 				assertArray(items);
 				for (const item of items.value) {
-					const v = await this._eval(node.for, scope.createChildScope(new Map([
-						[node.var, {
-							isMutable: false,
-							value: item,
-						}],
-					])));
+					const eachScope = scope.createChildScope();
+					this.define(eachScope, node.var, item, false);
+					const v = await this._eval(node.for, eachScope);
 					if (v.type === 'break') {
 						break;
 					} else if (v.type === 'return') {
@@ -495,7 +488,7 @@ export class Interpreter {
 				return FN(
 					await Promise.all(node.args.map(async (arg) => {
 						return {
-							name: arg.name,
+							dest: arg.dest,
 							default: arg.default ? await this._eval(arg.default, scope) : arg.optional ? NULL : undefined,
 							// type: (TODO)
 						};
diff --git a/src/interpreter/util.ts b/src/interpreter/util.ts
index 303eb33..6619309 100644
--- a/src/interpreter/util.ts
+++ b/src/interpreter/util.ts
@@ -195,7 +195,7 @@ export function reprValue(value: Value, literalLike = false, processedObjects =
 			// そのうちネイティブ関数の引数も表示できるようにしたいが、ホスト向けの破壊的変更を伴うと思われる
 			return '@( ?? ) { native code }';
 		} else {
-			return `@( ${(value.args.map(v => v.name)).join(', ')} ) { ... }`;
+			return `@( ${(value.args.map(v => v.dest.type === 'identifier' ? v.dest.name : '?')).join(', ')} ) { ... }`;
 		}
 	}
 
diff --git a/src/interpreter/value.ts b/src/interpreter/value.ts
index 3250afd..a5d271a 100644
--- a/src/interpreter/value.ts
+++ b/src/interpreter/value.ts
@@ -1,4 +1,4 @@
-import type { Node } from '../node.js';
+import type { Expression, Node } from '../node.js';
 import type { Type } from '../type.js';
 import type { Scope } from './scope.js';
 
@@ -42,7 +42,7 @@ export type VUserFn = VFnBase & {
 	scope: Scope;
 };
 export type VFnArg = {
-	name: string;
+	dest: Expression;
 	type?: Type;
 	default?: Value;
 }
diff --git a/src/node.ts b/src/node.ts
index a077c95..2961905 100644
--- a/src/node.ts
+++ b/src/node.ts
@@ -73,7 +73,7 @@ export type Return = NodeBase & {
 
 export type Each = NodeBase & {
 	type: 'each'; // each文
-	var: string; // イテレータ変数名
+	var: Expression; // イテレータ宣言
 	items: Expression; // 配列
 	for: Statement | Expression; // 本体処理
 };
@@ -265,7 +265,7 @@ export type If = NodeBase & {
 export type Fn = NodeBase & {
 	type: 'fn'; // 関数
 	args: {
-		name: string; // 引数名
+		dest: Expression; // 引数名
 		optional: boolean;
 		default?: Expression; // 引数の初期値
 		argType?: TypeSource; // 引数の型
diff --git a/src/parser/plugins/validate-keyword.ts b/src/parser/plugins/validate-keyword.ts
index 6bd4bbb..1198193 100644
--- a/src/parser/plugins/validate-keyword.ts
+++ b/src/parser/plugins/validate-keyword.ts
@@ -101,9 +101,7 @@ function validateNode(node: Ast.Node): Ast.Node {
 			break;
 		}
 		case 'each': {
-			if (reservedWord.includes(node.var)) {
-				throwReservedWordError(node.var, node.loc);
-			}
+			validateDest(node.var);
 			break;
 		}
 		case 'for': {
@@ -114,9 +112,7 @@ function validateNode(node: Ast.Node): Ast.Node {
 		}
 		case 'fn': {
 			for (const arg of node.args) {
-				if (reservedWord.includes(arg.name)) {
-					throwReservedWordError(arg.name, node.loc);
-				}
+				validateDest(arg.dest);
 			}
 			break;
 		}
diff --git a/src/parser/syntaxes/common.ts b/src/parser/syntaxes/common.ts
index 1e123a9..9b1bc54 100644
--- a/src/parser/syntaxes/common.ts
+++ b/src/parser/syntaxes/common.ts
@@ -9,7 +9,24 @@ import type * as Ast from '../../node.js';
 
 /**
  * ```abnf
- * Params = "(" [IDENT [":" Type] *(SEP IDENT [":" Type])] ")"
+ * Dest = IDENT / Expr
+ * ```
+*/
+export function parseDest(s: ITokenStream): Ast.Expression {
+	// 全部parseExprに任せるとparseReferenceが型注釈を巻き込んでパースしてしまうためIdentifierのみ個別に処理。
+	if (s.is(TokenKind.Identifier)) {
+		const nameStartPos = s.getPos();
+		const name = s.getTokenValue();
+		s.next();
+		return NODE('identifier', { name }, nameStartPos, s.getPos());
+	} else {
+		return parseExpr(s, false);
+	}
+}
+
+/**
+ * ```abnf
+ * Params = "(" [Dest [":" Type] *(SEP Dest [":" Type])] ")"
  * ```
 */
 export function parseParams(s: ITokenStream): Ast.Fn['args'] {
@@ -23,9 +40,7 @@ export function parseParams(s: ITokenStream): Ast.Fn['args'] {
 	}
 
 	while (!s.is(TokenKind.CloseParen)) {
-		s.expect(TokenKind.Identifier);
-		const name = s.getTokenValue();
-		s.next();
+		const dest = parseDest(s);
 
 		let optional = false;
 		let defaultExpr: Ast.Expression | undefined;
@@ -42,7 +57,7 @@ export function parseParams(s: ITokenStream): Ast.Fn['args'] {
 			type = parseType(s);
 		}
 
-		items.push({ name, optional, default: defaultExpr, argType: type });
+		items.push({ dest, optional, default: defaultExpr, argType: type });
 
 		// separator
 		switch (s.getTokenKind()) {
diff --git a/src/parser/syntaxes/statements.ts b/src/parser/syntaxes/statements.ts
index 0a84487..4f1b592 100644
--- a/src/parser/syntaxes/statements.ts
+++ b/src/parser/syntaxes/statements.ts
@@ -1,7 +1,7 @@
 import { AiScriptSyntaxError } from '../../error.js';
 import { CALL_NODE, NODE } from '../utils.js';
 import { TokenKind } from '../token.js';
-import { parseBlock, parseParams, parseType } from './common.js';
+import { parseBlock, parseDest, parseParams, parseType } from './common.js';
 import { parseExpr } from './expressions.js';
 
 import type * as Ast from '../../node.js';
@@ -100,7 +100,7 @@ export function parseBlockOrStatement(s: ITokenStream): Ast.Statement | Ast.Expr
 
 /**
  * ```abnf
- * VarDef = ("let" / "var") (IDENT / Expr) [":" Type] "=" Expr
+ * VarDef = ("let" / "var") Dest [":" Type] "=" Expr
  * ```
 */
 function parseVarDef(s: ITokenStream): Ast.Definition {
@@ -122,16 +122,7 @@ function parseVarDef(s: ITokenStream): Ast.Definition {
 	}
 	s.next();
 
-	let dest: Ast.Expression;
-	// 全部parseExprに任せるとparseReferenceが型注釈を巻き込んでパースしてしまうためIdentifierのみ個別に処理。
-	if (s.is(TokenKind.Identifier)) {
-		const nameStartPos = s.getPos();
-		const name = s.getTokenValue();
-		s.next();
-		dest = NODE('identifier', { name }, nameStartPos, s.getPos());
-	} else {
-		dest = parseExpr(s, false);
-	}
+	const dest = parseDest(s);
 
 	let type: Ast.TypeSource | undefined;
 	if (s.is(TokenKind.Colon)) {
@@ -209,8 +200,8 @@ function parseOut(s: ITokenStream): Ast.Call {
 
 /**
  * ```abnf
- * Each = "each" "(" "let" IDENT "," Expr ")" BlockOrStatement
- *      / "each"     "let" IDENT "," Expr     BlockOrStatement
+ * Each = "each" "(" "let" Dest "," Expr ")" BlockOrStatement
+ *      / "each"     "let" Dest "," Expr     BlockOrStatement
  * ```
 */
 function parseEach(s: ITokenStream): Ast.Each {
@@ -228,9 +219,7 @@ function parseEach(s: ITokenStream): Ast.Each {
 	s.expect(TokenKind.LetKeyword);
 	s.next();
 
-	s.expect(TokenKind.Identifier);
-	const name = s.getTokenValue();
-	s.next();
+	const dest = parseDest(s);
 
 	if (s.is(TokenKind.Comma)) {
 		s.next();
@@ -248,7 +237,7 @@ function parseEach(s: ITokenStream): Ast.Each {
 	const body = parseBlockOrStatement(s);
 
 	return NODE('each', {
-		var: name,
+		var: dest,
 		items: items,
 		for: body,
 	}, startPos, s.getPos());
diff --git a/unreleased/destr-define.md b/unreleased/destr-define.md
index d8bb2a7..1ec0c52 100644
--- a/unreleased/destr-define.md
+++ b/unreleased/destr-define.md
@@ -1,1 +1,1 @@
-- 変数宣言で分割代入ができるように（名前空間内では分割代入を使用した宣言はできません。）
+- 変数宣言（each文での宣言を含む）と関数の仮引数で分割代入ができるように（名前空間内では分割代入を使用した宣言はできません。）


Test patch:
diff --git a/test/syntax.ts b/test/syntax.ts
index fc89268..84d2553 100644
--- a/test/syntax.ts
+++ b/test/syntax.ts
@@ -434,6 +434,16 @@ describe('separator', () => {
 			`);
 			eq(res, NUM(3));
 		});
+
+		test.concurrent('destructuring param', async () => {
+			const res = await exe(`
+			@f([a, b]) {
+				a + b
+			}
+			<: f([1, 2])
+			`);
+			eq(res, NUM(3));
+		});
 	});
 });
 
@@ -729,6 +739,15 @@ describe('each', () => {
 		eq(res, ARR([STR('ai!'), STR('chan!'), STR('kawaii!')]));
 	});
 
+	test.concurrent('destructuring declaration', async () => {
+		const res = await exe(`
+			each let { value: a }, [{ value: 1 }] {
+				<: a
+			}
+		`);
+		eq(res, NUM(1));
+	});
+
 	test.concurrent('Break', async () => {
 		const res = await exe(`
 		let msgs = []