Skip to content
Merged
Show file tree
Hide file tree
Changes from all 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
18 changes: 17 additions & 1 deletion etc/aiscript.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -411,13 +411,25 @@ export class Interpreter {
execFn(fn: VFn, args: Value[]): Promise<Value>;
execFnSimple(fn: VFn, args: Value[]): Promise<Value>;
// (undocumented)
pause(): void;
// (undocumented)
registerAbortHandler(handler: () => void): void;
// (undocumented)
registerPauseHandler(handler: () => void): void;
// (undocumented)
registerUnpauseHandler(handler: () => void): void;
// (undocumented)
scope: Scope;
// (undocumented)
stepCount: number;
// (undocumented)
unpause(): void;
// (undocumented)
unregisterAbortHandler(handler: () => void): void;
// (undocumented)
unregisterPauseHandler(handler: () => void): void;
// (undocumented)
unregisterUnpauseHandler(handler: () => void): void;
}

// @public (undocumented)
Expand Down Expand Up @@ -820,7 +832,11 @@ type VNativeFn = VFnBase & {
call: (fn: VFn, args: Value[]) => Promise<Value>;
topCall: (fn: VFn, args: Value[]) => Promise<Value>;
registerAbortHandler: (handler: () => void) => void;
registerPauseHandler: (handler: () => void) => void;
registerUnpauseHandler: (handler: () => void) => void;
unregisterAbortHandler: (handler: () => void) => void;
unregisterPauseHandler: (handler: () => void) => void;
unregisterUnpauseHandler: (handler: () => void) => void;
}) => Value | Promise<Value> | void;
};

Expand Down Expand Up @@ -866,7 +882,7 @@ type VUserFn = VFnBase & {

// Warnings were encountered during analysis:
//
// src/interpreter/index.ts:44:4 - (ae-forgotten-export) The symbol "LogObject" needs to be exported by the entry point index.d.ts
// src/interpreter/index.ts:47: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)
Expand Down
8 changes: 7 additions & 1 deletion playground/src/App.vue
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,11 @@
</footer>
</div>
<div id="logs" class="container">
<header>Output</header>
<header>
Output
<div v-if="paused" class="actions"><button @click="interpreter.unpause(), paused = false">Unpause</button></div>
<div v-else class="actions"><button @click="interpreter.pause(), paused = true">Pause</button></div>
</header>
<div>
<div v-for="log in logs" class="log" :key="log.id" :class="[{ print: log.print }, log.type]"><span class="type">{{ log.type }}</span> {{ log.text }}</div>
</div>
Expand Down Expand Up @@ -66,6 +70,7 @@ const ast = ref(null);
const logs = ref([]);
const syntaxErrorMessage = ref(null);
const showSettings = ref(false);
const paused = ref(false);

watch(script, () => {
window.localStorage.setItem('script', script.value);
Expand Down Expand Up @@ -95,6 +100,7 @@ const run = async () => {
logs.value = [];

interpreter?.abort();
paused.value = false;
interpreter = new Interpreter({}, {
in: (q) => {
return new Promise(ok => {
Expand Down
47 changes: 47 additions & 0 deletions src/interpreter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,8 +29,11 @@ type CallInfo = {
export class Interpreter {
public stepCount = 0;
private stop = false;
private pausing: { promise: Promise<void>, resolve: () => void } | null = null;
public scope: Scope;
private abortHandlers: (() => void)[] = [];
private pauseHandlers: (() => void)[] = [];
private unpauseHandlers: (() => void)[] = [];
private vars: Record<string, Variable> = {};
private irqRate: number;
private irqSleep: () => Promise<void>;
Expand Down Expand Up @@ -265,7 +268,11 @@ export class Interpreter {
call: (fn, args) => this._fn(fn, args, [...callStack, info]),
topCall: this.execFn,
registerAbortHandler: this.registerAbortHandler,
registerPauseHandler: this.registerPauseHandler,
registerUnpauseHandler: this.registerUnpauseHandler,
unregisterAbortHandler: this.unregisterAbortHandler,
unregisterPauseHandler: this.unregisterPauseHandler,
unregisterUnpauseHandler: this.unregisterUnpauseHandler,
});
// eslint-disable-next-line @typescript-eslint/no-unnecessary-condition
return result ?? NULL;
Expand Down Expand Up @@ -311,6 +318,7 @@ export class Interpreter {
@autobind
private async __eval(node: Ast.Node, scope: Scope, callStack: readonly CallInfo[]): Promise<Value> {
if (this.stop) return NULL;
if (this.pausing) await this.pausing.promise;
// irqRateが小数の場合は不等間隔になる
if (this.irqRate !== 0 && this.stepCount % this.irqRate >= this.irqRate - 1) {
await this.irqSleep();
Expand Down Expand Up @@ -764,11 +772,27 @@ export class Interpreter {
public registerAbortHandler(handler: () => void): void {
this.abortHandlers.push(handler);
}
@autobind
public registerPauseHandler(handler: () => void): void {
this.pauseHandlers.push(handler);
}
@autobind
public registerUnpauseHandler(handler: () => void): void {
this.unpauseHandlers.push(handler);
}

@autobind
public unregisterAbortHandler(handler: () => void): void {
this.abortHandlers = this.abortHandlers.filter(h => h !== handler);
}
@autobind
public unregisterPauseHandler(handler: () => void): void {
this.pauseHandlers = this.pauseHandlers.filter(h => h !== handler);
}
@autobind
public unregisterUnpauseHandler(handler: () => void): void {
this.unpauseHandlers = this.unpauseHandlers.filter(h => h !== handler);
}

@autobind
public abort(): void {
Expand All @@ -779,6 +803,29 @@ export class Interpreter {
this.abortHandlers = [];
}

@autobind
public pause(): void {
if (this.pausing) return;
let resolve: () => void;
const promise = new Promise<void>(r => { resolve = () => r(); });
this.pausing = { promise, resolve: resolve! };
for (const handler of this.pauseHandlers) {
handler();
}
this.pauseHandlers = [];
}

@autobind
public unpause(): void {
if (!this.pausing) return;
this.pausing.resolve();
this.pausing = null;
for (const handler of this.unpauseHandlers) {
handler();
}
this.unpauseHandlers = [];
}

@autobind
private async define(scope: Scope, dest: Ast.Expression, value: Value, isMutable: boolean): Promise<void> {
switch (dest.type) {
Expand Down
52 changes: 36 additions & 16 deletions src/interpreter/lib/std.ts
Original file line number Diff line number Diff line change
Expand Up @@ -638,41 +638,61 @@ export const std: Record<string, Value> = {
if (immediate.value) opts.call(callback, []);
}

const id = setInterval(() => {
opts.topCall(callback, []);
}, interval.value);

const abortHandler = (): void => {
let id: ReturnType<typeof setInterval>;

const start = (): void => {
id = setInterval(() => {
opts.topCall(callback, []);
}, interval.value);
opts.registerAbortHandler(stop);
opts.registerPauseHandler(stop);
opts.unregisterUnpauseHandler(start);
};
const stop = (): void => {
clearInterval(id);
opts.unregisterAbortHandler(stop);
opts.unregisterPauseHandler(stop);
opts.registerUnpauseHandler(start);
};

opts.registerAbortHandler(abortHandler);
start();

// stopper
return FN_NATIVE(([], opts) => {
clearInterval(id);
opts.unregisterAbortHandler(abortHandler);
stop();
opts.unregisterUnpauseHandler(start);
});
}),

'Async:timeout': FN_NATIVE(async ([delay, callback], opts) => {
assertNumber(delay);
assertFunction(callback);

const id = setTimeout(() => {
opts.topCall(callback, []);
}, delay.value);

const abortHandler = (): void => {
let id: ReturnType<typeof setInterval>;

const start = (): void => {
id = setTimeout(() => {
opts.topCall(callback, []);
opts.unregisterAbortHandler(stop);
opts.unregisterPauseHandler(stop);
}, delay.value);
opts.registerAbortHandler(stop);
opts.registerPauseHandler(stop);
opts.unregisterUnpauseHandler(start);
};
const stop = (): void => {
clearTimeout(id);
opts.unregisterAbortHandler(stop);
opts.unregisterPauseHandler(stop);
opts.registerUnpauseHandler(start);
};

opts.registerAbortHandler(abortHandler);
start();

// stopper
return FN_NATIVE(([], opts) => {
clearTimeout(id);
opts.unregisterAbortHandler(abortHandler);
stop();
opts.unregisterUnpauseHandler(start);
});
}),
//#endregion
Expand Down
4 changes: 4 additions & 0 deletions src/interpreter/value.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,11 @@ export type VNativeFn = VFnBase & {
call: (fn: VFn, args: Value[]) => Promise<Value>;
topCall: (fn: VFn, args: Value[]) => Promise<Value>;
registerAbortHandler: (handler: () => void) => void;
registerPauseHandler: (handler: () => void) => void;
registerUnpauseHandler: (handler: () => void) => void;
unregisterAbortHandler: (handler: () => void) => void;
unregisterPauseHandler: (handler: () => void) => void;
unregisterUnpauseHandler: (handler: () => void) => void;
}) => Value | Promise<Value> | void;
};

Expand Down
70 changes: 70 additions & 0 deletions test/interpreter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -254,3 +254,73 @@ describe('IRQ', () => {
});
});
});

describe('pause', () => {
async function exePausable() {
let count = 0;

const interpreter = new Interpreter({
count: values.FN_NATIVE(() => { count++; }),
}, {});

// await to catch errors
await interpreter.exec(Parser.parse(
`Async:interval(100, @() { count() })`
));

return {
pause: interpreter.pause,
unpause: interpreter.unpause,
getCount: () => count,
resetCount: () => count = 0,
};
}

beforeEach(() => {
vi.useFakeTimers();
})

afterEach(() => {
vi.restoreAllMocks();
})

test('basic', async () => {
const p = await exePausable();
await vi.advanceTimersByTimeAsync(500);
p.pause();
await vi.advanceTimersByTimeAsync(400);
return expect(p.getCount()).toEqual(5);
});

test('unpause', async () => {
const p = await exePausable();
await vi.advanceTimersByTimeAsync(500);
p.pause();
await vi.advanceTimersByTimeAsync(400);
p.unpause();
await vi.advanceTimersByTimeAsync(300);
return expect(p.getCount()).toEqual(8);
});

describe('randomly scheduled pausing', () => {
function rnd(min: number, max: number): number {
return Math.floor(min + (Math.random() * (max - min + 1)));
}
const schedule = Array(rnd(2, 10)).fill(0).map(() => rnd(1, 10) * 100);
const title = schedule.map((v, i) => `${i % 2 ? 'un' : ''}pause ${v}`).join(', ');

test(title, async () => {
const p = await exePausable();
let answer = 0;
for (const [i, v] of schedule.entries()) {
if (i % 2) {
p.unpause();
answer += v / 100;
}
else p.pause();
await vi.advanceTimersByTimeAsync(v);
}
return expect(p.getCount()).toEqual(answer);
});
});
});
3 changes: 3 additions & 0 deletions unreleased/pause.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
- For Hosts: `interpreter.pause()`で実行の一時停止ができるように
- `interpreter.unpause()`で再開
- 再開後に`Async:`系の待ち時間がリセットされる不具合がありますが、修正の目処は立っていません