Repository: aiscript-dev/aiscript
Pull request #821 (2024-10-23 19:03:04)

lintが./built内の*.jsファイルに反応する問題を修正
<!-- ℹ お読みください
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? -->
現状では、ビルド後にlintを行うとなぜか`./built`下のいくつかのファイルがスキャンされてエラーを発生させる問題があります。
eslintのglobal ignoreという機能を使うことで解決するようなので、そのようにconfigを修正します。
# Why
<!-- なぜそうするのか？ どういう意図なのか？ 何が困っているのか？ -->
<!-- Why do you do it? What are your intentions? What is the problem? -->
https://stackoverflow.com/a/79115209/22200513
# Additional info (optional)
<!-- テスト観点など -->
<!-- Test perspective, etc -->
CIではビルドを挟まずにlintを行っているため、この問題の影響がありません。

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/821?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/821/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/workflows/api.yml b/.github/workflows/api.yml
index d388e4b..486f8de 100644
--- a/.github/workflows/api.yml
+++ b/.github/workflows/api.yml
@@ -14,10 +14,10 @@ jobs:
 
     steps:
       - name: Checkout
-        uses: actions/checkout@v4.2.1
+        uses: actions/checkout@v4.2.2
 
       - name: Setup Node.js
-        uses: actions/setup-node@v4.0.4
+        uses: actions/setup-node@v4.1.0
         with:
           node-version: 20.x
 
diff --git a/.github/workflows/gh-pages.yml b/.github/workflows/gh-pages.yml
index 96c1a85..cce0243 100644
--- a/.github/workflows/gh-pages.yml
+++ b/.github/workflows/gh-pages.yml
@@ -12,10 +12,10 @@ jobs:
 
     steps:
       - name: Checkout
-        uses: actions/checkout@v4.2.1
+        uses: actions/checkout@v4.2.2
 
       - name: Setup Node.js
-        uses: actions/setup-node@v4.0.4
+        uses: actions/setup-node@v4.1.0
         with:
           node-version: 20.x
 
diff --git a/.github/workflows/lint.yml b/.github/workflows/lint.yml
index d6645d1..6f2f357 100644
--- a/.github/workflows/lint.yml
+++ b/.github/workflows/lint.yml
@@ -14,10 +14,10 @@ jobs:
 
     steps:
       - name: Checkout
-        uses: actions/checkout@v4.2.1
+        uses: actions/checkout@v4.2.2
 
       - name: Setup Node.js
-        uses: actions/setup-node@v4.0.4
+        uses: actions/setup-node@v4.1.0
         with:
           node-version: 20.x
 
diff --git a/.github/workflows/publish-nightly.yml b/.github/workflows/publish-nightly.yml
index 87f8995..dcc57a8 100644
--- a/.github/workflows/publish-nightly.yml
+++ b/.github/workflows/publish-nightly.yml
@@ -12,7 +12,7 @@ jobs:
       matrix: ${{ steps.getb.outputs.matrix }}
     steps:
       - name: Checkout
-        uses: actions/checkout@v4.2.1
+        uses: actions/checkout@v4.2.2
         with:
           fetch-depth: 0
           
@@ -46,12 +46,12 @@ jobs:
 
     steps:
       - name: Checkout ${{ matrix.branch }}
-        uses: actions/checkout@v4.2.1
+        uses: actions/checkout@v4.2.2
         with:
           ref: ${{ matrix.branch }}
 
       - name: Setup Node.js
-        uses: actions/setup-node@v4.0.4
+        uses: actions/setup-node@v4.1.0
         with:
           node-version: 20.x
 
diff --git a/docs/std-math.md b/docs/std-math.md
index ffa2f84..987462f 100644
--- a/docs/std-math.md
+++ b/docs/std-math.md
@@ -95,7 +95,7 @@ _y_ /_x_ の正接を返しますが、_x_ が負値の場合はπだけずれ
 
 ## 指数・対数関数
 ### @Math:pow(_x_: num, _y_: num): num
-_x_ の _y_ 乗を計算します。結果がNaNとなることを許容する点、内部的にJavascriptの`**`演算子ではなく`Math.pow`関数を用いている点の２つを除き、ほぼ`Core:pow`と同じものです。
+_x_ の _y_ 乗を計算します。内部的にJavascriptの`**`演算子ではなく`Math.pow`関数を用いている点を除き、ほぼ`Core:pow`と同じものです。
 
 ### @Math:exp(_x_: num): num
 eの _x_ 乗を計算します。  
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 34eb921..a7d9375 100644
--- a/etc/aiscript.api.md
+++ b/etc/aiscript.api.md
@@ -34,6 +34,13 @@ abstract class AiScriptError extends Error {
     pos?: Pos;
 }
 
+// @public
+class AiScriptHostsideError extends AiScriptError {
+    constructor(message: string, info?: unknown);
+    // (undocumented)
+    name: string;
+}
+
 // @public
 class AiScriptIndexOutOfRangeError extends AiScriptRuntimeError {
     constructor(message: string, info?: unknown);
@@ -281,7 +288,8 @@ declare namespace errors {
         AiScriptNamespaceError,
         AiScriptRuntimeError,
         AiScriptIndexOutOfRangeError,
-        AiScriptUserError
+        AiScriptUserError,
+        AiScriptHostsideError
     }
 }
 export { errors }
@@ -305,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;
@@ -326,7 +334,7 @@ const FN_NATIVE: (fn: VNativeFn["native"]) => VNativeFn;
 // @public (undocumented)
 type FnTypeSource = NodeBase & {
     type: 'fnTypeSource';
-    args: TypeSource[];
+    params: TypeSource[];
     result: TypeSource;
 };
 
@@ -391,6 +399,8 @@ export class Interpreter {
         log?(type: string, params: LogObject): void;
         maxStep?: number;
         abortOnError?: boolean;
+        irqRate?: number;
+        irqSleep?: number | (() => Promise<void>);
     });
     // (undocumented)
     abort(): void;
@@ -635,6 +645,7 @@ export class Scope {
     exists(name: string): boolean;
     get(name: string): Value;
     getAll(): Map<string, Variable>;
+    getNsPrefix(): string;
     // (undocumented)
     name: string;
     // (undocumented)
@@ -735,7 +746,7 @@ declare namespace values {
         VObj,
         VFn,
         VUserFn,
-        VFnArg,
+        VFnParam,
         VNativeFn,
         VReturn,
         VBreak,
@@ -797,7 +808,7 @@ type VError = {
 type VFn = VUserFn | VNativeFn;
 
 // @public (undocumented)
-type VFnArg = {
+type VFnParam = {
     dest: Expression;
     type?: Type;
     default?: Value;
@@ -847,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:39: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/package-lock.json b/package-lock.json
index fca6e62..f98e6fd 100644
--- a/package-lock.json
+++ b/package-lock.json
@@ -14,21 +14,21 @@
 				"uuid": "10.0.0"
 			},
 			"devDependencies": {
-				"@eslint/js": "9.12.0",
-				"@microsoft/api-extractor": "7.47.9",
+				"@eslint/js": "9.13.0",
+				"@microsoft/api-extractor": "7.47.11",
 				"@types/eslint__js": "8.42.3",
-				"@types/node": "22.7.5",
+				"@types/node": "22.7.9",
 				"@types/seedrandom": "3.0.8",
 				"@types/uuid": "10.0.0",
-				"@vitest/coverage-v8": "2.1.2",
+				"@vitest/coverage-v8": "2.1.3",
 				"chalk": "5.3.0",
-				"eslint": "9.12.0",
+				"eslint": "9.13.0",
 				"eslint-plugin-import": "2.31.0",
 				"semver": "7.6.3",
 				"ts-node": "10.9.2",
-				"typescript": "5.6.2",
-				"typescript-eslint": "8.8.1",
-				"vitest": "2.1.2"
+				"typescript": "5.6.3",
+				"typescript-eslint": "8.11.0",
+				"vitest": "2.1.3"
 			}
 		},
 		"node_modules/@ampproject/remapping": {
@@ -563,11 +563,10 @@
 			}
 		},
 		"node_modules/@eslint/core": {
-			"version": "0.6.0",
-			"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.6.0.tgz",
-			"integrity": "sha512-8I2Q8ykA4J0x0o7cg67FPVnehcqWTBehu/lmY+bolPFHGjh49YzGBMXTvpqVgEbBdvNCSxj6iFgiIyHzf03lzg==",
+			"version": "0.7.0",
+			"resolved": "https://registry.npmjs.org/@eslint/core/-/core-0.7.0.tgz",
+			"integrity": "sha512-xp5Jirz5DyPYlPiKat8jaq0EmYvDXKKpzTbxXMpT9eqlRJkRKIz9AGMdlvYjih+im+QlhWrpvVjl8IPC/lHlUw==",
 			"dev": true,
-			"license": "Apache-2.0",
 			"engines": {
 				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
 			}
@@ -634,11 +633,10 @@
 			}
 		},
 		"node_modules/@eslint/js": {
-			"version": "9.12.0",
-			"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.12.0.tgz",
-			"integrity": "sha512-eohesHH8WFRUprDNyEREgqP6beG6htMeUYeCpkEgBCieCMme5r9zFWjzAJp//9S+Kub4rqE+jXe9Cp1a7IYIIA==",
+			"version": "9.13.0",
+			"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.13.0.tgz",
+			"integrity": "sha512-IFLyoY4d72Z5y/6o/BazFBezupzI/taV8sGumxTAVw3lXG9A6md1Dc34T9s1FoD/an9pJH8RHbAxsaEbBed9lA==",
 			"dev": true,
-			"license": "MIT",
 			"engines": {
 				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
 			}
@@ -800,9 +798,9 @@
 			}
 		},
 		"node_modules/@microsoft/api-extractor": {
-			"version": "7.47.9",
-			"resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.47.9.tgz",
-			"integrity": "sha512-TTq30M1rikVsO5wZVToQT/dGyJY7UXJmjiRtkHPLb74Prx3Etw8+bX7Bv7iLuby6ysb7fuu1NFWqma+csym8Jw==",
+			"version": "7.47.11",
+			"resolved": "https://registry.npmjs.org/@microsoft/api-extractor/-/api-extractor-7.47.11.tgz",
+			"integrity": "sha512-lrudfbPub5wzBhymfFtgZKuBvXxoSIAdrvS2UbHjoMT2TjIEddq6Z13pcve7A03BAouw0x8sW8G4txdgfiSwpQ==",
 			"dev": true,
 			"dependencies": {
 				"@microsoft/api-extractor-model": "7.29.8",
@@ -811,7 +809,7 @@
 				"@rushstack/node-core-library": "5.9.0",
 				"@rushstack/rig-package": "0.5.3",
 				"@rushstack/terminal": "0.14.2",
-				"@rushstack/ts-command-line": "4.22.8",
+				"@rushstack/ts-command-line": "4.23.0",
 				"lodash": "~4.17.15",
 				"minimatch": "~3.0.3",
 				"resolve": "~1.22.1",
@@ -1253,9 +1251,9 @@
 			}
 		},
 		"node_modules/@rushstack/ts-command-line": {
-			"version": "4.22.8",
-			"resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.22.8.tgz",
-			"integrity": "sha512-XbFjOoV7qZHJnSuFUHv0pKaFA4ixyCuki+xMjsMfDwfvQjs5MYG0IK5COal3tRnG7KCDe2l/G+9LrzYE/RJhgg==",
+			"version": "4.23.0",
+			"resolved": "https://registry.npmjs.org/@rushstack/ts-command-line/-/ts-command-line-4.23.0.tgz",
+			"integrity": "sha512-jYREBtsxduPV6ptNq8jOKp9+yx0ld1Tb/Tkdnlj8gTjazl1sF3DwX2VbluyYrNd0meWIL0bNeer7WDf5tKFjaQ==",
 			"dev": true,
 			"dependencies": {
 				"@rushstack/terminal": "0.14.2",
@@ -1341,9 +1339,9 @@
 			"license": "MIT"
 		},
 		"node_modules/@types/node": {
-			"version": "22.7.5",
-			"resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.5.tgz",
-			"integrity": "sha512-jML7s2NAzMWc//QSJ1a3prpk78cOPchGvXJsC3C6R6PSMoooztvRVQEz89gmBTBY1SPMaqo5teB4uNHPdetShQ==",
+			"version": "22.7.9",
+			"resolved": "https://registry.npmjs.org/@types/node/-/node-22.7.9.tgz",
+			"integrity": "sha512-jrTfRC7FM6nChvU7X2KqcrgquofrWLFDeYC1hKfwNWomVvrn7JIksqf344WN2X/y8xrgqBd2dJATZV4GbatBfg==",
 			"dev": true,
 			"dependencies": {
 				"undici-types": "~6.19.2"
@@ -1364,16 +1362,16 @@
 			"license": "MIT"
 		},
 		"node_modules/@typescript-eslint/eslint-plugin": {
-			"version": "8.8.1",
-			"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.8.1.tgz",
-			"integrity": "sha512-xfvdgA8AP/vxHgtgU310+WBnLB4uJQ9XdyP17RebG26rLtDrQJV3ZYrcopX91GrHmMoH8bdSwMRh2a//TiJ1jQ==",
+			"version": "8.11.0",
+			"resolved": "https://registry.npmjs.org/@typescript-eslint/eslint-plugin/-/eslint-plugin-8.11.0.tgz",
+			"integrity": "sha512-KhGn2LjW1PJT2A/GfDpiyOfS4a8xHQv2myUagTM5+zsormOmBlYsnQ6pobJ8XxJmh6hnHwa2Mbe3fPrDJoDhbA==",
 			"dev": true,
 			"dependencies": {
 				"@eslint-community/regexpp": "^4.10.0",
-				"@typescript-eslint/scope-manager": "8.8.1",
-				"@typescript-eslint/type-utils": "8.8.1",
-				"@typescript-eslint/utils": "8.8.1",
-				"@typescript-eslint/visitor-keys": "8.8.1",
+				"@typescript-eslint/scope-manager": "8.11.0",
+				"@typescript-eslint/type-utils": "8.11.0",
+				"@typescript-eslint/utils": "8.11.0",
+				"@typescript-eslint/visitor-keys": "8.11.0",
 				"graphemer": "^1.4.0",
 				"ignore": "^5.3.1",
 				"natural-compare": "^1.4.0",
@@ -1397,15 +1395,15 @@
 			}
 		},
 		"node_modules/@typescript-eslint/parser": {
-			"version": "8.8.1",
-			"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.8.1.tgz",
-			"integrity": "sha512-hQUVn2Lij2NAxVFEdvIGxT9gP1tq2yM83m+by3whWFsWC+1y8pxxxHUFE1UqDu2VsGi2i6RLcv4QvouM84U+ow==",
+			"version": "8.11.0",
+			"resolved": "https://registry.npmjs.org/@typescript-eslint/parser/-/parser-8.11.0.tgz",
+			"integrity": "sha512-lmt73NeHdy1Q/2ul295Qy3uninSqi6wQI18XwSpm8w0ZbQXUpjCAWP1Vlv/obudoBiIjJVjlztjQ+d/Md98Yxg==",
 			"dev": true,
 			"dependencies": {
-				"@typescript-eslint/scope-manager": "8.8.1",
-				"@typescript-eslint/types": "8.8.1",
-				"@typescript-eslint/typescript-estree": "8.8.1",
-				"@typescript-eslint/visitor-keys": "8.8.1",
+				"@typescript-eslint/scope-manager": "8.11.0",
+				"@typescript-eslint/types": "8.11.0",
+				"@typescript-eslint/typescript-estree": "8.11.0",
+				"@typescript-eslint/visitor-keys": "8.11.0",
 				"debug": "^4.3.4"
 			},
 			"engines": {
@@ -1425,13 +1423,13 @@
 			}
 		},
 		"node_modules/@typescript-eslint/scope-manager": {
-			"version": "8.8.1",
-			"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.8.1.tgz",
-			"integrity": "sha512-X4JdU+66Mazev/J0gfXlcC/dV6JI37h+93W9BRYXrSn0hrE64IoWgVkO9MSJgEzoWkxONgaQpICWg8vAN74wlA==",
+			"version": "8.11.0",
+			"resolved": "https://registry.npmjs.org/@typescript-eslint/scope-manager/-/scope-manager-8.11.0.tgz",
+			"integrity": "sha512-Uholz7tWhXmA4r6epo+vaeV7yjdKy5QFCERMjs1kMVsLRKIrSdM6o21W2He9ftp5PP6aWOVpD5zvrvuHZC0bMQ==",
 			"dev": true,
 			"dependencies": {
-				"@typescript-eslint/types": "8.8.1",
-				"@typescript-eslint/visitor-keys": "8.8.1"
+				"@typescript-eslint/types": "8.11.0",
+				"@typescript-eslint/visitor-keys": "8.11.0"
 			},
 			"engines": {
 				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1442,13 +1440,13 @@
 			}
 		},
 		"node_modules/@typescript-eslint/type-utils": {
-			"version": "8.8.1",
-			"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.8.1.tgz",
-			"integrity": "sha512-qSVnpcbLP8CALORf0za+vjLYj1Wp8HSoiI8zYU5tHxRVj30702Z1Yw4cLwfNKhTPWp5+P+k1pjmD5Zd1nhxiZA==",
+			"version": "8.11.0",
+			"resolved": "https://registry.npmjs.org/@typescript-eslint/type-utils/-/type-utils-8.11.0.tgz",
+			"integrity": "sha512-ItiMfJS6pQU0NIKAaybBKkuVzo6IdnAhPFZA/2Mba/uBjuPQPet/8+zh5GtLHwmuFRShZx+8lhIs7/QeDHflOg==",
 			"dev": true,
 			"dependencies": {
-				"@typescript-eslint/typescript-estree": "8.8.1",
-				"@typescript-eslint/utils": "8.8.1",
+				"@typescript-eslint/typescript-estree": "8.11.0",
+				"@typescript-eslint/utils": "8.11.0",
 				"debug": "^4.3.4",
 				"ts-api-utils": "^1.3.0"
 			},
@@ -1466,9 +1464,9 @@
 			}
 		},
 		"node_modules/@typescript-eslint/types": {
-			"version": "8.8.1",
-			"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.8.1.tgz",
-			"integrity": "sha512-WCcTP4SDXzMd23N27u66zTKMuEevH4uzU8C9jf0RO4E04yVHgQgW+r+TeVTNnO1KIfrL8ebgVVYYMMO3+jC55Q==",
+			"version": "8.11.0",
+			"resolved": "https://registry.npmjs.org/@typescript-eslint/types/-/types-8.11.0.tgz",
+			"integrity": "sha512-tn6sNMHf6EBAYMvmPUaKaVeYvhUsrE6x+bXQTxjQRp360h1giATU0WvgeEys1spbvb5R+VpNOZ+XJmjD8wOUHw==",
 			"dev": true,
 			"engines": {
 				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1479,13 +1477,13 @@
 			}
 		},
 		"node_modules/@typescript-eslint/typescript-estree": {
-			"version": "8.8.1",
-			"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.8.1.tgz",
-			"integrity": "sha512-A5d1R9p+X+1js4JogdNilDuuq+EHZdsH9MjTVxXOdVFfTJXunKJR/v+fNNyO4TnoOn5HqobzfRlc70NC6HTcdg==",
+			"version": "8.11.0",
+			"resolved": "https://registry.npmjs.org/@typescript-eslint/typescript-estree/-/typescript-estree-8.11.0.tgz",
+			"integrity": "sha512-yHC3s1z1RCHoCz5t06gf7jH24rr3vns08XXhfEqzYpd6Hll3z/3g23JRi0jM8A47UFKNc3u/y5KIMx8Ynbjohg==",
 			"dev": true,
 			"dependencies": {
-				"@typescript-eslint/types": "8.8.1",
-				"@typescript-eslint/visitor-keys": "8.8.1",
+				"@typescript-eslint/types": "8.11.0",
+				"@typescript-eslint/visitor-keys": "8.11.0",
 				"debug": "^4.3.4",
 				"fast-glob": "^3.3.2",
 				"is-glob": "^4.0.3",
@@ -1531,15 +1529,15 @@
 			}
 		},
 		"node_modules/@typescript-eslint/utils": {
-			"version": "8.8.1",
-			"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.8.1.tgz",
-			"integrity": "sha512-/QkNJDbV0bdL7H7d0/y0qBbV2HTtf0TIyjSDTvvmQEzeVx8jEImEbLuOA4EsvE8gIgqMitns0ifb5uQhMj8d9w==",
+			"version": "8.11.0",
+			"resolved": "https://registry.npmjs.org/@typescript-eslint/utils/-/utils-8.11.0.tgz",
+			"integrity": "sha512-CYiX6WZcbXNJV7UNB4PLDIBtSdRmRI/nb0FMyqHPTQD1rMjA0foPLaPUV39C/MxkTd/QKSeX+Gb34PPsDVC35g==",
 			"dev": true,
 			"dependencies": {
 				"@eslint-community/eslint-utils": "^4.4.0",
-				"@typescript-eslint/scope-manager": "8.8.1",
-				"@typescript-eslint/types": "8.8.1",
-				"@typescript-eslint/typescript-estree": "8.8.1"
+				"@typescript-eslint/scope-manager": "8.11.0",
+				"@typescript-eslint/types": "8.11.0",
+				"@typescript-eslint/typescript-estree": "8.11.0"
 			},
 			"engines": {
 				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -1553,12 +1551,12 @@
 			}
 		},
 		"node_modules/@typescript-eslint/visitor-keys": {
-			"version": "8.8.1",
-			"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.8.1.tgz",
-			"integrity": "sha512-0/TdC3aeRAsW7MDvYRwEc1Uwm0TIBfzjPFgg60UU2Haj5qsCs9cc3zNgY71edqE3LbWfF/WoZQd3lJoDXFQpag==",
+			"version": "8.11.0",
+			"resolved": "https://registry.npmjs.org/@typescript-eslint/visitor-keys/-/visitor-keys-8.11.0.tgz",
+			"integrity": "sha512-EaewX6lxSjRJnc+99+dqzTeoDZUfyrA52d2/HRrkI830kgovWsmIiTfmr0NZorzqic7ga+1bS60lRBUgR3n/Bw==",
 			"dev": true,
 			"dependencies": {
-				"@typescript-eslint/types": "8.8.1",
+				"@typescript-eslint/types": "8.11.0",
 				"eslint-visitor-keys": "^3.4.3"
 			},
 			"engines": {
@@ -1582,9 +1580,9 @@
 			}
 		},
 		"node_modules/@vitest/coverage-v8": {
-			"version": "2.1.2",
-			"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.2.tgz",
-			"integrity": "sha512-b7kHrFrs2urS0cOk5N10lttI8UdJ/yP3nB4JYTREvR5o18cR99yPpK4gK8oQgI42BVv0ILWYUSYB7AXkAUDc0g==",
+			"version": "2.1.3",
+			"resolved": "https://registry.npmjs.org/@vitest/coverage-v8/-/coverage-v8-2.1.3.tgz",
+			"integrity": "sha512-2OJ3c7UPoFSmBZwqD2VEkUw6A/tzPF0LmW0ZZhhB8PFxuc+9IBG/FaSM+RLEenc7ljzFvGN+G0nGQoZnh7sy2A==",
 			"dev": true,
 			"dependencies": {
 				"@ampproject/remapping": "^2.3.0",
@@ -1604,8 +1602,8 @@
 				"url": "https://opencollective.com/vitest"
 			},
 			"peerDependencies": {
-				"@vitest/browser": "2.1.2",
-				"vitest": "2.1.2"
+				"@vitest/browser": "2.1.3",
+				"vitest": "2.1.3"
 			},
 			"peerDependenciesMeta": {
 				"@vitest/browser": {
@@ -1614,13 +1612,13 @@
 			}
 		},
 		"node_modules/@vitest/expect": {
-			"version": "2.1.2",
-			"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.2.tgz",
-			"integrity": "sha512-FEgtlN8mIUSEAAnlvn7mP8vzaWhEaAEvhSXCqrsijM7K6QqjB11qoRZYEd4AKSCDz8p0/+yH5LzhZ47qt+EyPg==",
+			"version": "2.1.3",
+			"resolved": "https://registry.npmjs.org/@vitest/expect/-/expect-2.1.3.tgz",
+			"integrity": "sha512-SNBoPubeCJhZ48agjXruCI57DvxcsivVDdWz+SSsmjTT4QN/DfHk3zB/xKsJqMs26bLZ/pNRLnCf0j679i0uWQ==",
 			"dev": true,
 			"dependencies": {
-				"@vitest/spy": "2.1.2",
-				"@vitest/utils": "2.1.2",
+				"@vitest/spy": "2.1.3",
+				"@vitest/utils": "2.1.3",
 				"chai": "^5.1.1",
 				"tinyrainbow": "^1.2.0"
 			},
@@ -1629,12 +1627,12 @@
 			}
 		},
 		"node_modules/@vitest/mocker": {
-			"version": "2.1.2",
-			"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.2.tgz",
-			"integrity": "sha512-ExElkCGMS13JAJy+812fw1aCv2QO/LBK6CyO4WOPAzLTmve50gydOlWhgdBJPx2ztbADUq3JVI0C5U+bShaeEA==",
+			"version": "2.1.3",
+			"resolved": "https://registry.npmjs.org/@vitest/mocker/-/mocker-2.1.3.tgz",
+			"integrity": "sha512-eSpdY/eJDuOvuTA3ASzCjdithHa+GIF1L4PqtEELl6Qa3XafdMLBpBlZCIUCX2J+Q6sNmjmxtosAG62fK4BlqQ==",
 			"dev": true,
 			"dependencies": {
-				"@vitest/spy": "^2.1.0-beta.1",
+				"@vitest/spy": "2.1.3",
 				"estree-walker": "^3.0.3",
 				"magic-string": "^0.30.11"
 			},
@@ -1642,7 +1640,7 @@
 				"url": "https://opencollective.com/vitest"
 			},
 			"peerDependencies": {
-				"@vitest/spy": "2.1.2",
+				"@vitest/spy": "2.1.3",
 				"msw": "^2.3.5",
 				"vite": "^5.0.0"
 			},
@@ -1656,11 +1654,10 @@
 			}
 		},
 		"node_modules/@vitest/pretty-format": {
-			"version": "2.1.2",
-			"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.2.tgz",
-			"integrity": "sha512-FIoglbHrSUlOJPDGIrh2bjX1sNars5HbxlcsFKCtKzu4+5lpsRhOCVcuzp0fEhAGHkPZRIXVNzPcpSlkoZ3LuA==",
+			"version": "2.1.3",
+			"resolved": "https://registry.npmjs.org/@vitest/pretty-format/-/pretty-format-2.1.3.tgz",
+			"integrity": "sha512-XH1XdtoLZCpqV59KRbPrIhFCOO0hErxrQCMcvnQete3Vibb9UeIOX02uFPfVn3Z9ZXsq78etlfyhnkmIZSzIwQ==",
 			"dev": true,
-			"license": "MIT",
 			"dependencies": {
 				"tinyrainbow": "^1.2.0"
 			},
@@ -1669,12 +1666,12 @@
 			}
 		},
 		"node_modules/@vitest/runner": {
-			"version": "2.1.2",
-			"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.2.tgz",
-			"integrity": "sha512-UCsPtvluHO3u7jdoONGjOSil+uON5SSvU9buQh3lP7GgUXHp78guN1wRmZDX4wGK6J10f9NUtP6pO+SFquoMlw==",
+			"version": "2.1.3",
+			"resolved": "https://registry.npmjs.org/@vitest/runner/-/runner-2.1.3.tgz",
+			"integrity": "sha512-JGzpWqmFJ4fq5ZKHtVO3Xuy1iF2rHGV4d/pdzgkYHm1+gOzNZtqjvyiaDGJytRyMU54qkxpNzCx+PErzJ1/JqQ==",
 			"dev": true,
 			"dependencies": {
-				"@vitest/utils": "2.1.2",
+				"@vitest/utils": "2.1.3",
 				"pathe": "^1.1.2"
 			},
 			"funding": {
@@ -1682,12 +1679,12 @@
 			}
 		},
 		"node_modules/@vitest/snapshot": {
-			"version": "2.1.2",
-			"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.2.tgz",
-			"integrity": "sha512-xtAeNsZ++aRIYIUsek7VHzry/9AcxeULlegBvsdLncLmNCR6tR8SRjn8BbDP4naxtccvzTqZ+L1ltZlRCfBZFA==",
+			"version": "2.1.3",
+			"resolved": "https://registry.npmjs.org/@vitest/snapshot/-/snapshot-2.1.3.tgz",
+			"integrity": "sha512-qWC2mWc7VAXmjAkEKxrScWHWFyCQx/cmiZtuGqMi+WwqQJ2iURsVY4ZfAK6dVo6K2smKRU6l3BPwqEBvhnpQGg==",
 			"dev": true,
 			"dependencies": {
-				"@vitest/pretty-format": "2.1.2",
+				"@vitest/pretty-format": "2.1.3",
 				"magic-string": "^0.30.11",
 				"pathe": "^1.1.2"
 			},
@@ -1696,9 +1693,9 @@
 			}
 		},
 		"node_modules/@vitest/spy": {
-			"version": "2.1.2",
-			"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.2.tgz",
-			"integrity": "sha512-GSUi5zoy+abNRJwmFhBDC0yRuVUn8WMlQscvnbbXdKLXX9dE59YbfwXxuJ/mth6eeqIzofU8BB5XDo/Ns/qK2A==",
+			"version": "2.1.3",
+			"resolved": "https://registry.npmjs.org/@vitest/spy/-/spy-2.1.3.tgz",
+			"integrity": "sha512-Nb2UzbcUswzeSP7JksMDaqsI43Sj5+Kry6ry6jQJT4b5gAK+NS9NED6mDb8FlMRCX8m5guaHCDZmqYMMWRy5nQ==",
 			"dev": true,
 			"dependencies": {
 				"tinyspy": "^3.0.0"
@@ -1708,12 +1705,12 @@
 			}
 		},
 		"node_modules/@vitest/utils": {
-			"version": "2.1.2",
-			"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.2.tgz",
-			"integrity": "sha512-zMO2KdYy6mx56btx9JvAqAZ6EyS3g49krMPPrgOp1yxGZiA93HumGk+bZ5jIZtOg5/VBYl5eBmGRQHqq4FG6uQ==",
+			"version": "2.1.3",
+			"resolved": "https://registry.npmjs.org/@vitest/utils/-/utils-2.1.3.tgz",
+			"integrity": "sha512-xpiVfDSg1RrYT0tX6czgerkpcKFmFOF/gCr30+Mve5V2kewCy4Prn1/NDMSRwaSmT7PRaOF83wu+bEtsY1wrvA==",
 			"dev": true,
 			"dependencies": {
-				"@vitest/pretty-format": "2.1.2",
+				"@vitest/pretty-format": "2.1.3",
 				"loupe": "^3.1.1",
 				"tinyrainbow": "^1.2.0"
 			},
@@ -2061,9 +2058,9 @@
 			}
 		},
 		"node_modules/chai": {
-			"version": "5.1.1",
-			"resolved": "https://registry.npmjs.org/chai/-/chai-5.1.1.tgz",
-			"integrity": "sha512-pT1ZgP8rPNqUgieVaEY+ryQr6Q4HXNg8Ei9UnLUrjN4IA7dvQC5JB+/kxVcPNDHyBcc/26CXPkbNzq3qwrOEKA==",
+			"version": "5.1.2",
+			"resolved": "https://registry.npmjs.org/chai/-/chai-5.1.2.tgz",
+			"integrity": "sha512-aGtmf24DW6MLHHG5gCx4zaI3uBq3KRtxeVs0DjFH6Z0rDNbsvTxFASFvdj79pxjxZ8/5u3PIiN3IwEIQkiiuPw==",
 			"dev": true,
 			"dependencies": {
 				"assertion-error": "^2.0.1",
@@ -2509,18 +2506,17 @@
 			}
 		},
 		"node_modules/eslint": {
-			"version": "9.12.0",
-			"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.12.0.tgz",
-			"integrity": "sha512-UVIOlTEWxwIopRL1wgSQYdnVDcEvs2wyaO6DGo5mXqe3r16IoCNWkR29iHhyaP4cICWjbgbmFUGAhh0GJRuGZw==",
+			"version": "9.13.0",
+			"resolved": "https://registry.npmjs.org/eslint/-/eslint-9.13.0.tgz",
+			"integrity": "sha512-EYZK6SX6zjFHST/HRytOdA/zE72Cq/bfw45LSyuwrdvcclb/gqV8RRQxywOBEWO2+WDpva6UZa4CcDeJKzUCFA==",
 			"dev": true,
-			"license": "MIT",
 			"dependencies": {
 				"@eslint-community/eslint-utils": "^4.2.0",
 				"@eslint-community/regexpp": "^4.11.0",
 				"@eslint/config-array": "^0.18.0",
-				"@eslint/core": "^0.6.0",
+				"@eslint/core": "^0.7.0",
 				"@eslint/eslintrc": "^3.1.0",
-				"@eslint/js": "9.12.0",
+				"@eslint/js": "9.13.0",
 				"@eslint/plugin-kit": "^0.2.0",
 				"@humanfs/node": "^0.16.5",
 				"@humanwhocodes/module-importer": "^1.0.1",
@@ -4257,9 +4253,9 @@
 			}
 		},
 		"node_modules/picocolors": {
-			"version": "1.1.0",
-			"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.0.tgz",
-			"integrity": "sha512-TQ92mBOW0l3LeMeyLV6mzy/kWr8lkd/hp3mTg7wYK7zJhuBStmGMBG0BdeDZS/dZx1IukaX6Bk11zcln25o1Aw==",
+			"version": "1.1.1",
+			"resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz",
+			"integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==",
 			"dev": true
 		},
 		"node_modules/picomatch": {
@@ -5165,9 +5161,9 @@
 			}
 		},
 		"node_modules/typescript": {
-			"version": "5.6.2",
-			"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.2.tgz",
-			"integrity": "sha512-NW8ByodCSNCwZeghjN3o+JX5OFH0Ojg6sadjEKY4huZ52TqbJTJnDo5+Tw98lSy63NZvi4n+ez5m2u5d4PkZyw==",
+			"version": "5.6.3",
+			"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.6.3.tgz",
+			"integrity": "sha512-hjcS1mhfuyi4WW8IWtjP7brDrG2cuDZukyrYrSauoXGNgx0S7zceP07adYkJycEr56BOUTNPzbInooiN3fn1qw==",
 			"dev": true,
 			"bin": {
 				"tsc": "bin/tsc",
@@ -5178,14 +5174,14 @@
 			}
 		},
 		"node_modules/typescript-eslint": {
-			"version": "8.8.1",
-			"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.8.1.tgz",
-			"integrity": "sha512-R0dsXFt6t4SAFjUSKFjMh4pXDtq04SsFKCVGDP3ZOzNP7itF0jBcZYU4fMsZr4y7O7V7Nc751dDeESbe4PbQMQ==",
+			"version": "8.11.0",
+			"resolved": "https://registry.npmjs.org/typescript-eslint/-/typescript-eslint-8.11.0.tgz",
+			"integrity": "sha512-cBRGnW3FSlxaYwU8KfAewxFK5uzeOAp0l2KebIlPDOT5olVi65KDG/yjBooPBG0kGW/HLkoz1c/iuBFehcS3IA==",
 			"dev": true,
 			"dependencies": {
-				"@typescript-eslint/eslint-plugin": "8.8.1",
-				"@typescript-eslint/parser": "8.8.1",
-				"@typescript-eslint/utils": "8.8.1"
+				"@typescript-eslint/eslint-plugin": "8.11.0",
+				"@typescript-eslint/parser": "8.11.0",
+				"@typescript-eslint/utils": "8.11.0"
 			},
 			"engines": {
 				"node": "^18.18.0 || ^20.9.0 || >=21.1.0"
@@ -5262,9 +5258,9 @@
 			"license": "MIT"
 		},
 		"node_modules/vite": {
-			"version": "5.4.8",
-			"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.8.tgz",
-			"integrity": "sha512-FqrItQ4DT1NC4zCUqMB4c4AZORMKIa0m8/URVCZ77OZ/QSNeJ54bU1vrFADbDsuwfIPcgknRkmqakQcgnL4GiQ==",
+			"version": "5.4.10",
+			"resolved": "https://registry.npmjs.org/vite/-/vite-5.4.10.tgz",
+			"integrity": "sha512-1hvaPshuPUtxeQ0hsVH3Mud0ZanOLwVTneA1EgbAM5LhaZEqyPWGRQ7BtaMvUrTDeEaC8pxtj6a6jku3x4z6SQ==",
 			"dev": true,
 			"dependencies": {
 				"esbuild": "^0.21.3",
@@ -5321,9 +5317,9 @@
 			}
 		},
 		"node_modules/vite-node": {
-			"version": "2.1.2",
-			"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.2.tgz",
-			"integrity": "sha512-HPcGNN5g/7I2OtPjLqgOtCRu/qhVvBxTUD3qzitmL0SrG1cWFzxzhMDWussxSbrRYWqnKf8P2jiNhPMSN+ymsQ==",
+			"version": "2.1.3",
+			"resolved": "https://registry.npmjs.org/vite-node/-/vite-node-2.1.3.tgz",
+			"integrity": "sha512-I1JadzO+xYX887S39Do+paRePCKoiDrWRRjp9kkG5he0t7RXNvPAJPCQSJqbGN4uCrFFeS3Kj3sLqY8NMYBEdA==",
 			"dev": true,
 			"dependencies": {
 				"cac": "^6.7.14",
@@ -5342,18 +5338,18 @@
 			}
 		},
 		"node_modules/vitest": {
-			"version": "2.1.2",
-			"resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.2.tgz",
-			"integrity": "sha512-veNjLizOMkRrJ6xxb+pvxN6/QAWg95mzcRjtmkepXdN87FNfxAss9RKe2far/G9cQpipfgP2taqg0KiWsquj8A==",
-			"dev": true,
-			"dependencies": {
-				"@vitest/expect": "2.1.2",
-				"@vitest/mocker": "2.1.2",
-				"@vitest/pretty-format": "^2.1.2",
-				"@vitest/runner": "2.1.2",
-				"@vitest/snapshot": "2.1.2",
-				"@vitest/spy": "2.1.2",
-				"@vitest/utils": "2.1.2",
+			"version": "2.1.3",
+			"resolved": "https://registry.npmjs.org/vitest/-/vitest-2.1.3.tgz",
+			"integrity": "sha512-Zrxbg/WiIvUP2uEzelDNTXmEMJXuzJ1kCpbDvaKByFA9MNeO95V+7r/3ti0qzJzrxdyuUw5VduN7k+D3VmVOSA==",
+			"dev": true,
+			"dependencies": {
+				"@vitest/expect": "2.1.3",
+				"@vitest/mocker": "2.1.3",
+				"@vitest/pretty-format": "^2.1.3",
+				"@vitest/runner": "2.1.3",
+				"@vitest/snapshot": "2.1.3",
+				"@vitest/spy": "2.1.3",
+				"@vitest/utils": "2.1.3",
 				"chai": "^5.1.1",
 				"debug": "^4.3.6",
 				"magic-string": "^0.30.11",
@@ -5364,7 +5360,7 @@
 				"tinypool": "^1.0.0",
 				"tinyrainbow": "^1.2.0",
 				"vite": "^5.0.0",
-				"vite-node": "2.1.2",
+				"vite-node": "2.1.3",
 				"why-is-node-running": "^2.3.0"
 			},
 			"bin": {
@@ -5379,8 +5375,8 @@
 			"peerDependencies": {
 				"@edge-runtime/vm": "*",
 				"@types/node": "^18.0.0 || >=20.0.0",
-				"@vitest/browser": "2.1.2",
-				"@vitest/ui": "2.1.2",
+				"@vitest/browser": "2.1.3",
+				"@vitest/ui": "2.1.3",
 				"happy-dom": "*",
 				"jsdom": "*"
 			},
diff --git a/package.json b/package.json
index b964331..f67ed1e 100644
--- a/package.json
+++ b/package.json
@@ -32,21 +32,21 @@
 		"prepublishOnly": "node scripts/check-release.mjs"
 	},
 	"devDependencies": {
-		"@eslint/js": "9.12.0",
-		"@microsoft/api-extractor": "7.47.9",
+		"@eslint/js": "9.13.0",
+		"@microsoft/api-extractor": "7.47.11",
 		"@types/eslint__js": "8.42.3",
-		"@types/node": "22.7.5",
+		"@types/node": "22.7.9",
 		"@types/seedrandom": "3.0.8",
 		"@types/uuid": "10.0.0",
-		"@vitest/coverage-v8": "2.1.2",
+		"@vitest/coverage-v8": "2.1.3",
 		"chalk": "5.3.0",
-		"eslint": "9.12.0",
+		"eslint": "9.13.0",
 		"eslint-plugin-import": "2.31.0",
 		"semver": "7.6.3",
 		"ts-node": "10.9.2",
-		"typescript": "5.6.2",
-		"typescript-eslint": "8.8.1",
-		"vitest": "2.1.2"
+		"typescript": "5.6.3",
+		"typescript-eslint": "8.11.0",
+		"vitest": "2.1.3"
 	},
 	"dependencies": {
 		"seedrandom": "3.0.5",
diff --git a/playground/src/App.vue b/playground/src/App.vue
index 8a870b1..9fb1351 100644
--- a/playground/src/App.vue
+++ b/playground/src/App.vue
@@ -1,6 +1,7 @@
 <template>
 <div id="root">
-	<h1>AiScript (v{{ AISCRIPT_VERSION }}) Playground</h1>
+	<Settings v-if="showSettings" @exit="showSettings = false" />
+	<h1>AiScript (v{{ AISCRIPT_VERSION }}) Playground<button id="show-settings-button" @click="showSettings = true">Settings</button></h1>
 	<div id="grid1">
 		<div id="editor" class="container">
 			<header>Input<div class="actions"><button @click="setCode">FizzBuzz</button></div></header>
@@ -57,12 +58,14 @@ import { highlight, languages } from 'prismjs/components/prism-core';
 import 'prismjs/components/prism-clike';
 import 'prismjs/components/prism-javascript';
 import 'prismjs/themes/prism-okaidia.css';
+import Settings, { settings } from './Settings.vue';
 
 const script = ref(window.localStorage.getItem('script') || '<: "Hello, AiScript!"');
 
 const ast = ref(null);
 const logs = ref([]);
 const syntaxErrorMessage = ref(null);
+const showSettings = ref(false);
 
 watch(script, () => {
 	window.localStorage.setItem('script', script.value);
@@ -119,7 +122,9 @@ const run = async () => {
 				}); break;
 				default: break;
 			}
-		}
+		},
+		irqRate: settings.value.irqRate,
+		irqSleep: settings.value.irqSleep,
 	});
 
 	try {
@@ -167,6 +172,11 @@ pre {
 #root > h1 {
 	font-size: 1.5em;
 	margin: 16px 16px 0 16px;
+	display: flex;
+}
+
+#show-settings-button {
+	margin-left: auto;
 }
 
 #grid1 {
diff --git a/playground/src/Settings.vue b/playground/src/Settings.vue
new file mode 100644
index 0000000..28f7f60
--- /dev/null
+++ b/playground/src/Settings.vue
@@ -0,0 +1,59 @@
+<template>
+<div ref="bg" id="settings-bg" @click="(e) => e.target === bg && $emit('exit')">
+	<div id="settings-body">
+		<div class="settings-item">
+			<header>IRQ Rate</header>
+			<input type="number" v-model="settings.irqRate" />
+		</div>
+		<div class="settings-item">
+			<header>IRQ Sleep Time</header>
+			<div>
+				<input type="radio" value="time" v-model="irqSleepMethod" />
+				time(milliseconds)
+				<input type="number" v-model="irqSleepTime" :disabled="irqSleepMethod !== 'time'" />
+			</div>
+			<div>
+				<input type="radio" value="requestIdleCallback" v-model="irqSleepMethod" />
+				requestIdleCallback
+			</div>
+		</div>
+	</div>
+</div>
+</template>
+
+<script>
+import { ref, computed } from 'vue';
+const irqSleepTime = ref(5);
+const irqSleepMethod = ref('time');
+export const settings = ref({
+	irqRate: 300,
+	irqSleep: computed(() => ({
+		time: irqSleepTime.value,
+		requestIdleCallback: () => new Promise(cb => requestIdleCallback(cb)),
+	})[irqSleepMethod.value]),
+});
+</script>
+
+<script setup>
+const emits = defineEmits(['exit']);
+const bg = ref(null);
+</script>
+
+<style>
+#settings-bg {
+	position: fixed;
+	top: 0;
+	left: 0;
+	z-index: 10;
+	width: 100vw;
+	height: 100vh;
+	background: #0008;
+	display: flex;
+	justify-content: center;
+	align-items: center;
+}
+#settings-body {
+	display: grid;
+	gap: 1em;
+}
+</style>
diff --git a/src/error.ts b/src/error.ts
index ba01d0b..e62c9b6 100644
--- a/src/error.ts
+++ b/src/error.ts
@@ -86,3 +86,12 @@ export class AiScriptUserError extends AiScriptRuntimeError {
 		super(message, info);
 	}
 }
+/**
+ * Host side configuration errors.
+ */
+export class AiScriptHostsideError extends AiScriptError {
+	public name = 'Host';
+	constructor(message: string, info?: unknown) {
+		super(message, info);
+	}
+}
diff --git a/src/interpreter/index.ts b/src/interpreter/index.ts
index 9f8c772..490b8aa 100644
--- a/src/interpreter/index.ts
+++ b/src/interpreter/index.ts
@@ -3,19 +3,16 @@
  */
 
 import { autobind } from '../utils/mini-autobind.js';
-import { AiScriptError, NonAiScriptError, AiScriptNamespaceError, AiScriptIndexOutOfRangeError, AiScriptRuntimeError } from '../error.js';
+import { AiScriptError, NonAiScriptError, AiScriptNamespaceError, AiScriptIndexOutOfRangeError, AiScriptRuntimeError, AiScriptHostsideError } from '../error.js';
+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';
 import type { JsValue } from './util.js';
 import type { Value, VFn } from './value.js';
-import type * as Ast from '../node.js';
-
-const IRQ_RATE = 300;
-const IRQ_AT = IRQ_RATE - 1;
 
 export type LogObject = {
 	scope?: string;
@@ -23,12 +20,19 @@ export type LogObject = {
 	val?: Value | Variable;
 };
 
+type CallInfo = {
+	name: string;
+	pos: Ast.Pos | undefined;
+};
+
 export class Interpreter {
 	public stepCount = 0;
 	private stop = false;
 	public scope: Scope;
 	private abortHandlers: (() => void)[] = [];
 	private vars: Record<string, Variable> = {};
+	private irqRate: number;
+	private irqSleep: () => Promise<void>;
 
 	constructor(
 		consts: Record<string, Value>,
@@ -39,6 +43,8 @@ export class Interpreter {
 			log?(type: string, params: LogObject): void;
 			maxStep?: number;
 			abortOnError?: boolean;
+			irqRate?: number;
+			irqSleep?: number | (() => Promise<void>);
 		} = {},
 	) {
 		const io = {
@@ -70,6 +76,25 @@ export class Interpreter {
 				default: break;
 			}
 		};
+
+		if (!((this.opts.irqRate ?? 300) >= 0)) {
+			throw new AiScriptHostsideError(`Invalid IRQ rate (${this.opts.irqRate}): must be non-negative number`);
+		}
+		this.irqRate = this.opts.irqRate ?? 300;
+
+		const sleep = (time: number) => (
+			(): Promise<void> => new Promise(resolve => setTimeout(resolve, time))
+		);
+
+		if (typeof this.opts.irqSleep === 'function') {
+			this.irqSleep = this.opts.irqSleep;
+		} else if (this.opts.irqSleep === undefined) {
+			this.irqSleep = sleep(5);
+		} else if (this.opts.irqSleep >= 0) {
+			this.irqSleep = sleep(this.opts.irqSleep);
+		} else {
+			throw new AiScriptHostsideError('irqSleep must be a function or a positive number.');
+		}
 	}
 
 	@autobind
@@ -77,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);
@@ -94,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');
@@ -108,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
@@ -204,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;
@@ -225,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,
@@ -237,32 +270,50 @@ 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 _eval(node: Ast.Node, scope: Scope): Promise<Value> {
-		return this.__eval(node, scope).catch(e => {
+	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, 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;
-		if (this.stepCount % IRQ_RATE === IRQ_AT) await new Promise(resolve => setTimeout(resolve, 5));
+		// irqRateが小数の場合は不等間隔になる
+		if (this.irqRate !== 0 && this.stepCount % this.irqRate >= this.irqRate - 1) {
+			await this.irqSleep();
+		}
 		this.stepCount++;
 		if (this.opts.maxStep && this.stepCount > this.opts.maxStep) {
 			throw new AiScriptRuntimeError('max step exceeded');
@@ -270,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._eval(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._eval(elseif.then, scope);
-							}
-						}
-						if (node.else) {
-							return this._eval(node.else, scope);
-						}
-					} else if (node.else) {
-						return this._eval(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._eval(qa.a, scope);
+						return await this._evalClause(qa.a, scope, callStack);
 					}
 				}
 				if (node.default) {
-					return await this._eval(node.default, scope);
+					return await this._evalClause(node.default, scope, callStack);
 				}
 				return NULL;
 			}
@@ -317,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') {
@@ -329,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._eval(node.for, scope);
+						const v = await this._evalClause(node.for, scope, callStack);
 						if (v.type === 'break') {
 							break;
 						} else if (v.type === 'return') {
@@ -340,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++) {
@@ -350,7 +396,7 @@ export class Interpreter {
 								isMutable: false,
 								value: NUM(i),
 							}],
-						])));
+						])), callStack);
 						if (v.type === 'break') {
 							break;
 						} else if (v.type === 'return') {
@@ -362,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') {
@@ -378,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;
 			}
@@ -398,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;
 			}
 
@@ -433,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)!;
@@ -457,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];
@@ -479,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)
 						};
 					})),
@@ -499,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': {
@@ -512,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);
 					}
 				}
@@ -520,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);
 			}
@@ -546,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;
 				}
@@ -672,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;
@@ -680,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;
@@ -744,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) {
@@ -768,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);
@@ -777,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/primitive-props.ts b/src/interpreter/primitive-props.ts
index bc3541b..df3c18c 100644
--- a/src/interpreter/primitive-props.ts
+++ b/src/interpreter/primitive-props.ts
@@ -334,12 +334,12 @@ const PRIMITIVE_PROPS: {
 		splice: (target: VArr): VFn => FN_NATIVE(async ([idx, rc, vs], opts) => {
 			assertNumber(idx);
 			const index = (idx.value < -target.value.length) ? 0
-						: (idx.value < 0) ? target.value.length + idx.value
-						: (idx.value >= target.value.length) ? target.value.length
-						: idx.value;
+				: (idx.value < 0) ? target.value.length + idx.value
+				: (idx.value >= target.value.length) ? target.value.length
+				: idx.value;
 
 			const remove_count = (rc != null) ? (assertNumber(rc), rc.value)
-								: target.value.length - index;
+				: target.value.length - index;
 
 			const items = (vs != null) ? (assertArray(vs), vs.value) : [];
 
@@ -378,7 +378,7 @@ const PRIMITIVE_PROPS: {
 			});
 			const mapped_vals = await Promise.all(vals);
 			return ARR(mapped_vals.flat());
-    }),
+		}),
 
 		every: (target: VArr): VFn => FN_NATIVE(async ([fn], opts) => {
 			assertFunction(fn);
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 9b1bc54..ce4d8ca 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();
@@ -138,6 +138,30 @@ export function parseType(s: ITokenStream): Ast.TypeSource {
 	}
 }
 
+/**
+ * ```abnf
+ * OptionalSeparator = [SEP]
+ * ```
+*/
+export function parseOptionalSeparator(s: ITokenStream): boolean {
+	switch (s.getTokenKind()) {
+		case TokenKind.NewLine: {
+			s.next();
+			return true;
+		}
+		case TokenKind.Comma: {
+			s.next();
+			if (s.is(TokenKind.NewLine)) {
+				s.next();
+			}
+			return true;
+		}
+		default: {
+			return false;
+		}
+	}
+}
+
 /**
  * ```abnf
  * FnType = "@" "(" ParamTypes ")" "=>" Type
@@ -176,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());
 }
 
 /**
diff --git a/src/parser/syntaxes/expressions.ts b/src/parser/syntaxes/expressions.ts
index 342644a..8088768 100644
--- a/src/parser/syntaxes/expressions.ts
+++ b/src/parser/syntaxes/expressions.ts
@@ -2,7 +2,7 @@ import { AiScriptSyntaxError } from '../../error.js';
 import { NODE } from '../utils.js';
 import { TokenStream } from '../streams/token-stream.js';
 import { TokenKind } from '../token.js';
-import { parseBlock, parseParams, parseType } from './common.js';
+import { parseBlock, parseOptionalSeparator, parseParams, parseType } from './common.js';
 import { parseBlockOrStatement } from './statements.js';
 
 import type * as Ast from '../../node.js';
@@ -399,14 +399,12 @@ 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());
 }
 
 /**
  * ```abnf
- * Match = "match" Expr "{" [MatchCases] [defaultCase] "}"
- * MatchCases = "case" Expr "=>" BlockOrStatement *(SEP "case" Expr "=>" BlockOrStatement) [SEP]
- * DefaultCase = "default" "=>" BlockOrStatement [SEP]
+ * Match = "match" Expr "{" [(MatchCase *(SEP MatchCase) [SEP DefaultCase] [SEP]) / DefaultCase [SEP]] "}"
  * ```
 */
 function parseMatch(s: ITokenStream): Ast.Match {
@@ -424,65 +422,27 @@ function parseMatch(s: ITokenStream): Ast.Match {
 	}
 
 	const qs: Ast.Match['qs'] = [];
-	while (!s.is(TokenKind.DefaultKeyword) && !s.is(TokenKind.CloseBrace)) {
-		s.expect(TokenKind.CaseKeyword);
-		s.next();
-		const q = parseExpr(s, false);
-		s.expect(TokenKind.Arrow);
-		s.next();
-		const a = parseBlockOrStatement(s);
-		qs.push({ q, a });
-
-		// separator
-		switch (s.getTokenKind()) {
-			case TokenKind.NewLine: {
-				s.next();
-				break;
-			}
-			case TokenKind.Comma: {
-				s.next();
-				if (s.is(TokenKind.NewLine)) {
-					s.next();
-				}
-				break;
-			}
-			case TokenKind.DefaultKeyword:
-			case TokenKind.CloseBrace: {
-				break;
-			}
-			default: {
+	let x: Ast.Match['default'];
+	if (s.is(TokenKind.CaseKeyword)) {
+		qs.push(parseMatchCase(s));
+		let sep = parseOptionalSeparator(s);
+		while (s.is(TokenKind.CaseKeyword)) {
+			if (!sep) {
 				throw new AiScriptSyntaxError('separator expected', s.getPos());
 			}
+			qs.push(parseMatchCase(s));
+			sep = parseOptionalSeparator(s);
 		}
-	}
-
-	let x: Ast.Match['default'];
-	if (s.is(TokenKind.DefaultKeyword)) {
-		s.next();
-		s.expect(TokenKind.Arrow);
-		s.next();
-		x = parseBlockOrStatement(s);
-
-		// separator
-		switch (s.getTokenKind()) {
-			case TokenKind.NewLine: {
-				s.next();
-				break;
-			}
-			case TokenKind.Comma: {
-				s.next();
-				if (s.is(TokenKind.NewLine)) {
-					s.next();
-				}
-				break;
-			}
-			case TokenKind.CloseBrace: {
-				break;
-			}
-			default: {
+		if (s.is(TokenKind.DefaultKeyword)) {
+			if (!sep) {
 				throw new AiScriptSyntaxError('separator expected', s.getPos());
 			}
+			x = parseDefaultCase(s);
+			parseOptionalSeparator(s);
 		}
+	} else if (s.is(TokenKind.DefaultKeyword)) {
+		x = parseDefaultCase(s);
+		parseOptionalSeparator(s);
 	}
 
 	s.expect(TokenKind.CloseBrace);
@@ -491,6 +451,34 @@ function parseMatch(s: ITokenStream): Ast.Match {
 	return NODE('match', { about, qs, default: x }, startPos, s.getPos());
 }
 
+/**
+ * ```abnf
+ * MatchCase = "case" Expr "=>" BlockOrStatement
+ * ```
+*/
+function parseMatchCase(s: ITokenStream): Ast.Match['qs'][number] {
+	s.expect(TokenKind.CaseKeyword);
+	s.next();
+	const q = parseExpr(s, false);
+	s.expect(TokenKind.Arrow);
+	s.next();
+	const a = parseBlockOrStatement(s);
+	return { q, a };
+}
+
+/**
+ * ```abnf
+ * DefaultCase = "default" "=>" BlockOrStatement
+ * ```
+*/
+function parseDefaultCase(s: ITokenStream): Ast.Match['default'] {
+	s.expect(TokenKind.DefaultKeyword);
+	s.next();
+	s.expect(TokenKind.Arrow);
+	s.next();
+	return parseBlockOrStatement(s);
+}
+
 /**
  * ```abnf
  * Eval = "eval" Block
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/src/utils/random/genrng.ts b/src/utils/random/genrng.ts
index f6a6a4a..799308f 100644
--- a/src/utils/random/genrng.ts
+++ b/src/utils/random/genrng.ts
@@ -1,9 +1,9 @@
 import seedrandom from 'seedrandom';
 import { FN_NATIVE, NULL, NUM } from '../../interpreter/value.js';
+import { textEncoder } from '../../const.js';
 import { SeedRandomWrapper } from './seedrandom.js';
 import { ChaCha20 } from './chacha20.js';
 import type { VNativeFn, VNull, Value } from '../../interpreter/value.js';
-import { textEncoder } from '../../const.js';
 
 export function GenerateLegacyRandom(seed: Value | undefined) : VNativeFn | VNull {
 	if (!seed || seed.type !== 'num' && seed.type !== 'str') return NULL;
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/irq-config.md b/unreleased/irq-config.md
new file mode 100644
index 0000000..5e3edf6
--- /dev/null
+++ b/unreleased/irq-config.md
@@ -0,0 +1,3 @@
+- For Hosts: Interpreterのオプションに`irqRate`と`irqSleep`を追加
+  - `irqRate`はInterpreterの定期休止が何ステップに一回起こるかを指定する数値
+  - `irqSleep`は休止時間をミリ秒で指定する数値、または休止ごとにawaitされるPromiseを返す関数
diff --git a/unreleased/match-sep-before-default.md b/unreleased/match-sep-before-default.md
new file mode 100644
index 0000000..3b87a32
--- /dev/null
+++ b/unreleased/match-sep-before-default.md
@@ -0,0 +1,1 @@
+- **Breaking Change** match式において、case節とdefault節の間に区切り文字が必須になりました。case節の後にdefault節を区切り文字なしで続けると文法エラーになります。
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 にリネームしました。
diff --git a/unreleased/single-statement-clause-scope.md b/unreleased/single-statement-clause-scope.md
new file mode 100644
index 0000000..a86206a
--- /dev/null
+++ b/unreleased/single-statement-clause-scope.md
@@ -0,0 +1,1 @@
+- **Breaking Change** if式やmatch式、for文の内容が1つの文である場合にもスコープが生成されるようになりました。これらの構文内で定義された変数は外部から参照できなくなります。


Test patch:
diff --git a/.github/workflows/test.yml b/.github/workflows/test.yml
index 9c55704..6918802 100644
--- a/.github/workflows/test.yml
+++ b/.github/workflows/test.yml
@@ -18,10 +18,10 @@ jobs:
 
     steps:
       - name: Checkout
-        uses: actions/checkout@v4.2.1
+        uses: actions/checkout@v4.2.2
 
       - name: Setup Node.js ${{ matrix.node-version }}
-        uses: actions/setup-node@v4.0.4
+        uses: actions/setup-node@v4.1.0
         with:
           node-version: ${{ matrix.node-version }}
 
diff --git a/test/interpreter.ts b/test/interpreter.ts
index 70c4089..09cb545 100644
--- a/test/interpreter.ts
+++ b/test/interpreter.ts
@@ -1,9 +1,9 @@
 import * as assert from 'assert';
-import { describe, expect, test } from 'vitest';
+import { describe, expect, test, vi, beforeEach, afterEach } from 'vitest';
 import { Parser, Interpreter, values, errors, utils, Ast } from '../src';
 
 let { FN_NATIVE } = values;
-let { AiScriptRuntimeError, AiScriptIndexOutOfRangeError } = errors;
+let { AiScriptRuntimeError, AiScriptIndexOutOfRangeError, AiScriptHostsideError } = errors;
 
 describe('Scope', () => {
 	test.concurrent('getAll', async () => {
@@ -114,3 +114,143 @@ describe('error location', () => {
 		`)).resolves.toEqual({ line: 2, column: 6});
 	});
 });
+
+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> {
+			let count = 0;
+			const interpreter = new Interpreter({}, {
+				irqRate,
+				// It's safe only when no massive loop occurs
+				irqSleep: async () => count++,
+			});
+			await interpreter.exec(Parser.parse(`
+			'Ai-chan kawaii'
+			'Ai-chan kawaii'
+			'Ai-chan kawaii'
+			'Ai-chan kawaii'
+			'Ai-chan kawaii'
+			'Ai-chan kawaii'
+			'Ai-chan kawaii'
+			'Ai-chan kawaii'
+			'Ai-chan kawaii'
+			'Ai-chan kawaii'`));
+			return count;
+		}
+
+		test.concurrent.each([
+			[0, 0],
+			[1, 10],
+			[2, 5],
+			[10, 1],
+			[Infinity, 0],
+		])('rate = %d', async (rate, count) => {
+			return expect(countSleeps(rate)).resolves.toEqual(count);
+		});
+
+		test.concurrent.each(
+			[-1, NaN],
+		)('rate = %d', async (rate, count) => {
+			return expect(countSleeps(rate)).rejects.toThrow(AiScriptHostsideError);
+		});
+	});
+
+	describe('irqSleep is number', () => {
+		// This function does IRQ 10 times so takes 10 * irqSleep milliseconds in sum when executed.
+		async function countSleeps(irqSleep: number): Promise<void> {
+			const interpreter = new Interpreter({}, {
+				irqRate: 1,
+				irqSleep,
+			});
+			await interpreter.exec(Parser.parse(`
+			'Ai-chan kawaii'
+			'Ai-chan kawaii'
+			'Ai-chan kawaii'
+			'Ai-chan kawaii'
+			'Ai-chan kawaii'
+			'Ai-chan kawaii'
+			'Ai-chan kawaii'
+			'Ai-chan kawaii'
+			'Ai-chan kawaii'
+			'Ai-chan kawaii'`));
+		}
+
+		beforeEach(() => {
+			vi.useFakeTimers();
+		})
+
+		afterEach(() => {
+			vi.restoreAllMocks();
+		})
+
+		test('It ends', async () => {
+			const countSleepsSpy = vi.fn(countSleeps);
+			countSleepsSpy(100);
+			await vi.advanceTimersByTimeAsync(1000);
+			return expect(countSleepsSpy).toHaveResolved();
+		});
+
+		test('It takes time', async () => {
+			const countSleepsSpy = vi.fn(countSleeps);
+			countSleepsSpy(100);
+			await vi.advanceTimersByTimeAsync(999);
+			return expect(countSleepsSpy).not.toHaveResolved();
+		});
+
+		test.each(
+			[-1, NaN]
+		)('Invalid number: %d', (time) => {
+			return expect(countSleeps(time)).rejects.toThrow(AiScriptHostsideError);
+		});
+	});
+});
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 }, { });
+	});
 });
diff --git a/test/syntax.ts b/test/syntax.ts
index 84d2553..913dc32 100644
--- a/test/syntax.ts
+++ b/test/syntax.ts
@@ -176,6 +176,24 @@ describe('separator', () => {
 			`);
 			eq(res, STR('c'));
 		});
+
+		test.concurrent('no separator', async () => {
+			await assert.rejects(async () => {
+				await exe(`
+				let x = 1
+				<:match x{case 1=>"a" case 2=>"b"}
+				`);
+			});
+		});
+
+		test.concurrent('no separator (default)', async () => {
+			await assert.rejects(async () => {
+				await exe(`
+				let x = 1
+				<:match x{case 1=>"a" default=>"b"}
+				`);
+			});
+		});
 	});
 
 	describe('call', () => {
@@ -725,6 +743,15 @@ describe('for', () => {
 		}
 		assert.fail();
 	});
+
+	test.concurrent('scope', async () => {
+		await assert.rejects(async () => {
+			await exe(`
+			for 1 let a = 1
+			<: a
+			`);
+		});
+	});
 });
 
 describe('each', () => {
@@ -1460,6 +1487,27 @@ describe('if', () => {
 		`);
 		eq(res2, STR('kawaii'));
 	});
+
+	test.concurrent('scope', async () => {
+		await assert.rejects(async () => {
+			await exe(`
+			if true let a = 1
+			<: a
+			`);
+		});
+		await assert.rejects(async () => {
+			await exe(`
+			if false null elif true let a = 1
+			<: a
+			`);
+		});
+		await assert.rejects(async () => {
+			await exe(`
+			if false null else let a = 1
+			<: a
+			`);
+		});
+	});
 });
 
 describe('eval', () => {
@@ -1541,6 +1589,21 @@ describe('match', () => {
 		`);
 		eq(res, STR('ai'));
 	});
+
+	test.concurrent('scope', async () => {
+		await assert.rejects(async () => {
+			await exe(`
+			match 1 { case 1 => let a = 1 }
+			<: a
+			`);
+		});
+		await assert.rejects(async () => {
+			await exe(`
+			match 1 { default => let a = 1 }
+			<: a
+			`);
+		});
+	});
 });
 
 describe('exists', () => {