From 52ebf2055e0d6a238796ceacf92a7073bf007127 Mon Sep 17 00:00:00 2001 From: syuilo Date: Wed, 1 May 2019 14:54:34 +0900 Subject: [PATCH] Improve AiScript --- locales/ja-JP.yml | 5 +- src/client/app/common/scripts/aiscript.ts | 134 +++++++++--------- .../page-editor/page-editor.script-block.vue | 9 +- .../components/page-editor/page-editor.vue | 1 + .../app/common/views/pages/page/page.if.vue | 2 +- .../app/common/views/pages/page/page.vue | 4 +- 6 files changed, 80 insertions(+), 75 deletions(-) diff --git a/locales/ja-JP.yml b/locales/ja-JP.yml index 558408ce2a..fa5e1e1475 100644 --- a/locales/ja-JP.yml +++ b/locales/ja-JP.yml @@ -1853,6 +1853,7 @@ pages: variables-info: "変数を使うことで動的なページを作成できます。テキスト内で { 変数名 } と書くとそこに変数の値を埋め込めます。例えば Hello { thing } world! というテキストで、変数(thing)の値が ai だった場合、テキストは Hello ai world! になります。" variables-info2: "変数の評価(値を算出すること)は上から下に行われるので、ある変数の中で自分より下の変数を参照することはできません。例えば上から A、B、C と3つの変数を定義したとき、Cの中でABを参照することはできますが、Aの中でBCを参照することはできません。" variables-info3: "ユーザーからの入力を受け取るには、ページに「ユーザー入力」ブロックを設置し、「変数名」に入力を格納したい変数名を設定します(変数は自動で作成されます)。その変数を使ってユーザー入力に応じた動作を行えます。" + variables-info4: "関数を使うと、値の算出処理を再利用可能な形にまとめることができます。関数を作るには、「関数」タイプの変数を作成します。関数にはスロット(引数)を設定することができ、スロットの値は関数内で変数として利用可能です。また、AiScript標準で関数を引数に取る関数(高階関数と呼ばれます)も存在します。関数は予め定義しておくほかに、このような高階関数のスロットに即席でセットすることもできます。" more-details: "詳しい説明" title: "タイトル" url: "ページURL" @@ -2057,9 +2058,6 @@ pages: _numberToString: arg1: "数値" ref: "変数" - in: "スロット入力" - _in: - arg1: "スロット番号" fn: "関数" _fn: slots: "スロット" @@ -2080,3 +2078,4 @@ pages: emptySlot: "空のスロット" enviromentVariables: "環境変数" pageVariables: "ページ要素" + argVariables: "入力スロット" diff --git a/src/client/app/common/scripts/aiscript.ts b/src/client/app/common/scripts/aiscript.ts index 70edce9b7b..1c4fe217c3 100644 --- a/src/client/app/common/scripts/aiscript.ts +++ b/src/client/app/common/scripts/aiscript.ts @@ -101,7 +101,6 @@ const literalDefs = { textList: { out: 'stringArray', category: 'value', icon: faList, }, number: { out: 'number', category: 'value', icon: faSortNumericUp, }, ref: { out: null, category: 'value', icon: faSuperscript, }, - in: { out: null, category: 'value', icon: faSuperscript, }, fn: { out: 'function', category: 'value', icon: faSuperscript, }, }; @@ -139,6 +138,50 @@ const envVarsDef = { YMD: 'string', }; +class AiScriptError extends Error { + constructor(...params) { + super(...params); + + // Maintains proper stack trace for where our error was thrown (only available on V8) + if (Error.captureStackTrace) { + Error.captureStackTrace(this, AiScriptError); + } + } +} + +class Scope { + private layerdStates: Record[]; + public name: string; + + constructor(layerdStates: Scope['layerdStates'], name?: Scope['name']) { + this.layerdStates = layerdStates; + this.name = name || 'anonymous'; + } + + @autobind + public createChildScope(states: Record, name?: Scope['name']): Scope { + const layer = [states, ...this.layerdStates]; + return new Scope(layer, name); + } + + /** + * 指定した名前の変数の値を取得します + * @param name 変数名 + */ + @autobind + public getState(name: string): any { + for (const later of this.layerdStates) { + const state = later[name]; + if (state !== undefined) { + return state; + } + } + + throw new AiScriptError( + `No such variable '${name}' in scope '${this.name}'`); + } +} + export class AiScript { private variables: Variable[]; private pageVars: PageVar[]; @@ -298,7 +341,7 @@ export class AiScript { return null; } if (v.type === 'fn') return null; // todo - if (v.type === 'in') return null; // todo + if (v.type.startsWith('fn:')) return null; // todo const generic: Type[] = []; @@ -350,43 +393,34 @@ export class AiScript { } @autobind - private interpolate(str: string, values: { name: string, value: any }[]) { + private interpolate(str: string, scope: Scope) { return str.replace(/\{(.+?)\}/g, match => { - const v = this.getVarVal(match.slice(1, -1).trim(), values); + const v = scope.getState(match.slice(1, -1).trim()); return v == null ? 'NULL' : v.toString(); }); } @autobind - public evaluateVars() { - const values: { name: string, value: any }[] = []; + public evaluateVars(): Record { + const values: Record = {}; - for (const v of this.variables) { - values.push({ - name: v.name, - value: this.evaluate(v, values) - }); + for (const [k, v] of Object.entries(this.envVars)) { + values[k] = v; } for (const v of this.pageVars) { - values.push({ - name: v.name, - value: v.value - }); + values[v.name] = v.value; } - for (const [k, v] of Object.entries(this.envVars)) { - values.push({ - name: k, - value: v - }); + for (const v of this.variables) { + values[v.name] = this.evaluate(v, new Scope([values])); } return values; } @autobind - private evaluate(block: Block, values: { name: string, value: any }[], slotArg: Record = {}): any { + private evaluate(block: Block, scope: Scope): any { if (block.type === null) { return null; } @@ -396,7 +430,7 @@ export class AiScript { } if (block.type === 'text' || block.type === 'multiLineText') { - return this.interpolate(block.value || '', values); + return this.interpolate(block.value || '', scope); } if (block.type === 'textList') { @@ -404,28 +438,27 @@ export class AiScript { } if (block.type === 'ref') { - return this.getVarVal(block.value, values); - } - - if (block.type === 'in') { - return slotArg[block.value]; + return scope.getState(block.value); } if (isFnBlock(block)) { // ユーザー関数定義 return { slots: block.value.slots.map(x => x.name), - exec: slotArg => this.evaluate(block.value.expression, values, slotArg) + exec: slotArg => { + return this.evaluate(block.value.expression, scope.createChildScope(slotArg, block.id)); + } }; } if (block.type.startsWith('fn:')) { // ユーザー関数呼び出し const fnName = block.type.split(':')[1]; - const fn = this.getVarVal(fnName, values); + const fn = scope.getState(fnName); + const args = {}; for (let i = 0; i < fn.slots.length; i++) { const name = fn.slots[i]; - slotArg[name] = this.evaluate(block.args[i], values); + args[name] = this.evaluate(block.args[i], scope); } - return fn.exec(slotArg); + return fn.exec(args); } if (block.args === undefined) return null; @@ -447,8 +480,9 @@ export class AiScript { for: (times, fn) => { const result = []; for (let i = 0; i < times; i++) { - slotArg[fn.slots[0]] = i + 1; - result.push(fn.exec(slotArg)); + result.push(fn.exec({ + [fn.slots[0]]: i + 1 + })); } return result; }, @@ -476,40 +510,12 @@ export class AiScript { }; const fnName = block.type; - const fn = funcs[fnName]; if (fn == null) { - console.error('Unknown function: ' + fnName); - throw new Error('Unknown function: ' + fnName); + throw new AiScriptError(`No such function '${fnName}'`); + } else { + return fn(...block.args.map(x => this.evaluate(x, scope))); } - - const args = block.args.map(x => this.evaluate(x, values, slotArg)); - - return fn(...args); - } - - /** - * 指定した名前の変数の値を取得します - * @param name 変数名 - * @param values ユーザー定義変数のリスト - */ - @autobind - private getVarVal(name: string, values: { name: string, value: any }[]): any { - const v = values.find(v => v.name === name); - if (v) { - return v.value; - } - - const pageVar = this.pageVars.find(v => v.name === name); - if (pageVar) { - return pageVar.value; - } - - if (AiScript.envVarsDef[name] !== undefined) { - return this.envVars[name]; - } - - throw new Error(`Script: No such variable '${name}'`); } @autobind diff --git a/src/client/app/common/views/components/page-editor/page-editor.script-block.vue b/src/client/app/common/views/components/page-editor/page-editor.script-block.vue index 2f78f7de3a..7a3942ec80 100644 --- a/src/client/app/common/views/components/page-editor/page-editor.script-block.vue +++ b/src/client/app/common/views/components/page-editor/page-editor.script-block.vue @@ -25,6 +25,9 @@
-
- -
{{ $t('script.blocks._fn.slots') }} @@ -115,6 +113,7 @@ export default Vue.extend({ }, typeText(): any { if (this.value.type === null) return null; + if (this.value.type.startsWith('fn:')) return this.value.type.split(':')[1]; return this.$t(`script.blocks.${this.value.type}`); }, }, diff --git a/src/client/app/common/views/components/page-editor/page-editor.vue b/src/client/app/common/views/components/page-editor/page-editor.vue index f8959fb0f1..8007970bed 100644 --- a/src/client/app/common/views/components/page-editor/page-editor.vue +++ b/src/client/app/common/views/components/page-editor/page-editor.vue @@ -77,6 +77,7 @@ diff --git a/src/client/app/common/views/pages/page/page.if.vue b/src/client/app/common/views/pages/page/page.if.vue index 9dbeaf64fb..417ef0c553 100644 --- a/src/client/app/common/views/pages/page/page.if.vue +++ b/src/client/app/common/views/pages/page/page.if.vue @@ -1,5 +1,5 @@ diff --git a/src/client/app/common/views/pages/page/page.vue b/src/client/app/common/views/pages/page/page.vue index 88598d3527..27e9a7aec4 100644 --- a/src/client/app/common/views/pages/page/page.vue +++ b/src/client/app/common/views/pages/page/page.vue @@ -27,7 +27,7 @@ import { url } from '../../../../config'; class Script { public aiScript: AiScript; - public vars: any; + public vars: Record; constructor(aiScript) { this.aiScript = aiScript; @@ -41,7 +41,7 @@ class Script { public interpolate(str: string) { if (str == null) return null; return str.replace(/\{(.+?)\}/g, match => { - const v = this.vars.find(x => x.name === match.slice(1, -1).trim()).value; + const v = this.vars[match.slice(1, -1).trim()]; return v == null ? 'NULL' : v.toString(); }); }