Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

IRQをカスタマイズ可能に #817

Merged
merged 11 commits into from
Oct 24, 2024
Merged
Show file tree
Hide file tree
Changes from 10 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 12 additions & 2 deletions etc/aiscript.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -281,7 +288,8 @@ declare namespace errors {
AiScriptNamespaceError,
AiScriptRuntimeError,
AiScriptIndexOutOfRangeError,
AiScriptUserError
AiScriptUserError,
AiScriptHostsideError
}
}
export { errors }
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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)
Expand Down
14 changes: 12 additions & 2 deletions playground/src/App.vue
Original file line number Diff line number Diff line change
@@ -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>
Expand Down Expand Up @@ -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);
Expand Down Expand Up @@ -119,7 +122,9 @@ const run = async () => {
}); break;
default: break;
}
}
},
irqRate: settings.value.irqRate,
irqSleep: settings.value.irqSleep,
});

try {
Expand Down Expand Up @@ -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 {
Expand Down
59 changes: 59 additions & 0 deletions playground/src/Settings.vue
Original file line number Diff line number Diff line change
@@ -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>
9 changes: 9 additions & 0 deletions src/error.ts
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,7 @@
this.info = info;

// Maintains proper stack trace for where our error was thrown (only available on V8)
if (Error.captureStackTrace) {

Check warning on line 15 in src/error.ts

View workflow job for this annotation

GitHub Actions / lint

Unnecessary conditional, value is always truthy
Error.captureStackTrace(this, AiScriptError);
}
}
Expand Down Expand Up @@ -86,3 +86,12 @@
super(message, info);
}
}
/**
* Host side configuration errors.
*/
export class AiScriptHostsideError extends AiScriptError {
public name = 'Host';
constructor(message: string, info?: unknown) {
super(message, info);
}
}
33 changes: 28 additions & 5 deletions src/interpreter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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';
Expand All @@ -14,9 +14,6 @@
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;
Expand All @@ -29,6 +26,8 @@
public scope: Scope;
private abortHandlers: (() => void)[] = [];
private vars: Record<string, Variable> = {};
private irqRate: number;
private irqSleep: () => Promise<void>;

constructor(
consts: Record<string, Value>,
Expand All @@ -39,6 +38,8 @@
log?(type: string, params: LogObject): void;
maxStep?: number;
abortOnError?: boolean;
irqRate?: number;
irqSleep?: number | (() => Promise<void>);
} = {},
) {
const io = {
Expand Down Expand Up @@ -70,6 +71,25 @@
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;
salano-ym marked this conversation as resolved.
Show resolved Hide resolved

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
Expand Down Expand Up @@ -262,7 +282,10 @@
@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');
Expand All @@ -282,7 +305,7 @@
if (cond.value) {
return this._eval(node.then, scope);
} else {
if (node.elseif && node.elseif.length > 0) {

Check warning on line 308 in src/interpreter/index.ts

View workflow job for this annotation

GitHub Actions / lint

Unnecessary conditional, value is always truthy
for (const elseif of node.elseif) {
const cond = await this._eval(elseif.cond, scope);
assertBoolean(cond);
Expand Down Expand Up @@ -316,7 +339,7 @@

case 'loop': {
// eslint-disable-next-line no-constant-condition
while (true) {

Check warning on line 342 in src/interpreter/index.ts

View workflow job for this annotation

GitHub Actions / lint

Unnecessary conditional, value is always truthy
const v = await this._run(node.statements, scope.createChildScope());
if (v.type === 'break') {
break;
Expand Down
94 changes: 92 additions & 2 deletions test/interpreter.ts
Original file line number Diff line number Diff line change
@@ -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 () => {
Expand Down Expand Up @@ -114,3 +114,93 @@ describe('error location', () => {
`)).resolves.toEqual({ line: 2, column: 6});
});
});

describe('IRQ', () => {
salano-ym marked this conversation as resolved.
Show resolved Hide resolved
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.concurrent('It ends', async () => {
const countSleepsSpy = vi.fn(countSleeps);
countSleepsSpy(100);
await vi.advanceTimersByTimeAsync(1000);
return expect(countSleepsSpy).toHaveResolved();
});

test.concurrent('It takes time', async () => {
const countSleepsSpy = vi.fn(countSleeps);
countSleepsSpy(100);
await vi.advanceTimersByTimeAsync(999);
return expect(countSleepsSpy).not.toHaveResolved();
});

test.concurrent.each(
[-1, NaN]
)('Invalid number: %d', (time) => {
return expect(countSleeps(time)).rejects.toThrow(AiScriptHostsideError);
});
uzmoi marked this conversation as resolved.
Show resolved Hide resolved
});
});
3 changes: 3 additions & 0 deletions unreleased/irq-config.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
- For Hosts: Interpreterのオプションに`irqRate`と`irqSleep`を追加
- `irqRate`はInterpreterの定期休止が何ステップに一回起こるかを指定する数値
- `irqSleep`は休止時間をミリ秒で指定する数値、または休止ごとにawaitされるPromiseを返す関数
Loading