Repository: aiscript-dev/aiscript
Pull request #824 (2024-10-24 19:15:29)

Bump the gh-actions group across 1 directory with 2 updates
Bumps the gh-actions group with 2 updates in the / directory: [actions/checkout](https://github.com/actions/checkout) and [actions/setup-node](https://github.com/actions/setup-node).

Updates `actions/checkout` from 4.2.1 to 4.2.2
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/actions/checkout/releases">actions/checkout's releases</a>.</em></p>
<blockquote>
<h2>v4.2.2</h2>
<h2>What's Changed</h2>
<ul>
<li><code>url-helper.ts</code> now leverages well-known environment variables by <a href="https://github.com/jww3"><code>@​jww3</code></a> in <a href="https://redirect.github.com/actions/checkout/pull/1941">actions/checkout#1941</a></li>
<li>Expand unit test coverage for <code>isGhes</code> by <a href="https://github.com/jww3"><code>@​jww3</code></a> in <a href="https://redirect.github.com/actions/checkout/pull/1946">actions/checkout#1946</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a href="https://github.com/actions/checkout/compare/v4.2.1...v4.2.2">https://github.com/actions/checkout/compare/v4.2.1...v4.2.2</a></p>
</blockquote>
</details>
<details>
<summary>Changelog</summary>
<p><em>Sourced from <a href="https://github.com/actions/checkout/blob/main/CHANGELOG.md">actions/checkout's changelog</a>.</em></p>
<blockquote>
<h2>v4.2.2</h2>
<ul>
<li><code>url-helper.ts</code> now leverages well-known environment variables by <a href="https://github.com/jww3"><code>@​jww3</code></a> in <a href="https://redirect.github.com/actions/checkout/pull/1941">actions/checkout#1941</a></li>
<li>Expand unit test coverage for <code>isGhes</code> by <a href="https://github.com/jww3"><code>@​jww3</code></a> in <a href="https://redirect.github.com/actions/checkout/pull/1946">actions/checkout#1946</a></li>
</ul>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/actions/checkout/commit/11bd71901bbe5b1630ceea73d27597364c9af683"><code>11bd719</code></a> Prepare 4.2.2 Release (<a href="https://redirect.github.com/actions/checkout/issues/1953">#1953</a>)</li>
<li><a href="https://github.com/actions/checkout/commit/e3d2460bbb42d7710191569f88069044cfb9d8cf"><code>e3d2460</code></a> Expand unit test coverage (<a href="https://redirect.github.com/actions/checkout/issues/1946">#1946</a>)</li>
<li><a href="https://github.com/actions/checkout/commit/163217dfcd28294438ea1c1c149cfaf66eec283e"><code>163217d</code></a> <code>url-helper.ts</code> now leverages well-known environment variables. (<a href="https://redirect.github.com/actions/checkout/issues/1941">#1941</a>)</li>
<li>See full diff in <a href="https://github.com/actions/checkout/compare/v4.2.1...v4.2.2">compare view</a></li>
</ul>
</details>
<br />

Updates `actions/setup-node` from 4.0.4 to 4.1.0
<details>
<summary>Release notes</summary>
<p><em>Sourced from <a href="https://github.com/actions/setup-node/releases">actions/setup-node's releases</a>.</em></p>
<blockquote>
<h2>v4.1.0</h2>
<h2>What's Changed</h2>
<ul>
<li>Resolve High Security Alerts by upgrading Dependencies by <a href="https://github.com/aparnajyothi-y"><code>@​aparnajyothi-y</code></a> in <a href="https://redirect.github.com/actions/setup-node/pull/1132">actions/setup-node#1132</a></li>
<li>Upgrade IA Publish by <a href="https://github.com/Jcambass"><code>@​Jcambass</code></a> in <a href="https://redirect.github.com/actions/setup-node/pull/1134">actions/setup-node#1134</a></li>
<li>Revise <code>isGhes</code> logic by <a href="https://github.com/jww3"><code>@​jww3</code></a> in <a href="https://redirect.github.com/actions/setup-node/pull/1148">actions/setup-node#1148</a></li>
<li>Add architecture to cache key by <a href="https://github.com/pengx17"><code>@​pengx17</code></a> in <a href="https://redirect.github.com/actions/setup-node/pull/843">actions/setup-node#843</a>
This addresses issues with caching by adding the architecture (arch) to the cache key, ensuring that cache keys are accurate to prevent conflicts.
Note: This change may break previous cache keys as they will no longer be compatible with the new format.</li>
</ul>
<h2>New Contributors</h2>
<ul>
<li><a href="https://github.com/jww3"><code>@​jww3</code></a> made their first contribution in <a href="https://redirect.github.com/actions/setup-node/pull/1148">actions/setup-node#1148</a></li>
<li><a href="https://github.com/pengx17"><code>@​pengx17</code></a> made their first contribution in <a href="https://redirect.github.com/actions/setup-node/pull/843">actions/setup-node#843</a></li>
</ul>
<p><strong>Full Changelog</strong>: <a href="https://github.com/actions/setup-node/compare/v4...v4.1.0">https://github.com/actions/setup-node/compare/v4...v4.1.0</a></p>
</blockquote>
</details>
<details>
<summary>Commits</summary>
<ul>
<li><a href="https://github.com/actions/setup-node/commit/39370e3970a6d050c480ffad4ff0ed4d3fdee5af"><code>39370e3</code></a> fix: add arch to cached path (<a href="https://redirect.github.com/actions/setup-node/issues/843">#843</a>)</li>
<li><a href="https://github.com/actions/setup-node/commit/abb238b1313d8ef4d3e3a96d204e08329cafbf85"><code>abb238b</code></a> Revise <code>isGhes</code> logic (<a href="https://redirect.github.com/actions/setup-node/issues/1148">#1148</a>)</li>
<li><a href="https://github.com/actions/setup-node/commit/aca7b64a59c0063db8564e0ffdadd3887f1cbae5"><code>aca7b64</code></a> Merge pull request <a href="https://redirect.github.com/actions/setup-node/issues/1134">#1134</a> from actions/Jcambass-patch-1</li>
<li><a href="https://github.com/actions/setup-node/commit/88de2a3d99c13aa734c49200e8bc673fb0a80ab8"><code>88de2a3</code></a> Resolve High Security Alerts by upgrading Dependencies (<a href="https://redirect.github.com/actions/setup-node/issues/1132">#1132</a>)</li>
<li><a href="https://github.com/actions/setup-node/commit/d6ebc7b438e9cf1b22a3c440a164b179ed0e37d6"><code>d6ebc7b</code></a> Upgrade IA Publish</li>
<li>See full diff in <a href="https://github.com/actions/setup-node/compare/v4.0.4...v4.1.0">compare view</a></li>
</ul>
</details>
<br />


Dependabot will resolve any conflicts with this PR as long as you don't alter it yourself. You can also trigger a rebase manually by commenting `@dependabot rebase`.

[//]: # (dependabot-automerge-start)
[//]: # (dependabot-automerge-end)

---

<details>
<summary>Dependabot commands and options</summary>
<br />

You can trigger Dependabot actions by commenting on this PR:
- `@dependabot rebase` will rebase this PR
- `@dependabot recreate` will recreate this PR, overwriting any edits that have been made to it
- `@dependabot merge` will merge this PR after your CI passes on it
- `@dependabot squash and merge` will squash and merge this PR after your CI passes on it
- `@dependabot cancel merge` will cancel a previously requested merge and block automerging
- `@dependabot reopen` will reopen this PR if it is closed
- `@dependabot close` will close this PR and stop Dependabot recreating it. You can achieve the same result by closing it manually
- `@dependabot show <dependency name> ignore conditions` will show all of the ignore conditions of the specified dependency
- `@dependabot ignore <dependency name> major version` will close this group update PR and stop Dependabot creating any more for the specific dependency's major version (unless you unignore this specific dependency's major version or upgrade to it yourself)
- `@dependabot ignore <dependency name> minor version` will close this group update PR and stop Dependabot creating any more for the specific dependency's minor version (unless you unignore this specific dependency's minor version or upgrade to it yourself)
- `@dependabot ignore <dependency name>` will close this group update PR and stop Dependabot creating any more for the specific dependency (unless you unignore this specific dependency or upgrade to it yourself)
- `@dependabot unignore <dependency name>` will remove all of the ignore conditions of the specified dependency
- `@dependabot unignore <dependency name> <ignore condition>` will remove the ignore condition of the specified dependency and ignore conditions


</details>


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/etc/aiscript.api.md b/etc/aiscript.api.md
index 34eb921..ad73857 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 }
@@ -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;
@@ -854,7 +864,7 @@ type VUserFn = VFnBase & {
 
 // 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/index.ts:38:4 - (ae-forgotten-export) The symbol "LogObject" needs to be exported by the entry point index.d.ts
 // src/interpreter/value.ts:46:2 - (ae-forgotten-export) The symbol "Type" needs to be exported by the entry point index.d.ts
 
 // (No @packageDocumentation comment for this package)
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..a9517de 100644
--- a/src/interpreter/index.ts
+++ b/src/interpreter/index.ts
@@ -3,7 +3,7 @@
  */
 
 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 { 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';
@@ -14,9 +14,6 @@ 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;
 	var?: string;
@@ -29,6 +26,8 @@ export class Interpreter {
 	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 +38,8 @@ export class Interpreter {
 			log?(type: string, params: LogObject): void;
 			maxStep?: number;
 			abortOnError?: boolean;
+			irqRate?: number;
+			irqSleep?: number | (() => Promise<void>);
 		} = {},
 	) {
 		const io = {
@@ -70,6 +71,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
@@ -262,7 +282,10 @@ export class Interpreter {
 	@autobind
 	private async __eval(node: Ast.Node, scope: Scope): 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');
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/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/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を返す関数


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..203b5a4 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,93 @@ describe('error location', () => {
 		`)).resolves.toEqual({ line: 2, column: 6});
 	});
 });
+
+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);
+		});
+	});
+});