Repository: aiscript-dev/aiscript
Pull request #833 (2024-10-29 08:13:01)

PRテンプレートのchangelogの案内を更新
<!-- ℹ お読みください
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? -->
Pull Requestテンプレートにおいて、CHANGELOG.mdに変更点を追記としていた部分を、unreleasedディレクトリにファイルを追加する旨に変更しました。

# Why
<!-- なぜそうするのか？ どういう意図なのか？ 何が困っているのか？ -->
<!-- Why do you do it? What are your intentions? What is the problem? -->
説明が実態と異なっていた

# Additional info (optional)
<!-- テスト観点など -->
<!-- Test perspective, etc -->
Related: #662, #666

codecov-commenter: :warning: Please install the !['codecov app svg image'](https://github.com/codecov/engineering-team/assets/152432831/e90313f4-9d3a-4b63-8b54-cfe14e7ec20d) to ensure uploads and comments are reliably processed by Codecov.

## [Codecov](https://app.codecov.io/gh/aiscript-dev/aiscript/pull/833?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
All modified and coverable lines are covered by tests :white_check_mark:

:exclamation: Your organization needs to install the [Codecov GitHub app](https://github.com/apps/codecov/installations/select_target) to enable full functionality.

[see 30 files with indirect coverage changes](https://app.codecov.io/gh/aiscript-dev/aiscript/pull/833/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)


Patch:
diff --git a/.github/pull_request_template.md b/.github/pull_request_template.md
index 8384cfa..77fac63 100644
--- a/.github/pull_request_template.md
+++ b/.github/pull_request_template.md
@@ -4,7 +4,7 @@ PRありがとうございます！ PRを作成する前に、以下をご確認
   - fix / refactor / feat / enhance / perf / chore
   - また、PRの粒度が適切であることを確認してください。ひとつのPRに複数の種類の変更や関心を含めることは避けてください。
 - このPRによって解決されるIssueがある場合は、そのIssueへの参照を本文内に含めてください。
-- CHANGELOG.mdに変更点を追記してください。リファクタリングなど、利用者に影響を与えない変更についてはこの限りではありません。
+- unreleasedディレクトリ以下にファイルを新規に作成し、変更点を記してください。リファクタリングなど、利用者に影響を与えない変更についてはこの限りではありません。
 - この変更により新たに作成、もしくは更新すべきドキュメントがないか確認してください。
 - 機能追加やバグ修正をした場合は、可能であればテストケースを追加してください。
 - テスト、Lintが通っていることを予め確認してください。
@@ -18,7 +18,7 @@ Thank you for your PR! Before creating a PR, please check the following:
   - 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.
+- 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.
diff --git a/eslint.config.mjs b/eslint.config.mjs
index 12fabbb..d125652 100644
--- a/eslint.config.mjs
+++ b/eslint.config.mjs
@@ -3,6 +3,9 @@ import js from "@eslint/js";
 import ts from 'typescript-eslint';
 
 export default ts.config({
+	// https://stackoverflow.com/a/79115209/22200513
+	ignores: ["**/*.js"]
+}, {
 	files: ["src/**/*.ts"],
 	extends: [
 		js.configs.recommended,
diff --git a/etc/aiscript.api.md b/etc/aiscript.api.md
index ad73857..a7d9375 100644
--- a/etc/aiscript.api.md
+++ b/etc/aiscript.api.md
@@ -313,12 +313,12 @@ const FALSE: {
 };
 
 // @public (undocumented)
-const FN: (args: VUserFn["args"], statements: VUserFn["statements"], scope: VUserFn["scope"]) => VUserFn;
+const FN: (params: VUserFn["params"], statements: VUserFn["statements"], scope: VUserFn["scope"]) => VUserFn;
 
 // @public (undocumented)
 type Fn = NodeBase & {
     type: 'fn';
-    args: {
+    params: {
         dest: Expression;
         optional: boolean;
         default?: Expression;
@@ -334,7 +334,7 @@ const FN_NATIVE: (fn: VNativeFn["native"]) => VNativeFn;
 // @public (undocumented)
 type FnTypeSource = NodeBase & {
     type: 'fnTypeSource';
-    args: TypeSource[];
+    params: TypeSource[];
     result: TypeSource;
 };
 
@@ -645,6 +645,7 @@ export class Scope {
     exists(name: string): boolean;
     get(name: string): Value;
     getAll(): Map<string, Variable>;
+    getNsPrefix(): string;
     // (undocumented)
     name: string;
     // (undocumented)
@@ -745,7 +746,7 @@ declare namespace values {
         VObj,
         VFn,
         VUserFn,
-        VFnArg,
+        VFnParam,
         VNativeFn,
         VReturn,
         VBreak,
@@ -807,7 +808,7 @@ type VError = {
 type VFn = VUserFn | VNativeFn;
 
 // @public (undocumented)
-type VFnArg = {
+type VFnParam = {
     dest: Expression;
     type?: Type;
     default?: Value;
@@ -857,15 +858,16 @@ type VStr = {
 // @public (undocumented)
 type VUserFn = VFnBase & {
     native?: undefined;
-    args: VFnArg[];
+    name?: string;
+    params: VFnParam[];
     statements: Node_2[];
     scope: Scope;
 };
 
 // Warnings were encountered during analysis:
 //
-// src/interpreter/index.ts:38:4 - (ae-forgotten-export) The symbol "LogObject" needs to be exported by the entry point index.d.ts
-// src/interpreter/value.ts:46:2 - (ae-forgotten-export) The symbol "Type" needs to be exported by the entry point index.d.ts
+// src/interpreter/index.ts:43:4 - (ae-forgotten-export) The symbol "LogObject" needs to be exported by the entry point index.d.ts
+// src/interpreter/value.ts:47:2 - (ae-forgotten-export) The symbol "Type" needs to be exported by the entry point index.d.ts
 
 // (No @packageDocumentation comment for this package)
 
diff --git a/src/interpreter/index.ts b/src/interpreter/index.ts
index 33520fe..490b8aa 100644
--- a/src/interpreter/index.ts
+++ b/src/interpreter/index.ts
@@ -7,7 +7,7 @@ import { AiScriptError, NonAiScriptError, AiScriptNamespaceError, AiScriptIndexO
 import * as Ast from '../node.js';
 import { Scope } from './scope.js';
 import { std } from './lib/std.js';
-import { assertNumber, assertString, assertFunction, assertBoolean, assertObject, assertArray, eq, isObject, isArray, expectAny, reprValue } from './util.js';
+import { assertNumber, assertString, assertFunction, assertBoolean, assertObject, assertArray, eq, isObject, isArray, expectAny, reprValue, isFunction } from './util.js';
 import { NULL, RETURN, unWrapRet, FN_NATIVE, BOOL, NUM, STR, ARR, OBJ, FN, BREAK, CONTINUE, ERROR } from './value.js';
 import { getPrimProp } from './primitive-props.js';
 import { Variable } from './variable.js';
@@ -20,6 +20,11 @@ export type LogObject = {
 	val?: Value | Variable;
 };
 
+type CallInfo = {
+	name: string;
+	pos: Ast.Pos | undefined;
+};
+
 export class Interpreter {
 	public stepCount = 0;
 	private stop = false;
@@ -97,7 +102,7 @@ export class Interpreter {
 		if (script == null || script.length === 0) return;
 		try {
 			await this.collectNs(script);
-			const result = await this._run(script, this.scope);
+			const result = await this._run(script, this.scope, []);
 			this.log('end', { val: result });
 		} catch (e) {
 			this.handleError(e);
@@ -114,7 +119,7 @@ export class Interpreter {
 	 */
 	@autobind
 	public async execFn(fn: VFn, args: Value[]): Promise<Value> {
-		return await this._fn(fn, args)
+		return await this._fn(fn, args, [])
 			.catch(e => {
 				this.handleError(e);
 				return ERROR('func_failed');
@@ -128,7 +133,7 @@ export class Interpreter {
 	 */
 	@autobind
 	public execFnSimple(fn: VFn, args: Value[]): Promise<Value> {
-		return this._fn(fn, args);
+		return this._fn(fn, args, []);
 	}
 
 	@autobind
@@ -224,7 +229,14 @@ export class Interpreter {
 						throw new AiScriptNamespaceError('No "var" in namespace declaration: ' + node.dest.name, node.loc.start);
 					}
 
-					const value = await this._eval(node.expr, nsScope);
+					const value = await this._eval(node.expr, nsScope, []);
+					if (
+						node.expr.type === 'fn'
+						&& isFunction(value)
+						&& !value.native
+					) {
+						value.name = nsScope.getNsPrefix() + node.dest.name;
+					}
 					await this.define(nsScope, node.dest, value, node.mut);
 
 					break;
@@ -245,10 +257,11 @@ export class Interpreter {
 	}
 
 	@autobind
-	private async _fn(fn: VFn, args: Value[]): Promise<Value> {
+	private async _fn(fn: VFn, args: Value[], callStack: readonly CallInfo[], pos?: Ast.Pos): Promise<Value> {
 		if (fn.native) {
+			const info: CallInfo = { name: '<native>', pos };
 			const result = fn.native(args, {
-				call: this.execFnSimple,
+				call: (fn, args) => this._fn(fn, args, [...callStack, info]),
 				topCall: this.execFn,
 				registerAbortHandler: this.registerAbortHandler,
 				unregisterAbortHandler: this.unregisterAbortHandler,
@@ -257,35 +270,45 @@ export class Interpreter {
 			return result ?? NULL;
 		} else {
 			const fnScope = fn.scope!.createChildScope();
-			for (const i of fn.args.keys()) {
-				const argdef = fn.args[i]!;
-				if (!argdef.default) expectAny(args[i]);
-				this.define(fnScope, argdef.dest, args[i] ?? argdef.default!, true);
+			for (const [i, param] of fn.params.entries()) {
+				const arg = args[i];
+				if (!param.default) expectAny(arg);
+				this.define(fnScope, param.dest, arg ?? param.default!, true);
 			}
-			return unWrapRet(await this._run(fn.statements!, fnScope));
+
+			const info: CallInfo = { name: fn.name ?? '<anonymous>', pos };
+			return unWrapRet(await this._run(fn.statements!, fnScope, [...callStack, info]));
 		}
 	}
 
 	@autobind
-	private _evalClause(node: Ast.Statement | Ast.Expression, scope: Scope): Promise<Value> {
-		return this._eval(node, Ast.isStatement(node) ? scope.createChildScope() : scope);
+	private _evalClause(node: Ast.Statement | Ast.Expression, scope: Scope, callStack: readonly CallInfo[]): Promise<Value> {
+		return this._eval(node, Ast.isStatement(node) ? scope.createChildScope() : scope, callStack);
 	}
 
 	@autobind
-	private _eval(node: Ast.Node, scope: Scope): Promise<Value> {
-		return this.__eval(node, scope).catch(e => {
+	private _eval(node: Ast.Node, scope: Scope, callStack: readonly CallInfo[]): Promise<Value> {
+		return this.__eval(node, scope, callStack).catch(e => {
 			if (e.pos) throw e;
 			else {
 				const e2 = (e instanceof AiScriptError) ? e : new NonAiScriptError(e);
 				e2.pos = node.loc.start;
-				e2.message = `${e2.message} (Line ${e2.pos.line}, Column ${e2.pos.column})`;
+				e2.message = [
+					e2.message,
+					...[...callStack, { pos: e2.pos }].map(({ pos }, i) => {
+						const name = callStack[i - 1]?.name ?? '<root>';
+						return pos
+							? `  at ${name} (Line ${pos.line}, Column ${pos.column})`
+							: `  at ${name}`;
+					}).reverse(),
+				].join('\n');
 				throw e2;
 			}
 		});
 	}
 
 	@autobind
-	private async __eval(node: Ast.Node, scope: Scope): Promise<Value> {
+	private async __eval(node: Ast.Node, scope: Scope, callStack: readonly CallInfo[]): Promise<Value> {
 		if (this.stop) return NULL;
 		// irqRateが小数の場合は不等間隔になる
 		if (this.irqRate !== 0 && this.stepCount % this.irqRate >= this.irqRate - 1) {
@@ -298,46 +321,41 @@ export class Interpreter {
 
 		switch (node.type) {
 			case 'call': {
-				const callee = await this._eval(node.target, scope);
+				const callee = await this._eval(node.target, scope, callStack);
 				assertFunction(callee);
-				const args = await Promise.all(node.args.map(expr => this._eval(expr, scope)));
-				return this._fn(callee, args);
+				const args = await Promise.all(node.args.map(expr => this._eval(expr, scope, callStack)));
+				return this._fn(callee, args, callStack, node.loc.start);
 			}
 
 			case 'if': {
-				const cond = await this._eval(node.cond, scope);
+				const cond = await this._eval(node.cond, scope, callStack);
 				assertBoolean(cond);
 				if (cond.value) {
-					return this._evalClause(node.then, scope);
-				} else {
-					if (node.elseif && node.elseif.length > 0) {
-						for (const elseif of node.elseif) {
-							const cond = await this._eval(elseif.cond, scope);
-							assertBoolean(cond);
-							if (cond.value) {
-								return this._evalClause(elseif.then, scope);
-							}
-						}
-						if (node.else) {
-							return this._eval(node.else, scope);
-						}
-					} else if (node.else) {
-						return this._evalClause(node.else, scope);
+					return this._evalClause(node.then, scope, callStack);
+				}
+				for (const elseif of node.elseif) {
+					const cond = await this._eval(elseif.cond, scope, callStack);
+					assertBoolean(cond);
+					if (cond.value) {
+						return this._evalClause(elseif.then, scope, callStack);
 					}
 				}
+				if (node.else) {
+					return this._evalClause(node.else, scope, callStack);
+				}
 				return NULL;
 			}
 
 			case 'match': {
-				const about = await this._eval(node.about, scope);
+				const about = await this._eval(node.about, scope, callStack);
 				for (const qa of node.qs) {
-					const q = await this._eval(qa.q, scope);
+					const q = await this._eval(qa.q, scope, callStack);
 					if (eq(about, q)) {
-						return await this._evalClause(qa.a, scope);
+						return await this._evalClause(qa.a, scope, callStack);
 					}
 				}
 				if (node.default) {
-					return await this._evalClause(node.default, scope);
+					return await this._evalClause(node.default, scope, callStack);
 				}
 				return NULL;
 			}
@@ -345,7 +363,7 @@ export class Interpreter {
 			case 'loop': {
 				// eslint-disable-next-line no-constant-condition
 				while (true) {
-					const v = await this._run(node.statements, scope.createChildScope());
+					const v = await this._run(node.statements, scope.createChildScope(), callStack);
 					if (v.type === 'break') {
 						break;
 					} else if (v.type === 'return') {
@@ -357,10 +375,10 @@ export class Interpreter {
 
 			case 'for': {
 				if (node.times) {
-					const times = await this._eval(node.times, scope);
+					const times = await this._eval(node.times, scope, callStack);
 					assertNumber(times);
 					for (let i = 0; i < times.value; i++) {
-						const v = await this._evalClause(node.for, scope);
+						const v = await this._evalClause(node.for, scope, callStack);
 						if (v.type === 'break') {
 							break;
 						} else if (v.type === 'return') {
@@ -368,8 +386,8 @@ export class Interpreter {
 						}
 					}
 				} else {
-					const from = await this._eval(node.from!, scope);
-					const to = await this._eval(node.to!, scope);
+					const from = await this._eval(node.from!, scope, callStack);
+					const to = await this._eval(node.to!, scope, callStack);
 					assertNumber(from);
 					assertNumber(to);
 					for (let i = from.value; i < from.value + to.value; i++) {
@@ -378,7 +396,7 @@ export class Interpreter {
 								isMutable: false,
 								value: NUM(i),
 							}],
-						])));
+						])), callStack);
 						if (v.type === 'break') {
 							break;
 						} else if (v.type === 'return') {
@@ -390,12 +408,12 @@ export class Interpreter {
 			}
 
 			case 'each': {
-				const items = await this._eval(node.items, scope);
+				const items = await this._eval(node.items, scope, callStack);
 				assertArray(items);
 				for (const item of items.value) {
 					const eachScope = scope.createChildScope();
 					this.define(eachScope, node.var, item, false);
-					const v = await this._eval(node.for, eachScope);
+					const v = await this._eval(node.for, eachScope, callStack);
 					if (v.type === 'break') {
 						break;
 					} else if (v.type === 'return') {
@@ -406,17 +424,25 @@ export class Interpreter {
 			}
 
 			case 'def': {
-				const value = await this._eval(node.expr, scope);
+				const value = await this._eval(node.expr, scope, callStack);
 				if (node.attr.length > 0) {
 					const attrs: Value['attr'] = [];
 					for (const nAttr of node.attr) {
 						attrs.push({
 							name: nAttr.name,
-							value: await this._eval(nAttr.value, scope),
+							value: await this._eval(nAttr.value, scope, callStack),
 						});
 					}
 					value.attr = attrs;
 				}
+				if (
+					node.expr.type === 'fn'
+					&& node.dest.type === 'identifier'
+					&& isFunction(value)
+					&& !value.native
+				) {
+					value.name = node.dest.name;
+				}
 				await this.define(scope, node.dest, value, node.mut);
 				return NULL;
 			}
@@ -426,30 +452,30 @@ export class Interpreter {
 			}
 
 			case 'assign': {
-				const v = await this._eval(node.expr, scope);
+				const v = await this._eval(node.expr, scope, callStack);
 
-				await this.assign(scope, node.dest, v);
+				await this.assign(scope, node.dest, v, callStack);
 
 				return NULL;
 			}
 
 			case 'addAssign': {
-				const target = await this._eval(node.dest, scope);
+				const target = await this._eval(node.dest, scope, callStack);
 				assertNumber(target);
-				const v = await this._eval(node.expr, scope);
+				const v = await this._eval(node.expr, scope, callStack);
 				assertNumber(v);
 
-				await this.assign(scope, node.dest, NUM(target.value + v.value));
+				await this.assign(scope, node.dest, NUM(target.value + v.value), callStack);
 				return NULL;
 			}
 
 			case 'subAssign': {
-				const target = await this._eval(node.dest, scope);
+				const target = await this._eval(node.dest, scope, callStack);
 				assertNumber(target);
-				const v = await this._eval(node.expr, scope);
+				const v = await this._eval(node.expr, scope, callStack);
 				assertNumber(v);
 
-				await this.assign(scope, node.dest, NUM(target.value - v.value));
+				await this.assign(scope, node.dest, NUM(target.value - v.value), callStack);
 				return NULL;
 			}
 
@@ -461,18 +487,20 @@ export class Interpreter {
 
 			case 'str': return STR(node.value);
 
-			case 'arr': return ARR(await Promise.all(node.value.map(item => this._eval(item, scope))));
+			case 'arr': return ARR(await Promise.all(
+				node.value.map(item => this._eval(item, scope, callStack))
+			));
 
 			case 'obj': {
 				const obj = new Map<string, Value>();
 				for (const [key, value] of node.value) {
-					obj.set(key, await this._eval(value, scope));
+					obj.set(key, await this._eval(value, scope, callStack));
 				}
 				return OBJ(obj);
 			}
 
 			case 'prop': {
-				const target = await this._eval(node.target, scope);
+				const target = await this._eval(node.target, scope, callStack);
 				if (isObject(target)) {
 					if (target.value.has(node.name)) {
 						return target.value.get(node.name)!;
@@ -485,8 +513,8 @@ export class Interpreter {
 			}
 
 			case 'index': {
-				const target = await this._eval(node.target, scope);
-				const i = await this._eval(node.index, scope);
+				const target = await this._eval(node.target, scope, callStack);
+				const i = await this._eval(node.index, scope, callStack);
 				if (isArray(target)) {
 					assertNumber(i);
 					const item = target.value[i.value];
@@ -507,17 +535,20 @@ export class Interpreter {
 			}
 
 			case 'not': {
-				const v = await this._eval(node.expr, scope);
+				const v = await this._eval(node.expr, scope, callStack);
 				assertBoolean(v);
 				return BOOL(!v.value);
 			}
 
 			case 'fn': {
 				return FN(
-					await Promise.all(node.args.map(async (arg) => {
+					await Promise.all(node.params.map(async (param) => {
 						return {
-							dest: arg.dest,
-							default: arg.default ? await this._eval(arg.default, scope) : arg.optional ? NULL : undefined,
+							dest: param.dest,
+							default:
+								param.default ? await this._eval(param.default, scope, callStack) :
+								param.optional ? NULL :
+								undefined,
 							// type: (TODO)
 						};
 					})),
@@ -527,7 +558,7 @@ export class Interpreter {
 			}
 
 			case 'block': {
-				return this._run(node.statements, scope.createChildScope());
+				return this._run(node.statements, scope.createChildScope(), callStack);
 			}
 
 			case 'exists': {
@@ -540,7 +571,7 @@ export class Interpreter {
 					if (typeof x === 'string') {
 						str += x;
 					} else {
-						const v = await this._eval(x, scope);
+						const v = await this._eval(x, scope, callStack);
 						str += reprValue(v);
 					}
 				}
@@ -548,7 +579,7 @@ export class Interpreter {
 			}
 
 			case 'return': {
-				const val = await this._eval(node.expr, scope);
+				const val = await this._eval(node.expr, scope, callStack);
 				this.log('block:return', { scope: scope.name, val: val });
 				return RETURN(val);
 			}
@@ -574,120 +605,120 @@ export class Interpreter {
 			case 'pow': {
 				const callee = scope.get('Core:pow');
 				assertFunction(callee);
-				const left = await this._eval(node.left, scope);
-				const right = await this._eval(node.right, scope);
-				return this._fn(callee, [left, right]);
+				const left = await this._eval(node.left, scope, callStack);
+				const right = await this._eval(node.right, scope, callStack);
+				return this._fn(callee, [left, right], callStack);
 			}
 
 			case 'mul': {
 				const callee = scope.get('Core:mul');
 				assertFunction(callee);
-				const left = await this._eval(node.left, scope);
-				const right = await this._eval(node.right, scope);
-				return this._fn(callee, [left, right]);
+				const left = await this._eval(node.left, scope, callStack);
+				const right = await this._eval(node.right, scope, callStack);
+				return this._fn(callee, [left, right], callStack);
 			}
 
 			case 'div': {
 				const callee = scope.get('Core:div');
 				assertFunction(callee);
-				const left = await this._eval(node.left, scope);
-				const right = await this._eval(node.right, scope);
-				return this._fn(callee, [left, right]);
+				const left = await this._eval(node.left, scope, callStack);
+				const right = await this._eval(node.right, scope, callStack);
+				return this._fn(callee, [left, right], callStack);
 			}
 
 			case 'rem': {
 				const callee = scope.get('Core:mod');
 				assertFunction(callee);
-				const left = await this._eval(node.left, scope);
-				const right = await this._eval(node.right, scope);
-				return this._fn(callee, [left, right]);
+				const left = await this._eval(node.left, scope, callStack);
+				const right = await this._eval(node.right, scope, callStack);
+				return this._fn(callee, [left, right], callStack);
 			}
 
 			case 'add': {
 				const callee = scope.get('Core:add');
 				assertFunction(callee);
-				const left = await this._eval(node.left, scope);
-				const right = await this._eval(node.right, scope);
-				return this._fn(callee, [left, right]);
+				const left = await this._eval(node.left, scope, callStack);
+				const right = await this._eval(node.right, scope, callStack);
+				return this._fn(callee, [left, right], callStack);
 			}
 
 			case 'sub': {
 				const callee = scope.get('Core:sub');
 				assertFunction(callee);
-				const left = await this._eval(node.left, scope);
-				const right = await this._eval(node.right, scope);
-				return this._fn(callee, [left, right]);
+				const left = await this._eval(node.left, scope, callStack);
+				const right = await this._eval(node.right, scope, callStack);
+				return this._fn(callee, [left, right], callStack);
 			}
 
 			case 'lt': {
 				const callee = scope.get('Core:lt');
 				assertFunction(callee);
-				const left = await this._eval(node.left, scope);
-				const right = await this._eval(node.right, scope);
-				return this._fn(callee, [left, right]);
+				const left = await this._eval(node.left, scope, callStack);
+				const right = await this._eval(node.right, scope, callStack);
+				return this._fn(callee, [left, right], callStack);
 			}
 
 			case 'lteq': {
 				const callee = scope.get('Core:lteq');
 				assertFunction(callee);
-				const left = await this._eval(node.left, scope);
-				const right = await this._eval(node.right, scope);
-				return this._fn(callee, [left, right]);
+				const left = await this._eval(node.left, scope, callStack);
+				const right = await this._eval(node.right, scope, callStack);
+				return this._fn(callee, [left, right], callStack);
 			}
 
 			case 'gt': {
 				const callee = scope.get('Core:gt');
 				assertFunction(callee);
-				const left = await this._eval(node.left, scope);
-				const right = await this._eval(node.right, scope);
-				return this._fn(callee, [left, right]);
+				const left = await this._eval(node.left, scope, callStack);
+				const right = await this._eval(node.right, scope, callStack);
+				return this._fn(callee, [left, right], callStack);
 			}
 
 			case 'gteq': {
 				const callee = scope.get('Core:gteq');
 				assertFunction(callee);
-				const left = await this._eval(node.left, scope);
-				const right = await this._eval(node.right, scope);
-				return this._fn(callee, [left, right]);
+				const left = await this._eval(node.left, scope, callStack);
+				const right = await this._eval(node.right, scope, callStack);
+				return this._fn(callee, [left, right], callStack);
 			}
 
 			case 'eq': {
 				const callee = scope.get('Core:eq');
 				assertFunction(callee);
-				const left = await this._eval(node.left, scope);
-				const right = await this._eval(node.right, scope);
-				return this._fn(callee, [left, right]);
+				const left = await this._eval(node.left, scope, callStack);
+				const right = await this._eval(node.right, scope, callStack);
+				return this._fn(callee, [left, right], callStack);
 			}
 
 			case 'neq': {
 				const callee = scope.get('Core:neq');
 				assertFunction(callee);
-				const left = await this._eval(node.left, scope);
-				const right = await this._eval(node.right, scope);
-				return this._fn(callee, [left, right]);
+				const left = await this._eval(node.left, scope, callStack);
+				const right = await this._eval(node.right, scope, callStack);
+				return this._fn(callee, [left, right], callStack);
 			}
 
 			case 'and': {
-				const leftValue = await this._eval(node.left, scope);
+				const leftValue = await this._eval(node.left, scope, callStack);
 				assertBoolean(leftValue);
 
 				if (!leftValue.value) {
 					return leftValue;
 				} else {
-					const rightValue = await this._eval(node.right, scope);
+					const rightValue = await this._eval(node.right, scope, callStack);
 					assertBoolean(rightValue);
 					return rightValue;
 				}
 			}
 
 			case 'or': {
-				const leftValue = await this._eval(node.left, scope);
+				const leftValue = await this._eval(node.left, scope, callStack);
 				assertBoolean(leftValue);
 
 				if (leftValue.value) {
 					return leftValue;
 				} else {
-					const rightValue = await this._eval(node.right, scope);
+					const rightValue = await this._eval(node.right, scope, callStack);
 					assertBoolean(rightValue);
 					return rightValue;
 				}
@@ -700,7 +731,7 @@ export class Interpreter {
 	}
 
 	@autobind
-	private async _run(program: Ast.Node[], scope: Scope): Promise<Value> {
+	private async _run(program: Ast.Node[], scope: Scope, callStack: readonly CallInfo[]): Promise<Value> {
 		this.log('block:enter', { scope: scope.name });
 
 		let v: Value = NULL;
@@ -708,7 +739,7 @@ export class Interpreter {
 		for (let i = 0; i < program.length; i++) {
 			const node = program[i]!;
 
-			v = await this._eval(node, scope);
+			v = await this._eval(node, scope, callStack);
 			if (v.type === 'return') {
 				this.log('block:return', { scope: scope.name, val: v.value });
 				return v;
@@ -772,15 +803,20 @@ export class Interpreter {
 	}
 
 	@autobind
-	private async assign(scope: Scope, dest: Ast.Expression, value: Value): Promise<void> {
+	private async assign(
+		scope: Scope,
+		dest: Ast.Expression,
+		value: Value,
+		callStack: readonly CallInfo[],
+	): Promise<void> {
 		switch (dest.type) {
 			case 'identifier': {
 				scope.assign(dest.name, value);
 				break;
 			}
 			case 'index': {
-				const assignee = await this._eval(dest.target, scope);
-				const i = await this._eval(dest.index, scope);
+				const assignee = await this._eval(dest.target, scope, callStack);
+				const i = await this._eval(dest.index, scope, callStack);
 				if (isArray(assignee)) {
 					assertNumber(i);
 					if (assignee.value[i.value] === undefined) {
@@ -796,7 +832,7 @@ export class Interpreter {
 				break;
 			}
 			case 'prop': {
-				const assignee = await this._eval(dest.target, scope);
+				const assignee = await this._eval(dest.target, scope, callStack);
 				assertObject(assignee);
 
 				assignee.value.set(dest.name, value);
@@ -805,14 +841,14 @@ export class Interpreter {
 			case 'arr': {
 				assertArray(value);
 				await Promise.all(dest.value.map(
-					(item, index) => this.assign(scope, item, value.value[index] ?? NULL),
+					(item, index) => this.assign(scope, item, value.value[index] ?? NULL, callStack),
 				));
 				break;
 			}
 			case 'obj': {
 				assertObject(value);
 				await Promise.all([...dest.value].map(
-					([key, item]) => this.assign(scope, item, value.value.get(key) ?? NULL),
+					([key, item]) => this.assign(scope, item, value.value.get(key) ?? NULL, callStack),
 				));
 				break;
 			}
diff --git a/src/interpreter/scope.ts b/src/interpreter/scope.ts
index a440963..3414255 100644
--- a/src/interpreter/scope.ts
+++ b/src/interpreter/scope.ts
@@ -70,6 +70,15 @@ export class Scope {
 			{ scope: this.layerdStates });
 	}
 
+	/**
+	 * 名前空間名を取得します。
+	 */
+	@autobind
+	public getNsPrefix(): string {
+		if (this.parent == null || this.nsName == null) return '';
+		return this.parent.getNsPrefix() + this.nsName + ':';
+	}
+
 	/**
 	 * 指定した名前の変数が存在するか判定します
 	 * @param name - 変数名
diff --git a/src/interpreter/util.ts b/src/interpreter/util.ts
index 6619309..ccc5a9a 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.dest.type === 'identifier' ? v.dest.name : '?')).join(', ')} ) { ... }`;
+			return `@( ${(value.params.map(v => v.dest.type === 'identifier' ? v.dest.name : '?')).join(', ')} ) { ... }`;
 		}
 	}
 
diff --git a/src/interpreter/value.ts b/src/interpreter/value.ts
index a5d271a..6e03882 100644
--- a/src/interpreter/value.ts
+++ b/src/interpreter/value.ts
@@ -37,11 +37,12 @@ type VFnBase = {
 };
 export type VUserFn = VFnBase & {
 	native?: undefined; // if (vfn.native) で型アサーション出来るように
-	args: VFnArg[];
+	name?: string;
+	params: VFnParam[];
 	statements: Node[];
 	scope: Scope;
 };
-export type VFnArg = {
+export type VFnParam = {
 	dest: Expression;
 	type?: Type;
 	default?: Value;
@@ -127,9 +128,9 @@ export const ARR = (arr: VArr['value']): VArr => ({
 	value: arr,
 });
 
-export const FN = (args: VUserFn['args'], statements: VUserFn['statements'], scope: VUserFn['scope']): VUserFn => ({
+export const FN = (params: VUserFn['params'], statements: VUserFn['statements'], scope: VUserFn['scope']): VUserFn => ({
 	type: 'fn' as const,
-	args: args,
+	params: params,
 	statements: statements,
 	scope: scope,
 });
diff --git a/src/node.ts b/src/node.ts
index 2961905..d8e9476 100644
--- a/src/node.ts
+++ b/src/node.ts
@@ -264,7 +264,7 @@ export type If = NodeBase & {
 
 export type Fn = NodeBase & {
 	type: 'fn'; // 関数
-	args: {
+	params: {
 		dest: Expression; // 引数名
 		optional: boolean;
 		default?: Expression; // 引数の初期値
@@ -363,6 +363,6 @@ export type NamedTypeSource = NodeBase & {
 
 export type FnTypeSource = NodeBase & {
 	type: 'fnTypeSource'; // 関数の型
-	args: TypeSource[]; // 引数の型
+	params: TypeSource[]; // 引数の型
 	result: TypeSource; // 戻り値の型
 };
diff --git a/src/parser/plugins/validate-keyword.ts b/src/parser/plugins/validate-keyword.ts
index 1198193..024a1de 100644
--- a/src/parser/plugins/validate-keyword.ts
+++ b/src/parser/plugins/validate-keyword.ts
@@ -111,8 +111,8 @@ function validateNode(node: Ast.Node): Ast.Node {
 			break;
 		}
 		case 'fn': {
-			for (const arg of node.args) {
-				validateDest(arg.dest);
+			for (const param of node.params) {
+				validateDest(param.dest);
 			}
 			break;
 		}
diff --git a/src/parser/plugins/validate-type.ts b/src/parser/plugins/validate-type.ts
index 3dbc0cf..6fc8660 100644
--- a/src/parser/plugins/validate-type.ts
+++ b/src/parser/plugins/validate-type.ts
@@ -11,9 +11,9 @@ function validateNode(node: Ast.Node): Ast.Node {
 			break;
 		}
 		case 'fn': {
-			for (const arg of node.args) {
-				if (arg.argType != null) {
-					getTypeBySource(arg.argType);
+			for (const param of node.params) {
+				if (param.argType != null) {
+					getTypeBySource(param.argType);
 				}
 			}
 			if (node.retType != null) {
diff --git a/src/parser/scanner.ts b/src/parser/scanner.ts
index 7560216..ae161bc 100644
--- a/src/parser/scanner.ts
+++ b/src/parser/scanner.ts
@@ -118,7 +118,7 @@ export class Scanner implements ITokenStream {
 			const pos = this.stream.getPos();
 
 			if (lineBreakChars.includes(this.stream.char)) {
-				this.stream.next();
+				this.skipEmptyLines();
 				return TOKEN(TokenKind.NewLine, pos, { hasLeftSpacing });
 			}
 
@@ -578,6 +578,33 @@ export class Scanner implements ITokenStream {
 		return TOKEN(TokenKind.Template, pos, { hasLeftSpacing, children: elements });
 	}
 
+	private skipEmptyLines(): void {
+		while (!this.stream.eof) {
+			// skip spacing
+			if (spaceChars.includes(this.stream.char) || lineBreakChars.includes(this.stream.char)) {
+				this.stream.next();
+				continue;
+			}
+
+			if (this.stream.char === '/') {
+				this.stream.next();
+				if (!this.stream.eof && (this.stream.char as string) === '*') {
+					this.stream.next();
+					this.skipCommentRange();
+					continue;
+				} else if (!this.stream.eof && (this.stream.char as string) === '/') {
+					this.stream.next();
+					this.skipCommentLine();
+					continue;
+				} else {
+					this.stream.prev();
+					break;
+				}
+			}
+			break;
+		}
+	}
+
 	private skipCommentLine(): void {
 		while (true) {
 			if (this.stream.eof) {
diff --git a/src/parser/syntaxes/common.ts b/src/parser/syntaxes/common.ts
index c94226b..48cb68b 100644
--- a/src/parser/syntaxes/common.ts
+++ b/src/parser/syntaxes/common.ts
@@ -29,8 +29,8 @@ export function parseDest(s: ITokenStream): Ast.Expression {
  * Params = "(" [Dest [":" Type] *(SEP Dest [":" Type])] ")"
  * ```
 */
-export function parseParams(s: ITokenStream): Ast.Fn['args'] {
-	const items: Ast.Fn['args'] = [];
+export function parseParams(s: ITokenStream): Ast.Fn['params'] {
+	const items: Ast.Fn['params'] = [];
 
 	s.expect(TokenKind.OpenParen);
 	s.next();
@@ -200,7 +200,7 @@ function parseFnType(s: ITokenStream): Ast.TypeSource {
 
 	const resultType = parseType(s);
 
-	return NODE('fnTypeSource', { args: params, result: resultType }, startPos, s.getPos());
+	return NODE('fnTypeSource', { params, result: resultType }, startPos, s.getPos());
 }
 
 /**
@@ -211,9 +211,15 @@ function parseFnType(s: ITokenStream): Ast.TypeSource {
 function parseNamedType(s: ITokenStream): Ast.TypeSource {
 	const startPos = s.getPos();
 
-	s.expect(TokenKind.Identifier);
-	const name = s.getTokenValue();
-	s.next();
+	let name: string;
+	if (s.is(TokenKind.Identifier)) {
+		name = s.getTokenValue();
+		s.next();
+	} else {
+		s.expect(TokenKind.NullKeyword);
+		s.next();
+		name = "null";
+	}
 
 	// inner type
 	let inner: Ast.TypeSource | undefined;
diff --git a/src/parser/syntaxes/expressions.ts b/src/parser/syntaxes/expressions.ts
index 2736e0f..8088768 100644
--- a/src/parser/syntaxes/expressions.ts
+++ b/src/parser/syntaxes/expressions.ts
@@ -399,7 +399,7 @@ function parseFnExpr(s: ITokenStream): Ast.Fn {
 
 	const body = parseBlock(s);
 
-	return NODE('fn', { args: params, retType: type, children: body }, startPos, s.getPos());
+	return NODE('fn', { params: params, retType: type, children: body }, startPos, s.getPos());
 }
 
 /**
diff --git a/src/parser/syntaxes/statements.ts b/src/parser/syntaxes/statements.ts
index 4f1b592..52719db 100644
--- a/src/parser/syntaxes/statements.ts
+++ b/src/parser/syntaxes/statements.ts
@@ -174,7 +174,7 @@ function parseFnDef(s: ITokenStream): Ast.Definition {
 	return NODE('def', {
 		dest,
 		expr: NODE('fn', {
-			args: params,
+			params: params,
 			retType: type,
 			children: body,
 		}, startPos, endPos),
diff --git a/src/parser/visit.ts b/src/parser/visit.ts
index d3aa0d5..84c30e3 100644
--- a/src/parser/visit.ts
+++ b/src/parser/visit.ts
@@ -61,9 +61,9 @@ export function visitNode(node: Ast.Node, fn: (node: Ast.Node) => Ast.Node): Ast
 			break;
 		}
 		case 'fn': {
-			for (const i of result.args.keys()) {
-				if (result.args[i]!.default) {
-					result.args[i]!.default = visitNode(result.args[i]!.default!, fn) as Ast.Fn['args'][number]['default'];
+			for (const param of result.params) {
+				if (param.default) {
+					param.default = visitNode(param.default!, fn) as Ast.Fn['params'][number]['default'];
 				}
 			}
 			for (let i = 0; i < result.children.length; i++) {
diff --git a/src/type.ts b/src/type.ts
index 94e835a..92f03b4 100644
--- a/src/type.ts
+++ b/src/type.ts
@@ -35,14 +35,14 @@ export function T_GENERIC<N extends string>(name: N, inners: Type[]): TGeneric<N
 
 export type TFn = {
 	type: 'fn';
-	args: Type[];
+	params: Type[];
 	result: Type;
 };
 
-export function T_FN(args: Type[], result: Type): TFn {
+export function T_FN(params: Type[], result: Type): TFn {
 	return {
 		type: 'fn',
-		args,
+		params,
 		result,
 	};
 }
@@ -80,10 +80,10 @@ export function isCompatibleType(a: Type, b: Type): boolean {
 			assertTFn(b); // NOTE: TypeGuardが効かない
 			// fn result
 			if (!isCompatibleType(a.result, b.result)) return false;
-			// fn args
-			if (a.args.length !== b.args.length) return false;
-			for (let i = 0; i < a.args.length; i++) {
-				if (!isCompatibleType(a.args[i]!, b.args[i]!)) return false;
+			// fn parameters
+			if (a.params.length !== b.params.length) return false;
+			for (let i = 0; i < a.params.length; i++) {
+				if (!isCompatibleType(a.params[i]!, b.params[i]!)) return false;
 			}
 			break;
 		}
@@ -101,7 +101,7 @@ export function getTypeName(type: Type): string {
 			return `${type.name}<${type.inners.map(inner => getTypeName(inner)).join(', ')}>`;
 		}
 		case 'fn': {
-			return `@(${type.args.map(arg => getTypeName(arg)).join(', ')}) { ${getTypeName(type.result)} }`;
+			return `@(${type.params.map(param => getTypeName(param)).join(', ')}) { ${getTypeName(type.result)} }`;
 		}
 	}
 }
@@ -117,9 +117,9 @@ export function getTypeNameBySource(typeSource: Ast.TypeSource): string {
 			}
 		}
 		case 'fnTypeSource': {
-			const args = typeSource.args.map(arg => getTypeNameBySource(arg)).join(', ');
+			const params = typeSource.params.map(param => getTypeNameBySource(param)).join(', ');
 			const result = getTypeNameBySource(typeSource.result);
-			return `@(${args}) { ${result} }`;
+			return `@(${params}) { ${result} }`;
 		}
 	}
 }
@@ -153,7 +153,7 @@ export function getTypeBySource(typeSource: Ast.TypeSource): Type {
 		}
 		throw new AiScriptSyntaxError(`Unknown type: '${getTypeNameBySource(typeSource)}'`, typeSource.loc.start);
 	} else {
-		const argTypes = typeSource.args.map(arg => getTypeBySource(arg));
-		return T_FN(argTypes, getTypeBySource(typeSource.result));
+		const paramTypes = typeSource.params.map(param => getTypeBySource(param));
+		return T_FN(paramTypes, getTypeBySource(typeSource.result));
 	}
 }
diff --git a/unreleased/accept-multi-new-line.md b/unreleased/accept-multi-new-line.md
new file mode 100644
index 0000000..2a85e35
--- /dev/null
+++ b/unreleased/accept-multi-new-line.md
@@ -0,0 +1,8 @@
+- 複数の改行が1つの改行と同等に扱われるようになりました。次に示す部分で複数の改行を挿入できます。
+  - 空のmatch式の波括弧内および、match式の各case節やdefault節の前後
+  - 引数のない関数呼び出しの丸括弧内および、関数呼び出しの各引数の前後
+  - 引数のない関数定義の丸括弧内および、関数定義の各引数の前後
+  - if式において、then節やelif節、else節の間
+  - 単項演算や二項演算において、バックスラッシュの後
+  - 変数定義において、等号と式の間
+  - 属性と文の間
diff --git a/unreleased/callstack.md b/unreleased/callstack.md
new file mode 100644
index 0000000..c7ec308
--- /dev/null
+++ b/unreleased/callstack.md
@@ -0,0 +1,1 @@
+- ランタイムエラーにコールスタックの情報を追加。
diff --git a/unreleased/rename-arg-param.md b/unreleased/rename-arg-param.md
new file mode 100644
index 0000000..dfc71e0
--- /dev/null
+++ b/unreleased/rename-arg-param.md
@@ -0,0 +1,1 @@
+- For Hosts: いくつかの型で args を params にリネームしました。


Test patch:
diff --git a/test/index.ts b/test/index.ts
index 6a1c361..af97bf1 100644
--- a/test/index.ts
+++ b/test/index.ts
@@ -816,6 +816,22 @@ describe('type declaration', () => {
 		`);
 		eq(res, ARR([NUM(1), NUM(2), NUM(3), NUM(0), NUM(5)]));
 	});
+
+	test.concurrent('def (null)', async () => {
+		const res = await exe(`
+		let a: null = null
+		<: a
+		`);
+		eq(res, NULL);
+	});
+
+	test.concurrent('fn def (null)', async () => {
+		const res = await exe(`
+		@f(): null {}
+		<: f()
+		`);
+		eq(res, NULL);
+	});
 });
 
 describe('Attribute', () => {
diff --git a/test/interpreter.ts b/test/interpreter.ts
index 203b5a4..09cb545 100644
--- a/test/interpreter.ts
+++ b/test/interpreter.ts
@@ -115,6 +115,56 @@ describe('error location', () => {
 	});
 });
 
+describe('callstack', () => {
+	const exeAndGetErrMessage = (src: string): Promise<string> => new Promise((ok, ng) => {
+		const aiscript = new Interpreter({
+			emitError: FN_NATIVE((_args, _opts) => {
+				throw Error('emitError');
+			}),
+		}, {
+			err(e) { ok(e.message) },
+		});
+		aiscript.exec(Parser.parse(src)).then(() => ng('error has not occurred.'));
+	});
+
+	test('error in function', async () => {
+		const result = await exeAndGetErrMessage(`
+			@function1() { emitError() }
+			@function2() { function1() }
+			function2()
+		`);
+		expect(result).toMatchInlineSnapshot(`
+			"emitError
+			  at function1 (Line 2, Column 28)
+			  at function2 (Line 3, Column 28)
+			  at <root> (Line 4, Column 13)"
+		`);
+	});
+	test('error in function in namespace', async () => {
+		const result = await exeAndGetErrMessage(`
+			:: Ai {
+				@function() { emitError() }
+			}
+			Ai:function()
+		`);
+		expect(result).toMatchInlineSnapshot(`
+			"emitError
+			  at Ai:function (Line 3, Column 28)
+			  at <root> (Line 5, Column 15)"
+		`);
+	});
+	test('error in anonymous function', async () => {
+		const result = await exeAndGetErrMessage(`
+			(@() { emitError() })()
+		`);
+		expect(result).toMatchInlineSnapshot(`
+			"emitError
+			  at <anonymous> (Line 2, Column 20)
+			  at <root> (Line 2, Column 25)"
+		`);
+	});
+});
+
 describe('IRQ', () => {
 	describe('irqSleep is function', () => {
 		async function countSleeps(irqRate: number): Promise<number> {
diff --git a/test/newline.ts b/test/newline.ts
new file mode 100644
index 0000000..8d19501
--- /dev/null
+++ b/test/newline.ts
@@ -0,0 +1,322 @@
+import { describe, test } from "vitest";
+import { utils } from '../src';
+import { NUM, STR, NULL, ARR, OBJ, BOOL, TRUE, FALSE, ERROR ,FN_NATIVE } from '../src/interpreter/value';
+import { exe, getMeta, eq } from './testutils';
+
+describe('empty lines', () => {
+    describe('match', () => {
+		test.concurrent('empty line', async () => {
+			const res = await exe(`
+			<: match 1 {
+				// comment
+			}
+			`);
+			eq(res, NULL);
+		});
+
+		test.concurrent('empty line before case', async () => {
+			const res = await exe(`
+			<: match 1 {
+				// comment
+				case 1 => 1
+			}
+			`);
+			eq(res, NUM(1));
+		});
+
+		test.concurrent('empty line after case', async () => {
+			const res = await exe(`
+			<: match 1 {
+				case 1 => 1
+				// comment
+			}
+			`);
+			eq(res, NUM(1));
+		});
+
+		test.concurrent('empty line before default', async () => {
+			const res = await exe(`
+			<: match 1 {
+				// comment
+				default => 1
+			}
+			`);
+			eq(res, NUM(1));
+		});
+
+		test.concurrent('empty line after default', async () => {
+			const res = await exe(`
+			<: match 1 {
+				default => 1
+				// comment
+			}
+			`);
+			eq(res, NUM(1));
+		});
+    });
+
+    describe('call', () => {
+		test.concurrent('empty line', async () => {
+			const res = await exe(`
+			@f() {
+				1
+			}
+			<:f(
+				// comment
+			)
+			`);
+			eq(res, NUM(1));
+		});
+
+		test.concurrent('empty line before', async () => {
+			const res = await exe(`
+			@f(a) {
+				a
+			}
+			<:f(
+				// comment
+				1
+			)
+			`);
+			eq(res, NUM(1));
+		});
+
+		test.concurrent('empty line after', async () => {
+			const res = await exe(`
+			@f(a) {
+				a
+			}
+			<:f(
+				1
+				// comment
+			)
+			`);
+			eq(res, NUM(1));
+		});
+    });
+
+    describe('function params', () => {
+		test.concurrent('empty line', async () => {
+			const res = await exe(`
+			@f(
+				// comment
+			) {
+				1
+			}
+			<: f()
+			`);
+			eq(res, NUM(1));
+		});
+
+		test.concurrent('empty line before', async () => {
+			const res = await exe(`
+			@f(
+				// comment
+				a
+			) {
+				a
+			}
+			<: f(1)
+			`);
+			eq(res, NUM(1));
+		});
+
+		test.concurrent('empty line after', async () => {
+			const res = await exe(`
+			@f(
+				a
+				// comment
+			) {
+				a
+			}
+			<: f(1)
+			`);
+			eq(res, NUM(1));
+		});
+    });
+
+    describe('if', () => {
+        test.concurrent('empty line between if ~ elif', async () => {
+            const res = await exe(`
+            <: if true {
+                1
+            }
+            // comment
+            elif true {
+                2
+            }
+            `);
+            eq(res, NUM(1));
+        });
+
+        test.concurrent('empty line between if ~ elif ~ elif', async () => {
+            const res = await exe(`
+            <: if true {
+                1
+            }
+            // comment
+            elif true {
+                2
+            }
+            // comment
+            elif true {
+                3
+            }
+            `);
+            eq(res, NUM(1));
+        });
+
+        test.concurrent('empty line between if ~ else', async () => {
+            const res = await exe(`
+            <: if true {
+                1
+            }
+            // comment
+            else {
+                2
+            }
+            `);
+            eq(res, NUM(1));
+        });
+
+        test.concurrent('empty line between if ~ elif ~ else', async () => {
+            const res = await exe(`
+            <: if true {
+                1
+            }
+            // comment
+            elif true {
+                2
+            }
+            // comment
+            else {
+                3
+            }
+            `);
+            eq(res, NUM(1));
+        });
+    });
+
+    describe('unary operation', () => {
+        test.concurrent('empty line after', async () => {
+            const res = await exe(`
+            ! \\
+            // comment
+            true
+            `);
+            eq(res, BOOL(false));
+        });
+    });
+
+    describe('binary operation', () => {
+        test.concurrent('empty line before', async () => {
+            const res = await exe(`
+            <: 2 \\
+            // comment
+            * 3
+            `);
+            eq(res, NUM(6));
+        });
+    });
+
+    describe('binary operation', () => {
+        test.concurrent('empty line after', async () => {
+            const res = await exe(`
+            <: 2 * \\
+            // comment
+            3
+            `);
+            eq(res, NUM(6));
+        });
+    });
+
+    describe('variable definition', () => {
+        test.concurrent('empty line after equal', async () => {
+            const res = await exe(`
+            let a =
+            // comment
+            1
+            <: a
+            `);
+            eq(res, NUM(1));
+        });
+    });
+
+    describe('attribute', () => {
+        test.concurrent('empty line after', async () => {
+            const res = await exe(`
+            #[abc]
+            // comment
+            let a = 1
+            <: a
+            `);
+            eq(res, NUM(1));
+        });
+    });
+
+    describe('obj literal', () => {
+        test.concurrent('empty line', async () => {
+            const res = await exe(`
+            <: {
+                // comment
+            }
+            `);
+            eq(res, OBJ(new Map()));
+        });
+
+        test.concurrent('empty line before', async () => {
+            const res = await exe(`
+            let x = {
+                // comment
+                a: 1
+            }
+            <: x.a
+            `);
+            eq(res, NUM(1));
+        });
+
+        test.concurrent('empty line after', async () => {
+            const res = await exe(`
+            let x = {
+                a: 1
+                // comment
+            }
+            <: x.a
+            `);
+            eq(res, NUM(1));
+        });
+    });
+
+    describe('arr literal', () => {
+        test.concurrent('empty line', async () => {
+            const res = await exe(`
+            <: [
+                // comment
+            ]
+            `);
+            eq(res, ARR([]));
+        });
+
+        test.concurrent('empty line before', async () => {
+            const res = await exe(`
+            let x = [
+                // comment
+                1
+            ]
+            <: x[0]
+            `);
+            eq(res, NUM(1));
+        });
+
+        test.concurrent('empty line after', async () => {
+            const res = await exe(`
+            let x = [
+                1
+                // comment
+            ]
+            <: x[0]
+            `);
+            eq(res, NUM(1));
+        });
+    });
+});
diff --git a/test/parser.ts b/test/parser.ts
index 89f9aaf..c2588b0 100644
--- a/test/parser.ts
+++ b/test/parser.ts
@@ -144,4 +144,14 @@ describe('Scanner', () => {
 		next(stream, TokenKind.Identifier, { line: 1, column: 2 }, { value: 'abc' });
 		next(stream, TokenKind.OpenParen, { line: 1, column: 5 }, { });
 	});
+	test.concurrent('empty lines', async () => {
+		const source = "match 1{\n// comment\n}";
+		const stream = init(source);
+		next(stream, TokenKind.MatchKeyword, { line: 1, column: 1 }, { });
+		next(stream, TokenKind.NumberLiteral, { line: 1, column: 7 }, { hasLeftSpacing: true, value: '1' });
+		next(stream, TokenKind.OpenBrace, { line: 1, column: 8 }, { });
+		next(stream, TokenKind.NewLine, { line: 1, column: 9 }, { });
+		next(stream, TokenKind.CloseBrace, { line: 3, column: 1 }, { });
+		next(stream, TokenKind.EOF, { line: 3, column: 2 }, { });
+	});
 });