Skip to content
Open
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
16 changes: 15 additions & 1 deletion etc/aiscript.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -314,6 +314,12 @@ declare namespace errors {
}
export { errors }

// @public (undocumented)
export type ExecResultOptions = {
value?: boolean;
variables?: boolean | string[];
};

// @public (undocumented)
type Exists = NodeBase & {
type: 'exists';
Expand Down Expand Up @@ -432,12 +438,18 @@ export class Interpreter {
static collectMetadata(script?: Ast.Node[]): Map<string | null, JsValue> | undefined;
// (undocumented)
exec(script?: Ast.Node[]): Promise<void>;
// Warning: (ae-forgotten-export) The symbol "ExecResult" needs to be exported by the entry point index.d.ts
//
// (undocumented)
exec<T extends ExecResultOptions>(script: Ast.Node[], resultOpts: T): Promise<ExecResult<T> | void>;
execFn(fn: VFn, args: Value[]): Promise<Value>;
execFnSimple(fn: VFn, args: Value[]): Promise<Value>;
execFnSync(fn: VFn, args: Value[]): Value;
// (undocumented)
execSync(script?: Ast.Node[]): Value | undefined;
// (undocumented)
execSync<T extends ExecResultOptions>(script: Ast.Node[], resultOpts: T): ExecResult<T>;
// (undocumented)
pause(): void;
// (undocumented)
registerAbortHandler(handler: () => void): void;
Expand Down Expand Up @@ -698,6 +710,8 @@ export class Scope {
exists(name: string): boolean;
get(name: string): Value;
getAll(): Map<string, Variable>;
// (undocumented)
getByNames(names: string[]): Map<string, Variable>;
getNsPrefix(): string;
// (undocumented)
name: string;
Expand Down Expand Up @@ -918,7 +932,7 @@ type VUserFn = VFnBase & {

// Warnings were encountered during analysis:
//
// src/interpreter/index.ts:49:4 - (ae-forgotten-export) The symbol "LogObject" needs to be exported by the entry point index.d.ts
// src/interpreter/index.ts:50: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
2 changes: 2 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
//export * from './interpreter/index';
//export * as utils from './interpreter/util';
//export * as values from './interpreter/value';
import { ExecResultOptions } from './interpreter/exec-result.js';
import { Interpreter } from './interpreter/index.js';
import { Scope } from './interpreter/scope.js';
import * as utils from './interpreter/util.js';
Expand All @@ -12,6 +13,7 @@ import * as errors from './error.js';
import * as Ast from './node.js';
import { AISCRIPT_VERSION } from './constants.js';
import type { ParserPlugin, PluginType } from './parser/index.js';
export { ExecResultOptions };
export { Interpreter };
export { Scope };
export { utils };
Expand Down
51 changes: 51 additions & 0 deletions src/interpreter/exec-result.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,51 @@
import { map } from '../utils/iterator.js';
import type { Variable } from './variable.js';
import type { Scope } from './scope.js';
import type { Value } from './value.js';

export type ExecResultOptions = {
value?: boolean;
variables?: boolean | string[];
};

type ExecResultConfig = {
value: {
condition: true;
result: Value;
};
variables: {
condition: true | string[];
result: Map<string, Value>;
};
};

export type ExecResult<T extends ExecResultOptions = ExecResultOptions> = {
[K in keyof ExecResultOptions]?: ExecResultConfig[K]['result'];
} & {
[K in RequiredKeys<T>]: ExecResultConfig[K]['result'];
};

type RequiredKeys<T extends ExecResultOptions> = {
[K in Extract<keyof T, keyof ExecResultOptions>]: T[K] extends ExecResultConfig[K]['condition'] ? K : never;
}[Extract<keyof T, keyof ExecResultOptions>];

export function constructResult<T extends ExecResultOptions>(opts: T, lastExpressionValue: Value, scope: Scope): ExecResult<T> {
const resultObj: ExecResult = {};
if (opts.value) {
resultObj.value = lastExpressionValue;
}
if (opts.variables != null && opts.variables !== false) {
resultObj.variables = pickVariables(scope, opts.variables);
}
return resultObj as ExecResult<T>;
}

function pickVariables(scope: Scope, opt: Exclude<NonNullable<ExecResultOptions['variables']>, false>): Map<string, Value> {
let vars: Map<string, Variable>;
if (opt === true) {
vars = scope.getAll();
} else {
vars = scope.getByNames(opt);
}
return new Map(map(vars, ([name, variable]) => [name, variable.value]));
}
15 changes: 13 additions & 2 deletions src/interpreter/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
import { getPrimProp } from './primitive-props.js';
import { Variable } from './variable.js';
import { Reference } from './reference.js';
import { constructResult, type ExecResult, type ExecResultOptions } from './exec-result.js';
import type { JsValue } from './util.js';
import type { Value, VFn, VUserFn } from './value.js';

Expand Down Expand Up @@ -103,25 +104,35 @@
}
}

public async exec(script?: Ast.Node[]): Promise<void>;
public async exec<T extends ExecResultOptions>(script: Ast.Node[], resultOpts: T): Promise<ExecResult<T> | void>;
@autobind
public async exec(script?: Ast.Node[]): Promise<void> {
public async exec(script?: Ast.Node[], resultOpts?: ExecResultOptions): Promise<ExecResult | void> {
if (script == null || script.length === 0) return;
try {
await this.collectNs(script);
const result = await this._run(script, this.scope, []);
assertValue(result);
this.log('end', { val: result });
if (resultOpts != null) {
return constructResult(resultOpts, result, this.scope);
}
} catch (e) {
this.handleError(e);
}
}

public execSync(script?: Ast.Node[]): Value | undefined;
public execSync<T extends ExecResultOptions>(script: Ast.Node[], resultOpts: T): ExecResult<T>;
@autobind
public execSync(script?: Ast.Node[]): Value | undefined {
public execSync(script?: Ast.Node[], resultOpts?: ExecResultOptions): Value | undefined | ExecResult {
if (script == null || script.length === 0) return;
this.collectNsSync(script);
const result = this._runSync(script, this.scope, []);
assertValue(result);
if (resultOpts != null) {
return constructResult(resultOpts, result, this.scope);
}
return result;
}

Expand Down Expand Up @@ -548,7 +559,7 @@

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

Check warning on line 562 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(), callStack);
if (v.type === 'break') {
if (v.label != null && v.label !== node.label) {
Expand Down Expand Up @@ -1081,7 +1092,7 @@

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

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

View workflow job for this annotation

GitHub Actions / lint

Unnecessary conditional, value is always truthy
const v = this._runSync(node.statements, scope.createChildScope(), callStack);
if (v.type === 'break') {
if (v.label != null && v.label !== node.label) {
Expand Down Expand Up @@ -1631,7 +1642,7 @@
public pause(): void {
if (this.pausing) return;
let resolve: () => void;
const promise = new Promise<void>(r => { resolve = () => r(); });

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

View workflow job for this annotation

GitHub Actions / lint

Missing return type on function
this.pausing = { promise, resolve: resolve! };
for (const handler of this.pauseHandlers) {
handler();
Expand Down
9 changes: 9 additions & 0 deletions src/interpreter/scope.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { autobind } from '../utils/mini-autobind.js';
import { AiScriptRuntimeError } from '../error.js';
import { filter } from '../utils/iterator.js';
import type { Value } from './value.js';
import type { Variable } from './variable.js';
import type { LogObject } from './index.js';
Expand Down Expand Up @@ -107,6 +108,14 @@ export class Scope {
return new Map(vars);
}

@autobind
public getByNames(names: string[]): Map<string, Variable> {
const vars = this.layerdStates.reduce((arr, layer) => {
return [...arr, ...filter(layer, ([name, _]) => names.includes(name))];
}, [] as [string, Variable][]);
return new Map(vars);
}

/**
* 指定した名前の変数を現在のスコープに追加します。名前空間である場合は接頭辞を付して親のスコープにも追加します
* @param name - 変数名
Expand Down
27 changes: 27 additions & 0 deletions src/utils/iterator.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,27 @@
export function filter<T, U extends T>(
iterable: Iterable<T, unknown, undefined>,
predicate: (value: T) => value is U
): IteratorObject<U, undefined, unknown>;
export function filter<T>(
iterable: Iterable<T, unknown, undefined>,
predicate: (value: T) => boolean
): IteratorObject<T, undefined, unknown>;
export function* filter<T>(
iterable: Iterable<T, unknown, undefined>,
predicate: (value: T) => boolean
): IteratorObject<T, undefined, unknown> {
for (const value of iterable) {
if (predicate(value)) {
yield value;
}
}
}

export function* map<T, U>(
iterable: Iterable<T, unknown, undefined>,
mapper: (value: T) => U,
): IteratorObject<U, undefined, unknown> {
for (const value of iterable) {
yield mapper(value);
}
}
80 changes: 80 additions & 0 deletions test/interpreter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,6 +29,86 @@ describe('Scope', () => {
});
});

describe('ExecResult', () => {
describe('value', () => {
const script = Parser.parse('"ai"; 42');

test.concurrent('true', async () => {
const aiscript = new Interpreter({});
const result = await aiscript.exec(script, { value: true });
assert.ok(result != null);
expect(result.value).toStrictEqual(NUM(42));
});

test.concurrent('false', async () => {
const aiscript = new Interpreter({});
const result = await aiscript.exec(script, { value: false });
assert.ok(result != null);
expect(result.value).toBeUndefined();
});

test.concurrent('undefined', async () => {
const aiscript = new Interpreter({});
const result = await aiscript.exec(script, {});
assert.ok(result != null);
expect(result.value).toBeUndefined();
});
});

describe('variables', () => {
const script = Parser.parse(`
let a = 1
@b() {
let x = a + 1
x
}
if true {
var y = 2
}
var c = true
`);

test.concurrent('all', async () => {
const aiscript = new Interpreter({});
const result = await aiscript.exec(script, { variables: true });
assert.ok(result != null);
const vars = result.variables;
expect(vars.get('a')).not.toBeUndefined();
expect(vars.get('b')).not.toBeUndefined();
expect(vars.get('c')).not.toBeUndefined();
expect(vars.get('x')).toBeUndefined();
expect(vars.get('y')).toBeUndefined();
});

test.concurrent('some', async () => {
const aiscript = new Interpreter({});
const result = await aiscript.exec(script, { variables: ['a', 'b', 'x'] });
assert.ok(result != null);
const vars = result.variables;
expect(new Set(vars.keys())).toStrictEqual(new Set(['a', 'b']))
});

test.concurrent('empty', async () => {
const aiscript = new Interpreter({});
const result = await aiscript.exec(script, { variables: [] });
assert.ok(result != null);
const vars = result.variables;
expect(vars.size).toBe(0);
});

test.each([
['false', { variables: false }],
['undefined', {}],
])('none (%s)', async (_, opts) => {
const aiscript = new Interpreter({});
const result = await aiscript.exec(script, opts);
assert.ok(result != null);
const vars = result.variables;
expect(vars).toBeUndefined();
});
});
});

describe('error handler', () => {
test.concurrent('error from outside caller', async () => {
let outsideCaller: () => Promise<void> = async () => {};
Expand Down
3 changes: 3 additions & 0 deletions unreleased/exec-return-value.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
- For Hosts: `Interpreter`クラスの`exec`メソッドおよび`execSync`メソッドにオーバーロードを追加しました。第2引数に以下のプロパティをもつオブジェクトを渡すことができるようになります。
- `value`: 最後に評価された式の値を返します。
- `variables`プロパティ: グローバルスコープの変数の値を返します。
Loading