Repository: syuilo/aiscript
Pull request #203 (2022-08-28 12:51:05)

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


codecov[bot]: # [Codecov](https://codecov.io/gh/syuilo/aiscript/pull/203?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=syuilo) Report
> Merging [#203](https://codecov.io/gh/syuilo/aiscript/pull/203?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=syuilo) (588e07c) into [master](https://codecov.io/gh/syuilo/aiscript/commit/55c91dd53f486887b4f2eef7b84444fe170e42ed?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=syuilo) (55c91dd) will **increase** coverage by `0.57%`.
> The diff coverage is `99.41%`.

```diff
@@            Coverage Diff             @@
##           master     #203      +/-   ##
==========================================
+ Coverage   75.48%   76.05%   +0.57%     
==========================================
  Files          18       19       +1     
  Lines        2688     2677      -11     
  Branches      360      375      +15     
==========================================
+ Hits         2029     2036       +7     
+ Misses        653      635      -18     
  Partials        6        6              
```


| [Impacted Files](https://codecov.io/gh/syuilo/aiscript/pull/203?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=syuilo) | Coverage Δ | |
|---|---|---|
| [src/parser/plugins/validate-keyword.ts](https://codecov.io/gh/syuilo/aiscript/pull/203/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=syuilo#diff-c3JjL3BhcnNlci9wbHVnaW5zL3ZhbGlkYXRlLWtleXdvcmQudHM=) | `97.61% <92.85%> (+9.18%)` | :arrow_up: |
| [src/parser/plugins/transform-chain.ts](https://codecov.io/gh/syuilo/aiscript/pull/203/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=syuilo#diff-c3JjL3BhcnNlci9wbHVnaW5zL3RyYW5zZm9ybS1jaGFpbi50cw==) | `94.87% <100.00%> (-3.86%)` | :arrow_down: |
| [src/parser/plugins/validate-type.ts](https://codecov.io/gh/syuilo/aiscript/pull/203/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=syuilo#diff-c3JjL3BhcnNlci9wbHVnaW5zL3ZhbGlkYXRlLXR5cGUudHM=) | `100.00% <100.00%> (ø)` | |
| [src/parser/visit.ts](https://codecov.io/gh/syuilo/aiscript/pull/203/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=syuilo#diff-c3JjL3BhcnNlci92aXNpdC50cw==) | `100.00% <100.00%> (ø)` | |
| [src/error.ts](https://codecov.io/gh/syuilo/aiscript/pull/203/diff?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=syuilo#diff-c3JjL2Vycm9yLnRz) | `100.00% <0.00%> (+31.57%)` | :arrow_up: |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=syuilo). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=syuilo)


Patch:
diff --git a/src/parser/plugins/transform-chain.ts b/src/parser/plugins/transform-chain.ts
index ff6ae21..4a99490 100644
--- a/src/parser/plugins/transform-chain.ts
+++ b/src/parser/plugins/transform-chain.ts
@@ -1,13 +1,8 @@
 import * as Ast from '../node';
+import { visitNode } from '../visit';
 
-function visitNode(node: Ast.Expression): Ast.Expression
-function visitNode(node: Ast.Statement): Ast.Statement
-function visitNode(node: Ast.Statement | Ast.Expression): Ast.Statement | Ast.Expression
-function visitNode(node: Ast.Node): Ast.Node
-function visitNode(node: Ast.Node): Ast.Node {
-	let result: Ast.Node = node;
-
-	// transform chain
+function transformNode(node: Ast.Node): Ast.Node {
+	// chain
 	if (Ast.hasChainProp(node) && node.chain != null) {
 		const { chain, ...hostNode } = node;
 		let parent: Ast.Expression = hostNode;
@@ -30,128 +25,15 @@ function visitNode(node: Ast.Node): Ast.Node {
 				}
 			}
 		}
-		result = parent;
-	}
-
-	// nested nodes
-	switch (result.type) {
-		case 'def': {
-			result.expr = visitNode(result.expr);
-			break;
-		}
-		case 'return': {
-			result.expr = visitNode(result.expr);
-			break;
-		}
-		case 'forOf': {
-			result.items = visitNode(result.items);
-			result.for = visitNode(result.for);
-			break;
-		}
-		case 'for': {
-			if (result.from != null) {
-				result.from = visitNode(result.from);
-			}
-			if (result.to != null) {
-				result.to = visitNode(result.to);
-			}
-			if (result.times != null) {
-				result.times = visitNode(result.times);
-			}
-			result.for = visitNode(result.for);
-			break;
-		}
-		case 'loop': {
-			visitNodes(result.statements);
-			break;
-		}
-		case 'inc':
-		case 'dec':
-		case 'assign': {
-			result.expr = visitNode(result.expr);
-			result.dest = visitNode(result.dest);
-			break;
-		}
-		case 'infix': {
-			visitNodes(result.operands);
-			break;
-		}
-		case 'if': {
-			result.cond = visitNode(result.cond);
-			result.then = visitNode(result.then);
-			for (const prop of result.elseif) {
-				prop.cond = visitNode(prop.cond);
-				prop.then = visitNode(prop.then);
-			}
-			if (result.else != null) {
-				result.else = visitNode(result.else);
-			}
-			break;
-		}
-		case 'fn': {
-			visitNodes(result.children);
-			break;
-		}
-		case 'match': {
-			result.about = visitNode(result.about);
-			for (const prop of result.qs) {
-				prop.q = visitNode(prop.q);
-				prop.a = visitNode(prop.a);
-			}
-			if (result.default != null) {
-				result.default = visitNode(result.default);
-			}
-			break;
-		}
-		case 'block': {
-			visitNodes(result.statements);
-			break;
-		}
-		case 'tmpl': {
-			for (let i = 0; i < result.tmpl.length; i++) {
-				const item = result.tmpl[i];
-				if (typeof item !== 'string') {
-					result.tmpl[i] = visitNode(item);
-				}
-			}
-			break;
-		}
-		case 'obj': {
-			for (const item of result.value) {
-				result.value.set(item[0], visitNode(item[1]));
-			}
-			break;
-		}
-		case 'arr': {
-			visitNodes(result.value);
-			break;
-		}
-		case 'call': {
-			result.target = visitNode(result.target);
-			visitNodes(result.args);
-			break;
-		}
-		case 'index': {
-			result.target = visitNode(result.target);
-			result.index = visitNode(result.index);
-			break;
-		}
-		case 'prop': {
-			result.target = visitNode(result.target);
-			break;
-		}
+		return parent;
 	}
 
-	return result;
+	return node;
 }
 
-function visitNodes(nodes: Ast.Node[]) {
+export function transformChain(nodes: Ast.Node[]): Ast.Node[] {
 	for (let i = 0; i < nodes.length; i++) {
-		nodes[i] = visitNode(nodes[i]);
+		nodes[i] = visitNode(nodes[i], transformNode);
 	}
-}
-
-export function transformChain(nodes: Ast.Node[]): Ast.Node[] {
-	visitNodes(nodes);
 	return nodes;
 }
diff --git a/src/parser/plugins/validate-keyword.ts b/src/parser/plugins/validate-keyword.ts
index d38ef29..b67e4a2 100644
--- a/src/parser/plugins/validate-keyword.ts
+++ b/src/parser/plugins/validate-keyword.ts
@@ -1,5 +1,6 @@
 import * as aiscript from '../..';
 import * as Ast from '../node';
+import { visitNode } from '../visit';
 
 const reservedWord = [
 	'null',
@@ -44,78 +45,40 @@ function throwReservedWordError(name: string) {
 	throw new aiscript.SemanticError(`Reserved word "${name}" cannot be used as variable name.`);
 }
 
-function validate(node: Ast.Node): void {
+function validateNode(node: Ast.Node): Ast.Node {
 	switch (node.type) {
 		case 'def':
-		case 'ns': {
+		case 'attr':
+		case 'ns':
+		case 'var':
+		case 'propChain': {
 			if (reservedWord.includes(node.name)) {
 				throwReservedWordError(node.name);
 			}
 			break;
 		}
-		case 'inc':
-		case 'dec':
-		case 'assign': {
-			if (node.dest.type === 'var') {
-				if (reservedWord.includes(node.dest.name)) {
-					throwReservedWordError(node.dest.name);
-				}
+		case 'meta': {
+			if (node.name != null && reservedWord.includes(node.name)) {
+				throwReservedWordError(node.name);
 			}
-			validate(node.dest);
-			validate(node.expr);
 			break;
 		}
 		case 'fn': {
-			for (const inner of node.children) {
-				validate(inner);
-			}
-			break;
-		}
-		case 'block': {
-			for (const inner of node.statements) {
-				validate(inner);
-			}
-			break;
-		}
-		case 'if': {
-			if (node.then.type === 'block') {
-				for (const inner of node.then.statements) {
-					validate(inner);
-				}
-			}
-			for (const n of node.elseif) {
-				if (n.then.type === 'block') {
-					for (const inner of n.then.statements) {
-						validate(inner);
-					}
-				}
-			}
-			if (node.else?.type === 'block') {
-				for (const inner of node.else.statements) {
-					validate(inner);
+			for (const arg of node.args) {
+				if (reservedWord.includes(arg.name)) {
+					throwReservedWordError(arg.name);
 				}
 			}
 			break;
 		}
-		// TODO: match
 	}
 
-	if (Ast.hasChainProp(node)) {
-		if (node.chain != null) {
-			for (const item of node.chain) {
-				if (item.type === 'propChain') {
-					if (reservedWord.includes(item.name)) {
-						throwReservedWordError(item.name);
-					}
-				}
-			}
-		}
-	}
+	return node;
 }
 
 export function validateKeyword(nodes: Ast.Node[]): Ast.Node[] {
 	for (const inner of nodes) {
-		validate(inner);
+		visitNode(inner, validateNode);
 	}
 	return nodes;
 }
diff --git a/src/parser/plugins/validate-type.ts b/src/parser/plugins/validate-type.ts
index 8fcc1a3..a8d609b 100644
--- a/src/parser/plugins/validate-type.ts
+++ b/src/parser/plugins/validate-type.ts
@@ -2,14 +2,14 @@ import * as aiscript from '../..';
 import { SemanticError } from '../../error';
 import * as Ast from '../node';
 import { getTypeBySource } from '../../type';
+import { visitNode } from '../visit';
 
-function validate(node: Ast.Node) {
+function validateNode(node: Ast.Node): Ast.Node {
 	switch (node.type) {
 		case 'def': {
 			if (node.varType != null) {
 				getTypeBySource(node.varType);
 			}
-			validate(node.expr);
 			break;
 		}
 		case 'fn': {
@@ -21,18 +21,16 @@ function validate(node: Ast.Node) {
 			if (node.ret != null) {
 				getTypeBySource(node.ret);
 			}
-			for (const n of node.children) {
-				validate(n);
-			}
 			break;
 		}
 	}
-	// TODO: ブロックも全部スキャン
+
+	return node;
 }
 
 export function validateType(nodes: Ast.Node[]): Ast.Node[] {
 	for (const node of nodes) {
-		validate(node);
+		visitNode(node, validateNode);
 	}
 	return nodes;
 }
diff --git a/src/parser/visit.ts b/src/parser/visit.ts
new file mode 100644
index 0000000..c0bdfdb
--- /dev/null
+++ b/src/parser/visit.ts
@@ -0,0 +1,146 @@
+import * as Ast from './node';
+
+export function visitNode(node: Ast.Node, fn: (node: Ast.Node) => Ast.Node): Ast.Node {
+	const result = fn(node);
+
+	// nested nodes
+	switch (result.type) {
+		case 'def': {
+			result.expr = visitNode(result.expr, fn) as Ast.Definition['expr'];
+			break;
+		}
+		case 'return': {
+			result.expr = visitNode(result.expr, fn) as Ast.Return['expr'];
+			break;
+		}
+		case 'forOf': {
+			result.items = visitNode(result.items, fn) as Ast.Each['items'];
+			result.for = visitNode(result.for, fn) as Ast.Each['for'];
+			break;
+		}
+		case 'for': {
+			if (result.from != null) {
+				result.from = visitNode(result.from, fn) as Ast.For['from'];
+			}
+			if (result.to != null) {
+				result.to = visitNode(result.to, fn) as Ast.For['to'];
+			}
+			if (result.times != null) {
+				result.times = visitNode(result.times, fn) as Ast.For['times'];
+			}
+			result.for = visitNode(result.for, fn) as Ast.For['for'];
+			break;
+		}
+		case 'loop': {
+			for (let i = 0; i < result.statements.length; i++) {
+				result.statements[i] = visitNode(result.statements[i], fn) as Ast.Loop['statements'][number];
+			}
+			break;
+		}
+		case 'inc':
+		case 'dec':
+		case 'assign': {
+			result.expr = visitNode(result.expr, fn) as Ast.Assign['expr'];
+			result.dest = visitNode(result.dest, fn) as Ast.Assign['dest'];
+			break;
+		}
+		case 'infix': {
+			for (let i = 0; i < result.operands.length; i++) {
+				result.operands[i] = visitNode(result.operands[i], fn) as Ast.Infix['operands'][number];
+			}
+			break;
+		}
+		case 'if': {
+			result.cond = visitNode(result.cond, fn) as Ast.If['cond'];
+			result.then = visitNode(result.then, fn) as Ast.If['then'];
+			for (const prop of result.elseif) {
+				prop.cond = visitNode(prop.cond, fn) as Ast.If['elseif'][number]['cond'];
+				prop.then = visitNode(prop.then, fn) as Ast.If['elseif'][number]['then'];
+			}
+			if (result.else != null) {
+				result.else = visitNode(result.else, fn) as Ast.If['else'];
+			}
+			break;
+		}
+		case 'fn': {
+			for (let i = 0; i < result.children.length; i++) {
+				result.children[i] = visitNode(result.children[i], fn) as Ast.Fn['children'][number];
+			}
+			break;
+		}
+		case 'match': {
+			result.about = visitNode(result.about, fn) as Ast.Match['about'];
+			for (const prop of result.qs) {
+				prop.q = visitNode(prop.q, fn) as Ast.Match['qs'][number]['q'];
+				prop.a = visitNode(prop.a, fn) as Ast.Match['qs'][number]['a'];
+			}
+			if (result.default != null) {
+				result.default = visitNode(result.default, fn) as Ast.Match['default'];
+			}
+			break;
+		}
+		case 'block': {
+			for (let i = 0; i < result.statements.length; i++) {
+				result.statements[i] = visitNode(result.statements[i], fn) as Ast.Block['statements'][number];
+			}
+			break;
+		}
+		case 'tmpl': {
+			for (let i = 0; i < result.tmpl.length; i++) {
+				const item = result.tmpl[i];
+				if (typeof item !== 'string') {
+					result.tmpl[i] = visitNode(item, fn) as Ast.Tmpl['tmpl'][number];
+				}
+			}
+			break;
+		}
+		case 'obj': {
+			for (const item of result.value) {
+				result.value.set(item[0], visitNode(item[1], fn) as Ast.Expression);
+			}
+			break;
+		}
+		case 'arr': {
+			for (let i = 0; i < result.value.length; i++) {
+				result.value[i] = visitNode(result.value[i], fn) as Ast.Arr['value'][number];
+			}
+			break;
+		}
+		case 'callChain': {
+			for (let i = 0; i < result.args.length; i++) {
+				result.args[i] = visitNode(result.args[i], fn) as Ast.Call['args'][number];
+			}
+			break;
+		}
+		case 'indexChain': {
+			result.index = visitNode(result.index, fn) as Ast.Index['index'];
+			break;
+		}
+		case 'call': {
+			result.target = visitNode(result.target, fn) as Ast.Call['target'];
+			for (let i = 0; i < result.args.length; i++) {
+				result.args[i] = visitNode(result.args[i], fn) as Ast.Call['args'][number];
+			}
+			break;
+		}
+		case 'index': {
+			result.target = visitNode(result.target, fn) as Ast.Index['target'];
+			result.index = visitNode(result.index, fn) as Ast.Index['index'];
+			break;
+		}
+		case 'prop': {
+			result.target = visitNode(result.target, fn) as Ast.Prop['target'];
+			break;
+		}
+	}
+
+	if (Ast.hasChainProp(result)) {
+		if (result.chain != null) {
+			for (let i = 0; i < result.chain.length; i++) {
+				result.chain[i] = visitNode(result.chain[i], fn) as Ast.ChainMember;
+			}
+		}
+	}
+
+	return result;
+}


Test patch:
diff --git a/test/index.ts b/test/index.ts
index 24f35ec..0afe152 100644
--- a/test/index.ts
+++ b/test/index.ts
@@ -534,6 +534,96 @@ describe('Var name starts with reserved word', () => {
 	});
 });
 
+describe('name validation of reserved word', () => {
+	it('def', async () => {
+		try {
+			await exe(`
+			let let = 1
+			`);
+		} catch (e) {
+			assert.ok(true);
+			return;
+		}
+		assert.fail();
+	});
+
+	it('attr', async () => {
+		try {
+			await exe(`
+			#[let 1]
+			@f() { 1 }
+			`);
+		} catch (e) {
+			assert.ok(true);
+			return;
+		}
+		assert.fail();
+	});
+
+	it('ns', async () => {
+		try {
+			await exe(`
+			:: let {
+				@f() { 1 }
+			}
+			`);
+		} catch (e) {
+			assert.ok(true);
+			return;
+		}
+		assert.fail();
+	});
+
+	it('var', async () => {
+		try {
+			await exe(`
+			let
+			`);
+		} catch (e) {
+			assert.ok(true);
+			return;
+		}
+		assert.fail();
+	});
+
+	it('prop', async () => {
+		try {
+			await exe(`
+			let x = { let: 1 }
+			x.let
+			`);
+		} catch (e) {
+			assert.ok(true);
+			return;
+		}
+		assert.fail();
+	});
+
+	it('meta', async () => {
+		try {
+			await exe(`
+			### let 1
+			`);
+		} catch (e) {
+			assert.ok(true);
+			return;
+		}
+		assert.fail();
+	});
+
+	it('fn', async () => {
+		try {
+			await exe(`
+			@let() { 1 }
+			`);
+		} catch (e) {
+			assert.ok(true);
+			return;
+		}
+		assert.fail();
+	});
+});
+
 describe('Object', () => {
 	it('property access', async () => {
 		const res = await exe(`
@@ -653,94 +743,149 @@ it('Array item assign', async () => {
 	eq(res, ARR([STR('ai'), STR('taso'), STR('kawaii')]));
 });
 
-it('chain access (prop + index + call)', async () => {
-	const res = await exe(`
-	let obj = {
-		a: {
-			b: [@(name) { name }, @(str) { "chan" }, @() { "kawaii" }];
-		};
-	}
+describe('chain', () => {
+	it('chain access (prop + index + call)', async () => {
+		const res = await exe(`
+		let obj = {
+			a: {
+				b: [@(name) { name }, @(str) { "chan" }, @() { "kawaii" }];
+			};
+		}
 
-	<: obj.a.b[1]("ai")
-	`);
-	eq(res, STR('ai'));
-});
+		<: obj.a.b[1]("ai")
+		`);
+		eq(res, STR('ai'));
+	});
 
-it('chained assign left side (prop + index)', async () => {
-	const res = await exe(`
-	let obj = {
-		a: {
-			b: ["ai", "chan", "kawaii"];
-		};
-	}
+	it('chained assign left side (prop + index)', async () => {
+		const res = await exe(`
+		let obj = {
+			a: {
+				b: ["ai", "chan", "kawaii"];
+			};
+		}
 
-	obj.a.b[2] = "taso"
+		obj.a.b[2] = "taso"
 
-	<: obj
-	`);
-	eq(res, OBJ(new Map([
-		['a', OBJ(new Map([
-			['b', ARR([STR('ai'), STR('taso'), STR('kawaii')])]
-		]))]
-	])));
-});
+		<: obj
+		`);
+		eq(res, OBJ(new Map([
+			['a', OBJ(new Map([
+				['b', ARR([STR('ai'), STR('taso'), STR('kawaii')])]
+			]))]
+		])));
+	});
 
-it('chained assign right side (prop + index + call)', async () => {
-	const res = await exe(`
-	let obj = {
-		a: {
-			b: ["ai", "chan", "kawaii"];
-		};
-	}
+	it('chained assign right side (prop + index + call)', async () => {
+		const res = await exe(`
+		let obj = {
+			a: {
+				b: ["ai", "chan", "kawaii"];
+			};
+		}
 
-	var x = null
-	x = obj.a.b[1]
+		var x = null
+		x = obj.a.b[1]
 
-	<: x
-	`);
-	eq(res, STR('ai'));
-});
+		<: x
+		`);
+		eq(res, STR('ai'));
+	});
 
-it('chained inc/dec left side (index + prop)', async () => {
-	const res = await exe(`
-	let arr = [
-		{
-			a: 1;
-			b: 2;
+	it('chained inc/dec left side (index + prop)', async () => {
+		const res = await exe(`
+		let arr = [
+			{
+				a: 1;
+				b: 2;
+			}
+		]
+
+		arr[1].a += 1
+		arr[1].b -= 1
+
+		<: arr
+		`);
+		eq(res, ARR([
+			OBJ(new Map([
+				['a', NUM(2)],
+				['b', NUM(1)]
+			]))
+		]));
+	});
+
+	it('chained inc/dec left side (prop + index)', async () => {
+		const res = await exe(`
+		let obj = {
+			a: {
+				b: [1, 2, 3];
+			};
 		}
-	]
 
-	arr[1].a += 1
-	arr[1].b -= 1
+		obj.a.b[2] += 1
+		obj.a.b[3] -= 1
 
-	<: arr
-	`);
-	eq(res, ARR([
-		OBJ(new Map([
-			['a', NUM(2)],
-			['b', NUM(1)]
-		]))
-	]));
-});
+		<: obj
+		`);
+		eq(res, OBJ(new Map([
+			['a', OBJ(new Map([
+				['b', ARR([NUM(1), NUM(3), NUM(2)])]
+			]))]
+		])));
+	});
 
-it('chained inc/dec left side (prop + index)', async () => {
-	const res = await exe(`
-	let obj = {
-		a: {
-			b: [1, 2, 3];
-		};
-	}
+	it('prop in def', async () => {
+		const res = await exe(`
+		let x = @() {
+			let obj = {
+				a: 1
+			}
+			obj.a
+		}
 
-	obj.a.b[2] += 1
-	obj.a.b[3] -= 1
+		<: x()
+		`);
+		eq(res, NUM(1));
+	});
 
-	<: obj
-	`);
-	eq(res, OBJ(new Map([
-		['a', OBJ(new Map([
-			['b', ARR([NUM(1), NUM(3), NUM(2)])]
-		]))]
-	])));
+	it('prop in return', async () => {
+		const res = await exe(`
+		let x = @() {
+			let obj = {
+				a: 1
+			}
+			return obj.a
+			2
+		}
+
+		<: x()
+		`);
+		eq(res, NUM(1));
+	});
+
+	it('prop in each', async () => {
+		const res = await exe(`
+		let msgs = []
+		let x = { a: ["ai", "chan", "kawaii"] }
+		each let item, x.a {
+			let y = { a: item }
+			Arr:push(msgs, Arr:join([y.a, "!"]))
+		}
+		<: msgs
+		`);
+		eq(res, ARR([STR('ai!'), STR('chan!'), STR('kawaii!')]));
+	});
+
+	it('prop in for', async () => {
+		const res = await exe(`
+		let x = { times: 10, count: 0 }
+		for (let i, x.times) {
+			x.count = (x.count + i)
+		}
+		<: x.count
+		`);
+		eq(res, NUM(55));
+	});
 });
 
 describe('Template syntax', () => {