From 5ae596b25cf0fddd881750c59e1eb411e7c627b9 Mon Sep 17 00:00:00 2001 From: takejohn Date: Sun, 19 Apr 2026 23:21:26 +0900 Subject: [PATCH 1/3] =?UTF-8?q?exec=E3=83=A1=E3=82=BD=E3=83=83=E3=83=89?= =?UTF-8?q?=E3=81=AE=E3=82=AA=E3=83=BC=E3=83=90=E3=83=BC=E3=83=AD=E3=83=BC?= =?UTF-8?q?=E3=83=89=E3=82=92=E8=BF=BD=E5=8A=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- etc/aiscript.api.md | 16 ++++++- src/index.ts | 2 + src/interpreter/exec-result.ts | 51 ++++++++++++++++++++++ src/interpreter/index.ts | 15 ++++++- src/interpreter/scope.ts | 9 ++++ src/utils/iterator.ts | 27 ++++++++++++ test/interpreter.ts | 80 ++++++++++++++++++++++++++++++++++ 7 files changed, 197 insertions(+), 3 deletions(-) create mode 100644 src/interpreter/exec-result.ts create mode 100644 src/utils/iterator.ts diff --git a/etc/aiscript.api.md b/etc/aiscript.api.md index 4974625da..81ca90a52 100644 --- a/etc/aiscript.api.md +++ b/etc/aiscript.api.md @@ -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'; @@ -432,12 +438,18 @@ export class Interpreter { static collectMetadata(script?: Ast.Node[]): Map | undefined; // (undocumented) exec(script?: Ast.Node[]): Promise; + // Warning: (ae-forgotten-export) The symbol "ExecResult" needs to be exported by the entry point index.d.ts + // + // (undocumented) + exec(script: Ast.Node[], resultOpts: T): Promise | void>; execFn(fn: VFn, args: Value[]): Promise; execFnSimple(fn: VFn, args: Value[]): Promise; execFnSync(fn: VFn, args: Value[]): Value; // (undocumented) execSync(script?: Ast.Node[]): Value | undefined; // (undocumented) + execSync(script: Ast.Node[], resultOpts: T): ExecResult; + // (undocumented) pause(): void; // (undocumented) registerAbortHandler(handler: () => void): void; @@ -698,6 +710,8 @@ export class Scope { exists(name: string): boolean; get(name: string): Value; getAll(): Map; + // (undocumented) + getByNames(names: string[]): Map; getNsPrefix(): string; // (undocumented) name: string; @@ -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) diff --git a/src/index.ts b/src/index.ts index ee600c18a..58875e9f7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -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'; @@ -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 }; diff --git a/src/interpreter/exec-result.ts b/src/interpreter/exec-result.ts new file mode 100644 index 000000000..84f3da8e3 --- /dev/null +++ b/src/interpreter/exec-result.ts @@ -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; + }; +}; + +export type ExecResult = { + [K in keyof ExecResultOptions]?: ExecResultConfig[K]['result']; +} & { + [K in RequiredKeys]: ExecResultConfig[K]['result']; +}; + +type RequiredKeys = { + [K in Extract]: T[K] extends ExecResultConfig[K]['condition'] ? K : never; +}[Extract]; + +export function constructResult(opts: T, lastExpressionValue: Value, scope: Scope): ExecResult { + 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; +} + +function pickVariables(scope: Scope, opt: Exclude, false>): Map { + let vars: Map; + if (opt === true) { + vars = scope.getAll(); + } else { + vars = scope.getByNames(opt); + } + return new Map(map(vars, ([name, variable]) => [name, variable.value])); +} diff --git a/src/interpreter/index.ts b/src/interpreter/index.ts index 1ea3d16c3..32f8fdd22 100644 --- a/src/interpreter/index.ts +++ b/src/interpreter/index.ts @@ -14,6 +14,7 @@ import { NULL, FN_NATIVE, BOOL, NUM, STR, ARR, OBJ, FN, ERROR } from './value.js 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'; @@ -103,25 +104,35 @@ export class Interpreter { } } + public async exec(script?: Ast.Node[]): Promise; + public async exec(script: Ast.Node[], resultOpts: T): Promise | void>; @autobind - public async exec(script?: Ast.Node[]): Promise { + public async exec(script?: Ast.Node[], resultOpts?: ExecResultOptions): Promise { 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(script: Ast.Node[], resultOpts: T): ExecResult; @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; } diff --git a/src/interpreter/scope.ts b/src/interpreter/scope.ts index 341425510..953d0d9e1 100644 --- a/src/interpreter/scope.ts +++ b/src/interpreter/scope.ts @@ -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'; @@ -107,6 +108,14 @@ export class Scope { return new Map(vars); } + @autobind + public getByNames(names: string[]): Map { + const vars = this.layerdStates.reduce((arr, layer) => { + return [...arr, ...filter(layer, ([name, _]) => names.includes(name))]; + }, [] as [string, Variable][]); + return new Map(vars); + } + /** * 指定した名前の変数を現在のスコープに追加します。名前空間である場合は接頭辞を付して親のスコープにも追加します * @param name - 変数名 diff --git a/src/utils/iterator.ts b/src/utils/iterator.ts new file mode 100644 index 000000000..a16bc3bf7 --- /dev/null +++ b/src/utils/iterator.ts @@ -0,0 +1,27 @@ +export function filter( + iterable: Iterable, + predicate: (value: T) => value is U +): IteratorObject; +export function filter( + iterable: Iterable, + predicate: (value: T) => boolean +): IteratorObject; +export function* filter( + iterable: Iterable, + predicate: (value: T) => boolean +): IteratorObject { + for (const value of iterable) { + if (predicate(value)) { + yield value; + } + } +} + +export function* map( + iterable: Iterable, + mapper: (value: T) => U, +): IteratorObject { + for (const value of iterable) { + yield mapper(value); + } +} diff --git a/test/interpreter.ts b/test/interpreter.ts index 0b8bc0ea1..cbead523f 100644 --- a/test/interpreter.ts +++ b/test/interpreter.ts @@ -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 = async () => {}; From 11407eccba06b11db5d76ebaf5d05c2bfe654717 Mon Sep 17 00:00:00 2001 From: takejohn Date: Sun, 19 Apr 2026 23:52:28 +0900 Subject: [PATCH 2/3] =?UTF-8?q?=E5=A4=89=E6=9B=B4=E7=82=B9=E3=81=AE?= =?UTF-8?q?=E8=A8=98=E8=BF=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- unreleased/exec-return-value.md | 3 +++ 1 file changed, 3 insertions(+) create mode 100644 unreleased/exec-return-value.md diff --git a/unreleased/exec-return-value.md b/unreleased/exec-return-value.md new file mode 100644 index 000000000..666f0c619 --- /dev/null +++ b/unreleased/exec-return-value.md @@ -0,0 +1,3 @@ +- For Hosts: `Interpreter`クラスの`exec`メソッドおよび`execSycn`メソッドにオーバーロードを追加しました。第2引数に以下のプロパティをもつオブジェクトを渡すことができるようになります。 + - `value`: 最後に評価された式の値を返します。 + - `variables`プロパティ: グローバルスコープの変数の値を返します。 From 8644a24ccf10191e8b7431111cc9dab214d1aad9 Mon Sep 17 00:00:00 2001 From: takejohn Date: Sun, 19 Apr 2026 23:53:22 +0900 Subject: [PATCH 3/3] typo --- unreleased/exec-return-value.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/unreleased/exec-return-value.md b/unreleased/exec-return-value.md index 666f0c619..1e46fd272 100644 --- a/unreleased/exec-return-value.md +++ b/unreleased/exec-return-value.md @@ -1,3 +1,3 @@ -- For Hosts: `Interpreter`クラスの`exec`メソッドおよび`execSycn`メソッドにオーバーロードを追加しました。第2引数に以下のプロパティをもつオブジェクトを渡すことができるようになります。 +- For Hosts: `Interpreter`クラスの`exec`メソッドおよび`execSync`メソッドにオーバーロードを追加しました。第2引数に以下のプロパティをもつオブジェクトを渡すことができるようになります。 - `value`: 最後に評価された式の値を返します。 - `variables`プロパティ: グローバルスコープの変数の値を返します。