Repository: syuilo/aiscript (ts)

Issue:
Whitespace should be required between "let" and the variable name in the each/for statements
The grammar for the for loop is written like this:

`"for" _* "(" "let" _* varn:NAME _*`...

([source](https://github.com/syuilo/aiscript/blob/8ab9c279009fe8453c9a65cf3bb7b359d081bb56/src/parser/parser.peggy#L234C4-L234C38))

Which means that I could write something like this and it would still be valid:
```
for (leti = 1,10) {
    <: i
}
```

I'm assuming this is unintended behavior, since the VarDef statement clearly requires a space between `let` and the variable name.

Edit: The `each` statement also exhibits the same behavior.

Pull request:
fix: each/for statement: Require whitespace between 'let' and the variable name
<!-- ℹ お読みください
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? -->
Require whitespace between `let` and the variable name in the `each` and `for` statements

# Why
<!-- なぜそうするのか？ どういう意図なのか？ 何が困っているのか？ -->
<!-- Why do you do it? What are your intentions? What is the problem? -->
Resolves #284 

# Additional info (optional)
<!-- テスト観点など -->
<!-- Test perspective, etc -->


Patch:
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e3150d1..3f5661d 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,6 +1,7 @@
 [Read translated version (en)](./translations/en/CHANGELOG.md)
 
 - オブジェクトの値を配列化する`Obj:vals`を追加
+- 文字列が`Json:parse`でパース可能であるかを判定する関数`Json:parsable`を追加
 
 # 0.13.3
 - 乱数を生成するとき引数の最大値を戻り値に含むように
diff --git a/docs/std.md b/docs/std.md
index 7fa5d3f..ffa17fb 100644
--- a/docs/std.md
+++ b/docs/std.md
@@ -33,6 +33,9 @@ JSONを生成します。
 ### @Json:parse(_json_: str): value
 JSONをパースします。  
 
+### @Json:parsable(_str_: str): bool
+文字列がJSONとしてパース可能であるかの判定を行います。 
+
 ## :: Date
 
 ### @Date:now(): num
diff --git a/src/interpreter/lib/std.ts b/src/interpreter/lib/std.ts
index bec9de8..cfb70ac 100644
--- a/src/interpreter/lib/std.ts
+++ b/src/interpreter/lib/std.ts
@@ -148,6 +148,16 @@ export const std: Record<string, Value> = {
 		assertString(json);
 		return jsToVal(JSON.parse(json.value));
 	}),
+
+	'Json:parsable': FN_NATIVE(([str]) => {
+		assertString(str);
+		try {
+			JSON.parse(str.value);
+		} catch (e) {
+			return BOOL(false);
+		}
+		return BOOL(true);
+	}),
 	//#endregion
 
 	//#region Date
diff --git a/src/parser/parser.peggy b/src/parser/parser.peggy
index aeb7127..a88e387 100644
--- a/src/parser/parser.peggy
+++ b/src/parser/parser.peggy
@@ -1,36 +1,4 @@
 {
-	function group(arr, predicate) {
-		const dest = [];
-
-		for (let i = 0; i < arr.length; i++) {
-			if (i != 0 && predicate(arr[i - 1], arr[i])) {
-				dest[dest.length - 1].push(arr[i]);
-			} else {
-				dest.push([arr[i]]);
-			}
-		}
-
-		return dest;
-	}
-
-	function ungroup(groupes) {
-		return groupes.reduce((acc, val) => acc.concat(val), []);
-	}
-
-	function concatTemplate(arr) {
-		let groupes = group(arr, (prev, current) => (typeof current == typeof prev));
-
-		// concat string
-		groupes = groupes.map(g => {
-			if (typeof g[0] == 'string') {
-				return [g.join('')];
-			}
-			return g;
-		});
-
-		return ungroup(groupes);
-	}
-
 	function createNode(type, params, children) {
 		const node = { type };
 		params.children = children;
@@ -211,7 +179,7 @@ Attr
 // each statement
 
 Each
-	= "each" _* "(" "let" _* varn:NAME _* ","? _* items:Expr ")" _* x:BlockOrStatement
+	= "each" _* "(" "let" _+ varn:NAME _* ","? _* items:Expr ")" _* x:BlockOrStatement
 {
 	return createNode('each', {
 		var: varn,
@@ -219,7 +187,7 @@ Each
 		for: x,
 	});
 }
-	/ "each" _+ "let" _* varn:NAME _* ","? _* items:Expr _+ x:BlockOrStatement
+	/ "each" _+ "let" _+ varn:NAME _* ","? _* items:Expr _+ x:BlockOrStatement
 {
 	return createNode('each', {
 		var: varn,
@@ -231,7 +199,7 @@ Each
 // for statement
 
 For
-	= "for" _* "(" "let" _* varn:NAME _* from_:("=" _* v:Expr { return v; })? ","? _* to:Expr ")" _* x:BlockOrStatement
+	= "for" _* "(" "let" _+ varn:NAME _* from_:("=" _* v:Expr { return v; })? ","? _* to:Expr ")" _* x:BlockOrStatement
 {
 	return createNode('for', {
 		var: varn,
@@ -240,7 +208,7 @@ For
 		for: x,
 	});
 }
-	/ "for" _+ "let" _* varn:NAME _* from_:("=" _* v:Expr { return v; })? ","? _* to:Expr _+ x:BlockOrStatement
+	/ "for" _+ "let" _+ varn:NAME _* from_:("=" _* v:Expr { return v; })? ","? _* to:Expr _+ x:BlockOrStatement
 {
 	return createNode('for', {
 		var: varn,
@@ -415,19 +383,19 @@ Identifier
 // template literal
 
 Tmpl
-	= "`" items:(!"`" i:TmplEmbed { return i; })* "`"
-{ return createNode('tmpl', { tmpl: concatTemplate(items) }); }
+	= "`" items:(!"`" @TmplEmbed)* "`"
+{ return createNode('tmpl', { tmpl: items }); }
 
 TmplEmbed
-	= TmplEsc
-	/ "{" __* expr:Expr __* "}"
-{ return expr; }
-	/ .
+	= "{" __* @expr:Expr __* "}"
+	/ str:TmplAtom+ {return str.join("")}
+
+TmplAtom
+  = TmplEsc
+	/ [^`{]
 
 TmplEsc
-	= "\\{" { return text()[1]; } // avoid syntax error of PEG.js (bug?)
-	/ "\\}" { return text()[1]; } // avoid syntax error of PEG.js (bug?)
-	/ "\\`" { return '`'; }
+	= "\\" @[{}`]
 
 // string literal
 


Test patch:
diff --git a/test/index.ts b/test/index.ts
index 0e4f2da..d99c679 100644
--- a/test/index.ts
+++ b/test/index.ts
@@ -957,6 +957,18 @@ describe('Template syntax', () => {
 		eq(res, STR('1 + 1 = 2'));
 	});
 
+	test.concurrent('invalid', async () => {
+		try {
+			await exe(`
+			<: \`{hoge}\`
+			`);
+		} catch (e) {
+			assert.ok(true);
+			return;
+		}
+		assert.fail();
+	});
+
 	test.concurrent('Escape', async () => {
 		const res = await exe(`
 		let message = "Hello"
@@ -966,7 +978,7 @@ describe('Template syntax', () => {
 	});
 });
 
-test.concurrent('Throws error when divied by zero', async () => {
+test.concurrent('Throws error when divided by zero', async () => {
 	try {
 		await exe(`
 		<: (0 / 0)
@@ -1485,6 +1497,20 @@ describe('for', () => {
 		`);
 		eq(res, NUM(10));
 	});
+
+	test.concurrent('var name without space', async () => {
+		try {
+			await exe(`
+			for (leti, 10) {
+				<: i
+			}
+			`);
+		} catch (e) {
+			assert.ok(true);
+			return;
+		}
+		assert.fail();
+	});
 });
 
 describe('for of', () => {
@@ -1519,6 +1545,20 @@ describe('for of', () => {
 		`);
 		eq(res, ARR([STR('ai!'), STR('chan!'), STR('kawaii!')]));
 	});
+
+	test.concurrent('var name without space', async () => {
+		try {
+			await exe(`
+			each letitem, ["ai", "chan", "kawaii"] {
+				<: item
+			}
+			`);
+		} catch (e) {
+			assert.ok(true);
+			return;
+		}
+		assert.fail();
+	});
 });
 
 describe('not', () => {
@@ -2491,8 +2531,8 @@ describe('std', () => {
 				let random = Math:gen_rng(seed)
 				return random(0 100)
 			}
-			let seed1 = \`{@Util:uuid()}\`
-			let seed2 = \`{@Date:year()}\`
+			let seed1 = \`{Util:uuid()}\`
+			let seed2 = \`{Date:year()}\`
 			let test1 = if (test(seed1) == test(seed1)) {true} else {false}
 			let test2 = if (test(seed1) == test(seed2)) {true} else {false}
 			<: [test1 test2]
@@ -2550,6 +2590,37 @@ describe('std', () => {
 			`);
 			eq(res, STR('"<function>"'));
 		});
+
+		test.concurrent('parsable', async () => {
+			[
+				'',
+				'null',
+				'hoge',
+				'"hoge"',
+				'[',
+				'[]',
+				'{}'
+			].forEach(async (str)=>{
+				const res=await exe(`
+					<: Json:parsable('${str}')
+				`);
+				assert.deepEqual(res.type, 'bool');
+				if (res.value){
+					await exe(`
+						<: Json:parse('${str}')
+					`);
+				} else {
+					try {
+						await exe(`
+							<: Json:parse('${str}')
+						`);
+					} catch (e) {
+						if (e instanceof SyntaxError) return;
+					}
+					assert.fail()
+				}
+			}):
+		});
 	});
 });